Gönderi

ReVanced Bytecode ve Resource Patch Yazımı

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 dizininde gradle.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ı.

idea64 idea64

Ardından Proje Yapısı > Modüller kısmından patches projesini modül olarak ekleyin.

idea64 idea64

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.

idea64 idea64

Program komutları kısmını aynı terminalde CLI kullanıyormuş gibi yazıyoruz. ibaresinin değişken olabileceğine ve dizin konumuna dikkat edin. Çalışma dizini `revanced-cli` projesi olarak ayarlanmışsa ona göre dizini ayarlayın.

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.

idea64 idea64

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, sadece resources 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.

111325820532/asylum-art-erik-söderberg-fractal-experience-on

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 operasyonunu if-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ığında true 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"
        )
    }
}
Bu gönderi CC BY 4.0 lisansı altındadır.