某辅助软件注册逻辑分析
本次分析的软件样本是来自吾爱的一个求助帖,原贴现已锁帖。https://static.getgetai.com/img/161170b8bbca46939bff73aa5ff9adb9/20200928075332627.png
下载作者提供的样本打开后是这个样子的界面。
https://static.getgetai.com/img/161170b8bbca46939bff73aa5ff9adb9/20200928075706702.png
左侧为软件的主界面,可以看到一个机器码未通过的字样,所以需要通过逆向分析实现让验证通过。
先查壳,如右图所示。
基础差一点儿的同学可以先去看我写的《.NET零基础逆向教程》
没有壳,直接dnspy载入即可。
https://static.52pojie.cn/static/image/hrline/5.gif
机器码的生成:
因为打开软件后在我们没有干涉的情况下,该软件直接读出了一个机器码,还判断出了这个机器码是错误的。
因此能想到在窗口加载后,会有一个生成注册码并验证的逻辑。
所以直接静态分析,从入口点跟入。
https://static.getgetai.com/img/161170b8bbca46939bff73aa5ff9adb9/20200928080941120.png
入口点是Main方法,中规中矩的窗口程序。
https://static.getgetai.com/img/161170b8bbca46939bff73aa5ff9adb9/20200928081143275.png
Main中实例化了一个名叫FFToolsMain的窗口对象,也就是这个软件的主界面。
跟进去,看到实例化中也没有特别之处,载入组件,然后做一些初始化设置。
https://static.getgetai.com/img/161170b8bbca46939bff73aa5ff9adb9/20200928081317236.png
在左侧的列表树中找到这个窗口的加载事件。
https://static.getgetai.com/img/161170b8bbca46939bff73aa5ff9adb9/20200928081428877.png
代码如下:
private void FFToolsMain_Load(object sender, EventArgs e)
{
this.getCDValue();
new Thread(new ThreadStart(this.checkzuce)).Start();
}
可以看到这个方法里首先通过getCDValue方法去获取机器码,然后通过线程调用了checkzuce方法来判断是否注册了。
先跟进getCDValue方法看一下机器码是怎么获取的。
string text = "";
foreach(ManagementBaseObject managementBaseObject in new ManagementClass("win32_Processor").GetInstances())
{
ManagementObject managementObject = (ManagementObject) managementBaseObject;
try
{
text = managementObject.Properties["Processorid"].Value.ToString();
break;
}
catch
{}
}
if(text == "")
{
this.showlog("获取机器码失败.");
return;
}
FFToolsMain.CDValue += text;
text = "";
foreach(ManagementBaseObject managementBaseObject2 in new ManagementClass("Win32_NetworkAdapterConfiguration").GetInstances())
{
ManagementObject managementObject2 = (ManagementObject) managementBaseObject2;
try
{
if((bool) managementObject2["IPEnabled"])
{
text = managementObject2.Properties["MACAddress"].Value.ToString();
text = text.Replace(":", "");
break;
}
}
catch
{}
}
if(text == "")
{
this.showlog("获取机器码失败.");
return;
}
FFToolsMain.CDValue += text;
text = "";
for(int i = 0; i < FFToolsMain.CDValue.Length; i += 2)
{
int num = (int) Convert.ToInt16(FFToolsMain.CDValue.Substring(i, 2), 16);
if(num % 2 == 0)
{
num = num / 16 * 16 + (num % 16 + 9) % 16;
}
else
{
num = num / 16 * 16 + (num % 16 + 7) % 16;
}
text = num.ToString("X2") + text;
}
this.showlog("机器码: " + text);
FFToolsMain.CDValue = text;
在VS里创建一个控制台项目来看看这个获取方式。
稍微改一下代码就行。
namespace FFTools
{
class Program
{
static void Main(string[] args)
{
FFToolsMain.jiqima();
}
}
internal class FFToolsMain
{
public static string CDValue
{
get;
internal set;
}
public static void jiqima()
{
string text = "";
foreach(ManagementBaseObject managementBaseObject in new ManagementClass("win32_Processor").GetInstances())
{
ManagementObject managementObject = (ManagementObject) managementBaseObject;
try
{
text = managementObject.Properties["Processorid"].Value.ToString();
break;
}
catch
{}
}
if(text == "")
{
Console.WriteLine("获取机器码失败.");
return;
}
FFToolsMain.CDValue += text;
text = "";
foreach(ManagementBaseObject managementBaseObject2 in new ManagementClass("Win32_NetworkAdapterConfiguration").GetInstances())
{
ManagementObject managementObject2 = (ManagementObject) managementBaseObject2;
try
{
if((bool) managementObject2["IPEnabled"])
{
text = managementObject2.Properties["MACAddress"].Value.ToString();
text = text.Replace(":", "");
break;
}
}
catch
{}
}
if(text == "")
{
Console.WriteLine("获取机器码失败.");
return;
}
FFToolsMain.CDValue += text;
text = "";
for(int i = 0; i < FFToolsMain.CDValue.Length; i += 2)
{
int num = (int) Convert.ToInt16(FFToolsMain.CDValue.Substring(i, 2), 16);
if(num % 2 == 0)
{
num = num / 16 * 16 + (num % 16 + 9) % 16;
}
else
{
num = num / 16 * 16 + (num % 16 + 7) % 16;
}
text = num.ToString("X2") + text;
}
Console.WriteLine("机器码: " + text);
FFToolsMain.CDValue = text;
Console.ReadLine();
}
}
}
跑起来,可以看到获取到的机器码是一样的。
https://static.getgetai.com/img/161170b8bbca46939bff73aa5ff9adb9/20200928084627329.png
因此这个程序生成机器码的原理就是如上代码,通过网卡的MAC地址,进行了简单的加密处理生成了一个机器码。
https://static.52pojie.cn/static/image/hrline/5.gif
注册验证算法
接下来看注册的验证,在checkzuche,话说作者哪里人,z和zh,c和ch不分嘞……
算法如下:
private void checkzuce()
{
this.注册ToolStripMenuItem.Enabled = false;
string text = "";
ArrayList arrayList;
if(!FFToolsMain.NetValue)
{
arrayList = new ArrayList();
arrayList.AddRange(commdata.RGdata);
for(int i = 0; i < arrayList.Count; i++)
{
string text2 = (string) arrayList;
if(!string.IsNullOrEmpty(text2) && text2 == FFToolsMain.CDValue)
{
this.zctext.Text = "注册版";
this.Text = "KeyboardDriverhelp";
this.zcsjtxt.Text = "内部版本无期限";
this.showlog("机器码认证通过!");
return;
}
}
this.showlog("机器码未通过!");
this.注册ToolStripMenuItem.Enabled = true;
return;
}
arrayList = new ArrayList(FFToolsMain.sqlmainrw.readdb("RegValue", commdata.fnRegValue));
if(arrayList != null && arrayList.Count > 0)
{
text = (string) arrayList;
}
if(text == "")
{
this.showlog("请注册注册!");
this.注册ToolStripMenuItem.Enabled = true;
return;
}
if(this.zhuce(text))
{
this.zctext.Text = "注册版";
this.Text = "FFTools";
return;
}
this.showlog("请注册注册!");
this.注册ToolStripMenuItem.Enabled = true;
}
第一个敏感位置,在于一个循环判断:
arrayList = new ArrayList();
arrayList.AddRange(commdata.RGdata);
for(int i = 0; i < arrayList.Count; i++)
{
string text2 = (string) arrayList;
if(!string.IsNullOrEmpty(text2) && text2 == FFToolsMain.CDValue)
{
this.zctext.Text = "注册版";
this.Text = "KeyboardDriverhelp";
this.zcsjtxt.Text = "内部版本无期限";
this.showlog("机器码认证通过!");
return;
}
}
this.showlog("机器码未通过!");
这段算法是从软件中读出了一些内部的机器码,如果是这些特殊的机器码,则这台电脑就是内部版本。
下个断点就可以看到这些机器码。
https://static.getgetai.com/img/161170b8bbca46939bff73aa5ff9adb9/20200928101921063.png
所以其实可以直接修改把你的机器码变成内置的。
https://static.getgetai.com/img/161170b8bbca46939bff73aa5ff9adb9/20200928102045028.png
第二处敏感位置在if判断处:
if(this.zhuce(text))
{
this.zctext.Text = "注册版";
this.Text = "FFTools";
return;
}
所以只要zhuce这个方法返回ture就行了。
跟进这个方法去看。
private bool zhuce(string dbreg)
{
string text = "";
string text2 = "";
string text3 = "";
string text4 = "";
if(FFToolsMain.CDValue.Length < 28)
{
return false;
}
for(int i = 0; i < FFToolsMain.CDValue.Length / 2; i += 2)
{
text += ((int)(Convert.ToInt16(FFToolsMain.CDValue.Substring(i, 2), 16) ^ Convert.ToInt16(FFToolsMain.CDValue.Substring(FFToolsMain.CDValue.Length - i - 2, 2), 16))).ToString("X2");
}
byte[] array = new MD5CryptoServiceProvider().ComputeHash(Encoding.Default.GetBytes(text));
for(int j = 0; j < array.Length; j++)
{
text2 += array.ToString("X2");
}
if(dbreg.Length < 40)
{
return false;
}
int num = 0;
for(int k = 0; k < dbreg.Length; k++)
{
if(num + 1 < dbreg.Length)
{
text3 += dbreg.Substring(num, 2);
num += 2;
}
if(k % 2 == 0 && num < dbreg.Length)
{
if(text4.Length < 8)
{
text4 += dbreg.Substring(num, 1);
}
num++;
}
if(num == dbreg.Length)
{
k = dbreg.Length;
}
}
if(text4.Length != 8)
{
this.showlog("注册码过期...");
return false;
}
int num2 = this.TDClient();
if(num2 == 0)
{
this.showlog("无法获得数据,稍后再试...");
return false;
}
if(!(text2 == text3))
{
this.showlog("注册码无效");
return false;
}
int num3 = Convert.ToInt32(text4.Substring(0, 2), 16) - 128;
int num4 = Convert.ToInt32(text4.Substring(2, 2), 16) - 128;
int num5 = Convert.ToInt32(text4.Substring(4, 2), 16) - 128;
int num6 = Convert.ToInt32(text4.Substring(6, 2), 16) - 128;
this.showlog(string.Concat(new object[]
{
"有效期至",
num3, "年",
num4, "月",
num5, "日",
num6, "时"
}));
this.zcsjtxt.Text = string.Concat(new object[]
{
"有效期至",
num3, "年",
num4, "月",
num5, "日",
num6, "时"
});
if(num3 * 1000000 + num4 * 10000 + num5 * 100 + num6 > num2)
{
this.showlog("注册码通过");
return true;
}
this.showlog("注册码过期...");
return false;
}
有点儿复杂……
直接返回true就行了。
https://static.getgetai.com/img/161170b8bbca46939bff73aa5ff9adb9/20200928102500634.png
然后注册下看到可以通过。
https://static.getgetai.com/img/161170b8bbca46939bff73aa5ff9adb9/20200928102440716.png
爆破的话就这样就行了。
https://static.52pojie.cn/static/image/hrline/5.gif
分析注册算法
这个算法不难,所以分析一下具体的算法。
string text = "";
string text2 = "";
string text3 = "";
string text4 = "";
if(FFToolsMain.CDValue.Length < 28)
{
return false;
}
for(int i = 0; i < FFToolsMain.CDValue.Length / 2; i += 2)
{
text += ((int)(Convert.ToInt16(FFToolsMain.CDValue.Substring(i, 2), 16) ^ Convert.ToInt16(FFToolsMain.CDValue.Substring(FFToolsMain.CDValue.Length - i - 2, 2), 16))).ToString("X2");
}
一开始定义了4个sring,然后判断了下机器码的位数是不是28位。
接着循环读出了一个text,没啥好说的,照抄就行。
byte[] array = new MD5CryptoServiceProvider().ComputeHash(Encoding.Default.GetBytes(text));
for(int j = 0; j < array.Length; j++)
{
text2 += array.ToString("X2");
}
if(dbreg.Length < 40)
{
return false;
}
往下,md5加密,然后得出了text2,也照抄就行。
到判断这里,有个dbreg,db是数据库,reg是注册,猜想这里大概是从数据库里读取旧的注册码。
看了下这是zhuce方法的参数,返回到调用里下断看一下。
https://static.getgetai.com/img/161170b8bbca46939bff73aa5ff9adb9/20200928104220009.png
很尴尬的是断不下来……
分析一下原因,找一下zhuce方法被调用的记录。
https://static.getgetai.com/img/161170b8bbca46939bff73aa5ff9adb9/20200928103921813.png
两处调用,第一处是软件打开时触发的checkzuce,由于这个软件的设计思路,当注册码正确时候才会写入数据库,而我们之前的数据库里是没有注册码的,所以当软件打开时并没有触发zhuce。
第二处调用是手动触发,当按注册按钮时的点击事件。
跟进去看看。
private void Menuregister_Click(object sender, EventArgs e)
{
this.checkzuce();
if(this.zctext.Text == "注册版")
{
this.showlog("已注册!");
return;
}
string text = "";
if(FFToolsMain.CDValue.Length < 28)
{
return;
}
try
{
text = (string) Clipboard.GetData(DataFormats.Text);
}
catch
{}
if(!string.IsNullOrEmpty(text) && text.Length == 40)
{
text = FFToolsMain.InputBox("请输入注册码", FFToolsMain.CDValue, text);
}
else
{
text = FFToolsMain.InputBox("请输入注册码", FFToolsMain.CDValue, "请输入注册码或直接粘贴");
}
if(string.IsNullOrEmpty(text))
{
return;
}
if(!this.zhuce(text))
{
this.showlog("输入的注册码有误或已过期,注册失败,检查后重新注册!");
return;
}
this.zctext.Text = "注册版";
this.showlog("注册成功!");
FFToolsMain.sqlmainrw.insdb("RegValue", commdata.fnRegValue, new object[]
{
text
});
}
checkzuce肯定是验证不通过的。
下一行的判断有点儿奇妙:
if(this.zctext.Text == "注册版")
{
this.showlog("已注册!");
return;
}
随便使用一个功能,会提示:
https://static.getgetai.com/img/161170b8bbca46939bff73aa5ff9adb9/20200928104410678.png
暂停,调用堆栈,然后也能看到这个判断:
https://static.getgetai.com/img/161170b8bbca46939bff73aa5ff9adb9/20200928104443057.png
这就很魔性……
给this.zctext.Text赋值,也能成功注册并使用所有功能……
https://static.getgetai.com/img/161170b8bbca46939bff73aa5ff9adb9/20200928104554801.png
再往下看算法:
string text = "";
if(FFToolsMain.CDValue.Length < 28)
{
return;
}
try
{
text = (string) Clipboard.GetData(DataFormats.Text);
}
catch
{}
if(!string.IsNullOrEmpty(text) && text.Length == 40)
{
text = FFToolsMain.InputBox("请输入注册码", FFToolsMain.CDValue, text);
}
else
{
text = FFToolsMain.InputBox("请输入注册码", FFToolsMain.CDValue, "请输入注册码或直接粘贴");
}
if(string.IsNullOrEmpty(text))
{
return;
}
如果注册码的长度小于28,直接return,说明注册码长度是大于28的。
然后尝试从剪贴板读取已经复制的文本,如果为空或者长度是40,提示请输入注册码,否则就自动把剪贴板的内容粘贴到编辑框,顺带赋值给text。
不晓得40长度的是个啥,不管他。
最后判断了一下text的内容是不是空的。
然后才能走到zhuce方法里。
那手动触发一下zhuce方法。
大于16且不为40,随便输入个假码触发就行。
https://static.getgetai.com/img/161170b8bbca46939bff73aa5ff9adb9/20200928110139381.png
瞄了一眼这个参数确实是我刚才输入的假码。
那继续回来跟踪zhuce方法。
如果这个注册码长度小于40,返回假。
那注册码的长度就是大于40呗。
然后是一个循环,算出了text3和text4。
输入一个长度40的假码看看,结果为:
https://static.getgetai.com/img/161170b8bbca46939bff73aa5ff9adb9/20200928112239653.png
有个注册码无效的错误,看看原因:
https://static.getgetai.com/img/161170b8bbca46939bff73aa5ff9adb9/20200928112354059.png
哦,text2要等于text3,那下断看看这两个都是啥。
https://static.getgetai.com/img/161170b8bbca46939bff73aa5ff9adb9/20200928112908651.png
随着循环次数的增加,text2没变,3一直在增加位数。
那我知道了……注册码的前32位就是text2呗。
改改我们的假码。
然后发现还是不对……
https://static.getgetai.com/img/161170b8bbca46939bff73aa5ff9adb9/20200928113958173.png
读了一遍这个循环,才发现它循环到一定位置时会抽取数值。
其中, 注册码的第3、8、13、18、23、28、33、38位会被抽出来,组成text4。
https://static.getgetai.com/img/161170b8bbca46939bff73aa5ff9adb9/20200928114541545.png
改改我们的假码算法,注意数组下标是从0开始的。
https://static.getgetai.com/img/161170b8bbca46939bff73aa5ff9adb9/20200928121802599.png
现在text2和text3就匹配了。
https://static.getgetai.com/img/161170b8bbca46939bff73aa5ff9adb9/20200928121934143.png
再往下看:
if(text4.Length != 8)
{
this.showlog("注册码过期...");
return false;
}
int num2 = this.TDClient();
if(num2 == 0)
{
this.showlog("无法获得数据,稍后再试...");
return false;
}
text4的长度要为8,然后联网获取了一个num2,实际上是从网络上获取了当前的时间。
int num3 = Convert.ToInt32(text4.Substring(0, 2), 16) - 128;
int num4 = Convert.ToInt32(text4.Substring(2, 2), 16) - 128;
int num5 = Convert.ToInt32(text4.Substring(4, 2), 16) - 128;
int num6 = Convert.ToInt32(text4.Substring(6, 2), 16) - 128;
this.showlog(string.Concat(new object[]
{
"有效期至",
num3, "年",
num4, "月",
num5, "日",
num6, "时"
}));
再往下是处理时间的,获取text4的内容然后-128。
写对应的逆向算法就行。
https://static.getgetai.com/img/161170b8bbca46939bff73aa5ff9adb9/20200928125820633.png
分析完毕。
https://static.52pojie.cn/static/image/hrline/5.gif
样本文件:
好多地方已经非常接近答案了,换作是我,直接编辑dll/exe文件,另存一份就可以了, 工具Reflexil tjl52pj 发表于 2020-9-29 00:48
想从头学软件破解类 要看哪些教程 老哥
本论坛有很多基础教程,视频区里找那种一个系列的 分析得很清楚~ 还算不错,看得很清楚 分析得很清楚,讲的很仔细。{:1_893:} 楼主分析得很清楚,学到了。 你这分析的,对我的帮助很大 很清楚,学习了,感谢