m0leCon 2026 — In (Secure) Vault 2 Çözümü

Bir .NET 8 WPF kasa uygulaması kapısını istemci tarafında CheckPassword ile koruyor: ptm bayrağı MD5, SHA-256, base64 ve base32 ile doğrulanan leet parçalara bölünmüş, ikili dosya decompile edilerek birleştiriliyor.

5 dk okuma
ibrahimsql
970 kelime

m0leCon 2026 — In (Secure) Vault 2

"In (Secure) Vault 2" (Beginner, rev) tek bir arşiv gönderiyor: InsecureVault.tar. Açıldığında karşımıza kendi kendine yeten bir .NET 8 WPF masaüstü uygulaması çıkıyor — InsecureVault.exe + InsecureVault.dll ve birkaç runtime assembly'si. Uygulama, parolası bayrağın kendisi olan bir giriş penceresi gösteriyor: CheckPassword true döndürürse uygulama "Database Manager" ekranını açıyor.

Yazarın açıklamada adını koyduğu nokta ("şirketlerim için süper güvenli bir kasa") kontrolün tamamen istemcide yaşaması ve hatta iyi saklanmamış olması — sadece küçük hash bulmacaları zinciri. Yönetilen ikili dosyayı decompile etmek tüm kısıtları önünüze seriyor.

Kapsam: gönderilen ikili dosyaların statik reverse-engineering'i. Uygulama Windows WPF hedefliyor, dolayısıyla çalıştırmadım; bayrak yalnızca IL'den geri kazanılabiliyor.

  • Etkinlik: m0leCon CTF 2026
  • Kategori: rev (Beginner)
  • Bayrak biçimi: ptm{...}

1. Keşif

Arşiv üzerinde tar/inceleme bir apphost ve bir yönetilen assembly gösteriyor. Hızlı bir strings taraması, decompiler olmadan parçaları adlandırıyor:

Bash
strings -n 5 InsecureVault.dll | grep -iE "sha|md5|password|flag|Login"
# SHA256
# HashAlgorithm
# CheckPassword
# loginPasswordTBox
# LoginButton_Click
# " Inserisci la flag per continuare

İki şey öne çıkıyor. Birincisi, LoginButton_Click içinden çağrılan bir CheckPassword metodu — kapının kendisi bu. İkincisi, strings içinde hiçbir düz metin bayrak yok; karşılaştırma bir digeste karşı olmalı, bir literal'e karşı değil. Arayüz metni İtalyanca ("devam etmek için bayrağı gir"), İtalyan konferansıyla tutarlı.

ILSpy tarzı bir sınıf listesi hedefi küçük tutuyor:

Text
Class InsecureVault.App
Class InsecureVault.DbManager
Class InsecureVault.Company / Employee / Project / Document
Class InsecureVault.MainWindow

MainWindow girişi tutuyor; DbManager giriş sonrası ekran.

2. Decompile etmek

İkili dosyalar yönetilen (.NET) olduğu için ilspycmd neredeyse özgün C# geri kazanıyor. Bunu bir .NET SDK konteynerinde çalıştırdım (makinede dotnet araç zinciri yoktu). Kayda değer iki ortam pürüzü:

  • ilspycmd 8.2 bir net6.0 aracı; net8.0 SDK runtime'ı altında roll-forward gerektiriyor (DOTNET_ROLL_FORWARD=Major).
  • Bağlanan dizin salt-okunurdu, dolayısıyla -o <dir> Read-only file system ile başarısız oldu; tek bir tipi stdout'a decompile etmek bunu aşıyor.
dotnet tool install -g ilspycmd --version 8.2.0.7535
DLL=$(find /root/.dotnet/tools/.store -name ilspycmd.dll | head -1)
DOTNET_ROLL_FORWARD=Major dotnet "$DLL" -t InsecureVault.MainWindow InsecureVault/InsecureVault.dll

3. CheckPassword

Tüm görev bu metot. Bayrağı tam olarak 45 karaktere ve ptm{ önekine sabitliyor, ardından 4 ve 5 karakterlik dilimleri hash, kodlama ve tamsayı hileleri karışımıyla doğruluyor:

csharp
private static bool CheckPassword(string s)
{
    if (s.Length != 45) return false;
    if (s.Substring(0, 4) != "ptm{") return false;
 
    using (MD5 md = MD5.Create())
        if (BitConverter.ToString(md.ComputeHash(Encoding.UTF8.GetBytes(s.Substring(4, 4))))
              .Replace("-", "").ToLower() != "33635414851f62863a5cb7825481433d") return false;
 
    if (BitConverter.ToString(Encoding.UTF8.GetBytes(s.Substring(8, 4)))
          .Replace("-", "").ToLower() != "5f345f46") return false;
 
    if (Convert.ToBase64String(Encoding.UTF8.GetBytes(s.Substring(12, 4))) != "dW5ueQ==") return false;
 
    string text = "0x" + ((int)s[19]).ToString("x");
    if (s[16] != '_' || (s[17] ^ 5) != 114 || s[18] != '4' || text != "0x79") return false;
 
    using (SHA256 sha = SHA256.Create())
        if (BitConverter.ToString(sha.ComputeHash(Encoding.UTF8.GetBytes(s.Substring(20, 4))))
              .Replace("-", "").ToLower() != "c1cd3414aea97cfb005cf3bf3f39f3c1b5412f5dd8a2602c74a38181d94f5a2d") return false;
 
    if (ToBase32String(Encoding.UTF8.GetBytes(s.Substring(24, 5))) != "MNUDGY3L") return false;
 
    string text2 = "0b" + Convert.ToString(s[30], 2);
    if (s[29] != '_' || text2 != "0b1100110" || s[31] != '0' || s[32] != 'R') return false;
 
    if (s.Substring(33, 3) != "_4_" || (int)s[36] >> 4 != 7) return false;
 
    int num = int.Parse(s[39].ToString());
    if ((s[37] ^ s[38]) != 1 || s[37] != '4' || num + 10 != 15 || s[40] != 'w') return false;
 
    if (BitConverter.ToString(Encoding.UTF8.GetBytes(s.Substring(41)))
          .Replace("-", "").ToLower() != "3072647d") return false;
 
    return true;
}

Her dilim bağımsız olarak tersinir. İlginç çağrı ToBase32String: A–Z2–7 alfabesi üzerinde elle yazılmış bir RFC 4648 kodlayıcısı (= ile 8'in katına dolgu yapıyor), dolayısıyla MNUDGY3L sıradan base32 gibi çözülüyor.

4. Yeniden birleştirme

Dilimler indeks sırasıyla işlendiğinde:

DilimKontrolDeğer
[0:4]literalptm{
[4:8]MD5 = 33635414851f...Wh4t
[8:12]baytlar = 5f 34 5f 46_4_F
[12:16]base64 = dW5ueQ==unny
[16:20]_, s[17]^5==114, 4, hex(s[19])=="0x79"_w4y
[20:24]SHA-256 = c1cd3414..._t0_
[24:29]base32 = MNUDGY3Lch3ck
[29:33]_, bin(s[30])=="1100110", 0, R_f0R
[33:37]_4_, s[36]>>4==7_4_p
[37:41]s[37]^s[38]==1, 4, int(s[39])+10==15, w455w
[41:45]baytlar = 30 72 64 7d0rd}

İki dilim 4 karakterlik hash preimage'idir, dolayısıyla brute-force edilir. Bariz tahminler (such, just, r34l, n0t_) tutmuyor; MD5 ve SHA-256 hedefleri üzerinde tam yazdırılabilir-ASCII araması onları hemen buluyor:

Python
def brute(target, hashfn, charset):
    for combo in itertools.product(charset, repeat=4):
        s = "".join(combo)
        if hashfn(s.encode()).hexdigest() == target:
            return s
 
charset = [chr(i) for i in range(32, 127)]     # yazdırılabilir ASCII
# MD5  33635414851f62863a5cb7825481433d -> 'Wh4t'
# SHA256 c1cd3414aea97cfb005cf3bf3f39f3c1b5412f5dd8a2602c74a38181d94f5a2d -> '_t0_'

MNUDGY3L ise ch3ck olarak çözülüyor. Geri kalan her şey düz aritmetik ve kodlama tersleri.

5. Doğrulama

CheckPassword'ı Python'da yeniden uyguladım (elle yazılmış base32 dahil) ve adayı her dala soktum:

Text
PASS length==45
PASS prefix
PASS md5 s[4:8]
PASS hex s[8:12]
PASS b64 s[12:16]
PASS s[16:20]
PASS sha256 s[20:24]
PASS base32 s[24:29]
PASS s[29:33]
PASS s[33:37]
PASS s[37:41]
PASS s[41:]
 
FLAG: ptm{Wh4t_4_Funny_w4y_t0_ch3ck_f0R_4_p455w0rd} len 45
ALL PASS

Leet'i sesli okumak şakayı veriyor: "What a funny way to check for a password" (parolayı kontrol etmenin ne komik bir yolu). Parola bayrağın kendisi ve "kasa" sunucuda hiçbir şeyi korumuyor — tüm kontroller istemcide çalışıyor.

Bütünlük için, giriş sonrası DbManager, aynı assembly'de sabit kodlanmış kimlik bilgileriyle (giovanni / MD5'e benzeyen bir literal) insecurevault.challs.m0lecon.it:5432'ye bağlanıp Companies/Employees/Projects/Documents yüklüyor. Bu kimlik bilgileri de istemci ikilisinin içinde — uygulamanın hiçbir güven sınırı yok.

6. Çıkarımlar

  • İstemci taraflı bir parola kontrolü dekoratiftir. İstemcinin bir parolayı kabul etmesi için ihtiyaç duyduğu her şey — digestler, kodlamalar, sıralama — saldırgana gider. Yönetilen bir .NET assembly'si okunabilir C#'a decompile olur.
  • Tek bir sırrı MD5/SHA-256/base64/base32/bit hilelerine bölmek güvenlik katmaz; her parça bağımsız olarak tersinirdir ve kısa preimage'ler saniyeler içinde brute-force'a düşer.
  • Aynı assembly'de sabit kodlanmış veritabanı kimlik bilgileri, görevin adının arkasındaki asıl hata sınıfıdır: kasa, ikili dosyaya sahip olan herkese güveniyor.

7. Bayrak

ptm{Wh4t_4_Funny_w4y_t0_ch3ck_f0R_4_p455w0rd}
---
Bu yazıyı paylaş:
TwitterLinkedInFacebook

Ne düşünüyorsun?

Tepki bırakarak geri bildirim ver

İlgili Yazılar