howsk 发表于 2016-12-16 14:57

百分百企*录8.3网络验证逆向过程

0x1前言:
       因本人最近创业,公司刚成立,需要联系一些没有制作过网站、App等的公司法人代表来增加自己的客户,于是想到了企*录这样的一个软件,后来百度了一下,较为有名的有卓迅和百分百的软件,后来也就选了百分百吧,发现收费800,让我这个刚开始的创业狗有些心疼,于是就有了今天的这篇文章,不足之处,还望各为指点迷津,毕竟第一逆向.NET的软件。


0x2主程序:
1、百分百客户综合采集软件8.3(启动程序.exe)【MD5: D4F99FB54F24E7E3AAEB363E7CD2471F】
2、/Plug/bfbqf.AppMain.dll【MD5: F79795D12DFC61038B6BCA7F1821E482】
3、/Plug/bfbqf.AppLogin.dll【MD5: D60205FE302D8F5D947965C8C3C97D51】
4、/Plug/bfbqf.AppObject.dll【MD5: 1109D47D611D277EE32FB8FED43E2B4E】
5、/Plug/bfbqf.PublicLib.dll【MD5: 836F482D6E1F83FE890B97AA4D94B484】

0x3侦查:
-0x3-01工具:
1、ProcessMonitor
2、PEiD
3、ScanId

-0x3-02
软件采用.NET编写


于是利用ScanId看看它有没有加什么混淆,然而并没有发现什么


于是运行它,在ProcessMonitor上看它的一些进程信息

如此一来,可以大致地分析出,该软件主要是调用bfbqf.AppMain.dll、bfbqf.AppLogin.dll、bfbqf.AppObject.dll、bfbqf.PublicLib.dll来实现主要的功能,不过,我们有必要也反编译一下exe的程序,毕竟是可执行程序嘛。

0x04反编译:
-0x04-01工具:
1、dnSpy

-0x04-02过程:
可以看到所有的函数都是检查的功能,如果要K掉更新什么的,直接可以在函数体内入手,但这不是我们今天的主题,下面开始分析今天的主角——Dll。


--0x04-02-01bfbqf.AppLogin.dll

看文件名的话,经验告诉我,先分析bfbqf.AppLogin.dll会比较好一点。
该Dll存在5个程序集,我们进入bfbqf.AppLogin的程序集里面去愁一愁。


       定位到private bool Login(string UserName, string Password)和public bool Login(string UserName, string Password, out UserCenter.TUserInfo UserInfo)的函数,这里需要注意的是这两个函数的参数不一样,第二个函数会把返回值传给UserCenter.TUserInfo UserInfo

// bfbqf.AppLogin.LoginWindow
// Token: 0x0600001D RID: 29 RVA: 0x00002E44 File Offset: 0x00001044
public bool Login(string UserName, string Password, out UserCenter.TUserInfo UserInfo)
{
      string text = UserCenter.Login(UserName, Password, out this.OutUserInfo);
      bool flag = text.Contains("Success");
      bool result;
      if (flag)
      {
                UserCenter.m_UserName = UserName;
                UserCenter.m_PassWord = Password;
                UserCenter.m_Uid = this.OutUserInfo.uid;
                this.OutUserInfo.UserName = UserName;
                this.OutUserInfo.Password = Password;
                bool flag2 = this.OutUserInfo.UserType == UserCenter.UserTypeEnum.Test;
                if (flag2)
                {
                        MsgBox.ShowInfoBox("免费试用时软件将会做出以下限制:\n1、只能浏览搜索结果的前100条数据;\n2、无法将搜索结果数据导出;\n3、无法使用高级筛选功能。\n若想无限制使用请联系客服购买!");
                }
                else
                {
                        bool flag3 = this.OutUserInfo.UserType == UserCenter.UserTypeEnum.User;
                        if (flag3)
                        {
                              MsgBox.ShowInfoBox("使用普通用户登录时软件将会做出以下限制:\n1、只能浏览搜索结果的前100条数据;\n2、无法将搜索结果数据导出;\n3、无法使用高级筛选功能。\n若想无限制使用请联系客服购买!");
                        }
                }
                UserInfo = this.OutUserInfo;
                LoginWindow.SaveSetting("qiyeminglu", string.Concat(new object[]
                {
                        this.txt_UserName.Text,
                        "'",
                        this.txt_Password.Password,
                        "'",
                        this.chk_SavePassword.IsChecked,
                        "'",
                        this.chk_AutoLogin.IsChecked
                }));
                result = true;
      }
      else
      {
                bool flag4 = text.Contains("UserName Error");
                if (flag4)
                {
                        MsgBox.ShowErrorBox("用户不存在。");
                        frmYihaotong frmYihaotong = new frmYihaotong();
                        OutputForGetAgentId outputForGetAgentId = new OutputForGetAgentId();
                        bool flag5 = Constant.OEM2 == "553ef1349aa9e82922a664f5c7adfaac";
                        if (flag5)
                        {
                              outputForGetAgentId = GetAgentIdUtil.GetAgentId(13, UserName, Constant.OEM2);
                              frmYihaotong.linkReg.Tag = outputForGetAgentId.RegLink;
                        }
                        else
                        {
                              frmYihaotong.linkReg.Tag = PublicValue.RWXCom.GetOEM("reglink");
                        }
                        frmYihaotong.ShowDialog();
                }
                else
                {
                        bool flag6 = text.Contains("Password Error");
                        if (flag6)
                        {
                              MsgBox.ShowErrorBox("登录密码错误。");
                        }
                        else
                        {
                              bool flag7 = text.Contains("Repeat");
                              if (flag7)
                              {
                                        MsgBox.ShowErrorBox("同一帐号同一时间只能一人登录!!!\n如果您上次异常退出程序,请2-3分钟后尝试重新登录。");
                              }
                              else
                              {
                                        bool flag8 = text.Contains("Timeout");
                                        if (flag8)
                                        {
                                                MsgBox.ShowErrorBox("登录超时,请稍后再试。");
                                        }
                                        else
                                        {
                                                bool flag9 = text.Contains("Database connect error");
                                                if (flag9)
                                                {
                                                      MsgBox.ShowErrorBox("数据库连接失败,请联系在线客服协助解决");
                                                }
                                                else
                                                {
                                                      MsgBox.ShowErrorBox("出现未知错误,请联系在线客服协助解决");
                                                }
                                        }
                              }
                        }
                }
                UserInfo = this.OutUserInfo;
                result = false;
      }
      return result;
}


       可以清楚地看到,一句关键的赋值比较“bool flag2 = this.OutUserInfo.UserType == UserCenter.UserTypeEnum.Test;”它的意思就是把this.OutUserInfo.UserType赋值给flag2并且与UserCenter.UserTypeEnum.Test做比较,知道了这个,我们来看一看UserCenter.UserTypeEnum存放了哪些东西。
                // Token: 0x02000027 RID: 39
                public enum UserTypeEnum
                {
                        // Token: 0x04000054 RID: 84
                        Test,
                        // Token: 0x04000055 RID: 85
                        User,
                        // Token: 0x04000056 RID: 86
                        VIP
                }

可以大致地判断出,程序的用户会有三个状态,分别是Test(测试用户)、User(普通用户)和VIP(收费用户),如此一来,我们的目的,就是要让我们的变成VIP。再来看一看OutUserInfo.UserType存放了一些什么东西吧。
                // Token: 0x02000028 RID: 40
                public struct TUserInfo
                {
                        // Token: 0x04000057 RID: 87
                        public Guid uid;

                        // Token: 0x04000058 RID: 88
                        public string UserName;

                        // Token: 0x04000059 RID: 89
                        public string Password;

                        // Token: 0x0400005A RID: 90
                        public UserCenter.UserTypeEnum UserType;

                        // Token: 0x0400005B RID: 91
                        public DateTime LoginTime;

                        // Token: 0x0400005C RID: 92
                        public DateTime ExpTime;

                        // Token: 0x0400005D RID: 93
                        public string SubscribeStatus;

                        // Token: 0x0400005E RID: 94
                        public string VerifyCode;

                        // Token: 0x0400005F RID: 95
                        public string AgentID;
                }

       可以看出它存放了很多关于用户的一些Flag,其中最为主要的就是UserType,它最终会指向Public enum UserTypeEnum,然后返回其中的值(Test、User、VIP),也就是说,最终影响用户状态的就是它,也就是public bool Login(string UserName, string Password, out UserCenter.TUserInfo UserInfo)的第三个参数,知道了这些,我们在该函数体内下一个断点,来动态调试它看看。然而可惜的是发现并没有断下来,于是进入到另一个Login函数,也就是private bool Login(string UserName, string Password)。

// bfbqf.AppLogin.LoginWindow
// Token: 0x0600001E RID: 30 RVA: 0x00003094 File Offset: 0x00001294
private bool Login(string UserName, string Password)
{
      string text = UserCenter.Login(UserName, Password, out this.OutUserInfo);
      bool flag = string.IsNullOrWhiteSpace(text);
      bool result;
      if (flag)
      {
                bool flag2 = UserCenter.GetURLText("http://api.houtain.100public.com:9005/api/checkforeign.ashx?username=" + UserName) == "1";
                if (flag2)
                {
                        File.WriteAllText(Application.StartupPath + "\\RES\\jump", UserCenter.UserMd5("r1w2x3faobrcedieggn_" + UserName), Encoding.UTF8);
                }
                MsgBox.ShowErrorBox("从服务器获取数据超时。请检查您的网络是否正常");
      }
      else
      {
                bool flag3 = text.Contains("Success");
                if (flag3)
                {
                        UserCenter.m_UserName = UserName;
                        UserCenter.m_PassWord = Password;
                        UserCenter.m_Uid = this.OutUserInfo.uid;
                        this.OutUserInfo.UserName = UserName;
                        this.OutUserInfo.Password = Password;
                        bool flag4 = this.OutUserInfo.UserType == UserCenter.UserTypeEnum.Test;
                        if (flag4)
                        {
                              MsgBox.ShowInfoBox("免费试用时软件将会做出以下限制:\n1、只能浏览搜索结果的前100条数据;\n2、无法将搜索结果数据导出;\n3、无法使用高级筛选功能。\n若想无限制使用请联系客服购买!");
                        }
                        else
                        {
                              bool flag5 = this.OutUserInfo.UserType == UserCenter.UserTypeEnum.User;
                              if (flag5)
                              {
                                        MsgBox.ShowInfoBox("使用普通用户登录时软件将会做出以下限制:\n1、只能浏览搜索结果的前100条数据;\n2、无法将搜索结果数据导出;\n3、无法使用高级筛选功能。\n若想无限制使用请联系客服购买!");
                              }
                        }
                        LoginWindow.SaveSetting("qiyeminglu", string.Concat(new object[]
                        {
                              this.txt_UserName.Text,
                              "'",
                              this.txt_Password.Password,
                              "'",
                              this.chk_SavePassword.IsChecked,
                              "'",
                              this.chk_AutoLogin.IsChecked
                        }));
                        result = true;
                        return result;
                }
                bool flag6 = text.Contains("UserName Error");
                if (flag6)
                {
                        MsgBox.ShowErrorBox("用户不存在。");
                        frmYihaotong frmYihaotong = new frmYihaotong();
                        OutputForGetAgentId outputForGetAgentId = new OutputForGetAgentId();
                        bool flag7 = Constant.OEM2 == "553ef1349aa9e82922a664f5c7adfaac";
                        if (flag7)
                        {
                              outputForGetAgentId = GetAgentIdUtil.GetAgentId(13, UserName, Constant.OEM2);
                              frmYihaotong.linkReg.Tag = outputForGetAgentId.RegLink;
                        }
                        else
                        {
                              frmYihaotong.linkReg.Tag = PublicValue.RWXCom.GetOEM("reglink");
                        }
                        frmYihaotong.ShowDialog();
                }
                else
                {
                        bool flag8 = text.Contains("Password Error");
                        if (flag8)
                        {
                              MsgBox.ShowErrorBox("登录密码错误。");
                        }
                        else
                        {
                              bool flag9 = text.Contains("Repeat");
                              if (flag9)
                              {
                                        MsgBox.ShowErrorBox("同一帐号同一时间只能一人登录!!!\n如果您上次异常退出程序,请2-3分钟后尝试重新登录。");
                              }
                              else
                              {
                                        bool flag10 = text.Contains("Timeout");
                                        if (flag10)
                                        {
                                                MsgBox.ShowErrorBox("登录超时,请稍后再试。");
                                        }
                                        else
                                        {
                                                bool flag11 = text.Contains("Database connect error");
                                                if (flag11)
                                                {
                                                      MsgBox.ShowErrorBox("数据库连接失败,请联系在线客服协助解决");
                                                }
                                                else
                                                {
                                                      MsgBox.ShowErrorBox("出现未知错误,请联系在线客服协助解决");
                                                }
                                        }
                              }
                        }
                }
      }
      result = false;
      return result;
}


发现这里有一个接口,而这个接口成功地亮瞎了我的眼,不知道大家有没有发现,这两个函数,往下面都是大同小异的,主要就是用flagN来接收返回值,不管怎样,我们现在这个函数体内下一个断点运行看一看。


       已经成功断下来了,我的UserName和PassWord已经获取到了。发现在这里才开始调用public bool Login(string UserName, string Password, out UserCenter.TUserInfo UserInfo)的,F11进去瞅瞅,看看我们在上面的猜想是否正确。


       这一句的意思就是把UserName、PassWord、HardDeviceUtils.GetDeviceCode().Replace(" ", "")、DateTime.Now.Ticks,填写到{0}、{1}、{2}、{3}中,然后调用UserCenter.GetURLText()函数把Format到的数据当作参数以POST的方式提交给Login.php,最后把返回值给text,后面的两个参数大概就是和硬盘以及时间有关系的了,这里必须得一步一步地分析,我们F11跟进去瞅瞅。


首先GetDeviceCOde(),然会HardDeviceUtils.GetHardDiskID(),应该是硬盘ID,继续F11。
namespace RWXComLibrary.APISdk.Utils
{
      // Token: 0x02000026 RID: 38
      public class HardDeviceUtils
      {
                // Token: 0x0600004E RID: 78 RVA: 0x0000411C File Offset: 0x0000231C
                private static string GetHardDiskID()
                {
                        string result = "";
                        try
                        {
                              using (ManagementObjectSearcher managementObjectSearcher = new ManagementObjectSearcher("SELECT * FROM Win32_PhysicalMedia"))
                              {
                                        ManagementObjectCollection managementObjectCollection = managementObjectSearcher.Get();
                                        if (managementObjectCollection != null)
                                        {
                                                using (ManagementObjectCollection.ManagementObjectEnumerator enumerator = managementObjectCollection.GetEnumerator())
                                                {
                                                      while (enumerator.MoveNext())
                                                      {
                                                                ManagementObject managementObject = (ManagementObject)enumerator.Current;
                                                                if (managementObject["SerialNumber"] != null)
                                                                {
                                                                        result = managementObject["SerialNumber"].ToString().Trim();
                                                                        managementObjectSearcher.Dispose();
                                                                        managementObjectCollection.Dispose();
                                                                        break;
                                                                }
                                                      }
                                                }
                                        }
                              }
                              return result;
                        }
                        catch
                        {
                              result = "";
                        }
                        return result;
                }

                // Token: 0x0600004F RID: 79 RVA: 0x000041EC File Offset: 0x000023EC
                public static string GetDeviceCode()
                {
                        return HardDeviceUtils.GetHardDiskID();
                }
      }
}

这里已经是属于RWXComLibrary.APISdk.Utils了,我们就不用理会了,因为我看不懂,也懒得看懂它,反正就是一路F10,最后要看到返回值。


可以清楚地看到,最后返回给我的硬盘ID是5VMB24HJ。然后继续F11,继续分析咯。


同上,不必理会,因为反正也看不懂,不过最后还是得留意它的返回值。一路F11之后,会进入到public static string Concat(string str0, string str1)函数,这里是关键点,看不懂也得看!
                // Token: 0x06000465 RID: 1125 RVA: 0x0000CBBC File Offset: 0x0000ADBC
               
                public static string Concat(string str0, string str1)
                {
                        if (string.IsNullOrEmpty(str0))
                        {//检查str0是否为空
                              if (string.IsNullOrEmpty(str1))
                              {//检查str1是否为空
                                        return string.Empty;
                              }
                              return str1;//如果都不为空,返回str1
                        }
                        else
                        {
                              if (string.IsNullOrEmpty(str1))
                              {
                                        return str0;//同上
                              }
                              int length = str0.Length;//计算str0的长度
                              string text = string.FastAllocateString(length + str1.Length);//给text赋值为str0.Length+str1.Length的占位符
                              string.FillStringChecked(text, 0, str0);//拼接text的前str0.Length个占位符为str0
                              string.FillStringChecked(text, length, str1);//同上
                              return text;//返回一个完整的接口
                        }
                }

具体的意思我已经在上面的代码中给出了注释,来看一下最终的返回值吧。


成功返回出一个接口,我们直接在浏览器里面访问一下,看看回显结果是啥样子的。


OK,差不多已经分析出来了,把这一串Json赋值给text。


我们继续F10单步往下走,看看它验证的过程是什么样子的。


这里请注意UserType,它存在login_Json.data里面,而后面的login_Json.msg返回的是“Success”,然后可以拿刚才获取到的Json来对比一下,也就是取msg和UserType的值。


       我们继续F10单步往下走,程序会回到private bool Login(string UserName, string Password)的位置,而text最终返回为Success,我刚开始的时候想,直接把text赋值为Success不就得了?然而并没有什么卵用,不信你们试试看,不然我也不会写这篇文章了。我们继续往下看。


       后面又通过flag3接收Success,之后又有一个关键的比较bool flag4 = this.OutUserInfo.UserType == UserCenter.UserTypeEnum.Test和bool flag5 = this.OutUserInfo.UserType == UserCenter.UserTypeEnum.User;
这里大家应该都能明白了吧?总之,最后不是UserCenter.UserTypeEnum.VIP,就给你弹个框,然后就限制你的操作行为,看了很心疼。

这里因为限制上上传图片的数量,就只能传刚开始的图片了。

       既然如此,知道了它是如何判断你的UserType的,那么就好办了,不是访问接口之后会返回一个Json吗?然后再读取里面的msg和UserType吗?那好的很啊,我们现在把这一串Json撸下来,然后把里面的UserType手动改成VIP,然后再搭建本地服务器,不就过了它的网络验证了吗?不就完事儿了吗?不就通关了吗?
{"msg":"Success","data":{"uid":"f2eeb691-c292-11e6-bcbf-002590e21c76","UserName":"howsk","LoginTime":"2016\/12\/15 14:51:47","UserType":"VIP","ExpTime":"2016\/12\/16 14:51:47","SubscribeStatus":"0","VerifyCode":"urvpckwrpt","LoginUID":"f782f923897e9414829f0e917111fea3","AgentID":"125135"}}
好了,环境搭建好了,现在我们来开始动手修改吧!(限制了上传图片的数量,只能贴代码了)
                public static string Login_RWX(string UserName, string Password, out UserCenter.TUserInfo OutUser)
                {
                        string text = "";
                        UserCenter.GetURLText("127.0.0.1/1.htm");
                        string result;
                        try
                        {
                              UserCenter.Login_Json login_Json = JsonConvert.DeserializeObject<UserCenter.Login_Json>(text);
                              OutUser = login_Json.data;
                              result = login_Json.msg;
                        }
                        catch
                        {
                              OutUser = default(UserCenter.TUserInfo);
                              UserCenter.ErrorLog("【Login_RWX】" + text);
                              result = text;
                        }
                        return result;
                }

然而很遗憾,发现根本没有什么卵用,依然是熟悉的画面,依然是同一个弹框,初恋般的感觉。


不过没有关系,我们大致地分析出来了它的检查原理,这里忘记了还有其它几个Dll,我这里就不卖关子了,我们直接到bfbqf.PublicLib.dll里面看一看吧。在UserCenter里面又发现了一处Login的函数,通过上面的讲解,我们继续下断点,最后得出的结论是,修改这里的接口为本地链接方可达到破解的目的。


放在本地的一个目的就可以改变任意用户名,任意密码,全部都是任意的,然后所有的限制功能也可以用了。

0x05总结:
1、作者并没有对软件进行混淆之类的保护。
2、在整个逆向过程中,可以发现,在这个接口上,根本没有再次验证硬盘ID和时间,这也说明了接口中的Login.php需要的参数没有做严格的要求和验证。
3、作者在对服务器交互的时候并没有采用Token或者Session来做验证,从而恶意者可以投机取巧。

*注:
1、该文章首发52破解论坛,其它地方看到此文章,均属盗版。
2、该文章仅供参考和学习,切勿用于非法和商业用途,否则一切后果与本人无关。
3、最后祝自己公司开业大吉,生意兴隆。


howsk 发表于 2016-12-17 22:21

610100 发表于 2016-12-17 20:30
我小白一个,不知大神用什么软件破解.NET。有什么好的视频教程吗?
谢谢大神。

使用dnSpy反编译。

KingHLi 发表于 2016-12-20 20:44

howsk 发表于 2016-12-17 22:22
你微信多少?

大神,请问为什么我用dnspy导入程序(exe或者DLL),就修改了验证链接地址,导出程序后启动就出现 程序已停止工作的问题?
问题签名:
问题事件名称:      CLR20r3
问题签名 01:      启动程序_cr
问题签名 02:      1.0.0.0
问题签名 03:      58524063
问题签名 04:      PresentationFramework
问题签名 05:      4.6.81.0
问题签名 06:      5584f3d7
问题签名 07:      2b2

我的电脑是WIN7 64,请问是什么原因呢?请大神解疑,非常感谢!

Hmily 发表于 2016-12-16 16:04

@howsk 文章图片是粘贴的?得上传才行,没上传成功。

howsk 发表于 2016-12-16 16:45

Hmily 发表于 2016-12-16 16:04
@howsk 文章图片是粘贴的?得上传才行,没上传成功。

是粘贴的,H大您稍等,我现在重新编辑上传

howsk 发表于 2016-12-16 17:43

Hmily 发表于 2016-12-16 16:04
@howsk 文章图片是粘贴的?得上传才行,没上传成功。

好像只能上传20个图片附件啊?往后面上传就提示错误了。一些图片只能用代码来代替了。然后我删除了几张图片,发现也上传不了了,不知道为什么。

Hmily 发表于 2016-12-16 18:47

howsk 发表于 2016-12-16 17:43
好像只能上传20个图片附件啊?往后面上传就提示错误了。一些图片只能用代码来代替了。然后我删除了几张图 ...

上传图片没限制的,你那有什么提示不?

howsk 发表于 2016-12-16 19:20

Hmily 发表于 2016-12-16 18:47
上传图片没限制的,你那有什么提示不?

Invot Error()什么的,现在能上传了,当时正好上传了第21张的时候就这样了

qtfreet00 发表于 2016-12-16 19:21

楼主说要低调一点,不能证明他太吊

howsk 发表于 2016-12-16 19:21

世事繁华皆成空 发表于 2016-12-16 19:19
@Hmily 楼主小伙伴是我前通付盾同事,他要精华,

我看到了什么?

qtfreet00 发表于 2016-12-16 19:24

楼主答应我的,创业后要招我去做ufo的

howsk 发表于 2016-12-16 19:28

世事繁华皆成空 发表于 2016-12-16 19:24
楼主答应我的,创业后要招我去做ufo的

行了行了,你来做CEO吧
页: [1] 2 3 4 5 6 7 8 9 10
查看完整版本: 百分百企*录8.3网络验证逆向过程