IDA Pro - 使用IDA Pro逆向C++程序
原文地址:Reversing C++ programs with IDA pro and Hex-rays
简介
在假期期间,我花了很多时间学习和逆向用C++写的程序。这是我第一次学习C++逆向,并且只使用IDA进行分析,感觉难度还是比较大的。
这是你用Hex-ways分析一个有意思的函数时看到的东西
v81 = 9;
v63 = *(_DWORD *)(v62 + 88);
if ( v63 )
{
v64 = *(int (__cdecl **)(_DWORD, _DWORD, _DWORD,
_DWORD, _DWORD))(v63 + 24);
if ( v64 )
v62 = v64(v62, v1, *(_DWORD *)(v3 + 16), *(_DWORD
*)(v3 + 40), bstrString);
}
我们的任务是添加一些符号名称、分辨出类等,让hex-rays能够有足够的信息给出我们一个可靠、易于理解的输出
padding = *Dst;
if ( padding < 4 )
return -1;
buffer_skip_bytes(this2->decrypted_input_buffer, 5u);
buffer_skip_end(this2->decrypted_input_buffer, padding);
if ( this2->encrypt_in != null )
{
if ( this2->compression_in != null )
{
buffer_reinit(this2->compression_buffer_in);
packet_decompress(this2,
this2->decrypted_input_buffer,
this2->compression_buffer_in);
buffer_reinit(this2->decrypted_input_buffer);
avail_len = buffer_avail_bytes(this2->compression_buffer_in);
ptr = buffer_get_data_ptr(this2->compression_buffer_in);
buffer_add_data_and_alloc(this2->decrypted_input_buffer, ptr, avail_len);
}
}
packet_type = buffer_get_u8(this2->decrypted_input_buffer);
*len = buffer_avail_bytes(this2->decrypted_input_buffer);
this2->packet_len = 0;
return packet_type;
当然hex-rays不会自己命名这些变量名,你需要理解这些代码,至少给这些类一个合适的名字能帮你分析代码。
这里我的所有例子都是用visual studio或者Gnu C++编译的,这两个编译器的结果是相似,即使他们在某些语法上并不兼容。如果自己的编译器遇到问题,自己改下代码吧。
C++程序的结构
这里我就不介绍OOP编程的知识了,你也应该已经知道了。我们只从整体看下OOP是如何工作的和实现的。
Class = data structure + code (methods).
类的数据结构只能在源码里看到,函数则会显示在你的反汇编器里。
Object = memory allocation + data + virtual functions.
对象是一个类的一个实例,你可以在IDA里看到它。一个对象需要内存,所以你会看到调用new()或者栈分配内存,调用构造函数或者析构函数。你也会看到访问成员变量(成员对象),调用虚函数。
虚函数很蠢,如果不下断点运行程序,你很难知道哪些代码会被执行。
成员函数简单点,他们就像C语言里的结构。并且IDA有非常顺手的工具声明结构,hex-rays能在反汇编过程中很好的用到这些结构信息。
接下来我们将回到具体的问题上来。
对象的创建
int __cdecl sub_80486E4()
{
void *v0; // ebx@1
v0 = (void *)operator new(8);
sub_8048846(v0);
(**(void (__cdecl ***)(void *))v0)(v0);
if ( v0 )
(*(void (__cdecl **)(void *))(*(_DWORD *)v0 + 8))(v0);
return 0;
}
这是一个我用G++编译的小程序的反汇编结果,我们能看到new(8),意思是这个对象大小为8bytes,而不是我们有一个8bytes大小的变量。
函数sub_8048846在调用new()之后立刻被调用,并把new()产生的指针作为参数,这肯定就是构造函数了。
下一个函数就有点让人头大了,它在调用v0之前对v0做了两次解引用。这是一个虚函数调用。
所有的多态对象在他们变量中都有一个特殊的指针,被称作vtable。这个表包含了所有虚函数的地址,所以C++程序在需要的时候能够调用他们。在多种编译器中,我测试出vtable总是一个对象的第一个元素,总是待在相同的位置,即使是在子类中。(这也许对多继承不合适,我没有测试过)。
让我们开始用IDA进行分析:
重命名符号名称
点击一个名字,然后按n,就会弹出修改名字的窗口,你可以把它改成一个有意义的名字。目前我们还不知道这个类在做什么,所以我建议把这个类命名成“class1”,直到我们理解了这个类在做些什么。在我们完成分析class1之前我们很可能会遇到其他类,所以我建议遇到他们的时候只改下这些类的名字。
int __cdecl main()
{
void *v0; // ebx@1
v0 = (void *)operator new(8);
class1::ctor(v0);
(**(void (__cdecl ***)(void *))v0)(v0);
if ( v0 )
(*(void (__cdecl **)(void *))(*(_DWORD *)v0 + 8))(v0);
return 0;
}
创建结构
IDA的结构(structures)窗口非常有用。按shitf + f9能够调出来。我建议你把它拖出来放到IDA窗口的右边(IDA的QT版能这么做),然后你就能同时看到反汇编窗口和结构窗口。
按Insert键并创建一个新的结构“class1”。我们已经知道这个结构是8bytes长,按d键增加变量,直到我们有两个dd变量。重命名第一个变量为“vtable”,然后就变成下面的样子了。
接下里我们添加函数的类型信息,右键v0,选择Convert to struct * ,选择class1。此外,按y,然后输入“ class1 * ”也能得到一样的结果。
创建一个新的长度为12bytes的结构并把它命名成“class1_vtable”。现在我们并不知道vtable有多大,但改结构的大小很容易。点击class1结构里的vtable,按y,把它的类型改成“class1_vtable *”。按F5刷新下伪代码的窗口,结果如下:
我们可以把方法命名成"method1"到“method3”。method3当然就是析构函数。根据编程约定和所使用的编译器,第一个函数经常是析构函数,但这里有一个反例。现在我们分析下构造函数。
分析构造函数
int __cdecl class1::ctor(void *a1)
{
sub_80487B8(a1);
*(_DWORD *)a1 = &off_8048A38;
return puts("B::B()");
}
你可以先把a1的类型改一下。puts()调用证实了这个是构造函数,我们甚至能了解到这个类叫“B”。
sub_80487B8() 在构造函数里被直接调用,这个函数也许是class1的经函数,但也可能是父类的构造函数。
off_8048A38是class1的vtable,到这里你已经能知道vtable的大小了(只需要看vtable附近有Xref的数据的数量)和一个class1虚函数的列表。你可以把他们命名成“ class1_mXX”,但需要注意的是其中的一些函数可能与其他类共享。
更改这个vtable的类型信息也是没有问题的。但我不推荐这么做,因为你会丢掉IDA的经典窗口,并且这样做也提供不了任何你在经典窗口里看不到的东西。
构造函数里的奇怪调用:
int __cdecl sub_80487B8(int a1)
{
int result; // eax@1
*(_DWORD *)a1 = &off_8048A50;
puts("A::A()");
result = a1;
*(_DWORD *)(a1 + 4) = 42;
return result;
}
构造函数里的sub_80487b8() 函数是同样类型的函数:一个虚函数表 指针放到了vtable成员里,puts()调用告诉我们我们在另外一个构造函数里。
不要把参数a1的类型改成class1,因为我我们已经不在class1里了。我们找到了一个新的类,把它命名成class2。这个类class1的父类。我们做下和class1一样的工作。他们之间的区别仅仅是我们不知道class2成员的具体大小。这里有两种方法找到它:
- 看对class2 ::ctor的xref,如果我们能找到一个对它的直接调用,例如一个对class2的实例化,我们就能知道class2成员函数的大小。
- 看vtable里的函数,尝试找出被访问过的最高的成员。
在我们这种情况下,class2 ::ctor访问了最开始的4个字节之后的4个字节。因为class2的子类class1是8个字节长,所以class2的大小也是8个字节。
为所有的子类做同样的操作,从父类到子类给这些虚函数进行命名。
对析构函数的研究
Let’s go back to our main function. We can see that the last call, before our v0 object becomes a memory leak, is a call to the third virtual method of class2. Let’s study it.
if ( v0 )
((void (__cdecl *)(class1 *))
v0->vtable->method_3)(v0);
void __cdecl class1::m3(class1 *a1)
{
class1::m2(a1);
operator delete(a1);
}
void __cdecl class1::m2(class1 *a1)
{
a1->vtable = (class1_vtable *)&class1__vtable;
puts("B::~B()");
class2::m2((class2 *)a1);
}
void __cdecl class2::m2(class2 *a1)
{
a1->vtable = (class2_vtable *)&class2__vtable;
puts("A::~A()");
}
我们可以看到, class1::m3是一个析构函数,调用了class1::m2这一class1的主要析构函数。这个析构函数通过设置vtable为class1确保我们在class1的上下文。然后调用了class2的析构函数,这个析构函数也把vtable设置为class2的上下文。这种方法被用来遍历整个类的继承树,因为继承树的所有类的虚析构函数都要被调用。
这些映射是怎么回事,为什么两个结构里定义了一样的变量?
在用C表示OOP的过程中,我们遇到了和你一样的问题:有时候某些变量在所有的继承树里都会出现。下面是我避免变量重复定义的方法:
对每一个类,定义一个classXX_members, classXX_vtable, classXX结构
classXX 包含
+++ vtable (typed to classXX_vtable *)
+++ classXX-1_members (members of the superclass)
+++ classXX_members, if any
classXX_vtable contains
+++classXX-1_vtable
+++classXX’s vptrs, if any
理想情况下,你应该从父类开始到子类结束,直到你分析到一个没有子类的类位置。在这个例子里,下面使我们的解决办法:
00000000 class1 struc ; (sizeof=0x8)
00000000 vtable dd ? ; offset
00000004 class2_members class2_members ?
00000008 class1 ends
00000008
00000000 ; ----------------------------------------------00000000
00000000 class1_members struc ; (sizeof=0x0)
00000000 class1_members ends
00000000
00000000 ; ----------------------------------------------00000000
00000000 class1_vtable struc ; (sizeof=0xC)
00000000 class2_vtable class2_vtable ?
0000000C class1_vtable ends
0000000C
00000000 ; ----------------------------------------------00000000
00000000 class2 struc ; (sizeof=0x8)
00000000 vtable dd ? ; offset
00000004 members class2_members ?
00000008 class2 ends
00000008
00000000 ; ----------------------------------------------00000000
00000000 class2_vtable struc ; (sizeof=0xC)
00000000 method_1 dd ? ; offset
00000004 dtor dd ? ; offset
00000008 delete dd ? ; offset
0000000C class2_vtable ends
0000000C
00000000 ; ----------------------------------------------00000000
00000000 class2_members struc ; (sizeof=0x4)
00000000 field_0 dd ?
00000004 class2_members ends
00000004
int __cdecl main()
{
class1 *v0; // ebx@1
v0 = (class1 *)operator new(8);
class1::ctor(v0);
((void (__cdecl *)(class1 *)) v0->vtable->class2_vtable.method_1)(v0);
if ( v0 )
((void (__cdecl *)(class1 *)) v0->vtable->class2_vtable.delete)(v0);
return 0;
}
int __cdecl class1::ctor(class1 *a1)
{
class2::ctor((class2 *)a1);
a1->vtable = (class1_vtable *)&class1__vtable;
return puts("B::B()");
}
class2 *__cdecl class2::ctor(class2 *a1)
{
class2 *result; // eax@1
a1->vtable = (class2_vtable *)&class2__vtable;
puts("A::A()");
result = a1;
a1->members.field_0 = 42;
return result;
}
总结
- 当你找到一个新的类时,对其进行命名,在分析出这个类的有意义的名字前分析出整个继承树。
- 从父类开始分析到子类。
- 先查看构造函数和析构函数,找到对new()和静态方法的调用。
- 同一个类的函数在编译过的文件里一般彼此相邻。而相关的类(继承关系)可能彼此之间离得很远。有时候构造函数会在子类的构造函数里内联,甚至在实例化的地方出现。
- 如果你想在逆向继承关系比较复杂的结构时,使用“结构包含结构”的技巧只需要命名一次变量。
- 尽管使用hex-rays的类型系统,它非常强大。
- 纯虚类很让人头大,你可以发现几个类有相似的vtable,但却通常没有代码,要注意他们。
本文中用到的代码
#include <iostream>
#include <stdio.h>
class A {
public:
A(){
printf("A::A()\n");
id = 42;
}
virtual void a(){
printf("Virtual A::a()\n");
}
virtual ~A(){
printf("A::~A()\n");
}
private:
int id;
};
class B : public A {
public:
B(){
printf("B::B()\n");
}
virtual ~B(){
printf("B::~B()\n");
}
virtual void a(){
printf("Virtual B::a()\n");
A::a();
}
};
int main(){
A *b = new(B);
b->a();
delete(b);
return 0;
}
为了方便我直接把二进制文件后缀改成jpg了,下载下来把文件后缀去掉就OK了
编译之后的二进制文件:
IDA Pro - 使用IDA Pro逆向C++程序的更多相关文章
- 使用IDA Pro逆向C++程序
使用IDA Pro逆向C++程序 附:中科院李_硕博 : IDA用来做二进制分析还是很强大的 .lib程序是不是很容易分析出源码? 这个得看编译选项是怎么设置的 如果没混淆 没太过优化 大体能恢复源码 ...
- 【逆向工具】IDA使用4-控制台逆向分析 Reverse004.exe 获取密码
工具 吾爱破解版本OD.IDA6.8 OD使用-动态分析 OD快捷方式 F2 下断点,也就是指定断点的地址F3加载一个可执行程序,进行调试分析F4程序执行到光标处 F5 缩小.还原当前窗口 F7 单步 ...
- LoadLibrary(C:\soft\IDA 7.0\IDA 7.0\plugins\python64.dll) error: 找不到指定的模块。 C:\soft\IDA 7.0\IDA 7.0\plugins\python64.dll: can't load file LoadLibrary(C:\soft\IDA 7.0\IDA 7.0\plugins\python64.dll) erro
LoadLibrary(C:\soft\IDA 7.0\IDA 7.0\plugins\python64.dll) error: 找不到指定的模块. C:\soft\IDA 7.0\IDA 7.0\p ...
- 逆向分析-IDA动态调试WanaCrypt0r的wcry.exe程序
0x00 前言 2017年5月12日全球爆发大规模蠕虫勒索软件WanaCrypt0r感染事件,各大厂商对该软件做了深入分析,但针对初学者的分析教程还比较少,复现过程需要解决的问题有很多,而且没有文章具 ...
- 列举一些 MacBook Pro 必需的外设和应用程序推荐
来源:知乎 文章收录于:风云社区SCOEE,提供上千款mac软件下载 基于从事Apps设计或开发者,使用 MacBook Pro,以下罗列一些必需的外设和应用程序推荐. Retina 256GB SS ...
- VS2010 + IDA SDK 搭建IDA Plugin开发环境
http://www.h4ck.org.cn/2011/11/vs2010-idasdk6-2-ida-plugin-development/ 1. 执行菜单的File->New->Pro ...
- 逆向MFC程序
目录 @ 1 MFC执行流程 1.1 环境支持 1.2 分析 1.3 实践探索 1.3.1 创建一个MFC程序 1.3.2 下关键断点并调试 1.4 转向MFC库源文件中观测 2 逆向 2.1 特征码 ...
- 图论--最短路--第K短路(IDA*)(IDA Star)模板
#include <iostream> #include <cstdio> #include <cstring> #include <queue> us ...
- 使用IDA pro逆向ARM M系核心的Bin固件
使用IDA pro逆向ARM M系核心的Bin固件 物联网和智能设备这两年还是比较火的,我们的手中或多或少都有了几个智能设备,比如手环,智能手表,或者门锁什么之类的东西,但是同学们在做逆向的时候, ...
随机推荐
- Python浮点型数据小数点的取舍
python默认的是17位小数的精度 1.round()内置方法 π=3.1415926535 new_num=round(π,2) #四舍五入保留两位小数 print(new_num) ...
- 问题 1014: [编程入门]阶乘求和python):(本地测试正确;但提交不对!!??)求教
问题 1014: [编程入门]阶乘求和 时间限制: 1Sec 内存限制: 128MB 提交: 27629 解决: 5450 题目描述 求Sn=1!+2!+3!+4!+5!+…+n!之值,其中n是一个数 ...
- JavaScript基础入门04
目录 JavaScript 基础入门04 JavaScript 对象 介绍 关于键名 对象的引用 语句和表达式需要注意的地方 对象属性常见的操作 with语句 JSON 特点 语法规则 JSON合法示 ...
- nvm 安装及操作 node版本管理
安装 > curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.31.1/install.sh | bash 安装完成后重启下 ...
- 如何在VUE中使用leaflet地图框架
前言:在leaflet的官方文档只有静态的HTML演示并没有结合VUE的demo 虽然也有一些封装好的leaflet库例如Vue-Leaflet,但是总感觉用起来不是那么顺手,有些业务操作还是得用l ...
- JS创建对象的四种简单方式 (工厂模式和自定义构造函数创建对象的区别)
// 对象:特指的某个事物,具有属性和方法(一组无序的属性的集合) // 特征------>属性 // 行为------>方法 // 创建对象的四种方式 1 // 1.字面量的方式,就是实 ...
- Python爬虫学习==>第九章:正则表达式基础
学习目的: 正则表达式是对字符串操作的一种逻辑公式,就是用事先定义好的一些特点字符.及这些特点字符组合,组成一个“规则字符串”,这个“规则字符串”用来表达对字符串的一种过滤逻辑. 正式步骤 Step1 ...
- LeetCode.1122-相对排序数组(Relative Sort Array)
这是小川的第393次更新,第427篇原创 01 看题和准备 今天介绍的是LeetCode算法题中Easy级别的第258题(顺位题号是1122).给定两个数组arr1和arr2,arr2中的元素是不同的 ...
- 论文翻译:LP-3DCNN: Unveiling Local Phase in 3D Convolutional Neural Networks
引言 传统的3D卷积神经网络(CNN)计算成本高,内存密集,容易过度拟合,最重要的是,需要改进其特征学习能力.为了解决这些问题,我们提出了整流局部相位体积(ReLPV)模块,它是标准3D卷积层的有效替 ...
- selenium 安装流程
安装 1.安装python 2.cmd中输入:pip install selenium 输入pip show selenium检查是否安装完成 3.下载chromedriver 在浏览器中输入chro ...