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

解けた問題の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は無料で遊べるので、暇なときにやってみるとよいかもしれません。
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->0か1 - フラグのビットが
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でした。運営の皆さんありがとうございました。