Thanks drome for sharing his knowledge and skills! He completed all 10 challenges and this series of writeups is done by him :)
Details | Links |
---|---|
Official Challenge Site | https://flare-on.com/ |
Official Challenge Announcement | https://www.fireeye.com/blog/threat-research/2021/08/announcing-the-eighth-annual-flare-on-challenge.html |
Official Solutions | https://www.mandiant.com/resources/flare-on-8-challenge-solutions |
Official Challenge Binaries | http://flare-on.com/files/Flare-On8_Challenges.zip |
10_wizardcult
We have one final task for you. We captured some traffic of a malicious cyber-space computer hacker interacting with our web server. Honestly, I padded my resume a bunch to get this job and don’t even know what a pcap file does, maybe you can figure out what’s going on.
7zip password:flare
Here we’re given a pcap file called wizardcult.pcap
.
TCP stream 1 has this GET request from the client (attacker) .249
to the server at .245
.
|
|
In stream 2, the server makes a GET request (through wget
) to the attacker at /induct
, to which the attacker replies with an ELF file.
In stream 3 and 4, the attacker sends 2 more GET requests with the following cmd
parameters (URL encoded),
chmod +x /mages_tower/induct
/mages_tower/induct
This is clearly an SQLi attack where the attacker has RCE and is making the server execute the ELF binary.
Afterwards in stream 5, the server makes a connection to what seems to be the attacker’s IRC server, and sends many messages.
ELF File
arch x86
baddr 0x400000
binsz 7694693
bintype elf
bits 64
canary false
class ELF64
crypto false
endian little
havecode true
intrp /lib64/ld-linux-x86-64.so.2
laddr 0x0
lang go
linenum true
lsyms true
machine AMD x86-64 architecture
nx true
os linux
pic false
relocs true
relro no
rpath NONE
sanitize false
static false
stripped false
subsys linux
va true
Looks like a Go binary from all the function names like net_http_fixTrailer_func1
. Initially we analyzed it on Windows since we had IDA Pro on it, and used IDAGolangHelper plugin to try to make it nicer, but the improvements weren’t that noticeable. In addition, the decompiler view was not significantly better than graph view because the control flow isn’t that complicated, and the decompiler wasn’t able to analyze the arguments in function calls properly.
The binary uses the girc library for its IRC communication with the server at wizardcult.flare-on.com
.
From main_main_func1
, the following are mapped together
Potion of Acid Resistance
,"The beast smells quite foul."
,Graf's Infernal Disco
, triggers the command potion which runs a command.Potion of Water Breathing
,"The beast sits in the water, waiting for you to approach it."
,The Sunken Crypt
, triggers the readfile potionPotion of Watchful Rest
,"The beast sings you a lullaby."
,Pits of the Savage Mag
Potion of Superior Healing
,"You are wounded."
,Burrows of the Brutal Desert
There is a wizardcult_comms_ProcessDMMessage
, called by main_main_func2
, and it looks important. It replies
", what is your quest?"
with"My quest is to retrieve the Big Pterodactyl Fossil"
, then expects"welcome to the party."
"you have learned how to create the "
then expects"Potion of"
…"."
, then callswizardcult_vm_LoadProgram
.
There is an wizardcult_vm___ptr_Cpu__Execute
, and other similarly named functions
This suggests that the potions that the server is sending the client are actually custom VM instructions.
- In
wizardcult_potion_CommandPotion
, after executing the command usingos.exec.Command
, it callswizardcult_vm___ptr_Program__Execute
. - Similarly,
wizardcult_potion_ReadFilePotion
will also callwizardcult_vm___ptr_Program__Execute
after reading the file. wizardcult_tables_Ingredients
seems to convert potion ingredients to instructionswizardcult_comms_CastSpells
callssprintf
using the string"I cast %s on the %s for %dd%d damage!"
.
Combining what we know here with what we see in the IRC conversation, it seems that when the server sends the recipe for a potion, it is actually instructions in their custom VM. The potion is tied to a specific dungeon, and a command (read file or execute command), and must be triggered with some key phrase like "The beast smells quite foul"
. Afterwards the client will reply with the command result passed through the VM instructions, in the form "I cast %s on the Goblin for %dd%d damage!"
.
Dynamic Analysis
We launch our Linux machine, set up a simple server to replay the traffic that we saw in the pcap, set our hosts file to point the server to the localhost, then run the malware, and see where it runs in the binary.
Note that when replaying the traffic, we have to change the name to the nickname that it set, and put a newline at the end of every message, if not it will not process and transfer execution flow to wizardcult_comms_ProcessDMMessage
.
We replay the traffic up to the point where the player enters Graf's Internal Disco
, then breakpoint at wizardcult_potion_CommandPotion
.
We are able to see the decoded command through the function arguments, and find that it was ls /mages_tower
, which corresponds to the message frightening, virtual, danish, flimsy, gruesome great, dark oppressive, bad, average, virtual, last, more strange, inhospitable, slimy, average, and few dismal
that we sent.
Here, we can tell that one comma-separated phrase corresponds to a single character in the decoded command.
It is hard to immediately identify which specific phrases in the binary are converted to characters since they are not all grouped together statically in the binary. The function responsible for this seems to be wizardcult_tables_GetBytesFromTable
but instead of analyzing that function, we reran the binary but this time sent this message that we found in the second command in the IRC conversation
flimsy, gruesome great, dark oppressive, bad, average, virtual, last, more strange, inhospitable, slimy, average, few dismal, flimsy, dark and gruesome, inhospitable, inhospitable, frightening, last, slimy, nicest, solid, dark oppressive, few dismal, deep subterranean, last, gruesome great, average, gruesome great, average, cruel, damned, common, and bad..
Then we breakpoint at wizardcult_potion_CommandPotion
and see that this corresponds to /mages_tower/cool_wizard_meme.png
, which makes sense as the second message was in The Sunken Crypt
which is a read file command.
We run this script to get the mappings
|
|
which gives us this
[
(' ', 'danish'),
('.', 'cruel'),
('/', 'flimsy'),
('_', 'last'),
('a', 'dark oppressive'),
('c', 'dark and gruesome'),
('d', 'deep subterranean'),
('e', 'average'),
('g', 'bad'),
('i', 'nicest'),
('l', 'frightening'),
('m', 'gruesome great'),
('n', 'on'),
('o', 'inhospitable'),
('p', 'damned'),
('r', 'few dismal'),
('s', 'virtual'),
('t', 'more strange'),
('w', 'slimy'),
('z', 'solid'),
]
Note that in order for the command to work, we have to send the and
for the last item and the ending ..
after it.
Analyzing the command response
We change the command to ls mages_tower
(by removing flimsy
in the message), create that folder, add the file cool_wizard_meme.png
to the folder so that it is as close as we can get to the original message, then get the following response:
PRIVMSG #dungeon :I quaff my potion and attack!
PRIVMSG #dungeon :I cast Moonbeam on the Goblin for 205d205 damage!
PRIVMSG #dungeon :I cast Reverse Gravity on the Goblin for 253d213 damage!
PRIVMSG #dungeon :I cast Water Walk on the Goblin for 216d195 damage!
PRIVMSG #dungeon :I cast Mass Suggestion on the Goblin for 198d253 damage!
PRIVMSG #dungeon :I cast Planar Ally on the Goblin for 199d207 damage!
PRIVMSG #dungeon :I cast Water Breathing on the Goblin for 140d210 damage!
PRIVMSG #dungeon :I cast Conjure Barrage on the Goblin for 197d168 damage!
PRIVMSG #dungeon :I do believe I have slain the Goblin
The original response to ls /mages_tower
was:
PRIVMSG #dungeon :I quaff my potion and attack!
PRIVMSG #dungeon :I cast Moonbeam on the Goblin for 205d205 damage!
PRIVMSG #dungeon :I cast Reverse Gravity on the Goblin for 253d213 damage!
PRIVMSG #dungeon :I cast Water Walk on the Goblin for 216d195 damage!
PRIVMSG #dungeon :I cast Mass Suggestion on the Goblin for 198d253 damage!
PRIVMSG #dungeon :I cast Planar Ally on the Goblin for 199d207 damage!
PRIVMSG #dungeon :I cast Water Breathing on the Goblin for 140d210 damage!
PRIVMSG #dungeon :I cast Conjure Barrage on the Goblin for 197d168 damage!
PRIVMSG #dungeon :I cast Water Walk on the Goblin for 204d198 damage!
PRIVMSG #dungeon :I cast Call Lightning on the Goblin for 193d214 damage!
PRIVMSG #dungeon :I cast Branding Smite on the Goblin!
PRIVMSG #dungeon :I do believe I have slain the Goblin
Only the last 3 lines of the original response differ from the new one.
We change the folder structure and add AAAAA...AAA
as the only file in that folder, then get the following response:
PRIVMSG #dungeon :I quaff my potion and attack!
PRIVMSG #dungeon :I cast Divine Favor on the Goblin for 227d227 damage!
PRIVMSG #dungeon :I cast Divine Favor on the Goblin for 227d227 damage!
PRIVMSG #dungeon :I cast Divine Favor on the Goblin for 227d227 damage!
PRIVMSG #dungeon :I cast Divine Favor on the Goblin for 227d227 damage!
PRIVMSG #dungeon :I cast Divine Favor on the Goblin for 227d227 damage!
...
PRIVMSG #dungeon :I cast Divine Favor on the Goblin for 227d227 damage!
PRIVMSG #dungeon :I cast Divine Favor on the Goblin for 227d227 damage!
PRIVMSG #dungeon :I cast Divine Favor on the Goblin for 168 raw damage!
PRIVMSG #dungeon :I do believe I have slain the Goblin
There is some kind of encoding going on here, probably from the VM instructions.
We try with a file name AAABBBCCCDDDEEE...
and get this response:
PRIVMSG #dungeon :I cast Divine Favor on the Goblin for 227d227 damage!
PRIVMSG #dungeon :I cast Animate Dead on the Goblin for 224d224 damage!
PRIVMSG #dungeon :I cast Mind Blank on the Goblin for 225d225 damage!
PRIVMSG #dungeon :I cast Blight on the Goblin for 230d230 damage!
PRIVMSG #dungeon :I cast Barkskin on the Goblin for 231d231 damage!
PRIVMSG #dungeon :I cast Telepathy on the Goblin for 228d228 damage!
PRIVMSG #dungeon :I cast Vicious Mockery on the Goblin for 229d229 damage!
PRIVMSG #dungeon :I cast Find Traps on the Goblin for 234d234 damage!
PRIVMSG #dungeon :I cast Animal Shapes on the Goblin for 235d235 damage!
PRIVMSG #dungeon :I cast Counterspell on the Goblin for 232d232 damage!
PRIVMSG #dungeon :I cast Conjure Fey on the Goblin for 233d233 damage!
PRIVMSG #dungeon :I cast Warding Bond on the Goblin for 238d238 damage!
...
This means that each spell / damage number probably corresponds to a single byte, meaning that the VM instructions for the command potion probably does a simple substitution of one plaintext byte for one ciphertext byte. If this were the case, we could simply find out all the mappings of ciphertext bytes to plaintext bytes, and see what was sent in the pcap. However, judging from how there were only 3 lines differentiating the original response from the one we got, the flag isn’t likely to be in the first command response, and we should look into the read file potion response instead.
Analyzing the read file response
From what we gathered previously, the second exchange between the server and client in the original pcap was probably a read file command to mages_tower/cool_wizard_meme.png
, then through some encoder defined by the Potion of Water Breathing
potion instructions.
We change the read file command to point to mages_tower/cool_wizard_meme.png
, then set the file cool_wizard_meme.png
to contain b"\x00\x00\x00\x01\x01\x01..."
, hoping to see something like what we got for the AAABBBCCCDDDEEE...
filename response. However, we don’t get something nice like that, and it actually looks quite random. This means that the VM instructions in the read file potion doesn’t do the same thing as the command potion.
We try to simplify our plaintext to see if there’s any patterns we can exploit, so we send 256 null bytes b"\x00\x00\x00..."
, and get this
PRIVMSG #dungeon :I quaff my potion and attack!
PRIVMSG #dungeon :I cast Feather Fall on the Wyvern for 218d197 damage!
PRIVMSG #dungeon :I cast Flesh to Stone on the Wyvern for 40d117 damage!
PRIVMSG #dungeon :I cast Shillelagh on the Wyvern for 3d248 damage!
PRIVMSG #dungeon :I cast Spider Climb on the Wyvern for 197d211 damage!
PRIVMSG #dungeon :I cast Cure Wounds on the Wyvern for 145d131 damage!
PRIVMSG #dungeon :I cast Disintegrate on the Wyvern for 93d13 damage!
PRIVMSG #dungeon :I cast Shillelagh on the Wyvern for 203d64 damage!
PRIVMSG #dungeon :I cast Animate Dead on the Wyvern for 133d234 damage!
PRIVMSG #dungeon :I cast Feather Fall on the Wyvern for 218d197 damage!
PRIVMSG #dungeon :I cast Flesh to Stone on the Wyvern for 40d117 damage!
PRIVMSG #dungeon :I cast Shillelagh on the Wyvern for 3d248 damage!
PRIVMSG #dungeon :I cast Spider Climb on the Wyvern for 197d211 damage!
PRIVMSG #dungeon :I cast Cure Wounds on the Wyvern for 145d131 damage!
PRIVMSG #dungeon :I cast Disintegrate on the Wyvern for 93d13 damage!
PRIVMSG #dungeon :I cast Shillelagh on the Wyvern for 203d64 damage!
PRIVMSG #dungeon :I cast Animate Dead on the Wyvern for 133d234 damage!
PRIVMSG #dungeon :I cast Feather Fall on the Wyvern for 218d197 damage!
PRIVMSG #dungeon :I cast Flesh to Stone on the Wyvern for 40d117 damage!
PRIVMSG #dungeon :I cast Shillelagh on the Wyvern for 3d248 damage!
PRIVMSG #dungeon :I cast Spider Climb on the Wyvern for 197d211 damage!
PRIVMSG #dungeon :I cast Cure Wounds on the Wyvern for 145d131 damage!
PRIVMSG #dungeon :I cast Disintegrate on the Wyvern for 93d13 damage!
PRIVMSG #dungeon :I cast Shillelagh on the Wyvern for 203d64 damage!
PRIVMSG #dungeon :I cast Animate Dead on the Wyvern for 133d234 damage!
...
PRIVMSG #dungeon :I do believe I have slain the Wyvern
We sent 256 bytes, and there are 256 entries (spells and damage numbers) here, so there probably isn’t any compression going on, just encryption of some kind. In addition, we see that there are repeats every 24 bytes. If there’s no rearrangement of bytes within the block, and each byte in the plaintext corresponds to the ciphertext byte at the same offset, then we could take a shortcut, and just map out the pair (value, position mod 24)
to the plaintext value, instead of having to analyze the custom VM instructions.
We change the plaintext file to something slightly more complicated but still follows the 24 block format
|
|
Then run it again and get this
PRIVMSG #dungeon :I quaff my potion and attack!
PRIVMSG #dungeon :I cast Feather Fall on the Wyvern for 218d197 damage!
PRIVMSG #dungeon :I cast Flesh to Stone on the Wyvern for 40d117 damage!
PRIVMSG #dungeon :I cast Shillelagh on the Wyvern for 3d248 damage!
PRIVMSG #dungeon :I cast Spider Climb on the Wyvern for 197d211 damage!
PRIVMSG #dungeon :I cast Cure Wounds on the Wyvern for 145d131 damage!
PRIVMSG #dungeon :I cast Disintegrate on the Wyvern for 93d13 damage!
PRIVMSG #dungeon :I cast Shillelagh on the Wyvern for 203d64 damage!
PRIVMSG #dungeon :I cast Animate Dead on the Wyvern for 133d234 damage!
PRIVMSG #dungeon :I cast Bestow Curse on the Wyvern for 76d83 damage!
PRIVMSG #dungeon :I cast Blinding Smite on the Wyvern for 98d154 damage!
PRIVMSG #dungeon :I cast Blade Ward on the Wyvern for 60d41 damage!
PRIVMSG #dungeon :I cast Tongues on the Wyvern for 83d231 damage!
PRIVMSG #dungeon :I cast Hail of Thorns on the Wyvern for 115d214 damage!
PRIVMSG #dungeon :I cast Insect Plague on the Wyvern for 84d137 damage!
PRIVMSG #dungeon :I cast Blade Ward on the Wyvern for 18d36 damage!
PRIVMSG #dungeon :I cast Contingency on the Wyvern for 252d168 damage!
PRIVMSG #dungeon :I cast Feather Fall on the Wyvern for 218d197 damage!
PRIVMSG #dungeon :I cast Flesh to Stone on the Wyvern for 40d117 damage!
PRIVMSG #dungeon :I cast Shillelagh on the Wyvern for 3d248 damage!
PRIVMSG #dungeon :I cast Spider Climb on the Wyvern for 197d211 damage!
PRIVMSG #dungeon :I cast Hail of Thorns on the Wyvern for 115d214 damage!
PRIVMSG #dungeon :I cast Insect Plague on the Wyvern for 84d137 damage!
PRIVMSG #dungeon :I cast Blade Ward on the Wyvern for 18d36 damage!
PRIVMSG #dungeon :I cast Contingency on the Wyvern for 252d168 damage!
PRIVMSG #dungeon :I do believe I have slain the Wyvern
This response should be split up into 3 parts, each of 8 lines (24 bytes) — 1st block is purely 0x00
bytes, 2nd block is purely 0x01
bytes, and 3rd block is mixed where the first 12 bytes are 0x00
bytes and the next 12 are 0x01
.
As we hoped, the first 4 lines of the pure 0x00
block match the first 4 lines of the last mixed block, and the last 4 lines of the pure 0x01
block matches the last 4 of the mixed block, which means that there is probably no intra-block rearrangement involved and we should try doing the mapping as previously described.
To get a comprehensive map of each (ciphertext entity, offset mod 24)
pair to its corresponding plaintext byte, we need to know the ciphertext entities for every pair of (offset mod 24, plaintext byte)
. To do this, use this script to create a file that has every pair we need:
|
|
Then we run the server script again. Note that because the response is so big, we have to constantly call recv
on our server script or else we’ll have issues with our TCP window filling up. We take the response from the server, then generate our mapping
|
|
Afterwards, we take the original response from the pcap and apply the mapping
|
|
and check the first few bytes to see if it makes sense. We get b'\x89PNG\r\n\x1a\n\x00\x00'
, which has the PNG magic bytes, so it looks like we were successful.
We dump out our entire decoded message, and open it, which gives us our flag.
Solution Script
|
|
Flag
wh0_n33ds_sw0rds_wh3n_you_h4ve_m4ge_h4nd@flare-on.com