// ccs_obfuscator/src/obfuscator.cc // // Standalone CCS-based transformation tool. Exposes Phase 2.2b // (binary -> runtime) — a phase the cppcc CLI does not surface. // // obfuscator -a [-k ] .fgr|.bfgr # forward // obfuscator -d [-k ] .fgr|.bfgr # inverse // // Algorithms (step8.24): // // 0 = no-op forward (load -> SCR -> re-serialize). No -k needed // since nothing is ciphered. Round-trip oracle from step8.22. // // 1 = XTEA cipher on every edp's edpfix_ and edpdyn_ (in place at // the SCR level). Identity fields (cnmnum_/kwnnum_/edpnum_) // and container shapes are preserved; only rule-body payloads // are scrambled. -k is any string; passed through // xtea::make_key to derive the 128-bit key. If -k is omitted, // the hardcoded kDefaultKeySeed below is used. // // Output filenames mirror step8.22 (.obf..*): // // forward (-a ): .obf..{scbtxt,scrtxt,fgr,bfgr} // inverse (-d ): .de.{scbtxt,scrtxt,fgr,bfgr} // // The .scbtxt/.scrtxt files are decimal-text dumps of binary_.memory_ // (pre- and post-transform respectively). For algo 0 they're // byte-identical; for algo 1 they differ in edp-payload regions. #include #include #include #include #include "Logger.h" #include "SyntaxControlledRuntime.h" #include "SyntaxControlledBinary.h" #include "SyntaxControlledConverter.h" #include "cipher.h" #include "xtea.h" namespace { // Default key seed used when -k is not supplied. Passed through // xtea::make_key to derive a 128-bit Key. Anyone running the // obfuscator without -k gets reproducible cipher output under this // seed; obfuscation strength against an attacker who knows the // source therefore depends entirely on the -k value (or on the // attacker not knowing this default). For real obfuscation use, // always pass -k . const char* const kDefaultKeySeed = "ccs_obfuscator-default-key-step8.24"; void printUsage(std::ostream& o) { o << "Usage:\n" << " obfuscator -a [-k ] .fgr|.bfgr # forward\n" << " obfuscator -d [-k ] .fgr|.bfgr # inverse\n" << " obfuscator -b .fgr # populate .bfgr\n" << "\n" << "-b (cppcc step.146): read the portable decimal-text .fgr and write\n" << "the sibling .bfgr in THIS machine's native byte order — run on the\n" << "machine that will load the .bfgr, so endian mismatch cannot arise.\n" << "Pure SCB repack: no algorithm, no -k.\n" << "\n" << "Algorithms:\n" << " 0 no-op forward (round-trip oracle; -k ignored)\n" << " 1 XTEA cipher on every edp's edpfix_ + edpdyn_ vectors\n" << "\n" << "-k is any string; passed through xtea::make_key to derive\n" << "the 128-bit key. If omitted, a hardcoded default seed is used.\n" << "\n" << "Outputs (alongside ):\n" << " forward (-a ): .obf..{scbtxt,scrtxt,fgr,bfgr}\n" << " inverse (-d ): .de.{scbtxt,scrtxt,fgr,bfgr}\n"; } bool endsWith(const std::string& s, const std::string& suffix) { if (s.size() < suffix.size()) return false; return 0 == s.compare(s.size() - suffix.size(), suffix.size(), suffix); } std::string stripExt(const std::string& path) { if (endsWith(path, ".bfgr")) return path.substr(0, path.size() - 5); if (endsWith(path, ".fgr")) return path.substr(0, path.size() - 4); return path; } // Write binary_.memory_ as one TagLong per line (decimal text). Used // for the .scbtxt / .scrtxt outputs. bool writeMemoryDump(const std::string& path, const ccsscb::scb::SyntaxControlledBinary& bin) { std::ofstream f(path.c_str()); if (!f) { std::cerr << "obfuscator: cannot open " << path << "\n"; return false; } for (std::size_t i = 0, n = bin.memory_.size(); i < n; ++i) { f << bin.memory_[i] << "\n"; } return true; } } // namespace int main(int argc, char** argv) { // step.146: -b — populate .bfgr from .fgr. The same // SyntaxControlledBinary::populateRawFromText that cppcc's -b flag // drives through libruntime.a, here through the libscbcpp.a clone // (byte-identical outputs are pinned by cppcc's // tests/fgr_to_bfgr/run.sh clone-parity leg). if (argc == 3 && std::string(argv[1]) == "-b") { try { ccsscb::scb::SyntaxControlledBinary binary; binary.populateRawFromText(argv[2]); return 0; } catch (const std::exception& e) { std::cerr << "obfuscator: " << e.what() << "\n"; return 1; } } // Parse args: -a|-d [-k ] if (argc != 4 && argc != 6) { printUsage(std::cerr); return 2; } const std::string switchArg = argv[1]; if (switchArg != "-a" && switchArg != "-d") { std::cerr << "obfuscator: first arg must be -a or -d; got " << switchArg << "\n"; printUsage(std::cerr); return 2; } const bool forward = (switchArg == "-a"); // Parse . const std::string algoStr = argv[2]; unsigned algo = 0; try { algo = static_cast(std::stoul(algoStr)); } catch (...) { std::cerr << "obfuscator: must be a non-negative integer; got " << algoStr << "\n"; return 2; } if (algo > 1) { std::cerr << "obfuscator: only algorithms 0 (no-op) and 1 (XTEA) " << "wired in step8.24; got " << algo << "\n"; return 2; } // Optional -k ; falls back to kDefaultKeySeed. std::string keySeed = kDefaultKeySeed; std::string input; if (argc == 4) { input = argv[3]; } else { // argc == 6, expect -k if (std::string(argv[3]) != "-k") { std::cerr << "obfuscator: 4th arg must be -k when present; got " << argv[3] << "\n"; printUsage(std::cerr); return 2; } keySeed = argv[4]; input = argv[5]; } if (!endsWith(input, ".fgr") && !endsWith(input, ".bfgr")) { std::cerr << "obfuscator: must end in .fgr or .bfgr; got " << input << "\n"; return 2; } // Output prefix. // -a: .obf..{scbtxt,scrtxt,fgr,bfgr} // -d: .de.{scbtxt,scrtxt,fgr,bfgr} // (For -d the input is typically already named like foo.obf.1.fgr; // stripExt + .de yields foo.obf.1.de.{...} which is what we want.) const std::string prefix = forward ? (stripExt(input) + ".obf." + std::to_string(algo)) : (stripExt(input) + ".de"); const std::string outScbtxt = prefix + ".scbtxt"; const std::string outScrtxt = prefix + ".scrtxt"; const std::string outFgr = prefix + ".fgr"; const std::string outBfgr = prefix + ".bfgr"; // Minimal logger. ccsscb::log::Logger logger( ccsscb::com::LOGGER_LEVEL_ERROR, std::string(), "obfuscator"); (void)logger; // Derive key (only used when algo > 0). const auto key = ccs_obf::xtea::make_key(keySeed); try { // Phase 2.2b: binary -> runtime. ccsscb::scr::tag::Long axiom = 0; ccsscb::cvt::SyntaxControlledConverter conv(axiom); conv.generateRuntimeFromBinary(input); // .scbtxt = pre-transform memory dump (= the loaded binary's // text form). For both forward and inverse this is what was // on disk before this run started. if (!writeMemoryDump(outScbtxt, conv.binary_)) return 1; // Apply the algorithm's SCR-level transform. switch (algo) { case 0: // No-op forward. -d 0 is also a no-op (inverse of no-op). break; case 1: if (forward) { ccs_obf::cipher::encipher_runtime(conv.runtime_, key); } else { ccs_obf::cipher::decipher_runtime(conv.runtime_, key); } break; } // Phase 2.2a: runtime -> binary. After algo 0 this is byte- // identical to the loaded binary. After algo 1 forward this // is the ciphered binary; after algo 1 inverse this is the // recovered original binary. conv.generateBinaryFromRuntime(); // .scrtxt = post-transform memory dump. Symmetric to .scbtxt // for algo 0; differs in edp-payload regions for algo 1. if (!writeMemoryDump(outScrtxt, conv.binary_)) return 1; // Write both .fgr (decimal text) and .bfgr (raw native bytes). conv.binary_.writeBinary(outFgr); conv.binary_.writeBinaryRaw(outBfgr); std::cout << "obfuscator: " << switchArg << " " << algo << (argc == 6 ? std::string(" -k ") + keySeed : std::string()) << " " << input << "\n" << " wrote " << outScbtxt << "\n" << " wrote " << outScrtxt << "\n" << " wrote " << outFgr << "\n" << " wrote " << outBfgr << "\n"; return 0; } catch (const std::exception& e) { std::cerr << "obfuscator: " << e.what() << "\n"; return 1; } }