前置き
みなさんはシェルコードというものについて知っていますか?次のように定義されているようです。
シェルコード(英: Shellcode)とは、コンピュータセキュリティにおいて、ソフトウェアのセキュリティホールを利用するペイロードとして使われるコード断片である。
侵入したマシンを攻撃者が制御できるようにするため、シェルを起動することが多いことから「シェルコード」と呼ぶ。
昔はシェルを起動するコードが多かったためにこのような定義になっていますが、現代においてはほとんどのコード断片をシェルコードと呼んで大丈夫なようです。
なのでシェルだけではなく、現代では悪意を持った処理や他の悪意のあるプログラムを呼び出すときによく使用されてしまうようになりました。
というのも、シェルコードのいい点として、実行権限があるメモリ領域を確保さえできれば、そこにシェルコードを書き込むだけで実行できるスタンドアローンのようなプログラムとして成り立つ点です。
今回は、Windowsで動く簡単なシェルコードをRustで作ってみようと思います!
誰もやってなさそうだし!...と思ったら先駆者がいました -> b1tg/rust-windows-shellcode
実はこちらについては既に手元で試しましたが、後半で説明する部分でうまくできてない部分があったので、この記事はこちらの改良版を改めて説明する、といった感じのものととらえてよきです!
間違ってるとことかあったら教えてください >_<!
実装
前提として、Windowsの実行ファイル周りについてそこそこ知ってる事とします。
※ 当初はコードをまるまる載せる予定でしたが、使い方によっては悪用出来てしまうので、一部を疑似コードまたはコメントに置き換えるようにしました。
用意するもの:
- Rust nightly版
- Windows10 x64 (今回は64bitのみをターゲットにします)
では早速実装していきます。
今回は実際に悪意のあるアクションを実行するわけではなく、メッセージボックスを出すだけにしておきます。
実装する前に、注意しておくことがあります。
最終的に出来たものはシェルコードとして動くようになりますが、それはメモリに直接バイナリとして書き込まれるので、Windowsのローダーに依存しないように実装しなければならない、ということです。
どういうことかというと、Windows上で例えば新しくメモリを確保しようと思った時、それは内部的にWindowsのAPI関数を呼び出しますが、Windowsのローダーを介さないで実行されるためにAPI関数の依存のような物が解決されていません。
私たちが普段 .exe
をダブルクリック等で実行した時、Windowsはこういった複雑なロード処理を事前に行っているので、普通は気にすることはないのですが、今回はシェルコードとして実行したいため、ローダーに依存しないように注意して実装していきます。
とは言ってもそこまで難しくは無くて、基本的に no_std
のような感じでカキカキすればよいです。
まずはおまじないから!
#![no_std]
#![no_main]
#![feature(asm)]
#[no_mangle]
pub unsafe extern "C" fn main() {
...
}
-
#![no_std]
... シェルコードはそれ単体で動かせるためにno_std
な環境に近いため、定義 -
#![no_main]
...main
関数は必要ないため、定義 -
#![feature(asm)]
... 後ほどWindowsマジック(笑)を使用するため、asm
のfeature
をONにします -
#[no_mangle]
... 関数のマングリングをOFF!
unsafe
が必要な理由は何となくわかるかと思いますが、extern "C"
が必須な理由はちょっと調査不足でわかりませんでした...精進精進
後no_main
をつけていますが、メインとなる関数の名前は main
にしておきます。わかりやすいので!
ちなみにもう少し後で説明するコンパイルフラグを調整するとmain
じゃなくても大丈夫です。
おまじないはひとまずこれで良いと思います。
次に、使用するWindowsの様々な型についてバインドが欲しいところです。
winapi-rs
のようなものを使用せず、自分で定義していきましょう。
適当に binding.rs
とでもしてバインドを適当に書いて生きます。
型が多すぎるので全てバインドする必要はなく、自分が使用したいものだけでいいですね。
// こんな感じでてきとーにいっぱい書いていく
pub type uint64 = u64;
...
バインドではあまり説明することが無いので、全部書き終わった前提で進みます。
main
を埋める前に、先に実装しておきたい関数を作ります。
先に、文字列を比較するだけの簡単な関数を作っておきましょう。
Windowsは本当にめんどくさくて、*const u8
、*const i8
、*const u16
という感じで複数の文字列ポインタが混同しているので、これをいい感じに比較できる関数を作っておきたいのです。
これを一つの関数で実装するために、num_traits
の力を借りて実現します!
use num_traits::Num;
pub fn compare_raw_str<T>(s: *const T, u: *const T) -> bool
where
T: Num,
{
unsafe {
// 文字列の最後がNULL文字になっているので、is_zeroでチェックすることで長さを取得できる
let u_len = (0..).take_while(|&i| !(*u.offset(i)).is_zero()).count();
// メモリから指定された長さでスライスを取得できるやつ。めっちゃ便利~!
let u_slice = core::slice::from_raw_parts(u, u_len);
// 上に同じ
let s_len = (0..).take_while(|&i| !(*s.offset(i)).is_zero()).count();
let s_slice = core::slice::from_raw_parts(s, s_len);
// 長さが違えばそもそも違う文字列なので、チェックせずにfalseを返す!
if s_len != u_len {
return false;
}
// 一文字ずつチェック!
for i in 0..s_len {
if s_slice[i] != u_slice[i] {
return false;
}
}
return true;
}
}
これでu8
だろうがu16
だろうが文字列ポインタを投げてやるだけで比較できるようになりました!
この関数は他のWindows APIプログラミングで割と便利すぎて感動、涙がぽろぽろ
文字列比較関数を作ったところで次に進みます。
次に、main
の処理の中でWindowsのAPIを使用したいのですけど、ローダーを介さないのでそれらを普通に使用出来ません。
そこで、Windowsのローダーが行っていることを自分で実装することでそれを可能にします。
必要なのは2つ!
- メモリからシステムDLLへのポインタを取得
- システムDLLから使いたいWindows API関数を取り出す
これが出来れば自由にWindowsのAPI関数を呼び出して使用することが出来ます。
まずは メモリからシステムDLLへのポインタを取得
から!
(ここからWindowsの実行ファイル周りの用語とか出てきますが、知ってる前提で進みます!雰囲気で伝われ!)
システムDLLはWindows起動時に特定の場所に配置されているのですが、実はそこへの仮想アドレスは全てのプロセスで全く一緒です。
じゃあそれをどのように取り出すかというと、PEBと呼ばれるプロセスに関する様々な情報が入った構造体の特定のフィールドから仮想アドレスを割り出すことが出来ます。
PEB
の特定のフィールドにロードされたモジュール、つまりDLLのリストの情報が入っているので、そこから割り出していく流れになります。
じゃあそのPEB
をどうやって取得するの?となりますが、安心してください、ここでようやく**速攻魔法 Windowsマジック(笑)**を使います。
PEB
の特性として、とあるセグメントレジスタの特定の場所に配置されているというものがあります。
32bitではfs
の0x30
、64bitではgs
の0x60
にPEB
が隠れています。
これをアセンブリコードで取り出し、ロードされているDLLリストを取り出して、欲しいシステムDLLを特定してそのアドレスを返す関数を作ります。
その他細かい部分はコード中のコメントに残しました。
unsafe fn get_module_by_name(module_name: *const u16) -> PVOID {
// PEB構造体のポインタ変数
let mut ppeb = NULL as *mut PEB;
// 記事では64bit前提にしてるので、今回はgs
asm!(
"mov {}, gs:[0x60]",
out(reg) ppeb,
);
// LdrにLDR_DATA_TABLE_ENTRY構造体が入っているので、そこをたどる
// ref: https://docs.microsoft.com/en-us/windows/win32/api/winternl/ns-winternl-peb_ldr_data
let p_peb_ldr_data = (*ppeb).Ldr;
// InLoadOrder, InMemoryOrder, InInitializationOrderの3種類あるのですが、
// うちの環境ではInLoadOrderじゃないとNULLになってしまったのでこっちを使う
let mut module_list = (*p_peb_ldr_data).InLoadOrderModuleList.Flink as *mut LDR_DATA_TABLE_ENTRY;
// DllBaseがNULLになったら終わりの合図なので、それまでループ
while (*module_list).DllBase != NULL {
// このバッファフィールドにDLLの名前が入ってる
// もちろんNULL終端なので、自作した比較関数で比較できます!
let dll_name = (*module_list).BaseDllName.Buffer;
if compare_raw_str(module_name, dll_name) {
return (*module_list).DllBase;
}
module_list = (*module_list).InLoadOrderLinks.Flink as *mut LDR_DATA_TABLE_ENTRY;
}
NULL
}
これで、この関数にUTF-16な文字列(最悪)を突っ込むことでシステムDLLへのポインタが取得できるようになりました。
UTF-16を手で扱うのはだるいので、utf16_literal
crateを使わせてもらいましょう!
use utf16_literal::utf16;
// ちなみにこの名前のシステムDLLは存在しません
let unko32 = get_module_by_name(utf16!("unko32.dll\x00").as_ptr());
次に、取ってきたシステムDLLから、DLLがエクスポートしている関数を取得する関数を作ろうと思うのですが...ここがなかなか厄介な部分です。
取ってきたシステムDLLのバッファを自分でパースし、複雑なポインタ演算をしなければならないのです。
流れとしては、
1: DLLのポインタを逆参照し、PE構造体としてパース
2: エクスポート情報が入った構造体フィールドの仮想アドレスをオプショナルヘッダーから取得
3: 2で取ってきた仮想アドレスへアクセス、エクスポート関数情報の構造体としてキャスト
4: 3の構造体で、関数群の数、関数群の最初の関数のポインタ、関数名へのポインタ、序数を取得
5: 4で取ってきた4つの値を使っていい感じにポインタ演算し、関数と関数名へのアドレスを取得
6: 目的の関数名とマッチしたら関数へのアドレスをreturn、関数名が違ったらポインタをずらし、4へ戻る
という感じの流れです。
特に4番目の序数が入ってくる演算で細かなミスをしがちなんですよね...
念のために悪用出来ないように計算部分は伏せますが、まぁググったら載せてくれてる人いるので、どう計算してるのかは自己責任でそちらへ!
unsafe fn get_func_by_name(module: PVOID, func_name: *const u8) -> PVOID {
// DLLへのポインタをキャストし、NT_HEADERS構造体とする
let nt_header = (module as u64 + (*(module as *mut IMAGE_DOS_HEADER)).e_lfanew as u64)
as *mut IMAGE_NT_HEADERS64;
// エクスポート関数の情報が入った構造体への仮想アドレス
let export_dir_rva = (*nt_header).OptionalHeader.DataDirectory[0].VirtualAddress as u64;
if export_dir_rva == 0x0 {
return NULL;
};
// キャスト!
let export_dir = (module as u64 + export_dir_rva) as *mut IMAGE_EXPORT_DIRECTORY;
let number_of_names = (*export_dir).NumberOfNames;
let addr_of_funcs = (*export_dir).AddressOfFunctions;
let addr_of_names = (*export_dir).AddressOfNames;
let addr_of_ords = (*export_dir).AddressOfNameOrdinals;
for i in 0..number_of_names {
// ここでポインタ演算と関数名マッチの処理
}
return NULL;
}
ここまで実装出来たら、ようやくシェルコードのmain
関数を埋めることが出来ます!やったねたえちゃん!
早速メッセージボックスの関数を呼びたいですが、なんとこいつが格納されているuser32.dll
は標準で読み込まれていないようです。
なので、LoadLibrary
関数を使って手動で読み込んでやる必要が出てきます。
LoadLibrary
関数はkernel32.dll
に存在するので、これを自作した関数で取ってきた後、更にGetProcAddress
関数も取ってきて、
LoadLibray
関数の戻り値からMessageBox
を取得します。
pub type PLoadLibraryA = unsafe extern "system" fn(LPCSTR) -> HMODULE;
pub type PGetProcAddress = unsafe extern "system" fn(HMODULE, LPCSTR) -> LPVOID;
pub type PMessageBoxW = unsafe extern "system" fn(h: PVOID, text: LPCWSTR, cation: LPCWSTR, t: u32) -> u32;
let kernel32 = get_module_by_name(utf16!("KERNEL32.DLL\x00").as_ptr());
// transmuteでポインタから関数に出来る!すごーい
let LoadLibraryA: PLoadLibraryA = transmute(get_func_by_name(kernel32, "LoadLibraryA\x00".as_ptr() as _));
let GetProcAddress: PGetProcAddress = transmute(get_func_by_name(
kernel32,
"GetProcAddress\x00".as_ptr() as _,
));
let u32_dll = LoadLibraryA("user32.dll\x00".as_ptr() as _);
let MessageBoxW: PMessageBoxW = transmute(GetProcAddress(u32_dll, "MessageBoxW\x00".as_ptr() as _));
これでMessageBoxW
変数はMessageBoxW
関数となりましたので、さっそく呼び出しコードを書いてあげます。
MessageBoxW
関数のドキュメントを参考にしつつ、引数を埋めてあげます。
MessageBoxA
というのもありますが、こっちは日本語とか突っ込まない時とかに!今回は日本語突っ込むのでW
の方を使います。
MessageBoxW(
NULL,
utf16!("うんちぶりぶり\0").as_ptr(),
utf16!("タイトルテキスト\0").as_ptr(),
0x00,
);
ちなみにここまでのコードを一応そのままコンパイルして実行できるはずです。
うんちぶりぶりが表示されたら成功!
...ではなく、本題のシェルコード作成に入りましょう!
上でコンパイルして出てきた実行可能ファイルから必要な部分を切り出してパッチを当ててバイナリファイルを作成、という流れなのですが、
まず上のコンパイルで特殊なコンパイルフラグが必要なので、先にそれを設定しちゃいます。
.cargo
ディレクトリをsrc
と同じ階層に作り、以下をconfig.toml
として保存します。
[build]
target = "x86_64-pc-windows-msvc"
rustflags = [
"-Z", "pre-link-arg=/NOLOGO",
"-Z", "pre-link-arg=/NODEFAULTLIB",
"-C", "link-arg=/ENTRY:main",
"-C", "link-arg=/MERGE:.edata=.rdata",
"-C", "link-arg=/MERGE:.rustc=.data",
"-C", "link-arg=/MERGE:.rdata=.text",
"-C", "link-arg=/MERGE:.pdata=.text",
"-C", "link-arg=/DEBUG:NONE",
"-C", "link-arg=/EMITPOGOPHASEINFO",
"-C", "target-feature=-mmx,-sse,+soft-float",
"--emit", "asm",
]
コンパイルフラグについてはあまり詳しいわけではありませんが、不必要なデバッグ情報を抜いたり、セクションと呼ばれるものをマージしてなるべくまとまるように?してるようです。
これが無いと動かないので、必要なんです!!!(強行突破)
では、実行可能ファイルにパッチを当てるコードを別で作成していきましょう。
どうしてパッチを当てるのかというと、スレッドの起点として、等の状況でシェルコードを走らせたとき、そのままいくと正常にスレッドが終了しないという問題があります。
実行可能ファイルから自分が実装した処理部分だけを切り出してるので、その処理の前後にはWindowsのABIルールに乗っ取ったアセンブリが必要になります。(合ってるよね...?)
てことは、WindowsのABIドキュメントを見れば全て解決し...出来ませんでしたので、先駆者の知恵を借りて、アセンブリを少し弄って流用したいと思います。
先駆者のアセンブリとこちらを読みながら見ると、わかりやすいかもしれんです: x64 での呼び出し規則
; 次の命令を呼び出す、ここではpop rcx
call 0x5
; 現在のメモリ位置をキャプチャします
pop rcx
; 現在のrsiを保存
push rsi
; 後で使うので、rspを押し込む
mov rsi,rsp
; スタックを16bytes境界に揃える
and rsp,0xfffffffffffffff0
; 引数が無いので、呼び出し規約にのっとって4つの引数、
; つまり0x20分だけ確保
sub rsp,0x20
; 呼び出しを転送
; 下のretという部分から下のアセンブリに飛ぶ
call 0x5
; スタックポインターを元に戻す
mov rsp,rsi
; スタックを元に戻す
pop rsi
; 呼び出し元に戻る
ret
こちらがブートストラップとなり、最後のret部分の後に実行可能ファイルから切り出した処理部分なりをくっつけていきます。
必要なのは上のbootstrapコード、.text
のバイナリ、jmp
パッチ、の3つです。
jmp
パッチをしないと、うまく処理が走りませんでした。
let src_path = "shellcode.exe";
let mut buffer = get_binary_from_file...(src_path)?;
let pe = PE::parse(&mut buffer)?;
let standard_fileds = pe.header.optional_header.unwrap().standard_fields;
let entry_offset = standard_fileds.address_of_entry_point - standard_fileds.base_of_code;
for section in pe.sections {
let name = String::from_utf8(section.name.to_vec())?;
if !name.starts_with(".text") {
continue;
}
let start = section.pointer_to_raw_data as usize;
let size = section.size_of_raw_data as usize;
let dst_path = ".\\shellcode.bin";
let shellcode = File::create(dst_path)?;
let mut bootstrap: Vec<u8> = Vec::new();
bootstrap.extend_from_slice(b"\xe8\x00\x00\x00\x00");
bootstrap.push(b'\x59');
bootstrap.push(b'\x56');
bootstrap.extend_from_slice(b"\x48\x89\xe6");
bootstrap.extend_from_slice(b"\x48\x83\xe4\xf0");
bootstrap.extend_from_slice(b"\x48\x83\xec\x20");
bootstrap.push(b'\xe8');
bootstrap.push(5 as u8);
bootstrap.extend_from_slice(b"\x00\x00\x00");
bootstrap.extend_from_slice(b"\x48\x89\xf4");
bootstrap.push(b'\x5e');
bootstrap.push(b'\xc3');
let mut buf_writer = BufWriter::new(shellcode);
// write bootstrap first
for b in bootstrap {
buf_writer.write(&[b])?;
}
// write jmp to entry code
buf_writer.write(&[0xe9])?;
for byte in &(entry_offset as u32).to_le_bytes() {
buf_writer.write(&[*byte])?;
}
// write .text section
for i in start..start + size {
buf_writer.write(&[buffer[i]])?;
}
buf_writer.flush()?;
}
これにメッセージボックスを出す実行可能ファイルへのパスを食わせると、カレントディレクトリにパッチを当て終わったバイナリファイルが出てきます。
そのバイナリファイルをスレッドの起点なりに指定してからスレッド再開、うんちぶりぶり
メッセージが出てスレッドが正常終了すればどっきり大成功です!
まとめ
今回はRustを用いてWindows上で動くシェルコードを作ってみました。
後半はかなり駆け足説明になってしまって、おそらくセキュリティに興味のない大半の人は何言ってるか伝わってないかもしれませんが、「何言ってるか分からん」と、はなくそほじっておけば大丈夫です!
もしかしたら私文章書くの向いてないかもしれへん...へこむ...
私は独自にマルウェア技術的な物を調査するのが大好きで、その過程で得た物をRustを使用してアウトプット出来て良かったと思います。
また、今回は全てのコードを完全に載せたわけではありませんが、こういった技術は簡単に悪用することが出来てしまうので、そういう方向で使用せず、あくまで技術的な興味、身を守るために攻撃方法を知るといった目的で読んでもらえると嬉しいです...
最後に
なんかあんまRust関係なくね?
さんこーぶんけん
b1tg/rust-windows-shellcode
mattifestation/PIC_Bindshell
monoxgas/sRDI
x64 での呼び出し規則