Saturday, September 29, 2012

Cleaning off anti-disassembly code: the IDC way.

Every code that can be executed, can also be reversed. Although, there are some tricks to make this task: more time-consuming; more intricate; more difficult to be turned into an automated analysis.

What's the problem? A sequence of executable code can be disassembled in many different ways, so disassemblers have to use some heuristics that, for their nature, are subject to limitations. Anti-Disassembly techniques take advantage of them!

When an executable code is disassembled, each byte of it occurs in the representation of one, and only one, instruction at the time. So, if the disassembler is forced to make an instruction starting from the wrong offset, the instruction shown by it won't match the one being executed.

In this article I will discuss one of these tricks. The trick itself is not really a big deal as it's a well known one, but I found it to be very annoying as I had to deal with it recently, in the attempt of reversing a malware. For this reason, I decided to automate the task of cleaning off the code and developed a little IDC script, based on some assumptions I'm going to explain.

Let's consider a code in the following form:

call loc_40106A
401050: db 'string01', 0
40106A: * assembly instructions *
......: .....


It will be disassembled as:

call loc_401050+0A
401050: * assembly instructions corresponding to the string interpreted as a code *
40106A: * assembly instructions *
......: .....


(This is an approximate representation: note that the bytes of the string interpreted as opcodes may unalign the instruction at 40106A)

The idea behind the trick is very simple: IDA is unable to distinguish between text and code and makes wrong assumptions while disassembling the executable. In particular, IDA doesn't realize that, following the control-flow, the bytes it interpreted as code (right after the call instruction) are never executed and, thus, they are only text.

Moreover, it is very disturbing from the analyst perspective as it basically hides strings, making it useless to search for them, and also results in some weird disassembled instructions, that complicate the listing.

So, what's the idea to clean off the code?

First, we have to search for a short call, that corresponds to the "E8" opcode and we assume that its operand, that is the following 4 bytes, will have a value between 1 and 100.

Once we find a similar situation, we undefine the bytes of code corresponding to the string with "MakeUnkwown", and then we use "MakeStr" to recompose the original string correctly.

Unfortunatly, even if this procedure will solve most of the cases, it isn't general enough to solve all of them.

Let's consider the following example:

seg000:00403E2A E8 09 00 00 00                          call    near ptr loc_403E37+1
seg000:00403E2F 69 64 65 6E 74 69 74 79                 imul    esp, [ebp+6Eh], 79746974h
seg000:00403E37                         loc_403E37:                            
seg000:00403E37 00 57 8D                                add     [edi-73h], dl
seg000:00403E3A 83 8D 28 40 00 FF D0                    or      dword ptr [ebp-0FFBFD8h], 0FFFFFFD0h
seg000:00403E41 83 C7 08                                add     edi, 8
seg000:00403E44 2B CF                                   sub     ecx, edi


After the procedure described above, it will become:

seg000:00403E2A E8 09 00 00 00                          call    near ptr unk_403E38
seg000:00403E2F 69 64 65 6E 74 69 74 79+aIdentity       db 'identity',0
seg000:00403E38 57                      unk_403E38      db  57h 
seg000:00403E39 8D                                      db  8Dh 
seg000:00403E3A 83 8D 28 40 00 FF D0                    or      dword ptr [ebp-0FFBFD8h], 0FFFFFFD0h
seg000:00403E41 83 C7 08                                add     edi, 8
seg000:00403E44 2B CF                                   sub     ecx, edi


As you can see, IDA has also undefined some bytes after the string (which should have been legitimate assembly instructions); so, to solve the problem, we may think to go at the end of it and reconvert everything in code, using "MakeCode". But is that enough?

seg000:00403E2A E8 09 00 00 00                          call    loc_403E38
seg000:00403E2F 69 64 65 6E 74 69 74 79+aIdentity       db 'identity',0
seg000:00403E38
seg000:00403E38                         loc_403E38:                             
seg000:00403E38 57                                      push    edi
seg000:00403E39 8D                                      db  8Dh 
seg000:00403E3A 83 8D 28 40 00 FF D0                    or      dword ptr [ebp-0FFBFD8h], 0FFFFFFD0h
seg000:00403E41 83 C7 08                                add     edi, 8
seg000:00403E44 2B CF                                   sub     ecx, edi


No, it isn't! IDA tries to convert the bytes into code, but if they are unaligned it might do that starting from the wrong offset or even fail to complete the task. This happens because IDA originally disassembled the string bytes as code, which led to the disassembly of the following bytes in incorrect instructions like the above "or  dword ptr [ebp-0FFBFD8h]". Because of this, when you try to realign the code, and assemble the correct instruction starting from the byte "8D", IDA fails because the opcode "8D" needs to take the bytes from the "or" instruction, and IDA won't break an instruction that already exists.

Even undefining some of the bytes after the string and then trying to translate them back into code doesn't work because, for the nature of the problem, you don't know exacly how many bytes is better to consider to do this task. You can make some heuristic and try with about ten bytes, but this solution doesn't always give accurate results. Moreover, if there's another call in those ten bytes the things go even worse!

The basic idea to try to solve the problem is to manually parse the raw bytes. I wrote a little IDC script to do that; here is a brief explanation of how it works:

  • It searches for the "0xE8" byte and takes its operand, that is the size of the string.
  • It undefines the subsequent "size" bytes and recomposes them as a string.
  • Then, it iterates the following procedure:

    1. It tries to undefine a byte and to make an instruction from it (after the first execution of this step, the undefining operation won't have any effect in case of step 3.).

    2. If the instruction is made, go back to 1. and continue with the bytes after the instruction.

    3. If the instruction wasn't made, undefine one more byte (to a maximum of 16), in the attempt at making it, and repeat from step 1.

In this way, only one byte at a time is undefined and so is the building of the corresponding instruction, whenever possible.

I also assumed that if there are four subsequent instructions that are already interpreted as code, then the bytes are aligned and the work is done. Finally, if any of the recomposed instructions is a call, the algorithm will start over again.

Here is the final script (change "MIN" and "MAX" with the addresses that define the range of the code in which you want to run the script - e.g. MIN = 0x00401000, MAX = 0x00404FFF):

auto i, j;
auto Size;
auto Delta, DeltaTemp, DeltaUndef;

for(i = MIN; i < MAX; i++)
{
   if(Byte(i) == 0xE8)
   {
       Size = Dword(i + 1);
       if(Size > 1 && Size < 100)
       {
            Message(" %08x \n ", i);
            MakeUnknown(i+5, Size,  DOUNK_DELNAMES);
            MakeStr(i+5, i+5+Size);
            Delta = 0;
            DeltaUndef = 0;
            for(j = 0; j < 4; j++)
            {
                 if(Byte(i+5+Size + Delta) == 0xE8)
                 {
                     break;
                 }
                 MakeUnknown(i+5+Size + Delta, 1, DOUNK_DELNAMES);
                 DeltaTemp = MakeCode(i+5+Size + Delta);
                 if(DeltaTemp != 0)
                 {
                     Delta = Delta + DeltaTemp;
                     DeltaUndef = 0;
                 }
                 else
                 {
                    j = 0;
                    DeltaUndef++;
                    if(DeltaUndef > 16)
                    {
                       break;
                    }
                    if(Byte(i+5+Size + Delta + DeltaUndef) == 0xE8)
                    {
                        break;
                    }
                    MakeUnknown(i+5+Size + Delta + DeltaUndef, 1, DOUNK_DELNAMES);
                    Message("-- Undef %08x \n", i+5+Size + Delta + DeltaUndef);
                 }
            }
       }
   }
}


P.S. I know the code looks twisty and more complicated than it needs to be, but I had to write it this way to bypass some glitches I was having with a couple of IDC APIs.

No comments:

Post a Comment