feature image

2024年4月26日 | ブログ記事

CPCTF2024 作問者writeup そらめあ 編

はじめに

こんにちは、23Bのそらめあと申します。traPではCTFのPwnをやっていたり、SysAd班として活動したり、パズルを作ったり食べたりしています。

本記事は、2024年4月20,21日に開催されたCPCTF2024の作問者writeupです。

目次

Pwn

Binary

Pwn

出題した問題の難易度感や内容に関しては良かったのではないかと思うのですが、そもそもの問題数がかなり少なくなってしまいました……

明らかに僕の実力が不足していたので、次に作問する機会が訪れるまでにもう少し強くなっておきたいですね。

Attack! Attack! Win!

難易度: Newbie

問題文

flagを盗まれてしまいました……
敵を倒して取り返してきてもらえませんか?

概要

実行ファイルとソースコードが与えられます。
どうやらコマンドRPGのようなもののようです。

#include <stdio.h>
#include <stdlib.h>

int enemyHp, playerHp;
void (*win)();
void (*enemyCommand)();
void (*playerCommands[3])();

void init(){
	setvbuf(stdin, NULL, _IONBF, 0);
	setvbuf(stdout, NULL, _IONBF, 0);
	setvbuf(stderr, NULL, _IONBF, 0);
	alarm(60);
}

void getFlag(){
	puts("You got the flag!");
	system("cat flag.txt");
}

void hocusPocus(){
	puts("Memory leak!");
	printf("Attack     : %p\n", &playerCommands[0]);
	printf("Heal       : %p\n", &playerCommands[1]);
	printf("HocusPocus : %p\n", &playerCommands[2]);
	printf("win        : %p\n\n", &win);
}

void attack(){
	enemyHp -= 40;
	if(enemyHp < 0){
		enemyHp = 0;
	}
}

void heal(){
	playerHp += 30;
	if(playerHp > 100){
		playerHp = 100;
	}
}

void enemyAttack(){
	playerHp -= 50;
	if(playerHp < 0){
		playerHp = 0;
	}
}

void printHp(){
	printf("YourHP:%d\nenemyHp:%d\n\n", playerHp, enemyHp);
}

void printCommands(){
	puts("1: Attack\n2: Heal\n3: Hocus Pocus\n");
}

int main(){
	int input;

	init();
	enemyHp = 100;
	playerHp = 100;
	playerCommands[0] = attack;
	playerCommands[1] = heal;
	playerCommands[2] = hocusPocus;
	enemyCommand = enemyAttack;
	win = getFlag;

	puts("\n");
	puts("Defeat the enemy to get the flag!\n\n");

	while(1){
		printHp();
		if(playerHp <= 0){
			puts("You lose...");
			break;
		}else if(enemyHp <= 0){
			win();
		}
		printCommands();

		scanf("%d", &input);
		printf("\n");

		if(input > 3){
			puts("Nothing happens.\n");
		}else{
			playerCommands[input - 1]();
		}
		enemyCommand();
	}
	return 0;
}

getFlag関数を呼び出すことができれば、system("cat flag.txt")が実行されflagが取れます。

解法

enemyHpを0にすることができればgetFlag関数が実行されるのですが、うまくいきません。
HocusPocusコマンドを選択するとアドレスが見られます。

Memory leak!
Attack     : 0x5573e3484070
Heal       : 0x5573e3484078
HocusPocus : 0x5573e3484080
win        : 0x5573e3484058

ここから、AttackとHeal、HealとHocusPocusの差がそれぞれ8であることがわかります。Attackが1、Healが2、HocusPocusが3に対応していることから1大きいコマンドには8大きな値が対応しているのでは?と推測できます。

ここで、ソースコードをよく見ると、win = getFlagと書かれていますね。winコマンドを実行できればgetFlag関数が実行されそうです。Attackとwinの差は24なので、-2を入力すればいいでしょう。
実際、条件分岐はif(input > 3)となっており、負数を入力することができました。

接続し、-2を入力することでflagが得られます。

flag

CPCTF{4_c1eVeR_4nd_p4CifI5t_7hi3F}

結果

10pts/80solved

ヒントなし ヒント1 ヒント2 ヒント3
66 - - 14

感想

Newbie用に負のインデックスにアクセスする問題を作ろうと思ったのですが、CTFを触った事のない人にstackの知識を求めるのは良くないと思い推測できる形をとりました。

ソースコードが伸びてしまったこと、ELFファイルなどlinux関連の知識を前提としたことはよくなかったですね。正答率もNewbieの中では少し低めになってしまいました。

Pwnはとっつきにくいのでテーマや中身で中和したかったんですが、上手くいかず……

CPCT......

難易度: Easy

問題文

等価交換って良いですよね。ということで、入力した文字数の分だけflagをあげます!
でも貰いすぎても困るので4文字以内でお願いします……

概要

実行ファイルとソースコードが与えられます。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

void init(){
	setvbuf(stdin, NULL, _IONBF, 0);
	setvbuf(stdout, NULL, _IONBF, 0);
	setvbuf(stderr, NULL, _IONBF, 0);
	alarm(60);
}

int main() {
	init();

	char buf[5] = "";
	int length = 0;
	char flag[100] = "";

	FILE *fp = fopen("flag.txt", "r");
	if (fp == NULL) {
		puts("flag.txt does not exist.");
		return 1;
	}
	fgets(flag, 100, fp);
	fclose(fp);

	printf("Please enter some string! (max 4 character)\n");
	read(0, buf, 5);
	for (int i = 0; i < 4; i++) {
		if (buf[i] == '\n') {
			buf[i] = '\0';
			break;
		}
	}
	buf[4] = '\0';

	printf("Thank you!\nYour input:");
	length = printf(buf);
	printf("\n");
	printf("Length: %d\n", length);

	printf("This is your reward!\n");

	for (int i = 0; i < length; i++) {
		printf("%c", flag[i]);
		if(i >= strlen(flag)) {
			break;
		}
	}
	printf("\n");

	return 0;
}

入力した文字数に応じてflagの一部が帰ってきます。

解法

39行目にlength = printf(buf)とあるのでFSBがありそうです。
printfの返り値は出力した文字数なので、flagの文字数を超える出力ができればよいです。
4文字しか入力を受け付けていないため4文字で大量の文字を出力できる方法を考えましょう。

%99cとすることで99文字の入力をしたことになるので、これでflagをすべて見ることができます。

flag

CPCTF{1m_50rrY_bu7_i_Hav3_0nLy_45_ch4raCteRs}

結果

120.72pts/61solved

ヒントなし ヒント1 ヒント2 ヒント3
48 7 2 4

感想

開始してから気づいたんですが、この問題は%n$sを使っても解けます。非想定でした。
また、これは終了後に聞いたのですが%b%aを使った人もいたみたいですね。二回分入力できるので十分な出力がされます。賢い。

FSB問を一問は入れたいな、と思い作ったら、ちょっと変わった問題になりました。

実はこの問題をデプロイした当初は4文字の入力が入らず、慌てて5文字入力し5文字目を上書きするように実装を変更しています。気づいてよかった~

The sky's the limit

難易度: Easy

問題文

あまりにも長い入力は危険らしいので弾くようにしました!

概要

実行ファイルとソースコードが与えられます。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#define BUF_SIZE 16

int init(){
	setvbuf(stdin, NULL, _IONBF, 0);
	setvbuf(stdout, NULL, _IONBF, 0);
	setvbuf(stderr, NULL, _IONBF, 0);
	alarm(60);

	return 0;
}

int win() {
	system("cat flag.txt");

	return 0;
}

int main() {
	init();

	char buf[BUF_SIZE];

	printf("input:");
	gets(buf);

	if(strlen(buf) > BUF_SIZE) {
		printf("Too long.\n");
		exit(0);
	}

	return 0;
}

解法

win関数を呼ぶとflagが出力されます。

checksecの結果は以下の通りです。

RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x400000)

28行目のgetsは入力文字数の指定ができません。そのため、BUF_SIZEより長い入力をすることによってbuffer overflowが発生します。canaryもなく、PIEも無効化されているので問題なさそうです。

ゆえに、winへのROPを書けば終わり……と思いきや、strlen(buf) > BUF_SIZEが真の時、Too long.と怒られてしまいます。
ここで、strlenの終端の判定方法を利用します。strlenはヌルバイトまでの文字数を返す関数です。つまり、入力のはじめ16文字のどこかにヌルバイトを混ぜることで先ほどの条件を偽にすることができます。

以上より、'\x00'*24、ret命令、win関数のアドレス、の順に入力することでflagを得られます。

flag

CPCTF{Nu11_s7rin6_m4De_y0u_fr3E}

結果

268.23pts/35solved

ヒントなし ヒント1 ヒント2 ヒント3
20 2 0 13

感想

僕が去年CPCTF2023に参加した時にはBOFの説明を読んでも「?」になっていたので厳しいだろうな……と思いつつ、出題しないわけにはいかない内容なので出しました。が、Easy以上でもっともヒント3での正解が多い問題になってしまいました。stackを見せるなどもう少しやり方はありましたね、反省しています……

ヌルバイトを使う部分を仕込んでみたら少しだけ考える余地が生まれたようで、そこに関してはよかったのではないでしょうか。

Binary

どうやらglibcの使用バージョンが異なっていたせいでwebshellでの実行ができなかったようです。申し訳ありません。冷や汗をかきながら修正しました。

全体的な問題傾向がちょっとかぶり気味だった気はしますが、悪くはなかったんじゃないでしょうか?

peeping

難易度: newbie

問題文

僕の考えたflagを当てられますか?

概要

ELF形式のファイルが一つ与えられます。
文字列を入力すると、それが正しいflagか判定してくれるようです。

解法

グローバル変数としてflagが直接書き込まれていたので、stringsをするとflagが出てきます。

flag

CPCTF{b3_4_cLa1rv0yANt}

結果

10pts/79solved

ヒントなし ヒント1 ヒント2 ヒント3
76 - - 3

感想

知ってれば解けるし、知らないと解けない悪い問題ではあると思ってます。せめてもう少し問題文で誘導してもよかったですね。ELFの説明も入れてあげるべきでした。

Just reversing?

難易度: Easy

問題文

CTFにはReversingというジャンルがあるらしいですね。
面白そうなので雰囲気で問題を作ってみました!

概要

C言語のソースコードとflag_enc.txtが与えられます。

#include <stdio.h>
#include <string.h>

int main() {
	char flag_enc[30] = "";
	char flag[30];
	FILE *f;
	f = fopen("flag.txt", "r");
	if (f == NULL) {
		printf("flag.txt not found\n");
		return 1;
	}
	fscanf(f, "%s", flag);
	fclose(f);

	for (int i = 0; i < strlen(flag); i++) {
		char chr = flag[i];
		flag_enc[strlen(flag) - i - 1] = chr / 16 + chr % 16 * 16;
	}

	f = fopen("flag_enc.txt", "w");
	fprintf(f, "%s", flag_enc);
	fclose(f);

	return 0;
}

解法

このソースコードではflag.txtを読み込み、ある操作をした後にflag_enc.txtに書き込んでいるようです。
flagのi文字目に対して、flag_enc[strlen(flag) - i - 1] = chr / 16 + chr % 16 * 16;を行っていることから、flag[strlen(flag) - j - 1] = flag_enc[j] / 16 + flag_enc[j] % 16 * 16であることがわかります。
これを実行するプログラムを書くことでflagの復元ができます。

flag

CPCTF{l17Er4llY_r3vErs1nG}

結果

106.00pts/60solved

ヒントなし ヒント1 ヒント2 ヒント3
56 3 0 1

感想

elfファイルを渡すタイプ以外になんか作りたかったので、暗号化されたものが渡される問題を作りました。個人的にはかなり気に入っています。

作問時点ではジャンル名はRevだと思っていたのでこんなタイトルにしましたが、実際にはBinaryだったのでちょっと微妙な感じになってしまいました。悲しい……

Number Guesser

難易度: Easy

問題文

正しい数字を入力できたらflagを差し上げます。

概要

elf形式のファイルが一つ与えられます。
実行すると"Guess the number!"と出てきます。どうやら数字が正解なら何か起こるようです。

解法

objdump で逆アセンブルします。すると、main関数に条件分岐が見つかります。
cmp $0x31 %alといった命令が5つあるので、この$0x31の部分を順番に文字コードで変換すると
17704が得られます。
これを入力するとflagが出力されます。

flag

CPCTF{l4Ck3Y_NuMb3R!}

結果

133.09pts/52solved

ヒントなし ヒント1 ヒント2 ヒント3
49 0 2 1

感想

objdumpが知識依存なので心配していたんですが、ヒント1での正解者がいないということで、安心しました。peepingが前提知識として活用されていたら嬉しいですね。
ヒント2をかなり丁寧に書いたのでここでの正解がいたのも嬉しいです。

解析が必要となる問題の中では一番標準的でしょうか?できるだけシンプルにしたかったので、数字5桁を入力したらflagが復元されるという形をとりました。

まぁ、総当たりはつぶさなくてもいいだろ!とか思ってたらちゃんと総当たりで解いた人がいたみたいです。偉い。

Power down

難易度: Medium

問題文

落ち着いて、ゆっくり、順番に……

概要

elf形式のファイルが与えられます。
与えられた文字列が正しいflagか判定してくれるようです。

解法

ghidraなどで解析します。
入力をpower_down関数に流していることがわかります。ここでこの関数を見ると、どうやら再帰の形で何かしているようだとわかります。

power_down関数はchar型とint型を引数に取る関数です。それぞれをa,bとします。
a <= b*bのときにはb, b*b-aを、そうでないときにはpower_down(a, b+1)を返しているので、結局aの値を復元するためにはb*b-(b*b-a)をすればよさそうだなとわかります。

power_downの結果の比較先はハードコーディングされているので簡単に見つけることができます。これを使って復元すればflagが手に入ります。

結果

344.19pts/12solved

ヒントなし ヒント1 ヒント2 ヒント3
12 0 0 0

感想

これはblack boxをhardに送ることにした結果急いで作ることになった問題です。再帰や構造体を使ってみたいな~と思ったのがきっかけだったのですが、not strippedだったからか、かなり読みやすかったですね。

新入生で解いた人いるんでしょうか?

black box

難易度: Hard

問題文

箱の中身はなんだろな?

概要

elf形式のファイルが与えられます。flagを判定してくれるみたいです。

解法

not strippedなので、cubeA、cubeB、cmpの三つの値が見えます。

入力をなんらかの法則でcubeA、cubeBを用いて変換し、それをcmpと比較しているようです。
0 <= i,j,k < 3に対して、cmp[i*9+j*3+k]cubeB[l*9+j*3+k]*input[l*9+j*3+i]*cubeA[i*9+l*3+k]l=0,1,2すべての和を比較しているようです。

ここで、(i,j)を固定して考えてみましょう。
そうすると、inputがlのみに依存することになります。それに対しcubeA,cubeB,cmpはすべてkを変数として持っているので、計3つの関係式が立てられます。これを使えばすべてのlに対してinputを求められるでしょう。

実際には、cubeAとcubeBの積、cubeCを3x3行列、cmpとinputを3x1行列とみなした時、(cubeC)(input)=cmpとなっているので、cubeCの逆行列を求めてcmpに左からかけることで正しいinput、すなわちflagを復元できます。

結果

376.23pts/9solved

ヒントなし ヒント1 ヒント2 ヒント3
9 0 0 0

感想

もともとmediumで出すつもりだったのでnot strippedにしたんですが、hardにするならstrippedでよかったかも?

3次元配列をテーマに、行列の積を立体にしたものをイメージして作りました。各要素に対して同じ軸のものを掛け合わせる感じですね。Z3を使うとだいぶ楽できそうです。

まとめ

今回が(ほぼ)初めての作問だったのですが、いざ作ってみると知識の抜けや不足が多くあり大変でした。そもそもCを書くのが久しぶり、gdbがバッファリングの問題で機能しない、Docker周りを書くのに慣れていなかったなど……
マジの限界開発で、勉強にもなり、いい経験だったなぁと思います。二度とやりたくはないですが。

それでも無事開催でき、みなさんに「楽しかった!」と言っていただけて安心しました。参加していただいたみなさん本当にありがとうございました!!

soramea icon
この記事を書いた人
soramea

23B 謎解きとパズルを食べて生きている

この記事をシェア

このエントリーをはてなブックマークに追加
共有

関連する記事

2023年9月7日
Sekai CTFに参加しました
kaitoyama icon kaitoyama
記事一覧 タグ一覧 Google アナリティクスについて 特定商取引法に基づく表記