Skip to main content

Command Palette

Search for a command to run...

Understanding Alcatraz ~ Obfuscator Analysis [TR]

Updated
9 min read

Bu yazının Türkçe versiyonu özel olarak TTMO için Türkiye'deki tersine mühendislik topluluğuna hazırlandı.

Introduction

Özellikle zararlı yazılım geliştiricileri ile kaynak kodunu korumak isteyen kullanıcılar tarafından sıklıkla tercih edilen binary-to-binary (bin2bin) obfuscator’lar, sahip oldukları gelişmiş tekniklerle birlikte her geçen gün zararlı yazılım analizcilerinin ve tersine mühendislik uzmanlarının işini daha da zorlaştırmakta, analiz süreçlerini karmaşıklaştırmaktadır. Bu yazıda, bu araçlardan bir tanesi olan “Alcatraz” obfuscator’ın süreçlerinin analizi yapılmıştır.

Alcatraz Obfuscator

Alcatraz, x64 tabanlı PE dosyalarını obfuscate edebilen bir obfuscator olup, içerdiği tekniklerle birlikte açık kaynak kodlu binary-to-binary obfuscator çözümleri arasında öne çıkmaktadır.

  • Obfuscation of immediate moves

  • Control flow flattening

  • ADD mutation

  • Entry-point obfuscation

  • Lea obfuscation

  • Anti disassembly

Direkt olarak bir sample üzerinde (crackme, malware etc.) analiz yapmıyorsak, kendi hazırladığımız bir dosya üzerinde hedef obfuscator'un yaptığı değişiklikleri daha rahat gözlemleyebiliriz. Bunun için analiz etmeden önce bir test uygulaması hazırladım.

#include <stdio.h>

char* func1() {
    return "0xreverse.com";
}

int func2() {
    return 24;
}

int main()
{
    printf("%s\n", func1());
    printf("%d\n", func2());
    getchar();
    return 1;
}

Release (x64) modunda derlediğim bu uygulamayı Alcatraz üzerinde tüm özellikler açık olacak şekilde obfuscate ettim. Yazının devamında bu test uygulamasından "sample" olarak bahsedeceğim.

Analysis

Entry-Point Obfuscation

Sample'ı IDA üzerinde açtığımda karşıma gelen ilk fonksiyonun (Entry-Point) PE Header'lar ile bir işlem yaptığını gördüm.

ImageBaseAddress = (char *)NtCurrentPeb()->ImageBaseAddress;
v7 = &ImageBaseAddress[*((int *)ImageBaseAddress + 15)];
v8 = *((unsigned __int16 *)v7 + 3);
v9 = (__int64)&v7[*((unsigned __int16 *)v7 + 10) + 24];

IDA, yapıları tanımadığı için bunları pointer işlemleriyle gösteriyor. Bu süreci iyileştirmek için Type libraries (Shift + F11 veya View > Open Subviews) sekmesinden MS SDK (Windows 7 x64) Type'larını içeriye aktardım. Değişkenlerin tipini değiştirmek üzere Structures (Shift + F9) sekmesinden kullanacağım tipleri Ins tuşuna basarak "standard structure" olarak ekledim. Structure’ları ekledikten sonra koddaki değişkenleri uygun tiplere dönüştürdüm.

dosHeader = (IMAGE_DOS_HEADER *)NtCurrentPeb()->ImageBaseAddress;
ntHeader = (IMAGE_NT_HEADERS *)((char *)dosHeader + dosHeader->e_lfanew);
NumberOfSections = ntHeader->FileHeader.NumberOfSections;
firstSectionHeader = (IMAGE_SECTION_HEADER *)((char *)&ntHeader->OptionalHeader
                                          + ntHeader->FileHeader.SizeOfOptionalHeader);
if (ntHeader->FileHeader.NumberOfSections)
{
    qmemcpy(obfuscatorSection, ".0Dev", sizeof(obfuscatorSection));
    do
    {
      v10 = obfuscatorSection;
      v11 = *(_OWORD *)&firstSectionHeader->SizeOfRawData;
      v12 = *(_QWORD *)&firstSectionHeader->NumberOfRelocations;
      hasSectionName = _mm_cvtsi128_si32(*(__m128i *)firstSectionHeader->Name);
      *(_OWORD *)selectedSectionName.Name = *(_OWORD *)firstSectionHeader->Name;
      *(_QWORD *)&selectedSectionName.NumberOfRelocations = v12;
      *(_OWORD *)&selectedSectionName.SizeOfRawData = v11;
      if ( hasSectionName )
      {
        a3 = (char *)&selectedSectionName - obfuscatorSection;
        v14 = hasSectionName;
//...

Bu işlemler sonrası start fonksiyonu daha kolay anlamlandırabilir hale geldi. Fonksiyon en başta ismi 0Dev olan section'u bulmaya çalışıyor. Aşağıda da bu section'ın header'ı içerisindeki bazı değerler üzerinde matematiksel operasyonlar yaparak Original Entry-Point'i hesaplıyor.

return ((__int64 (__fastcall *)(_QWORD, signed __int64, signed __int64, IMAGE_SECTION_HEADER *))((char *)dosHeader + (unsigned int)__ROR4__(LODWORD(ntHeader->OptionalHeader.SizeOfStackCommit) ^ *(_DWORD *)((char *)&dosHeader->e_magic + v3->VirtualAddress), ntHeader->FileHeader.TimeDateStamp)))(
       a2,
       v4,
       a3,
       firstSectionHeader);

Kaynak kodu üzerinden de bu süreci teyit edebiliriz:

__declspec(safebuffers) int obfuscator::custom_main(int argc, char* argv[]) {
    auto peb = (uint64_t)__readgsqword(0x60); // peb
    auto base = *(uint64_t*)(peb + 0x10); // peb + 0x10 = DOS Header (MZ)
    PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)(base + ((PIMAGE_DOS_HEADER)base)->e_lfanew);

    PIMAGE_SECTION_HEADER section = nullptr;
    auto first = IMAGE_FIRST_SECTION(nt);
    for (int i = 0; i < nt->FileHeader.NumberOfSections; i++) {
        auto currsec = first[i];

        char dev[5] = { '.', '0', 'D', 'e', 'v' };
        if (!_strcmp((char*)currsec.Name, dev)) {
            section = &currsec;
        }
    }

    uint32_t real_entry = *(uint32_t*)(base + section->VirtualAddress);
    real_entry ^= nt->OptionalHeader.SizeOfStackCommit;
    real_entry = _rotr(real_entry, nt->FileHeader.TimeDateStamp);
    return reinterpret_cast<int(*)(int, char**)>(base + real_entry)(argc, argv);
}

Writing OEP Finder with Qiling Framework

Tüm bu işlemlerin birçok çözüm yolu olabilir (örn. statik olarak binary üzerinde LIEF gibi pe parser kütüphaneleri hesaplamayı yapmak). Ben tüm bu işlemin Qiling Framework ile nasıl çözülebileceğini göstermek istiyorum. IDAPython apilerini kullanarak sample'ın entry-point'ini ve bu adrese ait fonksiyonun başlangıç bitiş adresini alabiliriz.

from qiling import Qiling
from qiling.const import QL_VERBOSE  

# Qiling Version: 1.4.8 f4464b95
# IDA Pro Version 7.7

ROOTFS_PATH = r"ROOTFS_PATH"

def find_oep(sample_path, func) -> int:
    # I added the log_devices parameter because I got the following error.
    # TypeError: unexpected logging device type: IDAPythonStdOut
    ql = Qiling([sample_path], rootfs=ROOTFS_PATH, verbose=QL_VERBOSE.OFF, log_devices=[])
    # To prevent Qiling from continuing emulation, the RAX value
    # must be taken one instruction before the jmp opcode is executed.
    ql.run(begin=func.start_ea, end=func.end_ea - 0x4) # - jmp opcode size
    original_entry_point = ql.arch.regs.read("RAX")
    print(f"[+] Found Original Entry Point: 0x{original_entry_point:x}")
    return original_entry_point


def main():
    print("[~] 0xReverse - Alcatraz Deobfuscator IDA Script [~]")
    sample_path = idaapi.get_input_file_path()
    # IDAPython CheatSheet
    # https://gist.github.com/icecr4ck/7a7af3277787c794c66965517199fc9c
    info = idaapi.get_inf_structure()
    entrypoint = info.start_ea  # EntryPoint start address
    if func := ida_funcs.get_func(entrypoint):
        function_name = idc.get_func_name(entrypoint)
        print(f"[+] Emulating Function Name: {function_name}, Address: {entrypoint:x}")        
        original_entry_point = find_oep(sample_path, func)
        # Rename OEP address to original_entry_point
        idc.set_name(original_entry_point, "original_entry_point", idc.SN_NOWARN)
    else:
        print(f"[-] This address is not a function")
        exit(-1)

main()

Yazdığım bu scripti File > Script File... (Alt + F7) ile çalıştırdıktan sonra aldığım çıktı bu:

[~] 0xReverse - Alcatraz Deobfuscator IDA Script [~]
[+] Emulating Function Name: start, Address: 14000e2ba
[+] Found Original Entry Point: 0x140001340

Hedef adrese ait fonksiyonun ismini de original_entry_point olarak değiştirdiğimiz için Jump to Address (G) yapmak yerine direkt Functions sekmesinde bu fonksiyon ismiyle arama yaptığımızda gerçek entry-point noktasını görebiliyoruz.

LEA Obfuscation

lea rdx, cs:7FF7D996FE33h
pushf
sub rdx, 421FDBD3h
popf
lea rcx, cs:7FF7FD145F19h
pushf
sub rcx, 659D3CA9h
popf
call loc_7FF797778E4A

Load Effective Address (LEA) komutu hedef adresi belirlenen register’a yükleme işlemini gerçekleştiriyor. pushf ve popf ile sarmalanmış sub komutu ise yüklenen rdx register’ına yüklenen adresten belirli bir değeri çıkartarak gerçek adresi açığa çıkartıyor.

pushf ve popf ile sarmalanmasının sebebi CPU üzerindeki RFLAGS’lerin sub işleminden etkilenmemesini sağlamak. pushf ile RFLAGS hafızaya yüklenir ve popf ile korunan RFLAGS geri yüklenir.

Dolayısıyla yukardaki işlemin gösterimi:

$$RDX = 0x7FF7D996FE33 - 0x421FDBD3$$

$$RCX = 0x7FF7FD145F19 - 0x659D3CA9$$

Alcatraz Codebase içerisinde lea.cpp dosyası içerisinde de bunu nasıl hesapladığını görebilirsiniz:

auto rand_add_val = distribution(generator);
instruction->location_of_data += rand_add_val;
assm.pushf();
assm.sub(x86_register_map->second, rand_add_val);
assm.popf();
void* fn;
auto err = rt.add(&fn, &code);

Debugger üzerindeki gösterimi:

Anti-Disassembly

Buradaki açıklamaya göre 0xFF ile başlayan talimatı 0xEB 0xFF haline çevirmesi lineer disassembler’ları şaşırtıyor. Buradaki tekniği (Impossible Disassembly) ve diğer anti-* teknikleri farklı bir blog yazısında anlatmayı planladığım için burada detayına girmeyeceğim. Kaynak kodu üzerinden bunu nasıl yaptığını açıkça görebiliyoruz.

bool obfuscator::obfuscate_ff(std::vector<obfuscator::function_t>::iterator& function, std::vector<obfuscator::instruction_t>::iterator& instruction) {

    instruction_t conditional_jmp{}; conditional_jmp.load(function->func_id, { 0xEB });
    conditional_jmp.isjmpcall = false;
    conditional_jmp.has_relative = false;
    instruction = function->instructions.insert(instruction, conditional_jmp);
    instruction++;

    return true;
}

MOV & ADD Obfuscation

LEA obfuscation’da bahsettiğim gibi pushf ve popf ile sarmalanmış birtakım işlemleri burada da yapıyor.

mov     edx, 0AA045890h
pushf
not     edx
add     edx, 0E6CAF175h
xor     edx, 0B7625898h
rol     edx, 0C4h
popf
pushf
not     edx
add     edx, 811E9B28h
xor     edx, 0C6E2935Fh
rol     edx, 0Fh
popf

Control Flow Obfuscation

Fonksiyon içerisindeki normal akışı yeniden oluşturulan bloklar içerisine yerleştirerek akış okumayı zorlaştırma işlemi gerçekleştiren kısım. Main fonksiyonun normal akışı bu iken:

Buna çeviriyor (graph view):

Pseudo-Code gösterimi:

Writing MOV Calculator Script with IDAPython

3.4 başlığında bahsettiğim MOV Obfuscation işlemine yönelik bir hesaplayıcı yazmak gerekiyor. Bu script’te kullanılacak olan fonksiyonlar Control Flow Unflattening işlemi için de gerekli olacak.

Scriptin main fonksiyonunda IDA üzerinde seçilen adres alınarak analyze_mov_obfuscation fonksiyonu çağırılıyor.

def main():
   print("[~] 0xReverse - Alcatraz Deobfuscator IDA Script [~]")
   # Select a mutated function on IDA Screen
   start_address = idaapi.get_screen_ea()
   if selected_function := ida_funcs.get_func(start_address):
      function_name = idc.get_func_name(start_address)
      print(f"[+] Analyzing Function Name: {function_name}, Address: {start_address:x}")
      deobfuscated_mov_ops = analyze_mov_obfuscation(func=selected_function)
      print(f"[+] Deobfuscated {deobfuscated_mov_ops} MOV obfuscation")
   else:
        print(f"[-] This address is not a function")
        exit(-1)

analyze_mov_obfuscation fonksiyonu, kısaca hedef fonksiyon içerisinde dolaşıp aşağıdaki pattern’i bulduktan sonra gerekli değeri hesaplıyor.

pattern:
{
    mov     eax, VALUE
    pushf
    not     eax
    add     eax, VALUE
    xor     eax, VALUE
    rol     eax, VALUE
    popf
}

analyze_mov_obfuscation fonksiyonunu yazmadan önce ROL, ROR ve pushf&popf ile sarmalanmış operasyonları gerçekleştiren fonksiyonları da script içerisine eklemek gerekiyor:

rol = lambda val, r_bits, max_bits: ((val << (r_bits % max_bits)) & (2**max_bits - 1)) | ((val & (2**max_bits - 1)) >> (max_bits - (r_bits % max_bits)))
ror = lambda val, r_bits, max_bits: ((val & (2**max_bits - 1)) >> (r_bits % max_bits)) | ((val << (max_bits - (r_bits % max_bits))) & (2**max_bits - 1))
MASK = 0xFFFFFFFF

def calculate_mov_value(init_value, add_value, xor_value, rol_value):
   # mov     eax, VALUE
   calculated_value = init_value
   # not     eax         -> bitwise NOT
   calculated_value = (~calculated_value) & MASK
   # add     eax, VALUE
   calculated_value = (calculated_value + add_value) & MASK
   # xor     eax, VALUE
   calculated_value = calculated_value ^ xor_value
   # rol     eax, VALUE
   calculated_value = rol(calculated_value, rol_value, 32)
   return calculated_value

Bu pattern için sadece baştaki mov ve pushf değerlerini kontrol etmek yeterli. Bu yüzden current_address değerinin bulunduğu instruction ve bir sonraki instruction’u kontrol ediyoruz:

# Get function start address
current_address = func.start_ea
while current_address < func.end_ea:
  instruction = idautils.DecodeInstruction(current_address)
  if not instruction:
     break

  if instruction.itype == idaapi.NN_mov:
     # Check if the next opcode is pushf
     next_instruction = idautils.DecodeInstruction(current_address + instruction.size)
     if next_instruction.itype == idaapi.NN_pushf:
        print("[+] MOV Obfuscation pattern found!")
        ...
     else:
        current_address += instruction.size
  else:
     current_address += instruction.size

Initial adres bir register’a atıldıktan sonra özel hesaplama işleminin kaç kere yapıldığı random şekilde seçildiğinden dolayı bu pattern’i bir kere hesaplayamıyoruz. Bu yüzden yine bir döngü içerisinde belirli opcode’lar dışında farklı bir opcode gelene kadar devam etmek gerekiyor. Bu işlemin 3 kere yapılan bir örneği de aynı fonksiyon içerisinde bulunuyor:

mov     ecx, 5600B3EBh
pushf
not     ecx
add     ecx, 84DD6D12h
xor     ecx, 84F2EEE7h
rol     ecx, 2Bh
popf
pushf
not     ecx
add     ecx, 0B4156550h
xor     ecx, 0B460F07Dh
rol     ecx, 5Bh
popf
pushf
not     ecx
add     ecx, 0E02D13C8h
xor     ecx, 0C483568Bh
rol     ecx, 66h
popf

Script içerisinde hesaplama işlemini de bu şekilde yapıyoruz:

calculated_value = 0
add_value = xor_value = rol_value = 0
# Get value from {mov eax, VALUE} and MASK for 32bit
initial_value = instruction.ops[1].value & MASK
# Add current_address and mov and pushf
current_address += instruction.size + next_instruction.size
while True:
   current_instruction = idautils.DecodeInstruction(current_address)
   if current_instruction.itype == idaapi.NN_not:
      current_address += current_instruction.size
   elif current_instruction.itype == idaapi.NN_add:
      add_value = current_instruction.ops[1].value & MASK
      current_address += current_instruction.size
   elif current_instruction.itype == idaapi.NN_xor:
      xor_value = current_instruction.ops[1].value & MASK
      current_address += current_instruction.size 
   elif current_instruction.itype == idaapi.NN_rol:
      rol_value = current_instruction.ops[1].value & MASK
      current_address += current_instruction.size
   elif current_instruction.itype == idaapi.NN_popf:
      calculated_value = calculate_mov_value(
         init_value=initial_value,
         add_value=add_value,
         xor_value=xor_value,
         rol_value=rol_value
      )
      current_address += current_instruction.size
   elif current_instruction.itype == idaapi.NN_pushf:
      # The pattern we are looking for can be repetitive,
      # so we need to keep the process going.
      initial_value = calculated_value
      current_address += current_instruction.size
   else:
      break
print(f"[+] Calculated MOV value: {hex(calculated_value)}")

Github Repo

YARA Rule

import "pe"

rule SUSP_EXE_Alcatraz_Obfuscator_April_23 { 
    meta:
        description = "This rule detects samples obfuscated with Alcatraz."
        author      = "Utku Corbaci (rhotav) / 0xReverse"
        date        = "2025-04-23"
        sharing     = "TLP:CLEAR"
        tags        = "windows,exe,suspicious,obfuscator"
        os          = "Windows"

    strings:
        // B8 41 CD A8 27   mov     eax, 27A8CD41h
        // 66 9C            pushf
        // F7 D0            not     eax
        // 05 AB FD E1 DD   add     eax, 0DDE1FDABh
        // 35 CA 3C 0F BF   xor     eax, 0BF0F3CCAh
        // C1 C0 62         rol     eax, 62h
        // 66 9D            popf
        $obfuscation_mov = {B8 ?? ?? ?? ?? 66 9C F7 D0 05 ?? ?? ?? ?? 35 ?? ?? ?? ?? C1 C0 ?? 66 9D}

        // 48 8D 05 74 81 47 77        lea     rax, cs:1B748621Eh
        // 66 9C                       pushf
        // 48 2D A6 2B 48 77           sub     rax, 77482BA6h
        // 66 9D                       popf
        $obfuscation_lea = {48 8D ?? ?? ?? ?? ?? 66 9C 48 2D ?? ?? ?? ?? 66 9D}
    condition: 
        pe.is_pe
        and for any i in (0..pe.number_of_sections - 1): (
            (pe.sections[i].name == ".0Dev")
        )
        and (all of ($obfuscation_*))
}

unpac.me Hunting Results

References

  1. https://keowu.re/posts/Analyzing-Mutation-Coded-VM-Protect-and-Alcatraz-English

  2. https://gist.github.com/icecr4ck/7a7af3277787c794c66965517199fc9c

  3. https://python.docs.hex-rays.com/

  4. https://grazfather.github.io/posts/2016-09-18-anti-disassembly/

  5. https://gist.github.com/trietptm/5cd60ed6add5adad6a34098ce255949a

  6. https://phrack.org/issues/71/15