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(×tamp, 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(×tamp, 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.
> Proencryptor İndirmek İçin <