慕若曦 发表于 2020-9-28 15:02

某辅助软件注册逻辑分析

本次分析的软件样本是来自吾爱的一个求助帖,原贴现已锁帖。
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


样本文件:



EnterpriseSolu 发表于 2020-9-28 15:59

好多地方已经非常接近答案了,换作是我,直接编辑dll/exe文件,另存一份就可以了, 工具Reflexil

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

tjl52pj 发表于 2020-9-29 00:48
想从头学软件破解类 要看哪些教程 老哥

本论坛有很多基础教程,视频区里找那种一个系列的

Bds1r 发表于 2020-9-28 15:17

分析得很清楚~

jkwdxhh 发表于 2020-9-28 17:00

shaunkelly 发表于 2020-9-28 17:02

还算不错,看得很清楚

gagmeng 发表于 2020-9-28 17:08

Snprszy 发表于 2020-9-28 17:16

分析得很清楚,讲的很仔细。{:1_893:}

zjghc 发表于 2020-9-28 17:43

楼主分析得很清楚,学到了。

王兰花秀丽 发表于 2020-9-28 17:44

你这分析的,对我的帮助很大

hxd97244 发表于 2020-9-28 17:55

很清楚,学习了,感谢
页: [1] 2 3 4 5 6
查看完整版本: 某辅助软件注册逻辑分析