MinGW+GCCクロスコンパイル環境でOllyDbg Plugin開発

妙に遠回りしている感じもしますが、以下のような理由による物です。

  • メインの開発環境がLinux
  • 極力GUI依存しないTerminal経由で開発したい
  • 今更Borland C++やVisual C++の古いバージョンを入れたくない

こんな環境でやる人はまずいないと思いますが、手順としてCygwin+MinGW+GCCにも流用できるのと
纏めてるところが見あたらなかったのでメモとして記載。
Linux上でクロスコンパイルしているため、各コマンドにi686-pc-mingw32-等のプレフィックス
付いていますが、Cygwin+MinGW+GCCでやる場合はその部分を削ればそのまま動くと思います。


用意する物は以下


gccでは.lib形式でリンクできないので.a形式のファイルを生成する必要があります。
.lib形式から変換する手順もあるようですが、うまくいかなかったので.defファイルから
生成します。対象の関数の名前と呼び出し規約だけ分かっていれば良いので、生成に元のバイナリは必要ありません。

# dlltoolが処理できない不要な宣言削除、 関数名の_を外す
$ sed -e 's/^CODE.*//' -e 's/^EXETYPE.*//' -e 's/^DATA.*//' -e '/^$/d' -e 's/_//' Ollydbg.def > Ollydbg.def.mingw
# gccで扱える形式に変換
$ i686-pc-mingw32-dlltool -U --dllname ollydbg.exe --input-def Ollydbg.def.mingw --output-lib ollydbg.a


PDK付属のPlugin.hもGCCを想定して書かれていないので書き換える必要があります。
コールバック関数の辻褄あわせと、windows.hあたりで定義されている_exportの上書きが必要です。

  // If you like Microsoft compiler, this will force byte alignment and verify
  // that character is set to unsigned.
  #ifdef _MSC_VER
  ...
  #endif
+ // for MinGW gcc
+ #ifdef __GNUC__
+   #pragma pack(1)
+   #ifndef cdecl
+     #define cdecl __cdecl
+   #endif
+   #undef _export
+   #define ODBG_Plugindata      _ODBG_Plugindata
+   #define ODBG_Plugininit      _ODBG_Plugininit
+   #define ODBG_Pluginmainloop  _ODBG_Pluginmainloop
+   #define ODBG_Pluginsaveudd   _ODBG_Pluginsaveudd
+   #define ODBG_Pluginuddrecord _ODBG_Pluginuddrecord
+   #define ODBG_Pluginmenu      _ODBG_Pluginmenu
+   #define ODBG_Pluginaction    _ODBG_Pluginaction
+   #define ODBG_Pluginshortcut  _ODBG_Pluginshortcut
+   #define ODBG_Pluginreset     _ODBG_Pluginreset
+   #define ODBG_Pluginclose     _ODBG_Pluginclose
+   #define ODBG_Plugindestroy   _ODBG_Plugindestroy
+   #define ODBG_Paused          _ODBG_Paused
+   #define ODBG_Pausedex        _ODBG_Pausedex
+   #define ODBG_Plugincmd       _ODBG_Plugincmd
+ #endif


サンプルで付いているBookmark.c等の場合は必要ありませんが、ダイアログなどを使う場合は
リソースも含める必要があります。ResEdit等を使用して.rcファイルとresource.hを生成します。
今回はリソースを必要としないので詳細は割愛。
生成した.rcファイルは以下の通りコンパイルします。
windresのエラーは分かりにくいことこの上ないですが、生成した.rcファイルで起きたエラーは
.rcファイルでのインクルードが足りてない事が原因の場合が多いです。(windows.h等)

# テキストのrc形式からバイナリのres形式に変換
$ i686-pc-mingw32-windres --input-format=rc --output-format=coff -o Foo.res Foo.rc


メインのソースコードコンパイルします。-funsigned-char はOllyDbg Pluginビルドする際の
おまじないみたいな物です。Cygwin環境の場合は -mno-cygwin も付けた方が良いでしょう。
サンプルではDllEntryPointを使用するようになっていますが、MinGW+GCCの場合はこの
シンボルは定義していても参照してくれないので無視されます。
代わりにDllMainで定義すれば問題無く動きます。

- BOOL WINAPI DllEntryPoint(HINSTANCE hi,DWORD reason,LPVOID reserved) {
+ BOOL WINAPI DllMain(HINSTANCE hi,DWORD reason,LPVOID reserved) {
    if (reason==DLL_PROCESS_ATTACH)
      hinst=hi;                          // Mark plugin instance
    return 1;                            // Report success
  };
# ソースコードをオブジェクトファイルにコンパイル
$ i686-pc-mingw32-gcc -g -W -Wall -Wno-unused-parameter -funsigned-char -mwindows -o Bookmark.o Bookmark.c


ここまでで必要な物はすべてコンパイル出来たのでリンクをします。
直接ldでやってもよいですが、取りあえずgcc経由にしています。

# ollydbg.aはlibollydbg.aとかで生成して-L./と-lollydbgオプションでリンクさせてもよいかも
$ i686-pc-mingw32-gcc -g -W -Wall -Wno-unused-parameter -funsigned-char -mwindows Bookmark.o ollydbg.a \
    -o Bookmark.dll -W -shared -Wl,--dll


生成された.dllファイルをOllyDbgのプラグインディレクトリにコピーして起動すれば読み込まれます。
参照しているシンボルの整合性取れてないとか、何か間違っている場合は起動時にエラーが出るので
それを元に修正しましょう。
関数のインポートやエクスポートがどの名前で行われているかはobjdump, PEView,Stud_PE等を
使って確認すると分かりやすいと思います。
概ね引っかかるのは関数名先頭の_有無や関数名末尾の@有無あたりだと思います。(cdecl/stdcallとかその辺の話)


Immunity Debuggerも大体同じですが、途中で仕様で変わったりしてるのでまた今度。

CentOS5.2 リリース

リリースされた感じ。

まだあまり弄ってないけど目に付いた変更点は以下。

  • rsyslog追加
  • kernelリビジョンアップデート


既存のsysklogdが消えることもなくrsyslogが別パッケージで追加されてる模様。
config形式は既存のsysklogd互換の記述が通るけど、色々と機能追加されている。
後で何が出来るか調べてみよう。


カーネルは2.6.18-53から2.6.18-92へと大幅に上がっている。
バイスサポート周りの追加はいつも通りだが、nfs周りのバグ修正が
大量に入ってるのでキャッシュで不整合起きるバグも直ってるかもしれず。
あとは、e1000eドライバがまともに動くようになったのが嬉しい感じ。
#以前はmtu変えるとkernel panicとか

PXEでiSCSI boot (multipath)

前回の続き。今度はマルチパス構成でiSCSI bootさせてみる。
基本的な所は前回と共通なので差分がある箇所だけ記述。
ソフトウェアとしてはdevice-mapper-multipathを使用。

iSCSI Target設定

テストなのでiSCSI Enterprise Targetを使用。
同じSCSI identifierで見えれば良いだけなので一台のハードで問題ない。

Target iqn.sample.target0
        Lun 0 Path=/dev/sdaXX

で、IPアドレスを2つ付けて以下の様に見えるようにする。イニシエータからは
それぞれのIPに対して同じIQNでログインさせる。
IPアドレスを分けるのはパス障害のテストをやり易くするため(iptables等で遮断等)

iSCSI Initiator設定

バイナリは前回の物に加えて以下が必要。

dmsetup
 デバイスファイル作成用。
kpartx
 パーティション情報取得用。dmsetupで使う。
multipath
 マルチパス操作用。
 multipath.confはblacklistを空にしておく。
  /etc/multipath.conf
scsi_id
 unique SCSI identifier取得用。
  /etc/scsi_id.config
LKM(multipath用)
 依存関係によってinsmodに転けるのでロードする順番に注意
  dm-mod.ko
  dm-mirror.ko
  dm-multipath.ko
  dm-round-robin.ko

initスクリプトは以下のような感じ。
差分はmultipathの初期化と複数のディスクを認識させる所程度。
unique SCSI identifierが同じ物が見つかればそれを自動的に束ねてくれる。

 ...
 ##iSCSI+FS初期化
 insmod /lib/jbd.ko
 insmod /lib/ext3.ko
 insmod /lib/scsi_mod.ko
 insmod /lib/sd_mod.ko
 insmod /lib/scsi_transport_iscsi.ko
 insmod /lib/libiscsi.ko
 insmod /lib/iscsi_tcp.ko
 iscsid
 ##Disk 1つめ認識
 iscsiadm -m discovery -t sendtargets -p 
 iscsiadm -m node -T  -p  --login
 ##Disk 2つめ認識
 iscsiadm -m discovery -t sendtargets -p 
 iscsiadm -m node -T  -p  --login
 ##Multipathデバイス認識
 insmod /lib/dm-mod.ko
 insmod /lib/dm-mirror.ko
 insmod /lib/dm-multipath.ko
 insmod /lib/dm-round-robin.ko
 stabilized --hash --interval 250 /proc/scsi/scsi
 mkblkdevs
 multipath
 dmsetup ls --target multipath --exec "/sbin/kpartx -a -p p"
 ##Rootマウント
 mkrootdev -t ext3 -o defaults,ro /dev/mapper/mpath0p1
 mount /sysroot
 setuproot
 switchroot

通常、device-mapperのデバイスファイル作成はudevdに任せるが
作成が非同期になってしまうのでinitrd内部の処理では都合が悪い。
そのためdmsetupで直接作成させている。(内部でmknodが呼ばれる)


パスの監視や再投入はmultipathdの管轄なので、通常起動した後に
上げておく必要がある。
デフォルトではセクタ0のread可否によって疎通を確認している(readsector0)
他にもdirectio,tur,ベンダー固有のチェック(emc_clariion等)があるが
iscsiなので事実上readsector0,directio,tur以外は使えない感じ。
##追記
そんなことは無かった。
prio_callout,path_checker共にtargetが対応していればiscsiでもemcの物が使える。

# multipath -ll -v 3
...
sda: path checker = readsector0 (config file default)
...
sdb: path checker = readsector0 (config file default)


どのパスにI/Oを乗せるかはmultipath.confのpath_grouping_policyで設定できる。
Target側がActive-Activeに対応している場合はmultibus、Active-Standby
の場合はfailoverかな。multibus構成の場合はtcpdumpでパケットカウントした所、
大体均等に振り分けてくれていた。

path_grouping_policy  multibus
 # multipath -l
 mpath0 (XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX) dm-0 IET,VIRTUAL-DISK
 [size=20.0G][features=0][hwhandler=0]
 \_ round-robin 0 [prio=2][undef]
  \_ 7:0:0:0  sdb 8:16  [active][ready]
  \_ 10:0:0:0 sda 8:32  [active][ready]

path_grouping_policy  failover
 # multipath -l
 mpath0 (XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX) dm-0 IET,VIRTUAL-DISK
 [size=20.0G][features=0][hwhandler=0]
 \_ round-robin 0 [prio=0][active]
  \_ 10:0:0:0 sda 8:32  [active][undef]
 \_ round-robin 0 [prio=0][enabled]
  \_ 7:0:0:0  sdb 8:16  [active][undef]

どちらの構成でも障害時には縮退して片側パスでの動作となる。

PXEでiSCSI boot

iSCSI SAN bootはHBAが高い、使用できるハードウェアが限られる等あるので
PXE経由でiSCSI BOOT出来るように頑張ってみた。
InitiatorとしてはLinux-iSCSIはもうメンテされてないし、色々面倒なので
Open-iSCSIを使用。なお、CentOS5にはiscsi-initiator-utilsとして入っている。

iSCSI Target設定

iSCSI TargetはiSCSI Enterprise Target等を使って適当にでっち上げる。
モジュールをビルド、インストールはCentOS5等だと特に問題なく可能。
最終的には専用のストレージ使うにしろテストにはこれで十分。


設定は以下の様に /etc/ietd.conf に追記。
最低限の設定はIQNとブロックデバイスの割り当てを記述するだけ。
ブロックデバイスの代わりにファイルも使えたりもする。

Target iqn.sample.target0
	Lun 0 Path=/dev/sdaXX
Target iqn.sample.target1
	Lun 0 Path=/dev/sdaYY

で以下の通り起動、至ってシンプル。

 # /etc/init.d/iscsi-target start

これでTargetは準備完了。


確認は以下の様な感じで可能。
loginした時点で/dev/sdXのブロックデバイスとして見えるはず。

 # iscsid
 # iscsiadm -m discovery -t sendtargets -p <target ip>
 # iscsiadm -m node -T <target iqn> -p <target ip> --login

この領域にOSを入れる必要があるが、CentOS5はiSCSI領域に直接
標準のインストーラからインストール可能。面倒な場合は
他のホストでmountしてrestoreしたりそのままddするのも良い。

iSCSI Initiator設定

こっちがメイン。
方針としてパッケージの再コンパイル等は行わずにディストリビューション(この場合CentOS)で
配布されているバイナリだけを使用する。(sharedバイナリのまま使用)
システムは作ったら終わりというわけでは無いので、メンテナンスに掛かるコストは
少なければ少ないほど良いし。


ということで起動用initrdの準備。昔はext2だったが最近はcpioで固めた物がgzipされている。
全部最初から作るよりは、既に入っているinitrdを展開して追加、変更した後
再packするのが楽。要はmkinitrdのやっていることを代わりにやるだけ。


また、手の込んだことをやる場合はbusybox等を使っている人も居るようだが、
最近はそこまで領域の節約をする必要が無いのと、上記理由でupdateに追従
し易くするため、必要なライブラリ、バイナリを直接コピーして使うことにした。
必要なライブラリは使いたいバイナリをlddして特定。(LKMはlsmod等)
巨大なlibc.soが間違いなくinitrdに入ることになるが気にしない。


最低限必要なバイナリは以下(ライブラリは適宜必要な物を追加)。
debugしたい場合はstraceやらbashなども入れておくと便利。

ip
 インターフェイス操作用
dhclient
 IPアドレス取得用
 実際にはdhclient-scriptで設定
iscsid
 NETLINKソケット経由でiSCSI制御用LKMと通信する。(嵌り所多いので以下にmemo)
  以下のディレクトリが必要
   /var/lock/iscsi /var/run /var/lib/iscsi/{ifaces,isns,nodes,send_targets,slp,static}
  nss関連のライブラリも必要かも(間接的に呼ばれてる)
  /etc/passwdが必要(rootエントリだけで良い)
  /etc/iscsi/{iscsid.conf,initiatorname.iscsi} も必要
iscsiadm
 iscsidとの通信用
LKM(Ether用)
 e1000.ko,e100.ko 等々使ってるEther次第(ここではe1000を使用)
LKM(iSCSI用)
 依存関係によってinsmodに転けるのでロードする順番に注意
  sd_mod.ko
  scsi_mod.ko
  scsi_transport_iscsi.ko
  libiscsi.ko
  iscsi_tcp.ko
LKM(FS用)
 とりあえずext3で使いたい場合は以下
  jbd.ko
  ext3.ko

initrd内部のinitは以下のような感じ。モジュールをロードし始める所より下を
以下のスクリプトに置き換える。(echo "Loading xxxxx module"のあたり)
直接IPを記述するようにしているが、dhcpのオプションで色々なパラメータは渡せるので
適当に使わなそうな物を使ってIQNやTargetのIPを渡すと、複数のホストで共通のinitrdが使用できる。

 ##Network初期化
 insmod /lib/e1000.ko
 ip -o link set dev eth0 up
 dhclient -lf /var/run/dhcpc.lease -pf /var/run/dhcpc.pid
 ##iSCSI+FS初期化
 insmod /lib/jbd.ko
 insmod /lib/ext3.ko
 insmod /lib/scsi_mod.ko
 insmod /lib/sd_mod.ko
 insmod /lib/scsi_transport_iscsi.ko
 insmod /lib/libiscsi.ko
 insmod /lib/iscsi_tcp.ko
 iscsid
 iscsiadm -m discovery -t sendtargets -p <target ip>
 iscsiadm -m node -T <target iqn> -p <target ip> --login
 ##Rootマウント
 stabilized --hash --interval 250 /proc/scsi/scsi
 mkblkdevs
 mkrootdev -t ext3 -o defaults,ro /dev/sda1
 mount /sysroot
 setuproot
 switchroot

/dev/sdaに見えてる通り以降はローカルディスクから起動したのと何ら変わりなく使用可能。
ブロックサイズの性質上大きめのパケットが流れるので、Jumbo Frameが
使える場合は有効にしておくとppsが格段に下がって良い感じになる。


DHCPPXEの設定については色々な所で見かけるので割愛。
なおNFSROOTでのPXE BOOTも同様の方法で可能。iSCSIの代わりにNFSモジュールを
ロードしてmount.nfsでそのままmountするだけ。
カーネル組み込みのNFSROOTはあまりメンテされていないようでよく問題が
起きてるし、updateのコスト考えるとこっちの方が楽な感じ。

dlsymをLD_PRELOADでフック

LD_PRELOADで関数をフックしたい場合の常套手段は以下のような感じだが
ここで使用するdlsym自体をフックする必要があったのでどうすればいいのか考えてみた。

void (*target_func)(void *p);
void libinit() __attribute__((constructor));
void libinit(){
    orig_func = dlsym(RTLD_NEXT, "target_func");
}
void target_func(void *p){
	...
	orig_func(p);
}

そのままtarget_func=>dlsymにして呼ぶと、ローカルのdlsymが呼ばれてしまうのでNG。
困ったときのlibc頼みでnmしながら眺めていたら__libc_dlsymという物があった。
試してみたところdlsymと同じような動きをしてくれる模様。
unwindといい本当に色々入ってるなlibc。


よってこんな感じのコードに。

static void* (*orig_dlsym)(void * handle, const char * name);
extern void* __libc_dlsym(void * handle, const char * name);
void libinit() __attribute__((constructor));
void libinit(){
    void * haldle = dlopen("libdl.so.2",RTLD_NOW);
    orig_dlsym = __libc_dlsym(haldle, "dlsym");
}
void* dlsym(void * handle, const char * name){
	...
	return orig_dlsym(handle,name);
}

行儀はよろしくないがとりあえず動く模様。

net-snmpとlinuxの64bitカウンタ実装

誤動作の調査で調べたのでついでに覚え書き。
snmpdで取得できるネットワークインターフェイスカウンタの値は
/proc/net/devを元にしている。

中身はこんなの。

# cat /proc/net/dev
Inter-|   Receive                                                |  Transmit ...
 face |bytes    packets errs drop fifo frame compressed multicast|bytes      ...
    lo:  268198    1760    0    0    0     0          0         0   268198   ...
  eth0:1438145878 2202918421    0    0     0     0          0         6 36330...

出力している箇所は以下。(@net/core/dev.c)
net_device_statsに格納されているカウンタ値がunsigned longなので32bitカーネルだと
32bitカウンタ値までしか取得できないが、64bitカーネルの場合はlongが64bitになるため
そのまま64bitカウンタが取得できる。(LP64等の場合)

static void dev_seq_printf_stats(struct seq_file *seq, struct net_device *dev)
{
        if (dev->get_stats) {
                struct net_device_stats *stats = dev->get_stats(dev);

                seq_printf(seq, "%6s:%8lu %7lu %4lu %4lu %4lu %5lu %10lu %9lu "
                                "%8lu %7lu %4lu %4lu %4lu %5lu %7lu %10lu\n",
                           dev->name, stats->rx_bytes, stats->rx_packets,
                           stats->rx_errors,
                           stats->rx_dropped + stats->rx_missed_errors,
                           stats->rx_fifo_errors,
                           stats->rx_length_errors + stats->rx_over_errors +
                             stats->rx_crc_errors + stats->rx_frame_errors,
                           stats->rx_compressed, stats->multicast,
                           stats->tx_bytes, stats->tx_packets,
                           stats->tx_errors, stats->tx_dropped,
                           stats->tx_fifo_errors, stats->collisions,
                           stats->tx_carrier_errors +
                             stats->tx_aborted_errors +
                             stats->tx_window_errors +
                             stats->tx_heartbeat_errors,
                           stats->tx_compressed);
        } else
                seq_printf(seq, "%6s: No statistics available.\n", dev->name);
}

net_device_statsへ値を格納しているのはドライバ側で用意している関数で
bnx2(Broadcom NetXtreme II)のドライバを見てみると以下のような感じ。
32bitモードで動いてる限り上位がそのまま切り捨てられている。
(@drivers/net/bnx2.c)

#define GET_NET_STATS64(ctr)                                    \
        (unsigned long) ((unsigned long) (ctr##_hi) << 32) +    \
        (unsigned long) (ctr##_lo)

#define GET_NET_STATS32(ctr)            \
        (ctr##_lo)

#if (BITS_PER_LONG == 64)
#define GET_NET_STATS   GET_NET_STATS64
#else
#define GET_NET_STATS   GET_NET_STATS32
#endif
...
static struct net_device_stats *
bnx2_get_stats(struct net_device *dev)
...
        net_stats->rx_bytes =
                GET_NET_STATS(stats_blk->stat_IfHCInOctets);

        net_stats->tx_bytes =
                GET_NET_STATS(stats_blk->stat_IfHCOutOctets);
...


で、実際の所はsnmpdがクライアントから問い合わせが無い場合も定期的に
インターフェイスの情報をポーリングしており、32bitカーネルでも取りこぼしが
起きないようになっている。(内部的には上位、下位32bitでそれぞれ管理)
これにより32bitカーネルの場合でも64bitカウンタを擬似的に作り出せているが
当然のことながら、カーネルからは下位32bitしか取得できないのでsnmpdを落とすと
上位32bitはリセットされる。
64bitカーネルの場合はそのまま値が拾えるので何も問題なし。