首页
社区
课程
招聘
[原创]深入浅出 Keystone 框架学习
发表于: 3小时前 133

[原创]深入浅出 Keystone 框架学习

3小时前
133

通过此前的文章,我们已经掌握了两大框架。在逆向工程的工具链中,它们各司其职:

Unicorn (大脑) :负责代码。它模拟 CPU 的状态流转与内存交互。

[原创]深入浅出 Unicorn 框架学习

Capstone (眼睛) :负责代码。它将枯燥的机器码翻译成人类可读的汇编语言。

[原创]深入浅出 Capstone 框架学习

然而,仅有“大脑”和“眼睛”是不够的。当我们想要修改程序的逻辑(例如 Patch 掉反调试检查、注入 Shellcode)时,我们需要——Keystone (双手)

Keystone 是一个轻量级、多平台、多架构的汇编框架。它的作用与 Capstone 正好相反:Capstone 将机器码转为汇编,而 Keystone 将汇编代码编译回机器码。

在 Python 脚本中,我们主要使用以下两个类:

Ks(Keystone Engine)
这是汇编引擎的主入口。你需要实例化它来创建一个汇编器对象。

KsError(Exception)
异常处理类。当你的汇编代码有语法错误(如拼写错误、操作数不匹配)时,Keystone 会抛出此异常。

Keystone 的常量命名规范与 Unicorn/Capstone 保持高度一致,只需将前缀 UC_​ 或 CS_​ 换成 KS_ 即可。

常用组合对照表

对于 x86 架构,汇编语言有不同的格式。Keystone 允许通过 KS_OPT_SYNTAX 选项来切换语法风格。

KS_OPT_SYNTAX_INTEL (默认):Intel 语法。

KS_OPT_SYNTAX_ATT:AT&T 语法。

KS_OPT_SYNTAX_NASM:NASM 语法。

设置方法

在将 Keystone 集成到 Unicorn 之前,我们先单独运行它,体验一下如何将一句汇编代码翻译成机器码。

目标

将汇编指令 "xor rax, rax; inc rax" 编译为 x64 机器码。

示例代码:

运行结果:

image

ks.asm()​ 方法的返回值是一个元组 (encoding, count)

encoding(list[int])

count(int)

需求​:
在逆向分析中,我们经常遇到反调试指令(如 RDTSC​、CPUID​)或者不需要执行的垃圾代码。
假设在地址 0x400000​ 处有一条复杂的指令(例如 XOR EAX, EAX​,长度 2 字节),我们希望将其“抹去”,替换为 NOP(空指令),让 CPU 什么都不做直接滑过去。

示例代码

运行结果

image

需求​:
在破解 CrackMe 或去除混淆时,我们经常遇到关键的分支跳转,例如 TEST EAX, EAX; JZ 0xTarget​。如果验证失败,程序会跳转到错误处理分支。
我们的目标是:强制将 JZ​(条件跳转)修改为 JMP(无条件跳转),确保程序始终走向我们预期的路径,从而绕过校验。

核心挑战 (JIT 陷阱) ​:
Unicorn 使用 JIT(即时编译)技术。如果在 UC_HOOK_CODE​ 回调中动态修改当前指令的内存,CPU 实际上已经完成了该指令的取指和解码,Patch 仅对下一次执行有效。
因此,对于确定的逻辑修改,最佳实践是在模拟启动前 (Pre-Patch) 就完成内存修改。

示例代码:

运行结果:

image

通过这种方式,我们成功地利用 Keystone 改变了程序的控制流,让它按照我们的意愿执行了跳转逻辑。

需求​:
有时我们需要在程序的空白区域(Cave)注入一段全新的逻辑,比如打印调试信息、Dump 内存数据等。
我们需要在内存 0x500000​ 处写入一段 Shellcode,调用 Linux 的 write 系统调用打印 "HACKED",然后控制 CPU 跳转执行。

示例代码:

运行结果

image

通过 Keystone,我们无需手动拼凑机器码,直接用汇编语言就实现了复杂的代码注入功能。

在基础用法之外,Keystone 还提供了一些高级特性来应对特定的编译需求,比如切换汇编风格、处理相对地址计算以及解析符号标签。

对于 x86 架构,汇编语言存在多种格式。Keystone 默认使用逆向工程中最通用的 Intel 语法,但也完美支持 Linux/GDB 风格的 AT&T 语法 以及更严格的 NASM 语法

支持的语法常量:

KS_OPT_SYNTAX_INTEL (默认):Intel 语法。

KS_OPT_SYNTAX_ATT:AT&T 语法。

KS_OPT_SYNTAX_NASM:NASM 语法。

下面的代码展示了如何切换语法,并验证两种写法的编译结果是否一致。

运行结果

image

在编写汇编代码时,使用标签(Label)跳转是人类的本能,例如 jmp loop_start。然而,Keystone 是一个轻量级编译器,在 Python 绑定中默认并不支持复杂的符号解析回调(Symbol Resolver Callback)。

Pythonic 解决方案:利用 Python 强大的字符串格式化功能(f-string),在将代码送入 Keystone 编译之前,手动完成符号的“链接”工作。

示例:

假设我们需要在地址 0x401020​ 处编写一段逻辑,其中包含跳转到 0x401000​ 和 0x401050 的指令。

运行结果:

image

至此,我们已经学完了二进制分析与逆向工程的“三剑客”。

这三个框架各司其职,共同构成了一个完整的操控闭环:

协作流程图:

图片描述

# 正确做法:安装 Keystone 汇编引擎
pip install keystone-engine
 
# 错误做法:安装 'keystone'
# 'keystone' 是 OpenStack 的身份认证组件,装错会导致冲突!
# 正确做法:安装 Keystone 汇编引擎
pip install keystone-engine
 
# 错误做法:安装 'keystone'
# 'keystone' 是 OpenStack 的身份认证组件,装错会导致冲突!
目标环境 Unicorn Capstone Keystone
x86 (32位) UC_ARCH_X86​,UC_MODE_32 CS_ARCH_X86​,CS_MODE_32 KS_ARCH_X86,KS_MODE_32
x64 (64位) UC_ARCH_X86​,UC_MODE_64 CS_ARCH_X86​,CS_MODE_64 KS_ARCH_X86,KS_MODE_64
ARM (32位) UC_ARCH_ARM​,UC_MODE_ARM CS_ARCH_ARM​,CS_MODE_ARM KS_ARCH_ARM,KS_MODE_ARM
ARM Thumb UC_ARCH_ARM​,UC_MODE_THUMB CS_ARCH_ARM​,CS_MODE_THUMB KS_ARCH_ARM,KS_MODE_THUMB
ks = Ks(KS_ARCH_X86, KS_MODE_64)
# 切换到 AT&T 语法
ks.syntax = KS_OPT_SYNTAX_ATT
ks = Ks(KS_ARCH_X86, KS_MODE_64)
# 切换到 AT&T 语法
ks.syntax = KS_OPT_SYNTAX_ATT
from keystone import *
 
# 1. 准备汇编代码
# 使用分号 ; 分隔多条指令
ASSEMBLY = "xor rax, rax; inc rax"
 
try:
    # 2. 初始化汇编引擎
    # 架构: x86, 模式: 64位
    ks = Ks(KS_ARCH_X86, KS_MODE_64)
 
    # 3. 执行编译 (Assemble)
    # ks.asm(code, addr=0)
    # code: 汇编字符串
    # addr: (可选) 起始地址,用于计算相对跳转偏移,默认为 0
    encoding, count = ks.asm(ASSEMBLY)
 
    # 4. 输出结果
    print(f"Assembly: {ASSEMBLY}")
    print(f"Encoding: {encoding}"# 原始列表
    print(f"Count   : {count}")     # 指令数量
 
    # 关键步骤:将 list[int] 转换为 bytes
    # Unicorn 的 mem_write 需要 bytes 类型
    machine_code = bytes(encoding)
    print("Machine Code: ",end="")
    for i in range(len(machine_code)):
        print(f"{machine_code[i]:02x}", end=" ")
    print()
 
except KsError as e:
    print(f"ERROR: {e}")
from keystone import *
 
# 1. 准备汇编代码
# 使用分号 ; 分隔多条指令
ASSEMBLY = "xor rax, rax; inc rax"
 
try:
    # 2. 初始化汇编引擎
    # 架构: x86, 模式: 64位
    ks = Ks(KS_ARCH_X86, KS_MODE_64)
 
    # 3. 执行编译 (Assemble)
    # ks.asm(code, addr=0)
    # code: 汇编字符串
    # addr: (可选) 起始地址,用于计算相对跳转偏移,默认为 0
    encoding, count = ks.asm(ASSEMBLY)
 
    # 4. 输出结果
    print(f"Assembly: {ASSEMBLY}")
    print(f"Encoding: {encoding}"# 原始列表
    print(f"Count   : {count}")     # 指令数量
 
    # 关键步骤:将 list[int] 转换为 bytes
    # Unicorn 的 mem_write 需要 bytes 类型
    machine_code = bytes(encoding)
    print("Machine Code: ",end="")
    for i in range(len(machine_code)):
        print(f"{machine_code[i]:02x}", end=" ")
    print()
 
except KsError as e:
    print(f"ERROR: {e}")
from unicorn import *
from unicorn.x86_const import *
from keystone import *
from capstone import *
 
# 1. 初始化所有引擎
mu = Uc(UC_ARCH_X86, UC_MODE_64)
ks = Ks(KS_ARCH_X86, KS_MODE_64)
cs = Cs(CS_ARCH_X86, CS_MODE_64)
 
# 2. 准备内存环境
ADDRESS = 0x400000
MEM_SIZE = 2 * 1024 * 1024
mu.mem_map(ADDRESS, MEM_SIZE)
 
# 写入原始代码: xor eax, eax (2字节) + inc eax (2字节)
# 机器码: 31 c0 ff c0
ORIGINAL_CODE = b"\x31\xc0\xff\xc0"
mu.mem_write(ADDRESS, ORIGINAL_CODE)
 
print("--- [Before Patch] ---")
for insn in cs.disasm(mu.mem_read(ADDRESS, 4), ADDRESS):
    print(f"0x{insn.address:x}:\t{insn.mnemonic}\t{insn.op_str}")
 
# ==========================================
# 3. 核心操作:NOP Patch
# ==========================================
TARGET_ADDR = 0x400000
PATCH_LEN = 2  # 需要覆盖的长度 (xor eax, eax 是 2 字节)
 
print(f"\n[*] Patching {TARGET_ADDR:x} with {PATCH_LEN} NOPs...")
 
# 生成 NOP 机器码 ("nop; nop")
# encoding -> [0x90, 0x90]
encoding, _ = ks.asm("nop; " * PATCH_LEN)
 
# 写入内存覆盖原指令
mu.mem_write(TARGET_ADDR, bytes(encoding))
 
# ==========================================
# 4. 验证结果
# ==========================================
print("\n--- [After Patch] ---")
# 读取内存查看变化
patched_code = mu.mem_read(ADDRESS, 4)
for insn in cs.disasm(patched_code, ADDRESS):
    print(f"0x{insn.address:x}:\t{insn.mnemonic}\t{insn.op_str}")
 
# 模拟执行验证
print("\n[*] Executing...")
mu.reg_write(UC_X86_REG_RAX, 10) # 初始值 10
mu.emu_start(ADDRESS, ADDRESS + 4)
print(f"RAX = {mu.reg_read(UC_X86_REG_RAX)}")
# 结果应为 11 (因为 XOR 被 NOP 掉了,只有 INC 执行了)
from unicorn import *
from unicorn.x86_const import *
from keystone import *
from capstone import *
 
# 1. 初始化所有引擎
mu = Uc(UC_ARCH_X86, UC_MODE_64)
ks = Ks(KS_ARCH_X86, KS_MODE_64)
cs = Cs(CS_ARCH_X86, CS_MODE_64)
 
# 2. 准备内存环境
ADDRESS = 0x400000
MEM_SIZE = 2 * 1024 * 1024
mu.mem_map(ADDRESS, MEM_SIZE)
 
# 写入原始代码: xor eax, eax (2字节) + inc eax (2字节)
# 机器码: 31 c0 ff c0
ORIGINAL_CODE = b"\x31\xc0\xff\xc0"
mu.mem_write(ADDRESS, ORIGINAL_CODE)
 
print("--- [Before Patch] ---")
for insn in cs.disasm(mu.mem_read(ADDRESS, 4), ADDRESS):
    print(f"0x{insn.address:x}:\t{insn.mnemonic}\t{insn.op_str}")
 
# ==========================================
# 3. 核心操作:NOP Patch
# ==========================================
TARGET_ADDR = 0x400000
PATCH_LEN = 2  # 需要覆盖的长度 (xor eax, eax 是 2 字节)
 
print(f"\n[*] Patching {TARGET_ADDR:x} with {PATCH_LEN} NOPs...")
 
# 生成 NOP 机器码 ("nop; nop")
# encoding -> [0x90, 0x90]
encoding, _ = ks.asm("nop; " * PATCH_LEN)
 
# 写入内存覆盖原指令
mu.mem_write(TARGET_ADDR, bytes(encoding))
 
# ==========================================
# 4. 验证结果
# ==========================================
print("\n--- [After Patch] ---")
# 读取内存查看变化
patched_code = mu.mem_read(ADDRESS, 4)
for insn in cs.disasm(patched_code, ADDRESS):
    print(f"0x{insn.address:x}:\t{insn.mnemonic}\t{insn.op_str}")
 
# 模拟执行验证
print("\n[*] Executing...")
mu.reg_write(UC_X86_REG_RAX, 10) # 初始值 10
mu.emu_start(ADDRESS, ADDRESS + 4)
print(f"RAX = {mu.reg_read(UC_X86_REG_RAX)}")
# 结果应为 11 (因为 XOR 被 NOP 掉了,只有 INC 执行了)
from unicorn import *
from unicorn.x86_const import *
from keystone import *
from capstone import *
 
# 1. 初始化引擎
mu = Uc(UC_ARCH_X86, UC_MODE_64)
ks = Ks(KS_ARCH_X86, KS_MODE_64)
cs = Cs(CS_ARCH_X86, CS_MODE_64)
 
# 2. 准备内存与代码
ADDRESS = 0x400000
MEM_SIZE = 2 * 1024 * 1024
mu.mem_map(ADDRESS, MEM_SIZE)
 
# --- 构造模拟场景 ---
# 汇编逻辑:
# 0x400000: XOR EAX, EAX  (EAX = 0)
# 0x400002: INC EAX       (EAX = 1, ZF = 0)
# 0x400004: JZ  0x40000A  (关键跳转:此时不应跳转)
# 0x400006: INC EAX       (EAX = 2, 正常路径)
# 0x40000A: DEC EAX       (EAX = 0, 跳转目标)
 
CODE_ASM = """
    xor eax, eax;
    inc eax;
    jz 0x40000a;
    inc eax;
    nop; nop;
    dec eax;
"""
 
# 编译并写入原始代码
code_bytes, _ = ks.asm(CODE_ASM, ADDRESS)
mu.mem_write(ADDRESS, bytes(code_bytes))
 
print("--- [Original Logic] ---")
print("Expectation: EAX=1, ZF=0 -> JZ NOT taken -> EAX becomes 2")
for insn in cs.disasm(bytes(code_bytes), ADDRESS):
    print(f"0x{insn.address:x}:\t{insn.mnemonic}\t{insn.op_str}")
 
# ==========================================
# 3. 核心操作:静态 Patch (Pre-Patch)
# ==========================================
# 目标:将 0x400004 处的 JZ 修改为 JMP
KEY_JUMP_ADDR = 0x400004
PATCH_ASM = "jmp 0x40000a"
 
print(f"\n[*] Patching {KEY_JUMP_ADDR:x} to '{PATCH_ASM}'...")
 
# 编译新指令
# 关键:传入 KEY_JUMP_ADDR 以计算相对偏移
patch_code, _ = ks.asm(PATCH_ASM, KEY_JUMP_ADDR)
 
# 覆盖内存
mu.mem_write(KEY_JUMP_ADDR, bytes(patch_code))
 
# ==========================================
# 4. 执行验证
# ==========================================
print("[*] Executing...")
 
try:
    # 从头开始执行
    mu.emu_start(ADDRESS, ADDRESS + len(code_bytes))
except UcError as e:
    print(e)
 
# 检查结果
final_eax = mu.reg_read(UC_X86_REG_EAX)
print(f"\n[*] Execution finished. Final EAX = {final_eax}")
 
# 逻辑验证:
# 原逻辑:1 -> inc -> 2
# Patch后:1 -> jmp -> dec -> 0
if final_eax == 0:
    print(">>> SUCCESS: Jump taken (Logic altered)!")
else:
    print(">>> FAILED: Jump not taken.")
from unicorn import *
from unicorn.x86_const import *
from keystone import *
from capstone import *
 
# 1. 初始化引擎
mu = Uc(UC_ARCH_X86, UC_MODE_64)
ks = Ks(KS_ARCH_X86, KS_MODE_64)
cs = Cs(CS_ARCH_X86, CS_MODE_64)
 
# 2. 准备内存与代码
ADDRESS = 0x400000
MEM_SIZE = 2 * 1024 * 1024
mu.mem_map(ADDRESS, MEM_SIZE)
 
# --- 构造模拟场景 ---
# 汇编逻辑:
# 0x400000: XOR EAX, EAX  (EAX = 0)
# 0x400002: INC EAX       (EAX = 1, ZF = 0)
# 0x400004: JZ  0x40000A  (关键跳转:此时不应跳转)
# 0x400006: INC EAX       (EAX = 2, 正常路径)
# 0x40000A: DEC EAX       (EAX = 0, 跳转目标)
 
CODE_ASM = """
    xor eax, eax;
    inc eax;
    jz 0x40000a;
    inc eax;
    nop; nop;
    dec eax;
"""
 
# 编译并写入原始代码
code_bytes, _ = ks.asm(CODE_ASM, ADDRESS)
mu.mem_write(ADDRESS, bytes(code_bytes))
 
print("--- [Original Logic] ---")
print("Expectation: EAX=1, ZF=0 -> JZ NOT taken -> EAX becomes 2")
for insn in cs.disasm(bytes(code_bytes), ADDRESS):
    print(f"0x{insn.address:x}:\t{insn.mnemonic}\t{insn.op_str}")
 
# ==========================================
# 3. 核心操作:静态 Patch (Pre-Patch)
# ==========================================
# 目标:将 0x400004 处的 JZ 修改为 JMP
KEY_JUMP_ADDR = 0x400004
PATCH_ASM = "jmp 0x40000a"
 
print(f"\n[*] Patching {KEY_JUMP_ADDR:x} to '{PATCH_ASM}'...")
 
# 编译新指令
# 关键:传入 KEY_JUMP_ADDR 以计算相对偏移
patch_code, _ = ks.asm(PATCH_ASM, KEY_JUMP_ADDR)
 
# 覆盖内存
mu.mem_write(KEY_JUMP_ADDR, bytes(patch_code))
 
# ==========================================
# 4. 执行验证
# ==========================================
print("[*] Executing...")
 
try:
    # 从头开始执行
    mu.emu_start(ADDRESS, ADDRESS + len(code_bytes))
except UcError as e:
    print(e)
 
# 检查结果
final_eax = mu.reg_read(UC_X86_REG_EAX)
print(f"\n[*] Execution finished. Final EAX = {final_eax}")
 
# 逻辑验证:

[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!

收藏
免费 4
支持
分享
最新回复 (6)
雪    币: 204
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
66
3小时前
0
雪    币: 104
活跃值: (7159)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
tql
3小时前
0
雪    币: 361
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
4
学习一下
3小时前
0
雪    币: 369
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
5
6
3小时前
0
雪    币: 6
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
6
学习一下
2小时前
0
雪    币: 8628
活跃值: (6377)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
感谢分享
2小时前
0
游客
登录 | 注册 方可回帖
返回