windows 汇编ecx_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会优化,不利于初始学习
class A
{
public:
    int fun(int iNum) { return m_iA * iNum; }
    int m_iA = 100;
};

A* pkA = new A;
int iRet = pkA->fun(10);

//    int iRet = pkA->fun(10); 对这句进行反汇编,注这里是debug吧,如果是Release版本,编译器很强的,
// 这种过于简单的代码,可能直接函数调用开销都没有了,直接给结果。
00122978  push        0Ah //将10压入栈
0012297A  mov         ecx,dword ptr [pkA]  ;//将pkA地址放到ecx中,也就this*
0012297D  call        A::fun (0103A91h) ;// pkA->fun(10)函数调用,实际为A::fun(pkA,10);
00122982  mov         dword ptr [iRet],eax  // 函数返回值放到iRet中,完成int iRet = pkA->fun(10);

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 A
{
public:
    int m_iA = 111;
    virtual int fun(int iA) { return m_iA*iA; }
    virtual float fun2(float iF) { return m_iA * iF; }
    virtual int funA(int iA) { return m_iA * iA; }
};

class B :  public A
//class B :  virtual public A
{
public:
    int m_iB = 222;
    virtual int fun(int iA) { return m_iB * iA; }
    virtual float fun2(float iF) { return m_iB * iF; }
    virtual int funB(int iA) { return m_iB * iA; }
};

class C : public B
{
public:
    int m_iC = 333;
    virtual int fun(int iA) { return m_iC * iA; }
    virtual float fun2(float iF) { return m_iC * iF; }
    virtual int funC(int iA) { return m_iC * iA; }
};

int main()
{
    C* pkC = new C;
    printf("size:%d",sizeof(C));
    pkC->funB(1);
    retrun 0;
}

我们可以借助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函数,查看反汇编代码

628e7c930ac25b8b985a07922b742a8c.png

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

.text:00401090 ; int __cdecl main()
.text:00401090 _main           proc near            
.text:00401090                 push    esi;放入栈区,为了保护ESI
.text:00401091                 push    10h;这时new C的大小16字节,为了给new传参
.text:00401093                 call    ??2@YAPAXI@Z    ; 这里实际是调用new分配内存
.text:00401098                 mov     esi, eax;参见第2条,eax是返回值,即new分配的地址,放esi
.text:0040109A                 push    10h;传参16,这里是为了printf函数
.text:0040109C                 push    offset _Format  ; 取常量字符串"size:%d"的地址到栈区
.text:004010A1                 mov     dword ptr [esi+4], 6Fh;初如化pkC ->m_iA = 111;m_iA = this +4
.text:004010A8                 mov     dword ptr [esi+8], 0DEh;初如化pkC->m_iB = 222;m_iB = this +8
.text:004010AF                 mov     dword ptr [esi], offset ??_7C@@6B@;取虚表地址到pkC地址
.text:004010B5                 mov     dword ptr [esi+0Ch], 14Dh;初如化pkC->m_iC = 333;m_iC=this+12
.text:004010BC                 call    _printf;调用系统printf函数打印sizeof(C)
.text:004010C1                 mov     edx, [esi];取得虚表所在地址到edx中
.text:004010C3                 add     esp, 0Ch;维护栈,平衡上面push
.text:004010C6                 mov     ecx, esi;让pkC放到ecx,也就是为this call第一个参数准备
.text:004010C8                 push    1;传funB(int);参数为1
.text:004010CA                 call    dword ptr [edx+0Ch];调表虚表+12对应函数代码段,pkC->funB(1)
                                       ;对比上面layout打印虚表<3|&B::funB,也就偏12个字节>,完全符合
.text:004010CD                 xor     eax, eax;eax清零
.text:004010CF                 pop     esi;弹出栈区,保护ESI不受污染,参见第8条
.text:004010D0                 retn;结束
.text:004010D0 _main           endp

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

a3ebdb01bac3d2983d72ee7709012cb0.png

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

    std::vector<long*> vNum;
    vNum.push_back((long*)100);
    std::cout << vNum.size() << std::endl;

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

.text:004011BA                 lea     ecx, [ebp+Memory];取栈区std::vector<long> vNum的地址到ecx
.text:004011BD                 mov     [ebp+var_20], 64h;将100放到栈区
.text:004011C4                 call    sub_4012D0;很容易推出这名完成(&vNum)->push_back(100)调用
.text:004011C9                 mov     eax, [ebp+Memory+4];vNum地址偏移4个字节,也就是_Mylast
.text:004011CC                 mov     esi, [ebp+Memory];vNum地址着地址,也就是_Myfirst
.text:004011CF                 sub     eax, esi;地址相减(_Mylast - _Myfirst)
.text:004011D1                 mov     ecx, ds:?cout@std@@3V?$basic_ostream@DU?@std@@@1@A ;cout 
.text:004011DC                 sar     eax, 2;vNum.size()= eax>>2=eax/4,32位指针是4字节,64位就除8
.text:004011DF                 push    eax;size()传参压栈
.text:004011E0                 call    ds:??6?$basic_ostream@DU?$@std@@@std@@QAEAAV01@I@Z ;调用cout

从上面反汇编可看出,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文件

c14c812d01821e9834e365c365bae36a.png

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

81dfdf8d678d74995f7dc74e2adfc208.png

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

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

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

struct aaa
{
	int ia;
	int ib;
	std::vector<void*> vb;
};

void* TestFunCrash(aaa* pkaaa)
{
	if (!pkaaa) return 0;
	int iSize = pkaaa->vb.size();
	for (int i = 0; i < iSize; i++)
	{
		if (i == 2)
		{
			return pkaaa->vb[i];
		}
	}
}

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

.text:0000000000002BD4 ; TestFunCrash(aaa *)
.text:0000000000002BD4                 EXPORT _Z12TestFunCrashP3aaa
.text:0000000000002BD4 _Z12TestFunCrashP3aaa      ; DATA XREF: LOAD:0000000000000990↑o
.text:0000000000002BD4                 CBZ             X0, locret_2C04;对pkaaa进行判空
.text:0000000000002BD8                 LDP             X8, X9, [X0,#8];取pkaaa偏移8个字节,就pkaaa
                                        ;->vb,由STL知识可知,x8放vector的_Myfirst,x9放_Mylast
.text:0000000000002BDC                 SUB             X9, X9, X8;获得vector容器地址差
.text:0000000000002BE0                 LSR             X9, X9, #3;除指针大小8,获得size()
.text:0000000000002BE4                 CMP             W9, #1;size和1比较
.text:0000000000002BE8                 B.LT            locret_2C08;小于的话,可以结束了
.text:0000000000002BEC                 MOV             W10, WZR;设i = 0
.text:0000000000002BF0
.text:0000000000002BF0 loc_2BF0                 ; CODE XREF: TestFunCrash(aaa *)+2C↓j
.text:0000000000002BF0                 CMP             W10, #2;将i和2比较
.text:0000000000002BF4                 B.EQ            loc_2C0C;相等,跳到loc_2C0C
.text:0000000000002BF8                 ADD             W10, W10, #1
.text:0000000000002BFC                 CMP             W10, W9
.text:0000000000002C00                 B.LT            loc_2BF0
.text:0000000000002C04
.text:0000000000002C04 locret_2C04              ; CODE XREF: TestFunCrash(aaa *)↑j
.text:0000000000002C04                 RET
.text:0000000000002C08 ; -----------------------------------------------------
.text:0000000000002C08
.text:0000000000002C08 locret_2C08                 ; CODE XREF: TestFunCrash(aaa *)+14↑j
.text:0000000000002C08                 RET
.text:0000000000002C0C ; ----------------------------------------------------------
.text:0000000000002C0C
.text:0000000000002C0C loc_2C0C                   ; CODE XREF: TestFunCrash(aaa *)+20↑j
.text:0000000000002C0C                 LDR             X0, [X8,#0x10];这里崩溃了也就是pkaaa->vb[i]
;这行,取容器第三个元素。查看崩溃时寄储器,x8 = 0,x9 = 0x3FFFFFFF,x8= 0必崩啊,x9这个也说明野指针。
.text:0000000000002C10                 RET

B:空指针

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

struct aaa
{
	int ia;
	int ib;
	std::vector<void*> vb;
};

struct bbb
{
	long l1;
	long l2;
	aaa* pkA;
};

struct ccc
{
	float* pkFloat;
	bbb* pkB;
};

int TestEmptyPointer(ccc* pkC)
{
	if (!pkC) return 0;
	return pkC->pkB->pkA->vb.size();
}

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

.text:0000000000002C14 ; =============== S U B R O U T I N E =====================
.text:0000000000002C14
.text:0000000000002C14
.text:0000000000002C14 ; TestEmptyPointer(ccc *)
.text:0000000000002C14                 EXPORT _Z16TestEmptyPointerP3ccc
.text:0000000000002C14 _Z16TestEmptyPointerP3ccc               ; DATA XREF: LOAD:0000000000000560
.text:0000000000002C14                 CBZ             X0, locret_2C2C;羊断pkC是否为空
.text:0000000000002C18                 LDR             X8, [X0,#8];取pkC->pkB,pkB在ccc中偏移8字节
.text:0000000000002C1C                 LDR             X8, [X8,#0x10];取pkC->pkB->pkA,pkA偏移16字节
.text:0000000000002C20                 LDP             X9, X8, [X8,#8];crash,本意pkA->vb.size()计算
;查看此时x8寄储器为空,说明struct bbb中的pkA为空,所以x8+8必然崩
.text:0000000000002C24                 SUB             X8, X8, X9
.text:0000000000002C28                 LSR             X0, X8, #3
.text:0000000000002C2C
.text:0000000000002C2C locret_2C2C                             ; 
.text:0000000000002C2C                 RET
.text:0000000000002C2C ; End of function TestEmptyPointer(ccc *)
.text:0000000000002C2C

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

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值