free Q.744303 (只接受志同道合的技术研究伙伴,验证说明“看雪”,无聊人士勿扰)
潜水很多年,第一次发技术贴,不会排版还请版主帮忙。。
下载了某游戏的客户端,目的就是练习一下算法逆向,并且目标不是汇编代码,是C++代码。
研究了两天,有可能有疏漏,还请大侠批评指教!
用到的调试工具只有OllyDBG。
一、获取与运算封包Key
获取很简单,直接连接服务器,会收到服务器返回的包含了SendKey和RecvKey的钥匙包。
经过一定的算法,得到两个Key,在后续的游戏中,将使用这两个Key进行加密通信。
封包[0][1]为封包长度,不进行加解密操作。
封包:
2C 00 20 00 00 0A 00 0A 00 BD AD F2 A5 D5 75 61 6E 78 75 0B 73 D6 7C 38 38 38 40 31 36 33 2E 63 6F 6D 00 00 00 00 00 00 00 00 00 00
下断点在 AD 这个字节上,运行,会断在访问的地址,如下汇编代码:
mov edx, dword ptr [esi+11] //取得 0B 73 D6 7C
mov esi, dword ptr [esi+8] //取得 AD F2 A5 D5
mov ecx, edx //第二个KEY开始运算
mov ebx, edx
xor ecx, 6D23CF
sub ecx, 6D2399
xor edx, FFFFFFCF
not ecx
add edx, 67
xor ebx, 2E6D23CF
and ecx, 0FFFF00
not edx
sub ebx, 2E6D2399
shl edx, 18
not ebx
shr ebx, 18
or ecx, ebx
or ecx, edx
push ebp
push ecx
mov edx, esi //第一个KEY开始运算
mov ecx, esi
xor edx, 6D23CF
xor ecx, 2E6D23CF
sub edx, 6D2399
sub ecx, 2E6D2399
xor esi, FFFFFFCF
not edx
not ecx
add esi, 67
and edx, 0FFFF00
shr ecx, 18
or edx, ecx
not esi
shl esi, 18
or edx, esi
经过逆向,逆出C++代码如下:
DWORD __stdcall GetRecvKey(DWORD PacketRecvKey)
{
DWORD sKey = 0;
sKey = (((~((PacketRecvKey ^ 0x006D23CF) - 0x006D2399)) & 0x0FFFF00)
| (~((PacketRecvKey ^ 0x2E6D23CF) - 0x2E6D2399)) >> 0x18)
| ((~((PacketRecvKey ^ 0xFFFFFFCF) + 0x67)) << 0x18);
return sKey;
}
DWORD __stdcall GetSendKey(DWORD PacketSendKey)
{
DWORD sKey = 0;
sKey = (((~((PacketSendKey ^ 0x6D23CF) - 0x6D2399)) & 0xffff00)
| ((~((PacketSendKey ^ 0x2E6D23CF) - 0x2E6D2399)) >> 0x18))
| ((~((PacketSendKey ^ 0xFFFFFFCF) + 0x67)) << 0x18);
return sKey ;
}
这两个KEY逆完后,继续逆封包加解密算法,中间碰到一个包,经过分析是心跳包,以07 00 01开头。
其实只是以01开头,刚才说过第一位和第二位是长度,所以07 00 是封包长度而已。心跳包是提交时间的。
这个封包的组成是这样的:
07 00 01 (XX XX XX XX)_time32()
蓝色是长度,红色是封包类型,后面的四位由MSVCR80.DLL里面的_time32() 函数得到结果。
在VS 2005+ 里可以直接使用这个函数,头文件:time.h。
通过类似的断点方法,找到解密用的函数,汇编如下:
push ebx
push ebp
mov ebp, dword ptr [esp+14]
mov ecx, dword ptr [ebp]
push esi
mov esi, dword ptr [esp+10]
mov ebx, esi
and ebx, 3 //求余数,下面以DWORD(4Byte)操作,如果有余数还要另行操作
shr esi, 2
push edi
mov edi, dword ptr [esp+18]
je short 004D5605
lea ecx, dword ptr [ecx] //取得传入的经过运算的KEY
sub esi, 1
lea eax, dword ptr [ecx+esi]
xor edx, edx
div dword ptr [51FEDC]
add edi, 4
mov ecx, dword ptr [edx*4+51A620] //进行完上面的运算,进行异或,注意是以DWORD(4字节)为单位的
add ecx, 2E6D23C1
xor dword ptr [edi-4], ecx
test esi, esi //全完成了吗?
ja short 004D55E0 //没有全完成的话继续
xor edx, edx
mov eax, ebx
div dword ptr [51FEDC]
xor ecx, dword ptr [edx*4+51A620]
test ebx, ebx //有余数吗?
jbe short 004D562F //没有的话就不继续处理剩下的字节了
lea ebx, dword ptr [ebx]
xor byte ptr [edi], cl
sub ebx, 1
add edi, 1
shr ecx, 8
test ebx, ebx //余下的几个字节处理完了吗?
ja short 004D5620 //没处理完继续
mov eax, dword ptr [ebp]
mov ecx, eax
shl ecx, 5
sub ecx, eax
pop edi
add ecx, 8088405
pop esi
mov dword ptr [ebp], ecx
pop ebp
mov eax, 1
pop ebx
retn
经过分析,逆出的C++代码如下:
VOID __stdcall EncryptionPacket(DWORD *enbyte,int len,DWORD PacketKey)
{
int NextLen = len % 4,PacketLength = len / 4;
int i=0,j=0;
DWORD NextKey = 0,AddNum = 0;
BYTE *NewPacketAddr = (BYTE *)enbyte;
AddNum = PacketKey;
while (PacketLength--)
{
AddNum += PacketLength;
NextKey = Pack_Tabel[AddNum % 0x162F] + 0x2E6D23C1;
enbyte[i] = enbyte[i] ^ NextKey;
AddNum = NextKey;
i++;
}
j=i*4;
NextKey ^= Pack_Tabel[NextLen % 0x162F];
if (NextLen != 0)
{
while (NextLen--)
{
(BYTE)NewPacketAddr[j] = (BYTE)NewPacketAddr[j] ^ (BYTE)NextKey;
NextKey = NextKey >> 8;
j++;
}
}
}
至此封包算法已经逆完了,经过测试,是正确的:
取得服务端封包验证KEY...
开始发送帐号密码...
验证正常...
取得人物信息了...
帐号:h3389@vip.qq.com
人物:凌行恶
性别:男
等级:4
其实直接套汇编速度非常快,解了此游戏算法的人也不少,这里只是作为真正逆向代码的练习方法罢了。
接下来说一下是如何将算法逆成C++代码的,上面只给出了结果,其实我在个人空间里面分了两个贴子来说的,在这里直接就贴在下面给各位朋友参考,有不妥的地方还请大家批评指教:
寻找修改变量的代码,一直找到计算完毕,废话不多说,拿昨天的代码再来继续做个示例,看看我是如何把它逆成C++代码的:
(一)使用变量修改追踪找出单独的计算代码:
mov edx, dword ptr [esi+11] //取得 0B 73 D6 7C
mov esi, dword ptr [esi+8] //取得 AD F2 A5 D5
mov ecx, edx //第二个KEY开始运算
mov ebx, edx
xor ecx, 6D23CF
sub ecx, 6D2399
xor edx, FFFFFFCF
not ecx
add edx, 67
xor ebx, 2E6D23CF
and ecx, 0FFFF00
not edx
sub ebx, 2E6D2399
shl edx, 18
not ebx
shr ebx, 18
or ecx, ebx
or ecx, edx
push ebp
push ecx
上面是recvkey的计算方法,很容易可以看得出,第一行取得了recvkey即将运算的封包里的代码,第二行取得的是sendkey的,所以第二行与目前这段程序没有任何关联,删除它就可以了。
继续下来分析代码:
mov edx, dword ptr [esi+11] //取得 0B 73 D6 7C
这一行把取得的KEY放到了EDX里面,继续向下看:
mov ecx, edx
mov ebx, edx
可以看到 ecx和ebx都等于 edx了,也就是复制了两个变量,可能要进行其它运算了
先来搞定EDX,看看它最终会怎么样,其它的都不管,向下寻找修改了EDX值的语句:
xor edx, FFFFFFCF
修改了EDX之后,没有把EDX换到其它寄存器,还是EDX,那再向下找修改了EDX的语句:
add edx, 67
看样子还要继续找……
最终,形成的代码如下:
mov edx, dword ptr [esi+11]
xor edx, FFFFFFCF
add edx, 67
not edx
shl edx, 18
再来看ECX和EBX,最终ECX形成代码:
mov ecx, edx
xor ecx, 6D23CF
sub ecx, 6D2399
not ecx
and ecx, 0FFFF00
寻找EBX的相关代码:
xor ebx, 2E6D23CF
sub ebx, 2E6D2399
not ebx
shr ebx, 18
再继续向下看,最后做了什么操作:
or ecx, ebx
or ecx, edx
看样子ECX,EBX,EDX这三个变量,最终形成了ECX的一个结果。
(二)寻找并使用等同运算符
先从第一段程序开始,我们先做一个函数,以便传入待运算的KEY,返回值是DWORD,以便返回一个运算好的KEY,
选一门能胜任并熟悉的语言,这里以C++代码做为示例,红色的是汇编,等于号代表左右代码执行的结果相等,蓝色的是翻译之后的代码:
DWORD GetSendKey(DWORD SendKey)
{
}
做好之后,向里面开始写代码:
mov edx, dword ptr [esi+11] = DWORD sKey = SendKey;
上面这句,等同于将一个变量读入另一个变量,这里我们可以先声明一个变量:
接下来用这个sKey进行运算,经过分析,其实这句可以省掉,因为可以直接用实参做运算。
下面这一句,我们看到了,将我们的变量进行异或运算,C++里异或运算符是 ^,其它语言跟据定义和语法不同自行改变。
xor edx, FFFFFFCF = sKey = sKey ^ 0xFFFFFFCF;
好,接下来再看下面:
add edx, 67 = sKey = sKey + 0x67;
not edx = sKey = ~sKey;
shl edx, 18 = sKey = sKey << 0x18;
最终,我们形成了代码:
DWORD GetSendKey(DWORD SendKey)
{
DWORD sKey = SendKey;
sKey = sKey ^ 0xFFFFFFCF;
sKey = sKey + 0x67;
sKey = ~sKey;
sKey = sKey << 0x18;
}
其实可以看出,前一个运算的返回值是下一个运算要用的运算值,我们再来简化它,让它更接近原始代码:
把这句删掉,直接用实参操作:
DWORD sKey = SendKey;
变成了:
SendKey = SendKey ^ 0xFFFFFFCF;
SendKey = SendKey - 0x67;
SendKey = ~SendKey;
SendKey = SendKey << 0x18;
接下来第一句,我们直接取它的运算结果:
(SendKey ^ 0xFFFFFFCF)
第二句直接用第一句的结果,再取它的运算结果:
((SendKey ^ 0xFFFFFFCF) + 0x67)
第三句直接用第二句的运算结果,并取它的运算结果:
(~((SendKey ^ 0xFFFFFFCF) + 0x67))
第四句用第三句的运算结果:
((~((SendKey ^ 0xFFFFFFCF) + 0x67)) << 0x18);
这样,我们把所有的语句整合成了一句:
再定义一个标识返回值的变量,最终,我们形成了代码:
DWORD GetSendKey(DWORD SendKey)
{
DWORD retKey = ((~((SendKey ^ 0xFFFFFFCF) + 0x67)) << 0x18);
}
其它两个(EBX,ECX)按照这种方法,加上上面生成的,最终生成三行代码:
((~((SendKey ^ 0xFFFFFFCF) + 0x67)) << 0x18) //EDX
((~((PacketSendKey ^ 0x2E6D23CF) - 0x2E6D2399)) >> 0x18)) //EBX
(((~((PacketSendKey ^ 0x6D23CF) - 0x6D2399)) & 0xffff00) //ECX
再看最后两行代码:
or ecx, ebx
or ecx, edx
其实就是把最终运算好的ecx代码和ebx代码 或运算之后,再与运算好的edx 代码进行或运算,先来第一步:
((((~((PacketSendKey ^ 0x6D23CF) - 0x6D2399)) & 0xffff00)
| ((~((PacketSendKey ^ 0x2E6D23CF) - 0x2E6D2399)) >> 0x18)))
这样我们就得到了ecx | ebx
然后再把它们的结果与edx的运算结果再进行或运算:
((((~((PacketSendKey ^ 0x6D23CF) - 0x6D2399)) & 0xffff00)
| ((~((PacketSendKey ^ 0x2E6D23CF) - 0x2E6D2399)) >> 0x18)))
| ((~((SendKey ^ 0xFFFFFFCF) + 0x67)) << 0x18)
最终,我们得到了这个函数的全部代码:
DWORD GetSendKey(DWORD SendKey)
{
DWORD retKey = ((((~((PacketSendKey ^ 0x6D23CF) - 0x6D2399)) & 0xffff00)
| ((~((PacketSendKey ^ 0x2E6D23CF) - 0x2E6D2399)) >> 0x18)))
| ((~((SendKey ^ 0xFFFFFFCF) + 0x67)) << 0x18);
return retKey;
}
其它的代码也用类似这种方法逆出来整合到一起的,就不再赘述了。
希望大家看过之后,可以提出批评意见,或是指出不合理的地方,或是有更好的方法,都可以一起进行讨论。
声明:本人完全不知道这款游戏是什么游戏,也完全不知道这款游戏是哪家公司运营的,以上内容都是我作梦的时候说的梦话,我老婆记录下来的。
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课