Curse Breaker
14 minutos de lectura
Se nos proporciona un binario llamado breaker
:
$ file breaker
breaker: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=276c71525dd765da538440a4615fa5b717e331ad, for GNU/Linux 3.2.0, not stripped
Ingeniería inversa
Usando Ghidra, podemos leer el código fuente descompilado en C. Esta es la función main
:
int main() {
size_t newline_index;
long i5;
char magic_word[50];
uint i;
magic_word._0_8_ = 0;
magic_word._8_8_ = 0;
magic_word._16_8_ = 0;
magic_word._24_8_ = 0;
magic_word._32_8_ = 0;
magic_word._40_8_ = 0;
magic_word._48_2_ = 0;
printf("Say the magic word: ");
fgets(magic_word, 50, stdin);
newline_index = strcspn(magic_word, "\n");
magic_word[newline_index] = '\0';
install_filter();
for (i = 0; i < 5; i = i + 1) {
i5 = (long) (int) (i * 5);
if (magic_word[i5] == '\0') break;
syscall(0x258, (ulong) i, (ulong) (uint) (int) magic_word[i5], (ulong) (uint) (int) magic_word[i5 + 1], (ulong) (uint) (int) magic_word[i5 + 2], (ulong) (uint) (int) magic_word[i5 + 3], (ulong) (uint) (int) magic_word[i5 + 4]);
}
puts("Free at last!");
return 0;
}
Básicamente, el programa solicita una palabra mágica, luego llama a install_filter
y finalmente ejecuta instrucciones syscall
extrañas.
Esta función es install_filter
:
int install_filter() {
int iVar1;
long i;
undefined8 *puVar2;
undefined8 *puVar3;
undefined2 local_498[4];
undefined8 *local_490;
undefined8 local_488[144];
puVar2 = &DAT_00102060;
puVar3 = local_488;
for (i = 144; i != 0; i = i + -1) {
*puVar3 = *puVar2;
puVar2 = puVar2 + 1;
puVar3 = puVar3 + 1;
}
local_498[0] = 144;
local_490 = local_488;
iVar1 = prctl(38, 1, 0, 0, 0);
if (iVar1 != 0) {
perror("prctl(NO_NEW_PRIVS)");
/* WARNING: Subroutine does not return */
exit(1);
}
iVar1 = prctl(22, 2, local_498);
if (iVar1 != 0) {
perror("prctl(SECCOMP)");
/* WARNING: Subroutine does not return */
exit(2);
}
return 0;
}
No acabo de entender bien qué hace esta función. Sin embargo, estoy seguro de que tiene que ver con reglas seccomp
, como indica un mensaje de error.
Extracción de reglas seccomp
Usando seccomp-tools
podemos extraer todas las reglas aplicadas:
$ seccomp-tools dump ./breaker
Say the magic word: asdf
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x01 0x00 0xc000003e if (A == ARCH_X86_64) goto 0003
0002: 0x06 0x00 0x00 0x00000000 return KILL
0003: 0x20 0x00 0x00 0x00000000 A = sys_number
0004: 0x15 0x01 0x00 0x00000258 if (A == 0x258) goto 0006
0005: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0006: 0x20 0x00 0x00 0x00000010 A = args[0]
0007: 0x15 0x00 0x21 0x00000000 if (A != 0x0) goto 0041
0008: 0x00 0x00 0x00 0x00000000 A = 0
0009: 0x02 0x00 0x00 0x00000000 mem[0] = A
0010: 0x20 0x00 0x00 0x00000018 A = args[1]
0011: 0x61 0x00 0x00 0x00000000 X = mem[0]
0012: 0x1c 0x00 0x00 0x00000000 A -= X
0013: 0x02 0x00 0x00 0x00000000 mem[0] = A
0014: 0x15 0x01 0x00 0x00000048 if (A == 72) goto 0016
0015: 0x06 0x00 0x00 0x00000000 return KILL
0016: 0x20 0x00 0x00 0x00000020 A = args[2]
0017: 0x61 0x00 0x00 0x00000000 X = mem[0]
0018: 0x1c 0x00 0x00 0x00000000 A -= X
0019: 0x02 0x00 0x00 0x00000000 mem[0] = A
0020: 0x15 0x01 0x00 0x0000000c if (A == 12) goto 0022
0021: 0x06 0x00 0x00 0x00000000 return KILL
0022: 0x20 0x00 0x00 0x00000028 A = args[3]
0023: 0x61 0x00 0x00 0x00000000 X = mem[0]
0024: 0x1c 0x00 0x00 0x00000000 A -= X
0025: 0x02 0x00 0x00 0x00000000 mem[0] = A
0026: 0x15 0x01 0x00 0x00000036 if (A == 54) goto 0028
0027: 0x06 0x00 0x00 0x00000000 return KILL
0028: 0x20 0x00 0x00 0x00000030 A = args[4]
0029: 0x61 0x00 0x00 0x00000000 X = mem[0]
0030: 0x1c 0x00 0x00 0x00000000 A -= X
0031: 0x02 0x00 0x00 0x00000000 mem[0] = A
0032: 0x15 0x01 0x00 0x00000045 if (A == 69) goto 0034
0033: 0x06 0x00 0x00 0x00000000 return KILL
0034: 0x20 0x00 0x00 0x00000038 A = args[5]
0035: 0x61 0x00 0x00 0x00000000 X = mem[0]
0036: 0x1c 0x00 0x00 0x00000000 A -= X
0037: 0x02 0x00 0x00 0x00000000 mem[0] = A
0038: 0x15 0x01 0x00 0x0000001c if (A == 28) goto 0040
0039: 0x06 0x00 0x00 0x00000000 return KILL
0040: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0041: 0x15 0x00 0x21 0x00000001 if (A != 0x1) goto 0075
0042: 0x00 0x00 0x00 0x00000000 A = 0
0043: 0x02 0x00 0x00 0x00000000 mem[0] = A
0044: 0x20 0x00 0x00 0x00000018 A = args[1]
0045: 0x61 0x00 0x00 0x00000000 X = mem[0]
0046: 0x1c 0x00 0x00 0x00000000 A -= X
0047: 0x02 0x00 0x00 0x00000000 mem[0] = A
0048: 0x15 0x01 0x00 0x00000062 if (A == 98) goto 0050
0049: 0x06 0x00 0x00 0x00000000 return KILL
0050: 0x20 0x00 0x00 0x00000020 A = args[2]
0051: 0x61 0x00 0x00 0x00000000 X = mem[0]
0052: 0x1c 0x00 0x00 0x00000000 A -= X
0053: 0x02 0x00 0x00 0x00000000 mem[0] = A
0054: 0x15 0x01 0x00 0x00000010 if (A == 16) goto 0056
0055: 0x06 0x00 0x00 0x00000000 return KILL
0056: 0x20 0x00 0x00 0x00000028 A = args[3]
0057: 0x61 0x00 0x00 0x00000000 X = mem[0]
0058: 0x1c 0x00 0x00 0x00000000 A -= X
0059: 0x02 0x00 0x00 0x00000000 mem[0] = A
0060: 0x15 0x01 0x00 0x00000024 if (A == 36) goto 0062
0061: 0x06 0x00 0x00 0x00000000 return KILL
0062: 0x20 0x00 0x00 0x00000030 A = args[4]
0063: 0x61 0x00 0x00 0x00000000 X = mem[0]
0064: 0x1c 0x00 0x00 0x00000000 A -= X
0065: 0x02 0x00 0x00 0x00000000 mem[0] = A
0066: 0x15 0x01 0x00 0x0000003f if (A == 63) goto 0068
0067: 0x06 0x00 0x00 0x00000000 return KILL
0068: 0x20 0x00 0x00 0x00000038 A = args[5]
0069: 0x61 0x00 0x00 0x00000000 X = mem[0]
0070: 0x1c 0x00 0x00 0x00000000 A -= X
0071: 0x02 0x00 0x00 0x00000000 mem[0] = A
0072: 0x15 0x01 0x00 0x00000022 if (A == 34) goto 0074
0073: 0x06 0x00 0x00 0x00000000 return KILL
0074: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0075: 0x15 0x00 0x21 0x00000002 if (A != 0x2) goto 0109
0076: 0x00 0x00 0x00 0x00000000 A = 0
0077: 0x02 0x00 0x00 0x00000000 mem[0] = A
0078: 0x20 0x00 0x00 0x00000018 A = args[1]
0079: 0x61 0x00 0x00 0x00000000 X = mem[0]
0080: 0x1c 0x00 0x00 0x00000000 A -= X
0081: 0x02 0x00 0x00 0x00000000 mem[0] = A
0082: 0x15 0x01 0x00 0x0000002d if (A == 45) goto 0084
0083: 0x06 0x00 0x00 0x00000000 return KILL
0084: 0x20 0x00 0x00 0x00000020 A = args[2]
0085: 0x61 0x00 0x00 0x00000000 X = mem[0]
0086: 0x1c 0x00 0x00 0x00000000 A -= X
0087: 0x02 0x00 0x00 0x00000000 mem[0] = A
0088: 0x15 0x01 0x00 0x00000046 if (A == 70) goto 0090
0089: 0x06 0x00 0x00 0x00000000 return KILL
0090: 0x20 0x00 0x00 0x00000028 A = args[3]
0091: 0x61 0x00 0x00 0x00000000 X = mem[0]
0092: 0x1c 0x00 0x00 0x00000000 A -= X
0093: 0x02 0x00 0x00 0x00000000 mem[0] = A
0094: 0x15 0x01 0x00 0x0000001f if (A == 31) goto 0096
0095: 0x06 0x00 0x00 0x00000000 return KILL
0096: 0x20 0x00 0x00 0x00000030 A = args[4]
0097: 0x61 0x00 0x00 0x00000000 X = mem[0]
0098: 0x1c 0x00 0x00 0x00000000 A -= X
0099: 0x02 0x00 0x00 0x00000000 mem[0] = A
0100: 0x15 0x01 0x00 0x00000044 if (A == 68) goto 0102
0101: 0x06 0x00 0x00 0x00000000 return KILL
0102: 0x20 0x00 0x00 0x00000038 A = args[5]
0103: 0x61 0x00 0x00 0x00000000 X = mem[0]
0104: 0x1c 0x00 0x00 0x00000000 A -= X
0105: 0x02 0x00 0x00 0x00000000 mem[0] = A
0106: 0x15 0x01 0x00 0x0000001f if (A == 31) goto 0108
0107: 0x06 0x00 0x00 0x00000000 return KILL
0108: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0109: 0x15 0x00 0x21 0x00000003 if (A != 0x3) goto 0143
0110: 0x00 0x00 0x00 0x00000000 A = 0
0111: 0x02 0x00 0x00 0x00000000 mem[0] = A
0112: 0x20 0x00 0x00 0x00000018 A = args[1]
0113: 0x61 0x00 0x00 0x00000000 X = mem[0]
0114: 0x1c 0x00 0x00 0x00000000 A -= X
0115: 0x02 0x00 0x00 0x00000000 mem[0] = A
0116: 0x15 0x01 0x00 0x0000006f if (A == 111) goto 0118
0117: 0x06 0x00 0x00 0x00000000 return KILL
0118: 0x20 0x00 0x00 0x00000020 A = args[2]
0119: 0x61 0x00 0x00 0x00000000 X = mem[0]
0120: 0x1c 0x00 0x00 0x00000000 A -= X
0121: 0x02 0x00 0x00 0x00000000 mem[0] = A
0122: 0x15 0x01 0x00 0xfffffffe if (A == 4294967294) goto 0124
0123: 0x06 0x00 0x00 0x00000000 return KILL
0124: 0x20 0x00 0x00 0x00000028 A = args[3]
0125: 0x61 0x00 0x00 0x00000000 X = mem[0]
0126: 0x1c 0x00 0x00 0x00000000 A -= X
0127: 0x02 0x00 0x00 0x00000000 mem[0] = A
0128: 0x15 0x01 0x00 0x00000072 if (A == 114) goto 0130
0129: 0x06 0x00 0x00 0x00000000 return KILL
0130: 0x20 0x00 0x00 0x00000030 A = args[4]
0131: 0x61 0x00 0x00 0x00000000 X = mem[0]
0132: 0x1c 0x00 0x00 0x00000000 A -= X
0133: 0x02 0x00 0x00 0x00000000 mem[0] = A
0134: 0x15 0x01 0x00 0xffffffaf if (A == 4294967215) goto 0136
0135: 0x06 0x00 0x00 0x00000000 return KILL
0136: 0x20 0x00 0x00 0x00000038 A = args[5]
0137: 0x61 0x00 0x00 0x00000000 X = mem[0]
0138: 0x1c 0x00 0x00 0x00000000 A -= X
0139: 0x02 0x00 0x00 0x00000000 mem[0] = A
0140: 0x15 0x01 0x00 0x000000ce if (A == 206) goto 0142
0141: 0x06 0x00 0x00 0x00000000 return KILL
0142: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0143: 0x06 0x00 0x00 0x00000000 return KILL
¡Impresionante! Hay muchas reglas. En realidad, parece un programa dentro de otro programa.
Idea feliz
Por experiencia, sé que las flags en HTB tienen formato HTB{...}
. Además, sé que H
es 72
como código ASCII decimal (0x48
en formato hexadecimal). Podemos encontrar 72
al comienzo de las reglas (línea 0014):
$ seccomp-tools dump ./breaker
Say the magic word: asdf
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x01 0x00 0xc000003e if (A == ARCH_X86_64) goto 0003
0002: 0x06 0x00 0x00 0x00000000 return KILL
0003: 0x20 0x00 0x00 0x00000000 A = sys_number
0004: 0x15 0x01 0x00 0x00000258 if (A == 0x258) goto 0006
0005: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0006: 0x20 0x00 0x00 0x00000010 A = args[0]
0007: 0x15 0x00 0x21 0x00000000 if (A != 0x0) goto 0041
0008: 0x00 0x00 0x00 0x00000000 A = 0
0009: 0x02 0x00 0x00 0x00000000 mem[0] = A
0010: 0x20 0x00 0x00 0x00000018 A = args[1]
0011: 0x61 0x00 0x00 0x00000000 X = mem[0]
0012: 0x1c 0x00 0x00 0x00000000 A -= X
0013: 0x02 0x00 0x00 0x00000000 mem[0] = A
0014: 0x15 0x01 0x00 0x00000048 if (A == 72) goto 0016
0015: 0x06 0x00 0x00 0x00000000 return KILL
0016: 0x20 0x00 0x00 0x00000020 A = args[2]
0017: 0x61 0x00 0x00 0x00000000 X = mem[0]
0018: 0x1c 0x00 0x00 0x00000000 A -= X
0019: 0x02 0x00 0x00 0x00000000 mem[0] = A
0020: 0x15 0x01 0x00 0x0000000c if (A == 12) goto 0022
0021: 0x06 0x00 0x00 0x00000000 return KILL
0022: 0x20 0x00 0x00 0x00000028 A = args[3]
0023: 0x61 0x00 0x00 0x00000000 X = mem[0]
0024: 0x1c 0x00 0x00 0x00000000 A -= X
0025: 0x02 0x00 0x00 0x00000000 mem[0] = A
0026: 0x15 0x01 0x00 0x00000036 if (A == 54) goto 0028
0027: 0x06 0x00 0x00 0x00000000 return KILL
0028: 0x20 0x00 0x00 0x00000030 A = args[4]
0029: 0x61 0x00 0x00 0x00000000 X = mem[0]
0030: 0x1c 0x00 0x00 0x00000000 A -= X
0031: 0x02 0x00 0x00 0x00000000 mem[0] = A
0032: 0x15 0x01 0x00 0x00000045 if (A == 69) goto 0034
...
El siguiente bloque if
espera un valor 12
. El carácter T
es 84
en decimal, que es 72 + 12
. Luego B
es 66
, que es 12 + 54
. Luego {
es 123
, que resulta ser 54 + 69
. Bastante claro, ¿verdad?
Recuperando la flag
Usemos un poco de shell scripting para obtener los valores esperados de los bloques if
:
$ seccomp-tools dump ./breaker | grep 'A == '
asdf
0001: 0x15 0x01 0x00 0xc000003e if (A == ARCH_X86_64) goto 0003
0004: 0x15 0x01 0x00 0x00000258 if (A == 0x258) goto 0006
0014: 0x15 0x01 0x00 0x00000048 if (A == 72) goto 0016
0020: 0x15 0x01 0x00 0x0000000c if (A == 12) goto 0022
0026: 0x15 0x01 0x00 0x00000036 if (A == 54) goto 0028
0032: 0x15 0x01 0x00 0x00000045 if (A == 69) goto 0034
0038: 0x15 0x01 0x00 0x0000001c if (A == 28) goto 0040
0048: 0x15 0x01 0x00 0x00000062 if (A == 98) goto 0050
0054: 0x15 0x01 0x00 0x00000010 if (A == 16) goto 0056
0060: 0x15 0x01 0x00 0x00000024 if (A == 36) goto 0062
0066: 0x15 0x01 0x00 0x0000003f if (A == 63) goto 0068
0072: 0x15 0x01 0x00 0x00000022 if (A == 34) goto 0074
0082: 0x15 0x01 0x00 0x0000002d if (A == 45) goto 0084
0088: 0x15 0x01 0x00 0x00000046 if (A == 70) goto 0090
0094: 0x15 0x01 0x00 0x0000001f if (A == 31) goto 0096
0100: 0x15 0x01 0x00 0x00000044 if (A == 68) goto 0102
0106: 0x15 0x01 0x00 0x0000001f if (A == 31) goto 0108
0116: 0x15 0x01 0x00 0x0000006f if (A == 111) goto 0118
0122: 0x15 0x01 0x00 0xfffffffe if (A == 4294967294) goto 0124
0128: 0x15 0x01 0x00 0x00000072 if (A == 114) goto 0130
0134: 0x15 0x01 0x00 0xffffffaf if (A == 4294967215) goto 0136
0140: 0x15 0x01 0x00 0x000000ce if (A == 206) goto 0142
$ seccomp-tools dump ./breaker | grep -o 'A == .*)'
asdf
A == ARCH_X86_64)
A == 0x258)
A == 72)
A == 12)
A == 54)
A == 69)
A == 28)
A == 98)
A == 16)
A == 36)
A == 63)
A == 34)
A == 45)
A == 70)
A == 31)
A == 68)
A == 31)
A == 111)
A == 4294967294)
A == 114)
A == 4294967215)
A == 206)
$ seccomp-tools dump ./breaker | grep -o 'A == .*)' | tr -d ')' | grep -vi x
asdf
A == 72
A == 12
A == 54
A == 69
A == 28
A == 98
A == 16
A == 36
A == 63
A == 34
A == 45
A == 70
A == 31
A == 68
A == 31
A == 111
A == 4294967294
A == 114
A == 4294967215
A == 206
$ seccomp-tools dump ./breaker | grep -o 'A == .*)' | tr -d ')' | grep -vi x | cut -c6-
asdf
72
12
54
69
28
98
16
36
63
34
45
70
31
68
31
111
4294967294
114
4294967215
206
$ seccomp-tools dump ./breaker | grep -o 'A == .*)' | tr -d ')' | grep -vi x | cut -c6- | xargs
asdf
72 12 54 69 28 98 16 36 63 34 45 70 31 68 31 111 4294967294 114 4294967215 206
$ seccomp-tools dump ./breaker | grep -o 'A == .*)' | tr -d ')' | grep -vi x | cut -c6- | xargs | tr ' ' ,
asdf
72,12,54,69,28,98,16,36,63,34,45,70,31,68,31,111,4294967294,114,4294967215,206
Hay dos números grandes. Estos son probablemente números negativos que se muestran como enteros sin signo (unsigned):
$ python3 -q
>>> hex(4294967294)
'0xfffffffe'
>>> hex(4294967215)
'0xffffffaf'
Para obtener el entero negativo que representan, podemos calcular el complemento a dos:
>>> ((~ 4294967294) + 1) & 0xffffffff
2
>>> ((~ 4294967215) + 1) & 0xffffffff
81
Entonces tenemos esta lista:
>>> x = [72,12,54,69,28,98,16,36,63,34,45,70,31,68,31,111,-2,114,-81,206]
Vimos que la flag estaba codificada como la suma de dos elementos consecutivos (empezando por 0
). Entonces, vamos a calcular todos los caracteres:
>>> x = [72,12,54,69,28,98,16,36,63,34,45,70,31,68,31,111,-2,114,-81,206]
>>> last = 0
>>> flag = ''
>>> while x:
... flag += chr(last + x[0])
... last = x.pop(0)
...
>>> flag
'HTB{a~r4caOsecc\x8emp!}'
Esta flag no es correcta, pero estamos cerca.
Podemos asumir que HTB{a
es correcto, pero el siguiente ~
es un carácter extraño que no suele aparecer en una flag. Mirando nuevamente las reglas seccomp
, podemos ver que cada cinco caracteres, el algoritmo se restablece a 0
:
0032: 0x15 0x01 0x00 0x00000045 if (A == 69) goto 0034
0033: 0x06 0x00 0x00 0x00000000 return KILL
0034: 0x20 0x00 0x00 0x00000038 A = args[5]
0035: 0x61 0x00 0x00 0x00000000 X = mem[0]
0036: 0x1c 0x00 0x00 0x00000000 A -= X
0037: 0x02 0x00 0x00 0x00000000 mem[0] = A
0038: 0x15 0x01 0x00 0x0000001c if (A == 28) goto 0040
0039: 0x06 0x00 0x00 0x00000000 return KILL
0040: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0041: 0x15 0x00 0x21 0x00000001 if (A != 0x1) goto 0075
0042: 0x00 0x00 0x00 0x00000000 A = 0
0043: 0x02 0x00 0x00 0x00000000 mem[0] = A
0044: 0x20 0x00 0x00 0x00000018 A = args[1]
0045: 0x61 0x00 0x00 0x00000000 X = mem[0]
0046: 0x1c 0x00 0x00 0x00000000 A -= X
0047: 0x02 0x00 0x00 0x00000000 mem[0] = A
0048: 0x15 0x01 0x00 0x00000062 if (A == 98) goto 0050
0049: 0x06 0x00 0x00 0x00000000 return KILL
0050: 0x20 0x00 0x00 0x00000020 A = args[2]
0051: 0x61 0x00 0x00 0x00000000 X = mem[0]
0052: 0x1c 0x00 0x00 0x00000000 A -= X
0053: 0x02 0x00 0x00 0x00000000 mem[0] = A
0054: 0x15 0x01 0x00 0x00000010 if (A == 16) goto 0056
Como se muestra arriba, tenemos la comprobación para {
(línea 0032), la comprobación de a
(línea 0038). Luego, vemos A = 0
en la línea 0042, que es a lo que me refiero con restablecer el algoritmo. Entonces tendremos 98
, que es el código ASCII para la b
. Luego 98 + 16
, que corresponde con r
, y así sucesivamente.
Flag
Como resultado, solo necesitamos ajustar nuestro algoritmo de decodificación:
>>> x = [72,12,54,69,28,98,16,36,63,34,45,70,31,68,31,111,-2,114,-81,206]
>>> flag = ''
>>> while x:
... if len(flag) % 5 == 0:
... last = 0
... flag += chr(last + x[0])
... last = x.pop(0)
...
>>> flag
'HTB{abr4ca-seccomp!}'
Obviamente, es la flag correcta:
$ ./breaker
Say the magic word: HTB{abr4ca-seccomp!}
Free at last!