Description

I am tired of hackers evading my signatures, why don’t you try triggering it for a change?

Solution

The file given for the challenge is a “.cbc” with the magic bytes “ClamBC”, which, with a bit of googling, we can easily find out that is a “ClamAV signature bytecode”.

This is a feature offered by the ClamAV AntiVirus where an analyst can write a logical signature in C, which is then compiled into bytecode and added to the traditional signatures of the antivirus.

The first result of the google search is the (ClamBC)[https://linux.die.net/man/1/clambc] tool, installed with ClamAV, which is essential to the challenge.

The first thing one can try is the comand to extract the source code, which fails:

> clambc -p chall.cbc
Nice_try_but_it_cant_be_this_easy%

This is beacause the bytecode was compiled with a COPYRIGHT header, meaning the source code is not included in the bytecode.

For this reason we have to work with the bytecode which we can extract with the following command:

> clambc -c chall.cbc

Before looking at the bytecode we can check which is the initial signature that triggers the bytecode, which we can see with the following command:

> clambc --info chall.cbc
...
bytecode logical signature: The_RuleGod...{Congrats_You_got_it,It_just_needs_a_little_fixing-come_on,Perfect_form_now_we_just_need_whats_inside,Well_thats_a_start_but_its_not_quite_it_yet,You_read_the_CTF_rules_i_like_that};Engine:56-255,Target:0;0;0:5452587b
...

We can see at the end of the line after “Target” that the rule triggers when the file contains “5452587b”, which is “TRX{”, our flag format.

Now we can start looking at the bytecode, the bytecode is structured in several sections, only two are useful, the constants section and the function section. The function section contains the bytecode we need to reverse, the constants section contains the value of all the costants of the program which are going to be accessed by ID in the bytecode.

The first check is on the last character of the flag which needs to be equal to “}”, and here alreay we can see that “bb.4” is the “FAIL” block so it’s what we need to avoid.

0    1  OP_BC_CALL_API      [33 /168/  3]  3 = seek[3] (1775, 1776) -- constants[1775] = -1, constants[1776] = SEEK_END
0    2  OP_BC_CALL_API      [33 /168/  3]  4 = read[1] (p.1, 1777) -- constants[1777] = 1
0    3  OP_BC_COPY          [34 /171/  1]  cp 1 -> 5
0    4  OP_BC_ICMP_EQ       [21 /106/  1]  6 = (5 == 1778)  -- constants[1778] = "}"
0    5  OP_BC_COPY          [34 /174/  4]  cp 1779 -> 0
0    6  OP_BC_BRANCH        [17 / 85/  0]  br 6 ? bb.1 : bb.4

Next is the check on the size of the flag which is 44:

1    8  OP_BC_ICMP_EQ       [21 /108/  3]  8 = (3 == 1781) -- constants[1781] = 44
1    9  OP_BC_COPY          [34 /174/  4]  cp 1782 -> 0
1   10  OP_BC_BRANCH        [17 / 85/  0]  br 8 ? bb.2 : bb.4

Now after this we see a series of operation on value of the file which follow the following format:

2   20  OP_BC_CALL_API      [33 /168/  3]  18 = seek[3] (1792, 1793)
2   21  OP_BC_CALL_API      [33 /168/  3]  19 = read[1] (p.1, 1794)
2   22  OP_BC_COPY          [34 /171/  1]  cp 1 -> 20
2   23  OP_BC_AND           [11 / 56/  1]  21 = 20 & 1795
2   24  OP_BC_ICMP_NE       [22 /111/  1]  22 = (21 != 1796)

which is equal to:

seek(X, SEEK_SET/SEEK_END)
read(buf, 1)
check = (buf & Y) == Z

with the value of X,Y and Z all contained in the constants section.

Or:

2  1363  OP_BC_CALL_API      [33 /168/  3]  1361 = seek[3] (3135, 3136)
2  1364  OP_BC_CALL_API      [33 /168/  3]  1362 = read[1] (p.1, 3137)
2  1365  OP_BC_COPY          [34 /171/  1]  cp 1 -> 1363
2  1366  OP_BC_ICMP_UGT      [23 /116/  1]  1364 = (1363 > 3138)

which is equal to:

seek(X, SEEK_SET/SEEK_END)
read(buf, 1)
check = (buf > Y)

And if we scroll all the way to the bottom, we can there the check is performed:

...
2  1759  OP_BC_SELECT        [31 /155/  0]  1757 = 1756 ? 3531 : 71)
2  1760  OP_BC_SELECT        [31 /155/  0]  1758 = 1757 ? 3532 : 67)
2  1761  OP_BC_SELECT        [31 /155/  0]  1759 = 1758 ? 3533 : 63)
2  1762  OP_BC_SELECT        [31 /155/  0]  1760 = 1759 ? 3534 : 59)
2  1763  OP_BC_SELECT        [31 /155/  0]  1761 = 1760 ? 3535 : 54)
2  1764  OP_BC_SELECT        [31 /155/  0]  1762 = 1761 ? 3536 : 50)
2  1765  OP_BC_SELECT        [31 /155/  0]  1763 = 1762 ? 3537 : 45)
2  1766  OP_BC_SELECT        [31 /155/  0]  1764 = 1763 ? 3538 : 41)
2  1767  OP_BC_SELECT        [31 /155/  0]  1765 = 1764 ? 3539 : 36)
2  1768  OP_BC_SELECT        [31 /155/  0]  1766 = 1765 ? 3540 : 31)
2  1769  OP_BC_SELECT        [31 /155/  0]  1767 = 1766 ? 3541 : 27)
2  1770  OP_BC_SELECT        [31 /155/  0]  1768 = 1767 ? 3542 : 22)
2  1771  OP_BC_SELECT        [31 /155/  0]  1769 = 1768 ? 3543 : 17)
2  1772  OP_BC_SELECT        [31 /155/  0]  1770 = 1769 ? 3544 : 1461)
2  1773  OP_BC_COPY          [34 /174/  4]  cp 3545 -> 0
2  1774  OP_BC_BRANCH        [17 / 85/  0]  br 1770 ? bb.4 : bb.3

which is just an efficient way of making sure that all the checks are equal to 0. Meaning that all the conditions above need to be False.

After noticing this we just need to write a parser that extracts all the conditions and passes then to z3 which is what i have done in the “solve.py” script.

Fun Fact: The AND contitions are by themselfs almost enough to get the flag, except for one character which is incorrectly capitalized, after adding the LESS condition the solution becomes correct.