ISTA作为宝马维修车间系统,供全世界经销商机构中使用的BMW Group车辆诊断和编程应用使用。近日获取到了安装程序与对应的数据库,故对其进行了相应的探究。

作为经销商使用的软件,宝马为该程序添加了许可证校验无可厚非。但是它在互联网流传过程中,不但其本体获取存在一定的困难,其许可证的获取也颇有意思,与我们通常能够看到的破解软件有着一定的区别。

程序安装前总计约23.1GB,完成安装后总计约124GB,蔚为壮观,主要由对应的数据库文件所组成。

完成安装后打开程序,第一屏就能看到提示许可证失效,需要进行激活。点击两次下一步后就能看到输入具体激活码的页面。随便输入一些字符,显然是无效的,下一步按钮始终为灰色。

许可证警告 许可证信息填写 许可证激活

程序本身,不论是从哪里找到下载也好,还是从什么论坛购买也好,这都是很正常的情况。但是到这个具体激活的地方,不论是国内论坛还是国外论坛,都可以见到一种,你执行到这一步,获取到key,将这个key通过私信发给某个人,由他将对应的license发给你的操作流程。

而这一步,是免费的。这里,就是有意思的地方了。似乎是有人掌握了keygen但是并没有将其与软件一并公开,虽然不公开,也没有借此进行盈利,着实有点没有理解其背后具体的原因。

扯远了,还是回归主题,那么从程序主入口入手,稍作检查,GUI部分是使用.NETFramework 4.8编写的,那就好办了,代码的查看、编辑等都会方便很多。


以下内容均以版本4.34.40.26161为基础,在升级4.35.18.26579后发现该方案尚不完善,无法在新版本上直接应用,具体原因待排查。

从程序入口出发,使用dnSpy打开程序ISTAGUI.exe,检查程序组成,原则上,是宁可错杀不可放过,对于相关的校验逻辑,都进行对应的处理。

稍作检查,能发现,程序使用dotfuscator进行了较为轻度的混淆,主要是部分逻辑流程的打乱和字符串的不可直接识读。使用de4dot可较为方便得除去相关混淆逻辑。

完整性校验

很快,类型BMW.Rheingold.SecurityAndLicense.IntegrityManager引起了注意,其构造函数是校验程序完整性的代码,对其稍作整理,可得如下代码。

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;

namespace BMW.Rheingold.SecurityAndLicense
{
  class IntegrityManager
  {
    private readonly byte[] _salt = { 0xd, 0xca, 0x32, 0xe0, 0x7f, 0xa4, 0xdf, 0xf1 };

    private const int _iterations = 1100;

    private const string _password = "3/3HexbKKFs4LqpiCSgKAXGUYCtqjoFchfPitAmI8wE=";
    
    private readonly string pk_xml = "<RSAKeyValue><Modulus>xW33nQA29jyJSYn24fVcSIU3gQmzQArcT0lrPAj94PS8wuZZBpPZsLEWo4pkq2/w9ne4V9PTOkB2frVBvA/bmGF/gyHivqkzi7znX/TwcTM6GbX/MN4isNeXqgFZzjmxOh9EYPt8pnJ/j02Djbg8LceG98grBCehBe/2wFxxYQQa+YoJ0a1ymzs/3geBTeqtwYgayZeLEWOxckoDuDu0RWF8zvVcWxUNpwqHNH/4Boo+xLqByfEv2wDS1zchGtjCL+g2qdDWlHgASEgGZ6Z8hbirrxxWYZ7zaZxjSADQM8nweKn4t4+p44uD1Aoktq3Mm+jZtTsgk8i1YjbCQN8J1Q==</Modulus><Exponent>AQAB</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,对其稍作整理,可得如下代码。

using BMW.Rheingold.CoreFramework.IndustrialCustomer;
using BMW.Rheingold.CoreFramework.IndustrialCustomer.Manager;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Management;
using System.Security.Cryptography;
using System.Text.RegularExpressions;
using System.Xml.Serialization;

namespace BMW.Rheingold.CoreFramework.LicenseManagement
{
  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("6HCAK5V6K6BeeSyEt2ywgS2SmoCNDD4Y+JJ3imZjQIkiL0z0TOBZ9VbuUXzLCE30bwuCcprQDlg+mkd7xYqnBmDdkOFYliv43dHwAWQN+jxwQYSohr7EAEIoAsy4J2/y8scuJTsUXJ15uyeyYafZduiriJutNny1jQDhJCNR8fiT/cO27c8oJb5vF1eH7geaG3fj3RCgg+nTRAEQUnywaYNLT5F1ULRqwO7qcVYcOtw9eVqtvkagVXtysSyKbvW7nAzgGwnbVFyXPkO86kmjraa5iqan+TWjh5oAkjR50xuKVC2O1P7lHezrKfHHTtAtaEtrDMf/WfWry8muMmKDNQ=="),
        Exponent = Convert.FromBase64String("AQAB")
      });
      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验证签名是否有效。

同时还可以看到简单的对虚拟机的校验,这应该也就是有些地方反复强调不能在虚拟机下安装的原因吧。

显然,简单地将IsLicenseValid的返回值,修改为LicenseStatus.VALID,就不再存在校验这个操作了。


但是,仅对以上部分进行修改,看似系统已经能够正常使用了,但是选择完车辆信息后,仍然无法查看维护信息,进步检索发现类型BMW.Rheingold.CoreFramework.LicenseManager中的VerifyLicense函数,由于校验函数本身的状态机反编译出来缺乏可读性,同时字符串进行了一定的编码,也无法直接识读,就不再进行粘贴了。同样进行相应的移除处理。

以上就是系统能够正常查看维护手册所需要进行的补丁内容,后续还找到额外的一些检查,具体各个函数涉及到的范围尚不明确,具体详见文末的总清单。

完成激活

对相应文件进行替换之后,理论上启动时无需输入相关信息。

完成激活

保险起见,可以执行如下注册表文件,确保程序在一些情况下,能正常读取到一个非空的证书信息

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\BMWgroup\ISPI\Rheingold]
"License"="<?xml version=\"1.0\"?><LicenseInfo xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns=\"http://tempuri.org/LicenseInfo.xsd\"><Name>BMW Group MOSSR2</Name><Email>[email protected]</Email><Expiration>2099-05-01T00:00:00</Expiration><Comment /><ComputerName xsi:nil=\"true\" /><UserName>*</UserName><AvailableBrandTypes>*</AvailableBrandTypes><AvailableLanguages>*</AvailableLanguages><AvailableOperationModes>*</AvailableOperationModes><DistributionPartnerNumber>*</DistributionPartnerNumber><ComputerCharacteristics>wePC9Q9xawMjVPA2fkMVvioHNtA=</ComputerCharacteristics><LicenseKey>cLx6S3h9q7XdEbVuTMn4qNgkG+PQk2NDwbytMFK6mMvPtm+qC5t67bMsx/lntZkyPALFlMrbhHYWnx4xCsP5G6CHUxpUCP5XLhak3ipm3Bou+229lJRwAAHz0IH91vC4QnILJEuBWlPcrUV/oZKoN47hXiLxQ19O0jHw1cuuhqs=</LicenseKey><LicenseServerURL xsi:nil=\"true\" /><LicenseType>offline</LicenseType></LicenseInfo>"

另外的,编程功能需要修改注册表配置为True方可启用,路径为HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\BMWgroup\ISPI\Rheingold

  • BMW.Rheingold.Programming.Enabled
  • BMW.Rheingold.Programming.ExpertMode

范围统计

此处仅挑选最主要的补丁内容,其余的请参见具体程序实现。

应用完整性校验IntegrityManager::.ctor

  • ISTAGUI.exe
  • IstaOperation.exe
  • IstaOperationImpl.dll
  • IstaServicesHost.exe
  • IstaServicesImpl.dll

应用完整性校验IstaProcessStarter::CheckSignature

  • RheingoldCoreFramework.dll

激活码有效性校验LicenseStatusChecker::IsLicenseValid

  • Authoring.dll
  • ISTAGUI.exe
  • IstaOperationController.dll
  • IstaOperationImpl.dll
  • IstaServicesImpl.dll
  • RGSPC.exe
  • RheingoldDatabaseOracleConnector.dll
  • RheingoldDatabasePostgreSQLConnector.dll
  • RheingoldDatabaseSQLiteConnector.dll
  • RheingoldDiagnostics.dll
  • RheingoldFASTA.dll
  • RheingoldInfoProvider.dll
  • RheingoldISPINext.dll
  • RheingoldISTACoreFramework.dll
  • RheingoldMeasurement.dll
  • RheingoldMeasurementCommunication.dll
  • RheingoldPresentationFramework.dll
  • RheingoldProgramming.dll
  • RheingoldSessionController.dll
  • RheingoldVehicleCommunication.dll
  • RheingoldxVM.dll
  • WsiDataProvider.dll

激活码有效性校验LicenseManager::VerifyLicense

  • Authoring.dll
  • ISTAGUI.exe
  • IstaOperationController.dll
  • IstaOperationImpl.dll
  • IstaServicesImpl.dll
  • RGSPC.exe
  • RheingoldDatabaseOracleConnector.dll
  • RheingoldDatabasePostgreSQLConnector.dll
  • RheingoldDatabaseSQLiteConnector.dll
  • RheingoldDiagnostics.dll
  • RheingoldFASTA.dll
  • RheingoldInfoProvider.dll
  • RheingoldISPINext.dll
  • RheingoldISTACoreFramework.dll
  • RheingoldMeasurement.dll
  • RheingoldMeasurementCommunication.dll
  • RheingoldPresentationFramework.dll
  • RheingoldProgramming.dll
  • RheingoldSessionController.dll
  • RheingoldVehicleCommunication.dll
  • RheingoldxVM.dll
  • WsiDataProvider.dll

总结

由于当前仅在系统中纯安装了该软件,而未实际连接OBD测试是否具体功能上也已经可用,当前还待进一步的测试。有机会后续再进行更新。

而这时候反过来翻看论坛的版本其主程序甚至使用safengine shielden进行了加壳。并对签名校验部分的公钥进行了调换。属实有趣。

追记

在编写程序化补丁程序的过程中发现了程序中额外的几个激活码校验函数,由于已经完成补丁程序的编写,就不赘述其他部分的变动了,详见ISTA-Patcher

原版程序可以在这里下到。 Supra程序可以在其官网下到。