← Back to list

Sıfırdan C2 Framework’ü İnşa Etmek: Mimariye Derinlemesine Bakış

81,727 satır C++17, Go, Kotlin, ve TypeScript — ve arkalarındaki tüm dizayn meseleleri. Versiyon: v1.0.0

Yiğit Aydemir · 2026-05-11 01:56 · 1 claps · 13.9 min read
#c2-framework #command-and-control #c2 #antivirus-evasion #edr-evasion
Open on Medium ↗
Wiki topics: MIC · Microbiology & Immunology 🌐 · Web Development 📱 · Mobile Development 🥊 · Combat Sports

Sıfırdan C2 Framework’ü İnşa Etmek: Mimariye Derinlemesine Bakış

81,727 satır C++17, Go, Kotlin, ve TypeScript — ve arkalarındaki tüm dizayn meseleleri. Versiyon: v1.0.0

TelekinetiC2'yi, sıfırdan, authorized red team operasyonları için modern bir Komuta ve Kontrol framework’ü olarak geliştirdim. Cobalt Strike veya Havoc’un bir kopyası olarak değil, her katmanın ardındaki gerçek mühendisliği anlamanın bir yolu olarak geliştirdim. Bunlardan bazıları: Şifrelenmiş beacon göndermenin byte düzeyinde nasıl çalıştığı, yalnızca bellekte bulunan payloadların işlem restartlarından nasıl sağ çıktığı, bir ajanın sleep zamanındayken nasıl görünmez kaldığı gibi konular… Bu yazı, pazarlama diliyle anlatılan versiyonu değil, gerçek kararları, ödünleri ve bazen acı verici dersleri içeren eksiksiz bir mimariyi ele alıyor.

Sisteme Genel Bakış

Listener-Ajan Network Haritası

Listener-Ajan Network Haritası

Cross-Platform Ajanlar

Cross-Platform Ajanlar

telekinetiC2, yedi bileşenli dağıtık bir sistemdir. Hedef makinelerdeki ajanlar, yalnızca HTTPS üzerinden Listener sunucusuyla iletişim kurar. Listener sunucusu hiçbir zaman doğrudan Admin sunucusuyla iletişim kurmaz, yalnızca Redis üzerinden iletişim kurarlar. Admin sunucusu, WebSocket ve REST üzerinden React web arayüzünü çalıştırır.

┌──────────────────────────────────────────────────────────────┐
│                        Operator Workstation                  │
│  ┌──────────────────────────────────────────────────────┐    │
│  │         Web Interface (React 19 + TypeScript)        │    │
│  │  Dashboard · Agent Manager · Network Map · Builder   │    │
│  └──────────────────┬───────────────────────────────────┘    │
│           Websocket │ REST API                               │
└─────────────────────┼────────────────────────────────────────┘
                      ▼                          
          ┌───────────────────────┐
          │    Admin Server (Go)  │ ◄─── PostgreSQL (agents, tasks, vault)
          │  JWT · RBAC · Builder │ ◄─── Redis (session keys, agent state)
          └──────────┬────────────┘
                     │ Redis Pub/Sub & Task Queues
          ┌──────────▼────────────┐
          │  Listener Server (Go) │
          │  Binary protocol +    │
          │  Hybrid crypto layer  │
          └──────────┬────────────┘
                     │ HTTPS  (AES-256-GCM inside RSA-4096 envelope)
          ┌──────────▼────────────┐
          │         Agent         │
          │  C++17 (Linux/Win)    │
          │  Kotlin+JNI (Android) │
          └───────────────────────┘

Temel mimari karar: Listener ve Admin sunucuları tamamen birbirinden bağımsızdır. Asla doğrudan iletişim kurmazlar. Listener’lar, ajanların bağlandığı burner(feda edilebilir) bir IP adresinde bulunur; Admin sunucusu ise başka bir makinede yer alır. Bir listener sunucusu devre dışı bırakılırsa (IP adresi kara listeye alınırsa, trafiği yakalanırsa, hacklenirse), Admin sunucusu ve depolanan tüm veriler etkilenmeden kalır. Koordinasyon yalnızca Redis Pub/Sub ve list işlemleri aracılığıyla gerçekleşir.

Tavsiye edilen framework kurulumu şu şekildedir: Listener sunucuları dışarıya expose edilmiş, konteynerda çalışan VPS’lerdir(önlerinde cloudflare, nginx gibi hizmetler ile gerçek IP adresleri korunabilir). Admin sunucusu, Redis ve Postgres aynı makinede(OPSEC açısından farklı makinelerde olmaları daha iyidir fakat operasyon hızımızı düşürür), ayrı konteynerlarda çalışırlar. Her bir listener sunucusu, Wireguard ile Redis’in de bu ağa dahil olduğu bir VPN ağına bağlıdır. Böylece listener sunucuları bu VPN ile Redis’le iletişim kurar ve Redis’in gerçek IP’si ifşa olmaz.(listener sunucusunun hacklenmesi senaryosunda)

Bileşenlerin Rolleri ve Detayları

[embed]

İletşim Katmanı: Hibrit Şifreleme + Binary Protokolü

Listener sunucuları ve ajanlar arasında iletilen her istek/cevap iki katmanlı bir şifreleme şemasından geçiyor ve veri paketi JSON değil, kompakt bir binary formattadır.

Neden JSON Değil?: Aslında bir C2 protokolü için JSON açık ara uygulanması en uygun seçeneklerden biridir. Her Go/C++/Kotlin kütüphanesi bunu kolayca halleder. Ancak bu kullanım durumu için üç sorun mevcut:

  1. Field adları bellek dumplarında görünür: “hostname”, “agent_id”, “task_type” gibi stringler, heap dumplarında, ağ trafik incelemelerinde ve bellek tarayıcıların çıktısında aynen görünür. Binary formatta field adları sızdırılmaz.
  2. Parser footprint’leri: nlohmann/json’ı C++ ajanına bağlamak, binary dosyaya ~180 kB şablon örneklemesi ekledi. Bunu kaldırmak ve kompakt bir binary encoder yazmak, bu sorunu tamamen ortadan kaldırdı.
  3. Yapısal öngörülebilirlik: JSON parserları, protokol fingerprint oluşturucularının eşleştirebileceği tanınabilir kalıplar (tırnak işaretleri(“”), colon ayırıcılar(:), parantez nesting({[]})) bırakır. Sabit ofsetli binaryformatta bu tür ipuçları yoktur.

İletişimde Kullanılan Zarf Yapıları

Ajan ile dinleyiciler arasındaki tüm trafik aynı dış zarf yapısını kullanır, ancak ilk temas (register) ve sonraki tüm trafik (beaconing) arasında anlamsal farklılıklar vardır.

Registration zarfı:
┌─────────────────────────────────────────────────────────────────┐
│ Offset │ Size  │ Field                                          │
├────────┼───────┼────────────────────────────────────────────────┤
│   0    │   1   │ version = 0x01                                 │
│   1    │  512  │ RSA-4096-OAEP( AES-256 session key )           │
│  513   │  12   │ AES-GCM nonce (random, per-request)            │
│  525   │  N+16 │ AES-GCM ciphertext + authentication tag        │
|525+N+16| 0-255 | Random junk data(all packets have random size) |
└─────────────────────────────────────────────────────────────────┘

Beacon zarfı:
┌─────────────────────────────────────────────────────────────────┐
│ Offset │ Size  │ Field                                          │
├────────┼───────┼────────────────────────────────────────────────┤
│   0    │   1   │ version = 0x01                                 │
│   1    │  512  │ RSA-4096-OAEP( session UUID — 16 bytes )       │
│  513   │  12   │ AES-GCM nonce                                  │
│  525   │  N+16 │ AES-GCM ciphertext + authentication tag        │
|525+N+16| 0-255 | Random junk data(all packets have random size) |
└─────────────────────────────────────────────────────────────────┘

İnce fark şudur: Registration sırasında, RSA bloğu 32 baytlık AES oturum anahtarını taşır. Sonraki her beacon’da ise 16 baytlık oturum UUID’sini taşır. Listener’lar, RSA private keyleri ile RSA bloğunu çözerek buradan elde ettikleri UUID’yi kullanarak oturum anahtarını Redis’ten elde eder.

Bu neden önemli? Bir saldırgan, listener server’ı hackleyip RSA private keyini ele geçirse bile, geçmişe ait beacon trafiğini çözemez. AES anahtarları bellekte oluşturulur ve Redis’te belli bir TTL ile saklanır; asla ağ üzerinden tekrar görünmez. UUID, yalnızca sunucu tarafında var olan Redis eşlemesi olmadan anlamsızdır.

┌──────────────────────────────────────────────────────────────────────┐
│ Agent                     │ Wire                │ Listener           │
│                           │                     │                    │
│  Plaintext binary struct  │                     │                    │
│  ──AES-256-GCM──►         │ nonce+ciphertext+tag│                    │
│  RSA-OAEP(session_key)    │ ──────────────────► │ RSA decrypt        │
│                           │                     │ → AES key (reg)    │
│                           │                     │ → UUID (beacon)    │
│                           │                     │   └→ Redis lookup  │
│                           │                     │ AES-GCM decrypt    │
│                           │                     │ → plaintext struct │
└──────────────────────────────────────────────────────────────────────┘

Binary Protokolü

İç payload(AES-GCM’nin koruduğu şey), sabit ofsetli little endian binary formatındadır. Şu an desteklenen görev(task) türü tablosu:

// binary_proto.hpp
static constexpr uint8_t TASK_TYPE_SHELL            = 0x00;
static constexpr uint8_t TASK_TYPE_RECON            = 0x01;
static constexpr uint8_t TASK_TYPE_FILE_MANAGER     = 0x02;
static constexpr uint8_t TASK_TYPE_FILE_TRANSFER    = 0x03;
static constexpr uint8_t TASK_TYPE_MODULE_MANAGER   = 0x04;
static constexpr uint8_t TASK_TYPE_LOCATION_MANAGER = 0x05;
static constexpr uint8_t TASK_TYPE_CORE_CONTROL     = 0x06;
static constexpr uint8_t TASK_TYPE_PERSISTENCE_MGR  = 0x07;

Bir beacon isteği, aynı binary çerçeve içinde ajanın canlı telemetri verilerini taşır: CPU/RAM kullanımı, çalışma süresi, aktif modül bildirimi, GPS koordinatları (Android, opsiyonel) ve sunucu tarafı konum belirleme için yakındaki WiFi BSSID’leri(Masaüstü, opsiyonel); bunların tümü tek bir string field olmadan paketlenmiştir.

Sunucu Mimarisi: Redis Message Bus

Listener ve Admin sunucuları asla doğrudan iletişim kurmaz. Tüm koordinasyon Redis üzerinden gerçekleşir:

Agent → Listener:  POST /api/v1/REG_ENDPOINT   (register)
Listener → Redis:  HSET c2:agent:<id> ...
                   HSET c2:session:<session_id> aes_key ...
                   PUBLISH c2:event:agent:register

Redis → Admin:     (subscriber receives event)
Admin → Postgres:  INSERT INTO agents ...
Admin → WebSocket: broadcast "agent:register" to UI clients

─────────────────────────────────────────────────────────

Agent → Listener:  POST /api/v1/BEACON_ENDPOINT  (beacon)
Listener → Redis:  LPOP c2:pending_tasks:<agent_id>
Listener → Agent:  binary response (tasks)

─────────────────────────────────────────────────────────

UI → Admin:  POST /tasks  (operator dispatches task)
Admin → Redis:  RPUSH c2:pending_tasks:<agent_id>

Task kuyruğu bir Redis listesidir. Listener, her beacon’da LPOP ile taskları listeden çıkarır ve binary yanıtla ajana geri gönderir. Admin sunucusu taskları gönderir (RPUSH) ve durum olaylarını publish eder. Her iki sunucunun da diğerinin şu anda ne yaptığını bilmesine gerek yoktur.

Redis key schema:

c2:agent:<agent_id>              Hash    Runtime state, metrics, location, modules
c2:session:<session_id>          Hash    { agent_id, aes_key } — TTL = beacon_interval + grace
c2:pending_tasks:<agent_id>      List    Task queue (FIFO, LPOP by listener)
c2:beacons                       ZSet    Agent last-seen Unix timestamps
c2:module:<id>:<ver>:<os>:<arch> String  Raw module binary blob
c2:build:<build_id>              Hash    Build config (timing, host, port, template)
c2:wifi_cache:<agent_id>         String  WiFi geolocation result (TTL 1h)
c2:download:count:<payload_id>   String  Rate-limit counter (TTL 60s, max 10/min)

Aşamalı(Staged) Payload Mimarisi

Ajanlar iki biçimde derlenebilir. Tek parça(monolithic) halindeki sürüm, her şeyi içinde barındıran(core C2 mekanizmaları, modüller, vb.) tek bir binary dosyadır; geliştirme ve hızlı dağıtım için kullanışlıdır. Aşamalı sürüm ise üretimde kullanılan sürümdür, modülerliği ve OPSEC açısından öne çıkar.

Disk üzerindeki stager binary dosyası neredeyse hiçbir şey içermez; yalnızca bir build_id ve bir URL. Stage2, HTTPS üzerinden indirilir, asla diske yazılmaz ve tamamen anonim bir bellek mapping’te bulunur. Bellek tarayıcıları bunu ELF/PE sihirli başlığı olmayan anonim bir sayfa olarak görür (başlıklar, stage2 yüklemesinden sonra silinir). Böylece, diskten kurtarılacak bir dosya olmaz, /proc/PID/maps dizininde adlandırılmış bir dosyaya işaret eden bir giriş olmaz.

Disk (stager) → EnvGuard → ETW/AMSI patch (Win) → unseal config
     → POST /assets/bootstrap → AES-GCM encrypted payload
     → SHA-256 verify → reflective load in-memory → wipe headers
     → stage2 runs → register → beacon loop
     → load modules on demand (same channel, per-session key)

Stage2 ve modül indirmeleri rate limite tabidir (her build_id için dakikada 10 istek) ve Redis’te mevcut olması gereken geçerli bir build ID gerektirir.

Platform Matrisi

Ajanlar üç işletim sistemini hedef alıyor, ancak evasion mekanizmaları platforma özgüdür:

                  ┌─────────────┬────────────┐
                  │  Linux x64  │ Windows x64│
┌─────────────────┼─────────────┼────────────┤
│Staged loader    │      ✓      │     ✓      |
│Reflective load  │  ELF loader │  PE loader │
│Anti-analysis    │  EnvGuard   │  EnvGuard  │
│io_uring I/O     │      ✓      │     ✗      │
│Indirect syscall │      ✗      │     ✓      │
│ETW/AMSI patch   │      ✗      │     ✓      │
│Sleep obfuscation│      ✗      │     ✓      │
│IAT obfuscation  │      ✗      │     ✓      │
│Cascade injection│      ✗      │     ✓      │
└─────────────────┴─────────────┴────────────┘

Windows OPSEC Stack

Windows ajanı, sekiz farklı evasion tekniğini katmanlar halinde kullanır. Bunlar bağımsız değildir; sıralama önemlidir ve bazı teknikler diğerlerini derinden etkileyen kısıtlamalar yaratır.

Stager starts
     │
     ▼
EnvGuard ──── hostile? ──► silent exit (code 0)
     │
     │ clean
     ▼
ETW patch: EtwEventWrite → xor eax,eax; ret
     │
     ▼
AMSI patch: AmsiScanBuffer → AMSI_RESULT_CLEAN
     │
     ▼
Unseal config blob (AES-GCM, HKDF-derived key)
     │
     ▼
Download stage2 (hybrid envelope over TLS)
     │
     ▼
SHA-256 integrity verify
     │
     ▼
Spawn sacrificial process (dllhost.exe with real CLSID, suspended)
     │
     ▼
Map stage2 → anonymous section → base reloc
     │
     ▼
Cascade injection (g_pfnSE_DllLoaded overwrite)
     │
     ▼
Resume thread → stager exits → stage2 _run()
                                    │
                                    ▼
                           self-resolve IAT
                           re-patch ETW/AMSI
                           enter beacon loop

Teknik 1: Derleme Zamanı Şifreleme(CTE)

Release sürümlerindeki her değişmez string değeri CTE() makrosundan geçer. Anahtar, derleme zamanında __TIME__ ve __DATE__’den türetilir, bu nedenle her sürüm için farklıdır. Düz metin asla .rodata dosyasında görünmez.

Teknik 2: Indirect Syscalls + Halo’s Gate

EDR’lar, sistem çağrılarını yakalamak için ntdll stub’larının ilk birkaç baytını hooklar. Buna bir savunma yöntemi olarak, bu hook yöntemini atlatmamız gerekir, yani syscall komutunu doğrudan çağırabiliriz(direct syscall). Fakat direct syscall’lar, syscall’ların direkt olarak bizim ajan prosesimizden çıkmasına neden olur(normalde ntdll.dll’den vb. kaynaklanmalıdırlar) ve günümüzde büyük bir red flag’tır. Bu yüzden dolaylı syscall’lar(indirect syscalls) kullanarak syscall’ın sanki ntdll.dll’den çıkmış gibi göstermesini sağlıyoruz.

Aşağıdaki implementasyon, binary dosyada herhangi bir NT fonksiyon adını saklamaktan kaçınır. Bunun yerine, her fonksiyon adının djb2 hash’ı derleme zamanında hesaplanır ve çalışma zamanında export edilmiş fonksiyonları bulmak için kullanılır:

// syscalls.hpp
constexpr uint32_t djb2Hash(const char *str) {
    uint32_t h = 5381;
    while (*str)
        h = ((h << 5) + h) + static_cast<uint8_t>(*str++);
    return h;
}

// At runtime: walk ntdll export table, hash each name, compare against
// pre-computed constants. No plaintext NT function names in the binary.

Bir ntdll stub’u hooklandığında (ilk baytlar bir JMP için E9 veya dolaylı bir JMP için FF25’tir), Halo’s Gate fallback mekanizması devreye girer: Komşu ntdll exportlarının ardışık SSN’leri vardır, bu nedenle hedefin SSN’i, ±N ofsetindeki temiz bir komşu exportundan çıkarılabilir.

Yüksek riskli işlemler (remote bellek tahsisi, section mapping) için syscall’lar bir Thread Pool Proxy’si aracılığıyla gönderilir. Gerçek syscall komutu, bir pool worker thread’ında yürütülür, bu nedenle EDR tarafından görülen çağrı yığını, beacon thrad’ımızı değil, temiz bir thread pool worker’ıdır.

Teknik 3: AlertShroud — Sleep Obfuscation

En kompleks ve yapmasında çok zorlandığım tekniktir. Neredeyse novel düzeyde olan bir tekniktir. Ajan beacon için uykuya daldığında:

  1. .text bölümünün tamamını, her uyku döngüsü için 16 baytlık rastgele bir anahtarla XOR ile şifreler.
  2. .text dosyasını PAGE_READWRITE olarak işaretler.
  3. NtWaitForAlertByThreadId aracılığıyla sahte bir çağrı yığını kullanarak beacon thread’ını bekleme moduna alır.
  4. Ayrı bir thread’te bir pool timer’ı tetiklenir, .text dosyasının şifresini çözer, PAGE_EXECUTE_READ durumunu geri yükler ve beacon thread’ini uyarır.
Beacon thread:                           Pool thread (timer callback):

enterSleep():                            [timer fires]
  XOR-encrypt .text                      wakeCallback():
  NtProtect .text → PAGE_READWRITE       XOR-decrypt .text
  build fake stack:                      NtProtect .text → RX
    RtlWaitOnAddress   ←                 NtAlertThreadByThreadId
    SleepConditionVarCS                    (beacon thread wakes)
    BaseThreadInitThunk
  RSP ← fake stack
  NtWaitForAlertByThreadId
  [parked]

Belirli aralıklarla yoklama yapan bir bellek tarayıcısı için, .text uyku sırasında rastgele anlamsız veriler gibi görünüyor. Thread çağrı yığını, ntdll içindeki bir koşul değişkeni beklemesine benziyor; bu, Chrome’un renderer thread’larıyla aynı bir model.

Bu durumun yarattığı katı kısıtlama şudur: .text PAGE_READWRITE iken bazı kodların çalışması gerekir(şifreleme/şifre çözme döngüsünün kendisi, thread bekleme işlevi ve zamanlayıcı geri çağırma işlevi). Bu işlevler, asla şifrelenmeyen özel bir .tcwake PE bölümüne derlenir. .tcwake içine yerleştirilen herhangi bir işlev yalnızca inline assembly kodu ve doğrudan sistem çağrıları kullanmalıdır. CRT çağrıları yok, IAT thunk’ları yok. IAT, uyku sırasında erişilemeyen .text içinde bulunur.

// wakeCallback and enterSleep are in .tcwake — never encrypted
static void CALLBACK wakeCallback(PTP_CALLBACK_INSTANCE, PVOID ctx, PTP_TIMER)
    __attribute__((section(".tcwake"), used, noinline));

static void enterSleep(SleepCtx* ctx)
    __attribute__((section(".tcwake"), used, noinline));

Teknik 4: Cascade Injection ve Reflective ELF Loader

Stage2'nun farklı bir işlem içinde yürütülmesi gerekiyor. CreateRemoteThread, EDR ürünlerinde en çok izlenen sistem çağrısı dizilerinden biridir, dolayısıyla buna bir alternatif bulmam lazımdı: ntdll’nin shim motorundaki g_pfnSE_DllLoaded fonksiyon pointer’ını overwrite etmek. Bu pointer, DLL’ler yüklendiğinde shim altyapısından çağrılır — meşru, belgelenmiş bir callback mekanizmasıdır. Bunu, maplenmiş stage2 giriş noktasına işaret eden bir pointer ile overwrite etmek, hedef işlemdeki bir sonraki DLL yükleme olayında stage2 yürütülmesini tetikler. Böylece, herhangi bir uzaktan thread oluşturma syscall’ı kullanmadan, önceden map ettiğim stage2'yu Windows’a çalıştırtabiliyorum.

Stage2 imajı, anonim NtCreateSection + NtMapViewOfSection aracılığıyla maplenir; WriteProcessMemory veya VirtualAllocEx kullanılmaz. 66 baytlık köprüleme shellcode’u (shim callback→ _run() geçişini ayarlayan kod), stager binary dosyasında XOR ile gizlenmiştir.

Linux’ta yabancı binary dosyaları belleğe yüklemek için VirtualAllocEx/WriteProcessMemory eşdeğeri bir işlev bulunmamaktadır. Custom ELF yükleyicisi bunu sıfırdan yapar.

// elf_loader.cpp (simplified)
// No dlopen. No memfd_create. No filesystem artifacts.

extern struct r_debug _r_debug;  // ld.so's debug rendezvous structure

ElfLoader::load(bytes):
  // 1. Anonymous mapping — no backing file
  mmap(NULL, file_size, PROT_READ|WRITE, MAP_PRIVATE|ANON)

  // 2. Parse PT_LOAD segments, apply base address relocations
  parse ELF header → PT_LOAD segments → reloc table

  // 3. Resolve external symbols by walking ld.so's link_map
  //    _r_debug.r_map is the head of the loaded library linked list
  self_resolve_imports():
    for each DT_NEEDED symbol:
      walk _r_debug.r_map → foreach library → walk export hash table
      match → patch GOT entry

  // 4. Set correct memory permissions per segment
  mprotect(PT_LOAD with PF_X) → PROT_READ | PROT_EXEC
  mprotect(PT_LOAD without PF_X) → PROT_READ | PROT_WRITE

  // 5. Wipe ELF magic + header in the mapped image
  //    After this: /proc/PID/maps shows an anonymous mapping with no name
  memset(mapped_base, 0, sizeof(Elf64_Ehdr))

  return entry_point

_r_debug.r_map walk, Windows’taki PEB InMemoryOrderModuleList walk’ının Linux karşılığıdır. Her ikisi de herhangi bir işletim sistemi API’sini çağırmadan prosese yüklenen paylaşımlı kütüphane(shared library) listesini verir. Hızlı sembol çözümlemesi için GNU hash tablo araması kullanılır; bu, ld.so’nun dahili olarak kullandığı algoritmayla aynıdır.

Teknik 5: Linux io_uring Without liburing

Linux çekirdeği 5.1 ve üzeri sürümlerde ajan, tüm HTTP ve dosya G/Ç işlemlerini io_uring üzerinden gerçekleştirir. liburing’i (standart kullanıcı sarmalayıcısı) kullanmaktan kaçınma kararı kasıtlıydı: liburing, binary dosyaya tanınabilir bir kütüphane adı ekler ve ldd çıktısında görünen bir bağımlılık ekler. Bunun yerine, çekirdek ABI yapıları inline olarak tanımlanır:

// iouring_defs.hpp — no <linux/io_uring.h>, no liburing

static constexpr long __NR_io_uring_setup = 425;   // x86_64
static constexpr long __NR_io_uring_enter = 426;

// 64-byte Submission Queue Entry — must match kernel ABI exactly
struct io_uring_sqe {
    uint8_t  opcode;
    uint8_t  flags;
    uint16_t ioprio;
    int32_t  fd;
    union { uint64_t off; uint64_t addr2; };
    union { uint64_t addr; uint64_t splice_off_in; };
    uint32_t len;
    union { uint32_t rw_flags; uint32_t open_flags; /* ... */ };
    uint64_t user_data;
    // ... (64 bytes total, static_assert enforced)
};

static_assert(sizeof(io_uring_sqe) == 64, "kernel ABI mismatch");

// 16-byte Completion Queue Entry
struct io_uring_cqe {
    uint64_t user_data;
    int32_t  res;
    uint32_t flags;
};

Runtime factory, başlangıçta uname().release değerini 5.1 ile karşılaştırır ve daha eski çekirdeklerde cpp-httplib’e geri döner. io_uring yolu, HTTP connect, send ve recv işlemlerini tek bir ring boşaltma işleminde gruplandırır; bu da daha az sistem çağrısı gidiş-dönüşünü sağlar ve read/write/send işlemlerini yakalayan strace seviyesindeki kancaların G/Ç’yi yakalamasına engel olur.

Config Mühürleme

Listener adresi, port, beacon zamanlaması ve build_id binary dosyada düz metin olarak saklanamaz; statik analiz araçları bunları hemen çıkarır ve C2 altyapısını ele verir.

Ancak mühürleme anahtarının kendisi bir yerden gelmelidir. Onu düz metin olarak saklamak amacın dışına çıkar(tavuk mu yumurtadan çıkar, yumurta mı tavuktan çıkar?). Çözüm: Anahtarı, binary dosyadaki dört bilinen işaretleyici ofsetine bölmek ve her derleme için rastgele bir seed’ten türetmek.

Binary layout (simplified):
┌──────────────────────────────────────────────────┐
│ ... code ...                                     │
│ [MARKER_A][seed_fragment_0: 8 bytes][padding]    │  ← offset discovered by builder
│ ... code ...                                     │
│ [MARKER_B][seed_fragment_1: 8 bytes][padding]    │
│ ... code ...                                     │
│ [MARKER_C][seed_fragment_2: 8 bytes][padding]    │
│ ... code ...                                     │
│ [MARKER_D][seed_fragment_3: 8 bytes][padding]    │
│ ... code ...                                     │
│ [BlobHeader (128 bytes)][ciphertext + GCM tag]   │  ← config blob slot
│ [BlobHeader (128 bytes)][ciphertext + GCM tag]   │  ← RSA key blob slot
└──────────────────────────────────────────────────┘

Key derivation:
  seed = seed_fragment_0 || seed_fragment_1 || seed_fragment_2 || seed_fragment_3
  key  = HKDF-SHA256(IKM=seed, salt=blob.salt, info=build_id||platform||stage||abi||blob_type||key_version)
  config_plaintext = AES-256-GCM-Decrypt(key, nonce=blob.nonce, AAD=BlobHeader, ciphertext)

Builder, yama zamanında seed parçalarını yazar, anahtarı türetir, config’i mühürler ve bloğu yazar. Çalışma zamanında, ajan aynı dört işaretleyici ofsetini okur, seed’i yeniden oluşturur, aynı anahtarı türetir ve şifreyi çözer. Mühürleme anahtarı hiçbir yerde görünmez; dağınık parçalardan talep üzerine hesaplanır ve ardından atılır.

BlobHeader yapısı, derleme zamanında static_assert ile tam olarak 128 bayttır. BlobHeader başlığının tamamı, AES-GCM’nin Additional Authenticated Data (AAD) olarak kullanılır. Bu, build_id veya platform alanlarındaki bitlerin değiştirilmesi de dahil olmak üzere başlıkta yapılan herhangi bir değişikliğin GCM etiket doğrulamasını başarısız kılacağı anlamına gelir. Başlık şifrelenmemiş olsa bile bütünlüğü korunmaktadır.

Android Ajanlar: Kotlin + JNI + DEX Staging

Android ajanı, masaüstü aşamalı mimarisini yansıtır ancak JVM/ART çalışma zamanını hedefler. Yerel kriptografi (RSA-OAEP, AES-256-GCM, HKDF) C++ dilinde uygulanır ve JNI aracılığıyla sunulur; Kotlin katmanı orkestrayı yönetir, C++ katmanı ise tüm kriptografik işlemleri yönetir.

agent/android-kotlin/
├── app/         ← Monolithic APK (all task handlers inline)
├── stager/      ← Stage 1 APK (downloads stage2 DEX)
├── stage2/      ← Stage 2 DEX (loaded via DexClassLoader)
└── modules/     ← External DEX modules loaded at runtime
    ├── shell/
    ├── recon/
    ├── file_manager/
    ├── file_transfer/
    └── location/

Android sınıf yükleyici sınırları, önemli bir mühendislik problemidir. Stage2 DEX, ayrı bir DexClassLoader instance’ına yüklenir; doğrudan stager sınıflarını içe aktaramaz. Bir shared-contracts Gradle modülü, her iki tarafın da bağlı olduğu arayüzleri tanımlar ve stage2 instance’ı, bu sınırlarda yalnızca java.lang.Object türleri kullanılarak EntryPoint üzerinde yansıma yoluyla üretilir.

R8, sınıf yükleyici sınırları boyunca stager’ın küçültülmüş sınıf adlarıyla isim çakışmalarını önlemek için stage2 (‘x’) için farklı bir -repackageclasses ad alanı kullanır.

Android CTE uygulaması, her derlemede rastgele 16 baytlık bir XOR anahtarıyla modül başına Str.kt oluşturan bir Gradle derleme görevidir. Stringler, DEX string sabitleri değil, ByteArray tamsayı değişmezleri olarak saklanır, bu nedenle hiçbir zaman bir DEX dosyasının strings tablosunda görünmezler:

// Generated Str.kt (build artifact, not checked in)
// Strings stored as byte arrays, never as DEX string constants
object Str {
    private val k = byteArrayOf(0x3F, 0x7A, /* ... 16 bytes */ )

    val BEACON_PATH = byteArrayOf(0x5E, 0x1B, /* XOR-encrypted bytes */)

    fun d(b: ByteArray): String = String(b.mapIndexed { i, v ->
        (v.toInt() xor k[i % k.size].toInt()).toByte()
    }.toByteArray())
}

// Usage: Str.d(Str.FIELD_JITTER) → "jitter"

-assumenosideeffects R8 kuralları, android.util.Log çağrılarını ve bunların argüman oluşturma işlemlerini (string şablonları ve StringBuilder zincirleri dahil) release DEX’inden kaldırır. Endpointler, task türlerini veya oturum durumunu açığa çıkaracak log çağrıları, release derlemelerinde sıfır DEX bayt kodu üretir.

Application Factory

Initial Access kısmı için yaptığım bu özellik, beş adet üretim kalitesinde APK temasında (VPN uygulaması, el feneri, hava durumu, temizleyici, launcher) aynı ajan mantığını içerir(custom template’ler de destekleniyor), böylece ajanın legit bir uygulama gibi görünülmesi sağlanır. Operatör, builder arayüzünde bir tema seçer. Admin sunucusu, seçilen önceden derlenmiş şablon APK’yı C2 config bloğu ve RSA anahtar bloğu ile yamalar, ardından zipalign + apksigner komutlarını çalıştırır. Sunucu, derleme sırasında kaynak kodunu asla yeniden derlemez; mühürlenmiş binary blokları önceden oluşturulmuş bir şablona yamalar.

Payload Builder

Payload Builder

Payload Builder

Admin sunucusunun builder’ı kaynak kodlarına asla dokunmaz. Şunları yapar:

  1. Seçilen listener sunucusunun mevcut RSA genel anahtarını Redis’ten alır (dinleyici başlangıçta bir anahtar çifti oluşturur ve genel anahtarı saklar)
  2. İstenen işletim sistemi/mimari/tür için önceden derlenmiş binary şablonu bulur
  3. Binary dosyada dört seed parçası işaretçilerini bulur
  4. Derleme başına rastgele bir seed oluşturur, parçaları yazar, HKDF anahtarını türetir
  5. Config ve RSA anahtarını AES-256-GCM binary dosyaları olarak mühürler ve şablona ekler
  6. APK’lar için: zipalignapksigner çalıştırır
Operator selects:              builder_service.go
  OS + arch + type    ──────►  fetch RSA pubkey from Redis
  listener host/port           find pre-compiled template
  beacon interval              locate fragment markers
  APK theme (Android)          generate seed → write fragments
                               HKDF-SHA256 → AES-GCM seal
                               patch config blob + RSA blob
                    ┌──────── APK? → zipalign + apksigner
                    │         ELF/PE? → done
                    ▼
               signed payload → served for download

Operatör, derleme adımı gerektirmeyen, kullanıma hazır bir binary dosya indirir. Admin sunucusu, teslimat sırasında config dosyasını asla düz metin olarak içermez; dosyayı hemen şifreler ve yalnızca ajan, çalışma zamanında şifreyi çözebilir.

Bu C2 Mimarisi Bizi Nelerden Korur?

Her tasarım kararı belirli bir tehdide karşılık geliyor:

[embed]

Sırada Ne Var?

Bu yazı mimariye genel bir bakıştı. Gelecek yazılarımda alt sistemlere daha detaylı olarak değineceğim:

  • AlertShroud’un Derinlemesine İncelenmesi.tcwake bölüm kısıtlaması, sahte yığın çerçevesi oluşturma ve VEH kurtarma yolu
  • Dolaylı sistem çağrıları: SSN çözülmesi + Halo’s Gate — EDR kancalarının düz metin NT adları olmadan nasıl tespit edildiği ve atlandığı
  • C2 iletişimi için io_uring — ham çekirdek ABI’sine bir ring buffer G/Ç yolunun uygulanması
  • Sınıf yükleyici sınırları içerisinde Android DEX staging DexClassLoader izolasyonunun ve R8 etkileşiminin mühendislik kısıtlamaları
  • Config mühürleme sistemi — Dağınık binaryparçalardan HKDF anahtar türetimi ve builder pipeline
  • İşletim sistemlerine özgü kalıcılık mekanizmaları, single-instance, lokasyon verisi mantığı, vb. gibi konular…

UYARI!

telekinetiC2, authorized red team çalışmaları için geliştirilmiş kapalı kaynaklı bir C2 frameworküdür. Burada açıklanan tüm teknikler akademik literatürde belgelenmiştir ve Cobalt Strike, Havoc ve Sliver gibi açık kaynaklı araçlarda uygulanmıştır.


메타데이터
post_id
107d1be27ad3
slug
sıfırdan-c2-frameworkü-i̇nşa-etmek-mimariye-derinlemesine-bakış-107d1be27ad3
url
https://medium.com/@st4inl3s5/s%C4%B1f%C4%B1rdan-c2-framework%C3%BC-i%CC%87n%C5%9Fa-etmek-mimariye-derinlemesine-bak%C4%B1%C5%9F-107d1be27ad3
canonical_url
https://medium.com/@st4inl3s5/s%C4%B1f%C4%B1rdan-c2-framework%C3%BC-i%CC%87n%C5%9Fa-etmek-mimariye-derinlemesine-bak%C4%B1%C5%9F-107d1be27ad3
author_url
https://medium.com/@st4inl3s5
status
ok
fetched_at
2026-06-09 21:21:26