oniki maymun

Kimlik Sorunu

Her yazıda somut birşey olsun diye kendi kendimi köşeye sıkıştırdığımdan, uzunca bir süredir yazacak birşey bulamıyordum. Derken, Anonymous tüm Türkiye'nin kimlik bilgilerini içeren bir dosyayı torrent olarak paylaştı.

Torrent inerken bir taraftan da bu konudaki gündemin nabzını tutayım diye bakınırken Fetonymous manşetini görünce, benim neden böyle şuursuzca saçmalayıp üzerine bir de para aldığım bir işim yok diye hayıflandım. Sonra gidip yattım.

Ertesi sabah torrent inmişti ve ilk iş olarak Komodo Ejderi isimli antivirüs yazılımıyla 3 defa tarama yaptırdım. İlkinde virüs bulamadı ama ikincisinde buldu. Üçüncüsünde bilgisayar kilitlendi, tekrar açıp bu kumarı oynamaya karar verdim ve zip dosyasının üzerine çift tıkladım. O anda bilgisayarım alev aldı. Şu anda bu yazıyı komşunun bilgisayarından yazıyorum. Komodo Ejderi'ne de çok kırgınım :(((

Zip dosyası açmayı bilen herkesin söylediği gibi, arşivin içinde MySQL veritabanı dosyaları ve Sorgu.exe isimli bir program var. Genel ayrıntıları buradan okuyabilirsiniz (yorumları da okuyun).

Sorgu.exe, Delphi ile yazılmış ve bu exe dosyasını bir text editörü ile açıp gözlerinizi kısarak 1-2 saat bakarsanız, bir köşede duran telefon numarasını gözden kaçırmanız mümkün değil. Telefon numarasının Mersin'de Dominant Hukuk diye bir firmaya ait olduğunu öğrenmek ise bir Google araması kadar yakınımızda. Dominant. Ben de bir hukuk firması kuracak olsam böyle mütevazi bir isim koyardım. Neyse, bu firmanın sahibi/ortağı veya bir çalışanı tarafından yazılan, yılın pişkinlik ödülüne aday gösterilebilecek, bir kaç forum mesajı da var. Yıl 2010. Dahası da var. Aynı kişi 2015 yılında DSP'den Mersin milletvekili adayı olmuş.

MySQL verisine gelirsek, rnd ve rnd001'den rnd081'e kadar giden tablolar var. Sonunda sayı olanlar illere göre ayrılmış veriler. rnd tablosu ise tüm veriyi içeriyor. Yani aslında verinin yarısı tekrar. rnd tablosunun dosyalarını kendi MySQL data dizinimize kopyalayıp bakınca aşağıdaki gibi anlaşılmaz şeyler görüyoruz.

Oysa ne hayallerimiz vardı. Terazi burcu kaç kişi var? En yaşlı Türk kaç yaşında? Kaç tane dansöz var? gibi sorularımız cevapsız kalmıştı. Veriler şifrelenmişti ve sadece Sorgu.exe üzerinden sorgulama yapılabiliyordu.

Şiştik mi?

Hayır. Şifrelenmiş veriye ilk bakışta görülen üç şey var:

  • Tablodaki veriler standart şifreleme algoritmalarının çıktılarına benzemiyor.
  • Şifrelenmiş veri uzunlukları değişken.
  • Şifrelenmiş veriler, 0-9 arası sayılardan ve ingiliz alfabesinin büyük/küçük harflerinden oluşuyor (bir de '/' var ama şu an için dikkate almayalım).

Bu noktada ilk varsayımımızı yapıyoruz ve diyoruz ki şifreleme için büyük ihtimalle forumun birinden arak bir algoritma kullanılmış. İkinci varsayımı ise giriş(şifrelenmemiş) ve çıkış(şifrelenmiş) alfabelerini tanımlarken yapacağız. Giriş alfabesi ASCII tablosundaki karakterler (tabloda sadece bir kısmı var):

Çıkış alfabesi ise 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz olsun diye bir atış yapıyoruz.

Tabloların çirkinliği için üzgünüm. Açık mavi alanlar karakterlerin sayısal karşılıklarını gösteriyor. Seçtiğimiz giriş alfabesinde türkçe karakter desteği olmadığından şimdilik sadece ingilizce karakter içeren girdiler ile çalışacağız.

Sorgu.exe'yi çalıştırıp isim alanına bilindik birşeyler yazarak arama yaptıktan sonra MySQL loglarına bakarak (veya hızlı davranıp SHOW PROCESSLIST; ile konsoldan) gerçekleştirilen sorguyu görebiliriz.

Örneğin isim alanına "0" girersek gerçekleştirilen sorgu:

SELECT RND3PIFIX,RNDGG4FL3,RND70UWQP,RNDPOV6BQ,RNDY5YM2W,RNDU0XFY2 FROM rnd WHERE (RNDGG4FL3 LIKE 'C0') AND (1=1)

Böylece RNDGG4FL3 kolonunun "isim" kolonu olduğunu da anlamış oluyoruz. Devam edelim:

0 -> C0
1 -> CG
2 -> CW
3 -> Cm
4 -> D0
5 -> DG
6 -> DW
7 -> Dm
8 -> E0
9 -> EG
...

Sözelciler anlamamış olabilir ancak son karakterler 0,G,W,m şeklinde birbirini tekrarlıyor. İlk karakterler ise C,D,E şeklinde sıra ile gidiyor.

Giriş alfabesinde '0' karakterinin sayı karşılığının 48 olduğunu görüyoruz. "C0"'ın çıkış alfabesindeki sayı karşılıkları 12 ve 0. Sıradan devam edersek:

'0' -> 48 -> "C0" -> 12,0
'1' -> 49 -> "CG" -> 12,16
'2' -> 50 -> "CW" -> 12,32 
'3' -> 51 -> "Cm" -> 12,48
'4' -> 52 -> "D0" -> 13,0

Giriş karakterinin sayısal karşılığının 4'e bölündüğünü (tamsayı olarak) anlamışsınızdır sanırım. Bölme işlemindeki kalan ise 16 ile çarpılıyor. Henüz karşılığını bilmediğimiz bir karakter olan ('A') ile deneme yaparsak:

'A' -> 65 -> 65/4=16, 65%4=1*16=16 

Böylece 'A' karakterinin sayısal karşılığının 16,16, alfabedeki karşılığının ise "GG" olması gerektiğini hesaplamış olduk. Gerçekten öyle mi diye bakarsak:

SELECT RND3PIFIX,RNDGG4FL3,RND70UWQP,RNDPOV6BQ,RNDY5YM2W,RNDU0XFY2 FROM rnd WHERE (RNDGG4FL3 LIKE 'GG') AND (1=1)

öyle olduğunu görüyoruz. Doğru yoldayız. Biraz daha şifrelenmiş veri üretelim.

00 -> C30
01 -> C34
02 -> C38

İki karakterli girdilerde bir sorunumuz var. Şifrelenmiş veri 4 karakter olması gerekirken 3 karakterde kaldı. "00"'ın karşılığı "C0C0" olsaydı işimiz burada bitmişti ama "C30" görüyoruz. Peki daha uzun girdilerde neler oluyor?

0    -> C0
00   -> C30
000  -> C30m
0000 -> C30mC0

1    -> CG
11   -> CJ4
111  -> CJ4n
1111 -> CJ4nCG

Görüldüğü gibi 3 karakterden sonra şifreleme baştan başlıyor (şifrelenmiş veri 4. karakterden sonra tekrarlıyor). Yani üç karakterden oluşan girdilerin nasıl şifrelendiğini bulmak yeterli olacak.

"000" -> 48,48,48 --> "C30m" -> 12,3,0,48
"111" -> 49,49,49 --> "CJ4n" -> 12,19,4,49

Tek karakter varken sayısal karşılığını 4'e bölüp kalanı 16 ile çarpmıştık. "000"a bakınca ikinci karakterin şifrelenmiş karşığının 3 olduğunu görüp 48/16=3 diye düşünmeden edemiyoruz. Ama "111"e bakınca ikinci sayısal değer olarak 49/16=3 olması gerekirken 19 var. Hmmm. Birinci karakterden kalan 1*16 vardı zaten. 16+3 etti mi 19? (Burada Devlet Bahçeli mode on).

Geriye üçüncü karakter kaldı. Onu da kesin 64'e bölüyoruzdur. Gerçekten de "000" için 48/64=0. Kalanı da (48) şifrelenmiş tarafta 4. karakter olarak yazıyoruz. "111" için de aynı hesabı yapıp bölme işleminden kalanlarla gördüğümüz sayıları denkleştirince "şifrelemeyi" çözmüş oluyoruz. (3. karakter için 16'ya bölme işleminden kalan değer 4 ile çarpılıyor). Aşağıda hesabın tamamını görebilirsiniz.

1. 49/4=48 -> C
   r1=49%4=1
2. 49/16=3+r1*16=19 -> J
   r2=49%16=1
3. 49/64=0+r2*4=4 -> 4
   r3=49%64=49 -> n

İşimiz henüz tam bitmedi. Daha türkçemizin 'Ğ','İ','Ş' gibi güzide harflerini ne yapacağımız sorunu var. Bu harfleri içeren birşey şifrelediğimizde saçma şeyler görülüyor doğal olarak. Giriş alfabesi olarak başta standart ASCII tablosunu seçmiştik, ISO-8859-9'a geçince herşey yerli yerine oturuyor.

Farkettiyseniz sadece şifreleme algoritmasını çözdük. Şifrelenmiş veriyi çözmedik. Şifrelenmiş veriyi çözmek için bu işleri tersinden yapıyoruz. Onu uzun uzun anlatmayacağım. Onun yerine encode/decode işlemlerini yapan aşağıdaki C# kodunu kullanabilirsiniz. Koda bakarsanız özellikle Decode fonksiyonunda bazı ucubelikler göreceksiniz. Onlar, hadi artık sıkıldım aşamasında eklediğim şeyler (bazı girdiler için Decode fonksiyonu doğru çalışmıyordu).

public class SorguDecrypt
{
    // Şifrelenmiş veri alfabesi
    private char[] outAlphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz/++".ToCharArray();
    // Şifrelenmemiş veri alfabesi (ISO-8859-9)
    private char[] inAlphabet = null;

    private Encoding turkishEncoding = null;

    public SorguDecrypt()
    {
        turkishEncoding = Encoding.GetEncoding("iso-8859-9");
        var bytes = Enumerable.Range(0, 255).Select(a => (byte)a).ToArray();
        inAlphabet = turkishEncoding.GetChars(bytes);
    }

    public string Encode(string clearText)
    {
        if (String.IsNullOrEmpty(clearText)) return String.Empty;

        StringBuilder encodedString = new StringBuilder();

        clearText = clearText.PadRight(((clearText.Length - 1) / 3 + 1) * 3, (char)0);

        for (int j = 0; j < clearText.Length; j += 3)
        {
            var bytes = turkishEncoding.GetBytes(clearText.Substring(j, 3));

            int c1 = (int)(bytes[0]);
            int c2 = (int)(bytes[1]);
            int c3 = (int)(bytes[2]);

            int k1 = (c1) / 4;
            int k2 = (c1) % 4;
            int k3 = (c2) / 16;
            int k4 = (c2) % 16;
            int k5 = (c3) / 64;
            int k6 = (c3) % 64;

            char ec1 = outAlphabet[k1];
            char ec2 = outAlphabet[k3 + k2 * 16];
            char ec3 = outAlphabet[k5 + k4 * 4];
            char ec4 = outAlphabet[k6];

            encodedString.Append(ec1);
            encodedString.Append(ec2);
            if (c2 != 0) encodedString.Append(ec3);
            if (c3 != 0) encodedString.Append(ec4);
        }

        return encodedString.ToString();
    }

    public string Decode(string cipherText)
    {
        if (String.IsNullOrEmpty(cipherText)) return String.Empty;

        StringBuilder decodedString = new StringBuilder();

        cipherText = cipherText.PadRight(((cipherText.Length - 1) / 4 + 1) * 4, (char)0);

        for (int j = 0; j < cipherText.Length; j += 4)
        {
            string s = cipherText.Substring(j, 4);
            
            int c1 = Array.IndexOf(outAlphabet,(s[0]));
            int c2 = Array.IndexOf(outAlphabet, (s[1])) + (s[0] == '/' ? 64 : 0) ;
            int c3 = Array.IndexOf(outAlphabet, (s[2])) + (s[1] == '/' ? 64 : 0) ;
            int c4 = Array.IndexOf(outAlphabet, (s[3])) + (s[2] == '/' ? 64 : 0) ;

            int r1 = (c2 * 16) / 255;
            int r2 = (c3 * 64) / 255;

            char dc1 = inAlphabet[(c1 * 4 + r1)];
            char dc2 = inAlphabet[((c2 * 16 + r2) % 256)];
            int r3 = Math.Min(254,(c3 - Array.IndexOf(inAlphabet, dc2) % 16 * 4) % 64 * 64 + c4);
            char dc3 = c4 != -1 ? inAlphabet[r3] : (char)0;

            decodedString.Append((dc1));
            if (c3 != -1) decodedString.Append((dc2));
            if (c4 != -1) decodedString.Append((dc3));
        }

        return decodedString.ToString();
    }

    public static void Test()
    {
        SorguDecrypt decrypter = new SorguDecrypt();
        string enc = decrypter.Encode("DENEME");
        string dec = decrypter.Decode(enc);
        Console.WriteLine("{0}: {1}",enc,dec);
    }

}

Yukarıdaki koda Github üzerinden de erişebilirsiniz. Encode/Decode fonksiyonları ile ister veritabanını bu haliyle kullanın, isterseniz "temiz" halini dump edecek birşeyler yazın. Ben kendime eziyet etmeyi sevmediğimden rnd tablosunun temiz halini oluşturma yolunu seçtim (biraz uzun sürüyor). Son olarak, tablodaki kolon isimlerinin karşılıkları aşağıda:

RND3PIFIX : ID
RNDKE16GA : TC Kimlik No
RNDGG4FL3 : Adı
RND70UWQP : Soyadı
RND9TXRYM : Ana Adı
RND0HBYW9 : Baba Adı
RNDBGNC3K : Cinsiyet
RNDAM2KY8 : Doğum Yeri
RNDH0RH76 : Doğum Tarihi
RNDPOV6BQ : Nüfus İl
RND7FJWCH : Nüfus İlçe
RNDY5YM2W : Adres İl
RNDU0XFY2 : Adres İlçe
RNDQ1Y4D5 : Adres Muhtarlık
RND070KRE : Adres Cadde/Sokak
RNDSREP1W : Kapı No
RNDYG1P4F : Daire No

Adventure Time!

Şimdi gelsin komiklikler, şakalar.

Veri ne zamandan kalma?
MariaDB [kisisorgu]> SELECT dogumtarihi FROM tckimlik ORDER BY dogumtarihi DESC
LIMIT 1;
+-------------+
| dogumtarihi |
+-------------+
| 9/9/1990    |
+-------------+
1 row in set (0.00 sec)

Verilerin oy verme yaşındaki (>=18) kişilerden oluştuğunu bildiğimizden (gündemin nabzını tutmuştuk, oradan biliyoruz) bu tarihe 18 yıl ekleyip Eylül 2008 sonucuna ulaşıyoruz.

En yaşlı insan kaç yaşında?

Kayıtlara göre 2008 itibariyle Türkiye'deki en yaşlı insan:

MariaDB [kisisorgu]> SELECT dogumtarihi FROM tckimlik ORDER BY dogumtarihi ASC
LIMIT 1;
+-------------+
| dogumtarihi |
+-------------+
| //1330      |
+-------------+
1 row in set (0.00 sec)

Tam tamına 678 yaşında! Murad Hüdavendigâr hala hayatta. Şaka şaka. Hicri (Rumi) takvim kullanılıyormuş bir zamanlar. Hani şu her yıl 10 gün kayan. 1330 aslında 1914 demek. 94 yaşında bir insan sözkonusu.

Hicri takvim girdilerini atlayıp, akıl var mantık var takviminde en küçük değer nedir diye bakarsak 1894 görüyoruz. 114 yaşında birisi varmış 2008 yılında. Hala var mıdır bilmiyorum.

Kaç terazi burcu var?

Eminim ki hepimiz Türkiye'de kaç tane terazi burcu insan olduğunu merak ediyor ve bunu tek SQL sorgusuyla bulabilecek kadar SQL biliyoruz. Ben de herkes gibi merak ediyordum ve aşağıdaki sorguyu da yine herkes gibi tek seferde yazdım.

MariaDB [kisisorgu]> SELECT COUNT(*) FROM (SELECT @date:=STR_TO_DATE(dogumtarihi
, '%d/%m/%Y') AS date  FROM tckimlik  HAVING ((month(date)='9' AND day(date) bet
ween '24' AND '31') OR (month(date)='10' AND day(date) between '1' AND '23')) )
as `terazi`;
+----------+
| COUNT(*) |
+----------+
|  3240489 |
+----------+
Binlerce dansöz var mı?
MariaDB [kisisorgu]> SELECT dogumtarihi FROM tckimlik WHERE soyadi='DANSÖZ';
Empty set (0.00 sec)

Yok. Ama soyadı MUCUK olan 1622 kişi varmış. Liseyi atlatıp hayatta kalabilmişler.

Angaralı Turgut'ların sayısı
MariaDB [kisisorgu]> SELECT COUNT(*) FROM tckimlik WHERE adi='TURGUT' AND 
dogumyeri='ANKARA';
+----------+
| COUNT(*) |
+----------+
|      207 |
+----------+
1 row in set (1 min 1.12 sec)

Türkçe isim listesi

Son olarak ~1900 ile 1990 yılları arasında Türk vatandaşlarına verilmiş tüm isimleri, görülme sayıları ile birlikte erkek/kadın ayrı dosyalar halinde Github'da paylaştım.

Bitti, bu kadar.

Sevgiler.