X

Oracle Solaris, Oarcle ハードウェア製品に関する情報

  • Sun
    May 25, 2010

Solaris でハンドアセンブル

Guest Author


はじめに

今回は Solaris でハンドアセンブルを行う方法をご紹介します。ハンドアセンブルは、コンピュータの内部で何が行われているのかを勉強するにはとても良い題材ですし、機械語を自在に操るのも面白い体験です。機械語と聞くと難解で面倒だという印象を持たれているかもしれませんが、今回は難しい理屈は抜きで、実際にハンドアセンブルで実行ファイルを生成する例を見て頂きたいと思います。


ndisasm

zsh や bash に built-in されている echo コマンドと、NASM の配布物に含まれている ndisasm コマンドを使用すると、気軽にハンドアセンブルの気分を味わう事が出来ます。

zsh の echo コマンドには '\\xNN' 形式で指定した 16 進数をバイナリに変換して出力する機能があります。この機能を利用して 16 進数の機械語を入力すると、実際の実行ファイルに含まれている機械語命令と全く同じバイナリ列を生成する事が出来ます。これが最も単純なハンドアセンブルです。

ndisasm はディスアセンブラです。ディスアセンブラは、バイナリの機械語命令を読み込んで、人間に理解し易いニーモニックに変換してくれます。ディスアセンブラを使用する事で、ハンドアセンブルが正しく出来ているかを確認する事が出来ます。

ndisasm の他にもディスアセンブラは沢山ありますが、ndisasm はデータを標準入力から読み込む機能を持っており、読み込んだデータを何でも x86 の機械語命令と仮定して解釈してくれるので、こういった用途には最適です。

試しにハンドアセンブルで nop 命令(何もしない命令)を発行してみましょう。nop 命令を Intel 社のサイトにある "Instruction Set Reference" で引くと、機械語の命令 (Opcode) は 90 番が割り当てられている事が分かります。そこで echo の引数に '\\x90' を 3 つ指定し、ndisasm コマンドに渡してみます。実行結果を見ると、以下の通り、確かに nop が 3 つ続けて出力されました。

 # zsh
# echo -ne '\\x90\\x90\\x90' | ndisasm -u - <= '\\x90' x3 を ndisasm に渡す
00000000 90 nop <= 正しく nop と認識されました
00000001 90 nop
00000002 90 nop

この様に、実行したい命令があったら、まずマニュアルから Opcode を引く等して機械語の命令を生成します。命令が複数ある場合は単純に続けて記述して下さい。機械語の命令列が完成したら、それを echo コマンドを使ってハンドアセンブルし、ndisasm コマンドでディスアセンブルする事で、正しくハンドアセンブル出来ているかを確認します。


Hello World!!

次に定番の Hello World!! をハンドアセンブルで書いてみます。まずは以下の実行例をご覧下さい。

 # echo '68210A0000 68726C6421 686F20576F 6848656C6C 8BD4 6A0E FFF2 6A01 6A00
B804000000 CD91 6A00 6A00 B801000000 CD91' | ./hex| ndisasm -u -
00000000 68210A0000 push dword 0xa21 // "!\\n\\0\\0"
00000005 68726C6421 push dword 0x21646c72 // "rld!"
0000000A 686F20576F push dword 0x6f57206f // "o Wo"
0000000F 6848656C6C push dword 0x6c6c6548 // "Hell"
00000014 8BD4 mov edx,esp // save the string address
00000016 6A0E push byte +0xe // arg3 : 14 characters
00000018 FFF2 push edx // arg2 : string address
0000001A 6A01 push byte +0x1 // arg1 : stdout
0000001C 6A00 push byte +0x0 // return address
0000001E B804000000 mov eax,0x4 // 0x4 : write syscall
00000023 CD91 int 0x91 // syscall interuppt
00000025 6A00 push byte +0x0 // arg1 : 0
00000027 6A00 push byte +0x0 // return address
00000029 B801000000 mov eax,0x1 // 0x1 : exit syscall
0000002E CD91 int 0x91 // syscall interuppt

今度は echo コマンドに渡す文字列が先ほどより多少長くなりました。機械語の命令を 8bit 毎に '\\x' を付けてエスケープするのが多少大変になって来たので、ここでは 16 進数をバイナリに変換する hex コマンドを作成して使用しています(hex コマンドのソースコードはこの記事の一番最後に載せてあります)。'\\x' を付けない事以外は先ほどの nop の例と基本的に全く同じ手順です。なお、ndisasm の出力のコメント部分は解説の為に後から付け加えた物です。

それでは、上から順番に機械語の命令を追って行きたいと思います。冒頭の '68210A0000 68726C6421 686F20576F 6848656C6C' の部分は出力する文字列 ("Hello World!!" + \\n) をスタックに積む命令です。16 進数の 68 は push 命令で、直後にある 32bit 分のデータをスタックに積むという動作を行います。ここでは、'68210A0000' は 68 が push 命令で、続く 210A0000 がデータという事になります。スタックなので文字列の後ろ側から先に積んでおり、最後に "Hell" という文字列がスタックに積まれます。

次の '8BD4' は、今スタックに積んだ文字列のアドレスを一時退避しています。退避したアドレスは後で使用します。8B は mov 命令の一部で、8BD4 でスタックポインタを EDX レジスタに保存するという動きになります。

'6A0E' は write システムコールの第 3 引数(出力する文字数)をスタックに積んでいます。6A も push 命令です。68 の場合と違い、後続の 8 bit をスタックに積む働きをします。ここでは 0E (10 進数で 14) がスタックに積まれます。積まれたデータは 32bit 幅になります。

続いて、'FFF2' で write システムコールの第 2 引数(出力する文字列のアドレス)をスタックに積んでいます。これが先ほど退避したアドレスです。FF も push 命令の一部で、FFF2 は EDX レジスタの中身をスタックに積む命令です。

その次の '6A01' は write システムコールの第 1 引数(出力先)を標準出力に設定しています。

続く '6A00' はダミーです。

'B804000000' は write システムコールを発行する準備をしています。B8 は mov 命令で、後続の 32bit 分のデータを EAX レジスタに格納します。ここで EAX に格納している数値は 4 ですが、これは write システムコールのシステムコール番号です。システムコール番号は /usr/include/sys/syscall.h にあります。

最後に 'CD91' で int 0x91 を実行して write システムコールを呼び出しています。x86 Solaris のシステムコール呼び出しは syscall 命令 (AMD) と sysenter 命令 (Intel) を /usr/lib/libc/libc_hwcapN.so.1 の lofs マウント で切り替える様に実装されていますが、この例の様に int 0x91 を使って行う事も可能です。int 0x91 を使用すると AMD/Intel 両方の CPU で動作するコードになります。

その後は、同じ様に exit システムコールを呼び出してプログラムを終了させています。

以上、たった 15 個の命令だけで Hello World!! という文字列を出力する事が出来ました。ここで使用した命令は全て先ほどの Intel 社のマニュアルに記載されていますので、全てを憶える必要はありません。


実行可能な形式にする

ここまでご紹介した手順だけではまだ実行可能なプログラムは作成出来ません。もう一工夫加えて、実際に Solaris 上で実行出来るプログラムを作成してみましょう。コマンドとして実行可能なプログラムにするには機械語の列を ELF 形式のファイルにまとめる必要があります。文末に掲載した has.c をコンパイルして、以下の様に実行してみて下さい。

 # echo '68210A0000 68726C6421 686F20576F 6848656C6C 8BD4 6A0E FFF2 6A01 6A00
B804000000 CD91 6A00 6A00 B801000000 CD91' | ./has > hello
# chmod +x hello
# ./hello
Hello World!!

先ほどと同じ機械語の命令列から、Hello World!! と出力するプログラムが実際に作成出来た事が分かります。has コマンドは 16 進数の機械語をバイナリに変換して、実行形式として出力するプログラムです。先ほどの hex コマンドとの違いは、機械語の命令列に ELF ファイルに必要な ELF ヘッダとプログラムヘッダを付加している点だけです。Hello World!! を出力するだけの単純なプログラムであれば、この様に簡単な仕組みだけで作成する事が可能になっています。


日本語への対応

ハンドアセンブルでももちろん日本語を表示する事が可能です。ついでにある程度のランダム性も追加して、おみくじコマンドを作成してみました。

 # echo '0F31 31D2 BB05000000 F7F3 83FA00 7511 6890890A00 68E5A4A7E5 89E3 6A08
53 EB47 83FA01 750C 68E590890A 89E3 6A04 53 EB36 83FA02 7511 6890890A00
68E4B8ADE5 89E3 6A08 53 EB20 83FA03 7511 6890890A00 68E5B08FE5 89E3 6A08
53 EB0A 68E587B60A 89E3 6A04 53 6A01 6A00 B804000000 CD91 6A00 6A00
B801000000 CD91' | ./has > omikuji
# chmod +x omikuji
# while :; do echo -n "本日の運勢は: "; ./omikuji; sleep 1; done
本日の運勢は: 中吉
本日の運勢は: 大吉
本日の運勢は: 凶
本日の運勢は: 大吉
本日の運勢は: 大吉

先ほどと比べるとプログラムがだいぶ長くなり、使用している命令の数も増えています。中身の詳しい説明は省きますが、Hello World!! の時と大きく異なるのは、現在時刻に応じて処理の内容を変えている事です。現在時刻は rdtsc 命令で取得しています。それを div 命令で割り算し、余りの数値を cmp 命令で条件と比較し、jnz 命令で実行したいコードに飛んでいます。コメントを振っておきましたので、マニュアルと付き合わせて機械語の成り立ちを調べてみて下さい。

 # echo '0F31 31D2 BB05000000 F7F3 83FA00 7511 6890890A00 68E5A4A7E5 89E3 6A08
53 EB47 83FA01 750C 68E590890A 89E3 6A04 53 EB36 83FA02 7511 6890890A00
68E4B8ADE5 89E3 6A08 53 EB20 83FA03 7511 6890890A00 68E5B08FE5 89E3 6A08
53 EB0A 68E587B60A 89E3 6A04 53 6A01 6A00 B804000000 CD91 6A00 6A00
B801000000 CD91' | ./hex | ndisasm -u -
00000000 0F31 rdtsc // 時刻を計測
00000002 31D2 xor edx,edx
00000004 BB05000000 mov ebx,0x5 // 割る数 : 5
00000009 F7F3 div ebx // 時間を 5 で割る
0000000B 83FA00 cmp edx,byte +0x0 // 割った余りが 0 か?
0000000E 7511 jnz 0x21 // 違うなら 0x21 へジャンプ
00000010 6890890A00 push dword 0xa8990 // "大吉" 続き
00000015 68E5A4A7E5 push dword 0xe5a7a4e5 // "大吉" 前半
0000001A 89E3 mov ebx,esp // 文字列のアドレスを保存
0000001C 6A08 push byte +0x8 // arg2 : 文字数 : 8
0000001E 53 push ebx // arg1 : 文字列のアドレス
0000001F EB47 jmp short 0x68 // 0x68 へジャンプ
00000021 83FA01 cmp edx,byte +0x1 // 割った余りが 1 か?
00000024 750C jnz 0x32 // 違うなら 0x32 へジャンプ
00000026 68E590890A push dword 0xa8990e5 // "吉"
0000002B 89E3 mov ebx,esp // 文字列のアドレスを保存
0000002D 6A04 push byte +0x4 // arg2 : 文字数 : 4
0000002F 53 push ebx // arg1 : 文字列のアドレス
00000030 EB36 jmp short 0x68 // 0x68 へジャンプ
00000032 83FA02 cmp edx,byte +0x2 // 以下同...
00000035 7511 jnz 0x48
00000037 6890890A00 push dword 0xa8990
0000003C 68E4B8ADE5 push dword 0xe5adb8e4
00000041 89E3 mov ebx,esp
00000043 6A08 push byte +0x8
00000045 53 push ebx
00000046 EB20 jmp short 0x68
00000048 83FA03 cmp edx,byte +0x3
0000004B 7511 jnz 0x5e
0000004D 6890890A00 push dword 0xa8990
00000052 68E5B08FE5 push dword 0xe58fb0e5
00000057 89E3 mov ebx,esp
00000059 6A08 push byte +0x8
0000005B 53 push ebx
0000005C EB0A jmp short 0x68
0000005E 68E587B60A push dword 0xab687e5
00000063 89E3 mov ebx,esp
00000065 6A04 push byte +0x4
00000067 53 push ebx
00000068 6A01 push byte +0x1 // arg1 : 出力先 : stdout
0000006A 6A00 push byte +0x0 // return address
0000006C B804000000 mov eax,0x4 // 0x4 : write syscall
00000071 CD91 int 0x91 // syscall interrupt
00000073 6A00 push byte +0x0 // arg1 : 0
00000075 6A00 push byte +0x0 // return address
00000077 B801000000 mov eax,0x1 // 0x1 : exit syscall
0000007C CD91 int 0x91 // syscall interrupt

なお、日本語の文字列に関しても特殊な事はしていません。ただ単に文字列を 16 進数に直して使用しているだけです。日本語の文字列の 16 進数表現は od コマンドで調べる事が出来ます。

 # echo "吉" | od -x
0000000 90e5 0a89
0000004


おわりに

以上、実行形式のファイルをハンドアセンブルで作成する方法をご紹介しました。ハンドアセンブルは、知っている人はとてつもなく深く知っており、コンピュータを理解する上でもとても役に立ちますが、知らない人は全く知らない、どこから手を付けていいかもよく分からない様な話だと思います。今回は、自分が新入社員だった頃にこんな記事があったらなあと思い、ご紹介させて頂きました。皆さんのご参考になれば幸いです。


使用したプログラム


ndisasm のインストール

 # wget http://www.nasm.us/pub/nasm/releasebuilds/2.08.01/nasm-2.08.01.tar.bz2
# gtar jxf nasm-2.08.01.tar.bz2
# cd nasm-2.08.01
# ./configure
# gmake && gmake install


Hand Assemble Assistant by C

 /\*
\* Usage:
\* # gcc has.c -o has
\* # echo -n 6A00 B801000000 0F05 | ./has > foo
\* # chmod +x foo
\* # ./foo
\*/
#include <stdio.h>
#include <sys/elf.h>
#define PROGRAM_SIZE 2048
int main() {
int size = 0;
unsigned char prog[PROGRAM_SIZE] = {0};
unsigned int ehdr_size = sizeof(Elf32_Ehdr);
unsigned int phdr_size = sizeof(Elf32_Phdr);
Elf32_Ehdr ehdr = {
{ELFMAG0, ELFMAG1, ELFMAG2, ELFMAG3, ELFCLASS32, ELFDATA2LSB, EV_CURRENT},
ET_EXEC,
EM_386,
EV_CURRENT,
0x8050000 + ehdr_size + phdr_size, // e_entry
ehdr_size, // e_phoff
0, // e_shoff
0, // e_flags
ehdr_size, // e_ehsize
phdr_size, // e_phentsize
1, // e_phnum
0, // e_shentsize
0, // e_shnum
SHN_UNDEF // e_shstrndx
}; // 52bytes
Elf32_Phdr phdr = {
PT_LOAD, // p_type
0, // p_offset
0x8050000, // p_vaddr
NULL, // p_paddr
0, // p_filesz
0, // p_memsz
PF_R + PF_W + PF_X, // p_flags
0x10000 // p_align
}; // 32bytes
int c, nibble = 0;
while(1) {
if(size >= PROGRAM_SIZE) {
perror("input too large.");
break;
}
c = fgetc(stdin);
if(c == EOF) {
break;
} else if(('0' <= c) && (c <= '9')) {
if(nibble == 0) {
prog[size] = (c - 48) << 4;
nibble = 1;
} else {
prog[size] += c - 48;
size++;
nibble = 0;
}
} else if(('A' <= c) && (c <= 'F')) {
if(nibble == 0) {
prog[size] = (c - 55) << 4;
nibble = 1;
} else {
prog[size] += c - 55;
size++;
nibble = 0;
}
} else if(('a' <= c) && (c <= 'f')) {
if(nibble == 0) {
prog[size] = (c - 87) << 4;
nibble = 1;
} else {
prog[size] += c - 87;
size++;
nibble = 0;
}
}
}
phdr.p_filesz = phdr.p_memsz = size + ehdr_size + phdr_size;
write(1, &ehdr, ehdr_size);
write(1, &phdr, phdr_size);
write(1, prog, size);
exit(0);
}


Hex to Binary by C

 /\*
\* Usage:
\* # gcc hex.c -o hex
\* # echo -n 48656c6c6f | ./hex > foo
\* # od -c foo
\*/
#include <stdio.h>
int main() {
int c, out, hasdata = 0;
while(1) {
c = fgetc(stdin);
if(c == EOF) {
if(hasdata == 1) {
fputc(out, stdout);
}
break;
} else if(('0' <= c) && (c <= '9')) {
if(hasdata == 0) {
out = (c - 48) << 4;
hasdata = 1;
} else {
out += c - 48;
fputc(out, stdout);
out = 0;
hasdata = 0;
}
} else if(('A' <= c) && (c <= 'F')) {
if(hasdata == 0) {
out = (c - 55) << 4;
hasdata = 1;
} else {
out += c - 55;
fputc(out, stdout);
out = 0;
hasdata = 0;
}
} else if(('a' <= c) && (c <= 'f')) {
if(hasdata == 0) {
out = (c - 87) << 4;
hasdata = 1;
} else {
out += c - 87;
fputc(out, stdout);
out = 0;
hasdata = 0;
}
}
}
}

Be the first to comment

Comments ( 0 )
Please enter your name.Please provide a valid email address.Please enter a comment.CAPTCHA challenge response provided was incorrect. Please try again.