When you pull apart a packed Windows binary, one of the first questions is always the same: where is the real payload, and how is it stored? A common answer is that it never left the file. The packer carried it along the whole time, compressed and tucked away inside the executable’s resource section, and only revealed it in memory once the process was running.
This post walks through that technique, resource dropping, from the analyst’s seat. To recover a payload that a packer stashed in the .rsrc section, you have to understand exactly how it got there, so we reconstruct the full chain: we hide a small binary inside a carrier, then write the unpacker that finds it, extracts it, and decompresses it back into memory. The recovery side is the point; the packing side is shown so you can recognise and reverse it.
We work with 64-bit PE (PE32+) binaries throughout, and we compile everything on Linux with mingw. The principle is identical for 32-bit (PE32). We will not re-explain what a packer is or detail the PE format here; the Wikipedia page on PE is excellent.
The knowledge in this article is for strictly educational and defensive purposes: understanding how packers conceal code so you can analyse and recover it. Do not use these techniques to build or distribute malicious software. That is both unethical and illegal, and we accept no responsibility for misuse.
Prerequisites
To follow along and reproduce the work:
- A relatively recent Linux system.
- The usual build tools (
gcc,make). - The mingw cross-compiler and
windres(thegcc-mingw-w64-x86-64package on Ubuntu 20.04). - The
zlib1g-devpackage. readpe(pev) to inspect PE sections.- A text editor.
Unlike kernel work, nothing here puts your system at risk: it all compiles and runs as ordinary user-land code.
Vocabulary
A few terms are used precisely throughout:
- Binary: a compiled object, such as an executable or a library.
- Payload: a piece of code or data necessary and sufficient to carry out some action inside a process, often malicious.
- Packing / unpacking: respectively, encrypting, compressing, or hiding a binary or payload, and the reverse, decrypting, decompressing, or revealing it.
- Packer / unpacker: the software (or the act) that performs packing and unpacking.
How the payload is hidden
Some packers and malware families hide secret code inside an executable that looks harmless at a glance. One way to do this is to store a payload, or an entire second executable, directly in the resource section (.rsrc) of the carrier binary, usually compressed and often encrypted. When the carrier runs, that hidden binary is unpacked in memory and used in the rest of the unpacking chain.
To recover such a payload we first need to understand how it was placed there. So we build the packing side ourselves: hide a small binary inside a carrier’s resources, then retrieve and decompress it in memory. This is one method among many; real samples vary.
The payload to hide
The hidden binary is deliberately trivial. What it does is irrelevant; what matters is how it is concealed and recovered. So it is just a program that prints Hello!.
#include <stdio.h>
int main(void)
{
printf("Hello !\n");
return 0;
}
We compile it as a PE32+ executable with mingw:
$ x86_64-w64-mingw32-gcc hidden.c -o hidden.exe
$ file hidden.exe
hidden.exe: PE32+ executable (console) x86-64, for MS Windows
Compressing the payload
Before embedding it, the packer compresses the binary. Here we use zlib’s compress2() at maximum level. The program below reads a file, compresses it, and writes the result to compressed_binary.
#include <zlib.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <stdio.h>
int main(int ac, char **av)
{
if (ac != 3) {
printf("Usage: %s <src_file> <size_of_file>\n", av[0]);
return EXIT_FAILURE;
}
/* input */
char *clear_filename = av[1];
int src_size = atoi(av[2]);
char *clear = (char *)malloc(sizeof(char) * src_size);
/* output */
char *compressed = (char *)malloc(sizeof(char) * src_size);
uLongf dst_size;
/* reading and compression */
int fd_rd = open(clear_filename, O_RDONLY);
read(fd_rd, clear, src_size);
close(fd_rd);
compress2((Bytef *)compressed, &dst_size, (Bytef *)clear, (uLong)src_size, 9);
/* writing */
int fd_wr = open("compressed_binary", O_WRONLY | O_CREAT, 0444);
write(fd_wr, compressed, dst_size);
close(fd_wr);
return EXIT_SUCCESS;
}
Reading it through: the program takes the file name and its size in bytes as arguments. It allocates a clear buffer for the source data and a compressed buffer for the output, with dst_size receiving the final compressed length. It reads the source with open() and read(), then calls zlib’s compress2(), whose prototype is:
int compress2(Bytef *dest, uLongf *destLen, const Bytef *source, uLong sourceLen, int level);
dest: where the compressed data is written.destLen: where the compressed size in bytes is written.source: where the data to compress is read from.level: compression level,9being the maximum.
Finally it writes the compressed bytes to compressed_binary. Compile against zlib (-lz) and run it:
gcc compress.c -o compress -lz
ls -l hidden.exe
# -rwxrwxr-x 1 ech0 ech0 316853 avril 13 23:55 hidden.exe
./compress hidden.exe 316853
file compressed_binary
# compressed_binary: zlib compressed data
The payload compressed cleanly, shrinking from 316853 to 89766 bytes. Size reduction is a side effect; the real goal is to stash the binary in the resources.
In a real sample there would usually be an additional encryption layer at this point, for example a one-time-pad mask also stored in the resources. For this walkthrough we stay with compression alone, but expect to peel off a cipher in practice.
Embedding it as a resource
With the compressed payload ready, we describe it to windres with a resource (.rc) file:
IDR_RCDATA0 RCDATA compressed_binary
The syntax is straightforward:
IDR_RCDATA0: the resource name.RCDATA: the resource type (raw data).compressed_binary: the file to include as a resource.
Then windres turns the .rc file into a COFF object (a Windows object file) we can link in:
x86_64-w64-mingw32-windres compressed_binary.rc -O coff -o compressed_binary.rc.o
At this point the working directory looks like this:
.
├── compress
├── compress.c
├── compressed_binary
├── compressed_binary.rc
├── compressed_binary.rc.o
├── hidden.c
└── hidden.exe
0 directories, 7 files
The resource is ready. Everything from here is the recovery side: the code that finds this embedded resource, pulls it out, and decompresses it in memory.
Recovering the payload
The unpacker is itself a PE32+ binary, since this is the code that runs on the target and recovers the dropped payload from its own resources.
Finding and extracting the resource
Recovery uses three Windows API calls: FindResourceA, LoadResource, and LockResource. The Microsoft documentation covers them in full, so here is just the code that pulls a resource out of the binary’s own .rsrc section:
#include <windows.h>
int main(void)
{
HRSRC hRes;
HGLOBAL hResLoad;
PUCHAR Data;
hRes = FindResourceA(NULL, "IDR_RCDATA0", RT_RCDATA);
hResLoad = LoadResource(NULL, hRes);
Data = LockResource(hResLoad);
}
It compiles:
x86_64-w64-mingw32-gcc depacker.c -o depacker.exe
But we have not yet linked our resource into this binary, so the lookup for IDR_RCDATA0 can only fail. In fact, there is no .rsrc section in the binary at all:
$ readpe -S depacker.exe | grep 'Name:'
Name: .text
Name: .data
Name: .rdata
Name: .pdata
Name: .xdata
Name: .bss
Name: .idata
Name: .CRT
Name: .tls
We fix that by linking the resource object file into the build:
$ x86_64-w64-mingw32-gcc depacker.c compressed_binary.rc.o -o depacker.exe
$ readpe -S depacker.exe | grep 'Name:'
Name: .text
Name: .data
Name: .rdata
Name: .pdata
Name: .xdata
Name: .bss
Name: .idata
Name: .CRT
Name: .tls
Name: .rsrc
Now .rsrc is present, carrying our compressed hidden.exe. The unpacker can extract the resource into memory. What remains is to decompress it.
Decompressing in memory
To unpack the payload we need four things: the compressed data, the size of the compressed data, the size of the decompressed data (to allocate the output buffer), and a decompression function. We have the data and the function. The two sizes have to be supplied to the program; here we simply hard-code them.
Hard-coding the sizes is a shortcut for the walkthrough. A real packer would carry them the same way it carries the payload, for instance in a second resource (IDR_RCDATA1) that the unpacker reads first. When analysing a sample, that is exactly the kind of companion resource worth looking for.
We also need zlib for Windows. There is no need for a Windows machine to build it: we cross-compile it with mingw on Linux.
wget http://zlib.net/zlib-1.2.12.tar.gz
tar xf zlib-1.2.12.tar.gz
rm zlib-1.2.12.tar.gz
cd zlib-1.2.12
# Affect PREFIX = x86_64-w64-mingw32-
vim win32/Makefile.gcc
# Cross-Compilation of zlib via mingw
BINARY_PATH=/usr/x86_64-w64-mingw32/bin INCLUDE_PATH=/usr/x86_64-w64-mingw32/include LIBRARY_PATH=/usr/x86_64-w64-mingw32/lib make -f win32/Makefile.gcc
With zlib in hand, the final unpacker retrieves the resource and decompresses it in place:
#include <windows.h>
#include "zlib.h"
int main(void)
{
HRSRC hRes;
HGLOBAL hResLoad;
PUCHAR Data;
PUCHAR uncompressed;
ULONG src_size = 89766; // Hard-coded size of compressed binary
ULONG dst_size = 316853; // Hard-coded size of decompressed binary
hRes = FindResourceA(NULL, "IDR_RCDATA0", RT_RCDATA);
hResLoad = LoadResource(NULL, hRes);
Data = LockResource(hResLoad);
uncompressed = (PUCHAR)malloc(sizeof(char) * (dst_size + 1));
uncompress(uncompressed, &dst_size, Data, src_size);
}
The uncompress() function mirrors the compress2() we used to pack the payload in the first place. We compile, pointing at our cross-compiled zlib with -L, -lz, and -I:
x86_64-w64-mingw32-gcc depacker.c compressed_binary.rc.o -o depacker.exe -L./zlib-1.2.12/ -lz -I zlib-1.2.12/
The unpacker is complete: it locates the resource, extracts it, and decompresses the original hidden.exe back into memory. You can run it in a Windows environment to confirm.
Where the trail leads next
As written, the unpacker stops at “in memory”: it recovers the payload but never runs it. In a real sample that final step, getting the decompressed binary to execute, is the whole purpose, and it is called dropping (here, dropping from the resources), performed by a dropper. There are several ways to reach it: writing the payload to disk and launching it, mapping and running it directly in memory, DLL injection, process hollowing, and so on. Each of those is its own analysis problem.
Resource storage is also just one source among many. The same dropper pattern applies whether the hidden binary sits in the resources as it does here, in a separate section, split across several sections, or fetched over the network at runtime. The carrier changes; the shape of the technique does not. Recognise that shape, know where to look, and the payload stops hiding.
