Assignment: TRICKY

Changelog:

Your Task

  1. Create a C or Python program called infect1.c or infect1.cc or infect1.py that will produce a modified copy of target1.exe so that when run outputting to a terminal instead of producing:

    Initialize application.
    Begin application execution.
    Terminate application.
    

    it will produce the output:

    Initialize application.
    You have been infected with a virus!
    Begin application execution.
    Terminate application. 
    

    and still run the same application code as the original version of target1.exe. (That is, your modified version should replace or add to the existing code in target1.exe, not replace target1.exe with a new program. If we supplied an alternate version of target1.exe with the same layout but slightly different application code, your infect1 program should still work on that modified version without changing the different application code.)

    Given a copy of infect1.c (or infect1.cc) and target1.exe, we should be able to do:

    gcc -Os -o infect1 infect1.c
    ./infect1 target1.exe target1-infected.exe
    chmod +x target1-infected.exe
    ./target1-infected.exe
    

    (or the same with g++ for .cc files, adding an option like -std=gnu++17 if needed) and see the output above. Similarly, given infect1.py, we should be able to do:

    python3 infect1.py target1.exe target1-infected.exe
    chmod +x target1-infected.exe
    ./target1-infected.exe
    

    We have given what we hope are very extensive hints below on one way to accomplish this (but you are welcome to find alternate strategies).

  2. Do the same for target2.exe in a program called infect2.c or infect2.cc or infect2.py. (If you end up writing one program that works for both, you may submit the same infect program twice.)

  3. In a file called answers.txt, briefly answer the following questions. (We do not expect more than a sentence or two for the first two questions.)

    1. How did you identify the file offsets in the target executables to overwrite (for the jump and and for the virus code)?

    2. How did you produce the machine code to insert for the jump to the virus code?

    3. If your infect programs have a hard-coded offsets or something similar, how would you automate finding the locations in target executables to overwrite so that it would work on other target programs?

  4. Upload your three files to the submission site.

Hints

General advice

  1. You can divide this task into two parts:

    • writing and insert code for the “virus” (that prints out “You have been infected with a virus!”)
    • add a jump to the virus code you inserted
  2. To simplify the assignment, you can hardcode the input and output file names in your infect program. That is, infect1.c opens and reads target1.exe and opens and writes target1-infected.exe.

  3. You should use the utility objdump to examine the executable target.exe. The option --disassemble is useful. In particular, you need to determine the starting address of the virus code. The dissasembly will also help you determine the opcodes of the instructions that you need to insert (i.e., a push instruction and a ret instruction). You may wish to consult the objdump manual.

  4. A very useful program to examine the file is a hex editor such as ghex or bless. If it is not already installed, you can install ghex using sudo apt-get install ghex and similar for bless.

Encoding a jump operation

  1. One way to encode a jump to AddressOfVirusFunction is using:

    pushq $AddressOfVirusFunction
    ret
    

    This results in 6 bytes of machine code on x86-64. Compared to using a normal jump instruction, this has the advantage of not being dependent on where the machine code is placed.

  2. Alternately one could also encode a jump using a conventional jump instruction:

    jmp AddressOfVirusFunction
    

    but the machine code for this will encode the address relatively (as an offset from the address of the jump instruction), so you will need to change the machine code based on where it is inserted.

  3. A push of a 32-bit constant (on 32- or 64-bit x86) can be encoded as an 0x68 byte followed by the (little-endian) constant. A ret is encded as c3. A jump can be encoded as an 0xe9 byte followed by a 32-bit offset from the address of the following instruction.

Placing the jump/returning to the application

  1. Identify where the constant stings “Initialize appliation.” and “Begin application execution.” are referenced to locate relevant parts of the application code.

  2. A problem with placing either version of the jump is that:

    1. we need to do whatever the code that was replaced with the jump so the application still functions normally; and
    2. we need to make sure the virus code returns back to the application afterwards.

    A convenient solution for this is to replace a function return with a jump.

    This allows us to solve issue 2 by having the virus code end with a ret instruction that will substitute for the original code’s ret.

  3. It seems, however, like it would be a problem that a ret instruction in x86-64 is one byte but either version of our tricky jump is at least 5 bytes. However, often there is padding after a function return like:

      400661:       c3                      retq       
      400662:       66 66 66 66 66 2e 0f    data32 data32 data32 data32 nopw %cs:0x0(%rax,%rax,1)
      400669:       1f 84 00 00 00 00 00 
    

    The instruction at address 0x400662 is an unreachable no-op instruction, which is inserted only because the compiler and/or linker wanted to ensure that the next function started at an address that was a multiple of something. This is a “cavity” that gives a virus writer some room to work.

  4. To insert both the virus code and the jump itself, you must determine the location in the executable to insert them, which is not the same as address at which they will be loaded in memory. The location that objdump most prominently shows is the expected memory address.

    • One option is to look at the program headers object dump can output to figure out what offsets in the executable are loaded into what bytes of memory.
    • Another option is to pass options to objdump to get it to display the offset of code within the executable file.
    • Another option is to get a hexadecimal dump of the raw file and look for bytes shown in objdump output in the actual executable file to find their location.
    • Yet another option would be for your infect.c to search for particular bytes in the executable file itself.

Writing virus code

  1. The “virus” code we want you to insert could be written as follows (assuming the strategy of replacing a ret instruction with the jump to this code; you will need to modify this if you use a different strategy):

    virus:
        movl $1, %eax  // 1 = SYS_write
        movl $1, %edi  // system call first argument = stdout
        leal string(%rip), %esi // system call second argument = string
        movl $37, %edx // system call third argument = length of string
        syscall
        retq
    string: 
        .asciz "You have been infected with a virus!\n"
    

    This code assembly is carefully written to avoid dependencies on its location in the executable.

    (This code makes a Linux system call to write a string to stdout. We could also have made a function call to the puts function by calling the puts@plt stub, but the location of puts@plt varies between the two executables.)

    (When using the above assembly, if you redirect the infected executable’s output to a file instead of sending it to the terminal normally, you may see the output in an unexpected order: This assembly code outputs via a write system call directly and the application code uses buffered stdio.h functions. When stdout isn’t a terminal, stdio.h’s buffering will not write lines of output immediately. For this assignment, we only require you to support stdout being a terminal, so you should not need to worry about this issue.)

  2. You can put this assembly in a .S file and then use something like

    gcc -c file.S
    

    to get an .o file. You can then examine this .o file with objdump --file-offset -d file.o or similar.

Find space for the virus code

  1. Look for a large area of nops in the disassembly to determine where to insert the virus code. Record the address of this location in memory to generate the “tricky jump” code you will insert elsewhere in the executable.

  2. In target1.exe, there is a large segment of padding (nops) between functions that is a suitable place to put the virus code.

  3. In target2.exe, there is not any obvious space to put a non-trivial amount of virus code based on the disassembly. But you can take advantage of how programs are loaded in “page” sized units. On our x86-64 Linux systems, pages are 4096 bytes (= 0x1000 bytes), and typically whole pages of an executable are loaded into memory.

    This is refleced in the program headers of target2.exe:

    LOAD off    0x0000000000000000 vaddr 0x0000000000400000 paddr 0x0000000000400000 align 2**12
         filesz 0x0000000000000520 memsz 0x0000000000000520 flags r--
    LOAD off    0x0000000000001000 vaddr 0x0000000000401000 paddr 0x0000000000401000 align 2**12
         filesz 0x00000000000005d5 memsz 0x00000000000005d5 flags r-x
    LOAD off    0x0000000000002000 vaddr 0x0000000000402000 paddr 0x0000000000402000 align 2**12
         filesz 0x0000000000000218 memsz 0x0000000000000218 flags r--
    LOAD off    0x0000000000002e10 vaddr 0x0000000000403e10 paddr 0x0000000000403e10 align 2**12
         filesz 0x0000000000000240 memsz 0x0000000000789e78 flags rw-
    

    The load instructions use offests 0x0, 0x1000, 0x2000 and target addresses 0x400000, 0x401000, 0x402000 because these align with the beginning of 4096 byte pages in memory and the executable file. Because this is done there are some additional cavaties in the executable file: for example, the second load instruction only loads bytes 0x1000 through 0x15d5 of the executable and the third loads bytes 0x2000 through 0x2218, so apparently bytes 0x15d5 through 0x2000 are not used.

    Although these bytes appear to be unused and unloaded from the program headers, they are actually loaded because of how x86-64 Linux memory management works. For example though the program headers of target2.exe requests to load 0x5d5 bytes from ofsets 0x1000 through 0x2000 into address 0x401000 through 0x4015d5:

    LOAD off    0x0000000000001000 vaddr 0x0000000000401000 paddr 0x0000000000401000 align 2**12
         filesz 0x00000000000005d5 memsz 0x00000000000005d5 flags r-x
    

    because x86-64 Linux manages memory in page-sized chunks, it implements this by loading 0x1000 bytes from the file into addresses 0x401000 through 0x402000. It doesn’t know how to load less than 0x1000 bytes.

    This means that — even if the “LOAD” program header is not modified — data placed at offset 0x15d6 will be loaded into memory the same as data placed at offset 0x1500 would be because it’s part of the same page.

File I/O

  1. We are reading and writing binary files—not textfiles. You may need to open files in binary mode, next text mode.

  2. To read from and write to a binary file in C, you can use fopen, fread, and fwrite. You can run man fopen, man fread, etc. to get documentation for how these functions are called, or search online. An example usage of a program that copies “input.dat” to “output.dat” is the following:

     #include <stdio.h> 
     #include <stdlib.h>
    
     int main(void) { 
         FILE *in;
         FILE *out;
         char *buffer;
         int size;
         in = fopen("input.dat", "rb");
         /* get size of input.dat, by 
            moving to the end of the file */
         fseek(in, 0, SEEK_END);
         size = ftell(in);
         /* then, return to the
            beginning of the file */
         fseek(in, 0, SEEK_SET);
         buffer = malloc(size);
         fread(buffer, 1, size, in);
         fclose(in);
         out = fopen("output.dat", "wb");
         fwrite(buffer, 1, size, out);
         fclose(in);
     }