crash工具解析_IDA反汇编静态调试Android平台C++的so文件Crash入门

最近刚接触到Android的C++编译出的.so文件崩溃,从后台上报系统能得到ARM64平台下发生crash时pc寄储器地址和函数名称,但没有行号,以及当时栈帧对应的31个寄储器的值,,第一次用IDA调试,边用边百度,不到2小时就上手,很就查出野指针问题了。本文引入两个实战,一个野指针Crash,一个空指针Crash。

由于以前做windows多,有C++反汇编基础,也有STL略底层点知识,只是没有用过IDA。win32指令简单一些,先介绍一下win32反汇编的一些知识,只要简单学习一下,就可以很快扩展到arm64平台中,两者很相通。

win32平台下10个寄储器,加粗的比较重要

  • EAX, EBX,ECX,EDX 通用寄储器,
  • ESI,EDI ,索引寄储器
  • EIP ,指令指针寄储器
  • ESP,EBP ,栈寄储器,ESP表标栈顶指针
  • EFL,标志寄存器

Win32常用指令:只取常用的,也不强记,不过用的多就能记住了,平时可以随时查

  • mov 传送指令
  • push 压栈指令
  • pop 出栈指令
  • lea 取地址
  • jmp 无条件跳转
  • je 相等跳转
  • jle 小于跳转
  • call 函数调用
  • ret 返回
  • cmp比较指令
  • add 相加
  • sub 相减
  • inc 加1
  • mul 相乘
  • div 相除
  • and 与运算
  • or 或运算
  • xor 异或运算
  • not 取反
  • test 测试
  • 其它他有很多指令,就单纯跳转就有很多,不再一一列举了。

上面的随意可以百度到,下面来说些对刚接触的来说相对比较有价值点或者比较难搜索到的:

1.要深入理解点栈,栈对汇编来说非常非常重要,也是函数调用关系的基础。一些高级用法,比如协程,比如自定义子程序调度,都离不开这个和跳转指令。栈可以理解为一个固定大小的地址空间,win32没有记错是1M,当然也可以在VS编译器设置。esp表示栈顶指针,最开始esp,ebp相同的,栈的内存增长和一般的不同,是向下生长,先进后出,就是push进去,地址越来越小,pop出来,地址变大。

2.EAX常用作函数返回值,非常重要

3.ECX常用作C++普通成员函数函数第一个参数,也就是this指针,C++代码编写时默认隐了this指针,编译器生成代码时,第一个参数是this*,这个参数是不能省掉的,这种也有人称为this call。

4.如果寄储器不够传参时,会用栈来传参

//关于2,3,4验证,VS2019 debug版本,Release会优化,不利于初始学习

5.EIP指向下一条指令代码,不能直接修改,可以用call,jmp,ret等修改

6.理解Push ,Pop 指令的等效过程

push eax; 
// 等同两条伪指令
1.mov esp,eax; // 将eax 放到esp中,伪指令,仅供理解
2.sub esp,4;   // 将esp减4,因为栈向下生长的,伪指令,仅供理解

pop eax;  
// 等同两条伪指令
1.mov eax,esp; // 将esp 放到eax中,伪指令,仅供理解
2.add esp,4;    // 将esp加4,因为栈向下生长的,伪指令,仅供理解

7.深入理解call,jmp,ret指令的等效过程

call 内存/立即数/寄储器;
//1.原下个EIP对应指令地址入栈;(esp-4) 
//2.修改EIP为新的; 
//3.跳转EIP执行;

jmp 内存/立即数/寄储器; 
//1.修改EIP为新的; 
//2.跳转EIP执行; 注:无条件执行,不改变栈

ret; 
//1.修改EIP为新的栈顶数据; 
//2.esp +4;(恢复栈) 
//3.跳转EIP执行;

8.为什么有时看到函数反汇编开头会push ebp,push ebx,push esi等?

答:因为为了保护这些寄储器的值在函数调用结束不被错误修改。如果研究的比较深,比如函数加上__declspec(naked)修饰 ,或者完全自己写汇编,的确是可以不进行这样操作,只要能维护好就行。下面给个简单伪代码,举例为什么push edi

int a = fun1()+fun2(1,2);

// 伪代码,edi用来保存fun1()的结果,然后与fun2()返回值相加,所以edi最好要在fun2内部过程保护好
call fun1_address; // 调用fun1()
mov edi eax; // fun1()返回值放到edi中
push 2;
push 1;// 放入参数(1,2)
call fun2_address; // 调用fun2(1,2)
add edi eax; // 执行fun1()+fun2(1,2),并放到edi中
mov ptr [a] edi; // 放到a对应栈内存中

9.C++虚函数的与反汇编

我们知道如果一个class没有虚函数时,开始的第一个字段为第一个成员,但有了虚函数,第一个字段就变成虚表指针的地址,如果有多种继承与虚继承,这个复杂些,加入各个vbptr以及各个的vfptr,平同编译器策略还是不同的,限于篇幅与主题,这个不细讨论了,有兴趣可以自己写个demo调试一下。这里先以VS2019平台编译的win32分析

mainclass 

我们可以借助VS自带工具查看类的内存分布,tools->command line->Developer Command Prompt打开,输入cl -d1reportSingleClassLayoutXXXX XXXX.cpp便可得到类的layout信息,可以观测到C是16字节,第一个4字节内存是虚函数表vftab,然后是继成A的成员m_iA,再次之B的成员m_iB,最后是C的对象m_iC。其中虚表函数分布也是简单易看。

class C size(16):
        +---
 0      | +--- (base class B)
 0      | | +--- (base class A)
 0      | | | {vfptr}
 4      | | | m_iA
        | | +---
 8      | | m_iB
        | +---
12      | m_iC
        +---

C::$vftable@:
        | &C_meta
        |  0
 0      | &C::fun
 1      | &C::fun2
 2      | &A::funA
 3      | &B::funB
 4      | &C::funC

C::fun this adjustor: 0
C::fun2 this adjustor: 0
C::funC this adjustor: 0

我们用IDA打开这个exe进行查看,32位就用32位IDA,64位就用64位的

File->open->生成exe文件路径

定位到main函数,查看反汇编代码

3e8f96b43ec4e193e64724c086da083d.png

我们来逐行解析IDA的反汇编

.text:

由上面分析可知,在汇编层不一定和书写完全一致,有时会遇到编译器很强大的优化。我们再解释一下内存分布,以便更加强化理解,本例pkC在堆上分配,pkC的虚表放在只读数据段,里面有5个函数地址,(0 | &C::fun 1 | &C::fun2 2 | &A::funA 3 | &B::funB 4 | &C::funC),代码段当然就是各种函数的指令实体,这5个函数实体也放在这里。

873ed9c351f1c74bf3f4dcfd14c582f5.png

10. STL的知识,最好看一下STL源码解析那本书,没看过也问题不大。以vector.size()反汇编解析。

std

我们进行IDA反汇编静态分析

.text:

从上面反汇编可看出,std::vector<long> vNum对象分配在栈区,本例在ebp+Memory中,大小是12个字节,是3个指针占用_Myfirst,_Mylast,_Myend,这要看源码才清楚;vector容器的元素对象实际在堆区分配,这里放了一个long型100,占用8个字节,由STL的Alloc分配器new出来,Alloc的代码也是值得研读。这里size()函数根本没有调用,由内联复制过来了,且中间还被取std::cout地址插了一脚。size计算两个指针地址相减,然后除以4,这些略为背后点知识,可能都要深入了解一下STL。



好了,有了上面知识准备,开始进行Android Arm64调试,这里有几个知识点

1.Arm64有31个通用整型寄存器,r0-r30。当使用64bits时候,命名x0-x30;使用32bits时,命名w0-w30

2.ESP变成sp,EIP变成pc

3.一般函数可以用r0到r7传递时不用栈,不够时也用栈传

4.返回值一般用r0,有时也用r8

5.常用指令,指令也是挺多的,记不住的也可以查。

  • MOV x1,x0; 传送指令,x0->x1
  • ADD x0,x1,x2; 相加,x1+x2 ->x0
  • SUB x0,x1,x2; 相减,x1-x2->x0
  • AND x0,x0,#8; 相与,(x0&8)->x0
  • ORR x0,x0,#8; 相或,(x0|8)->x0
  • EOR x0,x0,#8; 相或,(x0^8)->x0
  • LDR x0,[x1,#8]; 取内存(x1+8)地址放到x0
  • STR x0, [sp, #8]; 取x0数据到SP+8内存地址
  • STP x3, x4, [sp, #8]; 入栈
  • LDP x3, x4, [sp, #8]; 出栈
  • CMP x3,x4;比较x3,x4值
  • CBZ x3, xxxxxx; x3为0,就跳到xxxxxx代码段
  • CBNZ x3, xxxxxx; x3不为0就跳到xxxxxx代码段
  • B/BL ; 绝对跳转,返回地址保存到LR(x30)

实战:1例野指针,一例没有判空

1.打开IDA选择New,打开要调试的so文件

09c5fe96a063a72b43c5c141f8e8f4e0.png

2.打开主界面后,看崩溃的pc代码段值,比如0x0000000014A3287C,按G键跳转过到0x0000000014A3287C,然后手动找到函数开头,也可以按空格键看调用关系图。

0b08fc6408635cc6c1ea77b9547eb10a.png

3.对着代码反推反汇编,这个需要一些经验,对指令熟悉的越多,通常会越快,也和复杂度正相关,对于比较重要的地方,可以按;键加一些注释帮助理解。

4.由于实际项目代码非常复杂且不便公开,要推较长时间,我这里写些假的代码,只取最关键部分,模拟一下崩溃环境。

A:野指针,直接看这代小代码,也问题不大,该判断的也都判断了

struct 

我们崩溃在0000000000002C0C,用IDA进行Arm64反汇编,一行一行的分析

.text:

B:空指针

这种问题是比较简单,但实际工程量大了,代码量大了,比如1000万行以上,要判断的地方太多,或者有的地方理论上就不应该为空,都进行判断有时也挺讨厌,这种问题对工程大的相对有点恶心,但这种问题修复起来比较难,因为问题根源不在这,只是这里先发生而已。简单示意一下,略去前面要推的代码,只取最核心的。

struct 

我们崩溃在0000000000002C20,用IDA进行Arm64反汇编,一行一行的分析

.text:

通过汇编分析我们可知pkA为空指针,要进行进一步修补。

文章就介绍到这里,希望和大家一起进步。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值