65.9K
CodeProject 正在变化。 阅读更多。
Home

检查时钟篡改以延长许可证有效期

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.37/5 (12投票s)

2016 年 5 月 26 日

CPOL

5分钟阅读

viewsIcon

44298

downloadIcon

1074

帮助防止时钟篡改并强制执行软件许可证

引言

如果您提供的应用程序使用基于时间的许可证,您如何防止用户通过简单地更改时钟来规避您的许可证条款?

我有一个我下载的试用版防病毒软件,但在安装时,我将我的时钟向前拨了 20 年。现在试用期直到 2035 年才过期。该软件的作者本可以轻易地阻止我这样做……但怎么做呢?

背景

几年前,我编写了一个快速而粗糙的系统,使用 RSA 非对称加密来实现许可证检查,其中许可证数据使用 RSA 签名并使用公钥进行检查。

这是一篇非常受欢迎的文章,引发了大量的评论和求助请求。

其中一个问题是如何防止用户通过简单地更改时钟来阻止许可证条款过期。

从那时起,互联网连接已从某些计算机偶尔可用,发展到几乎所有计算机都可随时连接,这对于许可证检查来说是一个巨大的帮助。

例如,如果您将时钟设置为比当前时间早数年或数月,那么相当一部分互联网服务将无法正常工作。这也意味着您可以通过互联网时间服务器检查时间。

基本检查

我知道的最简单的时钟篡改检查形式是检查某个必需文件(例如配置文件)的日期,确保它在过去,然后将日期设置为当前日期。每次应用程序启动时都这样做。如果用户将时钟回拨,那么该文件的日期将是未来的,您就知道时钟被篡改了。

        /// <summary>
        /// check the creation date/time on an essential file.
        /// </summary>
        /// <returns></returns>
        public static bool EssentialFileDateCheck()
        {
            // use the config file:
            var me = Assembly.GetExecutingAssembly().Location + ".config";
            if (File.Exists(me))
            {
                // get the existing last-write
                var createdOn = File.GetCreationTime(me);
                if (createdOn < DateTime.Now)
                {
                    // set the last write time on the assembly:
                    File.SetCreationTime(me, DateTime.Now);
                    return true;
                }
            }
            return false;
        }

这是一个非常天真的实现,很容易被绕过——虽然在操作系统中设置文件的创建时间很棘手,但在 .NET 中显然很容易做到。

更高级的检查

我认为我们可以假设任何真正试图规避许可过程的人都可以使用 .NET Reflector 或 JustDecompile 查看程序集。

因此,应该做的是检查一个不容易查找和重置的文件,所以首先,它不应该在应用程序的目录中,并且它的文件名不应该容易预测,并且从代码中很难确切地知道那个文件是什么。

我选择通过哈希程序集名称和计算机名称来创建文件名,然后将哈希转换为十六进制字符串,并将其放入 `Environment.SpecialFolder.ApplicationData` 特殊路径下作为一个文件夹,文件位于其下方,伪装成一个 DLL……

这是因为 Microsoft 经常留下看起来非常相似的文件夹,并且包含 DLL。文件夹的日期设置为一个很早的时间点,并且文件日期会有一个差异,因此它不会显示为今天创建或修改。

// compute a hash from assembly name and machine name, use this to create a folder name (like ms temp install folders)
var folder = (HashAlgorithmName.MD5.ComputeHash(Encoding.UTF8.GetBytes(Environment.MachineName + Assembly.GetEntryAssembly().FullName))).ToOneWayHex();

// work out a spot for the time file
var fn = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), 
                      folder, 
                      Path.ChangeExtension(Environment.MachineName.Clean(Path.GetInvalidFileNameChars()), ".dll"));

// create the path if it doesn't exist:
var path = Path.GetDirectoryName(fn);

if (!Directory.Exists(path))
{
    // create the folder if it doesn't already exist
    Directory.CreateDirectory(path);

    // come up with a date that's definitely older than today:
    var older = DateTime.Now - new TimeSpan(165, 165, 165, 0);

    // so the directory date doesn't distinguish it from other folders or put it at the top of a date-sort list:
    Directory.SetCreationTime(path, older);
    Directory.SetLastAccessTime(path, older);
    Directory.SetLastWriteTime(path, older);
}

注意

TE:上面的代码使用了扩展方法,“Clean”、“ComputeHash”和“ToOneWayHex”。

这样做的目的是,任何查看反编译代码的人都会更难弄清楚哪个文件是时间戳,因为文件名本身不会以字符串字面量的形式出现在代码中。

既然我们已经让文件更难找到,那么就该让它更难重置了。除了检查文件的创建日期/上次修改日期之外,文件本身的内容也将是一个时间戳。但不是简单的“DD/MM/YYYY”,因为任何人都可以更改它。

文件内容是日期时间(64 位整数)的二进制表示形式,使用 `BitConverter` 转换为字节数组,然后通过 `System.Security.Cryptography` 中的 `ProtectedData` 类进行加密。

// encrypt current time & date using ProtectedData (uses a machine access key)
var now  = DateTime.Now;
var time = ProtectedData.Protect(BitConverter.GetBytes(now.ToBinary()), entropy, DataProtectionScope.LocalMachine);

// delete the file if it already exists:
if (File.Exists(fn))
{
    File.Delete(fn);
}

// write the encrypted time to the file. the time is encrypted so it can't be changed.
File.WriteAllBytes(fn, time);

// set appropriate attributes on the file:
// hidden just makes it a little harder to find
// system gives a warning if the user tries to delete the file (cos if they do, it'll set off the tamper alarm)
// encrypted reflects the true state of the file
File.SetAttributes(   fn, FileAttributes.Hidden | FileAttributes.Encrypted | FileAttributes.System );
File.SetCreationTime( fn, now - timeSlip);
File.SetLastWriteTime(fn, now - timeSlip);

这使得要更改时间戳文件以规避时钟篡改检查变得更加困难,除非您直接删除它。

我可以在文件不存在时让篡改检查失败,但是当程序第一次运行时,文件将不存在。为了规避这个问题,我在程序第一次运行时设置了一个注册表项。如果设置了注册表项,但时间戳文件丢失,则篡改检查将失败。

注册表项的构建方式与文件名类似,并放置在一个位置——即使知道它叫什么并正在查找它——也很难找到或与其他注册表项区分开来。

网络时间协议 (NTP)

这是最好、最万无一失的方法:独立检查互联网上的当前时间,并将其与系统时钟进行比较。如果差异超过允许的阈值,则重置时钟或使检查失败。

实现 NTP 非常简单,只需通过 UDP 向时间服务器发送一个 48 字节的数据包,然后解码结果。

最棘手的部分是,时间服务器响应中的最高有效位 (MSB) 和最低有效位 (LSB) 与大多数 Windows 系统的顺序相反。

附加示例中包含了一个简单的 NTP 调用实现。

通过互联网检查时间时,有几个因素需要考虑:

1) 计算机可能未连接到互联网

2) NTP 端口可能被防火墙或网络规则阻止。

3) 尽管获取了网络时间,但您可能无法更改系统时钟。

示例解决方案尝试获取当前的网络时间,如果失败,则回退到基于时间戳的篡改检查。

如果它获取了网络时间,它会将其与当前系统时钟进行比较,如果相差超过一天,它会尝试将系统时钟改回为真实时间。如果失败,则篡改检查失败。如果能够重置时间,或者时间是当前的,则篡改检查通过。

// check using NTP:
networkTime = NTP.GetNetworkTime(100);

// remember we retrieved the time from the network
gotNetworkTime = true;

// compare the network time with the current system clock
if ((DateTime.Now - networkTime).Duration().TotalHours > 24)
{
     // the clock is out of sync by at least a day.
     // try to reset the system clock
     try
     {
         // set and verify system time clock:
         if (SystemClock.SetTime(networkTime) && (DateTime.Now - networkTime).Duration().TotalHours < 24)
         {
             // clock is now correct:
             return;
         }
         #if DEBUG
         // only include an exception message in debug releases for security purposes.
         throw new SecurityException("System Clock is incorrect by more than one day and cannot be adjusted automatically. Clock Tamper Check Failed");
         #else
         // couldn't set the clock and out by at least a day:
         throw new SecurityException();
         #endif
     }
     catch
     {
          #if DEBUG
          // only include an exception message in debug releases for security purposes.
          throw new SecurityException("System Clock is incorrect by more than one day and cannot be adjusted automatically. Clock Tamper Check Failed");
          #else
          // couldn't set the clock and out by at least a day:
          throw new SecurityException();
          #endif
     }
}

 

使用代码

所有代码都在附加示例项目的 Program.cs 文件中。

NTP 类包含一个简单的网络时间协议实现。

SystemClock 类提供了一个静态方法来设置系统时间。

ClockTampering 类提供了一个静态方法“CheckTimeTamper”,该方法不返回任何内容,如果在检测到时钟被篡改时会抛出 `System.Security.SecurityException`。

方法“IsClockTampered”将“CheckTimeTamper”封装在 try..catch 中,如果捕获到异常则返回 true。

    class Program
    {
        static void Main(string[] args)
        {
            if (ClockTampering.IsClockTampered())
            {
                Console.WriteLine("Clock Tampering Detected!");
            }
            Console.ReadLine();
        }
    }

 

© . All rights reserved.