Call of Duty: Black Ops 3, oyun kodunun Runtime'deki bütünlüğünü koruyan bir DRM ile korunmaktadır. Bu yazıda, tam olarak bunu nasıl yaptığını detaylı bir şekilde açıklayacağım, hadi başlayalım.
Giriş
Oyunun çalıştırılabilir belleğine yama yaparken üç şey olduğunu fark ettim: Bazen oyun çöküyor, bazen kilitlenmiş gibi takılıyor ve bazen de anında
İlk olarak, belki de belleğin farklı bölgelerini koruyan birden fazla bütünlük kontrolü olmalı gibi görünüyordu. Hangi kontrolün ihlal edildiğine bağlı olarak, oyun farklı bir şekilde "crashlanıyor".
Bunun doğru olması durumunda bu, bütünlük kontrollerinin birbirini koruyabileceği anlamına geliyordu. Bu nedenle bir handler eventine yama yapmak yine de çökmeye neden olacaktı çünkü farklı bir handler ihlali tespit edecekti. Bu nedenle, başarılı olmak için tüm kontrolleri yamalamak gerekir, aksi takdirde oyun yine de crash olur.
Oyun bazen kilitlendiği için, cezanın oyunun yaptığı ani bir eylem olmadığını düşündüm. Daha ziyade, bu bütünlük kontrollerinin sonucunun yürütmenin kontrol akışına entegre edildiğini varsaydım.
Yani yüksek seviyede, değerlendirme şöyle bir şey olabilir:
Bu, bütünlük kontrolünün sonucunun basitçe orijinal kodun mevcut mantıksal koşullarına göre hesaplandığı anlamına gelir. Bütünlük ihlal edilirse, yürütme bir noktada farklı bir yol izler. Bu da hemen hemen her şeye neden olabilir ancak sonunda oyunun yanlış davranmasına ve dolayısıyla crashlanmasına yol açacaktır.
Durumun gerçekten böyle olup olmadığını bilmiyorum ancak çökmelerin dökümlerine baktığımda, bütünlük kontrolünün bir parçası gibi görünen hiçbir şey bulamadım.
Bu da bana kontrol ve cezanın muhtemelen kodun farklı yerlerine dağılmış olduğunu, hatta muhtemelen farklı zamanlarda yürütüldüğünü düşündürdü.
Tüm bu varsayımlar nedeniyle aşağıdaki sorunlarla karşılaştım:
Bütünlük kontrollerini bulmak ve analiz etmek için hangi tekniği kullanabilirim?
Bunu yaptıktan sonra, ilerleme kaydedip kaydetmediğimi görmek için bunları tek tek yamalamaya nasıl başlayabilirim?
Bir analiz tekniği bulma
Kontrolleri keşfetmek ve analiz etmek için benim fikrim, kimin okuduğunu görmek için bazı kod parçalarına bir watchpoint yerleştirmekti.
Kod genellikle CPU tarafından yürütüldüğü ve aslında veri olarak okunmadığı için, böyle bir breakpoint'in tetiklenmesi, doğrulamak için kodu okuyan bir bütünlük kontrolü anlamına gelecektir.
Ancak, oyun hata ayıklayıcılara karşı bir korumaya sahip olduğundan ve bütünlük kontrollerinin kendileri nedeniyle, hata ayıklama bir seçenek değildir.
Bu nedenle, analiz için farklı bir tekniğe ihtiyacım vardı.
Sayfa izinleri
Aklıma gelen ilk şey, sayfa izinlerini kötüye kullanmaktı. Bellek sayfaları yürütülebilir, okunabilir ve yazılabilir olabilir.
Koruma ihlal edildiğinde, örneğin yürütülebilir olmayan bellek yürütüldüğünde, CPU tarafından bir erişim ihlali istisnası oluşturulur. Hata ayıklama araçları seçenek olmadığı için, enjekte edilmiş bir DLL aracılığıyla çalışma zamanında exception handler kurmak gerekir.
Teoride, bir sayfanın okuma erişimini devre dışı bırakmak, söz konusu sayfanın tüm bellek okumalarını izlemeyi mümkün kılar. Ne yazık ki, Intel CPU'ları sayfaların yazılabilir veya yürütülebilir olmasını sağlamak için aynı zamanda okunabilir olması gerektiğini desteklemiyor.
Ancak, okumalar hala sayfa izinlerinin tamamen devre dışı bırakılmasıyla izlenebilir. Bu, okumalar ve yürütmeler için istisnaların oluşturulmasına neden olur.
Bir exception handler eventinde, kötü yürütme izinleri tarafından oluşturulan tüm istisnaların geçirilebilir, böylece eksik okuma izinleri ile ilgili olanlar kalır.
Bir istisna işlendikten sonra, normal yürütme devam etmelidir. Ancak, sadece sayfa izinlerini geri yüklemek işe yaramaz. Geri yükleme işleminden sonra gerçekleşen herhangi bir bellek erişimi artık izlenemez. Bunu düzeltmek için, orijinal sayfa izinlerini geri yüklemek, yalnızca hatalı komutu yürütmek ve ardından izleme sayfa izinlerini geri yüklemek gibi bir yol izlenebilir.
Bu, Intel CPU'larının tek adımlı modunu kullanarak yapılabilir. Tek adımlı modda, CPU tam olarak bir komutu yürütür, ardından single-step exception oluşturur.
Bu nedenle, süreç şöyle aşağı yukarı şöyle olabilir:
Bu yaklaşımla ilgili olarak, Intel CPU'sunu doğrudan emüle etmek vardır. Bunun hakkında kısa bir süre düşündüm, ancak çok fazla kaynak bulamadım ve sayfa izni yaklaşımından çok daha hızlı olmayabileceği gerçeğini göz önünde bulundurarak, bu fikri de bıraktım.
Hipervizörler
Yürütmeyi okuma izinlerinden ayıramamak, sayfa izni yaklaşımını kullanışsız kılan şeydir.
Bu sınırlama Intel VT-X için geçerli değildir. Bu, Intel VT-X için bir hipervizörün, bellek okumalarını engelleme konusunda istenen davranışı elde etmek için Intel'in ikinci seviye adres çevirisi olan EPT'yi kullanabileceği anlamına gelir.
Tıpkı sıradan adres çeviri katmanındaki sayfa izinleri gibi, EPT de kendi izinler kümesine sahip ikinci bir çeviri katmanı ekler. Ancak çalıştırılabilir sayfaların okunabilir olması gerekmez (AMD bu işleme izin vermez, bu nedenle yöntem sadece Intel'de çalışır).
Bunu test etmek için hipervizörüme bir watchpoint modu entegre ettim. Çalışma şekli EPT-Hooking'in çalışma şekline benziyor. Bir işlemin sanal sayfalarını fiziksel sayfalara çevirir ve yalnızca yürütme olarak ayarlanmış izinlerle karşılık gelen EPT girişleri oluşturur. Bir EPT ihlali VM çıkışı gerçekleştiğinde, izinleri orijinal olanlara geri döndürüyorum.
Ancak sayfa izinlerinin geri yüklenmesi henüz doğru şekilde uygulanmıyor. Bunu yapmanın yolu, ihlali tetikleyen sayfa dışındaki tüm sayfalar için bir EPT ihlali üzerine tüm sayfa izinlerini sıfırlamaktır. Ne kadar az sayfa izlenirse, o kadar az geri yükleme gerçekleşir. Bu kesinlikle ideal değil, çünkü izinler yeterince kısa sürede kilitlenmezse erişimler kaçırılabilir. Tek adımlı davranışın hipervizör seviyesinde de uygulanabileceğinden eminim, muhtemelen istisna/NMI VM çıkışını ele alarak çözebiliriz. Benim uygulamamın bir başka zayıflığı da çekirdek başına bir tane yerine tek bir global sayfa tablosu kullanmamdır. Bu EPT hookları için avantajlıydı ancak okumaları izlemek için kullanıldığında çok iş parçacıklı senaryolarda potansiyel olarak ek işlemlere yol açabilir.
Donanım Breakpointleri
Donanım breakpointleri, bellek okumalarını, yazmalarını ve yürütmelerini izlemeye izin verir. Bu, CPU'nun özel hata ayıklama kayıtlarını yapılandırarak yapılabilir.
Oyun, hata ayıklama kayıtlarının etkin olup olmadığını görmek için düzenli olarak
Yine, okumalar ayrı ayrı izlenemezken, genel yürütmeyi etkilemeden yazmalarla birlikte izlenebilirler. Yürütülebilir sayfalara normalde yazılmadığından, yazmaları da izlemek önemli bir performans faktörü değildir.
Böyle bir kesme noktası tetiklendiğinde, bir hata ayıklayıcı tarafından veya bu durumda enjekte edilen DLL'nin exception handler aracılığıyla yakalanabilen tek adımlı bir istisna ortaya çıkar.
Bununla birlikte, birkaç sınırlama vardır:
Ancak, bu sınırlamalar mevcut olsa bile, oyunun bütünlük kontrollerini analiz edecek kadar güçlüydüler.
Bütünlük kontrollerini analiz etme
Artık elimde bir analiz tekniği olduğuna göre, bütünlük kontrollerini keşfetmek ve analiz etmek bir sonraki görevdi.
Bunu yapmak için kodun herhangi bir yerine bir okuma/yazma donanım breakpoint'i yerleştirdim. Aslında exception handler tetiklendi ve bu kod parçasına yol açtı:
Bu aslında bir tür 4 baytlık checksum'un hesaplanmasının bir parçası gibi görünüyor.
Benim varsayımım, bir noktada değerin değerlendirilmesi ve doğru checksum'la karşılaştırılması gerektiğiydi.
Bu konumu bulmak için
Bunu yaptığımda, handler farklı konumlarda birkaç kez tetiklendi. Örneğin bir tanesi burada:
Bu aslında ilk parçayla aynı şeyi yapar, ancak 4 bayt düzeyinde. Diğer kontroller de benzerdi, ya 1 ya da 4 baytlık bellek parçaları checksum içinde hesaplanıyordu.
Ancak, son iki break konumu çok ilginçti.
Değerlendirme
İlk olarak, sonuncusuna odaklanalım: Bunun checksum'unu değerlendiren kısım olmasını bekliyordum. İlk başta,
Breakpoint ilk kırmızı işaretli satırda tetiklenir. Bu, hesaplanan checksum'u yükler.
İkinci kırmızı işaret doğru checksum'u yükler. Her ikisi de üçüncü işaretin olduğu yerde birbiriyle karşılaştırılır.
Kod yollarının iki adresi daha sonra sonraki iki işaretin bulunduğu kayıtlara yüklenir. Kod biraz karmaşıktır, ancak son işaret karşılaştırmanın sonucuna bağlı olarak iki konumdan birini seçer. Sondaki atlama daha sonra seçilen kod yolunu çalıştırır. Bu aslında sıkıştırılmış bir if-then-else ifadesidir.
Şimdi checksum'un nasılelde edildiğine bakalım:
İlk kırmızı işaretli satır,
Doğru checksum ikinci kırmızı işarette yüklenir,
Buradan, stack frame'ın
Yamalama
Herhangi bir yama yapmayı düşünmeden önce, değerlendirmeden önce tetiklenen breakpoint'e bir göz atalım:
Breakpoint,
Bununla birlikte, son iki satır çok ilginçtir:
Bu kod çalıştırıldıktan sonra yukarıdaki değerlendirme kısmında bu bütünlük handler'ine yama yapıldığını düşünelim. Bu handler'in koruduğu oyunun kodunu zaten kurcalayabilirim. Ancak, bu parça başka bir kontrolün koruduğu bölgeye yanlış checksum yazdığı için oyun yine de çökecektir. Yani iki handler birbirine zincirlenmiş durumda. Her koruma bunu yaparsa, bu potansiyel olarak tüm handlerleri ve dolayısıyla tüm belleği birbirine zincirleyebilir.
Bu nedenle, yalnızca checksum'un değerlendirilmesini değil, aynı zamanda buradaki zincirlemeyi de düzeltmek önemlidir.
Bir kontrolün yamalanması
En kolay yol, her şey
Tek bir kontrolü invaziv olmayan bir şekilde yamamak için, exception handler'i donanım breakpoint ile yeniden kullanmaya karar verdim.
Handler, zincirleme konumunda tetiklenir tetiklenmez,
Oradan, hesaplanan checksum üzerine orijinal checksum yazdım:
Bu aslında işe yaradı. Başlangıçta donanım breakpoint'i yerleştirdiğim baytı yamalayarak, oyunun belleğini ilk kez kurcalayabildim. En azından oyun bir süre sonra yine de çökene kadar. Muhtemelen başka bir kontrol aynı bellek bölgesini koruduğu için.
Tüm kontrollerin yamalanması
Artık tek bir kontrol yamalanabildiğine göre, soru şu: tüm kontroller nasıl yamalanabilir? Yine fikir, hesaplanan checksum'u düzeltmek için tüm kontrollerin zincirleme parçalarını bağlamaktı.
Etrafa bakarken birkaç şey fark ettim:
Her şeyden önce,
Fark ettiğim ikinci şey, kodun temel blokları rastgele olarak atlamalarla birbirine bağlanan birden fazla temel bloğa bölünebilir. Yani parçalar şöyle görünebilir:
Sağlam bloklar ise şu şekilde görünür:
Bununla birlikte, kalıplar göze çarpmaya başladı:
Hızlı bir kalıp taraması yaptım ve bu iki kalıp için tüm oluşumların zincirleme bloklar gibi göründüğü ortaya çıktı.
O zaman aklıma gelen fikir, desen taraması yoluyla tüm bu konumları kancalamak ve kancalamayı düzelten ve aynı zamanda checksum'unu düzeltmeye izin veren bir assembler saplamasına yönlendirmekti. Ancak, böyle bir hook çağrılırsa, stack frame'deki bütünlük bağlamının ofsetini nasıl bulabilirim? İki şey fark ettim: Orijinal checksum her zaman
Bunun üzerine hızlı ve kirli bir sezgisel yöntem yazdım: Yığındaki ilk
Hooklarımdaki ve assembler taslaklarımdaki birkaç hatayı düzelttikten sonra, 1219 bütünlük handler'i yamalayabildim.
İşte exception handlerlerimin hata ayıklama çıktısı:
Sonuç
Bütünlük kontrollerini yamalamanın yalnızca iki kalıpla mümkün olması çok şaşırtıcı. Çok daha fazla varyasyon, çok daha fazla analiz ve farklı kalıplar gerektiren farklı bütünlük kontrolleri bekliyordum. Analiz ederken, kontrollerin zaman içinde kendilerini ayarladıklarını fark ettim. Yani oyun ne kadar uzun süre çalışırsa o kadar az uygulanıyor gibi görünüyorlar. Bu, oyunun performansını korumanın oldukça zarif bir yolu.
Bir noktada bütünlük kontrollerinin nerede başladığını bile buldum. Burası bağlamın kurulduğu yer:
Genel olarak, oyunun korunması bence çok iyi görünüyor, çünkü bütünlük kontrollerinin yanı sıra bir sürü başka koruma da uygulanıyor.
Bu da oyunun tersine mühendisliğini zor bir iş haline getiriyor...
Kaynak: momo5502.
Giriş
Oyunun çalıştırılabilir belleğine yama yaparken üç şey olduğunu fark ettim: Bazen oyun çöküyor, bazen kilitlenmiş gibi takılıyor ve bazen de anında
0 koduyla çıkıyor. Bellekteki hangi Bayt'ların yamandığına bağlı olarak, bu şeylerden biri gerçekleşiyor. Bu tür bir davranış, yaptığım bir dizi varsayıma yol açtı.İlk olarak, belki de belleğin farklı bölgelerini koruyan birden fazla bütünlük kontrolü olmalı gibi görünüyordu. Hangi kontrolün ihlal edildiğine bağlı olarak, oyun farklı bir şekilde "crashlanıyor".
Bunun doğru olması durumunda bu, bütünlük kontrollerinin birbirini koruyabileceği anlamına geliyordu. Bu nedenle bir handler eventine yama yapmak yine de çökmeye neden olacaktı çünkü farklı bir handler ihlali tespit edecekti. Bu nedenle, başarılı olmak için tüm kontrolleri yamalamak gerekir, aksi takdirde oyun yine de crash olur.
Oyun bazen kilitlendiği için, cezanın oyunun yaptığı ani bir eylem olmadığını düşündüm. Daha ziyade, bu bütünlük kontrollerinin sonucunun yürütmenin kontrol akışına entegre edildiğini varsaydım.
Yani yüksek seviyede, değerlendirme şöyle bir şey olabilir:
Kod:
if(BaziOyunKosullari && isButunlukKontroluGecerli) {
BunuYap();
} else {
BunuYap();
}
Bu, bütünlük kontrolünün sonucunun basitçe orijinal kodun mevcut mantıksal koşullarına göre hesaplandığı anlamına gelir. Bütünlük ihlal edilirse, yürütme bir noktada farklı bir yol izler. Bu da hemen hemen her şeye neden olabilir ancak sonunda oyunun yanlış davranmasına ve dolayısıyla crashlanmasına yol açacaktır.
Durumun gerçekten böyle olup olmadığını bilmiyorum ancak çökmelerin dökümlerine baktığımda, bütünlük kontrolünün bir parçası gibi görünen hiçbir şey bulamadım.
Bu da bana kontrol ve cezanın muhtemelen kodun farklı yerlerine dağılmış olduğunu, hatta muhtemelen farklı zamanlarda yürütüldüğünü düşündürdü.
Tüm bu varsayımlar nedeniyle aşağıdaki sorunlarla karşılaştım:
Bütünlük kontrollerini bulmak ve analiz etmek için hangi tekniği kullanabilirim?
Bunu yaptıktan sonra, ilerleme kaydedip kaydetmediğimi görmek için bunları tek tek yamalamaya nasıl başlayabilirim?
Bir analiz tekniği bulma
Kontrolleri keşfetmek ve analiz etmek için benim fikrim, kimin okuduğunu görmek için bazı kod parçalarına bir watchpoint yerleştirmekti.
Kod genellikle CPU tarafından yürütüldüğü ve aslında veri olarak okunmadığı için, böyle bir breakpoint'in tetiklenmesi, doğrulamak için kodu okuyan bir bütünlük kontrolü anlamına gelecektir.
Ancak, oyun hata ayıklayıcılara karşı bir korumaya sahip olduğundan ve bütünlük kontrollerinin kendileri nedeniyle, hata ayıklama bir seçenek değildir.
Bu nedenle, analiz için farklı bir tekniğe ihtiyacım vardı.
Sayfa izinleri
Aklıma gelen ilk şey, sayfa izinlerini kötüye kullanmaktı. Bellek sayfaları yürütülebilir, okunabilir ve yazılabilir olabilir.
Koruma ihlal edildiğinde, örneğin yürütülebilir olmayan bellek yürütüldüğünde, CPU tarafından bir erişim ihlali istisnası oluşturulur. Hata ayıklama araçları seçenek olmadığı için, enjekte edilmiş bir DLL aracılığıyla çalışma zamanında exception handler kurmak gerekir.
Teoride, bir sayfanın okuma erişimini devre dışı bırakmak, söz konusu sayfanın tüm bellek okumalarını izlemeyi mümkün kılar. Ne yazık ki, Intel CPU'ları sayfaların yazılabilir veya yürütülebilir olmasını sağlamak için aynı zamanda okunabilir olması gerektiğini desteklemiyor.
Ancak, okumalar hala sayfa izinlerinin tamamen devre dışı bırakılmasıyla izlenebilir. Bu, okumalar ve yürütmeler için istisnaların oluşturulmasına neden olur.
Bir exception handler eventinde, kötü yürütme izinleri tarafından oluşturulan tüm istisnaların geçirilebilir, böylece eksik okuma izinleri ile ilgili olanlar kalır.
Bir istisna işlendikten sonra, normal yürütme devam etmelidir. Ancak, sadece sayfa izinlerini geri yüklemek işe yaramaz. Geri yükleme işleminden sonra gerçekleşen herhangi bir bellek erişimi artık izlenemez. Bunu düzeltmek için, orijinal sayfa izinlerini geri yüklemek, yalnızca hatalı komutu yürütmek ve ardından izleme sayfa izinlerini geri yüklemek gibi bir yol izlenebilir.
Bu, Intel CPU'larının tek adımlı modunu kullanarak yapılabilir. Tek adımlı modda, CPU tam olarak bir komutu yürütür, ardından single-step exception oluşturur.
Bu nedenle, süreç şöyle aşağı yukarı şöyle olabilir:
- İhlali yakalayarak bellek okumasını anlık olarak izleme,
- Orijinal sayfa izinlerini geri yükleme,
- CPU'yu tek adımlı moda almak,
- Hatalı komutu yeniden çalıştırma,
- Komutun yürütülmesinden sonra tek adımlı istisnayı yakalama,
- İzleme sayfa izinlerini geri yükleme ve tek adımlı moddan çıkma.
Bu yaklaşımla ilgili olarak, Intel CPU'sunu doğrudan emüle etmek vardır. Bunun hakkında kısa bir süre düşündüm, ancak çok fazla kaynak bulamadım ve sayfa izni yaklaşımından çok daha hızlı olmayabileceği gerçeğini göz önünde bulundurarak, bu fikri de bıraktım.
Hipervizörler
Yürütmeyi okuma izinlerinden ayıramamak, sayfa izni yaklaşımını kullanışsız kılan şeydir.
Bu sınırlama Intel VT-X için geçerli değildir. Bu, Intel VT-X için bir hipervizörün, bellek okumalarını engelleme konusunda istenen davranışı elde etmek için Intel'in ikinci seviye adres çevirisi olan EPT'yi kullanabileceği anlamına gelir.
Tıpkı sıradan adres çeviri katmanındaki sayfa izinleri gibi, EPT de kendi izinler kümesine sahip ikinci bir çeviri katmanı ekler. Ancak çalıştırılabilir sayfaların okunabilir olması gerekmez (AMD bu işleme izin vermez, bu nedenle yöntem sadece Intel'de çalışır).
Bunu test etmek için hipervizörüme bir watchpoint modu entegre ettim. Çalışma şekli EPT-Hooking'in çalışma şekline benziyor. Bir işlemin sanal sayfalarını fiziksel sayfalara çevirir ve yalnızca yürütme olarak ayarlanmış izinlerle karşılık gelen EPT girişleri oluşturur. Bir EPT ihlali VM çıkışı gerçekleştiğinde, izinleri orijinal olanlara geri döndürüyorum.
Ancak sayfa izinlerinin geri yüklenmesi henüz doğru şekilde uygulanmıyor. Bunu yapmanın yolu, ihlali tetikleyen sayfa dışındaki tüm sayfalar için bir EPT ihlali üzerine tüm sayfa izinlerini sıfırlamaktır. Ne kadar az sayfa izlenirse, o kadar az geri yükleme gerçekleşir. Bu kesinlikle ideal değil, çünkü izinler yeterince kısa sürede kilitlenmezse erişimler kaçırılabilir. Tek adımlı davranışın hipervizör seviyesinde de uygulanabileceğinden eminim, muhtemelen istisna/NMI VM çıkışını ele alarak çözebiliriz. Benim uygulamamın bir başka zayıflığı da çekirdek başına bir tane yerine tek bir global sayfa tablosu kullanmamdır. Bu EPT hookları için avantajlıydı ancak okumaları izlemek için kullanıldığında çok iş parçacıklı senaryolarda potansiyel olarak ek işlemlere yol açabilir.
Donanım Breakpointleri
Donanım breakpointleri, bellek okumalarını, yazmalarını ve yürütmelerini izlemeye izin verir. Bu, CPU'nun özel hata ayıklama kayıtlarını yapılandırarak yapılabilir.
Oyun, hata ayıklama kayıtlarının etkin olup olmadığını görmek için düzenli olarak
GetThreadContext'i çağırarak bunları algılamasını sağlıyor. Bunu bağlamak ve hata ayıklama kayıtlarını temizlemek işe yaradı. GetThreadContext bir API çağrısı olduğundan ve oyunun kodunun bir parçası olmadığından, bu işlevi koruyan bir bütünlük kontrolü yoktur.Yine, okumalar ayrı ayrı izlenemezken, genel yürütmeyi etkilemeden yazmalarla birlikte izlenebilirler. Yürütülebilir sayfalara normalde yazılmadığından, yazmaları da izlemek önemli bir performans faktörü değildir.
Böyle bir kesme noktası tetiklendiğinde, bir hata ayıklayıcı tarafından veya bu durumda enjekte edilen DLL'nin exception handler aracılığıyla yakalanabilen tek adımlı bir istisna ortaya çıkar.
Bununla birlikte, birkaç sınırlama vardır:
- Hata ayıklama kayıtları bir iş parçacığı için yereldir, yani donanım breakpointleri bir işlemin her iş parçacığı için ayrı ayrı etkinleştirilmelidir.
- Intel CPU'lar aynı anda yalnızca 4 adede kadar etkin breakpoint destekler. (iş parçacığı başına)
- Bir breakpoint, yalnızca 8 bayta kadar ardışık belleği izleyebilir ancak daha fazlasını izleyemez.
GetThreadContext kullanılarak hata ayıklama kayıtlarının durumu okunarak kolayca tespit edilebilir. Hatta bazı DRM'ler yürütme akışını gizlemek için hata ayıklama kesme noktalarını gizleme mekanizması olarak kullanır. Bu da onları analiz için kullanılamaz hale getirebilir. Eski Call of Duty oyunları bunu yapardı.Ancak, bu sınırlamalar mevcut olsa bile, oyunun bütünlük kontrollerini analiz edecek kadar güçlüydüler.
Bütünlük kontrollerini analiz etme
Artık elimde bir analiz tekniği olduğuna göre, bütünlük kontrollerini keşfetmek ve analiz etmek bir sonraki görevdi.
Bunu yapmak için kodun herhangi bir yerine bir okuma/yazma donanım breakpoint'i yerleştirdim. Aslında exception handler tetiklendi ve bu kod parçasına yol açtı:
rax'ta bir bayt okunur, bu da breakpoint'imin yerleştirildiği bellektir. Bu bayt sıfıra genişletilir ve [rbp + 58h] ile xored edilir. xor sonucu daha sonra tekrar [rbp + 58h] içinde saklanır.Bu aslında bir tür 4 baytlık checksum'un hesaplanmasının bir parçası gibi görünüyor.
[rbp + 58h] ara hesaplama sonucuna işaret ediyor gibi görünüyor.Benim varsayımım, bir noktada değerin değerlendirilmesi ve doğru checksum'la karşılaştırılması gerektiğiydi.
Bu konumu bulmak için
[rbp + 58h]'ye bir breakpoint yerleştirme fikri vardı. Bunu yapmak için exception handler ayarladım. İlk breakpoint tetiklendiğinde, onu kaldırdım ve mevcut exception context'i kullanarak [rbp + 58h]'yi değerlendirdim ve oraya bir breakpoint yerleştirdim.Bunu yaptığımda, handler farklı konumlarda birkaç kez tetiklendi. Örneğin bir tanesi burada:
Bu aslında ilk parçayla aynı şeyi yapar, ancak 4 bayt düzeyinde. Diğer kontroller de benzerdi, ya 1 ya da 4 baytlık bellek parçaları checksum içinde hesaplanıyordu.
Ancak, son iki break konumu çok ilginçti.
Değerlendirme
İlk olarak, sonuncusuna odaklanalım: Bunun checksum'unu değerlendiren kısım olmasını bekliyordum. İlk başta,
[rbp + 58h] breakpoint'in bir ara bellek konumuna yerleştirilmiş olabileceğinden ve hesaplamanın başka bir yerde devam edebileceğinden ve orada izlemeye devam etmem gerektiğinden korktum. Ancak, bunun gerçek değerlendirme olduğu ortaya çıktı:Breakpoint ilk kırmızı işaretli satırda tetiklenir. Bu, hesaplanan checksum'u yükler.
İkinci kırmızı işaret doğru checksum'u yükler. Her ikisi de üçüncü işaretin olduğu yerde birbiriyle karşılaştırılır.
Kod yollarının iki adresi daha sonra sonraki iki işaretin bulunduğu kayıtlara yüklenir. Kod biraz karmaşıktır, ancak son işaret karşılaştırmanın sonucuna bağlı olarak iki konumdan birini seçer. Sondaki atlama daha sonra seçilen kod yolunu çalıştırır. Bu aslında sıkıştırılmış bir if-then-else ifadesidir.
Şimdi checksum'un nasılelde edildiğine bakalım:
İlk kırmızı işaretli satır,
mov eax, [rdx+rax*4], hesaplanan checksum'u yükler. İstisna bağlamı rax'ın sıfır olduğunu ortaya çıkarır. Bu, rdx'in hesaplanan checksum'una bir pointer içerdiği anlamına gelir. rdx'in mov rdx, [rbp+40h] aracılığıyla atandığı görülebilir.Doğru checksum ikinci kırmızı işarette yüklenir,
mov edx, [rbx+rcx*4]. Daha önce olduğu gibi, rcx sıfır olarak görünüyor. Yani rbx doğru checksum'una bir pointer'dir. rbx'in mov rbx, [rbp+48h] aracılığıyla atandığı görülebilir.Buradan, stack frame'ın
0x40 ofsetinin hesaplanan checksum'una bir pointer olduğu ve 0x48 ofsetinin doğru checksum'una pointer olduğu sonucuna varabiliriz. Bundan 0x40 ofsetinden başlayan bir C yapısı türetebiliriz:
C:
struct integrity_handler_context
{
uint32_t* computed_checksum;
uint32_t* original_checksum;
};
Yamalama
Herhangi bir yama yapmayı düşünmeden önce, değerlendirmeden önce tetiklenen breakpoint'e bir göz atalım:
Breakpoint,
mov eax, [rdx+rax*4] satırında tetiklendi. Bu da checksum'u eax'a yükledi. Tıpkı daha önce olduğu gibi, rax o noktada sıfırdı. Yani rdx buradaki ilginç değerdir. mov rdx, [rbp+38h] aracılığıyla atanır. Bu noktada, stack frame ofseti 0x38'in integrity_handler_context olduğu ortaya çıkıyor.Bununla birlikte, son iki satır çok ilginçtir:
lea rdx, dword_14259841A .text bölümünde bir yerde bir bellek adresi yükler. Son satır, mov [rdx+rcx*4], eax, checksum'unu .text bölümündeki o konumda saklıyor gibi görünüyor (rcx yine sıfır). DRM'nin Runtime'sinde kodu yeniden yazması ve şifresini çözmesi nedeniyle .text bölümünün RWX olduğunu unutmayın.Bu kod çalıştırıldıktan sonra yukarıdaki değerlendirme kısmında bu bütünlük handler'ine yama yapıldığını düşünelim. Bu handler'in koruduğu oyunun kodunu zaten kurcalayabilirim. Ancak, bu parça başka bir kontrolün koruduğu bölgeye yanlış checksum yazdığı için oyun yine de çökecektir. Yani iki handler birbirine zincirlenmiş durumda. Her koruma bunu yaparsa, bu potansiyel olarak tüm handlerleri ve dolayısıyla tüm belleği birbirine zincirleyebilir.
Bu nedenle, yalnızca checksum'un değerlendirilmesini değil, aynı zamanda buradaki zincirlemeyi de düzeltmek önemlidir.
Bir kontrolün yamalanması
En kolay yol, her şey
integrity_handler_context'te el altında olduğundan, zincirlemeden hemen önce hesaplanan checksum'un üzerine doğru checksum değerini yazmak gibi görünüyordu.Tek bir kontrolü invaziv olmayan bir şekilde yamamak için, exception handler'i donanım breakpoint ile yeniden kullanmaya karar verdim.
Handler, zincirleme konumunda tetiklenir tetiklenmez,
rbp + 38h adresindeki integrity_handler_context'e baktım.Oradan, hesaplanan checksum üzerine orijinal checksum yazdım:
C:
auto context_addr = info->ContextRecord->Rbp + 0x38;
auto* context = (integrity_handler_context*)context_addr;
context->computed_checksum = context->original_checksum;
Bu aslında işe yaradı. Başlangıçta donanım breakpoint'i yerleştirdiğim baytı yamalayarak, oyunun belleğini ilk kez kurcalayabildim. En azından oyun bir süre sonra yine de çökene kadar. Muhtemelen başka bir kontrol aynı bellek bölgesini koruduğu için.
Tüm kontrollerin yamalanması
Artık tek bir kontrol yamalanabildiğine göre, soru şu: tüm kontroller nasıl yamalanabilir? Yine fikir, hesaplanan checksum'u düzeltmek için tüm kontrollerin zincirleme parçalarını bağlamaktı.
Etrafa bakarken birkaç şey fark ettim:
Her şeyden önce,
integrity_handler_context hareket edebiliyor. Her zaman 0x38 ofsetinde bulunmuyor. Bazı kontrollerde 0x40'da, bazılarında 0x48'de vb.Fark ettiğim ikinci şey, kodun temel blokları rastgele olarak atlamalarla birbirine bağlanan birden fazla temel bloğa bölünebilir. Yani parçalar şöyle görünebilir:
Sağlam bloklar ise şu şekilde görünür:
Bununla birlikte, kalıplar göze çarpmaya başladı:
mov [rdx+rcx*4], eax, ardından blok iki parçaya ayrılmışsa bir jmp veya sağlam bloklar için bir add dword ptr komutu.Hızlı bir kalıp taraması yaptım ve bu iki kalıp için tüm oluşumların zincirleme bloklar gibi göründüğü ortaya çıktı.
O zaman aklıma gelen fikir, desen taraması yoluyla tüm bu konumları kancalamak ve kancalamayı düzelten ve aynı zamanda checksum'unu düzeltmeye izin veren bir assembler saplamasına yönlendirmekti. Ancak, böyle bir hook çağrılırsa, stack frame'deki bütünlük bağlamının ofsetini nasıl bulabilirim? İki şey fark ettim: Orijinal checksum her zaman
.text bölümünde yer alıyordu. Hesaplanan checksum her zaman stack frame üzerinde bir bölgede bulunuyordu.Bunun üzerine hızlı ve kirli bir sezgisel yöntem yazdım: Yığındaki ilk
0x80 baytı taradım. Yığına işaret eden bir pointer varsa ve hemen ardından .text'e işaret eden bir pointer varsa, bunun bir bütünlük bağlamı olması gerektiğini varsaydım. Bu kararsız gibi görünse de, gayet iyi çalıştı.Hooklarımdaki ve assembler taslaklarımdaki birkaç hatayı düzelttikten sonra, 1219 bütünlük handler'i yamalayabildim.
İşte exception handlerlerimin hata ayıklama çıktısı:
Sonuç
Bütünlük kontrollerini yamalamanın yalnızca iki kalıpla mümkün olması çok şaşırtıcı. Çok daha fazla varyasyon, çok daha fazla analiz ve farklı kalıplar gerektiren farklı bütünlük kontrolleri bekliyordum. Analiz ederken, kontrollerin zaman içinde kendilerini ayarladıklarını fark ettim. Yani oyun ne kadar uzun süre çalışırsa o kadar az uygulanıyor gibi görünüyorlar. Bu, oyunun performansını korumanın oldukça zarif bir yolu.
Bir noktada bütünlük kontrollerinin nerede başladığını bile buldum. Burası bağlamın kurulduğu yer:
Genel olarak, oyunun korunması bence çok iyi görünüyor, çünkü bütünlük kontrollerinin yanı sıra bir sürü başka koruma da uygulanıyor.
Bu da oyunun tersine mühendisliğini zor bir iş haline getiriyor...
Kaynak: momo5502.