まえがき
先日SECCON CTF for Beginnersが開催された。
その中で、revジャンルの一問目としてアセンブリでfile.txt
を標準出力に読み出すというチャレンジがあった。
面白いチャレンジだったので、どうせならと思って簡易的なcat
コマンドを自作してみることした。
機能
とりあえず今回はオプション等は考えないこととする。
実装する機能は第一引数として与えたパスのファイルの中身を標準出力に書き出すというものにする。
本来ならcat
は引数を渡さなければ標準入力を読むがいったん考えないことにする。
アセンブリ言語について
一応基本的な話を解説しておくことにする
書いたアセンブリを読むための必要最低限の情報のみを記載する
そのため厳密さはあまり考えない。例えばシステムコールに渡す値というのは厳密には引数と呼ばないのでは..?といったこと等についてである
ニーモニック
アセンブリので用いられる命令のこと
以下例と雑な説明
-
mov
: 値をコピー -
call
: あるラベルへ実行位置を移動 -
cmp
: 2つの値を比較, 結果に応じてフラグを操作
これらの命令を並べてアセンブリを書いていく
構文
アセンブリの構文にはIntel記法とAT&T記法の2つがある
筆者はAT&T記法が苦手なので本記事ではIntel記法を採用する
基本的な構文は以下のようになっている
<Mnemonic> <Destination>, <Source>
Destinationが前Sourceが後であることに注意する
例えばrax
レジスタの値をrbx
レジスタにコピーしたい際は以下のように書く(レジスタについては後述)
mov rbx, rax
また、;
の後はコメントとなる
mov rbx, rax ; This is a comment
レジスタ
レジスタとは、簡単にはCPUの持つ、高速かつ小さなメモリのこと
CPUのアーキテクチャによって名前や構造は異なる
本記事ではx64アーキテクチャを採用していることに留意されたし
コードで使用しているレジスタには以下のようなものがある
rax
rbx
rdi
rsi
-
rdx
r...
といったレジスタは64bitのメモリを持っている
それに対しe...
といったレジスタはrから始まるレジスタの中の後ろ32bitの部分を指す。つまりrax
とeax
は同じレジスタを指す。ただし範囲は異なる。
アーキテクチャによって、ある程度度のレジスタを何の用途で使用すべきかということが決まっている
特に、関数やシステムコールを呼び出す際のレジスタの使い方は厳密に定まっている
本記事を読む上では以下の用途を理解しておけばよい
-
syscall
を呼び出したいときは呼び出す前にrax
レジスタにシステムコール番号を入れておく -
syscall
を呼び出すとき、第一引数はrdi
レジスタに入れておく -
syscall
を呼び出すとき、第二引数はrsi
レジスタに入れておく -
syscall
を呼び出すとき、第三引数はrdx
レジスタに入れておく -
syscall
の返り値はrax
レジスタに入る
システムコール番号
システムコールを呼び出すには、前述のとおりsyscall
命令を呼ぶ前にrax
にシステムコール番号を入れておく必要がある。システムコール番号を調べる際はこの辺のサイトが便利
もしくは/usr/include/asm/unistd_(32|64).h
に記載されているので参照するとよい
実装
section .data
err_msg db 'Error opening file', 0xA
section .bss
buffer resb 4096
section .text
global _start
_start:
mov rdi, [rsp + 16]
call _open
cmp rax, 0
js _open_error
mov rbx, rax ; Save file descriptor in rbx
jmp read_loop
_open:
mov eax, 2 ; sys_open
mov rsi, 0 ; O_RDONLY
syscall
ret
_read:
mov eax, 0 ; sys_read
mov rdi, rbx ; Use saved file descriptor from rbx
mov rsi, buffer
mov rdx, 4096
syscall
ret
_write:
mov eax, 1 ; sys_write
mov rdi, 1 ; stdout
mov rsi, buffer
syscall
ret
_close:
mov eax, 3 ; sys_close
mov rdi, rbx ;
syscall
jmp _exit
_exit:
mov eax, 60 ; sys_exit
xor rdi, rdi
syscall
read_loop:
call _read
test rax, rax
jz end_read
mov rdx, rax ; byte length
call _write
jmp read_loop
end_read:
call _close
jmp _exit
_open_error:
mov rsi, err_msg
mov rdi, 1
mov rdx, 19
mov eax, 1 ; sys_write
syscall
jmp _exit_error
_exit_error:
mov eax, 60 ; sys_exit
mov rdi, 1
syscall
dataセクション
open
に失敗した際に使用するエラーメッセージをハードコードしている
0xA
は\n
を表す
bssセクション
ファイルを読む際のバッファを用意している
とりあえず0x1000
である4096
にしておいた
textセクション
ここまでの説明にない要素について簡単な説明を添える
start
プログラムの開始位置となる
以下の命令でc言語でいうところのargv[1]
の先頭アドレスをrdi
に乗せている
mov rdi, [rsp + 16]
open
の後に返り値がrax
に入る。これが負の値だった場合ファイルディスクリプタの取得に失敗しているため、エラーを発生させる
cmp rax, 0
js _open_error
open
で得たファイルディスクリプタは今後の使用のためrbx
レジスタに保管しておく
mov rbx, rax ; Save file descriptor in rbx
open
open
システムコールを使用して、読みたいファイルのファイルディスクリプタを取得している。取得したファイルディスクリプタは返り値としてrax
レジスタに格納される
rdi
レジスタに入るべき第一引数はstart
内で用意しているため、ラベルの中では操作していない
read
mov rdi, rbx
保存しておいたファイルディスクリプタをrdi
レジスタに設定する
read_loop
read_loop
はファイルからデータを読み出して、バッファに書き込み、その内容を標準出力に出力するループである
test rax, rax
jz end_read
この部分でファイルを最後まで読んだかどうかを確認している
ファイルを最後まで読んでいればループを抜ける
コンパイル
以下のコマンドでコンパイルした
nasm -f elf64 -o cat.o cat.s
ld cat.o -o cat
実行すると実際にファイルの中身を読むことができた
実際のソースコードは以下から参照できる
github
小話
コマンドライン引数や環境変数はスタック上に載っていることはある程度実験的に確かめた。
以下のyoutube上の動画にもそんな感じの図が乗っているがソース情報を見つけることができなかった。
2024/06/20追記
http://6.s081.scripts.mit.edu/sp18/x86-64-architecture-guide.html
参考文献
https://www.mztn.org/lxasm/asm06.html
https://youtu.be/xtFs1yBVinc?si=u8bIFni7fMn7Gxf2