objcopy. In fact, if you read that
section, you may find an exercise suggesting the use of
objcopy to update the droppers in Chapter 7 to drop a file
packed together with the binary. If you are having trouble solving this
exercise, you can find a potential solution here.In case you haven’t read that great book ;) , let’s introduce some concepts so this post is self-contained. A dropper is a simple program whose purpose is to drop some other program, either on disk or in memory, and execute it. They are popular in malware deployment because droppers themselves don’t really perform any suspicious actions and they can easily bypass antivirus checks.
The file being dropped can come from two main sources: a network connection or data stored in the dropper itself. Chapter 7 of Heavy Wizardry 101 explores mainly how to drop programs out of a network connection. In this post, we’re going to see how we can easily pack a binary inside another binary and drop and run it.
The payload
I’m not going to use any malware here, and actually we don’t have to. For our educational purposes, any program will work, so let’s use a static “hello world!” program. Let’s make it static so the file has a size closer to a real-world example. We can produce this payload very easily using:
$ gcc -static -xc - -o payload << EOM
>#include
>int main() {puts("Hello, world!");}
>EOM
Once we have our dropee, we can use objcopy to
convert it into an object file, which we can use with the linker, with
some extra advantages as we will see in a sec.
$ objcopy -I binary -O elf64-x86-64 payload payload.elf
You can obtain the same result using the linker with a command like this:
ld -r -b binary payload -o payload.elf
This may be confusing because you might think: Hey, pico!,
“hello” is already an ELF file, why do you have to convert
it again? Well, you can try it yourself. What will happen is that the
linker will try to link our dropper and our dropee and will
find out that there are two main functions. However, the
big advantage of converting the file this way is something else. Let’s
see.
$ readelf -a hello.elf
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 758656 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 5
Section header string table index: 4
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .data PROGBITS 0000000000000000 00000040
00000000000b9280 0000000000000000 WA 0 0 1
[ 2] .symtab SYMTAB 0000000000000000 000b92c0
0000000000000060 0000000000000018 3 1 8
[ 3] .strtab STRTAB 0000000000000000 000b9320
000000000000003a 0000000000000000 0 0 1
[ 4] .shstrtab STRTAB 0000000000000000 000b935a
0000000000000021 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
D (mbind), l (large), p (processor specific)
There are no section groups in this file.
There are no program headers in this file.
There is no dynamic section in this file.
There are no relocations in this file.
No processor specific unwind information to decode
Symbol table '.symtab' contains 4 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 NOTYPE GLOBAL DEFAULT 1 _binary_hello_start
2: 00000000000b9280 0 NOTYPE GLOBAL DEFAULT 1 _binary_hello_end
3: 00000000000b9280 0 NOTYPE GLOBAL DEFAULT ABS _binary_hello_size
No version information found in this file.
There are three main things you have to pay attention to:
- The file is now of type
REL. In other words, it’s like any other object file we produce using our compiler or assembly. - Keeping aside the symbols (we’ll talk about this in a sec), it only
contains a
datasegment and therefore it will be treated as raw data by the linker. - It automatically defines three symbols that we can use in our program. Those symbols will tell us where the data starts, ends, and its size in memory—which is very convenient.
The dropper
The dropper is pretty simple. It just has to copy a memory block into a file. Let’s take a quick look at the complete code, and then we can dive into the specifics.
#include <fcntl.h>
#include <unistd.h>
extern unsigned char _binary_payload_start[];
extern unsigned char _binary_payload_end[];
#define BLOCK_SIZE 1024
int main () {
int fd, n;
size_t size = _binary_payload_end - _binary_payload_start;
if ((fd = open ("/tmp/a", O_CREAT | O_TRUNC| O_WRONLY, 0777)) > 0) {
for (size_t i = 0; i < size; i += write (fd, _binary_payload_start + i, BLOCK_SIZE));
close (fd);
}
execve("/tmp/a",NULL,NULL);
}The dropper just accesses the _binary_payload_start and
_binary_payload_end generated by the linker to get access
to its payload. As the whole payload was in a .data section
that ends up in a PT_LOAD segment, the content in the file
is directly loaded into memory when the dropper is executed, so the
dropper just needs to write that memory area into a file. In the general
case, to reduce the detection chances, the payload may also be
encrypted. In that case, the loop writing the file should decrypt the
data before writing to the file.
The way to access the symbols shown in the code is the easiest way to
get it right without compiler warnings. Note, however, that getting
access to these symbols may be confusing if done directly. The gotcha
behind this is that, for this kind of symbols, the symbol value in the
ELF file is the actual value we want, while for regular symbols (like
variables or functions), the symbol actually contains a pointer to the
real entity it represents (a variable or a function). Overall, this
means that in this case, in order to get the value we want, we have to
get the address of the symbol with the & operator,
which, in the general case, returns the value of the symbol (that is,
the pointer to the variable, function), but in this case, we want the
actual value, not the pointer.
What all this means is that, if you declare your external variables
as the type they should have (a generic pointer), you need to use the
& operator and a cast to obtain the value you really
want.
extern void *_binary_payload_start;
(...)
unsigned char *buf = (unsigned char*)&_binary_payload_start;
That’s it. We are now ready to try our file dropper.
Testing our new dropper
To be able to try our dropper, we need to compile our code and include the payload in the resulting binary. We can get this using the following command:
$ gcc -o dropper-file dropper-file.c payload.elf
$ ./dropper-file
Hello, world!
Hope you found this interesting. Now you can extend this example following the different techniques in Chapter 7 of the book to drop the files in memory and make forensic analysis a bit more complicated.