PERSES is a X86 code obfuscation engine that works with Portable Executable files. The obfuscation works by replacing a specific instruction with a larger more sophisticated set that is semantically compatible to the original. PERSES only mutates 4 instructions yet has adverse effects on analyzers/decompilers due to the method of mutation. For more information on PERSES' inner workings, please check out the article written here.
PERSES is a work in progress and does not attempt to be a replacement for any established code obfuscation engine, so please be mindful when using it to protect your code. Furthermore, X64 support can be slightly improved and augmented to ensure semantical accuracy.
PERSES by default works off a command line. Listed below are the arguments requried to utilize PERSES.
Argument | Description | May Require |
---|---|---|
-f or --file |
Path to the input PE file. | ✔️ |
-a or -address |
Address or list of addresses required to be mutated. | ✔️ |
-s or --symbol |
Symbol or list of symbols to be mutated. This requires a linked .map file. |
❌ |
--map |
Map file to be linked. IDA Pro .map files must have their extension replaced with .ida . |
❌ |
--list |
List of functions to be mutated. Each entry must envelop one line and be formatted as 0x1000:0x2000 where 0x1000 is the start and 0x2000 is the end of the routine. |
❌ |
--rets |
Allow PERSES to build a RET gadget pool used to create JMP s to random locations. |
❌ |
--scan |
Force PERSES to scan for code protection markers. | ❌ |
Both symbols and addresses can be used, but atleast one of them must be present.
ℹ️ Due to limitations in the
argparse
library, if more than one address or symbol are required, please append the-a
or-s
argument and parameters last.
If desired, PERSES can be used manually to generate mutated binaries. To begin, one must declare a X86BinaryApplication
instance with the provided template parameter.
X86BinaryApplication<PERSES_64BIT>(filepath)
to generate aX86_64
instance.X86BinaryApplication<PERSES_32BIT>(filepath)
to generate aX86
instance.
After instantiating the object, the RET
gadget pool mentioned above can be optionally created with perses::buildKnownRetGadgets(app)
.
MAP files are optionally produced by compilers to show symbols and sections present in a binary with their corresponding address. PERSES allows MAP files from IDA Pro
and MSVC
(tested on VS2022) to be linked via X86BinaryApplication::linkMapFile
. Afterwards, symbols can be traversed via X86BinaryApplication::getSymbols
or added directly into the mutation queue by calling addRoutineBySymbol
. For instance, after linking the map file, adding main
to the mutation queue is as simple as:
app->addRoutineBySymbol("main", PERSES_MARKER_MUTATION);
MAP files can aid function size calculation by exposing known symbols, however, MAP linking is completely optional and only added as a convenience.
If mutating a batch of functions is wanted, function lists can be parsed in order to add the specified routines automatically. This is done by calling parseFunctionList
. Please be mindful of the required format listed above in the argument table. Futhermore, the end address supplied is expected to be the end of a function, providing anything else will likely result in instability of the output program.
The PersesSDK can be included into a project to emit a scannable pattern into code. PERSES makes use of compiler intrinsics to generate unique patterns. Beginning and end macros named PERSES_MUTATION_START()
and PERSES_MUTATION_END()
are provided.
Applying mutation on all routines in the queue is done by calling X86BinaryApplication::transformRoutines
. Transforms are applied via the corresponding schema. At the moment, there is only one schema supplied; MutationLight
.
Compilation of the new binary can be done with X86BinaryApplication::compile
. PERSES creates a new file and appends .perses
after the original filename.
Below are some example programs created to show the efficacy in regards to crippling decompiler output.
int main()
{
PERSES_MUTATION_START()
printf("Hello, world!\n");
Sleep(100);
PERSES_MUTATION_END()
return getchar();
}
int main()
{
int input = 0;
std::cin >> input;
switch (input)
{
case 0:
std::cout << "Value is zero" << std::endl;
break;
case 1:
std::cout << "Value is one" << std::endl;
break;
case 2:
std::cout << "Value is two" << std::endl;
break;
case 3:
std::cout << "Value is three" << std::endl;
break;
case 4:
std::cout << "Value is four" << std::endl;
break;
default:
std::cerr << "Unhandled value!" << std::endl;
break;
}
return getchar();
}
Full function mutation using command line
perses -f MutationTesting.exe --map MutationTesting.map --rets -s _main
Additional schemas can be created then attached to X86BinaryApplication::buildSchema
. Alternatively, MutationLight
can be extended as it only supports a minimal set of instructions. In order to modify the existing schema, please thoroughly read and understand MutationLight.cpp.
PERSES utilizes cmkr. In order to build the PERSES project, please run the following commands:
git clone --recursive https://github.com/mike1k/perses.git
cmake -B build
PERSES makes use of multiple great libraries in order to achieve it's objective.