土日の勉強ノート

AI、機械学習、最適化、Pythonなどについて、技術調査、技術書の理解した内容、ソフトウェア/ツール作成について書いていきます

C言語、アセンブラでシェルを起動するプログラムを作る(ARM64)

前回 は、K&Rのmalloc関数とfree関数を動かして理解しました。

今回は、Exploitコードの部品となりそうな C言語からシェルを起動するプログラムを実装してみたいと思います。

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

参考文献

はじめに

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

セキュリティの記事一覧
・第1回:Ghidraで始めるリバースエンジニアリング(環境構築編)
・第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言語からシェルを起動するプログラムを実装してみたいと思います。

inaz2.hatenablog.com

上の記事の ARM32bit版の記事もありました。

inaz2.hatenablog.com

また、64bitARM(Aarch64)のアセンブラについては、以下のサイトを参考にさせて頂きました。

www.mztn.org

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

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 でした。

221番はexecveで合ってる
221番はexecveで合ってる

SVC命令の詳細は、以下で詳しく解説がされています。

www.mztn.org

確かに、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言語とアセンブラで、シェルを起動するプログラムの実装とデバッグを行いました。

次回は、今回作ったアセンブラのプログラムのバイナリの確認と、バッファオーバーフローなどで任意のコードとして埋め込めるように加工していきたいと思います。

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

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

今回は以上です!

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