曖昧

確かなことなんてなにもない

パッカーを自作した -コードを書く-

前回,パッカーを作るための前知識を説明しました.今回からコードを書いて,作っていこうと思います.前回にも言いましたが,コードの内容は「アナライジング・マルウェア」という書籍を大いに参考にしていますが,こちらは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)

アナライジング・マルウェア ―フリーツールを使った感染事案対処 (Art Of Reversing)

ELF入門 - 情弱ログ

ELF Format