64位逆向

64位游戏逆向经验

64位

64位逆向比32位还要简单一点

32位与64位的区别

内存大小的区别

32位:FFFFFFFF

64位:FFFFFFFFFFFFFFFF

寄存器的区别

32位:eax ebx ecx edx ebp esp esi edi

64位:rax rbx rcx rdx rbp rsp rsi rdi r8 r9 r10 r11 r12 r13 r14 r15

64的高8位很多是0

浮点寄存器的区别

32位:XMM0–XMM15 64位

64位:XMM0–XMM15 128位

64位添加了YMM0–YMM15 128位

寄存器的作用的区别

rax和eax依然作为返回值

32位的函数约定有很多种

64位的函数约定默认设置了fastcall,因此基本只用一种:fastcall 函数约定。函数传递方式几乎是固定的了

32位的fastcall:

优先ecx和edx来传递参数,再有参数的话用push

64位的fastcall:

优先rcx,rdx,r8,r9来传递参数,再有的话,就从堆栈传递(和32位有一点区别,开辟堆栈空间直接往里面赋值的,看不到push)

注意:再有参数从[esp+20]开始传递

因为编译器默认还会把那四个用寄存器传的值依然预留四个堆栈位置[rsp] [rsp+8] [rsp+10] [rsp+18],因为有时候依然会使用堆栈来访问传入参数。

rsp和esp一样依然是栈顶指针

rbp偶尔作为栈底指针,偶尔作为通用寄存器,偶尔作为其他指针。但ebp常作为栈底指针,而64位rbp更乱。

除了返回值:rax 参数:rcx rdx r8 r9 栈顶指针: rsp 这6个有特殊意义的寄存器,如果你想要在函数调用过程中使用其他所有寄存器,必须先push保存,然后retn前还原。

call的内部必须保存和还原寄存器了

64位的堆栈空间开辟都固定在头部了,局部变量和内部call的参数统一开辟,不能像32位先开辟空间,然后又push参数等方式继续开辟空间。然后在retn前统一还原堆栈

64位做了很多规范,使很多东西统一了,却反而更好逆向了。

64位堆栈与函数

小于4个参数的时候

(1)看rcx,edx,r8,r9看是否在call xxxxxx之前被赋值了,若被赋值了,很大概率就是参数。

(2)进入call内,若出现mov rcx,【123456】,说明rcx不是参数,无用。若出现mov rax,rdx说明rdx是参数有用。(但正常情况下,用rdx和后面的寄存器却不用rcx的情况特别少,但也有)

大于4个参数的时候

只要在call上面看到有mov [rsp+20],xxxxx就说明他是一个参数,另外rcx,rdx,r8,r9也是四个参数。

64位几乎不用考虑其他寄存器的值,就写完参数直接call就可以了,并且几乎不用考虑堆栈平衡。

image-20200703162725216

因此函数里面不存在改变堆栈的操作了

栈地址,下断看看是返回到的上面还是返回到的下面,如果是参数就直接返回去追,如果是局部变量就直接往上追就可以了

rbp+-判断是局部变量还是参数就不行了,而是就正常的去往上追

x64写汇编

读写内存的时候涉及到64位的QWORD

另一个最重要的区别就是不能直接内联汇编了。

操作如下:

新建了x64项目后

右键项目选生成自定义,然后勾上masm,如下:

image-20200703171847869

点确定。

image-20200703172127821

新建后缀名为xxx.asm的文件

image-20200703172206294

xxxx.asm后缀文件上右键属性

image-20200703172351245

image-20200703172513677

然后在xxx.asm中加上如下正确的代码:

1
2
3
4
5
.code

//所有汇编代码写在.code和end之间

end

在汇编中写main函数示例

 extern可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。
  另外,extern也可用来进行链接指定。

1
2
3
4
5
6
7
8
9
10
11
12
13
extern getchar:far
.code
main proc//main函数开始
sub rsp,100h

call getchar//调用getchar函数


add rsp,100h
ret
main endp//main函数结束

end

重点1:注意汇编中16进制必须后缀加h的写法

重点2:想使用函数必须在.code外面声明extern 函数名:far,然后才能到.code和end中call 函数名

重点3:内联汇编文件中的代码不能加注释(会报错),上面的注释只是写给自己看的。

cpp文件中的函数怎么由内敛汇编中调用

 C++语言在编译的时候为了解决函数的多态问题,会将函数名和参数联合起来生成一个中间的函数名称,而C语言则不会,因此会造成链接时找不到对应函数的情况,此时C函数就需要用extern “C”进行链接指定,这告诉编译器,请保持我的名称,不要给我生成用于链接的中间函数名。

比如说cpp中要声明如下:

1
extern "C" char* str1="hello world!";

然后汇编文件中也要进行相应的声明如下:

1
extern str1:far

然后才可以在汇编中使用这个变量

注意:内联汇编中哪怕只有一点点错误也会编译不成功,所以最好加一条代码你就编译一下

添加库:

image-20200703181915797

或者:

1
includelib legacy_stdio_definitions.lib

调用printf示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
includelib legacy_stdio_definitions.lib
extern printf:far
extern getchar:far
extern str1:far


.code
main proc
sub rsp,100h

mov rcx,str1
call printf

call getchar


add rsp,100h
ret
main endp
end

显示结果:

image-20200703182110347

断点分析,发现莫名其妙的

出错分析结果:

image-20200703183451160

image-20200703184017739

rcx中存的是str1的地址!rcx里面存值指向的内容才是str1指针!

因此显示如上结果

因此改成以下代码可以正常运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
includelib legacy_stdio_definitions.lib
extern printf:far
extern getchar:far
extern str1:far


.code
main proc
sub rsp,100h

mov rcx,str1
mov rcx,[rcx]//加了这一句
call printf

call getchar


add rsp,100h
ret
main endp
end

改成如下代码代码后(把指针换成字符数组)也可以正确打印:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
includelib legacy_stdio_definitions.lib
extern printf:far
extern getchar:far
extern str1:far


.code
main proc
sub rsp,100h

mov rcx,str1
call printf

call getchar


add rsp,100h
ret
main endp
end
1
extern "C" char str1[13] = "hello world!";

效果:

image-20200703182820304

事实上,从cpp传到xxxx.asm汇编文件中的都是传过来是指向所传内容的地址,因此真正取值的时候要再往里取值取一层。

如下案例:

1
2
extern "C" char *str1 = "hello world!%d";
extern "C" int num = 10;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
includelib legacy_stdio_definitions.lib
extern printf:far
extern getchar:far
extern str1:far
extern num:far

.code
main proc
sub rsp,100h

mov rcx,str1
mov rcx,[rcx]
mov rdx,num
mov rdx,[rdx]
call printf

call getchar


add rsp,100h
ret
main endp
end

效果如下:

image-20200703192132536

调call案例:

1
2
3
4
5
6
7
8

#include <stdio.h>
extern "C" char *str1 = "hello world!%d";
extern "C" int num = 10;
extern "C" void printfcall()
{
printf("\nhello world!\n");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
includelib legacy_stdio_definitions.lib
extern printf:far
extern getchar:far
extern str1:far
extern num:far
extern printfcall:far

.code
main proc
sub rsp,100h

mov rcx,str1
mov rcx,[rcx]
mov rdx,num
mov rdx,[rdx]
call printf
call printfcall
call getchar


add rsp,100h
ret
main endp
end

效果如下:

image-20200703192518898

在代码中调用call的案例:

代码如下:

1
2
3
4
5
6
7
8
#include <stdio.h>
extern "C" int add2(int a, int b);
void main()
{
printf("%d", add2(22, 33));
getchar();
}

1
2
3
4
5
6
7
8
9
10
11
12
13
includelib legacy_stdio_definitions.lib

.code
add2 proc
sub rsp,100h
mov rax,rcx
add rax,rdx

add rsp,100h
ret
add2 endp

end

效果如下:

image-20200703193218752

x64位工具

CE,其实只是多了一个8字节搜索的选项

xdbg64,没有dd了,ctrl+g输入地址跳过去

注释位置:

image-20200703202714548

符号按钮就是原od的模块

高亮操作=先点h,再点寄存器。

硬件断点也在断点菜单中

复制代码段的时候一定要把数据都撑开

ctrl+F9直到ret,还要按f7才能返回

x64多出来的某些汇编命令

movups xmmword ptr ss:[rsp+20],xmm0

xmmword代表128位,因此相当于xmm0赋值给rsp+20和rsp+28.

注意点

image-20200806181628127

call中出来继续追[rcx],发现追到[rsp+20],由于rcx来源于一个堆栈地址rsp+20,因此[rcx]的值的来源可能是往后追而非往前追,因为可能是先开开辟地址,然后给地址所在位置赋值因此实际上【rcx】来源于xmm0,即选中那一句

WOW

为新手做的准备,血量特别容易崩

劫持注入

注入方式如下:

  1. 十几种游戏注入方法-做辅助必备
  2. (INI)注入
  3. 360安全输入法注入
  4. TP后 自动注入
  5. 劫持注入
  6. 控制台注入
  7. 命令注入
  8. 搜狗输入法注入
  9. 网络注入
  10. 线程注入

利用Window可以先加载当前目录下的dll特性,仿造系统的LPK.DLL,让应用程序先加载我们的伪LPK.DLL,然后在我们的dll中去调用原来系统的原函数.

操作步骤:

禁用优化,并且设置成64位

image-20200812161736366

内联汇编要设置一下

image-20200812161908313

image-20200812162021217

添加新建项,64位的汇编文件

image-20200812162201151

asm汇编文件代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
extern g_oldcalladdr:far

.code

GetFileVersionInfoA proc
;jmp 真实地址(;代表注释)
jmp qword ptr [g_oldcalladdr+8*0]
GetFileVersionInfoA endp

GetFileVersionInfoByHandle proc
;jmp 真实地址(;代表注释)
jmp qword ptr [g_oldcalladdr+8*1]
GetFileVersionInfoByHandle endp

;等等等等,将要劫持的所有函数写到这里(按照顺序写到这里)


end

劫持注入.c代码如下

1
2
3
4
5
6
7
8
9
10
11
extern "C" UINT_PTR g_oldcalladdr[20]={0};

void Call_获取原函数地址()
{
HMODULE h模块句柄=LoadLibraryA("oldVersion");
for(int i=0;i<17;i++)
{
g_oldcallladdr[i]=(UINT_PTR)GetProcAddress(h模块句柄,(char*)i);
}

}

dll初始化位置DLLMAIN中代码如下:

1
2
3
4
5
case DLL_PROCESS_ATTACH:
{
Call_获取原函数地址();
LoadLibraryA("Mydll.dll");//注入我们真实想注入的dll
}

导出表.def要按照顺序写

清理掉自动生成的东西,不然会影响导出函数

p.s. UINT_PTR 相当于QWORD,无类型的64位

工具代替:

是的,纯手写伪造很麻烦,一会儿还得写转发代码~~~。 如果某DLL的导出函数比较多,那真是一个累人的活儿。

在这里我介绍一款比较好用的自动生成工具:

aheadlib

功能:根据输入DLL的导出表生成劫持源码。

把生成的源码载入vc编译器是可以正常编译的。

选项很简单,自己摸索,一般默认即可。

在生成的代码中,你能找到:

AheadLib_ + 原始DLL导出函数名 ()

这样的函数

●DLL劫持的实现●

这一步我们的工作就是通过编程来实现一个LPK.DLL文件,它与系统目录下的LPK.DLL导出表相同,并能加载系统目录下的LPK.DLL,并且能将导出表转发到真实的LPK.DLL。可以看出我们要实现的这个DLL需求如下:
1、构造一个与系统目录下LPK.DLL一样的导出表;
2、加载系统目录下的LPK.DLL;
3、将导出函数转发到系统目录下的LPK.DLL上;

4、在初始化函数中加入我们要执行的代码。

特定引擎

UE5

特定引擎特征

GNAME

CE搜ByteProperty,配合领近内存浏览,可以找到存放字符串的结构 None..ByteProperty.各种xxxProperty ,ByteProperty字符串的地址减掉0x8的地址用CE搜索可以找到一个基地址,该地址-0x10 = GNAME

UE5查找学习|720x360

逆向开发库

minhook库

X86/x64通用

代码语言为C/C++,采用的是MinHook库,作用是hook Windows API函数,拦截函数的执行,在这个过程中可以修改函数的参数,常用于封包拦截工具开发,封包过检测,当然也可以实现任意的关于拦截并修改API执行的功能

开源地址1 开源地址2

img

DLL劫持注入库

开源地址

X86/x64通用

逆向通用函数

特征码搜索相关

kmp[[算法]]实现

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
#include<Windows.h>
#include<iostream>
#include<vector>
#include<time.h>
#include <TlHelp32.h>
using namespace std;

#define BLOCKMAXSIZE 409600//每次读取内存的最大大小
BYTE* MemoryData;//每次将读取的内存读入这里
short Next[260];




DWORD GetProcessId(char* szProcessName)
{
HANDLE hProcessSnapShot = NULL;
PROCESSENTRY32 pe32 = { 0 };

hProcessSnapShot = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);

if (hProcessSnapShot == (HANDLE)-1) return NULL;

pe32.dwSize = sizeof(PROCESSENTRY32);

if (Process32First(hProcessSnapShot, &pe32))
{
do {
if (!strcmp(szProcessName, pe32.szExeFile)) return pe32.th32ProcessID;
} while (Process32Next(hProcessSnapShot, &pe32));
}
else
::CloseHandle(hProcessSnapShot);

return NULL;
}

//特征码转字节集
WORD GetTzmArray(char* Tzm, WORD* TzmArray)
{
int len = 0;
WORD TzmLength = strlen(Tzm) / 3 + 1;

for (int i = 0; i < strlen(Tzm); )//将十六进制特征码转为十进制
{
char num[2];
num[0] = Tzm[i++];
num[1] = Tzm[i++];
i++;
if (num[0] != '?' && num[1] != '?')
{
int sum = 0;
WORD a[2];
for (int i = 0; i < 2; i++)
{
if (num[i] >= '0' && num[i] <= '9')
{
a[i] = num[i] - '0';
}
else if (num[i] >= 'a' && num[i] <= 'z')
{
a[i] = num[i] - 87;
}
else if (num[i] >= 'A' && num[i] <= 'Z')
{
a[i] = num[i] - 55;
}

}
sum = a[0] * 16 + a[1];
TzmArray[len++] = sum;
}
else
{
TzmArray[len++] = 256;
}
}
return TzmLength;
}

//获取Next数组
void GetNext(short* next, WORD* Tzm, WORD TzmLength)
{
//特征码(字节集)的每个字节的范围在0-255(0-FF)之间,256用来表示问号,到260是为了防止越界
for (int i = 0; i < 260; i++)
next[i] = -1;
for (int i = 0; i < TzmLength; i++)
next[Tzm[i]] = i;
}

//搜索一块内存
void SearchMemoryBlock(HANDLE hProcess, WORD* Tzm, WORD TzmLength, unsigned __int64 StartAddress, unsigned long size, vector<unsigned __int64>& ResultArray)
{
if (!ReadProcessMemory(hProcess, (LPCVOID)StartAddress, MemoryData, size, NULL))
{
return;
}

for (int i = 0, j, k; i < size;)
{
j = i; k = 0;

for (; k < TzmLength && j < size && (Tzm[k] == MemoryData[j] || Tzm[k] == 256); k++, j++);

if (k == TzmLength)
{
ResultArray.push_back(StartAddress + i);
}

if ((i + TzmLength) >= size)
{
return;
}

int num = Next[MemoryData[i + TzmLength]];
if (num == -1)
i += (TzmLength - Next[256]);//如果特征码有问号,就从问号处开始匹配,如果没有就i+=-1
else
i += (TzmLength - num);
}
}

//搜索整个程序
int SearchMemory(HANDLE hProcess, char* Tzm, unsigned __int64 StartAddress, unsigned __int64 EndAddress, int InitSize, vector<unsigned __int64>& ResultArray)
{
int i = 0;
unsigned long BlockSize;
MEMORY_BASIC_INFORMATION mbi;

WORD TzmLength = strlen(Tzm) / 3 + 1;
WORD* TzmArray = new WORD[TzmLength];

GetTzmArray(Tzm, TzmArray);
GetNext(Next, TzmArray, TzmLength);

//初始化结果数组
ResultArray.clear();
ResultArray.reserve(InitSize);

while (VirtualQueryEx(hProcess, (LPCVOID)StartAddress, &mbi, sizeof(mbi)) != 0)
{
//获取可读可写和可读可写可执行的内存块
if (mbi.Protect == PAGE_READWRITE || mbi.Protect == PAGE_EXECUTE_READWRITE)
{
i = 0;
BlockSize = mbi.RegionSize;
//搜索这块内存
while (BlockSize >= BLOCKMAXSIZE)
{
SearchMemoryBlock(hProcess, TzmArray, TzmLength, StartAddress + (BLOCKMAXSIZE * i), BLOCKMAXSIZE, ResultArray);
BlockSize -= BLOCKMAXSIZE; i++;
}
SearchMemoryBlock(hProcess, TzmArray, TzmLength, StartAddress + (BLOCKMAXSIZE * i), BlockSize, ResultArray);

}
StartAddress += mbi.RegionSize;

if (EndAddress != 0 && StartAddress > EndAddress)
{
return ResultArray.size();
}
}
free(TzmArray);
return ResultArray.size();
}

int main()
{
//初始化MemoryData大小
MemoryData = new BYTE[BLOCKMAXSIZE];


//HWND hWnd = FindWindow(NULL, "实时RVC变声器");
DWORD dwProcId = GetProcessId((char*)"web-client.exe");
//GetWindowThreadProcessId(hWnd, &dwProcId);



DWORD pid = 0;
vector<unsigned __int64> ResultArray;

if (dwProcId != 0)
{
cout << "进程ID:" << dwProcId << endl;
}
else
{
cout << "进程ID获取失败" << endl;
}


//cin >> pid;



//通过进程ID获取进程句柄
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, false, pid);

//68 74 74 70 73 3A 2F 2F 73 70 61 63 65 2E 62 69 6C 69 62 69 6C 69 2E 63 6F 6D 2F 31 39 37 36 39 31 37 37 34 7D 22 00 00
int start = clock();
SearchMemory(hProcess, (char*)"68 74 74 70 73 3A 2F 2F 73 70 61 63 65 2E 62 69 6C 69 62 69 6C 69 2E 63 6F 6D 2F 31 39 37 36 39 31 37 37 34 7D 22 00 00", 0x0000000000000000, 0x00007fffffffffff, 30, ResultArray);
int end = clock();

cout << "用时:" << end - start << "毫秒" << endl;
cout << "搜索到" << ResultArray.size() << "个结果" << endl;

long long addr = NULL;

for (vector<unsigned __int64>::iterator it = ResultArray.begin(); it != ResultArray.end(); it++)
{
printf("%llx\n", *it);

addr = *it;

}

printf("%llx\n", addr);
//68 74 74 70 3A 2F 2F 73 70 61 63 65 2E 62 69 6C 69 62 69 6C 69 2E 63 6F 6D 2F 31 37 39 37 39 36 30 37 35 32 7D 22 00 00
BYTE Buffer[] = { 0x68,0x74,0x74,0x70,0x3A,0x2F,0x2F,0x73,0x70,0x61,0x63,0x65,0x2E,0x62,0x69,0x6C,0x69,0x62,0x69,0x6C,0x69,0x2E,0x63,0x6F,0x6D,0x2F,0x31,0x37,0x39,0x37,0x39,0x36,0x30,0x37,0x35,0x32,0x7D,0x22,0x00,0x00 };

WriteProcessMemory(hProcess, (LPVOID)addr, Buffer, sizeof(Buffer), NULL);



getchar();
getchar();
getchar();
getchar();
system("pause");
return 0;
}

浮点类型与二进制位的转换

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
29
30
31
32
33
#include <stdio.h>
#include <stdint.h>
#include <limits.h> /* for CHAR_BIT */ /** formatted output of ieee-754 representation of float */
void show_ieee754 (float f)
{
union
{
float f;
uint32_t u;
} fu = {.f = f};
int i = sizeof f * CHAR_BIT;

printf(" ");
while (i--)
printf("%d ", (fu.u >> i) & 0x1);

putchar('\n');
printf(" |- - - - - - - - - - - - - - - - - - - - - - "
"- - - - - - - - - -|\n");
printf(" |s| exp | mantissa"
" |\n\n");
}

int main(void)
{

float f = 3.14159f;

printf("\nIEEE-754 Single-Precision representation of: %f\n\n", f);
show_ieee754(f);

return 0;
}