Zenn
🙄

[C++]コンパイル時に文字列リテラルに軽い難読化を施す

2025/03/24に公開

C++で文字列リテラルを使用するとその文字列は直接バイナリに埋め込まれます。
そのため、exeファイルなどをバイナリエディタで開けば簡単にどのような文字列が埋め込まれているのかを確認できます。

以下のようなHello, World!を出力するプログラムをビルドします。

int main()
{
    constexpr auto str = "Hello, World!";

    std::cout << str << std::endl;

    return 0;
}

ビルド後に出力されたexeをバイナリエディタで開きHello, World!を検索すると文字列がバイナリに埋め込まれているのが確認できます。

プログラム中に埋め込む必要のある文字列で誰かに見られて困るもの、というのはそう多くはありません。開発中に変更することがほとんどない文字列は事前に難読化を施しておき実行時に複合化する程度でも十分です。
ですが、せっかくC++を書いているのでコンパイル時に難読化できないかを試してみます。

実装例

MSVC++で動作確認を行っています。

手始めに簡単な難読化プログラムを実装します。

難読化を施した文字列を格納するクラス
template<class T, std::size_t N>
struct encrypted_string;

template<std::size_t N>
struct encrypted_string<char, N> final
{
    constexpr encrypted_string(const std::array<char, N>& data) noexcept
        : data(data)
    {
    }

    constexpr std::string_view to_str_view() const noexcept
    {
        return { data.data(), data.size() };
    }

    constexpr char operator[](std::size_t i) const noexcept
    {
        return data[i];
    }

    const std::array<char, N> data;
};

文字列リテラルには終端文字(\0)が含まれているので除外しておきます。
難読化コードにはconstexprを使用し、コンパイル時に難読化が行えるようにします。

難読化コード
template<std::size_t N>
constexpr auto encrypt(const char(&input)[N])
{
    constexpr auto SIZE = N - 1;
    std::array<char, SIZE> result;
    for (std::size_t i = 0; i < SIZE; i++)
    {
        // +1ずらす
        result[i] = static_cast<char>(input[i] + 1);
    }

    return encrypted_string<char, SIZE>{ result };
}

複合化プログラムではconstexprを使用しません。
コンパイル時に複合化を行ってしまうと複合化された文字列が埋め込まれてしまいます。

複合化子コード
template<std::size_t N>
std::string decrypt(const encrypted_string<char, N>& input)
{
    std::string result;
    result.resize(input.data.size());
    for (std::size_t i = 0; i < result.size(); i++)
    {
        // -1もどす
        result[i] = static_cast<char>(input[i] - 1);
    }

    return result;
}

上記のコードを使用し、難読化・複合化を出力します。
難読化関数を使用する場合は必ずconstexprを使用してください。

int main()
{
    constexpr auto ret = encrypt("Hello, World!");
    std::cout << "encrypted: " << ret.to_str_view() << std::endl;
    std::cout << "decrypted: " << decrypt(ret.to_str_view()) << std::endl;

    return 0;
}
encrypted: Ifmmp-!Xpsme"
decrypted: Hello, World!

exeをバイナリエディタで開き確認するとHello, World!の文字列がどこにも見当たらないのが確認できます。
難読化されたデータであるIfmmp-!Xpsmeはそのまま連続した領域には埋め込まれてはいませんが4byteごとの塊で埋め込まれていることが確認できます。
(コンパイラ実装や最適化による理由などがあるかもしれません)

もう少し複雑に

文字の加算や減算では単純で、加算や減算によってオーバーフローが発生してしまうのも避けたいです。
なので文字ごとにXORを、XORで使用する値を乱数にすることで少し複雑な難読化を行います。

乱数生成器はコンパイル時に計算できるものであればなんでもいいですが今回はxor shiftを採用しました。
https://ja.wikipedia.org/wiki/Xorshift

xorshift
class xorshift_random_generator final
{
public:
    constexpr xorshift_random_generator(std::uint32_t seed) noexcept
        : seed(seed)
    {
    }

    template<class Out>
    constexpr Out next() noexcept
    {
        seed ^= seed << 13;
        seed ^= seed >> 17;
        seed ^= seed << 5;
        // 縮小変換が行われる可能性があるので変換先の型のbitでマスクする
        return static_cast<Out>(seed & std::numeric_limits<Out>::max());
    }
private:
    std::uint32_t seed;
};

文字列格納クラスに乱数のシード値を格納できるようにします。

難読化を施した文字列を格納するクラス
struct encrypted_string<char, N> final
{
    constexpr encrypted_string(const std::array<char, N>& data, std::uint32_t seed) noexcept
        : data(data), seed(seed)
    {
    }
    // ~~ 中略 ~~ 
    const std::uint32_t seed = 0;
};

難読化・複合化関数にはシード値を指定できるようにし、XORで文字列の値のビットを反転させます。

難読化・複合化コード
template<std::size_t N>
constexpr auto encrypt(const char(&input)[N], std::uint32_t seed)
{
    constexpr auto SIZE = N - 1;
    xorshift_random_generator gen{ seed };
    std::array<char, SIZE> result;
    for (std::size_t i = 0; i < SIZE; i++)
    {
        result[i] = input[i] ^ gen.next<char>();
    }

    return encrypted_string<char, SIZE>{ result, seed };
}

template<std::size_t N>
std::string decrypt(const encrypted_string<char, N>& input)
{
    xorshift_random_generator gen{ input.seed };
    std::string result;
    result.resize(input.data.size());
    for (std::size_t i = 0; i < result.size(); i++)
    {
        result[i] = input[i] ^ gen.next<char>();
    }

    return result;
}

ここまでのプログラムを実行するとより複雑な文字列が出力されることが確認できました。

int main()
{
    constexpr auto ret = encrypt("Hello, World!", 1);
    std::cout << "encrypted: " << ret.to_str_view() << std::endl;
    std::cout << "decrypted: " << decrypt(ret) << std::endl;

    return 0;
}
encrypted: id)#>|:eJ'S+
decrypted: Hello, World!

これで乱数によるXORを使用した難読化・複合化が実現できました。
__COUNTER__を使用して呼び出し箇所ごとに乱数のシード値を変化をさせたり、より複雑にするのであればブロック暗号化の方式の採用を検討するのも面白いかもしれません。

使いどころは少ないですがカジュアルハック対策にはなるかと思います。

Discussion

ログインするとコメントできます