曖昧

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

パッカーを自作した -elfの勉強-

今まで普通に圧縮のアルゴリズムを学んでたのですが,今回少し趣向を変えて,パッカーというものを作りました.パッカーやその周辺の技術について詳しく解説されている文献が非常に少なかったため,「アナライジング・マルウェア」という書籍を大いに参考にさせていただいてます.丸写しなどは全く行なっておりません.また,この書籍にて解説されているパッカーのコードはwindowsで動くように書かれているのですが,勉強のためLinux(32bit)で動くよう書き換えていきます.

パッカーとは

パッカーは実行ファイル圧縮と呼ばれるもので,wikipediaには

実行ファイル圧縮(英: Executable compression)とは、実行ファイルを何らかの手段で圧縮し、それをデータとして伸張(解凍)用コードと共に1つの実行ファイルとすること。

圧縮された実行ファイルを実行する場合には、まず伸張(解凍)して本来の実行コードを取り出し、それを自動的に実行する。圧縮していないオリジナルの実行ファイルを実行したのと同じ効果が得られるので、一般ユーザーには区別がつかない。 "wikipedia 実行ファイル圧縮"

とあります.実行形式のファイルをパッカーで圧縮すると,圧縮したまま実行できるというものです.基本的には実行時にメモリに展開されるのですが,ハードディスクなどに小さいまま保存できるという利点があります.有名なオープンソースのパッカーにはUPXなどがあります.

本来,データ量を削減するために開発されたツールであると思うのですが,現在パッカーというと主にマルウェアを圧縮するために用いられるものとなっています.圧縮されるということは,保存されているコードが読める状態になっていないため,難読化する目的で利用されています.

パッカーの仕組み

圧縮されたコードと一緒にそれを展開するコードが埋め込荒れており,実行時にまず展開コードが実行されます.展開コードが実行されると,圧縮されたコードが展開され,展開先に実行が移ります.

パッカーの作り方

今回作るパッカーの作り方の手順を説明します.パッカーにも様々な種類がありますし,私自身初心者であるため,今回のものが全てではないことをご容赦ください.

  1. パック対象のファイルを読み込む
  2. ヘッダの情報を集める
  3. 対象のテキストセグメントを圧縮する(今回は単純化のためxorでエンコードする)
  4. 展開ルーチンを作成し,追加する
  5. 出来上がったデータを書き出す

xorエンコードは参考書籍で解説されていたコードがそうなっていたので,本当はせっかく学んだハフマン符号とかで実際に圧縮するところまでやりたかったのですが,力が及ばずできませんでした.なので,厳密にパッカーではないのですが,この辺をこんな感じで改造したら圧縮できそうだなーみたいな感じで読んでください.

実行ファイル

ここから実際に作っていきたいのですが,とにかく実行ファイルがどういう構造をしているかわからないと何もできないため,実行ファイルの解説をしたいと思います.

Linuxの実行ファイルは現在では,COFFというものを少し改造したELFというフォーマットで構築されています.ELFの構造を図にすると以下のようになります.

f:id:attox:20180722220800p:plain
Wikipediaより

elf headerというヘッダが先頭に配置されており,program header tableがその次に配置され,コード領域が続いていき,一番下に,section header tableが配置される,という構造になっています.配置されている順序はこうなっていますが,実際にアクセスする場合は各ヘッダに保存されている値を用いてアクセスします.

program header table, section header tabeleは文字通り,各ヘッダのテーブルとなっており,ここに各領域の数だけヘッダが並べられています.

program headerの指す領域はセグメントと呼ばれ,section headerの指す領域はセクションと呼ばれています.

次に各ヘッダの解説をします.今回主に使うものだけにコメントをつけて軽く説明しています.

elf headerの中身は次のようになっています.

#define EI_NIDENT 16

typedef struct {
    unsigned char e_ident[EI_NIDENT]; //マジックナンバーなど
    uint16_t      e_type;
    uint16_t      e_machine;
    uint32_t      e_version;
    Elf32_Addr     e_entry;           //エントリポイントのアドレス
    Elf32_Off      e_phoff;           //program headerのアドレスのオフセット
    Elf32_Off      e_shoff;           //section headerのアドレスのオフセット
    uint32_t      e_flags;            
    uint16_t      e_ehsize;           //elf header(このヘッダ)のサイズ
    uint16_t      e_phentsize;        //一つのprogram headerのサイズ
    uint16_t      e_phnum;            //program header の数
    uint16_t      e_shentsize;     //一つのsection headerのサイズ
    uint16_t      e_shnum;            //section headerの数
    uint16_t      e_shstrndx;
} Elf32_Ehdr;

このヘッダでは,プログラム全体の管理をしている感じですね.おそらく一番よく見ることになるのがe_entryです.プログラムが実行される際にどのアドレスから始めるのかが記録されています.

次にprogram headerです

typedef struct {
    uint32_t   p_type;
    Elf32_Off  p_offset;  //セグメントのアドレスのオフセット
    Elf32_Addr p_vaddr;   //セグメントの仮想アドレス
    Elf32_Addr p_paddr;   //セグメントの物理アドレス
    uint32_t   p_filesz;  //セグメントのファイルイメージのサイズ
    uint32_t   p_memsz;   //セグメントのメモリ上のサイズ
    uint32_t   p_flags;   //セグメントの属性
    uint32_t   p_align;
} Elf32_Phdr;

program headerは各セグメントに対する情報が保存されています. セグメントの属性は次のようなものがあります. - PF_X  実行可能セグメント - PF_R  読み取り可能セグメント - PF_W  書き込み可能セグメント

最後にsection headerです

typedef struct {
    uint32_t   sh_name;
    uint32_t   sh_type;
    uint32_t   sh_flags;
    Elf32_Addr sh_addr;     //セクションのアドレス
    Elf32_Off  sh_offset;   //セクションのアドレスのオフセット
    uint32_t   sh_size;     //セクションのサイズ
    uint32_t   sh_link;
    uint32_t   sh_info;
    uint32_t   sh_addralign;
    uint32_t   sh_entsize;
} Elf32_Shdr;

このヘッダではアドレスとオフセットが重要です.

だいぶ雑ではありますが,このようにelfは出来上がっています.

まとめ

パッカーの自作というタイトルですが,いったん色々解説したタイミングで今回は終了します.私がパッカーを作った際に使用した部分のみの解説になってしまったので,詳しくは参考文献にあげた記事などを参考にしてもらいたいです.非常にわかりやすいです. また,section headerとprogram headerの違いが正直まだよくわかっていません.詳しくは次回以降に書きますが,よくわかっていないせいで,非常に悩んだ部分もありました.詳しい人がいたら教えてください... 次回,実際にコードを書いていきます.

参考文献

sugawarayusuke.hatenablog.com

ELF Format

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

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