百分百企*录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、最后祝自己公司开业大吉,生意兴隆。
610100 发表于 2016-12-17 20:30
我小白一个,不知大神用什么软件破解.NET。有什么好的视频教程吗?
谢谢大神。
使用dnSpy反编译。 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,请问是什么原因呢?请大神解疑,非常感谢! @howsk 文章图片是粘贴的?得上传才行,没上传成功。 Hmily 发表于 2016-12-16 16:04
@howsk 文章图片是粘贴的?得上传才行,没上传成功。
是粘贴的,H大您稍等,我现在重新编辑上传 Hmily 发表于 2016-12-16 16:04
@howsk 文章图片是粘贴的?得上传才行,没上传成功。
好像只能上传20个图片附件啊?往后面上传就提示错误了。一些图片只能用代码来代替了。然后我删除了几张图片,发现也上传不了了,不知道为什么。 howsk 发表于 2016-12-16 17:43
好像只能上传20个图片附件啊?往后面上传就提示错误了。一些图片只能用代码来代替了。然后我删除了几张图 ...
上传图片没限制的,你那有什么提示不? Hmily 发表于 2016-12-16 18:47
上传图片没限制的,你那有什么提示不?
Invot Error()什么的,现在能上传了,当时正好上传了第21张的时候就这样了 楼主说要低调一点,不能证明他太吊 世事繁华皆成空 发表于 2016-12-16 19:19
@Hmily 楼主小伙伴是我前通付盾同事,他要精华,
我看到了什么? 楼主答应我的,创业后要招我去做ufo的 世事繁华皆成空 发表于 2016-12-16 19:24
楼主答应我的,创业后要招我去做ufo的
行了行了,你来做CEO吧