セキュリティを楽しく学ぶ、触れる。セキュリティごった煮ブログ

ネットエージェント
セキュリティごった煮ブログ

 コース:元祖こってり

「元祖こってり」記事はネットエージェント旧ブログ[netagent-blog.jp]に掲載されていた記事であり、現在ネットエージェントに在籍していないライターの記事も含みます。

TLS Callbacks

池添徹

6 明けましておめでとうございます、ネットエージェント株式会社、研究開発部の池添徹です。今回は、昨年11月に行いました Netagent Security Contest 2010 の Level6 Unpack EXE で使用した EXE パッカーについて書かせていただこうと思います。

-----

■ EXE パッカーとは
 EXE パッカー(EXE Packer)とは、一般的には、実行可能ファイルを「実行可能なまま圧縮・暗号化等を行う」ツールのことです。ファイルに対して圧縮・暗号化を行うだけであれば、LHA や ZIP 等のアーカイブ形式でも可能ですが、EXE パッカーは「実行可能なまま圧縮・暗号化を行う」という点がミソです。
 リバースエンジニアリングの世界においては、実行ファイルのパッキング技術は古くから研究されており、すでに多くの EXE パッカーが存在します。また、UPX というオープンソースの代表的な EXE パッカーもありますので、パッキングのコア技術に興味がある方は、ソースコードを読んでみても面白いかもしれません。

■ EXE パッカーの概要
 Level6 Unpack EXE の問題を作成する際、既存のパッカーを使用してもよかったのですが、それでは面白くないため、今回の問題では、既存のものは使わず、新しくパッカーを作成しました。一般的なパッカーとは若干異なるテクニックを使っていますので、それを解説したいと思います。
 まず、パッカーの実装についてですが、一般的なパッカーでは、ターゲットの実行ファイルに対して、大まかに以下の処理を行います。

  1. 実行コード部分の圧縮・暗号化
  2. 実行コード部分の展開・復号コードの付与
  3. エントリーポイントの変更
 

パッキングされた実行ファイルは、プログラム開始直後にまずパッカーに付与された展開等の処理が実行され、圧縮されている実行コード部分の展開等を行い、その後、元々のエントリポイントへジャンプします。
 ただ、今回作成したパッカーは、一般的なパッカーとは異なり、エントリーポイントの変更を行わずに、展開等の処理を「エントリポイントより前の部分」に割り込ませています。プログラム開始地点であるエントリーポイントを変更しない、なおかつ、エントリーポイントよりも前に展開等の処理を行う、というのが、今回のパッカーの主な特徴となります。
 では、具体的な方法について説明していきたいと思います。

■ TLS Callbacks
 今回のパッカーでは TLS Callbacks と呼ばれる仕組みを利用し、エントリーポイントより前で任意のコードを実行させる方法を使用しました。TLS Callbacks とは、TLS(Thread Local Storage) と呼ばれるスレッド毎に固有な記憶領域を利用した場合に、スレッド起動時とスレッド終了時に呼び出されるコールバック関数のことです。簡単な例を下記に示します。
 まず、単純な TLS を使用した例を挙げます。なお、本記事のプログラムはすべて Microsoft Visual C++ 2008 Express Edition にて動作を確認しています。

// simple_tls.cpp
#include <stdio.h>
#include <process.h>
#include <windows.h>

__declspec(thread) int value1;
int value2;

void thread_func( void* index )
{
printf( " thread %d : %p, %p\n",
(int)index, &value1, &value2 );
}

int main( int argc, char** argv )
{
printf( "thread num : value1 , value2\n" );
for( int i = 0; i < 10; ++i )
{
_beginthread( thread_func, 0, (void*)i );
}
Sleep(1000);
return 0;
}

C:\>simple_tls.exe
thread num : value1 , value2
thread 0 : 00383174, 00233018
thread 2 : 003840A4, 00233018
thread 1 : 003842EC, 00233018
thread 4 : 003830CC, 00233018
thread 3 : 00383174, 00233018
thread 5 : 00383174, 00233018
thread 7 : 003831AC, 00233018
thread 6 : 003842DC, 00233018
thread 9 : 003831BC, 00233018
thread 8 : 00384304, 00233018

 この例では、スレッドを10個起動し、それぞれのスレッドで value1, value2 のアドレスを表示しています。value1 は __declspec(thread) と言うキーワードを使用し TLS オブジェクトとして宣言しているため、スレッドごとに異なる実体をもっています。一方 value2 は通常のグローバル変数として宣言しているため、プロセス内に一つの実体しかありません。実行環境にもよりますが、value1 のアドレスは変動し、value2 のアドレスは固定されているのがわかると思います。value1 の値が一部重複しているのは、解放済みの領域を再利用しているためです。このサンプルにおいては、value1 は int 型であり、特殊な初期化等は必要ないため、TLS Callbacks は利用されません。
 次に、TLS Callbacks を使用した例を挙げます。尚、このサンプルでリリースビルドを行う場合は、コンパイラオプションで /GL を外す必要があります。

// tls_callbacks.cpp
#include <stdio.h>
#include <process.h>
#include <windows.h>
#include <vector>

__declspec(thread) std::vector<int>* vec_tls;

/* start of Adding TLS Callback */
void __stdcall tls_callback( void*, DWORD dwReason, void* )
{
switch( dwReason )
{
case DLL_PROCESS_ATTACH:
printf( "process attach\n" );
break;
case DLL_PROCESS_DETACH:
printf( "process detach\n" );
break;
case DLL_THREAD_ATTACH:
vec_tls = new std::vector<int>;
printf( " thread attach\n" );
break;
case DLL_THREAD_DETACH:
delete vec_tls;
printf( " thread detach\n" );
break;
}
}

#pragma section(".CRT$XLB",long,read)
extern "C" __declspec(allocate(".CRT$XLB"))
PIMAGE_TLS_CALLBACK _xl_b = tls_callback;
/* end of Adding TLS Callback */

void thread_func( void* index )
{
printf( " thread %d : %p\n", (int)index, vec_tls );
}

int main( int argc, char** argv )
{
for( int i = 0; i < 10; ++i )
{
_beginthread( thread_func, 0, (void*)i );
}
Sleep(1000);
return 0;
}

C:\>tls_callbacks.exe
process detach
process attach
thread attach
thread 0 : 00981AA0
thread attach
thread 1 : 00981AD0
thread detach
thread detach
thread attach
thread 2 : 00270BE0
(省略)
thread detach
process detach

 この例では、std::vector<int>* 型を TLS オブジェクトとして宣言しています。なぜポインター型なのか? それは TLS オブジェクトは初期化時に値が定まっている必要があるため、コンストラクタを呼ぶ必要があるものは TLS オブジェクトとして宣言できないからです。そこで、TLS Callbacks を利用し、各スレッド毎に実体を生成する関数を作成し、TLS Callbacks として登録します。TLS Callbacks の登録の仕方はコンパイラーごとに異なるため、ここでは Microsoft Visual C++ 2008 Express Edition での例とします。
 上記のソースコードの太字の部分が TLS Callbacks を登録する一連のコードとなります。内容の詳細は割合させていただきますが、この様なコードを追加することで、スレッド作成、破棄時に tls_callback 関数が呼ばれ vec_tls の実体の作成、破棄が行えます。実際にこのコードを実行すると、以下の流れで各関数が呼び出されます。

  1. tls_callback(DLL_PROCESS_ATTACH) [プロセス開始]
  2. main
  3. _beginthread
  4. tls_callback(DLL_THREAD_ATTACH)  [スレッド開始]
  5. thread_func
  6. tls_callback(DLL_THREAD_DETACH)  [スレッド終了]
  7. 3から繰り返し
  8. tls_callback(DLL_PROCESS_DETACH) [プロセス終了]
  9. exit
 

実際には、tls_callback(DLL_THREAD_ATTACH) の段階で、別のスレッドとして実行されるため、複数のスレッドを一度に生成した場合は、tls_callback(DLL_THREAD_ATTACH) が同時に実行される可能性があります。この辺りは通常のマルチスレッドプログラミングと同様なので、TLS Callbacks 内でのリソースの扱いには注意する必要があります。また、TLS Callbacks はプロセス内唯一のスレッド(main 関数が動いているスレッド)に関しては、DLL_PROCESS_ATTACH/DLL_PROCESS_DETACH で通知が来ることにも注意しなければなりません。
 さて、TLS Callbacks を用いれば、エントリーポイントを変更しないパッカーが作れる事は解りました。しかし、上記で解説した方法はソースコードがある状態での TLS Callbacks の使用方法でした。パッカーと名乗る以上、ソースコードの無いビルド済みの実行ファイルに対して処理を行う必要があります。そこで、TLS Callbacks がどの様に実行ファイル内に存在するのかを調査します。

■ TLS Callbacks ルーチンの解析
 先ほど例として挙げた TLS Callbacks を使用したサンプルプログラムをビルドし、そのファイルに対して dumpbin コマンドを実行しヘッダー情報を確認します。この時、コマンドラインオプションとして /HEADERS と /TLS を指定します。/HEADERS は 実行ファイルのヘッダーの情報を、/TLS は TLS に関するデータの情報を表示するオプションとなります。

C:\>dumpbin /HEADERS /TLS tls_sample.exe
Microsoft (R) COFF/PE Dumper Version 9.00.30729.01
Copyright (C) Microsoft Corporation. All rights reserved.


Dump of file tls_sample.exe

PE signature found

File Type: EXECUTABLE IMAGE

FILE HEADER VALUES
14C machine (x86)
6 number of sections
4D11B1FF time date stamp Wed Dec 22 17:08:31 2010
0 file pointer to symbol table
0 number of symbols
E0 size of optional header
102 characteristics
Executable
32 bit word machine

OPTIONAL HEADER VALUES
10B magic # (PE32)
9.00 linker version
C00 size of code
1400 size of initialized data
0 size of uninitialized data
14C8 entry point (004014C8)
1000 base of code
2000 base of data
400000 image base (00400000 to 00406FFF)
1000 section alignment
200 file alignment
5.00 operating system version
0.00 image version
5.00 subsystem version
0 Win32 version
7000 size of image
400 size of headers
5ACC checksum
3 subsystem (Windows CUI)
8140 DLL characteristics
Dynamic base
NX compatible
Terminal Server Aware
100000 size of stack reserve
1000 size of stack commit
100000 size of heap reserve
1000 size of heap commit
0 loader flags
10 number of directories
0 [ 0] RVA [size] of Export Directory
2284 [ 3C] RVA [size] of Import Directory
5000 [ 2B0] RVA [size] of Resource Directory
0 [ 0] RVA [size] of Exception Directory
0 [ 0] RVA [size] of Certificates Directory
6000 [ 194] RVA [size] of Base Relocation Directory
0 [ 0] RVA [size] of Debug Directory
0 [ 0] RVA [size] of Architecture Directory
0 [ 0] RVA [size] of Global Pointer Directory
2198 [ 18] RVA [size] of Thread Storage Directory
2150 [ 40] RVA [size] of Load Configuration Directory
0 [ 0] RVA [size] of Bound Import Directory
2000 [ B8] RVA [size] of Import Address Table Directory
0 [ 0] RVA [size] of Delay Import Directory
0 [ 0] RVA [size] of COM Descriptor Directory
0 [ 0] RVA [size] of Reserved Directory


SECTION HEADER #1
.text name
A65 virtual size
1000 virtual address (00401000 to 00401A64)
C00 size of raw data
400 file pointer to raw data (00000400 to 00000FFF)
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
60000020 flags
Code
Execute Read

SECTION HEADER #2
.rdata name
68E virtual size
2000 virtual address (00402000 to 0040268D)
800 size of raw data
1000 file pointer to raw data (00001000 to 000017FF)
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
40000040 flags
Initialized Data
Read Only

Section contains the following TLS directory:

00404000 Start of raw data
00404008 End of raw data
00403020 Address of index
004020D8 Address of callbacks
0 Size of zero fill
00000000 Characteristics

TLS Callbacks

Address
--------
00401130
00000000

SECTION HEADER #3
(省略)

 OPTIONAL HEADER の DataDirectory の Thread Storage Directory や SECTION HEADER #2(.rdata) に TLS に関する情報が含まれていそうです。DataDirecroty 内の Thread Storage Directory は RVA値として 2198h が指定されています。RVA 値 2000h 台は .rdata セクションにマッピングされているようなので、.rdata セクションのダンプデータを見てみましょう。

C:\>dumpbin /SECTION:.rdata /RAWDATA tls_sample.exe
Microsoft (R) COFF/PE Dumper Version 9.00.30729.01
Copyright (C) Microsoft Corporation. All rights reserved.


Dump of file tls_sample.exe

File Type: EXECUTABLE IMAGE

SECTION HEADER #2
.rdata name
68E virtual size
2000 virtual address (00402000 to 0040268D)
800 size of raw data
1000 file pointer to raw data (00001000 to 000017FF)
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
40000040 flags
Initialized Data
Read Only

RAW DATA #2
00402000: 78 23 00 00 5E 26 00 00 48 26 00 00 38 26 00 00
00402010: 1E 26 00 00 0A 26 00 00 EC 25 00 00 D0 25 00 00
00402020: BC 25 00 00 A8 25 00 00 8A 25 00 00 74 25 00 00
00402030: 74 26 00 00 00 00 00 00 F8 23 00 00 08 24 00 00
00402040: 12 24 00 00 1A 24 00 00 28 24 00 00 30 24 00 00
00402050: 3C 24 00 00 48 24 00 00 56 24 00 00 6C 24 00 00
00402060: BE 23 00 00 90 24 00 00 A0 24 00 00 AE 24 00 00
00402070: C0 24 00 00 EA 23 00 00 E6 24 00 00 FC 24 00 00
00402080: 06 25 00 00 14 25 00 00 1C 25 00 00 26 25 00 00
00402090: 38 25 00 00 52 25 00 00 64 25 00 00 B4 23 00 00
004020A0: 9E 23 00 00 8E 23 00 00 CE 23 00 00 D2 24 00 00
004020B0: 80 24 00 00 00 00 00 00 00 00 00 00 26 12 40 00
004020C0: 00 00 00 00 00 00 00 00 E7 13 40 00 1A 16 40 00
004020D0: 00 00 00 00 00 00 00 00 30 11 40 00 00 00 00 00
004020E0: 62 61 64 20 61 6C 6C 6F 63 61 74 69 6F 6E 00 00
004020F0: 20 20 74 68 72 65 61 64 20 25 64 20 3A 20 25 70
00402100: 0A 00 00 00 20 74 68 72 65 61 64 20 64 65 74 61
00402110: 63 68 0A 00 20 74 68 72 65 61 64 20 61 74 74 61
00402120: 63 68 0A 00 70 72 6F 63 65 73 73 20 64 65 74 61
00402130: 63 68 0A 00 70 72 6F 63 65 73 73 20 61 74 74 61
00402140: 63 68 0A 00 48 30 40 00 A0 30 40 00 00 00 00 00
00402150: 48 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00402160: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00402170: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00402180: 00 00 00 00 00 00 00 00 00 00 00 00 04 30 40 00
00402190: B0 21 40 00 03 00 00 00 00 40 40 00 08 40 40 00
004021A0: 20 30 40 00 D8 20 40 00 00 00 00 00 00 00 00 00
004021B0: F5 18 00 00 18 1A 00 00 4B 1A 00 00 00 00 00 00
(省略)

 さて、Thread Storage Directory の RVA 値 は 2198h でした。RVA 値はプロセスがロードされた時のベースアドレスを加算することで、本来のアドレスになります。つまり、RVA 値に OPTIONAL HEADER で指定されている image base を加算することでメモリー上のアドレスになります。image base は 400000h となっているので、402198h が Thread Storage Directory が示すデータの実体のアドレスとなり、ダンプデータの 青色の部分がそのデータとなります。このデータの構造体は IMAGE_TLS_DIRECTORY となり、定義は以下の通りです。

// WinNT.h
typedef struct _IMAGE_TLS_DIRECTORY32 {
DWORD StartAddressOfRawData;
DWORD EndAddressOfRawData;
DWORD AddressOfIndex;
DWORD AddressOfCallBacks; // PIMAGE_TLS_CALLBACK *
DWORD SizeOfZeroFill;
DWORD Characteristics;
} IMAGE_TLS_DIRECTORY32;
typedef IMAGE_TLS_DIRECTORY32 * PIMAGE_TLS_DIRECTORY32;

 先ほどのダンプデータをこの構造体に当てはめると以下のようになり、AddressOfCallbacks(TLS Callbacks で呼び出す関数ポインタ配列へのアドレス)は 4020D8h となります。

IMAGE_TLS_DIRECTORY32::StartAddressOfRawData        404000h
IMAGE_TLS_DIRECTORY32::EndAddressOfRawData 404008h
IMAGE_TLS_DIRECTORY32::AddressOfIndex 403020h
IMAGE_TLS_DIRECTORY32::AddressOfCallBacks 4020D8h
IMAGE_TLS_DIRECTORY32::SizeOfZeroFill 0h
IMAGE_TLS_DIRECTORY32::Characteristics 0h

 4020D8h のデータは赤色の部分であり、このデータが実際の TLS Callback 関数のポインター配列になっています。一つ目の TLS Callback は 401130h、二つ目は 0h となっており、TLS Callback 関数が一つだけ登録されている状態です(Null Terminate な配列のため)。401130h は tls_callback 関数の先頭となります。
 以上のリサーチ結果から、エントリポイントを変更せずとも上記のポインター配列に TLS Callback の関数アドレスを登録すれば、プログラムの実行前に展開ルーチンを差し込めることが分かりました。

-----

 以上のテクニックを利用して作成されたのが、Netagent Security Contest 2010 の Level6 Unpack EXE で使用した EXE パッカーとなります。
 実際の問題では、このテクニックの他に OllyDbg 1.xx のみで通用するちょっとしたネタも含んでいますが、基本的には上記の理論を理解していれば解答に辿りつけるようになっています。もし競技中には解くことができなかった方も、よろしければ再度挑戦してみてください。

 最後になりましたが、本記事が、読者の方々の知識の引き出しを一つでも増やす事ができたなら幸いです。

メルマガ読者募集 採用情報 2020年卒向けインターンシップ

月別