大神论坛

找回密码
快速注册
查看: 152 | 回复: 0

[原创] 对某商业版带VMP网络验证的逆向破解 附详细教程和代码

主题

帖子

0

积分

初入江湖

UID
563
积分
0
精华
威望
0 点
违规
大神币
68 枚
注册时间
2023-09-16 14:57
发表于 2024-01-27 22:30
本帖最后由 xuna2009 于 2024-01-27 22:30 编辑

实战某商业网络验证(带VMP)的多维破解

提示:本文仅供参考学习,请勿用于非法用途!如有侵权联系我删除,为了保障他人权益,所以文中数据都是残缺或修改过的,更多的是思路分析。

1.查壳

vmp的壳,静态分析基本戏不大,那就只能动态调试了。但是为了思路完整,我们先抓包看看这个网络验证是那家的。

维度1:内存补丁

2.破解开始

随便输入一些错误的数据进行登陆得到网络信息如上,即可知道改网络验证的官网,那就访问注册一个开发者账号

得到一个作者提供的对接源码,下载下来观摩一下

果然是vmp,但是不影响,继续找找其他重要的逻辑

例子中进入启动窗口首先把登陆成功窗口可视状态设置为假,调用自定义窗口,这个窗口就是账号密码验证窗口,那么我们就看这个窗口逻辑

哦,核心就是这个xx子程序_软件初始化了,里面的参数很关键,按照参数注解得知这些参数决定了该软件在验证平台的id和加解密方式和密钥,接着往下看

作者也是直接用他封装的模块函数来调用登陆过程和结果,防止有心之人更为详细的分析,只不过这种小把戏防君子不妨小人啊(开玩笑),那就直接开始动调,一般的OD调不了,大家自己在论坛找有反反调试插件的OD或者自己装一下插件

3.流程分析

首先,我们需要拿到能用的信息能够帮我们更好的分析

3.1 抓包得到的数据格式

{
"data":"2xxxxxxxxD645FFCEB812Dxxxxxxxxxx3ED54A10B9EC20F13D1C833556413AAEC0Cxxxxxxxxx51826FE3CF8A7DCA3EDBF766F5F31D8088DC67F6BB1998EB028CCC97F1EA523xxxxxxxxxxxxxxF6B5",
"sign":"xx8xxxxa6axx5",
"goodscode":"xxxxxx5xxxxe3",
"timestamp":1706067168549,
"businessid":2002,
"inisoftkey":"xxxxxxxxxxx7cxxxxxxa2e68xxxxxefdd3xxxx05f8d7xxxxxxxxxxxxx3605xxxxxxxxxxe10xxxxxb85",
"encrypttypeid":3,
"platformtypeid":1,
"platformusercode":"xxxxx60xxx41xxxx1"
}

3.2 参数填充

我们可以看到包里有好几个参数英文名对应的就是初始化时候的参数名,那我们在第一个窗口创建的时候我们就下断点看静态资源的这些参数是否解密出来,断点CreateWindowExW(易语言用这个创建),之所以我们下在这里是因为我们的目的是找到是哪里访问了这些参数,也就可以间接的找出这个初始化过程,也就可以有更大的动手空间。如果我们下其他api可能他是在第二个窗口(验证登陆窗口)创建结束以后才调用,导致我们不能通过访问断点找到它的验证初始化过程。

成功断下,我们直接去暴力搜索内存有没有platformusercode对应的值

在内存映射窗口呢直接搜就搜索到了,那么我们就直接下硬件访问断点就找到了验证初始化的地方了

这里有7个字符串,我们就将这几个字符串参数按照官网给的长度提示多次测试那些字符串对应的是那些值,因为提供的对接源码里只要你参数填写正确就可以初始化成功,经过几次测试呢我也就调试出来了这些对应的值。此时我的思路就是在初始化的时候将这些值修改为我自己的参数,那不就可以了吗?因为vmp脱壳修复成本太大,所以就打内存补丁算了,说干就干,直接放我写的内存补丁源码:

#include <windows.h>
#include <tlhelp32.h>
#include <iostream>
#include <vector>

BOOL EnableDebugPrivilege(BOOL bEnable)
{

BOOL fOK = FALSE;
HANDLE hToken;
if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken)) //打开进程访问令牌
{
//试图修改“调试”特权
TOKEN_PRIVILEGES tp;
tp.PrivilegeCount = 1;
LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &tp.Privileges[0].Luid);
tp.Privileges[0].Attributes = bEnable ? SE_PRIVILEGE_ENABLED : 0;
AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL);
fOK = (GetLastError() == ERROR_SUCCESS);
CloseHandle(hToken);
}
return fOK;
}

// 根据进程名获取进程ID
DWORD GetProcessIdByName(const wchar_t* processName) {
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnapshot == INVALID_HANDLE_VALUE) {
return 0;
}

PROCESSENTRY32 processEntry;
processEntry.dwSize = sizeof(PROCESSENTRY32);

if (Process32First(hSnapshot, &processEntry)) {
do {
if (_wcsicmp(processEntry.szExeFile, processName) == 0) {
CloseHandle(hSnapshot);
return processEntry.th32ProcessID;
}
} while (Process32Next(hSnapshot, &processEntry));
}

CloseHandle(hSnapshot);
return 0;
}

// 回调函数用于枚举窗口
BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam) {
std::vector<HWND>* windowList = reinterpret_cast<std::vector<HWND>*>(lParam);
windowList->push_back(hwnd);
return TRUE;
}

std::vector<HWND> GetWindowsByProcessId(DWORD targetProcessId) {
std::vector<HWND> windowsList;

EnumWindows(EnumWindowsProc, reinterpret_cast<LPARAM>(&windowsList));

for (HWND hwnd : windowsList) {
DWORD windowProcessId;
GetWindowThreadProcessId(hwnd, &windowProcessId);

if (windowProcessId == targetProcessId) {

SetWindowLong(hwnd, GWL_STYLE, 382337024);
}
}

return windowsList;
}

int main() {
const wchar_t* targetProcessName = L"xxxx.exe"; // 你的目标进程名

EnableDebugPrivilege(TRUE);

DWORD targetProcessId = GetProcessIdByName(targetProcessName);
if (targetProcessId == 0) {
std::cerr << "无法获取目标进程ID。" << std::endl;
return 1;
}

HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, targetProcessId);
if (hProcess == NULL) {
std::cerr << "无法打开目标进程。错误代码:" << GetLastError() << std::endl;
return 1;
}

LPVOID targetAddress = (LPVOID)0x11111111111;

DWORD orgnal;

VirtualProtectEx(hProcess, targetAddress, 1, PAGE_EXECUTE_READWRITE, &orgnal);

unsigned char dataToWrite[] = { 0x00, 0x11, 0x11, 0x2E, 0x111, 0x00, 0x11, 0x66, 0x31, 0x32,
0x30, 0x11, 0x11, 0x11, 0x00, 0x65, 0x62, 0x30, 0x66, 0x64,
0x39, 0x11, 0x11, 0x00, 0x34, 0x34, 0x11, 0x65, 0x65, 0x35,
0x33, 0x15, 0x35, 0x34, 0x61, 0x11, 0x11, 0x11, 0x63, 0x39,
0x00, 0x11, 0x35, 0x63, 0x66, 0x11, 0x11, 0x11, 0x36, 0x38,
0x30, 0x11, 0x66, 0x33, 0x65, 0x31, 0x64 };

// 写入内存
if (WriteProcessMemory(hProcess, targetAddress, &dataToWrite, sizeof(dataToWrite), NULL)) {
std::cout << "破解成功" << std::endl;
}
else {
std::cerr << "破解失败" << GetLastError() << std::endl;
}

// 关闭目标进程的句柄
CloseHandle(hProcess);

getchar();

return 0;
}

自己申请账号后把自己验证信息替换为它原来的即可,一登陆就成功了

但是,进入软件界面发现没效果,一看官网文档发现作者提供了一个功能并给出了解释

哎,他说他想累死我,越想越气,那我就一定要干开你

维度2:网络解密

回到包数据和对接源码中去,买了一个正版被破解的软件账号后开始抓所有数据流

果不出其然,登陆成功以后就下发了很多个包而且短时间内没有增多,也就是说这些包只有一个可能是心跳,其他的可能就是作者说的远程变量了,那我开始了,你想那个封装的登陆函数去(验证作者提供的模块内),易语言直接有ec解e的,大家网上找就行,我直接给出加密部分的部分逻辑(加密环节中的一节)

解密反过来就可,那我们就对所有请求数据包解密查看内容,其中有几个数据包部分内容是:

{"token":"fasdsdaasdddsd4d127","maccode":"ddd","varname":"刂叵蓖嚼纟","timestamp":1706070671844,"requestflag":"ddd","cardnumorusername":"ddd"}

我们看到有一个varname的值,值变量?这就是远程变量的名字吗?再看验证作者提示的源码,的确是通过远程名字获取值

邪恶的想法来了,我在原来山寨的基础上给我的服务端加上这些远程变量不久行了吗?(只不过前提是把变量值和名称都需要得到)

结果我一添加,诶,您猜怎么着,那叫一个地道,不允许名称重复

那我就只能直接欺骗接管咯(应该说这样最省事,否则又要去打补丁替换远程变量名)

维度3  本地s端服务接管

这里使用了易语言编写,将原来的端口服务本地host转到本地端口来启动服务,处理post和get请求即可,心跳也是在数据包了,解密处理对应请求即可,展示部分服务代码


最后就是成功截图了

完结!


注:若转载请注明大神论坛来源(本贴地址)与作者信息。

返回顶部