4 minute read

ゲームとかアプリを作るときって、やっぱ解析されづらい最強バイナリ作りたいよね~

デバッグ検知とかだと IsDebuggerPresent だったり NtSetInformationThread だったり使ってデバッガ対策するのが淑女の嗜みだけど 普通に Windows API を呼び出そうものなら簡単にばれてしまう

普通に Windows API を呼び出す

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#[cfg(windows)]
extern crate winapi;


#[cfg(windows)]
fn main() {
    use winapi::um::debugapi::IsDebuggerPresent;
    unsafe {
        if IsDebuggerPresent() != 0 {
            println!("Debugger detected");
        } else {
            println!("No debugger detected");
        }
    }
}

これを適当にコンパイルして Ghidra で解析すると

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void UndefinedFunction_14000100c(undefined8 param_1,undefined8 param_2)

{
  BOOL BVar1;
  undefined **ppuStack_30;
  undefined8 uStack_28;
  undefined8 uStack_20;
  undefined auStack_18 [16];

  BVar1 = IsDebuggerPresent();
  if (BVar1 == 0) {
    ppuStack_30 = &PTR_s_No_debugger_detected_1400173f8;
  }
  else {
    ppuStack_30 = &PTR_s_Debugger_detected_140017420;
  }
  uStack_28 = 1;
  uStack_20 = 8;
  auStack_18 = ZEXT816(0);
  FUN_140010e20((longlong *)&ppuStack_30,param_2);
  return;
}

と IsDebuggerPresent を使っているのがばれる。

LoadLibrary を使う

LoadLibrary と GetProcAddress を使う

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#[cfg(windows)]
use winapi::um::libloaderapi::{LoadLibraryA, GetProcAddress};
use std::ffi::CString;

macro_rules! cstr {
    ($str:expr) => {
        CString::new($str).unwrap()
    };
}
#[cfg(windows)]
fn main() {
    unsafe {
        let h_module = {
            let file_name = cstr!("kernel32.dll");
            LoadLibraryA(file_name.as_ptr())
        };
        let p_is_debugger_present = {
            let func_name = cstr!("IsDebuggerPresent");
            GetProcAddress(h_module, func_name.as_ptr())
        };
        let is_debugger_present_imported = std::mem::transmute::<_, fn() -> i32>(p_is_debugger_present);
        if is_debugger_present_imported() != 0 {
            println!("Debugger is present");
        } else {
            println!("Debugger is not present");
        }
    }
}

Ghidra

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
void UndefinedFunction_1400011db(void)

{
  code *pcVar1;
  LPCSTR lpLibFileName;
  HMODULE hModule;
  FARPROC pFVar2;
  INT_PTR IVar3;
  longlong lVar4;
  undefined4 uStack_78;
  undefined4 uStack_74;
  undefined4 uStack_70;
  undefined4 uStack_6c;
  undefined4 uStack_68;
  undefined4 uStack_64;
  undefined **ppuStack_58;
  undefined8 uStack_50;
  undefined8 uStack_48;
  undefined auStack_40 [16];

  FUN_1400016e0((longlong *)&uStack_78,"kernel32.dll<redacted>",(void *)0xc);
  if (CONCAT44(uStack_74,uStack_78) == -0x8000000000000000) {
    lpLibFileName = (LPCSTR)CONCAT44(uStack_6c,uStack_70);
    lVar4 = CONCAT44(uStack_64,uStack_68);
    hModule = LoadLibraryA(lpLibFileName);
    FUN_1400010d7(lpLibFileName,lVar4);
    FUN_1400016e0((longlong *)&uStack_78,"IsDebuggerPresentDebugger is not present\n",(void *)0x11);
    if (CONCAT44(uStack_74,uStack_78) == -0x8000000000000000) {
      lVar4 = CONCAT44(uStack_64,uStack_68);
      pFVar2 = GetProcAddress(hModule,(LPCSTR)CONCAT44(uStack_6c,uStack_70));
      FUN_1400010d7((LPCSTR)CONCAT44(uStack_6c,uStack_70),lVar4);
      IVar3 = (*pFVar2)();
      if ((int)IVar3 == 0) {
        ppuStack_58 = (undefined **)&DAT_140017468;
      }
      else {
        ppuStack_58 = &PTR_s_Debugger_is_present_140017490;
      }
      uStack_50 = 1;
      uStack_48 = 8;
      auStack_40 = ZEXT816(0);
      FUN_1400112d0((longlong *)&ppuStack_58,lVar4);
      return;
    }
  }
  FUN_1400159f0("called `Result::unwrap()` on an `Err` value",0x2b,&ppuStack_58,&PTR_LAB_1400173e0,
                &DAT_140017420);
  pcVar1 = (code *)swi(3);
  (*pcVar1)();
  return;
}

これだけでもかなり分かりづらくなった。でも IsDebuggerPresent の文字があるのが気になる。

文字列難読化する

obfstr crate を使う

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#[cfg(windows)]
use winapi::um::libloaderapi::{LoadLibraryA, GetProcAddress};
use std::ffi::CString;
use obfstr::obfstr;

macro_rules! cstr {
    ($str:expr) => {
        CString::new($str).unwrap()
    };
}
#[cfg(windows)]
fn main() {
    unsafe {
        let h_module = {
            let file_name = cstr!(obfstr!("kernel32.dll"));
            LoadLibraryA(file_name.as_ptr())
        };
        let p_is_debugger_present = {
            let func_name = cstr!(obfstr!("IsDebuggerPresent"));
            GetProcAddress(h_module, func_name.as_ptr())
        };
        let is_debugger_present_imported = std::mem::transmute::<_, fn() -> i32>(p_is_debugger_present);
        if is_debugger_present_imported() != 0 {
            println!("Debugger is present");
        } else {
            println!("Debugger is not present");
        }
    }
}

Ghidra 長いので一部省略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
void UndefinedFunction_140001219(void)
{
  while (bVar8) {
    *(ulonglong *)(auStack_68 + lVar6) =
         ((ulonglong)(byte)(&DAT_140017430)[lVar6] | 0xb8c06533fd74dc00) ^
         *(ulonglong *)(lVar3 + lVar6);
    lVar6 = 8;
    bVar8 = false;
  }
  for (uVar7 = 8; uVar7 < 0xc; uVar7 = uVar7 + 4) {
    *(uint *)(auStack_68 + uVar7) =
         (uint)(byte)(&UNK_140017432)[uVar7] * 0x10000 + (uint)*(ushort *)(&DAT_140017430 + uVar7) +
         0x38000000 ^ *(uint *)(lVar3 + uVar7);
  }
  uStack_30 = (undefined4)uStack_60;
  lStack_38 = (longlong)auStack_68;
  FUN_140001820((longlong *)&uStack_88,&lStack_38,(void *)0xc);
  if (CONCAT44(uStack_84,uStack_88) == -0x8000000000000000) {
    lpLibFileName = (LPCSTR)CONCAT44(uStack_7c,uStack_80);
    lVar3 = CONCAT44(uStack_74,uStack_78);

    hModule = LoadLibraryA(lpLibFileName);

    FUN_1400010d7(lpLibFileName,lVar3);
    auStack_68 = (undefined  [8])0x140011192;
    auStack_68._0_4_ = 0xef43362b;
    lVar3 = FUN_1400011f5(0x140011192,0xef43362b);
    _auStack_68 = ZEXT816(0);
    for (uVar7 = 0; uVar7 < 0x10; uVar7 = uVar7 + 8) {
      *(ulonglong *)(auStack_68 + uVar7) =
           *(ulonglong *)((longlong)&DAT_14001743c + uVar7) ^ *(ulonglong *)(lVar3 + uVar7);
    }
    bStack_28 = *(byte *)(lVar3 + 0x10) ^ 0x65;
    lStack_38 = SUB168(_auStack_68,0);
    uStack_30 = (undefined4)uStack_60;
    uStack_2c = uStack_60._4_4_;
    FUN_140001820((longlong *)&uStack_88,&lStack_38,(void *)0x11);
    if (CONCAT44(uStack_84,uStack_88) == -0x8000000000000000) {

      pFVar4 = GetProcAddress(hModule,(LPCSTR)CONCAT44(uStack_7c,uStack_80));

    }
  return;
}

これで完璧?いいえまだです。

debugger_result1

デバッガを使うと GetProcAddress の引数がばれてしまいます。

GetProcAddress を改造

ループ処理で dll の関数名からハッシュ値を生成し比較。マッチしたら返す。ハッシュアルゴリズムには fnv-1a の 64bit を使用。ついでにマクロを作成。ちょっとした嫌がらせ程度にハッシュ関数をインライン展開。

ちょっとした裏話:最初は windows-rs crate を使っていたんだけど IMAGE_NT_HEADERS とかがうまくうごかなかったり調子が悪そうだったので winapi に変更した。

以下コード。長くなってきたので主な追加箇所のみ

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
macro_rules! func_to_hash {
    ($function_name:expr) => {
        fnv1a_64($function_name)
    };
}

fn get_proc_address(dll_handle: HMODULE, func_hash: u64) -> *mut __some_function {
    unsafe {
        let p_dos_hdr = dll_handle as *mut IMAGE_DOS_HEADER;
        let p_nt_hdr = (dll_handle as *mut u8).add((*p_dos_hdr).e_lfanew as usize) as *mut IMAGE_NT_HEADERS;
        let p_export_table = (dll_handle as *mut u8).add((*p_nt_hdr).OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT as usize].VirtualAddress as usize) as *mut IMAGE_EXPORT_DIRECTORY;
        let number_of_names = (*p_export_table).NumberOfNames;
        let functions_address = (dll_handle as *mut u8).add((*p_export_table).AddressOfFunctions as usize) as *mut u32;
        let functions_names = (dll_handle as *mut u8).add((*p_export_table).AddressOfNames as usize) as *mut u32;
        let functions_ordinals = (dll_handle as *mut u8).add((*p_export_table).AddressOfNameOrdinals as usize) as *mut u16;

        let mut ret = null_mut();
        for i in 0..number_of_names {
            let name = (dll_handle as *mut u8).add(*functions_names.add(i as usize) as usize);
            let name = std::ffi::CStr::from_ptr(name as *const i8).to_string_lossy();
            if fnv1a_64(&name) == func_hash {
                ret = (dll_handle as *mut u8).add(*functions_address.add(*functions_ordinals.add(i as usize) as usize) as usize) as *mut __some_function;
            }
        }
        ret
    }
}

#[inline(always)]
fn fnv1a_64(s: &str) -> u64 {
    let mut hash: u64 = 0xcbf29ce484222325;
    for c in s.as_bytes() {
        hash ^= *c as u64;
        hash = hash.wrapping_mul(0x100000001b3);
    }
    hash
}

マッチしたタイミングでループを抜けるとループの直後にブレークポイントを設置するとばれた。

なのでループ脱出後に return するようにした。

debugger_result2

まとめ

これで CTF の rev 問でもつくったら楽しそうだけど解けるのかな?

更新日時: