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.configappSettings下增加License键值对,注意字符串转义。

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

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

由于本人实际并没有具体使用经验,若出现问题,欢迎反馈并共同排查。

相关链接