Introduction

To keep things simple, in cybersecurity operations, there are two teams : the blue team and the red team, the defenders and the attackers. Even though they seem completely different, each one’s goal is to make the other team’s job as hard as possible and eventually to defeat it. To do that, blue-teamers often use reverse-engineering to get a grasp on how malware works and how to mitigate its effect. Here, we will see how we can try to make their job harder if they want to analyze our virus. There is usually two stages in the malware analysis : the static analysis and the dynamic one. The static consists in deassembling/decompiling the code to try to understand it though sometimes you cannot really understand a malware internal function just by reading it so you have to dynamically analyze it using debuggers to trace the execution and analyze ot step by step.

Countering static analysis

Preliminary analysis

The fist step of static analysis will obviously be to list symbol names, variables or anything relevant in the code. For usual malware, it is recommended to strip the binary and to alter the functions/variables names to avoid it being useful in any way. There are a lot of automated software for that. In our case, let’s assume that it is the code of an infected binary that has been recovered. There won’t be any symbol names related to the viral code so that won’t be a problem. There is still one thing that is critical for analysis : raw strings. By using the strings command, you can list all the alphanumerical strings present in the binary. Even though our code usually won’t rely on strings too much, we will still use some of them that would be very useful to analysts like the infection folders or /bin/sh for the backdoor. To avoid having those raw strings in the code a good thing would be to store an encrypted value that would be decrypted at runtime, preferably not only alphanumerical so it doesn’t appear in the string list (avoid base 64, ROT or this kind of algorithm, XOR-based ones are usually a better solution).

Code analysis

Once the preliminary checks are done, it is usually time to analyze the code. In our case, let’s assume that the analyst found where the malicious code is, it will disassemble it to try to understand it. To deceive them, we will use several obfuscation to fool automated tools or even their comprehension of the code. Let’s review some of these.

Here is a code sample that we are gonna alter and see the differences objdump is gonna show us and how it can help us to deceive analysts.


BITS 64

section .text

_start:
    push    rbp
    mov     rbp, rsp
    xor     rax, rax
    add     rax, 0xff
    push    rax
    mov     rdx, rax
    sub     rax, rdx
    leave
    ret

Once compiled it gives us the following output when we disassemble it with objdump: obf_sample

Let’s see how we are gonna alter this output and try to hide what the code is doing without altering its function with a few techniques.

Dead code

The first and very easy way to obfuscate code is to add dead code inside that will show up but never be executed. It is very easy to do but also to undo. The principle is to add an unconditional jump to skip all the useless code. Obviously since it is very easy to do, it is also very easy to undo as an analysts. Since the jump is unconditional, the execution flow is very easy to follow so dead code will be very easy to detect. One of the other downside of this technique is that putting useless code takes place so when the payload size matters, it is not a very good idea to use it.


BITS 64

section .text

_start:
    push    rbp
    mov     rbp, rsp
    xor     rax, rax
    jmp     end_of_dead_code ; All the code that follows before the label will never get executed
    push    rax
    pop     rbx
    mov     rcx, rax
    call    end_of_dead_code
    ret
    jmp     _start
end_of_dead_code:
    add     rax, 0xff
    push    rax
    mov     rdx, rax
    sub     rax, rdx
    leave
    ret

Now objdump is gonna show us all those useless instructions that are gonna add some noise to the analysis :

obf_dead_code

Fake jumps

Let’s get to real things now, the “fake jump” technique is one that I quite like and it is very simple to use. Its principle is very simple, it will jump on the next “real” instruction that we want to execute, and we put one or two dead bytes between the jump and the instruction. By carefully controlling these dead byte values to opcodes, we will make some tools like objdump or the gdb disassembler that those dead bytes are the beginning of an instruction which is gonna alter their interpretations of the next few instructions to totally different ones though the execution will remain the same.


BITS 64

section .text

_start:
    push    rbp
    mov     rbp, rsp
    jmp $+4
    db 0x48, 0x8d ; Here we use REX.W LEA
    xor     rax, rax
    add     rax, 0xff
    push    rax
    mov     rdx, rax
    sub     rax, rdx
    leave
    ret

And here is what we get once we disassemble this sample with objdump :

obf_fake_jump

We can see that it changed the way our code was disassembled although it won’t change anything about its execution. Placing a lot of those in your code will completely affect the output of this kind of tool like objudmp or gdb. However, this technique won’t work with modern disassembler like IDA which will disassemble the destination of the jump, which is our instructions.

Fake ret

Another interesting technique to use to counter static code analysis is the fake return technique. First, let’s talk about what the call/ret instructions really do. call is like a jump at the difference that it will push the rip value on the stack before jumping so that when ret is called it will pop that value and jump on it to get back to where we were. It means that if we manage to push the address of the next “real” instruction on the stack and then use ret, it won’t do anything but most of the disassemblers/decompilers still nowadays are gonna stop decompiling the procedure as soon as it reaches the ret instruction. I will use a macro here for clarity.


BITS 64

%macro PUSH_RET 0
    push    rax
    push    rax
    lea     rax, [rel %%ret_to]
    mov     QWORD [rsp + 0x8], rax
    pop     rax
    ret
    %%ret_to:
%endmacro

section .text
    global _start

_start:
    push    rbp
    mov     rbp, rsp
    PUSH_RET
    xor     rax, rax
    add     rax, 0xff
    push    rax
    mov     rdx, rax
    sub     rax, rdx
    leave
    ret

obf_fake_ret

As you can see here, although the code that will be executed is basically the same, the disassembler shows it as being two different procedures, including one that will never be called (supposedly of course).

Countering dynamic analysis

Anti-AV (ultra-basic)

Let’s consider that the infected system might have an AV/EDR running and that we know that some of them will catch us. We can have a “blacklist” that contains program name that will stop the execution of our program if running. To do that, we can check, for every numeric folder in the /proc directory (which are fds), their status file inside which there is a field that contains the name of the executable. By doing that, we can check every program running and see if we are safe to proceed with malicious activity.

Anti-debugger

To be fair, anti-debugging is quite hard in the sense that a debugger can alter our program at runtime and by doing so, it can change return or register values and defeat our anti-debugging. Though, it is still useful to try to avoid tampering for two reasons, an automated program tracing our execution would not be able to find out about the anti-tampering routine. The other reason is that even though a human manually tracing the execution would be able to patch the anti-tampering checks, it would still induce a more or less significant loss of time depending on the technique used. The technique that is the most common is to use the ptrace syscall in various way to check that. The first way os to try to attach to the parent process. In case it fails, it means that it is already tracing us which would imply that the parent process is probably a debugger. Since a process can be traced by only one process, another way to do so is to fork and then trace the child while the child will trace the parent. By doing so, we can check if one of them is already being traced (our own tracing would fail) but if we manage to “double trace” ourself, no process would be able to attach after the check since we already did. Even though those ptrace techniques are great and useful, in the case of file infector, they can alter the way our host program works (it breaks bash for example), which is something we absolutely don’t want for stealth reasons. To avoid that, the technique I used for my viruses is to check the /proc/self/status file. This file contains plenty of information about the current process (ourself) including a TracerPid field. If it is not null, we are being debugged.

Conclusion

This is a little introduction to code obfuscation and anti-tampering. Even though most of these techniques are not applicable in the world of today, it is still a good way to learn the notions and the interest of this kind of techniques. For more advanced obfuscation stuff, I advise you tyo read my next post on oligomorphism.