Selbstmodifizierende Binaries

Date: 2013-09-12

Mir ist gerade so ein Gedanke gekommen. Eine Geschichte die ich mir mal vor Jahren ausgedacht habe um in einem kleinen Programmiercontest an der Uni schneller zu sein als alle anderen. Die Aufgabe damals war es ein sort(1) zu schreiben, das schneller ist als das Standard GNU sort und schneller als die sorts aller anderen Teilnehmer. Was die Sache die ich hier beschreiben werde erst ermöglicht hat, war, dass man nur auf gewissen Listen schnell sortieren musste, nicht auf beliebigen Listen.

Also die Idee war folgende: Beim ersten mal laufen sortiert man ganz normal, egal wie langsam, aber man merkt sich die Umsortierungen, die es braucht um eine sortierte Datei zu erzeugen. Und zwar im Binary selbst. Also muss man hierfür sein eigenes Binary modifizieren, damit der in-binary Cache aufgefüllt wird.

/**
  * @param: self - path to the binary (probably argv[0])
  * @param:
  * @return: Pointer to beginning of the mmaped executable
  */
char *open_self(char *self, int *size) {
    // We open the binary in readonly mode, because
    // we cannot open it read/write, since the kernel locks executed files.
    int fd = open(self, O_RDONLY);
    if (fd < 0) return NULL;

    struct  stat fdinfo;
    /* Stat the filedescriptor and check if it is a actual file */
    assert ( fstat(fd, &fdinfo) != -1);
    *size = fdinfo.st_size;
    char *ptr = mmap(NULL, fdinfo.st_size + 5,
                     PROT_READ, MAP_SHARED,
                     fd, 0);
    // delete our own binary, since we cannot open it read/write, but
    // we can delete it, and add another binary instead
    if (unlink(self) < 0)
        return NULL;

    // create a new binary, that is executable
    int newfd = open(self, O_CREAT | O_RDWR, 0755);
    if (newfd < 0)
        return NULL;

    // make the new binary the right size
    if (ftruncate(newfd, *size) == -1)
        return NULL;
    char *newptr = mmap(NULL, fdinfo.st_size,
                        PROT_READ | PROT_WRITE, MAP_SHARED,
                        newfd, 0);
    if (!newptr)
        return NULL;

    // copy the old binary content to the new file
    memcpy(newptr, ptr, fdinfo.st_size);

    return newptr;
}

Hier zunächst die Funktion die es einem erlaubt das eigene Binary zu modifizieren, dazu muss das laufende Programm zunächst die Datei löschen, aus der sie inkarniert wurde, um danach eine neue Datei mit selbem Inhalt anzulegen. Das muss so gemacht werden, weil der Kernel es einem nicht erlaubt das eigene Binary schreibend zu öffen.

/**
   Simply find an byte pattern in the binary.
 */
int* find_pattern(int* start, int size, int marker, int marker1) {
    for (int i = 0; i< size -1; i++) {
        if (start[i] == marker && start[i+1] == marker1)
            return &start[i];
    }
    return NULL;
}

struct nodename {
    int marker[2];
    char nodename[255];
};

static struct nodename nodename = { {0xDEADBEEF, 0x55555555} };


int main(int argc, char *argv[]) {
        int self_size;
        if (nodename.marker) {
            int *self = (int *)open_self(argv[0], &self_size);
            int *datasection_foobar = find_pattern(self, self_size/4,
                                                   0xDEADBEEF, 0x55555555);
            if (datasection_foobar) {
                printf("binary is unmodified until now. Let's remember,"
                       "where we were run in the first place\n");
                struct utsname buf;
                uname(&buf);
                printf(".data.nodename.nodename = \"%s\"\n", buf.nodename);

                // Copy the name of the machine into the binary
                struct nodename * nodename = (struct nodename *)datasection_foobar;
                strcpy(nodename->nodename, buf.nodename);

                // Remove the marker from memory structure, to disable
                // this code path forever.
                nodename->marker[0] = 0;
                nodename->marker[1] = 0;
            }
        }
        printf("nodename.nodename = \"%s\"\n", nodename.nodename);
}

Hier dann der Rest des Beispielprograms, dass dan den zurückgegebenen Zeiger verwendet um ein Pattern zu finden. Wird das Pattern gefunden läuft man zum ersten mal und man "cached" sich im eigenen Binary, auf welcher Maschine man das erste mal ausgeführt wurde. Beim zweiten Aufruf ist der marker nicht mehr da und dieser Code Pfad ist nicht mehr da. Die Ausgabe sieht dann so aus:

$ gcc main.c  -std=c99

$ ./a.out
binary is unmodified until now. Let's remember, where we were run in the first place
.data.nodename.nodename = "terminus"
nodename.nodename = ""

$ ./a.out
nodename.nodename = "terminus"