0x00 前言
今天看到一个很经典的cve,顺便写一篇博客
漏洞是利用memcpy长度没限制导致的越界写,可覆盖关键变量,导致有一次任意地址写
原文:Exploiting an 18 Year Old Bug
0x01 源码
为了调试方便直接下载好二进制、配置文件、运行库,这样就无需自己编译和安装依赖性
我的本地是Ubuntu18.04,配置文件:
1 | [Global] |
Netatalk
是AppleTalk的开源实现方案,可以用作文件服务器来实现文件共享,afpd是其中的一个组件,类似于httpd的拿来做通信的部分,源码目录的./etc/afpd
子目录为相关源码树。
看一些关键函数:
dsi_tcp_open()
该函数首先会调用fork()函数建立新的子进程,并在子进程中执行如下逻辑:先从TCP会话中读取DSI header到结构体dsi->header,之后读取DSI payload内容放在dsi->commands指向的buf中。
1 | static pid_t dsi_tcp_open(DSI *dsi) |
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 | int dsi_getsession(DSI *dsi, server_child_t *serv_children, int tickleval, afp_child_t **childp) |
dsi_opensession()
函数首先根据commands[0]的内容决定处理逻辑,若为DSIOPT_ATTNQUANT,则执行memcpy,以commands[1]为大小,将commands[2]之后的内容拷贝至DSI结构体的attn_quantum成员变量(4 bytes)。之后程序会构建新的DSI消息到dsi->commands中,将server_quantum的值返回给客户端。
1 | void dsi_opensession(DSI *dsi) |
漏洞点也就在这个函数内,
1 | while (i < dsi->cmdlen) { |
memcpy的长度参数 dsi->commands[i] 由外部传入,且没检查大小,导致可溢出到DSI结构体中attn_quantum的后续成员。
1 |
|
但可见成员 commands 的类型为uint8_t*,所以memcpy的最大长度是0xff。并且由于 DSI_DATASIZ 为65535,所以溢出部分data后就无法向后溢出了,故最终只能溢出如上这些成员变量。
利用之前先搞懂dsi是什么:Data Stream Interface,按照协议格式可以构造其各个字段
1 | commands = "\x01" // DSIOPT_ATTNQUANT 选项的值 |
于是明白之后,我们可以交互试一下,构造一个dsi数据包,并给发送给目标的tcp端口(本地测试),看看能否返回server_quantum的值。
1 | from pwn import * |
测试确实可以收到bbbb
0x02 漏洞利用
漏洞点1:
接下来具体看一下内存情况,继续使用上面的脚本,启动gdb找到bbbb
对应DSI的结构体,可以知道0x00007f5f3d6c8010即commands,再看一下内存布局
该地址和libc地址比较相近,所以段基址都是4k页对齐,爆两个字节,最差65536次就可以了
从低位开始,依次覆盖commands指针的各个字节。当修改后的commands指针是一个可写的地址时,dsi_opensession()函数可以将server_quantum的值放进来并返回给我们,否则,子进程会由于访问不可写的地址而崩溃,socket异常。故,可以通过观察服务器是否给我们返回来确定当前我们是否爆破出一个合法的地址。
1 | def boom(): |
至于为什么3-6位从0xff到0x00爆是为了计算偏移时方便一点,结果更接近libc_base,且地址较高
在本地测试的方便之处在于可以很方便地找到libc基址,可是远程就没那么容易了,不知道机器版本的情况跨如何知道具体偏移呢,可以先在本地测试攻击方法,本地成功之后再远程爆破,直到远程成功为止
1 | leak_addr = 0x7f5f3d138000 |
为了方便,本文只在本地进行测试,所以假设现在已经拥有了libc基址,可以控制commands,下一步利用任意写漏洞,
漏洞点2:
dsi_stream_receive()
该函数从当前socket继续读取消息并保存在结构体中。具体地,先读取DSI header并保存到dsi->header结构体中,然后读取后续DSI payload保存到dsi->commands指向的buffer当中,长度由dsi->cmdlen指定。
1 | int dsi_stream_receive(DSI *dsi) |
谈一下后续流程,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 | function = (u_char) dsi->commands[0]; |
afp_switch
指向两种跳转表中的一个。一个名叫preauth_switch
,它包含了未认证用户仅能调用的四个函数,preauth_switch
也是默认的afp_switch
表。另一个名叫postauth_switch
,当用户进行认证后,它被换入到afp_switch
中。
这里的结构是这样的
1 | AFPCmd *afp_switch = preauth_switch; |
有了dsi_stream_receive(),我们就可以先通过memcpy覆写dsi->commands指针,再通过dsi_stream_read
实现任意地址写。
1 | from pwn import * |
查看内存,目标地址已被成功修改
漏洞点3:
有了前两个条件,最后一步就可以进行精准打击了,free_hook可以的,之前hitcon2019用的方法就是freehook,这里用一种新学的方法,世人称之为exit_hook
这里简单记录一下原理,写个exit()的程序,跟进会知道它的具体流程
这里的<_dl_fini+105>是rtld_lock_default_lock_recursive函数,rdi是_rtld_gobal+2312
看一下源码
1 |
|
调用了两个关键的函数,如上,这两个函数在_rtld_global结构体里面,也就是说如果我们能改写这两个函数其中一个就能控制流劫持并控制参数
函数指针:_dl_rtld_lock_recursive (_rtld_global+3840)
调用参数:_dl_load_lock (_rtld_global+2312)
所以只要有一次任意地址写大小0x600字节就可以
1 | from pwn import * |
改写后cmd成为system函数的参数,当程序exit后即可拿到shell
最后说一下free_hook的方法,hitcon2019官方的解法,布局比较复杂,用到了两个gadget
覆写__free_hook
为__libc_dlopen_mode+56
覆写_dl_open_hook
为_dl_open_hook+8
,_dl_open_hook+8
为fgetpos64+207
的这个magic_gadget
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 | sigframe = SigreturnFrame() |
0x03 小结
转眼2022年了,水平还是太菜代码看的好累,据说这个CVE是原作者在飞机上三个小时发现的…膜拜,写完博客感觉还是有收获的