ReVanced Bytecode ve Resource Patch Yazımı
Uygulamaların üzerinde değişiklik yapabiliyoruz ama bunlar sadece değiştirilen Android paketi halinde kapalı bir şekilde var oluyor. Yapılan değişiklikleri göstersek dahi, paylaşılan paket dosyasının sadece o değişikliklere sahip olduğu kesin değil. Bir ReVanced yaması, bir değişikliğin nasıl ve nereyi etkilediğini açık bir şekilde gösterir ve herkes yamaları uygulayabilir. Öncelikle, geliştirme ortamını hazırlayalım.
Araçlar
Geliştirme Ortamı
Bir dizin oluşturup revanced-cli
ve revanced-patches
depolarını klonlayın.
1
2
3
4
5
6
7
8
9
10
11
12
mkdir revanced && cd revanced
repositories=(
"revanced-cli"
"revanced-patches"
"revanced-integrations" (isteğe bağlı)
"revanced-patcher" (isteğe bağlı)
)
for repository in "${repositories[@]}" ; do
git clone -b dev --single-branch --depth 1 https://github.com/revanced/$repository
done
Patches projesinin dizininde ./gradlew build
komutunu çalıştırın.
Authentication hatası alıyorsanız bağlantıdan
read:packages
iznine sahip bir token oluşturun. Verilen tokeni kullanıcı dosyasında.gradle
dizinindegradle.properties
içerisine (yoksa oluşturarak) ekleyin.
1
2
gpr.user = <github kullanıcı adı>
gpr.key = <token>
API check failed for project revanced-patches hatası alırsanız
./gradlew apidump
komutunu çalıştırıp tekrar build edin.
BUILD SUCCESSFUL yazısını gördüyseniz kurulum tamamlandı.
IntelliJ’de Kurulum
IDEA’da CLI projesi açılıp Gradle senkronizasyon işlemi tamamlandıktan sonra MainCommand.kt
dosyasında üçgen sembolü gözüküyor olmalı.
Ardından Proje Yapısı > Modüller kısmından patches projesini modül olarak ekleyin.
Konfigürasyon
Pencereyi kapattıktan sonra Çalıştırma/Ayıklama Konfigürasyonları ayarlarından yeni bir Kotlin konfigürasyonu oluşturun. Ana sınıfı app.revanced.cli.command.MainCommandKt
olarak ayarlayın.
Program komutları kısmını aynı terminalde CLI kullanıyormuş gibi yazıyoruz.
1
2
3
patch
-p ..\revanced-patches\patches\build\libs\patches-<version>.rvp
..\to-be-patched.apk
Gradle türünde bir Before launch komutu ekleyin. Proje olarak patches projesini seçin ve komutlar kısmına build
yazın.
Debug Süreci
Projedeki patchlerden birine debug noktası koyarak düzgün çalışmakta olduğunu kontrol edin. Konfigürasyonda to-be-patched.apk
yerine patchlemek istediğiniz Android paketi ismi ve patches
projesinin versiyonu doğru şekilde yazılmış olmalı. Ben revanced.app sitesindeki önerilen versiyonu kullanacağım, dolayısıyla konfigürasyon şu şekilde. Ayrıca APK nodpi olmalıdır.
1
2
3
patch
-b ..\revanced-patches\patches\build\libs\patches-5.2.1-dev.5.rvp
..\com.google.android.youtube_19.47.53.apk
Patchlemekte olduğunuz uygulamaya uygun olanlardan birine debug noktası koyup başarılı bir şekilde durduğunu gördüyseniz başka bir şeye ihtiyaç kalmadı.
Patch Anatomisi
Patchlerin çoğu BytecodePatch ve ResourcePatch yöntemini kullanır. Bytecode, uygulamanın smali kodunda; Resource, Android’in uygulama kaynakları (resources.arsc)
kısmında değişiklik yapmaya yarar. İkisi birlikte de kullanılabilir (özel ayarlara sahip olan patchlerde bunu görebilirsiniz). Sonradan eklenen Hex patch desteğiyse byte değerleri üzerinde değişikliği mümkün kılar. Byte seviyesinde değişiklik, uygulamanın farklı dillerde yazılmış decode edilemeyen kütüphaneler (native library) kullanması durumunda gerekiyor. Örneğin Unity IL2CPP yöntemini getirerek oyunların incelenmesini zorlaştırmıştır.
RawResourcePatch
benzer isimdeki yöntemle aynı işe yarıyor, sadeceresources
dosyasını decode etmeden daha hızlı bir şekilde patchlemek için mevcut.
Bu rehberde temel düzeyinde Bytecode ve Resource patch işlemini anlatacağım.
Parmak İzi
Patchleme işlemi gerçekleşmeden önce yapılması gereken, hedef dosyanın/fonksiyonun/satırların nerede olduğunu belirtmektir. Bu noktada parmak izi yöntemi devreye girer. Bir fonksiyona ait dönüş değeri (void, int)
, erişim belirteçleri (PUBLIC, STATIC)
ve aldığı parametreler gibi değerleri belirterek, değiştireceğimiz noktayı hedefleriz.
Şu şekilde değerler belirtmiş olsaydık:
1
2
3
returnType = "V",
access = AccessFlags.PUBLIC,
parameters = listOf("Z"),
Bu izden anlaşılan; void
değer döndüren, PUBLIC
erişimli ve BOOLEAN
(smali kodunda Z) türünde parametre alan bir fonksiyon hedef alınmıştır. Geri dönen fingerprint
türündeki nesne BytecodePatch constructor fonksiyonuna içerisine parametre olarak sunulur.
1
2
3
4
5
object SomePatch : BytecodePatch(
setOf(SomeFingerprint)
) {
// ...
}
Ek bir bilgi olarak, parmak izi yöntemlerinin verimlilik sıralaması şu şekilde:
- En Hızlı:
[strings]
belirteciyle. Verilen dizelerden en az biri tam eşleşme sağlamalıdır. - Daha Hızlı:
[accessFlags]
,[returnType]
ve[parameters]
seçenekleri sağlanarak. - Hızlı:
[accessFlags]
ve[returnType]
kombinasyonu. - En Yavaş: Sadece
[custom]
ve[opcodes]
kullanarak.
İz yazarken istediğiniz parametreleri kullanabilirsiniz.
Patch İskeleti
Patch constructor fonksiyonunda ve içerisinde metadata bilgileri belirtilir.
1
2
3
4
5
6
7
8
9
10
val `patchName` = bytecodePatch(
name = "Some patch",
description = "Does some thing.",
) {
compatibleWith("com.some.app")
execute {
...
}
}
name
: Patch ismi. Belirteç olarak kullanılır. İsimsiz olursa PatchBundleLoader tarafından tanınmaz ama diğer patchler bağlılık olarak kullanabilir.description
: Patch açıklaması.compatibleWith
: Uygulamanın paket adı.
Dizin Yapısı
Patchler, revanced-patches/src/main/kotlin/app/revanced/patches/<uygulama-adı>
şeklinde düzene koyulur. Patch ismi işlevinden gelmektedir. Objektif bir dilde açıklama yazılır (örn. ‘Shorts butonunu gizler’). Parmak izi, olabildiğince az ve öz yazılır. Verilen izin, birçok sürümde geçerli olacak şekilde oluşturulması patchi daha erişilebilir ve kapsamlı hale getirecektir.
Patch Hazırlanışı
Rehberde kullanılmak üzere yazdığım ufak bir patchme
uygulaması bulunmakta. Bir metin kutusu ve butondan oluşuyor. İstenilen parolayı girdiğinizde doğru olduğunu belirten bir toast mesajı çıkarıyor. Bu uygulamayı patchleyerek her parolayı doğru kabul etmesini sağlayacağız.
JADX ile İnceleme
Bağlantıdan patchme.apk
dosyasını indirin.
Uygulamayı JADX ile açın. dev.seaque.patchme
paketinin MainActivity
dosyasında mantık kısmı bulunmakta. Java kodları gözüküyorsa alttan Smali sekmesine geçiş yapmanız gerekiyor.
.method isUnlocked(Ljava/lang/String;)Z
.registers 3
.param p1, "input" # Ljava/lang/String;
.line 39
invoke-virtual {p0}, Ldev/seaque/patchme/MainActivity;->generateRandomString()Ljava/lang/String;
move-result-object v0
if-ne p1, v0, :cond_8
.line 40
const/4 v0, 0x1
return v0
.line 42
:cond_8
const/4 v0, 0x0
return v0
.end method
Dalvik operasyon kodları listesine bakarsak if-ne vx,vy,target
kodunun açıklamasında “vx!=vy2 ise hedefe atlar.” yazıyor. Basitçe virgülün soluyla sağı eşit değilse hedefe atlıyor, buradaki hedef :cond_8
. Hedefe atladıktan sonra v0
registerına 0x0 değerinin geçirildiğini ve son olarak fonksiyonda döndürüldüğünü görüyoruz. Yani girilen dize, uygulamanın anahtarıyla eşit değilse 0 (false) değeri döndürülüyor. Burada yapılabilecek hamleyi az çok anlamış olabilirsiniz. Bir mekanizmayı kırmanın çoğunlukla birden fazla yolu vardır. Düşünürken şu güzel fraktal gifine bakabilirsiniz.
Smali Manipülasyonu
Burada mısınız? Devam edelim. Kendimizi yormayalım ve en basit çözümü uygulayalım. Fonksiyonun en sonunda false
değeri dönen kısımda true
döndürelim, son çalışacak satırlar bunlar olacağı için yukarda neler olduğunun hiçbir önemi kalmayacak.
Uygulamayı apktool d --no-res patchme.apk
ile açtıktan sonra smali_classesN/dev/seaque/patchme/MainActivity.smali dosyasını açıp sadece :cond_8
altındaki const/4 v0, 0x0
satırını 0x0
yerine 0x1
(1) yapıyorum ve build ediyorum. Uygulamayı açıp gönder butonuna basmanız yeterli, herhangi bir dize girmeye bile gerek kalmadı.
Diğer bir çözüm,
if-ne
operasyonunuif-eq
yapmak. Bu durumda kullanıcı hangi değeri girerse girsin program eşit kabul ederek:cond_8
hedefine atlamayacak ve devamındaki satırlar çalıştığındatrue
değeri dönecekti. Diğer farklı bir çözümse, butona basıldığında derleme anında oluşan dizeyi ekrana yazdırmak.
Parmak İzi Yazımı
Yama kısmını oluşturmadan önce parmak izini yazmamız gerekiyor. isUnlocked
fonksiyonunu düşünürsek, boolean
türünde değişken döndüren, string
parametresine sahip bir fonksiyonu hedeflersek denk gelir gibi.
Anlatıcı: Denk gelmedi.
1
2
3
4
5
6
7
8
9
internal val isUnlockedFingerPrint = fingerprint {
returns("Z")
parameters("Ljava/lang/String;")
opcodes(
Opcode.INVOKE_VIRTUAL,
Opcode.MOVE_RESULT_OBJECT,
Opcode.IF_NE
)
}
IDEA’da debug edersem parmak izinin doğru fonksiyonu bulduğunu görebilirim. Fakat opcode sıralamasına dayalı bu parmak izi, uzun vadede pek iyi değil. Örnek uygulama sade olduğundan yakalamak için çok şey belirtmek gerekti. O yüzden şimdilik customFingerprint
kullanarak direkt sınıfın konumunu vererek izi yazacağım. Custom izi diğer seçeneklerle birlikte de kullanabilirsiniz.
1
2
3
custom { method, _ ->
method.name == "isUnlocked" && method.definingClass == "Ldev/seaque/patchme/MainActivity;"
}
Patch Yazımı
Patch dosyasının tamamı bu kadar. ReVanced fonksiyonlarından replaceInstruction
kullanarak satırda değişiklik yapıyoruz.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package app.revanced.patches.patchme
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patcher.patch.bytecodePatch
@Suppress("unused")
val unlockApp = bytecodePatch(
name = "Unlock app",
description = "Unlocks app.",
) {
compatibleWith("dev.seaque.patchme")
execute {
isUnlockedFingerPrint.method.replaceInstruction(
1,
"const/4 v0, 0x1"
)
}
}