/ «2006-07-18 (Tue) ^ 2006-07-30 (Sun)» ?
   西田 亙の本:GNU 開発ツール -- hello.c から a.out が誕生するまで --

Categories Books | Hard | Hardware | Linux | MCU | Misc | Publish | Radio | Repository | Thoughts | Time | UNIX | Writing | プロフィール


2006-07-22 (Sat)

[UNIX] malloc failure (その3)

libc hijack は楽し

今回は、いよいよ malloc failure シリーズの最終回第3回。楽しい "libc hijack" を始める前に、軽く準備体操をしておきましょう。次に示す、puts_test.c をご用意ください。

int puts(const char*);

int main() {
  puts("Hello, world!");
  return 0;
 }

printf 関数の代わりに puts 関数を用いた、簡易版 Hello, world! プログラムです。

$ gcc -Wall -o puts_test puts_test.c 
$ ./puts_test 
Hello, world!
$ nm -u puts_test
         w __gmon_start__
         w _Jv_RegisterClasses
         U __libc_start_main@@GLIBC_2.0
         U puts@@GLIBC_2.0

いつも通りビルドを行い、実行可能ファイルの内部で __libc_start_main および puts シンボルが未定義になっていることを確認します。当然のことながら、puts_test は単体で正常に動作しますが、この時の puts 関数は共有Cライブラリ libc.so に由来しています。

それでは、ここからが本題です。puts_test を実行すると、裏方でダイナミックリンカーローダ(ld-linux.so.2)が起動され、最終リンクを完成させるのでした。

この時、ダイナミックリンカーローダはシンボル解決のために、あらかじめ指定された libc.so を探索するのですが、LD_PRELOAD と呼ばれる環境変数が定義されていると、指定されたファイルを先に探索するような仕掛けが組み込まれているのです。man ld.so を再度ご覧ください。

      LD_PRELOAD
             A  whitespace-separated  list  of additional, user-specified, ELF
             shared libraries to be loaded before all  others.   This  can  be
             used to selectively override functions in other shared libraries.
             For setuid/setgid ELF binaries, only libraries  in  the  standard
             search directories that are also setgid will be loaded.

ここに書かれている通り、ダイナミックリンカーローダは LD_PRELOAD が定義されていると、記述されている共有オブジェクトを先にメモリー上にロードし、シンボル探索を行います。結果として、普段使用しているCライブラリー関数を Override、分かりやすい言葉で表現すれば Hijack することが可能になるのです。

それでは、Hijack 用の puts 関数を用意してみましょう(puts.c)。

#include <stdio.h>     // fprintf()

int puts(const char* msg) {
  return fprintf(stderr, "puts.so: %s\n", msg);
 }

乗っ取り版 puts 関数は、内部で fprintf 関数を用いて引数のメッセージを標準エラー出力(ファイルディスクリプター2番)に出力しています。

実際に、puts.c から共有オブジェクトファイルを生成してみましょう。共有オブジェクトを出力する場合は、gcc に対してふたつのオプションを指定します。

$ gcc -Wall -fPIC -shared -o puts.so puts.c
$ file puts.so
puts.so: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), not stripped

-fPIC は Position Independent Code の略であり、日本語に訳すと位置独立型実行コードを意味しています。

次の -shared は、名前の通り共有オブジェクトファイルを出力するためのオプションです。慣習として、共有オブジェクトの拡張子は so (Shared Object)が指定されます。

共有オブジェクトの概念を理解するためには、機械語の知識が必要になりますので、詳細は割愛しますが、当面はふたつのオプションを定型句として覚えておきましょう。

それでは、libc-hijack に挑戦です。

$ LD_PRELOAD="./puts.so" ./puts_test
puts.so: Hello, world!
$ ./puts_test 
Hello, world!

環境変数 LD_PRELOAD に puts.so をセットしますが、この時カレントディレクトリを意味する ./ を忘れないようにしてください。ダイナミックリンカーローダは共有オブジェクトをロードする際に、絶対パスを必要としますので、puts.so だけではロードエラーとなります。

表示されたメッセージを見ると、"Hello, world!" の前に "puts.so: " が前置されていますから、libc-hijack は見事に成功しています。

同一の実行可能ファイルでありながら、LD_PRELOAD によってプロセスが全く違う挙動を示すことは、不思議な気もしますが、これこそが動的リンクの裏側でダイナミックリンカーローダが活躍している証拠です。

malloc_dumb

ここまでの知識があれば、failmalloc のひな形を作ることは簡単です。呼び出されると常に NULL を返す、malloc 関数を作成してみましょう(malloc_dumb.c)。

#include <sys/types.h>  // size_t

void* malloc(size_t size) {
  return (void*) 0;
 }

わずか3行のプログラムであり、説明の必要もありません。先ほどと同じように、共有オブジェクトを作成します。

$ gcc -Wall -fPIC -shared -o malloc_dumb.so malloc_dumb.c
$ LD_PRELOAD="./malloc_dumb.so" ls /
ls: memory exhausted

試しに ls コマンドで効果を試してみると、"memory exhausted" エラーが表示されました。malloc_dumb.so は hijack に成功したようです。

malloc_null

malloc_dumb.so でも実験には使えるのですが、出来れば malloc の呼び出し履歴を引数と共に観察してみたいものです。そこで、malloc 呼び出しを記録する malloc_null.c を用意しました(2006/7/23 UINT_MAX に修正)。

#include <unistd.h>    // write()
#include <limits.h>    // UINT_MAX

int itoa(unsigned int num, char* buf) {
  int cnt = 0;
  char local[ 11 ];
  char *b = buf, *l = local;

  if (num > UINT_MAX) {
    *b = 0;
    return 0;
   }

  while (num) {
    *l++ = (char) ((num % 10) + '0');
    num /= 10;
   }
  l--;

  while (l >= local) {
    *b++ = *l--;
    cnt++;
   }
  *b = 0;

  return cnt;
 }

void* malloc(unsigned size) {
  char buf[ 11 ];
  int len;
 
  write(1, "malloc_null: malloc(", 20);
  len = itoa(size, buf);
  if (len) write(1, buf, len);
  write(1, ")\n", 2);
  return (void*) 0;
 }

引数のサイズを表示するために printf は敢えて使わず、独自の itoa 関数(64bit環境の方は適宜コードを修正してください)と write 関数を用いていますが、これは printf 自身が内部で malloc を呼び出している場合への配慮です。

malloc_null.c は、呼び出された際の引数を表示し、呼び出し元にゼロを返すだけの簡単なプログラムですが、大変面白い実験を行うことができます(引数を表示している点がミソです)。

$ gcc -Wall -fPIC -shared -o malloc_null.so malloc_null.c 

failmalloc とはことなり、malloc_null.c は Linux, BSD どちらの環境上でもビルド可能です(Mac OS X は不可)。

gcc

それでは、手始めに gcc を解析してみましょう。

$ gcc -dumpversion
4.0.4
$ LD_PRELOAD="./malloc_null.so" gcc -dumpversion
malloc_null: malloc(60)
malloc_null: malloc(352)
malloc_null: malloc(31)
malloc_null: malloc(31)
malloc_null: malloc(60)
malloc_null: malloc(34)
malloc_null: malloc(34)
malloc_null: malloc(24)
malloc_null: malloc(8)
malloc_null: malloc(40)

gcc: out of memory allocating 40 bytes after a total of 0 bytes

結果ですが、何とも中途半端なエラー検出になっています。本来は最初の60バイトのアローケーション時にエラーを検出すべきですが、gcc は9回のエラーを見逃し、10回目でようやく停止しています。

良いように解釈すれば、内部で Allocation error を検出し、何度か再割り当てに挑戦しているとも考えられますが、引数の値から判断するとそうでもなさそうです。なぜ Segmentation fault を起こさずに、プロセスが走り続けているのか、興味があるところです。

as

GCC パッケージがこの調子ですと、binutils パッケージも先が思いやられそうです。

$ as -v
GNU assembler version 2.16.91 (i486-linux-gnu) using BFD version 2.16.91 20060118 Debian GNU/Linux
$ LD_PRELOAD="./malloc_null.so" as -v        
malloc_null: malloc(60)
malloc_null: malloc(352)
malloc_null: malloc(34)
malloc_null: malloc(34)
malloc_null: malloc(60)
malloc_null: malloc(31)
malloc_null: malloc(31)
malloc_null: malloc(20)
malloc_null: malloc(4)
malloc_null: malloc(33)

as: out of memory allocating 33 bytes after a total of 0 bytes

結果は予想通りでした。最初9回の見逃しパターンは gcc と似通っています。

スクリプト言語シリーズ

メモリ資源の扱いはお手の物のはずの、スクリプト言語を見てみましょう。

$ gawk --version
GNU Awk 3.1.5
Copyright (C) 1989, 1991-2005 Free Software Foundation.

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
$ LD_PRELOAD="./malloc_null.so" awk '{}'
malloc_null: malloc(60)
malloc_null: malloc(352)
malloc_null: malloc(31)
malloc_null: malloc(31)
malloc_null: malloc(60)
malloc_null: malloc(33)
malloc_null: malloc(33)
malloc_null: malloc(60)
malloc_null: malloc(34)
malloc_null: malloc(34)
malloc_null: malloc(60)
malloc_null: malloc(30)
malloc_null: malloc(30)
malloc_null: malloc(21)
malloc_null: malloc(5)
malloc_null: malloc(36)
awk: fatal: init_groupset: groupset: can't allocate 36 bytes of memory (Success)

まず最初に GNU awk 3.1.5 ですが、ご覧の通り満身創痍であります。

$ perl -v

This is perl, v5.8.8 built for i486-linux-gnu-thread-multi

Copyright 1987-2006, Larry Wall

Perl may be copied only under the terms of either the Artistic License or the
GNU General Public License, which may be found in the Perl 5 source kit.

Complete documentation for Perl, including FAQ lists, should be found on
this system using "man perl" or "perldoc perl".  If you have access to the
Internet, point your browser at http://www.perl.org/, the Perl Home Page.

$ LD_PRELOAD="./malloc_null.so" perl
malloc_null: malloc(2712)
Segmentation fault

Perl 5.8.8 は、最初のコールで撃沈。

$ ruby -v
ruby 1.8.4 (2005-12-24) [i486-linux]
$ LD_PRELOAD="./malloc_null.so" ruby   
malloc_null: malloc(80)
Segmentation fault

Ruby 1.8.4 も同じく一撃で停止。GNU ツールに比べると、Perl, Ruby の方が潔く健全に思えてきます。

$ python -V
Python 2.3.5
$ LD_PRELOAD="./malloc_null.so" python   
malloc_null: malloc(36)
Fatal Python error: Py_Initialize: can't make first interpreter
Aborted

Python 2.3.5 は、きちんと最初のエラーを検出し、エラーメッセージを出力しています。

$ lua -v
Lua 5.1  Copyright (C) 1994-2006 Lua.org, PUC-Rio
$ LD_PRELOAD="./malloc_null.so" lua           
Lua 5.1  Copyright (C) 1994-2006 Lua.org, PUC-Rio
malloc_null: malloc(3)
xmalloc: out of virtual memory

目下、私の大のお気に入りである Lua 5.1 も、最初のエラーを捉えており、今回チェックしたアプリケーションの中では最も適切なエラーメッセージを出力しています(xmallocはアロケーションエラーをトラップするためのライブラリ関数)。

今後の発展

次なる目標は、乗っ取り版 malloc から本来のCライブラリー版 malloc を呼び出し、一定の頻度や指定された回数後にアロケーションエラーを引き起こすことですが、このためにはダイナミックロード機能を活用する必要があります。

ということで、"その4"に続きます(次回は本当に最終回?)。