本篇将实现一个劫持内核内BL指令跳转到自身模块函数执行的简单inline hook。
跳转到相对于PC的指定地址,并将下一条指令地址存入LR寄存器。
跳转范围:±128MB
1
2
|
31
|
30
29
28
27
26
|
25
...
0
1
|
0
0
1
0
1
| imm26
|
imm26 负数使用补码表示
正数和0的补码就是该数字本身再补上符号位0。负数的补码则是将其对应正数按位取反再加1。
可能有人会有疑惑,明明立即数只有26位,跳转范围应该是±32MB才对,为什么会是±128MB呢?
因为arm64指令长度都是4字节,所以编码地址的时候除了4,比如跳转 0x4,imm26 是 1
1
2
3
4
5
|
BL
0x4
0b100101
00000000000000000000000001
BL
-
0x4
0b100101
11111111111111111111111111
|
1
2
3
4
5
6
|
from
keystone
import
*
import
struct
ks
=
Ks(KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN)
code, count
=
ks.asm(
"BL 0x4"
)
code
=
struct.unpack(
"<I"
, bytes(code))[
0
]
print
(
bin
(code))
|
以上代码输出:
0b10010100000000000000000000000001
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
|
from
capstone
import
*
from
keystone
import
*
from
unicorn
import
*
test_code
=
"""NOP
mov x0, #1
b 0x14
add x0, x0, #1
add x0, x0, #1
add x0, x0, #1
add x0, x0, #1
"""
def
hook_code(_mu, address, size, user_data):
instruction
=
_mu.mem_read(address, size)
# 将地址设置为0,来得到原始的相对跳转地址
# 设置成address可以得到实际跳转地址
for
i
in
cs.disasm(instruction,
0x0
):
print
(
'[0x%08X] %s\t%s'
%
(address, i.mnemonic, i.op_str))
ks
=
Ks(KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN)
cs
=
Cs(CS_ARCH_ARM64, CS_MODE_ARM)
code, code_count
=
ks.asm(test_code)
mu
=
Uc(UC_ARCH_ARM64, UC_MODE_ARM)
mu.mem_map(
0x0
,
0x1000
, UC_PROT_ALL)
mu.mem_write(
0x0
, bytes(code))
mu.hook_add(UC_HOOK_CODE, hook_code)
mu.emu_start(
0x0
, code_count
*
4
)
|
输出如下
[0x00000000] nop
[0x00000004] movz x0, #0x1
[0x00000008] b #0xc
[0x00000014] add x0, x0, #1
[0x00000018] add x0, x0, #1
见 arch/arm64/kernel/vmlinux.lds.S
见 arch/arm64/mm/mmu.c 的 map_kernel 函数
1
2
3
4
5
6
7
8
9
10
|
pgprot_t text_prot
=
rodata_enabled ? PAGE_KERNEL_ROX : PAGE_KERNEL_EXEC;
map_kernel_segment(pgdp, _text, _etext, text_prot, &vmlinux_text,
0
,
VM_NO_GUARD);
map_kernel_segment(pgdp, __start_rodata, __inittext_begin, PAGE_KERNEL,
&vmlinux_rodata, NO_CONT_MAPPINGS, VM_NO_GUARD);
map_kernel_segment(pgdp, __inittext_begin, __inittext_end, text_prot,
&vmlinux_inittext,
0
, VM_NO_GUARD);
map_kernel_segment(pgdp, __initdata_begin, __initdata_end, PAGE_KERNEL,
&vmlinux_initdata,
0
, VM_NO_GUARD);
map_kernel_segment(pgdp, _data, _end, PAGE_KERNEL, &vmlinux_data,
0
,
0
);
|
1
2
3
4
5
6
|
#define _PROT_DEFAULT (PTE_TYPE_PAGE | PTE_AF | PTE_SHARED)
#define PROT_DEFAULT (_PROT_DEFAULT | PTE_MAYBE_NG)
#define PROT_NORMAL (PROT_DEFAULT | PTE_PXN | PTE_UXN | PTE_WRITE | PTE_ATTRINDX(MT_NORMAL))
#define PAGE_KERNEL __pgprot(PROT_NORMAL)
#define PAGE_KERNEL_ROX __pgprot((PROT_NORMAL & ~(PTE_WRITE | PTE_PXN)) | PTE_RDONLY)
#define PAGE_KERNEL_EXEC __pgprot(PROT_NORMAL & ~PTE_PXN)
|
代码段:_text → _etext
rodata_enabled ? PAGE_KERNEL_ROX : PAGE_KERNEL_EXEC;
可读特权模式可执行,rodata_enabled为假时可写
只读数据段
PAGE_KERNEL
可读可写不可执行
初始化代码段:
rodata_enabled ? PAGE_KERNEL_ROX : PAGE_KERNEL_EXEC;
可读特权模式可执行,rodata_enabled为假时可写
初始化数据段:
PAGE_KERNEL
可读可写不可执行
数据段:
PAGE_KERNEL
可读可写不可执行
开了KASLR怎么办?摆!
解除内核符号限制
1
|
echo
0
>
/
proc
/
sys
/
kernel
/
kptr_restrict
|
部分设备需要 echo 1 才行
获取符号地址
1
|
cat
/
proc
/
kallsyms | grep xxxxx
|
上代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
uintptr_t kprobe_get_addr(const char
*
symbol_name) {
int
ret;
struct kprobe kp;
uintptr_t tmp
=
0
;
kp.addr
=
0
;
kp.symbol_name
=
symbol_name;
ret
=
register_kprobe(&kp);
tmp
=
kp.addr;
if
(ret <
0
) {
goto out;
/
/
not
function, maybe symbol
}
unregister_kprobe(&kp);
out:
return
tmp;
}
|
底层原理:
使用 kallsyms_lookup_name 解析符号地址
需高版本内核!
如果该函数导出,可直接使用该函数定位,但是大部分内核中该函数并未导出。
特征码定位
通过一些特征汇编代码定位
根据导出函数,结合偏移辅助定位
比如 &printk - offsetof(printk) + offsetof(foo)
修改内核,将 rodata_enabled 改为 0
优点:简单方便,快捷高效
缺点:安全性降低
评价:开发机要什么安全,方便就完了!
修改pte,给权限加上可写
详见下面代码
目标:__arm64_sys_faccessat 的 BL do_faccessat
目的:/memfd:
今天必须给我存在
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
__arm64_sys_faccessat
var_s0
=
0
HINT
#0x19
STR
X30, [X18],
#8
STP X29, X30, [SP,
#-0x10+var_s0]!
MOV X29, SP
LDR W8, [X0]
LDR X1, [X0,
#8]
LDR W2, [X0,
#0x10]
MOV W3, WZR
MOV W0, W8
BL do_faccessat
LDP X29, X30, [SP
+
var_s0],
#0x10
LDR X30, [X18,
#-8]!
HINT
#0x1D
RET
; End of function __arm64_sys_faccessat
|
代码:
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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
|
#include <linux/cpu.h>
#include <linux/memory.h>
#include <linux/uaccess.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kprobes.h>
#include <linux/printk.h>
#include <linux/string.h>
#include <asm-generic/errno-base.h>
#ifdef pr_fmt
#undef pr_fmt
#define pr_fmt(fmt) "InlineHookDemo: " fmt
#endif
static const uint32_t mbits
=
6u
;
static const uint32_t mask
=
0xfc000000u
;
/
/
0b11111100000000000000000000000000
static const uint32_t rmask
=
0x03ffffffu
;
/
/
0b00000011111111111111111111111111
static const uint32_t op_bl
=
0x94000000u
;
/
/
"bl"
ADDR_PCREL26
typedef
long
(
*
do_faccessat_t)(
int
, const char __user
*
,
int
,
int
) ;
static do_faccessat_t my_do_faccessat;
unsigned
int
orig_insn, hijack_insn;
unsigned
long
func_addr, insn_addr
=
0
;
uintptr_t kprobe_get_addr(const char
*
symbol_name) {
int
ret;
struct kprobe kp;
uintptr_t tmp
=
0
;
kp.addr
=
0
;
kp.symbol_name
=
symbol_name;
ret
=
register_kprobe(&kp);
tmp
=
(uintptr_t)kp.addr;
if
(ret <
0
) {
goto out;
/
/
not
function, maybe symbol
}
unregister_kprobe(&kp);
out:
return
tmp;
}
bool
is_bl_insn(unsigned
long
addr){
uint32_t insn
=
*
(uint32_t
*
)addr;
const uint32_t opc
=
insn & mask;
if
(opc
=
=
op_bl) {
return
true;
}
return
false;
}
uint64_t get_bl_target(unsigned
long
addr){
uint32_t insn
=
*
(uint32_t
*
)addr;
int64_t absolute_addr
=
(int64_t)(addr)
+
((int32_t)(insn << mbits) >> (mbits
-
2u
));
/
/
sign
-
extended
return
(uint64_t)absolute_addr;
}
uint32_t build_bl_insn(unsigned
long
addr, unsigned
long
target){
uint32_t insn
=
*
(uint32_t
*
)addr;
const uint32_t opc
=
insn & mask;
int64_t new_pc_offset
=
((int64_t)target
-
(int64_t)(addr)) >>
2
;
/
/
shifted
uint32_t new_insn
=
opc | (new_pc_offset & ~mask);
return
new_insn;
}
uint32_t get_insn(unsigned
long
addr){
return
*
(unsigned
int
*
)addr;
}
void set_insn(unsigned
long
addr, unsigned
int
insn){
cpus_read_lock();
*
(unsigned
int
*
)addr
=
insn;
cpus_read_unlock();
}
long
hijack_do_faccessat(
int
dfd, const char __user
*
filename,
int
mode,
int
flags){
char prefix[
8
];
pr_emerg(
"hijack success!"
);
copy_from_user(prefix, filename,
8
);
prefix[
7
]
=
0
;
pr_emerg(
"access: %s"
, prefix);
if
(strcmp(prefix,
"/memfd:"
)
=
=
0
) {
pr_emerg(
"magic!"
);
return
0
;
}
return
my_do_faccessat(dfd, filename, mode, flags);
}
int
ihd_init(void){
int
i;
/
/
获取函数地址
func_addr
=
kprobe_get_addr(
"__arm64_sys_faccessat"
);
pr_emerg(
"func_addr:%lX, "
, func_addr);
/
/
遍历内存找到BL指令地址
for
(i
=
0
; i <
0x100
; i
+
+
){
if
(is_bl_insn(func_addr
+
i
*
0x4
)) {
insn_addr
=
func_addr
+
i
*
0x4
;
break
;
}
}
if
(insn_addr
=
=
0
) {
/
/
未找到BL指令
return
-
ENOENT;
}
orig_insn
=
get_insn(insn_addr);
my_do_faccessat
=
(do_faccessat_t)insn_addr;
pr_emerg(
"insn_addr:%lX, "
, insn_addr);
pr_emerg(
"orig_insn:%X orig_target_addr:%lX"
, orig_insn, get_bl_target(insn_addr));
hijack_insn
=
build_bl_insn(insn_addr, (unsigned
long
)&hijack_do_faccessat);
set_insn(insn_addr, hijack_insn);
pr_emerg(
"new_insn:%X new_target_addr:%lX"
, hijack_insn, get_bl_target(insn_addr));
return
0
;
}
void ihd_exit(void){
/
/
恢复修改
set_insn(insn_addr, orig_insn);
}
module_init(ihd_init);
module_exit(ihd_exit);
MODULE_LICENSE(
"GPL"
);
MODULE_AUTHOR(
"Ylarod"
);
MODULE_DESCRIPTION(
"A simple inline hook demo"
);
|
问题:
加载成功后没多久就寄掉了,日志中没有原因,就戛然而止,但是还是成功打印了hijack success!
使用kprobe对内核进行hook,更方便,而且问题也更少
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
|
static struct kprobe kp
=
{
.symbol_name
=
"__arm64_sys_faccessat"
,
.pre_handler
=
handler_pre,
.post_handler
=
handler_post,
.fault_handler
=
handler_fault
};
static void handler_post(struct kprobe
*
p, struct pt_regs
*
regs, unsigned
long
flags) {
}
static
int
handler_fault(struct kprobe
*
p, struct pt_regs
*
regs,
int
trapnr) {
return
0
;
}
struct Param {
long
dfd;
const char __user
*
filename;
long
mode;
long
flags;
};
static
int
handler_pre(struct kprobe
*
p, struct pt_regs
*
regs) {
struct Param param
=
*
(struct Param
*
)regs
-
>regs[
0
];
/
/
注意,所有参数都被保存在x0寄存器指向的一段空间内,每个参数的长度限定为
8
字节
pr_emerg(
"hijack success!"
);
return
0
;
}
/
/
注册: register_kprobe(&kp);
|
特别注意:
kprobes回调函数的运行期间关闭了抢占,同时也可能关闭中断。因此不论在何种情况下,在回调函数中不能调用会放弃CPU的函数(如信号量、mutex锁等),否则会领取死机重启大礼包。
下期预告:
基于 DirtyPipe漏洞 实现一个简单的 kernel root
更多【 ARM64 内核 Hook 研究 (一)】相关视频教程:www.yxfzedu.com