┏━━┓
BACK
┗━━┛
╔══════════════════════════════════════════════════════════════════════════════════╗
║ Contents - 17-12-2025 committing crimes against readable assembly ║
║ -------------------------------------------------------------------------------- ║
║ yara rules and bytecodes ║
║ shellcode as accidental obfuscation ║
║ expanding maths equations as obfuscation ║
║ individual instruction obfuscation ║
║ issues and further stuff to do ║
╠══════════════════════════════════════════════════════════════════════════════════╣
║ ║
║ I was as at a conference about malware recently, and someone did a talk on yara ║
║ rules that apply to bytecodes, which got me thinking... ║
║ ║
║ For those of you (like myself) who had no idea what yara was - here is a brief ║
║ explanation: ║
║ ║
║ -> a language/tool for pattern matching ║
║ -> a rule contains 3 parts: [metadata] [strings-to-match] [condition] ║
║ -> generally used for identifying/classifying malware samples ║
║ ║
║ In theory you could create this functionality in whatever language you prefer ║
║ but people seem to like the way these rules are structured ║
║ ║
║ As an example, this is one from here: ║
║ ║
╠══------------------------------------------------------------------------------══╣
║ } ║
║ ║
║ rule prime_constants_char { ║
║ meta: ║
║ author = "_pusher_" ║
║ description = "list of primes [char]" ║
║ date = "2016-07" ║
║ strings: ║
║ $c0 = { 03 05 07 0b 0d 11 13 17 1d 1f 25 29 2b 2f 35 3b 3d 43 47 49 4f ║
║ 53 59 61 65 67 6b 6d 71 7f 83 89 8b 95 97 9d a3 a7 ad b3 b5 bf c1 c5 c7 ║
║ d3 df e3 e5 e9 ef f1 fb } ║
║ condition: ║
║ $c0 ║
║ } ║
╠══------------------------------------------------------------------------------══╣
║ ║
║ The above is a list of primes that a piece of malware may use for various crypto ║
║ applications - and can therefore be used to detect said malware from the ║
║ corresponding bytecodes ║
║ ║
║ Were I to be the author of said malware, I could make some small change to my ║
║ bytecode in order to make this yara rule invalid, say for instance, by reversing ║
║ the order that the primes are listed ║
║ ║
║ This works for static items such as tables, lists and hardcoded values - but the ║
║ more problematic signatures come from bytecode functions ║
║ ║
║ Here is the strings section from a yara rule for detecting CRT implementation in ║
║ some malware analysed by Miracl ║
║ ║
╠══------------------------------------------------------------------------------══╣
║ $c0 = { 51 56 57 e8 ?? ?? ?? ?? 8b 74 24 10 8b f8 89 7c 24 08 83 7e 0c 02 0f 8c ║
║ 99 01 00 00 8b 87 18 02 00 00 85 c0 0f 85 8b 01 00 00 8b 57 1c 42 8b c2 89 57 ║
║ 1c 83 f8 18 7d 17 c7 44 87 20 4a 00 00 00 8b 87 2c 02 00 00 85 c0 74 05 e8 ?? ║
║ ?? ?? ?? 8b 46 04 8b 54 24 14 53 55 8b 08 8b 02 51 50 e8 ?? ?? ?? ?? 8b 4e 0c ║
║ b8 01 00 00 00 83 c4 08 33 ed 3b c8 89 44 24 18 0f 8e c5 00 00 00 bf 04 00 00 ║
║ 00 8b 46 04 8b 0c 07 8b 10 8b 44 24 1c 51 52 8b 0c 07 51 e8 ?? ?? ?? ?? 8b 56 ║
║ 04 8b 4e 08 8b 04 } ║
╠══------------------------------------------------------------------------------══╣
║ ║
║ Our issue here is that this isn't just data we can scramble as we please, we ║
║ have to preserve its functionality. It is the difficulty of translating code ║
║ that does X into different code that still does X that means "signatures" can ║
║ often infer what malware does without running it ║
║ ║
║ The answer to this problem is obfuscation - creating binaries where the ║
║ instructions are scrambled in a way that renders them very tricky to understand, ║
║ but that still execute as intended ║
║ ║
╠══════════════════════════════════════════════════════════════════════════════════╣
║ ║
║ I first came across assembly obfuscation when writing shellcode. As with many ║
║ overflows - the target buffer was a NULL terminated c-string, so any "0x00" ║
║ values in the shellcode would indicate the end of the string, and halt execution ║
║ of the rest of the bytecode. ║
║ ║
║ Most shellcodes might look something like this in assembly: ║
║ ║
╠══------------------------------------------------------------------------------══╣
║ .global _start ║
║ _start: ║
║ .intel_syntax noprefix ║
║ mov rax, 0x59 # 59 = syscall for execve ║
║ lea rdi, [rip+binsh] # load pointer to bin/sh ║
║ mov rsi, 0x00 # sets second arg to null ║
║ mov rdx, 0x00 # sets third arg as 0 ║
║ syscall # invoke execve ║
║ binsh: ║
║ .string "/bin/sh" ║
╠══------------------------------------------------------------------------------══╣
║ ║
║ Even without looking at the bytecode, we can see that there will be "0x00" bytes ║
║ in the bytecode, when we look at it fully we can see there are even more: ║
║ ║
║ (hexdump of an executable when compiled from the above assembly) ║
╠══------------------------------------------------------------------------------══╣
║ 00000000 48 c7 c0 3b 00 00 00 48 8d 3d 08 00 00 00 48 31 |H..;...H.=....H1| ║
║ 00000010 f6 48 31 d2 0f 05 2f 62 69 6e 2f 73 68 00 |.H1.../bin/sh.| ║
╠══------------------------------------------------------------------------------══╣
║ ║
║ After a while of tinkering I ended up with the following assembly: ║
║ ║
╠══------------------------------------------------------------------------------══╣
║ .global _start ║
║ _start: ║
║ .intel_syntax noprefix ║
║ xor rax, rax ║
║ mov al, 59 # 59 = syscall for execve ║
║ mov byte ptr [rsp+0], 0x2f # "/bin/sh" byte by byte ║
║ mov byte ptr [rsp+1], 0x62 ║
║ mov byte ptr [rsp+2], 0x69 ║
║ mov byte ptr [rsp+3], 0x6e ║
║ mov byte ptr [rsp+4], 0x2f ║
║ mov byte ptr [rsp+5], 0x73 ║
║ mov byte ptr [rsp+6], 0x68 ║
║ xor cl, cl ║
║ mov byte ptr [rsp+7], cl ║
║ mov rdi, rsp ║
║ xor rsi, rsi # sets second arg to null ║
║ xor rdx, rdx # sets third arg as 0 ║
║ syscall # invoke execve ║
╠══------------------------------------------------------------------------------══╣
║ ║
║ Which yielded the following "0x00" free bytecode: ║
║ ║
╠══------------------------------------------------------------------------------══╣
║ 00000000 48 31 c0 b0 3b c6 04 24 2f c6 44 24 01 62 c6 44 |H1..;..$/.D$.b.D| ║
║ 00000010 24 02 69 c6 44 24 03 6e c6 44 24 04 2f c6 44 24 |$.i.D$.n.D$./.D$| ║
║ 00000020 05 73 c6 44 24 06 68 30 c9 88 4c 24 07 48 89 e7 |.s.D$.h0..L$.H..| ║
║ 00000030 48 31 f6 48 31 d2 0f 05 |H1.H1...| ║
╠══------------------------------------------------------------------------------══╣
║ ║
║ What exactly I did isn't important necessarily - but in the process of changing ║
║ around the instructions, it became clear to me that the first assembly code ║
║ block was a lot easier to read than the second. I'd done obfuscation by accident ║
║ ║
╠══════════════════════════════════════════════════════════════════════════════════╣
║ ║
║ Later on I came across this article in Prack 71, where the author (Ege BALCI) ║
║ uses a bunch of tricks to transform assembly instructions into obfuscated blocks ║
║ ║
║ The moment it sort of clicked for me was in comparing assembly instructions to ║
║ maths operations: ║
║ ║
║ (from the article): ║
╠══------------------------------------------------------------------------------══╣
║ mov, push, pop, lea -> = ║
║ cmp, sub, sbb -> - ║
║ add, adc -> + ║
║ imul, mul -> * ║
║ idiv, div -> / ║
║ test, and -> & ║
║ or -> | ║
║ xor -> || ║
║ shl -> << ║
║ shr -> >> ║
║ not -> ' ║
╠══------------------------------------------------------------------------------══╣
║ ║
║ Using this, we can sort of map assembly onto equations, as an example the ║
║ following code block is given (where [eax = x], [ebx = y]): ║
║ ║
║ (from the article) ║
╠══------------------------------------------------------------------------------══╣
║ mov ecx, 0x8 ) -------------> z = 8 ) ║
║ shl eax, 0x2 ) -------------> 4x ) ║
║ shl ebx, 0x1 ) -------------> 2y ) ---> ((4x+2y+8)**2) ║
║ add eax, ebx ) -------------> 4x+2y ) ║
║ add eax, ecx ) -------------> 4x+2y+8 ) ║
║ imul eax, eax ) -------------> (4x+2y+8)^2 ) ║
╠══------------------------------------------------------------------------------══╣
║ ║
║ If you were to expand: ║
║ -> ((4x+2y+8)**2) ║
║ ║
║ You would get: ║
║ -> 16x^2 + 16xy + 64x + 4y^2 + 32y + 64 ║
║ ║
║ The article then goes on to say: "When this expression is transformed back into ║
║ assembly code, you'll see that multiple instructions are changed, new ones are ║
║ added, and some disappear. The only problem for us is that some instructions ║
║ stay exactly the same, which may still trigger detection." ║
║ ║
║ No code is provided though to prove/assert this - so I decided to see if I could ║
║ try and create a representation of the expanded equation in assembly. It is ║
║ quite messy, but does result in the desired value in eax. (and, after all, isn't ║
║ messy what we are after with obfuscation?) ║
║ ║
║ For this one: [r10 = x] [r11 = y] ║
╠══------------------------------------------------------------------------------══╣
║ shl r10, 0x02 # r10 = "4x" ║
║ mov ebx, r10 ║
║ imul ebx, ebx ║
║ mov edx, ebx # add "16x**2" ║
║ xor ebx, ebx # clear ║
║ shl r11, 0x02 # r11 = "4y" ║
║ mov ebx, r11 ║
║ imul ebx, r11 # add "16xy" ║
║ add edx, ebx ║
║ xor ebx, ebx # clear ║
║ shl r10, 0x04 # r10 = "64x" ║
║ add edx, r10 # add "64x" ║
║ shr r11, 0x1 # r11 = "2y" ║
║ mov ebx, r11 ║
║ imul ebx, ebx ║
║ add edx, ebx # add "4y**2" ║
║ xor ebx, ebx # clear ║
║ shl r11, 0x04 # r11 = "32y" ║
║ add edx, r11 # add "32y" ║
║ add edx, 0x40 # add 64 ║
╠══------------------------------------------------------------------------------══╣
║ ║
║ As for the claim that "some instructions stay exactly the same", most are ║
║ quite different. Perhaps the shr and shl instructions betray the core ║
║ functionality of the code in a way that can still be picked up by yara rules? ║
║ ║
║ However, I am not in the business of doubting people much more knowledgeable ║
║ than I - so I'll take it as gospel that the above transformation is not ║
║ sufficient for obfuscation ║
║ ║
╠══════════════════════════════════════════════════════════════════════════════════╣
║ ║
║ The interesting bit comes once we commit to trying to translate individual ║
║ assembly instructions, rather than blocks of instructions. Take our little ║
║ heuristic trick from earlier; equating instructions to maths: ║
║ ║
║ If we take the add instruction to be equivalent to "+" ║
║ ║
║ add eax, 0x10 ║
║ ║
║ This can be rephrased as: ║
║ ║
║ x + 16 ║
║ ║
║ As with all additions, this can be expanded to: ║
║ ║
║ x - y - 10 + 2 + 21 + 3 + y ║
║ ║
║ The original instruction can be expanded to: ║
║ ║
╠══------------------------------------------------------------------------------══╣
║ sub eax, y ║
║ sub eax, 0xa ║
║ add eax, 0x2 ║
║ add eax, 0xa5 ║
║ add eax, 0x3 ║
║ add eax, y ║
╠══------------------------------------------------------------------------------══╣
║ ║
║ There are other instructions that can be approximated to "+" aswell: ║
║ ║
║ add ║
║ adc ║
║ sub # add - x ║
║ sbb ║
║ inc # add 1 ║
║ dec # add -1 ║
║ cmp # sub but with flags ║
║ ║
╠══════════════════════════════════════════════════════════════════════════════════╣
║ ║
║ What about logical operators? - turns out we can do a similar thing: ║
║ ║
║ If we take the xor instruction to be equivalent to "||" ║
║ ║
║ xor eax, 0x10 ║
║ ║
║ Can be rephrased as: ║
║ ║
║ x || 16 ║
║ ║
║ The general case for xor translation is (DeMorgan's laws): ║
║ ║
║ x || c = ('x & c) | (x & 'c) ║
║ ║
║ So we can translate our logical operation to: ║
║ ║
║ x || 0x10 = ('x & 0x10) | (x & '0x10) ║
║ ║
║ Where | = bitwise or ║
║ ' = bitwise not ║
║ & = bitwise and ║
║ || = bitwise xor ║
║ ║
║ This could look something like: ║
║ ║
╠══------------------------------------------------------------------------------══╣
║ mov r10, eax ║
║ not r10 ║
║ and r10, 0x10 ║
║ mov r11, 0x10 ║
║ not r11 ║
║ and r11, eax ║
║ or r10, r11 ║
║ mov eax, r10 # store result in eax ║
╠══------------------------------------------------------------------------------══╣
║ ║
║ Other logical instructions that have translations include: ║
║ ║
║ or x, y # -> not((not x) and (not y)) ║
║ and x, y # -> not((not x) or (not y)) ║
║ not x # -> x xor 1 ║
║ test x, y # same as "and" - but with flags ║
║ ║
╠══════════════════════════════════════════════════════════════════════════════════╣
║ ║
║ I'm sure you can see the pattern here, next are the bitwise shift operators: ║
║ ║
║ shl and shr are used (by me) as shorthands for multiplying and dividing by ║
║ powers of 2, or for moving bytes around from high to low ║
║ ║
║ So, if we take shl to be equivalent to "<<" ║
║ ║
║ shl eax, 0x5 ║
║ ║
║ Can be rephrased as: ║
║ ║
║ x << 5 ║
║ ║
║ As long as our shifts total 5, we can step through piecemeal: ║
║ ║
║ x << 2 ║
║ x << 3 ║
║ ║
║ we can't shift greater than 5 - as we could lose some bits at the end. Say we ║
║ have the following byte: ║
║ ║
║ -> [bin] 00000001 -> [dec] 1 ║
║ ║
║ if we use the shl operation on this twice, we would get: ║
║ ║
║ -> [bin] 00000100 -> [dec] 4 ║
║ ║
║ But if we do a shr operation first - then our two shl operations, that rightmost ║
║ bit would get discarded as shift operations discard any bits pushed off the ║
║ bottom of the byte ║
║ ║
║ The solution is to use the rol and ror instructions, which "roll" over the bits ║
║ from the lowest to the highest places - preserving our bits. As a general rule ║
║ we can translate shift operations in the following way: ║
║ ║
║ shr eax, 0x5 ║
║ ║
║ Which can be taken as: ║
║ ║
║ x >> 5 ║
║ ║
║ We need to know the size of our register, in our case eax is 32 bits. Then we ║
║ can choose a "target", which is 5 + n(32) where n = a random number ║
║ ║
║ Lets take rol and ror to be equivalent to "<<<" and ">>>" (I couldn't find any ║
║ convention about what symbol to use for this - so "<<<" it is) ║
║ ║
║ For this example lets take our "target" to be = 37: ║
║ ║
║ x >>> 100 >>> 1 <<< 64 ║
║ ║
║ Which could look something like: ║
║ ║
╠══------------------------------------------------------------------------------══╣
║ ror eax, 0x64 ║
║ ror eax, 0x1 ║
║ rol eax, 0x40 ║
╠══------------------------------------------------------------------------------══╣
║ ║
╠══════════════════════════════════════════════════════════════════════════════════╣
║ ║
║ I haven't quite worked out the equality ones, yet alone all the other super ║
║ important assembly instructions present in most binaries. This paper lists the ║
║ most commonly seen instructions in three popular opensource applications ║
║ ║
║ I've marked the ones I can scramble so far with "<": ║
║ ║
║ mov # 35% ║
║ push # 10% ║
║ call # 6% ║
║ cmp # 5% < ║
║ add # 4% < ║
║ pop # 4% ║
║ lea # 4% ║
║ test # 3% < ║
║ je # 3% ║
║ xor # 2% < ║
║ jmp # 2% ║
║ jne # 2% ║
║ ret # 1% ║
║ inc # 1% < ║
║ sub # 1% < ║
║ fld # 1% ║
║ and # 1% < ║
║ ftsp # 1% ║
║ shl # 1% < ║
║ or # 1% < ║
║ others # 11% ║
║ ║
║ Any load and loop instructions are going to be hard to represent as maths ║
║ operators - though I have had some thoughts about how to obfuscate those parts ║
║ which I may or may not share depending on how far I get with working it out ║
║ ║
║ While searching for information about this topic, I came across something that ║
║ felt very unintuitive. It turns out that addressing in x86 is Turing-complete ║
║ (who knew) so instructions that use addressing can, in theory, run any program. ║
║ mov is one such instruction, and the one this paper focussed on ║
║ ║
║ As an aside, it has one hell of an abstract: ║
║ ║
╠══------------------------------------------------------------------------------══╣
║ It is well-known that the x86 instruction set is baroque, overcomplicated, and ║
║ redundantly redundant. We show just how much fluff it has by demonstrating that ║
║ it remains Turing-complete when reduced to just one instruction. The instruction ║
║ we choose is mov, which can do both loads and stores. We use no unusual ║
║ addressing modes, self-modifying code, or runtime code generation. Using just ║
║ this instruction (and a single unconditional branch at the end of the program to ║
║ make nontermination possible), we demonstrate how an arbitrary Turing machine ║
║ can be simulated. ║
╠══------------------------------------------------------------------------------══╣
║ ║
║ Surely then, there must be a way of representing these troublesome ║
║ loop/move/load instructions using some other instruction? This has the added ║
║ benefit of making your binary look very funny and most likely make any reverse ║
║ engineer re-consider their life choices ║
║ ║
║ I think if I ever do manage to make an obfuscation compiler, it should only use ║
║ xor instructions, which gives me the opportunity to call it the "obfuscatXOR" ║
║ ║
║ CWW ║
╚══════════════════════════════════════════════════════════════════════════════════╝
┏━━┓
BACK
┗━━┛