一、目标
在做代码还原的时候,经常能看到一些奇怪的寄存器和奇怪的指令:
vldr s15, [r1]
vadd.f32 s15, s14, s15
很像某些流量明星,看上去很眼熟,仔细看看又不认识。
它们就是传说中的浮点数运算,今天我们来点亮一个很有用的技能树: Unidbg调试浮点数运算
二、步骤
先写个floatdemo
有这么一个祖传的算法函数。
extern "C" JNIEXPORT jstring JNICALL
Java_com_fenfei_app_floatdemo_MainActivity_stringFromJNI(
JNIEnv* env,
jobject Obj, jdouble value) {
std::string hello = "Hello from C++";
double p=3.14159;
double s,v,rc;
v = 2*p*value;
s = p*value*value;
rc = v+s;
hello = std::to_string(rc);
return env->NewStringUTF(hello.c_str());
}
算出圆的周长和面积,然后再把它们相加。
高级语言就是好,一目了然。
IDA一把
可以看出两个区别, 一个是寄存器不一样,普通运算使用的寄存器是R0-Rx,浮点数运算使用的是D0-Dx (其实还有 S0-Sx),另一个是指令不一样,普通运算是MOV、MUL,而浮点数运算使用的是VMOV,VMUL,感觉就是普通运算的VIP版。
第一个知识点就出来了,V开头的指令就是浮点数运算指令,Dx Sx Qx 就是浮点数寄存器。
Unidbg亮相
按照 Unidbg模拟执行某段子so实操教程(一) 先把框架搭起来 这个框架把我们刚才编译的 floatdemo.apk 跑起来,然后增加一个 stringFromJNI 函数的调用。
private String callfun(String methodSign, Object ...args) {
DvmObject mainactivity = MainActivity_dvmclass.newObject(null);
Object value = mainactivity.callJniMethodObject(emulator,methodSign,args).getValue();
return value.toString();
}
由于 stringFromJNI 不是静态(static)的类函数,所以我们需要先创建个一个 MainActivity 对象,才可以调用它的方法。
先跑一下看看结果
Find native function Java_com_fenfei_app_floatdemo_MainActivity_stringFromJNI(D)Ljava/lang/String; => RX@0x4000c6c9[libnative-lib.so]0xc6c9
JNIEnv->NewStringUTF("150.796320") was called from RX@0x4000c73d[libnative-lib.so]0xc73d
ret:150.796320
emulator destroy...
我们传了个参数6,半径是6的圆, 周长是 37.699, 面积是113.097 ,它们之和是 150.796。 结果没毛病,那我们开始调试了。
Unidbg调试
从刚才运行的结果里我们知道 stringFromJNI 函数的地址在 0xc6c9, 那么我们现在需要在这个地址下个断点,让调试器停在这个地址。
Unidbg的调试功能依然很强大,它支持三种调试模式 CONSOLE、GDB和IDA,目前我用的顺手的是 CONSOLE 模式,今天先介绍这个。
开启调试炒鸡简单,加上这两行代码就行
Debugger MyDbg = emulator.attach(DebuggerType.CONSOLE);
MyDbg.addBreakPoint(module.base + 0xc6c9);
运行一下,就顺利的进入到调试器命令行了,直接回车,会显示目前支持的调试命令。
我们是新手嘛,先掌握一个n和s两个命令就行了,n是单步步过,就是执行一条指令,步过函数调用;s是单步步入,就是执行一条指令,进入函数调用。
n命令跑几下来到我们要分析的浮点数运算的位置,发现尴尬了……
Unidbg调试器只显示了Rx寄存器,没有显示Dx系列的寄存器,这下怎么分析,不能盲摸呀?
打开Unidbg浮点数寄存器显示
Unidbg是支持浮点数运算模拟的,那么一定是有地方去读取浮点数寄存器的,只是没有显示出来而已。
我们先分析下Unidbg调试时寄存器显示部分的代码。
先搜索 r0= 在哪里处理的?
showRegs 就是显示寄存器, 当参数为null的时候,通过 ARM.getAllRegisters 来显示所有的寄存器。但是为啥没有显示浮点寄存器呢?奇怪。
我们再往下翻,发现在ARM64的模拟下显示了Q0-Q31寄存器,通过查阅资料,我们知道了原来它们都是一伙的。
那ARM32先放一下,我们把模拟环境切换到ARM64
emulator = AndroidEmulatorBuilder.for64Bit().setProcessName("com.fenfei.runfloatdemo").build(); // 创建模拟器实例,要模拟32位或者64位,在这里区分
再跑一下,调试器没有激活?
这是为什么? 原来我们把模拟器从Arm32切换到了Arm64,那么载入的so就是64位的了,所以 stringFromJNI 函数的地址也变了,需要把断点下在新的地址 0x12738 上面。
这下不一样了,浮点寄存器都显示出来了。
优化一下浮点寄存器的显示
李老板: 奋飞呀,这个0x400921f9f01b866e是啥意思呀,你是不是搞错了,浮点数寄存器显示的咋不是 3.14159 ,而是这个乱七八糟的数据?
奋飞: 老板,程序员的母语就是16进制,没有一眼把 0x400921f9f01b866e 认出是 3.14159的,晚饭是不配加鸡腿的,也不配变秃的。
有理想的同学请自行搜索 IEEE754 二进制浮点数算术标准
其他的同学请和我一起优化下浮点寄存器的显示。
由于飞哥目前为止还没有变秃,确实也看不出来这玩意就是 3.14159, 只好另辟蹊径,给大家传授一个神奇的函数:
public static double bytes2Double(byte[] arr) {
long value = 0;
for (int i = 0; i < 8; i++) {
value |= ((long) (arr[i] & 0xff)) << (8 * i);
}
return Double.longBitsToDouble(value);
}
// 在showRegs64函数里面加个显示
case Arm64Const.UC_ARM64_REG_Q0:
byte[] data = backend.reg_read_vector(reg);
double bOut = bytes2Double(data);
if (data != null) {
builder.append("\n>>>");
builder.append(String.format(Locale.US, " q0=0x%s(%.3f)", newBigInteger(data).toString(16),bOut));
}
break;
科学研究表明:在没有变秃的前提下,我们依然有机会变强。
其实C/C++ 有个神奇的玩意叫指针,这种显示一把梭就行
BYTE dPiByte[8] = {0x6e, 0x86, 0x1b, 0xf0, 0xf9, 0x21, 0x09, 0x40 };
double dPi = *(double*)dPiByte;
好了,后面的几步运算就是乘乘加加了,同学你自己n几下就ok了。
(此处应有掌声)
三、总结
为什么要去调试,直接F5大法不香吗?
现在Ollvm肆虐,掌握一些手撕汇编的良方可保你无忧。
为什么要用Unidbg去调试,IDA不香吗?
悟空,等你遇上内存防修改,无法下软件断点和一些BT的反调试的时候,你自会回来和为师唱这首歌的: Only You …
预告一下,下一篇是开启Arm32下的浮点数寄存器显示和TraceCode
知道为何自古红颜多薄命吗?因为没人在意丑的人活多久。