Cyber Security Articles

Proencryptor ELF 64-BIT Analizi

Proencryptor Dosyasının Analizi ve Algoritmasının Tersine Çevrilmesi

Dosya Bilgileri

Sabancı Üniversite Siber Güvenlik Kulubü tarafından düzenelen SUCTF etkinliğinde yer alan Reverse sorusunun Write-Up’ını içermektedir. Her ne kadar tam olarak daha detaylı bir biçimde aktarılmasada genel olarak kod yapısından bahsedilmiş.Bazı bölümler atlanılarak çözüm odaklı bir biçimde yapının işleyişi ve decrypt edilmesi için gerekli yazılımın tasarlanması ve çözüm sürecini içermektedir.

File olarak uygulamayı incelediğimizde dosya bilgileri aşağıda ki gibidir :

proencryptor: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter 
/lib64/ld-linux-x86-64.so.2, BuildID[sha1]=1aa16f04cd030b16774ee6232c7192a3d6c68092, for GNU/Linux 3.2.0, not stripped

Strace ile Yüzeysel Olarak İnceleme

Burada özetle göz attığımızda Dosya ile alakalı bir takım bilgilere ulaşabiliriz.Flag.enc dosyası üzerinde bazı işlemlerin gerçekleştiğini ve dosyanın açılması açılan dosyasının üzerine ise bazı verilerin yazıldığı sonucuna varılabilir.Tam olarak incelenerek daha kesin sonuçlara varabiliriz. Farklı bir dosya adı verildiğinde ise ona şifreleme işlemi yapılmadığını görmekteyiz. Programı çalıştırdığımızda “Test” olarak bir string verdiğimizde bize Flag.enc olarak bir dosya yarattığını görmekteyiz.

execve("./proencryptor", ["./proencryptor", "flag.enc", "a"], 0x7ffc62fd4e00 /* 55 vars */) = 0
getrandom("\xca\x80\x6d\x9f\xf5\x3d\xeb\x04", 8, GRND_NONBLOCK) = 8
write(3, "\240\3733fs\216\334\10\213\213\22\333", 12) = 12
openat(AT_FDCWD, "flag.enc", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3

Programda ./proencryptor "TestTestTest" komutu çalıştırıldığında bize 16 Byte bir dosya oluşturduğunu görmekteyiz. cat flag.enc | xxd -p | fold -w 2 | wc -l komutu ile veya wc -c flag.enc ile kontrol edebiliriz.

./proencryptor "TestTest" olarak oluşturduğumuzda ise 12 Byte oluşmaktadır. ./proencryptor "TestTestTestTest" ise 20 Byte oluşturmaktadır.

Ghidra ve Cutter ile Pseudo Code İncelemesi

Girilen argüman değerinin 0 olması durumunda oluşabilecek hata mesajı durumunu belirtmektedir. int main(int argc, char *argv[]) şöyle bir yapı olduğunu düşünürsek Argc ile toplam argüman sayısının kontrol edildiği görülmektedir. İf bloğu eğer çalışmaz ise Else bloğundan program akışı devam etmektedir.

00001281      if (argc <= 1)
0000127d      {
0000128a          puts("usage: proencyrptor <flag>");
0000128f          rax_1 = 1;
0000128f      }

Aşağıda bulunan Else Bloğu incelemesinde ise local_68 flag.enc adında dosya yazma modunda w ile açılmaktadır. tVar6 isminde bir değişkene ise Time() fonksiyonu çağrılarak değişkenine değer ataması yapılmaktadır. tVar6 değişkeni int türüne dönüştürülür ve tVar6’nın üst sınırına bölünerek bir local_74 değişkenine atanır. Buradaki 0xffffffff değeri genellikle 32 bitlik işaretli bir tam sayının maksimum değerini temsil eder. fwrite() fonksiyonu kullanılarak local_74 değişkeninin değeri dosyaya yazılır. Bu, dosyanın başına 4 byte’lık bir tam sayı yazılacağı anlamına geldiğini düşünebiliriz. srand() fonksiyonu, rastgele sayı üreteciyi başlatmak için local_74 değişkeninin değerini kullanır. Bu, programın sonraki rastgele sayı üretme işlemlerinin önceden belirlenmiş bir tohumdan (seed) başlamasını sağlar, böylece her çalıştırma aynı rastgele değerleri üretir.

Burada bu fonksiyonu anlatmak gerekirse srand() ile verilen bir Seed ile yani Time() ile daha sonra aynı şekilde bir sayı üretilebilir anlamına gelmektedir. Sadece Seed değerlerinin eşleştirilmesi ise Rastgele Sayı Üretiminde aynı değerler elde edilebilir.Bu yöntem aynı şekilde Python dilindede bulunmaktadır fakat bazı kütüphane fonksiyonlarında ise bu Rastgele Üretim daha güvenli bir şekilde üretilebilmektedir.Fakat şuan programda kullanılan Time ve Srand fonksiyonlarından yola çıkarak düşürsek burada Şifre çözmek için kullanacağımız yöntemler hakkında önemli bilgilere uşatığımız söylenebilir.Seed,Srand ve Time olarak aklımızda bulunmalıdır.

local_88 adresindeki stringin (*(char **)(local_88 + 8)) uzunluğu strlen() fonksiyonuyla hesaplanır ve local_98 değişkenine atanır. local_98 - 1 değeri local_60 değişkenine atanır. local_90 0 değeri atanmaktadır.

uVar7 değişkeni, gizlenecek metnin boyutunu yuvarlamak için kullanılır. (local_98 + 0xf) / 0x10) * 0x10 ifadesi, metnin boyutunu 16’ya yuvarlar.Bu sonuç bir döngünüde kullanılmak üzere tasarlanmıştır.

For döngüsünde ise

psVar10 değişkeni, &local_b8 adresinden uVar7‘nin altındaki bir adresi temsil eden bir adresle başlatılır. Döngü, psVar10‘un &local_b8 adresinden daha küçük olduğu sürece devam eder ve her adımda psVar10 adresi 0x1000 (4096) bayt azaltılır. Bu, döngünün local_b8 adresinden uVar7‘nin altına kadar çalışmasını sağlar. Döngünün her adımında, psVar10 adresinin -8 bayt gerisine bir değer kopyalanır. psVar10‘un gösterdiği bellek alanını temizlemeye benzer bir işlemdir.

   {
    local_68 = fopen("flag.enc","w");
    tVar6 = time((time_t *)0x0);
    local_74 = (int)tVar6 + (int)(tVar6 / 0xffffffff);
    fwrite(&local_74,4,1,local_68);
    srand(local_74);
    local_98 = strlen(*(char **)(local_88 + 8));
    local_60 = local_98 - 1;
    local_90 = 0;
    uVar7 = ((local_98 + 0xf) / 0x10) * 0x10;
    for (; psVar10 != (size_t *)((long)&local_b8 - (uVar7 & 0xfffffffffffff000));
        psVar10 = (size_t *)((long)psVar10 + -0x1000)) {
      *(undefined8 *)((long)psVar10 + -8) = *(undefined8 *)((long)psVar10 + -8);
  }

Else Bloğunun devamında ise şifrelemeye yönelik bir takım işlemler gerçekleştirilmektedir.Direkt olarak bunlara odaklanarak sonuca varmada daha hızlı hareket edilebilir. Olayın mantığını kavrayarak daha güzel bir şekilde sonuca ulaşabiliriz. Zaman damgası oluşturularak dosyaların şifrelendiğini düşünerek buna ait bir Kod bloğunu incelemeye alalım.

Anahtarların Oluşturulması

local_6c değişkeni, döngü içinde kullanılan bir sayaçtır.

local_88 bu işaretçinin bulunduğu bellek adresini temsil eder.Metnin uzunluğunu bulmak için pcVar1 adlı bir işaretçi kullanılır.

strlen() fonksiyonu, verilen işaretçinin işaret ettiği dizinin uzunluğunu hesaplar.Uzunluk değeri sVar8’e atanır. Eğer sayaç, dizinin uzunluğundan küçükse döngü devam eder,aksi takdirde döngüden çıkılır.

Eğer dizi elemanlarının uzunluğu, sayaçtan büyükse, o zaman rastgele bir değer oluşturulur ve local_58 dizisinin ilgili elemanına atanır. local_6c değişkeninde ise +1 arttırma işlemi gerçekleştirilir.

while (true) {
 
    uVar7 = (ulong)local_6c;

    pcVar1 = *(char **)(local_88 + 8);


    *(undefined8 *)((long)psVar10 + lVar2 + -8) = 0x101418;
    sVar8 = strlen(pcVar1);

    if (sVar8 <= uVar7)
        break;

    *(undefined8 *)((long)psVar10 + lVar2 + -8) = 0x1013e2;
    iVar4 = rand();
    local_58[local_6c] = (char)iVar4;

    local_6c = local_6c + 1;
}

Bu kod yapısı dahada yorumlanabilir fakat çok fazla karmaşık olduğundan dolayı özet olarak yorumlamak gerekirse sadece şifreleme fonksiyonlarını dikkate almak gerekmektedir.

local_70 indeksi kullanılarak bir diziye erişilir. Bu dizi, local_70 indeksine göre adresi local_88‘den başlayan bir diziden bir elemanın adresiyle XOR işlemine tabi tutulur ve sonuç, local_48 dizisine atanır.

local_48[local_70] = *(byte *)((long)local_70 + *(long *)(local_88 + 8)) ^ local_58[local_70];

Gerçekleştirilen işlemleri genel olarak bahsettim arada hala anlatımda yer almayan kod bloklarıda bulunmaktadır. Fakat özet vermek gerekirse genel işleyişi bakımından iyice analiz edildikten sonra bir sonuca varılabilir.

Decrypt Fonksiyonunun Oluşturulması

Decrypt_data fonksiyonu, bir dosyanın adını alır ve bu dosyanın içeriğini çözmek için kullanılır.

Dosya okuma işlemi için fopen fonksiyonu kullanılarak dosya açılır.

Eğer dosya açılamazsa, perror fonksiyonu kullanılarak bir hata mesajı yazdırılır ve program sonlandırılır.

Dosyanın başındaki 4 baytlık zaman damgası (timestamp) fread fonksiyonu ile okunur. Bu zaman damgası, rastgele anahtarları oluşturmak için kullanılacaktır.

Okunan zaman damgası srand fonksiyonuna verilerek rastgele sayı üretici başlatılır.

Dosyanın boyutu ftell fonksiyonu ile hesaplanır. Dosya imleci dosyanın sonuna götürülür (fseek(fp, 0, SEEK_END)), dosya boyutu ftell ile alınır, ve dosya imleci tekrar dosyanın başına götürülür (fseek(fp, sizeof(uint32_t), SEEK_SET)). Bu, zaman damgasının boyutunu atlamak içindir.

Dosyanın boyutu, zaman damgasının boyutu çıkarılarak anahtar dizisinin boyutu belirlenir. Bu anahtar dizisi, dosyanın içeriğini çözmek için kullanılacaktır.

malloc fonksiyonu ile anahtar dizisi için bellek ayrılır.

Bir döngü yardımıyla her bir anahtar değeri, 0 ile 255 arasında rastgele bir değerle doldurulur.

Dosyanın içeriği okunarak çözülür. Her bir bayt, anahtar dizisiyle XOR işlemine tabi tutulur ve orijinal değeri elde etmek için kullanılır. Çözülen veri printf fonksiyonu ile ekrana yazdırılır.

Dosya kapatılır (fclose) ve free fonksiyonu kullanılarak dinamik olarak ayrılan bellek serbest bırakılır.

main fonksiyonu, komut satırından alınan argüman sayısını kontrol eder. Eğer argüman sayısı 2 değilse, kullanıcıya doğru kullanımı belirten bir mesaj gönderilir ve programdan çıkılır.

Şifrelenmiş dosyanın adı, decrypt_data fonksiyonu ile çağrılarak çözülür.

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <time.h>

void decrypt_data(const char *filename) {
    FILE *fp = fopen(filename, "rb");
    if (fp == NULL) {
        perror("Error opening file");
        exit(1);
    }

    // Dosyanın başındaki zaman damgasını oku
    uint32_t timestamp;
    fread(&timestamp, sizeof(uint32_t), 1, fp);

    // Zaman damgasını kullanarak rastgele anahtarları oluştur
    srand(timestamp);

    // Dosyanın boyutunu al
    fseek(fp, 0, SEEK_END);
    long file_size = ftell(fp);
    fseek(fp, sizeof(uint32_t), SEEK_SET);

    // Anahtarları oluştur
    char *key = (char *)malloc(file_size - sizeof(uint32_t));
    for (long i = 0; i < file_size - sizeof(uint32_t); i++) {
        key[i] = rand() % 256;
    }

    // Veriyi çöz
    printf("Decrypted data: ");
    for (long i = 0; i < file_size - sizeof(uint32_t); i++) {
        int decrypted_byte = fgetc(fp) ^ key[i];
        printf("%c", decrypted_byte);
    }
    printf("\n");

    fclose(fp);
    free(key);
}

int main(int argc, char *argv[]) {
    if (argc != 2) {
        printf("Usage: %s <encrypted_file>\n", argv[0]);
        return 1;
    }

    // Şifrelenmiş dosyayı çözme
    decrypt_data(argv[1]);

    return 0;
}

Daha sonra çalıştırıldığında ise Bayrağa ulaşmaktayız.

./decryptx flag.enc              
Decrypted data: SUCTF{EnC_w1th0ut_a_Key_l1ke_a_Pro}

Ayrıca tekrardan şifreleme programının C halini inceleme isterseniz daha açıklayıcı olabilir ve yapıyı anlayabilirsiniz.

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <time.h>

void encrypt_text(const char *text) {
    FILE *output_fp = fopen("flag.enc", "wb");
    if (output_fp == NULL) {
        perror("Error opening output file");
        exit(1);
    }

    // Rastgele bir zaman damgası oluştur
    uint32_t timestamp = (uint32_t)time(NULL);

    // Zaman damgasını dosyanın başına yaz
    fwrite(&timestamp, sizeof(uint32_t), 1, output_fp);

    // Rastgele anahtarları oluştur
    srand(timestamp);

    // Dosyanın boyutunu al
    size_t text_length = strlen(text);

    // Anahtarları oluştur
    char *key = (char *)malloc(text_length);
    if (key == NULL) {
        perror("Error allocating memory for key");
        exit(1);
    }
    for (size_t i = 0; i < text_length; i++) {
        key[i] = rand() % 256;
    }

    // Metni şifrele ve şifreli dosyaya yaz
    for (size_t i = 0; i < text_length; i++) {
        fputc(text[i] ^ key[i], output_fp);
    }

    printf("Text encrypted successfully to flag.enc!\n");

    fclose(output_fp);
    free(key);
}

int main(int argc, char *argv[]) {
    if (argc != 2) {
        printf("Usage: %s <text>\n", argv[0]);
        return 1;
    }

    encrypt_text(argv[1]);

    return 0;
}

Kısaca bu kısmıda özetlemek gerekirse daaha çok şifreleme kod yapısına yönelik kısaca şunları açıllayabiliriz.

İlk olarak, key adında bir dizi oluşturuyoruz. Bu dizi, metni şifrelemek için kullanacağımız anahtarı temsil eder.Bu dizi, text_length kadar uzunlukta olacak şekilde bellekten ayrılıyor. text_length, metnin uzunluğunu temsil eder.

char *key = (char *)malloc(text_length);

Daha sonra, her bir karakter için rastgele bir sayı oluşturuyoruz ve bu sayıları key dizisine atıyoruz.

for (size_t i = 0; i < text_length; i++) {
    key[i] = rand() % 256;
}

Şimdi, metni şifrelemek için her bir karakteri anahtarla XOR işlemine tabi tutuyoruz ve şifreli metni dosyaya yazıyoruz.

for (size_t i = 0; i < text_length; i++) {
    fputc(text[i] ^ key[i], output_fp);
}

text[i] ^ key[i] ifadesi, text dizisinin i’inci karakterini ve key dizisinin i’inci elemanını XOR işlemine tabi tutar. Bu şekilde, her karakteri farklı bir anahtarla şifreleriz.

folder_s3

> Proencryptor İndirmek İçin <

folder_s3