ISTA作为宝马维修车间系统,供全世界经销商机构中使用的BMW Group车辆诊断和编程应用使用。近日获取到了安装程序与对应的数据库,故对其进行了相应的探究。
作为经销商使用的软件,宝马为该程序添加了许可证校验无可厚非。但是它在互联网流传过程中,不但其本体获取存在一定的困难,其许可证的获取也颇有意思,与我们通常能够看到的破解软件有着一定的区别。
程序安装前总计约23.1GB
,完成安装后总计约124GB
,蔚为壮观,主要由对应的数据库文件所组成。
完成安装后打开程序,第一屏就能看到提示许可证失效,需要进行激活。点击两次下一步后就能看到输入具体激活码的页面。随便输入一些字符,显然是无效的,下一步按钮始终为灰色。
程序本身,不论是从哪里找到下载也好,还是从什么论坛购买也好,这都是很正常的情况。但是到这个具体激活的地方,不论是国内论坛还是国外论坛,都可以见到一种,你执行到这一步,获取到key
,将这个key
通过私信发给某个人,由他将对应的license
发给你的操作流程。
而这一步,是免费的。这里,就是有意思的地方了。似乎是有人掌握了keygen
但是并没有将其与软件一并公开,虽然不公开,也没有借此进行盈利,着实有点没有理解其背后具体的原因。同时其主程序甚至使用safengine shielden
进行了加壳。并对签名校验部分的公钥进行了调换。属实有趣。
扯远了,还是回归主题,那么从程序主入口入手,稍作检查,GUI部分是使用.NETFramework 4.8
编写的,那就好办了,代码的查看、编辑等都会方便很多。
以下内容均以版本4.34.40.26161
为基础,在升级4.35.18.26579
后发现该方案尚不完善,无法在新版本上直接应用,具体原因待排查。
从程序入口出发,使用dnSpy
打开程序ISTAGUI.exe
,检查程序组成,原则上,是宁可错杀不可放过,对于相关的校验逻辑,都进行对应的处理。
稍作检查,能发现,程序使用dotfuscator
进行了较为轻度的混淆,主要是部分逻辑流程的打乱和字符串的不可直接识读。使用de4dot
可较为方便得除去相关混淆逻辑。
完整性校验
很快,类型BMW.Rheingold.SecurityAndLicense.IntegrityManager
引起了注意,其构造函数是校验程序完整性的代码,对其稍作整理,可得如下代码。
class IntegrityManager
{
private readonly byte[] _salt = { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff };
private const int _iterations = 1100;
private const string _password = "████████████████";
private readonly string pk_xml = "<RSAKeyValue><Modulus>████████████████</Modulus><Exponent>████</Exponent></RSAKeyValue>";
internal IntegrityManager()
{
const string filePath = "..\\..\\..\\Ecu\\enc_cne_1.prg";
const string sigPath = "..\\..\\..\\Ecu\\sig_gis_1.prg";
const string directoryName = "TesterGUI";
const string searchPattern = "*.dll,*.exe";
try
{
VerifyData(filePath, sigPath);
var encryptedHashFiles = DecryptFile(filePath, _password, _salt, _iterations);
var source = from s in Directory.EnumerateFiles(Environment.CurrentDirectory, "*.*", SearchOption.TopDirectoryOnly)
where searchPattern.Contains(Path.GetExtension(s).ToLower())
select s;
var istaHashFilesToCheck = (from path in source
select new HashFileInfo(path, directoryName)).ToList();
foreach (var istaHashFile in istaHashFilesToCheck)
{
var hashFileInfo = encryptedHashFiles.FirstOrDefault(item => item.FileName.Equals(istaHashFile.FileName));
if (hashFileInfo != null && hashFileInfo.Hash != istaHashFile.Hash)
{
Environment.Exit(0);
}
}
}
catch (Exception ex)
{
Environment.Exit(0);
throw ex;
}
}
private List<HashFileInfo> DecryptFile(string sourceFilename, string password, byte[] salt, int iterations)
{
try
{
var aesManaged = new AesManaged();
aesManaged.BlockSize = aesManaged.LegalBlockSizes[0].MaxSize;
aesManaged.KeySize = aesManaged.LegalKeySizes[0].MaxSize;
var rfc2898DeriveBytes = new Rfc2898DeriveBytes(password, salt, iterations);
aesManaged.Key = rfc2898DeriveBytes.GetBytes(aesManaged.KeySize / 8);
aesManaged.IV = rfc2898DeriveBytes.GetBytes(aesManaged.BlockSize / 8);
aesManaged.Mode = CipherMode.CBC;
var transform = aesManaged.CreateDecryptor(aesManaged.Key, aesManaged.IV);
using var memoryStream = new MemoryStream();
using var cryptoStream = new CryptoStream(memoryStream, transform, CryptoStreamMode.Write);
try
{
using (var fileStream = new FileStream(sourceFilename, FileMode.Open, FileAccess.Read, FileShare.Read))
{
fileStream.CopyTo(cryptoStream);
}
var bytes = memoryStream.ToArray();
return (from row in Encoding.UTF8.GetString(bytes).Split(";;\r\n", StringSplitOptions.RemoveEmptyEntries)
select new HashFileInfo(row.Split(";;", StringSplitOptions.RemoveEmptyEntries))).ToList();
}
catch (Exception)
{
Environment.Exit(0);
}
return null;
}
catch (Exception)
{
Environment.Exit(0);
}
return null;
}
private void VerifyData(string fileToVerify, string signaturePath)
{
try
{
using var rsacryptoServiceProvider = new RSACryptoServiceProvider();
var buffer = File.ReadAllBytes(fileToVerify);
var signature = File.ReadAllBytes(signaturePath);
try
{
rsacryptoServiceProvider.FromXmlString(pk_xml);
new SHA512Managed().ComputeHash(signature);
if (!rsacryptoServiceProvider.VerifyData(buffer, CryptoConfig.MapNameToOID("SHA1"), signature))
{
Environment.Exit(1);
}
}
catch (Exception ex)
{
Environment.Exit(1);
throw ex;
}
finally
{
rsacryptoServiceProvider.PersistKeyInCsp = false;
}
}
catch (Exception)
{
Environment.Exit(1);
}
}
public class HashFileInfo
{
internal string FileName { get; private set; }
internal string FilePath { get; private set; }
internal string Hash { get; set; }
protected internal HashFileInfo(string[] fileInfos)
{
FilePath = fileInfos[0];
FileName = Path.GetFileName(FilePath);
Hash = fileInfos[1];
}
protected internal HashFileInfo(string path, string directoryName)
{
FileName = Path.GetFileName(path);
FilePath = (string.IsNullOrEmpty(directoryName) ? path : path.Remove(0, path.IndexOf(directoryName) + directoryName.Length + 1));
Hash = CalculateHash(path);
}
private string CalculateHash(string pathFile)
{
using var sha = SHA256.Create();
using var fileStream = File.OpenRead(pathFile);
return Convert.ToBase64String(sha.ComputeHash(fileStream));
}
}
}
该部分代码,主要包含两个步骤:
- 通过校验对应的
RSA
签名,以校验enc_cne_1.prg
是否篡改,供进一步校验使用。 - 通过读取
enc_cne_1.prg
获取需要校验的文件,逐个计算SHA256
值以校验文件是否被篡改。
上述中任意一步的校验不一致,均将导致程序退出,使程序无法使用,那么显然,本次修改所涉及的最大范围,限于此列表。
考虑到程序端不存在RSA私钥,使用RSA公钥来校验完整性,看似安全,但是,通过自行生成一对密钥对文件重新进行签名。这样一来,就能够更新上述列表中任意文件而不会触发起校验的异常。
以上方法属于顺着程序本身的思路来的。实际操作上,直接将这段代码移除,就会使校验不通过导致程序退出这个情况彻底不存在。
PS: 此处有一个暗坑,enc_cne_1.prg
中的文件列表,不包含IstaOperation.exe
,如果以其其中的列表作为执行补丁的范围,则会导致该文件被漏掉,然而该程序中,却是包含IntegrityManager
的,从而导致整个程序的部分功能,没法正常执行。
那么还有哪里有程序完整性的校验阻碍修改程序呢,进行对应的调试与查找,BMW.Rheingold.CoreFramework.WcfCommon.IstaProcessStarter
下的中也存在相关的代码CheckSignature
,对其稍作整理,代码如下。
private static void CheckSignature(string pathToIstaProcessFile)
{
try
{
Assembly executingAssembly = Assembly.GetExecutingAssembly();
if (!Assembly.ReflectionOnlyLoadFrom(pathToIstaProcessFile).GetName().GetPublicKeyToken().SequenceEqual(executingAssembly.GetName().GetPublicKeyToken())) {
throw new InvalidOperationException();
}
}
catch (Exception)
{
throw new IstaProcessStartException();
}
}
在启动其他子程序之前,会通过校验当前程序的publickey
与子程序的publickey
是否一致,来校验程序的完整性。
那么显然的,只要不校验,就不存在这个问题了。
校验请求生成
在处理具体许可证生成前,可以先看一眼,程序的key是怎么生成的,可以在BMW.Rheingold.CoreFramework.LicenseManagement.LicenseWizardHelper
处,找到CalculateLicenseRequest
方法。没有什么特殊的,就是生成机器特征码,收集一些其他信息,生成xml
并转换为base64
编码。
许可证校验
一旦程序已经失去了完整性校验,那么程序对于许可证的校验也就完全失去了把控。
通过检索,可以找到namespace BMW.Rheingold.CoreFramework.LicenseManagement
下的LicenseStatusChecker
,对其稍作整理,可得如下代码。
public class LicenseStatusChecker
{
private CharacteristicsGenerator characteristicsGenerator = new CharacteristicsGenerator();
internal LicenseStatus Check(LicenseInfo testLicInfo) => LicenseStatusChecker.IsToyota() ? LicenseStatusChecker.IsValidToyotaLicense() : this.IsLicenseValid(testLicInfo, false);
private static bool IsToyota() => IndustrialCustomerManager.Instance.IsIndustrialCustomerBrand("TOYOTA");
private static LicenseStatus IsValidToyotaLicense() => IndustrialCustomerManager.Instance.Worker.LicenseStatus != IndustrialCustomerLicenseStatus.VALID ? LicenseStatus.INVALID : LicenseStatus.VALID;
internal LicenseStatus IsLicenseValid(LicenseInfo testLicInfo, bool isid)
{
try
{
if (testLicInfo == null)
return LicenseStatus.INVALID;
LicenseInfo licenseInfo = (LicenseInfo) testLicInfo.Clone();
this.GetComputerCharacteristics(isid, licenseInfo);
foreach (byte[] revocation in Licenses.RevocationList)
{
if (((IEnumerable<byte>) licenseInfo.ComputerCharacteristics).SequenceEqual<byte>((IEnumerable<byte>) revocation))
return LicenseStatus.INVALID;
}
if (LicenseStatusChecker.IsInvalidISIDLicense(isid, licenseInfo))
{
Log.Error(string.Empty, "ISTA Activation failed");
return LicenseStatus.INVALID;
}
byte[] licenseKey = licenseInfo.LicenseKey;
licenseInfo.LicenseKey = new byte[0];
byte[] hashValueFrom = this.GetHashValueFrom(licenseInfo);
if (hashValueFrom == null || licenseKey == null)
{
Log.Warning(string.Empty, "ISTA Activation failed");
return LicenseStatus.INVALID;
}
if (BMW.Rheingold.CoreFramework.CoreFramework.DebugLevel > 0)
Log.Info(string.Empty, "Start to verify...");
if (!this.GetRSAPKCS1SignatureDeformatter().VerifySignature(hashValueFrom, licenseKey))
{
Log.Warning(string.Empty, "ISTA Activation failed");
return LicenseStatus.INVALID;
}
ulong subversion1 = (ulong) RuntimeEnvironment.GetSubversion(0U);
ulong subversion2 = (ulong) RuntimeEnvironment.GetSubversion(1U);
uint eax = 0;
uint ebx = 0;
uint ecx = 0;
uint edx = 0;
RuntimeEnvironment.GetSubVersion(1U, out eax, out ebx, out ecx, out edx);
ecx.IsBitSet<uint>(31);
if (BMW.Rheingold.CoreFramework.CoreFramework.DebugLevel > 0)
{
Log.Info(string.Empty, "Environment found: {0:X} {1:X}", (object) subversion1, (object) subversion2);
Log.Info(string.Empty, "CPU features found: {0:X} {1:X} {2:X} {3:X}", (object) eax, (object) ebx, (object) ecx, (object) edx);
}
if (LicenseStatusChecker.EnvCheck())
{
if (licenseInfo.SubLicenses != null)
{
foreach (LicensePackage subLicense in licenseInfo.SubLicenses)
{
if (string.Equals(subLicense.PackageName, "SyntheticEnv") && (subLicense.PackageExpire == DateTime.MinValue || subLicense.PackageExpire > DateTime.Now))
return LicenseStatus.VALID;
}
}
return LicenseStatus.INVALID;
}
if (licenseInfo.Expiration > DateTime.Now)
{
Log.Debug(string.Empty, "ISTA Activation succeeded");
return LicenseStatus.VALID;
}
Log.Warning(string.Empty, "ISTA Activation failed");
return LicenseStatus.EXPIRED;
}
catch
{
Log.Warning(string.Empty, "ISTA Activation failed");
}
return LicenseStatus.INVALID;
}
private void GetComputerCharacteristics(bool isid, LicenseInfo licenseInfo)
{
if (isid)
licenseInfo.ComputerCharacteristics = this.characteristicsGenerator.GetISIDCharacteristics();
else
licenseInfo.ComputerCharacteristics = this.characteristicsGenerator.GetComputerCharacteristics();
}
private byte[] GetHashValueFrom(LicenseInfo licInfo)
{
MemoryStream memoryStream = new MemoryStream();
new XmlSerializer(typeof (LicenseInfo)).Serialize((Stream) memoryStream, (object) licInfo);
byte[] buffer = memoryStream.GetBuffer();
if (BMW.Rheingold.CoreFramework.CoreFramework.DebugLevel > 0)
Log.Info(string.Empty, "licInfo stream: {0}", (object) FormatConverter.ByteArray2String(buffer, (uint) buffer.Length));
return SHA1.Create().ComputeHash(buffer);
}
private RSAPKCS1SignatureDeformatter GetRSAPKCS1SignatureDeformatter()
{
RSACryptoServiceProvider key = new RSACryptoServiceProvider();
key.ImportParameters(new RSAParameters()
{
Modulus = Convert.FromBase64String("████████████████"),
Exponent = Convert.FromBase64String("████")
});
RSAPKCS1SignatureDeformatter signatureDeformatter = new RSAPKCS1SignatureDeformatter((AsymmetricAlgorithm) key);
signatureDeformatter.SetHashAlgorithm("SHA1");
return signatureDeformatter;
}
private static bool IsInvalidISIDLicense(bool isid, LicenseInfo licInfo) => isid && !string.IsNullOrEmpty(licInfo.ComputerName) && !Regex.Match(Environment.MachineName, licInfo.ComputerName).Success;
public static bool EnvCheck()
{
using (ManagementObjectSearcher managementObjectSearcher = new ManagementObjectSearcher("Select * from Win32_ComputerSystem"))
{
using (ManagementObjectCollection objectCollection = managementObjectSearcher.Get())
{
foreach (ManagementBaseObject managementBaseObject in objectCollection)
{
string lower = managementBaseObject["Manufacturer"].ToString().ToLower();
if (lower == "microsoft corporation" && managementBaseObject["Model"].ToString().ToUpperInvariant().Contains("VIRTUAL") || lower.Contains("vmware") || lower.Contains("parallels") || managementBaseObject["Model"].ToString() == "VirtualBox")
return true;
}
}
}
return false;
}
}
内容也很有意思,可以发现,换标Z4的丰田的Supra,也是使用这套程序进行车辆诊断,但是使用的是另一套激活码,两者并不通用。其程序本体两者也并不通用,破解方式也有所区别,暂时还未具体研究,这里不作展开了。
可以发现,首先,它对收到的LicenseInfo
序列化求SHA-1(不包含licenseKey
),同时提取出其中的licenseKey
,然后用程序预置的公钥初始化RSAPKCS1SignatureDeformatter
验证签名是否有效。所以另一种思路就更加直接了,只要我直接把你这个密钥对给替换了,并实现一下签名的操作,那我自己就能给license request签名给签上了,就不需要各种道高一尺魔高一丈的操作了,实现其实类似,主要的问题还是在如何通过程序化的方法将修改il的操作自动化。
同时还可以看到简单的对虚拟机的校验,这应该也就是有些地方反复强调不能在虚拟机下安装的原因吧。
显然,简单地将IsLicenseValid
的返回值,修改为LicenseStatus.VALID
,就不再存在校验这个操作了。
但是,仅对以上部分进行修改,看似系统已经能够正常使用了,但是选择完车辆信息后,仍然无法查看维护信息,进步检索发现类型BMW.Rheingold.CoreFramework.LicenseManager
中的VerifyLicense
函数,也是大同小异,分门别类地进行了校验,或者比如不允许系统时间早于程序编译时间等等。
以上就是系统能够正常查看维护手册所需要进行的补丁内容。
为了使程序能够正常对车辆完成诊断,程序还会通过PsdzServiceImpl.dll
与另一个使用java编写的PSdZ
模块进行交互,而交互的过程中,会抛出一组参数不能同时为空的异常,经过查找相关代码如下:
private static String generateOrganizationId(int dealerId, int plantId) {
String organizationId;
if (dealerId == 0) {
if (plantId == 0) {
throw new PSdZArgumentException(PSdZErrorCodes.DEALER_ID_AND_PLANT_ID_MUST_NOT_BOTH_BE_0);
}
organizationId = Integer.toString(plantId, 16);
} else if (plantId != 0) {
throw new PSdZArgumentException(PSdZErrorCodes.EXACTLY_ONE_OF_DEALER_ID_AND_PLANT_ID_MUST_BE_0);
} else {
organizationId = Integer.toString(dealerId, 16);
}
return organizationId;
}
所以,除了激活的校验以外,还需要定位到调用PsdzServiceImpl.dll
时,传入的参数对应的函数,将其修改为非零值,这样就能够正常进行诊断了。
完成激活
对相应文件进行替换之后,理论上启动时无需输入相关信息,可直接进入。
但是在使用过程中,日志里还是能看到许多提示激活失败的记录,查看无大碍,部份功能受影响,经排查,可以在注册表或者配置文件增加一个证书,确保程序在一些情况下,能正常读取到一个非空的证书信息,以减少这类报错。
当然,也可以通过在ISTAGUI.exe.config
的appSettings
下增加License
键值对,注意字符串转义。
另外的,编程功能需要修改注册表配置为True
方可启用,路径为HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\BMWgroup\ISPI\Rheingold
- BMW.Rheingold.Programming.Enabled
- BMW.Rheingold.Programming.ExpertMode
由于本人实际并没有具体使用经验,若出现问题,欢迎反馈并共同排查。
相关链接
- 基于本文实现的ISTA-Patcher
- 上文提到的需要添加的注册表文件