パッカーを自作した -コードを書く-
前回,パッカーを作るための前知識を説明しました.今回からコードを書いて,作っていこうと思います.前回にも言いましたが,コードの内容は「アナライジング・マルウェア」という書籍を大いに参考にしていますが,こちらはWindowsように書かれており,今回はLinuxで書くことでかなり違ったものになりました.
ファイルの読み込み
まずは,パックする対象のファイルを読み込みます.
target_filename = argv[1]; packed_filename = argv[2]; fd = open(target_filename, O_RDONLY); target_bin = fdopen(fd, "rb"); fstat(fd, &stbuf); unsigned char target_bin_buffer[stbuf.st_size]; fread(target_bin_buffer, 1, sizeof(target_bin_buffer), target_bin); fclose(target_bin);
fopenで読み込んでも良いのですが,読み込むファイルのサイズなどを取得しなければいけないことから,openを使ってファイルディスクリプターを取得するところからやっています.
ファイルのサイズと同サイズの領域を用意し,そこに読み込んでいます.
各ヘッダの読み込み
ファイルを読み込んだら,次に各ヘッダの値を読み込んでいきます.
ehdr = (Elf32_Ehdr *)target_bin_buffer; shdr = (Elf32_Shdr *)(&target_bin_buffer[ehdr->e_shoff]); phdr = (Elf32_Phdr *)(&target_bin_buffer[ehdr->e_phoff]);
ehdr, shdr, phdrがそれぞれ,elf header, section header, program headerとなっています. コードは少し読みにくくなってしまいましたが,先頭にあるelf headerを読み込んでから,オフセットを指定して,他のヘッダのポインタを読み込むようにしています.
コードセクションの検索
今回作るパッカーでは,エントリポイントを含むコードセクションのみエンコードを行います.UPXなどのパッカーでは,全てのセクションを圧縮することができるらしいのですが,何も考えずに全てのセクションをエンコードするものを作ってしまうと,おそらく,プログラム実行に必要な情報(シンボル情報や,plt,gotなど関数のリンク作業)まで失われてしまってデコードしても正しく実行できなくなってしまいます. ということで,エントリポイントを含むセクションの検索を行います.セクションの検索を行う関数は次のようになります.
Elf32_Shdr *search_oep_include_section_header( Elf32_Shdr *shdr, unsigned int oep, unsigned int shnum){ Elf32_Shdr *oep_shdr = NULL; unsigned int section_addr; unsigned int section_size; printf("search oep include section header\n"); for(int i=0; i < shnum; i++, shdr++){ section_addr = shdr->sh_addr; section_size = shdr->sh_size; //printf("addr:0x%08X size:0x%08X oep:0x%08x\n", shdr->p_vaddr, shdr->p_memsz, oep); if(section_addr <= oep && oep <= section_addr + section_size){ printf("oep section found!\n"); oep_shdr = shdr; break; } } return oep_shdr; }
ヘッダーテーブルの中を先頭から一つずつ見ていって,アドレスがエントリポイントを含んでいるかどうかを調べているだけです.見つけたらそのヘッダを返すということをしています.
見つけたら,そのセクションから必要な情報を取り出します.
//oep include section search oep_shdr = search_oep_include_section_header(shdr, ehdr->e_entry, ehdr->e_shnum); //get oep include section values section_vaddr = oep_shdr->sh_addr; section_vsize = oep_shdr->sh_size; section_raddr = oep_shdr->sh_offset;
また,直接エンコードと関係はないのですが,後半で説明するとある事情により,エントリポイントを含むセグメントも検索しないといけません.なので,同じ要領で検索します.
Elf32_Phdr *search_oep_include_segment_header( Elf32_Phdr *phdr, unsigned int oep, unsigned int phnum){ Elf32_Phdr *oep_phdr = NULL; unsigned int section_vaddr; unsigned int section_vsize; printf("search oep include section header\n"); for(int i=0; i < phnum; i++, phdr++){ section_vaddr = phdr->p_vaddr; section_vsize = phdr->p_memsz; printf("addr:0x%08X size:0x%08X oep:0x%08x\n", section_vaddr, section_vsize, oep); if(section_vaddr <= oep && oep <= section_vaddr + section_vsize){ printf("oep section found!\n"); oep_phdr = phdr; break; } } return oep_phdr; }
エンコード
次に,該当セクションをエンコードしていきます.エンコードする関数が次になります.
void xor_encoder(unsigned char *start, unsigned int size, unsigned char encoder) { unsigned int cnt=0; printf("Start Xor Encode by '0x%X'\n", encoder); for(cnt=0; cnt<size; cnt++){ start[cnt] ^= encoder; } printf("Encode Done\n"); }
開始アドレスとサイズ,エンコードする値を指定して,開始アドレスから1バイトずつ取ってきて順次エンコードしています. 使うときはこんな感じ
encoder = 0xFF; xor_encoder((unsigned char *)(oep_shdr->sh_offset + target_bin_buffer), oep_shdr->sh_size, encoder);
展開コードの生成
エンコードしたコードをデコードするルーチンを生成して追加します.生成する関数が次になります.
unsigned char decode_stub[] = { 0x60, // pushad 0xBE,0xFF,0xFF,0xFF,0xFF, // mov esi, decode_start 0xB9,0xFF,0xFF,0xFF,0xFF, // mov ecx, decode_size 0xB0,0xFF, // mov al, decoder 0x30,0x06, // xor byte ptr [esi], al (LOOP) 0x46, // inc esi 0x49, // dec ecx 0x75,0xFA, // jnz LOOP 0x61, // popad 0xE9,0xFF,0xFF,0xFF,0xFF // jmp OEP }; unsigned int decode_start_offset = 2; unsigned int decode_size_offset = 7; unsigned int decoder_offset = 12; unsigned int jmp_oep_addr_offset = 21; void create_decode_stub(unsigned int code_vaddr, unsigned int code_vsize, unsigned char decoder, unsigned int oep) { int cnt=0; int jmp_len_to_oep=0; jmp_len_to_oep = oep - (code_vaddr + code_vsize + sizeof(decode_stub)); printf("start : 0x%08X\n", code_vaddr); printf("size : 0x%08X\n", code_vsize); printf("decoder : 0x%02X\n", decoder); printf("oep : 0x%08X\n", oep); printf("jmp len : 0x%08X\n", jmp_len_to_oep); memcpy(&decode_stub[decode_start_offset], &code_vaddr, sizeof(DWORD)); memcpy(&decode_stub[decode_size_offset], &code_vsize, sizeof(DWORD)); memcpy(&decode_stub[decoder_offset], &decoder, sizeof(unsigned char)); memcpy(&decode_stub[jmp_oep_addr_offset], &jmp_len_to_oep, sizeof(DWORD)); return; }
decode_stubという展開コードのテンプレートのようなものを書いて,具体的な値をcreate_decode_stub関数で与えています.decode_stubで0xFFとなっているところは大体何かが代入されるところです. decode_stubの内容はコメントにつけたアセンブリコードのとおりなのですが,やっていることはxorでデコードしているだけです. このコードは参考書籍に載っているものをほんの少しだけLinux用に改造して転用させていただきました.ハフマン符号などでエンコードした場合,ハフマン符号のデコードも機械語で記述しなければならず,それが私にはできなかったため断念しました.
なにはともあれとりあえず作ったのでこんな感じで生成して,生成したコードを読み込んだファイルの後ろにくっつけます.
create_decode_stub(section_vaddr, section_vsize, encoder, ehdr->e_entry); memcpy((unsigned char *)(section_raddr + section_vsize + target_bin_buffer), decode_stub, sizeof(decode_stub));
情報の書き換え
当初読み込んだファイルから色々と変更しているため,元々の情報を書き換える必要があります.
ehdr->e_entry = section_vaddr + section_vsize; oep_phdr->p_flags |= PF_W;
一行目は,展開コードをまず実行してもらいたいので,エントリポイントを変更しています. 二行目は,展開コード内で,エンコードされているコードを書き換える必要があるため,本来持っていない書き込み権限を付与しています. 今まで,領域を操作するときはセクション単位で行ってきていたのですが,この権限の部分だけセグメントの単位で行わないとなぜかうまくいかなくて悩みました.そのため.エントリポイントを含むセグメントも同時に検索していました.
書き出し
最後は,出来上がったコードをファイルに書き出して,パッキング終了となります.
packed_bin = fopen(packed_filename, "wb"); fwrite(target_bin_buffer, sizeof(target_bin_buffer), 1, packed_bin);
実行
さてここまでで,一連のパッカーの処理を作り終えたので,実行してみます. まず,適当なプログラムを書き,testという名前で実行ファイルを作ります.
#include <stdio.h> int main(){ printf("Hello, World!\n"); return 0; }
パッキングします.
$ ./a.out test packed search oep include section header oep section found! search oep include section header addr:0x08048034 size:0x00000120 oep:0x08048310 addr:0x08048154 size:0x00000013 oep:0x08048310 addr:0x08048000 size:0x000005C8 oep:0x08048310 oep section found! Start Xor Encode by '0xFF' Encode Done start : 0x08048310 size : 0x00000192 decoder : 0xFF oep : 0x08048310 jmp len : 0xFFFFFE55
実行します.
$ ./packed Hello, World! Segmentation fault (core dumped)
なんかいらんもんがくっついてしまっとりますが,とりあえず出来たということにしてくださいm(_)m
検証
実際にパッキングできているのかどうか少し調べます. objdumpで逆アセンブルすると,
0804840b <main>: 804840b: 72 b3 jb 80483c0 <__do_global_dtors_aux> 804840d: db fb (bad) 804840f: 7c 1b jl 804842c <main+0x21> 8048411: 0f 00 8e 03 aa 76 1a str WORD PTR [esi+0x1a76aa03] 8048418: ae scas al,BYTE PTR es:[edi] 8048419: 7c 13 jl 804842e <main+0x23> 804841b: fb sti 804841c: 7c 13 jl 8048431 <main+0x26> 804841e: f3 97 repz xchg edi,eax 8048420: 3f aas 8048421: 7b fb jnp 804841e <main+0x13> 8048423: f7 17 not DWORD PTR [edi] 8048425: 48 dec eax 8048426: 01 00 add DWORD PTR [eax],eax 8048428: 00 7c 3b ef add BYTE PTR [ebx+edi*1-0x11],bh 804842c: 47 inc edi 804842d: ff (bad) 804842e: ff (bad) 804842f: ff (bad) 8048430: ff 74 b2 03 push DWORD PTR [edx+esi*4+0x3] 8048434: 36 72 9e ss jb 80483d5 <__do_global_dtors_aux+0x15> 8048437: 03 3c 99 add edi,DWORD PTR [ecx+ebx*4] 804843a: 6f outs dx,DWORD PTR ds:[esi] 804843b: 99 cdq 804843c: 6f outs dx,DWORD PTR ds:[esi] 804843d: 99 cdq 804843e: 6f outs dx,DWORD PTR ds:[esi] 804843f: 6f outs dx,DWORD PTR ds:[esi]
意味のわからないコードになっていることがわかると思います.
ただ,エンコードしているのはテキストセグメントだけなので,シンボル情報などは全てそのままになっているので,gdbでmainまで実行させると
$ b main Breakpoint 1 at 0x804840b $ run .... $ disas main Dump of assembler code for function main: 0x0804840b <+0>: jb 0x8048459 <__libc_csu_init+25> 0x0804840d <+2>: and al,0x4 0x0804840f <+4>: and esp,0xfffffff0 => 0x08048412 <+7>: push DWORD PTR [ecx-0x4] 0x08048415 <+10>: push ebp 0x08048416 <+11>: mov ebp,esp 0x08048418 <+13>: push ecx 0x08048419 <+14>: sub esp,0x4 0x0804841c <+17>: sub esp,0xc 0x0804841f <+20>: push 0x80484c0 0x08048424 <+25>: call 0x80482e0 <puts@plt> 0x08048429 <+30>: add esp,0x10 0x0804842c <+33>: mov eax,0x0 0x08048431 <+38>: mov ecx,DWORD PTR [ebp-0x4] 0x08048434 <+41>: leave 0x08048435 <+42>: lea esp,[ecx-0x4] 0x08048438 <+45>: ret End of assembler dump.
よく見る形のコードになっていますね.
ここに問題がないことがわかるので,あのSegmentation fault (core dumped)はretの後に起こってるんでしょうね.おそらく,単純に読み込んだバッファの後ろにそのまま展開コードをくっつけたのが悪かったんだと思うのですが,直すのはまた今度にします.
まとめ
今回は,前回解説した知識をもとに実際にパッカーのコードを書きました.書きましたと言っても,書籍のコードを書き換えただけなのですが,これが意外と大変だった.また,改めて見直すとめちゃくちゃ汚いコードを書いていることに気づきました.もうちょっと直したり改造して,また投稿したいと思います.
参考文献
アナライジング・マルウェア ―フリーツールを使った感染事案対処 (Art Of Reversing)
- 作者: 新井悠,岩村誠,川古谷裕平,青木一史,星澤裕二
- 出版社/メーカー: オライリージャパン
- 発売日: 2010/12/20
- メディア: 単行本(ソフトカバー)
- 購入: 8人 クリック: 315回
- この商品を含むブログ (22件) を見る