一、目标
在做代码还原的时候,经常能看到一些奇怪的寄存器和奇怪的指令:
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
知道为何自古红颜多薄命吗?因为没人在意丑的人活多久。
TIP: 本文的目的只有一个就是学习更多的逆向技巧和思路,如果有人利用本文技术去进行非法商业获取利益带来的法律责任都是操作者自己承担,和本文以及作者没关系,本文涉及到的代码项目可以去 奋飞的朋友们 知识星球自取,欢迎加入知识星球一起学习探讨技术。有问题可以加我wx: fenfei331 讨论下。
关注微信公众号: 奋飞安全,最新技术干货实时推送