RiseFromTheDead
11 minutos de lectura
Se nos proporciona un binario de 64 bits llamado rise
y también un archivo core:
$ file *
core: ELF 64-bit LSB core file, x86-64, version 1 (SYSV), SVR4-style, from './rise flag', real uid: 0, effective uid: 0, real gid: 0, effective gid: 0, execfn: './rise', platform: 'x86_64'
rise: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=8341bf2064b7903b6e87d69c63a1849338d3f1e6, for GNU/Linux 3.2.0, not stripped
El archivo core se corresponde con la ejecución de rise
(en concreto, el comando fue ./rise flag
).
Ingeniería inversa
Si abrimos rise
en Ghidra, veremos el siguiente código fuente descompilado en C para la función main
(se han renombrado algunas variables):
int main(int argc, char **argv) {
undefined8 *flag_chunk;
undefined8 uVar1;
ulong i;
char *pcVar3;
int ret;
undefined8 *puVar4;
byte zero;
zero = 0;
if (argc < 2) {
pcVar3 = "./program";
if (argc == 1) {
pcVar3 = *argv;
}
fprintf(stderr, "Usage: %s <secret file>\n", pcVar3);
ret = -1;
} else {
ret = open(argv[1], 0);
if (ret == -1) {
perror("Opening file");
ret = -1;
} else {
flag_chunk = (undefined8 *) mmap(NULL, 0x1000, 3, 2, ret, 0);
if (flag_chunk == NULL) {
perror("Mapping file");
ret = -1;
} else {
*(undefined8 *) ((long) flag_chunk + 0xaf) = 0;
flag_chunk[0x1ff] = 0;
puVar4 = (undefined8 *) ((long) flag_chunk + 0xb7U & 0xfffffffffffffff8);
i = (ulong) (((int) flag_chunk - (int) puVar4) + 0x1000U >> 3);
for (; i != 0; i--) {
*puVar4 = 0;
puVar4 = puVar4 + (ulong)zero * -2 + 1;
}
uVar1 = init_shuffle_list(flag_chunk);
*flag_chunk = 0;
*(undefined8 *) ((long) flag_chunk + 0xa7) = 0;
puVar4 = (undefined8 *) ((ulong) (flag_chunk + 1) & 0xfffffffffffffff8);
for (i = (ulong) (((int) flag_chunk - (int) (undefined8 *) ((ulong) (flag_chunk + 1) & 0xfffffffffffffff8)) + 0xafU >> 3); i != 0; i--) {
*puVar4 = 0;
puVar4 = puVar4 + (ulong) zero * -2 + 1;
}
shuf(uVar1, flag_chunk);
puts((char *) flag_chunk);
kill(0, 0xb);
ret = 0;
}
}
}
return ret;
}
En primer lugar, se abre el archivo cuyo nombre proviene de argv[1]
(por el archivo core, sabemos que está abriendo un archivo llamado flag
). A continuación, asigna la memoria con mmap
y copia el contenido del archivo en esa región de memoria.
Después de eso, usa init_shuffle_list
:
char * init_shuffle_list(char *flag_chunk) {
char cVar1;
int fd;
char *p_flag;
byte random_byte;
char *local_30;
byte b;
fd = open("/dev/urandom", 0);
local_30 = NULL;
p_flag = flag_chunk + 0xaf;
LAB_001012c4:
read(fd, &random_byte, 1);
do {
b = random_byte;
if (random_byte < 0xaf) {
cVar1 = pos_in_list(local_30, random_byte);
if (cVar1 == 0) break;
}
read(fd, &random_byte, 1);
} while (true);
append_list(&local_30, b, *flag_chunk);
flag_chunk = flag_chunk + 1;
if (flag_chunk == p_flag) {
close(fd);
return local_30;
}
goto LAB_001012c4;
}
Lo que vemos aquí es que algunos bytes aleatorios se generan con /dev/urandom
. Estos bytes aleatorios se almacenan justo al después de la flag real (offset 0xaf
). Entonces, ya sabemos que la longitud de la flag es 0xaf
(175 caracteres).
Si el byte aleatorio (en realidad, el número aleatorio) es menor que 0xaf
, usa pos_in_list
para determinar si hay un carácter en ese índice aleatorio:
undefined8 pos_in_list(char *local_30, byte int) {
if (local_30 == NULL) {
return 0;
}
do {
if (*(byte *) ((long) local_30 + 8) == int) {
return 1;
}
local_30 = *(char **) local_30;
} while (local_30 != NULL);
return 0;
}
Una vez que se encuentra un carácter en el índice aleatorio, se almacena con append_list
:
void append_list(char **param_1, undefined param_2,u ndefined param_3) {
long **pplVar1;
long **pplVar2;
long *plVar3;
undefined8 *puVar4;
pplVar1 = (long **) *param_1;
if ((long **) *param_1 == NULL) {
puVar4 = (undefined8 *) malloc(0x10);
*puVar4 = 0;
*(undefined *) (puVar4 + 1) = param_2;
*(undefined *) ((long) puVar4 + 9) = param_3;
*param_1 = (char *) puVar4;
} else {
do {
pplVar2 = pplVar1;
pplVar1 = (long **) *pplVar2;
} while ((long **) *pplVar2 != NULL);
plVar3 = (long *) malloc(0x10);
*pplVar2 = plVar3;
*plVar3 = 0;
*(undefined *) (*pplVar2 + 1) = param_2;
*(undefined *) ((long) *pplVar2 + 9) = param_3;
}
}
Esta función se ve un poco rara, pero básicamente toma el byte de la flag y lo almacena junto con el byte aleatorio de antes en un chunk de tamaño 0x20
en el heap. De alguna manera crea un mapeo entre los bytes de la flag y los bytes aleatorios. Veremos esto más tarde con GDB.
Finalmente, el programa llama a shuf
, que desordena la flag usando el mapeo anterior, y sobrescribe el buffer de la flag:
void shuf(long *param_1, long param_2) {
if (param_1 != NULL) {
do {
*(undefined *) (param_2 + (ulong) *(byte *) (param_1 + 1)) = *(undefined *) ((long) param_1 + 9);
*(undefined *) ((long) param_1 + 9) = 0;
param_1 = (long *) *param_1;
} while (param_1 != NULL);
}
}
Finalmente, el programa se rompe debido al uso de kill(0, 0xb)
(SIGSEGV).
Análisis dinámico
Ya sabemos que la longitud de la flag es 0xaf
, así que podemos crear una flag falsa para esto y probar con GDB:
$ python3 -q
>>> from string import ascii_letters, digits
>>> flag = 'HTB{' + ((digits + ascii_letters) * 10)[:170] + '}'
>>> flag
'HTB{0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJ}'
>>> with open('flag', 'w') as f:
... f.write(flag)
...
175
Ahora, podemos ejecutar el programa con GDB y establecer breakpoints:
# gdb -q rise
Reading symbols from rise...
(No debugging symbols found in rise)
gef> break shuf
Breakpoint 1 at 0x12ee
gef> run flag
Starting program: /tmp/rise flag
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Breakpoint 1, 0x00005555555552ee in shuf ()
En este punto, podemos ver ambos argumentos para shuf
:
gef> p/x $rdi
$1 = 0x5555555592a0
gef> p/x $rsi
$2 = 0x7ffff7ffa000
gef> vmmap heap
[ Legend: Code | Heap | Stack | Writable | ReadOnly | None | RWX ]
Start End Size Offset Perm Path
0x0000555555559000 0x000055555557a000 0x0000000000021000 0x0000000000000000 rw- [heap] <- $rdx, $rdi, $r9
gef> vmmap 0x7ffff7ffa000
[ Legend: Code | Heap | Stack | Writable | ReadOnly | None | RWX ]
Start End Size Offset Perm Path
0x00007ffff7ffa000 0x00007ffff7ffb000 0x0000000000001000 0x0000000000000000 rw- /tmp/flag <- $rbx, $rsi
La primera es una dirección del heap, que contiene el mapa entre bytes de flag y bytes aleatorios. Y el otro es el chunk de mmap
que se asignó con el texto de la flag (aunque se ha borrado).
En el heap, tenemos muchos chunks con las asignaciones aleatorias. Estos son algunos ejemplos:
gef> visual-heap -n
0x555555559000: 0x0000000000000000 0x0000000000000291 | ................ |
0x555555559010: 0x0000000000000000 0x0000000000000000 | ................ |
* 39 lines, 0x270 bytes
0x555555559290: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x5555555592a0: 0x00005555555592c0 0x000000000000484b | ..UUUU..KH...... |
0x5555555592b0: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x5555555592c0: 0x00005555555592e0 0x0000000000005498 | ..UUUU...T...... |
0x5555555592d0: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x5555555592e0: 0x0000555555559300 0x000000000000425f | ..UUUU.._B...... |
0x5555555592f0: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559300: 0x0000555555559320 0x0000000000007b30 | .UUUU..0{...... |
0x555555559310: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559320: 0x0000555555559340 0x000000000000302d | @.UUUU..-0...... |
0x555555559330: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559340: 0x0000555555559360 0x000000000000311e | `.UUUU...1...... |
0x555555559350: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559360: 0x0000555555559380 0x0000000000003210 | ..UUUU...2...... |
0x555555559370: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559380: 0x00005555555593a0 0x000000000000332a | ..UUUU..*3...... |
0x555555559390: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x5555555593a0: 0x00005555555593c0 0x0000000000003463 | ..UUUU..c4...... |
0x5555555593b0: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x5555555593c0: 0x00005555555593e0 0x0000000000003599 | ..UUUU...5...... |
0x5555555593d0: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x5555555593e0: 0x0000555555559400 0x0000000000003679 | ..UUUU..y6...... |
0x5555555593f0: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559400: 0x0000555555559420 0x000000000000373f | .UUUU..?7...... |
0x555555559410: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559420: 0x0000555555559440 0x0000000000003812 | @.UUUU...8...... |
0x555555559430: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559440: 0x0000555555559460 0x000000000000397f | `.UUUU...9...... |
0x555555559450: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559460: 0x0000555555559480 0x0000000000006149 | ..UUUU..Ia...... |
0x555555559470: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559480: 0x00005555555594a0 0x00000000000062a8 | ..UUUU...b...... |
0x555555559490: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x5555555594a0: 0x00005555555594c0 0x0000000000006368 | ..UUUU..hc...... |
0x5555555594b0: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x5555555594c0: 0x00005555555594e0 0x0000000000006477 | ..UUUU..wd...... |
0x5555555594d0: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x5555555594e0: 0x0000555555559500 0x00000000000065aa | ..UUUU...e...... |
0x5555555594f0: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559500: 0x0000555555559520 0x00000000000066a6 | .UUUU...f...... |
0x555555559510: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559520: 0x0000555555559540 0x0000000000006776 | @.UUUU..vg...... |
0x555555559530: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559540: 0x0000555555559560 0x0000000000006816 | `.UUUU...h...... |
Ahora podemos usar finish
para terminar la función shuf
y ver la flag desordenada:
gef> finish
...
gef> x/s $rsi
0x7ffff7ffa000: "gkI1jHrG3wBv4HLj2q8dTmhFZI5vxz1lXdYPrqnyZN3u80g2{oKnfOuDseHJySc7r9klWMGaqahH6xpDxBt6b0ttzEBjp2TBlDI4sPVucKwwAvU1CioQ5mgdA6J3k709Ni7QLeFVinaCYfJ9Xsm}OUGAT5z8yMCShEc4FRfEbbeoRWp"
Un carácter interesante es {
. En el mapeo anterior vemos 7b30
. Esto significa que el byte {
(0x7b
) se almacenará en el índice 0x30
:
gef> x/c $rsi + 0x30
0x7ffff7ffa030: 0x7b
Como resultado, podemos deducir que el mapeo indica dónde se colocará el carácter de la flag en el buffer de salida. Esto es HTB{
, usando el mapeo anterior:
gef> x/c $rsi + 0x4b
0x7ffff7ffa04b: 0x48
gef> x/c $rsi + 0x98
0x7ffff7ffa098: 0x54
gef> x/c $rsi + 0x5f
0x7ffff7ffa05f: 0x42
gef> x/c $rsi + 0x30
0x7ffff7ffa030: 0x7b
gef> printf "%c%c%c%c\n", ((char*) $rsi)[0x4b], ((char*) $rsi)[0x98], ((char*) $rsi)[0x5f], ((char*) $rsi)[0x30]
HTB{
Obsérvese que el mapeo se modifica una vez que se usa una entrada, se elimina el carácter de la flag:
gef> visual-heap -n
0x555555559000: 0x0000000000000000 0x0000000000000291 | ................ |
0x555555559010: 0x0000000000000000 0x0000000000000000 | ................ |
* 39 lines, 0x270 bytes
0x555555559290: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x5555555592a0: 0x00005555555592c0 0x000000000000004b | ..UUUU..KH...... |
0x5555555592b0: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x5555555592c0: 0x00005555555592e0 0x0000000000000098 | ..UUUU...T...... |
0x5555555592d0: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x5555555592e0: 0x0000555555559300 0x000000000000005f | ..UUUU.._B...... |
0x5555555592f0: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559300: 0x0000555555559320 0x0000000000000030 | .UUUU..0{...... |
0x555555559310: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559320: 0x0000555555559340 0x000000000000002d | @.UUUU..-0...... |
0x555555559330: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559340: 0x0000555555559360 0x000000000000001e | `.UUUU...1...... |
0x555555559350: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559360: 0x0000555555559380 0x0000000000000010 | ..UUUU...2...... |
0x555555559370: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559380: 0x00005555555593a0 0x000000000000002a | ..UUUU..*3...... |
0x555555559390: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x5555555593a0: 0x00005555555593c0 0x0000000000000063 | ..UUUU..c4...... |
0x5555555593b0: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x5555555593c0: 0x00005555555593e0 0x0000000000000099 | ..UUUU...5...... |
0x5555555593d0: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x5555555593e0: 0x0000555555559400 0x0000000000000079 | ..UUUU..y6...... |
0x5555555593f0: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559400: 0x0000555555559420 0x000000000000003f | .UUUU..?7...... |
0x555555559410: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559420: 0x0000555555559440 0x0000000000000012 | @.UUUU...8...... |
0x555555559430: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559440: 0x0000555555559460 0x000000000000007f | `.UUUU...9...... |
0x555555559450: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559460: 0x0000555555559480 0x0000000000000049 | ..UUUU..Ia...... |
0x555555559470: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559480: 0x00005555555594a0 0x00000000000000a8 | ..UUUU...b...... |
0x555555559490: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x5555555594a0: 0x00005555555594c0 0x0000000000000068 | ..UUUU..hc...... |
0x5555555594b0: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x5555555594c0: 0x00005555555594e0 0x0000000000000077 | ..UUUU..wd...... |
0x5555555594d0: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x5555555594e0: 0x0000555555559500 0x00000000000000aa | ..UUUU...e...... |
0x5555555594f0: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559500: 0x0000555555559520 0x00000000000000a6 | .UUUU...f...... |
0x555555559510: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559520: 0x0000555555559540 0x0000000000000076 | @.UUUU..vg...... |
0x555555559530: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559540: 0x0000555555559560 0x0000000000000016 | `.UUUU...h...... |
Análisis del archivo core
Podemos encontrar fácilmente la flag desordenada mirando las strings de este archivo:
# strings -100 core | sort -u
b5xo1ar_gmcrirttdne8ohsoshf6t/:ewm1ea8a_2h4r/ms7cetH6nig2oss}__eyer6aot{_ea_/peot./4-2tho.fb7Bac7_rerr__/su1_n!5p8e/c.7Tlcm_saos_sdm_rpa6p3tl2recty_22ffemef_i0e03u-vl5enh9deru
Ahora, necesitamos encontrar el mapeo. Si cargamos el archivo core en GDB, es posible que queramos mirar el heap, pero no funciona:
# gdb -q -c core
warning: Can't open file /mnt/rise during file-backed mapping note processing
warning: Can't open file /lib/x86_64-linux-gnu/libc-2.31.so during file-backed mapping note processing
warning: Can't open file /lib/x86_64-linux-gnu/ld-2.31.so during file-backed mapping note processing
warning: Can't open file /mnt/flag during file-backed mapping note processing
[New LWP 995]
Python Exception <class 'FileNotFoundError'>: [Errno 2] No such file or directory: '/proc/995/maps'
Core was generated by `./rise flag'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0 0x00007f7067d8b087 in ?? ()
gef> visual-heap -n
[!] Failed to get the arena, heap commands may not work properly.
[!] No valid arena
Por lo tanto, debemos encontrarlo usando otras opciones, como xxd
. Dado que todos los chunks del heap son de tamaño 0x21
, podemos usar grep
para encontrar los mapeos:
# xxd core | grep -A 1 '0000 0000 0000 0000 2100 0000 0000 0000'
00005290: 0000 0000 0000 0000 2100 0000 0000 0000 ........!.......
000052a0: c032 ea4b 4e56 0000 3300 0000 0000 0000 .2.KNV..3.......
000052b0: 0000 0000 0000 0000 2100 0000 0000 0000 ........!.......
000052c0: e032 ea4b 4e56 0000 7700 0000 0000 0000 .2.KNV..w.......
000052d0: 0000 0000 0000 0000 2100 0000 0000 0000 ........!.......
000052e0: 0033 ea4b 4e56 0000 5d00 0000 0000 0000 .3.KNV..].......
000052f0: 0000 0000 0000 0000 2100 0000 0000 0000 ........!.......
00005300: 2033 ea4b 4e56 0000 4700 0000 0000 0000 3.KNV..G.......
00005310: 0000 0000 0000 0000 2100 0000 0000 0000 ........!.......
00005320: 4033 ea4b 4e56 0000 5b00 0000 0000 0000 @3.KNV..[.......
00005330: 0000 0000 0000 0000 2100 0000 0000 0000 ........!.......
00005340: 6033 ea4b 4e56 0000 9200 0000 0000 0000 `3.KNV..........
00005350: 0000 0000 0000 0000 2100 0000 0000 0000 ........!.......
00005360: 8033 ea4b 4e56 0000 9300 0000 0000 0000 .3.KNV..........
00005370: 0000 0000 0000 0000 2100 0000 0000 0000 ........!.......
00005380: a033 ea4b 4e56 0000 0f00 0000 0000 0000 .3.KNV..........
...
Y aquí tenemos los índices donde se colocará cada byte de flag en la flag desordenada. Solo necesitamos revertir el algoritmo.
Por ejemplo, el primer byte es el del índice 0x33
en la flag desordenada, el segundo byte es el del índice 0x77
, el tercer byte es el del índice 0x5d
y así sucesivamente.
Solución
Para esto, escribí la salida anterior en un archivo (heap.txt
) y luego lo usé con Python para reordenar la flag:
#!/usr/bin/env python3
shuffled_flag = 'b5xo1ar_gmcrirttdne8ohsoshf6t/:ewm1ea8a_2h4r/ms7cetH6nig2oss}__eyer6aot{_ea_/peot./4-2tho.fb7Bac7_rerr__/su1_n!5p8e/c.7Tlcm_saos_sdm_rpa6p3tl2recty_22ffemef_i0e03u-vl5enh9deru'
positions = []
with open('heap.txt') as f:
f.readline()
while (line := f.readline()):
positions.append(int(line[30:32], 16))
f.readline()
flag = ''.join(shuffled_flag[p] for p in positions)
print(flag)
Si ejecutamos el script, obtendremos la flag:
$ python3 solve.py
HTB{by_the_powers_of_https://man7.org/linux/man-pages/man5/core.5.html_i_resurrect_this_process_from_the_dead-reveal_your_secrets_to_me!228da2f265e1f3c3c8f4b777600611e822649a}