Understanding Alcatraz ~ Obfuscator Analysis [TR]
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)}")
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_*))
}
