はじめに
こんにちは、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周りを書くのに慣れていなかったなど……
マジの限界開発で、勉強にもなり、いい経験だったなぁと思います。二度とやりたくはないですが。
それでも無事開催でき、みなさんに「楽しかった!」と言っていただけて安心しました。参加していただいたみなさん本当にありがとうございました!!