<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="http://www.cihataltuntas.com/feed.xml" rel="self" type="application/atom+xml" /><link href="http://www.cihataltuntas.com/" rel="alternate" type="text/html" /><updated>2025-12-11T09:45:21+00:00</updated><id>http://www.cihataltuntas.com/feed.xml</id><title type="html">Cihat Altuntaş</title><subtitle>Life, Software Engineering, Investing</subtitle><author><name>Cihat Altuntaş</name></author><entry><title type="html">SSH Trafiğini Çözümleyelim 4 - Bellekte Anahtar Avı 1</title><link href="http://www.cihataltuntas.com/2025/12/04/decrypt-ssh-traffic-4.html" rel="alternate" type="text/html" title="SSH Trafiğini Çözümleyelim 4 - Bellekte Anahtar Avı 1" /><published>2025-12-04T14:00:00+00:00</published><updated>2025-12-04T14:00:00+00:00</updated><id>http://www.cihataltuntas.com/2025/12/04/decrypt-ssh-traffic-4</id><content type="html" xml:base="http://www.cihataltuntas.com/2025/12/04/decrypt-ssh-traffic-4.html"><![CDATA[<p>Bu yazı serisi şu ana kadar 4 bölümden oluşmaktadır, diğer bölümlere aşağıdaki linklerden ulaşılabilir. Yazı içeriğinde geçen kodlara
<a href="https://github.com/caltuntas/ssh-decryption">bu linkten</a> ulaşabilirsiniz.</p>

<ol>
  <li><a href="https://www.cihataltuntas.com/2025/02/04/decrypt-ssh-traffic-1">SSH Trafiğini Çözümleyelim 1 - Patch</a>
    <ul>
      <li>Bu yazıda, genel olarak SSH protokolünün yapısı ve şifreleme için
kullanılan anahtar değişim algoritmalarının nasıl çalıştığı inceliyoruz.
Ardından var olan bir SSH kütüphanesinin kodu değiştirilerek ele geçirilen
şifreleme anahtarlarını kendi yazdığımız kod ile kaydedilmiş bir trafiği
çözümlemek için kullanıyoruz.</li>
    </ul>
  </li>
  <li><a href="https://www.cihataltuntas.com/2025/04/10/decrypt-ssh-traffic-2">SSH Trafiğini Çözümleyelim 2 - Wireshark</a>
    <ul>
      <li>Bu yazıda, Wireshark kullanarak trafiği çözümlemek istediğimizde
karşılaştığımız sorunu hata ayıklaması yaparak tespit ediyoruz, sonrasında
da Wireshark kodunu düzelterek, trafiği Wireshark üzerinde de
çözümlüyoruz.</li>
    </ul>
  </li>
  <li><a href="https://www.cihataltuntas.com/2025/11/22/decrypt-ssh-traffic-3">SSH Trafiğini Çözümleyelim 3 - Private Key</a>
    <ul>
      <li>Bu yazıda, Private Key nedir, eğer ele geçirebilirsek trafiği başka
herhangi bir değer kullanmadan çözümleyebilir miyiz diye inceleme yapıyoruz.</li>
    </ul>
  </li>
  <li><a href="https://www.cihataltuntas.com/2025-12-04-decrypt-ssh-traffic-4">SSH Trafiğini Çözümleyelim 4 - Bellekte Anahtar Avı 1</a> (Bu yazı)
    <ul>
      <li>Bu yazıda, diğer başlıklarda yapılanın aksine, kullandığımız kütüphanede
herhangi bir değişiklik yapmadan, trafiği çözmemiz için gerekli
anahtarları bellekten bulmaya çalışıyoruz.</li>
    </ul>
  </li>
</ol>

<p>Önceki yazılarda hatırlarsanız, SSH nasıl çalışır, trafik nasıl şifrelenir gibi konulara değindikten sonra trafiği çözmek için günün sonunda ya bütün gerekli 
şifreleme anahtarlarını kodun içinde değişiklik yaparak yazdırdık ya da özel anahtarı yine kodu değiştirerek elde edip sonrasında diğer anahtarları ondan türeterek
trafiği çözümledik. Bu zamana kadar yaptıklarımız bize biraz SSH protokolü, biraz şifreleme, anahtar değişimi gibi konular öğretti ama hep kaçak güreştiğimizi kabul etmem lazım.</p>

<p>Peki bunu hiç kod değiştiremeden, şifreleri ekrana yazdırmadan trafik çözümlemesi yapabilir miyiz?
Bu yazıda işin zor kısmı için kolları sıvayıp daha önce incelediğimiz SSH kütüphanesi ile oluşturulmuş canlı olarak çalışan, bir oturumun şifreleme anahtarlarını bellekten kendimiz
bularak trafiği çözümlemeye çalışacağız. Bu tarz bellekten kanıt bulmak, veri çıkarmak <a href="https://en.wikipedia.org/wiki/Memory_forensics">Memory Forensics</a> başlığı altında inceleniyor ve genellikle <a href="https://github.com/volatilityfoundation/volatility">Volatility</a> gibi bu işe özel araçlarla daha kolay yapılabiliyor. 
Fakat bu konuyu daha derinlemesine öğrenmek için bu tarz bir araç kullanmadım ve yaparken hem zorlandım, hem çok fazla şey öğrendim hem de inanılmaz keyif aldım, umarım siz de aynı şekilde okurken keyif alırsınız, hadi başlayalım.</p>

<h2 id="şifreleme-anahtarları-nerede">Şifreleme Anahtarları Nerede?</h2>

<p>SSH ile bir uzak sunucuya bağlantı kurduğumuzda aslında arka planda bir program çalıştırmış oluyoruz ve gelen giden trafik o program tarafından çözümlenip bizim ekranımızda beliriyor.
Önceki yazılardan anlamış olduğumuz gibi, biz dışarıdan bakanlar olarak trafiği ele geçirsek de içeriğini anlayamıyoruz fakat o program kendi içeriğinde tuttuğu şifreleme anahtarları ile
trafiği çözümleyip biz kullanıcılara anlamlı bilgiler gösterebilir. Bu da bize, programın şifreleme anahtarlarını vermese de kendi içeriğinde bunları bir yerde tutup sürekli kullandığını
gösteriyor, aksi durumda kendisi de trafiği çözümleyemezdi zaten. Biz de bu varsayımı kullanıp program bu bilgileri bellekte ya da diskte nerede tutuyorsa oradan ele geçirip trafiği çözümlemeye çalışacağız.</p>

<p>Diskte tutmak çok anlamlı değil, hem kötü niyetli kişilerin disk üzerinden bu bilgileri ele geçirmesi çok kolay olur, hem de performans olarak sürekli şifrelenmiş veri alış verişi sırasında diske erişmek 
oldukça yavaş olacağı için geriye tek seçenek kalıyor <code>bellek</code>, yani çok büyük ihtimalle şifreleme anahtarları çalışan programın belleğinde tutuluyor, o zaman aramaya başlayalım.</p>

<p>Benim planım şu şekilde,</p>

<ol>
  <li>SSH bağlantısı yapan bir kod geliştir.</li>
  <li>Bağlantı sırasında şifreleme anahtarlarını ekrana yazdır.</li>
  <li>Bağlantıyı açık bırak.</li>
  <li>Başka bir program ile SSH bağlantısı yapan programın belleğini oku.</li>
  <li>Bellekte ekrana yazdırdığın şifreleme anahtarlarını ara.</li>
</ol>

<p>Eğer bu adımlar sonrasında bellekte şifreleme anahtarlarını bulabilirsek, şifrelerin bellekte saklandığı tezini doğrulamış oluruz ve stratejimizi ona göre hazırlayıp ilerleyebiliriz.</p>

<h2 id="ssh-bağlantısı-yapalım">SSH Bağlantısı Yapalım</h2>

<p>Önceki yazılarda bağlantıyı genelde kısa süreli bir <code>ls -lah</code> komutu çalıştırıp çıkacak şekilde hazırlamıştık fakat bu sefer bellek analizi yapabilmek için biraz daha uzun süreli açık kalması gerekiyor
bundan dolayı, klasik bir SSH client gibi bağlantı açan ve <code>ssh2</code>  kütüphanesini kullanan kodu <a href="https://github.com/caltuntas/ssh-decryption/blob/main/client.js">bu şekilde</a> oluşturduk. Kodu buraya 
koymadım çünkü interaktif bir SSH oturumu açmaktan çok fazla bir esprisi yok, o yüzden linkten detaylarına bakabilirsiniz. Kodun bahsetmeye değecek en önemli kısmı bence aşağıdaki algoritmaları ayarladığımız bölüm</p>

<pre><code>const algorithms = {
	cipher: [
	  'aes128-ctr',
	],
	hmac: [
	  'hmac-sha2-256',
	],
  };
</code></pre>

<p>Bu kısımda SSH bağlantısının belirli algoritmalar üzerinden yapılmasını istiyoruz, eğer bunu boş bırakırsak sunucu ve istemci farklı bir algoritma da seçebilir, bu da bizim
hem trafiği çözümleme kodunu hem de bellekte anahtarların bulunmasına kadar bütün her şeyi etkiler, bundan dolayı ilk yazıdan itibaren aynı şifreleme algoritması olan <code>aes128-ctr</code>‘ı burada da kullanıyoruz.</p>

<p>Config ve secret değerlerini ekrana yazdırması için oluşturduğumuz <code>patch</code> uygulanmış kodu aşağıdaki gibi çalıştırdım.</p>

<pre><code>&gt; node client.js testuser@192.168.1.110 "testpassword"                                                                                                                                                            
...
...
Private Key :  302e020100300506032b656e0422042018f89dc8d1969faf7f5591d3231540028bccda47cc0e7530dcbb2e2e6fc3c263
Handshake: (local) computeSecret: 5ae3bfdd0ab389a97eb087630b036795becb7f32f45df07ad4c27bf37ca3d603
Verifying signature ...
Verified signature
SECRET: 000000205ae3bfdd0ab389a97eb087630b036795becb7f32f45df07ad4c27bf37ca3d603
------config------
{"inbound":{"seqno":3,"decipherInfo":{"sslName":"aes-128-ctr","blockLen":16,"keyLen":16,"ivLen":16,"authLen":0,"discardLen":0,"stream":true},"decipherIV":{"type":"Buffer","data":[216,202,100,22,211,215,90,170,215,60,6,39,220,103,111,241]},"decipherKey":{"type":"Buffer","data":[197,172,134,44,188,88,40,30,146,89,151,190,72,6,126,232]},"macInfo":{"sslName":"sha256","len":32,"actualLen":32,"isETM":false},"macKey":{"type":"Buffer","data":[251,97,80,55,31,137,183,180,54,47,130,116,17,131,185,164,78,42,12,151,216,115,62,150,142,50,47,237,253,108,181,34]}},"outbound":{"seqno":3,"cipherInfo":{"sslName":"aes-128-ctr","blockLen":16,"keyLen":16,"ivLen":16,"authLen":0,"discardLen":0,"stream":true},"cipherIV":{"type":"Buffer","data":[111,178,55,78,44,50,149,116,178,91,157,229,20,114,204,101]},"cipherKey":{"type":"Buffer","data":[68,8,24,54,141,197,22,166,120,166,61,98,77,47,146,26]},"macInfo":{"sslName":"sha256","len":32,"actualLen":32,"isETM":false},"macKey":{"type":"Buffer","data":[135,125,115,155,89,79,230,20,65,111,65,113,114,189,147,91,118,43,79,219,247,173,62,87,99,123,187,149,29,50,154,196]}}}
Handshake completed
...
...
</code></pre>

<p>Yukarıdaki gibi oturum başlatan ve açık bırakan kodu çalıştırdık ve ekranda doğrulama için kullanacağımız <strong>Private Key, SECRET, config</strong> gibi değerleri görebildik.</p>

<h2 id="bellekte-ne-arıyoruz">Bellekte Ne Arıyoruz?</h2>

<p>Öncelikle yukarıdaki çıktıdan gördük ki, bellekte arayabileceğimiz Private Key, SECRET ve config bir kaç farklı değer bulunuyor. Bir önceki yazımızda <code>Private Key</code> değerini 
alarak trafiği çözdüğümüz için en mantıklı onu aramak gibi düşünebiliriz fakat maalesef onu arasak da bulamayacağız, sebebi SSH bağlantısını yapan <a href="https://github.com/mscdex/ssh2/blob/844f1edfc41589737671f96a4f4e76afdf46abd4/lib/protocol/kex.js#L983">kütüphanenin</a> aşağıdaki kod satırlarında gizli</p>

<pre><code>...
// Cleanup/reset various state
this._public = null;
this._dh = null;
this._kexinit = this._protocol._kexinit = undefined;
this._remoteKexinit = undefined;
this._identRaw = undefined;
this._remoteIdentRaw = undefined;
this._hostKey = undefined;
this._dhData = undefined;
this._sig = undefined;
...
</code></pre>

<p>SSH kütüphanesi, <code>config</code> nesnesini ve içinde bulunan gerekli tüm şifreleme anahtarlarını oluşturduktan sonra, yukarıdaki işlemi yaparak, <code>this._dh = null;</code> ile Diffie-Hellman algoritmasını
oluşturan nesneyi ve dolaylı olarak içinde tutulan değerleri yani <code>Private, Public Key</code> değerlerini temizlemiş oluyor. Nodejs garbage collector çalıştığında ilgili değerleri bellekten temizlendiği
için bu işlemi Private Key üzerinden yapmamız mümkün değil. Teoride eğer GC çalışmadan programın belleğine erişip değeri okursanız bu mümkün ama Nodejs GC ne zaman çalışacak belli değil, bu yüzden private key bulmaya çalışmak 
hem gereksiz bir çaba olacak ve hem de daha iyi ve garanti bir yöntem olduğundan bunu eliyoruz.</p>

<p>Zaten biraz düşünürsek hangi değerleri bellekten aramanın daha mantıklı olduğunu hemen bulabiliriz,  önceki yazılarda incelediğimizde SSH protokolünün <code>KEX</code> anahtar değişim sürecinin tek amacı
oturum boyunca kullanacağı şifreleme anahtarlarının üretilmesi, Private Key, Shared Secret aslında bunları üretmek için kullandığımız ara değerler ama KEX sürecinin sonunda oluşturduğumuz nihai 
çıktılar aslında <code>config</code> içinde bulunan <code>Client-&gt;Server</code> ya da <code>Server-&gt;Client</code> haberleşmede kullanılan şifreleme anahtarları(AES, MAC, IV). Bunları bellekten silme şansı da yok çünkü yeni bir anahtar değişim 
süreci başlayana kadar trafiği programın kendisinin de çözümlemesi için aynı anahtarları kullanmak zorunda.</p>

<p>Ne aradığımıza karar verdiğimize göre ekrana yazılan <code>config</code> nesnesinin içeriğini hex değerlere dönüştürerek aşağıya koyalım ve arama işlemimiz daha kolay hale gelsin.
Bu nesne <code>json</code> formatında ve anahtarlar <code>Buffer</code> olarak tutulduğu ve integer olarak ekrana yazıldığı için önce ekrana yazılan config satırını <code>config.json</code> dosyasına kaydettim. Ardından <code>jq</code> ile hex değerlere aşağıdaki gibi çevirdim.</p>

<pre><code>&gt; pbpaste &gt; config.json
&gt; cat config.json | jq -r '
    "dec:\(.inbound.decipherKey.data | join(","))",
    "enc:\(.outbound.cipherKey.data | join(","))",
    "decIv:\(.inbound.decipherIV.data | join(","))",
    "encIv:\(.outbound.cipherIV.data | join(","))"
  ' | while IFS=":" read -r key val; do
    echo "Key: $key, Value: $val"
    for num in ${(s:,:)val}; do
      printf "%02x" $num
    done
    echo
  done
  
  
Key: dec, Value: 197,172,134,44,188,88,40,30,146,89,151,190,72,6,126,232
c5ac862cbc58281e925997be48067ee8
Key: enc, Value: 68,8,24,54,141,197,22,166,120,166,61,98,77,47,146,26
440818368dc516a678a63d624d2f921a
Key: decIv, Value: 216,202,100,22,211,215,90,170,215,60,6,39,220,103,111,241
d8ca6416d3d75aaad73c0627dc676ff1
Key: encIv, Value: 111,178,55,78,44,50,149,116,178,91,157,229,20,114,204,101
6fb2374e2c329574b25b9de51472cc65
</code></pre>

<p>Aramamız gereken değerler, AES encryption, decryption ve bunlar için kullanılan <code>IV</code> değerlerini yukarıda hex olarak gördük, 
bunları daha sonra ekrana yazdırmadan kendimiz bulacağız fakat tezimizi kanıtlayana kadar böyle kalsın. Trafiği çözümlemek için bu değerlerin
hepsini bulmamız gerekecek ama ben bir tanesini seçip tezimi doğrulamaya çalışacağım, şifreleme anahtarı olan <code>440818368dc516a678a63d624d2f921a</code> değerini bulmaya çalışacağım.</p>

<h2 id="nerede-arıyoruz">Nerede Arıyoruz?</h2>

<p>Yukarıda ne arayacağımıza karar verdik peki bunu nerede arayacağız biraz onun üzerinde düşünelim. Başta ne demiştik SSH bağlantısını kuran da aslında diğer programlar gibi bir <code>process</code>. 
Onun belleğine erişebilirsek aradığımız değeri orada bulmaya çalışabiliriz, peki belleğe nasıl erişebiliriz?</p>

<p>Linux tabanlı işletim sistemlerinde ek bir yazılım geliştirmeye ihtiyaç duymadan programın belleğine <a href="https://en.wikipedia.org/wiki/Procfs">procfs yöntemi</a> ile ulaşabiliriz.
Yukarıda <code>node client.js ...</code> ile SSH bağlantısını oluşturmuştuk, procfs aracılığı ile belleğe biraz göz atalım tabi öncesinde <code>pid</code> değerini bulmamız gerekiyor.</p>

<pre><code>&gt; pgrep node
32162
</code></pre>

<p>Pid değerini aldıktan sonra procfs ile bellek bölgelerine göz atalım</p>

<pre><code>&gt; cat /proc/32162/maps

...
...
55903c57b000-55903c57f000 r-xp 00001000 fe:01 4886091                    /usr/bin/node
55903c57f000-55903c580000 r--p 00005000 fe:01 4886091                    /usr/bin/node
55903c580000-55903c581000 r--p 00005000 fe:01 4886091                    /usr/bin/node
55903c581000-55903c582000 rw-p 00006000 fe:01 4886091                    /usr/bin/node
55904cd5d000-55904d000000 rw-p 00000000 00:00 0                          [heap]
7f56a8000000-7f5728000000 ---p 00000000 00:00 0
7f5728000000-7f5729000000 rw-p 00000000 00:00 0
...
...
</code></pre>

<p>Liste oldukça uzun kısaltarak yukarıya ekledim, burada Linux işletim sisteminde nasıl tutulduğuna değinmeyeceğim, ama özetle 
bellekte programın kullandığı kütüphaneler, programın kendisi, çalışan kod hepsi belleğin çeşitli bölgelerinde tutuluyor ve bu liste onu gösteriyor diyebiliriz.</p>

<p>Burada biraz temel programlama bilgisi kullanarak bir tahmin yapacağım, şifreleme anahtarları çalışma zamanı yani <code>runtime</code> sırasında oluşturulan değerler olduğu için,
bunların belleğin <code>heap</code> bölgesinde olmasını bekliyorum. Eğer bunlar <code>compile-time</code> sırasında belirlenen hard-coded sabit değerler olsaydı bunları farklı bir yerde aramam gerekirdi.</p>

<pre><code>&gt; cat /proc/32162/maps | grep  'heap'
55904cd5d000-55904d000000 rw-p 00000000 00:00 0                          [heap]
</code></pre>

<p>Yukarıda belleğin heap bölgesinin başlangıç ve bitiş adreslerini bulduk, yani değerleri arayacağımız yer belirlendi, şimdi aramaya başlayalım.</p>

<h2 id="şifreleme-anahtarlarını-arayalım">Şifreleme Anahtarlarını Arayalım</h2>

<p>Arama işlemini yapmak için, aslında daha modern araçlar olsa da, önce GDB gibi bir araç kullanmadan bunu el ile yapmayı denemek istiyorum.</p>

<p>Belleğin heap bölgesinde arama yapacağımızı söylemiştik, bu bölgenin başlangıç ve bitiş adresleri de elimizde, o yüzden belleğin bu bölgesini meşhur <code>dd</code> <a href="https://en.wikipedia.org/wiki/Dd_(Unix)">disk destroyer</a> komutu ile <code>heap.dump</code> adında bir
dosyaya kaydetmek istiyorum.</p>

<pre><code>&gt; sudo dd if=/proc/32162/mem bs=4096 skip=$((0x55904cd5d000/ 4096)) count=$(((0x55904d000000-0x55904cd5d000)/4096)) &gt; mem.dump
  xxd -p heap.dump | tr -d '\n' | grep -ci '440818368dc516a678a63d624d2f921a'
  1
</code></pre>

<p>Dd komutunda belki açıklamaya değer <code>/proc/32162/mem</code> kısmı olabilir, <code>/proc/32162/maps</code> ile programın belleğin hangi bölgelerini kullandığını yani bir nevi haritasını aldık, gerçek anlamda
belleği okuma işlemini ise <code>/proc/pid/mem</code> ile yapıyoruz. Diğer dd detayı ise <code>heap</code> başlangıç adresine kadar olan kısmı <code>skip</code> ile atlıyoruz, sonrasında ise o bölgenin boyutu kadar <code>count</code> ile okuma işlemi
gerçekleştiriyoruz.</p>

<p>Kaydettikten sonra <code>xxd</code> ile hex formatına çevirip sonrasında grep ile arama yaptık arama sonucunda <code>1</code> bulunan sayı olarak ekranda gözüktü, yani bellekte aradığımız anahtar değerini bulduk ve tezimizi doğruladık.</p>

<h2 id="iv-değerlerini-arayalım">IV Değerlerini Arayalım</h2>

<p>Önceki yazılardan hatırlarsanız eğer, trafiği çözerken sadece şifreleme anahtarı değil ayrıca IV yani <a href="https://en.wikipedia.org/wiki/Initialization_vector">initialization vector</a> değerini de kullanmamız gerekiyordu. 
Bu kullanılan şifreleme algoritması modunun(Aes Ctr) gerektirdiği bir durum, o yüzden bellekte IV değerlerini de bulmamız gerekiyor. Daha önce yaptığımız işlemin benzerini ekrana
yazdırdığımız IV değeri için de yapıyorum ve bellekte daha önce kaydettiğim <code>heap</code> dosyasında o değeri aşağıdaki gibi arıyorum.</p>

<pre><code>&gt; xxd -p heap.dump | tr -d '\n' | grep -ci '6fb2374e2c329574b25b9de51472cc65'
0
</code></pre>

<p>Maalesef aradığımız değeri bulamadık, peki neden? Bu kısım beni en çok uğraştıran ve hatta sonrasında olayı iyice anlamak için sıfırdan Aes-Ctr şifreleme algoritması geliştirmeme yol açan kısım.
Çok detaya şimdilik girmeden, IV değerinin son 2 karakterini silip tekrar aynı bellek dump dosyasında arama yaptım</p>

<pre><code>&gt; xxd -p heap.dump | tr -d '\n' | grep -ci '6fb2374e2c329574b25b9de51472cc'
1
</code></pre>

<p>Evet sonuç bu sefer pozitif, tam aradığımız IV değeri olmasa da son 2 değeri haricinde aynı IV değerini bellekte bulabildik. Bunun detaylı sebebine yani neden ilk ekranda yazan IV değerini bulamadığımız konusuna daha sonra değineceğiz.</p>

<h2 id="eğlenceli-kısım">Eğlenceli Kısım</h2>

<p>Buraya kadar tezimizi doğruladık, trafiği çözmemiz için gerekli materyal bellekte bulunuyor diye düşündük, bunları da doğrulamak için ekrana yazdırarak bellekte aradık ve değerleri bulabildik. 
Şimdi işin eğlenceli kısmına geldi, bu değerleri ekrana yazdırmadan bellekten nasıl bulabiliriz? Yani içeride bir Aes key var ise onun değerini bilmeden bellekten bulup çıkarmak istiyoruz.</p>

<p>Bunu yapabilmek için, tabi Aes algoritması nasıl çalışıyor bilmemiz lazım, en iyi öğrenme yöntemi hep dediğim gibi kolları sıvayıp kendimizin geliştirmesi ki, ben de böyle yaptım ve temel Aes şifreleme algoritması ve yaygın kullanılan
modlarının bazılarını kendim geliştirdim, <a href="https://github.com/caltuntas/aes">buradan</a> kaynak koduna erişebilirsiniz.</p>

<h3 id="aes-temelleri">AES Temelleri</h3>

<p>Aes şifreleme algoritmasının detaylarını
<a href="https://en.wikipedia.org/wiki/Advanced_Encryption_Standard">buradan</a> öğrenebilirsiniz ama ben yine de bizim işimizle ilgili olan kısmını şöyle açıklamaya çalışayım.
Aes şifreleme algoritması 128 bit bloklar üzerinde işlem yapıyor, bu şu anlama
geliyor. Örneğin şifreleme için verdiğiniz uzun bir metin var, Bunu 128 bitlik
bloklara ayırıyor ve ardından, her blok şifreleme anahtar uzunluğuna göre (128,192,256)
ilgili bloğu 10,12 ya da 14 defa şifreleme işleminden geçiriyor.</p>

<p>Bu blok şifreleme işlemini yaparken de, güvenlik açısından her turda ana şifreleme anahtarından türemiş farklı bir anahtar kullanıyor ve bunlar <code>round key</code> olarak adlandırıyor.
Genelde Aes şifreleme yapan kütüphaneler performans nedeniyle bu <code>round key</code> hesaplama işini her blok için ayrı ayrı yapmaktansa bir defa hesaplayıp bellekte saklıyor, çünkü diğer bloklar için de
aynı tur anahtarlarını kullanacak ve bunu tekrar hesaplamaya çalışırsa büyük boyutlu bir metni şifrelemek oldukça fazla CPU kaynağı tüketecek ve performansı kötü etkileyecek.</p>

<h3 id="bellekte-round-key-arıyoruz">Bellekte Round Key Arıyoruz</h3>

<p>Geliştirdiğim AES algoritmasına hesaplanan round key değerlerini ekrana yazdırması için bir özellik ekledim. Amacım bellekte bu tarz tur anahtarlarını da bulup bulamayacağımızı test etmek, çünkü ekrana yazdırmadan
AES anahtarı bulma stratejim tamamen buna bağlı. Yeni bir SSH oturumu açtım sonrasında ekrana yazılan AES anahtarlarından birisi olan <code>242ab0824289a9084e85f1c7727e2b02</code> alıp kendi yazdığım AES algoritmasına parametre olarak aşağıdaki gibi verdim.</p>

<pre><code>aes &gt; ./aes 242ab0824289a9084e85f1c7727e2b02
round[0] key=242ab0824289a9084e85f1c7727e2b02
round[1] key=d6dbc7c294526ecadad79f0da8a9b40f
round[2] key=0756b1009304dfca49d340c7e17af4c8
round[3] key=d9e959f84aed8632033ec6f5e244323d
round[4] key=caca7e608027f85283193ea7615d0c9a
round[5] key=9634c68f16133edd950a007af4570ce0
round[6] key=edca2730fbd919ed6ed319979a841577
round[7] key=f293d288094acb656799d2f2fd1dc785
round[8] key=d65545dcdf1f8eb9b8865c4b459b9bce
round[9] key=d941ceb2065e400bbed81c40fb43878e
round[10] key=f556d7bdf30897b64dd08bf6b6930c78
</code></pre>

<p>Daha önce yaptığımız gibi belleğin heap bölgesini dosyaya kaydettim ve ana anahtar ve bu tur anahtarlarını tek tek arıyorum.</p>

<pre><code>&gt; xxd -p mem.dump | tr -d '\n' | grep -ci '242ab0824289a9084e85f1c7727e2b02'
1
&gt; xxd -p mem.dump | tr -d '\n' | grep -ci 'd6dbc7c294526ecadad79f0da8a9b40f'
1
&gt; xxd -p mem.dump | tr -d '\n' | grep -ci '0756b1009304dfca49d340c7e17af4c8'
1
&gt; xxd -p mem.dump | tr -d '\n' | grep -ci 'd9e959f84aed8632033ec6f5e244323d'
1
...
</code></pre>

<p>Tahmin ettiğimiz gibi, <code>round key</code>  değerleri bir defa hesaplanıp bellekte heap bölgesinde saklanıyor, bu bilgi bizim için oldukça önemli çünkü bunu
kullanarak bellekten nasıl AES anahtarı çıkarabiliriz bunun algoritmasını çıkaracağız.</p>

<h3 id="algoritmayı-oluşturalım">Algoritmayı Oluşturalım</h3>

<p>Algoritmayı oluşturmadan önce, round key değerlerinin bellekte hangi adreste tutulduğunu doğrulamak istedim bunun için, GDB ile ilgili programa bağlanıp round-key değerlerini
belirli formata çevirdikten sonra <a href="https://github.com/hugsy/gef">GDB Enhanced Features</a> eklentisinin sağladığı <code>grep</code> komutu ile arama yapıp, bellekte nerede tutulduklarına bakıyorum.</p>

<pre><code>&gt; gdp -p 32162
...
...
gef➤  grep '\\x24\\x2a\\xb0\\x82\\x42\\x89\\xa9\\x08\\x4e\\x85\\xf1\\xc7\\x72\\x7e\\x2b'
[+] Searching '\x24\x2a\xb0\x82\x42\x89\xa9\x08\x4e\x85\xf1\xc7\x72\x7e\x2b' in memory
[+] In '[heap]'(0x55d61a621000-0x55d61a8bb000), permission=rw-
  0x55d61a703c60 - 0x55d61a703c9c  →   "\x24\x2a\xb0\x82\x42\x89\xa9\x08\x4e\x85\xf1\xc7\x72\x7e\x2b[...]"
gef➤  grep '\\xd6\\xdb\\xc7\\xc2\\x94\\x52\\x6e\\xca\\xda\\xd7\\x9f\\x0d\\xa8\\xa9\\xb4\\x0f'
[+] Searching '\xd6\xdb\xc7\xc2\x94\x52\x6e\xca\xda\xd7\x9f\x0d\xa8\xa9\xb4\x0f' in memory
[+] In '[heap]'(0x55d61a621000-0x55d61a8bb000), permission=rw-
  0x55d61a703c70 - 0x55d61a703cb0  →   "\xd6\xdb\xc7\xc2\x94\x52\x6e\xca\xda\xd7\x9f\x0d\xa8\xa9\xb4\x0f[...]"
gef➤  grep '\\x07\\x56\\xb1\\x00\\x93\\x04\\xdf\\xca\\x49\\xd3\\x40\\xc7\\xe1\\x7a\\xf4\\xc8'
[+] Searching '\x07\x56\xb1\x00\x93\x04\xdf\xca\x49\xd3\x40\xc7\xe1\x7a\xf4\xc8' in memory
[+] In '[heap]'(0x55d61a621000-0x55d61a8bb000), permission=rw-
  0x55d61a703c80 - 0x55d61a703cc0  →   "\x07\x56\xb1\x00\x93\x04\xdf\xca\x49\xd3\x40\xc7\xe1\x7a\xf4\xc8[...]"
</code></pre>

<p>Yukarıdaki çıktıdan zaten daha önce tespit ettiğimiz şeyi görüyoruz fakat ek olarak bellekte hangi adreste tutuldukları da elimizde var, baştan sonra tutuldukları adresler aşağıdaki gibi</p>

<pre><code>0x55d61a703c60 - 0x55d61a703c9c  →   "242ab0824289a9084e85f1c7727e2b (şifreleme anahtarı)"
0x55d61a703c70 - 0x55d61a703cb0  →   "d6dbc7c294526ecadad79f0da8a9b40f (round key 1)"
0x55d61a703c80 - 0x55d61a703cc0  →   "0756b1009304dfca49d340c7e17af4c8 (round key 2)"
...
</code></pre>

<p>Bu da bize şifreleme anahtarı ve ondan türeyen round-key değerlerinin bellekte art arda tutulduğunu gösteriyor, bunu adreslerin başlangıç değerlerinden son bölümlerinden(..60,..70,..80) anlayabiliyoruz.
Bu bilgiyi kullanarak şöyle bir algoritma oluşturabiliriz.</p>

<ol>
  <li>Belleğin ilk adresini başlangıç olarak al</li>
  <li>128 bit(16 byte) oku (şifreleme ve round-key boyutu)</li>
  <li>Aldığın bu değeri AES anahtarı gibi düşün ve round-key değerini hesapla</li>
  <li>Bir sonraki 128 bit(16 byte) değeri oku</li>
  <li>Hesapladığın round-key değeri ile karşılaştır, yanlış ile bellekten ilk seçtiğin adresi 1 byte kaydırarak devam et.</li>
  <li>Eğer karşılaştırdığın değer aynı ise, bir sonraki round-key hesabını yap ve onu da sonraki 128 bitlik değer ile karşılaştır</li>
  <li>Bu işlemi 10 defa yapıp her defasında hesapladığın round-key ile bellekte bulunan 128 bitlik değer aynı ise, tebrikler şifreleme anahtarını buldun.</li>
</ol>

<p>Temel algoritma genellikle <a href="https://www.geeksforgeeks.org/dsa/window-sliding-technique/">sliding window</a> olarak adlandırılıyor, tabi bizim burada yapacağımız, round-key hesaplaması doğrulaması gibi
farklılıklar içeriyor. Algoritma şöyle çalışacak diyebiliriz.</p>

<pre><code>Bellek:
[00|10|25|A1|7C|99|FF|3B|8D|C4|65|77|23|88|09|AE|01|02|03|04|05|EE|F5|...]

Window Size = 16 bytes
-------------------------------------------------------

Aday Anahtar: 0 [00|10|25|A1|7C|99|FF|3B|8D|C4|65|77|23|88|09|AE]
Aday Anahtar: 1    [10|25|A1|7C|99|FF|3B|8D|C4|65|77|23|88|09|AE|01]
Aday Anahtar: 2       [25|A1|7C|99|FF|3B|8D|C4|65|77|23|88|09|AE|01|02]
Aday Anahtar: 3          [A1|7C|99|FF|3B|8D|C4|65|77|23|88|09|AE|01|02|03]
</code></pre>

<p>Her aday anahtar seçimi sonrası yukarıda bahsettiğim, o anahtar için round-key hesaplanıp sonraki 16 byte ile karşılaştıracak doğru olması durumunda ise, aynı karşılaştırma 
10 defa yapılacak, hepsi doğru ise bu aday anahtarı artık gerçek anahtar olarak değerlendireceğiz, değil ise yeni bir aday anahtarı yukarıdaki gibi seçip devam edecek.</p>

<p>Yazı oldukça uzadı ama en azından algoritmayı oluşturabildik. Bir sonraki bölümde algoritmayı koda dönüştürüp, trafiği çözümlemede önümüze çıkacak diğer sorunları halledip bunu çalışan bir araca dönüştüreceğiz ki, bu
serinin en heyecanlı bölümü ve aynı zamanda kapanışı olacak.</p>]]></content><author><name>Cihat Altuntaş</name></author><category term="ssh" /><category term="nodejs" /><category term="openssl" /><category term="memory" /><category term="forensics" /><category term="aes" /><summary type="html"><![CDATA[SSH Trafiğini Çözümleyelim 4 - Bellekte Anahtar Avı]]></summary></entry><entry><title type="html">SSH Trafiğini Çözümleyelim 3 - Private Key</title><link href="http://www.cihataltuntas.com/2025/11/22/decrypt-ssh-traffic-3.html" rel="alternate" type="text/html" title="SSH Trafiğini Çözümleyelim 3 - Private Key" /><published>2025-11-22T14:00:00+00:00</published><updated>2025-11-22T14:00:00+00:00</updated><id>http://www.cihataltuntas.com/2025/11/22/decrypt-ssh-traffic-3</id><content type="html" xml:base="http://www.cihataltuntas.com/2025/11/22/decrypt-ssh-traffic-3.html"><![CDATA[<p>Bu yazı serisi şu ana kadar 3 bölümden oluşmaktadır, diğer bölümlere aşağıdaki linklerden ulaşılabilir. Yazı içeriğinde geçen kodlara
<a href="https://github.com/caltuntas/ssh-decryption">bu linkten</a> ulaşabilirsiniz.</p>

<ol>
  <li><a href="https://www.cihataltuntas.com/2025/02/04/decrypt-ssh-traffic-1">SSH Trafiğini Çözümleyelim 1 - Patch</a>
    <ul>
      <li>Bu yazıda, genel olarak SSH protokolünün yapısı ve şifreleme için
kullanılan anahtar değişim algoritmalarının nasıl çalıştığı inceliyoruz.
Ardından var olan bir SSH kütüphanesinin kodu değiştirilerek ele geçirilen
şifreleme anahtarlarını kendi yazdığımız kod ile kaydedilmiş bir trafiği
çözümlemek için kullanıyoruz.</li>
    </ul>
  </li>
  <li><a href="https://www.cihataltuntas.com/2025/04/10/decrypt-ssh-traffic-2">SSH Trafiğini Çözümleyelim 2 - Wireshark</a>
    <ul>
      <li>Bu yazıda, Wireshark kullanarak trafiği çözümlemek istediğimizde
karşılaştığımız sorunu hata ayıklaması yaparak tespit ediyoruz, sonrasında
da Wireshark kodunu düzelterek, trafiği Wireshark üzerinde de
çözümlüyoruz.</li>
    </ul>
  </li>
  <li><a href="https://www.cihataltuntas.com/2025/11/22/decrypt-ssh-traffic-3">SSH Trafiğini Çözümleyelim 3 - Private Key</a> (Bu yazı)
    <ul>
      <li>Bu yazıda, Private Key nedir, eğer ele geçirebilirsek trafiği başka
herhangi bir değer kullanmadan çözümleyebilir miyiz diye inceleme yapıyoruz.</li>
    </ul>
  </li>
</ol>

<p>SSH trafiğini çözümleme yolculuğumuzda, bir önceki yazımızda ilk yazıda deneyip başarısız olduğumuz <code>Wireshark</code> ile trafiği çözümleme işleminin Wireshark kodunda bulunan bir hata 
sebebiyle oluştuğunu tespit edip ardından düzeltip, kaydettiğimiz SSH trafiğini Wireshark üzerinde çözümlemeyi başarmıştık. Bunu yaparken SSH kodunun içinde yaptığımız değişiklik ile 
<code>Shared Key</code> değerini yazdırıp bu değeri de Wireshark üzerinde ayarlayarak sağlamıştık.</p>

<p>Bu sefer hep duyduğumuz fakat benim açımdan deneme fırsatımın olmadığı bir konu üzerine gidelim. Hani güvenlik konularında <code>Private Key</code> kavramı hep konuşulur ve 
bu değer olmadan trafiği ya da şifrelenmiş bilgileri çözümlemenin neredeyse imkansız olduğundan bahsedilir, bu yazıda daha önceki konularda incelediğimiz 
SSH trafiğini o bahsedilen meşhur <code>Private Key</code> anahtarını kullanarak çözümlemeye çalışacağız. Bunu yaparken SSH protokolünü, özel anahtarın nasıl üretildiğini, ve gerçekten özel
anahtarı ele geçiren birisinin trafiği nasıl çözümleyebileceğini anlayacak örnek kod ile pekiştireceğiz, başlayalım.</p>

<h2 id="hatırlayalım">Hatırlayalım</h2>

<p>İlk yazıyı hatırlayacak olursanız, trafiği çözümlemeyi koda uyguladığımız bir <code>patch</code> ile encryption ve decryption aşamalarında kullanılan tüm bilgileri 
içeren <code>config</code> objesinin içeriğini alarak yapmıştık. Bu nesne içinde hem gelen <code>inbound</code> hem de giden <code>outbound</code> trafik için kullanılan tüm şifreleme 
anahtarlarını, tiplerini ve özelliklerini aşağıdaki gibi içerdiği için trafiği kod ile çözmek bunları düzgün şekilde kullanmak kadar kolaydı.</p>
<pre><code>{
  "inbound": {
    "seqno": 3,
    "decipherInfo": {
      "sslName": "aes-128-ctr",
      "blockLen": 16,
      "keyLen": 16,
      "ivLen": 16,
      "authLen": 0,
      "discardLen": 0,
      "stream": true
    },
    "decipherIV": {
      "type": "Buffer",
      "data": [ 42, 89, 189, 83, 230, 140, 64, 64, 215, 11, 40, 159, 8, 122, 39, 0 ]
    },
    "decipherKey": {
      "type": "Buffer",
      "data": [ 102, 79, 234, 204, 184, 103, 135, 48, 72, 170, 49, 72, 101, 246, 79, 140 ]
    },
    "macInfo": {
      "sslName": "sha256",
      "len": 32,
      "actualLen": 32,
      "isETM": false
    },
    "macKey": {
      "type": "Buffer",
      "data": [ 172, 195, 86, 179, 48, 9, 240, 252, 8, 51, 81, 213, 27, 199, 239, 2, 218, 160, 169, 129, 213, 139, 247, 86, 59, 217, 220, 141, 18, 207, 121, 62 ]
    }
  },
  ...
  ...
</code></pre>

<p>Peki yukarıda gördüğümüz bu değerler nasıl oluşturuluyor? Bu değerler ilk
yazıda bahsettiğimiz Diffie-Hellman algoritmasının bir parçası olarak sunucu ve
istemcinin seçip kimse ile paylaşmadığı <code>Private Key</code> yani özel anahtar değerini kullanarak
hesaplanıyor. Hazırladığımız patch içeriğinde aslında özel anahtarı da ekrana yazdırmıştık ama kolaya kaçıp
direk hazırlanmış olan yukarıdaki şifreleme anahtarlarını kullanarak trafiği çözümlemiştik. Çıktı aşağıdaki gibiydi.</p>

<pre><code>Private Key :  302e020100300506032b656e04220420a852b2ec81f6f095bd1aa504a5af1d2dc40e52c459f53da38be47ef08682a86d
Handshake: (local) computeSecret: 00bcc3375a02c54aa7e33de84687dac73586419d8d7a230dfd32e7b803da5dcf47
Verifying signature ...
Verified signature
SECRET: 0000002100bcc3375a02c54aa7e33de84687dac73586419d8d7a230dfd32e7b803da5dcf47
------config------
{
  "inbound": {
  ...
  ...
</code></pre>

<p>Kısacası config içinde bulunan önce <code>SECRET</code> olarak yukarıda gördüğümüz paylaşılan anahtar, ve bu paylaşılan anahtar ile hesaplanan tüm diğer şifreleme 
anahtarları üst satırda görülen <code>Private Key</code> değeri ile ortaya çıkıyor. Biz de aynı şeyi yapıp eğer özel anahtar değerini ele geçirebilirsek trafiği çözümleme işlemini 
nasıl yaparız onu inceleyeceğiz.</p>

<p>Bu arada kodda kritik bir güvenlik açığı bulunmadığı sürece normal şartlarda özel anahtar değerini ele geçirmek imkansıza yakın diyebiliriz,
bu yüzden biz yine ilk kullandığımız patch yöntemi ile ekrana yazdırdığımız özel anahtar değerini kullanarak bu işlemleri gerçekleştireceğiz.</p>

<h2 id="özel-anahtarın-yeri">Özel Anahtarın Yeri</h2>

<p>İlk yazımızda <strong>Sihirli Kelimemiz: Diffie–Hellman</strong> başlığı altında başlığı altında kalem kağıt ile basit olarak özel anahtarın nerede ve nasıl kullanıldığından bahsetmiştim.
SSH protokolü özelinde daha fazla detaylara <a href="https://datatracker.ietf.org/doc/html/rfc4419">bu RFC dokümanı</a> üzerinden ulaşabilirsiniz. Hatırlamak için aşağıya bu sefer SSH 
protokolünde kullanılan isimleri ile koyalım ama basit hesaplanması için ilk yazıdaki bölüme bakmanızı tavsiye ederim.</p>

<pre><code>e = g^x mod p
f = g^y mod p
K = e^y mod p
K = f^x mod p
</code></pre>

<p>Yukarıda <code>x</code> ve <code>y</code> istemci ve sunucunun kendi seçip kimse ile paylaşmadığı özel anahtarları temsil ediyor. <code>K</code> ise
ulaşmak istediğimiz ortak şifreleme anahtarımız. Özet olarak hem sunucu, hem istemcinin amacı paylaşılmayan özel anahtarları <code>x ve y</code> ve paylaşılan <code>e</code> ve <code>f</code> gibi genel anahtarları ve
<code>p</code> , <code>g</code> gibi her iki tarafında bildiği değerleri kullanarak ortak anahtar <code>K</code> değerine ulaşmak.</p>

<h2 id="ssh-ve-anahtar-değişimi">SSH ve Anahtar Değişimi</h2>

<p>Yukarıdaki açıklamadan anlaşıldığı gibi, sunucu ve istemcinin <code>e</code> ve <code>f</code> gibi birbirlerinin genel anahtarlarını ve <code>p</code> ve <code>g</code> gibi formülde bulunan bazı değerleri
her iki tarafından da bilmesi gerekiyor ki aynı ortak anahtara ulaşabilsinler, ilgili RFC dokümanında detaylı belirtilse de ben kendi anlayışıma yardım 
etsin diye bunu bir akış diyagramına çevirmeyi daha kolay buluyorum. Karşılıklı bu bilgiler nasıl paylaşılıyor ve süreç nasıl işliyor görelim.</p>

<pre><code class="language-mermaid">sequenceDiagram
    Client-&gt;&gt;Server: SSH_MSG_KEXINIT
    Server-&gt;&gt;Client: SSH_MSG_KEXINIT

    Client-&gt;&gt;Client: Choose private key 'x'
    Client-&gt;&gt;Client: Public key e = g^x mod p
  
    Server-&gt;&gt;Server: Choose private key 'y'
    Server-&gt;&gt;Server: Public key f = g^y mod p
   

    Client-&gt;&gt;Server: SSH_MSG_KEXDH_INIT&lt;br/&gt;Send Public Key(e)
    Server-&gt;&gt;Client: SSH_MSG_KEXDH_REPLY&lt;br/&gt;Send Public Key(f)

    Client-&gt;&gt;Client: Compute shared secret K = e^y mod p
    Server-&gt;&gt;Server: Compute shared secret K = f^y mod p
</code></pre>

<p>Peki bu dediklerimiz gerçek bir SSH trafiği üzerinde nasıl gözüküyor ona bakalım, nede olsa amacımız böyle bir trafiği
alıp elimizdeki <code>Privaye Key</code> değerlerinden biri ile çözümlemek.</p>

<p><img src="/img/sshdecrypt/wireshark-kex1.png" alt="Capture 1" />
<img src="/img/sshdecrypt/wireshark-kex2.png" alt="Capture 2" />
<img src="/img/sshdecrypt/wireshark-kex3.png" alt="Capture 3" /></p>

<h2 id="ortak-anahtarın-hesaplanması">Ortak Anahtarın Hesaplanması</h2>

<p>Elimizde özel anahtar var diyelim, yukarıdaki formüle bakarak aslında ortak anahtar <code>K</code> değerini kolaylıkla hesaplayabiliriz.
Bunu kod ile yapacağımızı düşürsek de, aşağıdakine benzer bir kod, ortak anahtarı bize verecektir.</p>

<pre><code class="language-javascript">const privKey = "somerandomvalue";
const clientPrivateKey = Buffer.from(privKey, "hex"); 

const dh = crypto.createDiffieHellmanGroup('modp2');
const dhKey = crypto.createDiffieHellman(dh.getPrime(),dh.getGenerator());
dhKey.setPublicKey(clientDhPubKey);
dhKey.setPrivateKey(clientPrivateKey);
let secret = dhKey.computeSecret(serverDhGexReply.f);
</code></pre>

<p>Pcap formatında kaydettiğimiz trafiği tekrar okuyup genel anahtar <code>f,e</code> değerlerini ekranda gördüğümüz gibi açık olarak okuyabiliyoruz, elimizde ekrana yazdırarak bulduğumuz
özel anahtar <code>privKey</code> de var. Bunları kullanarak ortak anahtar <code>K</code> yani <code>secret</code> değerini yukarıdaki gibi hesaplayabiliriz.</p>

<p>Peki elimizde hesapladığımız ortak anahtar değeri ile bütün trafiği çözebilecek miyiz? Maalesef hayır, günün sonunda yukarıda <code>config</code> nesnesi içinde bulunan
neredeyse tüm değerlere önce ulaşmamız gerekecek. Ancak ardından trafiği çözebilecek hale geleceğiz. Bunun sadece ilk adımı ortak anahtar <code>shared secret</code>
değerinin hesaplanması. Bundan sonrası için SSH protokol dokümanından <a href="https://datatracker.ietf.org/doc/html/rfc4253#section-7.2">Output from Key Exchange</a> kısmına bakarak
başlayabiliriz.</p>

<blockquote>
  <blockquote>
    <p>The key exchange produces two values: a shared secret K, and an exchange hash H.  Encryption and authentication keys are derived from these.</p>
  </blockquote>
</blockquote>

<p>Dokümandan anlaşıldığı gibi, biz <code>K</code> değerini elde etmek sadece ilk adım sonrasında <code>H</code> yani <strong>hash</strong> değerini en sonunda da gerçek anlamda paketleri
çözümleyebilecek şifreleme anahtarlarını elde edeceğiz.</p>

<h2 id="hash-değerinin-hesaplanması">Hash Değerinin Hesaplanması</h2>

<p>Yine RFC dokümanında <a href="https://datatracker.ietf.org/doc/html/rfc4253#section-8">bu kısmın</a> sonunda <strong>hash</strong> değerinin nasıl hesaplandığı belirtilmiş.</p>

<pre><code>The hash H is computed as the HASH hash of the concatenation of the following:

  string    V_C, the client's identification string (CR and LF excluded)
  string    V_S, the server's identification string (CR and LF excluded)
  string    I_C, the payload of the client's SSH_MSG_KEXINIT
  string    I_S, the payload of the server's SSH_MSG_KEXINIT
  string    K_S, the host key
  mpint     e, exchange value sent by the client
  mpint     f, exchange value sent by the server
  mpint     K, the shared secret
</code></pre>

<p>Yukarıdaki açıklamaya bakarak, şöyle bir kod yazdım, detaylarına birazdan değineceğim.</p>

<pre><code>function concat(...strings){
  const buffers =[];
  strings.forEach(str =&gt; {
    const lenBuffer = Buffer.allocUnsafe(4);
    lenBuffer.writeUInt32BE(str.length,0);
    buffers.push(lenBuffer);
    buffers.push(Buffer.from(str));
  });
  return Buffer.concat(buffers);
}

const hash = crypto.createHash("sha1");
sessionId = hash.update(concat(
  clientIdentification,
  serverIdentification,
  clientKexInitPayload,
  serverKexInitPayload,
  serverDhGexReply.host_key,
  clientDhPubKey,
  serverDhGexReply.f,
  secret
)).digest();
</code></pre>

<p>Hash hesaplamasında kullanılan ortak anahtar <code>K</code> haricindeki bütün değerler zaten açık olarak gönderildiği için değerlerini paketleri parse ederken zaten alabiliyoruz. 
Bu değerleri SSH dokümanında dediği gibi <code>concat</code> işlemine tabi tutuyoruz, fakat tutarken de, SSH protokolünde bir tipi karşı tarafa iletirken ya da kullanırken her zaman <code>length</code>
değerini de önüne eklemeniz gerektiğinden <code>lenBuffer</code> ile bu işlemi yapıp sonuna değerin kendisini ekliyoruz.</p>

<p>Yukarıda kod içinde kullandığımız değerlerin Wireshark üzerinde nasıl gözüküyor bakalım, kod ile bu değerleri nasıl okuduğuma ayrıca değineceğim.</p>

<p><img src="/img/sshdecrypt/wireshark-kex4.png" alt="Capture 4" /></p>

<h2 id="şifreleme-anahtarlarının-hesaplanması">Şifreleme Anahtarlarının Hesaplanması</h2>

<p>Yine daha önce referans verdiğimiz <a href="https://datatracker.ietf.org/doc/html/rfc4253#section-7.2">Output from Key Exchange</a> kısmının en sonunda
diğer şifreleme anahtarlarının da nasıl elde edileceği yazılmış.</p>

<pre><code>   Encryption keys MUST be computed as HASH, of a known value and K, as follows:
   
   o  Initial IV client to server: HASH(K || H || "A" || session_id)
      (Here K is encoded as mpint and "A" as byte and session_id as raw
      data.  "A" means the single character A, ASCII 65).
   o  Initial IV server to client: HASH(K || H || "B" || session_id)
   o  Encryption key client to server: HASH(K || H || "C" || session_id)
   o  Encryption key server to client: HASH(K || H || "D" || session_id)
   o  Integrity key client to server: HASH(K || H || "E" || session_id)
   o  Integrity key server to client: HASH(K || H || "F" || session_id)
</code></pre>

<p>Evet son aşamaya gelmiş sayılırız, paketleri okuyabilmek için hesaplamamız gereken şifreleme anahtarlarının hepsi yukarıda belirtilmiş.
Elimizde <code>K</code> yani özel anahtar var, anahtar değişimi sırasında açık olarak gönderilen bilgilerle hesapladığımız hash yani <code>H</code> değerimiz de var,
geriye kalan her anahtarı yukarıda belirtildiği gibi hesaplamak. Bunu koda dökmeden şu kısmı da not olarak aynı referanstan buraya ekleyeyim.</p>

<blockquote>
  <blockquote>
    <p>The exchange hash H from the first key exchange is additionally used as the session identifier</p>
  </blockquote>
</blockquote>

<pre><code class="language-javascript">
function deriveKey(K, H, sessionId, char, hashAlgo) {
  const hash = crypto.createHash(hashAlgo);
  hash.update(K);
  hash.update(H);
  hash.update(char);
  hash.update(sessionId);
  return hash.digest();
}

const K = Buffer.allocUnsafe(4 + secret.length);
K.writeUInt32BE(secret.length, 0);
K.set(secret, 4);
ivCS = deriveKey(K, sessionId, sessionId, "A", "sha1");
ivCS = ivCS.subarray(0, 16);
ivSC = deriveKey(K, sessionId, sessionId, "B", "sha1");
ivSC = ivSC.subarray(0, 16);
keyCS = deriveKey(K, sessionId, sessionId, "C", "sha1");
keyCS = keyCS.subarray(0, 16);
keySC = deriveKey(K, sessionId, sessionId, "D", "sha1");
keySC = keySC.subarray(0, 16);
decipherCS = crypto.createDecipheriv("aes-128-ctr", keyCS, ivCS);
decipherSC = crypto.createDecipheriv("aes-128-ctr", keySC, ivSC);
</code></pre>

<p>Yukarıdaki kod parçası ile gerekli olan tüm materyalleri elde ettik, bundan sonra bir SSH trafiğini kaydedip ardından ekrandan aldığımız çıktıyı koda verip test etmek kaldı.</p>

<h2 id="test">Test</h2>

<p>Testimizi yaparken şunu da belirtmem lazım, SSH hem anahtar değişim, hem şifreleme hem de mesaj doğrulama konusunda çok fazla farklı algoritma ile çalışabiliyor. 
Ben yaptığım testlerde kullanılan algoritmaları özellikle sınırlı tuttum. Aksi durumda sunucu ve istemci farklı algoritmalar seçebilir ve kodunda o ilgili algoritmada 
kullanılan <code>cipher,hash..</code> yapılarını kullanması gerekir. Benim bütün bu makale serisi boyunca kullandığım algoritmalar aşağıda görülebilir.</p>

<pre><code>const algorithms = {
	kex: [
	  'diffie-hellman-group1-sha1',
	],
	cipher: [
	  'aes128-ctr',
	],
	hmac: [
	  'hmac-sha2-256',
	],
};
</code></pre>

<p>Yazdığımız kodu test etmek için basit bir SSH trafiği oluşturan <code>ssh-ls.js</code> kodunu çalıştırıp trafiği kaydetmeye başlıyoruz, ardından 
<code>Private Key</code> değerini alan ve onu <code>ssh-reconstruct-keys.js</code> koduna parametre olarak gönderen bir docker imajı hazırladım, bu şekilde paket kaydetme, çözme gibi işlemlerin hepsini kolayca otomatik hale getirdik.</p>

<pre><code>&gt; docker build -t sshdecrypt .
&gt; docker run --rm -e TARGET_USERNAME=testuser -e TARGET_PASSWORD="testpassword" -e TARGET_HOST=192.168.1.125  sshdecrypt private
...
...
Private Key :  302e020100300506032b656e04220420a852b2ec81f6f095bd1aa504a5af1d2dc40e52c459f53da38be47ef08682a86d
...
...
Encrypted SSH Packet, CS : d1332a363fa6ccc430517a01a3b2a18533b26a429c4c9220...
MAC, CS : 558114765d2a70d0a7f165f6e5041e3f908f35f25543ea1c35a8f30d57c6cd52
Decrypted Packet : 0000004c0d320000000963616c74756e7461730000000e7373682d63...
packet length=76
message code=USERAUTH_REQUEST
Username : testuser
Password : testpassword
...
...
</code></pre>

<p>Kodun önemli kısımlarından zaten bahsettim, ama bütün olarak önemli gördüğüm kısmını aşağıya yapıştırdım. Ayrıca en üstte bulunan link aracılığı ile
Github üzerinden tamamına erişebilirsiniz.</p>

<pre><code>let ivCS;
let keyCS;
let ivSC;
let keySC;
let decipherCS;
let decipherSC;
let newKeysSent = false;
let packet_number = 0;
let clientAddress;
let clientDhPubKey;
let serverDhGexReply;
let clientKexInitPayload;
let serverKexInitPayload;
let clientIdentification;
let serverIdentification;
let sessionId;
pcapSession.on("packet", (rawPacket) =&gt; {
  const packet = pcap.decode.packet(rawPacket);
  if (packet.payload.ethertype !== 2048) return;
  if (packet_number == 0) {
    clientAddress = packet.payload.payload.saddr.toString();
    serverAddress = packet.payload.payload.daddr.toString();
  }
  packet_number++;
  const tcp = packet.payload.payload.payload;
  const direction =
    packet.payload.payload.saddr.toString() === clientAddress ? "CS" : "SC";

  if (tcp &amp;&amp; tcp.data &amp;&amp; (tcp.sport === 22 || tcp.dport === 22)) {
    const sshData = tcp.data ? tcp.data.toString("utf-8") : "";
    if (sshData.startsWith("SSH-")) {
      console.log("SSH Protocol Version Exchange:");
      console.log(sshData.trim());
      if (direction === "CS") clientIdentification = sshData.trim();
      else if (direction === "SC") serverIdentification = sshData.trim();
    } else if (tcp.data) {
      let packet_len, msg_code;
      if (newKeysSent === false) {
        packet_len = tcp.data.subarray(0, 4).readInt32BE(0);
        padding_len = tcp.data[4];
        msg_code = tcp.data[5];
        const msg_name = Object.keys(MESSAGE).find(
          (key) =&gt; MESSAGE[key] === msg_code
        );
        console.log(`message code=${msg_code},${msg_name}`);
        const payload = tcp.data.subarray(5, tcp.data.length - padding_len);
        const payloadWithoutMessageType = tcp.data.subarray(6);
        console.log("direction = " + direction);
        console.log(payload.toString("hex"));
        const parser = packetParser(payloadWithoutMessageType, 0);
        if (msg_code === MESSAGE.KEXINIT) {
          const obj = parser.readObject(SSH_MSG_KEXINIT);
          if (direction === "CS") {
            clientKexInitPayload = Buffer.from(payload);
          } else if (direction === "SC") {
            serverKexInitPayload = Buffer.from(payload);
          }
          console.log(obj);
        } else if (
          msg_code === MESSAGE.KEXDH_GEX_INIT ||
          msg_code === MESSAGE.KEXDH_INIT
        ) {
          const obj = parser.readObject({
            e: "mpint",
          });
          clientDhPubKey = obj.e;
          console.log(obj);
        } else if (
          msg_code === MESSAGE.KEXDH_GEX_REPLY ||
          msg_code === MESSAGE.KEXDH_REPLY
        ) {
          const obj = parser.readObject(KEXDH_REPLY);
          serverDhGexReply = obj;
        }
      } else {
        let decryptedPacket;
        let encryptedPacket = tcp.data.subarray(0, tcp.data.length - 32);
        let mac = tcp.data.subarray(tcp.data.length - 32);
        if (direction === "CS") {
          decryptedPacket = decipherCS.update(encryptedPacket);
        } else if (direction === "SC") {
          decryptedPacket = decipherSC.update(encryptedPacket);
        }
        console.log(`Entire Packet, ${direction} :`, tcp.data.toString("hex"));
        console.log(
          `Encrypted SSH Packet, ${direction} :`,
          encryptedPacket.toString("hex")
        );
        console.log(`MAC, ${direction} :`, mac.toString("hex"));
        packet_len = decryptedPacket.subarray(0, 4).readInt32BE(0);
        padding_len = decryptedPacket[4];
        msg_code = decryptedPacket[5];
        const msg_name = Object.keys(MESSAGE).find(
          (key) =&gt; MESSAGE[key] === msg_code
        );
        console.log("Decrypted Packet :", decryptedPacket.toString("hex"));
        console.log(`packet length=${packet_len}`);
        console.log(`message code=${msg_name}`);
        if (msg_code === MESSAGE.USERAUTH_REQUEST) {
          const parser = packetParser(decryptedPacket, 6);
          const obj = parser.readObject(USERAUTH_REQUEST);
          if (obj.method === "password") {
            const passObj = parser.readObject({
              isChange: "boolean",
              password: "string",
            });
            console.log("Username : " + obj.user);
            console.log("Password : " + passObj.password);
          }
        }
      }
      if (msg_code == MESSAGE.NEWKEYS) {
        privKey = process.argv[2];
        const key=privKey.trim();
        console.log("private key passed="+key);
        const clientPrivateKey = Buffer.from(key, "hex");

        const dh = crypto.createDiffieHellmanGroup("modp2");
        const dhKey = crypto.createDiffieHellman(
          dh.getPrime(),
          dh.getGenerator()
        );
        dhKey.setPublicKey(clientDhPubKey);
        dhKey.setPrivateKey(clientPrivateKey);
        let secret = dhKey.computeSecret(serverDhGexReply.f);
        const hash = crypto.createHash("sha1");
        sessionId = hash
          .update(
            concat(
              clientIdentification,
              serverIdentification,
              clientKexInitPayload,
              serverKexInitPayload,
              serverDhGexReply.host_key,
              clientDhPubKey,
              serverDhGexReply.f,
              secret
            )
          )
          .digest();
        console.log("H=" + sessionId.toString("hex"));
        
        const K = Buffer.allocUnsafe(4 + secret.length);
        K.writeUInt32BE(secret.length, 0);
        K.set(secret, 4);
        ivCS = deriveKey(K, sessionId, sessionId, "A", "sha1");
        ivCS = ivCS.subarray(0, 16);
        ivSC = deriveKey(K, sessionId, sessionId, "B", "sha1");
        ivSC = ivSC.subarray(0, 16);
        keyCS = deriveKey(K, sessionId, sessionId, "C", "sha1");
        keyCS = keyCS.subarray(0, 16);
        keySC = deriveKey(K, sessionId, sessionId, "D", "sha1");
        keySC = keySC.subarray(0, 16);
        decipherCS = crypto.createDecipheriv("aes-128-ctr", keyCS, ivCS);
        decipherSC = crypto.createDecipheriv("aes-128-ctr", keySC, ivSC);
        newKeysSent = true;
      }
    }
  }
});

pcapSession.on("error", (err) =&gt; {
  console.error("Error:", err.message);
});

pcapSession.on("complete", () =&gt; {
  console.log("Finished reading pcap file.");
});

function deriveKey(K, H, sessionId, char, hashAlgo) {
  const hash = crypto.createHash(hashAlgo);
  hash.update(K);
  hash.update(H);
  hash.update(char);
  hash.update(sessionId);
  return hash.digest();
}

function concat(...strings) {
  const buffers = [];
  strings.forEach((str) =&gt; {
    const lenBuffer = Buffer.allocUnsafe(4);
    lenBuffer.writeUInt32BE(str.length, 0);
    buffers.push(lenBuffer);
    buffers.push(Buffer.from(str));
  });
  return Buffer.concat(buffers);
}
</code></pre>

<h2 id="ne-öğrendik">Ne Öğrendik?</h2>

<p>En başta özel anahtarın makinede duran sabit bir değer olmadığını öğrendik. Ayrıca eğer private key ele geçirilirse ya da geçirebilirsek, başından beri kaydettiğiniz SSH trafiğini çözümleyip bütün detaylarını görebilirsiniz. Fakat burada
boş hayallere de kapılmamak lazım, ilk olarak ele geçirsek bile bu kalıcı bir private key değil, her oturum sırasında değişen bir sayı hatta aynı oturum 
sırasında bile belirli bir mesaj sayısına ya da talebe göre SSH protokolü yeni bir <code>KEX</code> süreci başlatıp anahtarları yenileyebiliyor.</p>

<p>Diğer bir zorluk, trafiği başından dinlemeyip ortasından dinlemeye başladıysak başka anahtar oluşumu için gerekli olan bilgileri elde edemeyeceğiz ve elimizde özel anahtar dahi olsa trafiği çözemeyeceğiz. 
Yukarıda özel anahtarı oluşturmak için diğer gerekli bilgileri görmüştük hatırlarsanız onların hepsi, KEX sürecinin başında değiş tokuş yapılıyor.</p>

<p>Son olarak ilk yazıda kaçak güreşip bütün gerekli şifreleme anahtarlarını yazdırarak trafiği çözmüştük, bu yazıda biraz ilerleme kaydedip, biraz daha zor yoldan elimizde sadece özel anahtar olması durumunda
trafiği çözmeyi denedik ve başardık. Bir sonraki bölümde elimizde bu da olmadan trafiği çözebilir miyiz ona bakacağız, işler daha da zorlaşacak.</p>]]></content><author><name>Cihat Altuntaş</name></author><category term="ssh" /><category term="security" /><category term="cryptography" /><category term="nodejs" /><summary type="html"><![CDATA[SSH Trafiğini Çözümleyelim 3 - Private Key]]></summary></entry><entry><title type="html">Endian Tedirginliği</title><link href="http://www.cihataltuntas.com/2025/10/24/endian-worry.html" rel="alternate" type="text/html" title="Endian Tedirginliği" /><published>2025-10-24T14:00:00+00:00</published><updated>2025-10-24T14:00:00+00:00</updated><id>http://www.cihataltuntas.com/2025/10/24/endian-worry</id><content type="html" xml:base="http://www.cihataltuntas.com/2025/10/24/endian-worry.html"><![CDATA[<p>Kariyerimin ilk yıllarında daha çok Java, .NET gibi platformlarda kurumsal
uygulamalar tasarladığım için, tahmin edebileceğiniz gibi geliştirdiğimiz
yazılımların çok büyük oranı kullanıcıdan masa üstü ya da web tarayıcıda
çalışan uygulamalarla girdi alıp, bunu işledikten sonra veri tabanına yazıp
sonra da kullanıcıya sonuçları okumaktan, farklı bir şey yapmıyordu.</p>

<p>Kulağa oldukça sıkıcı geliyor değil mi? Maalesef günümüzde masa üstü programların da neredeyse ortadan kalkmasıyla
herhalde %95 gibi bir oranda herkes web uygulaması geliştiriyor. Bu yüzden burada değineceğim konu bu alanlarda çalışan çoğu kişi için 
pratikte çok anlamlı olmayabilir fakat bilgisayarların nasıl çalıştığı ve alt seviye işleyiş konusunda oldukça faydalı olacaktır.</p>

<p>Tabi, kendi açımdan hep kafamın köşesinde duran bu konuyu pratik bir örnekle uygulayıp hem daha da pekiştirmek hem de anlatmak açısında güzel bir fırsat olduğu
için daha fazla uzatmadan başlayalım.</p>

<p>Eğer network programlama yapıyorsanız, binary dosya okuma yazma, ya da alt
seviye bir programlama diliyle bit byte gibi veri tipleri ile uğraşıyorsanız, karşınıza çıkabilecek ve öğrenmeniz
gereken kavramlardan birisi <a href="https://en.wikipedia.org/wiki/Endianness">Endian-ness</a> kavramıdır.</p>

<h2 id="tedirginlik">Tedirginlik</h2>

<p>Son yıllarda network programlama ve biraz daha alt seviye işlerle uğraştığım için yazdığım kodun farklı bir Endian mimarisinde çalışan sistemde 
nasıl davranacağına dair hep bir tedirginlik olmuştur. Bir süredir x86 CPU kullanan Mac üzerinde işleri yaptığım için 
geliştirdiğim kod Little-Endian mimari ile uyumlu, bildiğim kadarı ile de gömülü cihazlar(MIPS,etc.) ve bazı main-frame mimariler dışında
çoğunluk Little-Endian yapısında çalışıyor ama örnek olarak network üzerinden bilgi alışverişi yapan bir yazılım geliştirdik, aklıma gelen ilk sorulardan biri
acaba Big-Endian bir sistemde gönderdiğimiz ya da oradan aldığımız veri doğru yorumlanacak mı?</p>

<p>Diğer bir sorun, yazdığımız kodu bu sistemlerde çalıştırmak zorunda kalabiliriz, örnek Go ile yazdığımız bir kod
<a href="https://go.dev/wiki/MinimumRequirements#architectures">burada</a> görülebildiği gibi bir çok farklı Big-Endian CPU mimarisinde de çalışabilir.
Go ya da farklı diller bu mimarilerde çalışmayı desteklese de, yazdığınız kodun da Endian farklılığını gözeterek geliştirilmesi gerekiyor, aksi durumda beklenmedik
hatalar ile karşılaşabilirsiniz. Bugün de bunlardan biri hakkında örnek yapacağız tabi öncelikle Endian kavramını biraz daha netleştirelim.</p>

<h2 id="birkaç-cümlede-endian">Birkaç Cümlede Endian</h2>

<p>Wikipedia sayfası oldukça detaylı anlatmış ama ben kendi yorumumu yapıp şöyle
bir özetle Endian kavramını açıklamak istiyorum.</p>

<p>Şimdi elimizde 1234567890 gibi bellekte 4 byte(32 bit) yer kaplayan bir sayı
olsun. Bu sayının hex karşılığı aşağıdaki gibi olacak.</p>

<pre><code>&gt; printf "%X\n" 1234567890 | xxd -r -p  | xxd -i |tr ',' ' '
  0x49  0x96  0x02  0xd2
</code></pre>
<p>Aynı sayıyı ikili sistemde göstermek istersek de aşağıdaki gibi gözükecek.</p>
<pre><code>&gt; printf "%X\n" 1234567890 | xxd -r -p | xxd -b -i | tr ',' ' '
  0b01001001  0b10010110  0b00000010  0b11010010
</code></pre>

<p>Peki bu sayıyı elimizdeki bellek adreslerine nasıl yerleştirebilirim?</p>

<p>Little Endian sistemlerde yani daha yaygın olarak kullanılan x86, Arm gibi mimarilerde ve büyük ihtimalle sizin de kullandığınız kişisel bilgisayarınızda 
en düşük değere sahip byte <code>0xd2</code> en düşük bellek adresinde <code>00000000</code> tutulacak.</p>

<table>
  <thead>
    <tr>
      <th>Adres</th>
      <th>Değer</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>00000000</td>
      <td>0xd2</td>
    </tr>
    <tr>
      <td>00000001</td>
      <td>0x02</td>
    </tr>
    <tr>
      <td>00000002</td>
      <td>0x96</td>
    </tr>
    <tr>
      <td>00000003</td>
      <td>0x49</td>
    </tr>
  </tbody>
</table>

<p>Big Endian sistemlerde yani MIPS, Spark gibi mimarilerde ise en düşük değer <code>0xd2</code> en yüksek bellek adresinde aşağıdaki gibi tutulacak.</p>

<table>
  <thead>
    <tr>
      <th>Adres</th>
      <th>Değer</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>00000000</td>
      <td>0x49</td>
    </tr>
    <tr>
      <td>00000001</td>
      <td>0x96</td>
    </tr>
    <tr>
      <td>00000002</td>
      <td>0x02</td>
    </tr>
    <tr>
      <td>00000003</td>
      <td>0xd2</td>
    </tr>
  </tbody>
</table>

<p>Kısaca Little Endian sistemler en yüksek değeri en
düşük bellek adresine yerleştirecek ve diğerleri ardından gelecek, Big Endian
sistemler ise tam tersi yani en yüksek değeri en yüksek bellek adresine
yerleştirip diğerlerini ardından sıralayacak.</p>

<p>En çok kafa karıştıran diğer bir nokta ise Endian yapısı 1 byte içinde olan bit sırasını değiştirmez, yani <code>0x49</code> değeri yani <code>01001001</code>
her iki mimaride de bit değerleri aynı şekilde bellekte tutulur.</p>

<blockquote>
  <blockquote>
    <p>Endianness bit sırasını etkilemez, birde fazla byte içeren değerlerin bellek üzerinde yerleşimini etkiler</p>
  </blockquote>
</blockquote>

<h2 id="belleğe-bir-göz-atalım">Belleğe Bir Göz Atalım</h2>

<h3 id="little-endian-sistemde-bellek">Little Endian Sistemde Bellek</h3>

<p>Yukarıda bahsettiğimiz değeri bir C kodu içine koyup kendi bilgisayarımda
derledim.</p>

<pre><code>// ...
int main() {
  uint32_t val=1234567890;
  printf("value=%d",val);
  return 0;
}
</code></pre>

<p>Ardından LLDB ile debug yaparak, ilgili değerin tekil byte
değerlerinin bellekte nereye yerleştirildiğine göz attım sonuç aşağıdaki gibi
çıktı.</p>

<pre><code>(lldb) f
frame #0: 0x0000000100003f97 a.out`main at main.c:7:3
   4    int main() {
   5      uint32_t val=1234567890;
   6      printf("value=%d",val);
-&gt; 7      return 0;
   8    }
(lldb) p &amp;val
(uint32_t *) $3 = 0x00007ff7bfefee18
(lldb) memory read --format hex --size 1 --num-per-line 1 --count 4 0x00007ff7bfefee18
0x7ff7bfefee18: 0xd2
0x7ff7bfefee19: 0x02
0x7ff7bfefee1a: 0x96
0x7ff7bfefee1b: 0x49
</code></pre>

<p>Önce 1234567890 değerinin bellekte adresini bulduk, ardından bu adresten başlayarak her satırda tek bir byte gösterecek şekilde 4 byte göstermesini istedik.
Little endian bir sistem olduğu için düşük bellek adresine <code>0x7ff7bfefee18</code> sayının en son byte değeri <code>0xd2</code>, yüksek bellek adresine <code>0x7ff7bfefee1b</code> ise en büyük <code>0x49</code>
değerini koydu.</p>

<h3 id="big-endian-sistemde-bellek">Big Endian Sistemde Bellek</h3>

<p>Kodun Big Endian sistemde nasıl belleğe yerleştiğini görmek için en güzel
yöntem tahmin edebileceğiniz gibi biraz teorinin dışına çıkıp onu gerçekten bir
Big Endian sistemde derleyip test etmek Bunun için
<a href="https://courses.cs.washington.edu/courses/cse333/15wi/lec/ppc.html">buradaki</a> adımları takip edip Qemu ile Big Endian bir Linux Debian ayağa kaldırabildim.</p>

<pre><code>&gt;lscpu
Architecture:          ppc
Byte Order:            Big Endian
CPU(s):                1
On-line CPU(s) list:   0
Thread(s) per core:    1
Core(s) per socket:    1
Socket(s):             1
Model:                 Power Macintosh
BogoMIPS:              33.21
L1d cache:             32K
L1i cache:             32K
</code></pre>

<p>Ayağa kalktıktan sonra Git, Gcc, Gdb gibi araçları tabi yüklemek gerekiyor
fakat eski bir sürüm olduğundan bunu yapmak için <code>/etc/apt/sources.list</code>
dosyasının içinde sadece <code>deb http://archive.debian.org/debian wheezy main</code>
satırını bırakırsanız bahsettiğim bütün paketleri yükleyebiliyorsunuz.</p>

<pre><code>Breakpoint 1, main () at main.c:5
5	uint32_t val = 1234567890;
6	  return 0;
$1 = 1234567890
$2 = (uint32_t *) 0xbffffb58
0xbffffb58:	0x49
0xbffffb59:	0x96
0xbffffb5a:	0x02
0xbffffb5b:	0xd2
</code></pre>

<h3 id="karşılaştırma">Karşılaştırma</h3>

<p>Debug oturumlarında incelediğimiz değerin belleğe yerleşimini karşılaştırdığımızda artık Endian defterini kapatabiliriz sanırım.</p>

<p>Little Endian</p>
<pre><code>0x7ff7bfefee18: 0xd2
0x7ff7bfefee19: 0x02
0x7ff7bfefee1a: 0x96
0x7ff7bfefee1b: 0x49
</code></pre>

<p>Big Endian</p>
<pre><code>0xbffffb58:	0x49
0xbffffb59:	0x96
0xbffffb5a:	0x02
0xbffffb5b:	0xd2
</code></pre>

<h2 id="gerçek-dünyadan-bir-sorun">Gerçek Dünyadan Bir Sorun</h2>

<p>Bir süredir cryptography alanı ilgimi çekiyor, derinlemesine öğrenmenin en iyi
yolu önce temelleri öğrenmek sonra örnekle pekiştirmek olduğuna inandığım için
şifreleme algoritmalarından en yaygın olarak kullanılan AES algoritmasını
kütüphane olmadan C ile geliştirmek istedim. Merak edenler kaynak kodlarına <a href="https://github.com/caltuntas/aes">buradan</a> erişebilir.</p>

<p>Algoritmanın <a href="https://en.wikipedia.org/wiki/AES_key_schedule">belirli</a> genelde <code>word</code> tipinde verilerde işlemler yapıyor bu yüzden bazı aşamalarında <code>word-&gt;byte array</code> ya da <code>byte array-&gt;word</code>
dönüşümleri yapmanız gerekiyor.Aslında basit bir işlem ama acemice yazılan bir
dönüştürme fonksiyonu aşağıdaki gibi gözükebilir.</p>

<pre><code>void convert_to_uint8_array(uint32_t word,uint8_t arr[4]) {
  uint8_t *ptr =(uint8_t*)&amp;word;
  arr[0] = ptr[3];
  arr[1] = ptr[2];
  arr[2] = ptr[1];
  arr[3] = ptr[0];
}
</code></pre>

<p>Elimizde bir byte array var, 4 tanesi bir word olacağı için, bunu çevirmek için hedef veri tipi yani word olan
değişkenin adresini alıyoruz, ardından sırayla en büyük word bileşen değerini alıp en küçük array değerine atayıp devam ediyoruz.
Little endian sistemde bellekte nasıl durduğunu hatırlarsanız, neden 3,2,1,0 diye azalarak gittiğini anlayacaksınız.</p>

<p>Kodu böyle yazdık ve sonrasında kendi Little Endian sistemimizde test ettik diyelim.</p>

<pre><code>aes &gt; make test
gcc -g test-framework/unity.c aes.c test_aes.c -o test_aes.out
./test_aes.out
test_aes.c:283:test_rot_word:PASS
test_aes.c:284:test_sub_word:PASS
test_aes.c:285:test_rcon:PASS
test_aes.c:286:test_convert:PASS
test_aes.c:287:test_expand_key:PASS
test_aes.c:288:test_add_round_key:PASS
test_aes.c:289:test_sub_bytes:PASS
test_aes.c:290:test_inv_sub_bytes:PASS
test_aes.c:291:test_shift_rows:PASS
test_aes.c:292:test_inv_shift_rows:PASS
test_aes.c:293:test_mix_columns:PASS
test_aes.c:294:test_inv_mix_columns:PASS
test_aes.c:295:test_aes_enc:PASS
test_aes.c:296:test_add_round_key_10:PASS
test_aes.c:297:test_mul:PASS
test_aes.c:298:test_aes_dec:PASS

-----------------------
16 Tests 0 Failures 0 Ignored
OK
</code></pre>

<p>Bütün testler kendi sisteminizde geçti, peki aynı kodu Big Endian bir sistemde test ettiğimizde neler oluyor ona bakalım. Yukarıda oluşturduğumuz PowerPC sisteminde
aynı kodu test ettim sonuç aşağıdaki gibi oldu.</p>

<pre><code>gcc -g -std=c99 test-framework/unity.c aes.c test_aes.c -o test_aes.out
./test_aes.out
test_aes.c:21:test_rot_word:FAIL: Expected 1338968380 Was -816890871
test_aes.c:29:test_sub_word:FAIL: Expected 32212106 Was -1971000575
test_aes.c:35:test_rcon:FAIL: Expected 15434890 Was -1954223359
test_aes.c:74:test_convert:FAIL: Element 0 Expected 0x09 Was 0x3C
test_aes.c:63:test_expand_key:FAIL: Element 0 Expected 0xA0 Was 0xFD
test_aes.c:288:test_add_round_key:PASS
test_aes.c:289:test_sub_bytes:PASS
test_aes.c:290:test_inv_sub_bytes:PASS
test_aes.c:291:test_shift_rows:PASS
test_aes.c:292:test_inv_shift_rows:PASS
test_aes.c:293:test_mix_columns:PASS
test_aes.c:294:test_inv_mix_columns:PASS
test_aes.c:196:test_aes_enc:FAIL: Element 0 Expected 0x39 Was 0x06
test_aes.c:296:test_add_round_key_10:PASS
test_aes.c:297:test_mul:PASS
test_aes.c:220:test_aes_dec:FAIL: Element 0 Expected 0x32 Was 0xCD

-----------------------
16 Tests 7 Failures 0 Ignored 
FAIL
</code></pre>

<p>Test sonuçlarına bakacak olursak, 7 tane case hata almış, ana algoritma da
çalışmamış. Çünkü en temel çevrim işlemi olduğundan bütün testler etkilenmiş
diyebiliriz.</p>

<h2 id="çözüm">Çözüm</h2>

<p>Peki yukarıdaki kodu Endian davranışından bağımsız olarak nasıl yazabiliriz? Alt seviye programlama ile uğraşan herkes 
bu ve benzeri bit hack işlemleri ile uğraşmıştır. Basit bir işlem olsa da, kafa karıştıran önemi bir noktası da var. 
Kodu aşağıdaki gibi yazıp iki sistemde de testleri çalıştırırsak bütün testlerin başarılı şekilde geçtiğini göreceksiniz.</p>

<pre><code>void convert_to_uint8_array(uint32_t w,uint8_t arr[4]) {
  arr[0] = w &gt;&gt; 24;
  arr[1] = w &gt;&gt; 16;
  arr[2] = w &gt;&gt; 8;
  arr[3] = w &gt;&gt; 0;
}
</code></pre>

<p>Peki neden? Elimizde olan ilk değerimiz üzerinden gidelim <code>1234567890</code> sayısını ikili sisteme çevirince aşağıdaki gibi bir değer bulmuştuk.</p>

<pre><code>01001001  10010110  00000010  11010010
</code></pre>

<p>İstediğimiz şey bu değerin her bir parçasını dizinin bir değerine atamak yani aşağıdakini yapmak.</p>

<pre><code>arr[0]=0x49
arr[1]=0x96
arr[2]=0x02
arr[3]=0xd2
</code></pre>

<p>Bunu daha görünür kılmak için aşağıdaki gibi gösterelim.</p>

<pre><code>arr[0]    arr[1]    arr[2]    arr[3]
--------  --------  --------  -------- 
01001001  10010110  00000010  11010010
--------  --------  --------  --------
0x49      0x96      0x02      0xd2
</code></pre>

<p>Yukarıdaki tabloya bakınca yapmamız gereken daha net görülebiliyor, elimizde 4 byte uzunluğundaki bir değerin her bir parçasını bir dizinin elamanına atıcaz,
eğer bu sayıyı <code>24</code> defa sağa kaydırırsak (sağa dediğime bakmayın önemli bir nokta var), ilk dizi elemanını elde etmiş oluruz.</p>

<pre><code>w &gt;&gt; 24                        arr[0]
--------  --------  --------  --------     x         x         x
00000000  00000000  00000000  01001001  10010110  00000010  11010010
--------  --------  --------  --------
                                0x49      0x96      0x02      0xd2
                                
w &gt;&gt; 16                        arr[1]
--------  --------  --------  --------     x         x    
00000000  00000000  01001001  10010110  00000010  11010010
--------  --------  --------  --------
                      0x49      0x96      0x02      0xd2

w &gt;&gt; 8                         arr[2]
--------  --------  --------  --------     x     
00000000  01001001  10010110  00000010  11010010
--------  --------  --------  --------
            0x49      0x96      0x02      0xd2

w &gt;&gt; 0                         arr[3]
--------  --------  --------  --------
01001001  10010110  00000010  11010010
--------  --------  --------  --------
0x49      0x96      0x02      0xd2
</code></pre>

<p>Yukarıda görsel olarak bu bit kaydırma işlemlerinin nasıl çalıştığını gösterdim, şöyle bir sorunuz olursa bunu çok iyi anlayabilirim. Peki Little Endian ya da Big Endian sistemlerde
bellekte dizilim sırası farklı olduğuna göre, (sağdan sola, ya da soldan sağa) bu sağa kaydırma işlemi nasıl oluyor da her sistemde aynı çalışıyor? Aynı kafa karışıklığı bende de olmuştu, bu yüzden üstüne
basa basa buraya not alalım.</p>

<blockquote>
  <blockquote>
    <p><code>&gt;&gt;,&lt;&lt;</code> gibi bit kaydırma işlemleri sayıları sağa ya da sola kaydırmaz,
çalıştığı işlemci mimarisine göre <code>&gt;&gt;</code> en küçük basamağın yerleştirildiği yöne
doğru kaydırır, <code>&lt;&lt;</code> ise en büyük basamağın yerleştirildiği yöne kaydırır.</p>
  </blockquote>
</blockquote>

<p>Yani Big Endian sistemde en büyük basamak hangi tarafta ise o yöne, Little Endian sistemde hangi yönde ise o yöne kaydırma yapılır, özetle bit kaydırma operatörleri 
Endian bağımsız olarak çalışırlar.</p>]]></content><author><name>Cihat Altuntaş</name></author><category term="c,endianness" /><summary type="html"><![CDATA[Endian Tedirginliği]]></summary></entry><entry><title type="html">Sıfırdan Regex Motoru - Bölüm 2: Backtracking Algoritması</title><link href="http://www.cihataltuntas.com/2025/08/11/regex-engine-2.html" rel="alternate" type="text/html" title="Sıfırdan Regex Motoru - Bölüm 2: Backtracking Algoritması" /><published>2025-08-11T14:00:00+00:00</published><updated>2025-08-11T14:00:00+00:00</updated><id>http://www.cihataltuntas.com/2025/08/11/regex-engine-2</id><content type="html" xml:base="http://www.cihataltuntas.com/2025/08/11/regex-engine-2.html"><![CDATA[<p>Bu yazı serisi şu ana kadar 2 bölümden oluşmaktadır, diğer bölümlere de hazır
oldukça aşağıdaki linklerden ulaşabileceksiniz. Yazı içeriğinde geçen kodlara
<a href="https://github.com/caltuntas/regex-poc">bu linkten</a> erişebilirsiniz.</p>

<ol>
  <li><a href="https://www.cihataltuntas.com/2025/07/25/regex-engine-1">Sıfırdan Regex Motoru - Bölüm 1: Parsing</a>
    <ul>
      <li>Bu yazıda, çok kısa ne yapmak istediğimizden, nasıl yapabileceğimizden ve
bize verilen regex ifadelerinin parse edilip istediğimiz veri yapısı
içinde nasıl tutabileceğimizden bahsedeceğiz.</li>
    </ul>
  </li>
  <li><a href="https://www.cihataltuntas.com/2025/07/25/regex-engine-2">Sıfırdan Regex Motoru - Bölüm 2: Backtracking Algoritması</a> (Bu yazı)
    <ul>
      <li>Bu yazıda, Backtracking algoritması nasıl çalışır, recursive algoritmaya göre
farkları nelerdir örnekler üzerinden karşılaştırarak anlamaya çalışacağız.</li>
    </ul>
  </li>
  <li><a href="https://www.cihataltuntas.com/#">Sıfırdan Regex Motoru - Bölüm 3: Backtracking Regex Motoru</a></li>
  <li><a href="https://www.cihataltuntas.com/#">Sıfırdan Regex Motoru - Bölüm 4: NFA Regex Motoru</a> (hazır değil)</li>
</ol>

<h2 id="giriş">Giriş</h2>

<p>Bir önceki yazıda regex motorumun temelini oluşturacak olan verilen regex
ifadesini bileşenlerine ayırıp sonrasında AST veri yapısına çevirecek gerekli
<code>parser</code> kodunu yazmıştık. Bu yazıda ise Java, .NET, Python gibi dillerde
kullanılan Regex motorunun temelini oluşturan <code>Backtracking</code> algoritmasının
nasıl çalıştığını ne avantaj sağladığını, hem klasik hem de onu kullanarak
geliştirdiğimiz örnekler üzerinden inceleyip karşılaştıracağız.</p>

<h2 id="nedir-bu-backtracking">Nedir Bu Backtracking?</h2>

<p><a href="https://en.wikipedia.org/wiki/Backtracking">Backtracking</a> algoritması sadece regex motoru geliştirirken değil, farklı bir çok problemin
çözümünde kullanılan bir algoritma, bu yüzden bizim problemimiz dışında nedir, ne yapar, faydası nedir
anlamaya çalışalım.</p>

<p>Benim bu algoritma için kullandığım açıklama ve kendi özetim, <code>akıllı brute-force</code> diyebilirim.
Örnek üzerinden neden bunu böyle olduğunu anlamaya çalışalım.</p>

<h2 id="adım-adım-backtracking">Adım Adım Backtracking</h2>

<p>Backtracking algoritması direk Regex motoru geliştirirken kullanmaya başlamadan
önce nasıl çalıştığını, hangi problemi çözdüğünü bir örnek yaparak öğrenmenizi
şiddetle tavsiye ederim.  Bu yüzden ben pekiştirmek için konu ile alakalı basit bir
Backtracking problemi seçip adım adım önce geleneksel yöntem ile bu problemi
nasıl çözeriz onu uyguladım, ardından iyileştirerek çözüp en sonunda da
Backtracking kullanarak aynı problemi çözdüm. Bunu yaptıktan sonra Regex motoru
üzerinde neden kullanıldığı çok daha anlaşılır olacaktır.</p>

<h3 id="problem--word-search">Problem : Word-Search</h3>

<p>Backtracking algoritmasının temelini öğretmek için kullanılan basit problemlerden bir tanesi <a href="https://en.wikipedia.org/wiki/Word_search">Word Search</a> problemi, bizim Regex motorumuz ile
çok benzerlik gösterdiği için özellikle bunu seçtim. Sonuçta biz de çeşitli regex yapılarına göre verilen bir text içerisinde aradığımız şeyi bulmaya çalışıyoruz, 
özetle Regex problemini çok daha basiti diyebiliriz.</p>

<p>Problem, verilen bir matris içerisinde çeşitli yönlerde(sağa,sola,yukarıya, aşağıya) birer birim hareket ederek aranılan kelimenin o matris içerisinde olup olmadığını söylemenizi bekliyor.</p>

<p>Örnek, aşağıdaki bir matris var ve içerisinde <code>GEEK</code> bulunuyor mu inceleyelim.</p>

<pre><code>{'T', 'E', 'E'},
{'S', 'G', 'K'},
{'T', 'E', 'L'},
</code></pre>

<p>İnsan olarak hemen fark etmiş olacaksınız ki aradığımız ifade matris içinde bulunuyor ve aşağıdaki gibi görülebilir.</p>

<p><img src="/img/regexengine/matrix.svg" alt="Capture 1" /></p>

<p>Tabi iş bunu programa yaptırmaya gelince o kadar kolay olmadığını görüyoruz. Bunu herhangi bir yapay zeka aracına ya da binlerce çözülmüş, kodlanmış haline 
bakmadan önce elinize kalem kağıt alıp nasıl siz çözersiniz uğraşmanızı tavsiye ederim.</p>

<p>Ben kalem kağıt ile önce kafamda çözdüğüm için koda aktarırken bunun daha kolay olduğunu söyleyebilirim.</p>

<h3 id="çözüm-1---loop">Çözüm 1 - Loop</h3>

<p>Öncelikle başlangıç yerimiz önemli, bir matris olduğu için en amatör yöntemle 0,0 noktasında başlayıp bütün satır ve sütunları döngü içerisinde dolaşarak 
bütün 4 harf içeren kombinasyonları çıkaralım, en son aralığım kelimenin bu liste içerisinde olup olmadığına karar verelim.</p>

<p>Örnek ilerleyişimiz şöyle olabilir, diyelim ki,</p>

<ol>
  <li>Adım <code>T=0,0</code> noktasından başladık, bir sonraki gideceğimiz harf <code>S=1,0</code> ya da <code>E=0,1</code> olabilir.</li>
  <li>Adım <code>S=1,0</code> seçtik diyelim, bir sonraki gideceğimiz harf <code>T=2,0</code> ya da <code>G=1,1</code> olabilir.</li>
  <li>Adım <code>T=2,0</code> seçtik diyelim, bir sonraki gideceğimiz sadece harf <code>E=2,1</code> olabilir</li>
  <li>Son adımda aralığımız kelimenin harf sayısına <code>{0 0}{1 0}{2 0}{2 1}TSTE</code> ile ulaştık.</li>
</ol>

<p>Peki önümüze birden fazla seçecek çıktığını fark ettiniz sanırım, peki seçmediklerimiz arasında olabilir mi aradığımız kelime? Bu sebeple
bütün kombinasyonları deneyerek ancak kesin karar verebiliriz. Sadece <code>T=0,0</code> konumundan başladığımızda aşağıdaki gibi farklı seçenekler var, bunların hepsini 
kod içerisinde gezmemiz lazım.</p>
<pre><code>{0 0}{1 0}{2 0}{2 1}TSTE
{0 0}{1 0}{1 1}{0 1}TSGE
{0 0}{1 0}{1 1}{2 1}TSGE
{0 0}{1 0}{1 1}{1 2}TSGK
{0 0}{0 1}{1 1}{2 1}TEGE
{0 0}{0 1}{1 1}{1 0}TEGS
{0 0}{0 1}{1 1}{1 2}TEGK
{0 0}{0 1}{0 2}{1 2}TEEK
</code></pre>

<p>Buraya kadar kağıt üzerinde ne yapmaya çalıştığımızı anladık diye düşünüyorum bunu olabilecek en basit yöntemi ile koda çevirelim, bunu yaparken de işimizi kolaylaştırsın diye
aradığımız kelimenin hep sabit yani <code>GEEK</code> gibi 4 karakterden oluştuğunu var sayalım. Gerçekte farklı uzunluklarda olabilir, buna sonra değineceğiz.
Bu örneğin geliştirdiğimiz basit regex motoru ile direk bir bağlantısı olmasa da tutarlılık açısından aynı programlama dilini örnekler için de kullandım, yani <code>Go</code> ile aradığımız kelimeyi
verilen yukarıdaki matris içinde bulan kodun ilk versiyonu aşağıdaki gibi gözüküyor.</p>

<pre><code>package main

import (
	"fmt"
	"slices"
)

type Location struct {
	Row    int
	Column int
}

func (loc Location) GetNeighbors() []Location {
	return []Location{ {loc.Row - 1, loc.Column}, {loc.Row + 1, loc.Column}, {loc.Row, loc.Column - 1}, {loc.Row, loc.Column + 1} }
}

func isValid(grid [][]byte, currentLoc Location, locs ...Location) bool {
	rowCount := len(grid)
	colCount := len(grid[0])
	for _, loc := range locs {
		if currentLoc.Row == loc.Row &amp;&amp; currentLoc.Column == loc.Column {
			return false
		}
	}
	return currentLoc.Row &gt;= 0 &amp;&amp; currentLoc.Column &gt;= 0 &amp;&amp; currentLoc.Row &lt; rowCount &amp;&amp; currentLoc.Column &lt; colCount
}

func traverse(grid [][]byte, loc Location) []string {
	result := make([]string,0)
	p0 := loc
	for _, p1 := range p0.GetNeighbors() {
		if isValid(grid, p1, p0) {
			for _, p2 := range p1.GetNeighbors() {
				if isValid(grid, p2, p0, p1) {
					for _, p3 := range p2.GetNeighbors() {
						if isValid(grid, p3, p0, p1, p2) {
							fmt.Print(p0)
							fmt.Print(p1)
							fmt.Print(p2)
							fmt.Print(p3)
							str := []byte{
								grid[p0.Row][p0.Column],
								grid[p1.Row][p1.Column],
								grid[p2.Row][p2.Column],
								grid[p3.Row][p3.Column],
							}
							fmt.Println(string(str))
							result = append(result, string(str))
						}
					}
				}
			}
		}
	}
	return result
}

func find(grid [][]byte, word string) bool {
	allResults :=make([]string,0)
	rows := len(grid)
	for i := 0; i &lt; rows; i++ {
		cols := len(grid[i])
		for j := 0; j &lt; cols; j++ {
			fmt.Printf("Starting point is { %d,%d }=%c\n",i,j,grid[i][j])
			result := traverse(grid, Location{i, j})
			allResults = append(allResults, result...)
		}
	}
	return slices.Contains(allResults, word)
}

func main() {
	tests := []struct {
		grid   [][]byte
		word   string
		result bool
	}{
		{
			[][]byte{
				{'T', 'E', 'E'},
				{'S', 'G', 'K'},
				{'T', 'E', 'L'},
			},
			"GEEK",
			true,
		},
	}

	for i, c := range tests {
		res := find(c.grid, c.word)
		if res != c.result {
			fmt.Printf("test case failed %d\n", i)
		}
	}
}
</code></pre>

<p>Oldukça basit olduğunu düşünsem de, <code>traverse</code> fonksiyonunu incelemenizi öneririm.
Bir konumdan taramaya başladıktan sonra aradığımız kelime 4 harfli olduğu ve elimizde bulmamız gereken 3 harf daha kaldığı için, 
iç içe, 3 döngü içe tüm olasılıkları çıkartıyoruz ve sonuç olarak dönüyoruz.</p>

<p>Ayrıca <code>GetNeighbors</code> içinde de bir konumdan aşağı, yukarı, sağ, sol yönlerinde gidebileceği komşularını listeleyip kodu basitleştiriyoruz diyebilirim.
Bu kodu ister derleyip isterseniz de <code>go run main.go</code> ile çalıştırırsanız aşağıdaki gibi tüm noktalardan başladığında gidilebilecek rotaları ve bulunan kelimeleri listeleyecek.</p>

<pre><code>Starting point is {0,0}=T
{0 0}{1 0}{2 0}{2 1}TSTE
{0 0}{1 0}{1 1}{0 1}TSGE
{0 0}{1 0}{1 1}{2 1}TSGE
{0 0}{1 0}{1 1}{1 2}TSGK
{0 0}{0 1}{1 1}{2 1}TEGE
{0 0}{0 1}{1 1}{1 0}TEGS
{0 0}{0 1}{1 1}{1 2}TEGK
{0 0}{0 1}{0 2}{1 2}TEEK
Starting point is {0,1}=E
{0 1}{1 1}{2 1}{2 0}EGET
{0 1}{1 1}{2 1}{2 2}EGEL
{0 1}{1 1}{1 0}{0 0}EGST
{0 1}{1 1}{1 0}{2 0}EGST
{0 1}{1 1}{1 2}{0 2}EGKE
{0 1}{1 1}{1 2}{2 2}EGKL
{0 1}{0 0}{1 0}{2 0}ETST
{0 1}{0 0}{1 0}{1 1}ETSG
{0 1}{0 2}{1 2}{2 2}EEKL
{0 1}{0 2}{1 2}{1 1}EEKG
...
...
</code></pre>

<h3 id="çözüm-2---recursion">Çözüm 2 - Recursion</h3>

<p><img src="/img/regexengine/recursion.jpg" alt="Capture 2" /></p>

<p>Yukarıdaki döngü kullanan kodumuz, 4 kelime içeren kelimeleri arama yapabilse de, bundan daha farklı kelime uzunluklarını aramada başarısız olacaktır, bunun sebebi de
kullandığımız iç içe döngü sayısı. Eğer 5 harf içeren bir kelime aramak istiyorsanız, bunu döngü yöntemi ile yapmanın en basit yöntemi bir <code>if-else</code> ekleyip 
aranılan kelime karakter sayısı <strong>5</strong> ise iç içe 4 döngü koymanız gerekir.</p>

<p>Tabi kelime sayısı 2,3,6 gibi durumlarda kod işin içinden çıkılamaz bir hal alacağı için yardımımıza <code>recursion</code> koşacak, kısacası hard-coded bir döngü yerine
<code>recursive</code> bir yapıda fonksiyon yazıp bütün uzunlukları kapsayacağız.</p>

<p>Koda geçmeden önce, backtracking ve recursion mantığını kavramak için kullanılan ve benim de kalem kağıtla ilk kullandığım yöntemlerden biri olan bir yöntemden bahsedeyim.
Bu tarz problemlerde aslında olasılıkları içeren bir ağaç yapısı yani <a href="https://en.wikipedia.org/wiki/State_space_(computer_science)">State space</a> oluşturup onun üzerinde
geziyoruz gibi düşünebilirsiniz.</p>

<p>Yukarıdaki örnek üzerinden hatırlarsanız <code>T</code> ile başlamamız durumunda gidebileceğimiz tüm konumları ve karşılık gelen ifadeleri çıkarmıştık onu ağaç gibi modellemek 
benim anlamama oldukça yardımcı olduğu için, koda bu ağacı <code>Graphviz</code> formatında oluşturan bir fonksiyon da ekledim.</p>

<p><img src="/img/regexengine/t.svg" alt="Capture 3" /></p>

<p>Bunları dedikten sonra aynı problemi <code>recursive</code> şekilde farklı uzunluklardaki kelimeler ile de arama yapabilecek kodu yazalım.</p>

<pre><code>//..
//..

func traverse(grid [][]byte, loc Location, word string, path []Location, result *[][]Location) {
	if len(path) == len(word) {
		c := make([]Location, len(path))
		copy(c, path)
		*result = append(*result, c)
		return
	}
	for _, p1 := range loc.GetNeighbors() {
		if isValid(grid, p1, path...) {
			path = append(path, p1)
			traverse(grid, p1, word, path, result)
			path = path[:len(path)-1]
		}
	}
}

func pathToString(grid [][]byte, paths [][]Location) []string {
	result := make([]string, len(paths))
	for j, p := range paths {
		chars := make([]byte, len(p))
		for i, l := range p {
			chars[i] = grid[l.Row][l.Column]
		}
		result[j] = string(chars)
	}
	return result
}

func find(grid [][]byte, word string) bool {
	allResults := make([]string, 0)
	rows := len(grid)
	for i := 0; i &lt; rows; i++ {
		cols := len(grid[i])
		for j := 0; j &lt; cols; j++ {
			result := make([][]Location, 0)
			fmt.Println("******new root******")
			traverse(grid, Location{i, j}, word, []Location{ {i, j} }, &amp;result)
			str := pathToString(grid, result)
			allResults = append(allResults, str...)
		}
	}

	return slices.Contains(allResults, word)
}

//...
//...
</code></pre>

<p>Ekrana çarşaf kadar kodu özellikle koymayıp aynı kalan ve Graphviz ile görselleştirme ve debug için koyduğum bazı 
fonksiyonları kaldırdım. Yukarıda döngüde yaptığımız işin aynısını recursion kullanarak farklı uzunluklardaki kelimeler ile
yapabilen kodu görüyorsunuz.</p>

<p>Kodun çalıştırdıktan sonra diğer arama rotalarını da görsel ağaç yapısı olarak görmek isterseniz, çıktıdaki <code>digraph</code> kısımlarını alıp 
<a href="https://magjac.com/graphviz-visual-editor/?dot=digraph%20G%20%7B%0A%20%20node%20%5Bshape%3Dcircle%5D%3B%0A%20%20E_01-%3ET_00_1%0A%20%20S_10_1-%3ET_20_2%0A%20%20E_21-%3EL_22%0A%20%20G_11-%3ES_10%0A%20%20S_10_1-%3EG_11_1%0A%20%20E_21-%3ET_20%0A%20%20G_11-%3EE_21%0A%20%20S_10-%3ET_00%0A%20%20G_11-%3EK_12%0A%20%20K_12-%3EL_22_1%0A%20%20E_01-%3EE_02_1%0A%20%20E_02_1-%3EK_12_1%0A%20%20E_01-%3EG_11%0A%20%20K_12-%3EE_02%0A%20%20T_00_1-%3ES_10_1%0A%20%20K_12_1-%3EL_22_2%0A%20%20K_12_1-%3EG_11_2%0A%20%20S_10-%3ET_20_1%0A%20%20L_22_2%20%5Blabel%3D%22L%22%5D%3B%0A%20%20E_01%20%5Blabel%3D%22E%22%5D%3B%0A%20%20K_12%20%5Blabel%3D%22K%22%5D%3B%0A%20%20E_02%20%5Blabel%3D%22E%22%5D%3B%0A%20%20T_00_1%20%5Blabel%3D%22T%22%5D%3B%0A%20%20E_02_1%20%5Blabel%3D%22E%22%5D%3B%0A%20%20E_21%20%5Blabel%3D%22E%22%5D%3B%0A%20%20S_10%20%5Blabel%3D%22S%22%5D%3B%0A%20%20T_00%20%5Blabel%3D%22T%22%5D%3B%0A%20%20T_20_1%20%5Blabel%3D%22T%22%5D%3B%0A%20%20L_22%20%5Blabel%3D%22L%22%5D%3B%0A%20%20S_10_1%20%5Blabel%3D%22S%22%5D%3B%0A%20%20T_20_2%20%5Blabel%3D%22T%22%5D%3B%0A%20%20K_12_1%20%5Blabel%3D%22K%22%5D%3B%0A%20%20G_11%20%5Blabel%3D%22G%22%5D%3B%0A%20%20T_20%20%5Blabel%3D%22T%22%5D%3B%0A%20%20L_22_1%20%5Blabel%3D%22L%22%5D%3B%0A%20%20G_11_1%20%5Blabel%3D%22G%22%5D%3B%0A%20%20G_11_2%20%5Blabel%3D%22G%22%5D%3B%0A%7D">bu link</a> gibi göz atabilirsiniz.</p>

<p>Recursion kullanan çözümde dikkat edilmesi gereken satır belki <code>path = path[:len(path)-1]</code> diyebilirim, bütün olasılıkları dolaşmak için
bunu yapmamız gerekiyor, genelde bu kısım <code>backtrack</code> ya da <code>undo</code> olarak adlandırılıyor ama tam anlamıyla değil, neden olmadığına ileride değineceğiz.
Ama bu satırda yapılanı şöyle özetleyebiliriz, örnek; şuanda path olarak <code>GEE</code> rotasında ilerliyorum, buradan gidebileceği tüm rotaları çıkardıktan sonra
<code>GE</code> ye geri dönüp oradan gidebileceğim, <code>GET</code>, <code>GETS</code> rotalarını da çıkarmak için yapıyoruz.</p>

<p>Recursive versiyonu çalıştırıp Graphviz görseline bakarsanız, aradığımız kelime <code>GEEK</code> bulunma senaryosunda hangi rotaları kontrol ettiğini aşağıdaki gibi görebilirsiniz.</p>

<p><img src="/img/regexengine/geek-recursive.svg" alt="Capture 4" /></p>

<h3 id="çözüm-3---backtracking">Çözüm 3 - Backtracking</h3>

<p>Adım adım ilerleyerek sona doğru yaklaştık, sıra aynı problemi Backtracking yaklaşımı ile çözmeye geldi. <a href="https://en.wikipedia.org/wiki/Backtracking">Backtracking</a> dediğimiz zaman aklımıza ilk gelmesi 
gereken şey recursion, fakat arada ufak farklılıklar var buna değineceğimizi belirtmiştik. Backtracking teknik yöntem olarak recursive fonksiyonları kullanıyor, fakat
bunu yaparken tüm olasılıkları çıkarıp gezmektense, bir rota belirli bir koşulu sağlamıyorsa onu baştan eleyip, sadece koşulları sağlayan olasılıklar üzerinde devam ediyor.</p>

<p>Tabi bunun en büyük avantajı performans oluyor, çünkü değerlendirdikleri olasılıklar arasında sayı olarak çok büyük fark oluyor, bu sebeple klasik recursive algoritmaya göre çok daha hızlı çalışıyor.</p>

<p>Önce kodu Backtracking yöntemine çevirmek için ne yaptım onu inceleyelim ardından performans ve çıktılarını değerlendiririz.</p>

<pre><code>//...
//...

func traverse(grid [][]byte, loc Location, word string, path []Location, result *[][]Location) {
	if word[len(path)-1] != grid[loc.Row][loc.Column] {
		return
	}
	if len(path) == len(word) {
		c := make([]Location, len(path))
		copy(c, path)
		*result = append(*result, c)
		return
	}
	for _, p1 := range loc.GetNeighbors() {
		if isValid(grid, p1, path...) {
			path = append(path, p1)
			traverse(grid, p1, word, path, result)
			path = path[:len(path)-1]
		}
	}
}

//...
//...
</code></pre>

<p>Kodun diğer bütün fonksiyonları bir önceli ile aynı hatta <code>traverse</code> fonksiyonu bile neredeyse aynı fakat arada büyük bir performans farkı oluşturan aşağıdaki
satırlar bulunuyor.</p>

<pre><code>if word[len(path)-1] != grid[loc.Row][loc.Column] {
	return
}
</code></pre>

<p>Basit olarak bu bize girdiğimiz rotanın aradığımız kelime ile uyumlu olup olmadığını adım adım kontrol ediyor, eğer değilse
rotadan erkenden çıkmamızı sağlıyor. Mesela aramaya <code>G</code> harfinden başladığımız durumda yukarıdaki başarılı sonucu bize vermek için
tüm ağacı dolaşmak yerine aşağıdaki ağacı dolaşıp doğru sonuca ulaşıyor.</p>

<p><img src="/img/regexengine/geek-backtrack.svg" alt="Capture 5" /></p>

<p>Bir önceki ağaç ile karşılaştıracak olursanız belirgin şekilde daha az rota gezerek sonuca ulaştığı görülebiliyor. Kırmızı okları
ben özellikle nerelerde backtrack yapıp, nasıl devam ettiğini göstermek için ekledim. Nasıl çalıştığını daha belirgin olarak gösterebildiğimi umuyorum.</p>

<h2 id="performans-karşılaştırması">Performans Karşılaştırması</h2>

<p>Yukarıda işleri basit tutmak ve gözle sonucu direk bulabilmemiz için harf matrisini oldukça küçük tuttum. Ama gerçek hayatta hem Regex motoruna verilen
girdiler çok daha uzun olduğu, hem de gerçek dünya senaryoları çok daha büyük veriler içerebildiği için Recursive ve Backtracking algoritmasını 
karşılaştırmak için daha büyük bir matris kullanalım.</p>

<pre><code>{'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J'},
{'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K'},
{'C', 'D', 'E', 'F', 'A', 'H', 'I', 'J', 'K', 'L'},
{'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M'},
{'E', 'F', 'G', 'H', 'I', 'A', 'K', 'L', 'M', 'N'},
{'F', 'G', 'H', 'I', 'J', 'K', 'A', 'M', 'N', 'O'},
{'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'A', 'A'},
{'H', 'I', 'J', 'K', 'A', 'M', 'N', 'O', 'P', 'Q'},
{'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R'},
{'J', 'K', 'L', 'M', 'N', 'O', 'A', 'Q', 'R', 'S'},
{'A', 'B', 'C', 'D', 'E', 'A', 'G', 'H', 'I', 'J'},
{'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K'},
{'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L'},
{'D', 'E', 'F', 'A', 'H', 'I', 'J', 'K', 'A', 'M'},
{'E', 'F', 'A', 'H', 'I', 'J', 'A', 'L', 'M', 'N'},
{'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O'},
{'G', 'H', 'I', 'J', 'A', 'L', 'M', 'N', 'O', 'P'},
{'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q'},
{'I', 'J', 'K', 'A', 'M', 'N', 'O', 'P', 'Q', 'R'},
{'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S'},
</code></pre>

<p>Bu sefer <strong>20x10</strong> bir matris içerisinde <code>ABDEFGHMN</code> arayalım, dikkatinizi çekti ise özellikle aralara <code>A</code> harfi serpiştirdim ki
başlayabilecek birden fazla nokta olsun. Ayrıca gözle herhalde aradığımız şeyin burada olmadığı anlaşılmıştır.</p>

<p>Karşılaştırmayı bu sefer derleme <code>go build .</code> yaptıktan sonra yaptım ki CPU var ise eğer yapacağı optimizasyonları uygulasın ve
production ortamına benzer bir işlem olsun. Bir de performans karşılaştırmasını <code>go benchmark</code> aracı ile değil de klasik <code>time</code> komutu 
ile yaptım. Hangi fonksiyon ne kadar süre harcamış, ne kadar memory kullanmış gibi şeyler ile şimdilik ilgilenmiyorum, ana odağım toplamda ne kadar zaman aldığı.</p>

<pre><code>recursion &gt; go build .
recursion &gt; /usr/bin/time -al ./wordsearchrecursive
        4.27 real         2.26 user         1.65 sys
            14462976  maximum resident set size
                   0  average shared memory size
                   0  average unshared data size
                   0  average unshared stack size
                3524  page reclaims
                 263  page faults
                   0  swaps
                   0  block input operations
                   0  block output operations
                   0  messages sent
                   0  messages received
                 356  signals received
                3365  voluntary context switches
               14264  involuntary context switches
         11981226538  instructions retired
         12442993821  cycles elapsed
            12054528  peak memory footprint
</code></pre>

<p>Recursive olan toplamda 5 saniyeye yakın bir sürede işlemi tamamladı, şimdi de Backtracking kullananı çalıştıralım.</p>

<pre><code>backtrack &gt; go build .
backtrack &gt; /usr/bin/time -al ./wordsearchbacktrack
        0.05 real         0.00 user         0.00 sys
             2002944  maximum resident set size
                   0  average shared memory size
                   0  average unshared data size
                   0  average unshared stack size
                 516  page reclaims
                 214  page faults
                   0  swaps
                   0  block input operations
                   0  block output operations
                   0  messages sent
                   0  messages received
                  15  signals received
                   0  voluntary context switches
                 152  involuntary context switches
            20796342  instructions retired
            30271984  cycles elapsed
             1126400  peak memory footprint
</code></pre>

<p>Neredeyse 0 saniyede işini bitirdi, arada kaç kat hız farklı var artık hesaplamasını size bırakıyorum.</p>

<p>Go Benchmark aracı ile daha sonradan çeşitli boyutlarda rastgele matris  üretip bunları da teste tabi tuttum. Bunu sonucunu da ikiye ayırdım.
Aranılan kelimeyi bulduğu ve bulamadığı durumlarda performans çok değiştiği için bunları ölçtüğümde aşağıdaki gibi bir sonuç çıktı.</p>

<p><img src="/img/regexengine/found.svg" alt="Capture 6" /></p>

<p><img src="/img/regexengine/notfound.svg" alt="Capture 7" /></p>

<p>Bu sonuçlara da bakacak olursanız her iki durumda da Backtracking algoritması matris boyutuna göre neredeyse 0 mikro saniye içinde sonuç vermiş,
klasik Recursive ise matris boyutu arttıkça sonucu bulması daha uzun vakit almış.</p>

<h2 id="sonuç">Sonuç</h2>

<p>Biz Regex motoru yazmayacak mıydık neden bu kadar Backtracking algoritmasını anlamak için uğraştık diyenler için şunu açıklayalım.
Backtracking mantığını anlamak günümüzde yaygın olarak kullanılan Regex motorlarını ve onlarda ortaya çıkabilecek performans sorunlarını anlamak 
ve çözmek için oldukça önemli. Çünkü temelinde bu algoritma kullanılıyor, ve büyük bir girdi ve düzgün yazılmamış bir regex pattern neden çok uzun süre alabilir hatta sisteminizi patlatabilir,
işin temelinde yatan kavramı anlayarak örneklerle gördük.</p>

<p>Bundan sonrası bu algoritmayı kendi geliştirdiğimiz Regex motoruna uygulamak olacak. Bu yazıda onu da dahil etmeyi düşünüyordum ama geri dönüp bakınca oldukça uzun olmuş, devamı sonraki yazıya artık.</p>]]></content><author><name>Cihat Altuntaş</name></author><category term="regex,nfa,backtracking,golang" /><summary type="html"><![CDATA[Sıfırdan Regex Motoru - Bölüm 2: Backtracking Algoritması]]></summary></entry><entry><title type="html">Sıfırdan Regex Motoru - Bölüm 1: Parsing</title><link href="http://www.cihataltuntas.com/2025/07/25/regex-engine-1.html" rel="alternate" type="text/html" title="Sıfırdan Regex Motoru - Bölüm 1: Parsing" /><published>2025-07-25T14:00:00+00:00</published><updated>2025-07-25T14:00:00+00:00</updated><id>http://www.cihataltuntas.com/2025/07/25/regex-engine-1</id><content type="html" xml:base="http://www.cihataltuntas.com/2025/07/25/regex-engine-1.html"><![CDATA[<p>Bu yazı serisi şu ana kadar 1 bölümden oluşmaktadır, diğer bölümlere de hazır
oldukça aşağıdaki linklerden ulaşabileceksiniz. Yazı içeriğinde geçen kodlara
<a href="https://github.com/caltuntas/regex-poc">bu linkten</a> erişebilirsiniz.</p>

<ol>
  <li><a href="https://www.cihataltuntas.com/2025/07/25/regex-engine-1">Sıfırdan Regex Motoru - Bölüm 1: Parsing</a> (Bu yazı)
    <ul>
      <li>Bu yazıda, çok kısa ne yapmak istediğimizden, nasıl yapabileceğimizden ve
bize verilen regex ifadelerinin parse edilip istediğimiz veri yapısı
içinde nasıl tutabileceğimizden bahsedeceğiz.</li>
    </ul>
  </li>
  <li><a href="https://www.cihataltuntas.com/2025/07/25/regex-engine-2">Sıfırdan Regex Motoru - Bölüm 2: Backtracking Algoritması</a>
    <ul>
      <li>Bu yazıda, Backtracking algoritması nasıl çalışır, recursive algoritmaya göre
farkları nelerdir örnekler üzerinden karşılaştırarak anlamaya çalışacağız.</li>
    </ul>
  </li>
  <li><a href="https://www.cihataltuntas.com/#">Sıfırdan Regex Motoru - Bölüm 3: Backtracking Regex Motoru</a></li>
  <li><a href="https://www.cihataltuntas.com/#">Sıfırdan Regex Motoru - Bölüm 4: NFA Regex Motoru</a> (hazır değil)</li>
</ol>

<h2 id="giriş">Giriş</h2>

<p>Birkaç ay önce Regex kütüphaneleri ve kullanırken dikkatli olmaz isek başımıza 
gelebilecek sorunlar konusunda bir <a href="https://www.cihataltuntas.com/2025/04/14/i-hate-regex-1">yazı serisi</a> hazırlamıştım.
Değindiğim konulardan biri ise dikkatli olunmaz ise Javascript, .NET ya da Python gibi daha birçok farklı
programlama dilinde kullandığınız Regex ifadelerinin canlı sistemleri çalışmaz hale getirebileceği idi.</p>

<p>Hem kendim hem de benim gibi sadece okuduğu, dinlediği, kullandığı bilgi ile yetinmeyip gerçek anlamda kavramak
için el ile tutulur bir şeyler geliştirmeyi sevenlere yeni bir yazı serisi hazırladım.</p>

<p>Bu yazı serisinde, farklı yöntemlerde geliştirilen Regex motorlarının hem hangi algoritmaları kullandığını, 
nasıl geliştirildiğini ve aradaki temel farklarını kendimiz geliştirerek, karşılaştırarak örnekler üzerinden anlamaya çalışacağız.
İleride Regex motorları nasıl çalışıyormuş diye hatırlamak istersem kendime not olarak da burayı kullanmayı planlıyorum.</p>

<h2 id="kapsam">Kapsam</h2>

<p>Eğer canlı sistemlerde kullanılabilecek bir Regex kütüphanesi geliştireceğiz
desek kendimizi kandırmış oluruz. Dışarıdan bir Regex motorunu kütüphane olarak
kullanırken oldukça basit olduğunu düşünebiliriz fakat <strong>production-ready</strong> bir
Regex kütüphanesi geliştirmek oldukça kapsamlı ve kolay olmayan bir konu. Bu
yüzden kapsamı oldukça daraltıp, daha önceki yazı serisinde canlı ortamda kendi
yaşadığım problemin temelini oluşturan Regex ifadesini kapsayacak kadar bir
Regex motoru geliştirmeyi planlıyorum, en azından bu yazı serisi için.</p>

<p>Geliştireceğimiz kodun iyileştirebilecek, çok fazla noktası olacağına eminim
burada ana odağımız koddan ziyade farklı Regex motorlarının farklı
yaklaşımlarla nasıl geliştirildiğini uygulayarak öğrenmek.</p>

<p>Eğer hatırlarsanız canlı ortamda aşağıdaki gibi bir regex pattern kullanmıştık ve bu oldukça soruna sebep olmuştu.</p>

<pre><code>parent {[\s\S]*type.*[\s\S]*subtype.*[\s\S]*element.*[\s\S]*attributes.*[\s\S]*value testvalue.*[\s\S]*owner person1
</code></pre>

<p>Bu yüzden burada, geliştireceğimiz mini regex motoru yukarıda kullanılan
yapıları destekleyecek. Günün birinde vaktim olursa eklemeyi planladığım tabi
başka özellikler de var ama kapsamda aşağıdaki maddeler var şuanda.</p>

<ul class="task-list">
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Desteklenmesi gereken Regex ifadeleri
    <ul class="task-list">
      <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Karakter eşleştirme yeteneği, örnek abc</li>
      <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Özel karakter eşleştirme, örnek .\s\S</li>
      <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Karakter kümesi eşleştirme, örnek [abc\s\S]</li>
      <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Sıfır ya da daha fazla eşleştirme , kleene star <code>*</code></li>
    </ul>
  </li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Backtracking algoritması kullanan Regex motoru</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />NFA algoritması kullanan Regex motoru</li>
</ul>

<p>Anlaşılacağı gibi üzerine istersek daha sonra diğer özellikleri de inşa
edebileceğimiz, ama önceki yazı serisinde problem yaşamamıza sebep olan Regex
ifadelerini destekleyen bir motor ile 2 farklı Regex algoritması yaklaşımını
geliştirerek göreceğiz.</p>

<h2 id="teori">Teori</h2>

<p>Regex, Regular Expressions yani düzenli ifadeler ne demek tarihçesi nereden
geliyor , neden bunlara <code>düzenli</code> ifadeler deniyor gibi işin teorik ve
tarihçesini bu yazıda anlatmak yazıyı oldukça sıkıcı yapacaktır, ama işin
teorisini ve geçmişini öğrenmenizi şiddetle tavsiye ederim, diğer şekilde
geliştirme yaparken konu biraz havada kalabilir.</p>

<p>Regex konusunu bir string veri yapıları üzerinde arama, bulma işlemine
özelleşmiş mini bir programlama dili olarak düşünebiliriz. Tam anlamıyla bir
programlama dili kadar kompleks değil, bu yüzden <strong>düzenli ifadeler</strong> içinde
bulunan <code>düzenli</code> kelimesi buradan geliyor diyebiliriz.</p>

<h2 id="gramer">Gramer</h2>

<p>Eğer <strong>compiler</strong> ya da <strong>parsing</strong> gibi bir konu ile daha önce ilgilendiyseniz, ilk aşamalardan birinin 
dilin destekleyeceği yapının çıkarılması olduğunu deneyimlemişsinizdir.</p>

<p>Aslında Regular Expression programlama dillerine göre <code>parse</code> etmesi oldukça
basit olsa da, ben yine de gramerin çıkarılmasının önemli olduğunu düşünüyorum.
Bu çıkardığınız gramer eğer parsing işlemini Bison, Flex, ANTLR gibi bir araç
ile yapacaksanız işinizi oldukça kolaylaştıracak, çünkü bu araçlara grameri
verdikten sonra size <code>parser</code> kodunu otomatik olarak oluşturabiliyor.  Yani
parser ya da lexer yazmak zorunda kalmıyorsunuz.</p>

<p>Bunları dedikten sonra yukarıda neleri destekleyeceğimizi yazmıştık ona göre
oluşturduğum <a href="https://en.wikipedia.org/wiki/Backus%E2%80%93Naur_form">BNF</a>
formatında grameri aşağıda görebilirsiniz.</p>

<pre><code>&lt;expression&gt;     ::= &lt;term&gt; | &lt;term&gt; &lt;expression&gt;
&lt;term&gt;          ::= &lt;factor&gt; &lt;quantifier&gt;?
&lt;factor&gt;        ::= &lt;char&gt; | "." | &lt;escaped_char&gt; | "[" &lt;char_class&gt; "]"
&lt;char_class&gt;    ::= &lt;char_class_item&gt;+
&lt;char_class_item&gt; ::= &lt;char&gt; | &lt;escaped_char&gt; | "."
&lt;char&gt;          ::= [a-z]
&lt;escaped_char&gt;  ::= "\\s" | "\\S"
&lt;quantifier&gt;    ::= "*"
</code></pre>

<p>Grameri test etmek için <a href="https://bnfplayground.pauliankline.com/?bnf=%3Cexpression%3E%20%20%20%20%20%3A%3A%3D%20%3Cterm%3E%20%7C%20%3Cterm%3E%20%3Cexpression%3E%0A%3Cterm%3E%20%20%20%20%20%20%20%20%20%20%3A%3A%3D%20%3Cfactor%3E%20%3Cquantifier%3E%3F%0A%3Cfactor%3E%20%20%20%20%20%20%20%20%3A%3A%3D%20%3Cchar%3E%20%7C%20%22.%22%20%7C%20%3Cescaped_char%3E%20%7C%20%22%5B%22%20%3Cchar_class%3E%20%22%5D%22%0A%3Cchar_class%3E%20%20%20%20%3A%3A%3D%20%3Cchar_class_item%3E%2B%0A%3Cchar_class_item%3E%20%3A%3A%3D%20%3Cchar%3E%20%7C%20%3Cescaped_char%3E%20%7C%20%22.%22%0A%3Cchar%3E%20%20%20%20%20%20%20%20%20%20%3A%3A%3D%20%5Ba-z%5D%0A%3Cescaped_char%3E%20%20%3A%3A%3D%20%22%5C%5Cs%22%20%7C%20%22%5C%5CS%22%0A%3Cquantifier%3E%20%20%20%20%3A%3A%3D%20%22*%22%0A&amp;name=Date%20Format">bu linki</a> kullanabilirsiniz</p>

<h2 id="lexer">Lexer</h2>

<p>Yukarıdaki açıklamayı yaptıktan sonra tabi işin kolayına kaçıp parser ve lexer
kodunu otomatik oluşturacağımızı düşünenler olabilir ama bir biraz daha
uğraştırıcı olanı seçip bunları kendimiz geliştireceğiz. Eğer kendimiz
geliştireceksek elimizde gramer dosyası olmasa da bu yapılabilir, fakat
elimizde gramerin formal olarak tanımı onu test etmek için ya da ileride farklı
araçlar kullanmak için her zaman faydalı olacaktır.</p>

<blockquote>
  <blockquote>
    <p>Regex ifadelerini parse etmek için benim izlediğim yaklaşım gibi Lexer ve Parser yazmadan daha
basit bir yöntemle <a href="https://en.wikipedia.org/wiki/Shunting_yard_algorithm">Shunting yard algorithm</a> algoritması
kullanılarak bileşenlerine ayrıştırılabilir, burada daha fazla kod yazacak olsam da benim önceki tecrübemden dolayı bu algoritmayı kullanmayıp Lexer ve Parser yazma işine girişiyorum.</p>
  </blockquote>
</blockquote>

<p>Programlama dili olarak önce <code>C</code> ile yapmayı düşünmüştüm ama sonra bellek
yönetimi, işimizi kolaylaştıracak bazı veri yapılarının ve fonksiyonların
eksikliğinden dolayı vazgeçtim ve bu konularda oldukça zengin bir standart
kütüphaneye sahip olduğunu düşündüğüm,ve yine C gibi basit bir dil olan <code>Go</code>
ile yapmaya karar verdim.</p>

<p>Her zaman ilk testi yazarak geliştirme yapmasam da, bu tarz çekirdek fonksiyonların önemli olduğunu düşündüğüm projelerde
test ile başlamayı tercih ediyorum, ileride değişiklik olduğunda işimi oldukça kolaylaştırıyor, ayrıca kafamda ne yapmam
gerektiğini de toparlamama yardımcı oluyor diyebilirim.</p>

<p>Aşağıdaki gibi Lexer için testimizi yazalım.</p>

<pre><code>package main

import "testing"

func TestNextToken(t *testing.T) {
	l := New("pa.*tpa[ab]cpa\\s")
	tests := []struct {
		expectedType TokenType
		expectedLiteral string
	} {
		{LITERAL, "p"},
		{LITERAL, "a"},
		{DOT, "."},
		{STAR, "*"},
		{LITERAL, "t"},
		{LITERAL, "p"},
		{LITERAL, "a"},
		{LBRACKET, "["},
		{LITERAL, "a"},
		{LITERAL, "b"},
		{RBRACKET, "]"},
		{LITERAL, "c"},
		{LITERAL, "p"},
		{LITERAL, "a"},
		{ESCAPE, "\\"},
		{LITERAL, "s"},
	}

	for i, test := range tests {
		token := l.NextToken()
		if token.Type != test.expectedType {
			t.Fatalf("test[%d], expected type doesn't match. expected = %q, got = %q",i, token.Type, test.expectedType)
		}
		if token.Value != test.expectedLiteral {
			t.Fatalf("test[%d], expected type doesn't match. expected = %q, got = %q",i, token.Value, test.expectedLiteral)
		}
	}
}
</code></pre>

<p>Testi biraz incelerseniz yapmaya çalıştığımız şeyin elimizde olan bir <code>string</code> değişkenin, her <code>NextToken</code> metodu ile 
ifadelerine ayrıştırılmasını yapıyoruz.</p>

<pre><code>pa.*tpa[ab]cpa\\s
</code></pre>

<p>Yukarıdaki gibi bir regex ifadesini, bileşenlerine ayırıp bize verecek ve testi geçecek kodu aşağıdaki gibi yazabiliriz.</p>

<pre><code>package main

type TokenType string

type Token struct {
	Type  TokenType
	Value string
}

const (
	STAR     = "*"
	DOT      = "."
	LBRACKET = "["
	RBRACKET = "]"
	ESCAPE   = "\\"
	EOF      = "EOF"
)

type Lexer struct {
	input        string
	position     int
	readPosition int
	ch           byte
}

func New(pattern string) *Lexer {
	l := &amp;Lexer{input: pattern}
	return l
}

func (l *Lexer) PeekChar() byte {
	if l.readPosition &gt;= len(l.input) {
		return 0
	}
	return l.input[l.readPosition]
}

func (l *Lexer) NextToken() Token {
	var token Token
	l.readChar()
	if l.ch == 0 {
		token.Type = EOF
		token.Value = ""
	} else if l.ch == '.' {
		token.Type = DOT
		token.Value = string(l.ch)
	} else if l.ch == '*' {
		token.Type = STAR
		token.Value = string(l.ch)
	} else if l.ch == '[' {
		token.Type = LBRACKET
		token.Value = string(l.ch)
	} else if l.ch == ']' {
		token.Type = RBRACKET
		token.Value = string(l.ch)
	} else if l.ch == '\\' {
		token.Type = ESCAPE
		token.Value = string(l.ch)
	} else {
		token.Type = LITERAL
		token.Value = string(l.ch)
	}
	return token
}

func (l *Lexer) readChar() {
	if l.readPosition &gt;= len(l.input) {
		l.ch = 0
	} else {
		l.ch = l.input[l.readPosition]
	}
	l.position = l.readPosition
	l.readPosition++
}
</code></pre>

<p>Lexer kodu görüldüğü gibi her çağrıldığında bileşenlerine ayırmaktan farklı bir şey yapmıyor, ama işimizi kolaylaştırıyor diyebiliriz.
Şimdi bu bileşenleri kullanarak <a href="https://en.wikipedia.org/wiki/Abstract_syntax_tree">Abstract Syntax Tree</a> oluşturacak ve aslında
bundan sonra yapacaklarımızın temelini oluşturacak kısmı yazacağız.</p>

<h2 id="parser">Parser</h2>

<p><a href="https://en.wikipedia.org/wiki/Parsing">Parsing</a> konusu oldukça geniş bir konu,
genel olarak çözülmüş bir bilgisayar bilimleri problemi olarak düşünülebilir.
Programlama dillerinin çıkışından beri üzerinde çalışılmış, optimize edilmiş ve
çeşitli gramer yapılarına göre farklı algoritmaların öne çıktığı bir alan diyebiliriz.</p>

<p>Biz yaptığımız geliştirmede <a href="https://en.wikipedia.org/wiki/Top-down_parsing">Top Down Parsing</a> yöntemi içinde
<a href="https://en.wikipedia.org/wiki/Recursive_descent_parser">Recursive descent parser</a> kullanarak problemi çözmeye çalışacağız.</p>

<h3 id="abstract-syntax-tree">Abstract Syntax Tree</h3>

<p>Parsing kodunu yazmaya başlamadan önce, ayrıştırdığımız token değerlerini Tree veri yapısında çevireceğiz, bunun için
ne tarz bir veri yapısı içinde tutacağımızı belirlememiz gerekiyor, ardından parsing kodu bize bu veri yapısında bir değişken dönecek.</p>

<p>Benim problem için tutmayı planladığım veri yapısı aşağıdaki gibi.</p>

<pre><code class="language-mermaid">classDiagram
    class Node {
        &lt;&lt;interface&gt;&gt;
        String() string
    }

    class StarNode {
        Child Node
    }

    class SequenceNode {
        Children Node[]
    }

    class CharacterNode {
        &lt;&lt;interface&gt;&gt;
        GetValue() string
    }

    class LiteralNode {
        Value byte
    }

    class MetaCharacterNode {
        Value string
    }

    class CharList {
        Chars CharacterNode[]
    }

    Node &lt;|-- StarNode
    Node &lt;|-- SequenceNode
    Node &lt;|-- CharacterNode
    CharacterNode &lt;|-- LiteralNode
    CharacterNode &lt;|-- MetaCharacterNode
    Node &lt;|-- CharList
</code></pre>

<p>Kısaca yukarıdaki tiplerin hangi amaç için kullanılacağını da açıklayalım, kodu incelerseniz zaten görülecek ama yine de burada dursun.</p>

<ul>
  <li><em>Node</em> : Tüm veri yapılarının temelini oluşturuyor, soyutlama için kullanacağız.</li>
  <li><em>SequenceNode</em> : <code>abc</code> gibi bir regex ifadesinde, sırayla bütün bileşenleri içeren veri tipi, özetle önce <code>a</code> sonra <code>b</code> sonra <code>c</code> içermesini söyleyen veri yapısı</li>
  <li><em>StarNode</em> : <code>pa.*</code> ifadesinde bulunan <code>*</code> için kullanılan veri yapısı</li>
  <li><em>CharacterNode</em> : Meta ya da Literal karakter olarak tutulan tipler için oluşturduğum üst interface</li>
  <li><em>LiteralNode</em> : <code>a</code> karakterini gerçek değeri ile eşleştirmesini söyleyen veri yapısı</li>
  <li><em>MetaCharacterNode</em> : <code>.</code> ya da <code>\s</code> gerçek anlamı dışında eşleşme yapmasını bildiren veri yapısı</li>
  <li><em>CharList</em> : <code>[abc]</code> gibi regex ifadelerini temsil eden veri yapısı</li>
</ul>

<p>Yukarıdaki gibi düşündüğümüz veri yapılarını tasarladığımız kod aşağıdaki gibi olacak.</p>

<pre><code>package main

import "fmt"

type Node interface {
	String() string
}

type StarNode struct {
	Child Node
}

type SequenceNode struct {
	Children []Node;
}

func (n *SequenceNode) String() string {
	str := ""
	str += fmt.Sprintln("SequenceNode, Children")
	for i,child := range n.Children {
		str += fmt.Sprintf("Child %d = %s\n", i, child.String())
	}
	return str
}

type CharacterNode interface {
	GetValue() string
	String() string
}

type LiteralNode struct {
	Value byte
}

func (n* LiteralNode) String() string {
	return string(n.Value)
}

func (n* LiteralNode) GetValue() string {
	return n.String()
}

type MetaCharacterNode struct {
	Value string
}

func (n* MetaCharacterNode) String() string {
	return n.Value
}

func (n* MetaCharacterNode) GetValue() string {
	return n.String()
}

func (n *StarNode) String() string {
	return n.Child.String() + "*"
}

type CharList struct {
	Chars []CharacterNode
}

func (n *CharList) String() string {
	str := "["
	for _,cn := range n.Chars {
		str += cn.String()
	}
	return str + "]"
}
//..
//..

</code></pre>

<p>Gerekli veri yapılarını oluşturduktan sonra işin en önemli kısmına yani parsing kodunu yazmaya sıra geldi.
Parsing kodu bize verilen Regular Expression ifadesini bileşenlerine ayırıp <code>Node</code> tipinde bir veri yapısı dönecek.
Önce basit bir pattern ile nasıl gözükmesi gerektiğini açıklayalım sonra koda geçelim.</p>

<p>Mesela <code>pa.*b</code> gibi bir pattern aşağıdaki gibi bir AST veri yapısına dönüşmesi gerekiyor.</p>

<pre><code>Sequence:
    Literal: 'p'
    Literal: 'a'
    Star:
      Meta: '.'
    Literal: 'b'
</code></pre>

<p>Yine test yazarak başlamak işimizi daha kolaylaştıracaktır, farklı bir çok senaryoya göre üretmesi gereken, veri yapılarını
test ettiğimiz aşağıdaki gibi bir test kodu yazabiliriz.</p>

<pre><code>//...
//...
var b NodeBuilder

func TestRegexParser(t *testing.T) {
	cases := map[string]Node{
		"p[ab]*c": b.Seq(
			b.Lit('p'),
			b.Star(b.List(b.Lit('a'), b.Lit('b'))),
			b.Lit('c'),
		),
		"p[\\sb]c": b.Seq(
			b.Lit('p'),
			b.List(b.Meta(WHITESPACE), b.Lit('b')),
			b.Lit('c'),
		),
		"pa\\s": b.Seq(
			b.Lit('p'),
			b.Lit('a'),
			b.Meta(WHITESPACE),
		),
		"pa[ab]c": b.Seq(
			b.Lit('p'),
			b.Lit('a'),
			b.List(b.Lit('a'), b.Lit('b')),
			b.Lit('c'),
		),
		"pa.*t": b.Seq(
			b.Lit('p'),
			b.Lit('a'),
			b.Star(b.Meta(DOT)),
			b.Lit('t'),
		),
		"pa {": b.Seq(
			b.Lit('p'),
			b.Lit('a'),
			b.Lit(' '),
			b.Lit('{'),
		),
	}
	for key, val := range cases {
		l := New(key)
		parser := NewParser(l)
		node := parser.Ast()
		ok := testNode(t, node.(*SequenceNode), val)
		if !ok {
			t.Fatalf("expected SequenceNode, got %T", node)
		}
	}
}
</code></pre>

<p>Bundan sonra son aşamada aşağıdaki gibi parser kodunu yazabiliriz.</p>

<pre><code>package main

type Parser struct {
	l            *Lexer
	currentToken Token
	nextToken    Token
}

func NewParser(l *Lexer) *Parser {
	p := &amp;Parser{l: l}
	p.readNextToken()
	p.readNextToken()
	return p
}

func (p *Parser) Ast() Node {
	return p.parseExpression()
}

func (p *Parser) parseExpression() Node {
	var node Node
	sequence := &amp;SequenceNode{}
	node = sequence
	for p.currentToken.Type != EOF {
		term := p.parseTerm()
		if term != nil {
			sequence.Children = append(sequence.Children, term)
		}
	}
	return node
}

func (p *Parser) parseTerm() Node {
	factor := p.parseFactor()
	if p.nextToken.Type == STAR {
		star := &amp;StarNode{}
		star.Child = factor
		p.readNextToken()
		p.readNextToken()
		return star
	}
	p.readNextToken()
	return factor
}

func (p *Parser) parseFactor() Node {
	var node Node
	switch p.currentToken.Type {
	case DOT:
		node = &amp;MetaCharacterNode{Value: "."}
	case LITERAL:
		node = &amp;LiteralNode{Value: p.currentToken.Value[0]}
	case ESCAPE:
		p.readNextToken()
		if p.currentToken.Value == "s" {
			return &amp;MetaCharacterNode{Value: WHITESPACE}
		}
		if p.currentToken.Value == "S" {
			return &amp;MetaCharacterNode{Value: NONWHITESPACE}
		}
	case LBRACKET:
		p.readNextToken()
		charList := &amp;CharList{}
		for p.currentToken.Type != RBRACKET {
			char := p.parseFactor()
			charList.Chars = append(charList.Chars, char.(CharacterNode))
			p.readNextToken()
		}
		return charList
	}
	return node
}

func (p *Parser) readNextToken() {
	p.currentToken = p.nextToken
	p.nextToken = p.l.NextToken()
}
</code></pre>

<p>Anlaşılması zor bir kod olduğunu düşünmüyorum, <code>resursive</code> olarak beklediğimiz ağaç veri yapısını oluşturup bize 
veriyor, bir sonraki aşamada bu veri yapısını kullanarak bir çok Regex motorunun yaptığı gibi <code>Backtracking</code> yaklaşımı ile
eşleştirme yapacağız.</p>]]></content><author><name>Cihat Altuntaş</name></author><category term="regex,nfa,backtracking,golang" /><summary type="html"><![CDATA[Sıfırdan Regex Motoru - Bölüm 1: Parsing]]></summary></entry><entry><title type="html">I Hate Regular Expressions - 2</title><link href="http://www.cihataltuntas.com/2025/05/17/i-hate-regex-2.html" rel="alternate" type="text/html" title="I Hate Regular Expressions - 2" /><published>2025-05-17T14:00:00+00:00</published><updated>2025-05-17T14:00:00+00:00</updated><id>http://www.cihataltuntas.com/2025/05/17/i-hate-regex-2</id><content type="html" xml:base="http://www.cihataltuntas.com/2025/05/17/i-hate-regex-2.html"><![CDATA[<p><img src="/img/hateregex/hateregex.png" alt="Capture 1" /></p>

<p>Bu yazı serisi şu ana kadar 2 bölümden oluşmaktadır, diğer bölümlere aşağıdaki linklerden ulaşılabilir. Yazı içeriğinde geçen kodlara
<a href="https://gist.github.com/caltuntas/c09ddd9e4297a0235924f65e6f72a4f6">bu linkten</a> ulaşabilirsiniz.</p>

<ol>
  <li><a href="https://www.cihataltuntas.com/2025/04/14/i-hate-regex-1">I Hate Regular Expressions - 1</a>
    <ul>
      <li>Bu yazıda, string içeriği değiştirmede dikkatli olmadan kullanılan Regular
Expression yönteminin ne tarz sorunlara yol açabileceğini ve alternatif
Olarak neler yapabileceğimizi inceliyoruz.</li>
    </ul>
  </li>
  <li><a href="https://www.cihataltuntas.com/2025/05/17/i-hate-regex-2">I Hate Regular Expressions - 2</a> (Bu yazı)
    <ul>
      <li>Bu yazıda, Regular Expression kullanırken başıma gelen ve dikkatli olunmaz ise 
çok fazla kişinin başına gelebilecek geliştirdiğiniz sistemin cevap veremeyebilecek duruma
gelmesine sebep olabilecek performans sorunlarına değiniyoruz.</li>
    </ul>
  </li>
</ol>

<p>Geçen yazıda basit string arama ve değiştirme işlemlerinde RegEx kullanırken ne tarz sorunlar olabileceğinden örneklerle bahsedip
sonrasında daha kritik ve çok daha fazla karşılaşılabilecek sorunlardan biri olan performans problemlerini konuşacağız demiştik, hadi başlayalım.</p>

<h3 id="kazık-2">Kazık 2</h3>

<p>Benim bile yıllar boyunca RegEx kullanıp sonrasında kafayı taşa vurup farkında olduğum dikkat edilmesi gereken konulardan biri de, Regular Expression kullanırken 
performans konusunda oldukça dikkatli olunması gerektiği. Konuyu uzatmadan direk canlı ortamda yaşadığım bir problemi burada oluşturup sonrasında arkasında yatan sebeplere değinelim.</p>

<p>Yediğimiz kazıklarda ikinci bölümdeyiz, aşağıdaki gibi bir konfigürasyon dosyamız var, formatı muhtemelen fark edeceksiniz JSON formatına benzerse de aslında değil.</p>

<pre><code>parent {
    type {
        subtype TestSubType {
            element TestElement {
                attributes {
                    name testname;
                    description testdescription;
                    count 1234;
                    value testvalue;
                }
                owner person1;
            }
        }
        subtype TestSubType2 {
            element TestElement2 {
                attributes {
                    name testname2;
                    description testdescription2;
                    count 2;
                    value 2
                }
                owner unknown;
            }
        }
    }
}
</code></pre>

<p>Bu konfigürasyon dosyasında ya da çıktısında aradığımız belirli bir <code>pattern</code> var. Aradığınız şey de <code>parent</code> altında <code>type</code> altında <code>attribute</code> altında <code>value</code> değeri <code>testvalue</code> olan ve owner alanı <code>person1</code> değerine sahip olan bir değer olsun. 
Eğer bu değer varsa aradığınızı bulmuş olacaksınız kodunuz ona göre davranacak yok ise farklı bir senaryo işleteceksiniz. Yani aradığımız <code>pattern</code>  biraz şuna benzeyecek diyebiliriz, <code>parent-&gt;type-&gt;subtype-&gt;element-&gt;attributes-&gt;value==testvalue-&gt;owner==person</code> </p>

<p>Hemen aklımıza süper bir fikir geldi, ve bu işi en basitçe <code>Regular Expressin</code> kullanarak çözebiliriz dedik, ne de olsa elimizde bir JSON dosyası yok, bu kadar basit bir iş için de yeni bir <code>parser</code> yazacak değiliz. 
Aşağıdaki gibi bir kod yazdık, Javascript kullandık, farklı diller de kullanabilirdik konu genel olarak dilden bağımsız ona çok takılmayalım şimdilik.</p>

<pre><code>const fs = require('fs');

const args = process.argv.slice(2);
if (args.length &lt; 1) {
    console.log('missing arguments');
    process.exit(1);
}
const configFile = args[0];
const config = fs.readFileSync(configFile, 'utf8');

const regex = /parent {[\s\S]*type.*[\s\S]*subtype.*[\s\S]*element.*[\s\S]*attributes.*[\s\S]*value testvalue.*[\s\S]*owner person1/;
const match = config.match(regex);
console.log("Match:", match);
</code></pre>

<p>Kodu açıklamaya gerek duyulmayacak kadar basit diye düşünüyorum. Konfigürasyonu okuduk, sonrasında <code>RegEx</code> oluşturup aradığımız şeyin orada olup olmadığını kontrol ediyoruz basitçe, çalıştırıp sonuca bakalım.</p>

<pre><code>code &gt; /usr/bin/time -al node validate-config.js settings1.conf
Match: [
  'parent {\n' +
    '    type {\n' +
    '        subtype TestSubType {\n' +
    '            element TestElement {\n' +
    '                attributes {\n' +
    '                    name testname;\n' +
    '                    description testdescription;\n' +
    '                    count 1234;\n' +
    '                    value testvalue;\n' +
    '                }\n' +
    '                owner person1',
  index: 0,
  input: 'parent {\n' +
    '    type {\n' +
    '        subtype TestSubType {\n' +
    '            element TestElement {\n' +
    '                attributes {\n' +
    '                    name testname;\n' +
    '                    description testdescription;\n' +
    '                    count 1234;\n' +
    '                    value testvalue;\n' +
    '                }\n' +
    '                owner person1;\n' +
    '            }\n' +
    '        }\n' +
    '        subtype TestSubType2 {\n' +
    '            element TestElement2 {\n' +
    '                attributes {\n' +
    '                    name testname2;\n' +
    '                    description testdescription2;\n' +
    '                    count 2;\n' +
    '                    value 2\n' +
    '                }\n' +
    '                owner unknown;\n' +
    '            }\n' +
    '        }\n' +
    '    }\n' +
    '}\n',
  groups: undefined
]
        0.14 real         0.10 user         0.02 sys
            23429120  maximum resident set size
                   0  average shared memory size
                   0  average unshared data size
                   0  average unshared stack size
                6135  page reclaims
                   3  page faults
                   0  swaps
                   0  block input operations
                   0  block output operations
                   0  messages sent
                   0  messages received
                   0  signals received
                   3  voluntary context switches
                 199  involuntary context switches
           507784527  instructions retired
           338288340  cycles elapsed
             9248768  peak memory footprint
code &gt;
</code></pre>

<p>Her şey harika, kodu yazdık, yaygın olarak kullanılan konfigürasyon dosyası içeriği ile test ettik hatasız bir şekilde <code>0.14</code> saniye gibi hızlı bir sürede çalıştı ve aradığımız şeyi buldu ve ekrana yazdı.
Tabi kodumuz canlı bir sistemde, bizim kafamızda olan en temel senaryoyu içeren konfigürasyon dosyasın dışında farklı girdiler ile test edilene kadar…</p>

<p>Farklı dosyalar işin içine girince işler karışmaya başlayabilir sıkı durun. Kodu bir de şöyle bir dosya ile test edelim bakalım neler olacak.</p>

<pre><code>parent {
    type {
        subtype TestSubType {
            element TestElement {
                attributes {
                    name testname;
                    description testdescription;
                    count 1234;
                    value testvalue;
                }
                owner person1;
            }
        }
        subtype TestSubType1 {
            element TestElement1 {
                attributes {
                    name testname1;
                    description testdescription1;
                    count 1;
                    value 1
                }
                owner person2;
            }
        }
        subtype TestSubType2 {
            element TestElement2 {
                attributes {
                    name testname2;
                    description testdescription2;
                    count 2;
                    value 2
                }
                owner unknown;
            }
        }
        subtype TestSubType3 {
            element TestElement3 {
                attributes {
                    name testname3;
                    description testdescription3;
                    count 3;
                    value 3
                }
                owner unknown;
            }
        }
        subtype TestSubType3 {
            element TestElement4 {
                attributes {
                    name testname4;
                    description testdescription4;
                    count 4;
                    value 4
                }
                owner unknown;
            }
        }
        subtype TestSubType4 {
            element TestElement5 {
                attributes {
                    name testname5;
                    description testdescription5;
                    count 5;
                    value 5
                }
                owner unknown;
            }
        }
        subtype TestSubType5 {
            element TestElement6 {
                attributes {
                    name testname6;
                    description testdescription6;
                    count 6;
                    value 6
                }
                owner unknown;
            }
        }
        subtype TestSubType6 {
            element TestElement7 {
                attributes {
                    name testname7;
                    description testdescription7;
                    count 7;
                    value 7
                }
                owner unknown;
            }
        }
        subtype TestSubType7 {
            element TestElement8 {
                attributes {
                    name testname8;
                    description testdescription8;
                    count 8;
                    value 8
                }
                owner unknown;
            }
        }
        subtype TestSubType8 {
            element TestElement9 {
                attributes {
                    name testname9;
                    description testdescription9;
                    count 9;
                    value 9
                }
                owner unknown;
            }
        }
        subtype TestSubType9 {
            element TestElement10 {
                attributes {
                    name testname10;
                    description testdescription10;
                    count 10;
                    value 10
                }
                owner unknown;
            }
        }
        subtype TestSubType10 {
            element TestElement11 {
                attributes {
                    name testname11;
                    description testdescription11;
                    count 11;
                    value 11
                }
                owner unknown;
            }
        }
    }
}
</code></pre>

<p>Yukarıdaki konfigürasyon dosyası ile test edelim bakalım neler olacak</p>

<pre><code>code &gt; /usr/bin/time -al node validate-config.js settings2.conf
Match: [
  'parent {\n' +
    '    type {\n' +
    '        subtype TestSubType {\n' +
    '            element TestElement {\n' +
    '                attributes {\n' +
    '                    name testname;\n' +
    '                    description testdescription;\n' +
    '                    count 1234;\n' +
    '                    value testvalue;\n' +
    '                }\n' +
    '                owner person1',
  index: 0,
  input: 'parent {\n' +
    '    type {\n' +
    '        subtype TestSubType {\n' +
    '            element TestElement {\n' +
    '                attributes {\n' +
    '                    name testname;\n' +
    '                    description testdescription;\n' +
    '                    count 1234;\n' +
    '                    value testvalue;\n' +
    '                }\n' +
    '                owner person1;\n' +
    '            }\n' +
    '        }\n' +
    ....
    ....
    ....
  groups: undefined
]
       18.96 real        18.81 user         0.04 sys
            23609344  maximum resident set size
                   0  average shared memory size
                   0  average unshared data size
                   0  average unshared stack size
                6185  page reclaims
                   0  page faults
                   0  swaps
                   0  block input operations
                   0  block output operations
                   0  messages sent
                   0  messages received
                   0  signals received
                   7  voluntary context switches
                4728  involuntary context switches
        154144862698  instructions retired
         64223225741  cycles elapsed
             9379840  peak memory footprint
</code></pre>

<p>Eğer kodu kendi ortamınızda denerseniz zaten direk farkı hissedeceksiniz ama yukarıdaki çıktıdan da göründüğü gibi, ikinci konfigürasyon dosyasında 10 adet fazladan bizim aramadığımız farklı tipte konfigürasyonlar olduğunda
toplam süre neredeyse 20 saniyeye çıktı yani ilk versiyondan 100 katından daha yavaş çalıştı.</p>

<p>Eğer testinizi kendi ortamınızda yapmak istemiyorsanız muhtemelen çoğunuzun bildiği <code>regex101</code> sitesini ve yukarıdaki ilk örneğin orada çalışan versiyonunu <a href="https://regex101.com/r/7C7Wvo/1">bu linkten</a> deneyebilirsiniz.</p>

<p><img src="/img/hateregex/regex101-backtrack.png" alt="Capture 2" /></p>

<p>Kodu JavaScript ile yazsak da RegEx motoru olarak sitedeki örneği <code>PCRE2</code> bıraktım çünkü <code>Debugger</code> kısmı JavaScript için mevcut değil ve özellikle yukarıdaki resimde görüldüğü gibi 100.000 den fazla deneme yapıp sonunda bize meşhur
<code>Catastrophic backtracking</code> hatasını verdiğini görmenizi istedim. RegEx motorunu JavaScript yaparsanız çalışıp bu size sonuç gösterecek ama bu işlemi yapmak için yüzlerce hatta binlerce defa deneme yaptığı gerçeğini değiştirmeyecek.
Zaten daha uzun konfigürasyon dosyası verdiğimizde sürenin logaritmik olarak 20 saniyeye çıkmasının sebebi de aranan şeyi bulmak için deneme sayılarının çok daha fazla artmasından kaynaklanıyor.</p>

<p>Bu sorun canlı ortamda bu konfigürasyonun çok benzeri ve çok daha uzunu ile
başıma geldiği için sistem bunu 20 saniyede de bitiremiyordu ve sorunun ana
sebebini bulmak için günler harcadık diyebilirim. İşin kötü tarafı bu sorun
NodeJs ve JavaScript diline özel değil, diğer bir çok programlama dili bu tarz
bir durumda aynı sorunla karşılaşıyor.  Burada NodeJs tarafında ek olarak işi
daha da kötü hale getiren başımızın belası <a href="https://nodejs.org/en/learn/asynchronous-work/event-loop-timers-and-nexttick">Event Loop</a>
bir işlem tarafından meşgul edildiğinde sistem cevap veremez hale geliyor.</p>

<h3 id="kazık-2---temel-sebep">Kazık 2 - Temel Sebep</h3>

<p>Sorunun sebebi NodeJs ve benzer diğer programlama dillerinde <code>Regular Expression</code> motorunun nasıl geliştirildiği ve yapısı ile alakalı ve çözümü de her durumda o kadar basit değil. JavaScript, C#, Perl.. gibi programlama dillerinde
RegEx kütüphaneleri genellikle <a href="https://en.wikipedia.org/wiki/Backtracking">Backtracking</a> algoritmasını içerecek şekilde geliştiriliyor. Bu da bir anlamda, aranan şeyi bulmak için <code>Brute Force</code> yöntemi yerine biraz daha akıllı olarak tüm olasılıkları denemeye dayanan
bir algoritma.</p>

<p>Böyle olunca bu tarz programlama dili ortamlarında kullandığınız RegEx pattern, size verilen yani içinde arama yapacağınız girdi çok fazla önem ifade ediyor. Yani bu tarz dillerde düzgün yazılmamış bir RegEx sisteminizi kitleyecek,
cevap veremeyecek hale getirmesi oldukça olası, bu tarz ataklara <code>ReDos</code> atakları deniyor, NodeJs özelinde <a href="https://nodejs.org/en/learn/asynchronous-work/dont-block-the-event-loop#blocking-the-event-loop-redos">bu linkten</a> örnekleri ile birlikte
inceleyebilirsiniz.</p>

<p>Yıllar önce programcıların kutsal bilgi kaynağı <code>stackoverflow.com</code> dahi aynı sebepten dolayı çökmüştü detaylara <a href="https://adtmag.com/blogs/dev-watch/2016/07/stack-overflow-crash.aspx">buradan</a> göz atabilirsiniz.</p>

<h3 id="kazık-2---alternatif-1">Kazık 2 - Alternatif 1</h3>

<p>Kullandığımız RegEx pattern çok düşünülmeden, performans etkisi göz ardı edilerek yazılmış onu biraz daha optimize ederek daha iyi sonuçlar alabiliriz diye düşünüyorum. Eski pattern çok fazla <code>greedy</code> yani bulabildiğin kadar bul tarzı <code>.*</code> ifadeleri
kullandığından çok fazla kontrol yapıyordu bunu azaltmak için kodu şu şekilde değiştirip tekrar deneyelim.</p>

<pre><code>const fs = require('fs');

const args = process.argv.slice(2);
if (args.length &lt; 1) {
    console.log('missing arguments');
    process.exit(1);
}
const configFile = args[0];
const config = fs.readFileSync(configFile, 'utf8');

const regex = /parent\s*\{\s*type\s*\{[^{}]*subtype\s+TestSubType\s*\{[^{}]*element\s+TestElement\s*\{[^{}]*attributes\s*\{[^{}]*value\s+testvalue[^{}]*\}[^{}]*owner\s+person1/;
const match = config.match(regex);
console.log("Match:", match);
</code></pre>

<p>Kodu tekrar çalıştıralım bakalım ne kadar sürecek.</p>

<pre><code>code &gt; /usr/bin/time -al node validate-config-alt-1.js settings2.conf
Match: [
  'parent {\n' +
    '    type {\n' +
    '        subtype TestSubType {\n' +
    '            element TestElement {\n' +
    '                attributes {\n' +
    '                    name testname;\n' +
    '                    description testdescription;\n' +
    '                    count 1234;\n' +
    '                    value testvalue;\n' +
    '                }\n' +
    '                owner person1',
  index: 0,
  input: 'parent {\n' +
    '    type {\n' +
    '        subtype TestSubType {\n' +
    '            element TestElement {\n' +
    '                attributes {\n' +
    '                    name testname;\n' +
    '                    description testdescription;\n' +
    '                    count 1234;\n' +
    '                    value testvalue;\n' +
    '                }\n' +
    '                owner person1;\n' +
    '            }\n' +
    '        }\n' +
    ...
    ...
    ...
    '    }\n' +
    '}\n',
  groups: undefined
]
        0.16 real         0.05 user         0.02 sys
            23588864  maximum resident set size
                   0  average shared memory size
                   0  average unshared data size
                   0  average unshared stack size
                6175  page reclaims
                   3  page faults
                   0  swaps
                   0  block input operations
                   0  block output operations
                   0  messages sent
                   0  messages received
                   0  signals received
                  13  voluntary context switches
                 127  involuntary context switches
           252044844  instructions retired
           217955836  cycles elapsed
             9351168  peak memory footprint
</code></pre>

<p>20 saniye süren konfigürasyon ile çalıştırmama rağmen <code>0.16</code> saniye gibi bir sürede tamamladı işlemi. Aynı şeyi <code>regex101</code> üzerinden görmek isterseniz <a href="https://regex101.com/r/Rvltc9/1">buradan</a> test edebilirsiniz.
Ama ekran görüntüsünden anlaşılacağı gibi kullanılan pattern içerisinde gelen her şeyi eşleştir yerine <code>{}</code> karakterleri dışında her şeyi eşleştir diyerek aradığını bulmak için yaptığı deneme sayısını çok aza indirmiş ve böylece süreyi kısaltmış olduk.</p>

<p><img src="/img/hateregex/regex101-backtrack-opt.png" alt="Capture 2" /></p>

<h3 id="kazık-2---alternatif-2">Kazık 2 - Alternatif 2</h3>

<p>Peki kullanılan RegEx pattern ilk olduğu gibi, hiç optimize edilmeden tüm her şeyi bulacak şekilde kalsın ama yine de hızlı çalışsın dersek ne yapabiliriz? Hatırlarsanız bunun çok yavaş çalışmasının sebebi kullanılan RegEx motorunun yapısından ve kullanılan
algoritmalardan kaynaklanıyor demiştik, o zaman biz de motoru değiştirerek sorunu çözebiliriz, nasıl fikir?</p>

<p>Kodun ilk halini, alıp aşağıdaki gibi farklı bir RegEx motoru ile değiştirerek çalıştırıyoruz.</p>

<pre><code>const fs = require('fs');
var RE2 = require("re2");

const args = process.argv.slice(2);
if (args.length &lt; 1) {
    console.log('missing arguments');
    process.exit(1);
}
const configFile = args[0];
const config = fs.readFileSync(configFile, 'utf8');

const regex = /parent {[\s\S]*type.*[\s\S]*subtype.*[\s\S]*element.*[\s\S]*attributes.*[\s\S]*value testvalue.*[\s\S]*owner person1/;
var re = new RE2(regex);
const match = config.match(re);
console.log("Match:", match);
</code></pre>

<p>Kodu aşağıdaki gibi çalıştırdığımızda sonuç ne olacak bakalım.</p>

<pre><code>hate-regex &gt; /usr/bin/time -al node validate-config-alt-2.js settings2.conf
Match: [
  'parent {\n' +
    '    type {\n' +
    '        subtype TestSubType {\n' +
    '            element TestElement {\n' +
    '                attributes {\n' +
    '                    name testname;\n' +
    '                    description testdescription;\n' +
    '                    count 1234;\n' +
    '                    value testvalue;\n' +
    '                }\n' +
    '                owner person1',
  index: 0,
  input: 'parent {\n' +
    '    type {\n' +
    '        subtype TestSubType {\n' +
    '            element TestElement {\n' +
    '                attributes {\n' +
    '                    name testname;\n' +
    '                    description testdescription;\n' +
    '                    count 1234;\n' +
    '                    value testvalue;\n' +
    '                }\n' +
    '                owner person1;\n' +
    '            }\n' +
    '        }\n' +
    ...
    ...
    ...
    '    }\n' +
    '}\n',
  groups: undefined
]
        0.09 real         0.06 user         0.02 sys
            24272896  maximum resident set size
                   0  average shared memory size
                   0  average unshared data size
                   0  average unshared stack size
                6357  page reclaims
                   0  page faults
                   0  swaps
                   0  block input operations
                   0  block output operations
                   0  messages sent
                   0  messages received
                   0  signals received
                   7  voluntary context switches
                 152  involuntary context switches
           265322523  instructions retired
           219898560  cycles elapsed
             9580544  peak memory footprint
</code></pre>

<p>İlk kullandığımız pattern üzerinden herhangi bir değişiklik yapmadan, büyük
konfigürasyon dosyası ile çalıştığında bile <code>0.09</code> saniyede çalışmış, yani
NodeJs tarafında kullanılan varsayın RegEx motorunun 200 katı daha hızlı
çalışmış.</p>

<p>Yukarıdaki örneğin hızlı çalışmasının ana sebebi farklı bir algoritma ile geliştirilmiş ile kullanılan <a href="https://github.com/uhop/node-re2">RE2</a> motor kullanması, tabi 
neden NodeJs core kütüphanesinin bunu kullanmadığı gibi bir soru sorabilirsiniz, bunun da bazı sebepleri var ama başka bir yazı konusu olduğu için şimdilik geçiyorum.</p>

<h3 id="kazık-2---alternatif-3">Kazık 2 - Alternatif 3</h3>

<p>Buradaki alternatif önerim için aşağıya bir kod koymayacağım çünkü bazen <code>Regular Expressin</code> kullanmamak da çözümün ta kendisi olabiliyor, bir önceki yazıda da yöntem olarak buna değinmiştim.
Aslında bu tarz bir durumda eğer bunu işimin gereği olarak sık sık kontrol etmem gereken bir şey olarak görür isem, yukarıdaki konfigürasyon formatı için yazılmış bir <code>parser</code> kullanmak ve onun üzerinden
bu kontrolü yapmak çok daha iyi bir çözüm olacaktır. Bir nevi JSON formatında bir konfigürasyon dosyasında bulunan spesifik bir değeri <code>RegEx</code> ile de kullanarak kontrol edebilirim ama, tavsiyem burada
düzgün bir JSON parser kullanıp ilgili değeri programlama dilinde kontrol etmek olacaktır.</p>

<p>Parser genelde bu formata ve işe özel yazıldığı ve optimize edildiği için hem daha az hata olacaktır hem de daha hızlı çalışabilir.</p>

<h3 id="ders">Ders</h3>

<p>Belki çoğu kişi RegEx kullanırken performans etkisini, hatta basit bir <code>pattern</code> aramanın bütün sistemi cevap veremez hale getireceğini hiç düşünmeden kullanıyor ama görüldüğü gibi 
kullandığımız <code>pattern</code> bize gelen beklemediğimiz bir <code>input</code> oldukça fazla önem arz ediyor. Bu yüzden geliştirdiğimiz sistemlerde <code>RegEx</code> kullanırken, bu yanlarını da bilmeli, en kötü senaryoya göre test etmeli,
hatta kullandığımız programlama dilinin de ne tarz bir <code>RegEx</code> algoritması kullandığını bilmemiz gerekiyor ki bu tarz hataları yapmayalım. Bu hataları canlı sistemlerde bulmak oldukça zor ve can sıkıcı olabiliyor.</p>

<h3 id="sırada-ne-var">Sırada Ne Var?</h3>

<p>Vakit bulabilirsem, neden NodeJs RegEx motorunda yavaş çalıştığını, kullanılan algoritmanın etkisini, diğer dillerde durumun nasıl olduğunu bir örnek yaparak başka bir yazıda açıklamaya anlatmaya çalışacağım.
En eğlenceli kısmı bu olacak sanırım, öğrendiklerimizi pekiştirmek için ufak bir <code>RegEx</code> motoru yapabiliriz.</p>]]></content><author><name>Cihat Altuntaş</name></author><category term="regex" /><summary type="html"><![CDATA[I Hate Regular Expressions - 2]]></summary></entry><entry><title type="html">I Hate Regular Expressions - 1</title><link href="http://www.cihataltuntas.com/2025/04/14/i-hate-regex-1.html" rel="alternate" type="text/html" title="I Hate Regular Expressions - 1" /><published>2025-04-14T14:00:00+00:00</published><updated>2025-04-14T14:00:00+00:00</updated><id>http://www.cihataltuntas.com/2025/04/14/i-hate-regex-1</id><content type="html" xml:base="http://www.cihataltuntas.com/2025/04/14/i-hate-regex-1.html"><![CDATA[<p><img src="/img/hateregex/hateregex.png" alt="Capture 1" /></p>

<p>Bu yazı serisi şu ana kadar 1 bölümden oluşmaktadır, diğer bölümlere aşağıdaki linklerden ulaşılabilir. Yazı içeriğinde geçen kodlara
<a href="https://gist.github.com/caltuntas/c09ddd9e4297a0235924f65e6f72a4f6">bu linkten</a> ulaşabilirsiniz.</p>

<ol>
  <li><a href="https://www.cihataltuntas.com/2025/04/14/i-hate-regex-1">I Hate Regular Expressions - 1</a> (Bu Yazı)
    <ul>
      <li>Bu yazıda, string içeriği değiştirmede dikkatli olmadan kullanılan Regular
Expression yönteminin ne tarz sorunlara yol açabileceğini ve alternatif
olarak neler yapabileceğimizi inceliyoruz.</li>
    </ul>
  </li>
</ol>

<p>Hani insanın parmağının ucuna batan ufak bir diken ya da kıymık gibi bir şey olur da çıkaramadığınızda canınızı sıkar ve
hayat kalitenizi düşürür ya, benim için bu yazıyı yazmak öyle hissettiriyor. Yazdığım gün parmağımdan çıkarmış gibi rahatlayacağım
çünkü gelecekteki kendime ve bunu okuyacaklara bir uyarı niteliğinde olacak, ve neden ve hangi durumlarda <code>Regular Expression</code> kullanmamaları gerektiğini anlatmaya çalışacağım.</p>

<p>İnsan tecrübe etmeden sanırım bir kavramı anladığını düşünse de  gerçek anlamda deneyimleyip artısını eksisini görmeden tam olarak kavrayamıyor, <code>Regular Expression</code> kullanımı da
benim için böyle bir konuydu. Neredeyse 16 sene önce, 2009 yılında <a href="https://www.cihataltuntas.com/2009/03/16/i-love-regex.html">I love Regular Expressions</a> yazımda ona olan
sevgimi kaleme dökmüştüm ve bana kolaylık sağlayan bir kullanım durumunu anlatmıştım. Yıllar içinde de kullanım oranım artarak pik yaptı, nerede bir <code>string</code> arama, değiştirme
eşleştirme problemi olsa, elimde olan çekici yani <code>RexEx</code> kullanarak kafasına vurup geçiyordum.</p>

<p>Sonrasında ise RegEx sayesinde yediğim kazıkların boyu pardon tecrübem artınca kullanmadan önce defalarca düşün, alternatif ara, eğer gerçekten mecbursan çok iyi test ederek karar ver seviyesine geldi.</p>

<blockquote>
  <blockquote>
    <p>Some people, when confronted with a problem, think “I know, I’ll use regular expressions.”  Now they have two problems.</p>

    <p>Jamie Zawinski</p>
  </blockquote>
</blockquote>

<p>Jamie herhalde ben daha <code>Regular Expression</code> ifadesinin adını bile duymadan 90’lı yılların sonunda ne güzel ifade etmiş, yukarıdaki alıntıyı o yüzden hep çok sevmişimdir. Tarihçesini merak edenler <a href="https://regex.info/blog/2006-09-15/247">buraya</a>
bakabilirler.</p>

<p>Bu arada yukarıdaki resmi RegEx çok seven ve ondan dolayı <a href="https://ihateregex.io/">bu
siteyi</a> hazırlayan arkadaşın sitesinden aldım.  Uzun
bir giriş yaptıktan sonra sıra geldi yediğim bazı kazıkları örnekler ile
anlatmaya ki aynı hataları tekrar yapmayalım. Tabi geri dönüp bakınca burada
anlatması ve kaçınılması basit gibi gözüküyor, fakat canlı ortamda saatlerini
ve günlerinizi bu tarz hataları bulmakla geçirebiliyorsunuz.</p>

<h3 id="kazık-1">Kazık 1</h3>

<p>Herhalde 10 sene önce NodeJs kullanarak şöyle bir kod yazmışım, tabi bu kod bu kadar izole çalışmıyor yüz binlerce satır kod ile birlikte
belirli durumlara göre canlı ortamda tetikleniyor gibi düşünün.</p>

<pre><code>const placeHolderPattern = /#{(\w+)}/;
const allPlaceHoldersPattern = new RegExp(placeHolderPattern, 'g');
const args = process.argv.slice(2);

if (args.length &lt; 2) {
  console.log('missing arguments');
  process.exit(1);
}

const replacements = [
  { name: 'placeholder1', value: args[0] },
  { name: 'placeholder2', value: args[1] },
];

let output = 'some text template #{placeholder1} and #{placeholder2} and some more text #{placeholder1}';
let matches;

do {
  console.log('after replacement output=' + output);
  matches = allPlaceHoldersPattern.exec(output);
  if (matches) {
    const placeHolder = matches[1];
    const replacement = replacements.find(r =&gt; r.name === placeHolder);
    
    if (!replacement) {
      throw new Error(`Replacement for ${placeHolder} place holder does not exist.`);
    }
    
    output = output.replace(placeHolderPattern, replacement.value || '');
    allPlaceHoldersPattern.lastIndex = 0;
  }
} while (matches);

console.log(output);
</code></pre>

<p>Oldukça basit bir kod değil mi? Genel formatlama, kontroller gibi şeylere takılmayıp biraz kodu bir AI aracına sormadan inceleyin derim bakalım
sorunu bulabilecek misiniz?</p>

<p>Genel olarak kodun yapmaya çalıştığı şeyi şu şekilde anlatayım. Elimizde aşağıdaki gibi bir şablon var diye düşünün, bu bir e-mail şablonu ya da farklı amaçla kullanılacak bir şey olabilir.</p>

<pre><code>some text template #{placeholder1} and #{placeholder2} and some more text #{placeholder1}
</code></pre>

<p>Yukarıdaki <code>#{}</code> içinde olan değerler aslında değiştirilecek alanlar, kullanıcıdan gelen ya da sistem tarafından hesaplanan değerler çalışma sırasında bu yeri tutan <code>placeholder1 ve placeholder2</code> değerleri ile değiştirilip,
sonrasında bu belki kullanıcıya gösterme, mail atma ya da sistemde saklama amacıyla kullanılıyor. Aslında çok sıradan bir senaryo gibi.</p>

<p>Kodu şu şekilde çalıştıralım ve birkaç girdi ile ürettiği çıktıya bakalım, o zamana kadar siz de sorunu belki çoktan tespit etmiş olursunuz.</p>

<pre><code>code &gt; node code/regex-replace.js test1 test2
after replacement output=some text template #{placeholder1} and #{placeholder2} and some more text #{placeholder1}
after replacement output=some text template test1 and #{placeholder2} and some more text #{placeholder1}
after replacement output=some text template test1 and test2 and some more text #{placeholder1}
after replacement output=some text template test1 and test2 and some more text test1
some text template test1 and test2 and some more text test1
</code></pre>

<p>Üretilen çıktı da aslında kodun ne yaptığını gösteriyor, yer tutucu olarak kullandığımız <code>placeholder</code> değerlerinin hepsini değiştirmek için bir <code>do-while</code> döngüsü içinde
her eşleşmede sıfırlama yapıp bir sonrakini değiştiriyor. Javascript dokümanına bakınca aşağıdaki gibi açıklamış</p>

<pre><code>... the replacement can be a string or a function called for each match. If pattern is a string, only the first occurrence will be replaced. The original string is left unchanged.
</code></pre>

<p>Şimdi bazı arkadaşların neden <code>replaceAll</code> kullanmadınız, ne gerek var kulağı tersten göstermeye dediğini duyar gibiyim, sebebi <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replaceAll">replaceAll</a> 
fonksiyonu <strong>Node</strong> ortamında 2020 yılında versiyon <strong>15</strong> sonrasında geldi, bu kod geliştirildiğinde öyle bir fonksiyonumuz yoktu maalesef. Dikiz aynasından bakınca
farklı alternatifler mutlaka bulunabilir ama o tarihte bir nevi kendi <strong>replaceAll</strong> fonksiyonumuzu yazmışız diyebilirim. Mutlaka kod daha iyi yazılabilirdi, hatta asıl soruna odaklandıktan sonra
nasıl yazabilirdik onlara da değinmeye çalışacağım fakat şimdilik fazla uzatmadan soruna odaklanalım.</p>

<p>Sorunu kendi başınıza bulduysanız tebrikler, canlı ortamında sorunu yaşayıp tatsız bir hata ayıklama süreci sonucunda ancak tespit edebilmiştim. Ama benim gibi bulamayanlar için kodu bir de aşağıdaki gibi çağırıp neler olduğuna bakalım.</p>

<pre><code>code &gt; node code/regex-replace.js test1 '668%X6g$&amp;8kNUkpZVvb'
after replacement output=some text template #{placeholder1} and #{placeholder2} and some more text #{placeholder1}
after replacement output=some text template test1 and #{placeholder2} and some more text #{placeholder1}
after replacement output=some text template test1 and 668%X6g#{placeholder2}8kNUkpZVvb and some more text #{placeholder1}
after replacement output=some text template test1 and 668%X6g668%X6g#{placeholder2}8kNUkpZVvb8kNUkpZVvb and some more text #{placeholder1}
after replacement output=some text template test1 and 668%X6g668%X6g668%X6g#{placeholder2}8kNUkpZVvb8kNUkpZVvb8kNUkpZVvb and some more text #{placeholder1}
after replacement output=some text template test1 and 668%X6g668%X6g668%X6g668%X6g#{placeholder2}8kNUkpZVvb8kNUkpZVvb8kNUkpZVvb8kNUkpZVvb and some more text #{placeholder1}
after replacement output=some text template test1 and 668%X6g668%X6g668%X6g668%X6g668%X6g#{placeholder2}8kNUkpZVvb8kNUkpZVvb8kNUkpZVvb8kNUkpZVvb8kNUkpZVvb and some more text #{placeholder1}
after replacement output=some text template test1 and 668%X6g668%X6g668%X6g668%X6g668%X6g668%X6g#{placeholder2}8kNUkpZVvb8kNUkpZVvb8kNUkpZVvb8kNUkpZVvb8kNUkpZVvb8kNUkpZVvb and some more text #{placeholder1}
after replacement output=some text template test1 and 668%X6g668%X6g668%X6g668%X6g668%X6g668%X6g668%X6g#{placeholder2}8kNUkpZVvb8kNUkpZVvb8kNUkpZVvb8kNUkpZVvb8kNUkpZVvb8kNUkpZVvb8kNUkpZVvb and some more text #{placeholder1}
after replacement output=some text template test1 and 668%X6g668%X6g668%X6g668%X6g668%X6g668%X6g668%X6g668%X6g#{placeholder2}8kNUkpZVvb8kNUkpZVvb8kNUkpZVvb8kNUkpZVvb8kNUkpZVvb8kNUkpZVvb8kNUkpZVvb8kNUkpZVvb and some more text #{placeholder1}
after replacement output=some text template test1 and 668%X6g668%X6g668%X6g668%X6g668%X6g668%X6g668%X6g668%X6g668%X6g#{placeholder2}8kNUkpZVvb8kNUkpZVvb8kNUkpZVvb8kNUkpZVvb8kNUkpZVvb8kNUkpZVvb8kNUkpZVvb8kNUkpZVvb8kNUkpZVvb and some more text #{placeholder1}
...
...
</code></pre>

<p>Kendi ortamınızda denerken bir eliniz Control+C tuşunda olsun aksi durumda hızlıca sonsuza doğru kayan yazıları görünce başınız dönebilir. Peki kodu patlatan <code>668%X6g$&amp;8kNUkpZVvb</code> parametresinin sorunu nedir?
Mozilla dokümanına bakarsanız, benim gözden kaçırdığım, hiç aklıma da gelmeyen <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#specifying_a_string_as_the_replacement">bu kısma</a>
göz atabilirsiniz.</p>

<pre><code>$&amp;  Inserts the matched substring.
...
...
$n and $&lt;Name&gt; are only available if the pattern argument is a RegExp object. If the pattern is a string, or if the corresponding capturing group isn't present in the regex, then the pattern will be replaced as a literal
</code></pre>

<p>Sistem <code>$&amp;</code> karakterlerini gördüğünde onları RegEx için özel karakterler olarak algılayıp, yerlerine eşleşen yani <strong>match</strong> olan ifadeyi yerleştiriyor. Yani biz ilk değiştirmesi gereken ve eşleşen <code>#{placeholder2}</code> yerine <code>668%X6g$&amp;8kNUkpZVvb</code> 
koymasını beklerken, bizim kontrolümüz dışında gelen değer içerisindeki özel karakterler yüzünden oraya <code>668%X6g#{placeholder2}8kNUkpZVvb</code> koyuyor ve değişim işi hiç bitmediğinden dolayı bu sonsuza kadar devam ediyor, geçmiş olsun ve canlı 
sistemde sorunu bulmakla uğraşacak arkadaşa da kolay gelsin diyelim.</p>

<h3 id="kazık-1---alternatif-1">Kazık 1 - Alternatif 1</h3>

<p>Hatalarımızdan ders çıkarıp kodu bugün nasıl yazardık ya da bu sorunla uğraşmamak için nasıl yazmalıydık onlara değinelim.</p>

<pre><code>const placeHolderPattern = /#{(\w+)}/g;
const args = process.argv.slice(2);

if (args.length &lt; 2) {
  console.log('missing arguments');
  process.exit(1);
}

const replacements = [
  { name: 'placeholder1', value: args[0] },
  { name: 'placeholder2', value: args[1] },
];

function replacer(match, placeHolderName) { 
  const replacement = replacements.find(r =&gt; r.name === placeHolderName);
  if (!replacement) {
    throw new Error(`Replacement for ${placeHolder} place holder does not exist.`);
  }
  return replacement.value || '';
} 

let output = 'some text template #{placeholder1} and #{placeholder2} and some more text #{placeholder1}';
output = output.replace(placeHolderPattern, replacer);
console.log(output);
</code></pre>

<p>Daha önce çok dikkate almadığımız ama eğer, RegEx pattern yerine bir fonksiyon gönderirseniz her eşleşme için bu fonksiyon çağrılıp dönen değer ile değiştirilir yazıyordu, yukarıdaki kod parçasında bu yöntemi kullandık. Çalıştıralım ve sonucu görelim.</p>

<pre><code>code &gt; node code/regex-replace-alt-1.js test1 '668%X6g$&amp;8kNUkpZVvb'
some text template test1 and 668%X6g$&amp;8kNUkpZVvb and some more text test1
</code></pre>

<h3 id="kazık-1---alternatif-2">Kazık 1 - Alternatif 2</h3>

<p>Sonuç düzgün çalıştı, bu sefer özel karakter olsa da, bir sorun olmadı çünkü bu yöntem ile onları dikkate almıyor. Peki başka ne yapabilirdik? Günümüzde olsak bunu <strong>replaceAll</strong> kullanarak daha da kolay yazabilirdik diye düşünüyorum, deneyelim.</p>

<pre><code>const args = process.argv.slice(2);

if (args.length &lt; 2) {
  console.log('missing arguments');
  process.exit(1);
}

const replacements = [
  { name: 'placeholder1', value: args[0] },
  { name: 'placeholder2', value: args[1] },
];

let output = 'some text template #{placeholder1} and #{placeholder2} and some more text #{placeholder1}';
replacements.forEach((item) =&gt; {
  output = output.replaceAll(`#{${item.name}}`, item.value || '');
}
);

console.log(output);
</code></pre>

<p>Bu sefer elimizde, yeni NodeJs versiyonu var ve <strong>replaceAll</strong> işimizi daha da kolaylaştırdı. Regex kullanmaya bile gerek kalmadı, basit mantık elimizde değiştirilmesi gereken tüm değerlerin üzerinden geçip
<strong>replaceAll</strong> ile değiştirme işlemini yapıyoruz. Tekrar deneyelim ve çalıştığını görelim.</p>

<pre><code>code &gt; node code/regex-replace-alt-2.js test1 '668%X6g$&amp;8kNUkpZVvb'
some text template test1 and 668%X6g$&amp;8kNUkpZVvb and some more text test1
</code></pre>

<h3 id="kazık-1---alternatif-3">Kazık 1 - Alternatif 3</h3>

<p>Peki geçmişe gidip tekrar yazacak olsam elimde <strong>replaceAll</strong> olmasaydı ne yapardım? Hiç RegEx falan uğraşmadan bildiğimiz döngü içerisinde her değiştirilmesi gereken
değeri değiştirip kalmayana kadar devam ederdim. Yani kod aşağıdaki gibi olurdu.</p>

<pre><code>const args = process.argv.slice(2);

if (args.length &lt; 2) {
  console.log('missing arguments');
  process.exit(1);
}

const replacements = [
  { name: 'placeholder1', value: args[0] },
  { name: 'placeholder2', value: args[1] },
];

let output = 'some text template #{placeholder1} and #{placeholder2} and some more text #{placeholder1}';
replacements.forEach((item) =&gt; {
  const placeholder = `#{${item.name}}`;
  while (output.includes(placeholder)) {
    output = output.replace(placeholder, ()=&gt;item.value || '');
  }
}
);

console.log(output);
</code></pre>

<p>Denediğimizde yine düzgün çalıştı.</p>

<pre><code>code &gt; node code/regex-replace-alt-3.js test1 '668%X6g$&amp;8kNUkpZVvb'
some text template test1 and 668%X6g$&amp;8kNUkpZVvb and some more text test1
</code></pre>

<p>Burada sinir bozucu olan şey, varsayılan olarak ben A değerini B değeri ile
değiştirmek istediğinizde RegEx kullanmasanız bile Javascript değiştirilmesi
gereken B içerisinde eğer özel bir karakter görürse, bunları yine yorumlamaya
çalışıyor.  Aşağıdaki çıktıya dikkat edin, herhangi bir RegEx kullanmadım</p>

<pre><code>code &gt; node --eval 'console.log("deneme cihat yanilma".replace("cihat", "aaa$&amp;bbb"));'
deneme aaacihatbbb yanilma
</code></pre>

<p>Bunun sebebi replacement olarak <code>$&amp;</code> gördüğünde yine bunu özel karakter olarak yorumluyor. Bunu önüne geçmek istiyorsanız ya <code>$</code> karakterlerini escape etmeniz gerekiyor ya da yukarıda ve aşağıda yaptığım gibi replacement değerini 
fonksiyon olarak vermeniz gerekiyor.</p>

<pre><code>code &gt; node --eval 'console.log("deneme cihat yanilma".replace("cihat", ()=&gt;"aaa$&amp;bbb"));'
deneme aaa$&amp;bbb yanilma
</code></pre>

<h3 id="ders">Ders</h3>

<p>Kendi adıma yukarıdaki yaşadığım problemden çıkardığım dersi özetleyecek olur isem, RegEx kullanmadan önce bunu standart <code>string</code> işlemleri ile yapabiliyor muyum 
ona bakarım, hatta standart RegEx kullanmadan bazen bir kaç satır data fazla kod yazmak bile RegEx kullanmaktan uzun vadede daha avantajlı olabilir. Kısacası gerçekten ihtiyaç olduğunda,
artılarını eksilerini değerlendirerek kullanmak, hatta mümkünse daha basit çözümler varsa onlarla ilerlemeyi tavsiye ederim.</p>

<p>Başlık biraz <code>Clickbait</code> gibi durabilir, tabi gerçekte herhangi bir teknolojiden nefret etmek ya da ona aşık olmak gibi bir yaklaşım pek mantılı değil fakat yıllar önceki eski yazıma ithafen 
böyle bir başlık seçmeyi istedim.</p>

<p>RegEx sebebiyle yediğimiz kazıklar tabi bitmedi, bu yazı biraz uzun olduğu için burada bitirelim. Sonraki yazıda farklı bir probleme değineceğim, hatta çoğu kişinin farkında olmadan 
çok fazla karşılaştığı bir sorun olduğunu düşünüyorum.</p>]]></content><author><name>Cihat Altuntaş</name></author><category term="regex" /><summary type="html"><![CDATA[I Hate Regular Expressions - 1]]></summary></entry><entry><title type="html">SSH Trafiğini Çözümleyelim 2 - Wireshark</title><link href="http://www.cihataltuntas.com/2025/04/10/decrypt-ssh-traffic-2.html" rel="alternate" type="text/html" title="SSH Trafiğini Çözümleyelim 2 - Wireshark" /><published>2025-04-10T14:00:00+00:00</published><updated>2025-04-10T14:00:00+00:00</updated><id>http://www.cihataltuntas.com/2025/04/10/decrypt-ssh-traffic-2</id><content type="html" xml:base="http://www.cihataltuntas.com/2025/04/10/decrypt-ssh-traffic-2.html"><![CDATA[<p>Bu yazı serisi şu ana kadar 2 bölümden oluşmaktadır, diğer bölümlere aşağıdaki linklerden ulaşılabilir. Yazı içeriğinde geçen kodlara
<a href="https://github.com/caltuntas/ssh-decryption">bu linkten</a> ulaşabilirsiniz.</p>

<ol>
  <li><a href="https://www.cihataltuntas.com/2025/02/04/decrypt-ssh-traffic-1">SSH Trafiğini Çözümleyelim 1 - Patch</a>
    <ul>
      <li>Bu yazıda, genel olarak SSH protokolünün yapısı ve şifreleme için
kullanılan anahtar değişim algoritmalarının nasıl çalıştığı inceliyoruz.
Ardından var olan bir SSH kütüphanesinin kodu değiştirilerek ele geçirilen
şifreleme anahtarlarını kendi yazdığımız kod ile kaydedilmiş bir trafiği
çözümlemek için kullanıyoruz.</li>
    </ul>
  </li>
  <li><a href="https://www.cihataltuntas.com/2025/04/10/decrypt-ssh-traffic-2">SSH Trafiğini Çözümleyelim 2 - Wireshark</a> (Bu yazı)
    <ul>
      <li>Bu yazıda, Wireshark kullanarak trafiği çözümlemek istediğimizde
karşılaştığımız sorunu hata ayıklaması yaparak tespit ediyoruz, sonrasında
da Wireshark kodunu düzelterek, trafiği Wireshark üzerinde de
çözümlüyoruz.</li>
    </ul>
  </li>
</ol>

<p>SSH trafiğini çözümleme yolculuğumuzda, <a href="https://www.cihataltuntas.com/2025/02/04/decrypt-ssh-traffic-1.html">bir önceki</a> yazımızda
ilk olarak <code>Wireshark</code> ile trafiği çözümlemeye çalışmış fakat başarılı olamamış ardından kendi geliştirdiğimiz NodeJs
kodu ile trafiği çözümleyip gönderilen kullanıcı ve şifre bilgilerini alabilmiştik. Tabi bunu yaparken biraz kaçak güreşip,
pratikte elimizde gizli ya da paylaşılan anahtar bilgileri olmadan trafiği çözümleme mümkün olmadığından, kullandığımız SSH kütüphanesi üzerinde bazı değişiklikler yapıp
gizli, paylaşılan anahtar ve diğer tüm gerekli bilgileri ekrana yazdırmış ardından da geliştirdiğimiz kod içinde bunları kullanarak trafiği çözümleyip aradığımız bilgileri yakalamıştık.</p>

<p>Her seferinde bu tarz kod ile trafiği çözümlemek çok pratik olmadığından bu yazıda, Wireshark ile neden bilgileri göremedik ya da görmek için neler yapmamız lazım onu inceleyelim.</p>

<p>Öncelikle bir önceki yazıdan SSH trafiğini yakalayıp Wireshark ile açtığımızda nasıl görünüyor onu hatırlayalım.</p>

<p><img src="/img/sshdecrypt/wireshark-ssh-enc.png" alt="Capture 1" /></p>

<p>Resimde görüldüğü üzere, ilk SSH protokol el sıkışması için kullanılan ilk 4-5 paket dışında diğer tüm paketler <code>encrypted</code> olarak işaretlenmiş içlerini açtığınızda da anlamlı bir veri görmek mümkün değil.
Zaten şifrelenmiş bir trafik olduğundan bunun Wireshark tarafından otomatik olarak çözülmesini de beklemiyoruz, mutlaka ona gizli ya da paylaşılan anahtar değerini bir şekilde vermemiz gerekiyor ki trafiği çözümleyebilsin.</p>

<p>Wireshark SSH <a href="https://wiki.wireshark.org/SSH">dökümanına</a> baktığımızda aşağıdaki gibi ifade karşımıza çıkıyor.</p>

<blockquote>
  <p>The SSH dissector in Wireshark is functional, dissecting most of the
connection setup packets which are not encrypted.  Unlike the TLS dissector, no
code has been written to decrypt encrypted SSH packets/payload (yet). This is
also not possible unless the shared secret (from the Diffie-Hellman key
exchange) is extracted from the SSH server or client (see, as an example of a
mechanism to extract internal information of that sort, the “SSLKEYLOGFILE”
method in TLS). Work on SSH2 decryption is tracked at
https://bugs.wireshark.org/bugzilla/show_bug.cgi?id=16054</p>
</blockquote>

<p>Döküman pek açıklayıcı diyemem, ama biraz ipucu vermiş, tahmin ettiğimiz gibi paylaşılan anahtarı TLS çözümleme benzeri bir yöntem ile 
verirseniz çözülebilir diye ifade etmiş. Daha önce TLS çözümleme için Wireshark kullandığımdan oradaki yöntemin nasıl çalıştığını biliyordum, benzer şekilde
SSH protokol ayarlarına girince aşağıdaki gibi bahsettiği anahtarı verebileceğimiz bir alan görebiliyoruz.</p>

<p><img src="/img/sshdecrypt/wireshark-ssh-pref.png" alt="Capture 1" /></p>

<p>Güzel, en azından anahtarı nasıl verebiliriz bunu bulduk, fakat bu dosyaya girebileceğimiz verinin formatı hala bilinmiyor, bunun için maalesef bir doküman bulamadığımdan en güzel doküman yani kaynak koda başvurmak zorunda kaldım.</p>

<p>Kaynak kod içinde SSH paketinden sorumlu olan <a href="https://gitlab.com/wireshark/wireshark/-/blob/master/epan/dissectors/packet-ssh.c">bu koda</a> bakıp biraz incelediğimizde,
<code>keylog</code> dosyası ile ilgilenen fonksiyonu <code>ssh_keylog_read_file</code> görebiliyoruz. Bu fonksiyonun içinde geliştiren arkadaş sağolsun yorum satırı olarak ne beklendiğini aşağıdaki gibi belirtmiş.</p>

<pre><code> /* File format: each line follows the format "&lt;cookie&gt; &lt;type&gt; &lt;key&gt;".
  * &lt;cookie&gt; is the hex-encoded (client or server) 16 bytes cookie
  * (32 characters) found in the SSH_MSG_KEXINIT of the endpoint whose
  * private random is disclosed.
  * &lt;type&gt; is either SHARED_SECRET or PRIVATE_KEY depending on the
  * type of key provided. PRIVAT_KEY is only supported for DH,
  * DH group exchange, and ECDH (including Curve25519) key exchanges.
  * &lt;key&gt; is the private random number that is used to generate the DH
  * negotiation (length depends on algorithm). In RFC4253 it is called
  * x for the client and y for the server.
  * For openssh and DH group exchange, it can be retrieved using
  * DH_get0_key(kex-&gt;dh, NULL, &amp;server_random)
  * for groupN in file kexdh.c function kex_dh_compute_key
  * for custom group in file kexgexs.c function input_kex_dh_gex_init
  * For openssh and curve25519, it can be found in function kex_c25519_enc
  * in variable server_key. One may also provide the shared secret
  * directly if &lt;type&gt; is set to SHARED_SECRET.
  *
  * Example:
  *  90d886612f9c35903db5bb30d11f23c2 PRIVATE_KEY DEF830C22F6C927E31972FFB20B46C96D0A5F2D5E7BE5A3A8804D6BFC431619ED10AF589EEDFF4750DEA00EFD7AFDB814B6F3528729692B1F2482041521AE9DC
  */
</code></pre>

<p>Tam aradığımız bilgi diyebiliriz, gizli ya da özel anahtarı bu formatta hazırlayıp bir dosyaya koyup ayarlar ekranından ilgili dosyayı belirtmemiz gerekiyor.
Tabi önce bir SSH oturumu oluşturup, gizli ve özel anahtar değerlerini yakalamamız lazım, önceki yazıdan hatırlarsanız bu işi yaparken ilgili anahtar değerlerini ekrana yazan bir <code>patch</code> hazırlamıştık.
SSH oturumu içinde bir Linux sunucuya bağlanıp basit bir <code>ls</code> komutu çalıştıran kod da hazırlamıştık. Kodumuz aşağıdaki gibiydi.</p>

<pre><code>const { Client } = require('ssh2');

const algorithms = {
	kex: [ 'diffie-hellman-group1-sha1' ],
	cipher: [ 'aes128-ctr' ],
	hmac: [ 'hmac-sha2-256' ],
  };

const options = {};
options.algorithms = algorithms;

const conn = new Client(options);
conn.on('ready', () =&gt; {
	conn.exec('cd /tmp &amp;&amp; ls -lah', { pty: false }, (err, stream) =&gt; {
		if (err) throw err;
		stream.on('close', function close(code, signal) {
			console.log('Stream :: close :: code: ' + code + ', signal: ' + signal);
			conn.end();
		});
		stream.on('data', function out(data) {
			console.log('STDOUT: ' + data);
		});
		stream.stderr.on('data', function err(data) {
			console.log('STDERR: ' + data);
		});
	})})
	.connect({
		host: process.env.TARGET_HOST,
		port: 22,
		algorithms: algorithms,
		username: process.env.TARGET_USERNAME,
		password: process.env.TARGET_PASSWORD,
		debug: function(msg) {
			console.log(msg);
		},
	});
</code></pre>

<h2 id="test">Test</h2>

<p>Yukarıdaki kodu çalıştırınca ilk sorunumuz ortaya aşağıdaki gibi çıktı, en azından benim kendi local ortamımda. Farklı bir SSH versiyonunda konfigürasyonunda daha farklı sonuçlar almak mümkün bunu birazdan açıklayacağım.</p>

<pre><code>...
Handshake: (local) KEX method: diffie-hellman-group1-sha1
Handshake: (remote) KEX method: curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group14-sha256
Handshake: No matching key exchange algorithm
...
...
Error: Handshake failed: no matching key exchange algorithm
</code></pre>

<p>Ben bu testi evdeki Debian 11 sunucuya bağlanmaya çalışarak test ettim, yukarıdaki mesajları biraz daha yakından okursak hata sebebini anlayabiliriz. SSH el sıkışma sürecinde <code>local</code> yani yukarıdaki kod parçası
<code>KEX</code> algoritması olarak <code>diffie-hellman-group1-sha1</code> önermiş ama <code>remote</code> yani Debian sunucumuz <code>curve25519-sha256</code> ile başlayarak farklı bir çok algoritma önermiş ama aralarında bizim istemci olarak kullanmak istediğimiz
<code>diffie-hellman-group1-sha1</code> algoritması bulunmuyor. Bu nedenle <strong>no matching key exchange algorithm</strong> hatasını almışız.</p>

<p>Biraz araştırınca hatanın sebebinin <a href="https://www.openssh.com/legacy.html">burada</a> açıklandığı görebiliriz.</p>

<pre><code>Unable to negotiate with legacyhost: no matching key exchange method found.
Their offer: diffie-hellman-group1-sha1
</code></pre>
<blockquote>
  <p>In this case, the client and server were unable to agree on the key exchange
algorithm. The server offered only a single method
diffie-hellman-group1-sha1. OpenSSH supports this method, but does not enable
it by default because it is weak and within theoretical range of the
so-called Logjam attack.</p>
</blockquote>

<p>Kısaca güvenlik açısından zayıf ve ataklara maruz kalabilecek bu algoritma yeni
versiyon SSH sunucularda <code>disable</code> olarak geliyor. Bunu tekrar aktif etme
yöntemini ilgili sayfada açıklamış, şimdilik burayı geçip şuanda desteklenen ve
sunucu tarafından önerilen algoritmalardan biri olan
<code>diffie-hellman-group16-sha512</code> biri ile yola devam edelim.</p>

<h2 id="algoritma-değişimi">Algoritma Değişimi</h2>

<p>Algoritmayı aşağıdaki kısımdaki gibi önerilenlerden biri yapıp tekrar deniyorum.</p>

<pre><code>const algorithms = {
	kex: [ 'diffie-hellman-group16-sha512' ],
	cipher: [ 'aes128-ctr' ],
	hmac: [ 'hmac-sha2-256' ],
  };
</code></pre>

<p>Kodu çalıştırdıktan sonra bu sefer bir hata almadık sunucudan başarılı şekilde cevabı alabildik. Ekranda da trafik çözümlemesi için Wireshark <code>keylog</code> dosyası için aradığımız değerler çıktı.</p>

<pre><code>...
...
Handshake: (local) randomFillSync: 8ee195701163311362f4c0c8ccae7c55
..
...
SECRET: 000002006b61880c960d953382077c4f695cda7187e31aed23a28679fff4e81f7242939401ec18cf607e02c6601e4947fe3eecc0d5d66b12b00da50a7ca481b53a521d9b11b358943b1e04884933194d0bffdfc5f894ee414072b1cd9e35c3785ff57a507c748101877930bc29bb9cc135deccbb0e85d365cff4fda25eae411b6fc91eaef2826397a990a93504f9d418a601ecaa5c285f221a0399a8217aa4c923f5c91f51ad81633b8601ab680b4423f459c89f3790fa6a075d6f0478519d3ab9ddd1ef2316cee47d4708d1bd3e3207675adc9bf1d71368fe59e358f8f430da4ffc8794da39b572f03fae56526bf98173ebc775ba6a754545a6790fe3c66ad77fef49c33b96c9bb8a7538dfd5e5eb3cd432a5cd98a78f0d1f9f943117ec2526f01d1ea27f992c70b1a40dead1789a03c6c542bdddc4e843041759e2cc502c2171b8dba713a78bc30afc85ed0b3e99699407fb8794187772154edd68217bd8f73684d7e4a62ebe750030919cb3d8644d85c9347baee12f1e589edea2ed6a0d1e0cf8737577a3b255899ffd7c72d5a7d4b51b2a6cd1597f9b68c7ea41b95ba8b5e475892bb6bdba8ae6878f72e5b1ba7e628de2b318c353d592d5c12fd1a520d4a1e794376e63e86e1d41162f83fc06cf72ca7c3222206cc1264f2ec5b4819aafd0e442bee4dde92eb46e9f96fcc3d2151615376a72df47baa9850634df69450a16c10c86
...
...
</code></pre>

<p>SSH <a href="https://datatracker.ietf.org/doc/html/rfc4253#section-7.1">Transport Layer</a> dokümanına bakarsak orada bu değerlerden <code>cookie</code> olanın özellikleri belirtilmiş.</p>

<pre><code>...
byte         SSH_MSG_KEXINIT
byte[16]     cookie (random bytes)
...
</code></pre>

<blockquote>
  <p>The ‘cookie’ MUST be a random value generated by the sender.  Its purpose is
to make it impossible for either side to fully determine the keys and the
session identifier.</p>
</blockquote>

<p>Yukarıda yakaladığımız <code>randomFillSync</code> değeri aslında tam olarak bunu yapıyor, yani <code>cookie</code> değerini bulduk. Bu değeri ayrıca Wireshark üzerinden yakaladığımız paketlerin içinde de görebiliriz.</p>

<p><img src="/img/sshdecrypt/wireshark-cookie.png" alt="Capture 1" /></p>

<p>Diğer <code>SECRET</code> olarak yakaladığımız değer de paylaşılan anahtar fakat burada bizim ekrana yazdırdığımız değer ile Wireshark’ın beklediği değer arasında ufak bir fark var, ekranda yazılan 
değer <code>length+value</code> olarak encode edilmiş Wireshark sadece <code>value</code> kısmını beklediği için başındaki 4 byte yer kaplayan <code>00000200</code> değerini çıkarıp kalanı onun istediği formatta <code>keylog</code> dosyası olarak kaydediyoruz
ve dosya içeriği aşağıdaki gibi oluyor.</p>

<pre><code>8ee195701163311362f4c0c8ccae7c55 SHARED_SECRET 6b61880c960d953382077c4f695cda7187e31aed23a28679fff4e81f7242939401ec18cf607e02c6601e4947fe3eecc0d5d66b12b00da50a7ca481b53a521d9b11b358943b1e04884933194d0bffdfc5f894ee414072b1cd9e35c3785ff57a507c748101877930bc29bb9cc135deccbb0e85d365cff4fda25eae411b6fc91eaef2826397a990a93504f9d418a601ecaa5c285f221a0399a8217aa4c923f5c91f51ad81633b8601ab680b4423f459c89f3790fa6a075d6f0478519d3ab9ddd1ef2316cee47d4708d1bd3e3207675adc9bf1d71368fe59e358f8f430da4ffc8794da39b572f03fae56526bf98173ebc775ba6a754545a6790fe3c66ad77fef49c33b96c9bb8a7538dfd5e5eb3cd432a5cd98a78f0d1f9f943117ec2526f01d1ea27f992c70b1a40dead1789a03c6c542bdddc4e843041759e2cc502c2171b8dba713a78bc30afc85ed0b3e99699407fb8794187772154edd68217bd8f73684d7e4a62ebe750030919cb3d8644d85c9347baee12f1e589edea2ed6a0d1e0cf8737577a3b255899ffd7c72d5a7d4b51b2a6cd1597f9b68c7ea41b95ba8b5e475892bb6bdba8ae6878f72e5b1ba7e628de2b318c353d592d5c12fd1a520d4a1e794376e63e86e1d41162f83fc06cf72ca7c3222206cc1264f2ec5b4819aafd0e442bee4dde92eb46e9f96fcc3d2151615376a72df47baa9850634df69450a16c10c86
</code></pre>

<p>Wireshark üzerinden ayarlar menüsüne girerek tekrar oluşturduğumuz key dosyasını ve çözümleme yaparken oluşturacağı <code>debug</code> çıktısını kaydedeceği dosyayı giriyoruz.</p>

<p><img src="/img/sshdecrypt/wireshark-ssh-pref-key.png" alt="Capture 1" /></p>

<p>Wireshark restart edildikten sonra kaydettiğim paketi tekrar açıyorum, ama maalesef hala aynı şekilde bütün paketler <code>encrypted</code> olarak gözükmeye devam ediyor.</p>

<p><img src="/img/sshdecrypt/wireshark-ssh-enc1.png" alt="Capture 1" /></p>

<p>Evet buraya kadar her şey güzeldi fakat bundan sonra saç baş yolduran kısmı başlıyor, beklediğimiz gibi olmadı, bütün adımları uygun yaptım istediği değerleri bulup sağladım derken değişen bir şey olmadı ayrıca debug çıktıları
açılmasına rağmen neden çözümleme yapamadığına dair bir log da üretmedi. Wireshark SSH <a href="https://gitlab.com/wireshark/wireshark/-/blob/master/epan/dissectors/packet-ssh.c">koduna</a> baktığımda oldukça fazla log cümlesi görüyorum,
fakat Wireshark’ı loglama seviyesi <code>debug, noisy</code> olarak başlatsam bile bu logların herhangi biri ne dosyada ne de ekranda gözükmüyor. En azından aşağıdaki satırların birine düşmesi kesin diye düşünüp böyle loglar bekliyordum.</p>

<pre><code>static void
ssh_keylog_process_line(const char *line)
{
    ws_noisy("ssh: process line: %s", line);
    ...
    ...
</code></pre>

<h2 id="wireshark-derliyoruz">Wireshark Derliyoruz</h2>

<p>Biraz dokümanları biraz da kodu kurcaladıktan sonra <a href="https://www.wireshark.org/docs/wsdg_html_chunked/ChSrcDebug.html">burada</a> şöyle bir ifade ile karşılaştım.</p>

<blockquote>
  <p>Full debug logs can be invaluable to investigate any issues with the code. By
default debug level logs are only enabled with Debug build type. You can
enable full debug logs and extra debugging code by configuring the
ENABLE_DEBUG CMake option. This in turn will define the macro symbol WS_DEBUG
and enable the full range of debugging code in Wireshark.</p>
</blockquote>

<p>Yani o kodda gördüğümüz <code>ws_noisy</code> ya da <code>ws_debug</code> sadece <code>DEBUG</code> build yapıldığında aktif oluyor, en azından ben başka bir yöntem bulamadım ve derleme işlemi için <a href="https://www.wireshark.org/docs/wsdg_html_chunked/ChapterSetup.html#ChSetupUNIX">bu dokümana</a>
bakarak bağımlılıkları yüklemeye başladım.</p>

<p>Herhalde C programlama dilini çok sevsem de projeleri derlemesi en nefret ettiğim yanlarından biri kabul ediyorum, modern diller gibi size otomatik bir paket yönetimi sağlamıyor
ve bu aşamada oldukça uğraşabiliyorsunuz ki bende de öyle oldu. Özellikle <code>QT</code> kütüphane bağımlılıklarını sağlamak baya uğraştırdı, çok fazla hata çıktı o yüzden sadece komut satırında çalışan versiyonu olan
<code>Tshark</code> aracını derlemeye karar verdim. Arayüz ile değil de komut satırından SSH trafiğini verip yine aynı şeyi yaptırıyorsunuz, aslında GUI versiyonu da aynı kodu çağırıyor, CLI versiyonu da bundan dolayı işi daha fazla uzatmamak için CLI versiyonu ile devam etme
kararı aldım. Gerekli bağımlılıklar, hatalar ve benzeri sorunları hallettikten sonra gün sonunda ve bu şekilde <code>DEBUG</code> derleme işlemi yapabildim.</p>

<pre><code>cmake -DCMAKE_BUILD_TYPE=Debug -DBUILD_wireshark=OFF ../
</code></pre>

<p>Ardından aşağıdaki gibi komut satırından tekrar çalıştırıyoruz ve sonuçları inceliyoruz.</p>

<pre><code>caltuntas@debian11:~/Projects/wireshark/build/run$ ./tshark  --log-level=noisy -o ssh.debug_file:./sshdebug.log -o ssh.keylog_file:/home/caltuntas/keys-dh-g16-512.txt -r /home/caltuntas/ssh-dh-g16.pcapng

 ** (tshark:51929) 01:51:38.173775 [packet-ssh NOISY] epan/dissectors/packet-ssh.c:947 -- ssh_dissect_ssh2(): ....ssh_dissect_ssh2[S]: frame_key_start=8, pinfo-&gt;num=14, frame_key_end=0, offset=0, frame_key_end_offset=0
 ** (tshark:51929) 01:51:38.199013 [packet-ssh NOISY] epan/dissectors/packet-ssh.c:2110 -- ssh_keylog_process_line(): ssh: process line: 8ee195701163311362f4c0c8ccae7c55 SHARED_SECRET 6b61880c960d953382077c4f695cda7187e31aed23a28679fff4e81f7242939401ec18cf607e02c6601e4947fe3eecc0d5d66b12b00da50a7ca481b53a521d9b11b358943b1e04884933194d0bffdfc5f894ee414072b1cd9e35c3785ff57a507c748101877930bc29bb9cc135deccbb0e85d365cff4fda25eae411b6fc91eaef2826397a990a93504f9d418a601ecaa5c285f221a0399a8217aa4c923f5c91f51ad81633b8601ab680b4423f459c89f3790fa6a075d6f0478519d3ab9ddd1ef2316cee47d4708d1bd3e3207675adc9bf1d71368fe59e358f8f430da4ffc8794da39b572f03fae56
 ** (tshark:51929) 01:51:38.199448 [packet-ssh NOISY] epan/dissectors/packet-ssh.c:2110 -- ssh_keylog_process_line(): ssh: process line: 526bf98173ebc775ba6a754545a6790fe3c66ad77fef49c33b96c9bb8a7538dfd5e5eb3cd432a5cd98a78f0d1f9f943117ec2526f01d1ea27f992c70b1a40dead1789a03c6c542bdddc4e843041759e2cc502c2171b8dba713a78bc30afc85ed0b3e99699407fb8794187772154edd68217bd8f73684d7e4a62ebe750030919cb3d8644d85c9347baee12f1e589edea2ed6a0d1e0cf8737577a3b255899ffd7c72d5a7d4b51b2a6cd1597f9b68c7ea41b95ba8b5e475892bb6bdba8ae6878f72e5b1ba7e628de2b318c353d592d5c12fd1a520d4a1e794376e63e86e1d41162f83fc06cf72ca7c3222206cc1264f2ec5b4819aafd0e442bee4dde92eb46e9f9
 ** (tshark:51929) 01:51:38.199849 [packet-ssh DEBUG] epan/dissectors/packet-ssh.c:2128 -- ssh_keylog_process_line(): ssh keylog: invalid format
 ** (tshark:51929) 01:51:38.199951 [packet-ssh NOISY] epan/dissectors/packet-ssh.c:2110 -- ssh_keylog_process_line(): ssh: process line: 6fcc3d2151615376a72df47baa9850634df69450a16c10c86
 ** (tshark:51929) 01:51:38.200208 [packet-ssh DEBUG] epan/dissectors/packet-ssh.c:2128 -- ssh_keylog_process_line(): ssh keylog: invalid format
</code></pre>

<p>Sonuçlarda ilk bakışta dikkatinizi çekti mi bilmem ama, <code>process line</code> log mesajı 3 defa yazılmış ve biraz dikkatli bakınca aslında bizim tek satırda olan <code>cookie SHARED_SECRET key</code> için verdiğimiz bilgileri 3 satıra bölmüş gibi gözüküyor.</p>

<h2 id="wireshark-bug-fix">Wireshark Bug Fix</h2>

<p>Wireshark log mesajlarını takip edersek yukarıdaki log mesajının nasıl yazdırıldığını koda bakarak kolayca anlayabiliriz, <code>ssh_keylog_read_file</code> içerisinde her satır için <code>ssh_keylog_process_line</code> fonksiyonu çağrılmış.
Sorun biz tek satır bilgi girmemize rağmen neden 3 defa bu işlemi yapmış onu bulmamız gerekiyor. Kod içine biraz daha dikkatli bakarsak sorunun nerede olduğunu bence anlayabiliriz. İlgili fonksiyonun içeriği aşağıda görülebilir.</p>

<pre><code>static void
ssh_keylog_read_file(void)
//...
//...
//...
for (;;) {
    char buf[512];
    buf[0] = 0;

    if (!fgets(buf, sizeof(buf), ssh_keylog_file)) {
        if (ferror(ssh_keylog_file)) {
            ws_debug("Error while reading %s, closing it.", pref_keylog_file);
            ssh_keylog_reset();
            g_hash_table_remove_all(ssh_master_key_map);
        }
        break;
    }

    size_t len = strlen(buf);
    while(len&gt;0 &amp;&amp; (buf[len-1]=='\r' || buf[len-1]=='\n')){len-=1;buf[len]=0;}

    ssh_keylog_process_line(buf);
}
</code></pre>

<p>Yukarıdaki satırlarda <code>char buf[512]</code> dikkatinizi çekti mi bilmiyorum ama sorun burada yatıyor. Bizim oluşturduğumuz dosyada <code>keylog</code> dosyasında hatırlarsanız, <code>cookie SHARED_SECRET key</code>  formatında bir veri bulunuyordu.
Şimdi ufak bir hesaplama yaparsak</p>

<ul>
  <li>cookie = 32 karakter</li>
  <li>SHARED_SECRET = 13 karakter</li>
  <li>key = 1024 karakter</li>
  <li>boşluklar + yeni satır= 3 karakter</li>
  <li>Toplam = 1072</li>
</ul>

<p>Bu hesaplama aslında neden <code>512</code> karakterlik bir buffer yeterli değil onu gösteriyor. Demek ki ilgili buffer değerini arttırırsak sorunu düzeltebiliriz. Tabi arttırırken kaça çıkarmak lazım onu düşünmemiz lazım, 
burada algoritmaya göre sabit bir buffer değeri vermek yerine dinamik olarak da hesaplanabilir ama daha kompleks bir çözüm olacağı için şimdilik ondan uzak duruyorum.</p>

<p>Aşağıda benim testi yaptığım sunucu üzerinde kullanılan, <code>KEX</code> algoritmalarının listesini görebilirsiniz.</p>

<pre><code>caltuntas@debian11:~ ssh -Q kex
diffie-hellman-group1-sha1
diffie-hellman-group14-sha1
diffie-hellman-group14-sha256
diffie-hellman-group16-sha512
diffie-hellman-group18-sha512
diffie-hellman-group-exchange-sha1
diffie-hellman-group-exchange-sha256
ecdh-sha2-nistp256
ecdh-sha2-nistp384
ecdh-sha2-nistp521
curve25519-sha256
curve25519-sha256@libssh.org
sntrup4591761x25519-sha512@tinyssh.org
</code></pre>

<p>Listede görüldüğü gibi algoritmalar tarafından üretilen anahtarların hepsi <code>hash</code> işleminden geçirilip ortaya çıkıyor, hash algoritması olarak da, <code>sha1,sha2,sha256,sha512</code> yani maksimum 512 byte yani 1024 karakter uzunluğunda olabilir gözüküyor.
Diğer <code>cookie</code> gibi şeyleri de içerdiğinden 1024 vermek mantıklı olmaz, bu sebeple 1100 rakamı işimizi görse de, ben diğer kullanılabilecek algoritmaları kontrol etmediğimden olası bir duruma karşı <strong>2048</strong> vermeyi mantılı buluyorum.</p>

<p>Özetle <code>char buf[512];</code> yerine <code>char buf[2048];</code> gibi basit bir değişiklik yapıp kodu tekrar derleyip <code>tshark</code> ile deniyorum.</p>

<pre><code>caltuntas@debian11:~/Projects/wireshark/build/run$ ./tshark -V -o ssh.debug_file:./sshdebug.log -o ssh.keylog_file:/home/caltuntas/keys-dh-g16-512.txt -r /home/caltuntas/ssh-dh-g16.pcapng
...
...
** (tshark:54010) 15:20:52.539820 [packet-ssh NOISY] epan/dissectors/packet-ssh.c:3339 -- ssh_decrypt_packet(): Getting raw bytes of length 64
** (tshark:54010) 15:20:52.540179 [packet-ssh NOISY] epan/dissectors/packet-ssh.c:3416 -- ssh_decrypt_packet(): MAC OK
...
...
SSH Protocol
    SSH Version 2 (encryption:aes128-ctr mac:hmac-sha2-256 compression:none)
        Packet Length: 76
        Padding Length: 13
        Message: User Authentication (generic)
            Message Code: User Authentication Request (50)
            User Name length: 9
            User Name: testusername
            Service Name length: 14
            Service Name: ssh-connection
            Method Name length: 8
            Method Name: password
            Change password: False
            Password length: 13
            Password: testpassword
            Payload: 320000000963616c74756e7461730000000e7370682d636f6e6e651374696f6e000a0008706173737c6f7264000000000d7361746e75746c612a32303232
        Padding String: e55ac62dfd281f98c9aaf48d68
        MAC: 7b9a04b10ba7c9f984354410fb5e9349c9b255226bc75bd6635c9b29c1399244 [correct]
        [MAC Status: Good]
        [Sequence number: 5]
    [Direction: client-to-server]
...
...
</code></pre>

<p>Yukarıda paketleri çözebildiği ve <code>User Name</code> ve <code>Password</code> olarak gönderilen değerleri de açık olarak artık görebiliyoruz yani fix işe yaramış. Sorunu çözmeden önce aynı paket şu şekilde gözüküyordu.</p>

<pre><code>...
...
** (tshark:54005) 15:19:35.684498 [packet-ssh NOISY] epan/dissectors/packet-ssh.c:3339 -- ssh_decrypt_packet(): Getting raw bytes of length 128
** (tshark:54005) 15:19:35.684594 [packet-ssh DEBUG] epan/dissectors/packet-ssh.c:3361 -- ssh_decrypt_packet(): ssh: unreasonable message length 1997047085/92
...
...
SSH Protocol
    SSH Version 2 (encryption:aes128-ctr mac:hmac-sha2-256 compression:none)
        Packet Length (encrypted): 89bf5b74
        Encrypted Packet: 13616c8ddfe4a3f034fb3bbc655f32dff594f19e04bc20496d77b97758348be0cdd678f8bcfbae3515e8fee4b4fe1681ef9da4ff533ebd28d305ca14970c185bd91389f90ad62ffdba5320d1
        MAC: 7b9a04b10ba7c9f984354410fb5e9349c9b255226bc75bd6635c9b29c1399244
    [Direction: client-to-server]
</code></pre>

<h2 id="wireshark-gui">Wireshark GUI</h2>

<p>Uzun uğraşlar sonucunda, zar zor da olsa hatayı bulup, düzeltip Wireshark arayüz olmasa da onun CLI aracı ile trafiği çözümlemeyi başardık. Tabi iyi bir açık kaynak topluluğu vatandaşı olarak, sadece
sorunları iletip, yeni özellik istemekle yetinmeyip sorunu tespit edip biz de katkıda bulunuyoruz. Bunun için bir <a href="https://gitlab.com/wireshark/wireshark/-/issues/20332">issue</a> oluşturdum, çözümü de önerdim, farklı bir görüş varsa duymak istedim değilde 
yukarıda yaptığım değişikliği <strong>PR</strong> olarak iletmeyi düşünüyorum yakında.</p>

<p>Ama hala çözümlenmiş trafiği Wireshark ara yüzünde göremediğim için eksik hissettiğimden, ama MacOS ortamında derlemek için bağımlılıklarla boğuşacak enerjim de kalmadığından şöyle bir fikir geldi aklıma.
Neden 512 karakteri geçmeyecek bir KEX algoritması kullanmıyorum? Hatırlarsanız, desteklenmeyen algoritma hatası almıştık ilk başlarda, <code>diffie-hellman-group1-sha1</code> algoritması bulunsa da güvenlik sebebiyle <code>disabled</code> 
olarak geliyordu.</p>

<p>Eğer bu algoritmayı sunucuda aktif hale getirirsek toplam karakter uzunluğu SHA1 olduğundan 256 karakter olacak ve diğer <code>cookie</code> gibi alanlarla da olsa 512 karakteri geçmeyecek. Bende denemeye değer, Debian üzerinde bunu aktif hale getirmek için
aşağıdaki satırları <code>/etc/ssh/sshd_config</code> dosyasına ekleyerek o algoritmayı tekrar aktif hale getirebiliriz.</p>

<pre><code>...
#Legacy changes
KexAlgorithms +diffie-hellman-group1-sha1
</code></pre>

<p>Sonrasında tekrar bir SSH trafiği oluşturacak kodun eski halini çalıştırıyorum.</p>

<pre><code>...
...
Handshake: (local) randomFillSync: fb22cb9bb3693944ea0d0c5d870fef11
..
...
SECRET: 6cb634341dd916bbd8508e6b035cb361068eb8abd0e174f0dd930c2557bca7bb1692f6967469d985c78eea2fc0a7e92ae3e99082d2938002a576da7b944fb89a154ff3b81d4faf40a4c3bb2e6b994528f931d0f4ccf6e955ce29cc03ade69477f0b48653887be97337d0fbe7ab029f2653a918e0e9bb44de4a9cb365ce4abfb1
...
...
</code></pre>

<p>Yukarıdaki çıktıdan aldığım bu değerleri daha önce yaptığım gibi <code>keylog</code> dosyası içine aşağıdaki gibi yerleştirdim.</p>

<pre><code>fb22cb9bb3693944ea0d0c5d870fef11 SHARED_SECRET 6cb634341dd916bbd8508e6b035cb361068eb8abd0e174f0dd930c2557bca7bb1692f6967469d985c78eea2fc0a7e92ae3e99082d2938002a576da7b944fb89a154ff3b81d4faf40a4c3bb2e6b994528f931d0f4ccf6e955ce29cc03ade69477f0b48653887be97337d0fbe7ab029f2653a918e0e9bb44de4a9cb365ce4abfb1
</code></pre>

<p><img src="/img/sshdecrypt/wireshark-ssh-dec.png" alt="Capture 1" /></p>

<p>Evet sonunda Wireshark GUI üzerinde de istediğimiz sonucu aldık, son resme bakacak olursanız eskisinden farklı olarak, 1 numara olarak işaretlediğim kısımda eskiden olmayan, SSH paket tipleri gözüküyor, 2 ve 3 numarası kısımlarda ise
trafiğinden içinden daha önceki yazıda yaptığımıza bender <code>Username</code> ve <code>Password</code> bilgilerini net bir şekilde görebiliyoruz.</p>

<h2 id="sırada-ne-var">Sırada Ne Var?</h2>

<p>Şuana kadar genelde biraz kaçak güreştik, kullandığımız kütüphanenin kodunu değiştirerek, hesaplanmış olan <code>shared key</code> değerini kullanarak Wireshark üzerinde bunu çözümledik. 
Paylaşılan anahtar aslında istemci ve sunucu tarafından gizli tutulan <code>private key</code> ve sonrasında açık olarak paylaşılan tüm bilgilerin hesaplanması sonucunda ortaya çıkıyor.
Biraz kolayına kaçıp kütüphanenin hesapladığı değeri aldık kullandık.Bir sonraki yazıda, elimizde sadece özel anahtar değeri olursa, yani hep dediğimiz özel anahtarı ele geçiren trafiği çözümler iddiasını ispatlamaya çalışacağız.</p>]]></content><author><name>Cihat Altuntaş</name></author><category term="ssh,nodejs,wireshark" /><summary type="html"><![CDATA[SSH Trafiğini Çözümleyelim 2 - Wireshark]]></summary></entry><entry><title type="html">VMWare ESXi ve Linux Sunucu Arasındaki Disk Alanı Tutarsızlığı</title><link href="http://www.cihataltuntas.com/2025/04/06/esxi-and-linux-disk-space-mismatch.html" rel="alternate" type="text/html" title="VMWare ESXi ve Linux Sunucu Arasındaki Disk Alanı Tutarsızlığı" /><published>2025-04-06T14:00:00+00:00</published><updated>2025-04-06T14:00:00+00:00</updated><id>http://www.cihataltuntas.com/2025/04/06/esxi-and-linux-disk-space-mismatch</id><content type="html" xml:base="http://www.cihataltuntas.com/2025/04/06/esxi-and-linux-disk-space-mismatch.html"><![CDATA[<p>Geçenlerde lab ortamında Linux sunucu üzerinde bir işlem yaparken, birden ilgili sunucuya erişimim kesildi.
SSH ile bağlandığım için sıradan bir bağlantı kopma sorunu olduğunu düşündürdü ama tekrar denediğimde de bağlantı kuramadım.</p>

<p>Linux sunucu sanallaştırma ortamı olarak VMWare ESXi üzerinde koştuğu için, ilk olarak orayı kontrol ettim ve hemen ilk girişte disk uyarısı
mesajı ile beni karşıladı, en azından az da olsa VMWare disk tarafından bir yer açıp Linux tarafına tekrar erişip disk dolduysa orada büyük temizliği yaparım diye düşündüm.</p>

<p>VMWare ESXi sunucuya girip ilgili Linux sunucuya karşılık gelen sanal makinayı bulduktan sonra disk boyutunu kontrol ettiğimde aşağıdaki gibi bir sonuç ortaya çıkıyordu.</p>

<pre><code>[root@esxi:/vmfs/volumes/7849317b-fa83-44b9-8924-0fedcd6ef784/linuxserver] ls -lah *.vmdk
-rw-------    1 root     root      750.0G Apr  6 13:43 linuxserver-flat.vmdk
-rw-------    1 root     root         534 Mar 26 13:26 linuxserver.vmdk
</code></pre>

<p>Burada kadar normal bir görüntü var, çünkü sunucu disk konfigürasyonu olarak <a href="https://en.wikipedia.org/wiki/Thin_provisioning">thin provisioning</a> kullanılarak kuruldu, 750GB yer ayrıldı.
Yani mantıksal olarak 750GB yer ayrıldı ama fiziksel olarak bu yeri gerçekten kullanmadığı durumda daha az disk tüketecektir. Bunu da aşağıdaki gibi görebiliriz.</p>

<pre><code>[root@esxi:/vmfs/volumes/7849317b-fa83-44b9-8924-0fedcd6ef784/linuxserver] du -sh *.vmdk
186.0G  linuxserver-flat.vmdk
0       linuxserver.vmdk
</code></pre>

<p>Yukarıdaki kullanıma bakarsanız mantıksal olarak 750GB yer kaplayan disk
aslında ESXi üzerinde fiziksel olarak 186GB yer kaplamış. Fakat Linux tarafına
biraz yer boşalttıktan sonra giriş yapıp disk kontrolü yaptığımda aşağıdaki
gibi bir sonuç ortaya çıktı.</p>

<pre><code>root@linuxserver:~# df -h --total
Filesystem                            Size  Used Avail Use% Mounted on
tmpfs                                 2.4G  1.5M  2.4G   1% /run
/dev/mapper/ubuntu--vg-root           501G   16G  460G   4% /
tmpfs                                  12G     0   12G   0% /dev/shm
tmpfs                                 5.0M     0  5.0M   0% /run/lock
/dev/mapper/ubuntu--vg-tmp            4.9G  136K  4.6G   1% /tmp
/dev/sda2                             2.0G  245M  1.6G  14% /boot
/dev/mapper/ubuntu--vg-var            147G   16G  124G  12% /var
/dev/mapper/ubuntu--vg-home           4.9G  6.2M  4.6G   1% /home
/dev/mapper/ubuntu--vg-var_tmp        4.9G   64K  4.6G   1% /var/tmp
/dev/mapper/ubuntu--vg-var_log        4.9G  547M  4.1G  12% /var/log
/dev/mapper/ubuntu--vg-var_log_audit   30G  1.9G   27G   7% /var/log/audit
localhost:/volume1                    501G   67G  414G  14% /opt/app/data
tmpfs                                 2.4G  4.0K  2.4G   1% /run/user/0
total                                 1.2T  101G  1.1T   9% -
</code></pre>

<p>Son satıra dikkat ederseniz 186GB olarak ESXi sunucu üzerinde yer kaplayan Linux sunucu aslında yaklaşık 101GB disk tüketiyor. 
Linux sunucu <code>thin provisioning</code> olarak ayarlandığında, mesela büyük bir dosya oluşturdunuz ama sonrada o dosyayı Linux üzerinden <code>rm</code> ile sildiniz,
bu silme işlemi sonunda Linux işletim sistemi gerçek disk üzerinde tutulan fiziksel dosyayı değil, ona olan bağlantıyı yani <code>pointer</code> adresini ortadan kaldırıyor.</p>

<p>Aslında gerçek anlamda üzerine veri yazılmadığı sürece dosyanın verileri fiziksel disk üzerinde tutulmaya devam ediyor, ve bu çalışma mantığı dosya kurtarma araçlarının nasıl çalıştığını da özetlemiş oluyor. Durum böyle olunca VMWare ESXi için,
dışarıdan bakan bir göz olarak disk üzerinde hala veriler var ve provision edilmiş olarak görülüyor ve yer kaplamaya devam ediyor.</p>

<p>Bundan dolayı Linux ve diğer işletim sistemleri üzerinden genellikle veriler silindikten sonra ilgili verilerin diskten de silinmesi ya da sıfırlanması için
çeşitli araçlar bulunuyor. Bunlardan bir tanesi <a href="https://manpages.ubuntu.com/manpages/focal/man8/zerofree.8.html">zerofree</a> aracı. Bunu bizim ortamda dosya sistemi olarak
<code>extfs</code> kullanıldığından dolayı kullanıyorum, farklı dosya sistemleri için farklı araçlar da bulunuyor.</p>

<p>Zerofree çalıştırmak için dosya sistemini <code>read-only</code> olarak mount etmeniz gerekiyor bunun için de <code>recovery</code> ya da <code>emergency</code> mode ile sistemi açıp ardından
şu şekilde işlemi başlatabilirsiniz.</p>

<pre><code>root@linuxserver:~# echo "u" &gt; /proc/sysrq-trigger
mount /dev/mapper/ubuntu-vg-root / -o remount,ro
zerofree -v /dev/mapper/ubuntu-vg-root
</code></pre>

<p>İlk satırda bulunan <a href="https://www.kernel.org/doc/html/v4.10/admin-guide/sysrq.html">SysRq</a> dosyasını <code>u</code> parametresi ile tetikleyerek bütün diskleri read-only olarak tekrar mount etmesi gerektiğini söylüyoruz işletim sistemine.
Ardından <code>zerofree</code> ile disk üzerinden silinmiş fakat hala fiziksel disk üzerinde bulunan verileri sıfırlıyoruz.</p>

<p>Bu işlem disk boyutuna göre uzun sürebiliyor, sonrasında VM hala kapalı iken ESXi üzerinden tekrar disk alanını kazanmak için aşağıdaki işlemi başlatıyoruz.</p>

<pre><code>[root@esxi:/vmfs/volumes/7849317b-fa83-44b9-8924-0fedcd6ef784/linuxserver] vmkfstools -K linuxserver.vmdk
vmfsDisk: 1, rdmDisk: 0, blockSize: 1048576
Hole Punching: 19% done.
</code></pre>

<p>Bu işlem bittiğinde tekrar ESXi üzerinde kapladığı yeri kontrol ettiğinizde muhtemelen kapladığı yerin aşağıdaki gibi Linux sunucuda raporlanan yer ile oldukça yaklaştığını göreceksiniz.</p>

<pre><code>[root@esxi:/vmfs/volumes/7849317b-fa83-44b9-8924-0fedcd6ef784/linuxserver] du -sh *.vmdk
102.0G  linuxserver-flat.vmdk
0       linuxserver.vmdk
</code></pre>

<h2 id="zerofree-ne-yapıyor">Zerofree Ne Yapıyor?</h2>

<p>Şöyle bir <a href="https://github.com/haggaie/zerofree/blob/master/zerofree.c">kaynak koduna</a> göz atmak istedim, aslında yukarıdaki bahsettiğim mantığın işletim sistemi fonksiyonlarını yapıyor diyebiliriz.</p>

<pre><code>empty = (unsigned char *)malloc(fs-&gt;blocksize);
//...
//...
if ( !dryrun ) {
  if (!discard) {
    ret = io_channel_write_blk(fs-&gt;io, blk, 1, empty);
    if ( ret ) {
      fprintf(stderr, "%s: error while writing block\n", argv[0]);
      return 1;
    }
  } else 
  //...
  //...
}
</code></pre>

<p>Önce dosya sisteminin blok boyutuna göre sıfırlamak için kullanacağı <code>empty</code> verisini oluşturuyor. Ardından bir döngü içeresinde
yukarıdaki gibi <code>io_channel_write_blk</code> silinmiş ama diskte hala bulunan blokları sıfırlıyor.</p>

<h2 id="sırada-ne-var">Sırada Ne Var?</h2>

<p>Bu yaptığımız işlem aslında dosyalama sistemlerinin ve işletim sistemlerinin temellerinde olan bir konu. Genelde <strong>hole punching</strong> olarak isimlendiriliyor.
Konu ile çok yakından ilgili olan ve arka planda yatan kavram aslında <a href="https://en.wikipedia.org/wiki/Sparse_file">Sparse File</a> ve bu konuyu Linux üzerinde daha detaylı olarak farklı bir yazıda inceleme niyetim var.</p>]]></content><author><name>Cihat Altuntaş</name></author><category term="linux,vmware" /><summary type="html"><![CDATA[VMWare ESXi ve Linux Sunucu Arasındaki Disk Alanı Tutarsızlığı]]></summary></entry><entry><title type="html">Traceroute Nasıl Çalışır? - Farklı Bir İnceleme</title><link href="http://www.cihataltuntas.com/2025/02/14/how-does-traceroute-work.html" rel="alternate" type="text/html" title="Traceroute Nasıl Çalışır? - Farklı Bir İnceleme" /><published>2025-02-14T14:00:00+00:00</published><updated>2025-02-14T14:00:00+00:00</updated><id>http://www.cihataltuntas.com/2025/02/14/how-does-traceroute-work</id><content type="html" xml:base="http://www.cihataltuntas.com/2025/02/14/how-does-traceroute-work.html"><![CDATA[<p>Geçenlerde sunucuda bazı ağlara erişim için routing konfigürasyonu yaptık ve hedef adresi nasıl gittiğini kontrol etmek için
meşhur <strong>traceroute</strong> komutunu kullandık. Fakat bu sefer diğerlerinden farklı olarak, daha önce bu komutu kullanıp sonuçları değerlendirmiş olsam da,
gerçekten nasıl çalıştığını öğrenmek istedim.</p>

<p>Bazı şeylerin nasıl çalıştığını <a href="https://en.wikipedia.org/wiki/Clarke%27s_three_laws">sihir</a> ya da bizim tabirimizle <strong>şeytan işi</strong> olarak tanımlamak kolayımıza gelebiliyor.</p>

<blockquote>
  <blockquote>
    <p>Any sufficiently advanced technology is indistinguishable from magic.</p>
  </blockquote>
</blockquote>

<p>Tabi diğer notlarımdan da belki gözlemlediğiniz üzere, biz genelde <code>There is no magic</code> yaklaşımı ile sihrin arkasında yatan mantığı ortaya çıkarıp 
anlamaya çalışıyoruz. Bu yazıda genel yaklaşımdan farklı olarak, traceroute nasıl çalıştığını klasik anlamda anlatmaktansa kendim nasıl çalıştığını anlamak için neler
yaptım ne sonuçlara vardım ondan bahsedeceğim.</p>

<p>İlk olarak tabi kullandığım MacOS bilgisayar üzerinde terminal açıp komutu bilinen bir adresle çalıştırmak oldu.</p>
<pre><code> &gt; traceroute www.google.com
traceroute to www.google.com (216.239.38.120), 64 hops max, 52 byte packets
 1  172.16.33.1 (172.16.33.1)  18.249 ms  1.462 ms  1.840 ms
 2  88.243.80.1.dynamic.ttnet.com.tr (88.243.80.1)  10.927 ms  9.714 ms  9.555 ms
 3  81.212.73.201.static.turktelekom.com.tr (81.212.73.201)  11.230 ms  28.809 ms  8.312 ms
 4  * * 34-acibadem-sr14s-t2-2---34-kartal-t3-3.statik.turktelekom.com.tr (81.212.217.1)  4240.849 ms
 5  * * *
 6  307-sof-col-2---34-ebgp-acibadem-sr12e-k.statik.turktelekom.com.tr (212.156.104.158)  17.957 ms  17.988 ms  18.058 ms
 7  142.250.168.28 (142.250.168.28)  17.592 ms  16.475 ms
    142.250.167.192 (142.250.167.192)  19.013 ms
 8  * * *
 9  any-in-2678.1e100.net (216.239.38.120)  16.574 ms  15.592 ms  16.383 ms
</code></pre>

<p>Yukarıda <code>www.google.com</code> adresine traceroute çektiğimizde, gelen her satırı ulaşmak istediğimiz adrese giderken genellikle paketlerimizin yönlendirildiği birer <code>hop</code> yani durak ya da birer <code>router</code> gibi düşünebilirsiniz.<br />
Diğer kafa karıştıran oluşturan konu ise birden fazla <code>ms</code> yani süre değerleri olabilir. Bunlar bir <code>hop</code> için 3 adet sordu atıp üçünün de dönüş sürelerini gösteriyor.</p>

<p>Bunu yaparken, çok kullandığımız <code>ping</code> aracının da kullandığı <strong>ICMP</strong> paketleri gönderiliyor. Traceroute her hop için gönderilen paketin <a href="https://en.wikipedia.org/wiki/Time_to_live">TTL</a> değeri 1 rakamından başlayarak arttırılıyor. 
Bu şekilde gönderilen ve yolculuğuna başlayan paketin her durakta TTL değeri 1 azaltılıyor, ve bu değer 0 olduğunda ise, ilgili router bize <a href="https://en.wikipedia.org/wiki/Internet_Control_Message_Protocol#Time_exceeded">TTL Exceeded</a> cevabı geri dönüyor.
Bu cevabı alan, bizim kullandığımız araç olan traceroute ise istemci olarak TTL değerini 1 arttırıp, bir sonra hangi router üzerinden bu paket geçecek, evet ise ne kadar sürecek, değil ise
hangi router üzerinde takılacak bunu anlamaya çalışıyor. Bunu diyagrama dökersek aşağıdaki gibi gözükecektir.</p>

<pre><code class="language-mermaid">sequenceDiagram
    participant Local as Local
    participant H1 as 172.16..
    participant H2 as 88.243..
    participant H3 as 81.212..
    participant H4 as 81.212..
    participant H5 as Unreachable
    participant H6 as 212.156..
    participant H7 as 142.250..
    participant H8 as Unreachable
    participant Dest as Google
    
    Local-&gt;&gt;H1: TTL=1
    H1--&gt;&gt;Local: 18.249 ms, 1.462 ms, 1.840 ms
    
    Local-&gt;&gt;H2: TTL=2
    H2--&gt;&gt;Local: 10.927 ms, 9.714 ms, 9.555 ms
    
    Local-&gt;&gt;H3: TTL=3
    H3--&gt;&gt;Local: 11.230 ms, 28.809 ms, 8.312 ms
    
    Local-&gt;&gt;H4: TTL=4
    H4--&gt;&gt;Local: *, *, 4240.849 ms
    
    Local-&gt;&gt;H5: TTL=5
    H5--&gt;&gt;Local: No Response
    
    Local-&gt;&gt;H6: TTL=6
    H6--&gt;&gt;Local: 17.957 ms, 17.988 ms, 18.058 ms
    
    Local-&gt;&gt;H7: TTL=7
    H7--&gt;&gt;Local: 17.592 ms, 16.475 ms, 19.013 ms
    
    Local-&gt;&gt;H8: TTL=8
    H8--&gt;&gt;Local: No Response
    
    Local-&gt;&gt;Dest: TTL=9
    Dest--&gt;&gt;Local: 16.574 ms, 15.592 ms, 16.383 ms
</code></pre>

<p>Buraya kadar normal, işin teorik kısmını güzel özetledik fakat bunu yaparken , <strong>Wireshark</strong> ile gelen giden paketleri incelediğimde şöyle bir sonuç ortaya çıktı.</p>

<p><img src="/img/traceroute/icmp-wireshark.png" alt="Capture 1" /></p>

<p>Benim beklentim, her satır için TTL değerlerinin 1,2,3 diye arttığını görmekti ama ekran görüntüsünde gözüktüğü gibi hepsinde <code>Time to Live</code> kolonunda <code>,1</code> yani 1 olarak gözüküyor.</p>

<h2 id="traceroute-kodunu-i̇nceleyelim">Traceroute Kodunu İnceleyelim</h2>

<p>Yukarıdaki kafama takılan soruyu biraz daha araştırarak, sorarak, okuyarak
bulacağımdan eminim fakat pratik ile yaptığımda çok daha iyi kavradığımı
bildiğim için farklı bir yönden bulmaya karar verdim.  Traceroute dediğimiz şey
sonunda bir kod parçasının derlenmiş hali olan bir araç, aynı routing mantığını
koşan network cihazları, paketimizi gönderen işletim sistemi gibi. En iyi
doküman da kod olduğuna göre neden kodunu indirip, inceleyip, debug edip yani
pratik yaparak anlamıyoruz?</p>

<p>Kullandığım bilgisayar bir MacOS işletim sistemi üzerinde çalıştığından, hemen
<strong>Apple</strong> firmasına başvuruyoruz. MacOS aslında bir Unix türevi işletim sistemi
,Linux ile aynı araçlara genelde sahip olsa da aynı kaynak kodlara sahip
olmak zorunda değil. Genelde MacOS GNU lisans kısıtlamaları sebebiyle <strong>BSD</strong> türevi işletim sistemlerinin araçlarını
kullanıyor. Yani klasik bir Linux işletim sisteminde çalıştırdığınız aynı
<strong>traceroute</strong> GNU kodu iken, MacOS üzerinde bulunan <strong>traceroute</strong> bir BSD
türevi kaynak koda sahip.</p>

<p>Apple bu tarz kullandığı open source araçlar, teknolojiler için <a href="https://opensource.apple.com/releases/">bu adreste</a> kaynak kodlarının linklerini
yayınlamış. Benim aradığım araç bir network komutu olduğu için <code>network_cmds</code>
altında bulunuyor ve <a href="https://github.com/apple-oss-distributions/network_cmds/tree/network_cmds-624.100.5">bu adresten</a>
kaynak kod paylaşılmış.</p>

<h2 id="derleme-sorunları">Derleme Sorunları</h2>

<p>Kodu indirdikten sonra önce derlemek ile biraz uğraştım diyebilirim, en büyük
sebebi de bir türlü kullanılan bir <strong>SO_RECV_ANYIF</strong> değerin hem kod içinde hem
de sistem kütüphaneleri içinde bulunmamasıydı. Derleme sırasında aşağıdaki hatayı bir türlü geçemedim.</p>

<pre><code>&gt; cc -g -I../network_cmds_lib ../network_cmds_lib/network_cmds_lib.c \
traceroute.c as.c ifaddrlist.c version.c findsaddr-socket.c  -o traceroute -lpcap

traceroute.c:826:35: error: use of undeclared identifier 'SO_RECV_ANYIF'
        (void) setsockopt(s, SOL_SOCKET, SO_RECV_ANYIF, (char *)&amp;on,
                                         ^
1 error generated.
</code></pre>

<p>Tahminim <strong>Xcode</strong> SDK
güncellemesi yaparsam ya da ek bir SDK kurarsam düzeleceği yönündeydi fakat bununla uğraşmak istemeyip
aradığı tanımı <code>traceroute.h</code> içinde tanımlayıp aşağıdaki gibi geçtim sonra da
başarılı şekilde derlendi. Bu arada bu değerin nereden geldiğini bilen varsa öğrenmek isterim.</p>

<pre><code>#ifndef SO_RECV_ANYIF
#define SO_RECV_ANYIF 0x1104
#endif
</code></pre>

<p>Başarılı derlendi derlenmesine ama diğer uğraştıran konulardan biri ise bu şekilde derleyip çalıştırdıktan sonra aşağıdaki hataya takıldım bir süre.</p>

<pre><code>&gt; cc -g -I../network_cmds_lib ../network_cmds_lib/network_cmds_lib.c \
traceroute.c as.c ifaddrlist.c version.c findsaddr-socket.c  -o traceroute -lpcap
&gt; sudo ./traceroute www.google.com
traceroute: ifaddrlist: SIOCGIFADDR: awdl0: Can't assign requested address
</code></pre>
<p>Biraz kodu inceledikten sonra içinde bol bol <a href="https://en.wikipedia.org/wiki/Conditional_compilation">koşullu derleme</a> yönergeleri
olduğunu fark ettim ve sonrasında <code>HAVE_SYS_SOCKIO_H</code> ve <code>HAVE_SOCKADDR_SA_LEN</code>
derleme parametrelerini göndermediğim zaman başarılı şekilde derlenip çalıştı.</p>

<pre><code>cc -g -DHAVE_SYS_SOCKIO_H -DHAVE_SOCKADDR_SA_LEN \
-I../network_cmds_lib ../network_cmds_lib/network_cmds_lib.c \
traceroute.c as.c ifaddrlist.c version.c findsaddr-socket.c  -o traceroute -lpcap
</code></pre>

<p>Kod beklediğimden daha kompleks çıktı diyebilirim, sanırım diğer Unix türevi işletim sistemlerinde de çalışması
için bol bol koşullu derlemede kullanılan macro parametreleri kullanılmış. Yukarıdaki beni uğraştıran
iki parametre de sanırım MacOS için derlendiğinde kullanılması gerekiyordu, indirdiğim kaynak kod içinde 
CMake, Make gibi bir araç ya da build dokümanı olmadığından bunu biraz deneme yanılma biraz da kodu
inceleyerek buldum diyebilirim.</p>

<h2 id="debugging">Debugging</h2>

<p>Artık kodu <code>debug</code> sembolleri ile derleyebildik ve sıra geldi adım adım kodu çalıştırıp neler yaptığını anlamaya ve ilk aklımıza takılan neden <code>TTL</code> değerinin artmadığını öğrenmeye.
Bu aşamada ben <code>LLDB</code> kullanarak kodu debug ettim, ana çalışma akışını öğrendim diyebilirim tabi burada bunu yazmak zor olacağı için, Linux sistemlerde kullandığım <code>ftrace</code> benzeri bir araçla
çalışan fonksiyonları çıkarmak istedim, hem ana resim çıkmış olur hem de buraya daha kolay aktarılır.</p>

<p>Dtrace ile her fonksiyon çağrısını izleyebileceğiniz bir script hazırlayıp ardından onun aracılığı ile programı çağırırsanız size tüm fonksiyon çağrılarını gösteriyor.</p>

<pre><code>pid$target:traceroute::entry
{
    printf("%s[%d] %s:%s\n", execname, pid, probemod, probefunc);
}
</code></pre>

<p>Yukarıdaki kodu <code>user_functions.d</code> dosyası olarak kaydedip <code>dtrace</code> ile programı çalıştırdığımda aşağıdaki gibi bir çıktı üretiyor.</p>

<pre><code>&gt; sudo dtrace -s user_functions.d -c './traceroute www.google.com'
 1  172.16.33.1 (172.16.33.1)  8.701 ms  1.894 ms  2.130 ms
 2  88.243.80.1.dynamic.ttnet.com.tr (88.243.80.1)  8.757 ms  9.586 ms  9.861 ms
 3  81.212.73.201.static.turktelekom.com.tr (81.212.73.201)  8.834 ms  12.060 ms  11.553 ms
cpu     id                    function:name
  1  65465                       main:entry traceroute[35587] traceroute:main
  2  65470                gethostinfo:entry traceroute[35587] traceroute:gethostinfo
  1  65491        clean_non_printable:entry traceroute[35587] traceroute:clean_non_printable
  1  65471                     setsin:entry traceroute[35587] traceroute:setsin
  1  65472               freehostinfo:entry traceroute[35587] traceroute:freehostinfo
  1  65489                 ifaddrlist:entry traceroute[35587] traceroute:ifaddrlist
  1  65490                  findsaddr:entry traceroute[35587] traceroute:findsaddr
  3  65455                   udp_prep:entry traceroute[35587] traceroute:udp_prep
  3  65482               _osswapint16:entry traceroute[35587] traceroute:_osswapint16
  3  65482               _osswapint16:entry traceroute[35587] traceroute:_osswapint16
  3  65482               _osswapint16:entry traceroute[35587] traceroute:_osswapint16
  3  65484                    p_cksum:entry traceroute[35587] traceroute:p_cksum
  3  65482               _osswapint16:entry traceroute[35587] traceroute:_osswapint16
  3  65493                   in_cksum:entry traceroute[35587] traceroute:in_cksum
  3  65493                   in_cksum:entry traceroute[35587] traceroute:in_cksum
  3  65493                   in_cksum:entry traceroute[35587] traceroute:in_cksum
  3  65474                 send_probe:entry traceroute[35587] traceroute:send_probe
  3  65482               _osswapint16:entry traceroute[35587] traceroute:_osswapint16
  3  65475             wait_for_reply:entry traceroute[35587] traceroute:wait_for_reply
  ...
  ...
</code></pre>

<h2 id="dikkat-çeken-detay">Dikkat Çeken Detay</h2>

<p>Yukarıdaki debug çıktısına baktığınızda belki dikkatinizi çekmemiş olabilir ama ben adım adım debug yaparken o fonksiyona girdiğinde şaşırmıştım.
Fonksiyon çağrılarından birisinin adı <code>udp_prep</code> olduğunu görüyorsunuz sonrasında da, <code>send_probe</code> ve ardından <code>wait_for_reply</code> tekrar edip duruyor.</p>

<p>Koda girip ilgili udp_prep fonksiyonunu aratırsanız ilk karşılaşacağınız yerlerden bir tanesi aşağıda, aslında en büyük ipucu orada açıklanmış.</p>
<pre><code>/* List of supported protocols. The first one is the default. The last
   one is the handler for generic protocols not explicitly listed. */
struct	outproto protos[] = {
	{
		.name = "udp",
		.key = "spt dpt len sum",
		.num = IPPROTO_UDP,
		.hdrlen = sizeof(struct udphdr),
		.port = 32768 + 666,
		.prepare = udp_prep,
		.check = udp_check
	},
  ...
  struct	outproto *proto = &amp;protos[0];
</code></pre>

<p>Yukarıdaki açıklamayı okuyunca aslında bendeki en büyük kafa karışıklığı aydınlanmış oldu. Devam edip kodun içinde diğer fonksiyonların içinde nasıl kullanıldığına bakarsanız bunu görebiliyorsunuz.
<code>Traceroute</code> aslında sadece <code>ICMP</code> protokolü ile değil, UDP, TCP gibi protokollerle de çalışıyor hatta varsayılan protokol UDP olarak ayarlanmış ve kullandığı port numarası da <code>32768 + 666</code> yani <code>33434</code>.</p>

<h2 id="trafiği-tekrar-gözden-geçirelim">Trafiği Tekrar Gözden Geçirelim</h2>

<p>Daha önce yakaladığımız ve TTL sayısının artmadığı trafiği tekrar yukarıdaki bilgi ile kontrol ettiğimizde, aşağıdaki gibi aslında TTL değerlerinin arttığını ve ilk başta beklediğimiz akışa çok benzediğini görebiliyoruz.</p>

<p><img src="/img/traceroute/udp-wireshark.png" alt="Capture 2" /></p>

<p><code>Time to Live</code> sütununda artan TTL değerlerini şimdi görebiliyoruz.</p>

<h2 id="kafa-karışıklığının-nedeni-neydi">Kafa Karışıklığının Nedeni Neydi?</h2>

<p>Benim asıl kafa karışıklığımın sebebi traceroute nasıl çalışıyor diye araştırdığımda aslında sadece ICMP ile çalıştığı yazıyordu. Aslında durum bu değil, hatta örnek olarak Fortigate <a href="https://www.fortinet.com/resources/cyberglossary/traceroutes">sitesinden</a> alıntıyı görebilirsiniz.</p>

<blockquote>
  <blockquote>
    <p>A traceroute works by sending Internet Control Message Protocol (ICMP) packets, and every router involved in transferring the data gets these packets. The ICMP packets provide information about whether the routers used in the transmission are able to effectively transfer the data.</p>
  </blockquote>
</blockquote>

<p>Genelde nasıl çalıştığı ile ilgili yazılar sadece ICMP protokolünden bahsediyor sanırım bir sebebi de <strong>Windows</strong> işletim sistemi üzerindeki muadili <strong>tracert</strong> bu şekilde çalışıyor yani varsayılan protokol olarak ICMP kullanıyor. Fakat MacOS ve Linux işletim sistemlerinde
ilk olarak UDP ile paket gönderiyor hatta isterseniz, ICMP ya da TCP üzerinden de paketleri göndermesini sağlayabiliyorsunuz.</p>

<h2 id="kapanış">Kapanış</h2>

<p>Traceroute özellikle sorun network ile ilgili çözme çalışmalarında alet çantamızda bulunması gereken araçlardan bir tanesi, nasıl çalıştığını hangi durumda ne paket gönderdiğini, hangi portu kullandığını kod üzerinden debug ederek, paket dinleyip detaylarını inceleyerek
anlamış olduk. Tabi birisi ya bu kadar uğraşa gerek var mıydı, gidip <strong>man</strong> sayfasını okusaydınız zaten orada yazıyordu da diyebilir ve haklıdır. Teknolojide doküman okumamak adettendir ama <strong>man traceroute</strong> okusaydık aşağıdaki satırları görebilirdik.</p>

<blockquote>
  <blockquote>
    <p>This program attempts to trace the route an IP packet would follow to some internet host by launching UDP probe packets with a small ttl (time to live) then listening for an ICMP “time exceeded” reply from a gateway.  We start our probes with a ttl of one and increase by one until we get an ICMP “port unreachable” (which means we got to “host”) or hit a max (which defaults to net.inet.ip.ttl hops &amp; can be changed with the -m flag).</p>
  </blockquote>
</blockquote>

<p>Ama yine de bence oldukça faydalık bir debugging oturumu oldu, sağlıcakla…</p>]]></content><author><name>Cihat Altuntaş</name></author><category term="network,macos,traceroute" /><summary type="html"><![CDATA[Traceroute Nasıl Çalışır? - Farklı Bir İnceleme]]></summary></entry></feed>