间接跳转这个高级混淆,接触了unicorn,unidbg这种模拟执行脚本后,发现这种特别适合对抗反编译器无法分辨的一些混淆,本文记录学习过程以及以京麒2024的drillbeam为样本尝试去去除间接跳转。笔者的脚本能力确实有限,写的不好的地方大佬们勿喷(

样本

unsigned char dizhi[] = { 0x1,0x2,0x3,0x4,0x5 };
void rc4_crypt(unsigned char* Data, unsigned long Len_D, unsigned char* key, unsigned long Len_k) //加解密
{
unsigned char s[256];
rc4_init(s, key, Len_k);
_asm {
lea eax, label1
add eax,8
sub eax,7
push ebx
mov ebx,OFFSET dizhi
movzx ecx,byte ptr [ebx+2]
pop ebx
add eax, ecx
jmp eax
label1:
_emit 0x90
_emit 0x90
_emit 0x90
_emit 0x90

}

如上的样本就是经典的间接跳转混淆,这种jmp eax,可以让反编译工具无法计算跳转的地址,导致反编译出错,这种混淆手段在vmp中也有体现,我们分别看看这个在ida和bn的效果

ida:

直接导致cfg丢失

bn:

bn虽然能分析个大概,不过好像仍然有问题,这里粗暴的把查表的操作分析成了switch

不过有办法

把data段改成只读

就可以正常分析了,因为计算地址存储在了data段(比如我这里的查表),反编译器不会去解析data段的数据,因为data段默认可写,导致被引用都的值被看作的是变量而非常量,阻止了常量传播,设置为可读的话,bn会去把data段看成常量,这也的话动态分析就可能解析出跳转的地址,但是这种方法并不高枕无忧,还是会存在问题

解决手段

对于一般的ctf题的话,可能就只会存在一两种间接跳转的计算方式,这个时候我们就可以手动计算出然后写脚本简单去除就行,但是我们把情况放极端一点,如果一个程序充斥着几千种不重样的间接跳转,或者说ollvm里面还嵌套了间接跳转的话,挨个计算然后去除岂不是效率低

回到手动计算,总之就是计算,有没有自动化工具计算然后识别,然后去除,这不就满足了我们手动计算->ida脚本去除的完整过程了吗

这种一般就是模拟执行的过程,模拟执行的框架一般有unicorn unidbg qiling angr等等,我写unidbg要稍微多一点

  • unicorn可以看成裸的CPU,只给了CPU+内存+hook,不给os…..
  • Qiling有系统调用API,可以做到迷你OS
  • unidbg对于模拟安卓的so很友好,因为补齐了linker,JNI等等
  • angr就是符号执行,路径探索,约束求解等等

这几种可以说是都在unicorn基础上成立的,为了更加理解这种去除思路,故用unicorn写一下,而且unicorn更适合跑指令

unicorn基本使用

我们先模拟执行一下这两条指令体会一下

from unicorn import *
from unicorn.x86_const import *
#导入ARM平台寄存器常量和核心API

x86code=b'\x83\xC0\x08\x83\xE8\x07'


def hook_code(uc,address,size,user_data):
print("Tracing instruction at 0x%x, instruction size = 0x%x" %(address, size))
try:
mu=Uc(UC_ARCH_X86,UC_MODE_32)
ADDRESS=0x10000
mu.mem_map(ADDRESS,0x1000) #映射内存
mu.mem_write(ADDRESS,x86code)#指令载入内存
mu.reg_write(UC_X86_REG_EAX,0x3)


mu.hook_add(UC_HOOK_CODE,hook_code,begin=ADDRESS,end=ADDRESS)

mu.emu_start(ADDRESS,ADDRESS+len(x86code))

rax=mu.reg_read(UC_X86_REG_EAX)
print("rax =0x%x" % rax)
except UcError as e:
print("ERROR %s" %e)

#Tracing instruction at 0x10000, instruction size = 0x3
#rax =0x4

可以很好的模拟出结果

可是unicorn是纯裸的CPU,意味着我们如果暴力的把整个exe直接载入CPU的内存里,一般是运行不起来的,还需要对一些系统函数进行重定位的操作

最小化demo模拟去除

我们先假设只有一个间接跳转,先试着模拟一下

对于这种只有一个的话,我们的模拟手段就很简单的,因为我们已经找到了间接跳转的位置,所以自然可以找到需要模拟开始的地址以及结束地址,但是,这一路也不能直接裸调用,因为可以看到这里的查表操作是涉及到了读取数据段的东西了,我选择的是直接载入整个exe,然后单个开机模拟执行一段即可

1.映射并解析整个PE

模拟PE的话,跟我在模拟ELF是不一样的,ELF我模拟的时候,基地址设置0,然后模拟区间和ida看到的偏移一样即可,但是PE文件不一样,因为PE的RAW和RVA是按照节表换算的

pe = pefile.PE(PATH)
mu = Uc(UC_ARCH_X86, UC_MODE_32)

image_base = pe.OPTIONAL_HEADER.ImageBase
image_size= align_up(pe.OPTIONAL_HEADER.SizeOfImage)
mu.mem_map(image_base, image_size)

for s in pe.sections:
va = image_base + s.VirtualAddress
raw= s.get_data()
size= align_up(max(s.Misc_VirtualSize, s.SizeOfRawData))
mu.mem_write(va, raw + b"\x00" * (size - len(raw)))
2.映射栈
stack_bae=0x30000000
stack_size = 0x100000
mu.mem_map(stack_bae, stack_size)
mu.reg_write(UC_X86_REG_EBP, stack_bae + stack_size)
mu.reg_write(UC_X86_REG_ESP, stack_bae + stack_size - 0x4)

这里是把栈映射进去,把这个映射进去也是模拟栈帧创建,局部变量等等,这里减少4是为了防止栈崩掉

3.安装hook
mu.hook_add(UC_HOOK_CODE, hook_code)
def hook_code(uc,address,size,user_data):
print("Tracing instruction at 0x%x, instruction size = 0x%x eax=0x%x" %(address, size,mu.reg_read(UC_X86_REG_EAX)))

方便我们看到是哪条指令崩掉了,快速查找或者看值

4.开机(
begin = image_base + 0x11CAF
end = image_base + 0x11CC8
print(mu.mem_read(begin, 8).hex())
mu.emu_start(begin, end)
eax=mu.reg_read(UC_X86_REG_EAX)
print("eax=0x%x" %eax)

这样可以看到已经完成了模拟

5.patch

现在我们可以加一点东西了,首先,需要知道何时去patch,这里我们可以用到反汇编引擎,去识别到jmp eax,然后直接改成jmp xxxx

machine=mu.mem_read(address,size)
code=md.disasm(machine,address)
for ins in code:
if ins.mnemonic =="jmp":
if ins.op_str=="eax":
print(f"{ins.mnemonic} {ins.op_str}")
print("eax" ,mu.reg_read(UC_X86_REG_EAX))

right_m=mu.reg_read(UC_X86_REG_EAX)
right_rell=right_m-address-5
new=b"\xE9"+right_rell.to_bytes(4,"little")
n=md.disasm(new,address)
for nn in n:
print("plcae-> "f"{nn.mnemonic} {nn.op_str}")


# jmp eax
# eax 4267214
# plcae-> jmp 0x411cce

这里涉及到了keystone和capstone,感觉只要是模拟执行都绕不过这两个东西

上面就是简单的修改,不过这种修改有个bug感觉,就是修改后的指令大小有一点怕覆盖下面如果有用的指令(当然这里不成立,因为下面四个字节都没用)

这里修好了,那如何去patch回文件呢

这里可以选择直接把unicorn的内存直接修改后复制到新文件

我们选择去把unicorn内存修改然后试着覆盖回程序

但是为了防止覆盖后面的指令,所以我们先设置一个可写区域

0x11CAF-0x11CC8其实都是可以随便修改的(根据间接跳转而不同)

总脚本
from unicorn import *
from unicorn.x86_const import *
import pefile, struct
from capstone import *
from keystone import *
def align_up(x, a=0x1000): return (x + a - 1) & ~(a - 1)

md = Cs(CS_ARCH_X86, CS_MODE_32)
ks = Ks(KS_ARCH_X86, KS_MODE_32)


PATH = r"D:\Android\unicorn-pandaos\sample\132.exe"

pe = pefile.PE(PATH)
mu = Uc(UC_ARCH_X86, UC_MODE_32)

image_base = pe.OPTIONAL_HEADER.ImageBase
image_size = align_up(pe.OPTIONAL_HEADER.SizeOfImage)


fix_ins=dict()
write_size=19
def patch(patch_byte):
for address,dest in fix_ins.items():
print("address->dest",hex(address),"->",hex(dest))
start=address-write_size

right_rell=dest-start-5
new=b"\xE9"+right_rell.to_bytes(4,"little")
n=md.disasm(new,start)
for nn in n:
print("plcae-> "f"{nn.mnemonic} {nn.op_str}")


mu.mem_write(start,new)
mu.mem_write(start+len(new),b"\x90"*(write_size)) #patch
#print(mu.mem_read(start,30))


src=bytearray(open(PATH,"rb").read())
for s in pe.sections:
raw_size=s.SizeOfRawData
if raw_size==0:
continue
sec=image_base+s.VirtualAddress
sec_off=s.PointerToRawData
buf=mu.mem_read(sec,raw_size)
src[sec_off:sec_off+raw_size]=buf

with open(patch_byte,"wb") as f:
f.write(src)
print("dump")

def hook_memory(uc,access,address,size,value,userdata):
pc=uc.reg_read(UC_X86_REG_EIP)
print("memory error :pc %x address: %x size %x" %(pc,address,size))

def hook_code(uc,address,size,user_data):
machine=mu.mem_read(address,size)
code=md.disasm(machine,address)
for ins in code:
if ins.mnemonic =="jmp":
if ins.op_str=="eax":
print(f"{ins.mnemonic} {ins.op_str}")
print("eax" ,mu.reg_read(UC_X86_REG_EAX))

right_m=mu.reg_read(UC_X86_REG_EAX)
# right_rell=right_m-address-5
# new=b"\xE9"+right_rell.to_bytes(4,"little")
# n=md.disasm(new,address)
# for nn in n:
# print("plcae-> "f"{nn.mnemonic} {nn.op_str}")
fix_ins[address]=right_m
#mu.emu_stop()




print("Tracing instruction at 0x%x, instruction size = 0x%x eax=0x%x" %(address, size,mu.reg_read(UC_X86_REG_EAX)))


try:


mu.mem_map(image_base, image_size)

for s in pe.sections:
va = image_base + s.VirtualAddress
raw= s.get_data()
size= align_up(max(s.Misc_VirtualSize, s.SizeOfRawData))
mu.mem_write(va, raw + b"\x00" * (size - len(raw)))


stack_bae=0x30000000
stack_size = 0x100000
mu.mem_map(stack_bae, stack_size)
mu.reg_write(UC_X86_REG_EBP, stack_bae + stack_size)
mu.reg_write(UC_X86_REG_ESP, stack_bae + stack_size - 0x1000)

begin = image_base + 0x11CAF
end = image_base + 0x011CCe
mu.hook_add(UC_HOOK_CODE, hook_code,begin=begin,end=end)
mu.hook_add(UC_HOOK_MEM_UNMAPPED, hook_memory, 0)
mu.reg_write(UC_X86_REG_EIP,image_base+0x011CAF)

mu.emu_start(begin, end)
eax=mu.reg_read(UC_X86_REG_EAX)

patch_byte=PATH+"__pathced"
patch(patch_byte)
except UcError as e:
print(e)

对比一下

去除前

去除后

缺陷:以上只是简单的单个模拟,目的只是学习,缺陷也十分明显

第一是eax寄存器是固定的情况,当然可能会出现jmp ecx这种,如果为了更全面的考虑可以把所有通用寄存器都加入进去

第二是如果间接跳转存在条件判断的问题,例如arm的 CSEL和BR模式的话,也无能为力了

第三是回溯修改的区间被我固定了,如果遇到大小不一致的话,也很难去去除

京麒CTF2024 drillbeam

这个题是间接跳转+ollvm混淆,导致解题难度比较大,赛中尝试的是去用trace到每一个函数,然后挨个看,找到了xxtea加密,结合d810去混淆勉强能看,不过init_array也有间接跳转,而且是存在修改delta的操作的

这里我看了一个师傅的unidbg脚本去间接跳转和去平坦化

https://bbs.kanxue.com/thread-277086.htm

学习一下并且尝试为这个题写一下

注:这里只讨论如何去除间接跳转,不讨论做这个题的过程

(含有检测模拟器的操作)

可以看到间接跳转数量多,而且是这种存在条件判断的,不能简单的去除,如果只是patch,cfg不会被修复

我们试一下unidbg而不是unicorn因为unidbg在模拟安卓层面可以让我们少走一些弯路

unidbg基础使用

像unidbg这种模拟执行框架,属于是比较成熟的了,可以帮我们完成so的加载和解析

初始化

public jingqi(){
emulator=AndroidEmulatorBuilder.for64Bit()
.addBackendFactory(new Unicorn2Factory(true))//后端,指令执行引擎
.setProcessName("com.primite.drillbeam")
.build();

Memory memory =emulator.getMemory();
//获取内存管理模块
memory.setLibraryResolver(new AndroidResolver(23));
vm= emulator.createDalvikVM(new File("D:\\下载\\ctf题目\\2025京麒杯\\drillbeam.apk"));
vm.setJni(this);
//将chal对象注册位JNI回调处理器
vm.setVerbose(true);
//打印DAlivik调用日志
NativeApi =vm.resolveClass("com.primite.drillbeam.Check");
DalvikModule dm= vm.loadLibrary("re0",true);


Module module=dm.getModule();//获取.so的模块,可以调用方法



dm.callJNI_OnLoad(emulator);
}

这里就是一些初始化的操作了,加载了模拟器,内存管理模块以及要调用的so模块,如果这里我们需要补充其他的so的加载,也只需要去加载一下就行了

Unidbg Trace

注意把代码放在loadlibrary之前

PrintStream pp= null;
try {
pp = new PrintStream(new FileOutputStream(tracefile),true);
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
module = dm.getModule();
emulator.traceCode(module.base,module.base+module.size).setRedirect(pp);

这种trace手段的话,是有寄存器信息的,这里的setRedirect是设置重定向,把输出结果从默认的控制台保存到文本文件里面。

Trace init_array函数

memory.setCallInitFunction(true);
memory.addModuleListener(new ModuleListener() {
@Override
public void onLoaded(Emulator<?> emulator, Module module) {
if(module.name.contains("sunstring")){
try {
emulator.traceCode(module.base, module.base+module.size).setRedirect(new PrintStream(new FileOutputStream("trace111111.txt"),true));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
});

unidbg动态调试

  1. 基于unicorn的console debugger同样不用管地址是否 + 1,会自己转换
  2. 附加下断点
    Debugger debugger = emulator.attach();
    debugger.addBreakPoint(module.base + 0x1AF4);
    debugger.addBreakPoint(module.base + 0x1AEC);
  3. emulator.attach支持几种调试方式,默认是console debugger
  4. 类似IDA动态调试,断点触发后,会显示寄存器信息,汇编指令
  5. 可以通过输入命令进行打印内存、写寄存器、跳到下一个断点、打印函数栈等操作
  6. 回车两下或者随便输错一个指令,就会打印出命令的帮助信息
    b用于下断点(blr、b0x40228000)
    m用于读内存(mr0、mx0、m0x40228000、msp)
    bt用于查看函数栈
    c 跳到下一个断点
    s 单步调试

配合前面的init_array就可以成功调试init_array函数


hook

public void logIns()
{
emulator.getBackend().hook_add_new(new CodeHook() {
@Override
public void hook(Backend backend, long address, int size, Object user) {
Capstone capstone = new Capstone(Capstone.CS_ARCH_ARM64,Capstone.CS_MODE_ARM);
byte[] bytes = emulator.getBackend().mem_read(address, 4);
Instruction[] disasm = capstone.disasm(bytes, 0);
System.out.printf("%x:%s %s\n",address-module.base ,disasm[0].getMnemonic(),disasm[0].getOpStr());
}

@Override
public void onAttach(UnHook unHook) {

}

@Override
public void detach() {

}
}, module.base, module.base+module.size, null);
}

间接跳转分析

这一类的间接跳转感觉都比较相似

都是CMP CSEL的形式或者有

不过都是 比较,然后根据比较的结果选取值作为偏移,然后去查表加基址常量,然后br跳转

这种条件是不固定的话,我们就需要去计算不同情况下的跳转情况

我们发现程序的间接跳转可以说有很多种,比如cmp到br之间有CSEL LT 和CSEL EQ CSINC NE

以及cmp和br中间还有有用的逻辑,但凡多patch一点,就会导致程序进入死循环,作者应该是采用了LLVM的间接跳转去替换掉了一些条件跳转,所以去除难道比较大,而且兼容必须要好才行,因为cmp到br之间的长度也不固定,计算方式不一样

去除思路

首先我们希望去除后的最好形式是

CMP             W8, W19
CSEL W9, W26, W24, LT
LDR X9, [X28,W9,UXTW#3]
ADD X9, X9, X20
BR X9
CMP             W8, W19
BLT xxx
B xxxx
nop n

1.寻找到间接跳转,可以选择匹配到CMP之后向下找,找到CSEL BR就说明找到了间接跳转

2.模拟计算跳转地址,向下找到操作符LT,然后分w9两种情况去计算最后的跳转地址

3.patch cmp之后的知道BR

之所以采用第二个模拟器去模拟执行得到第二个结果,是因为计算的 话兼容性不太好,这里的计算寄存器种类多,而且还有多余的结果

1.trace拿到值

用bn试试

可以看到这里是有查表的操作的

package jingqi;
import capstone.Capstone;
import capstone.api.Instruction;
import com.alibaba.fastjson.util.IOUtils;
import com.alibaba.fastjson.util.ModuleUtil;
import com.github.unidbg.*;
import com.github.unidbg.arm.backend.Backend;
import com.github.unidbg.arm.backend.CodeHook;
import com.github.unidbg.arm.backend.UnHook;
import com.github.unidbg.arm.backend.Unicorn2Factory;
import com.github.unidbg.arm.context.Arm32RegisterContext;
import com.github.unidbg.arm.context.RegisterContext;
import com.github.unidbg.debugger.BreakPointCallback;
import com.github.unidbg.debugger.Debugger;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.memory.Memory;
import com.sun.jna.Pointer;
import sample.sample;
import unicorn.Arm64Const;
import unicorn.ArmConst;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.lang.annotation.Native;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;


public class jingqi extends AbstractJni {

private static final java.util.Map<String, Integer> REG_MAP = new java.util.HashMap<>();

static {
// X0~X30
REG_MAP.put("x0", Arm64Const.UC_ARM64_REG_X0);
REG_MAP.put("x1", Arm64Const.UC_ARM64_REG_X1);
REG_MAP.put("x2", Arm64Const.UC_ARM64_REG_X2);
REG_MAP.put("x3", Arm64Const.UC_ARM64_REG_X3);
REG_MAP.put("x4", Arm64Const.UC_ARM64_REG_X4);
REG_MAP.put("x5", Arm64Const.UC_ARM64_REG_X5);
REG_MAP.put("x6", Arm64Const.UC_ARM64_REG_X6);
REG_MAP.put("x7", Arm64Const.UC_ARM64_REG_X7);
REG_MAP.put("x8", Arm64Const.UC_ARM64_REG_X8);
REG_MAP.put("x9", Arm64Const.UC_ARM64_REG_X9);
REG_MAP.put("x10", Arm64Const.UC_ARM64_REG_X10);
REG_MAP.put("x11", Arm64Const.UC_ARM64_REG_X11);
REG_MAP.put("x12", Arm64Const.UC_ARM64_REG_X12);
REG_MAP.put("x13", Arm64Const.UC_ARM64_REG_X13);
REG_MAP.put("x14", Arm64Const.UC_ARM64_REG_X14);
REG_MAP.put("x15", Arm64Const.UC_ARM64_REG_X15);
REG_MAP.put("x16", Arm64Const.UC_ARM64_REG_X16);
REG_MAP.put("x17", Arm64Const.UC_ARM64_REG_X17);
REG_MAP.put("x18", Arm64Const.UC_ARM64_REG_X18);
REG_MAP.put("x19", Arm64Const.UC_ARM64_REG_X19);
REG_MAP.put("x20", Arm64Const.UC_ARM64_REG_X20);
REG_MAP.put("x21", Arm64Const.UC_ARM64_REG_X21);
REG_MAP.put("x22", Arm64Const.UC_ARM64_REG_X22);
REG_MAP.put("x23", Arm64Const.UC_ARM64_REG_X23);
REG_MAP.put("x24", Arm64Const.UC_ARM64_REG_X24);
REG_MAP.put("x25", Arm64Const.UC_ARM64_REG_X25);
REG_MAP.put("x26", Arm64Const.UC_ARM64_REG_X26);
REG_MAP.put("x27", Arm64Const.UC_ARM64_REG_X27);
REG_MAP.put("x28", Arm64Const.UC_ARM64_REG_X28);
REG_MAP.put("x29", Arm64Const.UC_ARM64_REG_X29);
REG_MAP.put("x30", Arm64Const.UC_ARM64_REG_X30);

// W0~W30 (低 32 位寄存器)
REG_MAP.put("w0", Arm64Const.UC_ARM64_REG_W0);
REG_MAP.put("w1", Arm64Const.UC_ARM64_REG_W1);
REG_MAP.put("w2", Arm64Const.UC_ARM64_REG_W2);
REG_MAP.put("w3", Arm64Const.UC_ARM64_REG_W3);
REG_MAP.put("w4", Arm64Const.UC_ARM64_REG_W4);
REG_MAP.put("w5", Arm64Const.UC_ARM64_REG_W5);
REG_MAP.put("w6", Arm64Const.UC_ARM64_REG_W6);
REG_MAP.put("w7", Arm64Const.UC_ARM64_REG_W7);
REG_MAP.put("w8", Arm64Const.UC_ARM64_REG_W8);
REG_MAP.put("w9", Arm64Const.UC_ARM64_REG_W9);
REG_MAP.put("w10", Arm64Const.UC_ARM64_REG_W10);
REG_MAP.put("w11", Arm64Const.UC_ARM64_REG_W11);
REG_MAP.put("w12", Arm64Const.UC_ARM64_REG_W12);
REG_MAP.put("w13", Arm64Const.UC_ARM64_REG_W13);
REG_MAP.put("w14", Arm64Const.UC_ARM64_REG_W14);
REG_MAP.put("w15", Arm64Const.UC_ARM64_REG_W15);
REG_MAP.put("w16", Arm64Const.UC_ARM64_REG_W16);
REG_MAP.put("w17", Arm64Const.UC_ARM64_REG_W17);
REG_MAP.put("w18", Arm64Const.UC_ARM64_REG_W18);
REG_MAP.put("w19", Arm64Const.UC_ARM64_REG_W19);
REG_MAP.put("w20", Arm64Const.UC_ARM64_REG_W20);
REG_MAP.put("w21", Arm64Const.UC_ARM64_REG_W21);
REG_MAP.put("w22", Arm64Const.UC_ARM64_REG_W22);
REG_MAP.put("w23", Arm64Const.UC_ARM64_REG_W23);
REG_MAP.put("w24", Arm64Const.UC_ARM64_REG_W24);
REG_MAP.put("w25", Arm64Const.UC_ARM64_REG_W25);
REG_MAP.put("w26", Arm64Const.UC_ARM64_REG_W26);
REG_MAP.put("w27", Arm64Const.UC_ARM64_REG_W27);
REG_MAP.put("w28", Arm64Const.UC_ARM64_REG_W28);
REG_MAP.put("w29", Arm64Const.UC_ARM64_REG_W29);
REG_MAP.put("w30", Arm64Const.UC_ARM64_REG_W30);

// 特殊寄存器
REG_MAP.put("sp", Arm64Const.UC_ARM64_REG_SP);
REG_MAP.put("wsp", Arm64Const.UC_ARM64_REG_WSP);
REG_MAP.put("xzr", Arm64Const.UC_ARM64_REG_XZR);
REG_MAP.put("wzr", Arm64Const.UC_ARM64_REG_WZR);
}




private final AndroidEmulator emulator;
//模拟器对象,负责指令执行,内存管理,系统调用等
private final DvmClass NativeApi;
//调用的类
public Module module;
private final VM vm;
//Dalvik VM环境,加载和运行APK/DEX代码
public jingqi(){
emulator=AndroidEmulatorBuilder.for64Bit()
.addBackendFactory(new Unicorn2Factory(true))//后端,指令执行引擎
.setProcessName("com.primite.drillbeam")
.build();

Memory memory =emulator.getMemory();
//获取内存管理模块
memory.setLibraryResolver(new AndroidResolver(23));
vm= emulator.createDalvikVM(new File("D:\\下载\\ctf题目\\2025京麒杯\\drillbeam.apk"));
vm.setJni(this);
//将chal对象注册位JNI回调处理器
vm.setVerbose(true);
//打印DAlivik调用日志
NativeApi =vm.resolveClass("com.primite.drillbeam.Check");
memory.setCallInitFunction(true);
memory.addModuleListener(new ModuleListener() {
@Override
public void onLoaded(Emulator<?> emulator, Module module) {
if(module.name.contains("re0")){
lo1g(module);
}
}
});
DalvikModule dm= vm.loadLibrary("re0",true);
module=dm.getModule();//获取.so的模块,可以调用方法

Debugger debugger=emulator.attach();
//debugger.addBreakPoint(module.base+0x33a4);


dm.callJNI_OnLoad(emulator);
}
public String calls(){
String arg1="aaaaaaaaaa";
String ret = NativeApi.newObject(null).callJniMethodObject(emulator, "calc(Ljava/lang/String;)Ljava/lang/String;",arg1).getValue().toString();
//如果是动态绑定的方法就需要写签名,而且可以在unidbg的执行流中找到这个签名(或者jadx切换位smail模式可以看到),如果目标方法是静态绑定的话可以直接传入方法名
return ret;
}
public static void main(String []args){
jingqi xv=new jingqi();
xv.lo1g(xv.module);
String result=xv.calls();
System.out.println(result);
}



public void lo1g(Module module){
emulator.getBackend().hook_add_new(new CodeHook() {
@Override
public void hook(Backend backend, long address, int size, Object user) {
Capstone capstone=new Capstone(Capstone.CS_ARCH_ARM64,Capstone.CS_MODE_ARM); //创建Capstone反汇编器实例
byte[] bytes=emulator.getBackend().mem_read(address,4); //读取当前指 令的机器码
Instruction[] disasm=capstone.disasm(bytes,0); //把bytes反汇编成Intstruction对象数组
String machine=disasm[0].getMnemonic();

if(machine.equals("br")){
String register=disasm[0].getOpStr().trim();
int ID=REG_MAP.get(register);

RegisterContext registerContext=emulator.getContext();
long value=registerContext.getLongByReg(ID)-module.base;

String hex=String.format("0x%06x",value);

int aaa= (int) (address-module.base);
System.out.println("0x"+Integer.toHexString(aaa) +" "+ disasm[0].getMnemonic()+ " " +disasm[0].getOpStr() +"----"+hex);
}
}
@Override
public void onAttach(UnHook unHook) {

}
@Override
public void detach() {

}
},module.base,module.base+module.size,null);
}

}

我们先简单的把包含这种间接跳转的trace出来,这种的话,其实是分两种情况的,第一张就是静态跳转,一个地址对应的跳转指令只有一个,另外一种就是一个地址对应了两个跳转逻辑,这种情况下想去patch成b xxx是会修改了逻辑的

但是发现其实大量的都是直接跳转,我们简单统计一下,然后把能改的直接改成 b xxx 试试

没啥效果啊。单纯改成这个B的话,只能改小部分,可是程序是有循环BR的,这就无法通用解决

2.收集

发现cmp csel之间有些长度不固定。比如

不能单一匹配连续,我这里把容错放到20长度

匹配到之后,cmp知道br 之间,每一条汇编指令,包括地址,汇编指令对应的寄存器的值封装成一个类对象然后放入栈里面

 public void brbr(Module module){
emulator.getBackend().hook_add_new(new CodeHook() {
@Override
public void hook(Backend backend, long address, int size, Object user) {
if(jingqi.haspatched.get(address)!=null ){
return;
}
// if(address-module.base<=0x2244||address-module.base>=0x2300){
// return;
// }

Capstone capstone;
capstone=new Capstone(Capstone.CS_ARCH_ARM64,Capstone.CS_MODE_ARM);

byte[] bytes=emulator.getBackend().mem_read(address,4);
Instruction[] disasm=capstone.disasm(bytes,0);

if(flagcsel==0&&(!disasm[0].getMnemonic().toLowerCase().equals("cmp"))){
return ;
}

if(flagcsel==1&&(disasm[0].getMnemonic().toLowerCase().equals("br"))){
Regs myReg=new Regs(address-module.base,emulator.getBackend());
instructAndReg ins=new instructAndReg(address-module.base,disasm,myReg);
System.out.println("push " + Long.toHexString(ins.address)+" "+disasm[0].getMnemonic() +"---- "+disasm[0].getOpStr());
st.add(ins);

String bropstr=disasm[0].getOpStr();
caluateClass clz=new caluateClass(st);
List <Integer> result=clz.caluate(emulator,module,bropstr);

flagcsel=0;
cselflag=0;
}
if(disasm[0].getMnemonic().toLowerCase().equals("cmp")){

int aaa=(int)(address-module.base);
// System.out.println("0x"+Integer.toHexString(aaa) +" "+ disasm[0].getMnemonic()+ " " +disasm[0].getOpStr());

int cnt=4;
while(cnt!=28)
{
if(address-module.base==0x024F4||address-module.base==0x2BBC||address- module.base==0x2838){ //对于没有被识别进去的过长指令可以采用这种直接手动设置的方式
flagcsel=1;
break;
}
long add=address+cnt;
long ss=add-module.base;
byte[] mach=emulator.getBackend().mem_read(add,4);
Instruction [] ddd=capstone.disasm(mach,0);
if (ddd == null || ddd.length == 0) {
break;
}
if (ddd[0].getMnemonic().equals("br") && cselflag == 0) {

break;
}
if(ddd[0].getMnemonic().equals("b")){
break;
}//||ddd[0].getMnemonic().equals("csinc")

if(ddd[0].getMnemonic().equals("csel")||ddd[0].getMnemonic().equals("csinc")||ddd[0].getMnemonic().equals("cinc")||ddd[0].getMnemonic().equals("cset")){
//System.out.println("ddd = " + ddd[0].getMnemonic() +"---- "+ddd[0].getOpStr());
cselflag=1;
}
if(cselflag==1&&ddd[0].getMnemonic().equals("br")){
flagcsel=1;
}

cnt+=4;
}
if(flagcsel==0){
cselflag=0;
}
}
if(flagcsel==1){

Regs myReg=new Regs(address-module.base,emulator.getBackend());

//List<Number> regs=myReg.getReg();
instructAndReg ins=new instructAndReg(address-module.base,disasm,myReg);

System.out.println("push " + Long.toHexString(ins.address)+" "+disasm[0].getMnemonic() +"---- "+disasm[0].getOpStr());
st.push(ins);
}

}
@Override
public void onAttach(UnHook unHook) {

}
@Override
public void detach() {

}
},module.base,module.base+module.size,null);
}
}

这里是把收集的代码写好了,这里注意unidbg的hook是链式的,我这里写两个hook无妨,下一步是如何模拟执行得到不同结果

如果存在cmp到br过长的情况的话也可以手动添加一段地址让它处于我们需要patch的区间

if(address-module.base==0x024F4||address-module.base==0x2BBC||address- module.base==0x2838){  //对于没有被识别进去的过长指令可以采用这种直接手动设置的方式
flagcsel=1;
break;
}
3.模拟执行得到不同的结果

之所以采用模拟执行去设置虚假条件得到第二个条件,是为了兼容不同寄存器计算的效果,以及多种计算方式的效果,所以兼容性比较高,但是效率就下降了

原本我是采用的递归去去除的,但是后面unidbg不太支持递归,故直接采用的开启一个unicorn模拟器去计算

class caluateClass{
public Stack<instructAndReg> stk;
public List<Integer> toAddress;
public long bAddress=0;
public long ltAddress=0;
public long brAddress=0;

public caluateClass(Stack<instructAndReg> stk){
this.stk=stk;
}
public boolean evalCond(String cond, long nzcv) {
int N = (int) ((nzcv >> 31) & 1);
int Z = (int) ((nzcv >> 30) & 1);
int C = (int) ((nzcv >> 29) & 1);
int V = (int) ((nzcv >> 28) & 1);

cond = cond.toLowerCase();
switch (cond) {
case "eq": // equal
return Z == 1;
case "ne": // not equal
return Z == 0;

case "cs": // carry set (aka hs)
case "hs":
return C == 1;
case "cc": // carry clear (aka lo)
case "lo":
return C == 0;

case "mi": // minus
return N == 1;
case "pl": // plus
return N == 0;

case "vs": // overflow set
return V == 1;
case "vc": // overflow clear
return V == 0;

case "hi": // unsigned >
return C == 1 && Z == 0;
case "ls": // unsigned <=
return C == 0 || Z == 1;

case "ge": // signed >=
return N == V;
case "lt": // signed <
return N != V;

case "gt": // signed >
return Z == 0 && N == V;
case "le": // signed <=
return Z == 1 || N != V;

case "al": // always
return true;

default:
throw new IllegalArgumentException("unknown cond: " + cond);
}
}

public List<Integer> caluate(AndroidEmulator emulator,Module module,String bropstr){
instructAndReg insReg=stk.pop();
Regs regreg;
String cond;
Instruction [] ins =insReg.instructions;
int number=Integer.parseInt(ins[0].getOpStr().substring(1));
//System.out.println("br " + ((long)insReg.regs.reg.get(number)-module.base));
brAddress=((long)insReg.regs.reg.get(number)-module.base); //计算出br的跳转地址
long startAddress;
long lastAddress=insReg.address;
while(true){
instructAndReg qiu=stk.pop();//||qiu.instructions[0].getMnemonic().equals("cset")
if(qiu.instructions[0].getMnemonic().equals("csel")||qiu.instructions[0].getMnemonic().equals("csinc")||qiu.instructions[0].getMnemonic().equals("cinc")||qiu.instructions[0].getMnemonic().equals("cset")){
//System.out.println(" "+ qiu.instructions[0].getOpStr());
//System.out.println("w9 "+ qiu.regs.reg.get(9)+ " "+"w26 "+qiu.regs.reg.get(26)+" w24 "+qiu.regs.reg.get(24));
long cselAddress=qiu.address+module.base;
long nzcv=qiu.regs.reg.get(31).longValue();
String [] parts=qiu.instructions[0].getOpStr().split(", ");
if(qiu.instructions[0].getMnemonic().equals("cinc")){
cond=parts[2].trim();
}else if(qiu.instructions[0].getMnemonic().equals("cset")){
cond=parts[1].trim();
}
else {
cond = parts[3].trim();
} boolean taken=evalCond(cond,nzcv);
if(taken==true){
ltAddress=brAddress;
}else{
bAddress=brAddress;
}
while(true){
instructAndReg cmp=stk.pop();
if(cmp.instructions[0].getMnemonic().equals("cmp")){

regreg=cmp.regs;
startAddress=cmp.address;
break;
}
}

byte[] bytes=emulator.getBackend().mem_read(startAddress+ module.base,(lastAddress-startAddress)+4);

twice twoMi = new twice(
emulator,
bytes,
ltAddress,
bAddress,
regreg,
startAddress + module.base,
lastAddress + module.base,
bropstr,
module,
cond
);


if(cond.equals("lt")||cond.equals("eq")||cond.equals("ne")) {
Patch1 patch = new Patch1(
emulator,
bytes,
twoMi.ltAddress,
twoMi.bAddress,
startAddress + module.base,
lastAddress + module.base,
module,
cselAddress,
cond
);
}
for(long i=startAddress+module.base;i<=lastAddress+module.base;i++) {
jingqi.haspatched.put(i, 1);
}
System.out.println("taken = " + taken);
break;
}
}




return this.toAddress;
}
}

class twice {
long ltAddress, bAddress;
long startAddress, endAddress;

public twice(AndroidEmulator emulator,
byte[] bytes,
long ltAddress,
long bAddress,
Regs reg,
long startAddress,
long endAddress,
String bropstr,
Module module,
String cond
) {

this.bAddress = bAddress;
this.ltAddress = ltAddress;
this.startAddress = startAddress; // 绝对地址
this.endAddress = endAddress; // 绝对地址

Backend mainBackend = emulator.getBackend();
Capstone capstone = new Capstone(Capstone.CS_ARCH_ARM64, Capstone.CS_MODE_ARM);
Instruction[] ins = capstone.disasm(bytes, 0);

for (Instruction i : ins) {
System.out.println(" " + i.getMnemonic() + " " + i.getOpStr());
}
System.out.println("baddress->" + Long.toHexString(bAddress) +
" b.ltaddress->" + Long.toHexString(ltAddress));

if (ins.length == 0 || !"cmp".equalsIgnoreCase(ins[0].getMnemonic())) {
return;
}

long modBase = module.base;
long modSize = module.size;

long mapStart = modBase & ~0xfffL;
long offset = modBase - mapStart;
long rawSize = offset + modSize;
long mapSize = (rawSize + 0xfffL) & ~0xfffL;

byte[] snapshot = mainBackend.mem_read(modBase, (int) modSize);

byte[] patched = Arrays.copyOf(bytes, bytes.length);

for (int i = 0; i < ins.length; i++) {
String m = ins[i].getMnemonic().toLowerCase();
int off = i * 4;

if (i == 0 && m.equals("cmp")) {
patched[off] = 0x1f;
patched[off + 1] = 0x20;
patched[off + 2] = 0x03;
patched[off + 3] = (byte) 0xd5;
System.out.println("[twice] patch cmp -> nop");
continue;
}

if (m.startsWith("str") || m.startsWith("stp") || m.startsWith("stur")) {
patched[off] = 0x1f;
patched[off + 1] = 0x20;
patched[off + 2] = 0x03;
patched[off + 3] = (byte) 0xd5;
System.out.println("[twice] patch store -> nop at ins#" + i + " (" + m + ")");
}
}

int lastIndex = ins.length - 1;
if (lastIndex >= 0) {
String lastM = ins[lastIndex].getMnemonic().toLowerCase();
if (lastM.equals("br")) {
int off = lastIndex * 4;
patched[off] = 0x1f;
patched[off + 1] = 0x20;
patched[off + 2] = 0x03;
patched[off + 3] = (byte) 0xd5;
System.out.println("[twice] patch br -> nop at ins#" + lastIndex);
}
}

Unicorn uc = new Unicorn(UnicornConst.UC_ARCH_ARM64, UnicornConst.UC_MODE_ARM);

try {
uc.mem_map(mapStart, mapSize, UnicornConst.UC_PROT_ALL);
uc.mem_write(modBase, snapshot);

uc.mem_write(startAddress, patched);

for (int i = 0; i < 29; i++) {
long v = reg.reg.get(i).longValue(); // 0..28 -> X0..X28
uc.reg_write(Arm64Const.UC_ARM64_REG_X0 + i, v);
}
long fp = reg.reg.get(29).longValue();
long lr = reg.reg.get(30).longValue();
uc.reg_write(Arm64Const.UC_ARM64_REG_FP, fp);
uc.reg_write(Arm64Const.UC_ARM64_REG_LR, lr);


boolean realTaken = (this.ltAddress != 0);
boolean wantTaken = !realTaken; // 在第二个引擎里我们要“走相反的那条路”

long nzcvFake = buildNzcvFor(cond.toLowerCase(), wantTaken);
uc.reg_write(Arm64Const.UC_ARM64_REG_NZCV, nzcvFake);


uc.reg_write(Arm64Const.UC_ARM64_REG_PC, startAddress);


uc.emu_start(startAddress, endAddress, 0, 50);

String brRegName = bropstr.trim(); // "x9" / "x8" ...
Integer brRegId = jingqi.REG_MAP.get(brRegName);
if (brRegId == null) {
System.out.println("[twice] unknown br reg: " + brRegName);
return;
}
long aim = ((Number) uc.reg_read(brRegId)).longValue();
long aa = aim - module.base;

System.out.printf("[twice] alt branch target reg=%s -> 0x%x (off=0x%x)%n",
brRegName, aim, aa);

if (this.bAddress == 0) {
this.bAddress = aa;
} else if (this.ltAddress == 0) {
this.ltAddress = aa;
}
} catch (Exception e) {
System.out.println("[twice] emu error: " + e);
} finally {
try { uc.emu_stop(); } catch (Exception ignored) {}
try { uc.mem_unmap(mapStart, mapSize); } catch (Exception ignored) {}
try { uc.closeAll(); } catch (Exception ignored) {}
}
}


private static long buildNzcvFor(String cond, boolean wantTrue) {
int N = 0, Z = 0, C = 0, V = 0;

switch (cond) {
case "eq": // Z == 1
Z = wantTrue ? 1 : 0;
break;
case "ne": // Z == 0
Z = wantTrue ? 0 : 1;
break;
case "lt": // N != V
if (wantTrue) {
N = 1; V = 0;
} else {
N = 0; V = 0;
}
break;
case "ge": // N == V
if (wantTrue) {
N = 0; V = 0;
} else {
N = 1; V = 0;
}
break;
case "gt": // Z == 0 && N == V
if (wantTrue) {
Z = 0; N = 0; V = 0;
} else {
Z = 1; N = 0; V = 0;
}
break;
case "le": // Z == 1 || N != V
if (wantTrue) {
Z = 1;
} else {
Z = 0; N = 0; V = 0;
}
break;
case "cs":
case "hs": // C == 1
C = wantTrue ? 1 : 0;
break;
case "cc":
case "lo": // C == 0
C = wantTrue ? 0 : 1;
break;
case "mi": // N == 1
N = wantTrue ? 1 : 0;
break;
case "pl": // N == 0
N = wantTrue ? 0 : 1;
break;
case "vs": // V == 1
V = wantTrue ? 1 : 0;
break;
case "vc": // V == 0
V = wantTrue ? 0 : 1;
break;
case "hi": // C == 1 && Z == 0
if (wantTrue) {
C = 1; Z = 0;
} else {
C = 0; Z = 0;
}
break;
case "ls": // C == 0 || Z == 1
if (wantTrue) {
C = 0;
} else {
C = 1; Z = 0;
}
break;
case "al":
default:
Z = wantTrue ? 0 : 1;
break;
}

return ((long) N << 31) |
((long) Z << 30) |
((long) C << 29) |
((long) V << 28);
}

// 现在已经不用这个函数了,但保留不会影响
private static int parseRegIndex(String reg) {
reg = reg.trim();
if (reg.startsWith("w") || reg.startsWith("x")) {
try {
return Integer.parseInt(reg.substring(1));
} catch (NumberFormatException e) {
return -1;
}
}
return -1;
}
}

这里似乎有问题的代码,递归调用 了start了,出现了内部bug… 那就只有采用新的unicorn模拟器去模拟执行这一小段,直接拷贝unidbg的内存副本,直接映射到unicorn里面

采用unicorn去计算第二个结果

4.dump so
public void dumpSo(String origSoPath, String outSoPath) {
try {
java.nio.file.Files.copy(
java.nio.file.Paths.get(origSoPath),
java.nio.file.Paths.get(outSoPath),
java.nio.file.StandardCopyOption.REPLACE_EXISTING);

Backend backend = emulator.getBackend();
long base = module.base;

try (java.io.RandomAccessFile raf = new java.io.RandomAccessFile(outSoPath, "rw")) {
for (Long addr : jingqi.haspatched.keySet()) {

long fileOffset = addr - base;

byte[] b = backend.mem_read(addr, 4);

raf.seek(fileOffset);
raf.write(b);
}
}

System.out.printf("[overlay dump] base=0x%x, patched bytes=%d -> %s%n",
base, jingqi.haspatched.size(), outSoPath);

} catch (Exception e) {
e.printStackTrace();
}
}
处理了csel eq/lt的代码

确实难,要处理情况,又要去处理逻辑分支,patch,勉强搓出来一个可以去除 cmp csel eq/lt br 的情况, 下面还有一个 CSINC 的情况,

问题

去除前后对比

前

后:

可以看到cfg算是恢复的很好的了,这题是存在ollvm平坦化

总脚本
package jingqi;
import capstone.Capstone;
import capstone.api.Instruction;
import capstone.api.RegsAccess;
import com.alibaba.fastjson.util.IOUtils;
import com.alibaba.fastjson.util.ModuleUtil;
import com.github.unidbg.*;
import com.github.unidbg.arm.backend.Backend;
import com.github.unidbg.arm.backend.CodeHook;
import com.github.unidbg.arm.backend.UnHook;
import com.github.unidbg.arm.backend.Unicorn2Factory;
import com.github.unidbg.arm.context.Arm32RegisterContext;
import com.github.unidbg.arm.context.RegisterContext;
import com.github.unidbg.debugger.BreakPointCallback;
import com.github.unidbg.debugger.Debugger;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.memory.Memory;
import com.sun.jna.Pointer;
import keystone.Keystone;
import keystone.KeystoneArchitecture;
import keystone.KeystoneMode;
import sample.sample;
import unicorn.*;

import javax.sound.midi.Patch;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.lang.annotation.Native;
import java.nio.charset.StandardCharsets;
import java.util.*;


public class jingqi extends AbstractJni {

public static final java.util.Map<String, Integer> REG_MAP = new java.util.HashMap<>();
public static final Map<Long, Integer> haspatched = new HashMap<>();

static {
// X0~X30
REG_MAP.put("x0", Arm64Const.UC_ARM64_REG_X0);
REG_MAP.put("x1", Arm64Const.UC_ARM64_REG_X1);
REG_MAP.put("x2", Arm64Const.UC_ARM64_REG_X2);
REG_MAP.put("x3", Arm64Const.UC_ARM64_REG_X3);
REG_MAP.put("x4", Arm64Const.UC_ARM64_REG_X4);
REG_MAP.put("x5", Arm64Const.UC_ARM64_REG_X5);
REG_MAP.put("x6", Arm64Const.UC_ARM64_REG_X6);
REG_MAP.put("x7", Arm64Const.UC_ARM64_REG_X7);
REG_MAP.put("x8", Arm64Const.UC_ARM64_REG_X8);
REG_MAP.put("x9", Arm64Const.UC_ARM64_REG_X9);
REG_MAP.put("x10", Arm64Const.UC_ARM64_REG_X10);
REG_MAP.put("x11", Arm64Const.UC_ARM64_REG_X11);
REG_MAP.put("x12", Arm64Const.UC_ARM64_REG_X12);
REG_MAP.put("x13", Arm64Const.UC_ARM64_REG_X13);
REG_MAP.put("x14", Arm64Const.UC_ARM64_REG_X14);
REG_MAP.put("x15", Arm64Const.UC_ARM64_REG_X15);
REG_MAP.put("x16", Arm64Const.UC_ARM64_REG_X16);
REG_MAP.put("x17", Arm64Const.UC_ARM64_REG_X17);
REG_MAP.put("x18", Arm64Const.UC_ARM64_REG_X18);
REG_MAP.put("x19", Arm64Const.UC_ARM64_REG_X19);
REG_MAP.put("x20", Arm64Const.UC_ARM64_REG_X20);
REG_MAP.put("x21", Arm64Const.UC_ARM64_REG_X21);
REG_MAP.put("x22", Arm64Const.UC_ARM64_REG_X22);
REG_MAP.put("x23", Arm64Const.UC_ARM64_REG_X23);
REG_MAP.put("x24", Arm64Const.UC_ARM64_REG_X24);
REG_MAP.put("x25", Arm64Const.UC_ARM64_REG_X25);
REG_MAP.put("x26", Arm64Const.UC_ARM64_REG_X26);
REG_MAP.put("x27", Arm64Const.UC_ARM64_REG_X27);
REG_MAP.put("x28", Arm64Const.UC_ARM64_REG_X28);
REG_MAP.put("x29", Arm64Const.UC_ARM64_REG_X29);
REG_MAP.put("x30", Arm64Const.UC_ARM64_REG_X30);

// W0~W30 (低 32 位寄存器)
REG_MAP.put("w0", Arm64Const.UC_ARM64_REG_W0);
REG_MAP.put("w1", Arm64Const.UC_ARM64_REG_W1);
REG_MAP.put("w2", Arm64Const.UC_ARM64_REG_W2);
REG_MAP.put("w3", Arm64Const.UC_ARM64_REG_W3);
REG_MAP.put("w4", Arm64Const.UC_ARM64_REG_W4);
REG_MAP.put("w5", Arm64Const.UC_ARM64_REG_W5);
REG_MAP.put("w6", Arm64Const.UC_ARM64_REG_W6);
REG_MAP.put("w7", Arm64Const.UC_ARM64_REG_W7);
REG_MAP.put("w8", Arm64Const.UC_ARM64_REG_W8);
REG_MAP.put("w9", Arm64Const.UC_ARM64_REG_W9);
REG_MAP.put("w10", Arm64Const.UC_ARM64_REG_W10);
REG_MAP.put("w11", Arm64Const.UC_ARM64_REG_W11);
REG_MAP.put("w12", Arm64Const.UC_ARM64_REG_W12);
REG_MAP.put("w13", Arm64Const.UC_ARM64_REG_W13);
REG_MAP.put("w14", Arm64Const.UC_ARM64_REG_W14);
REG_MAP.put("w15", Arm64Const.UC_ARM64_REG_W15);
REG_MAP.put("w16", Arm64Const.UC_ARM64_REG_W16);
REG_MAP.put("w17", Arm64Const.UC_ARM64_REG_W17);
REG_MAP.put("w18", Arm64Const.UC_ARM64_REG_W18);
REG_MAP.put("w19", Arm64Const.UC_ARM64_REG_W19);
REG_MAP.put("w20", Arm64Const.UC_ARM64_REG_W20);
REG_MAP.put("w21", Arm64Const.UC_ARM64_REG_W21);
REG_MAP.put("w22", Arm64Const.UC_ARM64_REG_W22);
REG_MAP.put("w23", Arm64Const.UC_ARM64_REG_W23);
REG_MAP.put("w24", Arm64Const.UC_ARM64_REG_W24);
REG_MAP.put("w25", Arm64Const.UC_ARM64_REG_W25);
REG_MAP.put("w26", Arm64Const.UC_ARM64_REG_W26);
REG_MAP.put("w27", Arm64Const.UC_ARM64_REG_W27);
REG_MAP.put("w28", Arm64Const.UC_ARM64_REG_W28);
REG_MAP.put("w29", Arm64Const.UC_ARM64_REG_W29);
REG_MAP.put("w30", Arm64Const.UC_ARM64_REG_W30);

// 特殊寄存器
REG_MAP.put("sp", Arm64Const.UC_ARM64_REG_SP);
REG_MAP.put("wsp", Arm64Const.UC_ARM64_REG_WSP);
REG_MAP.put("xzr", Arm64Const.UC_ARM64_REG_XZR);
REG_MAP.put("wzr", Arm64Const.UC_ARM64_REG_WZR);
}

public static volatile boolean ISEMU=false;
public int flagcsel=0;
public int cselflag=0;
Stack<instructAndReg> st = new Stack<instructAndReg>();
private final AndroidEmulator emulator;
//模拟器对象,负责指令执行,内存管理,系统调用等
private final DvmClass NativeApi;
//调用的类
public Module module;
private final VM vm;
public jingqi(){
emulator=AndroidEmulatorBuilder.for64Bit()
.addBackendFactory(new Unicorn2Factory(true))//后端,指令执行引擎
.setProcessName("com.primite.drillbeam")
.build();

Memory memory =emulator.getMemory();
//获取内存管理模块
memory.setLibraryResolver(new AndroidResolver(23));
vm= emulator.createDalvikVM(new File("D:\\下载\\ctf题目\\2025京麒杯\\drillbeam.apk"));
vm.setJni(this);
//将chal对象注册位JNI回调处理器
vm.setVerbose(true);
//打印DAlivik调用日志
NativeApi =vm.resolveClass("com.primite.drillbeam.Check");
memory.setCallInitFunction(true);
memory.addModuleListener(new ModuleListener() {
@Override
public void onLoaded(Emulator<?> emulator, Module module) {
if(module.name.contains("re0")){
String tracefile="D:\\下载\\ctf题目\\2025京麒杯\\drillbeam\\lib\\arm64-v8a\\trace.txt";
PrintStream pp= null;
try {
pp = new PrintStream(new FileOutputStream(tracefile),true);
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
//emulator.traceCode(module.base, module.base+module.size);
//lo1g(module);
brbr(module);
//Debugger debugger=emulator.attach();
//debugger.addBreakPoint(module.base+0x24F4);
}
}
});
DalvikModule dm= vm.loadLibrary("re0",true);
module=dm.getModule();//获取.so的模块,可以调用方法

// Debugger debugger=emulator.attach();
// debugger.addBreakPoint(module.base+0x02224);

dm.callJNI_OnLoad(emulator);
}
public String calls(){
String arg1="aaaaaaaaaa";
String ret = NativeApi.newObject(null).callJniMethodObject(emulator, "calc(Ljava/lang/String;)Ljava/lang/String;",arg1).getValue().toString();
//如果是动态绑定的方法就需要写签名,而且可以在unidbg的执行流中找到这个签名(或者jadx切换位smail模式可以看到),如果目标方法是静态绑定的话可以直接传入方法名
return ret;
}

public static void main(String []args){
jingqi xv=new jingqi();
String result=xv.calls();
System.out.println(result);
xv.dumpSo("D:\\下载\\ctf题目\\2025京麒杯\\drillbeam\\lib\\arm64-v8a\\libre0_brbr.so","D:\\下载\\ctf题目\\2025京麒杯\\drillbeam\\lib\\arm64-v8a\\111_path.so");
}

public void dumpSo(String origSoPath, String outSoPath) {
try {
java.nio.file.Files.copy(
java.nio.file.Paths.get(origSoPath),
java.nio.file.Paths.get(outSoPath),
java.nio.file.StandardCopyOption.REPLACE_EXISTING);

Backend backend = emulator.getBackend();
long base = module.base;

try (java.io.RandomAccessFile raf = new java.io.RandomAccessFile(outSoPath, "rw")) {
for (Long addr : jingqi.haspatched.keySet()) {

long fileOffset = addr - base;

byte[] b = backend.mem_read(addr, 4);

raf.seek(fileOffset);
raf.write(b);
}
}

System.out.printf("[overlay dump] base=0x%x, patched bytes=%d -> %s%n",
base, jingqi.haspatched.size(), outSoPath);

} catch (Exception e) {
e.printStackTrace();
}
}



public void brbr(Module module){
emulator.getBackend().hook_add_new(new CodeHook() {
@Override
public void hook(Backend backend, long address, int size, Object user) {
if(jingqi.haspatched.get(address)!=null ){
return;
}
// if(address-module.base<=0x2244||address-module.base>=0x2300){
// return;
// }

Capstone capstone;
capstone=new Capstone(Capstone.CS_ARCH_ARM64,Capstone.CS_MODE_ARM);

byte[] bytes=emulator.getBackend().mem_read(address,4);
Instruction[] disasm=capstone.disasm(bytes,0);

if(flagcsel==0&&(!disasm[0].getMnemonic().toLowerCase().equals("cmp"))){
return ;
}

if(flagcsel==1&&(disasm[0].getMnemonic().toLowerCase().equals("br"))){
Regs myReg=new Regs(address-module.base,emulator.getBackend());
instructAndReg ins=new instructAndReg(address-module.base,disasm,myReg);
System.out.println("push " + Long.toHexString(ins.address)+" "+disasm[0].getMnemonic() +"---- "+disasm[0].getOpStr());
st.add(ins);

String bropstr=disasm[0].getOpStr();
caluateClass clz=new caluateClass(st);
List <Integer> result=clz.caluate(emulator,module,bropstr);

flagcsel=0;
cselflag=0;
}
if(disasm[0].getMnemonic().toLowerCase().equals("cmp")){

int aaa=(int)(address-module.base);
// System.out.println("0x"+Integer.toHexString(aaa) +" "+ disasm[0].getMnemonic()+ " " +disasm[0].getOpStr());

int cnt=4;
while(cnt!=28)
{
if(address-module.base==0x024F4||address-module.base==0x2BBC||address- module.base==0x2838){ //对于没有被识别进去的过长指令可以采用这种直接手动设置的方式
flagcsel=1;
break;
}
long add=address+cnt;
long ss=add-module.base;
byte[] mach=emulator.getBackend().mem_read(add,4);
Instruction [] ddd=capstone.disasm(mach,0);
if (ddd == null || ddd.length == 0) {
break;
}
if (ddd[0].getMnemonic().equals("br") && cselflag == 0) {

break;
}
if(ddd[0].getMnemonic().equals("b")){
break;
}//||ddd[0].getMnemonic().equals("csinc")

if(ddd[0].getMnemonic().equals("csel")||ddd[0].getMnemonic().equals("csinc")||ddd[0].getMnemonic().equals("cinc")||ddd[0].getMnemonic().equals("cset")){
//System.out.println("ddd = " + ddd[0].getMnemonic() +"---- "+ddd[0].getOpStr());
cselflag=1;
}
if(cselflag==1&&ddd[0].getMnemonic().equals("br")){
flagcsel=1;
}

cnt+=4;
}
if(flagcsel==0){
cselflag=0;
}
}
if(flagcsel==1){

Regs myReg=new Regs(address-module.base,emulator.getBackend());

//List<Number> regs=myReg.getReg();
instructAndReg ins=new instructAndReg(address-module.base,disasm,myReg);

System.out.println("push " + Long.toHexString(ins.address)+" "+disasm[0].getMnemonic() +"---- "+disasm[0].getOpStr());
st.push(ins);
}

}
@Override
public void onAttach(UnHook unHook) {

}
@Override
public void detach() {

}
},module.base,module.base+module.size,null);
}
}
class Regs{
public long RegAddress;
public List<Number> reg;
public List<byte[]> vec;
public Regs(long address,Backend bk){
this.RegAddress=address;
List<Number> nb = new ArrayList<>();
for(int i=0;i<29;i++)
{
nb.add(bk.reg_read(i+ Arm64Const.UC_ARM64_REG_X0));
}
nb.add(bk.reg_read(Arm64Const.UC_ARM64_REG_FP));
nb.add(bk.reg_read(Arm64Const.UC_ARM64_REG_LR));
nb.add(bk.reg_read(Arm64Const.UC_ARM64_REG_NZCV));
this.reg=nb;

}

public List<Number> getReg(){
return this.reg;
}

}

class instructAndReg{
public long address;
public Instruction[] instructions;
public Regs regs;
public instructAndReg(long address,Instruction[] instructions,Regs regs){
this.address=address;
this.instructions=instructions;
this.regs=regs;
}
}

class caluateClass{
public Stack<instructAndReg> stk;
public List<Integer> toAddress;
public long bAddress=0;
public long ltAddress=0;
public long brAddress=0;

public caluateClass(Stack<instructAndReg> stk){
this.stk=stk;
}
public boolean evalCond(String cond, long nzcv) {
int N = (int) ((nzcv >> 31) & 1);
int Z = (int) ((nzcv >> 30) & 1);
int C = (int) ((nzcv >> 29) & 1);
int V = (int) ((nzcv >> 28) & 1);

cond = cond.toLowerCase();
switch (cond) {
case "eq": // equal
return Z == 1;
case "ne": // not equal
return Z == 0;

case "cs": // carry set (aka hs)
case "hs":
return C == 1;
case "cc": // carry clear (aka lo)
case "lo":
return C == 0;

case "mi": // minus
return N == 1;
case "pl": // plus
return N == 0;

case "vs": // overflow set
return V == 1;
case "vc": // overflow clear
return V == 0;

case "hi": // unsigned >
return C == 1 && Z == 0;
case "ls": // unsigned <=
return C == 0 || Z == 1;

case "ge": // signed >=
return N == V;
case "lt": // signed <
return N != V;

case "gt": // signed >
return Z == 0 && N == V;
case "le": // signed <=
return Z == 1 || N != V;

case "al": // always
return true;

default:
throw new IllegalArgumentException("unknown cond: " + cond);
}
}

public List<Integer> caluate(AndroidEmulator emulator,Module module,String bropstr){
instructAndReg insReg=stk.pop();
Regs regreg;
String cond;
Instruction [] ins =insReg.instructions;
int number=Integer.parseInt(ins[0].getOpStr().substring(1));
//System.out.println("br " + ((long)insReg.regs.reg.get(number)-module.base));
brAddress=((long)insReg.regs.reg.get(number)-module.base); //计算出br的跳转地址
long startAddress;
long lastAddress=insReg.address;
while(true){
instructAndReg qiu=stk.pop();//||qiu.instructions[0].getMnemonic().equals("cset")
if(qiu.instructions[0].getMnemonic().equals("csel")||qiu.instructions[0].getMnemonic().equals("csinc")||qiu.instructions[0].getMnemonic().equals("cinc")||qiu.instructions[0].getMnemonic().equals("cset")){
//System.out.println(" "+ qiu.instructions[0].getOpStr());
//System.out.println("w9 "+ qiu.regs.reg.get(9)+ " "+"w26 "+qiu.regs.reg.get(26)+" w24 "+qiu.regs.reg.get(24));
long cselAddress=qiu.address+module.base;
long nzcv=qiu.regs.reg.get(31).longValue();
String [] parts=qiu.instructions[0].getOpStr().split(", ");
if(qiu.instructions[0].getMnemonic().equals("cinc")){
cond=parts[2].trim();
}else if(qiu.instructions[0].getMnemonic().equals("cset")){
cond=parts[1].trim();
}
else {
cond = parts[3].trim();
} boolean taken=evalCond(cond,nzcv);
if(taken==true){
ltAddress=brAddress;
}else{
bAddress=brAddress;
}
while(true){
instructAndReg cmp=stk.pop();
if(cmp.instructions[0].getMnemonic().equals("cmp")){

regreg=cmp.regs;
startAddress=cmp.address;
break;
}
}

byte[] bytes=emulator.getBackend().mem_read(startAddress+ module.base,(lastAddress-startAddress)+4);

twice twoMi = new twice(
emulator,
bytes,
ltAddress,
bAddress,
regreg,
startAddress + module.base,
lastAddress + module.base,
bropstr,
module,
cond
);


if(cond.equals("lt")||cond.equals("eq")||cond.equals("ne")) {
Patch1 patch = new Patch1(
emulator,
bytes,
twoMi.ltAddress,
twoMi.bAddress,
startAddress + module.base,
lastAddress + module.base,
module,
cselAddress,
cond
);
}
for(long i=startAddress+module.base;i<=lastAddress+module.base;i++) {
jingqi.haspatched.put(i, 1);
}
System.out.println("taken = " + taken);
break;
}
}




return this.toAddress;
}
}

class twice {
long ltAddress, bAddress;
long startAddress, endAddress;

public twice(AndroidEmulator emulator,
byte[] bytes,
long ltAddress,
long bAddress,
Regs reg,
long startAddress,
long endAddress,
String bropstr,
Module module,
String cond
) {

this.bAddress = bAddress;
this.ltAddress = ltAddress;
this.startAddress = startAddress; // 绝对地址
this.endAddress = endAddress; // 绝对地址

Backend mainBackend = emulator.getBackend();
Capstone capstone = new Capstone(Capstone.CS_ARCH_ARM64, Capstone.CS_MODE_ARM);
Instruction[] ins = capstone.disasm(bytes, 0);

for (Instruction i : ins) {
System.out.println(" " + i.getMnemonic() + " " + i.getOpStr());
}
System.out.println("baddress->" + Long.toHexString(bAddress) +
" b.ltaddress->" + Long.toHexString(ltAddress));

if (ins.length == 0 || !"cmp".equalsIgnoreCase(ins[0].getMnemonic())) {
return;
}

long modBase = module.base;
long modSize = module.size;

long mapStart = modBase & ~0xfffL;
long offset = modBase - mapStart;
long rawSize = offset + modSize;
long mapSize = (rawSize + 0xfffL) & ~0xfffL;

byte[] snapshot = mainBackend.mem_read(modBase, (int) modSize);

byte[] patched = Arrays.copyOf(bytes, bytes.length);

for (int i = 0; i < ins.length; i++) {
String m = ins[i].getMnemonic().toLowerCase();
int off = i * 4;

if (i == 0 && m.equals("cmp")) {
patched[off] = 0x1f;
patched[off + 1] = 0x20;
patched[off + 2] = 0x03;
patched[off + 3] = (byte) 0xd5;
System.out.println("[twice] patch cmp -> nop");
continue;
}

if (m.startsWith("str") || m.startsWith("stp") || m.startsWith("stur")) {
patched[off] = 0x1f;
patched[off + 1] = 0x20;
patched[off + 2] = 0x03;
patched[off + 3] = (byte) 0xd5;
System.out.println("[twice] patch store -> nop at ins#" + i + " (" + m + ")");
}
}

int lastIndex = ins.length - 1;
if (lastIndex >= 0) {
String lastM = ins[lastIndex].getMnemonic().toLowerCase();
if (lastM.equals("br")) {
int off = lastIndex * 4;
patched[off] = 0x1f;
patched[off + 1] = 0x20;
patched[off + 2] = 0x03;
patched[off + 3] = (byte) 0xd5;
System.out.println("[twice] patch br -> nop at ins#" + lastIndex);
}
}

Unicorn uc = new Unicorn(UnicornConst.UC_ARCH_ARM64, UnicornConst.UC_MODE_ARM);

try {
uc.mem_map(mapStart, mapSize, UnicornConst.UC_PROT_ALL);
uc.mem_write(modBase, snapshot);

uc.mem_write(startAddress, patched);

for (int i = 0; i < 29; i++) {
long v = reg.reg.get(i).longValue(); // 0..28 -> X0..X28
uc.reg_write(Arm64Const.UC_ARM64_REG_X0 + i, v);
}
long fp = reg.reg.get(29).longValue();
long lr = reg.reg.get(30).longValue();
uc.reg_write(Arm64Const.UC_ARM64_REG_FP, fp);
uc.reg_write(Arm64Const.UC_ARM64_REG_LR, lr);


boolean realTaken = (this.ltAddress != 0);
boolean wantTaken = !realTaken; // 在第二个引擎里我们要“走相反的那条路”

long nzcvFake = buildNzcvFor(cond.toLowerCase(), wantTaken);
uc.reg_write(Arm64Const.UC_ARM64_REG_NZCV, nzcvFake);


uc.reg_write(Arm64Const.UC_ARM64_REG_PC, startAddress);


uc.emu_start(startAddress, endAddress, 0, 50);

String brRegName = bropstr.trim(); // "x9" / "x8" ...
Integer brRegId = jingqi.REG_MAP.get(brRegName);
if (brRegId == null) {
System.out.println("[twice] unknown br reg: " + brRegName);
return;
}
long aim = ((Number) uc.reg_read(brRegId)).longValue();
long aa = aim - module.base;

System.out.printf("[twice] alt branch target reg=%s -> 0x%x (off=0x%x)%n",
brRegName, aim, aa);

if (this.bAddress == 0) {
this.bAddress = aa;
} else if (this.ltAddress == 0) {
this.ltAddress = aa;
}
} catch (Exception e) {
System.out.println("[twice] emu error: " + e);
} finally {
try { uc.emu_stop(); } catch (Exception ignored) {}
try { uc.mem_unmap(mapStart, mapSize); } catch (Exception ignored) {}
try { uc.closeAll(); } catch (Exception ignored) {}
}
}


private static long buildNzcvFor(String cond, boolean wantTrue) {
int N = 0, Z = 0, C = 0, V = 0;

switch (cond) {
case "eq": // Z == 1
Z = wantTrue ? 1 : 0;
break;
case "ne": // Z == 0
Z = wantTrue ? 0 : 1;
break;
case "lt": // N != V
if (wantTrue) {
N = 1; V = 0;
} else {
N = 0; V = 0;
}
break;
case "ge": // N == V
if (wantTrue) {
N = 0; V = 0;
} else {
N = 1; V = 0;
}
break;
case "gt": // Z == 0 && N == V
if (wantTrue) {
Z = 0; N = 0; V = 0;
} else {
Z = 1; N = 0; V = 0;
}
break;
case "le": // Z == 1 || N != V
if (wantTrue) {
Z = 1;
} else {
Z = 0; N = 0; V = 0;
}
break;
case "cs":
case "hs": // C == 1
C = wantTrue ? 1 : 0;
break;
case "cc":
case "lo": // C == 0
C = wantTrue ? 0 : 1;
break;
case "mi": // N == 1
N = wantTrue ? 1 : 0;
break;
case "pl": // N == 0
N = wantTrue ? 0 : 1;
break;
case "vs": // V == 1
V = wantTrue ? 1 : 0;
break;
case "vc": // V == 0
V = wantTrue ? 0 : 1;
break;
case "hi": // C == 1 && Z == 0
if (wantTrue) {
C = 1; Z = 0;
} else {
C = 0; Z = 0;
}
break;
case "ls": // C == 0 || Z == 1
if (wantTrue) {
C = 0;
} else {
C = 1; Z = 0;
}
break;
case "al":
default:
Z = wantTrue ? 0 : 1;
break;
}

return ((long) N << 31) |
((long) Z << 30) |
((long) C << 29) |
((long) V << 28);
}

// 现在已经不用这个函数了,但保留不会影响
private static int parseRegIndex(String reg) {
reg = reg.trim();
if (reg.startsWith("w") || reg.startsWith("x")) {
try {
return Integer.parseInt(reg.substring(1));
} catch (NumberFormatException e) {
return -1;
}
}
return -1;
}
}


class Patch1 {
public long ltaddress, bAddress;
public AndroidEmulator emulator;
public long startAddress;
public long endAddress;

public Patch1(AndroidEmulator emulator,
byte[] bytes,
long ltaddress,
long bAddress,
long startAddress,
long endAddress,
Module module,
long cselAddress
,String cond) {

this.emulator = emulator;
this.ltaddress = ltaddress;
this.bAddress = bAddress;
this.startAddress = startAddress;
this.endAddress = endAddress;

Backend backend = emulator.getBackend();
Keystone keystone = new Keystone(
KeystoneArchitecture.Arm64,
KeystoneMode.LittleEndian
);

long ltAbs = module.base + ltaddress;
long bAbs = module.base + bAddress;



byte[] nop = keystone.assemble("nop", 0).getMachineCode();
backend.mem_write(endAddress, nop);
System.out.printf("[Patch1] NOPed BR instruction at 0x%x%n", endAddress);

String op1;
if(cond.equals("eq")){
op1 = String.format("b.eq 0x%x", ltAbs);
}else if(cond.equals("lt")){
op1 = String.format("b.lt 0x%x", ltAbs);
}else {
op1 = String.format("b.ne 0x%x", ltAbs);
}
long ldraddress=endAddress-8;


byte[] bltCode = keystone.assemble(op1, (int) ldraddress).getMachineCode();

long bPc = ldraddress + bltCode.length;
String op2 = String.format("b 0x%x", bAbs);
byte[] bCode = keystone.assemble(op2, (int) bPc).getMachineCode();

backend.mem_write(ldraddress, bltCode);
backend.mem_write(bPc, bCode);
byte[] cur = backend.mem_read(endAddress, 4);
// System.out.printf(
// "[Patch1] mem @0x%x = %02x %02x %02x %02x%n",
// endAddress, cur[0], cur[1], cur[2], cur[3]
// );
// long nopStart = bPc + bCode.length;
//
// for (long addr = nopStart; addr <= endAddress; addr += nop.length) {
// backend.mem_write(addr, nop);
// }
// byte[] patched = new byte[4];
// int off=0;
// patched[off] = 0x1f;
// patched[off + 1] = 0x20;
// patched[off + 2] = 0x03;
// patched[off + 3] = (byte) 0xd5;
// backend.mem_write(endAddress, patched);

}

}

之后就特别明显了可以猜出是异或一大段,可以发现

脚本优势

1.unicorn动态计算得到的第二个结果,而非固定的计算,但在patch的效率上来说是比较差的

2.兼容了cmp csel cinc cset lt/eq的情况,并且cmp到br长度兼容达到28字节长度,可以说大部分比较好去除

3.方便后续添加其他条件,

脚本不足

1.本身是unidbg的框架,所以免不了需要补环境的操作。

2.模拟执行到的才能去除,对于有些没有执行到的分支,是去除不了 的

3.变量名比较简陋(