IDA Pro ClassInformer使用指南(翻译)

文章目录
  1. 1. 0x00 介绍
  2. 2. 0x01 例子
  3. 3. 0x02 准备工作
  4. 4. 0x03 设置IDA环境
  5. 5. 0x05 最后致谢
  6. 6. 外部参考

0x00 介绍

本文主要针对几乎没有使用IDA Pro经验及逆向水平一般的人。IDA对初学者还是有一定的门槛的,主要是由于IMO没有详细介绍IDA使用环境设置和插件使用的指南。我的目标是展示当我在客户端(这里应该是开发外挂)开发遇到困难的时候,我是如何解决这个问题的方法和策略。在本指南中我将演示如何从SDK中的一个字符串识别出二进制文件中反汇编代码中的函数。目标是将IDA中原始的汇编代码转换为ClientMode::CreateMove函数。

0x01 例子

在开始之前,下面是一个能直观展示IDA强大功能的例子。这是两张图片但都是steamclient.dll中同一个函数反汇编后的结果,第一张图片是原始的反汇编代码,第二张是相同函数反汇编结果但是经过人工逆向解析。





使用IDA逆向就像解决成千上万个难题一样。未命名的全局变量(dword_X)和函数(sub_Y)就像拼图碎片。在逆向过程中识别全局变量和函数会对整个逆向过程产生连锁反应。举例说明下,当你逆向一个函数时识别出了全局变量g_pGlobals,然后你将变量名从dword_Z重命名为g_PGlobals。现在模块中访问此变量的所有函数中都会使用g_pGlobals这个名称。如果你想知道一个使用g_pGlobals变量函数的作用,那么你又将需要逆向一个函数。
逆向出来的越多,后面逆向会更容易,这将会很有成就感,这种成就感会让你上瘾,停不下来。

0x02 准备工作

开始之前,我建议你安装下载如下工具:
IDA:

其他

0x03 设置IDA环境

每个客户端(应该是外挂程序)都需要hook CreateMove去改变用户命令,并且Hook clientMode模块的函数是最好的因为不需要像Hook CHLClient模块之后还需要修复CRC校验值,我们猜测ClientMode类是在CS:GO客户端模块中定义的。使用IDA打开CS:GO游戏模块client.dll。将该文件拖入IDA你将会看到下图所示界面:



我们需要关注“Manual load”这个选项,选中它将会有一系列的提示,提醒我是否需要反汇编/解析模块的某些部分。




第一个提示是设置模块基地址,将其设置为0,以便从相对地址从0开始。



接下来一系列提示会询问我们是否分析某些模块,一直选择”Yes”直到提示是否加载.pdb文件:



因为我们没有pdb,所以选择“No”。现在IDA会花一些时间完成反汇编。当左下脚信息栏显示“AU:idle”时,说明IDA已经完成了分析:



现在看到的是IDA反汇编程序的关键部分,IDA View-A选项卡是client.dll的完整反汇编代码。IDA递归的反编译原始二进制为asm汇编文件。
首选我们需要设置几个默认选项以显示一些默认情况下不显示的有用信息,通过点击菜单栏“Options->General”进行设置,下面我会对我已经设置的选项进行说明。



1.这是主要的不同。设置后在左边的地址显示会被改变,改变后的地址会变成如’sub_7E0CB7+offset’的形式,而不是默认的’.text:0035235A’。
2.设置后会在地址显示的右侧增加一个3位数值,可以通过函数跟踪当前栈指针的值。
3.该选项会为所有的汇编操作指令生成简单的注释,如果你对汇编不熟悉,可能会有帮助。但当你熟悉常见的汇编指令后,你会想要禁用该选项,因为这些评论可能会扰乱IDA的反汇编显示。
4.该选项表示指令和左边显示栏的宽度。这纯粹是个人喜好,我喜欢小数字因为减少了死角。

## 0x04 识别ClientMode::CreateMove函数
现在我们准备好开始逆向了,我们从启动Class Informer开始。在目录栏选择“Edit->Plugins->Class Informer”。如果你没有发现这个选项可能是你没有安装插件到正常的目录(安装插件后记得重启IDA)。你会看到下面这个默认界面弹出,然后点击”CONTINUE“继续。



Class Informer将会解析反汇编的RTTI。如果还没有阅读OpenRCE关于RTTI的文章,请马上去看一下。RTTI是非常重要的,这是Class Informer插件实现的基础。如果解析完成,会在IDA上弹出一个新的选项卡。



[Class Informer]选项卡(图中用黄色框勾选),Vftable:类的RTTI在反汇编中的地址。Methods:该类虚函数的个数。Flag:’M’意味这个类有多重继承。Type:类的名称,通常和SDK中定义的有相同的类名。Hierarchy:该类继承的类名称的列表。
点击列表中任何一个类将转到汇编窗口中该类的虚函数列表。搜索“ClientMode”,将会有很多结果,因为“ClientMode”也是其他类名称的子字符串,向下滚动直到找到“IClientMode”函数:



这里有一个问题,我们在向下滚动时会看到ClientMode的抽象类及该类的几个派生类。我们如何确定哪一个类的虚表包含我们想要hook的CreateMove函数呢?
在sdk中搜索!打开命令行,通过cd命令切换到2013 SDK目录。然后执行下面的命令:
1
grep -lir "::CreateMove"


-l:查询多文件时只输出包含匹配字符的文件名,-i:忽略大小写,-r:递归搜索。执行该命令后将打印类或命名空间中定义了CreateMove的文件名。
输出类似下面的:
1
2
3
4
5
6
7
8
9
10
./src/game/client/cdll_client_int.cpp
./src/game/client/clientmode_shared.cpp
./src/game/client/c_baseplayer.cpp
./src/game/client/hl2/c_basehlplayer.cpp
./src/game/client/hl2/hl2_clientmode.cpp
./src/game/client/hltvcamera.cpp
./src/game/client/in_main.cpp
./src/game/client/replay/replaycamera.cpp
./src/game/server/ai_basenpc.cpp
./src/game/shared/weapon_ifmsteadycam.cpp


clientmode_shared.cpp很可能是我们要找的,打开这个文件,搜索”::CreateMove“定位到定义的位置:
1
2
3
4
5
6
7
8
9
10
bool ClientModeShared::CreateMove( float flInputSampleTime, CUserCmd *cmd )
{
// Let the player override the view.
C_BasePlayer *pPlayer = C_BasePlayer::GetLocalPlayer();
if(!pPlayer)
return true;

// Let the player at it
return pPlayer->CreateMove( flInputSampleTime, cmd );
}


ClientModeShared肯定是要找的类,但是在ClassInformer选项卡中有两行都是ClientModeShared。这种情况主要是由于ClientModeShared类使用了多重继承的原因(注意M标志)。通过分析clientmode_shared.h中类的继承结构定义我们可以确定有57个函数的类是我们要找的类。双击所在的行直接转到ClientModeShared虚表:



现在的位置是一个可点击的函数指针列表,点击任何黄色字段都会跳转到该函数实现的位置。nullsub_X表示该函数没有实现,有很多原因都会造成这种情况但现在这些都不重要。我们的目标是确定哪一个函数是CreateMove.Function String Associate插件会使这个任务更加容易。和执行Class Informer插件一样在目录中执行Function String Associate插件,选择”Continue“在命令行中,IDA显示应该会像下面一样:



右边空白处的注释是该行函数引用的字符串。如你双击”sub_2694A0“你将会看到LevelInit, game_newmap'和mapname`在该函数的实现中使用。 当我们对一个虚函数表进行分析的时候,需要确定一些函数,我们可以通过比较SDK进行参考。我们来看一下能不能通过这三个字符串找到这个函数。
1
2
cd 2013_SDK_src
grep -lir game_newmap .


输出如下:
1
2
3
4
5
6
7
./src/game/client/clientmode_shared.cpp
./src/game/client/game_controls/basemodelpanel.cpp
./src/game/client/game_controls/baseviewport.cpp
./src/game/client/game_controls/MapOverview.cpp
./src/game/client/hltvcamera.cpp
./src/game/client/playerspawncache.cpp
./src/game/client/replay/replaycamera.cpp


我们可以根据这个结果继续进行分析。打开clientmode_shared.cpp搜索’game_newmap‘,可以找到3个结果,只有在函数ClientModeShared::LevelInit有出现使用。双击函数sub_2694A0,可以在IDA中看到这个函数的定义,你会看到如下内容,部分相关信息我已经标出来了:



- 1.IDA在函数的开头声明函数的参数和每个局部变量,IDA已经确定这个函数有一个4字节的(dword ptr 8)参数(arg_0)。8表示该变量在EBP+8(返回地址在EBP+4)。不过也需要注意IDA可能无法正确分析出函数参数数量和参数的大小。
- 2.这是一个字符串引用。表示该行会访问全局字符串变量”LevelInit“。除了IDA自动根据字符串内容重命名的的字符串,其他情况下字符串变量的名称为”dword_x“。
如果你有hexrays,将鼠标停留在函数体中然后按’F1‘(默认反编译快捷键),你会看到一个包含C代码的新页面。你可以通过在空白处右键然后取消选中”Show Casts“来清除它。下面是你将会看到的注释:



- 1.函数定义有两个参数。IDA会将类表示为显示指针的结果,如果一个函数是类的成员函数,第一个参数是指向类实例的指针,即this指针,a2才是真正的第一个参数。
- 2.局部变量声明。如果IDA在模块某些部分分析出现错误,这里的结果可能是不精确的,某些变量可能是重复、错误大小或则类型等。你需要及时学会去识别和修复这些错误。
- 3.这是虚函数调用在反编译中的形式。你可以分解者一样代码:v4=((v3+156))(v3);(v3 + 156):v3是一个将要被解引的一个指针然后加上156。如果v3是一个多态对象,这个表达式就是计算第39个虚函数的地址。(v3::method_39)(v3)这就是一个函数调用。解引用这个函数指针并将类的实例对象作为唯一的参数。由于IDA会显式显示C++类指针,因此此函数真实的参数是0个。v4是函数调用后返回的结果,v3::method_39函数的返回值。
- 4.只要你看到一个前缀是一个数据类型的变量,那么表示该变量是一个全家变量(可能是静态或则是一个声明的全局变量)。’dword‘前缀表示这个变量是一个4字节变量。在源代码中声明的形式可能是:static int g_iState;//在函数体外声明。
和sdk中的函数对比:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void ClientModeShared::LevelInit( const char *newmap )
{
m_pViewport->GetAnimationController()->StartAnimationSequence("LevelInit");

// Tell the Chat Interface
if ( m_pChatElement )
{
m_pChatElement->LevelInit( newmap );
}

// we have to fake this event clientside, because clients connect after that
IGameEvent *event = gameeventmanager->CreateEvent( "game_newmap" );
if ( event )
{
event->SetString("mapname", newmap );
gameeventmanager->FireEventClientSide( event );
}

// Create a vgui context for all of the in-game vgui panels...
if ( s_hVGuiContext == DEFAULT_VGUI_CONTEXT )
{
s_hVGuiContext = vgui::ivgui()->CreateContext();
}

// Reset any player explosion/shock effects
CLocalPlayerFilter filter;
enginesound->SetPlayerDSP( filter, 0, true );
}


函数结构几乎完全一致。这个未标记的函数肯定是ClientModeShared::LevelInit,点击’IDA View-A‘选项卡返回到汇编代码,将鼠标移动到函数”sub_2694a0“然后按键盘”N“重命名这个函数。



选择”Yes“忽略长文件名弹窗。然后返回到ClientModeShared的虚表。现在将LevelInit函数作为一个参考点,我喜欢在虚表中将函数的索引附加到函数。计算可得这个函数是虚函数表中的第25个函数(从0开始)。如果你会使用python,你可以将屏幕底部的窗口作为python解释器进行计算,如:



让我们开始使用这个参考函数。我们知道ClientModeShared继承自IClientMode抽象类和CGameEventListener类。我们正在处理的虚表代表的是IClientMode的继承。在Class Informer选项卡发现的另一个虚表是代表CGameEventListener。在iclientmode.h中CTRL+F搜索’LevelInit‘:
1
2
3
4
5
6
7
virtual vgui::Panel *GetMessagePanel() = 0;
virtual void OverrideMouseInput( float *x, float *y ) = 0;
virtual bool CreateMove( float flInputSampleTime, CUserCmd *cmd ) = 0;
virtual void LevelInit( const char *newmap ) = 0;
virtual void LevelShutdown( void ) = 0;
virtual bool ShouldDrawViewModel( void ) = 0;
virtual bool ShouldDrawCrosshair( void ) = 0;


我们知道虚函数时按照源文件中声明的顺序开始排序的。我们可以得出结论,CreateMove应该是LevelInit函数之前的函数。需要注意的是SDK通常不是最新的,函数的顺序可能不完全相同。你应该使用SDK作为一个简单的指南,去找到这些函数的引用。
返回IDA,双击函数指针”dd offset ClientModeShared__LevelInit”。我们可以通过比较IDA反汇编中参数的数量和SDK CreateMove函数原型是否一致,证明这就是CreateMove。hex view声明了两个与CreateMove原型匹配的函数参数,这看起来是有希望的。比较下ClientModeShared::CreateMove在sdk clientmode_shared.cpp中的声明是否和反编译的中的一致。
1
2
3
4
5
6
7
8
9
10
bool ClientModeShared::CreateMove( float flInputSampleTime, CUserCmd *cmd )
{
// Let the player override the view.
C_BasePlayer *pPlayer = C_BasePlayer::GetLocalPlayer();
if(!pPlayer)
return true;

// Let the player at it
return pPlayer->CreateMove( flInputSampleTime, cmd );
}





找到了,切换到hex view然后重命名这个函数。


0x05 最后致谢

感谢@Sirmabus开发了上面两个好用的插件:

  • 1.上面提到的方法不是找到函数CreateMove函数最简便最直观的方法
  • 2.鼓励每个人发布他们自定义的环境并指出我的文章中的任何错误。

外部参考