diff options
| author | Root THC | 2026-02-24 12:42:47 +0000 |
|---|---|---|
| committer | Root THC | 2026-02-24 12:42:47 +0000 |
| commit | c9cbeced5b3f2bdd7407e29c0811e65954132540 (patch) | |
| tree | aefc355416b561111819de159ccbd86c3004cf88 /other/burneye/doc/per-function-encryption.txt | |
| parent | 073fe4bf9fca6bf40cef2886d75df832ef4b6fca (diff) | |
initial
Diffstat (limited to 'other/burneye/doc/per-function-encryption.txt')
| -rw-r--r-- | other/burneye/doc/per-function-encryption.txt | 139 |
1 files changed, 139 insertions, 0 deletions
diff --git a/other/burneye/doc/per-function-encryption.txt b/other/burneye/doc/per-function-encryption.txt new file mode 100644 index 0000000..8754653 --- /dev/null +++ b/other/burneye/doc/per-function-encryption.txt | |||
| @@ -0,0 +1,139 @@ | |||
| 1 | |||
| 2 | BURNEYE preliminary documentation | ||
| 3 | |||
| 4 | |||
| 5 | Description of how per-function encryption works in burneye | ||
| 6 | =========================================================== | ||
| 7 | |||
| 8 | If the binary that is wrapped by burneye contains symbolic debug information | ||
| 9 | (usually unstripped binaries compiled with the -g option), we apply a special | ||
| 10 | encryption method. | ||
| 11 | |||
| 12 | Through the debug info, certain information about the functions within the | ||
| 13 | binary is extracted: The virtual address the function begins with, the length | ||
| 14 | in bytes of the functions' machine instructions and its name. | ||
| 15 | |||
| 16 | Each function is encrypted then. Since it is very difficult to move the | ||
| 17 | functions, we cannot prepend them with a decryption stub. Instead, we encrypt | ||
| 18 | the function, but overwrite its first eight bytes using a special sequence of | ||
| 19 | machine instructions: | ||
| 20 | |||
| 21 | push eax ; 0x50 | ||
| 22 | pusha ; 0x60 | ||
| 23 | pushf ; 0x9c | ||
| 24 | call cg_entry ; 0xe8 0x00 0x00 0x00 0x00 | ||
| 25 | |||
| 26 | After the call instruction the remaining encrypted function data is stored. The | ||
| 27 | first eight bytes - which we overwrite with our stub - are saved elsewhere (see | ||
| 28 | below). This eight byte stub is stored directly within the binary. The function | ||
| 29 | can only be in two states throughout runtime, it can be encrypted or decrypted. | ||
| 30 | If it is encrypted the first eight bytes of the function contains always this | ||
| 31 | stub bytes. On the other hand, if it is decrypted the real original bytes are | ||
| 32 | at that place. | ||
| 33 | |||
| 34 | Now for the interesting part. As the function calls the 'cg_entry' entry point, | ||
| 35 | its stack space looks like this: | ||
| 36 | |||
| 37 | [(possible) function arguments] | ||
| 38 | [return address from func caller] | ||
| 39 | [eax] | ||
| 40 | [pusha register block] | ||
| 41 | [saved flag register] | ||
| 42 | [cg_entry caller return address (func + 8)] | ||
| 43 | |||
| 44 | The code at 'cg_entry' pops the last address from the stack and computes the | ||
| 45 | original function address from it (just minus eight). Using the address, it | ||
| 46 | calls a search function which searchs through a table of structures, one for | ||
| 47 | each encrypted function. Once it finds the appropiate structure, it restores | ||
| 48 | the first eight bytes of the function with the saved encrypted bytes, then | ||
| 49 | calls a decryption function on the entire functions data. Normally it could | ||
| 50 | just restore the flags and all registers then and jump to the functions entry | ||
| 51 | point. This would work perfectly, but the more functions are called in the | ||
| 52 | application, the more it is decrypted. If all functions are called at least one | ||
| 53 | time, the entire .text segment is decrypted and can be dumped. | ||
| 54 | |||
| 55 | To avoid this 'lazy-decryption' problem, the 'cg_entry' code also replaces the | ||
| 56 | return address of the function that is decrypted. Thus, as the now-decrypted | ||
| 57 | function is returning through a simple 'ret' instruction, our code is called | ||
| 58 | again. The diagram shows how this works: | ||
| 59 | |||
| 60 | usual: [outside function]---[core function]---[called function] | ||
| 61 | |||
| 62 | cg: [outside function]. .[core function]. .[called function] | ||
| 63 | | | | | | ||
| 64 | .---------' '--------. .----' '------------. | ||
| 65 | | | | | | ||
| 66 | '-----[cg_entry]-----' '-----[cg_detry]----' | ||
| 67 | |||
| 68 | This way both the entry and return ('detry') point of the function is | ||
| 69 | redirected. As a clever reader you may have noticed that the parent function | ||
| 70 | remains decrypted in this setup. Therefore the 'cg_entry' code also re-encrypts | ||
| 71 | its caller function. | ||
| 72 | |||
| 73 | In detail the stub and 'cg_entry' code does: | ||
| 74 | |||
| 75 | 1. save necessary data (flags, registers) | ||
| 76 | 2. encrypt outside function | ||
| 77 | 3. restore first eigth encrypted bytes of core function | ||
| 78 | 4. decrypt core function | ||
| 79 | 5. restore necessary data (flags, registers) | ||
| 80 | 6. pass control to entry point of core function (through jmp) | ||
| 81 | |||
| 82 | |||
| 83 | The 'cg_detry' code has to mirror the behaviour from the opposite perspective: | ||
| 84 | |||
| 85 | 1. save necessary data (flags, registers) | ||
| 86 | 2. encrypt core function | ||
| 87 | 3. overwrite first eight bytes in core function with stub | ||
| 88 | 4. decrypt outside function | ||
| 89 | 5. restore necessary data (flags, registers) | ||
| 90 | 6. return to real core function return address | ||
| 91 | |||
| 92 | |||
| 93 | Possible runtime problems | ||
| 94 | ------------------------- | ||
| 95 | |||
| 96 | This function wrapping method is quite reliable and can cope with various | ||
| 97 | situations, such as goto's, signal handlers, function pointers and generally | ||
| 98 | non-linear code. However, there is one case where this does not work. | ||
| 99 | |||
| 100 | If there is an execution path which points from within one function to the | ||
| 101 | middle of another encrypted function, the target function is not decrypted. | ||
| 102 | This sounds complicated, so here is a rule of thumb: Do NOT use longjmp/setjmp | ||
| 103 | within your code. | ||
| 104 | |||
| 105 | If you have to use it or you have to protect a stock binary with symbols, you | ||
| 106 | can tag the function that the 'longjmp' passes control to (i.e. the function | ||
| 107 | that has the 'setjmp' call) with a decrypt-log. This means it is initially | ||
| 108 | decrypted once, before your binary receives any control at all and remains | ||
| 109 | decrypted throughout the whole runtime. | ||
| 110 | |||
| 111 | Note that this way the function that receives the 'longjmp' remains unprotected | ||
| 112 | through the whole time the binary runs. Hence, use this only if there is no way | ||
| 113 | to replace the 'longjmp'. In most cases there is a way. | ||
| 114 | |||
| 115 | Also, you can use the decrypt-lock functionality for performance improvements, | ||
| 116 | see below. | ||
| 117 | |||
| 118 | |||
| 119 | Performance overhead | ||
| 120 | -------------------- | ||
| 121 | |||
| 122 | TODO | ||
| 123 | |||
| 124 | Performance is an issue with this protection. However, I do not have made | ||
| 125 | concrete statistics about it. For most I/O based programs the overhead may lie | ||
| 126 | around 10 to 15 times the instructions executed in runtime than in the | ||
| 127 | unprotected version. With some per-function optimizations, leaving the most | ||
| 128 | often called functions unprotected at runtime these may drop to a level of five | ||
| 129 | or less. If only single core functions are protected there may be no overhead | ||
| 130 | at all. | ||
| 131 | |||
| 132 | However, to make a real decision, statistics are required once the encrypter is | ||
| 133 | finished. TODO | ||
| 134 | |||
| 135 | |||
| 136 | |||
| 137 | -- | ||
| 138 | vi:fo=tcrq:tw=79 | ||
| 139 | |||
