Bzi 发表于 2025-5-10 23:32:11

【C++】 编译时随机数生成以及应用于字符串混淆

本帖最后由 Bzi 于 2025-5-10 23:32 编辑



## 前言

本文仅供学习探讨之用,如果侵犯了您的权益请联系我删除。

## 原理

一般来说随机数种子使用`std::random_device`就好,它是基于硬件熵(如果支持)的随机数种子生成器,效果是比较不错的。

它的底层原理是调用 x86 指令集扩展指令`RDRAND`,如果硬件不支持,则回退到使用`/dev/random`或者`/dev/urandom`。

感兴趣的话可以自行阅读 Intel 白皮书或简单参考:uops.info/html-instr/RDSEED_R64.html

在 C++ 中我们可以通过 intrinsic 的方式直接调用这条指令,例如:

```cpp
#include <immintrin.h>

int main()
{
    unsigned long long seed = 0;

    auto invokeResult = _rdseed64_step(&seed);
    if (1 != invokeResult)
    {
      std::cerr << "[!] Failed to invoke _rdseed64_step" << std::endl;
      return -1;
    }

    std::cout << "[+] Seed: " << seed << std::endl;

    return 0;
}
```

但由于上述方法都无法在**编译期**使用,因此我们需要自定义一个“编译期版的 random_device”(它本质是伪随机,相比`std::random_device`的效果肯定会差很多)。

**核心思路是**:利用 `__LINE__`、`__COUNTER__`、`__TIMESTAMP__` 等编译期宏的值拼接成字符串,再计算哈希值作为随机数种子,然后使用`Xorshift1024*`算法来生成随机数。

## 随机数种子数据生成

先来实现生成随机种子数据的宏:

```cpp
#define ___RandomSeedStringify(x) #x
#define __RandomSeedExpand(x) ___RandomSeedStringify(x)
#define RandomSeedData() __RandomSeedExpand(__LINE__) "_" __RandomSeedExpand(__COUNTER__) "_" __TIMESTAMP__
```

说明:

+ `__LINE__`:当前代码的行号。
+ `__COUNTER__`:编译器内部计数器,每次展开都会递增。
+ `__TIMESTAMP__`:当前编译时间戳。

这三者结合后,几乎可以保证不同位置的随机数据不会重复。

因为拼接的宏需要多次展开,所以这里套了两层宏 `__RandomSeedExpand` 和 `___RandomSeedStringify` 来完成展开和字符串化。

## 随机数种子生成

随机数据有了之后,我们就可以计算哈希值了,这里直接抄 `STL` 的代码,具体实现如下:

PS:你可能会说为啥不直接用`std::hash<std::string_view>`,因为`std::hash`不能在编译期使用,难绷.jpg。

```cpp
consteval size_t RandomSeed(const std::string_view &seedGenData)
{
    constexpr size_t fnvOffsetBasis = 0x811C9DC5ull;
    constexpr size_t fnvPrime = 0x1000193ull;
    size_t hash = fnvOffsetBasis;

    for (size_t i = 0; i < seedGenData.size(); ++i)
    {
      hash ^= static_cast<size_t>(seedGenData);
      hash *= fnvPrime;
    }

    return hash;
}
```

然后我们就可以生成一个编译期的随机数种子了:

```cpp
constexpr size_t seed = RandomSeed(RandomSeedData());
```

## 随机数生成器

有了随机数种子之后,我们就可以来实现一个编译期的随机数生成器了,这里使用的是`Xorshift1024*`算法,具体实现如下:

```cpp
template <size_t data_size, typename value_t>
struct Xorshift1024Start
{
    using value_type = value_t;

    static constexpr size_t size = data_size;

    std::array<value_t, size> values;

    constexpr Xorshift1024Start(size_t seed, value_t min, value_t max)
    {
      std::array<uint64_t, 16> states{};
      size_t index = 0;

      // 使用 SplitMix64 算法生成初始状态
      for (auto &state : states)
      {
            uint64_t z = (seed += 0x9E3779B97F4A7C15ull);

            z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ull;
            z = (z ^ (z >> 27)) * 0x94D049BB133111EBull;
            state = z ^ (z >> 31);
      }

      // 使用 Xorshift1024* 算法生成 size 个随机数
      for (size_t i = 0; i < size; ++i)
      {
            uint64_t value = 0;
            {
                uint64_t state0 = states;
                uint64_t state1 = states[(index = (index + 1) & 15)];

                state1 ^= state1 << 31;
                state1 ^= state1 >> 11;
                state0 ^= state0 >> 30;

                states = state0 ^ state1;
                value = states * 0x106689D45497FDB5ull;
            }

            // 这里对生成的随机数进行映射,使其在 范围内
            if constexpr (std::is_integral_v<value_t>) // 判断是否为整形
                values = min + static_cast<value_t>(value % (max - min + 1));
            else
            {
                constexpr double scale = 1.0 / static_cast<double>(~0ull);
                values = min + static_cast<value_t>(value * scale * (max - min));
            }
      }
    }
};
```

`Xorshift1024Start`的模板参数`data_size`为随机数生成器的数据大小(要生成多少个随机数),`value_t`为随机数的类型。

`Xorshift1024Start`的构造函数接受三个参数:`seed`为随机数种子,`min`为随机数的最小值,`max`为随机数的最大值。

使用示例:

```cpp
// 演示生成10个int随机数,范围在0到100之间
constexpr size_t seed00 = RandomSeed(RandomSeedData());
constexpr Xorshift1024Start<10, int> intGenerator(seed00, 0, 100);

for (auto value : intGenerator.values)
    std::cout << value << " ";
std::cout << std::endl;

// 演示生成10个float随机数,范围在0到1之间
constexpr Xorshift1024Start<10, float> floatGenerator(RandomSeed(RandomSeedData()), 0.f, 1.f);

for (auto value : floatGenerator.values)
    std::cout << value << " ";
std::cout << std::endl;
```

## 字符串混淆

有了随机数生成器之后,我们就可以来实现编译期字符串混淆了。

### 原理

先说一下原理,我们使用`Xorshift1024Start`生成一串随机数,然后对字符串的每个字符进行异或操作,然后就可以在编译时得到混淆后的字符串数据,并将其存储为`char data`,其中`N`为字符串长度。

然后我们需要编写一个**运行时**函数来对混淆后的字符串进行解密操作,并返回解密后的字符串`std::string`。

**注意**:一定要运行时函数,否则可能会被编译器优化掉,直接给你优化成原始字符串,导致白混淆(憋笑.jpg)。

### 实现

我的思路是,先生成一个随机的序列的大小,然后再根据大小生成一个随机的`uint8_t`序列作为密钥`k`。

然后根据原始字符串`a`的长度(包含`\0`)再生成一个`1 ~ (a.length - 1)`的随机数`x`。

将`a`根据`x`分割成两个字符串`a1`和`a2`,分别将它们与`k`进行异或操作得到`b1`和`b2`。

记录`k`的第一个字节`k0`,然后循环将`k`的每个字节`k`都与下一个字节`k[(i + 1) % k.size()]`进行异或操作得到混淆值并存入`k`,最终得到`b3`。

最后按`b1`,`b3`,`k0`,`b2`的顺序将它们拼接起来就得到我们对于字符串`a`的最终的混淆数据了。

PS:解密操作与混淆操作反着来就行了,这里就不赘述了。

### 随机字节序列生成器

```cpp
template <size_t size, size_t seed>
struct RandomBytes
{
    // 根据种子生成0-255的随机数作为随机序列
    static constexpr Xorshift1024Start<size, uint8_t> value{seed, 0, 255};
};
```

使用示例:

```cpp
// 生成10个随机字节
constexpr auto randomBytes = RandomBytes<10, RandomSeed(RandomSeedData())>::value.values;

for (auto value : randomBytes)
    std::cout << static_cast<uint16_t>(value) << " ";
std::cout << std::endl;
```

### 随机大小生成

```cpp
template <size_t min, size_t max, size_t seed>
consteval size_t RandomSize()
{
    constexpr Xorshift1024Start<16, size_t> generator{seed, min, max};

    return generator.values;
}
```

使用示例:

```cpp
// 生成1到10之间的随机数作为大小
constexpr size_t size = RandomSize<1, 10, RandomSeed(RandomSeedData())>();
```

### 混淆字符串对象

```cpp
template <typename any_t>
concept IsRandomBytesValue = requires(any_t value) {
    typename any_t::value_type;
    any_t::size;
    { value.values } -> std::same_as<std::array<typename any_t::value_type, any_t::size> &>;
};

// 限定xor_keys的提供者只能是RandomBytes::value
template <size_t seed, size_t size, auto xor_keys>
    requires IsRandomBytesValue<decltype(xor_keys)>
class ObfuscatedString
{
public:
    operator std::string() const
    {
      return DecryptRuntimeOnly(m_xorKeys, KeysSize, m_firstBlock, FirstBlockSize, m_secondBlock, SecondBlockSize - 1, m_firstKey);
    }

public:
    // 限定只能编译时完成
    consteval ObfuscatedString(const char (&string))
    {
      constexpr auto keys = xor_keys.values;

      // 字符串混淆block0
      for (size_t i = 0; i < FirstBlockSize; ++i)
            m_firstBlock = string ^ keys;

      // 字符串混淆block1
      for (size_t i = 0; i < SecondBlockSize; ++i)
            m_secondBlock = string ^ keys;

      // 记录第一个key
      m_firstKey = keys;
      // 密钥自混淆
      for (size_t i = 0; i < KeysSize; ++i)
            m_xorKeys = keys ^ keys[(i + 1) % keys.size()];
    }

private:
    // 必须让其在运行时再解密,为了避免可能存在的优化,函数里必须尽量避免使用编译时已知信息。
    //   这里使用 inline 是为了期望编译器将函数内联到调用处,增加整体代码的复杂度,使得混淆效果更好。
    //   不过按我的设想,这里几乎不太可能会被 inline 就是了。
    inline static std::string DecryptRuntimeOnly(const char *key, size_t len, const char *block0, size_t len0, const char *block1, size_t len1, char firstKey)
    {
      // 防止被编译器优化
      //   这里取的是栈上的数据,并加上 volatile 关键字
      //   虽然数据可能因为 unused 而被优化掉,但还有 volatile,双重保险
      [] volatile void *avoidOptimization = reinterpret_cast<void **>(const_cast<char **>(&key));

      // 解密自混淆密钥
      std::vector<char> xorKeys(len, firstKey);
      for (size_t i = 1; i < KeysSize; ++i)
            xorKeys = xorKeys ^ key;

      // 解密后的字符串,为了防止可能被优化所以不预先分配内存,让其后续再分配
      std::string result;

      // 解密block0
      for (size_t i = 0; i < len0; ++i)
            result.push_back(block0 ^ xorKeys);

      // 解密block1
      for (size_t i = 0; i < len1; ++i)
            result.push_back(block1 ^ xorKeys);

      return result;
    }

private:
    // 随机生成block0的大小
    static constexpr size_t FirstBlockSize = random::RandomSize<1, (seed % (size - 2)) + 1, seed>();
    // 根据block0的大小计算block1的大小,其实就是相当于都随机
    static constexpr size_t SecondBlockSize = size - FirstBlockSize;
    // 存储密钥大小方便后续计算
    static constexpr size_t KeysSize = xor_keys.values.size();

    // 数据块
    char m_firstBlock;
    char m_xorKeys;
    char m_firstKey;
    char m_secondBlock;
};
```

使用示例:

```cpp
int main()
{
    meta_obfuscate::ObfuscatedString<
    meta_obfuscate::random::RandomSeed(RandomSeedData()),
    sizeof("Hello World!"),
    meta_obfuscate ::random ::RandomBytes<
      meta_obfuscate::random::RandomSize<16, 66, meta_obfuscate ::random ::RandomSeed(RandomSeedData())>(),
      meta_obfuscate ::random ::RandomSeed(RandomSeedData())>::value>
    oHelloWorld("Hello World!");

    std::string sHelloWorld = oHelloWorld;
    std::cout << sHelloWorld << std::endl;

    return 0;
}
```

### 混淆字符串宏

为了方便使用,肯定不能每次都写那么一大坨代码,所以这里写一个宏来简化混淆字符串的操作。

PS:可惜自定义字面量不能携带额外的参数(随机数据),不然可以更方便的声明混淆字符串,例如:`"Hello World"_O`。

```cpp
#define OBF(string_literal) static_cast<std::string>( // 将临时对象转换成std::string
    ObfuscatedString<                                     // 声明临时字符串混淆对象
      RandomSeed(RandomSeedData()),                         // 生成随机种子 -> ObfuscatedString<seed>
      sizeof(string_literal),                               // 获取字符串(字符数组)大小 -> ObfuscatedString<seed, size>
      RandomBytes<                                          // 生成随机密钥
            RandomSize<16, 66, RandomSeed(RandomSeedData())>(),   // 生成随机密钥大小 -> RandomBytes<size>
            RandomSeed(RandomSeedData())                        // 生成随机密钥种子 -> RandomBytes<size, seed>
      >::value                                              // 获取随机密钥 -> ObfuscatedString<seed, size, xor_keys>
    >(string_literal)                                     // 初始化混淆字符串对象 -> ObfuscatedString<seed, size, xor_keys>(string_literal)
)                                                   // 转换
```


使用示例:

```cpp
int main()
{
    std::cout << OBF("Hello World!") << std::endl;
    std::cout << OBF("Hello World!") << std::endl;
    std::cout << OBF("Hello World!") << std::endl;

    return 0;
}
```

## 最终效果

效果图在最下面

## 完整代码

代码已上传到附件中,同时 GitHub 地址:github.com/Bzi-Han/MetaObfuscate

## 结语

~~时隔3月也是终于水出了一篇文章~~。

那就这样了,有缘再见~

AyalaRs 发表于 2025-5-10 23:55:21

不知道这个是否可以解决代码被优化掉的问题
https://learn.microsoft.com/zh-cn/cpp/preprocessor/optimize?view=msvc-170
#pragma optimize( "", off )
/* unoptimized code section */
#pragma optimize( "", on )

Bzi 发表于 2025-5-11 00:20:26

AyalaRs 发表于 2025-5-10 23:55
不知道这个是否可以解决代码被优化掉的问题
https://learn.microsoft.com/zh-cn/cpp/preprocessor/optimize ...

可以,只是有兼容性问题,要对不同编译器的实现做适配。

弄个宏统一 一下实现应该还可以。

YY菌 发表于 4 天前

要想编译期间获取CPU的rdseed真随机数其实也可以,自己写个程序来调用 cl.exe 命令行编译(然后你的exe里面用 rdseed 获取真随机数,并在命令行中设置 /D RDSEED=你获取的真随机数),这样被编译的cpp文件就获得了一个编译期间的真随机宏。
页: [1]
查看完整版本: 【C++】 编译时随机数生成以及应用于字符串混淆