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]=203d702c3e6cc789e148886f986713f7cf21e2c2, 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 0x0000002e if (A == 46) 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 0x00000065 if (A == 101) 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 0xfffffffe if (A == 4294967294) 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 0x00000065 if (A == 101) 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 0xffffffcb if (A == 4294967243) 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 0x000000a2 if (A == 162) 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 0x00000070 if (A == 112) 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 0xffffffbd if (A == 4294967229) 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 0x000000a4 if (A == 164) 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 0xffffffc0 if (A == 4294967232) 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 0x00000074 if (A == 116) 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 0x00000062 if (A == 98) 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 0x00000010 if (A == 16) 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 0x00000024 if (A == 36) 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 0xfffffffd if (A == 4294967293) 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 0x00000080 if (A == 128) 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 0x0000002e if (A == 46) goto 0040
0048: 0x15 0x01 0x00 0x00000065 if (A == 101) goto 0050
0054: 0x15 0x01 0x00 0xfffffffe if (A == 4294967294) goto 0056
0060: 0x15 0x01 0x00 0x00000065 if (A == 101) goto 0062
0066: 0x15 0x01 0x00 0xffffffcb if (A == 4294967243) goto 0068
0072: 0x15 0x01 0x00 0x000000a2 if (A == 162) goto 0074
0082: 0x15 0x01 0x00 0x00000070 if (A == 112) goto 0084
0088: 0x15 0x01 0x00 0xffffffbd if (A == 4294967229) goto 0090
0094: 0x15 0x01 0x00 0x000000a4 if (A == 164) goto 0096
0100: 0x15 0x01 0x00 0xffffffc0 if (A == 4294967232) goto 0102
0106: 0x15 0x01 0x00 0x00000074 if (A == 116) goto 0108
0116: 0x15 0x01 0x00 0x00000062 if (A == 98) goto 0118
0122: 0x15 0x01 0x00 0x00000010 if (A == 16) goto 0124
0128: 0x15 0x01 0x00 0x00000024 if (A == 36) goto 0130
0134: 0x15 0x01 0x00 0xfffffffd if (A == 4294967293) goto 0136
0140: 0x15 0x01 0x00 0x00000080 if (A == 128) 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 == 46)
A == 101)
A == 4294967294)
A == 101)
A == 4294967243)
A == 162)
A == 112)
A == 4294967229)
A == 164)
A == 4294967232)
A == 116)
A == 98)
A == 16)
A == 36)
A == 4294967293)
A == 128)
$ seccomp-tools dump ./breaker | grep -o 'A == .*)' | tr -d ')' | grep -vi x
asdf
A == 72
A == 12
A == 54
A == 69
A == 46
A == 101
A == 4294967294
A == 101
A == 4294967243
A == 162
A == 112
A == 4294967229
A == 164
A == 4294967232
A == 116
A == 98
A == 16
A == 36
A == 4294967293
A == 128
$ seccomp-tools dump ./breaker | grep -o 'A == .*)' | tr -d ')' | grep -vi x | cut -c6-
asdf
72
12
54
69
46
101
4294967294
101
4294967243
162
112
4294967229
164
4294967232
116
98
16
36
4294967293
128
$ seccomp-tools dump ./breaker | grep -o 'A == .*)' | tr -d ')' | grep -vi x | cut -c6- | xargs
asdf
72 12 54 69 46 101 4294967294 101 4294967243 162 112 4294967229 164 4294967232 116 98 16 36 4294967293 128
$ seccomp-tools dump ./breaker | grep -o 'A == .*)' | tr -d ')' | grep -vi x | cut -c6- | xargs | tr ' ' ,
asdf
72,12,54,69,46,101,4294967294,101,4294967243,162,112,4294967229,164,4294967232,116,98,16,36,4294967293,128
There are some big numbers. These are probably negative numbers shown as unsigned integers:
$ python3 -q
>>> hex(4294967294)
'0xfffffffe'
To get the negative integer they represent, we can compute the two’s complement:
>>> two_c = lambda n: -(((~n) + 1) & 0xffffffff)
>>> two_c(4294967294)
-2
>>> two_c(4294967243)
-53
>>> two_c(4294967229)
-67
>>> two_c(4294967232)
-64
>>> two_c(4294967293)
-3
So we have this list:
>>> x = [72,12,54,69,46,101,-2,101,-53,162,112,-67,164,-64,116,98,16,36,-3,128]
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,46,101,-2,101,-53,162,112,-67,164,-64,116,98,16,36,-3,128]
>>> last = 0
>>> flag = ''
>>> while x:
... flag += chr(last + x[0])
... last = x.pop(0)
...
>>> flag
'HTB{s\x93cc0mĒ-ad4Ör4!}'
This flag is not correct, but we are close.
We can assume that HTB{s
is right, but the following \x93
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 0x0000002e if (A == 46) 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 0x00000065 if (A == 101) 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 0xfffffffe if (A == 4294967294) goto 0056
As shown above, we have the check for {
(line 0032), the check for s
(line 0038). Then, we see A = 0
at line 0042, which is what I refer to by resetting the algorithm. Then we will have 101
, which is the ASCII code for e
. Then 101 - 2
(4294967294
is -2
), which corresponds with c
, and so on.
Flag
As a result, we only need to tweak our decoding algorithm:
>>> x = [72,12,54,69,46,101,-2,101,-53,162,112,-67,164,-64,116,98,16,36,-3,128]
>>> flag = ''
>>> while x:
... if len(flag) % 5 == 0:
... last = 0
... flag += chr(last + x[0])
... last = x.pop(0)
...
>>> flag
'HTB{secc0mp-ad4br4!}'
Obviously, it is the correct flag:
$ ./breaker
Say the magic word: HTB{secc0mp-ad4br4!}
Free at last!