前回 は、C言語とアセンブラでシェルを起動するプログラムを作ってみました。
今回は、アセンブラで作ったプログラムをExploitコードの部品となりそうな C言語からシェルを起動するプログラムを実装してみたいと思います。
それでは、やっていきます。
- 参考文献
- はじめに
- 実装してデバッグしたアセンブラプログラムを確認する
- メモリに配置できるように機械語で埋め込む
- 機械語で埋め込んだプログラムをGDBで確認する
- 機械語を埋め込んだプログラムが実行できるように対策する
- おわりに
参考文献
はじめに
「セキュリティ」の記事一覧です。良かったら参考にしてください。
・第2回:Ghidraで始めるリバースエンジニアリング(使い方編)
・第3回:VirtualBoxにParrotOS(OVA)をインストールする
・第4回:tcpdumpを理解して出力を正しく見れるようにする
・第5回:nginx(エンジンエックス)を理解する
・第6回:Python+Flask(WSGI+Werkzeug+Jinja2)を動かしてみる
・第7回:Python+FlaskのファイルをCython化してみる
・第8回:shadowファイルを理解してパスワードを解読してみる
・第9回:安全なWebアプリケーションの作り方(徳丸本)の環境構築
・第10回:Vue.jsの2.xと3.xをVue CLIを使って動かしてみる(ビルドも行う)
・第11回:Vue.jsのソースコードを確認する(ビルド後のソースも見てみる)
・第12回:徳丸本:OWASP ZAPの自動脆弱性スキャンをやってみる
・第13回:徳丸本:セッション管理を理解してセッションID漏洩で成りすましを試す
・第14回:OWASP ZAPの自動スキャン結果の分析と対策:パストラバーサル
・第15回:OWASP ZAPの自動スキャン結果の分析と対策:クロスサイトスクリプティング(XSS)
・第16回:OWASP ZAPの自動スキャン結果の分析と対策:SQLインジェクション
・第17回:OWASP ZAPの自動スキャン結果の分析と対策:オープンリダイレクト
・第18回:OWASP ZAPの自動スキャン結果の分析と対策:リスク中すべて
・第19回:CTF初心者向けのCpawCTFをやってみた
・第20回:hashcatの使い方とGPUで実行したときの時間を見積もってみる
・第21回:Scapyの環境構築とネットワークプログラミング
・第22回:CpawCTF2にチャレンジします(この記事はクリア状況を随時更新します)
・第23回:K&Rのmalloc関数とfree関数を理解する
・第24回:C言語、アセンブラでシェルを起動するプログラムを作る(ARM64)
・第25回:機械語でシェルを起動するプログラムを作る(ARM64) ← 今回
前回に引き続き、今回も以下のサイトを参考にさせて頂きます。以下は x86 の記事です。
以下は、上の記事の ARM32bit版の記事です。
また、64bitARM(Aarch64)のアセンブラについては、以下のサイトを参考にさせて頂きました。
それでは、やっていきます。
実装してデバッグしたアセンブラプログラムを確認する
まず、前回実装したアセンブラのソースコードです。
.global _start // エントリーポイントをグローバルとして定義 _start: ldr x8, binsh // "/bin/sh" を含む文字列を x8 にセット mov x2, xzr // x2 レジスタを NULL に設定 mov x0, sp // x0 に sp の値をセット (第1引数) stp x8, x2, [sp], #-16 // x8("/bin/sh") と x2(NULL) の値をスタックにプッシュ (ポストインデックス) ※x2 は不要かも mov x1, sp // x1 に sp の値をセット (第2引数) stp x0, x2, [sp, #0] // x0("/bin/sh" のアドレス) と x2(NULL) の値をスタックにプッシュ (sp は動かない) mov x8, #221 // execve システムコール番号を x8 にセット (221はexecveのシステムコール番号) svc #0 // システムコールの呼び出し binsh: .asciz "/bin/sh" // シェルのパスをNULL終端文字列としてメモリに定義
次は、アセンブルと逆アセンブラ表示です。今回は、マップファイルも出力しておきます。
$ gcc -g -Wl,-Map=execve.map -o execve.out -nostdlib execve.s $ objdump -d execve.out execve.out: file format elf64-littleaarch64 Disassembly of section .text: 0000000000000244 <_start>: 244: 58000108 ldr x8, 264 <binsh> 248: aa1f03e2 mov x2, xzr 24c: 910003e0 mov x0, sp 250: a8bf0be8 stp x8, x2, [sp], #-16 254: 910003e1 mov x1, sp 258: a9000be0 stp x0, x2, [sp] 25c: d2801ba8 mov x8, #0xdd // #221 260: d4000001 svc #0x0 0000000000000264 <binsh>: 264: 6e69622f .word 0x6e69622f 268: 0068732f .word 0x0068732f
出力されたマップファイルを確認します。以下は、抜粋です。確かに、244番地からテキストセクションが始まっています。0x28(40)byte ということなので、記述したアセンブラは全て入っているようです。
.text 0x0000000000000244 0x28 *(.text.unlikely .text.*_unlikely .text.unlikely.*) *(.text.exit .text.exit.*) *(.text.startup .text.startup.*) *(.text.hot .text.hot.*) *(SORT_BY_NAME(.text.sorted.*)) *(.text .stub .text.* .gnu.linkonce.t.*) .text 0x0000000000000244 0x28 /tmp/ccmxWWSK.o 0x0000000000000244 _start
readelf で、ELFヘッダの内容を確認します。エントリポイントは、0x244 になっています。
$ readelf -h execve.out ELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: DYN (Position-Independent Executable file) Machine: AArch64 Version: 0x1 Entry point address: 0x244 Start of program headers: 64 (bytes into file) Start of section headers: 66864 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 56 (bytes) Number of program headers: 7 Size of section headers: 64 (bytes) Number of section headers: 19 Section header string table index: 18
続いて、プログラムヘッダです。64bitプログラムになって、アドレスが長すぎて見づらいです(笑)。
$ readelf -l execve.out Elf file type is DYN (Position-Independent Executable file) Entry point 0x244 There are 7 program headers, starting at offset 64 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040 0x0000000000000188 0x0000000000000188 R 0x8 INTERP 0x00000000000001c8 0x00000000000001c8 0x00000000000001c8 0x000000000000001b 0x000000000000001b R 0x1 [Requesting program interpreter: /lib/ld-linux-aarch64.so.1] LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x000000000000026c 0x000000000000026c R E 0x10000 LOAD 0x000000000000ff10 0x000000000001ff10 0x000000000001ff10 0x00000000000000f0 0x00000000000000f0 RW 0x10000 DYNAMIC 0x000000000000ff10 0x000000000001ff10 0x000000000001ff10 0x00000000000000d0 0x00000000000000d0 RW 0x8 NOTE 0x00000000000001e4 0x00000000000001e4 0x00000000000001e4 0x0000000000000024 0x0000000000000024 R 0x4 GNU_RELRO 0x000000000000ff10 0x000000000001ff10 0x000000000001ff10 0x00000000000000f0 0x00000000000000f0 R 0x1 Section to Segment mapping: Segment Sections... 00 01 .interp 02 .interp .note.gnu.build-id .gnu.hash .dynsym .dynstr .text 03 .dynamic .got .got.plt 04 .dynamic 05 .note.gnu.build-id 06 .dynamic .got .got.plt
セクションヘッダです。
$ readelf -S execve.out There are 19 section headers, starting at offset 0x10530: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .interp PROGBITS 00000000000001c8 000001c8 000000000000001b 0000000000000000 A 0 0 1 [ 2] .note.gnu.bu[...] NOTE 00000000000001e4 000001e4 0000000000000024 0000000000000000 A 0 0 4 [ 3] .gnu.hash GNU_HASH 0000000000000208 00000208 000000000000001c 0000000000000000 A 4 0 8 [ 4] .dynsym DYNSYM 0000000000000228 00000228 0000000000000018 0000000000000018 A 5 1 8 [ 5] .dynstr STRTAB 0000000000000240 00000240 0000000000000001 0000000000000000 A 0 0 1 [ 6] .text PROGBITS 0000000000000244 00000244 0000000000000028 0000000000000000 AX 0 0 4 [ 7] .dynamic DYNAMIC 000000000001ff10 0000ff10 00000000000000d0 0000000000000010 WA 5 0 8 [ 8] .got PROGBITS 000000000001ffe0 0000ffe0 0000000000000008 0000000000000008 WA 0 0 8 [ 9] .got.plt PROGBITS 000000000001ffe8 0000ffe8 0000000000000018 0000000000000008 WA 0 0 8 [10] .debug_aranges PROGBITS 0000000000000000 00010000 0000000000000030 0000000000000000 0 0 16 [11] .debug_info PROGBITS 0000000000000000 00010030 0000000000000028 0000000000000000 0 0 1 [12] .debug_abbrev PROGBITS 0000000000000000 00010058 0000000000000014 0000000000000000 0 0 1 [13] .debug_line PROGBITS 0000000000000000 0001006c 000000000000004e 0000000000000000 0 0 1 [14] .debug_str PROGBITS 0000000000000000 000100ba 0000000000000035 0000000000000001 MS 0 0 1 [15] .debug_line_str PROGBITS 0000000000000000 000100ef 0000000000000029 0000000000000001 MS 0 0 1 [16] .symtab SYMTAB 0000000000000000 00010118 00000000000002e8 0000000000000018 17 23 8 [17] .strtab STRTAB 0000000000000000 00010400 0000000000000071 0000000000000000 0 0 1 [18] .shstrtab STRTAB 0000000000000000 00010471 00000000000000bd 0000000000000000 0 0 1 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings), I (info), L (link order), O (extra OS processing required), G (group), T (TLS), C (compressed), x (unknown), o (OS specific), E (exclude), D (mbind), p (processor specific)
前回は、このプログラムを GDB を使って動作確認しました。
メモリに配置できるように機械語で埋め込む
参考サイトでは、この機械語を Linux のコマンドを使って加工しています。ちょっと難しかったので、ここでは手動で並べます(笑)。
先ほどの逆アセンブラのは、4byteずつ、10行並んでいました。この4byteは、リトルエンディアンなので、バイト単位でひっくり返します。それを連結しました。以下のようになりました。
main関数の1行は、shellcode を実行しています。shellcode は配列の先頭アドレスなので、これを関数だと思って見ると、関数の先頭アドレスとも言えます。この1行は関数ポインタを実行しているコードということになります。
関数ポインタのキャストは見にくいですが、void(*) が関数の戻り値の型(void)と関数ポインタから関数にするためのアスタリスクです。次の () は引数です。つまり、引数無し、戻り値無しの関数へのキャストです。先頭のアスタリスクは、なぜ付いてるのか分かりません。このアスタリスクは無くてもコンパイルも通りますし、実行についても変化はありません。たぶん、いらないんじゃないかと思っています。
char shellcode[] = "\x08\x01\x00\x58\xe2\x03\x1f\xaa\xe0\x03\x00\x91\xe8\x0b\xbf\xa8\xe1\x03\x00\x91\xe0\x0b\x00\xa9\xa8\x1b\x80\xd2\x01\x00\x00\xd4\x2f\x62\x69\x6e\x2f\x73\x68\x00"; int main() { ( *(void (*)())shellcode )(); }
これをコンパイルして、実行してみます。Segmentation Fault が出ました。
$ gcc -g -Wl,-Map=execve_str.map -static -o execve_str.out execve_str.c $ ./execve_str.out Segmentation fault
機械語で埋め込んだプログラムをGDBで確認する
GDB で確認します。長いので、いくつか省略します。0x490000 に 0x50 を加えたアドレスに分岐するようです。
$ gdb execve_str.out Reading symbols from execve_str.out... (gdb) start Temporary breakpoint 1 at 0x4006dc: file execve_str.c, line 7. Starting program: /home/daisuke/svn/experiment/c/execve_str.out Temporary breakpoint 1, main () at execve_str.c:7 7 (*(void (*)())shellcode)(); (gdb) disassemble Dump of assembler code for function main: 0x00000000004006d4 <+0>: stp x29, x30, [sp, #-16]! 0x00000000004006d8 <+4>: mov x29, sp => 0x00000000004006dc <+8>: adrp x0, 0x490000 0x00000000004006e0 <+12>: add x0, x0, #0x50 0x00000000004006e4 <+16>: blr x0 0x00000000004006e8 <+20>: mov w0, #0x0 // #0 0x00000000004006ec <+24>: ldp x29, x30, [sp], #16 0x00000000004006f0 <+28>: ret End of assembler dump.
分岐した後です。実装した通りのコードが並んでいます。どこで落ちるのかと言うと、最初の ldr命令を実行したところで落ちます。上は 40万番地付近でしたが、ここからは、49万番地となっています。
(gdb) disassemble Dump of assembler code for function shellcode: => 0x0000000000490050 <+0>: ldr x8, 0x490070 <shellcode+32> 0x0000000000490054 <+4>: mov x2, xzr 0x0000000000490058 <+8>: mov x0, sp 0x000000000049005c <+12>: stp x8, x2, [sp], #-16 0x0000000000490060 <+16>: mov x1, sp 0x0000000000490064 <+20>: stp x0, x2, [sp] 0x0000000000490068 <+24>: mov x8, #0xdd // #221 0x000000000049006c <+28>: svc #0x0 0x0000000000490070 <+32>: rsubhn2 v15.8h, v17.4s, v9.4s 0x0000000000490074 <+36>: .inst 0x0068732f ; undefined 0x0000000000490078 <+40>: udf #0 End of assembler dump.
マップファイルを見てみます。まず、分岐前の 40万番地付近です。普通の textセクションです。
*(.text.startup .text.startup.*) *fill* 0x0000000000400524 0xc 1f2003d5 .text.startup 0x0000000000400530 0x24 /usr/lib/gcc/aarch64-linux-gnu/12/libgcc.a(lse-init.o) *(.text.hot .text.hot.*) *(SORT_BY_NAME(.text.sorted.*)) *(.text .stub .text.* .gnu.linkonce.t.*) *fill* 0x0000000000400554 0x2c 1f2003d5 .text 0x0000000000400580 0x44 /usr/lib/gcc/aarch64-linux-gnu/12/../../../aarch64-linux-gnu/crt1.o 0x0000000000400580 _start 0x00000000004005c0 _dl_relocate_static_pie .text 0x00000000004005c4 0x14 /usr/lib/gcc/aarch64-linux-gnu/12/../../../aarch64-linux-gnu/crti.o *fill* 0x00000000004005d8 0x8 1f2003d5 .text 0x00000000004005e0 0xf4 /usr/lib/gcc/aarch64-linux-gnu/12/crtbeginT.o .text 0x00000000004006d4 0x20 /tmp/ccG8mGtX.o 0x00000000004006d4 main *fill* 0x00000000004006f4 0xc 1f2003d5 .text 0x0000000000400700 0x42c /usr/lib/gcc/aarch64-linux-gnu/12/../../../aarch64-linux-gnu/libc.a(libc-start.o) 0x00000000004007e4 __libc_start_main_impl 0x00000000004007e4 __libc_start_main *fill* 0x0000000000400b2c 0x4 1f2003d5
続いて、分岐後の機械語を埋め込んだ方を見てみます。dataセクションになっていました。ここは実行可能なところではないんだと思います。
.data 0x0000000000490040 0x1948
[!provide] PROVIDE (__data_start = .)
*(.data .data.* .gnu.linkonce.d.*)
.data 0x0000000000490040 0x4 /usr/lib/gcc/aarch64-linux-gnu/12/../../../aarch64-linux-gnu/crt1.o
0x0000000000490040 data_start
0x0000000000490040 __data_start
.data 0x0000000000490044 0x0 /usr/lib/gcc/aarch64-linux-gnu/12/../../../aarch64-linux-gnu/crti.o
*fill* 0x0000000000490044 0x4
.data 0x0000000000490048 0x8 /usr/lib/gcc/aarch64-linux-gnu/12/crtbeginT.o
0x0000000000490048 __dso_handle
.data 0x0000000000490050 0x29 /tmp/ccG8mGtX.o
0x0000000000490050 shellcode
セクションヘッダを見てみます。textセクションは X が付いてるので、実行可能な領域です。一方、dataセクションは X が付いていません。これが原因だと思われます。
$ readelf -S execve_str.out There are 35 section headers, starting at offset 0xab428: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .note.gnu.bu[...] NOTE 0000000000400190 00000190 0000000000000024 0000000000000000 A 0 0 4 [ 2] .note.ABI-tag NOTE 00000000004001b4 000001b4 0000000000000020 0000000000000000 A 0 0 4 [ 3] .rela.plt RELA 00000000004001d8 000001d8 00000000000000a8 0000000000000018 AI 32 18 8 [ 4] .init PROGBITS 0000000000400280 00000280 0000000000000018 0000000000000000 AX 0 0 4 [ 5] .plt PROGBITS 00000000004002a0 000002a0 0000000000000070 0000000000000000 AX 0 0 16 [ 6] .text PROGBITS 0000000000400340 00000340 0000000000056394 0000000000000000 AX 0 0 64 [ 7] __libc_freeres_fn PROGBITS 00000000004566e0 000566e0 0000000000000b04 0000000000000000 AX 0 0 16 [ 8] .fini PROGBITS 00000000004571e4 000571e4 0000000000000014 0000000000000000 AX 0 0 4 [ 9] .rodata PROGBITS 0000000000457200 00057200 000000000001a320 0000000000000000 A 0 0 16 [10] .eh_frame PROGBITS 0000000000471520 00071520 000000000000ba84 0000000000000000 A 0 0 8 [11] .gcc_except_table PROGBITS 000000000047cfa4 0007cfa4 00000000000000dc 0000000000000000 A 0 0 1 [12] .tdata PROGBITS 000000000048c710 0008c710 0000000000000018 0000000000000000 WAT 0 0 8 [13] .tbss NOBITS 000000000048c728 0008c728 0000000000000048 0000000000000000 WAT 0 0 8 [14] .init_array INIT_ARRAY 000000000048c728 0008c728 0000000000000010 0000000000000008 WA 0 0 8 [15] .fini_array FINI_ARRAY 000000000048c738 0008c738 0000000000000008 0000000000000008 WA 0 0 8 [16] .data.rel.ro PROGBITS 000000000048c740 0008c740 0000000000003498 0000000000000000 WA 0 0 16 [17] .got PROGBITS 000000000048fbd8 0008fbd8 0000000000000408 0000000000000008 WA 0 0 8 [18] .got.plt PROGBITS 000000000048ffe8 0008ffe8 0000000000000050 0000000000000008 WA 0 0 8 [19] .data PROGBITS 0000000000490040 00090040 0000000000001948 0000000000000000 WA 0 0 16 [20] __libc_subfreeres PROGBITS 0000000000491988 00091988 0000000000000048 0000000000000000 WAR 0 0 8 [21] __libc_IO_vtables PROGBITS 00000000004919d0 000919d0 0000000000000690 0000000000000000 WA 0 0 8 [22] __libc_atexit PROGBITS 0000000000492060 00092060 0000000000000008 0000000000000000 WAR 0 0 8 [23] .bss NOBITS 0000000000492070 00092068 00000000000054f8 0000000000000000 WA 0 0 16 [24] __libc_freer[...] NOBITS 0000000000497568 00092068 0000000000000020 0000000000000000 WA 0 0 8 [25] .comment PROGBITS 0000000000000000 00092068 000000000000001f 0000000000000001 MS 0 0 1 [26] .debug_aranges PROGBITS 0000000000000000 00092087 0000000000000030 0000000000000000 0 0 1 [27] .debug_info PROGBITS 0000000000000000 000920b7 0000000000000088 0000000000000000 0 0 1 [28] .debug_abbrev PROGBITS 0000000000000000 0009213f 0000000000000068 0000000000000000 0 0 1 [29] .debug_line PROGBITS 0000000000000000 000921a7 000000000000004f 0000000000000000 0 0 1 [30] .debug_str PROGBITS 0000000000000000 000921f6 0000000000000070 0000000000000001 MS 0 0 1 [31] .debug_line_str PROGBITS 0000000000000000 00092266 000000000000002d 0000000000000001 MS 0 0 1 [32] .symtab SYMTAB 0000000000000000 00092298 0000000000012318 0000000000000018 33 1953 8 [33] .strtab STRTAB 0000000000000000 000a45b0 0000000000006cfe 0000000000000000 0 0 1 [34] .shstrtab STRTAB 0000000000000000 000ab2ae 0000000000000178 0000000000000000 0 0 1 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings), I (info), L (link order), O (extra OS processing required), G (group), T (TLS), C (compressed), x (unknown), o (OS specific), E (exclude), R (retain), D (mbind), p (processor specific)
機械語を埋め込んだプログラムが実行できるように対策する
参考サイトでは、スタックに配置されたので、スタック領域を実行可能にする対策を行っていました。こちらは、dataセクションに配置されているので、別の対策が必要です。
いろいろ調べたところ、C言語の mprotect という関数があるようです。メモリのアドレスとサイズを指定して、Read/Write/Exec の属性を変更することが出来るようです。プロトタイプは、int mprotect(void *addr, size_t len, int prot); です。
注意点としては、第1引数の addr は、ページ境界でなければならないということです。
以下のようになりました。ページ境界とするため、ページサイズを表示しています。その後、
#include <stdio.h> #include <unistd.h> #include <sys/mman.h> // mprotect関数を使用するために必要 char shellcode[] = "\x08\x01\x00\x58\xe2\x03\x1f\xaa\xe0\x03\x00\x91\xe8\x0b\xbf\xa8\xe1\x03\x00\x91\xe0\x0b\x00\xa9\xa8\x1b\x80\xd2\x01\x00\x00\xd4\x2f\x62\x69\x6e\x2f\x73\x68\x00"; int main() { // メモリページサイズを取得 long page_size = sysconf( _SC_PAGESIZE ); printf( "page_size=0x%x\n", page_size ); if( mprotect((void *)0x490000, 0x1940, PROT_READ | PROT_WRITE | PROT_EXEC) == -1 ){ perror( "mprotect failed" ); return 1; } ( (void (*)())shellcode )(); }
コンパイルして、実行してみます。シェルの起動に成功しました!
$ gcc -g -Wl,-Map=execve_str_fixed.map -static -o execve_str_fixed.out execve_str_fixed.c $ ./execve_str_fixed.out page_size=0x1000 $ ls execve.map execve_chatgpt2.map execve_objdump.s execve_x86.txt execve.out execve_chatgpt2.out execve_str.c k_and_r_org.c execve.s execve_chatgpt2.s execve_str.map main_execve.s execve_c.c execve_chatgpt2_objdump.s execve_str.out memorymap.c execve_c.out execve_chatgpt_fixed.map execve_str_fixed.c memorymap.out execve_chatgpt.map execve_chatgpt_fixed.out execve_str_fixed.map execve_chatgpt.out execve_chatgpt_fixed.s execve_str_fixed.out execve_chatgpt.s execve_chatgpt_objdump.s execve_str_fixed_objdump.s $ exit
今回はここまでです。
おわりに
今回は、参考サイトを見ながら、execve を使ったシェルを起動するプログラムを、機械語を埋め込んだプログラムに変更して動作を確認しました。
結構時間がかかりましたが、低レイヤのプログラムについて、いろいろ周辺の知識が深まりました。
最後になりましたが、エンジニアグループのランキングに参加中です。
気楽にポチッとよろしくお願いいたします🙇
今回は以上です!
最後までお読みいただき、ありがとうございました。