Curse Breaker
14 minutes to read
We are given a binary called 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
Reverse engineering
Using Ghidra, we can read the decompiled source code in C. This is the main
function:
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;
}
Basically, the program asks for a magic word, then calls install_filter
and finally executes weird syscall
instructions.
This is 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;
}
I don’t quite understand what this function does. However, I’m sure it has to do with seccomp
rules, as stated by an error message.
Dumping seccomp
rules
Using seccomp-tools
we can dump all the rules that are applied:
$ 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
Impressive! There are a lot of rules. Actually, it looks like a program inside a program.
Guessing part
From experience, I know that flags in HTB have format HTB{...}
. Plus, I know that H
is 72
as a decimal ASCII code (0x48
in hexadecimal format). We can find 72
relatively at the beginning of the rules (line 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
...
The next if
statements expects value 12
. Character T
happens to be 84
in decimal, which is 72 + 12
. Then B
is 66
, which is 12 + 54
. Then {
is 123
, which happens to be 54 + 69
. Pretty clear, right?
Recovering the flag
Let’s use some shell scripting to get the expected values of the if
blocks:
$ 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
There are two big numbers. These are probably negative numbers shown as unsigned integers:
$ python3 -q
>>> hex(4294967294)
'0xfffffffe'
>>> hex(4294967215)
'0xffffffaf'
To get the negative integer they represent, we can compute the two’s complement:
>>> ((~ 4294967294) + 1) & 0xffffffff
2
>>> ((~ 4294967215) + 1) & 0xffffffff
81
So we have this list:
>>> x = [72,12,54,69,28,98,16,36,63,34,45,70,31,68,31,111,-2,114,-81,206]
We saw that the flag was encoded as the sum of two consecutive elements (starting from 0
). So, let’s get all characters:
>>> 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!}'
This flag is not correct, but we are close.
We can assume that HTB{a
is right, but the following ~
is a weird character to appear in a flag. Looking again at the seccomp
rules, we can see that every five characters, the algorithm is reset to 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
As shown above, we have the check for {
(line 0032), the check for a
(line 0038). Then, we see A = 0
at line 0042, which is what I refer to by resetting the algorithm. Then we will have 98
, which is the ASCII code for b
. Then 98 + 16
, which corresponds with r
, and so on.
Flag
As a result, we only need to tweak our decoding algorithm:
>>> 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!}'
Obviously, it is the correct flag:
$ ./breaker
Say the magic word: HTB{abr4ca-seccomp!}
Free at last!