CRC Check Bypass

0x00 前言

太久没有更博客了,今天记录一下前段时间学到的完整性检测绕过,比如绕过CRC检测。

像一些程序,总会有一些完整性检测比如CRC。当用逆向工程的方法修改程序的代码时,程序可能会做出响应的对抗操作。

0x01 分析

这类检测往往是对程序的代码段进行CRC检测来判断程序的代码段是否被修改,进行CRC检测有许多好处,如能防止通过修改程序代码来暴力破解一些软件等

CRC校验值往往会存在内存中的某个位置

CRC检测的代码也会位于代码段

CRC检测几乎都会有cmp 正常情况的CRC,实际检测到的CRC然后跳转的操作。

基于如上分析,不难有以下几种思路,可能有不对的地方,还望各位大佬斧正!

思路一

在不改变代码段的情况下,找到存放CRC校验值的内存地址,跟踪这个内存地址,找到CRC检测的相关汇编。

然后去修改cmp 正常情况的CRC,实际检测到的CRC后的跳转,强行让程序往”检测无误的方向”进行跳转。

本思路优点:简单粗暴,省时省力。

本思路缺点:可能会有暗门。

思路二

在不改变代码段的情况下,找到存放CRC校验值的内存地址,跟踪这个内存地址,找到CRC检测的相关汇编。

然后主动按照我们的意愿来修改代码段,然后主动用CRC算法来得到修改后的代码段的校验值,然后将这个校验值存到一个不用的内存地址中。

强行修改CRC检测相关汇编的传地址过程以实现cmp 我们存的CRC校验值,实际检测到的CRC校验值的效果,按照前面的操作,我们存的CRC校验值就应该与实际检测到的CRC校验值相等,从而使得程序往”检测无误的方向”跳转。

本思路优点:呃……

本思路缺点:①实现过程中(主动修改代码段但还没来得及修改cmp部分时),有可能直接触发CRC检测未通过;②实现起来较为麻烦;③用来存放我们计算得到的CRC校验值的时候,这个存放地址有可能选到奇奇怪怪的会触发异常的位置(虽然可以凭经验来选到一些好位置)

思路三

不改变代码段,将整个代码段复制到一个不用的内存地址,同思路一和二找到CRC检测的相关汇编

这次是去修改CRC检测相关汇编的扫描起点扫描终点,也有可能是扫描起点扫描长度的形式

让程序去扫描我们存的代码段备份,这样我们就能放心对程序的代码段进行修改了

本思路优点:①这种”乾坤大挪移”的思想很爽;②用这种思路写出来的脚本的通用性较强,大部分情况下只需要修改脚本的部分内容就能实现绕过检测。

本思路缺点:①实现起来略有些麻烦;②空间复杂度较高。

思路四

这个思路源自《加密与解密》第四版P546的⑧

思路类似于思路三,只不过这次是依然检测原始部分,但执行新部分。

1
重新将要HOOK的模块加载到内存中,然后人为制造异常,把执行流程切换到新加载的模块中。在新加载的模块中仍然可以进行各种HOOK操作,此时守方的检测程序仍然在检测原始的模块,这样就检测不到HOOK了。

0x02 Do it!

以CE自带的CE Game tutorials为例,采用思路三

当修改它的代码段时,标题会变成(Integrity check error)

将代码段还原以后,标题就恢复正常

定位与分析

首先我们要找一段汇编代码段,要找出是什么访问了该汇编代码段。

下面先来寻找一段代码段:这里就找一段和靶子血量相关的汇编代码吧,借助CE可以很简单地找到靶子血量的内存地址,找出什么访问了这个地址(靶子血量地址),从而发现这么一段给靶子血量赋值的汇编代码,并记录下来这段汇编代码的地址10003F45D

接下来,我们要找到是什么一直持续访问这段汇编代码(mov eax,[rcx+70])

由于我使用的无插件的CE,无法直接在Memory Viewer中右键汇编代码然后查找什么访问了这段汇编代码

无奈只能手动在CE中添加这段汇编的地址

确定后,进而右键-找出是什么访问了这个地址

可以发现这里有三个位置持续访问该代码段。

选择10003BABC处查看其反汇编

不难发现rdx+rcx*2处的值应该就是待比较的值。

下面需要理清一个关系,在代码段中,每条语句对于该代码段的基址的偏移是固定的,正如这句movzx edx,word ptr[rdx+rcx*2]相对于代码段的基址的偏移为0x389DC

那么同样的,如果我们将代码段复制一份到其他内存中,我们可以知道复制过去的代码段的基址,加上这个偏移,同样是可以得到这个语句!对任意代码段的语句均是如此!

复制过去的语句的地址 = 复制过去的代码段基址 + 偏移量

因此,需要移花接木,让负责CRC扫描的代码去扫描我们新复制过去的代码段。故可得如下伪代码:

1
2
3
4
5
6
7
8
复制代码段到内存的其他位置并记录复制过去的代码段的基址addressOfCopy
记录程序本身代码段的基址addressThatHoldsTheMoudleBase
本身代码段的基址+代码段的大小即为代码段结束的地址addressThatHoldsTheModuleEnd

对于被扫描的语句
首先进行边界判断,判断其是否大于等于addressThatHoldsTheMoudleBase且小于等于addressThatHoldsTheModuleEnd

然后移花接木,让CRC扫描代码去扫描这个地址(该语句地址 - addressThatHoldsTheMoudleBase + addressOfCopy)

修改

由于这三个位置也位于代码段,所以我们必须保证这三个位置同时被修改。

下面尝试CE自带的自动汇编

对这三个位置中任意一个右键-在反汇编程序中显示地址-工具-自动汇编-模板-CT表框架代码;模板-代码注入-确定即可

保持这个自动汇编窗口不关闭

然后依次对另外两个位置 右键-在反汇编程序中显示地址(目的主要是定位到这个位置,后续点击代码注入后框框里面直接就是这个地址了,算是借助CE的一些方便之处来省时省力了),然后模板-代码注入-确定。

这么弄完以后就已经生成了82行的框架了,不得不说CE在自动汇编这块确实比方便。

采用思路三,我们需要先将代码段进行一次备份。

1
2
3
4
{$lua}
if addressOfCopy==nil then
addressOfCopy=copyMemory(getAddress(process),getModuleSize(process))
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{$asm}

alloc(addressThatHoldsTheMoudleBase,8)
alloc(addressThatHoldsTheModuleEnd,8)
alloc(addressThatHoldsTheCopyBase,8)

addressThatHoldsTheMoudleBase:
dq $process //fill the address with the address of the module base

addressThatHoldsTheModuleEnd:
dq $process+getModuleSize(process)//works, otherwise we can use lua to fill this in

addressThatHoldsTheCopyBase://
dq $addressOfCopy

db定义字节类型变量,占1字节

dw定义字类型变量,占2字节

dd定义双字类型变量,占4字节

dq定义四字类型,占8字节

接下来就是HOOK了

这里以第一个位置10003B9DC为例,写满了注释,应该比较好理解,可以结合着程序执行流程图来更好的理解。

流程图

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
//below 4 lines are automatically generated by CE 
alloc(newmem,2048,"gtutorial-x86_64.exe"+3B9DC)
label(originalcode)
label(returnhere)
label(exit)

newmem: //this is allocated memory, you have read,write,execute access
//place your code here

push rax//store rax.Then use rax as a temp register. Don't forget to restore later :)
lea rax,[rdx+rcx*2]//because original code is 'movzx edx,word ptr [rdx+rcx*2]', we should get current scanning memory,it's currently rdx+rcx*2

cmp rax,[addressThatHoldsTheMoudleBase]//Boundary check
jb originalcode//if below , means it's not inside

cmp rax,[addressThatHoldsTheModuleEnd]//Boundary check
ja originalcode//if above , means it's not inside

//still inside the module. So we need to adjust it to our "copy code"
sub rax,[addressThatHoldsTheMoudleBase] //get the current offset
add rax,[addressThatHoldsTheCopyBase]//get the address of the "copy code"

//do the original code with the new address
movzx edx,word ptr [rax]//just use rax which points our "copy code",this code just imitates the original code 'movzx edx,word ptr [rdx+rcx*2]'
xor edx,[rbp-1C]//Don't forget to write this according to original code.
jmp exit


originalcode:
movzx edx,word ptr [rdx+rcx*2]
xor edx,[rbp-1C]

exit:
pop rax//Don't forget to restore RAX :)
jmp returnhere

"gtutorial-x86_64.exe"+3B9DC:
jmp newmem
nop 2

returnhere:

关于label,它只是一个标签,可以更方便的标识一段代码,是为了能更方便的实现跳转等。

接下来只需要复制粘贴,稍作修改,就能适配其他两个位置了。

需要修改的地方有

1
2
所有label的位置
以及jmp exit 上方那一句,那一句要根据original code来

0x03 完整脚本

CE脚本

因为是CT脚本,应该是可以保证这些操作的原子性?

不过可以确定的是,这些操作应该是可以在极短时间内(小于一个CRC检测周期与CRC检测间隔)完成,经过多次实验,并不会在脚本执行时导致检测未通过。

另外,顺序是按照第三部分、第二部分、第一部分来的,这也是由于CE模板先添加了第一部分,其次才添加了第二第三部分的缘故,有FILO的味道了。

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
[ENABLE]

//first make sure we have a copy of the target module
{$lua}
if addressOfCopy==nil then
addressOfCopy=copyMemory(getAddress(process),getModuleSize(process))
end
{$asm}

alloc(addressThatHoldsTheMoudleBase,8)
alloc(addressThatHoldsTheModuleEnd,8)
alloc(addressThatHoldsTheCopyBase,8)

addressThatHoldsTheMoudleBase:
dq $process //fill the address with the address of the module base

addressThatHoldsTheModuleEnd:
dq $process+getModuleSize(process)//works, otherwise we can use lua to fill this in

addressThatHoldsTheCopyBase://
dq $addressOfCopy


//code from here to '[DISABLE]' will be used to enable the cheat


//below 4 lines are automatically generated by CE
alloc(newmem3,2048,"gtutorial-x86_64.exe"+3BABC)
label(originalcode3)
label(returnhere3)
label(exit3)


newmem3: //this is allocated memory, you have read,write,execute access
//place your code here

push rax//store rax.Then use rax as a temp register. Don't forget to restore later :)
lea rax,[rdx+rcx*2]//because original code is 'movzx edx,word ptr [rdx+rcx*2]', we should get current scanning memory,it's currently rdx+rcx*2

cmp rax,[addressThatHoldsTheMoudleBase]//Boundary check
jb originalcode3//if below , means it's not inside

cmp rax,[addressThatHoldsTheModuleEnd]//Boundary check
ja originalcode3//if above , means it's not inside

//still inside the module. So we need to adjust it to our "copy code"
sub rax,[addressThatHoldsTheMoudleBase] //get the current offset
add rax,[addressThatHoldsTheCopyBase]//get the address of the "copy code"

//do the original code with the new address
movzx edx,word ptr [rax]//just use rax which points our "copy code",this code just imitates the original code 'movzx edx,word ptr [rdx+rcx*2]'
xor edx,[rbp-1C]//Don't forget to write this according to original code.
jmp exit3


originalcode3:
movzx edx,word ptr [rdx+rcx*2]
xor edx,[rbp-1C]

exit3:
pop rax //remember we pushed RAX?
jmp returnhere3

"gtutorial-x86_64.exe"+3BABC:
jmp newmem3
nop 2
returnhere3:



//below 4 lines are automatically generated by CE
alloc(newmem2,2048,"gtutorial-x86_64.exe"+3BA4C)
label(originalcode2)
label(returnhere2)
label(exit2)

newmem2: //this is allocated memory, you have read,write,execute access
//place your code here

push rax//store rax.Then use rax as a temp register. Don't forget to restore later :)
lea rax,[rdx+rcx*2]//because original code is 'movzx edx,word ptr [rdx+rcx*2]', we should get current scanning memory,it's currently rdx+rcx*2

cmp rax,[addressThatHoldsTheMoudleBase]//Boundary check
jb originalcode2//if below , means it's not inside

cmp rax,[addressThatHoldsTheModuleEnd]//Boundary check
ja originalcode2//if above , means it's not inside

//still inside the module. So we need to adjust it to our "copy code"
sub rax,[addressThatHoldsTheMoudleBase] //get the current offset
add rax,[addressThatHoldsTheCopyBase]//get the address of the "copy code"

//do the original code with the new address
movzx edx,word ptr [rax]//just use rax which points our "copy code",this code just imitates the original code 'movzx edx,word ptr [rdx+rcx*2]'
add edx,[rbp-1C]//Don't forget to write this according to original code.
jmp exit2

originalcode2:
movzx edx,word ptr [rdx+rcx*2]
add edx,[rbp-1C]

exit2:
pop rax
jmp returnhere2

"gtutorial-x86_64.exe"+3BA4C:
jmp newmem2
nop 2
returnhere2:


//below 4 lines are automatically generated by CE
alloc(newmem,2048,"gtutorial-x86_64.exe"+3B9DC)
label(originalcode)
label(returnhere)
label(exit)

newmem: //this is allocated memory, you have read,write,execute access
//place your code here

push rax//store rax.Then use rax as a temp register. Don't forget to restore later :)
lea rax,[rdx+rcx*2]//because original code is 'movzx edx,word ptr [rdx+rcx*2]', we should get current scanning memory,it's currently rdx+rcx*2

cmp rax,[addressThatHoldsTheMoudleBase]//Boundary check
jb originalcode//if below , means it's not inside

cmp rax,[addressThatHoldsTheModuleEnd]//Boundary check
ja originalcode//if above , means it's not inside

//still inside the module. So we need to adjust it to our "copy code"
sub rax,[addressThatHoldsTheMoudleBase] //get the current offset
add rax,[addressThatHoldsTheCopyBase]//get the address of the "copy code"

//do the original code with the new address
movzx edx,word ptr [rax]//just use rax which points our "copy code",this code just imitates the original code 'movzx edx,word ptr [rdx+rcx*2]'
xor edx,[rbp-1C]//Don't forget to write this according to original code.
jmp exit


originalcode:
movzx edx,word ptr [rdx+rcx*2]
xor edx,[rbp-1C]

exit:
pop rax//Don't forget to restore RAX :)
jmp returnhere

"gtutorial-x86_64.exe"+3B9DC:
jmp newmem
nop 2
returnhere:


[DISABLE]
//code from here till the end of the code will be used to disable the cheat
dealloc(newmem3)
"gtutorial-x86_64.exe"+3BABC:
movzx edx,word ptr [rdx+rcx*2]
xor edx,[rbp-1C]
//Alt: db 0F B7 14 4A 33 55 E4
dealloc(newmem2)
"gtutorial-x86_64.exe"+3BA4C:
movzx edx,word ptr [rdx+rcx*2]
add edx,[rbp-1C]
//Alt: db 0F B7 14 4A 03 55 E4
dealloc(newmem)
"gtutorial-x86_64.exe"+3B9DC:
movzx edx,word ptr [rdx+rcx*2]
xor edx,[rbp-1C]
//Alt: db 0F B7 14 4A 33 55 E4

0x04 写在最后

应该有一年多没有更博客了

一年内主要在备战考研,当然也在摸鱼

也算是运气不好吧,未能一战成硕

二战也未必是一件坏事,能让自己改掉一些毛病比如拖延症

在写这篇博客的时候,我确实感受到了学习知识然后分享知识的这种Input then output的快乐

我也有太多的东西想去学习

以后的日子里,我也会勤更一下博客,少摸鱼

二战,好好努力,愿不负自己的努力!

-------------至此本文结束感谢您的阅读-------------
如果觉得这篇文章对您有用,请随意打赏。 (๑•⌄•๑) 您的支持将鼓励我继续创作!