Ψάχνετε για τρωτά σημεία στο πρόγραμμα περιήγησης UC

Ψάχνετε για τρωτά σημεία στο πρόγραμμα περιήγησης UC

Εισαγωγή

Στα τέλη Μαρτίου εμείς έχουν αναφερθεί, ότι ανακάλυψαν μια κρυφή δυνατότητα φόρτωσης και εκτέλεσης μη επαληθευμένου κώδικα στο πρόγραμμα περιήγησης UC. Σήμερα θα εξετάσουμε λεπτομερώς πώς συμβαίνει αυτή η λήψη και πώς μπορούν οι χάκερ να τη χρησιμοποιήσουν για τους δικούς τους σκοπούς.

Πριν από λίγο καιρό, το UC Browser διαφημίστηκε και διανεμήθηκε πολύ επιθετικά: εγκαταστάθηκε στις συσκευές των χρηστών χρησιμοποιώντας κακόβουλο λογισμικό, διανεμήθηκε από διάφορους ιστότοπους υπό το πρόσχημα αρχείων βίντεο (δηλαδή, οι χρήστες νόμιζαν ότι κατέβαζαν, για παράδειγμα, ένα πορνό βίντεο, αλλά αντ' αυτού έλαβε ένα APK με αυτό το πρόγραμμα περιήγησης), χρησιμοποίησε τρομακτικά πανό με μηνύματα ότι το πρόγραμμα περιήγησης ήταν ξεπερασμένο, ευάλωτο και άλλα παρόμοια. Στην επίσημη ομάδα UC Browser στο VK υπάρχει θέμα, όπου οι χρήστες μπορούν να παραπονεθούν για αθέμιτη διαφήμιση, υπάρχουν πολλά παραδείγματα εκεί. Το 2016 υπήρξε ακόμη διαφήμιση βίντεο στα ρωσικά (ναι, διαφήμιση για πρόγραμμα περιήγησης αποκλεισμού διαφημίσεων).

Τη στιγμή της σύνταξης, το UC Browser έχει πάνω από 500 εγκαταστάσεις στο Google Play. Αυτό είναι εντυπωσιακό - μόνο το Google Chrome έχει περισσότερα. Μεταξύ των κριτικών μπορείτε να δείτε αρκετά παράπονα σχετικά με διαφημίσεις και ανακατευθύνσεις σε ορισμένες εφαρμογές στο Google Play. Αυτός ήταν ο λόγος της έρευνάς μας: αποφασίσαμε να δούμε αν το UC Browser έκανε κάτι κακό. Και αποδείχθηκε ότι το κάνει!

Στον κώδικα της εφαρμογής, ανακαλύφθηκε η δυνατότητα λήψης και εκτέλεσης εκτελέσιμου κώδικα, που αντίκειται στους κανόνες δημοσίευσης αιτήσεων στο Google Play. Εκτός από τη λήψη εκτελέσιμου κώδικα, το UC Browser το κάνει με μη ασφαλή τρόπο, ο οποίος μπορεί να χρησιμοποιηθεί για την εκτόξευση μιας επίθεσης MitM. Ας δούμε αν μπορούμε να κάνουμε μια τέτοια επίθεση.

Όλα όσα γράφονται παρακάτω σχετίζονται με την έκδοση του προγράμματος περιήγησης UC που ήταν διαθέσιμη στο Google Play τη στιγμή της μελέτης:

package: com.UCMobile.intl
versionName: 12.10.8.1172
versionCode: 10598
sha1 APK-файла: f5edb2243413c777172f6362876041eb0c3a928c

Διάνυσμα επίθεσης

Στο μανιφέστο του προγράμματος περιήγησης UC μπορείτε να βρείτε μια υπηρεσία με ένα αυτονόητο όνομα com.uc.deployment.UpgradeDeployService.

    <service android_exported="false" android_name="com.uc.deployment.UpgradeDeployService" android_process=":deploy" />

Όταν ξεκινά αυτή η υπηρεσία, το πρόγραμμα περιήγησης κάνει ένα αίτημα POST puds.ucweb.com/upgrade/index.xhtml, το οποίο μπορεί να δει κανείς στην κίνηση λίγη ώρα μετά την εκκίνηση. Σε απάντηση, μπορεί να λάβει μια εντολή για λήψη κάποιας ενημέρωσης ή νέας λειτουργικής μονάδας. Κατά την ανάλυση, ο διακομιστής δεν έδωσε τέτοιες εντολές, αλλά παρατηρήσαμε ότι όταν προσπαθούμε να ανοίξουμε ένα PDF στο πρόγραμμα περιήγησης, κάνει ένα δεύτερο αίτημα στη διεύθυνση που καθορίζεται παραπάνω, μετά την οποία κατεβάζει την εγγενή βιβλιοθήκη. Για να πραγματοποιήσουμε την επίθεση, αποφασίσαμε να χρησιμοποιήσουμε αυτήν τη δυνατότητα του προγράμματος περιήγησης UC: τη δυνατότητα ανοίγματος PDF χρησιμοποιώντας μια εγγενή βιβλιοθήκη, η οποία δεν βρίσκεται στο APK και την οποία κατεβάζει από το Διαδίκτυο εάν είναι απαραίτητο. Αξίζει να σημειωθεί ότι, θεωρητικά, το UC Browser μπορεί να αναγκαστεί να κατεβάσει κάτι χωρίς αλληλεπίδραση με τον χρήστη - εάν παρέχετε μια καλά διαμορφωμένη απάντηση σε ένα αίτημα που εκτελείται μετά την εκκίνηση του προγράμματος περιήγησης. Αλλά για να γίνει αυτό, πρέπει να μελετήσουμε λεπτομερέστερα το πρωτόκολλο αλληλεπίδρασης με τον διακομιστή, επομένως αποφασίσαμε ότι θα ήταν ευκολότερο να επεξεργαστούμε την υποκλαπόμενη απόκριση και να αντικαταστήσουμε τη βιβλιοθήκη για εργασία με PDF.

Έτσι, όταν ένας χρήστης θέλει να ανοίξει ένα PDF απευθείας στο πρόγραμμα περιήγησης, τα ακόλουθα αιτήματα μπορούν να φανούν στην κυκλοφορία:

Ψάχνετε για τρωτά σημεία στο πρόγραμμα περιήγησης UC

Πρώτα υπάρχει ένα αίτημα POST για puds.ucweb.com/upgrade/index.xhtml, μετά από αυτό
Γίνεται λήψη ενός αρχείου με βιβλιοθήκη για προβολή μορφών PDF και γραφείου. Είναι λογικό να υποθέσουμε ότι το πρώτο αίτημα μεταδίδει πληροφορίες σχετικά με το σύστημα (τουλάχιστον την αρχιτεκτονική για την παροχή της απαιτούμενης βιβλιοθήκης) και ως απάντηση σε αυτό το πρόγραμμα περιήγησης λαμβάνει ορισμένες πληροφορίες σχετικά με τη βιβλιοθήκη που πρέπει να ληφθεί: τη διεύθυνση και, πιθανώς , κάτι άλλο. Το πρόβλημα είναι ότι αυτό το αίτημα είναι κρυπτογραφημένο.

Αίτημα απόσπασμα

Απόσπασμα απάντησης

Ψάχνετε για τρωτά σημεία στο πρόγραμμα περιήγησης UC

Ψάχνετε για τρωτά σημεία στο πρόγραμμα περιήγησης UC

Η ίδια η βιβλιοθήκη είναι συσκευασμένη σε ZIP και δεν είναι κρυπτογραφημένη.

Ψάχνετε για τρωτά σημεία στο πρόγραμμα περιήγησης UC

Αναζήτηση κωδικού αποκρυπτογράφησης κυκλοφορίας

Ας προσπαθήσουμε να αποκρυπτογραφήσουμε την απόκριση του διακομιστή. Ας δούμε τον κωδικό τάξης com.uc.deployment.UpgradeDeployService: από τη μέθοδο onStartCommand παω σε com.uc.deployment.bx, και από αυτό έως com.uc.browser.core.dcfe:

    public final void e(l arg9) {
int v4_5;
String v3_1;
byte[] v3;
byte[] v1 = null;
if(arg9 == null) {
v3 = v1;
}
else {
v3_1 = arg9.iGX.ipR;
StringBuilder v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]product:");
v4.append(arg9.iGX.ipR);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]version:");
v4.append(arg9.iGX.iEn);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]upgrade_type:");
v4.append(arg9.iGX.mMode);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]force_flag:");
v4.append(arg9.iGX.iEo);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]silent_mode:");
v4.append(arg9.iGX.iDQ);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]silent_type:");
v4.append(arg9.iGX.iEr);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]silent_state:");
v4.append(arg9.iGX.iEp);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]silent_file:");
v4.append(arg9.iGX.iEq);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]apk_md5:");
v4.append(arg9.iGX.iEl);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]download_type:");
v4.append(arg9.mDownloadType);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]download_group:");
v4.append(arg9.mDownloadGroup);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]download_path:");
v4.append(arg9.iGH);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]apollo_child_version:");
v4.append(arg9.iGX.iEx);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]apollo_series:");
v4.append(arg9.iGX.iEw);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]apollo_cpu_arch:");
v4.append(arg9.iGX.iEt);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]apollo_cpu_vfp3:");
v4.append(arg9.iGX.iEv);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]apollo_cpu_vfp:");
v4.append(arg9.iGX.iEu);
ArrayList v3_2 = arg9.iGX.iEz;
if(v3_2 != null && v3_2.size() != 0) {
Iterator v3_3 = v3_2.iterator();
while(v3_3.hasNext()) {
Object v4_1 = v3_3.next();
StringBuilder v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]component_name:");
v5.append(((au)v4_1).getName());
v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]component_ver_name:");
v5.append(((au)v4_1).aDA());
v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]component_ver_code:");
v5.append(((au)v4_1).gBl);
v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]component_req_type:");
v5.append(((au)v4_1).gBq);
}
}
j v3_4 = new j();
m.b(v3_4);
h v4_2 = new h();
m.b(v4_2);
ay v5_1 = new ay();
v3_4.hS("");
v3_4.setImsi("");
v3_4.hV("");
v5_1.bPQ = v3_4;
v5_1.bPP = v4_2;
v5_1.yr(arg9.iGX.ipR);
v5_1.gBF = arg9.iGX.mMode;
v5_1.gBI = arg9.iGX.iEz;
v3_2 = v5_1.gAr;
c.aBh();
v3_2.add(g.fs("os_ver", c.getRomInfo()));
v3_2.add(g.fs("processor_arch", com.uc.b.a.a.c.getCpuArch()));
v3_2.add(g.fs("cpu_arch", com.uc.b.a.a.c.Pb()));
String v4_3 = com.uc.b.a.a.c.Pd();
v3_2.add(g.fs("cpu_vfp", v4_3));
v3_2.add(g.fs("net_type", String.valueOf(com.uc.base.system.a.Jo())));
v3_2.add(g.fs("fromhost", arg9.iGX.iEm));
v3_2.add(g.fs("plugin_ver", arg9.iGX.iEn));
v3_2.add(g.fs("target_lang", arg9.iGX.iEs));
v3_2.add(g.fs("vitamio_cpu_arch", arg9.iGX.iEt));
v3_2.add(g.fs("vitamio_vfp", arg9.iGX.iEu));
v3_2.add(g.fs("vitamio_vfp3", arg9.iGX.iEv));
v3_2.add(g.fs("plugin_child_ver", arg9.iGX.iEx));
v3_2.add(g.fs("ver_series", arg9.iGX.iEw));
v3_2.add(g.fs("child_ver", r.aVw()));
v3_2.add(g.fs("cur_ver_md5", arg9.iGX.iEl));
v3_2.add(g.fs("cur_ver_signature", SystemHelper.getUCMSignature()));
v3_2.add(g.fs("upgrade_log", i.bjt()));
v3_2.add(g.fs("silent_install", String.valueOf(arg9.iGX.iDQ)));
v3_2.add(g.fs("silent_state", String.valueOf(arg9.iGX.iEp)));
v3_2.add(g.fs("silent_file", arg9.iGX.iEq));
v3_2.add(g.fs("silent_type", String.valueOf(arg9.iGX.iEr)));
v3_2.add(g.fs("cpu_archit", com.uc.b.a.a.c.Pc()));
v3_2.add(g.fs("cpu_set", SystemHelper.getCpuInstruction()));
boolean v4_4 = v4_3 == null || !v4_3.contains("neon") ? false : true;
v3_2.add(g.fs("neon", String.valueOf(v4_4)));
v3_2.add(g.fs("cpu_cores", String.valueOf(com.uc.b.a.a.c.Jl())));
v3_2.add(g.fs("ram_1", String.valueOf(com.uc.b.a.a.h.Po())));
v3_2.add(g.fs("totalram", String.valueOf(com.uc.b.a.a.h.OL())));
c.aBh();
v3_2.add(g.fs("rom_1", c.getRomInfo()));
v4_5 = e.getScreenWidth();
int v6 = e.getScreenHeight();
StringBuilder v7 = new StringBuilder();
v7.append(v4_5);
v7.append("*");
v7.append(v6);
v3_2.add(g.fs("ss", v7.toString()));
v3_2.add(g.fs("api_level", String.valueOf(Build$VERSION.SDK_INT)));
v3_2.add(g.fs("uc_apk_list", SystemHelper.getUCMobileApks()));
Iterator v4_6 = arg9.iGX.iEA.entrySet().iterator();
while(v4_6.hasNext()) {
Object v6_1 = v4_6.next();
v3_2.add(g.fs(((Map$Entry)v6_1).getKey(), ((Map$Entry)v6_1).getValue()));
}
v3 = v5_1.toByteArray();
}
if(v3 == null) {
this.iGY.iGI.a(arg9, "up_encode", "yes", "fail");
return;
}
v4_5 = this.iGY.iGw ? 0x1F : 0;
if(v3 == null) {
}
else {
v3 = g.i(v4_5, v3);
if(v3 == null) {
}
else {
v1 = new byte[v3.length + 16];
byte[] v6_2 = new byte[16];
Arrays.fill(v6_2, 0);
v6_2[0] = 0x5F;
v6_2[1] = 0;
v6_2[2] = ((byte)v4_5);
v6_2[3] = -50;
System.arraycopy(v6_2, 0, v1, 0, 16);
System.arraycopy(v3, 0, v1, 16, v3.length);
}
}
if(v1 == null) {
this.iGY.iGI.a(arg9, "up_encrypt", "yes", "fail");
return;
}
if(TextUtils.isEmpty(this.iGY.mUpgradeUrl)) {
this.iGY.iGI.a(arg9, "up_url", "yes", "fail");
return;
}
StringBuilder v0 = new StringBuilder("[");
v0.append(arg9.iGX.ipR);
v0.append("]url:");
v0.append(this.iGY.mUpgradeUrl);
com.uc.browser.core.d.c.i v0_1 = this.iGY.iGI;
v3_1 = this.iGY.mUpgradeUrl;
com.uc.base.net.e v0_2 = new com.uc.base.net.e(new com.uc.browser.core.d.c.i$a(v0_1, arg9));
v3_1 = v3_1.contains("?") ? v3_1 + "&dataver=pb" : v3_1 + "?dataver=pb";
n v3_5 = v0_2.uc(v3_1);
m.b(v3_5, false);
v3_5.setMethod("POST");
v3_5.setBodyProvider(v1);
v0_2.b(v3_5);
this.iGY.iGI.a(arg9, "up_null", "yes", "success");
this.iGY.iGI.b(arg9);
}

Βλέπουμε τον σχηματισμό αιτήματος POST εδώ. Προσέχουμε τη δημιουργία πίνακα 16 byte και την πλήρωσή του: 0x5F, 0, 0x1F, -50 (=0xCE). Συμπίπτει με αυτό που είδαμε στο παραπάνω αίτημα.

Στην ίδια τάξη μπορείτε να δείτε μια ένθετη κλάση που έχει μια άλλη ενδιαφέρουσα μέθοδο:

        public final void a(l arg10, byte[] arg11) {
f v0 = this.iGQ;
StringBuilder v1 = new StringBuilder("[");
v1.append(arg10.iGX.ipR);
v1.append("]:UpgradeSuccess");
byte[] v1_1 = null;
if(arg11 == null) {
}
else if(arg11.length < 16) {
}
else {
if(arg11[0] != 0x60 && arg11[3] != 0xFFFFFFD0) {
goto label_57;
}
int v3 = 1;
int v5 = arg11[1] == 1 ? 1 : 0;
if(arg11[2] != 1 && arg11[2] != 11) {
if(arg11[2] == 0x1F) {
}
else {
v3 = 0;
}
}
byte[] v7 = new byte[arg11.length - 16];
System.arraycopy(arg11, 16, v7, 0, v7.length);
if(v3 != 0) {
v7 = g.j(arg11[2], v7);
}
if(v7 == null) {
goto label_57;
}
if(v5 != 0) {
v1_1 = g.P(v7);
goto label_57;
}
v1_1 = v7;
}
label_57:
if(v1_1 == null) {
v0.iGY.iGI.a(arg10, "up_decrypt", "yes", "fail");
return;
}
q v11 = g.b(arg10, v1_1);
if(v11 == null) {
v0.iGY.iGI.a(arg10, "up_decode", "yes", "fail");
return;
}
if(v0.iGY.iGt) {
v0.d(arg10);
}
if(v0.iGY.iGo != null) {
v0.iGY.iGo.a(0, ((o)v11));
}
if(v0.iGY.iGs) {
v0.iGY.a(((o)v11));
v0.iGY.iGI.a(v11, "up_silent", "yes", "success");
v0.iGY.iGI.a(v11);
return;
}
v0.iGY.iGI.a(v11, "up_silent", "no", "success");
}
}

Η μέθοδος παίρνει έναν πίνακα byte ως είσοδο και ελέγχει ότι το μηδενικό byte είναι 0x60 ή το τρίτο byte είναι 0xD0 και το δεύτερο byte είναι 1, 11 ή 0x1F. Εξετάζουμε την απάντηση από τον διακομιστή: το μηδενικό byte είναι 0x60, το δεύτερο είναι 0x1F, το τρίτο είναι 0x60. Ακούγεται σαν αυτό που χρειαζόμαστε. Κρίνοντας από τις γραμμές ("up_decrypt", για παράδειγμα), θα πρέπει να κληθεί εδώ μια μέθοδος που θα αποκρυπτογραφήσει την απόκριση του διακομιστή.
Ας προχωρήσουμε στη μέθοδο gj. Σημειώστε ότι το πρώτο όρισμα είναι το byte στο offset 2 (δηλαδή 0x1F στην περίπτωσή μας) και το δεύτερο είναι η απόκριση διακομιστή χωρίς
τα πρώτα 16 byte.

     public static byte[] j(int arg1, byte[] arg2) {
if(arg1 == 1) {
arg2 = c.c(arg2, c.adu);
}
else if(arg1 == 11) {
arg2 = m.aF(arg2);
}
else if(arg1 != 0x1F) {
}
else {
arg2 = EncryptHelper.decrypt(arg2);
}
return arg2;
}

Προφανώς, εδώ επιλέγουμε έναν αλγόριθμο αποκρυπτογράφησης και το ίδιο byte που υπάρχει στο δικό μας
περίπτωση ίση με 0x1F, υποδηλώνει μία από τις τρεις πιθανές επιλογές.

Συνεχίζουμε να αναλύουμε τον κώδικα. Μετά από μερικά άλματα βρισκόμαστε σε μια μέθοδο με ένα αυτονόητο όνομα decryptBytesByKey.

Εδώ διαχωρίζονται δύο ακόμη byte από την απάντησή μας και λαμβάνεται μια συμβολοσειρά από αυτά. Είναι σαφές ότι με αυτόν τον τρόπο επιλέγεται το κλειδί για την αποκρυπτογράφηση του μηνύματος.

    private static byte[] decryptBytesByKey(byte[] bytes) {
byte[] v0 = null;
if(bytes != null) {
try {
if(bytes.length < EncryptHelper.PREFIX_BYTES_SIZE) {
}
else if(bytes.length == EncryptHelper.PREFIX_BYTES_SIZE) {
return v0;
}
else {
byte[] prefix = new byte[EncryptHelper.PREFIX_BYTES_SIZE];  // 2 байта
System.arraycopy(bytes, 0, prefix, 0, prefix.length);
String keyId = c.ayR().d(ByteBuffer.wrap(prefix).getShort()); // Выбор ключа
if(keyId == null) {
return v0;
}
else {
a v2 = EncryptHelper.ayL();
if(v2 == null) {
return v0;
}
else {
byte[] enrypted = new byte[bytes.length - EncryptHelper.PREFIX_BYTES_SIZE];
System.arraycopy(bytes, EncryptHelper.PREFIX_BYTES_SIZE, enrypted, 0, enrypted.length);
return v2.l(keyId, enrypted);
}
}
}
}
catch(SecException v7_1) {
EncryptHelper.handleDecryptException(((Throwable)v7_1), v7_1.getErrorCode());
return v0;
}
catch(Throwable v7) {
EncryptHelper.handleDecryptException(v7, 2);
return v0;
}
}
return v0;
}

Κοιτάζοντας μπροστά, σημειώνουμε ότι σε αυτό το στάδιο δεν λαμβάνουμε ακόμη κλειδί, αλλά μόνο το «αναγνωριστικό» του. Η απόκτηση του κλειδιού είναι λίγο πιο περίπλοκη.

Στην επόμενη μέθοδο, προστίθενται δύο ακόμη παράμετροι στις υπάρχουσες, κάνοντας τέσσερις από αυτές: τον μαγικό αριθμό 16, το αναγνωριστικό κλειδιού, τα κρυπτογραφημένα δεδομένα και μια ακατανόητη συμβολοσειρά (στην περίπτωσή μας, κενή).

    public final byte[] l(String keyId, byte[] encrypted) throws SecException {
return this.ayJ().staticBinarySafeDecryptNoB64(16, keyId, encrypted, "");
}

Μετά από μια σειρά από μεταβάσεις φτάνουμε στη μέθοδο staticBinarySafeDecryptNoB64 διεπαφή com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent. Δεν υπάρχουν κλάσεις στον κύριο κώδικα εφαρμογής που υλοποιούν αυτήν τη διεπαφή. Υπάρχει μια τέτοια κλάση στο αρχείο lib/armeabi-v7a/libsgmain.so, το οποίο στην πραγματικότητα δεν είναι ένα .so, αλλά ένα .βάζο. Η μέθοδος που μας ενδιαφέρει υλοποιείται ως εξής:

package com.alibaba.wireless.security.a.i;
// ...
public class a implements IStaticDataEncryptComponent {
private ISecurityGuardPlugin a;
// ...
private byte[] a(int mode, int magicInt, int xzInt, String keyId, byte[] encrypted, String magicString) {
return this.a.getRouter().doCommand(10601, new Object[]{Integer.valueOf(mode), Integer.valueOf(magicInt), Integer.valueOf(xzInt), keyId, encrypted, magicString});
}
// ...
private byte[] b(int magicInt, String keyId, byte[] encrypted, String magicString) {
return this.a(2, magicInt, 0, keyId, encrypted, magicString);
}
// ...
public byte[] staticBinarySafeDecryptNoB64(int magicInt, String keyId, byte[] encrypted, String magicString) throws SecException {
if(keyId != null && keyId.length() > 0 && magicInt >= 0 && magicInt < 19 && encrypted != null && encrypted.length > 0) {
return this.b(magicInt, keyId, encrypted, magicString);
}
throw new SecException("", 301);
}
//...
}

Εδώ η λίστα των παραμέτρων μας συμπληρώνεται με δύο ακόμη ακέραιους αριθμούς: 2 και 0. Κρίνοντας από
Όλα, 2 σημαίνει αποκρυπτογράφηση, όπως στη μέθοδο doFinal κατηγορία συστήματος javax.crypto.Cipher. Και όλα αυτά μεταφέρονται σε ένα συγκεκριμένο Router με τον αριθμό 10601 - αυτός είναι προφανώς ο αριθμός εντολής.

Μετά την επόμενη αλυσίδα μεταβάσεων βρίσκουμε μια κλάση που υλοποιεί τη διεπαφή IRouterComponent και μέθοδος doCommand:

package com.alibaba.wireless.security.mainplugin;
import com.alibaba.wireless.security.framework.IRouterComponent;
import com.taobao.wireless.security.adapter.JNICLibrary;
public class a implements IRouterComponent {
public a() {
super();
}
public Object doCommand(int arg2, Object[] arg3) {
return JNICLibrary.doCommandNative(arg2, arg3);
}
}

Και επίσης τάξη Βιβλιοθήκη JNICL, στο οποίο δηλώνεται η εγγενής μέθοδος doCommandNative:

package com.taobao.wireless.security.adapter;
public class JNICLibrary {
public static native Object doCommandNative(int arg0, Object[] arg1);
}

Αυτό σημαίνει ότι πρέπει να βρούμε μια μέθοδο στον εγγενή κώδικα doCommandNative. Και εδώ αρχίζει η διασκέδαση.

Απόκρυψη του κώδικα μηχανής

Στο αρχείο libsgmain.so (το οποίο είναι στην πραγματικότητα ένα .jar και στο οποίο βρήκαμε την υλοποίηση ορισμένων διεπαφών που σχετίζονται με κρυπτογράφηση ακριβώς από πάνω) υπάρχει μια εγγενής βιβλιοθήκη: libsgmainso-6.4.36.so. Το ανοίγουμε στο IDA και λαμβάνουμε ένα σωρό παράθυρα διαλόγου με σφάλματα. Το πρόβλημα είναι ότι ο πίνακας κεφαλίδας ενότητας δεν είναι έγκυρος. Αυτό γίνεται επίτηδες για να περιπλέξει την ανάλυση.

Ψάχνετε για τρωτά σημεία στο πρόγραμμα περιήγησης UC

Αλλά δεν χρειάζεται: για να φορτώσετε σωστά ένα αρχείο ELF και να το αναλύσετε, αρκεί ένας πίνακας κεφαλίδων προγράμματος. Επομένως, απλώς διαγράφουμε τον πίνακα ενοτήτων, μηδενίζοντας τα αντίστοιχα πεδία στην κεφαλίδα.

Ψάχνετε για τρωτά σημεία στο πρόγραμμα περιήγησης UC

Ανοίξτε ξανά το αρχείο στο IDA.

Υπάρχουν δύο τρόποι για να πείτε στην εικονική μηχανή Java πού ακριβώς βρίσκεται στην εγγενή βιβλιοθήκη η υλοποίηση μιας μεθόδου που δηλώνεται στον κώδικα Java ως εγγενής. Το πρώτο είναι να του δώσετε ένα όνομα είδους Java_package_name_ClassName_MethodName.

Το δεύτερο είναι να το καταχωρήσετε κατά τη φόρτωση της βιβλιοθήκης (στη συνάρτηση JNI_OnLoad)
χρησιμοποιώντας μια κλήση συνάρτησης RegisterNatives.

Στην περίπτωσή μας, εάν χρησιμοποιήσουμε την πρώτη μέθοδο, το όνομα θα πρέπει να είναι ως εξής: Java_com_taobao_wireless_security_adapter_JNICLlibrary_doCommandNative.

Δεν υπάρχει τέτοια λειτουργία μεταξύ των εξαγόμενων συναρτήσεων, πράγμα που σημαίνει ότι πρέπει να αναζητήσετε μια κλήση RegisterNatives.
Πάμε στη συνάρτηση JNI_OnLoad και βλέπουμε αυτή την εικόνα:

Ψάχνετε για τρωτά σημεία στο πρόγραμμα περιήγησης UC

Τι συμβαίνει εδώ? Με την πρώτη ματιά, η αρχή και το τέλος της λειτουργίας είναι τυπικά για την αρχιτεκτονική ARM. Η πρώτη εντολή στη στοίβα αποθηκεύει τα περιεχόμενα των καταχωρητών που θα χρησιμοποιήσει η συνάρτηση στη λειτουργία της (στην περίπτωση αυτή, R0, R1 και R2), καθώς και τα περιεχόμενα του καταχωρητή LR, που περιέχει τη διεύθυνση επιστροφής από τη συνάρτηση . Η τελευταία εντολή επαναφέρει τους αποθηκευμένους καταχωρητές και η διεύθυνση επιστροφής τοποθετείται αμέσως στον καταχωρητή υπολογιστή - επιστρέφοντας έτσι από τη συνάρτηση. Αλλά αν κοιτάξετε προσεκτικά, θα παρατηρήσετε ότι η προτελευταία εντολή αλλάζει τη διεύθυνση επιστροφής που είναι αποθηκευμένη στη στοίβα. Ας υπολογίσουμε πώς θα είναι μετά
εκτέλεση κώδικα. Μια συγκεκριμένη διεύθυνση 1xB0 φορτώνεται στο R130, αφαιρείται το 5 από αυτό, στη συνέχεια μεταφέρεται στο R0 και προστίθεται 0x10 σε αυτό. Αποδεικνύεται 0xB13B. Έτσι, το IDA πιστεύει ότι η τελευταία εντολή είναι μια επιστροφή κανονικής συνάρτησης, αλλά στην πραγματικότητα πηγαίνει στην υπολογισμένη διεύθυνση 0xB13B.

Αξίζει να υπενθυμίσουμε εδώ ότι οι επεξεργαστές ARM έχουν δύο λειτουργίες και δύο σετ εντολών: ARM και Thumb. Το λιγότερο σημαντικό bit της διεύθυνσης λέει στον επεξεργαστή ποιο σύνολο εντολών χρησιμοποιείται. Δηλαδή, η διεύθυνση είναι στην πραγματικότητα 0xB13A και ένα στο λιγότερο σημαντικό bit δείχνει τη λειτουργία Thumb.

Ένας παρόμοιος «προσαρμογέας» έχει προστεθεί στην αρχή κάθε λειτουργίας σε αυτήν τη βιβλιοθήκη και
κωδικός σκουπιδιών. Δεν θα σταθούμε λεπτομερέστερα σε αυτά - απλώς θυμόμαστε
ότι η πραγματική αρχή σχεδόν όλων των λειτουργιών είναι λίγο πιο μακριά.

Δεδομένου ότι ο κωδικός δεν μεταβαίνει ρητά στο 0xB13A, το ίδιο το IDA δεν αναγνώρισε ότι ο κωδικός βρισκόταν σε αυτήν τη θέση. Για τον ίδιο λόγο, δεν αναγνωρίζει το μεγαλύτερο μέρος του κώδικα στη βιβλιοθήκη ως κώδικα, γεγονός που καθιστά την ανάλυση κάπως δύσκολη. Λέμε στο IDA ότι αυτός είναι ο κωδικός και συμβαίνει αυτό:

Ψάχνετε για τρωτά σημεία στο πρόγραμμα περιήγησης UC

Ο πίνακας ξεκινά σαφώς από 0xB144. Τι υπάρχει στο sub_494C;

Ψάχνετε για τρωτά σημεία στο πρόγραμμα περιήγησης UC

Όταν καλούμε αυτή τη συνάρτηση στον καταχωρητή LR, παίρνουμε τη διεύθυνση του προαναφερθέντος πίνακα (0xB144). Σε R0 - δείκτης σε αυτόν τον πίνακα. Δηλαδή, η τιμή λαμβάνεται από τον πίνακα, προστίθεται στο LR και το αποτέλεσμα είναι
τη διεύθυνση στην οποία θα πάτε. Ας προσπαθήσουμε να το υπολογίσουμε: 0xB144 + [0xB144 + 8* 4] = 0xB144 + 0x120 = 0xB264. Πηγαίνουμε στη διεύθυνση που λάβαμε και βλέπουμε κυριολεκτικά μερικές χρήσιμες οδηγίες και πηγαίνουμε ξανά στο 0xB140:

Ψάχνετε για τρωτά σημεία στο πρόγραμμα περιήγησης UC

Τώρα θα υπάρχει μια μετάβαση σε μετατόπιση με δείκτη 0x20 από τον πίνακα.

Κρίνοντας από το μέγεθος του πίνακα, θα υπάρχουν πολλές τέτοιες μεταβάσεις στον κώδικα. Γεννιέται το ερώτημα εάν είναι δυνατόν να το αντιμετωπίσουμε με κάποιο τρόπο αυτό πιο αυτόματα, χωρίς να υπολογίζουμε με μη αυτόματο τρόπο τις διευθύνσεις. Και τα σενάρια και η δυνατότητα επιδιόρθωσης κώδικα στο IDA έρχονται σε βοήθειά μας:

def put_unconditional_branch(source, destination):
offset = (destination - source - 4) >> 1
if offset > 2097151 or offset < -2097152:
raise RuntimeError("Invalid offset")
if offset > 1023 or offset < -1024:
instruction1 = 0xf000 | ((offset >> 11) & 0x7ff)
instruction2 = 0xb800 | (offset & 0x7ff)
patch_word(source, instruction1)
patch_word(source + 2, instruction2)
else:
instruction = 0xe000 | (offset & 0x7ff)
patch_word(source, instruction)
ea = here()
if get_wide_word(ea) == 0xb503: #PUSH {R0,R1,LR}
ea1 = ea + 2
if get_wide_word(ea1) == 0xbf00: #NOP
ea1 += 2
if get_operand_type(ea1, 0) == 1 and get_operand_value(ea1, 0) == 0 and get_operand_type(ea1, 1) == 2:
index = get_wide_dword(get_operand_value(ea1, 1))
print "index =", hex(index)
ea1 += 2
if get_operand_type(ea1, 0) == 7:
table = get_operand_value(ea1, 0) + 4
elif get_operand_type(ea1, 1) == 2:
table = get_operand_value(ea1, 1) + 4
else:
print "Wrong operand type on", hex(ea1), "-", get_operand_type(ea1, 0), get_operand_type(ea1, 1)
table = None
if table is None:
print "Unable to find table"
else:
print "table =", hex(table)
offset = get_wide_dword(table + (index << 2))
put_unconditional_branch(ea, table + offset)
else:
print "Unknown code", get_operand_type(ea1, 0), get_operand_value(ea1, 0), get_operand_type(ea1, 1) == 2
else:
print "Unable to detect first instruction"

Τοποθετήστε τον κέρσορα στη γραμμή 0xB26A, εκτελέστε το σενάριο και δείτε τη μετάβαση στο 0xB4B0:

Ψάχνετε για τρωτά σημεία στο πρόγραμμα περιήγησης UC

Το IDA και πάλι δεν αναγνώρισε αυτήν την περιοχή ως κωδικό. Τη βοηθάμε και βλέπουμε ένα άλλο σχέδιο εκεί:

Ψάχνετε για τρωτά σημεία στο πρόγραμμα περιήγησης UC

Οι οδηγίες μετά το BLX δεν φαίνεται να έχουν πολύ νόημα, είναι περισσότερο σαν κάποιου είδους μετατόπιση. Ας δούμε το sub_4964:

Ψάχνετε για τρωτά σημεία στο πρόγραμμα περιήγησης UC

Και πράγματι, εδώ λαμβάνεται ένα dword στη διεύθυνση που βρίσκεται στο LR, προστίθεται σε αυτήν τη διεύθυνση, μετά το οποίο λαμβάνεται η τιμή στη διεύθυνση που προκύπτει και τοποθετείται στη στοίβα. Επίσης, προστίθεται το 4 στο LR ώστε μετά την επιστροφή από τη συνάρτηση, να παραλείπεται αυτή η ίδια μετατόπιση. Μετά από αυτό, η εντολή POP {R1} παίρνει την τιμή που προκύπτει από τη στοίβα. Αν κοιτάξετε τι βρίσκεται στη διεύθυνση 0xB4BA + 0xEA = 0xB5A4, θα δείτε κάτι παρόμοιο με έναν πίνακα διευθύνσεων:

Ψάχνετε για τρωτά σημεία στο πρόγραμμα περιήγησης UC

Για να επιδιορθώσετε αυτό το σχέδιο, θα χρειαστεί να λάβετε δύο παραμέτρους από τον κωδικό: το offset και τον αριθμό μητρώου στον οποίο θέλετε να βάλετε το αποτέλεσμα. Για κάθε πιθανή εγγραφή, θα πρέπει να προετοιμάσετε ένα κομμάτι κώδικα εκ των προτέρων.

patches = {}
patches[0] = (0x00, 0xbf, 0x01, 0x48, 0x00, 0x68, 0x02, 0xe0)
patches[1] = (0x00, 0xbf, 0x01, 0x49, 0x09, 0x68, 0x02, 0xe0)
patches[2] = (0x00, 0xbf, 0x01, 0x4a, 0x12, 0x68, 0x02, 0xe0)
patches[3] = (0x00, 0xbf, 0x01, 0x4b, 0x1b, 0x68, 0x02, 0xe0)
patches[4] = (0x00, 0xbf, 0x01, 0x4c, 0x24, 0x68, 0x02, 0xe0)
patches[5] = (0x00, 0xbf, 0x01, 0x4d, 0x2d, 0x68, 0x02, 0xe0)
patches[8] = (0x00, 0xbf, 0xdf, 0xf8, 0x06, 0x80, 0xd8, 0xf8, 0x00, 0x80, 0x01, 0xe0)
patches[9] = (0x00, 0xbf, 0xdf, 0xf8, 0x06, 0x90, 0xd9, 0xf8, 0x00, 0x90, 0x01, 0xe0)
patches[10] = (0x00, 0xbf, 0xdf, 0xf8, 0x06, 0xa0, 0xda, 0xf8, 0x00, 0xa0, 0x01, 0xe0)
patches[11] = (0x00, 0xbf, 0xdf, 0xf8, 0x06, 0xb0, 0xdb, 0xf8, 0x00, 0xb0, 0x01, 0xe0)
ea = here()
if (get_wide_word(ea) == 0xb082 #SUB SP, SP, #8
and get_wide_word(ea + 2) == 0xb503): #PUSH {R0,R1,LR}
if get_operand_type(ea + 4, 0) == 7:
pop = get_bytes(ea + 12, 4, 0)
if pop[1] == 'xbc':
register = -1
r = get_wide_byte(ea + 12)
for i in range(8):
if r == (1 << i):
register = i
break
if register == -1:
print "Unable to detect register"
else:
address = get_wide_dword(ea + 8) + ea + 8
for b in patches[register]:
patch_byte(ea, b)
ea += 1
if ea % 4 != 0:
ea += 2
patch_dword(ea, address)
elif pop[:3] == 'x5dxf8x04':
register = ord(pop[3]) >> 4
if register in patches:
address = get_wide_dword(ea + 8) + ea + 8
for b in patches[register]:
patch_byte(ea, b)
ea += 1
patch_dword(ea, address)
else:
print "POP instruction not found"
else:
print "Wrong operand type on +4:", get_operand_type(ea + 4, 0)
else:
print "Unable to detect first instructions"

Τοποθετούμε τον κέρσορα στην αρχή της δομής που θέλουμε να αντικαταστήσουμε - 0xB4B2 - και εκτελούμε το σενάριο:

Ψάχνετε για τρωτά σημεία στο πρόγραμμα περιήγησης UC

Εκτός από τις ήδη αναφερθείσες δομές, ο κώδικας περιέχει επίσης τα ακόλουθα:

Ψάχνετε για τρωτά σημεία στο πρόγραμμα περιήγησης UC

Όπως και στην προηγούμενη περίπτωση, μετά την εντολή BLX υπάρχει μια μετατόπιση:

Ψάχνετε για τρωτά σημεία στο πρόγραμμα περιήγησης UC

Παίρνουμε το offset στη διεύθυνση από το LR, το προσθέτουμε στο LR και πηγαίνουμε εκεί. 0x72044 + 0xC = 0x72050. Το σενάριο για αυτό το σχέδιο είναι αρκετά απλό:

def put_unconditional_branch(source, destination):
offset = (destination - source - 4) >> 1
if offset > 2097151 or offset < -2097152:
raise RuntimeError("Invalid offset")
if offset > 1023 or offset < -1024:
instruction1 = 0xf000 | ((offset >> 11) & 0x7ff)
instruction2 = 0xb800 | (offset & 0x7ff)
patch_word(source, instruction1)
patch_word(source + 2, instruction2)
else:
instruction = 0xe000 | (offset & 0x7ff)
patch_word(source, instruction)
ea = here()
if get_wide_word(ea) == 0xb503: #PUSH {R0,R1,LR}
ea1 = ea + 6
if get_wide_word(ea + 2) == 0xbf00: #NOP
ea1 += 2
offset = get_wide_dword(ea1)
put_unconditional_branch(ea, (ea1 + offset) & 0xffffffff)
else:
print "Unable to detect first instruction"

Αποτέλεσμα εκτέλεσης σεναρίου:

Ψάχνετε για τρωτά σημεία στο πρόγραμμα περιήγησης UC

Μόλις διορθωθούν όλα στη συνάρτηση, μπορείτε να υποδείξετε το IDA στην πραγματική του αρχή. Θα συνδυάσει όλο τον κώδικα συνάρτησης και μπορεί να απομεταγλωττιστεί χρησιμοποιώντας HexRays.

Αποκωδικοποίηση συμβολοσειρών

Έχουμε μάθει να αντιμετωπίζουμε τη συσκότιση του κώδικα μηχανής στη βιβλιοθήκη libsgmainso-6.4.36.so από το UC Browser και έλαβε τον κωδικό λειτουργίας JNI_OnLoad.

int __fastcall real_JNI_OnLoad(JavaVM *vm)
{
int result; // r0
jclass clazz; // r0 MAPDST
int v4; // r0
JNIEnv *env; // r4
int v6; // [sp-40h] [bp-5Ch]
int v7; // [sp+Ch] [bp-10h]
v7 = *(_DWORD *)off_8AC00;
if ( !vm )
goto LABEL_39;
sub_7C4F4();
env = (JNIEnv *)sub_7C5B0(0);
if ( !env )
goto LABEL_39;
v4 = sub_72CCC();
sub_73634(v4);
sub_73E24(&unk_83EA6, &v6, 49);
clazz = (jclass)((int (__fastcall *)(JNIEnv *, int *))(*env)->FindClass)(env, &v6);
if ( clazz
&& (sub_9EE4(),
sub_71D68(env),
sub_E7DC(env) >= 0
&& sub_69D68(env) >= 0
&& sub_197B4(env, clazz) >= 0
&& sub_E240(env, clazz) >= 0
&& sub_B8B0(env, clazz) >= 0
&& sub_5F0F4(env, clazz) >= 0
&& sub_70640(env, clazz) >= 0
&& sub_11F3C(env) >= 0
&& sub_21C3C(env, clazz) >= 0
&& sub_2148C(env, clazz) >= 0
&& sub_210E0(env, clazz) >= 0
&& sub_41B58(env, clazz) >= 0
&& sub_27920(env, clazz) >= 0
&& sub_293E8(env, clazz) >= 0
&& sub_208F4(env, clazz) >= 0) )
{
result = (sub_B7B0(env, clazz) >> 31) | 0x10004;
}
else
{
LABEL_39:
result = -1;
}
return result;
}

Ας ρίξουμε μια πιο προσεκτική ματιά στις ακόλουθες γραμμές:

  sub_73E24(&unk_83EA6, &v6, 49);
clazz = (jclass)((int (__fastcall *)(JNIEnv *, int *))(*env)->FindClass)(env, &v6);

Σε λειτουργία sub_73E24 το όνομα της κλάσης ξεκάθαρα αποκρυπτογραφείται. Ως παράμετροι αυτής της συνάρτησης, μεταβιβάζονται ένας δείκτης σε δεδομένα παρόμοια με κρυπτογραφημένα δεδομένα, ένα συγκεκριμένο buffer και ένας αριθμός. Προφανώς, μετά την κλήση της συνάρτησης, θα υπάρχει μια αποκρυπτογραφημένη γραμμή στο buffer, αφού μεταβιβάζεται στη συνάρτηση FindClass, το οποίο παίρνει το όνομα της κλάσης ως δεύτερη παράμετρο. Επομένως, ο αριθμός είναι το μέγεθος του buffer ή το μήκος της γραμμής. Ας προσπαθήσουμε να αποκρυπτογραφήσουμε το όνομα της τάξης, θα πρέπει να μας πει αν πάμε στη σωστή κατεύθυνση. Ας ρίξουμε μια πιο προσεκτική ματιά στο τι συμβαίνει sub_73E24.

int __fastcall sub_73E56(unsigned __int8 *in, unsigned __int8 *out, size_t size)
{
int v4; // r6
int v7; // r11
int v8; // r9
int v9; // r4
size_t v10; // r5
int v11; // r0
struc_1 v13; // [sp+0h] [bp-30h]
int v14; // [sp+1Ch] [bp-14h]
int v15; // [sp+20h] [bp-10h]
v4 = 0;
v15 = *(_DWORD *)off_8AC00;
v14 = 0;
v7 = sub_7AF78(17);
v8 = sub_7AF78(size);
if ( !v7 )
{
v9 = 0;
goto LABEL_12;
}
(*(void (__fastcall **)(int, const char *, int))(v7 + 12))(v7, "DcO/lcK+h?m3c*q@", 16);
if ( !v8 )
{
LABEL_9:
v4 = 0;
goto LABEL_10;
}
v4 = 0;
if ( !in )
{
LABEL_10:
v9 = 0;
goto LABEL_11;
}
v9 = 0;
if ( out )
{
memset(out, 0, size);
v10 = size - 1;
(*(void (__fastcall **)(int, unsigned __int8 *, size_t))(v8 + 12))(v8, in, v10);
memset(&v13, 0, 0x14u);
v13.field_4 = 3;
v13.field_10 = v7;
v13.field_14 = v8;
v11 = sub_6115C(&v13, &v14);
v9 = v11;
if ( v11 )
{
if ( *(_DWORD *)(v11 + 4) == v10 )
{
qmemcpy(out, *(const void **)v11, v10);
v4 = *(_DWORD *)(v9 + 4);
}
else
{
v4 = 0;
}
goto LABEL_11;
}
goto LABEL_9;
}
LABEL_11:
sub_7B148(v7);
LABEL_12:
if ( v8 )
sub_7B148(v8);
if ( v9 )
sub_7B148(v9);
return v4;
}

Λειτουργία υπο_7AF78 δημιουργεί μια παρουσία ενός κοντέινερ για πίνακες byte του καθορισμένου μεγέθους (δεν θα σταθούμε λεπτομερώς σε αυτά τα κοντέινερ). Εδώ δημιουργούνται δύο τέτοια δοχεία: το ένα περιέχει τη γραμμή "DcO/lcK+h?m3c*q@" (είναι εύκολο να μαντέψει κανείς ότι πρόκειται για κλειδί), το άλλο περιέχει κρυπτογραφημένα δεδομένα. Στη συνέχεια, και τα δύο αντικείμενα τοποθετούνται σε μια συγκεκριμένη δομή, η οποία μεταβιβάζεται στη συνάρτηση sub_6115C. Ας επισημάνουμε επίσης ένα πεδίο με την τιμή 3 σε αυτήν τη δομή. Ας δούμε τι θα συμβεί με αυτήν τη δομή στη συνέχεια.

int __fastcall sub_611B4(struc_1 *a1, _DWORD *a2)
{
int v3; // lr
unsigned int v4; // r1
int v5; // r0
int v6; // r1
int result; // r0
int v8; // r0
*a2 = 820000;
if ( a1 )
{
v3 = a1->field_14;
if ( v3 )
{
v4 = a1->field_4;
if ( v4 < 0x19 )
{
switch ( v4 )
{
case 0u:
v8 = sub_6419C(a1->field_0, a1->field_10, v3);
goto LABEL_17;
case 3u:
v8 = sub_6364C(a1->field_0, a1->field_10, v3);
goto LABEL_17;
case 0x10u:
case 0x11u:
case 0x12u:
v8 = sub_612F4(
a1->field_0,
v4,
*(_QWORD *)&a1->field_8,
*(_QWORD *)&a1->field_8 >> 32,
a1->field_10,
v3,
a2);
goto LABEL_17;
case 0x14u:
v8 = sub_63A28(a1->field_0, v3);
goto LABEL_17;
case 0x15u:
sub_61A60(a1->field_0, v3, a2);
return result;
case 0x16u:
v8 = sub_62440(a1->field_14);
goto LABEL_17;
case 0x17u:
v8 = sub_6226C(a1->field_10, v3);
goto LABEL_17;
case 0x18u:
v8 = sub_63530(a1->field_14);
LABEL_17:
v6 = 0;
if ( v8 )
{
*a2 = 0;
v6 = v8;
}
return v6;
default:
LOWORD(v5) = 28032;
goto LABEL_5;
}
}
}
}
LOWORD(v5) = -27504;
LABEL_5:
HIWORD(v5) = 13;
v6 = 0;
*a2 = v5;
return v6;
}

Η παράμετρος διακόπτης είναι ένα πεδίο δομής στο οποίο είχε εκχωρηθεί προηγουμένως η τιμή 3. Δείτε την περίπτωση 3: στη συνάρτηση sub_6364C Οι παράμετροι περνούν από τη δομή που προστέθηκαν εκεί στην προηγούμενη συνάρτηση, δηλαδή το κλειδί και τα κρυπτογραφημένα δεδομένα. Αν κοιτάξετε προσεκτικά sub_6364C, μπορείτε να αναγνωρίσετε τον αλγόριθμο RC4 σε αυτό.

Έχουμε έναν αλγόριθμο και ένα κλειδί. Ας προσπαθήσουμε να αποκρυπτογραφήσουμε το όνομα της τάξης. Να τι συνέβη: com/taobao/wireless/security/adapter/JNICLlibrary. Εξαιρετική! Είμαστε στο σωστό δρόμο.

δέντρο εντολών

Τώρα πρέπει να βρούμε μια πρόκληση RegisterNatives, που θα μας υποδείξει τη συνάρτηση doCommandNative. Ας δούμε τις συναρτήσεις που καλούνται από JNI_OnLoad, και το βρίσκουμε σε sub_B7B0:

int __fastcall sub_B7F6(JNIEnv *env, jclass clazz)
{
char signature[41]; // [sp+7h] [bp-55h]
char name[16]; // [sp+30h] [bp-2Ch]
JNINativeMethod method; // [sp+40h] [bp-1Ch]
int v8; // [sp+4Ch] [bp-10h]
v8 = *(_DWORD *)off_8AC00;
decryptString((unsigned __int8 *)&unk_83ED9, (unsigned __int8 *)name, 0x10u);// doCommandNative
decryptString((unsigned __int8 *)&unk_83EEA, (unsigned __int8 *)signature, 0x29u);// (I[Ljava/lang/Object;)Ljava/lang/Object;
method.name = name;
method.signature = signature;
method.fnPtr = sub_B69C;
return ((int (__fastcall *)(JNIEnv *, jclass, JNINativeMethod *, int))(*env)->RegisterNatives)(env, clazz, &method, 1) >> 31;
}

Και πράγματι, μια εγγενής μέθοδος με το όνομα είναι καταχωρημένη εδώ doCommandNative. Τώρα ξέρουμε τη διεύθυνσή του. Ας δούμε τι κάνει.

int __fastcall doCommandNative(JNIEnv *env, jobject obj, int command, jarray args)
{
int v5; // r5
struc_2 *a5; // r6
int v9; // r1
int v11; // [sp+Ch] [bp-14h]
int v12; // [sp+10h] [bp-10h]
v5 = 0;
v12 = *(_DWORD *)off_8AC00;
v11 = 0;
a5 = (struc_2 *)malloc(0x14u);
if ( a5 )
{
a5->field_0 = 0;
a5->field_4 = 0;
a5->field_8 = 0;
a5->field_C = 0;
v9 = command % 10000 / 100;
a5->field_0 = command / 10000;
a5->field_4 = v9;
a5->field_8 = command % 100;
a5->field_C = env;
a5->field_10 = args;
v5 = sub_9D60(command / 10000, v9, command % 100, 1, (int)a5, &v11);
}
free(a5);
if ( !v5 && v11 )
sub_7CF34(env, v11, &byte_83ED7);
return v5;
}

Με το όνομα μπορείτε να μαντέψετε ότι εδώ είναι το σημείο εισόδου όλων των λειτουργιών που οι προγραμματιστές αποφάσισαν να μεταφέρουν στην εγγενή βιβλιοθήκη. Μας ενδιαφέρει η συνάρτηση με αριθμό 10601.

Μπορείτε να δείτε από τον κώδικα ότι ο αριθμός εντολής παράγει τρεις αριθμούς: εντολή/10000, εντολή % 10000 / 100 и εντολή % 10, δηλαδή, στην περίπτωσή μας, 1, 6 και 1. Αυτοί οι τρεις αριθμοί, καθώς και ένας δείκτης για JNIEnv και τα ορίσματα που μεταβιβάζονται στη συνάρτηση προστίθενται σε μια δομή και μεταβιβάζονται. Χρησιμοποιώντας τους τρεις αριθμούς που προέκυψαν (ας τους συμβολίσουμε N1, N2 και N3), δημιουργείται ένα δέντρο εντολών.

Κάτι σαν αυτό:

Ψάχνετε για τρωτά σημεία στο πρόγραμμα περιήγησης UC

Το δέντρο συμπληρώνεται δυναμικά JNI_OnLoad.
Τρεις αριθμοί κωδικοποιούν τη διαδρομή στο δέντρο. Κάθε φύλλο του δέντρου περιέχει τη διεύθυνση σε θήκη της αντίστοιχης συνάρτησης. Το κλειδί βρίσκεται στον γονικό κόμβο. Η εύρεση της θέσης στον κώδικα όπου η συνάρτηση που χρειαζόμαστε προστίθεται στο δέντρο δεν είναι δύσκολη εάν κατανοείτε όλες τις δομές που χρησιμοποιούνται (δεν τις περιγράφουμε για να μην φουσκώσει ένα ήδη αρκετά μεγάλο άρθρο).

Περισσότερη σύγχυση

Λάβαμε τη διεύθυνση της συνάρτησης που πρέπει να αποκρυπτογραφήσει την κυκλοφορία: 0x5F1AC. Αλλά είναι πολύ νωρίς για να χαρούμε: οι προγραμματιστές του UC Browser ετοίμασαν άλλη μια έκπληξη για εμάς.

Αφού λάβουμε τις παραμέτρους από τον πίνακα που σχηματίστηκε στον κώδικα Java, παίρνουμε
στη συνάρτηση στη διεύθυνση 0x4D070. Και εδώ μας περιμένει ένας άλλος τύπος συσκότισης κώδικα.

Βάζουμε δύο δείκτες σε R7 και R4:

Ψάχνετε για τρωτά σημεία στο πρόγραμμα περιήγησης UC

Μετατοπίζουμε τον πρώτο δείκτη στο R11:

Ψάχνετε για τρωτά σημεία στο πρόγραμμα περιήγησης UC

Για να λάβετε μια διεύθυνση από έναν πίνακα, χρησιμοποιήστε ένα ευρετήριο:

Ψάχνετε για τρωτά σημεία στο πρόγραμμα περιήγησης UC

Αφού μεταβείτε στην πρώτη διεύθυνση, χρησιμοποιείται το δεύτερο ευρετήριο, το οποίο βρίσκεται στο R4. Υπάρχουν 230 στοιχεία στον πίνακα.

Τι να κάνετε για αυτό; Μπορείτε να πείτε στο IDA ότι πρόκειται για έναν διακόπτη: Επεξεργασία -> Άλλο -> Καθορισμός ιδιώματος διακόπτη.

Ψάχνετε για τρωτά σημεία στο πρόγραμμα περιήγησης UC

Ο κωδικός που προκύπτει είναι τρομερός. Όμως, κάνοντας το δρόμο σας μέσα στη ζούγκλα της, μπορείτε να παρατηρήσετε μια κλήση σε μια λειτουργία που είναι ήδη γνωστή σε εμάς sub_6115C:

Ψάχνετε για τρωτά σημεία στο πρόγραμμα περιήγησης UC

Υπήρχε ένας διακόπτης στον οποίο στην περίπτωση 3 υπήρχε αποκρυπτογράφηση χρησιμοποιώντας τον αλγόριθμο RC4. Και σε αυτήν την περίπτωση, η δομή που μεταβιβάζεται στη συνάρτηση συμπληρώνεται από τις παραμέτρους που μεταβιβάζονται στις doCommandNative. Ας θυμηθούμε τι είχαμε εκεί magicInt με την τιμή 16. Εξετάζουμε την αντίστοιχη περίπτωση - και μετά από αρκετές μεταβάσεις βρίσκουμε τον κωδικό με τον οποίο μπορεί να αναγνωριστεί ο αλγόριθμος.

Ψάχνετε για τρωτά σημεία στο πρόγραμμα περιήγησης UC

Αυτό είναι το AES!

Ο αλγόριθμος υπάρχει, το μόνο που μένει είναι να λάβουμε τις παραμέτρους του: mode, key και, πιθανώς, το διάνυσμα αρχικοποίησης (η παρουσία του εξαρτάται από τον τρόπο λειτουργίας του αλγορίθμου AES). Η δομή μαζί τους πρέπει να διαμορφωθεί κάπου πριν από την κλήση συνάρτησης sub_6115C, αλλά αυτό το τμήμα του κώδικα είναι ιδιαίτερα ασαφές, επομένως προκύπτει η ιδέα να επιδιορθωθεί ο κώδικας έτσι ώστε όλες οι παράμετροι της συνάρτησης αποκρυπτογράφησης να απορρίπτονται σε ένα αρχείο.

Εμπλοκή

Για να μην γράψετε όλο τον κώδικα ενημέρωσης κώδικα στη γλώσσα συναρμολόγησης με μη αυτόματο τρόπο, μπορείτε να εκκινήσετε το Android Studio, να γράψετε μια συνάρτηση εκεί που λαμβάνει τις ίδιες παραμέτρους εισόδου με τη λειτουργία αποκρυπτογράφησης και γράφει σε ένα αρχείο και, στη συνέχεια, επικολλήστε τον κώδικα που θα κάνει ο μεταγλωττιστής παράγω.

Οι φίλοι μας από την ομάδα του UC Browser φρόντισαν επίσης για την ευκολία της προσθήκης κώδικα. Ας θυμηθούμε ότι στην αρχή κάθε λειτουργίας έχουμε κωδικό απορριμμάτων που μπορεί εύκολα να αντικατασταθεί με οποιονδήποτε άλλο. Πολύ βολικό 🙂 Ωστόσο, στην αρχή της συνάρτησης προορισμού δεν υπάρχει αρκετός χώρος για τον κώδικα που αποθηκεύει όλες τις παραμέτρους σε ένα αρχείο. Έπρεπε να το χωρίσω σε μέρη και να χρησιμοποιήσω μπλοκ σκουπιδιών από γειτονικές λειτουργίες. Υπήρχαν τέσσερα μέρη συνολικά.

Το πρώτο μέρος:

Ψάχνετε για τρωτά σημεία στο πρόγραμμα περιήγησης UC

Στην αρχιτεκτονική ARM, οι τέσσερις πρώτες παράμετροι συνάρτησης περνούν από τους καταχωρητές R0-R3, οι υπόλοιπες, εάν υπάρχουν, περνούν από τη στοίβα. Το μητρώο LR φέρει τη διεύθυνση επιστροφής. Όλα αυτά πρέπει να αποθηκευτούν ώστε η συνάρτηση να μπορεί να λειτουργήσει αφού αφαιρέσουμε τις παραμέτρους της. Πρέπει επίσης να αποθηκεύσουμε όλους τους καταχωρητές που θα χρησιμοποιήσουμε στη διαδικασία, οπότε κάνουμε PUSH.W {R0-R10,LR}. Στο R7 παίρνουμε τη διεύθυνση της λίστας των παραμέτρων που μεταβιβάζονται στη συνάρτηση μέσω της στοίβας.

Χρησιμοποιώντας τη λειτουργία fopen ας ανοίξουμε το αρχείο /data/local/tmp/aes σε λειτουργία "ab".
δηλαδή για προσθήκη. Στο R0 φορτώνουμε τη διεύθυνση του ονόματος αρχείου, στο R1 - τη διεύθυνση της γραμμής που υποδεικνύει τη λειτουργία. Και εδώ τελειώνει ο κωδικός σκουπιδιών, οπότε προχωράμε στην επόμενη λειτουργία. Για να συνεχίσει να λειτουργεί, βάζουμε στην αρχή τη μετάβαση στον πραγματικό κωδικό της συνάρτησης, παρακάμπτοντας τα σκουπίδια και αντί για τα σκουπίδια προσθέτουμε μια συνέχεια του patch.

Ψάχνετε για τρωτά σημεία στο πρόγραμμα περιήγησης UC

Καλούμε fopen.

Οι τρεις πρώτες παράμετροι της συνάρτησης AES έχουν τύπο int. Εφόσον αποθηκεύσαμε τους καταχωρητές στη στοίβα στην αρχή, μπορούμε απλά να περάσουμε τη συνάρτηση fwrite τις διευθύνσεις τους στη στοίβα.

Ψάχνετε για τρωτά σημεία στο πρόγραμμα περιήγησης UC

Στη συνέχεια έχουμε τρεις δομές που περιέχουν το μέγεθος των δεδομένων και έναν δείκτη προς τα δεδομένα για το κλειδί, το διάνυσμα αρχικοποίησης και τα κρυπτογραφημένα δεδομένα.

Ψάχνετε για τρωτά σημεία στο πρόγραμμα περιήγησης UC

Στο τέλος, κλείστε το αρχείο, επαναφέρετε τους καταχωρητές και μεταφέρετε τον έλεγχο στην πραγματική λειτουργία AES.

Συλλέγουμε ένα APK με μια επιδιορθωμένη βιβλιοθήκη, το υπογράφουμε, το ανεβάζουμε στη συσκευή/εξομοιωτή και το εκκινούμε. Βλέπουμε ότι δημιουργείται η χωματερή μας, και γράφονται πολλά δεδομένα εκεί. Το πρόγραμμα περιήγησης χρησιμοποιεί κρυπτογράφηση όχι μόνο για την κυκλοφορία, και όλη η κρυπτογράφηση περνά από την εν λόγω λειτουργία. Αλλά για κάποιο λόγο τα απαραίτητα δεδομένα δεν υπάρχουν και το απαιτούμενο αίτημα δεν είναι ορατό στην κίνηση. Για να μην περιμένουμε μέχρι το UC Browser να κάνει το απαραίτητο αίτημα, ας πάρουμε την κρυπτογραφημένη απάντηση από τον διακομιστή που λάβαμε νωρίτερα και ας ενημερώσουμε ξανά την εφαρμογή: προσθέστε την αποκρυπτογράφηση στο onCreate της κύριας δραστηριότητας.

    const/16 v1, 0x62
new-array v1, v1, [B
fill-array-data v1, :encrypted_data
const/16 v0, 0x1f
invoke-static {v0, v1}, Lcom/uc/browser/core/d/c/g;->j(I[B)[B
move-result-object v1
array-length v2, v1
invoke-static {v2}, Ljava/lang/String;->valueOf(I)Ljava/lang/String;
move-result-object v2
const-string v0, "ololo"
invoke-static {v0, v2}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I

Συναρμολογούμε, υπογράφουμε, εγκαθιστούμε, εκτοξεύουμε. Λαμβάνουμε ένα NullPointerException επειδή η μέθοδος επέστρεψε null.

Κατά την περαιτέρω ανάλυση του κώδικα, ανακαλύφθηκε μια συνάρτηση που αποκρυπτογραφεί ενδιαφέρουσες γραμμές: "META-INF/" και ".RSA". Φαίνεται ότι η εφαρμογή επαληθεύει το πιστοποιητικό της. Ή ακόμα και δημιουργεί κλειδιά από αυτό. Δεν θέλω πραγματικά να ασχοληθώ με το τι συμβαίνει με το πιστοποιητικό, οπότε απλώς θα του δώσουμε το σωστό πιστοποιητικό. Ας διορθώσουμε την κρυπτογραφημένη γραμμή έτσι ώστε αντί για "META-INF/" να λάβουμε "BLABLINF/", δημιουργήστε έναν φάκελο με αυτό το όνομα στο APK και προσθέστε το πιστοποιητικό του προγράμματος περιήγησης σκίουρου εκεί.

Συναρμολογούμε, υπογράφουμε, εγκαθιστούμε, εκτοξεύουμε. Λοταρία! Έχουμε το κλειδί!

Μιτ

Λάβαμε ένα κλειδί και ένα διάνυσμα αρχικοποίησης ίσο με το κλειδί. Ας προσπαθήσουμε να αποκρυπτογραφήσουμε την απόκριση διακομιστή σε λειτουργία CBC.

Ψάχνετε για τρωτά σημεία στο πρόγραμμα περιήγησης UC

Βλέπουμε τη διεύθυνση URL του αρχείου, κάτι παρόμοιο με το MD5, το "extract_unzipsize" και έναν αριθμό. Ελέγχουμε: το MD5 του αρχείου είναι το ίδιο, το μέγεθος της μη συσκευασμένης βιβλιοθήκης είναι το ίδιο. Προσπαθούμε να επιδιορθώσουμε αυτήν τη βιβλιοθήκη και να τη δώσουμε στο πρόγραμμα περιήγησης. Για να δείξουμε ότι η επιδιορθωμένη βιβλιοθήκη μας έχει φορτωθεί, θα εκκινήσουμε μια Πρόθεση δημιουργίας SMS με το κείμενο "PWNED!" Θα αντικαταστήσουμε δύο απαντήσεις από τον διακομιστή: puds.ucweb.com/upgrade/index.xhtml και να κατεβάσετε το αρχείο. Στο πρώτο αντικαθιστούμε το MD5 (το μέγεθος δεν αλλάζει μετά την αποσυσκευασία), στο δεύτερο δίνουμε το αρχείο με την επιδιορθωμένη βιβλιοθήκη.

Το πρόγραμμα περιήγησης προσπαθεί να πραγματοποιήσει λήψη του αρχείου πολλές φορές, μετά από αυτό δίνει ένα σφάλμα. Προφανώς κάτι
δεν του αρέσει. Ως αποτέλεσμα της ανάλυσης αυτής της θολής μορφής, αποδείχθηκε ότι ο διακομιστής μεταδίδει επίσης το μέγεθος του αρχείου:

Ψάχνετε για τρωτά σημεία στο πρόγραμμα περιήγησης UC

Είναι κωδικοποιημένο σε LEB128. Μετά την ενημέρωση κώδικα, το μέγεθος του αρχείου με τη βιβλιοθήκη άλλαξε λίγο, οπότε το πρόγραμμα περιήγησης θεώρησε ότι η λήψη του αρχείου έγινε στραβά και μετά από αρκετές προσπάθειες παρουσίασε ένα σφάλμα.

Προσαρμόζουμε το μέγεθος του αρχείου... Και – νίκη! 🙂 Το αποτέλεσμα είναι στο βίντεο.

https://www.youtube.com/watch?v=Nfns7uH03J8

Συνέπειες και αντίδραση προγραμματιστή

Με τον ίδιο τρόπο, οι χάκερ θα μπορούσαν να χρησιμοποιήσουν την ανασφαλή λειτουργία του προγράμματος περιήγησης UC για τη διανομή και την εκτέλεση κακόβουλων βιβλιοθηκών. Αυτές οι βιβλιοθήκες θα λειτουργούν στο πλαίσιο του προγράμματος περιήγησης, επομένως θα λαμβάνουν όλα τα δικαιώματα συστήματος. Ως αποτέλεσμα, η δυνατότητα εμφάνισης παραθύρων phishing, καθώς και πρόσβαση στα αρχεία εργασίας του πορτοκαλί κινέζικου σκίουρου, συμπεριλαμβανομένων των στοιχείων σύνδεσης, των κωδικών πρόσβασης και των cookies που είναι αποθηκευμένα στη βάση δεδομένων.

Επικοινωνήσαμε με τους προγραμματιστές του UC Browser και τους ενημερώσαμε για το πρόβλημα που βρήκαμε, προσπαθήσαμε να επισημάνουμε την ευπάθεια και την επικινδυνότητά του, αλλά δεν συζήτησαν τίποτα μαζί μας. Εν τω μεταξύ, το πρόγραμμα περιήγησης συνέχισε να επιδεικνύει το επικίνδυνο χαρακτηριστικό του σε κοινή θέα. Αλλά μόλις αποκαλύψαμε τις λεπτομέρειες της ευπάθειας, δεν ήταν πλέον δυνατό να το αγνοήσουμε όπως πριν. 27 Μαρτίου ήταν
κυκλοφόρησε μια νέα έκδοση του UC Browser 12.10.9.1193, η οποία είχε πρόσβαση στον διακομιστή μέσω HTTPS: puds.ucweb.com/upgrade/index.xhtml.

Επιπλέον, μετά τη "διόρθωση" και μέχρι τη στιγμή της σύνταξης αυτού του άρθρου, η προσπάθεια ανοίγματος ενός PDF σε ένα πρόγραμμα περιήγησης είχε ως αποτέλεσμα ένα μήνυμα σφάλματος με το κείμενο "Ωχ, κάτι πήγε στραβά!" Δεν υποβλήθηκε αίτημα στον διακομιστή κατά την προσπάθεια ανοίγματος ενός PDF, αλλά υποβλήθηκε αίτημα κατά την εκκίνηση του προγράμματος περιήγησης, το οποίο υποδηλώνει τη συνεχιζόμενη δυνατότητα λήψης εκτελέσιμου κώδικα κατά παράβαση των κανόνων του Google Play.

Πηγή: www.habr.com

Προσθέστε ένα σχόλιο