TL;DR

  • ptraceが邪魔なのでLD_PRELOADで偽装した共有オブジェクトを噛ませて無効化する
  • 元のコードがバラバラになるような難読化が施されているので集めて元に戻す
  • 先頭から1文字ずつ正解かどうかを見ているので、総当たりするGDBスクリプトを書く

Prerequisite

  • LD_PRELOADによる関数の差し替え
  • GDBのスクリプティング

Writeup

実行すると、次のように入力を促してくる。

$ ./dns_ropob
FLAG >

適当に入力すると、"wrong!"と言われてしまうのでここにフラグを入れると正解のメッセージが返って来そうである。

頼みのGhidraで眺めても解析が難しいのか芳しいデコンパイル結果は得られない。というわけでgdbやらstraceやらltraceやらで動きを追ってみようとするが、ptraceによってptraceが制限されているのか、こいつらが上手く動いてくれない。加えてデコンパイル結果を見る限りではptraceはどこからも呼ばれていないように見えるため、呼ばれている箇所をNOPで潰すようなことが出来ない。

というわけで何もしない偽のptraceを実行する共有オブジェクトを作ってLD_PRELOADにそれを指定することでこのチェックをバイパスする。次のページが参考になった。

gdbで適当に動きを追ってみるとpushfpopfを駆使した謎の挙動を見せる。常に同じような事をしているように思えるが、よく観察してみると確かに周期的に命令が実行されているものの、mainセクションにて、retの後に実行される命令は毎度異なっていることがわかる。

Ghidraだと、関数のret以降は指定しないとディスアセンブルしてくれないが、ret以降のアドレスが実行されており、さらに自己書き換え等で元の命令列から書き換えられてもいないためObjdumpでディスアセンブルしてみると普通に命令列が得られる1

ここからretの後の命令だけ抜き取ってみると次のような結果が得られる。

1b9f:	48 89 e5             	mov    rbp,rsp
1bd8:	48 83 ec 40          	sub    rsp,0x40
1c12:	64 48 8b 04 25 28 00 	mov    rax,QWORD PTR fs:0x28
1c51:	48 89 45 f8          	mov    QWORD PTR [rbp-0x8],rax
1c8b:	31 c0                	xor    eax,eax
1cc3:	c7 45 cc 00 00 00 00 	mov    DWORD PTR [rbp-0x34],0x0
1d00:	b9 00 00 00 00       	mov    ecx,0x0
1d3b:	ba 01 00 00 00       	mov    edx,0x1
1d76:	be 00 00 00 00       	mov    esi,0x0
1db1:	bf 00 00 00 00       	mov    edi,0x0
1dec:	b8 00 00 00 00       	mov    eax,0x0
1e27:	e8 84 e8 ff ff       	call   6b0 <ptrace@plt>
1e62:	48 83 f8 ff          	cmp    rax,0xffffffffffffffff
1e9c:	0f 85 ac 00 00 00    	jne    1f4e <main+0x41b>
1ed8:	bf 01 00 00 00       	mov    edi,0x1
1f13:	e8 b8 e7 ff ff       	call   6d0 <exit@plt>
1f4e:	48 8d 3d 45 07 00 00 	lea    rdi,[rip+0x745]        # 269a <_IO_stdin_used+0x2a>
1f8b:	e8 f0 e6 ff ff       	call   680 <puts@plt>
1fc6:	48 8d 45 d0          	lea    rax,[rbp-0x30]
2000:	48 89 c6             	mov    rsi,rax
2039:	48 8d 3d 61 06 00 00 	lea    rdi,[rip+0x661]        # 26a1 <_IO_stdin_used+0x31>
2076:	b8 00 00 00 00       	mov    eax,0x0
20b1:	e8 0a e6 ff ff       	call   6c0 <__isoc99_scanf@plt>
20ec:	8b 55 cc             	mov    edx,DWORD PTR [rbp-0x34]
2125:	48 8d 45 d0          	lea    rax,[rbp-0x30]
215f:	89 d6                	mov    esi,edx
2197:	48 89 c7             	mov    rdi,rax
21d0:	e8 62 e6 ff ff       	call   837 <func>
220b:	89 45 cc             	mov    DWORD PTR [rbp-0x34],eax
2244:	83 7d cc 00          	cmp    DWORD PTR [rbp-0x34],0x0
227e:	0f 85 24 01 00 00    	jne    23a8 <main+0x875>
22ba:	48 8d 3d e5 03 00 00 	lea    rdi,[rip+0x3e5]        # 26a6 <_IO_stdin_used+0x36>
22f7:	b8 00 00 00 00       	mov    eax,0x0
2332:	e8 69 e3 ff ff       	call   6a0 <printf@plt>
236d:	e9 e9 00 00 00       	jmp    245b <main+0x928>
23a8:	48 8d 3d 00 03 00 00 	lea    rdi,[rip+0x300]        # 26af <_IO_stdin_used+0x3f>
23e5:	b8 00 00 00 00       	mov    eax,0x0
2420:	e8 7b e2 ff ff       	call   6a0 <printf@plt>
245b:	b8 00 00 00 00       	mov    eax,0x0
2496:	48 8b 4d f8          	mov    rcx,QWORD PTR [rbp-0x8]
24d0:	64 48 33 0c 25 28 00 	xor    rcx,QWORD PTR fs:0x28
250f:	74 71                	je     2582 <main+0xa4f>
2547:	e8 44 e1 ff ff       	call   690 <__stack_chk_fail@plt>
2582:	c9                   	leave
25b9:	c3                   	ret

関数のプロローグもエピローグもしっかり存在し、よく見るような構造のアセンブリコードが得られた。

これをざっくり読んでみるとfuncという関数に入力を渡しているように思える。というわけでfuncを見てみると、mainと同じような難読化が施されていたので同じようにしてret直後の命令を取り出す。

86e:    48 89 e5                mov    rbp,rsp
8a7:    48 83 ec 50             sub    rsp,0x50
8e1:    48 89 7d b8             mov    QWORD PTR [rbp-0x48],rdi
91b:    89 75 b4                mov    DWORD PTR [rbp-0x4c],esi
954:    64 48 8b 04 25 28 00    mov    rax,QWORD PTR fs:0x28
993:    48 89 45 f8             mov    QWORD PTR [rbp-0x8],rax
9cd:    31 c0                   xor    eax,eax
a05:    48 8b 05 6c 1c 00 00    mov    rax,QWORD PTR [rip+0x1c6c]        # 2678 <_IO_stdin_used+0x8>
a42:    48 8b 15 37 1c 00 00    mov    rdx,QWORD PTR [rip+0x1c37]        # 2680 <_IO_stdin_used+0x10>
a7f:    48 89 45 d0             mov    QWORD PTR [rbp-0x30],rax
ab9:    48 89 55 d8             mov    QWORD PTR [rbp-0x28],rdx
af3:    48 8b 05 8e 1b 00 00    mov    rax,QWORD PTR [rip+0x1b8e]        # 2688 <_IO_stdin_used+0x18>
b30:    48 8b 15 59 1b 00 00    mov    rdx,QWORD PTR [rip+0x1b59]        # 2690 <_IO_stdin_used+0x20>
b6d:    48 89 45 e0             mov    QWORD PTR [rbp-0x20],rax
ba7:    48 89 55 e8             mov    QWORD PTR [rbp-0x18],rdx
be1:    0f b6 05 b0 1a 00 00    movzx  eax,BYTE PTR [rip+0x1ab0]        # 2698 <_IO_stdin_used+0x28>
c1e:    88 45 f0                mov    BYTE PTR [rbp-0x10],al
c57:    c6 45 ca 63             mov    BYTE PTR [rbp-0x36],0x63
c91:    c6 45 cb 71             mov    BYTE PTR [rbp-0x35],0x71
ccb:    c7 45 cc 00 00 00 00    mov    DWORD PTR [rbp-0x34],0x0
d08:    e9 1d 0c 00 00          jmp    192a <func+0x10f3>
d43:    8b 45 cc                mov    eax,DWORD PTR [rbp-0x34]
d7c:    99                      cdq
db3:    c1 ea 1f                shr    edx,0x1f
dec:    01 d0                   add    eax,edx
e24:    83 e0 01                and    eax,0x1
e5d:    29 d0                   sub    eax,edx
e95:    83 f8 01                cmp    eax,0x1
ece:    0f 85 29 05 00 00       jne    13fd <func+0xbc6>
f0a:    8b 45 cc                mov    eax,DWORD PTR [rbp-0x34]
f43:    48 63 d0                movsxd rdx,eax
f7c:    48 8b 45 b8             mov    rax,QWORD PTR [rbp-0x48]
fb6:    48 01 d0                add    rax,rdx
fef:    0f b6 00                movzx  eax,BYTE PTR [rax]
1028:   0f be d0                movsx  edx,al
1061:   0f b6 45 ca             movzx  eax,BYTE PTR [rbp-0x36]
109b:   89 d1                   mov    ecx,edx
10d3:   31 c1                   xor    ecx,eax
110b:   8b 45 cc                mov    eax,DWORD PTR [rbp-0x34]
1144:   48 63 d0                movsxd rdx,eax
117d:   48 8d 05 9c 1e 20 00    lea    rax,[rip+0x201e9c]        # 203020 <SEED1>
11ba:   0f b6 14 02             movzx  edx,BYTE PTR [rdx+rax*1]
11f4:   8b 45 cc                mov    eax,DWORD PTR [rbp-0x34]
122d:   48 98                   cdqe
1265:   0f b6 44 05 d0          movzx  eax,BYTE PTR [rbp+rax*1-0x30]
12a0:   31 d0                   xor    eax,edx
12d8:   0f b6 c0                movzx  eax,al
1311:   39 c1                   cmp    ecx,eax  # check character 1
1349:   0f 84 a1 05 00 00       je     18f0 <func+0x10b9>
1385:   c7 45 b4 01 00 00 00    mov    DWORD PTR [rbp-0x4c],0x1
13c2:   e9 d9 05 00 00          jmp    19a0 <func+0x1169>
13fd:   8b 45 cc                mov    eax,DWORD PTR [rbp-0x34]
1436:   48 63 d0                movsxd rdx,eax
146f:   48 8b 45 b8             mov    rax,QWORD PTR [rbp-0x48]
14a9:   48 01 d0                add    rax,rdx
14e2:   0f b6 00                movzx  eax,BYTE PTR [rax]
151b:   0f be d0                movsx  edx,al
1554:   0f b6 45 cb             movzx  eax,BYTE PTR [rbp-0x35]
158e:   89 d1                   mov    ecx,edx
15c6:   31 c1                   xor    ecx,eax
15fe:   8b 45 cc                mov    eax,DWORD PTR [rbp-0x34]
1637:   48 63 d0                movsxd rdx,eax
1670:   48 8d 05 a9 19 20 00    lea    rax,[rip+0x2019a9]        # 203020 <SEED1>
16ad:   0f b6 14 02             movzx  edx,BYTE PTR [rdx+rax*1]
16e7:   8b 45 cc                mov    eax,DWORD PTR [rbp-0x34]
1720:   48 98                   cdqe
1758:   0f b6 44 05 d0          movzx  eax,BYTE PTR [rbp+rax*1-0x30]
1793:   31 d0                   xor    eax,edx
17cb:   0f b6 c0                movzx  eax,al
1804:   39 c1                   cmp    ecx,eax  # check character 2
183c:   0f 84 ae 00 00 00       je     18f0 <func+0x10b9>
1878:   c7 45 b4 01 00 00 00    mov    DWORD PTR [rbp-0x4c],0x1
18b5:   e9 e6 00 00 00          jmp    19a0 <func+0x1169>
18f0:   83 45 cc 01             add    DWORD PTR [rbp-0x34],0x1
192a:   83 7d cc 1f             cmp    DWORD PTR [rbp-0x34],0x1f  # check index
1964:   0f 8e d9 f3 ff ff       jle    d43 <func+0x50c>
19a0:   8b 45 b4                mov    eax,DWORD PTR [rbp-0x4c]
19d9:   48 8b 75 f8             mov    rsi,QWORD PTR [rbp-0x8]
1a13:   64 48 33 34 25 28 00    xor    rsi,QWORD PTR fs:0x28
1a52:   74 71                   je     1ac5 <func+0x128e>
1a8a:   e8 01 ec ff ff          call   690 <__stack_chk_fail@plt>
1ac5:   c9                      leave
1afc:   c3                      ret

(※適当にコメントを付けている)

非常にゴチャゴチャしているが、やっていることは入力を1文字ずつ取り出して、複雑な処理を行い、その結果が目的の値と一致しているかを見るだけである。その結果がある値と一致しているかが、0x1311と0x1804のどちらかのcmp ecx, eaxで判定され、そうでなかったら即座に失敗としてリターンしている。これが0x20文字分行われている。

ということは、先頭からここのcmpで正解するまで文字を総当りすることで正解の文字を当てることが出来る。ただ人力でやると非常に骨が折れるのでスクリプトの力を借りる。偉大な先人(チームメイト)が書いてくれた他の問題のWriteupを読みながら、上記のcmpにブレークポイントを張って、止まった時にこれらのレジスタを覗いて値を得てからそれが等しいかを判定するというコードを書いてフラグを先頭から確定させていく。

Code

import gdb
import string


gdb.execute("file ./dns_ropob")
gdb.execute("set environment LD_PRELOAD ./ptrace.so")
gdb.execute("b *0x555555401804")
gdb.execute("b *0x555555401311")


candidates = string.ascii_letters + string.digits + "_{}"
flag = ""

TEST = False

if not TEST:
    for i in range(0x20):
        for c in candidates:
            with open("input.txt", "w") as f:
                f.write(flag+c)
            gdb.execute("run < input.txt")
            for _ in range(i):
                gdb.execute("c")
            rcx = int(gdb.execute("p $rcx", to_string=True).split(" = ")[-1])
            rax = int(gdb.execute("p $rax", to_string=True).split(" = ")[-1])
            print(rax, rcx)

            if rcx == rax:
                flag = flag + c
                print(f"[+] {flag=}")
                break

Flag

TSGCTF{I_am_inspired_from_ROPOB}

Resources


1

このWriteupを書いている途中にGhidraでも範囲指定してディスアセンブルが出来た事を思い出した