x86_64向けのELF難読化器を書く

#tech#Rust

2024-2-11

現在の所属先の1つである,筑波大学情報科学類において卒業のために履修する必要がある主専攻実験では,秋学期に「バイナリプログラムの解析」というテーマを選択した.これはその名前の通り,各回で主にバイナリプログラムを対象とした解析について学ぶ.

この講義は既に終了しており,2023年度における各回の実施内容はここにある[1]のだが,その中で難読化について取り扱う回が存在した.

概ね2週に1回のペースで複数[2]の課題が出て,全体的に手を動かして実装する系の課題が多かったが,特にこの回において出題された,"適当な難読化ツールを作成する(ターゲットはバイナリでもソースコードでも構わない)"といった趣旨の課題において作成した,x86_64向けのELFを難読化するRust製のアプリケーションについては,ブログのネタにするには丁度良い実装量だと思ったのでこうして記事を書いている.

尚,GitHub上のリポジトリはこれである.リポジトリ名はcargo newする際にヨルシカのカトレアを聴いていたことに由来する.

対応している機能

現状以下:

  • エンディアンビットの変更(64bitなのに32bitと偽る)
  • アーキテクチャビットの変更(エンディアンを偽る)
  • セクションヘッダ情報秘匿
  • シンボル情報秘匿
  • 入力された任意のセクション情報秘匿

それぞれを行うことでどのような効能が得られるのかについては,実行結果の章に記した.また,各処理は独立となるように作ってあるため,複数の処理を重ね掛けしても良い.

実装

難読化器を表す構造体

Obfuscator構造体を作成し,その中で各種操作をimplしている.構造体の定義はこれ:

pub struct Obfuscator {
    input: Mmap,
    pub output: MmapMut,
    sec_hdr: String,
    sec_hdr_num: u64,
    sec_hdr_size: u64,
    sec_hdr_offset: u64,
    sec_table: u64,
}

難読化対象のELFファイル(以下ターゲットファイル)をinputメンバがmemmap::Mmapとして持っていて,成果物はoutputメンバがmemmap::MmapMutとして持っている.

他のメンバについては,セクションヘッダに関するもので,sec_hdrが各セクション名が入った文字列,sec_hdr_numがセクションヘッダ数,sec_hdr_sizeがセクションヘッダサイズ,sec_hdr_offsetがセクションヘッダのオフセット,sec_tableがセクションテーブルのアドレスを示している.

前処理

各難読化手法を適用する前段階で,open関数で前処理を行っている.

まず,std::fs::OpenOptionsを用いて権限を適切に割り当てて開く:

let file = match OpenOptions::new().read(true).open(input_path) {
     Ok(file) => file,
     Err(e) => {
          panic!("failed to open file: {}", e);
     }
};

let mut output_file = match OpenOptions::new()
    .read(true)
    .write(true)
    .create(true)
    .open(output_path)
{
    Ok(file) => file,
    Err(e) => {
        panic!("failed to create file: {}", e);
    }
};

このままではoutput_fileは空なので,ひとまずターゲットファイルと同じ物を詰める:

let mut input_contents = Vec::new();
file.try_clone()?
    .take(usize::MAX as u64)
    .read_to_end(&mut input_contents)?;
output_file.write_all(&input_contents)?;

その後memmap::{Mmap, MmapMut}を用いてメモリマップを作る:

let input = unsafe { Mmap::map(&file)? };
let output = unsafe { MmapMut::map_mut(&output_file)? };

この後は,ターゲットファイルのELFヘッダを舐めてセクション情報を取ってくる.これは

use std::io::mem;

const ELF64_ADDR_SIZE: usize = mem::size_of::<u64>();
const ELF64_OFF_SIZE: usize = mem::size_of::<u64>();
const ELF64_WORD_SIZE: usize = mem::size_of::<u32>();
const ELF64_HALF_SIZE: usize = mem::size_of::<u16>();

のようなものを用意しておき,

const E_TYPE_START_BYTE: usize = 16;
const E_TYPE_SIZE_BYTE: usize = ELF64_HALF_SIZE;
const E_MACHINE_START_BYTE: usize = E_TYPE_START_BYTE + E_TYPE_SIZE_BYTE;
const E_MACHINE_SIZE_BYTE: usize = ELF64_HALF_SIZE;
const E_VERSION_START_BYTE: usize = E_MACHINE_START_BYTE + E_MACHINE_SIZE_BYTE;
const E_VERSION_SIZE_BYTE: usize = ELF64_WORD_SIZE;
const E_ENTRY_START_BYTE: usize = E_VERSION_START_BYTE + E_VERSION_SIZE_BYTE;
const E_ENTRY_SIZE_BYTE: usize = ELF64_ADDR_SIZE;

みたいにしておけばELFヘッダの各フィールドのインデックスを持てる.実際には各フィールドごとにconstを定義してある

これができればセクションヘッダ情報を取れて,

let sec_hdr_offset = u32::from_le_bytes(
     input[E_SHOFF_START_BYTE..E_SHOFF_START_BYTE + 4]
          .try_into()
          .unwrap(),
) as u64;
let sec_hdr_num = u16::from_le_bytes(
     input[E_SHNUM_START_BYTE..E_SHNUM_START_BYTE + 2]
          .try_into()
          .unwrap(),
) as u64;
let sec_hdr_size = u16::from_le_bytes(
     input[E_SHENTSIZE_START_BYTE..E_SHENTSIZE_START_BYTE + 2]
          .try_into()
          .unwrap(),
) as u64;

などとすればよい.

また,この段階でセクション一覧を取得することもしているから,これについても説明する.セクション一覧は,セクションヘッダテーブルに書かれているので,これを読み込むようなコードを書くと良い.

let sh_table_header_addr = (u16::from_le_bytes(input[62..64].try_into().unwrap()) as u64
     * sec_hdr_size
     + sec_table) as usize;

let sh_table_header =
     &input[sh_table_header_addr..sh_table_header_addr + sec_hdr_size as usize];

let sh_table_addr =
     u64::from_le_bytes(sh_table_header[24..32].try_into().unwrap()) as usize;

let mut curr_strings = -1;
let mut index = sh_table_addr;
let mut curr_byte;

while curr_strings < sec_hdr_num as isize {
     curr_byte = input[index] as isize;
     if curr_byte == 0 {
          curr_strings += 1;
     }
     index += 1;
}

sh_table_addrにはセクションヘッダテーブルの開始アドレスが入っている.このため,sec_hdr_num個のセクションを発見するまでindexを増加させることで,ループを抜ける際にindexにはセクションヘッダテーブルが終了するアドレスが入っていることになる.これにより,セクションヘッダテーブルの中身を取ってこられるため,次のようにしてセクションヘッダテーブルからセクション一覧を示す文字列を構成することができる.

let mut data_copy: Vec<u8> = vec![0; index - sh_table_addr];
data_copy.copy_from_slice(&input[sh_table_addr..index]);

for byte in &mut data_copy {
     if *byte == 0 {
          *byte = b' ';
     }
}

let sec_hdr = String::from_utf8_lossy(&data_copy).to_string();

これにより,セクション名が半角スペース区切りで入ったStringであるsec_hdrが手に入る.

ここまでの処理が完了したら,open関数はObfuscator構造体に必要なメンバを詰めてResultに包んで返す.

Ok(Obfuscator {
     input,
     output,
     sec_hdr,
     sec_hdr_num,
     sec_hdr_size,
     sec_hdr_offset,
     sec_table,
})

以上で前処理は完了.

エンディアンビットの変更

簡単で,4バイト目が1なら2,2なら1にすれば良いのでこう書ける.

pub fn change_class(&mut self) {
    self.output[4] = 3 - self.output[4];
}

アーキテクチャビットの変更

入力の5バイト目についてエンディアンと同じことをする.

pub fn change_endian(&mut self) {
    self.output[5] = 3 - self.output[5];
}

セクションヘッダ情報秘匿

前処理でターゲットのセクションヘッダ数とセクションヘッダオフセット,セクションヘッダサイズを取得しているからこれを基に埋め立てる.

pub fn nullify_sec_hdr(&mut self) {
    for i in 0..self.sec_hdr_num {
        let offset = self.sec_hdr_offset + i * self.sec_hdr_size;
        for j in offset..offset + self.sec_hdr_size {
            self.output[j as usize] = 0;
        }
    }
}

任意のセクション情報秘匿

シンボル情報の秘匿は,ここで述べる処理を.strtabに対して適用しているだけなので割愛する.

セクション情報を秘匿することを考えると,秘匿対象のセクションについて,その開始アドレスとサイズを知る必要がある.

このため,セクション名を引数に取って,該当セクションの開始アドレスとサイズのタプルを返す関数を雑に書いた:

fn get_section(&self, section: &str) -> (usize, usize) {
    let searched_idx = self.sec_hdr.find(section).unwrap_or(usize::MAX);
    if searched_idx == usize::MAX {
        panic!("section not found");
    }

    for i in 0..self.sec_hdr_num {
        let sec_hdr = self.input[(self.sec_table + i * self.sec_hdr_size) as usize
            ..(self.sec_table + (i + 1) * self.sec_hdr_size) as usize]
            .to_vec();
        let string_offset = u32::from_le_bytes(sec_hdr[0..4].try_into().unwrap());
        if string_offset == searched_idx as u32 {
            return (
                u64::from_le_bytes(sec_hdr[24..32].try_into().unwrap()) as usize,
                u64::from_le_bytes(sec_hdr[32..40].try_into().unwrap()) as usize,
            );
        }
    }

    (usize::MAX, usize::MAX)
}

self.sec_hdrには,セクション一覧が格納されているのでself.sec_hdr.find(section).unwrap_or(usize::MAX)として該当セクションのインデックスかusize::MAXが手に入る.インデックスが手に入れば各セクションについて,インデックスを調べて所望のものであれば開始アドレスとサイズをタプルに詰めて返すだけ.

このタプルが入手できれば,後はやるだけで,(開始アドレス)..(開始アドレス)+(サイズ)の間を埋め立てるだけで良い:

pub fn nullify_section(&mut self, section: &str) {
    let (section_addr, section_size) = self.get_section(section);
    if section_addr == usize::MAX {
        panic!("section not found");
    }
    for i in section_addr..section_addr + section_size {
        self.output[i] = 0;
    }
}

これで任意セクションの情報を秘匿することができる.

実行結果

壊れるやつを載せる.

エンディアンビットの変更

readelf -h,壊れる

ヘッダ情報が異常な数値を示す
$ readelf -h res_endian
ELF Header:
  Magic:   7f 45 4c 46 02 02 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, big endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              <unknown>: 300
  Machine:                           <unknown>: 0x3e00
  Version:                           0x1000000
  Entry point address:               0x6010000000000000
  Start of program headers:          4611686018427387904 (bytes into file)
  Start of section headers:          -3443564865078165504 (bytes into file)
  Flags:                             0x0
  Size of this header:               16384 (bytes)
  Size of program headers:           14336 (bytes)
  Number of program headers:         3328
  Size of section headers:           16384 (bytes)
  Number of section headers:         7936
  Section header string table index: 7680
readelf: Warning: The e_shentsize field in the ELF header is larger than the size of an ELF section header
readelf: Error: Reading 130023424 bytes extends past end of file for section headers
readelf: Error: Too many program headers - 0xd00 - the file is not that big

objdump -d,壊れる

逆アセンブルができない
$ objdump -d res_endian
objdump: res_endian: file format not recognized

gdb,壊れる

シンボルテーブルがロードできない
gdb-peda$ b main
No symbol table is loaded.  Use the "file" command.

アーキテクチャビットの変更

readelf -h,壊れる

ヘッダ情報が異常な数値を示す
$ readelf -h res_64bit
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              DYN (Shared object file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x1060
  Start of program headers:          0 (bytes into file)
  Start of section headers:          64 (bytes into file)
  Flags:                             0x0
  Size of this header:               14032 (bytes)
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           0 (bytes)
  Number of section headers:         0
  Section header string table index: 0
readelf: Warning: possibly corrupt ELF file header - it has a non-zero section header offset, but no section headers

objdump -d,壊れる

逆アセンブルができない
$ objdump -d res_64bit
objdump: res_64bit: file format not recognized

セクションヘッダ情報秘匿

readelf -S,壊れる

セクションヘッダ情報が異常な数値を示す
$ readelf -S res_sechdr
There are 31 section headers, starting at offset 0x36d0:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0] <no-strings>      NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] <no-strings>      NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 2] <no-strings>      NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 3] <no-strings>      NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 4] <no-strings>      NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 5] <no-strings>      NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 6] <no-strings>      NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 7] <no-strings>      NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 8] <no-strings>      NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 9] <no-strings>      NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [10] <no-strings>      NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [11] <no-strings>      NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [12] <no-strings>      NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [13] <no-strings>      NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [14] <no-strings>      NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [15] <no-strings>      NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [16] <no-strings>      NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [17] <no-strings>      NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [18] <no-strings>      NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [19] <no-strings>      NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [20] <no-strings>      NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [21] <no-strings>      NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [22] <no-strings>      NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [23] <no-strings>      NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [24] <no-strings>      NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [25] <no-strings>      NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [26] <no-strings>      NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [27] <no-strings>      NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [28] <no-strings>      NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [29] <no-strings>      NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [30] <no-strings>      NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  D (mbind), l (large), p (processor specific)
readelf: Error: no .dynamic section in the dynamic segment

任意のセクション情報秘匿

適当に,.strtabを秘匿した結果について見てみる.

gdb,壊れる

mainのシンボルを読めない
gdb-peda$ b main
Function "main" not defined.

また,stringsコマンドの出力を見ても,処理後にはシンボル名が出力されないことも見て取れる.

展望

パック

パッカーの機能を入れたいと思っている.パッカーとはプログラムが実行された際の動作は変えることなく,圧縮されたプログラムコードを自身の中に保持しており,実行時にはそれを自己解凍して実行するようなバイナリを作成するソフトウェア.これによりファイルサイズの削減と難読化を達成できる.

偽の命令を挿入する

x86などの命令長が可変であるようなCPUに対しては,ダミーデータを挿入することによって命令の切れ目を逆アセンブラから分からなくすることができる.例えば,ダミーデータとして,前半には命令として解釈可能なバイト列を,後半には命令の前半のバイト列を格納したり,もしくは命令の途中にジャンプするような命令を導入するなどの手法がある.

脚注
  1. ちなみに大課題というやつでは,Rustでx86_64向けのタイムトラベルデバッガを作成した.これについても時間があれば書きたいと思っている.また,講義についても大変満足しているが,担当教員が筑波大学に来たのが数年前で受講者が少ない講義だと思うので,全体のまとめも書きたい. ↩︎

  2. 任意課題も含めると毎回5~8個ほど存在した. ↩︎