LoginSignup
2
2

簡易catコマンドをアセンブリで自作する

Last updated at Posted at 2024-06-18

まえがき

先日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の部分を指す。つまりraxeaxは同じレジスタを指す。ただし範囲は異なる。

アーキテクチャによって、ある程度度のレジスタを何の用途で使用すべきかということが決まっている
特に、関数やシステムコールを呼び出す際のレジスタの使い方は厳密に定まっている
本記事を読む上では以下の用途を理解しておけばよい

  • 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

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2