Format! with string obfuscation

Hi!

Is it possible somehow to obfuscate string literals in format! like macros?

println!("aaa {} bbb {} ccc", s, s2); // "aaa ", " bbb " & " ccc" could be found in executable by strings utility

println! macro expands to this:

    {
        ::std::io::_print(::core::fmt::Arguments::new_v1(&["aaa ", " bbb ",
                            " ccc\n"],
                &[::core::fmt::ArgumentV1::new_display(&s),
                            ::core::fmt::ArgumentV1::new_display(&s2)]));
    };

I would like to write another proc macro on top of println! (format_args! & etc.) to modify the output to something like this:

    {
        ::std::io::_print(::core::fmt::Arguments::new_v1(&[obfuscate!("aaa "), obfuscate!(" bbb "),
                            obfuscate!(" ccc\n")],
                &[::core::fmt::ArgumentV1::new_display(&s),
                            ::core::fmt::ArgumentV1::new_display(&s2)]));
    };

Any ideas?

It is certainly possible, but the goal (and effectiveness) is highly questionable. Why are you trying to do this? Are you trying to store some sort of a password or encryption key in the code?

It won't work like that. The Arguments::new_v1 works with &'static str, so those strings would need to (be leaked or) exist in the executed binary (in not obfuscated form) anyways. And furthermore, perhaps more importantly, there is no way to modify the output of the println macro anyways. A proc macro can only do something about the println before its called.

Approaches that might work:

A proc macro obfuscate!("my string") that obfuscates the string at compile time and returns a value of some type struct ObfuscatedLiteral(&'static str) wrapping the obfuscated literal, and having a Display impl that would de-obfuscate the contents again.

Then

println!("aaa {} bbb {} ccc", s, s2);

would become

println!("{}{}{}{}{}", obfuscate!("aaa "), s, obfuscate!(" bbb "),  s2, obfuscate!(" ccc"));

Finally, of you want, you could also write another macro that makes this println! call easier to write by parsing the format string and factoring out the literal parts for you accordingly. I would only do this as a subsequent step, once you are sure the approach works as intended (with the indented effects on the string literals inside the binary) in the first place.

3 Likes

Thanks for the reply.

I would like to hide strings to make reverse engineering harder.

That's understandable, and a noble goal, but it's pretty ineffective against anyone but the least skilled attackers. (At least it doesn't incur a security vulnerability, like storing a password directly would be.)

Anyway, what I would do is create an equivalent to format_args!(). You can then use it to build equivalents of all the other macros as well. I don't see an easy way to re-use the existing format_args!() mechanism (at least not without parsing the format string for yourself).

1 Like

Note that what I suggested above only required partially parsing the format string in order to be fully usable like print/format_args. It would only be necessary to find and separate the {…} braces (while ignoring the {{ and }} escapes), because the original format_args macro would still be used to a large part, just the literal string sections would be factored out into arguments, too.

1 Like

I like your idea about partial parsing macro very much, thanks a lot!

This could be done only by using proc macro, am I right? Declarative macros looks not powerful enough for this job.

I have already done obfuscate! macro - it works just fine. I'm only asking about println! arguments parsing .

Yes, macro_rules macros cannot take apart string literals.

Almost done implementing the format obfuscator. Everything works but named arguments.

The issue could be reproduced with the following code. Don't know how avoid this error.

fn main() {
   let str = "some_str";
   println!(concat!("{str", "} {}"), "test");
}
error: there is no argument named `str`
 --> src/main.rs:4:13
  |
4 |    println!(concat!("{str", "} {}"), "test");
  |             ^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: did you intend to capture a variable `str` from the surrounding scope?
  = note: to avoid ambiguity, `format_args!` cannot capture variables when the format string is expanded from a macro
  = note: this error originates in the macro `concat` (in Nightly builds, run with -Z macro-backtrace for more info)

error: could not compile `playground` due to previous error

Consider using the defmt crate, which outputs numeric identifiers for the string constants and provides a separate lookup table for the receiving program to use.

Found a workaround.

This doesn't compile:
println!(concat!("{str", "} {}"), "test");

But this compiles:
println!(concat!("{str", "} {}"), "test", str=str);

1 Like

Thanks, I will take a look.

Hi, apologies for bumping this thread but if you (or anyone else) is looking for an answer to this question I have some crates that solve this specific problem (disclaimer: I am the author of these crates):

obfstr - General string obfuscation library.
fmtools - Extended formatting syntax for Rust with optional obfstr support by enabling the obfstr feature.

fmtools = { version = "0.1", features = ["obfstr"] }

The code in the OP would be written like this:

fmtools::println!("aaa "{s}" bbb "{s2}" ccc");

When the obfstr feature on fmtools is enabled you will not find the formatting string literals by simply looking at the binary.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.