代码混淆是逆向分析中最令人头疼的问题之一。
面对混淆,通常的做法是直接硬着头皮调试,因为混淆只能混淆代码的形,但是代码的神——执行逻辑是不会变的,单步调试下去往往能梳理出程序本身的执行逻辑。但是面对非常复杂的混淆,小弟我也是经常调试数十小时最终招架不住选择放弃。。。有位伟人曾经说过“安全保护的目的不在于让代码固若金汤,无法攻破,而是在于让破解者望而生畏,主动放弃”,正所谓“不战而屈人之兵,善之善也~”,小弟我深以为然。
面对混淆更好的做法是去混淆,或者说反混淆,就是将混淆过的代码还原回去。还原后的代码通常逻辑清晰,通俗易懂,可以很简单的进行分析。但是反混淆往往比较难,抛开各种定制的混淆不说,即使是最基本的ollvm混淆,还原起来也不是那么容易。因为还原混淆通常要依赖于符号执行,模拟执行,指令trace等一系列高级的逆向手法。小弟我最近刚好遇到一个混淆的样本,趁着五一放假,研究了一下反混淆的方法,最终成功还原出了真实逻辑,故借此机会和看雪各位安全爱好者做一分享。
混淆前的程序,可以看到在 br x9 后ida无法分析了,因为x9是一个寄存器,ida并不知道里面是什么值,所以无法继续向下分析。F5后可以看到,函数调用jni->GetEnv函数后就jumpout了。
往下查看汇编代码,可以看到函数里有许多根据寄存器跳转的指令:
去混淆通常要对代码进行模拟执行。
模拟执行届扛把子的是unicorn引擎。不过unicorn引擎相对比较底层,需要手动的开辟内存,将指令写入,设置堆栈的位置,设置寄存器初始值,然后执行。unidbg是基于unicorn引擎的模拟执行框架,使用java语言编写,并且可以直接加载so,进行调用(unidbg在加载动态库时自动帮你完成了文件解析,映射,重定位,写入unicorn引擎等一系列底层操作),同时unidbg内置了Jni相关的环境,而unicorn本身没有相关环境,需要手动补充。同时,unidbg还内置了hookZz,xHook等一系列hook框架,可以很方便的进行函数级,指令级的各种hook。总之,unidbg大大方便了逆向人员进行模拟执行的流程,所以我们选用unidbg来进行模拟执行。
unidbg的工作模式简单粗暴,直接git clone unidbg的项目,然后在项目里添加自己的模拟执行类,最终运行的时候是带着整个项目的源码一起运行的,这样做的好处是方便了定制化修改和源码调试。
下载好后使用idea进行打开。
直接在项目的unidbg-android/src/test/java目录下建立我们的模拟执行类:AntiOllvm
在进行模拟执行之前,需要做以下准备工作:
1.创建一个模拟器。
2.设置android系统库的版本
3.创建android虚拟机
4.加载动态库
这样,动态库就被unidbg加载了。
加载后需要先执行jni_onload,而DalvikModule这个类已经实现了callJNI_OnLoad方法,我们直接调用即可。
调用后,出现了报错和许多warning:
一方面,有许多libc的函数没有找到,另一方面,JNI返回了-1。
我们先补充所需的libc函数,只需要把手机里/system/lib64/libc.so等系统库交给unidbg加载即可。
将下列代码添加到加载目标so之前:
再次执行,这次运行成功了,并且unidbg hook 了所有的jni掉用,也将他们打印了出来:
可以看到,在0x5e900处调用了registernative,注册的函数名叫:initialize,签名是:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;)I,函数的地址在0x5e1e8处。
至此,我们成功使用unidbg加载了android的动态库,并且正常运行了动态库的jni_onload函数。
我们已经成功运行了目标动态库,接下来可以看看还能做些什么。我们做一个最基本的操作,使用指令hook,在每条指令执行之前将其打印出来。
使用到的是unicorn引擎提供的codehook,即指令hook。我们在指令执行前进行hook,然后初始化Capstone引擎(反汇编引擎),接着通过unicorn的接口从当前地址读取4字节机器码,再将机器码反汇编,然后打印输出。我们可以将hook的范围指定在一个函数中,这样就不会打印超出该函数范围的指令了:
可以看到,打印的指令和ida中解析出的jnionload函数指令一致。当然,我们也看到了br x9之后的那条指令:
跳转到0x5e480,又执行了若干组间接跳转。
至此,我们已经可以通过unidbg的接口,进行指令trace。同时我们还可以使用unidbg提供的其他接口,在hook的同时读取,写入寄存器,查看内存,修改内存,设置断点等等。接下来我们就可以开始着手去混淆了。
首先要解决的是 br x9 的问题,至少让ida能F5吧。
通过阅读汇编代码,可以看到该函数的混淆是这样做的:
1.比较 w8 和 wk(另外一个寄存器,不固定)寄存器。
2.根据比较的结果,选择xA或者xB寄存器的值赋值给X9
3.从x9 + x19的地址处读取一个值,赋值给x9
4.x9 = x9 + x24
5.跳转到x9指向的位置。
其中,x19和x24始终是固定的,我们看到,x19和x24在程序开始时被初始化了:
可以看到,X19是0x140080,而0x140080是一个数组
x24则为固定的值0x3a2cd438
这里就理解这个间接跳转是怎么做的了,每次根据比较结果在数组中选择一个偏移,然后用偏移加上基数,得到真实的跳转地址。
所以我们可以在每次执行到br x9的时候获取x9的真实值,然后将根据寄存器跳转的指令br 换成直接跳转b,这样ida就可以继续分析了。
直接这样做有一个问题,因为偏移值是根据比较结果不同而不同,如果我们直接将最终的跳转地址写死,那就相当于将原来的条件跳转改为了直接跳转,改变了逻辑。
所以正确的做法是保留条件,计算出条件成立与不成立时对应的跳转地址,然后将br x9改为一条条件跳转和一条直接跳转,这样做就和原来的逻辑完全一致了,并且所有的跳转都是有确定的地址,ida可以分析。
于是我们用这样的算法进行还原:
1.建立一个指令栈,每条指令执行前,保留该指令的地址,指令内容,当前所有寄存器的值。然后将当前指令的信息push进指令栈。
2.对指令进行回溯,如果栈顶指令是br x9,则进行3,否则执行下一条指令。
3.pop跳转指令,再次获取栈顶指令,检查是否为ADD x9,x9,x24指令,如果是,进行4,否则执行下一条指令。
4.pop add指令,再次获取栈顶指令,检查是否为LDR x9,[x19,x9]指令,如果是,则进行5,否则执行下一条指令。
5.pop 加载指令,再次获取栈顶指令,检查是否为条件选择指令。如果是,记录选择条件,记录为条件成立与不成立时对应的选择的值(通过保留的寄存器值获取)。否则执行下一条指令。
6.pop 条件选择指令,获取到上一次 比较指令,检查是否是cmp x8,类型的指令,如果是,进行7,否则执行下一条指令。
7.通过前六步,说明我们已经定位到了正确的间接跳转的位置。然后分别计算条件成立与不成立时对应的真实跳转地址,将Add 指令替换为 条件跳转->成立时地址,将br x9替换为直接跳转->条件不成立地址。
以以下指令为例:
在csel 一步,当条件LT成立时,x9 = x28。真实的跳转地址为
当条件不成立时,x9 = x23。真实的跳转地址为:
然后将add X9, X9, X24指令替换为b.lt addrT,
将br x9 指令替换为 b addrF。
将条件选择和加载指令nop掉。
替换后的指令为:
这样所有的间接跳转都变成了直接跳转,ida是可以分析的,并且逻辑与之前完全相同,我们所做的只不过是提前计算出目标地址,然后直接跳转罢了。
对应的java代码如下:
patch完成后,再次使用ida打开原so,发现Jni_onLoad函数已经可以F5了:
但是依然有一些jumpout。
原因是因为在模拟执行的是后并没有走遍所有的分支,我们只是将走过的分支处理了。
接下来需要在jumpout的地方手动修改寄存器的值,来控制他的走势,让unidbg最终走完所有分支。
比如对于这一处br,在blt时才会跳转过去,
可以看到这一处的跳转和w12有关,我们找到给w8赋值为w12的地方,通过修改寄存器来给其赋值:
重新跑一边代码,这一处的br x9 就修复了。将没有走到的分支处理完后,F5结果如下:
可以看到是标准的ollvm的控制流平坦化。
所以br x9的混淆其实是在ollvm控制流平坦化的基础上,将所有的跳转地址都加了一步运算。
间接跳转去除完后,需要去除控制流平坦化。根据控制流平坦化的工作原理,原来的代码会被打碎成代码片段,然后装进一个大的switch case里,最外层包一个while true循环,根据索引变量来按照原来的顺序执行代码。
考虑到编译的结果要在逻辑上和原来的代码完全一致,所以一个重要的结论是——真实的代码块一定在cmp eq 之后。(switch case 的一个case)
控制流平坦化对于顺序执行的处理比较简单,直接在前一个真实块的末尾,将索引值改为下一个真实块的索引,然后跳转到主分发器即可。
对于分支执行(判断,循环),在条件的部分会有一个条件选择指令CSEl,将索引寄存器的值根据条件结果设置为不同的索引,然后跳转到主分发器。
所以我们可以按照以下算法对控制流平坦化进行还原:
1.建立一个指令栈,每条指令执行前,保留该指令的地址,指令内容,当前所有寄存器的值。然后将当前指令的信息push进指令栈。
2.对指令进行回溯,如果栈顶指令是b.eq,则进行3(收集真实块),如果栈顶指令是直接跳转指令b,则进行5(处理分支块),否则继续执行下一条指令。
3.向上回溯指令栈,找到第一条cmp 指令,判断是否为与索引寄存器w8 比较,如果是,则进行4,否则继续执行下一条指令。
4.获取与w8比较的另一个寄存器的值,获取b.eq的目标地址,组成一个(索引,真实块)对。继续执行下一条指令。
5.判断上一条指令是否为CSEL W8。如果是,则记录一个(条件成立块索引,条件不成立块索引)对。否则,继续执行下一条指令。
同时,我们在主分发器处hook 指令,记录每次经过主分发器时的索引寄存器的值为索引值顺序。
在模拟执行收集完成信息之后,做以下处理:
1.去除索引值顺序中为条件块的索引。
2.获取索引值顺序中的第一个值,找到对应的真实块。将主分发器处patch为跳转向第一个真实块的跳转指令。
3.根据真实块结束时的新的索引值,寻找到对应的真实块,将结尾处patch为跳转到下一个真实块。
4.在CSEL指令处,根据条件成立块索引和条件不成立索引找到对应的真实块。将CSEL指令和后面的跳转向主分发器的指令patch为两条指令:
第一条为条件成立时的条件跳转,第二条为条件不成立时的直接跳转。
对应的代码如下:
在模拟执行完后,发现有一些索引值没有找到对应的块:
查看他们的索引,发现刚好是条件块中的索引。这个也好理解,因为在模拟执行的时候我们只走了条件的一个分支,没有走另一个分支,所以就没有记录下对应的真实块。这里我们手动根据b.eq和寄存器的值,添加对应的真实块:
添加完成后,再次运行程序,发现已经patch成功了。打开ida查看Jni_onload:
发现函数已经被完美的还原了。
借着五一假期简单研究了一下模拟执行和去混淆,感慨unidbg是真的强。我认为我使用的方法应该是没有任何逻辑漏洞,可以完全等效的还原的。
间接跳转的还原比较简单,控制流平坦化的还原则比较难。
控制流平坦化还原的最大难点在与找到真实块与索引之间的关系。由于我这次使用的样本对于跳转做了混淆,虽然难住了ida,但是也可能难住了编译器,导致对控制流平坦化部分没有做复杂的编译优化——所有的真实块都在b.eq后面,并且只有一个主分发器,没有次分发器。同时真实块执行结束都很乖巧的跳转到了分发器(有的编译优化会直接将两个真实块连在一起)。
总体来说,本次反混淆不是特别完美,有手工的部分,同时也不能通杀所有混淆,只能说提供一种反混淆思路和经验吧。
ollvm反混淆感觉是永无止境的,目前还没有见到有什么非常强大,通杀,可以完美还原的方案,只能针对目标混淆器定制反混淆策略。
安全攻防不息,吾辈仍需努力!
public AntiOllvm()
{
/
/
创建模拟器
emulator
=
AndroidEmulatorBuilder
.for64Bit()
.addBackendFactory(new Unicorn2Factory(true))
.setProcessName(
"com.example.antiollvm"
)
.build();
Memory memory
=
emulator.getMemory();
/
/
设置andorid系统库版本
memory.setLibraryResolver(new AndroidResolver(
26
));
/
/
创建虚拟机
vm
=
emulator.createDalvikVM();
vm.setVerbose(true);
/
/
加载动态库
dm
=
vm.loadLibrary(new
File
(
"f:\\kshs\\libtprt.so"
), false);
module
=
dm.getModule();
}
public AntiOllvm()
{
/
/
创建模拟器
emulator
=
AndroidEmulatorBuilder
.for64Bit()
.addBackendFactory(new Unicorn2Factory(true))
.setProcessName(
"com.example.antiollvm"
)
.build();
Memory memory
=
emulator.getMemory();
/
/
设置andorid系统库版本
memory.setLibraryResolver(new AndroidResolver(
26
));
/
/
创建虚拟机
vm
=
emulator.createDalvikVM();
vm.setVerbose(true);
/
/
加载动态库
dm
=
vm.loadLibrary(new
File
(
"f:\\kshs\\libtprt.so"
), false);
module
=
dm.getModule();
}
public static void main(String[] args) {
AntiOllvm ao
=
new AntiOllvm();
ao.callJniOnload();
}
public void callJniOnload()
{
dm.callJNI_OnLoad(emulator);
}
public static void main(String[] args) {
AntiOllvm ao
=
new AntiOllvm();
ao.callJniOnload();
}
public void callJniOnload()
{
dm.callJNI_OnLoad(emulator);
}
vm.loadLibrary(new
File
(
"f:\\androidlib\\libc.so"
),false);
vm.loadLibrary(new
File
(
"f:\\androidlib\\libm.so"
),false);
vm.loadLibrary(new
File
(
"f:\\androidlib\\libstdc++.so"
),false);
vm.loadLibrary(new
File
(
"f:\\androidlib\\ld-android.so"
),false);
vm.loadLibrary(new
File
(
"f:\\androidlib\\libdl.so"
),false);
vm.loadLibrary(new
File
(
"f:\\androidlib\\libc.so"
),false);
vm.loadLibrary(new
File
(
"f:\\androidlib\\libm.so"
),false);
vm.loadLibrary(new
File
(
"f:\\androidlib\\libstdc++.so"
),false);
vm.loadLibrary(new
File
(
"f:\\androidlib\\ld-android.so"
),false);
vm.loadLibrary(new
File
(
"f:\\androidlib\\libdl.so"
),false);
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);
}
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
W8, W27
CSEL X9, X28, X23, LT
LDR X9, [X19,X9]
ADD X9, X9, X24
BR X9
CMP
W8, W27
CSEL X9, X28, X23, LT
LDR X9, [X19,X9]
ADD X9, X9, X24
BR X9
*
(x19
+
x28)
+
x24
/
/
记为addrT。
*
(x19
+
x28)
+
x24
/
/
记为addrT。
*
(x19
+
x23)
+
x24
/
/
记为addrF。
*
(x19
+
x23)
+
x24
/
/
记为addrF。
CMP
W8, W27
NOP
NOP
BLT addrT
B addrF
CMP
W8, W27
NOP
NOP
BLT addrT
B addrF
/
/
保存指令和寄存器环境类:
class
InsAndCtx
{
long
addr;
Instruction ins;
List
<Number> regs;
public
long
getAddr() {
return
addr;
}
public void setAddr(
long
addr) {
this.addr
=
addr;
}
public void setIns(Instruction ins) {
this.ins
=
ins;
}
public Instruction getIns() {
return
ins;
}
public void setRegs(
List
<Number> regs) {
this.regs
=
regs;
}
public
List
<Number> getRegs() {
return
regs;
}
}
/
/
patch类
class
PatchIns{
long
addr;
/
/
patch 地址
String ins;
/
/
patch的指令
public
long
getAddr() {
return
addr;
}
public void setAddr(
long
addr) {
this.addr
=
addr;
}
public String getIns() {
return
ins;
}
public void setIns(String ins) {
this.ins
=
ins;
}
}
/
/
指令栈
private Stack<InsAndCtx> instructions;
/
/
所有需要patch的指令
private
List
<PatchIns> patchs;
/
/
保存指令寄存器环境
public
List
<Number> saveRegs(Backend bk)
{
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));
return
nb;
}
/
/
指令hook,每条指令执行前保存环境
public void processBr()
{
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
);
InsAndCtx iac
=
new InsAndCtx();
iac.setIns(disasm[
0
]);
iac.setRegs(saveRegs(backend));
iac.setAddr(address);
instructions.push(iac);
do_processbr();
}
@Override
public void onAttach(UnHook unHook) {
System.out.println(
"attach"
);
}
@Override
public void detach() {
System.out.println(
"detach"
);
}
},module.base
+
start, module.base
+
end,null);
}
/
/
指令栈回溯,根据处理结果,生成patchIns,供最后统一patch
public void do_processbr()
{
Instruction ins
=
instructions.peek().getIns();
if
(ins.getMnemonic().equals(
"br"
) && ins.getOpStr().equals(
"x9"
))
{
boolean finish
=
false;
long
base
=
-
1
;
long
listoffset
=
-
1
;
long
cond1
=
-
1
;
long
cond2
=
-
1
;
String cond
=
"";
long
addinstaddr
=
-
1
;
long
brinsaddr
=
instructions.peek().getAddr()
-
module.base;
long
selectaddr
=
-
1
;
long
ldaaddr
=
-
1
;
try
{
while
(!finish && !instructions.empty())
{
instructions.pop();
ins
=
instructions.peek().getIns();
if
(ins.getMnemonic().toLowerCase(Locale.ROOT).equals(
"add"
))
{
String[] split
=
ins.getOpStr().split(
","
);
if
(split.length
=
=
3
)
{
if
(split[
0
].toLowerCase(Locale.ROOT).trim().equals(
"x9"
) && split[
1
].toLowerCase(Locale.ROOT).trim().equals(
"x9"
))
{
String reg
=
split[
2
].trim().toLowerCase(Locale.ROOT);
base
=
getRegValue(reg,instructions.peek().getRegs()).longValue();
addinstaddr
=
instructions.peek().getAddr()
-
module.base;
}
else
{
break
;
}
}
else
{
break
;
}
}
if
(ins.getMnemonic().toLowerCase(Locale.ROOT).equals(
"ldr"
))
{
String[] sp
=
ins.getOpStr().toLowerCase().split(
","
);
if
(sp.length
=
=
3
)
{
if
(sp[
0
].trim().toLowerCase(Locale.ROOT).equals(
"x9"
) && sp[
2
].trim().toLowerCase(Locale.ROOT).equals(
"x9]"
))
{
String reg
=
sp[
1
].toLowerCase(Locale.ROOT).trim().substring(
1
);
listoffset
=
getRegValue(reg,instructions.peek().getRegs()).longValue()
-
module.base;
ldaaddr
=
instructions.peek().getAddr()
-
module.base;
}
}
}
if
(ins.getMnemonic().trim().toLowerCase(Locale.ROOT).equals(
"csel"
))
{
String[] sp
=
ins.getOpStr().toLowerCase(Locale.ROOT).split(
","
);
if
(sp.length
=
=
4
)
{
cond
=
sp[
3
].trim();
if
(sp[
0
].trim().equals(
"x9"
))
{
String reg1
=
sp[
1
].trim();
String reg2
=
sp[
2
].trim();
cond1
=
getRegValue(reg1,instructions.peek().getRegs()).longValue();
cond2
=
getRegValue(reg2,instructions.peek().getRegs()).longValue();
selectaddr
=
instructions.peek().getAddr()
-
module.base;
}
}
}
if
(ins.getMnemonic().trim().toLowerCase(Locale.ROOT).equals(
"cmp"
))
{
if
(base
=
=
-
1
|| listoffset
=
=
-
1
|| cond1
=
=
-
1
|| cond2
=
=
-
1
|| cond.equals("") || addinstaddr
=
=
-
1
|| ldaaddr
=
=
-
1
|| selectaddr
=
=
-
1
)
{
break
;
}
else
{
long
offset1
=
base
+
readInt64(emulator.getBackend(), module.base
+
listoffset
+
cond1)
-
module.base;
long
offset2
=
base
+
readInt64(emulator.getBackend(),module.base
+
listoffset
+
cond2)
-
module.base;
if
( brinsaddr
-
addinstaddr !
=
4
)
{
System.out.println(
"add ins and br ins gap more than 4 size,may make mistake"
);
}
String condBr
=
"b"
+
cond.toLowerCase(Locale.ROOT)
+
" 0x"
+
Integer.toHexString((
int
) (offset1
-
addinstaddr));
String br
=
"b 0x"
+
Integer.toHexString((
int
)(offset2
-
brinsaddr));
PatchIns pi1
=
new PatchIns();
pi1.setAddr(addinstaddr);
pi1.setIns(condBr);
patchs.add(pi1);
PatchIns pi2
=
new PatchIns();
pi2.setAddr(brinsaddr);
pi2.setIns(br);
patchs.add(pi2);
PatchIns pi3
=
new PatchIns();
pi3.setAddr(selectaddr);
pi3.setIns(
"nop"
);
patchs.add(pi3);
PatchIns pi4
=
new PatchIns();
pi4.setAddr(ldaaddr);
pi4.setIns(
"nop"
);
patchs.add(pi4);
finish
=
true;
}
}
}
}catch (Exception e)
{
e.printStackTrace();
}
}
}
/
/
遍历patch表,执行patch,生成新的so,使用Ketstone将汇编转为机器码。
public void patch()
{
try
{
File
f
=
new
File
(inName);
FileInputStream fis
=
new FileInputStream(f);
byte[] data
=
new byte[(
int
) f.length()];
fis.read(data);
fis.close();
for
(PatchIns pi:patchs)
{
System.out.println(
"procrss addr:"
+
Integer.toHexString((
int
) pi.addr)
+
",code:"
+
pi.getIns());
Keystone ks
=
new Keystone(KeystoneArchitecture.Arm64, KeystoneMode.LittleEndian);
KeystoneEncoded assemble
=
ks.assemble(pi.getIns());
for
(
int
i
=
0
;i<assemble.getMachineCode().length;i
+
+
)
{
data[(
int
) pi.addr
+
i]
=
assemble.getMachineCode()[i];
}
}
File
fo
=
new
File
(outName);
FileOutputStream fos
=
new FileOutputStream(fo);
fos.write(data);
fos.flush();
fos.close();
System.out.println(
"finish"
);
}
catch (Exception e)
{
e.printStackTrace();
}
}
/
/
保存指令和寄存器环境类:
class
InsAndCtx
{
long
addr;
Instruction ins;
[课程]Linux pwn 探索篇!
最后于 2023-5-3 23:04
被乐子人编辑
,原因: 排版