basic-file-exploit

picoCTF 2022の「basic-file-exploit」。テキトーに色々な入力をしていたらフラグが出てきた。

この問題はpwnというよりもrevの問題(cのソースコードだけ配布される)。

「どんな入力をしてフラグを得たのか」→「なぜその入力で得られたのか」の順にソースコードを読み解いていく。

プログラムの概要

shoebill@pwner:~/pico$ ./conn.sh
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
1
1
Please enter your data:
test
test
Please enter the length of your data:
4
4
Your entry number is: 1
Write successful, would you like to do anything else?
2
2
Please enter the entry number of your data:
1
1
test
Read successful, would you like to do anything else?

1でデータ入力+データの長さを入力、2でエントリーナンバーを指定してデータを読み出す。

フラグを得られた入力

適当に1を実行し後に次のように2を実行したらいきなりフラグが出てきた:

2
2
Please enter the entry number of your data:
%p
%p
picoCTF{M4K3_5UR3_70_CH3CK_Y0UR_1NPU75_E0394EC0}

ソースを読み解く

2番ではdata_read()関数が実行されている:

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

つまり次を実行させた:

  if ((entry_number = strtol(entry, NULL, 10)) == 0) {
    puts(flag);
    fseek(stdin, 0, SEEK_END);
    exit(0);
  }

strtol(const char *s, char **endptr, int base);は文字列をlong値に変換する関数。
strtolのリファレンス

  • const char *s: 変換対象文字列
  • char **endptr: 変換不可能な文字列へのポインタの格納先
    (NULLの場合には、変換不可能な文字列への処理は行わない)
  • int base: 基数

戻り値:

  • 成功時: sの値をlongで返却
  • 失敗時: 0

strtol(entry, NULL, 10))を失敗させるには?

strtol(entry, NULL, 10)) == 0となるすなわちstrtolを失敗させればputs(flag)されるから、strtolを失敗させればよい。

そのためには、10進変換できないものを入力してやればよい

strtolのリファレンスにもあるように、ABCDEFは10進変換できないからそれを与えてもフラグを得られる:

2
2
Please enter the entry number of your data:
ABCDEF
ABCDEF
picoCTF{M4K3_5UR3_70_CH3CK_Y0UR_1NPU75_E0394EC0}


ちなみにentryはどこから来たかのかというと...

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) {
...

さらにtargetinputを辿ると、その第一引数であるinputはこちら側が入力する値とわかる:

int tgetinput(char *input, unsigned int l)
{
   ...
    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;
...