记一次对".NET"某款奇葩的程序辅助工具的逆向之旅

作者:@Poacher
首发于:90WiKi
原地址:https://forum.90sec.org/forum.php?mod=viewthread&tid=10908&extra=page%3D1
发表时间: 2018-2-11 18:48:23

0x00前言:

在前几天,少年时期的一位童鞋问能不能帮看一款工具,是某款程序的辅助工具,需要注册码注册过后才能使用,我心中在想:”我这小菜鸡怎么搞得定呢“,最后在威逼利诱的情况下,我答应了。
本文很早之前就已经写完了,并未发出来,只是当作了一个过程记录写在了云笔记上,经过一番挣扎之后才决定发出来。
如果该工具作者看到该文并觉得不应该发出来,请联系我。我会对该篇文章进行删除。

0x01工具:

  • ExeInfo PE
  • dnSpy
  • De4dot

0x02正文:

鉴于这辅助工具的特别情况,所以下文称主程序为“程序客户端”以及插件为“辅助工具”
由于这款“辅助工具”是以插件形式加载到“程序客户端上”的,所以我们得先安装”程序客户端“上。首先由于对”辅助工具“的使用流程不是很熟悉,所以第一步就装上,然后运行了下。先熟悉一下流程,方便后续的干活。
附一张我对这个辅助工具目前所得到的信息的脑图。这样子可以更加清晰的让人知道做到哪。该做些啥。

我们先来看下正常情况下,我们运行”辅助工具“是提示什么。

image

image

经过前期得到的信息可知,该“辅助工具”的文件名为:TestProcess.dll
先用ExeInfo Pe查一下是否加固了。一般.NET的话很大可能都混淆了。

image

可以看到经过了dotfuscato进行混淆,可以直接使de4dot反混淆。

反混淆过后的新文件为:TestProcess-cleaned.dll

image

TestProcess.dll文件重命名一份。
然后把TestProcess-cleaned.dll改成TestProcess.dll
接下来直接将TestProcess.dll拖入进dnspy里。

image

首先我们看到“脑图”。我现在按照”脑图“的分析思路。从第一个思路开始分析一下。 可以看到输入注册码那个窗口,我们随便输入一个内容后点确定。

image

可以看到,从确定按钮的点击事件中,所触发的这个弹窗。我们针对该点击事件去找下具体代码。在Form1-》Button1_Click方法以下代码:

// TestProcess.Form1
// Token: 0x0600003A RID: 58 RVA: 0x00007BF0 File Offset: 0x00005FF0
private void Button1_Click(object sender, EventArgs e)
{
    RegistryKey currentUser = MyProject.Computer.Registry.CurrentUser;
    RegistryKey registryKey = currentUser.OpenSubKey("Software\\", true);
    string text = this.TextBox1.Text;
    bool flag = this.RegCodeeque(text) == 20;
    if (flag)
    {
        registryKey.SetValue("FastTooL_Register_RegCode", text);
        Interaction.MsgBox("注册成功!", MsgBoxStyle.OkOnly, null);
        this.vvv = 1;
        this.Close();
    }
    else
    {
        Interaction.MsgBox("输入的注册码错误,请重新输入!", MsgBoxStyle.OkOnly, null);
    }
}

从第七行开始看。可以看到获取的TextBox1控件的内容并赋值给text。然后到第8行去判断 this.RegCodeeque(text)返回值 是否等于20。并且将结果赋值给flag。如果等于则为true,如果不等于则为false

重要的内容从第9行开始,我们来分析下写的什么意思。

可以很明显的看出来这里写的是,如果flagtrue的话则将text内容就是输入的注册码内容写进注册表内。然后提示注册成功,窗口结束。如果flagfalse的话,则提示注册码错误。

很明显,我们刚刚为false,所以走进了注册码错误。接下来我们瞄准到this.RegCodeeque(text)这个函数中。

进入这个函数可以得知该函数的返回值为int型。看到上面验证代码 bool flag = this.RegCodeeque(text) == 20;可以看到返回函数返回值是否等于20。我们需要让这个程序的返回值为20,从而直接到注册成功。所以中间代码。

不管他。直接看最下面的代码。

最后如果算出来注册码为正确的话才返回20,否则话就返回0。所以我们可以选择尝试直接改result改成:return 20; 或者 重新把result赋值一遍。

image

修改完之后,我们保存一下。

image

重新打开软件,运行“辅助工具”然后在尝试一下注册。

image

提示注册成功了。说实在话,我第一眼看见这个我是非常无比的兴奋的,因为我不敢相信那么快就有结果了。然后在带着无比兴奋的心情重新点击了一下这个功能之后发现事情并没有那么简单....

image

因为无论你运行多少次。它都只会弹出这个验证窗并且随便输入都提示注册成功。顿时心情一落千丈。由此可以判断直接修改注册窗那块的代码返回值是不可行的。

于是思考了一下,每次运行“辅助工具”都会弹出这个注册窗。那么在“辅助工具”初始化的时候肯定做了判断,所以才会反复进入注册窗口。于是我们找到CHECKLOAD-》CheckReg处。

image

可以看到返回值也是int类型。

可以看到。每次运行“辅助工具”都会弹出无申请码这个提示。

image

所以我们可以确定就是这个函数做了初始化验证功能。
现在开始阅读一下代码。看看这些代码到底做了什么操作。接下来的代码一些操作流程。我都会以注释形式写在以下代码中。


// TestProcess.CHECKLOAD
// Token: 0x06000021 RID: 33 RVA: 0x00006750 File Offset: 0x00004B50
public int CheckReg() {  string location = Assembly.GetExecutingAssembly().Location;  string text = Strings.Mid(location, 1, Strings.InStrRev(location, "\\", -1, CompareMethod.Binary));  this.LastTime0 = Directory.GetLastAccessTime(text + "\\FastProcess.arx"); //获取“辅助工具”当前运行目录下的FastProcess.arx上一次访问时间
      checked   {      string text2 = Strings.Mid(text, 1, Strings.InStrRev(text, "\\", -1, CompareMethod.Binary) - 1);      text2 = Strings.Mid(text2, 1, Strings.InStrRev(text2, "\\", -1, CompareMethod.Binary)) + "CADTools\\SLD&BMP\\";      int num = 0;      bool flag;      DateTime value;      
        try       {        
            /*
                CB5文件内容:
                    0,20171010,30
                    1,LHR170479-93678-KC0E92BD6-30902515287231,2A275-EAF86EBC-F9D405BF22D
            */
                    flag = File.Exists(text2 + "CB5"); //判断CADTools\SLD&BMP\CB5文件是否存在,如果存在则为true否则为false
                    
            if (flag)         {          string[] txtFile = CHECKLOAD.GetTxtFile(text2 + "CB5"); //调用GetTextFile函数来对CB5文件进行读取。(该函数其实就是将cb5文件里面的两行内容读取了出来)
                          int num2 = 0;          this.ExeclMac = new string[txtFile.Length - 1 + 1];          this.ExeclReg = new string[txtFile.Length - 1 + 1];          string[] array = txtFile;          
                for (int i = 0; i < array.Length; i++)           {              string expression = array < i > ;
                    string[] array2 = Strings.Split(expression, ",", -1, CompareMethod.Binary); //使用,号对字符串进行分割成为数组。
                    this.ExeclMac[num2] = Strings.Trim(array2[1]); //第一次循环结果为:20171010,第二次循环结果为:LHR170479-93678-KC0E92BD6-30902515287231
                    this.ExeclReg[num2] = Strings.Trim(array2[2]); //第一次循环结果为:30,第二次循环结果为:2A275-EAF86EBC-F9D405BF22D
                    //下面的图片有cb5文件内容
                    num2 = 1 + num2;
                }
            } else {
                Interaction.MsgBox("未找到定义文件:Reg.csv,程序无法运行!", MsgBoxStyle.OkOnly, null);
            }
            value = Conversions.ToDate(Strings.Format(Conversion.Val(this.ExeclMac[0]), "0000-00-00")); //对第一次循环结果为:20171010进行了格式化处理
            num = Conversions.ToInteger(this.ExeclReg[0]); //获取的则是第一次循环结果为:30
        } catch(Exception expr_179) {
            ProjectData.SetProjectError(expr_179);
            Interaction.MsgBox("未注册!", MsgBoxStyle.OkOnly, null);
            ProjectData.ClearProjectError();
            goto IL_415;
        }
        string diskVolumeSerialNumber = CHECKLOAD.GetDiskVolumeSerialNumber();
        flag = (Array.IndexOf < string > (this.ExeclMac, diskVolumeSerialNumber) == -1); //如果this.ExeclMac里没有diskVolumeSerialNumber内容的话。就提示无申请码。很明显的告诉我们,我们需要把第二次循环结果为:LHR170479-93678-KC0E92BD6-30902515287231,替换成我们自己的序列号。
        int result;
        if (flag) {
            Interaction.MsgBox("无申请码!", MsgBoxStyle.OkOnly, null); //如果不是我们序列号的话。提示没有申请码。
        } else {
            string value2 = string.Concat(unchecked(new string[] {
                Conversion.Hex(Math.Round(Conversions.ToDouble(CHECKLOAD.Ddisk) / 3.5 + 156.0)),
                "-",
                Conversion.Hex(Math.Round((Conversions.ToDouble(CHECKLOAD.Cdisk) + 8011.0) / 3.0 + 92.0, 0)),
                "-",
                Conversion.Hex(Math.Round(CHECKLOAD.mac / 1.8 + 1247.0))
            }));
            flag = (Array.IndexOf < string > (this.ExeclReg, value2) == -1); //判断this.ExeclReg里面没有value2内容的话就提示无注册码。通过value2的方式来计算得到的注册码结果为:AFD634F-26CC46EE-4A8719E5033
            if (flag) {
                Interaction.MsgBox("无注册码!", MsgBoxStyle.OkOnly, null); //如果不是我们注册码的话。提示没有申请码。
            } else {
                //如果申请码,注册码都匹配上则进入判断程序是否过期的时候了。
                /*
this.LastTime0.Subtract(value)
这里判断了FastProcess.arx上一次访问时间减去第一次循环结果为:20171010的值。
最后判断计算出来的值大于第一次循环结果为:30的话就是程序到期。这里就开始判断剩余的有效天数了。
*/
                int num3 = Math.Abs((int) Math.Round(this.LastTime0.Subtract(value).TotalDays));
                flag = (num3 > num);
                if (flag) {
                    Interaction.MsgBox("程序到期!", MsgBoxStyle.OkOnly, null);
                } else {
                    string text3 = Conversions.ToString(Directory.GetLastWriteTime(text2 + "CB5"));
                    text3 = Strings.Replace(text3, "/", "", 1, -1, CompareMethod.Binary);
                    text3 = Strings.Mid(text3, 1, Strings.InStr(text3, " ", CompareMethod.Binary) - 1); //获取上一次cb5访问时间
                    flag = (Conversions.ToDouble(text3) != 20171010.0); //看到这里发现,上一次cb5访问时间就是当前的时间。所以对于这里的20171010.0的固定值表示有点懵(因为如果这里固定了时间那么如果不修改这时间就永远进不去下面的区间),于是就看了下下面。发现如果不进if里面的话。就直接跳到注册窗口。
                    if (!flag) {
                        string text4 = Conversions.ToString(Directory.GetLastAccessTime(text + "FastProcess.arx"));
                        text4 = Strings.Replace(Conversions.ToString(this.LastTime0), "/", "", 1, -1, CompareMethod.Binary);
                        text4 = Strings.Mid(text4, 1, Strings.InStr(text4, " ", CompareMethod.Binary) - 1);
                        flag = (2017925.0 < Conversion.Val(text4) & Conversion.Val(text4) > 20171031.0);
                        if (!flag) {
                            int num4 = 2;
                            Directory.SetLastAccessTime(text + "FastProcess.arx", DateAndTime.Now);
                            result = num4;
                            return result; //突然发现这里有个返回值,返回的值则为2。所以直接将代码全部删除掉。改成return 2;试试。
                        }
                        Interaction.MsgBox("授权时间已过期,请联系程序开发者.\r                     Tel:136223*****", MsgBoxStyle.OkOnly, null);
                    }
                }
            }
        }
        IL_415: Form1 form = new Form1();
        form.ShowDialog();
        result = 0;
        return result;
    }
}

首先按照我们上面代码的分析思路。
我们CB5文件的申请码改成我们自己的先。然后运行试试。

image

可以发现已经不会提示无申请码了。现在提示是无注册码。接下来,我们把计算出来的注册码也修改掉(经过测试这里注册码为3组)。

附上计算Value2代码(其实就是将他原来的代码复制了一遍,然后修改点东西,就可以出来了)。

private void button1_Click(object sender, EventArgs e) {
    object objectValue = RuntimeHelpers.GetObjectValue(Interaction.CreateObject("Scripting.FileSystemObject", ""));
    string Cdisk =   Conversions.ToString(NewLateBinding.LateGet(NewLateBinding.LateGet(objectValue, null, "drives", new object[] {
        "C:"
    },
    null, null, null), null, "SerialNumber", new object[0], null, null, null));
    string Ddisk = Conversions.ToString(NewLateBinding.LateGet(NewLateBinding.LateGet(objectValue, null, "drives", new object[] {
        "D:"
    },
    null, null, null), null, "SerialNumber", new object[0], null, null, null));
    NetworkInterface[] allNetworkInterfaces = NetworkInterface.GetAllNetworkInterfaces();
    double mac = (double) Conversions.ToLong("&H" + allNetworkInterfaces[0].GetPhysicalAddress().ToString());
    string value2 = string.Concat(unchecked(new string[] {
        Conversion.Hex(Math.Round(Conversions.ToDouble(Ddisk) / 3.5 + 156.0)),
        "-",
        Conversion.Hex(Math.Round((Conversions.ToDouble(Cdisk) + 8011.0) / 3.0 + 92.0, 0)),
        "-",
        Conversion.Hex(Math.Round(mac / 1.8 + 1247.0))
    }));
    textBox1.Text = value2;
}

image

现在则是直接提示程序到期,这个时候我们去吧cb5的日期修改掉

image

之后程序是出了异常。

然后是直接将整个代码删除了。只返回了一个2出去。

现在再来试试修改之后运行“辅助工具”

image

可以看到,已经没有验证提示。已经走进了正常的功能中。

0x03后续:

距离上一次分析这款辅助工具已经有一段时间了。之后朋友告诉我,他们老板财大气粗已经买下来了,他最开始给我的这款工具是测试版本,只是商家发给客户看的(难怪感觉代码那么奇怪)。

然后把正式版的辅助工具发了一份给我。解开了我在上一次分析的一点疑惑。就是:

flag = (Conversions.ToDouble(text3) != 20171010.0);//

看到这里发现,上一次cb5访问时间就是当前的时间。所以对于这里的20171010.0的固定值表示有点懵(因为如果这里固定了时间那么如果不修改这时间就永远进不去下面的区间),于是就看了下下面。发现如果不进if里面的话。就直接跳到注册窗口。

if (!flag)
{
/*代码省略*/
}

就是这个地方,为什么判断的时间日期是固定的20171010于是我重新看了下正版的工具,发现这一块代码地方改成如下:

if (Conversions.ToDouble(text3) == 20171129.0)
{
string text4 = Conversions.ToString(Directory.GetLastAccessTime(text + "FastProcess.arx"));
text4 = Strings.Replace(Conversions.ToString(this.dateTime_0), "/", "", 1, -1, CompareMethod.Binary);
text4 = Strings.Mid(text4, 1, Strings.InStr(text4, " ", CompareMethod.Binary) - 1);
if (!(2017925.0 < Conversion.Val(text4) & Conversion.Val(text4) > 20271220.0))
{
Directory.SetLastAccessTime(text + "FastProcess.arx", DateAndTime.Now);
result = 2;
return result;
}
Interaction.MsgBox("授权时间已过期,请联系程序开发者.\r                     *********@qq.com", MsgBoxStyle.OkOnly, "提示");
}

发是正版程序是Conversions.ToDouble(text3) != 20171010.0改成了Conversions.ToDouble(text3) == 20171129.0以及if (!(2017925.0 < Conversion.Val(text4) & Conversion.Val(text4) > 20271220.0))这个地方的日期也改了。

也就是说这个程序的注册窗口是没有作用的。重点是在cb5文件内容处。只要将Value2计算出来的结果以及电脑机器码写进去之后,正版程序即可正常使用了。

然后让朋友将他电脑上的该文件内容截图看了下发现的确是在cb5文件内加入了该机器注册码。也就是最上边文章内所改形式。

PS:文章开始的 思维脑图 是边分析边写的一个脑图,可能部分小地方有点对不上,是由于后边知道了一个大概的思路就没有继续对脑图进行修改了。画这个脑图的原因,主要就是为了方便分析