1. 小尾循序
intel 处理器使用小尾循序的方案存取内存数据。所以我们这样定义一块数据的时候:
在内存中实际的表示是:
2. movzx和movsx
当我们试图从一个较小的操作数想一个较大的操作数移动数据的时候就会发生问题:
当然我们可以用如下方式解决:
mov eax, 0
mov bx, 1234h
mov eax,ebx
但是当我们遇到有符号数字时候还是出错了。
所以intel设计了movzx和movsx指令来处理从较小的操作数向较大的操作数移动数据。
其中movzx给无符号的用, movsx给有符号的用。
3. 翻转字符串
1 ;翻转字符串
2 .data
3 source byte "this is the source string", 0
4 target byte sizeof source dup(0),0
5
6 .code
7 start:
8
9 mov esi, 0
10 mov ecx, sizeof source
11
12 L1:
13 movzx eax, source[esi]
14 push eax
15 inc esi
16 loop L1
17
18
19 mov esi, 0
20 mov ecx, sizeof source
21 L2:
22 pop eax
23 mov target[esi], al
24 inc esi
25 Loop L2
26
27
28 end start
4. 过程
;------------------------------------------------
;description: calculate and returns the sum of three 32-bit integers
;param: eax, ebx, ecx, the three intergets
;return: eax = sum, and the status flags are changed
;------------------------------------------------
sumof proc
add eax, ebx
add eax, ecx
ret
sumof endp
;3个整数相加
.data
.code
main:
mov eax, 1
mov ebx, 2
mov ecx, 3
call sumof
end main
5. 整数相加
1 ;整数求和
2 .data
3 intArray word 100h, 200h, 300h
4
5 .code
6 start:
7
8 mov edi, offset intArray
9 mov eax, 0
10 mov ecx, lengthof intArray
11
12 L1:
13 add eax, [edi]
14 add edi, type intArray
15 loop L1
16
17
18 end start
*******************************************************************************************************
这篇文章介绍喊话功能的实现, 游戏还是热血江湖。
1. 原理:
游戏中, 喊话的函数应该是这样的:
void speak(string contex)
{
doSomthing(contex);
}
我们要实现喊话功能, 只需要找到speak函数的地址, 压入参数然后调用之即可。
2. speak函数分析
逆向工程都是从数据开始的。 所以我们首先用CE来分析数据。
打开CE, 进行如下搜索:
然后,用22222222222过滤
这里我们发现0xD0A784和0x0470766c都可能为喊话内容的地址。修改者两个地址的内容如下:
然后在游戏中按下回车, 发现输出的是222222, 所以我们喊话内容的地址就为0x0470766C。
接下来我们就可以分析是谁调用了这个地址的内容了。用CE查看是谁访问了这个地址:
Ok 下,CE的使命完成, 下面轮到OD上场。
打开od,附加进程, 首先分析0058735f附近的代码:
这里分析得出,这里是字符串的预处理过程, 所以可以往上查看是谁调用了他。
发现是这里, 所以我们可以这样测试一下代码:
1 mov esi, 05FE6758
2 mov edx, [esi]
3 push 0d
4 push 0d
5 push 3ed
6 mov ecx, esi
7 call [edx+4]
验证一下是OK的, 所以这个位置就是喊话的call了。然后我们再来分析各个参数代表的意义,其中0x0D表示的是回车键的键值。
然后esi代表的意义呢, 我们用ce 搜索esi的值,发现如下:
所以ESI = [0X03D4A270]
一切OK了, 所以这里只剩下最后一步了, 往保存喊话内容的地址写入我们需要的字符串。首先我们往字符串的地址下一个内存断点,看什么代码往这一块地址写了值
其中
所以在这里我们只需继续查找esi是如何被赋值的。
分析(CTRL+A)看是谁执行到了0043CA07,
我们验证一下:
发现是OK的。
所以喊话的内容就非常简单了, 执行往[[d0a988]+13c]这个地址写入所需要的字符串,然后调用喊话call就好了。
3. 代码实现
有了前面的分析,代码的实现就非常简单了:
1 void CGameOperate::speak(char* contex)
2 {
3 int*tmp = (int*)(0x0d0a988);
4 char* s = (char*)(*tmp+0x13c);
5 memcpy(s, contex, strlen(contex));
6 _asm
7 {
8 mov eax, 0X03D4A270
9 mov esi, [eax]
10 mov edx, [esi]
11 push 0x0d
12 push 0x0d
13 push 0x3ed
14 mov ecx, esi
15 call [edx+4]
16 }
17 }
4. 总结
这个功能代码实现是非常简单, 主要的工作量就体现在分析上。
1. 基址的概念
基址是什么? 从C++内存模型上来讲, 基址就是一个全局变量, 也就是第一次申明的类的地址。比如有一个类
1 class test
2 {
3 test();
4 ~test();
5
6 private:
7 char* name_;
8 int hp_;
9 int mp_;
10 role* role_;
11 }
C++的内存模型是这样的。在new test()的时候, 其对象的地址为 this(比如:0x0040320),那成员变量 name的地址为this(因为没有虚函数列表),hp_的地址为this+0x4, mp_的地址为this+0x8, role_的地址就为this+oxC;有比如有如下代码:
1 struct role
2 {
3 string name;
4 int hp;
5 int mp;
6 };
7
8 class game
9 {
10 public:
11
12 game()
13 {
14 role_ = new role;
15 role_->hp = 100;
16 role_->mp = 50;
17 role_->name = "hello";
18 }
19 ~game()
20 {
21 if (role_)
22 {
23 delete role_;
24 role_ = 0;
25 }
26 }
27
28 void start();
29
30 private:
31 role* role_;
32
33 };
34
35 int _tmain(int argc, _TCHAR* argv[])
36 {
37
38 game* myGame = new game();
39
40
41 cout<<"hello world"<<endl;
42 return 0;
43 }
我们要读写role::hp的时候, 我们必定要知道 game::role_, 从而必定得知道 game对象的地址。 如果 在 game* myGame = new game(); 中, myGame是一个全局变量的话, 其就是所谓的基址了(base_addr).我们最终血(hp)的地址则为: [base_addr+0]+sizeof(string)。 当然如上的例子中,只有一级偏移, 如果多套嵌几个类就会有几次偏移了。
2. 某游戏例子
逆向分析都是从数据开始的, 所以我们要找基址的时候就从人物的血、蓝入手,分析出基址。这里以某游戏为列子。
1. 用CE 查找数据。
2. 然后用OD 看谁写入了这个地址。
3. 下硬件断点,看谁往这块内存写东西了。发现是这里:
1 2 3 4 5 |
|
看到代码: MOV DWORD PTR DS:[ECX+C],EAX , 就是往[ecx+c]地址写入血值。
4. 继续看ecx 的值是哪里来的的。此时ecx= 0x091ed200, 用ce 搜索这个值。
5. 取出一个值(0x0aa3e9a8), 看谁访问了这块地址。
这里就可以看出基址为[110F7B58],当前血的地址为[[110F7B58]+0x7F0]+0x0C