首页
社区
课程
招聘
[原创] ARM64 内核 Hook 研究 (一)
发表于: 2022-11-28 11:51 27092

[原创] ARM64 内核 Hook 研究 (一)

2022-11-28 11:51
27092

本篇将实现一个劫持内核内BL指令跳转到自身模块函数执行的简单inline hook。

跳转到相对于PC的指定地址,并将下一条指令地址存入LR寄存器。

跳转范围:±128MB

imm26 负数使用补码表示

正数和0的补码就是该数字本身再补上符号位0。负数的补码则是将其对应正数按位取反再加1。

可能有人会有疑惑,明明立即数只有26位,跳转范围应该是±32MB才对,为什么会是±128MB呢?

因为arm64指令长度都是4字节,所以编码地址的时候除了4,比如跳转 0x4,imm26 是 1

以上代码输出:
0b10010100000000000000000000000001

输出如下

[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 函数

代码段:_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怎么办?摆!

解除内核符号限制

部分设备需要 echo 1 才行

获取符号地址

上代码:

底层原理:

使用 kallsyms_lookup_name 解析符号地址

需高版本内核!

如果该函数导出,可直接使用该函数定位,但是大部分内核中该函数并未导出。

特征码定位

通过一些特征汇编代码定位

根据导出函数,结合偏移辅助定位

比如 &printk - offsetof(printk) + offsetof(foo)

修改内核,将 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: 今天必须给我存在

代码:

问题:

加载成功后没多久就寄掉了,日志中没有原因,就戛然而止,但是还是成功打印了hijack success!

使用kprobe对内核进行hook,更方便,而且问题也更少

特别注意:

kprobes回调函数的运行期间关闭了抢占,同时也可能关闭中断。因此不论在何种情况下,在回调函数中不能调用会放弃CPU的函数(如信号量、mutex锁等),否则会领取死机重启大礼包。

下期预告:

基于 DirtyPipe漏洞 实现一个简单的 kernel root

 
31 | 30 29 28 27 26 | 25 ... 0
1  | 0  0  1  0  1  |  imm26
31 | 30 29 28 27 26 | 25 ... 0
1  | 0  0  1  0  1  |  imm26
 
 
BL 0x4
0b100101 00000000000000000000000001
 
BL -0x4
0b100101 11111111111111111111111111
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))
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))
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)
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)
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);
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)
#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)
echo 0 > /proc/sys/kernel/kptr_restrict
echo 0 > /proc/sys/kernel/kptr_restrict
 
cat /proc/kallsyms | grep xxxxx
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;
}
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;
}
 
 
 
 
 
 
 
__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
__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");
#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

[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

最后于 2022-11-28 16:39 被Ylarod编辑 ,原因:
收藏
免费 10
支持
分享
最新回复 (8)
雪    币: 1504
活跃值: (9953)
能力值: ( LV9,RANK:240 )
在线值:
发帖
回帖
粉丝
2
66666期待下期
2022-11-28 13:58
0
雪    币: 4583
活跃值: (6836)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
内核各种寄存器段啥的还不了解。   内核方面的文章很期待
2022-11-28 14:44
0
雪    币: 18
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
4
我这有个rom是        
echo 0 > /proc/sys/kernel/kptr_restrict 或者        
echo 1 > /proc/sys/kernel/kptr_restrict都不行,kallsyms文件也不显示
2022-12-5 17:22
0
雪    币: 3785
活跃值: (3942)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
5
感谢分享!
2022-12-5 17:40
0
雪    币: 1
活跃值: (150)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
大佬接开发吗?可否留个联系方式
2023-4-1 22:10
0
雪    币: 206
活跃值: (2634)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
7
没有看雪币无法回私信了 麻烦你再私信个联系方式
2023-4-4 16:52
0
雪    币: 21
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
8
大佬知道6.1版本的内核替换sys_call_table函数hook为啥会崩 , 过掉写保护了 , 就是调用被hook的函数就崩
2024-11-26 11:25
0
雪    币: 1426
活跃值: (3105)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9
感谢分享
2024-11-26 17:10
0
游客
登录 | 注册 方可回帖
返回
//