多平台多角度分析XXX注册算法
本帖最后由 zjh16529 于 2019-6-6 18:25 编辑Emmm……先悄咪咪说一句,我想求个优秀或者精华,增加一点积分,提升一个用户组欸。版主满足我的愿望吧!
本次注册算法分析的过程需要用到dnSpy、IDA、UltraEdit,涉及一些密码学的知识。
注册算法可用于Windows,Linux,Mac三个平台。
过程中用到的附件和Keygen在此:
Reflector 3 这个软件是 Squirrels LLC 开发的一款屏幕镜像工具。它自己的软件介绍如下:
> Reflector 3 for Mac 可以将您的手机,平板电脑或计算机镜像到大屏幕上,无需连线或复杂的设置。从手掌中呈现,教导或娱乐。分享您的设备屏幕,反光板比以往更容易。
>
> AirPlay,Google Cast和Miracast
> Reflector将Mac,Windows和Android设备转换为AirPlay,Google Cast或Miracast接收器。使用Reflector在更大的屏幕上无线显示和录制iPhone,iPad,Chromebook,Android设备和Windows平板电脑。
我原本只是想逆向一下它实现AirPlay的过程,没想到这个软件还需要注册……那我就顺便把它注册的算法分析了吧!
废话不多说来了……那我们上手吧!
程序的下载地址 (https://www.airsquirrels.com/reflector/try)
首先从主程序的注册界面入手,主程序一看是.Net,直接拖入dnSpy。居然混淆都没有……简直太顺畅了。
输入注册码的窗口叫做TrialWindow,dnSpy里面一下子就找到了。里面的代码没什么可说的,就是监测输入框里的内容,注册码输完后调用SetFromKeyValidationState
可以看到,关键的验证步骤就在 ReflectorLicense.CheckLicenseIsValid() 我们跟过去看看。
返回值是一个bool,true代表注册成功,false代表注册失败。网上流传的破解版本就是简单粗暴地把这个函数改成
```
internal static bool? CheckLicenseIsValid(string license)
{
return new bool?(true);
}
```
就爆破成功了……网上能找到的破解版本几乎都是这样爆破的。没有暗装、没有功能限制,真的就完全破解了。
然鹅,主程序是有数字签名的,此种爆破之后数字签名就被破坏了。这对于我这种对软件有洁癖的人来说简直不能忍!!!
那行吧,我们继续往下研究。
看看ReflectorNativeInstance.ValidateLicenseKey()是从哪冒出来的
现在这个函数已经不在主程序里了,在一个叫做ReflectorNative.dll的文件里。这个dll没有数字签名,爆破在这里其实是可以的。
关键的的代码是
```
if (b != 0 && *(byte*)(ptr2 + 28L / (long)sizeof(SquirrelsLicense)) == 0 && *(byte*)(ptr2 + 29L / (long)sizeof(SquirrelsLicense)) == 3)
{
this.callback.RFServerWarapperLicenseClearErrorMessage();
return true;
}
```
再次然鹅…这个dll不是单纯的.Net程序集,而是一个混合程序集。因为里面有一些非托管的代码(我不知道是怎么做到的)。所以不能用直接用dnSpy修改了(重新编译会导致一些奇怪的问题)。
先把dnSpy切换成IL模式,如图
关键部分的代码就是我现在选中的那一块
我来解释一下此处的IL指令
```
/* 0x00457123 02 */ IL_0073: ldarg.0 /*将this入栈*/
/* 0x00457124 7BE6160004 */ IL_0074: ldfld class RFServerWrapper.IRFServerWrapperCallback RFServerWrapper.RFServerWrapper::callback /*在压入的this中找callback*/
/* 0x00457129 6F88070006 */ IL_0079: callvirtinstance void RFServerWrapper.IRFServerWrapperCallback::RFServerWarapperLicenseClearErrorMessage() /*从callback里调用RFServerWarapperLicenseClearErrorMessage()*/
/* 0x0045712E 17 */ IL_007E: ldc.i4.1 /*入栈true*/
/* 0x0045712F 28A300000A */ IL_007F: call valuetype System.Nullable`1<!0> valuetype System.Nullable`1<bool>::op_Implicit(!0) /*将bool的true转换为bool?的true*/
/* 0x00457134 2A */ IL_0084: ret /*返回转换后的true*/
```
然后用UltraEdit打开ReflectorNative.dll,IL最前面的地址找到修改的位置,函数其余部分用0x00(nop)填充。
图中选中部分就是函数原本,大部分都被填充了nop,不是nop的部分就是关键部分的代码的IL了。现在函数变为
```
public bool? ValidateLicenseKey(string license)
{
this.callback.RFServerWarapperLicenseClearErrorMessage();
return true;
}
```
这种不破坏数字签名的爆破方法,已经算是比较好的破解了。只是希望注册的话,到此就可以结束了。
不满足于此,就只能跟下去死磕注册码算法了!
前方高能预警!!!
从此之后我们分析的就不仅是.Net程序的IL了,而是本机Native代码!
IDA都拿出来准备好啦!!!
从之前dnSpy里看到的ValidateLicenseKey继续分析
此处的验证调用
```
SquirrelsLicense* ptr2 = <Module>.RFValidateLicenseKey(this.server, ptr);
```
这个函数就不是托管代码了,只有如下的定义
```
// Token: 0x060006B0 RID: 1712 RVA: 0x00414A70 File Offset: 0x00413E70
internal unsafe static extern SquirrelsLicense* RFValidateLicenseKey(_RFServer*, sbyte*);
```
那么函数的主体部分在哪里呢?
~~我对于ReflectorNative.dll这样的混合程序集并不了解,找了半天没有找到。~~
*我现在找到方法了,将ReflectorNative拖入IDA,IDA询问LoadFile方式的时候选择PE而不是.NetAssembly*
*然后在Functions窗口搜索函数的RVA(0x00414A70),前面的00直接忽略,输入414A70,然后就能看到函数了*
*Windows版本反编译之后函数名称不直观,我也就不改我后面的写法了……我还是按照Mac版本分析算法*
正当我没有头绪的时候,我突然想起Reflector还有Mac版本呢!
在Mac版本上不可能用到.Net的混合程序集,可能全部都是Native代码!
于是立即就把Mac版本下载下来,拖进IDA一探究竟。
Mac程序拖进IDA,函数名称居然都看得到!!!真的是把我震惊到了……
无论如何我们算是找到了RFValidateLicenseKey的主体部分!
Mac汇编不熟悉,那就不硬磕汇编了,直接F5大法,改改变量名称和类型,可读性还蛮高的
咦?居然有字符串欸!
```
-----BEGIN PUBLIC KEY-----
MDIwDQYJKoZIhvcNAQEBBQADIQAwHgIXDoK2SPT7JNSYxgO1oJwxO3dIZzYSICcC
AwEAAQ ==
-----END PUBLIC KEY-----
```
这玩意是个公钥,看来注册码用了非对称加密算法。
公钥使用Asn1编码保存,那么就Asn1Editor伺候。
可以看出这是一个RSA的公钥,简单讲讲RSA:
```
p,q为两个素数
n=p*q
e为与(p-1)(q-1)互质的任意数字
d为e^(-1) mod (p-1)(q-1)
(n,e)组成公钥
(n,d)组成私钥
```
看不懂也没关系,不影响分析算法。
大家看到RSA不要害怕,这么小的modulus分分钟就分解了好嘛……
```
从Asn1Editor得知
n=1389838782556614468794064557539356473141171394217910311
e=65537
```
去 http://factordb.com ,把n贴进去p和q直接秒解了
```
得到
p=1152497169371634301857542543
q=1205936829601398332841241577
n=1389838782556614468794064557539356473141171394217910311
计算后知道
e=65537
d=295561028312121944116802381945412767177035108281094065
```
至此,公钥对应的私钥就算得到了(后面会用到的)
来来来,继续上面的分析
函数返回的结果是v3,又v3=v2,v2=SquirrelsLicenseDecode(,)
其它语句不管他了,可能是类似于callback一样的东西。
我们跟进到SquirrelsLicenseDecode
挑些关键步骤解释一下
```
L22:
p_decoded = base32decode(license, &a2); //base32解码license,decode会修改a2为解码后的长度
......
L26:
if ( a2 != 180 ) //如果解码长度不是180bit,就返回NULL
goto LABEL_9;
......
L29:
p_decoded_1 |= *p_decoded_1 >> 4; //把p_decoded的高4位放到p_decoded的低4位
......
L32-43
调用openssl函数读取公钥
......
L46-55
公钥解密,结果存到to中
......
L56-74
从解密内容构建返回值。
```
大框架先理解了,然后我们看细节 base32decode
伪代码就不贴了,这个函数其实就是把encoded理解为一串5bit的字节,然后把5bit连起来之后按照8bit读取字节。
注册码长度36,base32编码,即一个字符表示5bit,因此注册码总共36*5bit=180bit。
180bit=180/8byte=22.5byte,decode函数从前往后写,因此base32decode之后的内容占用23字节,最后一个字节(p_decoded)只有高4位
```
举例:
注册码:KKKKKK-KKKKKK-KKKKKK-KKKKKK-KKKKKK-KKKKKK
比特(二进制5bit): 10001 10001 10001 10001 10001 10001 - 10001 10001 10001 10001 10001 10001 - 10001 10001 10001 10001 10001 10001 - 10001 10001 10001 10001 10001 10001 - 10001 10001 10001 10001 10001 10001 - 10001 10001 10001 10001 10001 10001
比特(二进制8bit):10001100 01100011 00011000 11000110 00110001 10001100 01100011 00011000 11000110 00110001 10001100 01100011 00011000 11000110 00110001 10001100 01100011 00011000 11000110 00110001 10001100 01100011 0001????
解码字节:0x8C,0x63,0x18,0xC6,0x31,0x8C,0x63,0x18,0xC6,0x31,0x8C,0x63,0x18,0xC6,0x31,0x8C,0x63,0x18,0xC6,0x31,0x8C,0x63,0x18,0x10
```
8bit中最后一组4个问号就是空余的4bit(当做4个0),此时空余的4bit在array的低4位
在下一步骤RSA解密时,因为modulus是180bit,所以密文也一定是180bit,此时空余的4bit在array的高4位。因此需要29行高位放到低位的过程
换过之后的密文变为
```
解码字节:0x8C,0x63,0x18,0xC6,0x31,0x8C,0x63,0x18,0xC6,0x31,0x8C,0x63,0x18,0xC6,0x31,0x8C,0x63,0x18,0xC6,0x31,0x8C,0x63,0x18,0x10
密文字节:0x0C,0x63,0x18,0xC6,0x31,0x8C,0x63,0x18,0xC6,0x31,0x8C,0x63,0x18,0xC6,0x31,0x8C,0x63,0x18,0xC6,0x31,0x8C,0x63,0x18,0x18
```
这一步其实很巧妙啊!我当时没能理解,浪费了不少时间呢。
然后就是调用openssl用PKCS1Encoding的RSA解密密文字节。
密文是23字节,PKCS1Encoding填充11个字节,因此解密后的长度为23-11=12字节。
最后构建的步骤其实还算简单,但是要能理解BYTE,WORD,DWORD等等的长度,结合之前dnSpy中反编译的内容分析每一字节的作用
理清思路之后总结如下
```
Decrypted:(解密后的12字节)
00 01 02 03 04 05 06 07 08 09 11
License:(返回值32字节)
00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
16-17 TrialDays(=0)=((Decrypted>>2)&0x3C) => Decrypted=0x00
24 Os(&2!=0)=Decrypted&0x1F => Decrypted=0x1F
28 Product(=0)=Decrypted>>6 => Decrypted=0x00-3F
29 Version(=3)=Decrypted>>4 => Decrypted=0x30-0x3F
```
最后四行,开头表示返回License中的字节位,空格后是作用/含义,括号内是期望值,"=>"箭头推出的就是对解密后12字节对的要求。
哈,只要四个字节符合要求就行了……看了来要求还是很少的嘛!
好了,现在我们可以开始准备生成注册码了!
伪代码如下
```
byte[] Decrypted = new byte[] {0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00};
/*符合要求的一个解密12字节数组*/
byte[] Decoded = Pkcs1Encoding.RSA.Encrypt(Decrypted, privateKey);
/*用RSA私钥Pkcs1Encoding加密12字节;私钥已在上文中求得*/
Decoded |= (byte)(Decoded << 4);
/*低位高位互换(空余4bit)*/
string Serial = Base32.Encode(Decoded);
/*编码为注册码*/
```
根据伪代码的Decrypted生成的注册码为 5RY7X52DQUYR4H58BVMYRSNXV3AG9BSNNE7P
注册码可以用于Win,Mac,Linux三平台上的Reflector 3
我用C#写了一个Keygen,在附件中有的,需要自己编译!
附件和Keygen在此:
Emmm为什么附件显示不出来……在文章开头那里有!
大家帮我评评分吧!
好久没看过这样的文章了。
dnSpy可以十六进制编辑程序集的,这样
点我箭头指着的地方,dnSpy会跳转到对应位置
会显示出是哪一条指令 xxxallen 发表于 2019-5-4 11:44
请问下大佬,32位win7 vs2015编译后出错,是平台的问题,还是其它,谢谢
错误提示是什么?报了什么错?
可能是.NetSDK版本的问题。我使用2017写的,可能你需要给项目降级到2015自带的SDK 一针见血,言简意赅,很好,666! 谢谢分享……写得好生动……。 谢谢楼主,希望能多多分享,写的很好 樓主很強!! {:1_893:} 大神牛逼,图文并茂!!
谢谢楼主,希望能多多分享,写的很好 学习了,太高深,谢谢分享。 wwh1004 发表于 2019-5-3 11:05
好久没看过这样的文章了。
dnSpy可以十六进制编辑程序集的,这样
wow!这个功能之前没发现欸……
学习了,谢谢!