Flare-on is an annual CTF style challenge organized by Fire-eye with a focus on reverse engineering. The contest falls into its fourth year this season. Taking part in these challenges gives us a nice opportunity to learn something new and this year was no exception. Overall, there were 12 challenges to complete. Official solution to the challenges has already been published at the FireEye blog. Hence instead of a detailed write-up, I will just cover the important parts.
The code simply performs a ROT-13 of the input and compares it with another string. To get back the flag, re-apply ROT-13.
Flag: ClientSideLoginsAreEasy@flare-on.com
#1 - Login.html
The first problem was as simple as it gets. There is an HTML file with a form. We need to provide a flag and check for its correctness.
Figure 1: Check thy flag |
Figure 2: ROT-13 again |
#2 - IgniteMe.exe
This is one of the classical crackme challenges. A PE is provided. It takes a text as input and checks whether it's correct or not. The following script obtains the flag.
Flag: R_y0u_H0t_3n0ugH_t0_1gn1t3@flare-on.com
target = [13, 38, 73, 69, 42, 23, 120, 68, 43, 108, 93, 94, 69,
18, 47, 23, 43, 68,111, 110, 86, 9, 95, 69, 71, 115, 38, 10,
13, 19, 23, 72, 66, 1, 64, 77, 12, 2, 105]
flag = []
v = 4
for x in reversed(target):
v = x ^ v
flag.append(v)
print ''.join(reversed(map(chr, flag)))
Flag: R_y0u_H0t_3n0ugH_t0_1gn1t3@flare-on.com
#3 - greek_to_me.exe
This is a PE which takes its input over the network. The input is a 32-bit integer. The most significant byte of this integer is used to decrypt a piece of encrypted code. A fletcher's checksum checks for correctness of the decrypted data. Like others have done, I bruteforced the key in C.
The decryption key is 0xa2. Running the program with this key we can retrieve the flag from memory.
Flag: et_tu_brute_force@flare-on.com
#include <stdio.h>
#include <string.h>
typedef unsigned char uint8_t;
typedef unsigned short uint16_t;
uint8_t data[] = {
51, 225, 196, 153, 17, 6, 129, 22, 240, 50, 159, 196, 145, 23, 6, 129, 20, 240,
6, 129, 21, 241, 196, 145, 26, 6, 129, 27, 226, 6, 129, 24, 242, 6, 129, 25, 241,
6, 129, 30, 240, 196, 153, 31, 196, 145, 28, 6, 129, 29, 230, 6, 129, 98, 239, 6,
129, 99, 242, 6, 129, 96, 227, 196, 153, 97, 6, 129, 102, 188, 6, 129, 103, 230,
6, 129, 100, 232, 6, 129, 101, 157, 6, 129, 106, 242, 196, 153, 107, 6, 129, 104,
169, 6, 129, 105, 239, 6, 129, 110, 238, 6, 129, 111, 174, 6, 129, 108, 227, 6,
129, 109, 239, 6, 129, 114, 233, 6, 129, 115, 124
};
uint8_t buf[121];
uint16_t fletcher16(uint8_t *data, int count )
{
uint16_t sum1 = 0;
uint16_t sum2 = 0;
int index;
for( index = 0; index < count; ++index )
{
sum1 = (sum1 + data[index]) % 255;
sum2 = (sum2 + sum1) % 255;
}
return (sum2 << 8) | sum1;
}
void main()
{
uint16_t checksum;
int i;
for (int key = 1; key <= 255; key++)
{
memcpy(buf, data, 121);
printf("[*] Trying key: %x -> ", key);
for (i = 0; i <= 121; i++)
{
buf[i] ^= key;
buf[i] += 0x22;
}
checksum = fletcher16(buf, 121);
printf("%x\n", checksum);
if (checksum == 0xFB5E)
{
printf("[!] Xor key: %x\n", key);
break;
}
}
}
The decryption key is 0xa2. Running the program with this key we can retrieve the flag from memory.
Figure 3: Yes, even we too bruteforce! |
Flag: et_tu_brute_force@flare-on.com
#4 - notepad.exe
This is one of those challenges which need a bit of spark to complete. The challenge searches for PE files in the directory %USERPROFILE%/flareon2016 challenge. The files must have a specific timestamp or else we fail. This set me off for quite some time as I didn't have any idea what it meant. Fortunately, searching for one of the timestamps on Google lead to a clue. The values are basically the timestamps of the first four PE files from last years challenge. Dropping those files in the proper directory and after a bit of fiddling gives the flag.
Flag: bl457_fr0m_th3_p457@flare-on.com
On completing all of the levels, it displays a message. Cleaning it up reveals.
Flag: y0u__sUnK_mY__P3Wp3w_b04t@flare-on.com
Flag: bl457_fr0m_th3_p457@flare-on.com
#5 - pewpewboat.exe
This is an x86_64 ELF although named as an exe. It's one of the hidden ships game. A 8x8 grid is provided. Some of the cells have a ship hidden beneath. The task is to complete all the levels. To play the game in a semi-automatic fashion I developed a script using the python bindings of radare.
#!/usr/bin/env python
import r2pipe
import sys
def get_ships(state):
print 'Writing moves...'
f = open('moves', 'w')
for row in xrange(8):
for col in xrange(8):
bitmask = 1 << ((row * 8) + col)
if state & bitmask != 0:
f.write('%s%s' %(chr(65+row), chr(49+col)))
f.write('\n')
f.close()
def main():
r2 = r2pipe.open('tcp://127.0.0.1:5555')
# r2.cmd('aa')
# r2.cmd('doo')
"""
.text:0000000000403EB1 mov rdi, rax
.text:0000000000403EB4 call play_map
.text:0000000000403EB9 mov [rbp+var_4C], eax
.text:0000000000403EBC cmp [rbp+var_4C], 1
"""
# Set breakpoint on play_map
r2.cmd('db 0x403EB4')
# Resume execution
r2 = r2pipe.open('tcp://127.0.0.1:5555')
r2.cmd('dc')
while True:
# Breakpoint hit, get address of map in rdi
r2 = r2pipe.open('tcp://127.0.0.1:5555')
map_addr = r2.cmdj('drj')['rdi']
# Get goal state
r2 = r2pipe.open('tcp://127.0.0.1:5555')
goal_state = r2.cmdj('pv8j @ %d' %map_addr)['value']
get_ships(goal_state)
# Resume execution
r2 = r2pipe.open('tcp://127.0.0.1:5555')
r2.cmd('dc')
if __name__ == '__main__':
main()
On completing all of the levels, it displays a message. Cleaning it up reveals.
Aye! You found some letters did ya? To find what you're looking for, you'll want to re-order them: 9, 1, 2, 7, 3, 5, 6, 5, 8, 0, 2, 3, 5, 6, 1, 4. Next you let 13 ROT in the sea! THE FINAL SECRET CAN BE FOUND WITH ONLY THE UPPER CASE.As instructed, we ROT-13 the letters to get the string BUTWHEREISTHERUM. Feeding this to the application, we can get the flag.
Flag: y0u__sUnK_mY__P3Wp3w_b04t@flare-on.com
#6 - payload.dll
A PE32+ dll is provided which exports a single function named EntryPoint. However, trying to call this export via rundll32 fails. This was because DllMain modified the export table when called. Dumping the running dll from memory we were clearly able to see that the exported function name was indeed getting changed. The exported function took a name depending on the value of (year+month)%26. Using an x64dbg script, I was able to recover all of the possible names.
Calling the obtained names using a batch script reveals the flag letter by letter,
Flag: wuuut-exp0rts@flare-on.com
ctr = 0
@run:
init "C:\Documents and Settings\Administrator\Desktop\payload.dll"
doSleep 100
run 180005DDD
mov eax, ctr
run 180005D24
log =========================================================
log {d:ctr}
find rdx+33, 00
log function: {s:$result+1}
log =========================================================
stop
ctr = ctr + 1
cmp ctr, 1a
jl @run
Calling the obtained names using a batch script reveals the flag letter by letter,
@echo off
setlocal enabledelayedexpansion
set fn[0]=filingmeteorsgeminately
set fn[1]=leggykickedflutters
set fn[2]=incalculabilitycombustionsolvency
set fn[3]=crappingrewardsanctity
set fn[4]=evolvablepollutantgavial
set fn[5]=ammoniatesignifiesshampoo
set fn[6]=majesticallyunmarredcoagulate
set fn[7]=roommatedecapitateavoider
set fn[8]=fiendishlylicentiouslycolouristic
set fn[9]=sororityfoxyboatbill
set fn[10]=dissimilitudeaggregativewracks
set fn[11]=allophoneobservesbashfulness
set fn[12]=incuriousfatherlinessmisanthropically
set fn[13]=screensassonantprofessionalisms
set fn[14]=religionistmightplaythings
set fn[15]=airglowexactlyviscount
set fn[16]=thonggeotropicermines
set fn[17]=gladdingcocottekilotons
set fn[18]=diagrammaticallyhotfootsid
set fn[19]=corkerlettermenheraldically
set fn[20]=ulnacontemptuouscaps
set fn[21]=impureinternationalisedlaureates
set fn[22]=anarchisticbuttonedexhibitionistic
set fn[23]=tantalitemimicryslatted
set fn[24]=basophileslapsscrapping
set fn[25]=orphanedirreproducibleconfidences
for /l %%n in (0,1,25) do (
set /a year=2001+%%n
date 1-2-!year!
rundll32 payload.dll !fn[%%n]! !fn[%%n]!
)
Flag: wuuut-exp0rts@flare-on.com
#7 - zsud.exe
The 7th challenge is a maze game implemented in PowerShell. The script was embedded inside a parent executable and ran using CLR Hosting. Dumping the script from the running process using dnSpy was easy.
The ps script implementing the game was obfuscated. After deobfuscating it manually, I was able to draw the maze by following the code.
We need to traverse the maze in a specific way or else we fail. The function which executed a move was as follows.
At a first glance, the use of random function seemed a bit strange. The first few correct moves could be obtained by brute-force. Inspecting the executable which hosted this script revealed that the rand function was actually hooked. Instead of returning random numbers the function returned numbers from the following list in sequential order.
3, 0, 0, 2, 2, 1, 1, 1, 0, 2, 3, 0, 2, 2, 3, 3, 3, 5, 4, 0, 5, 4, 0, 5, 4, 0, 1, 4, 0, 2, 4, 0, 1, 2, 3, 5, 4, 0, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 5, 4, 0
Mapping the numbers to the directions we get the following list of 53 moves.
w, n, n, e, e, s, s, s, n, e, w, n, e, e, w, w, w, d, u, n, d, u, n, d, u, n, s, u, n, e, u, n, s, e, w, d, u, n, s, e, w, s, e, w, s, e, w, s, e, w, d, u, n
I used a macro creator tool to send these moves to the game window. On performing the last move we are taken back to the starting point at the vestibule. From there it was a matter to go to the office to get the flag.
Flag: mudd1ng_by_y0ur53lph@flare-on.com
![]() |
Figure 4: Dumping the PS script |
Figure 5: The Maze |
function Invoke-MoveDirection($char, $room, $direction, $trailing) {
$nextroom = $null
$movetext = "You can't go $direction."
$statechange_tristate = $null
$nextroom = Get-RoomAdjoining $room $direction
if ($nextroom -ne $null) {
$key = Get-ThingByKeyword $char 'key'
if (($key -ne $null) -and ($script:okaystopnow -eq $false)) {
$dir_short = ([String]$direction[0]).ToLower()
${N} = ${sCRiPt:MSVcRt}::("rand").Invoke()%6
Write-Host $N
if ($directions_enum[$dir_short] -eq ($n)) {
$script:key_directions += $dir_short
$newdesc = Invoke-XformKey $script:key_directions $key.Desc
$key.Desc = $newdesc
if ($newdesc.Contains("@")) {
$nextroom = $script:map.StartingRoom
$script:okaystopnow = $true
}
$statechange_tristate = $true
} else {
$statechange_tristate = $false
}
}
$script:room = $nextroom
$movetext = "You go $($directions_short[$direction.ToLower()])"
if ($statechange_tristate -eq $true) {
$movetext += "`nThe key emanates some warmth..."
} elseif ($statechange_tristate -eq $false) {
$movetext += "`nHmm..."
}
if ($script:autolook -eq $true) {
$movetext += "`n$(Get-LookText $char $script:room $trailing)"
}
} else {
$movetext = "You can't go that way."
}
return "$movetext"
}
At a first glance, the use of random function seemed a bit strange. The first few correct moves could be obtained by brute-force. Inspecting the executable which hosted this script revealed that the rand function was actually hooked. Instead of returning random numbers the function returned numbers from the following list in sequential order.
3, 0, 0, 2, 2, 1, 1, 1, 0, 2, 3, 0, 2, 2, 3, 3, 3, 5, 4, 0, 5, 4, 0, 5, 4, 0, 1, 4, 0, 2, 4, 0, 1, 2, 3, 5, 4, 0, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 5, 4, 0
Mapping the numbers to the directions we get the following list of 53 moves.
w, n, n, e, e, s, s, s, n, e, w, n, e, e, w, w, w, d, u, n, d, u, n, d, u, n, s, u, n, e, u, n, s, e, w, d, u, n, s, e, w, s, e, w, s, e, w, s, e, w, d, u, n
I used a macro creator tool to send these moves to the game window. On performing the last move we are taken back to the starting point at the vestibule. From there it was a matter to go to the office to get the flag.
Flag: mudd1ng_by_y0ur53lph@flare-on.com
#8 - flair.apk
Every year there is at least one challenge related to Android. This is it. An apk is provided. The challenge consists of four levels implemented by four different activities named Michael, Brian, Milton and Printer. We need to solve them sequentially in order to get back the flag.
The first activity can be easily solved statically to get the password MYPRSHE__FTW. The second activity compared our input password with a string generated at runtime. The last two activities performs a calculation on our input to generate an array which was again compared. To solve these three levels, I used frida to hook on the relevant functions.
The script hooks the important functions and logs both the arguments and the return value. Running this we get a log.
From the trace, we can see that the password for the 2nd activity is hashtag_covfefe_Fajitas!.The third and fourth activity required a bit of brute-force to get the respective password.
Third Activity
Fourth activity
Flag: pc_lo4d_l3tt3r_gl1tch@flare-on.com
The first activity can be easily solved statically to get the password MYPRSHE__FTW. The second activity compared our input password with a string generated at runtime. The last two activities performs a calculation on our input to generate an array which was again compared. To solve these three levels, I used frida to hook on the relevant functions.
import frida
import time
jscode = """
console.log("[+] Script loaded successfully...");
console.log("[+] Java available: " + Java.available);
Java.perform(function x()
{
console.log("[+] Entered perform..");
// Second level
var brian = Java.use("com.flare_on.flair.Brian");
brian.teraljdknh.implementation = function(s1,s2)
{
console.log("[+] Entered com.flare_on.flair.Brian.teraljdknh");
console.log("[*] arg1=" + s1);
console.log("[*] arg2=" + s2);
return this.teraljdknh(s1, s2);
};
console.log("[+] Replaced com.flare_on.flair.Brian.teraljdknh");
// Third level
var milton = Java.use("com.flare_on.flair.Milton");
milton.nbsadf.implementation = function ()
{
console.log("[+] Entered com.flare_on.flair.Milton.nbsadf");
retval = this.nbsadf();
console.log("[*] retval="+retval);
return retval;
};
console.log("[+] Replaced com.flare_on.flair.flair.Milton.nbsadf");
//Fourth level
var stapler = Java.use("com.flare_on.flair.Stapler");
//Hook string decryptor
stapler.iemm.implementation = function (p)
{
console.log("[+] Entered com.flare_on.flair.Stapler.iemm");
retval = this.iemm(p);
console.log("[*] arg1="+p);
console.log("[*] retval="+retval);
return retval;
};
console.log("[+] Replaced com.flare_on.flair.flair.Stapler.iemm");
//Hook hardcoded array
stapler.poserw.implementation = function (p)
{
console.log("[+] Entered com.flare_on.flair.Stapler.poserw");
retval = this.poserw(p);
console.log("[*] retval="+retval);
return retval;
};
console.log("[+] Replaced com.flare_on.flair.flair.Stapler.poserw");
});
"""
device = frida.get_usb_device()
process = device.attach('com.flare_on.flair')
script = process.create_script(jscode)
script.load()
raw_input()
The script hooks the important functions and logs both the arguments and the return value. Running this we get a log.
[+] Script loaded successfully...
[+] Java available: true
[+] Entered perform..
[+] Replaced com.flare_on.flair.Brian.teraljdknh
[+] Replaced com.flare_on.flair.flair.Milton.nbsadf
[+] Replaced com.flare_on.flair.flair.Stapler.iemm
[+] Replaced com.flare_on.flair.flair.Stapler.poserw
[+] Entered com.flare_on.flair.Brian.teraljdknh
[*] arg1=hashtag_covfefe_Fajitas!
[*] arg2=hashtag_covfefe_Fajitas!
[+] Entered com.flare_on.flair.Milton.nbsadf
[+] Entered com.flare_on.flair.Stapler.poserw
[*] retval=16,-82,-91,-108,-125,30,11,66,-71,86,-59,120,-17,-102,109,68,-18,57,-109,-115
[*] retval=16,-82,-91,-108,-125,30,11,66,-71,86,-59,120,-17,-102,109,68,-18,57,-109,-115
[+] Entered com.flare_on.flair.Milton.nbsadf
[+] Entered com.flare_on.flair.Stapler.poserw
[*] retval=16,-82,-91,-108,-125,30,11,66,-71,86,-59,120,-17,-102,109,68,-18,57,-109,-115
[*] retval=16,-82,-91,-108,-125,30,11,66,-71,86,-59,120,-17,-102,109,68,-18,57,-109,-115
[+] Entered com.flare_on.flair.Stapler.iemm
[*] arg1=e.RP9SR8x9.GH.G8M9.GHkG
[*] retval=android.content.Context
[+] Entered com.flare_on.flair.Stapler.iemm
[*] arg1=LHG1@@!SxeGS9.M9.GHkG
[*] retval=getApplicationContext
--------------
snip
--------------
[+] Entered com.flare_on.flair.Stapler.iemm
[*] arg1=H?ye!v
[*] retval=equals
[+] Entered com.flare_on.flair.Stapler.poserw
[*] retval=95,27,-29,-55,-80,-127,-60,13,-33,-60,-96,35,-127,86,0,-114,-25,30,36,-92
[+] Entered com.flare_on.flair.Stapler.iemm
[*] arg1=e.RP9SR8x9.GH.G8M9.GHkG
[*] retval=android.content.Context
[+] Entered com.flare_on.flair.Stapler.iemm
[*] arg1=LHG1@@!SxeGS9.M9.GHkG
[*] retval=getApplicationContext
--------------
snip
--------------
[+] Entered com.flare_on.flair.Stapler.iemm
[*] arg1=,e}e8yGS!81PPe(v
[*] retval=java.util.Arrays
[+] Entered com.flare_on.flair.Stapler.iemm
[*] arg1=H?ye!v
[*] retval=equals
[+] Entered com.flare_on.flair.Stapler.poserw
[*] retval=95,27,-29,-55,-80,-127,-60,13,-33,-60,-96,35,-127,86,0,-114,-25,30,36,-92
From the trace, we can see that the password for the 2nd activity is hashtag_covfefe_Fajitas!.The third and fourth activity required a bit of brute-force to get the respective password.
Third Activity
public class Main
{
public static void main(String args[])
{
byte[] b_arr = new byte[] {16,-82,-91,-108,-125,30,11,66,-71,86,-59,120,-17,-102,109,68,-18,57,-109,-115};
char keyspace[] = "0123456789abcdef".toCharArray();
for (byte b: b_arr)
{
next:
for (char c1: keyspace)
{
for (char c2: keyspace)
{
byte x = (byte)((Character.digit(c1, 16) << 4) + Character.digit(c2, 16));
if (b == x)
{
System.out.print(c1 + "" + c2);
break next;
}
}
}
}
}
}
Password: 10aea594831e0b42b956c578ef9a6d44ee39938dFourth activity
public class Main
{
public static void main(String args[]) throws Exception
{
byte[] b_arr = new byte[] {95,27,-29,-55,-80,-127,-60,13,-33,-60,-96,35,-127,86,0,-114,-25,30,36,-92};
char keyspace[] = "0123456789abcdef".toCharArray();
for (byte b: b_arr)
{
next:
for (char c1: keyspace)
{
for (char c2: keyspace)
{
byte x = (byte)((Character.digit(c1, 16) << 4) + Character.digit(c2, 16));
if (b == x)
{
System.out.print(c1 + "" + c2);
break next;
}
}
}
}
}
}
Password: 5f1be3c9b081c40ddfc4a0238156008ee71e24a4Flag: pc_lo4d_l3tt3r_gl1tch@flare-on.com
#9 - remorse.ino.hex
The challenge consists of a binary for an Arduino Uno in Intel Hex format. The microcontroller board used in the Uno is ATmega328p. This is an 8-bit RISC microprocessor. After consulting the pinout diagram it was evident that the state of Pin D was used as an input.
The 8 bit input from the pin is used as a key to xor a block of bytes. If the decrypted data contained an @ sign at the appropriate position we have found the correct decryption key.
We can easily brute-force the decryption key and thus the flag in python.
The decryption key i.e. the state of Pin D must be 0xdb. We can also emulate the binary in AVR Studio with the XOR key to get the flag.
Flag: no_r3m0rs3@flare-on.com
![]() |
Figure 6: Pinout diagram of the ATmega328. Source: https://www.arduino.cc/en/Hacking/PinMapping168 |
Figure 7: The xor loop in IDA |
li = [0xB5, 0xB5, 0x86, 0xB4, 0xF4, 0xB3, 0xF1, 0xB0, 0xB0,
0xF1, 0xED, 0x80, 0xBB, 0x8F, 0xBF, 0x8D, 0xC6, 0x85, 0x87, 0xC0, 0x94, 0x81, 0x8C]
output = [0] * len(li)
for k in xrange(255):
for i in xrange(len(li)):
v = (li[i]^k)+i
if v > 255:
v -= 256
output[i] = chr(v)
if output[-3] == 'c' and output[-2] == 'o' and output[-1] == 'm':
print 'Xor key=', hex(k)
print ''.join(output)
break
The decryption key i.e. the state of Pin D must be 0xdb. We can also emulate the binary in AVR Studio with the XOR key to get the flag.
Figure 8: Simulating in AVR Studio |
#10 - shell.php
This one required a lot of manual work to go through. The overall idea of this level is to decrypt data encrypted using multi-byte xor in a chained fashion. Suppose the length of the key is n, then the crypto system can be represented by the following image.
P stands for plaintext, K for key and C for the resultant ciphertext. The first part of the plaintext is xored with the key, the remaining plaintext is xored with itself to form the ciphertext.
To break the crypto, the initial step is to find the length of the key. We know that the plaintext was a piece of PHP code. For the first php code, the possible length of the key could vary from 32 to 64 and can only consist of hexadecimal characters 0-9 a-f.
To find the exact key length, I wrote another script which tried all the key lengths from 32 to 64 such that the resultant plain text consists of only printable characters.
Running the code we immediately get the key length to be 64.
After finding the key length, the next step is to get the key. This was done in two steps. First, I wrote a script which showed which calculated which characters were possible at each of the 64 positions. The code for it is included in the same script above. Just switch the comments in main().
We know which characters are possible at each of the 64 positions in the key. Next, I designed an interactive GUI tool in python which allowed me to construct the key letter by letter from the list above. We need to choose a character such that decrypted code is valid PHP. The tool showed the result of the decryption as we gradually built the key. The source code of the tool can be found at github: https://gist.github.com/extremecoders-re/f8258764d38133d435a0f1ae053a1a0d
There are four such sub-levels in this challenge based on the same idea. Solving i.e finding the correct decryption key gives the flag.
Flag: th3_xOr_is_waaaay_too_w34k@flare-on.com
Figure 9: The crypto system |
To break the crypto, the initial step is to find the length of the key. We know that the plaintext was a piece of PHP code. For the first php code, the possible length of the key could vary from 32 to 64 and can only consist of hexadecimal characters 0-9 a-f.
$key = isset($_POST['key']) ? $_POST['key'] : "";
$key = md5($key) . substr(MD5(strrev($key)) , 0, strlen($key));
To find the exact key length, I wrote another script which tried all the key lengths from 32 to 64 such that the resultant plain text consists of only printable characters.
import string
#Put the base64 blob here
encoded = ''
ct = map(ord, list(encoded.decode('base64')))
def is_char_possible(char, pos, keylen):
char = ord(char)
while True:
if pos + keylen >= len(ct):
return True
pos += keylen
char ^= ct[pos]
if chr(char) not in string.printable:
return False
def is_keylen_possible(keylen):
for pos in xrange(0, keylen):
possible = False
# Iterate over all printable characters
for ch in string.printable:
if is_char_possible(ch, pos, keylen):
# Char ch is possible at this position pos
possible = True
break
# No printable char possible at position pos
if not possible:
return False
# All position have possible printable characters
return True
def find_possible_key_lengths():
for keylen in xrange(32,65):
if is_keylen_possible(keylen):
print '[+] Possible key length =', keylen
def find_possible_chars(pos, keylen):
possible = []
for ch in string.printable:
if is_char_possible(ch, pos, keylen):
possible.append(ch)
return possible
def find_possible_keys(pos, keylen):
for pos in xrange(0, 64):
print '[+] Position', pos, find_possible_chars(pos,64)
if __name__ == '__main__':
find_possible_key_lengths()
#find_possible_keys()
Running the code we immediately get the key length to be 64.
Figure 10: Calculating the key length |
After finding the key length, the next step is to get the key. This was done in two steps. First, I wrote a script which showed which calculated which characters were possible at each of the 64 positions. The code for it is included in the same script above. Just switch the comments in main().
$ python findcandidates.py
[+] Position 0 ['#', '$', '%', ' ']
[+] Position 1 ['b', 'c', 'd', 'e', 'g']
[+] Position 2 ['0', '1', '2', '3', '4', '5', '7', '8', '9', '!', '"', '#', '$', '%', '(', ')', '*', '+', ',', ':', ';', '<', '=', '>', '?']
[+] Position 3 ['#', '&', "'", ' ']
[+] Position 4 ['!', '$', '&', "'", ' ']
[+] Position 5 ['1', '2', '3', '5', '6', '7', '8', '9', '!', '"', '#', '$', '&', ')', '*', '+', ',', '-', '.', '/', ':', ';', '=', '>', '?']
[+] Position 6 ['\t', '\r', '\x0b', '\x0c']
[+] Position 7 ['\t', '\n', '\r', '\x0b', '\x0c']
[+] Position 8 ['"', '$', '%', ' ']
[+] Position 9 ['h', 'j', 'k', 'l']
[+] Position 10 ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'k', 'l', 'm', 'o', 'p', 'q', 's', 'u', 'v', 'w', 'z', '{', '|']
[+] Position 11 ['a', 'c', 'd', 'e', 'f', 'i', 'j', 'k', 'l', 'm', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '`', '{', '|', '}', '~']
[+] Position 12 ['!', '&', ' ']
[+] Position 13 [':', '<', '=']
[+] Position 14 ['!', '#', '&', ' ']
[+] Position 15 ['0', '2', '3', '4', '5', '6', '7', '!', '"', '#', '$', '%', '&', "'", '(', ')', '*', '+', ',', '-', '.', '/', ':', ';', '<', '=', '?', ' ']
[+] Position 16 ['0', '2', '4', '5', '6', '7', '8', '9', '!', '"', '#', '&', "'", '(', '+', ',', '-', '/', ';', '=', '>', '?', ' ']
[+] Position 17 ['1', '2', '3', '4', '5', '6', '8', '9', '"', '#', '$', '%', '&', '(', ')', '*', '-', '.', ':', ';', '<', '=', '>', '?']
[+] Position 18 ['\t', '\r', '\x0b', '\x0c']
[+] Position 19 ['\t', '\n', '\r', '\x0b', '\x0c']
[+] Position 20 ['h', 'i', 'm']
[+] Position 21 ['e', 'f', 'g']
[+] Position 22 ['0', '5', '6', '7', '8', '9', '!', '"', '#', '$', '%', '&', '(', ')', ',', '-', '.', '/', ';', '=', '>', '?', ' ']
[+] Position 23 ['(', ')', ',', '.', '/']
[+] Position 24 ['h', 'i', 'j', 'o']
[+] Position 25 ['a', 'b', 'd', 'e', 'f', 'i', 'k', 'l', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '}', '~']
[+] Position 26 ['r', 's', 't', 'u', 'w']
[+] Position 27 ['b', 'd', 'e', 'f']
[+] Position 28 ['p', 't', 'u']
[+] Position 29 ['(', ')', '+', '.']
[+] Position 30 ['0', '1', '2', '4', '5', '8', '9', '!', '"', '#', '$', '%', '&', "'", '*', '+', ',', '-', '.', ':', ';', '<', '=', '?', ' ']
[+] Position 31 ['B', 'C', 'E', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '@', '[', '\\', ']', '^', '_']
[+] Position 32 ['A', 'B', 'F', 'G', 'I', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'W', 'Y', 'Z', '[', '\\', '^', '_']
[+] Position 33 ['A', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'Y', 'Z', '\\', ']', '^', '_']
[+] Position 34 ['R', 'S', 'T', 'U', 'W']
[+] Position 35 ['R', 'S', 'T', 'U', 'W']
[+] Position 36 ['B', 'D', 'F', 'I', 'K', 'L', 'N', 'Q', 'R', 'S', 'T', 'U', 'X', 'Y', 'Z', '@', '[', ']']
[+] Position 37 ['!', '#', '&', "'", ' ']
[+] Position 38 ['h', 'i', 'n', 'o']
[+] Position 39 ['s', 't', 'u', 'X', 'Y', '\\', '^', '_']
[+] Position 40 ['a', 'c', 'd', 'f', 'h', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'r', 's', 't', 'u', 'v', 'x', 'y', '`', '{']
[+] Position 41 ['1', '2', '3', '5', '7', '8', '9', '"', '#', '$', '%', '&', "'", ')', '*', '-', '.', ':', '<', '>']
[+] Position 42 ['Y', '[', '\\', ']']
[+] Position 43 ['(', ')', '.', '/']
[+] Position 44 ['(', ')', '*']
[+] Position 45 ['\t', '\n', '\r', '\x0b', '\x0c']
[+] Position 46 ['\t', '\n', '\x0b', '\x0c']
[+] Position 47 ['0', '1', '2', '5', '7', '8', '9', '!', '#', '$', '%', "'", '(', ')', '*', '+', ',', '-', '.', '/', ';', '<', '=', '?', ' ']
[+] Position 48 ['!', ' ']
[+] Position 49 ['#', '$', '%']
[+] Position 50 ['h', 'j', 'k', 'l', 'm']
[+] Position 51 ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'k', 'l', 'm', 'o', 'p', 'q', 's', 't', 'u', 'v', 'w', 'z', '`', '{', '|', '}', '~']
[+] Position 52 ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'l', 'p', 's', 'u', 'w', 'x', 'y', 'z', '`', '{', '|', '}']
[+] Position 53 ['0', '1', '2', '3', '4', '5', '6', '7', '!', '"', '#', '$', '%', '&', "'", ')', '*', '.', ';', '=', '>', '?', ' ']
[+] Position 54 ['0', '2', '3', '4', '5', '7', '8', '9', '!', '"', '$', '&', "'", '(', ')', '*', '+', ',', '-', ':', '<', '=', '>', '?']
[+] Position 55 ['0', '1', '3', '4', '5', '7', '8', '9', '!', '"', '#', '$', '%', '&', "'", '(', ')', '*', '+', ',', '.', '<', '=', '>', '?', ' ']
[+] Position 56 ['1', '3', '4', '6', '8', '9', '!', '"', '$', '%', '&', "'", '(', '*', '+', '-', '.', '/', ':', ';', '=', ' ']
[+] Position 57 ['X', 'Y', '[', '^', '_']
[+] Position 58 ['P', 'Q', 'V', 'W']
[+] Position 59 ['H', 'I', 'L', 'N', 'O']
[+] Position 60 ['A', 'C', 'E', 'F', 'G', 'K', 'L', 'O', 'P', 'Q', 'R', 'S', 'V', 'W', 'Z', '\\', ']', '^', '_']
[+] Position 61 ['A', 'B', 'C', 'E', 'F', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'T', 'U', 'V', 'W', 'Y', 'Z', '@', '[', '\\', ']', '^']
[+] Position 62 ['Z', '[', '\\', '_']
[+] Position 63 ['$', '&', "'", ' ']
We know which characters are possible at each of the 64 positions in the key. Next, I designed an interactive GUI tool in python which allowed me to construct the key letter by letter from the list above. We need to choose a character such that decrypted code is valid PHP. The tool showed the result of the decryption as we gradually built the key. The source code of the tool can be found at github: https://gist.github.com/extremecoders-re/f8258764d38133d435a0f1ae053a1a0d
![]() |
Figure 11: A tool to decrypt interactively |
Flag: th3_xOr_is_waaaay_too_w34k@flare-on.com
#11 - covefefe.exe
This was definitely the finest challenge of Flare-On 2017. The problem is based on a VM the instruction set of which consists of only a single instruction - subtracting the contents of one memory location from another. A search on Google reveals that this is a Subleq VM. The entire VM is implemented in few lines of code.
With such a small code, I implemented the entire logic in python.
From here, I modified the VM code to print the instruction pointer during each loop. This did not help as the total number of executed instructions were well over 100k. The trace looked like the screenshot below.
With such a huge trace, it was extremely difficult to figure out what was going on. After a bit of trial and errors, I decided to run a taint analysis to track how the input flowed through the code. The VM is essentially an array of integers. The idea was to mark a cell dirty if it was controllable by user input. If there was a comparison it would immediately show through. The VM code was modified to introduce the tainting features.
The taint trace was significantly reduced in length compared to the execution trace.
From the trace, it was clear, that the characters at the odd positions very multiplied by 15 and left shifted seven times. Inspecting the VM bytecode there were a series of numbers which looked to be the result of the calculation on the characters at the odd positions.
Reversing the calculation, the characters at the odd position were found. The remaining characters at the even positions were found using Google and an English dictionary. The regex search feature in Notepad++ really helped here.
Flag: subleq_and_reductio_ad_absurdum@flare-on.com
Figure 12: The decompiled code implementing the VM |
from __future__ import print_function
from vm import code
import random
userinput = None
input_idx = -1
# printf("%c", char)
def printfc(char):
print(chr(char), end='')
# scanf("%c", &returned)
def scanfc():
global userinput, input_idx
if userinput is None:
userinput = raw_input()
input_idx = 0
if userinput == '':
return 0xA
else:
return ord(userinput[input_idx])
else:
input_idx += 1
if input_idx == len(userinput):
input_idx -= 1
return 0xA
else:
return ord(userinput[input_idx])
def dispatch(a, b, c):
code[b] -= code[a]
if c != 0:
return code[b] <= 0
else:
return False
def exec_vm(entry, size):
pc = entry
while pc + 3 <= size:
if dispatch(code[pc], code[pc+1], code[pc+2]):
if code[pc+2] == -1:
return 1
pc = code[pc+2]
else:
pc += 3
if code[4] == 1:
printfc(code[2])
code[4] = 0
code[2] = 0
if code[3] == 1:
code[1] = scanfc()
code[3] = 0
def main():
code[273] = random.randint(0, 32767) % code[272]
exec_vm(1123, 4352)
if __name__ == '__main__':
main()
From here, I modified the VM code to print the instruction pointer during each loop. This did not help as the total number of executed instructions were well over 100k. The trace looked like the screenshot below.
Figure 13: Execution trace of the VM |
from __future__ import print_function
from vm import code
import random
scanf_count = 0
user_input = 'abcdefghijklmnopqrstuvwxyz_0123'[::-1]
tainted = []
track_taints = False
tainted_at_least_once = []
def taint(address, taint_src):
if address not in tainted:
tainted.append(address)
print('[+] Tainted [{}] = {}'.format(address, code[address]))
else:
print('[+] Re-tainted [{}] = {}'.format(address, code[address]))
if address not in tainted_at_least_once:
tainted_at_least_once.append(address)
def is_tainted(address):
return address in tainted
def untaint(address):
if address in tainted:
tainted.remove(address)
print('[-] Untainted [{}]'.format(address))
# printf("%c", char)
def printfc(char):
print(chr(char), end='')
def dispatch(a, b, c):
global track_taints
code[b] -= code[a]
if track_taints:
if a == b and is_tainted(b):
untaint(b)
elif is_tainted(a):
taint(b, a)
elif is_tainted(b):
taint(b, a) # Retaint
if c != 0:
return code[b] <= 0
else:
return False
def exec_vm(entry, size):
global scanf_count, track_taints
pc = entry
while pc + 3 <= size:
if dispatch(code[pc], code[pc+1], code[pc+2]):
if code[pc+2] == -1:
return 1
pc = code[pc+2]
else:
pc += 3
if code[4] == 1:
if track_taints:
track_taints = False
print('[!] Taint tracking OFF')
printfc(code[2])
code[4] = 0
code[2] = 0
if code[3] == 1:
print('[*] Call scanf <<<<<<<<<<<<<<<<<<<<<<<<')
if scanf_count == 0:
print('[!] Taint tracking ON')
track_taints = True
code[1] = ord(user_input[scanf_count])
scanf_count += 1
taint(1, 'external')
elif scanf_count < len(user_input):
code[1] = ord(user_input[scanf_count])
scanf_count += 1
taint(1, 'external')
else:
code[1] = 0xA
code[3] = 0
def main():
global tainted, tainted_at_least_once
code[273] = 0 #random.randint(0, 32767) % code[272]
exec_vm(1123, 4352)
print(tainted)
for t in tainted:
print('[{}] = {}'.format(t, code[t]))
print('Tainted at least once')
print(tainted_at_least_once)
if __name__ == '__main__':
main()
The taint trace was significantly reduced in length compared to the execution trace.
Figure 14: Taint trace |
From the trace, it was clear, that the characters at the odd positions very multiplied by 15 and left shifted seven times. Inspecting the VM bytecode there were a series of numbers which looked to be the result of the calculation on the characters at the odd positions.
Figure 15: A strange sequence of integers! |
Flag: subleq_and_reductio_ad_absurdum@flare-on.com
#12 - [missing]
The last challenge is so convoluted that an entire CTF contest can be made on this. A malware ran on a machine and exfiltrated some files. The task is to reconstruct the exact set of events that occurred based on the network traffic capture. I am not going to describe everything in detail as it will easily cover many blog posts. Instead, I will only focus my approach.
The network topology can be described by the next figure.
The Command & Control communicates with the machine having an IP 192.168.221.91. This is used as a pivot to attack the other system 192.168.221.105. The pcap file which we have been provided contains the network traffic between C&C and the first system.
There are multiple stages in the malware. The first stage (coolprogram.exe) downloads the second stage (srv.exe) and executes it. The second stage is the principal malware. The functionality of this malware is built upon plugins. Plugins can be of three types - cryptography (CRYP), compression (COMP) and command (CMD). The traffic between the malware and its C&C is mostly encrypted except at the start when the CRYP plugins have not yet been loaded. Plugins are DLL files with a modified PE header.
To recreate the exact set of events, we need to replay the network traffic from the PCAP. I wrote a Python script using the pyshark library. Since we were replaying the packets there were no necessity to listen for the responses from the malware. However with this approach the malware freezed after running for some time as the send buffer filled up. To remedy the situation I had to patch ws2_32.send to discard all the packets sent to it.
When the malware is running under the control of the debugger it's possible to intercept the process and dump the buffer containing the plugins from memory. Using another script I corrected the PE header of the dumped DLLs so that IDA and other tools can analyze it.
The second stage running on 192.168.221.91 loads 9 plugins. Each plugin has a unique 16-byte signature. The signature is also present in each of packets to determine which plugin will process that particular data packet. Out of the 9 plugins - 4 deal with crypto, 1 with compression and the remaining are command plugins.
The second stage then uses psexec to copy itself over to the third system at 192.168.221.105 and execute it. One of the command plugin acts as a relay and forwards all traffic between the C&C and the third system in both directions. This third stage loads up 9 more plugins - all of which are relayed by the second system from the C&C.
Thus there are 18 plugins in total out of which 8 deals with cryptography, 2 with compression and remaining are command plugins. The crypto and compression plugins are shown in the table below.
The above plugins implement standard crypto/compression algorithms. Hence I reimplemented them in python.
I have provided the source code of all of the plugins implemented in python.
https://gist.github.com/extremecoders-re/b7caf1a5d2f884733a75dcdc80d8e384
Once the plugins were implemented in Python, decrypting the traffic was simple. The decrypted traffic contained a BMP image without a header. The entire image was split across multiple packets. After assembling them properly and adding the header we get the following image containing a password.
The third stage running on larryjohnson-pc encrypted a file lab10.zip to lab10.zip.cry and exfiltrated it to the server via the stage2 relay. Decrypting the traffic using our plugins and reassembling the pieces we can reconstruct the cry file.
The encryptor named cf.exe is present in the captured traffic. Based on the decompiled C# code of the encryptor we can build a decrypter to get back the zip file.
The decrypted zip file was password protected. The password can be found in the BMP image. Opening the zip there is an x86_64 ELF written in Golang. Running the ELF gives the flag.
Flag: n3v3r_gunna_l3t_you_down_1987_4_ever@flare-on.com
The network topology can be described by the next figure.
Figure 16: The network topology |
There are multiple stages in the malware. The first stage (coolprogram.exe) downloads the second stage (srv.exe) and executes it. The second stage is the principal malware. The functionality of this malware is built upon plugins. Plugins can be of three types - cryptography (CRYP), compression (COMP) and command (CMD). The traffic between the malware and its C&C is mostly encrypted except at the start when the CRYP plugins have not yet been loaded. Plugins are DLL files with a modified PE header.
To recreate the exact set of events, we need to replay the network traffic from the PCAP. I wrote a Python script using the pyshark library. Since we were replaying the packets there were no necessity to listen for the responses from the malware. However with this approach the malware freezed after running for some time as the send buffer filled up. To remedy the situation I had to patch ws2_32.send to discard all the packets sent to it.
import pyshark
import socket
import time
import threading
server_ip = '52.0.104.200'
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('', 9443))
sock.listen(5)
print '[++] Waiting for connection...'
(clientsocket, address) = sock.accept()
print '[++] Accepting connection from', address
cap = pyshark.FileCapture('data-only.pcap')
while idx < 4144:
packet = cap[idx]
data = packet.data.data.decode('hex')
buflen = len(data)
# Data packet from server to client
if packet.ip.src == server_ip:
clientsocket.send(data)
print '[<-] Sent %d bytes from index %d' %(buflen, idx)
time.sleep(0.1)
idx += 1
When the malware is running under the control of the debugger it's possible to intercept the process and dump the buffer containing the plugins from memory. Using another script I corrected the PE header of the dumped DLLs so that IDA and other tools can analyze it.
import sys
import struct
ifile = open(sys.argv[1], 'rb')
ofile = open(sys.argv[2], 'wb')
# MZ header
print '[+] Correcting MZ header'
assert ifile.read(2) == 'LM'
ofile.write('MZ')
# Write up to e_lfanew
ofile.write(ifile.read(0x3c-2))
e_lfanew = struct.unpack('<I', ifile.read(4))[0]
ofile.write(struct.pack('<I', e_lfanew))
ofile.write(ifile.read(e_lfanew - (0x3c + 4)))
# PE header
print '[+] Correcting PE header'
assert ifile.read(4) == 'NOP\0'
ofile.write('PE\0\0')
# Machine
print '[+] Correcting PE.Machine'
assert ifile.read(2) == '32'
ofile.write('\x4c\x01')
ofile.write(ifile.read(0x22))
print '[+] Correcting Address of entrypoint'
# Address of entrypoint
entrypoint = struct.unpack('<I', ifile.read(4))[0] ^ 0xabcdabcd
ofile.write(struct.pack('<I', entrypoint))
ofile.write(ifile.read())
ifile.close()
ofile.close()
print '[+] Done'
The second stage running on 192.168.221.91 loads 9 plugins. Each plugin has a unique 16-byte signature. The signature is also present in each of packets to determine which plugin will process that particular data packet. Out of the 9 plugins - 4 deal with crypto, 1 with compression and the remaining are command plugins.
The second stage then uses psexec to copy itself over to the third system at 192.168.221.105 and execute it. One of the command plugin acts as a relay and forwards all traffic between the C&C and the third system in both directions. This third stage loads up 9 more plugins - all of which are relayed by the second system from the C&C.
Thus there are 18 plugins in total out of which 8 deals with cryptography, 2 with compression and remaining are command plugins. The crypto and compression plugins are shown in the table below.
Algorithm | Type | Key Size | IV Size | Mode | |
---|---|---|---|---|---|
STAGE 2 | RC4 | Crypto | 16 | - | - |
Transposition cipher | Crypto | - | - | - | |
Custom base64 | Crypto | - | - | - | |
XTEA | Crypto | 16 | 8 | CBC | |
ZLIB | Comp | - | - | - | |
STAGE 3 | Blowfish | Crypto | 16 | 8 | CBC |
XOR | Crypto | 4 | - | ECB | |
Triple DES | Crypto | 24 | 8 | CBC | |
Camellia | Crypto | 16 | - | ECB | |
Aplib | Comp | - | - | - |
The above plugins implement standard crypto/compression algorithms. Hence I reimplemented them in python.
I have provided the source code of all of the plugins implemented in python.
https://gist.github.com/extremecoders-re/b7caf1a5d2f884733a75dcdc80d8e384
Once the plugins were implemented in Python, decrypting the traffic was simple. The decrypted traffic contained a BMP image without a header. The entire image was split across multiple packets. After assembling them properly and adding the header we get the following image containing a password.
Figure 17: Decrypted BMP |
The third stage running on larryjohnson-pc encrypted a file lab10.zip to lab10.zip.cry and exfiltrated it to the server via the stage2 relay. Decrypting the traffic using our plugins and reassembling the pieces we can reconstruct the cry file.
The encryptor named cf.exe is present in the captured traffic. Based on the decompiled C# code of the encryptor we can build a decrypter to get back the zip file.
using System;
using System.Text;
using System.IO;
using System.Security.Cryptography;
namespace cf_decrypter
{
class Program
{
static void Main(string[] args)
{
using (FileStream fileStream = File.Open("lab10.zip.cry", FileMode.Open))
{
byte[] signature = new byte[4];
fileStream.Read(signature, 0, 4);
string sign = Encoding.ASCII.GetString(signature);
if (sign.Equals("cryp"))
{
byte[] IV = new byte[16];
// Read IV
fileStream.Read(IV, 0, IV.Length);
byte[] sha256_hash = new byte[32];
//Read SHA256 hash
fileStream.Read(sha256_hash, 0, sha256_hash.Length);
int ciphertext_len = (int)(fileStream.Length - fileStream.Position);
byte[] ciphertext = new byte[ciphertext_len];
// Read cipher text
fileStream.Read(ciphertext, 0, ciphertext_len);
byte[] key = Convert.FromBase64String("tCqlc2+fFiLcuq1ee1eAPOMjxcdijh8z0jrakMA/jxg=");
Aes aes = Aes.Create();
aes.KeySize = 256;
aes.Key = key;
aes.IV = IV;
aes.Padding = PaddingMode.PKCS7;
aes.Mode = CipherMode.CBC;
ICryptoTransform transform = aes.CreateDecryptor();
using (MemoryStream memoryStream = new MemoryStream())
{
using (CryptoStream cryptoStream = new CryptoStream(memoryStream, transform, CryptoStreamMode.Write))
{
cryptoStream.Write(ciphertext, 0, ciphertext.Length);
//cryptoStream.FlushFinalBlock();
File.WriteAllBytes("decrypted", memoryStream.ToArray());
}
}
}
}
}
}
}
The decrypted zip file was password protected. The password can be found in the BMP image. Opening the zip there is an x86_64 ELF written in Golang. Running the ELF gives the flag.
Flag: n3v3r_gunna_l3t_you_down_1987_4_ever@flare-on.com
Final Words
Overall, I feel the challenges this year were harder than the previous years. Challenge #11 and #12 deserve special mention. Challenge #12, in particular, can get very tough and time taking if not approached in the proper way. With this, we come to the end of this blog post. I would like to thank Crystalboy, rand0m, Alexander Polyakov and Peter Huene for their tips.
No comments:
Post a Comment