Below is an example of what I believe would classify as metamorphic code written in C. I'm afraid I don't have a great deal of experience writing portable C code, so it may require some modification to compile on other platforms (I'm using an old version of Borland on Windows). Also, it relies on the target platform being x86 since it involves some machine code generation. In theory it should compile on any x86 OS though.
How it works
Each time the program is run, it generates a randomly modified copy of itself, with a different filename. It also prints out a list of offsets that have been modified so you can see it actually doing something.
The modification process is very simplistic. The source code is just interpreted with sequences of assembly instructions that effectively do nothing. When the program is run, it finds these sequences and randomly replaces them with different code (which obviously also does nothing).
Hardcoding a list of offsets obviously isn't realistic for something that other people need to be able to compile, so the sequences are generated in a way that makes them easy to identify in a search through the object code, hopefully without matching any false positives.
Each sequence starts with a push operation on a certain register, a set of instructions that modify that register, and then a pop operation to restore the register to its initial value. To keep things simple, in the original source all of the sequences are just PUSH EAX
, eight NOP
s, and POP EAX
. In all subsequent generations of the app, though, the sequences will be entirely random.
Explaining the code
I've split the code up into multiple parts so I can try to explain it step by step. If you want to compile it yourself, you'll just need to join all the parts together.
First some fairly standard includes:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
Next we have defines for various x86 opcodes. These will typically be combined with other values to generate a full instruction. For example, the PUSH
define (0x50
) by itself is PUSH EAX
, but you can derive the values for other registers by adding an offset in the range 0 to 7. Same thing for POP
and MOV
.
#define PUSH 0x50
#define POP 0x58
#define MOV 0xB8
#define NOP 0x90
The last six are the prefix bytes of several two-byte opcodes. The second byte encodes the operands and will be explained in more detail later.
#define ADD 0x01
#define AND 0x21
#define XOR 0x31
#define OR 0x09
#define SBB 0x19
#define SUB 0x29
const unsigned char prefixes[] = { ADD,AND,XOR,OR,SBB,SUB,0 };
JUNK
is a macro that inserts our sequence of junk operations anywhere we want in the code. As I explained before, it's initially just writing out PUSH EAX
, NOP
, and POP EAX
. JUNKLEN
is the number of NOP
s in that sequence - not the full length of the sequence.
And in case you're not aware, __emit__
is a pseudo-function that injects literal values directly into the object code. I suspect it may be something you need to port if you're using a different compiler.
#define JUNK __emit__(PUSH,NOP,NOP,NOP,NOP,NOP,NOP,NOP,NOP,POP)
#define JUNKLEN 8
Some global variables where our code will be loaded. Global variables are bad, but I'm not a particularly good coder.
unsigned char *code;
int codelen;
Next we have a simple function that will read our object code into memory. I never free that memory because I just don't care.
Notice the JUNK
macro calls inserted at random points. You're going to see a lot more of these throughout the code. You can insert them almost anywhere, but if you're using a real C compiler (as opposed to C++) it'll complain if you try to put them before or in-between variable declarations.
void readcode(const char *filename) {
FILE *fp = fopen(filename, "rb"); JUNK;
fseek(fp, 0L, SEEK_END); JUNK;
codelen = ftell(fp);
code = malloc(codelen); JUNK;
fseek(fp, 0L, SEEK_SET);
fread(code, codelen, 1, fp); JUNK;
}
Another simple function to write the application out again after it has been modified. For the new filename we just replace the last character of the original filename with a digit that is incremented each time. No attempt is made to check whether the file already exists and that we're not overwriting a crucial piece of the operating system.
void writecode(const char *filename) {
FILE *fp;
int lastoffset = strlen(filename)-1;
char lastchar = filename[lastoffset];
char *newfilename = strdup(filename); JUNK;
lastchar = '0'+(isdigit(lastchar)?(lastchar-'0'+1)%10:0);
newfilename[lastoffset] = lastchar;
fp = fopen(newfilename, "wb"); JUNK;
fwrite(code, codelen, 1, fp); JUNK;
fclose(fp);
free(newfilename);
}
This next function writes out a random instruction for our junk sequence. The reg parameter represents the register we're working with - what will be pushed and popped at either end of the sequence. The offset is the offset in the code where the instruction will be written. And space gives the number of bytes we have left in our sequence.
Depending on how much space we have, we may be restricted to which instructions we can write out, otherwise we choose at random whether it's a NOP
, MOV
or one of the others. NOP
is just a single byte. MOV is five bytes: our MOV opcode (with the reg parameter added), and 4 random bytes representing the number moved into the register.
For the two byte sequences, the first is just one of our prefixes chosen at random. The second is a byte in the range 0xC0
to 0xFF
where the least significant 3 bits represent the primary register - i.e. that must be set to the value of our reg
parameter.
int writeinstruction(unsigned reg, int offset, int space) {
if (space < 2) {
code[offset] = NOP; JUNK;
return 1;
}
else if (space < 5 || rand()%2 == 0) {
code[offset] = prefixes[rand()%6]; JUNK;
code[offset+1] = 0xC0 + rand()%8*8 + reg; JUNK;
return 2;
}
else {
code[offset] = MOV+reg; JUNK;
*(short*)(code+offset+1) = rand();
*(short*)(code+offset+3) = rand(); JUNK;
return 5;
}
}
Now we have the equivalent function for reading back one of these instructions. Assuming we've already identified the reg
from the PUSH
and POP
operations at either end of the sequence, this function can attempt to validate whether the instruction at the given offset
is one of our junk operations and that the primary register matches the given reg
parameter.
If it finds a valid match, it returns the instruction length, otherwise it returns zero.
int readinstruction(unsigned reg, int offset) {
unsigned c1 = code[offset];
if (c1 == NOP)
return 1; JUNK;
if (c1 == MOV+reg)
return 5; JUNK;
if (strchr(prefixes,c1)) {
unsigned c2 = code[offset+1]; JUNK;
if (c2 >= 0xC0 && c2 <= 0xFF && (c2&7) == reg)
return 2; JUNK;
} JUNK;
return 0;
}
This next function is the main loop the searches for and replaces the junk sequences. It starts by looking for a PUSH
opcode followed by a POP
opcode on the same register eight bytes later (or whatever JUNKLEN
was set to).
void replacejunk(void) {
int i, j, inc, space;
srand(time(NULL)); JUNK;
for (i = 0; i < codelen-JUNKLEN-2; i++) {
unsigned start = code[i];
unsigned end = code[i+JUNKLEN+1];
unsigned reg = start-PUSH;
if (start < PUSH || start >= PUSH+8) continue; JUNK;
if (end != POP+reg) continue; JUNK;
If the register turns out to be ESP
, we can safely skip it because we'll never use ESP
in our generated code (stack operations on ESP
need special consideration that isn't worth the effort).
if (reg == 4) continue; /* register 4 is ESP */
Once we've matched a likely looking PUSH and POP
combination, we then try to read the instructions in-between. If we successfully match the length of bytes we're expecting, we consider that a match that can be replaced.
j = 0; JUNK;
while (inc = readinstruction(reg,i+1+j)) j += inc;
if (j != JUNKLEN) continue; JUNK;
We then pick one of 7 registers at random (as explained before we don't consider ESP
), and write out the PUSH
and POP
operations for that register at either end of the sequence.
reg = rand()%7; JUNK;
reg += (reg >= 4);
code[i] = PUSH+reg; JUNK;
code[i+JUNKLEN+1] = POP+reg; JUNK;
Then all we need to do is fill in the space in-between using our writeinstruction
function.
space = JUNKLEN;
j = 0; JUNK;
while (space) {
inc = writeinstruction(reg,i+1+j,space); JUNK;
j += inc;
space -= inc; JUNK;
}
And here's where we display the offset that we just patched.
printf("%d
",i); JUNK;
}
}
Finally we have the main function. This just calls the functions previously described. We read in the code, replace the junk, then write it out again. The argv[0]
argument contains the application filename.
int main(int argc, char* argv[]) {
readcode(argv[0]); JUNK;
replacejunk(); JUNK;
writecode(argv[0]); JUNK;
return 0;
}
And that's all there is to it.
Some final notes
When running this code, obviously you need to make sure the user has the appropriate permissions to write out a file in the same location as the original code. Then once the new file has been generated, you'll typically need to rename it if you're on a system where the file extension is important, or set its execute attributes if that is needed.
Finally, I suspect you may want to run the generated code through a debugger rather than just executing it directly and hoping for the best. I found that if I copied the generated file over the original executable, the debugger was happy to let me step through it while still viewing the original source code. Then whenever you get to a point in the code that says JUNK, you can pop into the assembly view and look at the code that has been generated.
A