それSP戻せばできるよ(仮)

shinhさんの「ふとイヤなコードを思いつきました」インスパイヤされてみました。

...といいたいところなのですがあまりよいものはできず。

// なかなかポータブルなハローワールド
main() {
  char     _[0x40000000];
  char    __[0x40000000];
  char   ___[0x40000000];
  char  ____[0x3fffff60];
  char _____[0x00000091];
  return puts(*(char**)_____ + 2);
}

環境及び実行結果:

% uname -a
Linux localhost.localdomain 2.6.17-1.2174_FC5 #1 Tue Aug 8 15:30:55 EDT 2006 i686 athlon i386 GNU/Linux
% gcc -v
Target: i386-redhat-linux
gcc version 4.1.1 20060525 (Red Hat 4.1.1-1)

% gcc hello.c -o 'hello world!'
% ./hello\ world\!
hello world!
%

まず、引数ナシまたは2引数で呼ぶというネタが一部で確立している気がするputs関数を普通に1引数で呼んでいる時点で敗北しています。最後のサイズ0x91な変数をとっぱらえば引数ナシで呼べそうなものなんですが、自動変数の合計サイズが1GB(多分)をこえていると、gcc様が "error: total size of local objects too large" という見慣れない文句をつけてくるので、ダミーの変数を置き合計サイズが4GBを超えるようにして(size_tな値がwrap aroundするようにして)やらないとだめなのでした。


配列____[]を直接argv[0]の指し先に重ねてしまう方法もありますが、それだとスタック開始位置のランダム化を切らないとだめなんで、とりあえずargv[0]そのものを使うようにしてみたのは多少工夫した点ではあります(がちょっとなー)。なお上記コード、x86_64なgcc-4.1.1でgcc -m32でコンパイルしたらICEしました。なんで。


うーん。内容には関係ありませんが、シゴトで消防士に任命されて焼け死にそうな為、即座にテキトーな事を書けないのも無念。

const char* const p = "ABC"; と const char q[] = "ABC"; はどちらがよいか、みたいな与太

諸事情あって、ふと前に読んだドキュメントに書いてあった細かいことが気になった。いやいつも細かいけど。

const char* const p = "ABC";

より

const char q[] = "ABC";

のほうがいいのか?的な話。後者の方が良いらしいので、確認するととともに、すぐになんでも書くのはどうなんだと思いつつも無駄に細かく解説。いろいろ間違ってたらゴメンナサイ。C言語入門?

先に結論


共有ライブラリやPIEな実行ファイルを作る場合は、後者の書き方(const char q[] = "xxx")のほうが良さそうですね。PIEじゃない単なる実行ファイルを作るときは、最適化かけるならあんまりかわらないかも。

比較1) コンパイル時の最適化の効きやすさ


最適化といってもいろいろありますが、↓に限って言えば、const char q[] のほうが効きやすいようですね。


gcc version 4.1.1 20060525 (linux/x86) で、最適化なしだと、printf("%zu", strlen(p)); はstrlen関数を呼んでしまうけど、printf("%zu", strlen(q)); は第二引数部分が即値0x3にコンパイルされました。でも、-O2すれば両方0x3になるので、あんまり違わないともいえます。なお、"z" はsize_tとssize_tに対応する修飾文字です。どうでもいいトリビア

比較2) 実行中の時間的なコスト


比較2の結論も先に書くと、「単に実行ファイルを作る場合ならポインタ版と配列版に違いなし。PICかPIEにする場合、間接参照が1段少ない分、配列版のほうが速そうに見えますがどうなんだろ」となります。

static const char* const gsp   = "AAA";
       const char* const gpp   = "BBB";
static const char        gsa[] = "CCC";
       const char        gpa[] = "DDD";

void bar() {
  printf("static pointer = %s\n", gsp);
  printf("public pointer = %s\n", gpp);
  printf("static array   = %s\n", gsa);
  printf("public array   = %s\n", gpa);
}

のようなコードを準備。確認していきます。


(1) PIC

% gcc -fPIC -shared -o shared.so bar.c
% nm shared.so | grep 適当に抜粋 | sort
0000070a r gsa
0000070e R gpa
00001784 d gsp
00001788 D gpp
0000186c a _GLOBAL_OFFSET_TABLE_

PICでコンパイルしてnmしたら上記のようになりました。ポインタ版は.data (か.data.ro.relro)に、配列版は.rodataに置かれたようです。次に、readelfコマンドでセクションの一覧を出しておきます。

% eu-readelf -S shared.so | egrep -e '(got|data|rodata)'
[12] .rodata              PROGBITS     000006e0 0006e0 00008a  1 AMS    0   0  1
[17] .data.rel.ro         PROGBITS     00001784 000784 000010  0 WA     0   0  4
[19] .got                 PROGBITS     00001854 000854 000018  4 WA     0   0  4
[20] .got.plt             PROGBITS     0000186c 00086c 000018  4 WA     0   0  4
[21] .data                PROGBITS     00001884 000884 00000c  0 WA     0   0  4

適当に抜粋しました。最後にsoをobjdump(逆アセ)します。

% objdump -d shared.so | less

のあたりを見ます。ここから無駄に解説が細かいです。

00000616 <bar>:
...
 61d:   e8 a5 ff ff ff          call   5c7 <__i686.get_pc_thunk.bx>

__i686.get_pc_thunk.bx は、gccが用意してくれる関数で、PC(プログラムカウンタ、EIPレジスタの値)をebxにコピーします。姉妹関数で __i686.get_pc_thunk.cx もあります。こちらはPCをecxにコピーします。caller-saved registerであるecxにコピーのほうが若干コードを短く出来るので、状況*1によっては勝手にそちらが使われます。なんでここでPCが必要かというと、グローバル変数にアクセスしたいからです。PIC/PIEの場合、実行時に自分自身(ELFバイナリ)がどの仮想アドレスにmmapされるかはわかりませんので、アドレス決め打ちで変数にアクセスはできません。でも、いま実行している命令(61d:)から変数までのオフセットは*.oをリンクしてsoにした時点で判明します(soファイルのナカミがほぼそのままメモリに貼られるわけなんで)。


mov 0xほげ(%eip) ふが; とか出来れば一番いいんですが(x86_64はできる)、x86はそういうPC相対のアドレス指定はできません。というか、eipを直接的に得ることすら出来ません。なんで、仕方なく一度call命令を発行して、リターンアドレス(callを発行した命令の次の命令のアドレス)をスタック上に自動pushさせて、__i686.get_pc_thunk.bx内でebxレジスタにpopしてます。x86だと、PIC/PIEなコードでグローバル変数を触るだけで、関数呼び出しと同じようなコストがかかるんざますよ!奥様。なお、gcc3では確か、__i686.get_pc_thunk.bxみたいな関数はなくって、安直に次の命令に向かって相対なcallをしてたと思います。call いっこ先; popl %ebx; のように。なんで変化したのかは知りません。コードサイズ? 関数をcallという方式にしておけば、毎回popの分、1バイトづつケチれそうではありますね。さて次。

 622:   81 c3 4a 12 00 00       add    $0x124a,%ebx

PCの値にリンカが決めた適切なオフセットを足して、_GLOBAL_OFFSET_TABLE_ というシンボルのアドレスにします。なお、addする値は、*.o の時にはわからないのでダミーの値になっています。*.o の .rel.text というセクションに、「あとで適切な値をうめてケロ」と書いてあります。*.o を objdump -dr すると、R_386_ほげ みたいな記載になっているところが、TBDな場所です。*.o を so にまとめるときに適切な値を埋めることを、(リンク時の)リロケーションとか呼びます。


このadd命令の場所と、addで足される値の和をgdbを電卓として使って計算してみると、

(gdb) p/x 0x622+0x124a
$1 = 0x186c

で、nmしたときの __GLOBAL_OFFSET_TABLE__ シンボルの値と一致しています。実行時のadd結果は、この0x186cにDSOのロードアドレス(DSOの先頭のアドレス)を足したものになるわけです。これ以降、このebxに格納された _GLOBAL_OFFSET_TABLE_ のアドレスを基準として変数に触ります。このときのebxをPICレジスタとか呼んだりもします。たぶん。以降、面倒なので _G_O_T_ と略記します。GOTだと、.gotセクションの先頭アドレスと紛らわしいので。なんで一致していないのかは良く知りませんが、Sunのマニュアルを見た感じだと、「正および負のオフセットでなるべく多くの範囲を指せる様」にシンボルの位置を後ろにずらしてあるとかないとか。

;; ここから static const char* const gsp = "AAA"; へのアクセス処理

 628:   8b 83 18 ff ff ff       mov    0xffffff18(%ebx),%eax
 62e:   89 44 24 04             mov    %eax,0x4(%esp)

0xffffff18は、gdb電卓様によれば -232 です。_G_O_T_ のアドレス 0x186c からこれを引いてみると0x1784です。このあたりは、先のreadelf -S の結果と比べると、.data.rel.ro セクションです。nmの結果と 0x1784 を見比べると、ジャストでgspのアドレスであることがわかります。2番目のmov命令で、gspの内容(="AAA"のアドレス)をスタックに積んでいます。これがprintf関数に第二引数として渡ります。

 632:   8d 83 aa ee ff ff       lea    0xffffeeaa(%ebx),%eax
 638:   89 04 24                mov    %eax,(%esp)
 63b:   e8 84 fe ff ff          call   4c4 <printf@plt>

最初の2行は、フォーマット文字列を引数として積んでいるだけ、3行目はprintfを呼んでるだけです。PLT経由の関数呼び出しは、すでに前に書いたので省略します。今後、printfとその前の2行は略します。

;;	const char* const gpp = "BBB"; のアクセス

 640:   8b 83 e8 ff ff ff       mov    0xffffffe8(%ebx),%eax
 646:   8b 00                   mov    (%eax),%eax
 648:   89 44 24 04             mov    %eax,0x4(%esp)

0xffffffe8 は -24 で、計算すると丁度 .got セクションの先頭あたりです。1行目で、まずGOT経由*2で変数gppのアドレスを得ています。2行目で、gppの指し先のアドレス("BBB")をスタックに積んでいます。間接参照の段数が多く、まわりくどいですね!


何故わざわざGOTを経由するのかですが、これは、LD_PRELOADなどで変数gppをinterpose可能にするためです(よね)。DSOの外に公開されているシンボルは、interpose可能でなくてはなりません*3。gppなるシンボルをもつyokodori.soを作成して、LD_PRELOADしてやると、0xffffffe8(%ebx) はshared.soの変数gppではなく、yokodori.soの変数gppを指すようになります。dynamic linler(ld.so)がそのように仕向けます。

;; static const char gsa[] = "CCC"; のアクセス

 65a:   8d 83 9e ee ff ff       lea    0xffffee9e(%ebx),%eax
 660:   89 44 24 04             mov    %eax,0x4(%esp)

staticなのでGOTは経由しません。ということで、一見最初のと似ていますが、全然違います。まず、最初の命令がmovでなくlea*4です。そして、0xffffee9e(%ebx)は、.data.rel.ro ではなく .rodata です(計算して readelf -S と見比べてみましょう)。.rodataには、お目当ての CCC が格納されています。確認します:

% nm shared.so | grep 適当に抜粋 | sort
0000070a r gsa
0000186c a _GLOBAL_OFFSET_TABLE_

% gdb
(gdb) p/x 0x186c+0xffffee9e
$1 = 0x70a

% eu-readelf -S shared.so | grep -A 1 rodata
[12] .rodata              PROGBITS     000006e0 0006e0 00008a  1 AMS    0   0  1
[13] .eh_frame            PROGBITS     0000076c 00076c 000004  0 A      0   0  4

確かに、.rodata (というかシンボルgsaのアドレスそのもの)です。

% objdump -sj .rodata --start-address=0x70a shared.so

shared.so:     file format elf32-i386

Contents of section .rodata:
 070a 4343 43004444 44004142 43007374 6174 CCC.DDD.ABC.stat
 071a 6963 20706f69 6e746572 203d2025 730a ic pointer = %s.
 072a 0070 75626c69 6320706f 696e7465 7220 .public pointer
 073a 3d20 25730a00 73746174 69632061 7272 = %s..static arr
 074a 6179 2020203d 2025730a 00707562 6c69 ay   = %s..publi
 075a 6320 61727261 79202020 3d202573 0a00 c array   = %s..

そこにはちゃんと "CCC" が格納されています。一番目のアクセスでは、ポインタgspを経由して "AAA" にたどり着いたのですが、3番目のこれではダイレクトに "CCC" に到達しています。一番速そうです(本当のところは知りませんが)。

;; const char gpa[] = "DDD"; のアクセス

 672:   8b 83 f0 ff ff ff       mov    0xfffffff0(%ebx),%eax
 678:   89 44 24 04             mov    %eax,0x4(%esp)

そろそろ説明を端折ります。non-staticなのでGOTを経由しています。しかし、GOTの指し先は.rodataの"DDD"そのものです。2番目のように、まわりくどくポインタを経由したりはしません。3番目よりはまわりくどいですが、2番目よりは効率が良さそうな気がします。


(2) PIE


次、同じコードにカラのmain()を足して、PIEとしてコンパイルしてみます。gcc -fPIE -pie ですね。できた位置独立な実行ファイルをnmしてみます。すると、変数 g{sp}{ap} の置かれているセクションはおなじっぽいですね。こいつらのアドレスを覚えておきます。

% nm なんちゃら
000007e2 r gsa
000007e6 R gpa
00001858 d gsp
0000185c D gpp
0000193c a _GLOBAL_OFFSET_TABLE_

まえと同じく、eu-readelf -S すると下記。

% eu-readelf -S main_PIE | egrep -e '(got|data|rodata)'
[14] .rodata              PROGBITS     000007b0 0007b0 00008e  0 A      0   0  4
[19] .data.rel.ro         PROGBITS     00001858 000858 00000c  0 WA     0   0  4
[21] .got                 PROGBITS     0000192c 00092c 000010  4 WA     0   0  4
[22] .got.plt             PROGBITS     0000193c 00093c 00001c  4 WA     0   0  4
[23] .data                PROGBITS     00001958 000958 000010  0 WA     0   0  4

同じようにbar()を逆アセンブルしてみていきます。

 66b:   e8 a7 ff ff ff          call   617 <__i686.get_pc_thunk.bx>
 670:   81 c3 cc 12 00 00       add    $0x12cc,%ebx

PICレジスタのセットアップは一緒です。addlする数は、バイナリが異なるのでもちろん微妙に異なるわけですが。

;; static const char* const gsp = "AAA"; のアクセス

 676:   8b 83 1c ff ff ff       mov    0xffffff1c(%ebx),%eax
 67c:   89 44 24 04             mov    %eax,0x4(%esp)

これは、PICでのgspのアクセスとまったく一緒です。.data.rel.roのポインタの値をスタックに積んでます。

;;	const char* const gpp = "BBB"; のアクセス

 68e:   8b 83 20 ff ff ff       mov    0xffffff20(%ebx),%eax
 694:   89 44 24 04             mov    %eax,0x4(%esp)

PIEの1番目、つまりすぐ上のgspの例と同じ、です。PICでは公開された変数であるgppのアクセスはGOTを経由していましたが、PIEではGOTを経由しません。したがって、LD_PRELOADでの乗っ取りはできませんが、コードが若干効率的になります。PIEでは同一実行ファイル内の関数・変数のアクセスではGOTを経由しない決まりになっているようです。

;; static const char gsa[] = "CCC"; のアクセス

 6a6:   8d 83 a6 ee ff ff       lea    0xffffeea6(%ebx),%eax
 6ac:   89 44 24 04             mov    %eax,0x4(%esp)

PICの3番目と同じです。効率的なコードです。

;; const char gpa[] = "DDD"; のアクセス

 6be:   8d 83 aa ee ff ff       lea    0xffffeeaa(%ebx),%eax
 6c4:   89 44 24 04             mov    %eax,0x4(%esp)

すぐ上、あるいはPICの3番目と同じです。効率的なコードです。


(3) ただのexe


gsp, gpp, gsa, gpa のどのアクセスについても、下記のような安直かつ効率の良さそうなコードになります。

 80483e6:       c7 44 24 04 18 85 04    movl   $0x8048518,0x4(%esp)


と、ここまで書いて思ったのですが、-O2 かけると、それぞれがもちっと効率的なコードになりますね。でも書く気力が尽きたのでいいや。位置独立なコードのいい加減な解説文ということで・・・・。

比較3) 起動時の時間的なコスト(再配置回数)


何の話をしていたのか、完全に忘れた感もありますが、話を戻します。ポインタにするか配列にするかで、プロセス起動時(ロード時)のリロケーションのコストが変化します。PICとPIEで微妙に異なるようなので、分けて。下記挙動はすべて、eu-readelf -r binary で調べられます。このへんは(も)理解が浅いのであっさりです。ちゃんと追わないと駄目だ。。。


(1) PIC

  • const char* const x = ""; を使うたびに、R_386_RELATIVE な再配置と R_386_GLOB_DAT な再配置が一つづつ増加します。前者がポインタの指し先の分で、後者がクリリンのぶんというかGOTの分、であってますかね。
  • static const char* const x = ""; だと、R_386_RELATIVE が1つ増えるのみです。
  • const char x[] = ""; だと、R_386_GLOB_DAT が1つ増えるのみです。
  • static const char x[] = ""; だと、ロード時の再配置は起きないようです。

全体的に、配列方式のほうが得ですね。


(2) PIE

  • const char* const x = ""; を使うたびに、R_386_RELATIVE が一つづつ増加します。PICの時よりもマシになっています。
  • static const char* const x = ""; はstaticなしと同じです。R_386_RELATIVE が1つ。
  • const char x[] = ""; だと、ロード時の再配置は起きないようです。
  • static const char x[] = ""; でも、(もちろん)ロード時の再配置は起きないようです。

...というわけで、PIEでも配列の勝ちです。

比較4) 空間コスト


ポインタ方式は.dataセクションのポインタ一個分損なんじゃなかろうか。たぶん(やる気がなくなってきた)。

ベンチマーク


ポインタ型/配列型、それぞれのグローバル変数を数百万個含むDSOを作ってみて、

import List
main = mapM_ (\x -> putStrLn $ "const char* const "++x++"=\""++x++"\";") $ take 10000000 $ perm ['a'..'k']
  where perm [] = [[]]
        perm xs = concat [map (x:) $ perm (delete x xs) | x <- xs]

起動時間をくらべてみよ...うと思ったらgccの4.1.1がICEでコケてコンパイルできなかったのでやめ。

まとめ


というわけで、何がいいたいのかよくわからないエントリですが、わたしもわかりません。


とりあえず、可能ならstaticなり __attribute__((visibility("hidden"))) なりは付与するとして、

char* p = "hello";             //   2点
const char* p = "hello";       //  15点: 最適化がろくに効かないしちょっとなー。
                               //         ...っていうかそのポインタ、ほんとに指し変えるの? 
#define HELLO "hello"
                               //  40点: まぁ、それもアリかもわからんね。-fmerge-all-constants すると吉
const char* const p = "hello"; //  50点: タイプ量が多い割に PIC/PIE だと下のに負ける
const char p[] = "hello";      //  70点: イイ!

ってな感じでどうでしょう?


もちろん100点はRubyをつか(ry

*1:PLTにジャンプしない、とかか?

*2:GOTについては http://d.hatena.ne.jp/yupo5656/20060618/p1 を。変数の場合は、関数と違ってPLTを経由せずに直接参照されます

*3:DSO外に公開されていないシンボル、たとえば一番目のstaticなグローバル変数、はそうでなくてかまいません。だからGOTを経由しませんでした

*4:アドレスを計算してそのまま格納する命令

ASLR, setarch -RL, prelink, PIE and LD_USE_LOAD_BIAS

独り言です。(慣れないkernelの)コードをざーっと調べて、あまりまじめには検証しないで書いてます。FC5限定なので細かい記述の賞味期間はあと30日くらいでしょうか。

ASLR


私の使っているFC5のkernel/glibcには、一般にASLR(address space layout randomization)とか呼ばれている、日々のhackの邪魔になるセキュリティ的に大変よろしい機能があります。ASLRをフルに機能させると、プロセスの動作するアドレス空間において

  1. DSOをどこに貼り付けるか (glibcの elf/dl-load.c の _dl_map_object_from_fd())
  2. ELFのinterp (/lib/ld.so) をどこに貼り付けるか (kernel の fs/binfmt_elf.c の load_elf_interp())
  3. 実行ファイルをどこに貼り付けるか (fs/binfmt_elf.c の load_elf_binary())
  4. heapをどのへんから開始するか (fs/binfmt_elf.c の load_elf_binary())
  5. PROT_EXECではないmmapで返却するアドレスをどのへんにするか (arch/i386/mm/mmap.c の mmap_base())
  6. stackをどのへんから開始するか (fs/binfmt_elf.c の load_elf_binary())
  7. vDSOをどこを置くか (arch/i386/kernel/sysenter.c の arch_setup_additional_pages() あたり。細かいので以下では追わない)

くらいがランダムになります。カッコ内はrandomizationのおよその実装位置。ASLRが効いていると、プロセスへの攻撃が成功しにくいとかいろいろ嬉しいことがあるそーです。

FC5のこまった現象


で、このrandomize機能は /proc/sys/kernel/randomize_va_space を0にすれば完全にoffになると思っていたのですが、どうもそうはならない模様。。。stackやheapのアドレスは無事固定されるのですが*1、上記1-7のうちのいくつかのアドレスは依然randomizeされてしまうのですよ。たとえばまず、prelinkを完全に切った状態で、randomize_va_space を0にし、non-PIEなバイナリを実行すると、バイナリがリンクしているDSOのロードされるアドレスが毎回変化しちゃうんですよね。更にわるいことに、PIEなバイナリだと、DSOのアドレスのみならず、バイナリ自身のアドレス、dynamic linker*2のアドレスまでもが毎回変化しちゃいます。えーー。いろいろ遊ぶ上でいろいろ不都合が。


バグなのか仕様なのかはよくわかりませんが、どうなってるのか見てみました。ASLRがどのように実装されているか、思えばちゃんと見ていなかったので良い機会かなというのもあり。以下で参照しているkernelは、rpmbuild --target i686 -bp SPECS/kernel-2.6.spec にて展開およびpatchされた 2.6.17-1.2174_FC5 のものです。

stack開始位置 -1-


stack開始位置をランダムにしているのは、まず、linux-2.6.x/fs/binfmt_elf.c の randomize_stack_top()のようです。普通の開始位置(ユーザ/カーネルの3G/1Gの境界位置である0xbfffffffあたり)から0x0方向に最大8MB(ただしページ単位, 最大2048ページ)だけずれる模様。これが行われるのは、current->flags に PF_RANDOMIZE が含まれるときだけです。


このflagを立てているのは、同じく fs/binfmt_elf.c の load_elf_binary() の

        if ( !(current->personality & ADDR_NO_RANDOMIZE) && randomize_va_space)
                current->flags |= PF_RANDOMIZE;

っぽいです。ですから、

  • /proc/sys/kernel/randomize_va_space を 0 にしOS全体のランダム化をoffにするか
  • 親プロセスがpersonality(2)でADDR_NO_RANDOMIZEをsetしてから当該プロセスをexec

すれば、スタック開始位置のランダム化は無効になります。後者については、setarch(8)という便利コマンドがあるんで、自分でfork&exec処理をかかなくても、

% /usr/bin/setarch i686 -R cat /proc/self/maps

とするだけで(per-process basisで)ランダム化を切った状態でコマンド(ここではcat)を実行できます。setarch(8)はマイナーなコマンドっぽいので、紹介しておきます。そういえば、ASLRがonだとうまく動かないプログラムがあるそうですが(emacsの一部機能とか)、randomize_va_spaceで対処するのではなく、setarchで対処するほうがスマートかもしれませんね。

stack開始位置 -2-


そのあと、fs/exec.c の setup_arg_pages() で、 arch/i386/kernel/process.c の arch_align_stack() を呼ぶことでスタック開始位置を0-8192バイトの範囲(ただし16bytes単位)でさらにランダム化しています。

参考: http://kernel.org/pub/linux/kernel/people/arjan/randomize/08-stack-top-randomize

        stack_base = arch_align_stack(stack_top - MAX_ARG_PAGES*PAGE_SIZE);
        stack_base = PAGE_ALIGN(stack_base);

2度に分けている理由はここ。でも、直後でPAGE_ALIGNしてしまっているんで、意味があるんだかないんだかよくわかんない。あと、arch_align_stack() はrandomize_va_space だけで ADDR_NO_RANDOMIZE は見てない。うーん。ちなみに、ARG_PAGESというのはプログラムへの引数などを格納する場所で、デフォルトで32ページ、128kbytes。

heap開始位置


stackと同じく、load_elf_binary()から呼ばれている arch/i386/kernel/process.c の randomize_brk() でランダム化されているのかな? たぶん。振れ幅は32MB(0x02000000)らしい。

#ifdef __HAVE_ARCH_RANDOMIZE_BRK
        if (current->flags & PF_RANDOMIZE)
                randomize_brk(elf_brk);
#endif

実行ファイルの位置


fs/binfmt_elf.c の load_elf_binary() の、このへん:

                vaddr = elf_ppnt->p_vaddr;
                if (loc->elf_ex.e_type == ET_EXEC || load_addr_set)
                        elf_flags |= MAP_FIXED;
                else if (loc->elf_ex.e_type == ET_DYN)
#ifdef __i386__
                        load_bias = 0;
#else
                        load_bias = ELF_PAGESTART(ELF_ET_DYN_BASE - vaddr);
#endif

                error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt, elf_prot, elf_flags, 0);

ELFヘッダ(eu-readelf -h で確認可)の種別がET_EXECなら(=普通の実行ファイルなら)、MAP_FIXEDでプログラムヘッダのVirtAddrの位置(eu-readelf -l で出てくるPT_LOADのVirtAddrのこと)にマップ。


ET_DYNなら(=PIEな実行ファイルなら)、

  1. PIEがprelinkされていたら、なるべくそのアドレスにマップされる
  2. されていなければ(VirtAddrが0x0なら)、ランダムな位置にマップされる

のかな。後者の場合は、elf_map()から、include/linux/mm.h のdo_mmap() と、mm/mmap.c の do_mmap_pgoff() を経由して、同ファイルの arch_get_unmapped_exec_area() に到達して、

#define SHLIB_BASE             0x00111000
...
        if (!addr && !(flags & MAP_FIXED))
                addr = randomize_range(SHLIB_BASE, 0x01000000, len);

のような感じで drivers/char/random.c の randomize_range() で得られた適当なアドレスにマップされる感じです。このとき、/proc/sys/kernel/randomize_va_space なり、PF_RANDOMIZE なりはチェックされていない模様。実際、randmmize_va_spaceを0にしても、PIEなバイナリは毎回違ったアドレスにマップされてしまいます...(しょぼーん)。バグなのかなんなのか。

interpの位置


load_elf_interp() は、load_elf_binary() から呼ばれています。interpの位置決めはこのへん:

            vaddr = eppnt->p_vaddr;
            if (interp_elf_ex->e_type == ET_EXEC || load_addr_set)
                elf_type |= MAP_FIXED;
            else if (no_base && interp_elf_ex->e_type == ET_DYN)
                load_addr = -vaddr;

            map_addr = elf_map(interpreter, load_addr + vaddr, eppnt, elf_prot, elf_type, total_size);

PT_INTERPに変態チックなことを書いていない限り(普通に /lib/ld.so を使っている限り)、e_typeはET_DYNです。で、no_baseがtrueかどうかで5行目に到達するかが決まるんですが、どうも、実行ファイルがPIEの場合のみ、no_baseがtrueになるっぽいですね。load_elf_binary()のこのへんによります:

                        if (loc->elf_ex.e_type == ET_DYN) {
                                load_bias += error -
                                             ELF_PAGESTART(load_bias + vaddr);

というわけで、

  • PIEだとinterpもelf_map(0x0);されることになり*3、そのアドレスがランダムに決まることになります
  • interpがprelinkされていたら、そのアドレスになることが多いです
  • interpがprelinkされてなかったら、PIEかどうかにかかわらず、やっぱりランダムに決まることになります

ランダムに決まるケース2つについて、/proc/sys/kernel/randomize_va_space なり、PF_RANDOMIZE なりはやっぱり参照されてませんね。ショボーン2。

DSOの位置


DSOは、dynamic linkerがマップします。次の1-4の順で条件をみていき、さいしょにひっかかったところの方法でマップされるようです。

  1. 環境変数LD_USE_LOAD_BIASが1で、prelinkされていたら、そのアドレス
  2. 実行ファイルがPIEなら、ランダム
  3. DSOがprelinkされていたら、そのアドレス
  4. ランダム

DSOのマップはカーネルではなくて、dynamic linker (interp, ld.so) がやるわけなので、LD_USE_LOAD_BIASなる環境変数でちょっとだけ操作可能になってます。ランダムって書いてあるところは、実際には非MAP_FIXEDのPROT_EXECでmmap(0x0)する感じなんですが、結局行き着く先はカーネルのarch_get_unmapped_exec_area() なので、例によって randomize_va_space なり、PF_RANDOMIZE なりは参照されずにランダムになっちゃいます。ショボーン3。


prelinkなPIEの場合は...知りません。もうちょいglibcのソースを追わないと。ここに書いてあるような気もしますが、確認していません。

PROT_EXECではないmmap位置


PF_RANDOMIZEな場合だけ、arch/i386/mm/mmap.c のmmap_base()で、get_random_int() % (1024*1024) しています。

        if (current->flags & PF_RANDOMIZE)
                random_factor = get_random_int() % (1024*1024);
...
        return PAGE_ALIGN(TASK_SIZE - gap - random_factor);

この値は mm->mmap_base として覚えられ、PROT_EXECではないmmapの戻りを計算する際に使われます (mm/mmap.c参照)。

まとめ (ASLR万歳派向け)


ASLRが大好きな皆様は、randomize_va_spaceを1にしておくのは当然として、次の(1)(2)を行うとよいと思います。これはFC5固有の挙動に依存した話ではないので、暫くは一般的に使える話だと思います。


(1) ネットワークデーモンや、suidプログラムは、かならずPIEとして作成しましょう


がお薦めです。これでリスクの高いプロセスについては、fully randomized確定です。PIEかどうかが、DSOを常にrandomizeするかのスイッチになる感じです。DSOとinterpのprelinkは無視されます。Fedoraなんかだと、たとえばhttpdなんかは既にPIEとしてコンパイルされるようになっていますね。しかし、もしなんでもかんでもPIEにするのが難しいのであれば、


(2) prelinkをdisableしちゃいましょう


Fedoraなら、vi /etc/sysconfig/prelink で PRELINKING=no にして、一晩待てばOK。cron様がどうにかしてくれます。実行ファイルのアドレス以外は全てランダムになります。えー、prelinkにも実はrandomize機能(-R)がありまして、一応、他人の箱とは違ったアドレスにDSOがマップされるようにはなっていますし、Exec-Shieldがインスコされている箱の場合は、なるべくASCII-Armor領域*4にDSOをマップするようになってます。しかしながら、local exploitも視野にいれるならprelinkはdisableするほうが確実です。

まとめ (ASLR逝ってよし派向け 1)


ASLRが日々のhackの邪魔でしょうがないという皆様は、私も自宅マシンについてはそうだったりしますけど、うーん、、、


気持ち悪いworkaroundとしては、まず、/proc/sys/kernel/randomize_va_space を 0 にするだけでなく/proc/sys/vm/legacy_va_layout も 1 にしておくという手があります。これを行うと、仮想アドレス空間の使われ方がkernel2.4のころ(2.6.7まで?)と同じになり、randomizeもされなくなるようです(legacy_va_layout については、コードは見てません。レイアウトについては、LWNのReorganizing the address spaceという記事が図入りでgoodです。右上のレイアウトのpngが以前のレイアウト。左下のあたらしいほうはflexible-mmapと呼ばれているのかな??)。あるいは、setarch i686 -R -L でバイナリを実行するのでも(ほぼ、カーネルのpersonalityのハンドリングがバグってなければ)同じことです。

参考: http://gcc.gnu.org/wiki/Randomization


これで、一応、ランダムな部分はなくなります。

まとめ (ASLR逝ってよし派向け 2)


でもlegacy_va_layoutは気持ち悪い。DSOが妙なアドレスから張り付くのはキモすぎ、legacyという言葉が気に食わない、健全かつ完全にrandomizeをoffにしたい、というなら、確実なのは kernel のmm/mmap.cの下記部分をコメントにしてしまうことだと思います。確認してませんが...。

        if (!addr && !(flags & MAP_FIXED))
                addr = randomize_range(SHLIB_BASE, 0x01000000, len);

この状態で randomize_va_space を0にすればOKで、多分完全にランダム性はなくなります*5。kernelいじるのがめんどくさければ、

  • interpもDSOも全部prelinkして、
  • randomize_va_space を0にして
  • PIE使わない

ですかねえ。


なお、万歳と逝ってよしの中間は難しそう。「妙なコードを書いているときだけrandomizeを無効にしたいけど、普段はprelinkは切っておきたい」とか。kernelを好みに応じて調整するしかなさそうですね。上記のコードのifでPF_RANDOMIZEを見るとか。...やっぱり setarch -RL かなぁ。

(おまけ) 2.6.17-1.2174_FC5 で (PIE + no-randomize) だとheapの位置決めがおかしい?


heap上にコードを置いて実行するプログラムを作って、PIEにしておきます。
短くて便利なのでshinhさんの195ネタでいきます。

% cat heap_exec.c
#include <stdio.h>
#include <stdlib.h>
int main() {
  unsigned char* p = malloc(1);
  *p = 195;
  printf("heap = %p\n", p);
  ((void (*)())p)();
  return 0;
}

__attribute__((constructor)) void dump_maps() {
  FILE* fp;
  int c;
  fp = fopen("/proc/self/maps", "rb");
  while ((c = fgetc(fp)) != EOF) {
    putchar(c);
  }
  fclose(fp);
}

% gcc -fPIE -pie heap_exec.c

で、/proc/sys/kernel/exec_shield はon、randomize_va_spaceはoffで実行すると、通常heapには実行権限がないので

./a.out
004dc000-004dd000 r-xp 004dc000 00:00 0          [vdso]
004dd000-004f6000 r-xp 00000000 fd:00 1399431    /lib/ld-2.4.so
004f6000-004f7000 r-xp 00018000 fd:00 1399431    /lib/ld-2.4.so
004f7000-004f8000 rwxp 00019000 fd:00 1399431    /lib/ld-2.4.so
00881000-009ae000 r-xp 00000000 fd:00 1399445    /lib/libc-2.4.so
009ae000-009b0000 r-xp 0012d000 fd:00 1399445    /lib/libc-2.4.so
009b0000-009b1000 rwxp 0012f000 fd:00 1399445    /lib/libc-2.4.so
009b1000-009b4000 rwxp 009b1000 00:00 0
00fa1000-00fa2000 r-xp 00000000 fd:00 1530108    /home/yupo/a.out
00fa2000-00fa3000 rw-p 00000000 fd:00 1530108    /home/yupo/a.out
00fa3000-00fc4000 rw-p 00fa3000 00:00 0          [heap]
b7fef000-b7ff0000 rw-p b7fef000 00:00 0
b7ffe000-b8000000 rw-p b7ffe000 00:00 0
bffea000-bffff000 rw-p bffea000 00:00 0          [stack]
heap = 0xfa3008
Segmentation fault

こんな感じでsegvるわけなんですけど、3回に2回くらい、こうなります。

% ./a.out
009d1000-00afe000 r-xp 00000000 fd:00 1399445    /lib/libc-2.4.so
00afe000-00b00000 r-xp 0012d000 fd:00 1399445    /lib/libc-2.4.so
00b00000-00b01000 rwxp 0012f000 fd:00 1399445    /lib/libc-2.4.so
00b01000-00b04000 rwxp 00b01000 00:00 0
00ccb000-00ccc000 r-xp 00000000 fd:00 1530108    /home/yupo/a.out
00ccc000-00ccd000 rwxp 00000000 fd:00 1530108    /home/yupo/a.out
00ccd000-00cee000 rwxp 00ccd000 00:00 0          [heap]
00db4000-00db5000 r-xp 00db4000 00:00 0          [vdso]
00db5000-00dce000 r-xp 00000000 fd:00 1399431    /lib/ld-2.4.so
00dce000-00dcf000 r-xp 00018000 fd:00 1399431    /lib/ld-2.4.so
00dcf000-00dd0000 rwxp 00019000 fd:00 1399431    /lib/ld-2.4.so
b7fef000-b7ff0000 rw-p b7fef000 00:00 0
b7ffe000-b8000000 rw-p b7ffe000 00:00 0
bffea000-bffff000 rw-p bffea000 00:00 0          [stack]
heap = 0xccd008
%

[heap]が[vdso]とひっついていて、ろくにメモリ確保できないし、そもそも何故か[heap]がrwxだしで、まずい。
実際、 p=195 が実行できちゃってます。なんなんでしょうこれ。DSOとinterpはprelinkされていてもされていなくても再現しました。調べ中。

結論/総まとめ


毛嫌いしないでカーネルをたまに眺めてみるのも楽しい。

*1:実際にはFC5ではheapは固定されないことがあった、バグっぽい、後述

*2:以下interpと呼んだりもします。平たくいうと/lib/ld.soのことです

*3:load_addr + vaddr == 0x0になるから

*4:論理アドレス空間の先頭16MB @i386

*5:FC5では念のためPIEも使わない方が良いかも。heapの位置がおかしくなることがあった、後述

(補足)

最初のいちばんオーソドックスな奴、一応解説としてbacktraceを貼っておきます。

% gdb ./a.out
(gdb) b x if main>0
Breakpoint 1 at 0x80483ba: file iyara.c, line 1.
(gdb) r
Breakpoint 1, x () at iyara.c:1
1       main;__attribute((constructor,destructor))x(){main?puts("world!"):exit(main=puts("hello"));}
(gdb) bt
#0  x () at iyara.c:1
#1  0x0804837b in __do_global_dtors_aux ()
#2  0x08048494 in _fini ()
#3  0x00b5e0fd in _dl_fini () from /lib/ld-linux.so.2
#4  0x00b9798e in exit () from /lib/libc.so.6
#5  0x080483ef in x () at iyara.c:1
#6  0x08048465 in __do_global_ctors_aux ()
#7  0x080482b9 in _init ()
#8  0x08048416 in __libc_csu_init ()
#9  0x00b826c9 in __libc_start_main () from /lib/libc.so.6
#10 0x08048331 in _start ()

2度めのx()の呼び出しでのバックトレースです。これでようやく、先日のPTR_MANGLEのエントリと話がつながりました(_dl_fini)。いやそんなはずがない。

mainを一度も呼ばないばかりか蹂躙する

shinhさんの「ふとイヤなコードを思いつきました」インスパイヤされてみました。

% cat iyana.c
#include <stdio.h>
#include <stdlib.h>

int main;
__attribute__((constructor, destructor))
static void x() {
  if (main) puts("world!");
  else exit(main = puts("hello"));
}

% gcc -Wall iyana.c
iyana.c:4: warning: ‘main’ is usually a function

% ./a.out
hello
world!

意味はありません。っていうかこの警告ははじめて見たわ。教えてくれなくても存じていましてよ。

(追記) shinhさんの8/29の日記にさらに凄いのが。


トラックバックしてくれているのでそちらをクリック。素敵。特に2番目の、動的に生成したコードが自分自身を参照する様に感動しました。
えーと、1番目の数字でほげほげするのと、3番目のセクション移動を真似してみます。

__attribute((section(".text"))) main = 2425393296;
_() { __attribute((constructor)) _() { puts("hello"); } puts("world!"); }

やっぱり、常識としてmainは.textに置くべきだよなーっと(棒読み)。全ての環境で動くかどうかは知りません。わたしはFC5(x86_64)でgcc -m32。

(追記2)あんまりなコードなので解説します


上記をもうすこし読めるように書き直すと、次のようになります。関数がネストしていることに意味はないので(なるべく少ない行数でなんとかしたかったのでネストしただけ)、外に出しました。なお、このように書き直すと移植性(?)も微妙に向上します。

__attribute((section(".text"))) int main = 0x90909090;
void f1() { puts("world!"); }
__attribute((constructor)) void f2() { puts("hello"); }

0x90はx86の命令でNOPです。なので、0x90909090は NOP 4つと同じです。これを覚えておいて、ソースをコンパイル・リンク・逆アセンブルするとこうなります。

% objdump -d ./a.out | grep -A20 '<main>'
08048384 <main>:
 8048384:       90 90 90 90                                         ....

08048388 <f1>:
 8048388:       55                      push   %ebp
 8048389:       89 e5                   mov    %esp,%ebp
 804838b:       83 ec 08                sub    $0x8,%esp
 804838e:       c7 04 24 64 84 04 08    movl   $0x8048464,(%esp)
 8048395:       e8 0e ff ff ff          call   80482a8 <puts@plt>
 804839a:       c9                      leave
 804839b:       c3                      ret

0804839c <f2>:
 804839c:       55                      push   %ebp
 804839d:       89 e5                   mov    %esp,%ebp
 804839f:       83 ec 08                sub    $0x8,%esp
 80483a2:       c7 04 24 6b 84 04 08    movl   $0x804846b,(%esp)
 80483a9:       e8 fa fe ff ff          call   80482a8 <puts@plt>
 80483ae:       c9                      leave
 80483af:       c3                      ret

f2()がコンストラクタ指定されているのでmain()より前に呼ばれ、"hello" が出ます。次にmain()が普通に呼ばれますが、この関数(?)にはnopしかないので、華麗にfall throughしてf1()に突入します。で、"world!" が出て、f1()の終わりのretがmain()のretとみなされます。変数mainを.textセクションにもってきたのは、ELF上でmain変数をf1関数のすぐ上に位置させるためです。というわけで、コンパイラやリンカの気分次第で全く動作しなくなります。以上。


というかですね、

x86では__attribute__((naked))はサポートされていない、そんなふうに考えていた時期が俺にもありました

ということですよ(いや実際サポートされてないんだけど)。今回、リターンしないmainを書いて (いや main = 195; を見て) 目が覚めました。そうか、全部数字で書けばnakedじゃん、と。一見当然そうなことだけど、こういうのを気づきというんだな(違う)。


どう見ても堕落したCプログラマのレベル-9です。本(ry




(追記) objdump -d じゃなくて objdump -D のほうがよいですね。-Dなら、90 90 90 90 のところもちゃんと逆アセンブルしてくれます。-d だと、(.textの中であっても)何らかの方法でコードとデータを見分けようと頑張ってしまう模様。

最近のglibcではatexit関数やjmp_bufを狙った攻撃は効かない (PTR_MANGLE)


小ネタ。MSの中の人のblogをなんとなく眺めていたら、

という記事がありました。要約すると、「長生きするポインタ(特に関数ポインタ)は悪用されやすいので、値を素のまま格納しないほうがよい」という話です。長生きなポインタというのは、

  • atexit関数で登録された関数へのポインタ (プロセス終了時まで保持される)
  • heapの管理情報内のポインタ
  • jmp_buf内のポインタ (longjmpする可能性のある長い処理の間保持される)

などです。こういうポインタは、ソフトウェアの脆弱性(メモリ空間内の任意の4バイトを書き換え可能な脆弱性)を悪用して上書きされる対象になりやすく、上書きが即、攻撃成功につながります。たとえば、atexit関数で登録された関数ポインタを「シェルを起動する関数のポインタ」にすり替えておけば、プロセス終了時にシェルが起動してしまうことでしょう*1


いつだかに書いた、Linux.dtors overwrite 攻撃なんかだと、セクションをまるごとreadonlyにする(RELRO)ことで書き換え攻撃から身を守ることができるわけですが、atexit関数のポインタ覚え先などは、readonlyにするわけにもいきませんから厄介です。で、MSとしては「ならポインタの値を予測不可能な値でエンコード(XOR)してしまえばいいんじゃね?」という結論になったみたいです。Vistaからは、多くの長生きポインタがエンコードされて格納されます。


最近のバイナリ系攻撃(って何?)に対する防御技術は、WindowsでもUNIXでもおおよそ次の5つくらいに分類されるような気がしますが(括弧内はFedoraでの例、MACとかLeast Priv.とか署名はbinaryっぽくないので(?)略)、

  1. アドレスを事前に決めない (ASLR+DSO, ASLR+PIE)
  2. 書き込みを禁止する (RELRO)
  3. 実行を禁止する (NX)
  4. 予測不可能な値でのマーキング/エンコード (SSP)
  5. 標準関数から危険なfeatureを取り去る (FORTIFY_SOURCE)

今回のポインタの件では 4. の方法をとったということですね。

glibcでどうなってるか見てみた


我らがglibcで、長寿命ポインタの扱いがどうなっているか見てみました。FC5(x86)です。まずはatexit()のソースコードから。atexit()の実体は__cxa_atexit()なんで、そちらを見ます。

glibc-20060306T1239% lv stdlib/cxa_atexit.c
  ...(略)...
int
__cxa_atexit (void (*func) (void *), void *arg, void *d)
{
  struct exit_function *new = __new_exitfn ();

  if (new == NULL)
    return -1;

#ifdef PTR_MANGLE
  PTR_MANGLE (func);
#endif
  ...(略)...

グ・・グレート。PTR_MANGLEというマクロで、きっちりエンコードされています。atexit()の他、on_exit()*2、setjmp系関数、iconv系関数でもPTR_MANGLEが使われている模様。2005年の12月頃に入った変更のようですね。PTR_MANGLE は、sysdeps/unix/sysv/linux/i386/sysdep.h で定義されています。簡単に書くと、

% objdump -d /lib/libc-2.4.so | grep -A 20 __cxa_atexit
...
0012bb2b <__cxa_atexit>:
...
  12bb4a:       85 d2                   test   %edx,%edx
  12bb4c:       74 1e                   je     12bb6c <__cxa_atexit+0x41>
  12bb4e:       65 33 35 18 00 00 00    xor    %gs:0x18,%esi                <--- これ
...

PTR_MANGLE は(i386では)素のポインタの値と %gs:0x18 の値をXORするマクロです。もう一度PTR_MANGLEすれば元に戻ります。%gs:0x18 の値は、elf/rtld.c のdl_main()で決められます。ダイナミックリンカの冒頭部分ですね。

  /* Set up the stack checker's canary.  */
  uintptr_t stack_chk_guard = _dl_setup_stack_chk_guard ();
  THREAD_SET_STACK_GUARD (stack_chk_guard);

  /* Set up the pointer guard as well, if necessary.  */
  if (GLRO(dl_pointer_guard))
    {
      // XXX If it is cheap, we should use a separate value.
      uintptr_t pointer_chk_guard = stack_chk_guard;
      THREAD_SET_POINTER_GUARD (pointer_chk_guard);

一部略記していますが、だいたいこんな感じ。SSP(ProPolice)用のカナリアの値と同じ値が使われるのですね(うーん...大丈夫なのかな)。_dl_setup_stack_chk_guard()は、シンプルに /dev/urandom からポインタのバイト数だけ値を拾ってくる関数です。


また、LD_POINTER_GUARD なる環境変数も新設されているようです。env LD_POINTER_GUARD=0 ./hoge とすると、このセキュリティ強化機構を無効にできる模様。ガードの値が0x0になります。

動作を一応確認

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

uint32_t get_pointer_guard() {
  uint32_t r;
  asm("movl %%gs:0x18, %%eax" : "=a"(r)); //行儀よく
  return r;
}

void foo() {}
int main() {
  atexit(foo);
  return 0;
}

のようなコードを用意して、これをgdb上で実行します。foo関数の登録までステップ実行した後、__exit_funcs変数(atexit関数がポインタを登録する先)をダンプしてみます。

(gdb) x/12x __exit_funcs
0x230c80 <initial>:     0x00000000      0x00000002      0x00000004      0xa764ec0b
0x230c90 <initial+16>:  0x00000000      0x00000000      0x00000004      0xaf265083
0x230ca0 <initial+32>:  0x00000000      0x00000000      0x00000000      0x00000000

0xa764ec0b と 0xaf265083 の2つのポインタ(エンコード済)が登録されているようです。前者は、glibcが勝手に登録する関数のアドレスです。一方、ガードの値はというと、

// gdbで直接 %gs:0x18 の値を表示する方法がわからなかった... orz

(gdb) call/x get_pointer_guard()
$2 = 0xa722d33b

のようです。この値と、登録されているポインタの値をXORして(デコード)してみると、

(gdb) p/x 0xa764ec0b ^ $2
$3 = 0x463f30
(gdb) x/4i $3
0x463f30 <_dl_fini>:    push   %ebp
0x463f31 <_dl_fini+1>:  mov    %esp,%ebp
0x463f33 <_dl_fini+3>:  push   %edi
0x463f34 <_dl_fini+4>:  push   %esi

(gdb) p/x 0xaf265083 ^ $2
$4 = 0x80483b8
(gdb) x/4i $4
0x80483b8 <foo>:        push   %ebp
0x80483b9 <foo+1>:      mov    %esp,%ebp
0x80483bb <foo+3>:      pop    %ebp
0x80483bc <foo+4>:      ret

見事に _dl_fini と foo のアドレスになりました。

というわけで (まとめ)


世間に出回っている、単純なatexit関数の悪用法は、最近のglibcには効きません。

コードを書くときのプラクティス


自分でコードを書くときも、関数ポインタへの値の格納はエンコードしたほうがよいかもわからんですね。static(静的記憶域期間)なポインタの場合は特に。Windowsだと、XP SP2以降で

ってAPIが使えるようです。Linuxで似たようなことがしたいなら、

// x86用
void* encode_pointer(void* p) {
  __asm__("xorl %%gs:0x18, %0" : "=r"(p): "0"(p));
  return p;
}
extern __typeof(encode_pointer) decode_pointer __attribute__((__alias__("encode_pointer")));

とかですかね。エンコードとデコードは同じ処理になります。最終行で、簡単なことを難しく実現しようとしているように見えるのは気のせいです。


(補足) 0x18というのは、glibcが内部で使ってる構造体の先頭からのオフセット値なんで、将来変化するかも。offsetofでほげるほうがいいです。あと、攻撃者も便利に使える関数を作ってしまうのはよくないかも? どうなんだろ。

全然関係ないんですがFC5でのLD_ASSUME_KERNELについて


上の方でlibcを逆アセンブルしていてふと。


FC5から、LD_ASSUME_KERNELによるlibc切り替えができなくなっていますね。FC4までだと確か、libcのsoは三種類入っていて、それぞれ

  • /lib のものは .note.ABI-tag section の OS ABI version が 2.2.5 (LinuxThreads)
  • /lib/i686 のものは 2.4.1 (LinuxThreads)
  • /lib/tls のものは 2.4.20 (NPTL)

でした(デフォルトで使われるのは/lib/tlsのもの)。FC5では、i686/ と tls/ はカラになって、

% eu-readelf -n /lib64/libc-2.4.so

Note segment of 32 bytes at offset 0x270:
  Owner          Data size  Type
  GNU                   16  VERSION
    OS: Linux, ABI: 2.6.9

% readelf -x .note.ABI-tag /lib64/libc-2.4.so

セクション '.note.ABI-tag' の 16 進数ダンプ:
  0x3419c00270 00554e47 00000001 00000010 00000004 ............GNU.
  0x3419c00280 00000009 00000006 00000002 00000000 ................

/lib直下のlibc.soのOS ABIバージョンが2.6.9になりました(上はx86_64ですが、x86でも同じです)。FC5以降では、export LD_ASSUME_KERNEL=2.2.5 しているシェルスクリプトなんかは一切動かなくなるので要注意。


# なるほど、RHEL5でLinuxThreadsのサポートが切られる予定だから、その布石か。

*1:NX(Exec-Shield)が効いていない環境ならさらに話は簡単で、送り込んだshellcodeに制御を移すことで任意コード実行ができます

*2:SunOS4由来のatexit()もどき。非標準関数