flare24-aray-cover

aray is a Yara reversing challenge. The Yara language is used to classified and identify malware (and other binary) files. In aray, I’m given a complex rule with hundreds of conditions that define a 85 byte file. I’ll find the 38 conditions that actually define the 85 bytes, and write a Python script to parse the rule and return the file contents.

Challenge

The challenge prompt reads:

And now for something completely different. I’m pretty sure you know how to write Yara rules, but can you reverse them?

The download has a Yara file with 9 long lines:

oxdf@hacky$ file aray.yara 
aray.yara: ASCII text, with very long lines (14220)
oxdf@hacky$ wc -l aray.yara
9 aray.yara

Analysis

Yara Background

Yara is a language for writing rules to identify and classify different (typically) binary files. It allows for writing rules describing features of a file, and then these rules can be run across large collections of files to see which ones match. It can match on strings, bytes, regex, hashes, or other patterns.

An example rule from a sample on GitHub looks like:

rule MANITSME_APT1
{

    meta:
        author = "AlienVault Labs"
        info = "CommentCrew-threat-apt1"
        
    strings:
        $s1 = "Install an Service hosted by SVCHOST." wide ascii
        $s2 = "The Dll file that to be released." wide ascii
        $s3 = "SYSTEM\\CurrentControlSet\\Services\\" wide ascii
        $s4 = "svchost.exe" wide ascii
        $e1 = "Man,it's me" wide ascii
        $e2 = "Oh,shit" wide ascii
        $e3 = "Hallelujah" wide ascii
        $e4 = "nRet == SOCKET_ERROR" wide ascii
        $pdb1 = "rouji\\release\\Install.pdb" wide ascii
        $pdb2 = "rouji\\SvcMain.pdb" wide ascii

    condition:
        (all of ($s*)) or (all of ($e*)) or $pdb1 or $pdb2
}

The author has decided that having different sets of these strings identifies a file as an instance of the APT1 malware called manitsme.

Structure

Without line wrap, it’s pretty easy to see the structure of this file as a Yara rule with a very long condition:

import "hash"

rule aray
{
    meta:
        description = "Matches on b7dc94ca98aa58dabb5404541c812db2"
    condition:
        filesize == 85 and hash.md5(0, filesize) == "b7dc94ca98aa58dabb5404541c812db2" and filesize ^ uint8(11) != 107 and uint8(55) & 128 == 0 and uint8(58) + 25 == 122 and uint8(7) & 128 == 0 and uint8(48) % 12 < 12 and uint8(17) > 31 and uint8(68) > 10 and uint8(56) < 155 and uint32(52) ^ 425706662 == 1495724241 and uint8(0) % 25 < 25 and filesize ^ uint8(75) != 25 and filesize ^ uint8(28) != 12 and uint8(35) < 160 and uint8(3) & 128 == 0 and uint8(56) & 128 == 0 and uint8(28) % 27 < 27 and uint8(4) > 30 and uint8(15) & 128 == 0 and uint8(68) % 19 < 19 and uint8(19) < 151 and filesize ^ uint8(73) != 17 and filesize ^ uint8(31) != 5 and uint8(38) % 24 < 24 and uint8(3) > 21 and uint8(54) & 128 == 0 and filesize ^ uint8(66) != 146 and uint32(17) - 323157430 == 1412131772 and hash.crc32(8, 2) == 0x61089c5c and filesize ^ uint8(77) != 22 and uint8(75) % 24 < 24 and uint8(66) < 133 and uint8(21) % 11 < 11 and uint8(46) < 154 and hash.crc32(34, 2) == 0x5888fc1b and uint8(55) > 5 and uint8(36) + 4 == 72 and filesize ^ uint8(82) != 228 and filesize ^ uint8(13) != 42 and filesize ^ uint8(6) != 39 and uint8(33) < 160 and filesize ^ uint8(55) != 244 and filesize ^ uint8(15) != 205 and filesize ^ uint8(3) != 43 and filesize ^ uint8(54) != 39 and uint8(28) & 128 == 0 and uint8(10) < 146 and filesize ^ uint8(56) != 246 and filesize ^ uint8(32) != 77 and uint8(73) > 26 and uint8(36) > 11 and uint8(70) > 6 and filesize ^ uint8(33) != 27 and uint8(48) & 128 == 0 and filesize ^ uint8(74) != 45 and uint8(27) ^ 21 == 40 and uint8(60) % 23 < 23 and filesize ^ uint8(67) != 63 and filesize ^ uint8(0) != 16 and uint8(51) % 15 < 15 and uint8(50) > 19 and uint8(27) < 147 and filesize ^ uint8(40) != 230 and filesize ^ uint8(2) != 205 and uint8(79) % 24 < 24 and uint8(69) < 148 and uint8(16) & 128 == 0 and uint8(61) % 26 < 26 and uint8(63) > 31 and uint8(14) & 128 == 0 and uint8(35) > 1 and filesize ^ uint8(11) != 33 and uint8(52) < 136 and uint8(54) > 15 and filesize ^ uint8(20) != 83 and uint8(43) > 24 and uint8(82) < 152 and uint32(59) ^ 512952669 == 1908304943 and filesize ^ uint8(79) != 186 and filesize ^ uint8(83) != 197 and uint8(39) < 134 and filesize ^ uint8(43) != 33 and uint8(72) > 10 and uint8(83) < 134 and uint8(44) % 27 < 27 and uint8(40) < 131 and uint8(80) % 31 < 31 and filesize ^ uint8(47) != 11 and uint8(55) % 11 < 11 and filesize ^ uint8(71) != 3 and uint8(65) - 29 == 70 and uint8(58) > 30 and filesize ^ uint8(37) != 37 and uint8(60) < 130 and uint8(27) & 128 == 0 and uint8(3) < 141 and uint8(73) & 128 == 0 and filesize ^ uint8(70) != 209 and filesize ^ uint8(2) != 54 and filesize ^ uint8(20) != 17 and uint8(33) > 18 and uint8(37) % 19 < 19 and filesize ^ uint8(62) != 15 and filesize ^ uint8(10) != 44 and uint8(7) % 12 < 12 and uint8(71) > 19 and filesize ^ uint8(50) != 86 and uint8(45) ^ 9 == 104 and uint8(8) < 133 and uint8(31) < 145 and uint8(14) > 20 and uint8(54) % 25 < 25 and filesize ^ uint8(49) != 156 and uint8(47) > 13 and uint8(29) > 22 and uint8(14) % 19 < 19 and filesize ^ uint8(17) != 16 and filesize ^ uint8(12) != 226 and filesize ^ uint8(65) != 28 and uint8(45) & 128 == 0 and filesize ^ uint8(6) != 129 and uint8(18) % 30 < 30 and filesize ^ uint8(62) != 246 and uint8(78) % 13 < 13 and uint8(36) & 128 == 0 and uint8(10) & 128 == 0 and uint8(62) > 1 and uint8(33) & 128 == 0 and filesize ^ uint8(83) != 31 and uint8(83) % 21 < 21 and uint8(11) > 18 and uint8(80) < 143 and uint8(81) % 14 < 14 and uint8(43) < 160 and uint8(1) > 19 and uint8(42) % 17 < 17 and uint8(44) < 147 and filesize ^ uint8(63) != 34 and filesize ^ uint8(44) != 17 and uint32(28) - 419186860 == 959764852 and uint8(74) + 11 == 116 and uint8(48) < 136 and uint8(47) < 142 and hash.crc32(63, 2) == 0x66715919 and uint8(58) < 146 and filesize ^ uint8(71) != 128 and uint8(45) < 136 and uint8(31) % 17 < 17 and uint8(43) & 128 == 0 and filesize ^ uint8(43) != 251 and uint8(65) > 1 and uint8(24) & 128 == 0 and uint8(37) < 139 and filesize ^ uint8(28) != 238 and uint8(78) & 128 == 0 and filesize ^ uint8(13) != 219 and uint8(19) % 30 < 30 and hash.sha256(14, 2) == "403d5f23d149670348b147a15eeb7010914701a7e99aad2e43f90cfa0325c76f" and filesize ^ uint8(53) != 243 and uint8(81) & 128 == 0 and uint8(46) % 28 < 28 and filesize ^ uint8(65) != 215 and filesize ^ uint8(0) != 41 and uint8(84) < 129 and uint8(60) & 128 == 0 and uint8(20) > 1 and uint8(2) % 28 < 28 and uint8(58) % 14 < 14 and uint8(34) & 128 == 0 and uint8(21) & 128 == 0 and uint8(84) % 18 < 18 and uint8(74) % 10 < 10 and uint8(9) < 151 and uint8(73) % 23 < 23 and filesize ^ uint8(39) != 49 and uint8(4) % 17 < 17 and filesize ^ uint8(60) != 142 and filesize ^ uint8(69) != 30 and uint8(30) > 6 and uint8(65) & 128 == 0 and uint8(39) % 11 < 11 and uint8(13) % 27 < 27 and uint8(17) % 11 < 11 and uint8(56) % 26 < 26 and uint8(29) < 157 and uint8(57) & 128 == 0 and filesize ^ uint8(29) != 37 and uint8(77) > 5 and filesize ^ uint8(16) != 144 and uint8(37) & 128 == 0 and filesize ^ uint8(25) != 47 and uint8(67) & 128 == 0 and filesize ^ uint8(24) != 94 and uint8(68) < 138 and uint8(57) < 138 and filesize ^ uint8(27) != 43 and filesize ^ uint8(30) != 18 and filesize ^ uint8(59) != 13 and uint8(27) % 26 < 26 and uint8(56) > 8 and uint8(69) & 128 == 0 and uint8(18) & 128 == 0 and uint8(64) < 154 and uint8(76) & 128 == 0 and uint8(71) % 28 < 28 and filesize ^ uint8(84) != 3 and filesize ^ uint8(38) != 84 and uint8(32) < 140 and filesize ^ uint8(42) != 91 and uint8(40) > 15 and uint8(27) > 23 and uint8(6) % 12 < 12 and uint8(10) % 10 < 10 and uint8(8) % 21 < 21 and filesize ^ uint8(18) != 234 and uint8(68) & 128 == 0 and uint8(7) < 131 and uint8(72) < 134 and uint8(16) > 25 and uint8(12) % 23 < 23 and uint8(41) % 27 < 27 and uint8(1) % 17 < 17 and uint8(26) > 31 and hash.sha256(56, 2) == "593f2d04aab251f60c9e4b8bbc1e05a34e920980ec08351a18459b2bc7dbf2f6" and uint8(65) < 149 and filesize ^ uint8(51) != 0 and uint8(66) > 30 and filesize ^ uint8(68) != 8 and uint8(25) % 23 < 23 and uint8(1) & 128 == 0 and filesize ^ uint8(81) != 7 and uint8(36) % 22 < 22 and uint8(24) < 148 and uint8(12) < 147 and uint8(74) < 152 and filesize ^ uint8(21) != 27 and filesize ^ uint8(23) != 18 and uint8(38) & 128 == 0 and uint8(26) % 25 < 25 and filesize ^ uint8(19) != 31 and uint8(82) > 3 and uint8(5) % 27 < 27 and uint8(5) & 128 == 0 and uint8(75) - 30 == 86 and uint8(54) < 152 and uint8(75) < 142 and uint8(20) % 28 < 28 and uint8(30) & 128 == 0 and uint32(66) ^ 310886682 == 849718389 and uint8(64) % 24 < 24 and uint32(10) + 383041523 == 2448764514 and uint8(79) & 128 == 0 and filesize ^ uint8(59) != 194 and uint8(61) & 128 == 0 and uint8(70) < 139 and uint8(77) & 128 == 0 and uint8(13) & 128 == 0 and uint8(21) < 138 and filesize ^ uint8(46) != 186 and uint8(43) % 26 < 26 and uint8(61) < 160 and filesize ^ uint8(34) != 39 and uint8(6) > 6 and uint8(35) & 128 == 0 and uint8(23) < 141 and filesize ^ uint8(82) != 32 and filesize ^ uint8(48) != 29 and uint8(59) & 128 == 0 and uint8(40) % 19 < 19 and filesize ^ uint8(39) != 18 and filesize ^ uint8(45) != 146 and uint8(80) & 128 == 0 and uint8(16) < 134 and uint8(74) > 1 and uint8(23) & 128 == 0 and uint8(32) & 128 == 0 and filesize ^ uint8(47) != 119 and filesize ^ uint8(63) != 135 and uint8(64) > 27 and uint32(37) + 367943707 == 1228527996 and uint8(82) % 28 < 28 and uint8(32) > 28 and filesize ^ uint8(24) != 217 and uint8(53) < 144 and uint8(29) & 128 == 0 and uint32(22) ^ 372102464 == 1879700858 and uint8(52) % 23 < 23 and filesize ^ uint8(76) != 88 and filesize ^ uint8(55) != 17 and uint8(26) & 128 == 0 and uint8(51) > 7 and uint8(12) > 19 and filesize ^ uint8(14) != 99 and filesize ^ uint8(37) != 141 and filesize ^ uint8(14) != 161 and uint8(45) % 17 < 17 and uint8(33) % 25 < 25 and filesize ^ uint8(67) != 55 and filesize ^ uint8(53) != 19 and uint8(30) < 131 and uint8(0) & 128 == 0 and uint8(66) & 128 == 0 and uint8(41) > 5 and uint8(71) & 128 == 0 and uint8(29) % 12 < 12 and uint8(4) < 139 and uint8(77) < 154 and filesize ^ uint8(12) != 116 and uint8(39) > 7 and uint8(75) & 128 == 0 and uint8(78) > 24 and uint8(69) > 25 and uint8(2) + 11 == 119 and uint8(15) < 156 and filesize ^ uint8(69) != 241 and filesize ^ uint8(35) != 18 and filesize ^ uint8(17) != 208 and hash.md5(0, 2) == "89484b14b36a8d5329426a3d944d2983" and filesize ^ uint8(4) != 23 and uint8(15) % 16 < 16 and filesize ^ uint8(75) != 35 and uint32(46) - 412326611 == 1503714457 and uint8(11) % 27 < 27 and hash.crc32(78, 2) == 0x7cab8d64 and uint8(83) & 128 == 0 and filesize ^ uint8(26) != 161 and uint8(49) % 13 < 13 and filesize ^ uint8(18) != 33 and uint8(6) < 155 and uint8(41) < 140 and filesize ^ uint8(68) != 135 and filesize ^ uint8(9) != 5 and uint8(9) & 128 == 0 and filesize ^ uint8(36) != 95 and uint8(7) > 18 and filesize ^ uint8(23) != 242 and uint8(62) < 146 and uint8(49) & 128 == 0 and uint8(62) & 128 == 0 and uint8(4) & 128 == 0 and filesize ^ uint8(58) != 12 and uint8(72) & 128 == 0 and uint8(18) > 13 and filesize ^ uint8(42) != 1 and uint8(59) % 23 < 23 and uint8(53) & 128 == 0 and filesize ^ uint8(78) != 163 and uint8(60) > 14 and uint8(47) % 18 < 18 and uint8(79) > 31 and uint8(22) < 152 and filesize ^ uint8(64) != 50 and filesize ^ uint8(19) != 222 and uint8(81) < 131 and uint8(7) - 15 == 82 and filesize ^ uint8(51) != 204 and uint8(28) > 27 and uint32(70) + 349203301 == 2034162376 and filesize ^ uint8(61) != 94 and uint8(76) > 2 and filesize ^ uint8(77) != 223 and uint8(19) > 4 and uint8(80) > 2 and filesize ^ uint8(35) != 120 and filesize ^ uint8(22) != 31 and uint8(10) > 9 and uint8(22) > 20 and uint8(38) < 135 and filesize ^ uint8(10) != 205 and uint8(25) & 128 == 0 and uint8(13) < 147 and uint8(42) & 128 == 0 and hash.md5(76, 2) == "f98ed07a4d5f50f7de1410d905f1477f" and filesize ^ uint8(48) != 99 and filesize ^ uint8(16) != 7 and uint8(11) < 154 and filesize ^ uint8(76) != 30 and uint8(30) % 15 < 15 and filesize ^ uint8(74) != 193 and filesize ^ uint8(52) != 22 and filesize ^ uint8(36) != 6 and uint8(22) % 22 < 22 and uint8(44) & 128 == 0 and uint8(50) & 128 == 0 and filesize ^ uint8(25) != 224 and uint8(15) > 26 and filesize ^ uint8(60) != 43 and uint8(22) & 128 == 0 and uint8(82) & 128 == 0 and uint32(80) - 473886976 == 69677856 and uint8(75) > 30 and uint8(32) % 17 < 17 and filesize ^ uint8(15) != 27 and uint8(67) % 16 < 16 and uint8(23) > 2 and uint8(62) % 13 < 13 and uint8(34) < 138 and filesize ^ uint8(31) != 32 and uint8(72) % 14 < 14 and filesize ^ uint8(81) != 242 and filesize ^ uint8(54) != 141 and uint8(63) & 128 == 0 and uint8(0) < 129 and uint8(70) % 21 < 21 and uint8(8) & 128 == 0 and uint8(61) > 12 and uint8(24) > 22 and uint8(53) % 23 < 23 and uint8(46) & 128 == 0 and uint8(24) % 26 < 26 and uint32(3) ^ 298697263 == 2108416586 and uint8(21) - 21 == 94 and uint8(67) < 144 and uint8(48) > 15 and uint8(37) > 16 and uint8(42) < 157 and uint8(16) ^ 7 == 115 and uint8(13) > 21 and filesize ^ uint8(45) != 19 and uint8(47) & 128 == 0 and filesize ^ uint8(80) != 56 and filesize ^ uint8(78) != 6 and uint8(76) % 24 < 24 and uint8(73) < 136 and filesize ^ uint8(52) != 238 and uint8(50) % 11 < 11 and filesize ^ uint8(7) != 15 and filesize ^ uint8(66) != 51 and uint8(59) > 4 and uint8(46) > 22 and filesize ^ uint8(3) != 147 and uint8(63) % 30 < 30 and uint8(36) < 146 and uint8(26) < 132 and uint8(6) & 128 == 0 and filesize ^ uint8(30) != 249 and uint32(41) + 404880684 == 1699114335 and filesize ^ uint8(5) != 243 and uint8(70) & 128 == 0 and uint8(9) % 22 < 22 and uint8(59) < 141 and filesize ^ uint8(79) != 104 and filesize ^ uint8(5) != 43 and filesize ^ uint8(72) != 219 and uint8(52) > 25 and uint8(74) & 128 == 0 and uint8(28) < 160 and uint8(51) & 128 == 0 and hash.md5(50, 2) == "657dae0913ee12be6fb2a6f687aae1c7" and uint8(83) > 16 and uint8(31) > 7 and uint8(84) & 128 == 0 and filesize ^ uint8(46) != 18 and uint8(2) > 20 and uint8(5) < 158 and filesize ^ uint8(32) != 30 and filesize ^ uint8(50) != 219 and uint8(26) - 7 == 25 and uint8(53) > 24 and uint8(77) % 24 < 24 and uint8(3) % 13 < 13 and filesize ^ uint8(9) != 164 and filesize ^ uint8(80) != 236 and uint8(65) % 22 < 22 and filesize ^ uint8(84) != 231 and filesize ^ uint8(49) != 10 and uint8(67) > 27 and uint8(34) % 19 < 19 and uint8(64) & 128 == 0 and filesize ^ uint8(27) != 244 and uint8(12) & 128 == 0 and uint8(51) < 139 and uint8(35) % 15 < 15 and uint8(5) > 14 and filesize ^ uint8(34) != 115 and filesize ^ uint8(38) != 8 and filesize ^ uint8(72) != 37 and uint8(20) & 128 == 0 and uint8(17) < 150 and filesize ^ uint8(70) != 41 and uint8(66) % 16 < 16 and uint8(17) & 128 == 0 and uint8(19) & 128 == 0 and filesize ^ uint8(33) != 157 and uint8(21) > 7 and uint8(58) & 128 == 0 and uint8(71) < 130 and uint8(41) & 128 == 0 and uint8(57) > 11 and hash.md5(32, 2) == "738a656e8e8ec272ca17cd51e12f558b" and filesize ^ uint8(8) != 2 and filesize ^ uint8(57) != 186 and uint8(11) & 128 == 0 and uint8(2) < 147 and uint8(23) % 16 < 16 and uint8(78) < 141 and uint8(38) > 18 and filesize ^ uint8(41) != 233 and uint8(18) < 137 and uint8(40) & 128 == 0 and filesize ^ uint8(21) != 188 and filesize ^ uint8(57) != 14 and filesize ^ uint8(4) != 253 and uint8(14) < 153 and uint8(31) & 128 == 0 and uint8(81) > 11 and uint8(2) & 128 == 0 and filesize ^ uint8(22) != 191 and uint8(44) > 5 and uint8(84) + 3 == 128 and uint8(20) < 135 and filesize ^ uint8(73) != 61 and filesize ^ uint8(26) != 44 and uint8(1) < 158 and filesize ^ uint8(29) != 158 and uint8(49) < 129 and filesize ^ uint8(64) != 158 and uint8(25) < 154 and uint8(63) < 129 and uint8(84) > 26 and uint8(39) & 128 == 0 and uint8(25) > 27 and uint8(49) > 27 and uint8(9) > 23 and filesize ^ uint8(7) != 221 and uint8(50) < 138 and uint8(76) < 156 and filesize ^ uint8(61) != 239 and uint8(57) % 27 < 27 and filesize ^ uint8(8) != 107 and uint8(79) < 146 and filesize ^ uint8(40) != 49 and uint8(0) > 30 and uint8(45) > 17 and uint8(16) % 31 < 31 and filesize ^ uint8(1) != 232 and filesize ^ uint8(56) != 22 and uint8(42) > 3 and uint8(52) & 128 == 0 and uint8(69) % 30 < 30 and uint8(55) < 153 and filesize ^ uint8(41) != 74 and filesize ^ uint8(1) != 0 and filesize ^ uint8(44) != 96 and filesize ^ uint8(58) != 77 and uint8(34) > 18 and uint8(8) > 3
}

The string I’m looking for is 85 bytes long, and has an MD5 hash of b7dc94ca98aa58dabb5404541c812db2.

Conditions

Overview

The flag is defined by a series of conditions joined by and. I’ll use grep and sed to get each condition on it’s own line to better see what is in the file:

oxdf@hacky$ cat aray.yara | grep filesize | sed 's/ and /\n/g' | head
        filesize == 85
hash.md5(0, filesize) == "b7dc94ca98aa58dabb5404541c812db2"
filesize ^ uint8(11) != 107
uint8(55) & 128 == 0
uint8(58) + 25 == 122
uint8(7) & 128 == 0
uint8(48) % 12 < 12
uint8(17) > 31
uint8(68) > 10
uint8(56) < 155
oxdf@hacky$ cat aray.yara | grep filesize | sed 's/ and /\n/g' | wc -l
548

There’s 548 different conditions. The first two set the size and hash of the file.

uint8(X) means to make an unsigned 8 bit integer starting at offset X into the file. The various bytes of the file are defined in these conditions.

& 128

There are a bunch of conditions of the format uint(X) & 128 == 0. In fact, there’s 85 of them:

oxdf@hacky$ cat aray.yara | grep filesize | sed 's/ and /\n/g' | grep "& 128" | head
uint8(55) & 128 == 0
uint8(7) & 128 == 0
uint8(3) & 128 == 0
uint8(56) & 128 == 0
uint8(15) & 128 == 0
uint8(54) & 128 == 0
uint8(28) & 128 == 0
uint8(48) & 128 == 0
uint8(16) & 128 == 0
uint8(14) & 128 == 0
oxdf@hacky$ cat aray.yara | grep filesize | sed 's/ and /\n/g' | grep "& 128" | wc -l
85

These are basically saying that all 85 bytes in the file have a high bit of 0, roughly in the ASCII range. I can ignore these.

==

At least to start, I’ll focus on conditions with ==, as they distinctly define one or more bytes. After removing the & 128 conditions already discussed, there are 38 left:

oxdf@hacky$ cat aray.yara | grep filesize | sed 's/ and /\n/g' | grep -v "& 128" | grep == | wc -l
38

That may seem like not enough, but it turns out to be all I need. Even if it weren’t, this is a reasonable way to start working through the problem.

The full list of these conditions is:

oxdf@hacky$ cat aray.yara | grep filesize | sed 's/ and /\n/g' | grep -v "& 128" | grep == 
        filesize == 85
hash.md5(0, filesize) == "b7dc94ca98aa58dabb5404541c812db2"
uint8(58) + 25 == 122
uint32(52) ^ 425706662 == 1495724241
uint32(17) - 323157430 == 1412131772
hash.crc32(8, 2) == 0x61089c5c
hash.crc32(34, 2) == 0x5888fc1b
uint8(36) + 4 == 72
uint8(27) ^ 21 == 40
uint32(59) ^ 512952669 == 1908304943
uint8(65) - 29 == 70
uint8(45) ^ 9 == 104
uint32(28) - 419186860 == 959764852
uint8(74) + 11 == 116
hash.crc32(63, 2) == 0x66715919
hash.sha256(14, 2) == "403d5f23d149670348b147a15eeb7010914701a7e99aad2e43f90cfa0325c76f"
hash.sha256(56, 2) == "593f2d04aab251f60c9e4b8bbc1e05a34e920980ec08351a18459b2bc7dbf2f6"
uint8(75) - 30 == 86
uint32(66) ^ 310886682 == 849718389
uint32(10) + 383041523 == 2448764514
uint32(37) + 367943707 == 1228527996
uint32(22) ^ 372102464 == 1879700858
uint8(2) + 11 == 119
hash.md5(0, 2) == "89484b14b36a8d5329426a3d944d2983"
uint32(46) - 412326611 == 1503714457
hash.crc32(78, 2) == 0x7cab8d64
uint8(7) - 15 == 82
uint32(70) + 349203301 == 2034162376
hash.md5(76, 2) == "f98ed07a4d5f50f7de1410d905f1477f"
uint32(80) - 473886976 == 69677856
uint32(3) ^ 298697263 == 2108416586
uint8(21) - 21 == 94
uint8(16) ^ 7 == 115
uint32(41) + 404880684 == 1699114335
hash.md5(50, 2) == "657dae0913ee12be6fb2a6f687aae1c7"
uint8(26) - 7 == 25
hash.md5(32, 2) == "738a656e8e8ec272ca17cd51e12f558b"
uint8(84) + 3 == 128

These break down into five different categories:

oxdf@hacky$ cat aray.yara | grep filesize | sed 's/ and /\n/g' | grep -v "& 128" | grep == | cut -d'(' -f1 | sort | uniq -c | sort -nr
     13 uint8
     13 uint32
      5 hash.md5
      4 hash.crc32
      2 hash.sha256
      1         filesize == 85
  • 13 explicitly define a byte using a uint8

  • 13 explicitly define four bytes using a unit32

  • Ignoring the one that covers the entire file, four specify the MD5 hash for two bytes:
    oxdf@hacky$ cat aray.yara | grep filesize | sed 's/ and /\n/g' | grep -v "& 128" | grep == | grep md5
    hash.md5(0, filesize) == "b7dc94ca98aa58dabb5404541c812db2"
    hash.md5(0, 2) == "89484b14b36a8d5329426a3d944d2983"
    hash.md5(76, 2) == "f98ed07a4d5f50f7de1410d905f1477f"
    hash.md5(50, 2) == "657dae0913ee12be6fb2a6f687aae1c7"
    hash.md5(32, 2) == "738a656e8e8ec272ca17cd51e12f558b"
    
  • Four define the CRC32 checksum for two bytes

  • Two define the SHA256 hash for two bytes

All together, that’s \(13 + (13 \times 4) + (4 \times 2) + (4 \times 2) + (2 \times 2) = 85\). If I assume each of these covers a unique byte or set of bytes, then I should be able to define the entire file.

Solution

Python Script

Solution Class

I’m going to make a Python script to generate the file. I’ll create a class that processes the lines and prints the resulting file.

The class starts with an __init__ function that takes the conditions:

class Solution:
    def __init__(self, conditions: list[str]):
        self.size = int(conditions[0].strip().split(' == ')[-1])
        self.hash = conditions[1].split('"')[1]
        self.ans = [None]  * self.size
        self.parse_conditions(conditions[2:])
        print(''.join(chr(x) if x else '?' for x in self.ans))
        if hashlib.md5(bytes(self.ans)).hexdigest() == self.hash:
            print(f"Solution hash matches expected value of {self.hash}")

It gets the size and expected hash from the first two conditions, and then calls parse_conditions on the rest. Then it prints the results. For development, it’s nice to have the “?” in there to identify gaps. When the script is complete, ans will not have any None.

parse_conditions just loops over the lines, determines what kind of condition it is, and calls the appropriate internal function:

    def parse_conditions(self, lines: list[str]) -> None:
        for line in lines:
            match line.split('(')[0]:
                case "uint8" | "uint32":
                    self._unit(line)
                case "hash.md5" | "hash.crc32" | "hash.sha256":
                    self._hash(line)
                case _:
                    raise ValueError(f"Unexpected line: {line}")

When I first started I expect to need different functions for each of the five, but it turns out _uint and _hash were enough.

_uint uses regex to get the interesting parts of the command into variables:

    def _unit(self, line: str) -> None:
        size, offset, op, var, res = re.search(r'uint(\d+)\((\d+)\) (\S) (\d+) == (\d+)', line).groups()
        offset, var, res, size = map(int, (offset, var, res, size))
        assert size in [8, 32]
        if op == "^":
            val = res ^ var
        elif op == "+":
            val = res - var
        elif op == "-":
            val = res + var
        else:
            raise ValueError(f"Unexpected op: {op}")
        
        for i in range(size//8):
            self.ans[offset + i] = (val >> (i * 8)) % 256

Then it converts some to ints, calculates a value based on the op, and sets either one or four bytes depending on the size.

For _hash, I need to do roughly the same thing for each hash, but the calling is slightly different for CRC32 than it is for MD5 and SHA256. Therefore, I’ll create a lookup table of functions to pass the value into to get a result of the same format, a hex string:

    funcs = {
        "md5": lambda x: hashlib.md5(x).hexdigest(),
        "sha256": lambda x: hashlib.sha256(x).hexdigest(),
        "crc32": lambda x: f"{zlib.crc32(x):08x}",
    }

Then _hash starts the same way, using regex to parse values:

    def _hash(self, line: str) -> None:
        htype, offset, size, val = re.search(r'hash.(\w+)\((\d+), (\d)\) == (?:0x|")?([0-9a-f]+)', line).groups()
        offset, size = map(int, [offset, size])
        for candidate_bytes in itertools.product(range(128), repeat=size):
            candidate_byte_string = bytes(candidate_bytes)
            if self.funcs[htype](candidate_byte_string) == val:
                for i, c in enumerate(candidate_byte_string):
                    self.ans[offset + i] = c
                break
        else:
            raise ValueError(f"{htype} not found for {val}")

I’ll use itertools.product to get all possible byte combinations of size length (which happens to always be 2, but I can still make it generic), and loop over each, converting it to a byte string, and calculating the hash using the lookup table. If it matches the val, then I break and set the corresponding bytes in ans.

Main

With Solution ready, I just need to open the file, get the conditions and size, and pass them to create an instance:

with open('aray.yara', 'r') as f:
    yara = f.read()

(condition_line,) = [line for line in yara.split('\n') if 'filesize' in line]
conditions = [c for c in condition_line.split(' and ') if '==' in c and '& 128' not in c]

Solution(conditions)

I’m using the same logic here I did with bash above. get the line that has filesize (similar to grep above), and then split on “ and “ and filter to lines with “==” and not “& 128”. Then I pass the lines to a Solution object and it prints the result.

Solve

Running this script solves the challenge:

oxdf@hacky$ python solve.py 
rule flareon { strings: $f = "1RuleADayK33p$Malw4r3Aw4y@flare-on.com" condition: $f }
Solution hash matches expected value of b7dc94ca98aa58dabb5404541c812db2