CUBE SUGAR CONTAINER

技術系のこと書きます。

macOS (x86/x86-64) のシステムコールをアセンブラから呼んでみる

今回は、表題の通り x86/x86-64 の macOS でシステムコールをアセンブラから呼んでみる。 ただし、前回のエントリで FreeBSD についても同じようにシステムコールをアセンブラから呼んだ。 macOS は BSD を先祖に持つ XNU カーネルで動いている。 そのため、大筋は FreeBSD の場合と違いはない。 ようするに System V x86/x86-64 ABI の規約にもとづいて呼び出してやればいいだけだ。

blog.amedama.jp

とはいえ、FreeBSD と全く違いがないわけではない。 なので、それについて見ていくことにしよう。

使った環境は次の通り。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G103
$ uname -sr
Darwin 18.7.0
$ nasm -v
NASM version 2.14.02 compiled on Dec 27 2018
$ ld -v
@(#)PROGRAM:ld  PROJECT:ld64-450.3
BUILD 18:16:53 Apr  5 2019
configured to support archs: armv6 armv7 armv7s arm64 arm64e arm64_32 i386 x86_64 x86_64h armv6m armv7k armv7m armv7em
LTO support using: LLVM version 10.0.1, (clang-1001.0.46.4) (static support for 22, runtime is 22)
TAPI support using: Apple TAPI version 10.0.1 (tapi-1001.0.4.1)

もくじ

下準備

最初に、アセンブラの実装として NASM (Netwide Assembler) を Homebrew でインストールしておく。

$ brew install nasm

GAS (GNU Assembler) を使っても問題ないけど、INTEL 記法と AT&T 記法でちょっと手直しが必要になる。

x86 のアセンブラで exit(2) を呼び出すだけのプログラム

先のエントリと同じように、まずは終了するだけのプログラムを書いてみよう。 macOS にも、他の Unix 系 OS と同じようにプログラムを終了するためのシステムコールとして exit(2) がある。 macOS のカーネルである XNU のソースコードは、リリースからやや遅れはあるもののオープンソースとして公開されている。 そのため、次のようにシステムコールの識別子を確認できる。

github.com

上記から、exit(1) に対応する識別子が 1 であることがわかる。 ただし、実は上記の値をそのまま使ってもエラーになってしまう。

実際には、上記の識別子に 0x2000000 を加えなきゃいけない。 その理由は以下のファイルを見るとわかる。

github.com

どうやら macOS のカーネルである XNU が Mach と BSD から生まれた影響で、何に由来するシステムコールなのか指定が必要らしい。 その指定が、BSD であれば 2 を 24 ビット左シフトした値、ということみたいだ。

前置きが長くなったので、そろそろサンプルコードを見ることにしよう。 以下が exit(2) を呼び出すだけのサンプルコードになる。 FreeBSD の x86 版と異なるのは、前述したマスク値がひとつ。 もう一つがエントリポイントのシンボルが _start ではなく _main になっている。 まあ、これはデフォルト値の話なので、リンカで別途上書きしてしまっても良いはず。

global _main

section .text

_syscall:
  int 0x80
  ret

_main:
  ; exit system call (macOS / x86)
  push dword 0
  mov eax, 0x2000001  ; 2 << 24 + 1
  call _syscall
  add esp, byte 4  ; restore ESP
  ret  ; don't reach here

x86 (32bit) 向けにビルドする

コードが用意できたので、x86 (32bit) 向けのバイナリとして Mach-O 32bit フォーマットにアセンブルする。

$ nasm -f macho32 nop.asm
$ file nop.o                       
nop.o: Mach-O object i386

動作に必要な最低バージョンの指定を入れつつ実行可能オブジェクトファイルにリンクする。

$ ld -macosx_version_min 10.14 -lsystem -o nop nop.o
$ file nop  
nop: Mach-O executable i386

できたファイルを実行してみよう。

$ ./nop
$ echo $?
0

ちゃんと返り値もゼロが設定されている。 うまく動いているようだ。

ちなみに、今回使っている macOS のバージョンが Mojave (10.14) だからこそ上記は動く。 なぜなら、次のバージョン Catalina (10.15) からは 32bit アプリケーションのサポートがなくなってしまった。 なので、おそらくこのマシンも OS をアップデートしたら上記のバイナリが動かなくなるはずだ。

x86-64 (64bit) 向けにビルドする

じゃあ 64bit 向けにビルドすれば良いのか、というとそうもいかない。 なぜなら、先のエントリで取り扱った通り System V ABI は x86 と x86-64 で引数の渡し方が異なっている。 そのため、先ほどのサンプルコードは x86-64 (64bit) 向けのアプリケーションとしては動作しない。

やろうと思えば、一応は Mach-O 64bit フォーマットとしてアセンブリはできる。

$ nasm -f macho64 nop.asm
$ file nop.o
nop.o: Mach-O 64-bit object x86_64
$ ld -macosx_version_min 10.14 -lsystem -o nop nop.o
$ file nop  
nop: Mach-O 64-bit executable x86_64

しかし、実行しようとするとエラーになってしまう。

$ ./nop 
zsh: illegal hardware instruction  ./nop

x86-64 のアセンブラで exit(2) を呼び出すだけのプログラム

64 bit 向けのアプリケーションを作るためには、以下のように System V v86-64 ABI に準拠した呼び出しが必要になる。

global _main

section .text

_main:
  ; exit system call (macOS / x86-64)
  mov rax, 0x2000001
  mov rdi, 0
  syscall

上記を Mach-O 64 bit フォーマットとしてアセンブリする。

$ nasm -f macho64 nop.asm
$ file nop.o
nop.o: Mach-O 64-bit object x86_64

そして、リンクする。

$ ld -macosx_version_min 10.14 -lsystem -o nop nop.o
$ file nop  
nop: Mach-O 64-bit executable x86_64

できたファイルを実行してみよう。 今度はエラーにならない。

$ ./nop
$ echo $?
0

返り値についても、ちゃんとゼロが設定されている。

x86-64 のアセンブラでハローワールド

一応、ハローワールドについても以下にサンプルコードを示す。

global _main

section .data
  msg db 'Hello, World!', 0x0A
  msg_len equ $ - msg

section .text

_main:
  ; write system call (macOS / x86-64)
  mov rax, 0x2000004
  mov rdi, 1
  mov rsi, msg
  mov rdx, msg_len
  syscall

  ; exit system call (macOS / x86-64)
  mov rax, 0x2000001
  mov rdi, 0
  syscall

ビルドする。

$ nasm -f macho64 greet.asm
$ ld -macosx_version_min 10.14 -lsystem -o greet greet.o

実行する。

$ ./greet
Hello, World!
$ echo $?
0

うまく動いているようだ。

いじょう。

FreeBSD (x86/x86-64) のシステムコールをアセンブラから呼んでみる

今回は、表題の通り x86/x86-64 の FreeBSD でアセンブラからシステムコールを呼んでみる。 システムコールは、OS (ディストリビューション) のコアとなるカーネルがユーザ空間のプログラムに向けて提供しているインターフェースのこと。 ファイルの入出力など、ユーザープログラムは大抵のことはシステムコールを通じてカーネルにお願いしてやってもらうことになる。 ただ、普段は色々な API がラップして実体が見えにくいので、今回はアセンブラから直接呼んでみることにした。

使った環境は次の通り。

$ uname -sr
FreeBSD 12.0-RELEASE
$ nasm -v
NASM version 2.14.02 compiled on Aug 22 2019
$ ld -v
LLD 6.0.1 (FreeBSD 335540-1200005) (compatible with GNU linkers)

TL; DR

分かったことは次の通り。

  • 商用 UNIX の流れをくむ Unix 系 OS のシステムコールは ABI (Application Binary Interface) が x86 と x86-64 で異なる
    • x86 (32bit) ではスタックを使って引数を渡す
    • x86-64 (64bit) ではレジスタを使って引数を渡す

ちなみに Linux では、どちらもレジスタ経由でシステムコールの引数を渡していた点が異なる。

blog.amedama.jp

もくじ

下準備

あらかじめ、下準備として NASM (Netwide Assembler) をインストールしておく。

$ sudo pkg install nasm

NASM / x86 で exit(2) を呼び出すだけのプログラム

まずは、ハローワールドですらなく「終了するだけ」のプログラムを書いてみる。 FreeBSD には、プログラムを終了するためのシステムコールとして exit(2) がある。

最初は x86 (32bit) アーキテクチャのアセンブラを用いる。 FreeBSD で x86 のシステムコールに関するドキュメントとしては以下があった。

www.freebsd.org

以下が、上記のドキュメントにもとづいて x86 で exit(2) を呼び出すだけのアセンブラのソースコードとなる。 FreeBSD の x86 アセンブラでは、割り込み番号 0x80int 命令を発行するだけの関数経由でシステムコールを呼ぶことを想定しているらしい。 また、Linux の x86 のシステムコール呼び出しとは異なり、引数はシステムコールの識別子を除いてスタック経由で受け渡しされる。

global _start

section .text

_syscall:  ; FreeBSD expects to call system call via int 0x80 only function
  int 0x80
  ret

_start:
  ; exit system call (FreeBSD / x86)
  push dword 0  ; return code
  mov eax, 1  ; system call id (exit)
  call _syscall  ; call system call
  add esp, byte 4  ; restore ESP (DWORD x1)
  ret  ; don't reach here

x86 (32bit) 向けにビルドする

それでは、上記を実際にビルドして実行してみよう。

まずは NASM を使って 32bit の ELF (Executable and Linkable Format) としてアセンブルする。

$ nasm -f elf32 nop.asm
$ file nop.o
nop.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped

上記をリンカを使って実行可能オブジェクトファイルにする。

$ ld -m elf_i386_fbsd -o nop nop.o
$ file nop
nop: ELF 32-bit LSB executable, Intel 80386, version 1 (FreeBSD), statically linked, not stripped

できたファイルを実行してみよう。

$ ./nop

何も表示されないけど、エラーにならずに実行できていることがうまくいっていることを示している。

プログラムの返り値についてもスタック経由で指定したゼロになっている。

$ echo $?
0

x86-64 (64bit) 向けにビルドする

では、次に先ほどのサンプルコードを x86-64 (64bit) 向けのアプリケーションとしてビルドしてみよう。 x86-64 は機械語のレベルでは x86 と後方互換性があるけど、どういった結果になるだろうか。

まずは 64bit の ELF としてアセンブルする。

$ nasm -f elf64 nop.asm

実行可能オブジェクトファイルとしてリンクする。

$ ld -m elf_amd64_fbsd -o nop nop.o

実行してみよう。

$ ./nop

特にエラーにならず実行できているので、うまくいっていそうだけど…?

返り値を見ると非ゼロの値が入っている。

$ echo $?
144

どうやら返り値の受け渡しの部分がうまくいっていないようだ。

NASM / x86-64 で exit(2) を呼び出すだけのプログラム

実は FreeBSD というか商用 UNIX の流れをくんだ Unix 系 OS は x86 と x86-64 でシステムコールの ABI が異なっている。 具体的には、x86 ではスタック経由で引数を渡していたのが x86-64 ではレジスタ経由で渡すことになった。 この問題については以下が分かりやすい。

stackoverflow.com

また、詳しくは以下のリポジトリでメンテナンスされている PDF についても参照のこと。

github.com

上記にもとづいて 64 ビットのアプリケーションとしてビルドできるサンプルコードを以下に示す。 これはシステムコールの識別子は異なるものの、前述した GNU/Linux の記事に出てきたサンプルコードとほとんど同じもの。 x86-64 においては Linux と商用 UNIX の子孫たちでシステムコールの呼び出し方は同じになっている。

global _start

section .text

_start:
  ; exit system call (FreeBSD / x86-64)
  mov rax, 1  ; system call id
  mov rdi, 0  ; return value
  syscall  ; call system call

実際に上記を 64 bit の実行可能オブジェクトファイルとしてビルドしてみよう。

$ nasm -f elf64 nop.asm
$ file nop.o
nop.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
$ ld -m elf_amd64_fbsd -o nop nop.o
$ file nop
nop: ELF 64-bit LSB executable, x86-64, version 1 (FreeBSD), statically linked, not stripped

できたファイルを実行する。

$ ./nop
$ echo $?
0

今度は返り値についてもゼロが設定されている。

ちなみに Linux では同じシステムコールでも x86 t x86-64 で識別子が異なったけど、FreeBSD の場合は変わらないらしい。

github.com

write(2) を追加したハローワールドでも比較してみる

呼び出すシステムコールが exit(2) だけだと味気ないので write(2) も追加してハローワールドも見ておこう。

x86 版 ABI のハローワールド

まずは x86 版 ABI を使ったハローワールドが以下の通り。

global _start

section .data
  msg db 'Hello, World!', 0x0A
  msg_len equ $ - msg

section .text

_syscall:
  int 0x80
  ret

_start:
  ; write system call (FreeBSD / x86)
  push dword msg_len
  push dword msg
  push dword 1
  mov eax, 4
  call _syscall
  add esp, byte 12  ; restore ESP (DWORD x3)

  ; exit system call (FreeBSD / x86)
  push dword 0
  mov eax, 1
  call _syscall
  add esp, byte 4  ; restore ESP (DWORD x1)
  ret  ; don't reach here

x86 (32bit) 向けの実行可能オブジェクトファイルとしてビルドする。

$ nasm -f elf32 greet.asm
$ ld -m elf_i386_fbsd -o greet greet.o

できたファイルを実行する。

$ ./greet
Hello, World!
$ echo $?
0

ちゃんと動作しているようだ。

x86-64 版 ABI のハローワールド

続いて以下が x86-64 版 ABI のハローワールドになる。

global _start

section .data
  msg db 'Hello, World!', 0x0A
  msg_len equ $ - msg

section .text

_start:
  ; write system call (FreeBSD / x86-64)
  mov rax, 4
  mov rdi, 1
  mov rsi, msg
  mov rdx, msg_len
  syscall

  ; exit system call (FreeBSD / x86-64)
  mov rax, 1
  mov rdi, 0
  syscall

x86-64 (64bit) 向けの実行可能オブジェクトファイルとしてビルドする。

$ nasm -f elf64 greet.asm
$ ld -m elf_amd64_fbsd -o greet greet.o

できたファイルを実行する。

$ ./greet
Hello, World!
$ echo $?
0

うまくいっている。

FreeBSD の Linux バイナリ互換機能

ちなみに FreeBSD には Linux の ABI を使ったアプリケーションを実行するためのエミュレーション機能があるらしい。 実際に試してみよう。

エミュレーション機能はカーネルモジュールとして実行されているため linux.ko を読み込む。

$ sudo kldload linux.ko
$ kldstat | grep linux
 7    1 0xffffffff8284c000    39960 linux.ko
 8    1 0xffffffff82886000     2e28 linux_common.ko

Linux で動作する x86 版 ABI のサンプルコードを以下のように用意する。 レジスタで引数を渡しているので System V の x86 版 ABI とは異なる。

global _start

section .text

_start:
  mov eax, 1
  mov ebx, 0
  int 0x80

上記を 32 版の実行可能オブジェクトファイルとしてビルドする。

$ nasm -f elf32 nop.asm
$ ld -m elf_i386 -o nop nop.o

このままでは実行できない。

$ ./nop 
ELF binary type "0" not known.
-bash: ./nop: cannot execute binary file: Exec format error

そこで、brandelf コマンドを使って Linux 互換のバイナリとしてマークする。

$ brandelf -t Linux nop

実行してみる。

$ ./nop
$ echo $?
0

今度はちゃんと動作した。

GNU/Linux (x86/x86-64) のシステムコールをアセンブラから呼んでみる

今回は、表題の通り x86/x86-64 の GNU/Linux でシステムコールをアセンブラから呼んでみる。 システムコールは、OS (ディストリビューション) のコアとなるカーネルがユーザ空間のプログラムに向けて提供しているインターフェースのこと。

なお、アセンブラの実装に関しては以下の二つを試した。

  • NASM (Netwide Assembler)
  • GAS (GNU Assembler)

アセンブラには INTEL 記法と AT&T 記法という二つのシンタックスがある。 NASM はデフォルトで INTEL 記法を、GAS はデフォルトで AT&T 記法を使うことになる。

使った環境は次の通り。

$ uname -sr
Linux 4.15.0-65-generic
$ nasm -v
NASM version 2.13.02
$ as -v
GNU assembler version 2.30 (x86_64-linux-gnu) using BFD version (GNU Binutils for Ubuntu) 2.30
$ ld -v
GNU ld (GNU Binutils for Ubuntu) 2.30
$ gcc --version
gcc (Ubuntu 7.4.0-1ubuntu1~18.04.1) 7.4.0
Copyright (C) 2017 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=18.04
DISTRIB_CODENAME=bionic
DISTRIB_DESCRIPTION="Ubuntu 18.04.3 LTS"

もくじ

下準備

あらかじめ、使うパッケージをインストールしておく。

$ sudo apt -y install nasm binutils gcc

NASM / x86 で exit(2) を呼び出すだけのプログラム

まずは、ハローワールドですらなく「終了するだけ」のプログラムから書いてみる。 GNU/Linux には、プログラムを終了するためのシステムコールとして exit(2) がある。

以下のサンプルコードは NASM (Netwide Assembler) 向けの x86 アセンブラで Linux の exit(2) を呼び出すだけのプログラムとなっている。 ちょっとゴチャゴチャしてるけど ; はコメントで、決まりきった内容も含むので実質的には 3 行しかない。

global _start

section .text

_start:
  ; exit system call (Linux / x86)
  mov eax, 1  ; system call id
  mov ebx, 0  ; return code
  int 0x80  ; soft interrupt

まず、global _start_start というシンボルを外部に公開することを示している。 そして、シンボル _start: 以下が実際の処理になっている。 section .text については、プログラムのテキスト領域がここからありますよ、という指示になっている。

そして、Linux / x86 (32bit) の場合、システムコールを呼ぶには以下の手順が必要になる。

  1. eax レジスタに呼びたいシステムコールの識別子を代入する
  2. 必要に応じて各レジスタにシステムコールの引数を代入する
  3. 割り込み番号 0x80 を指定して INT 命令を実行する

先ほどのサンプルコードの _start では、各行が上記の (1) ~ (3) に対応している。

  1. eax レジスタに exit(2) システムコールに対応する識別子の 1 を代入している
  2. exit(2) システムコールの第一引数として、プログラムの返り値 0ebx レジスタに代入している
  3. int 0x80 を呼ぶことで、ソフトウェア割り込み経由でシステムコールを呼び出している

x86 (32bit) 向けにビルドする

それでは、実際にサンプルコードをビルドしてみる。 今どきそうそう必要になることはないだろうけど、まずは x86 (32bit) 向けのバイナリにしてみよう。

nasm コマンドを使ってソースコードをコンパイル…じゃなくてアセンブルする。 出力フォーマットには 32bit 版の ELF (Executable and Linkable Format) を指定する。

$ nasm -f elf32 nop.asm

これで、32bit 版の再配置可能オブジェクトファイルができた。

$ file nop.o 
nop.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped

リンカを使って上記を実行可能オブジェクトファイルにする。

$ ld -m elf_i386 -o nop nop.o
$ file nop
nop: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, not stripped

ちなみに、エントリポイントとなるシンボルを指定したいときは -e オプションを使えば良い。 デフォルトでは _start が使われるので、先ほどのサンプルコードを使っている限り特に指定する必要はない。

できあがった実行可能オブジェクトファイルを実際に実行してみよう。

$ ./nop

何も起こらないけど、そもそも何もエラーにならずプログラムが終了している、という点が上手くいっていることを示している。

プログラムの返り値を確認しても非ゼロなので正常終了している。

$ echo $?
0

x86-64 (64bit) 向けにビルドする

x86-64 (64bit) アーキテクチャは x86 (32bit) アーキテクチャと後方互換性がある。 なので、先ほどの x86 向けのアセンブラは x86-64 向けにもビルドできる。

実際に、x86-64 向けにビルドしてみよう。 今度は 64bit 版の ELF としてアセンブルする。

$ nasm -f elf64 nop.asm
$ file nop.o
nop.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

そして、リンカを使って実行可能オブジェクトファイルにリンクする。

$ ld -m elf_x86_64 -o nop nop.o
$ file nop
nop: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped

できたファイルを実行してみよう。

$ ./nop

こちらも、エラーにならずちゃんとプログラムが終了している。

返り値についても非ゼロなので正常終了しているようだ。

$ echo $?
0

NASM / x86-64 で exit(2) を呼び出すだけのプログラム

次こそはハローワールド…の前に、x86-64 アセンブラを扱っておく。 内容は先ほどと同じ exit(2) を呼び出すだけのプログラム。 純粋にシステムコールの呼び出し方の違いが分かりやすいはず。

以下が NASM 向けの x86-64 アセンブラで Linux の exit(2) を呼び出すだけのサンプルコード。 変わっているのは _start の中だけ。

global _start

section .text

_start:
  ; exit system call (Linux / x86-64)
  mov rax, 60  ; system call id
  mov rdi, 0  ; return code
  syscall  ; system call op code

x86 アセンブラとの違いを見ていくと、まず最初の 2 行で使うレジスタが異なっていることがわかる。 これは、x86-64 でレジスタが 64bit に拡張されたことに伴って名前が変わっている。

また、exit(2) システムコールを表す識別子も 1 から 60 に変わっていることがわかる。 この識別子はどこにあるの、という話なんだけど以下のように探せる。

$ locate syscalls | grep 32.h
/usr/src/linux-headers-4.15.0-62/arch/sh/include/asm/syscalls_32.h
/usr/src/linux-headers-4.15.0-62-generic/arch/x86/include/generated/asm/.syscalls_32.h.cmd
/usr/src/linux-headers-4.15.0-62-generic/arch/x86/include/generated/asm/syscalls_32.h
$ locate syscalls | grep 64.h
/usr/src/linux-headers-4.15.0-62/arch/sh/include/asm/syscalls_64.h
/usr/src/linux-headers-4.15.0-62-generic/arch/x86/include/generated/asm/.syscalls_64.h.cmd
/usr/src/linux-headers-4.15.0-62-generic/arch/x86/include/generated/asm/syscalls_64.h
$ grep sys_exit /usr/src/linux-headers-4.15.0-62-generic/arch/x86/include/generated/asm/syscalls_32.h
__SYSCALL_I386(1, sys_exit, )
__SYSCALL_I386(252, sys_exit_group, )
$ grep sys_exit /usr/src/linux-headers-4.15.0-62-generic/arch/x86/include/generated/asm/syscalls_64.h
__SYSCALL_64(60, sys_exit, )
__SYSCALL_64(231, sys_exit_group, )

ソースコードにあるテーブルを見ても良いかも。

linux/syscall_32.tbl at v4.15 · torvalds/linux · GitHub

linux/syscall_64.tbl at v4.15 · torvalds/linux · GitHub

そして 3 行目が決定的な違いで、x86-64 アセンブラでは syscall 命令という専用のオペコードが用意されている。

それでは、上記をビルドして実行してみよう。 x86-64 のアセンブラは x86 アーキテクチャでは動かないので、x86-64 向けにのみビルドする。

まずは NASM で x86-64 向けにアセンブリする。

$ nasm -f elf64 nop.asm
$ file nop.o
nop.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

リンクして実行可能オブジェクトファイルにする。

$ ld -m elf_x86_64 -o nop nop.o
$ file nop
nop: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped

次の通り、ちゃんと動くことがわかる。

$ ./nop
$ echo $?
0

NASM / x86-64 で write(2) も呼んでハローワールドしてみる

次は呼び出すシステムコールを増やしてコンソールの標準出力にハローワールドを出してみよう。 変わるのは、出力する文字列を扱うことになるのと、write(2) システムコールを呼び出すところ。

以下がコンソールに Hello, World! と出力するサンプルコードになる。 新たに増えた .data のセクションはプログラムのデータ領域を示している。 ただし、今回の本題はシステムコールなので、ここでは深入りしない。

global _start

section .data
  msg db 'Hello, World!', 0x0A  ; 0x0A = \n
  msg_len equ $ - msg

section .text

_start:
  ; write system call (Linux / x86-64)
  mov rax, 1
  mov rdi, 1
  mov rsi, msg
  mov rdx, msg_len
  syscall

  ; exit system call (Linux / x86-64)
  mov     rax, 60
  mov     rdi, 0
  syscall

上記のサンプルコードは先ほどに比べると syscall の呼び出しが増えている。 増えているのは write(2) の呼び出しで、これはファイルディスクリプタへのデータの書き込みをするシステムコールになっている。 mov rax, 1write(2) の識別子を rax レジスタに代入していて、その後ろがシステムコールの引数となっている。 rdi に代入している 1 は、標準出力のファイルディスクリプタの番号を示している。 rsi に代入している msg は出力する文字列のアドレスで、rdx に代入している msg_len が長さを示している。

それでは、上記を実際にビルドして実行してみよう。 先ほどと同じように実行可能オブジェクトファイルを作る。

$ nasm -f elf64 greet.asm
$ ld -m elf_x86_64 -o greet greet.o

できたファイルを実行すると、ちゃんと文字列が標準出力に表示される。

$ ./greet 
Hello, World!
$ echo $?
0

GAS / x86-64 で exit(2) を呼び出すだけのプログラム

続いては GAS (GNU Assembler) 向けのアセンブラを書いてみる。 書いていることは同じだけど記法が若干異なる。 NASM は INTEL 記法だったけど、GAS はデフォルトで AT&T 記法が使われる。 違いは、オペランドを書く順番が逆だったり、値やレジスタに記号が必要だったりする。

以下が AT&T 記法で書いた GAS 向けの exit(2) だけを呼び出す x86-64 アセンブラのサンプルコード。

.global _start

.text

_start:
  # exit system call (Linux / x86-64)
  mov $60, %rax
  mov $00, %rdi
  syscall

上記を as でアセンブルする。

$ as -o nop.o nop.s
$ file nop.o
nop.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

リンクして実行可能オブジェクトファイルにする。

$ ld -o nop nop.o
$ file nop
nop: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped_
$ ./nop
$ echo $?
0

GAS でプリプロセッサを使う

ちなみに GAS の場合はアセンブルに gcc を使うとプリプロセッサが使えるようだ。 プリプロセッサが使えると、例えばヘッダファイルを読み込んでシンボルを解決できたりする。

以下は GAS / x86-64 のハローワールドだけど、ヘッダとして <sys/syscall.h> を読み込んでいる。 ヘッダに含まれるシンボルはシステムコールを呼び出すところで $SYS_* という風に使っている。

#include <sys/syscall.h>

.global _start

.data
  message:
    .ascii "Hello, World!\n"
  message_len = . - message

.text

_start:
  # write system call
  mov $SYS_write, %rax
  mov $1, %rdi
  mov $message, %rsi
  mov $message_len, %rdx
  syscall

  # exit system call
  mov $SYS_exit, %rax
  mov $0, %rdi
  syscall

ファイル名の拡張子を大文字の .S にして gcc でアセンブルする。

$ gcc -c greet.S
$ ld -o greet greet.o

できたファイルを実行してみよう。

$ ./greet 
Hello, World!
$ echo $?
0

うまく動いているようだ。

参考

blog.packagecloud.io

python-livereload で Re:VIEW の執筆を捗らせてみる

普段、Sphinx でドキュメントを書くときは sphinx-autobuild というツールを使っている。 このツールを使うと、編集している内容をブラウザからリアルタイムで確認できるようになる。

blog.amedama.jp

今回は、上記のような環境が Re:VIEW でも欲しくて python-livereload というパッケージで実現してみた。 ちなみに、python-livereload は、前述した sphinx-autobuild が内部的に使っているパッケージの一つ。 利点としては、ブラウザ側に livereload 系のプラグインを入れる必要がない点が挙げられる。 これは、Web サーバの機能の中で livereload に使うスクリプトを HTML に動的に挿入することで実現している。

使った環境は次の通り。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G103
$ python -V          
Python 3.7.4

もくじ

Re:VIEW の原稿を用意する

まずは Re:VIEW の環境を用意しておく。 詳しくは以下のエントリを参照のこと。

blog.amedama.jp

python-livereload をインストールする

続いて python-livereload をインストールする。

$ pip install livereload

インストールすると livereload コマンドが使えるようになる。 基本的な機能であれば、このコマンドで完結することもある。

$ livereload --help
usage: livereload [-h] [--host HOST] [-p PORT] [-t TARGET] [-w WAIT]
                  [-o OPEN_URL_DELAY] [-d]
                  [directory]

Start a `livereload` server

positional arguments:
  directory             Directory to serve files from

optional arguments:
  -h, --help            show this help message and exit
  --host HOST           Hostname to run `livereload` server on
  -p PORT, --port PORT  Port to run `livereload` server on
  -t TARGET, --target TARGET
                        File or directory to watch for changes
  -w WAIT, --wait WAIT  Time delay in seconds before reloading
  -o OPEN_URL_DELAY, --open-url-delay OPEN_URL_DELAY
                        If set, triggers browser opening <D> seconds after
                        starting
  -d, --debug           Enable Tornado pretty logging

今回は、監視対象のファイルを指定する部分がちょっと複雑になったのでスクリプトから使うことにした。

監視・ビルド用のスクリプトを用意する

続いては python-livereload を使うスクリプトを用意する。 これは、ファイルを監視して、変更があったときはビルドを実行するというもの。

以下のようなスクリプトを用意した。 HTML の入ったディレクトリ以外を監視して、変更があったら rake web を実行するというもの。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os

from livereload import Server
from livereload import shell


def main():
    # livereload が Web サーバでホストするパス
    serve_path = 'webroot'
    # HTML をビルドするコマンド
    build_func = shell('rake web')
    # 監視対象のファイルパス
    watch_targets = ['*', '*/*']

    # パスがないときはあらかじめビルドしておく
    if not os.path.exists(serve_path):
        build_func()

    # 監視を開始する
    server = Server()
    for watch_target in watch_targets:
        server.watch(filepath=watch_target,
                     ignore=lambda path: path == serve_path,
                     func=build_func)
    server.serve(root=serve_path,
                 open_url_delay=1)


if __name__ == '__main__':
    main()

用意したスクリプトを実行すると、Web サーバが立ち上がって自動的にブラウザを開く。 監視対象の何らかのファイルを変更すると、ドキュメントがビルドされ直してブラウザがリロードされる。

$ python autobuild.py
[I 191009 00:17:14 server:296] Serving on http://127.0.0.1:5500
[W 191009 00:17:14 server:304] Use `open_url_delay` instead of `open_url`
[I 191009 00:17:14 handlers:62] Start watching changes
[I 191009 00:17:14 handlers:64] Start detecting changes
[I 191009 00:17:20 handlers:135] Browser Connected: http://127.0.0.1:5500/

ばっちり。

Rake のタスクにする

スクリプトとして実行するのでも良いんだけど、Rake のタスクにしておいた方が使うとき便利かもしれない。

次の通り Rake の設定ファイルを用意する。

$ cat << 'EOF' > lib/tasks/autobuild.rake 
require 'rake'

desc 'watch the directory and build if any files changed'
task :autobuild do
  sh('python autobuild.py')
end
EOF

これで rake autobuild するとスクリプトが実行されるようになる。

$ rake autobuild

捗りそうだ。

補足

本当は Ruby で完結させたくて、最初はその道を模索した。 ただ、ブラウザのプラグインを必要としない livereload 系の実装が Ruby には見当たらなくて。 加えて、Ruby に不慣れということもあって不本意ながら Python が混ざることになってしまった。 けどまあ、手間をほとんどかけずに実現できたのは良かったかな。

デジタル出版システム Re:VIEW を使ってみる

書籍の執筆環境として、最近は Re:VIEW の評判が良いので試してみることにした。 しばらく使い込んでみて良さそうだったら、既存の Sphinx の環境から移行するのもありかもしれない。 もちろん Sphinx もドキュメントを書くには良いツールなんだけど、はじめから書籍の執筆を試行しているか否かは Re:VIEW と異なる。

使った環境は次の通り。

$ sw_vers         
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G103
$ review version  
3.2.0

もくじ

Ruby をインストールする

Re:VIEW は Ruby で書かれたツールセットなので、インストールするのに Ruby が必要となる。 macOS のシステムにインストールされた Ruby でも大丈夫だとは思うけど、念のため rbenv で最新版を入れておく。

まずは Homebrew で rbenv をインストールする。

$ brew install rbenv ruby-build

現時点で最新版の Ruby をインストールする。

$ rbenv install 2.6.5

デフォルトのバージョンをインストールしたものに切り替える。

$ rbenv global 2.6.5

パスを通すためにシェルの設定ファイルに rbenv の初期設定用の記述を追加する。 以下は ZSH を使っている場合に、rbenv コマンドがあるときだけ初期化するというもの。

$ cat << 'EOF' >> ~/.zlogin
: "rbenv" && {
  which rbenv >/dev/null 2>&1
  if [ $? -eq 0 ]; then
    eval "$(rbenv init -)"
  fi
}
EOF

設定を追加したら読み込む。

$ source ~/.zlogin

Ruby のバージョンが切り替わればおっけー。

$ ruby --version
ruby 2.6.5p114 (2019-10-01 revision 67812) [x86_64-darwin18]

Mac TeX をインストールする

Re:VIEW は PDF をビルドするのに TeX Live を必要とする。 そこで、macOS であれば Homebrew Cask で Mac TeX を入れておく。

$ brew cask install mactex

Re:VIEW をインストールする

準備ができたので Gem を使って Re:VIEW をインストールする。

$ gem install review

review コマンドが叩けるようになれば良い。

$ review version
3.2.0

プロジェクトを作る

まずは review-init コマンドでプロジェクトのひな形を作る。

$ review-init helloworld

これで、必要なファイル群ができた。

$ ls helloworld 
Gemfile     config.yml  images      sty
Rakefile    doc     layouts     style.css
catalog.yml helloworld.re   lib

できたディレクトリに移動する。

$ cd helloworld

Re:VIEW では .re ファイルに Re:VIEW の軽量マークアップ言語を使って文章を書いていく。 試しに適当な文章を追加しておこう。

$ cat << 'EOF' > helloworld.re
= Hello, World!

hello, world
EOF

HTML にビルドする

各形式には Rake のタスクを実行することでビルドできる。 タスクは内部的には review-* のコマンドを叩いている。

$ rake web

例えば web タスクであれば webroot ディレクトリに HTML の成果物がビルドされる。

$ ls webroot
helloworld.html images      index.html  style.css   titlepage.html

PDF にビルドする

同様に、rake pdf なら PDF がビルドできる。

$ rake pdf
$ file book.pdf
book.pdf: PDF document, version 1.5

EPUB にビルドする

電子書籍のフォーマットである EPUB も rake epub でビルドできる。

$ rake epub
$ file book.epub
book.epub: EPUB document

所感

設定ファイルなどを見た限りでも、Re:VIEW は書籍の執筆というドメインにかなり特化している印象を受ける。 電子書籍を書く、という目的であれば既存のドキュメンテーションツールをカスタマイズしていくよりも手間が少なく済みそうだ。

シェルスクリプトで返り値のチェックを一時的に止める

シェルスクリプトで set -e しておくとコマンドの返り値が非ゼロ (エラー) のときにスクリプトを止めることができる。 ただ、常に返り値のチェックが有効だと、スクリプトが意図せず止まってしまうことがある。 そんなとき一時的に止める方法とハマりやすい挙動について。

使った環境は次の通り。

$ sw_vers     
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G103
$ bash --version
GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin18)
Copyright (C) 2007 Free Software Foundation, Inc.

もくじ

set -e について

シェルスクリプトで set -e しておくと、コマンドの返り値が非ゼロ (エラー) のときに、処理をそこで止めることができる。 例えば以下のスクリプトでは bash -c "exit 1" のところで止まる。

#!/usr/bin/env bash

# コマンドの実行履歴を出力する
set -x

# コマンドの返り値が非ゼロのとき停止する
set -e

# コマンドの返り値が非ゼロなので異常終了
bash -c "exit 1"  # ここで停止する

# ここまで到達しない
echo "Hello, World"

上記を実行してみよう。

$ bash errchk.sh 
+ set -e
+ bash -c 'exit 1'

最後まで到達しないことが分かる。

一応、コマンドがゼロを返すパターンについても確認しておく。

#!/usr/bin/env bash

# コマンドの実行履歴を出力する
set -x

# コマンドの返り値が非ゼロのとき停止する
set -e

# コマンドの返り値がゼロなので正常終了
bash -c "exit 0"

# エラーがないので、ここまで到達する
echo "Hello, World"

上記を実行すると、今度は最後まで到達する。

$ bash errchk.sh 
+ set -e
+ bash -c 'exit 0'
+ echo 'Hello, World'
Hello, World

返り値のチェックが有効なときの問題点について

ただ、常に返り値のチェックが有効だと困るときもある。 例えば、特定のコマンドの返り値の内容を元に処理を分岐したいとき。

以下のスクリプトは、bash -c ... の実行結果を元に処理を分岐することを意図したサンプルになっている。 しかし、返り値のチェックが有効な状況では後ろの分岐までそもそも到達できない。

#!/usr/bin/env bash

# コマンドの実行履歴を出力する
set -x

# コマンドの返り値が非ゼロのとき停止する
set -e

# コマンドの実行結果を元に処理を分岐させたい
bash -c "exit 1"  # しかし set -e があると停止してしまう

# 以下の分岐まで到達しない
if [ $? -eq 0 ]; then
  echo "ok"
else
  echo "ng"
fi

上記を実行してみよう。 コマンドの返り値が非ゼロを返した時点で止まってしまう。

$ bash errchk.sh 
+ set -e
+ bash -c 'exit 1'

返り値のチェックを一時的に止める

この問題点を解消するには、条件分岐のコマンドを実行するときだけ返り値のチェックを止めた方が良い。 set -e で有効にした返り値のチェックは set +e で無効にできる。 以下のサンプルコードでは、条件分岐の間は返り値のチェックを無効にしている。

#!/usr/bin/env bash

# コマンドの実行履歴を出力する
set -x

# コマンドの返り値が非ゼロのとき停止する
set -e

set +e  # 一時的に返り値のチェックを止める

# 非ゼロを返す可能性があるコマンドを実行する
bash -c "exit 1"
if [ $? -eq 0 ]; then
  echo "ok"
else
  echo "ng"
fi

set -e  # 終わったらエラーのチェックを戻す (各分岐処理の中でやっても良い)

上記を実行してみよう。 今度は、ちゃんと最後まで到達している。

$ bash errchk.sh 
+ set -e
+ set +e
+ bash -c 'exit 1'
+ '[' 1 -eq 0 ']'
+ echo ng
ng
+ set -e

返り値のチェックを無効にするのが上手くいかないパターン

ただし、返り値のチェックを無効にする範囲を最小化したいと考えて次のようにすると上手くいかない。 以下のサンプルコードでは条件分岐に使うコマンドを実行した直後に返り値のチェックを元に戻している。

#!/usr/bin/env bash

# コマンドの実行履歴を出力する
set -x

# コマンドの返り値が非ゼロのとき停止する
set -e

# こうしたいんだけどダメなパターン
set +e
bash -c "exit 1"
set -e  # これもコマンドの実行なので $? が上書きされてしまう

# 分岐が必ず真になってしまう
if [ $? -eq 0 ]; then
  echo "ok"
else
  echo "ng"
fi

しかし、上記では set -e もコマンドの実行なので、常に条件分岐が真になってしまう。

$ bash errchk.sh 
+ set -e
+ set +e
+ bash -c 'exit 1'
+ set -e
+ '[' 0 -eq 0 ']'
+ echo ok
ok

コマンドの返り値をシェル変数に退避させる

上記の問題を解消するには、コマンドの返り値をシェル変数などに退避させておくのが良いと思う。 これなら返り値のチェックを無効にする範囲をちゃんと小さくできる。

#!/usr/bin/env bash

# コマンドの実行履歴を出力する
set -x

# コマンドの返り値が非ゼロのとき停止する
set -e

# 返り値のチェックを止める影響を最小化する
set +e
bash -c "exit 1"
RET=$?  # コマンドの実行が終わったら返り値を変数に退避させる
set -e  # 退避したらエラーのチェックを戻す

# 分岐では退避させた変数を見る
if [ ${RET} -eq 0 ]; then
  echo "ok"
else
  echo "ng"
fi

上記を実行してみると、ちゃんと意図した通りに動作しているようだ。

$ bash errchk.sh 
+ set -e
+ set +e
+ bash -c 'exit 1'
+ RET=1
+ set -e
+ '[' 1 -eq 0 ']'
+ echo ng
ng

いじょう。

色々な Unix 系 OS の crypt(3) について調べたら面白かった話

今回は、色々な Unix 系 OS の crypt(3) について調べたら、過去の経緯などが分かって面白かったという話について。 crypt(3) というのは、標準 C ライブラリの libc ないし libcrypt で実装されている関数のこと。

調査した Unix 系 OS というのは、具体的には以下の通り。

  • GNU/Linux (Ubuntu 18.04 LTS)
$ uname -sr
Linux 4.15.0-65-generic
  • FreeBSD 12.0
$ uname -or
FreeBSD 12.0-RELEASE
  • OpenBSD 6.5
$ uname -sr
OpenBSD 6.5
  • macOS Mojave
$ uname -sr
Darwin 18.7.0

もくじ

TL; DR

  • crypt(3) が対応しているハッシュ化・暗号化方式は意外と OS によってバラバラ
  • crypt(3) でハッシュ化が使われ始めたのは Unix の歴史の中では意外と新しい
  • Python の passlib を使うとプラットフォームに依存せず MCF な文字列を生成できる

crypt(3) について

まずは crypt(3) の基本的な説明から。

前述した通り、crypt(3) は標準 C ライブラリの libc または libcrypt で実装されている関数を指している。 用途としては、ローカルのパスワード認証でシステムにログインするためのパスワードを、一般的にはハッシュ化するのに使う。

典型的には、shadow(5) の中に記述されている謎の文字列を生成するのに使われている。 補足しておくと shadow(5) は /etc/shadow のパスにある設定ファイルを指している。 システムは、ここに書かれている内容と、ユーザが入力した内容を比べることでログインさせて良いか判断する。

$ sudo cat /etc/shadow | grep ^$(whoami): | cut -d : -f 2
$6$hqcnshac$cIZ4Q88Lh/Mz9AybBGKuw8dYuTc/qZbicFX5NVmPVeGVYlwceHYTmuelzQp9i4m.5llRraxzCSdO5aAdmxHUF.

なお、この文字列はハッシュ化されているとはいえログインに使うパスワードから作られたものなので漏えいしないように気をつけよう。 上記は Vagrant で作ったパスワードが自明な環境の出力結果を載せている。

そして、この謎の文字列は、現在では次のようなフォーマットが一般的となっている。

$<id>$<salt>$<encrypted>

<id> はハッシュ化の方式を、<salt> はハッシュ化に使ったソルト、そして <encrypted> がハッシュ化されたパスワードを表している。 なんでハッシュ化 (Hashing) なのに暗号化 (Encrypted) なんだ?というのは後ほど分かってくる。

ちなみに、次のように <id><salt> の間に <option> を挟み込んだフォーマットも用いられることがある。

$<id>$<option>$<salt>$<encrypted>

なお、ここまでに使われている名前の後ろについた (3) みたいなカッコつきの数字は、man page のセクション番号を示している。 Unix の文脈では、何について述べているのかを明確にする目的でよく用いられる。 その内容について詳しく知りたいときは、システム上で次のようにすればマニュアルが読める。

$ man <section> <name>

例えば、今回扱う crypt(3) であれば、次のようにする。

$ man 3 crypt

GNU/Linux で crypt(3) を SHA-512 と一緒に使ってみる

何はともあれ、まずは GNU/Linux で crypt(3) を使ってみることにしよう。

以下の通り、パスワードの文字列を SHA-512 でハッシュ化するサンプルコードを用意した。

#define _XOPEN_SOURCE

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
  // パスワードに使う文字列
  char *password = "password";
  // ハッシュ化の方式とソルト
  char *salt = "$6$salt$";
  // crypt(3) でハッシュ化された文字列
  char *hash = (char *) crypt(password, salt);

  if (hash == NULL) {
      perror("can not crypt");
      exit(EXIT_FAILURE);
  }

  printf("%s\n", hash);
  return EXIT_SUCCESS;
}

上記をビルドする。 libcrypt を使うため -l crypt オプションをつける。

$ gcc -Wall -o sha512.o crypt-sha512.c -l crypt

できたファイルを実行してみよう。 すると、先ほど見たような文字列が出力される。

$ ./sha512.o
$6$salt$IxDD3jeSOb5eB1CX5LBsqZFVkJdido3OUILO5Ifz5iwMuTS4XMS130MTSuDDl3aCI6WouIL9AjRbLCelDCy.g.

このように、crypt(3) を使うことで shadow(5) に書かれる文字列を生成できる。

なお、C 言語がダルいときは次のように Python のインターフェースから使う手もある。

$ python3
...(snip)...
>>> import crypt
>>> crypt.crypt('password', '$6$salt$')
'$6$salt$IxDD3jeSOb5eB1CX5LBsqZFVkJdido3OUILO5Ifz5iwMuTS4XMS130MTSuDDl3aCI6WouIL9AjRbLCelDCy.g.'

ここからが本題なんだけど、実は対応しているハッシュ化・暗号化の方式は OS によって異なる。 例えば、GNU/Linux で crypt(3) のマニュアルを読むと次のように記述されている。

ID  | Method
─────────────────────────────────────────────────────────
1   | MD5
2a  | Blowfish (not in mainline glibc; added in some
    | Linux distributions)
5   | SHA-256 (since glibc 2.7)
6   | SHA-512 (since glibc 2.7)

基本的には MD5 と SHA2 系で、ディストリビューションによっては Blowfish があるかも、という感じ。 それも、古い glibc (GNU 実装の libc) では SHA2 系も使えないことがわかる。 なお、ここでいう Blowfish は、共通鍵暗号方式の Blowfish をベースに考案されたハッシュ関数の Bcrypt を厳密には指している。

FreeBSD がサポートしているアルゴリズムについて

続いて FreeBSD でも試してみよう。 GNU/Linux でビルドしたのと同じソースコードを用意する。

#define _XOPEN_SOURCE

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
  char *password = "password";
  char *salt = "$6$salt$";
  char *hash = (char *) crypt(password, salt);

  if (hash == NULL) {
      perror("can not crypt");
      exit(EXIT_FAILURE);
  }

  printf("%s\n", hash);
  return EXIT_SUCCESS;
}

そして、これをビルドする。

$ cc -Wall -o sha512.o crypt-sha512.c -l crypt

実行すると、先ほどと全く同じ文字列が出力されることがわかる。

$ ./sha512.o
$6$salt$IxDD3jeSOb5eB1CX5LBsqZFVkJdido3OUILO5Ifz5iwMuTS4XMS130MTSuDDl3aCI6WouIL9AjRbLCelDCy.g.

では、他にはどんなハッシュ化・暗号化方式に対応しているのだろうか。 マニュアルを確認してみよう。

$ man 3 crypt

すると、次のような記述が見つかる。 なるほど glibc と比較すると Blowfish が標準で入っていたり NT-Hash という方式に対応していることがわかる。

1.   MD5
2.   Blowfish
3.   NT-Hash
4.   (unused)
5.   SHA-256
6.   SHA-512

ただ、もうちょっとよく読むと、実は crypt(3) が出力するフォーマットには以下の 3 種類があるとも書かれている。

  • Extended
  • Modular
  • Traditional

ここまで見てきた $ で区切られたフォーマットは、どうやら Modular Crypt Format (MCF) というらしい。

そして、以下のような経緯があるようだ。

  • crypt(3) が実装されたのは AT&T UNIX V6 で rotor ベースのアルゴリズムを使っていた
  • 現在のスタイルになったのが V7 で DES ベースのアルゴリズムを使っていた
  • Traditional というのは V7 で実装された DES ベースのアルゴリズムを指している
  • DES ベースのアルゴリズムは今でも使える

注目すべきは、過去そして現在に至るまで DES ベースのアルゴリズムをサポートしているところ。 DES はハッシュ化に使われる一方向関数のアルゴリズムではなく、可逆な暗号化・復号に用いられるアルゴリズムだ。

AT&T UNIX V7 がリリースされたのは 1979 年のこと。 どうやら、crypt(3) でハッシュ化するというアイデアは、Unix の歴史の中ではそれなりに最近のものらしい。 上記のような経緯であれば、マニュアルの中で crypt(3) した文字列を encrypted と表現していたのも理解できる。

なお、FreeBSD に Modular Crypt Format (MCF) と MD5 が実装されたのは 1994 年のようだ。

svnweb.freebsd.org

FreeBSD で Traditional crypt (DES ベースの暗号化) を試してみる

それでは、実際に FreeBSD を使って Traditional crypt の挙動を試してみることにしよう。

Traditional crypt を試すには、先ほどからソルトの文字列を変更するだけで良い。 具体的には、$ もしくは _ から始まらない 2 文字をソルトとして指定する。

#define _XOPEN_SOURCE

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
  char *password = "password";
  // 2 文字のソルト
  char *salt = "XX";
  char *hash = (char *) crypt(password, salt);

  if (hash == NULL) {
      perror("can not crypt");
      exit(EXIT_FAILURE);
  }

  printf("%s\n", hash);
  return EXIT_SUCCESS;
}

上記をビルドしよう。

$ cc -Wall -o des.o crypt-des.c -l crypt

そして、実行してみる。 すると、先頭にソルトがついた短い文字列が得られる。 $ が含まれないことからも、あきらかに MCF とはフォーマットが異なる。

$ ./des.o
XXq2wKiyI43A2

上記が昔の Unix で使われていたフォーマットらしい。

実は Traditional crypt は GNU/Linux でも使える

マニュアルには記述がないんだけど、実は glibc も Traditional crypt をサポートしている。 さっきのソースコードを GNU/Linux でも用意しよう。

#define _XOPEN_SOURCE

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
  char *password = "password";
  char *salt = "XX";
  char *hash = (char *) crypt(password, salt);

  if (hash == NULL) {
      perror("can not crypt");
      exit(EXIT_FAILURE);
  }

  printf("%s\n", hash);
  return EXIT_SUCCESS;
}

そしてビルドする。

$ gcc -Wall -o des.o crypt-des.c -l crypt

そして、実行してみる。 すると、さっきの FreeBSD と同じ文字列が得られた。

$ ./des.o
XXq2wKiyI43A2

OpenBSD では Blowfish しか使えない

GNU/Linux と FreeBSD がサポートしているアルゴリズムが分かったところで、次は OpenBSD も見てみよう。 実は、意外な結果が得られる。

まずは、Traditional crypt から試してみよう。

#define _XOPEN_SOURCE

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
  char *password = "password";
  char *salt = "XX";
  char *hash = (char *) crypt(password, salt);

  if (hash == NULL) {
      perror("can not crypt");
      exit(EXIT_FAILURE);
  }

  printf("%s\n", hash);
  return EXIT_SUCCESS;
}

上記をビルドして実行してみよう。

$ cc -Wall -o des.o crypt-des.c
$ ./des.o
can not crypt: Invalid argument

すると、Invalid argument とエラーになってしまった。

では、SHA-512 を使った MCF はどうだろうか。

#define _XOPEN_SOURCE

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
  char *password = "password";
  char *salt = "$6$salt$";
  char *hash = (char *) crypt(password, salt);

  if (hash == NULL) {
      perror("can not crypt");
      exit(EXIT_FAILURE);
  }

  printf("%s\n", hash);
  return EXIT_SUCCESS;
}

上記をビルドして実行してみる。

$ cc -Wall -o sha512.o crypt-sha512.c
$ ./sha512.o
can not crypt: Invalid argument

こちらもエラーになってしまった。

では、次に以下のようなサンプルコードを用意する。 ハッシュ化の方式は 2b で、その指定の直後に 12 という数字が入るなど何だかこれまでとちょっと異なる。

#define _XOPEN_SOURCE

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
  char *password = "password";
  // Blowfish 2b
  char *salt = "$2b$12$0123456789012345678901$";
  char *hash = (char *) crypt(password, salt);

  if (hash == NULL) {
      perror("can not crypt");
      exit(EXIT_FAILURE);
  }

  printf("%s\n", hash);
  return EXIT_SUCCESS;
}

実は上記は Blowfish のバージョン 2b というアルゴリズムになっている。

上記をビルドしてみよう。

$ cc -o blowfish.o crypt-blowfish.c

そして、実行すると、MCF っぽい文字列が得られる。

$ ./blowfish.o 
$2b$12$012345678901234567890urJuVDjqhVLN4JtdA/LMQb5YGJelo1WO

ソルトの文字列が長いのは、Blowfish 2b のソルトに必ず 22 文字という制約があるため。 また、得られた MCF っぽい文字列は、ソルトとハッシュ化された文字列の間が $ で区切られていないのも特徴的。 ちなみに、ハッシュ化の方式の指定の直後にある 12 という数字は、ハッシュ化を何度繰り返すかというストレッチングの指定だった。 ちなみに、2 の階乗で表現されているため 12 なら 212 = 4096 回らしい。

OpenBSD はバージョン 5.8 から他の方式を捨てた

どうして DES ベースの Traditional crypt や、MCF でも SHA-512 が使えなかったのだろうか。 理由は単純で、OpenBSD はバージョン 5.8 で他の方式をバッサリ捨てて Blowfish 一本に絞ったから。

リポジトリを見ると OpenBSD 5.7 には、まだ Traditional crypt で使うようなコードも残っている。

cvsweb.openbsd.org

しかし、OpenBSD 5.8 になるとコードがバッサリと切り捨てられて Blowfish で使う bcrypt(3) のコードしかない。

cvsweb.openbsd.org

ちなみに、現在の crypt(3) では、それに伴ってマニュアルも改定されている。 そこにはもう Encryption/Encrypted という単語はなく、Hashing という表現があるのみ。

man.openbsd.org

後方互換を捨てたからこそ使える表現なのかもしれない。 古くて脆弱なアルゴリズムを切り捨てる姿勢には、何よりセキュリティを重視する OpenBSD らしさを感じた。

なお、そもそも Bcrypt を考案したのが OpenBSD の開発チームらしい。

https://www.usenix.org/legacy/event/usenix99/provos/provos.pdf

macOS は Traditional crypt しか使えない

最後に、macOS についても確認しておこう。

まずは MCF の SHA-512 から。

#define _XOPEN_SOURCE

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
  char *password = "password";
  char *salt = "$6$salt$";
  char *hash = (char *) crypt(password, salt);

  if (hash == NULL) {
      perror("can not crypt");
      exit(EXIT_FAILURE);
  }

  printf("%s\n", hash);
  return EXIT_SUCCESS;
}

上記をビルドする。

$ cc -Wall -o sha512.o crypt-sha512.c

そして実行する、と何かがおかしい。 MCF にしては短すぎるし $ も少ない。

$ ./sha512.o
$6FMi11BJFsAc

実は、上記では MCF を指定したつもりのソルトの先頭 2 文字が Traditional crypt で使うソルトとして認識されている。

改めて Traditional crypt のソースコードを用意しよう。

#define _XOPEN_SOURCE

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
  char *password = "password";
  char *salt = "XX";
  char *hash = (char *) crypt(password, salt);

  if (hash == NULL) {
      perror("can not crypt");
      exit(EXIT_FAILURE);
  }

  printf("%s\n", hash);
  return EXIT_SUCCESS;
}

ビルドして実行すると、今度はちゃんと動作する。

$ cc -Wall -o des.o crypt-des.c
$ ./des.o
XXq2wKiyI43A2

ええーっ、て感じだけど macOS ではそもそも MCF が使えないようだ。

プラットフォームに依存しない MCF の生成について

ここまで見てきた通り、crypt(3) の実装はプラットフォームに依存する。 そこで、異なるプラットフォーム向けに MCF な文字列を生成する方法についても紹介しておく。

使ったのは Traditional crypt しか使えない macOS Mojave さん。

$ uname -sr
Darwin 18.7.0
$ python -V
Python 3.7.4

Python の passlib というサードパーティ製のパッケージをインストールする。 Blowfish が使いたいときは bcrypt についても入れておく。

$ pip install passlib bcrypt

インストールできたら Python のインタプリタを起動する。

$ python

試しに SHA-512 で生成してみよう。

>>> from passlib.hash import sha512_crypt
>>> sha512_crypt.hash('password', rounds=5000, salt='salt')
'$6$salt$IxDD3jeSOb5eB1CX5LBsqZFVkJdido3OUILO5Ifz5iwMuTS4XMS130MTSuDDl3aCI6WouIL9AjRbLCelDCy.g.'

ちゃんと、先ほど見たのと同じ文字列が得られた。

ちなみに、先ほど指定した rounds というオプションはストレッチング回数を指している。 実は MCF の SHA-512 はデフォルトで 5000 回ストレッチングされている。

それ意外のストレッチング回数を指定すると、次のように <option> の領域に rounds の指定が入るようになる。 これはその回数ストレッチングされた文字列ですよ、ということ。

>>> sha512_crypt.hash('password', rounds=5001, salt='salt')
'$6$rounds=5001$salt$tlQV3WMrkQpSw9nT3Wa2jOpUhHPxDtgcpasv4rOXxXXufOxtxgrzd4O67kS/localLJb2M2o0fCRdv/yfX0SJ/'

Traditional crypt が生成する文字列が得たいなら次のようにする。

>>> from passlib.hash import des_crypt
>>> des_crypt.hash('password', salt='XX')
'XXq2wKiyI43A2'

Blowfish なら次の通り。

from passlib.hash import bcrypt
>>> bcrypt.hash('password', rounds=12, salt='0123456789012345678901')
'$2b$12$012345678901234567890urJuVDjqhVLN4JtdA/LMQb5YGJelo1WO'

いじょう。