Jelajahi bug iOS dengan Hopper

Halo! Nama saya Alexander Nikishin, saya sedang mengembangkan aplikasi iOS di Badoo. Dalam artikel itu, saya akan berbicara tentang bagaimana kami menyelidiki bug di UIKit, yang tidak ingin diperbaiki Apple selama enam bulan.



Semuanya dimulai pada Agustus 2019 dengan versi beta pertama dari iOS 13. Kemudian kami pertama kali menemui masalah. Dalam aplikasi Badoo dan Bumble, kami terus berupaya meningkatkan antarmuka dan, misalnya, kami mencoba mengoptimalkan proses pendaftaran yang membosankan dan tidak dicintai sebanyak mungkin. Tooltip prediktif sistematis di atas keyboard adalah cara terbaik untuk mengurangi jumlah klik pengguna saat memasukkan data. Namun, dalam versi baru iOS, kami terkejut menemukan bahwa permintaan untuk memasukkan nomor telepon hilang.



Ketika versi GM keluar, kami menyadari bahwa masalahnya tidak diperbaiki. Tampaknya bagi kami bahwa bug yang sangat jelas tidak dapat dilewatkan oleh tes regresi, yang mungkin dimiliki Apple di gudang persenjataannya, dan kami mulai menunggu koreksi dalam paket pembaruan besar pertama. Pengembang lain berharap untuk ini . Namun, dengan merilis versi 13.1, tidak ada yang berubah, dan kami tidak punya pilihan selain membuka radar, yang kami lakukan pada awal Oktober. Waktu berlalu, iOS 13.2 dan 13.3 keluar, dan bug itu tetap tidak diperbaiki.

Pada bulan Februari, tim pendaftaran kami mendapat waktu luang dari mengerjakan backlog, dan saya memutuskan untuk menyelidiki masalah ini sedikit lebih dalam.

Sebelum Anda mulai menggali, Anda harus mencari cara untuk melakukannya. Karena petunjuk terus bekerja pada beberapa jenis keyboard, pemikiran pertama adalah mempelajari hierarki pandangannya di berbagai versi iOS.


iOS 12 vs iOS 13

Segera menjadi jelas bahwa Apple di iOS 13 refactored implementasi keyboard dan menyorot petunjuk di pengontrol terpisah ( UIPredictionViewController). Jelas, kecenderungan modularisasi dan dekomposisi juga telah mencapainya (omong-omong, Artyom Loenko baru-baru ini berbicara tentang pengalaman kami dalam aplikasi mereka ). Kemungkinan besar, karena ini, ada regresi fungsi. Lingkaran pencarian mulai menyempit.

Saya pikir sebagian besar pengembang iOS tahu bahwa mudah untuk menemukan antarmuka kelas sistem pribadi di domain publik: cukup masukkan satu permintaan di mesin pencari. Dalam studi tentang antarmuka kelas UIPredictionViewController di mata menangkap salah satu metodenya:



Tampaknya ada petunjuk, yang cukup mudah untuk diperiksa, menggunakan fungsi implementasi spoofing alat lama yang baik (Swizzling). Kami akan menggunakannya sehingga fungsi yang "dicurigai" selalu mengembalikan nilai sebenarnya:

+(void)swizzleIsVisibleForInputDelegate {
    SEL targetSelector = sel_getUid("isVisibleForInputDelegate:inputViews:");
    Class targetClass = NSClassFromString(@”UIPredictionViewController”);
    if (targetClass == nil) {
        return;
    }
    if (![targetClass instancesRespondToSelector:targetSelector]) {
        return;
    }

    Method method = class_getInstanceMethod(targetClass, targetSelector);
    if (method == NULL) {
        return;
    }

    IMP originalImplementation = method_getImplementation(method);
    IMP newImp = imp_implementationWithBlock(^BOOL(id me, id delegate, id views) {
        //   ,        . 
        BOOL result = ((bool (*)(id,SEL,id,id))originalImplementation)(me, targetSelector, delegate, views);
        if ([delegate isKindOfClass:[UITextField class]] && [delegate keyboardType] == UIKeyboardTypePhonePad) {
            return YES;
        }
        return result;
    });

    method_setImplementation(method, newImp);
}

Mulai ulang proyek uji, saya menemukan bahwa permintaan telepon kembali ke iOS 13 dan bekerja "dalam mode normal". Hal ini dapat mengakhiri penyelidikan dan mungkin bahkan dengan sangat hati-hati menggunakan solusi pedoman Apple yang berbahaya dan terlarang ini dalam perakitan rilis dengan kemampuan untuk mengaktifkan / menonaktifkan dari jarak jauh untuk beberapa pengguna (untuk opsi manajemen fitur jarak jauh, Anda dapat melihat rekaman laporan rekan saya Katerina Trofimenko). Namun demikian, saya tidak pernah berhenti bertanya-tanya mengapa fungsi ini kembali salah ketika menggunakan jenis telepon keyboard.

Untuk sampai pada kebenaran, kita membutuhkan kode sumber fungsi. Jelas, Apple tidak mendistribusikan kode komponen iOS ke kanan dan kiri, jadi tidak mungkin untuk google itu. Hanya ada satu cara yang tersisa - rekayasa terbalik untuk mendekompilasi kode biner. Sebelumnya, saya telah mendengar tentang produk seperti Hopper beberapa kali dan membaca beberapa artikel tentang penggunaannya untuk menggali lebih dalam di bawah kap perpustakaan sistem, tetapi saya tidak pernah menggunakannya sendiri. Dan segera saya terkejut: ternyata untuk bermain-main dan mempelajari alat-alatnya, bahkan tidak perlu membeli versi lengkap. Versi demo mencakup sesi kerja 30 menit tanpa akhir tanpa kemampuan untuk menyimpan indeks dan membuat perubahan pada file biner, yang berarti itu adalah platform yang sangat baik untuk eksperimen.

Semuanya samaditerbitkan antarmuka pribadi, Anda dapat mengetahui apa yang UIPredictionViewControllermerupakan bagian dari kerangka kerja UIKitCore. Yang tersisa hanyalah menemukannya dan mengunggahnya ke Hopper. File framework biner terletak di kedalaman Xcode. Di sini, misalnya, adalah path lengkap ke UIKitCore yang kita butuhkan:

/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore

Kami membuat file seret-dan-jatuhkan di Hopper, mengkonfirmasi tindakan dalam dialog sistem dan menunggu pengindeksan selesai (dalam kasus kerangka besar seperti UIKit, ini bisa memakan waktu dari empat hingga enam menit).

Antarmuka program ini cukup sederhana, saya hanya akan mencatat elemen kunci yang diperlukan untuk penelitian saya. Di panel kiri antarmuka Anda dapat menemukan baris pencarian melalui mana kode navigasikan. Dengan mencari nama kelas yang kami minati, kami dengan cepat mendapatkan fungsi yang sedang dipelajari, kode assemblernya terbuka di jendela utama.



Di bilah tugas atas ada tombol untuk beralih mode tampilan kode. Dari kiri ke kanan:



  • Mode ASM - kode assembler;
  • Mode CFG - kode assembler dalam bentuk diagram blok (pohon), tempat potongan kode digabungkan menjadi blok, dan transisi ditampilkan sebagai cabang;
  • Mode pseudo-code - kode pseudo yang dihasilkan (kita akan membicarakan lebih lanjut di bawah ini);
  • Mode hex adalah representasi hexadecimal dari file biner, atau abracadabra, yang tidak banyak membantu kami dalam penelitian kami.

Sekarang Anda perlu mencari tahu apa yang terjadi di dalam fungsi. Tubuhnya cukup panjang, jadi hanya guru ASM, yang aku tidak bisa menyebut diriku, dapat memahami logika dengan melihat kode assembler. Untuk melakukan ini, mode kode semu akan membantu, di mana Hopper menyederhanakan operasi perakitan sebanyak mungkin, mengganti nama fungsi nyata di mana mungkin dan menggunakan nama register seperti variabel. Kelihatannya seperti ini:



Tetap hanya untuk pergi melalui logika fungsi dan memahami cabang mana yang kita masuki. Symbolic Breakpoints membantu saya dengan ini., yang dapat diinstal pada panggilan sistem, secara bersamaan mencetak semua variabel yang diperlukan dan hasil panggilan fungsi ke konsol Xcode. Dengan menggunakan cara sederhana ini, saya menemukan bahwa pelaksanaan fungsi terganggu oleh keluar awal karena fakta bahwa salah satu kondisi tidak berfungsi dalam contoh kode blok. Mari kita cari tahu apa yang terjadi di sini.

Sedikit konteks: di rbx register sedikit lebih tinggi dalam kode (yang saya hilangkan untuk kesederhanaan) adalah tautan ke objek yang mengimplementasikan protokol UITextInputTraits_Private(ini adalah versi lanjutan dari protokol publik UITextInputTraits).

Jadi, kondisi pertama adalah untuk memverifikasi bahwa prompt tidak disembunyikan oleh konfigurasi kolom input. Dalam debug, Anda dapat melihat bahwa ia menjalankan: propertihidePredictionmengembalikan salah. Kondisi kedua adalah memverifikasi bahwa keyboard tidak dalam mode "split" (di iPad Anda, tarik tombol kanan bawah ke atas , hanya 2-3% pengguna yang tahu tentang hal ini). Dengan ini juga, semuanya beres.

Berpindah. Pada tahap selanjutnya, manipulasi dengan permulaan keyboardType, yang mengisyaratkan kepada kita bahwa kebenaran ada di suatu tempat di dekatnya. Pertama kali diperiksa bahwa yang sekarang keyboardTypekurang dari atau sama dengan 0xb (atau 11 dalam format desimal). Jika kita membuka deklarasi dalam Xcode UIKeyboardType, kita akan melihat bahwa ada 13 jenis keyboard, dan salah satunya (UIKeyboardTypeAlphabet) sudah usang dan dideklarasikan sebagai referensi ke tipe lain. Yaitu, ada 12 jenis dalam enum: jika Anda mulai dari 0, yang terakhir akan memiliki nilai sama dengan 11. Dengan kata lain, kode memvalidasi nilai dalam bentuk pemeriksaan melimpah, dan sekali lagi, berhasil lulus.

Lebih jauh kita melihat kondisi yang sangat aneh if (!COND), dan untuk waktu yang lama saya tidak dapat memahami apa yang diperiksa, mengingat bahwa variabel COND tidak dideklarasikan di tempat lain dalam kode. Selain itu, Symbolic Breakpoints saya menunjukkan bahwa justru kegagalan untuk memenuhi kondisi ini yang menyebabkan keluar awal dari fungsi. Dan di sini kita tidak punya pilihan selain kembali ke mode ASM dan mempelajari bagian kode ini dalam bentuk assembler.

Untuk menemukan kondisi ini dalam daftar ASM, Anda dapat menggunakan opsi "Tidak ada duplikasi kode", yang akan menunjukkan kode semu tidak dalam bentuk kode sumber dengan kondisi if-else, tetapi dalam bentuk blok kode unik dan transisi dalam bentuk goto. Dalam hal ini, kita akan melihat posisi awal dari blok-blok ini dalam file biner dan menggunakan pointer ini untuk mencari dalam mode ASM. Jadi saya menemukan bahwa blok yang kami minati terletak di fe79f4:



Kembali ke mode ASM, kami dapat dengan mudah menemukan blok ini:



Kami sampai pada bagian yang paling sulit, di mana kami akan mengurai tiga baris kode assembler.

Saya mengenali baris pertama dari ingatan saya (terima kasih kepada pelajaran assembler di MIET asli saya, akhirnya saatnya telah datang dalam hidup saya ketika pendidikan tinggi berguna!). Semuanya cukup sederhana di sini: konstanta 0x930 (100100110000 dalam format biner) ditempatkan di register ecx.

Pada baris kedua, kita melihat instruksi bt dieksekusi pada excdan register eax. Nilai yang sudah kita ketahui, nilai yang kedua dapat dilihat pada screenshot sebelumnya: rax = [rbx keyboardType]- ini berisi jenis keyboard saat ini. rax- ini adalah register seluruh 64-bit, eax- ini adalah bagian 32-bit.

Dengan data yang diputuskan, itu masih untuk memahami logika tim. Google mengarahkan kita ke deskripsi ini :
Selects the bit in a bit string (specified with the first operand, called the bit base) at the bit-position designated by the bit offset operand (second operand) and stores the value of the bit in the CF flag.

Instruksi mengekstrak sedikit dari operan pertama (konstan 0x930) pada posisi yang ditentukan oleh operan kedua (tipe keyboard), dan menempatkannya dalam CF ( membawa bendera ). Artinya, sebagai hasilnya, CF akan berisi 0 atau 1, tergantung pada jenis keyboard.

Kami melanjutkan ke operasi terakhir jb, ia memiliki deskripsi berikut :

Jump short if below (CF=1)

Mudah untuk menebak bahwa ada transisi ke fungsi (keluar awal) jika CF adalah 1.

Pada titik ini, teka-teki mulai bertambah. Kami memiliki bit mask 100100110000 (ini berisi 12 bit sesuai dengan jumlah jenis keyboard yang tersedia), dan itu menentukan kondisi untuk keluar awal. Sekarang, jika kita memeriksa ketersediaan petunjuk untuk semua jenis keyboard dalam urutan menaik rawValue, semuanya akan ada di tempatnya.



Di iOS 12, kami tidak akan menemukan logika seperti itu - petunjuk di sana berfungsi untuk semua jenis keyboard. Saya menduga bahwa di iOS 13, Apple memutuskan untuk menonaktifkan petunjuk untuk papan ketik digital, yang pada prinsipnya dapat dimengerti: Saya tidak dapat membuat skenario di mana sistem perlu meminta angka. Rupanya, secara tidak sengaja, "tangan panas" juga mengenai UIKeyboardTypePhonePad, yang sangat mirip dengan tombol numerik biasa. Pada saat yang sama UIKeyboardTypeNamePhonePad, menggabungkan keyboard QWERTY untuk mencari kontak telepon dan tombol digital dinonaktifkan yang sama persis, ia terus menunjukkan petunjuk.

Yang mengejutkan saya, bekerja dengan Hopper ternyata sangat menyenangkan dan menghibur, untuk waktu yang lama saya tidak mendapatkan banyak penggemar. Saya berbagi temuan dengan insinyur Apple dalam laporan bug saya, dan statusnya berubah dari waktu ke waktu menjadi "Perbaikan potensial yang diidentifikasi - Untuk pembaruan OS di masa mendatang". Saya harap perbaikan ini akan menjangkau pengguna di pembaruan mendatang. Pada saat yang sama, Hopper dapat digunakan tidak hanya untuk menemukan penyebab bug Anda atau Apple, tetapi juga untuk mendeteksi perbaikan Apple untuk program pihak ketiga .

All Articles