慕若曦 发表于 2020-9-28 13:24:10

某辅助软件注册逻辑分析

本帖最后由 763837023 于 2020-9-28 13:29 编辑

本次分析的软件样本是来自吾爱的一个求助帖,原贴现已锁帖。


https://static.getgetai.com/img/161170b8bbca46939bff73aa5ff9adb9/20200928075332627.png下载作者提供的样本打开后是这个样子的界面。https://static.getgetai.com/img/161170b8bbca46939bff73aa5ff9adb9/20200928075706702.png
左侧为软件的主界面,可以看到一个机器码未通过的字样,所以需要通过逆向分析实现让验证通过。先查壳,如右图所示。基础差一点儿的同学可以先去看我写的《.NET零基础逆向教程》没有壳,直接dnspy载入即可。
static/image/hrline/5.gif

机器码的生成:因为打开软件后在我们没有干涉的情况下,该软件直接读出了一个机器码,还判断出了这个机器码是错误的。因此能想到在窗口加载后,会有一个生成注册码并验证的逻辑。所以直接静态分析,从入口点跟入。https://static.getgetai.com/img/161170b8bbca46939bff73aa5ff9adb9/20200928080941120.png入口点是Main方法,中规中矩的窗口程序。https://static.getgetai.com/img/161170b8bbca46939bff73aa5ff9adb9/20200928081143275.pngMain中实例化了一个名叫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地址,进行了简单的加密处理生成了一个机器码。

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
爆破的话就这样就行了。

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
分析完毕。


static/image/hrline/5.gif

样本地址:
**** Hidden Message *****


OSF 发表于 2020-9-28 23:05:10

谢谢大表哥分享。

daoben 发表于 2020-9-28 13:49:04

向大大学习

lemontree 发表于 2020-9-29 06:22:56

感谢分享,谢谢飘云阁

windxp 发表于 2020-9-29 07:07:44

学习一次完整的分析,谢谢分享经验

gyjijian525 发表于 2020-9-29 07:56:16

请问有没有keygen?

非诚勿扰 发表于 2020-9-29 10:35:14

表哥V5,感谢分享了

afredpg 发表于 2020-9-29 10:39:03

感谢大佬分享,学习一下

慕若曦 发表于 2020-9-29 11:53:12

gyjijian525 发表于 2020-9-29 07:56
请问有没有keygen?

提供了源码啊……自己编译

回忆无法挽留 发表于 2020-9-30 00:16:12

C#的啊   学习一下
页: [1] 2
查看完整版本: 某辅助软件注册逻辑分析