【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月也是终于水出了一篇文章~~。
那就这样了,有缘再见~
不知道这个是否可以解决代码被优化掉的问题
https://learn.microsoft.com/zh-cn/cpp/preprocessor/optimize?view=msvc-170
#pragma optimize( "", off )
/* unoptimized code section */
#pragma optimize( "", on )
AyalaRs 发表于 2025-5-10 23:55
不知道这个是否可以解决代码被优化掉的问题
https://learn.microsoft.com/zh-cn/cpp/preprocessor/optimize ...
可以,只是有兼容性问题,要对不同编译器的实现做适配。
弄个宏统一 一下实现应该还可以。 要想编译期间获取CPU的rdseed真随机数其实也可以,自己写个程序来调用 cl.exe 命令行编译(然后你的exe里面用 rdseed 获取真随机数,并在命令行中设置 /D RDSEED=你获取的真随机数),这样被编译的cpp文件就获得了一个编译期间的真随机宏。
页:
[1]