Threat Level: green Handler on Duty: Johannes Ullrich

SANS ISC: Jumping into Shellcode SANS ISC InfoSec Forums

Participate: Learn more about our honeypot network

Sign Up for Free!   Forgot Password?
Log In or Sign Up for Free!
Jumping into Shellcode

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$ 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$ 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
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...


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

I will be teaching next: Reverse-Engineering Malware: Malware Analysis Tools and Techniques - SANS Threat Hunting London 2022


652 Posts
ISC Handler
Mar 29th 2021

Sign Up for Free or Log In to start participating in the conversation!