Jumping into Shellcode

Published: 2021-03-29
Last Updated: 2021-03-29 06:14:14 UTC
by Xavier Mertens (Version: 1)
0 comment(s)

Malware analysis is exciting because you never know what you will find. In previous diaries[1], I already explained why it's important to have a look at groups of interesting Windows API call to detect some behaviors. The classic example is code injection. Usually, it is based on something like this:

1. You allocate some memory
2. You get a shellcode (downloaded, extracted from a specific location like a section, a resource, ...)
3. You copy the shellcode in the newly allocated memory region
4. You create a new threat to execute it.

But it's not always like this! Last week, I worked on an incident involving a malicious DLL that I analyzed. The technique used to execute the shellcode was slightly different and therefore interesting to describe it here.

The DLL was delivered on the target system with an RTF document. This file contained the shellcode:

remnux@remnux:/MalwareZoo/20210318$ rtfdump.py suspicious.rtf
    1 Level  1        c=    3 p=00000000 l=    1619 h=     143;       5 b=       0   u=     539 \rtf1
    2  Level  2       c=    2 p=00000028 l=      91 h=       8;       2 b=       0   u=      16 \fonttbl
    3   Level  3      c=    0 p=00000031 l=      35 h=       3;       2 b=       0   u=       5 \f0
    4   Level  3      c=    0 p=00000056 l=      44 h=       5;       2 b=       0   u=      11 \f1
    5  Level  2       c=    0 p=00000087 l=      33 h=       0;       4 b=       0   u=       2 \colortbl
    6  Level  2       c=    0 p=000000ac l=      32 h=      13;       5 b=       0   u=       5 \*\generator
    7 Remainder       c=    0 p=00000655 l=  208396 h=   17913;       5 b=       0   u=  182176 
      Whitespace = 4878  NULL bytes = 838  Left curly braces = 832  Right curly braces = 818

This file is completely valid from an RTF format point of view, will open successfully, and render a fake document. But the attacker appended the shellcode at the end of the file (have a look at stream 7 which has a larger size and a lot of unexpected characters ("u="). Let's try to have a look at the shellcode:

remnux@remnux:/MalwareZoo/20210318$ rtfdump.py suspicious.rtf -s 7 | head -20
00000000: 0D 0A 00 6E 07 5D A7 5E  66 D2 97 1F 65 31 FD 7E  ...n.].^f...e1.~
00000010: D9 8E 9A C4 1C FC 73 79  F0 0B DA EA 6E 06 C3 03  ......sy....n...
00000020: 27 7C BD D7 23 84 0B BD  73 0C 0F 8D F9 DF CC E7  '|..#...s.......
00000030: 88 B9 97 06 A2 F9 4D 8C  91 D1 5E 39 A2 F5 9A 7E  ......M...^9...~
00000040: 4C D6 C8 A2 2D 88 D0 C4  16 E6 2B 1C DA 7B DD F7  L...-.....+..{..
00000050: C4 FB 61 34 A6 BE 8E 2F  9D 7D 96 A8 7E 00 E2 E8  ..a4.../.}..~...
00000060: BB A2 D9 53 1C F3 49 81  77 93 30 16 11 9D 88 93  ...S..I.w.0.....
00000070: D2 6C 9D 56 60 36 66 BA  29 3E 73 45 CE 1A BE E3  .l.V`6f.)>sE....
00000080: 5A C7 96 63 E0 D7 DF C9  21 2F 56 81 BD 84 6C 2D  Z..c....!/V...l-
00000090: CF 4C 4E BE 90 23 47 DC  A7 A9 8E A2 C3 A3 2E D1  .LN..#G.........

It looks encrypted and a brute force of a single XOR encoding was not successful. Let's see how it works in a debugger.

First, the RTF file is opened to get a handle and its size is fetched with GetFileSize(). Then, a classic VirtualAlloc() is used to allocate a memory space equal to the size of the file. Note the "push 40" which means that the memory will contain executable code (PAGE_EXECUTE_READWRITE):


Usually, the shellcode is extracted from the file by reading the exact amount of bytes. The malware jumps to the position of the shellcode start in the file and reads bytes until the EOF. In this case, the complete RTF file is read then copied into the newly allocated memory:

This is the interesting part of the code which processes the shellcode:

The first line "mov word ptr ss:[ebp-18], 658" defines where the shellcode starts in the memory map. In a loop, all characters are XOR'd with a key that is generated in the function desktop.70901100. The next step is to jump to the location of the decoded shellcode:

The address where to jump is based on the address of the newly allocated memory (0x2B30000) + the offset (658). Let's have a look at this location (0x2B30658):

Sounds good, we have a NOP sled at this location + the string "MZ". Let's execute the unconditional JMP:

We reached our shellcode! Note the NOP instructions and also the method used to get the EIP:

02B30665 | E8 00000000 | call 2B3066A | call $0
02B3066A | 5B          | pop ebx      | 

Now the shellcode will execute and perform the next stages of the infection...

[1] https://isc.sans.edu/forums/diary/Malware+Triage+with+FLOSS+API+Calls+Based+Behavior/26156

Xavier Mertens (@xme)
Senior ISC Handler - Freelance Cyber Security Consultant

0 comment(s)


Diary Archives