本文为看雪论坛优秀文章
看雪论坛作者ID:Ylarod
本篇将实现一个劫持内核内BL指令跳转到自身模块函数执行的简单inline hook。
一
ARM64 指令基础
跳转到相对于PC的指定地址,并将下一条指令地址存入LR寄存器。
跳转范围:±128MB
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。
BL 0x4
0b100101 00000000000000000000000001
BL -0x4
0b100101 11111111111111111111111111
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
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 函数
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);
#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)
开了KASLR怎么办?摆!
解除内核符号限制
echo 0 > /proc/sys/kernel/kptr_restrict
部分设备需要 echo 1 才行
获取符号地址
cat /proc/kallsyms | grep xxxxx
上代码:
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 解析符号地址
需高版本内核!
如果该函数导出,可直接使用该函数定位,但是大部分内核中该函数并未导出。
三
让我们开始吧
修改内核,将 rodata_enabled 改为 0
优点:简单方便,快捷高效
缺点:安全性降低
评价:开发机要什么安全,方便就完了!
修改pte,给权限加上可写
详见下面代码:
https://github.com/null0333/aarch64_silent_syscall_hook/blob/master/set_page_flags.c#L48
目标:__arm64_sys_faccessat 的 BL do_faccessat
目的:/memfd: 今天必须给我存在
__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
代码:
#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,更方便,而且问题也更少。
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锁等),否则会领取死机重启大礼包。
看雪ID:Ylarod
https://bbs.kanxue.com/user-home-892096.htm
# 往期推荐
球分享
球点赞
球在看
点击“阅读原文”,了解更多!