前回 は、K&Rのmalloc関数とfree関数を動かして理解しました。
今回は、Exploitコードの部品となりそうな C言語からシェルを起動するプログラムを実装してみたいと思います。
それでは、やっていきます。
- 参考文献
- はじめに
- C言語でシェルを起動するプログラムを実装する
- C言語でシェルを起動するプログラムをVSCodeでデバッグする
- アセンブラでシェルを起動するプログラムを実装する
- アセンブラでシェルを起動するプログラムをGDBでデバッグする
- アセンブラでシェルを起動するプログラムを実装する(再)
- アセンブラでシェルを起動するプログラムをGDBでデバッグする(再)
- アセンブラでスタック上でシェル起動を実行するプログラムを実装する
- アセンブラでスタック上でシェル起動を実行するプログラムを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) ← 今回
例えば、バッファオーバーフローの不具合(脆弱性)があるプログラムがあるとすると、よく任意のコードが実行できる、とか聞くと思います。よく聞くんですけど、実際にどういうプログラムを実行するのか、今まで知りませんでした。
今回は、以下のサイトを参考させて頂いて、C言語からシェルを起動するプログラムを実装してみたいと思います。
上の記事の ARM32bit版の記事もありました。
また、64bitARM(Aarch64)のアセンブラについては、以下のサイトを参考にさせて頂きました。
それでは、やっていきます。
C言語でシェルを起動するプログラムを実装する
初めての内容なので、参考にさせて頂いたサイト(以下、参考サイト)のプログラムを、ほぼそのまま使わせて頂きました。参考サイトは、x86、ARM32bit のコードのようですが、ここでは、ラズパイ4 を使って、ARM64bit のプログラムでやってみたいと思います。
C言語のソースコードとしては、2行だけです。execve関数は、指定したファイル名のプログラムを実行します。
#include <unistd.h> // libc をスタティックリンクすること // $ gcc -static -o execve.out execve.c int main( int argc, char *argv[] ) { char *args[] = { "/bin/sh", NULL }; // 第1引数:プログラムパス → 必ずパスで指定すること (環境変数 PATH は参照されない) // 第2引数:プログラムに渡す引数の配列 // 第3引数:環境変数の配列 execve( args[0], args, NULL ); }
では、ラズパイ4 上で、実際にコンパイルして、動かしてみたいと思います。スタティックリンクで、コンパイルしたので、687KB の実行ファイルになりました。参考サイトが、スタティックリンクにした理由は、libc のアセンブラも見たかったからか、デバッグしたかったからでしょうか。
実際に、動的リンクにして、動かしてみると、main関数から、libc の execve関数を呼ぶのですが、初めて共有ライブラリ関数を実行するときは、動的リンカによる PLT のリンク解決の処理を実行する必要があるので、libc の execve関数にたどり着くまで、かなり長いアセンブラのステップ実行が必要になります。これを回避したかったのかもしれません。
少し脱線しました。コンパイルと実行です。シェルからシェルを起動してるようなものなので、分かりにくいので、コンパイルしたりするシェルの方には、カレントディレクトリを付けました。
execve.out を実行すると、シェルが起動して、ls と exit を実行することが出来ました。
~/svn/experiment/c $ gcc -static -o execve.out execve.c ~/svn/experiment/c $ ./execve.out $ ls execve execve.c execve.out k_and_r_org.c $ exit ~/svn/experiment/c $
C言語でシェルを起動するプログラムをVSCodeでデバッグする
参考サイトの行っている通りに、こちらでもやっていきます。
まず、objdumpコマンドで、アセンブラを確認します。
$ objdump -d execve.out > execve_objdump.s
かなり大きなファイル(3,868KB)が出力されました。main関数を検索で探しました。
x0、x1、x2 の 3つを引数として、400700 で、__execve関数を呼び出しているようです。
00000000004006d4 <main>: 4006d4: a9bd7bfd stp x29, x30, [sp, #-48]! // スタック退避 4006d8: 910003fd mov x29, sp // x29 ← sp コピー 4006dc: b9001fe0 str w0, [sp, #28] // w0 → [sp + 28] 1を[SP+28]にセット (32bit) 4006e0: f9000be1 str x1, [sp, #16] // x1 → [sp + 16] 0x0000007fffffef98を[sp+16]にセット 4006e4: f00002a0 adrp x0, 457000 <_nl_archive_subfreeres+0xe0> // x0 ← 0x457000 4006e8: 91092000 add x0, x0, #0x248 // x0 ← x0 + 0x248 (0x457000 + 0x248) 4006ec: f90013e0 str x0, [sp, #32] // x0 → [sp + 32] 0x0000000000457248を[sp+32]にセット 4006f0: f90017ff str xzr, [sp, #40] // #0 → [sp + 40] 0を[sp+40]にセット 4006f4: f94013e0 ldr x0, [sp, #32] // x0 ← [sp + 32] 第1引数に0x0000000000457248をセット 4006f8: 910083e1 add x1, sp, #0x20 // x1 ← sp + 20 第2引数に0x0000007fffffedd0をセット 4006fc: d2800002 mov x2, #0x0 // #0 第3引数にNULLをセット 400700: 94002660 bl 40a080 <__execve> 400704: 52800000 mov w0, #0x0 // #0 400708: a8c37bfd ldp x29, x30, [sp], #48 40070c: d65f03c0 ret
同様に、__execve関数の方も見てみます。
40a088 で、システムコールを呼んでいるようです。
000000000040a080 <__execve>: 40a080: d503201f nop 40a084: d2801ba8 mov x8, #0xdd // #221 40a088: d4000001 svc #0x0 // システムコール 40a08c: b13ffc1f cmn x0, #0xfff 40a090: 54000042 b.cs 40a098 <__execve+0x18> // b.hs, b.nlast 40a094: d65f03c0 ret 40a098: 14000e02 b 40d8a0 <__syscall_error> 40a09c: d503201f nop
参考サイトは、この後、GDB で追いかけたようですが、こちらでは、VSCode で追いかけてみます。
システムコール実行直前まできました。コメントでは、221番のシステムコールを呼び出そうとしているようです。

システムコールを調べると、221番は、確かに、execve でした。

SVC命令の詳細は、以下で詳しく解説がされています。
確かに、x8 レジスタに、221 をセットしています。
x0 から x5 に、システムコールの引数を設定するそうです。今回の execve は引数が 3つなので、x0 から x2 までを使用すると思います。
システムコールを実行直前のレジスタダンプと、スタックの状態です。
レジスタダンプは一部省略しています。左側が16進数で右側が10進数のようです。
-exec i r x0 0x457248 4551240 x1 0x7fffffedd0 549755809232 x2 0x0 0 x3 0x4005b4 4195764 x4 0x7fffffede0 549755809248 x5 0x235f6cea5f7fb822 2548875667995342882 x6 0x492908 4794632 x7 0x2 2 x8 0xdd 221 sp 0x7fffffedb0 0x7fffffedb0 pc 0x400704 0x400704 <main+48>
次にスタックを16個分ダンプしました。
-exec x/16xg $sp 0x7fffffedb0: 0x0000007fffffede0 0x00000000004007b8 0x7fffffedc0: 0x0000007fffffef98 0x000000010040077c 0x7fffffedd0: 0x0000000000457248 0x0000000000000000 0x7fffffede0: 0x0000007fffffeef0 0x0000000000400b34 0x7fffffedf0: 0x000000000048c710 0x00000000004005b4 0x7fffffee00: 0x0000000100490a60 0x0000007fffffef98 0x7fffffee10: 0x0000000000000001 0x0000007fffffef98 0x7fffffee20: 0x0000000000000002 0x0000007fffffefa8
x0レジスタが指しているメモリの値を見てみます。
ASCIIコードで見ると、"/bin/sh" です。バッチリです。
-exec x/8bx $x0
0x457248: 0x2f 0x62 0x69 0x6e 0x2f 0x73 0x68 0x00
次に、x1レジスタが指しているメモリを見てみます。
上の 0x457248 へのポインタと、NULL が入ってます。こちらもバッチリです。
-exec x/2xg $x1
0x7fffffedd0: 0x0000000000457248 0x0000000000000000
execveシステムコールを呼ぶのに必要な内容は見れたと思います。
参考サイトでは、ここから、アセンブラコードを実装していきます。こちらでも、ARM のアセンブラコードを実装してみたいと思います。
アセンブラでシェルを起動するプログラムを実装する
自力で書こうと思っていたのですが、調べるのに時間がかかりそうなので、まずは、ChatGPT に書いてもらいました(笑)。
.section .data args: .string "/bin/sh" // "/bin/sh" 文字列 .quad 0 // NULL 終端 .section .text .globl _start _start: // 引数の設定 ldr x0, =args // x0 に "/bin/sh" のアドレスをロード add x1, x0, #8 // x1 に args 配列のアドレスをロード ("/bin/sh" と NULL のポインタ配列) mov x2, #0 // x2 に NULL をセット (環境変数なし) // execve システムコールの呼び出し mov x8, #221 // x8 に execve のシステムコール番号をセット (221) svc #0 // システムコールの実行 // プログラム終了(エラー処理) mov x8, #93 // x8 に exit のシステムコール番号をセット (93) mov x0, #0 // x0 に終了コードをセット (0) svc #0 // システムコールの実行
それらしい感じで、うまくいきそうです。
では、アセンブルします。と言っても、gcc がアセンブルもリンクもやってくれるようです。スタティックリンクの代わり?でしょうか。-nostdlib を付けます。ついでに、逆アセンブラも出力しておきます。Web には、このあたりの情報は、かなり少ない印象です。正しいかどうかが少し不安です。
gcc -g -o execve.out -nostdlib execve.s $ objdump -d execve.out execve.out: file format elf64-littleaarch64 Disassembly of section .text: 0000000000000290 <_start>: 290: 58000100 ldr x0, 2b0 <_start+0x20> 294: 91002001 add x1, x0, #0x8 298: d2800002 mov x2, #0x0 // #0 29c: d2801ba8 mov x8, #0xdd // #221 2a0: d4000001 svc #0x0 2a4: d2800ba8 mov x8, #0x5d // #93 2a8: d2800000 mov x0, #0x0 // #0 2ac: d4000001 svc #0x0 2b0: 00020000 .word 0x00020000 2b4: 00000000 .word 0x00000000
このコードが正しいかをデバッガで確認します。
アセンブラファイルを、アセンブルして、VSCode でデバッグする方法を探したのですが、見つかりませんでした。対応していないのかもしれません。
仕方ないので、苦手ですけど、gdb を直接つかいます。
アセンブラでシェルを起動するプログラムをGDBでデバッグする
とにかく、GDB を起動します。シンボル情報は読み込まれたようです。
$ gdb execve.out GNU gdb (Debian 13.1-3) 13.1 Copyright (C) 2023 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "aarch64-linux-gnu". Type "show configuration" for configuration details. For bug reporting instructions, please see: <https://www.gnu.org/software/gdb/bugs/>. Find the GDB manual and other documentation resources online at: <http://www.gnu.org/software/gdb/documentation/>. For help, type "help". Type "apropos word" to search for commands related to "word"... Reading symbols from execve.out... (gdb)
続いて、ブレークポイントを設定します。アセンブラソース execve.s の10行目、0x290 にブレークポイントを設定できたようです。
(gdb) b _start Breakpoint 1 at 0x290: file execve.s, line 10.
では、デバッグ開始します。なぜか共有ライブラリをロードしますか?と聞かれますが、No です。dl-start.S が無いと言われてますが、よく分かりません。
(gdb) start Function "main" not defined. Make breakpoint pending on future shared library load? (y or [n]) n Starting program: /home/daisuke/svn/experiment/c/execve.out Breakpoint 1.2, _start () at ../sysdeps/aarch64/dl-start.S:22 22 ../sysdeps/aarch64/dl-start.S: No such file or directory. (gdb)
逆アセンブラを表示してみます。うーん、少し想定していたものではありません。アセンブラソースに書いた _start とは違ってます。C言語で言うところのスタートアップルーチン的なコードでしょうか。
(gdb) disassemble Dump of assembler code for function _start: => 0x0000007ff7fd8980 <+0>: nop 0x0000007ff7fd8984 <+4>: mov x29, #0x0 // #0 0x0000007ff7fd8988 <+8>: mov x30, #0x0 // #0 0x0000007ff7fd898c <+12>: mov x0, sp 0x0000007ff7fd8990 <+16>: bl 0x7ff7fd44c0 <_dl_start> 0x0000007ff7fd8994 <+20>: mov x21, x0 0x0000007ff7fd8998 <+24>: ldr x1, [sp] 0x0000007ff7fd899c <+28>: add x2, sp, #0x8 0x0000007ff7fd89a0 <+32>: add x3, x2, x1, lsl #3 0x0000007ff7fd89a4 <+36>: add x3, x3, #0x8 0x0000007ff7fd89a8 <+40>: adrp x16, 0x7ff7ffe000 <_dl_catch_exception@got.plt> 0x0000007ff7fd89ac <+44>: add x16, x16, #0x28 0x0000007ff7fd89b0 <+48>: ldr x0, [x16] 0x0000007ff7fd89b4 <+52>: bl 0x7ff7fc23b0 <_dl_init> 0x0000007ff7fd89b8 <+56>: adrp x0, 0x7ff7fc2000 <_dl_find_object_dlclose+160> 0x0000007ff7fd89bc <+60>: add x0, x0, #0xb0 0x0000007ff7fd89c0 <+64>: mov x16, x21 0x0000007ff7fd89c4 <+68>: br x16 End of assembler dump. (gdb) i r x0 0x0 0 x1 0x0 0 x2 0x0 0 x3 0x0 0 x4 0x0 0 x5 0x0 0 x6 0x0 0 x7 0x0 0 x8 0x0 0 x9 0x0 0 x10 0x0 0 x11 0x0 0 x12 0x0 0 x13 0x0 0 x14 0x0 0 x15 0x0 0 x16 0x0 0 x17 0x0 0 x18 0x0 0 x19 0x0 0 x20 0x0 0 x21 0x0 0 x22 0x0 0 x23 0x0 0 x24 0x0 0 x25 0x0 0 x26 0x0 0 x27 0x0 0 x28 0x0 0 x29 0x0 0 x30 0x0 0 sp 0x7ffffff400 0x7ffffff400 pc 0x7ff7fd8980 0x7ff7fd8980 <_start> cpsr 0x0 [ EL=0 BTYPE=0 ] fpsr 0x0 [ ] fpcr 0x0 [ Len=0 Stride=0 RMode=0 ] tpidr 0x0 0x0 tpidr2 0x0 0x0
続きを実行してみます。
うーん、これが最初に見えてたら、想定通りでした。ここも _start なんでしょうか。
x0 に、0x55555502b0 が指しているメモリの内容を格納しています。つまり、x0 には、0x55570000 が入ります。その後、x1 には、x0 を +8 した値が入ります(0x55570008)。
(gdb) c Continuing. warning: Temporarily disabling breakpoints for unloaded shared library "/lib/ld-linux-aarch64.so.1" Breakpoint 1.1, _start () at execve.s:10 10 ldr x0, =args // x0 に "/bin/sh" のアドレスをロード (gdb) disassemble Dump of assembler code for function _start: => 0x0000005555550290 <+0>: ldr x0, 0x55555502b0 <_start+32> 0x0000005555550294 <+4>: add x1, x0, #0x8 0x0000005555550298 <+8>: mov x2, #0x0 // #0 0x000000555555029c <+12>: mov x8, #0xdd // #221 0x00000055555502a0 <+16>: svc #0x0 0x00000055555502a4 <+20>: mov x8, #0x5d // #93 0x00000055555502a8 <+24>: mov x0, #0x0 // #0 0x00000055555502ac <+28>: svc #0x0 0x00000055555502b0 <+32>: .inst 0x55570000 ; undefined 0x00000055555502b4 <+36>: udf #85 End of assembler dump.
メモリの内容を確認してみます。まず、x0 に格納される値を確認すると、0x0000005555570000 です。合ってます。その次は、0x0000005555570000 が指しているアドレスのメモリは、"/bin/sh" っぽい内容です。
(gdb) x/xg 0x55555502b0 0x55555502b0 <_start+32>: 0x0000005555570000 (gdb) x/8xb 0x0000005555570000 0x5555570000: 0x2f 0x62 0x69 0x6e 0x2f 0x73 0x68 0x00
まずは、そのまま実行してみます。何やら、いろいろメッセージが出ますが、やりたいことは出来ているようです。
(gdb) c Continuing. process 2998 is executing new program: /usr/bin/dash Error in re-setting breakpoint 1: Function "_start" not defined. [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/aarch64-linux-gnu/libthread_db.so.1". $ ls [Detaching after vfork from child process 3006] a.out execve.c execve.o execve.s execve_x86.txt main_execve.s execve execve.map execve.out execve_objdump.s $ exit [Inferior 1 (process 2998) exited with code 0177] (gdb) q
アセンブラでシェルを起動するプログラムを実装する(再)
やりたいことは出来ていますが、少しおかしい気がしてきました。
以下の ChatGPT が作ってくれたアセンブラですが、第2引数の x1 に x0 + 8 をセットしてますが、x0 には "/bin/sh" のアドレスが入っていて、その 8byte 先には NULL(0)が入ってるだけです。execve の第2引数には、args のアドレスをセットしなければならないはずです。
他のサイトで、第2引数に NULL を指定していました。マニュアルを見る限り、NULL だけではダメだというように書かれてる気がします。実際に、上でやった C言語版で、第2引数を NULL で実行してみると、普通に動きました。なるべく、第2引数は設定した方がいい、と理解することにします。
さて、この第2引数だけ修正したかったのですが、mov x1, =args はアセンブルエラーになりました。mov x1, args は、なぜか、x1 に 0 が入るアセンブラになりました。
.section .data args: .string "/bin/sh" // "/bin/sh" 文字列 .quad 0 // NULL 終端 .section .text .globl _start _start: // 引数の設定 ldr x0, =args // x0 に "/bin/sh" のアドレスをロード add x1, x0, #8 // x1 に args 配列のアドレスをロード ("/bin/sh" と NULL のポインタ配列) mov x2, #0 // x2 に NULL をセット (環境変数なし) // execve システムコールの呼び出し mov x8, #221 // x8 に execve のシステムコール番号をセット (221) svc #0 // システムコールの実行 // プログラム終了(エラー処理) mov x8, #93 // x8 に exit のシステムコール番号をセット (93) mov x0, #0 // x0 に終了コードをセット (0) svc #0 // システムコールの実行
分からないので、もう1度、ChatGPT に別の文章で同じ内容でお願いしてみました。すると、今度は、想定したソースコードを書いてくれたような気がします。
.global _start // エントリーポイントをグローバルとして定義 _start: ldr x0, =binsh // "/bin/sh" を含む文字列のアドレスをレジスタ x0 にセット adr x1, argv // argv の配列のアドレスをレジスタ x1 にセット mov x2, xzr // x2 レジスタを NULL に設定(envp のため) mov x8, #221 // execve システムコール番号を x8 にセット (221はexecveのシステムコール番号) svc #0 // システムコールの呼び出し binsh: .asciz "/bin/sh" // シェルのパスをNULL終端文字列としてメモリに定義 argv: .quad binsh // argv[0] は "/bin/sh" を指す .quad 0 // argv[1] は NULL を指す
アセンブラでシェルを起動するプログラムをGDBでデバッグする(再)
アセンブルして、objdump もしてみます。
gcc -g -Wl,-Map=execve.map -o execve.out -nostdlib execve.s
objdump の結果です。いい感じです。
execve.out: file format elf64-littleaarch64 Disassembly of section .text: 0000000000000290 <_start>: 290: 58000180 ldr x0, 2c0 <argv+0x14> 294: 100000c1 adr x1, 2ac <argv> 298: aa1f03e2 mov x2, xzr 29c: d2801ba8 mov x8, #0xdd // #221 2a0: d4000001 svc #0x0 00000000000002a4 <binsh>: 2a4: 6e69622f .word 0x6e69622f 2a8: 0068732f .word 0x0068732f 00000000000002ac <argv>: 2ac: 000002a4 .word 0x000002a4 ... 2c0: 000002a4 .word 0x000002a4 2c4: 00000000 .word 0x00000000
GDB でデバッグしてみます。まずは、上のアセンブラが実行される直前まで行きます。
$ gdb execve.out Reading symbols from execve.out... (gdb) b _start Breakpoint 1 at 0x290: file execve.s, line 4. (gdb) start Function "main" not defined. Make breakpoint pending on future shared library load? (y or [n]) n Starting program: /home/daisuke/svn/experiment/c/execve.out Breakpoint 1.2, _start () at ../sysdeps/aarch64/dl-start.S:22 22 ../sysdeps/aarch64/dl-start.S: No such file or directory. (gdb) disas Dump of assembler code for function _start: => 0x0000007ff7fd8980 <+0>: nop 0x0000007ff7fd8984 <+4>: mov x29, #0x0 // #0 0x0000007ff7fd8988 <+8>: mov x30, #0x0 // #0 0x0000007ff7fd898c <+12>: mov x0, sp 0x0000007ff7fd8990 <+16>: bl 0x7ff7fd44c0 <_dl_start> 0x0000007ff7fd8994 <+20>: mov x21, x0 0x0000007ff7fd8998 <+24>: ldr x1, [sp] 0x0000007ff7fd899c <+28>: add x2, sp, #0x8 0x0000007ff7fd89a0 <+32>: add x3, x2, x1, lsl #3 0x0000007ff7fd89a4 <+36>: add x3, x3, #0x8 0x0000007ff7fd89a8 <+40>: adrp x16, 0x7ff7ffe000 <_dl_catch_exception@got.plt> 0x0000007ff7fd89ac <+44>: add x16, x16, #0x28 0x0000007ff7fd89b0 <+48>: ldr x0, [x16] 0x0000007ff7fd89b4 <+52>: bl 0x7ff7fc23b0 <_dl_init> 0x0000007ff7fd89b8 <+56>: adrp x0, 0x7ff7fc2000 <_dl_find_object_dlclose+160> 0x0000007ff7fd89bc <+60>: add x0, x0, #0xb0 0x0000007ff7fd89c0 <+64>: mov x16, x21 0x0000007ff7fd89c4 <+68>: br x16 End of assembler dump. (gdb) c Continuing. warning: Temporarily disabling breakpoints for unloaded shared library "/lib/ld-linux-aarch64.so.1" Breakpoint 1.1, _start () at execve.s:4 4 ldr x0, =binsh // "/bin/sh" を含む文字列のアドレスをレジスタ x0 にセット (gdb) disas Dump of assembler code for function _start: => 0x0000005555550290 <+0>: ldr x0, 0x55555502c0 <argv+20> 0x0000005555550294 <+4>: adr x1, 0x55555502ac <argv> 0x0000005555550298 <+8>: mov x2, xzr 0x000000555555029c <+12>: mov x8, #0xdd // #221 0x00000055555502a0 <+16>: svc #0x0 End of assembler dump.
直前まで来ました。x0 と x1 にセットされるところまで実行して、レジスタの値を見てみます。その後、それぞれが指しているメモリの内容をダンプしてみます。
(gdb) si 6 adr x1, argv // argv の配列のアドレスをレジスタ x1 にセット (gdb) si 7 mov x2, xzr // x2 レジスタを NULL に設定(envp のため) (gdb) i r x0 0x55555502a4 366503854756 x1 0x55555502ac 366503854764 x2 0x0 0 x3 0x0 0 x4 0x410 1040 x5 0x2 2 x6 0x3f 63 x7 0x7c1 1985 x8 0x7ff7fff380 549621592960 x9 0x7ff7ff7d00 549621562624 x10 0x420000000000 72567767433216 x11 0x7fffffea70 549755808368 x12 0x7ff7ff71b0 549621559728 x13 0x360ed96 56683926 x14 0x7ff7ffe028 549621588008 x15 0x1 1 x16 0x5555550290 366503854736 x17 0x0 0 x18 0x0 0 x19 0x0 0 x20 0x0 0 x21 0x5555550290 366503854736 x22 0x0 0 x23 0x0 0 x24 0x0 0 x25 0x0 0 x26 0x0 0 x27 0x0 0 x28 0x0 0 x29 0x0 0 x30 0x7ff7fd89b8 549621434808 sp 0x7ffffff400 0x7ffffff400 pc 0x5555550298 0x5555550298 <_start+8> cpsr 0x60200000 [ EL=0 BTYPE=0 SS C Z ] fpsr 0x0 [ ] fpcr 0x0 [ Len=0 Stride=0 RMode=0 ] tpidr 0x7ff7ff7d00 0x7ff7ff7d00 tpidr2 0x0 0x0 (gdb) x/8bx 0x55555502a4 0x55555502a4 <binsh>: 0x2f 0x62 0x69 0x6e 0x2f 0x73 0x68 0x00 (gdb) x/2gx $x1 0x55555502ac <argv>: 0x00000055555502a4 0x0000000000000000
想定通りの内容です!では、このまま続きを実行します。
(gdb) c Continuing. process 1985 is executing new program: /usr/bin/dash Error in re-setting breakpoint 1: Function "_start" not defined. [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/aarch64-linux-gnu/libthread_db.so.1". $ ls [Detaching after vfork from child process 1990] execve.c execve_chatgpt.map execve_chatgpt_fixed.out execve_str.map main_execve.s execve.map execve_chatgpt.out execve_chatgpt_fixed.s execve_str.out execve.out execve_chatgpt.s execve_chatgpt_objdump.s execve_x86.txt execve.s execve_chatgpt_fixed.map execve_str.c k_and_r_org.c $ exit [Inferior 1 (process 1985) exited normally] (gdb) q
動作としても問題ありませんでした。
アセンブラでスタック上でシェル起動を実行するプログラムを実装する
今回やりたかったのは、バッファオーバーフローさせて、そのメモリにシェルを起動するコードを置いて、実行するプログラムです。上のプログラムは、ldr x0, 2c0 というように、アドレスを直接指定してしまっています。これでは、どんなアドレスになるか分からない場所にコードを置くことは出来ません。よって、参考サイトのように、スタックを使って引数を作りこむ必要があります(でも、よく説明を見ると、ldr x0, 2c0 は、PC の相対アドレスに変換される、と書かれているので、実は問題ないのかもしれません)。
ChatGPT の作ってくれるアセンブラを見てきたので、そろそろ自分でも実装できそうです。以下に実装してみました。"ldr x8, binsh" を使ってますが、PC の相対アドレスに変換される、というのを信じてやってみます。
.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終端文字列としてメモリに定義
では、アセンブルして、objdump の結果を取得します。
$ gcc -g -Wl,-Map=execve.map -o execve.out -nostdlib execve.s $ objdump -d execve.out > execve_objdump.s
objdump の結果です。
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
アセンブラでスタック上でシェル起動を実行するプログラムをGDBでデバッグする
デバッグします。
$ gdb execve.out Reading symbols from execve.out... (gdb) b _start Breakpoint 1 at 0x244: file execve.s, line 4. (gdb) start Function "main" not defined. Make breakpoint pending on future shared library load? (y or [n]) n Starting program: /home/daisuke/svn/experiment/c/execve.out Breakpoint 1.2, _start () at ../sysdeps/aarch64/dl-start.S:22 22 ../sysdeps/aarch64/dl-start.S: No such file or directory. (gdb) c Continuing. warning: Temporarily disabling breakpoints for unloaded shared library "/lib/ld-linux-aarch64.so.1" Breakpoint 1.1, _start () at execve.s:4 4 ldr x8, binsh // "/bin/sh" を含む文字列のアドレスを x8 にセット (gdb) disas Dump of assembler code for function _start: => 0x0000005555550244 <+0>: ldr x8, 0x5555550264 <binsh> 0x0000005555550248 <+4>: mov x2, xzr 0x000000555555024c <+8>: mov x0, sp 0x0000005555550250 <+12>: stp x8, x2, [sp], #-16 0x0000005555550254 <+16>: mov x1, sp 0x0000005555550258 <+20>: stp x0, x2, [sp] 0x000000555555025c <+24>: mov x8, #0xdd // #221 0x0000005555550260 <+28>: svc #0x0 End of assembler dump. (gdb) x/8bx 0x5555550264 0x5555550264 <binsh>: 0x2f 0x62 0x69 0x6e 0x2f 0x73 0x68 0x00 (gdb) si 6 mov x2, xzr // x2 レジスタを NULL に設定 (gdb) 7 mov x0, sp // x0 に sp の値をセット (第1引数) (gdb) i r sp sp 0x7ffffff400 0x7ffffff400 (gdb) si 8 stp x8, x2, [sp], #-16 // x0("/bin/sh") と x2(NULL) の値をスタックにプッシュ (ポストインデックス) ※x2 は不要かも (gdb) i r x0 x0 0x7ffffff400 549755810816 (gdb) i r x8 x8 0x68732f6e69622f 29400045130965551 (gdb) i r x2 x2 0x0 0 (gdb) si _start () at execve.s:10 10 mov x1, sp // x1 に sp の値をセット (第2引数) (gdb) i r sp sp 0x7ffffff3f0 0x7ffffff3f0 (gdb) x/2gx 0x7ffffff3f0 0x7ffffff3f0: 0x0000000000000000 0x0000000000000000 (gdb) x/2gx 0x7ffffff400 0x7ffffff400: 0x0068732f6e69622f 0x0000000000000000 (gdb) i r x0 x0 0x7ffffff400 549755810816 (gdb) si 11 stp x0, x2, [sp, #0] // x0("/bin/sh" のアドレス) と x2(NULL) の値をスタックにプ ッシュ (sp は動かない) (gdb) i r x1 x1 0x7ffffff3f0 549755810800 (gdb) si 13 mov x8, #221 // execve システムコール番号を x8 にセット (221はexecveのシステムコール番号) (gdb) x/4gx 0x7ffffff3f0 0x7ffffff3f0: 0x0000007ffffff400 0x0000000000000000 0x7ffffff400: 0x0068732f6e69622f 0x0000000000000000 (gdb) i r x0 x1 x2 x0 0x7ffffff400 549755810816 x1 0x7ffffff3f0 549755810800 x2 0x0 0 (gdb) c Continuing. process 2550 is executing new program: /usr/bin/dash Error in re-setting breakpoint 1: Function "_start" not defined. [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/aarch64-linux-gnu/libthread_db.so.1". $ exit [Inferior 1 (process 2550) exited with code 0177] (gdb) q
うまくいってそうです!
今回はここまでです。
おわりに
今回は、参考サイトを見ながら、execve を使って、C言語とアセンブラで、シェルを起動するプログラムの実装とデバッグを行いました。
次回は、今回作ったアセンブラのプログラムのバイナリの確認と、バッファオーバーフローなどで任意のコードとして埋め込めるように加工していきたいと思います。
最後になりましたが、エンジニアグループのランキングに参加中です。
気楽にポチッとよろしくお願いいたします🙇
今回は以上です!
最後までお読みいただき、ありがとうございました。