前言
本文仅供学习探讨之用,如果侵犯了您的权益请联系我删除。
原理
一般来说随机数种子使用std::random_device
就好,它是基于硬件熵(如果支持)的随机数种子生成器,效果是比较不错的。
它的底层原理是调用 x86 指令集扩展指令RDRAND
,如果硬件不支持,则回退到使用/dev/random
或者/dev/urandom
。
感兴趣的话可以自行阅读 Intel 白皮书或简单参考:uops.info/html-instr/RDSEED_R64.html
在 C++ 中我们可以通过 intrinsic 的方式直接调用这条指令,例如:
#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*
算法来生成随机数。
随机数种子数据生成
先来实现生成随机种子数据的宏:
#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。
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[i]);
hash *= fnvPrime;
}
return hash;
}
然后我们就可以生成一个编译期的随机数种子了:
constexpr size_t seed = RandomSeed(RandomSeedData());
随机数生成器
有了随机数种子之后,我们就可以来实现一个编译期的随机数生成器了,这里使用的是Xorshift1024*
算法,具体实现如下:
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[index];
uint64_t state1 = states[(index = (index + 1) & 15)];
state1 ^= state1 << 31;
state1 ^= state1 >> 11;
state0 ^= state0 >> 30;
states[index] = state0 ^ state1;
value = states[index] * 0x106689D45497FDB5ull;
}
// 这里对生成的随机数进行映射,使其在 [min, max] 范围内
if constexpr (std::is_integral_v<value_t>) // 判断是否为整形
values[i] = min + static_cast<value_t>(value % (max - min + 1));
else
{
constexpr double scale = 1.0 / static_cast<double>(~0ull);
values[i] = min + static_cast<value_t>(value * scale * (max - min));
}
}
}
};
Xorshift1024Start
的模板参数data_size
为随机数生成器的数据大小(要生成多少个随机数),value_t
为随机数的类型。
Xorshift1024Start
的构造函数接受三个参数:seed
为随机数种子,min
为随机数的最小值,max
为随机数的最大值。
使用示例:
// 演示生成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]
,其中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[i]
都与下一个字节k[(i + 1) % k.size()]
进行异或操作得到混淆值并存入k[i]
,最终得到b3
。
最后按b1
,b3
,k0
,b2
的顺序将它们拼接起来就得到我们对于字符串a
的最终的混淆数据了。
PS:解密操作与混淆操作反着来就行了,这里就不赘述了。
随机字节序列生成器
template <size_t size, size_t seed>
struct RandomBytes
{
// 根据种子生成0-255的随机数作为随机序列
static constexpr Xorshift1024Start<size, uint8_t> value{seed, 0, 255};
};
使用示例:
// 生成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;
随机大小生成
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[15];
}
使用示例:
// 生成1到10之间的随机数作为大小
constexpr size_t size = RandomSize<1, 10, RandomSeed(RandomSeedData())>();
混淆字符串对象
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)[size])
{
constexpr auto keys = xor_keys.values;
// 字符串混淆block0
for (size_t i = 0; i < FirstBlockSize; ++i)
m_firstBlock[i] = string[i] ^ keys[i % keys.size()];
// 字符串混淆block1
for (size_t i = 0; i < SecondBlockSize; ++i)
m_secondBlock[i] = string[FirstBlockSize + i] ^ keys[i % keys.size()];
// 记录第一个key
m_firstKey = keys[0];
// 密钥自混淆
for (size_t i = 0; i < KeysSize; ++i)
m_xorKeys[i] = keys[i] ^ 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,双重保险
[[maybe_unused]] volatile void *avoidOptimization = reinterpret_cast<void **>(const_cast<char **>(&key))[8];
// 解密自混淆密钥
std::vector<char> xorKeys(len, firstKey);
for (size_t i = 1; i < KeysSize; ++i)
xorKeys[i] = xorKeys[i - 1] ^ key[i - 1];
// 解密后的字符串,为了防止可能被优化所以不预先分配内存,让其后续再分配
std::string result;
// 解密block0
for (size_t i = 0; i < len0; ++i)
result.push_back(block0[i] ^ xorKeys[i % KeysSize]);
// 解密block1
for (size_t i = 0; i < len1; ++i)
result.push_back(block1[i] ^ xorKeys[i % KeysSize]);
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[FirstBlockSize];
char m_xorKeys[KeysSize];
char m_firstKey;
char m_secondBlock[SecondBlockSize];
};
使用示例:
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
。
#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)
) // 转换
使用示例:
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月也是终于水出了一篇文章。
那就这样了,有缘再见~