Intro
This year I solved all the challenges of Flare-On 2024. Due to my unemployment, I had time to focus on those challenges. (If you’re hiring please email me mustafa at mustafadur.com) I spend so much time and money(challenge 9) to solve those challenges. I wrote a very short blog post for my solutions. My solutions are not very straightforward or pretty. You may check the official site for both the solutions and binaries.
I uploaded my notes and solution scripts to my GitHub repo
1 frog
I actually never ran this challenge to solve it. I opened the frog.py
file and saw a function named GenerateFlagText
. I searched how this function is used inside the file and found the following reference:
if player.x == victory_tile.x and player.y == victory_tile.y:
victory_mode = True
flag_text = GenerateFlagText(player.x, player.y)
So our x and y positions must be equal to victorytile coordinates. When we search this reference we find it as `victorytile = pygame.Vector2(10, 10)In order to invoke
GenerateFlagText` coordinates must be 10,10 So running the below scrip shows us the flag
def GenerateFlagText(x, y):
key = x + y*20
encoded = "\xa5\xb7\xbe\xb1\xbd\xbf\xb7\x8d\xa6\xbd\x8d\xe3\xe3\x92\xb4\xbe\xb3\xa0\xb7\xff\xbd\xbc\xfc\xb1\xbd\xbf"
return ''.join([chr(ord(c) ^ key) for c in encoded])
flag = GenerateFlagText(10,10)
print(flag)
welcome_to_11@flare-on.com
2 checksum
When the application starts it creates a random number between 3 and 8. This is the number of questions it is going to ask. For each iteration, two random numbers are generated and it asks for the result of the equation. Example:
Check sum: 7594 + 2828 = 10422
Good math!!!
------------------------------
Check sum: 9020 + 199 = 9219
Good math!!!
------------------------------
Check sum: 7741 + 5813 = 13554
Good math!!!
------------------------------
Check sum: 3657 + 5065 = 8722
Good math!!!
------------------------------
Check sum: 3201 + 1808 = 5009
Good math!!!
------------------------------
Checksum:
At this point it expects us to write a hex string since we see encoding_hex_Decode
function. Writing a random hex string gives us Maybe it's time to analyze the binary! ;)
error message.
We also see that this hex string must be 32 bytes when it was decoded. It then initializes chacha20poly1305_xchacha20poly1305
class. Then tries to decrypt main_encryptedFlagData
with checksum as both key and nonce. The size of this buffer is 0x2C52C.
After decoding, it calculates the SHA256 of the decoded main_encryptedFlagData
Then it compares this hash to our checksum. We have a dilemma here. Without the correct key we can’t decrypt the main_encryptedFlagData
we can’t brute-force the SHA256 hash too. However, when we analyze further we find another function named main_a
we see that it xors our checksum value with FlareOn2024
and creates a base64 encoded string, and then compares this with cQoFRQErX1YAVw1zVQdFUSxfAQNRBXUNAxBSe15QCVRVJ1pQEwd/WFBUAlElCFBFUnlaB1ULByRdBEFdfVtWVA==
To find our correct checksum following script will be enough
import base64
def decode_xor(encoded, key):
decoded = base64.b64decode(encoded)
return bytes([b ^ key[i % len(key)] for i, b in enumerate(decoded)])
encoded_string = 'cQoFRQErX1YAVw1zVQdFUSxfAQNRBXUNAxBSe15QCVRVJ1pQEwd/WFBUAlElCFBFUnlaB1ULByRdBEFdfVtWVA=='
key = b"FlareOn2024"
result = decode_xor(encoded_string, key)
print(result.decode('utf-8', errors='replace'))
Our checksum must be 7fd7dd1d0e959f74c133c13abb740b9faa61ab06bd0ecd177645e93b1e3825dd
When we use this value, the program extracts the flag and saves it as REAL_FLAREON_FLAG.JPG
to the path which is the result of os_UserCacheDir
When we open this picture we get our flag
Th3_M4tH_Do_b3_mAth1ng@flare-on.com
3 aray
We only get a single file named aray.yara
file. We need to create a file that should trigger this YARA rule. I didn’t solve this in a pretty way lol. I wrote a very basic parser that extracts the bytes of the flag for basic math operations. Such as
uint32 ( 52 ) ^ 425706662 == 1495724241
I combined those values to partially solve the string as
1R####DayK33p$Malw4r##w4y@##are-o##com
Then I manually tested the rest of the chars to extract the flag as
1RuleADayK33p$Malw4r3Aw4y@flare-on.com
4 Meme Maker 3000
There is a single html file which has obfuscated JavaScript file. You can use any tool such as obfuscator-io-deobfuscator to deobfuscate the JavaScript file. However, I prefer manually decoding the script using jsdom. You can check my solutions repo to see how I load the raw javascript and just call the functions manually.
wh0a_it5_4_cru3l_j4va5cr1p7@flare-on.com
5 sshd
The challenge has only one file called ssh_container.tar
When we list the files inside this tar file according to the last modified date we see the following list
./root/flag.txt - Last Modified: 2024-09-11 23:55:59
./var/lib/systemd/coredump/sshd.core.93794.0.0.11.1725917676 - Last Modified: 2024-09-10 00:34:36
./usr/lib/x86_64-linux-gnu/liblzma.so.5.4.1 - Last Modified: 2024-09-10 00:34:33
Of course flag.txt doesn’t have the flag. We need to check the next file which is a coredump of sshd process.
We load the coredump via gdb.
root@6a86c0a426d0:/# gdb sshd /var/lib/systemd/coredump/sshd.core.93794.0.0.11.1725917676
When we issue the bt
command we can see the backtrace as follows.
#0 0x0000000000000000 in ?? ()
(gdb) bt
#0 0x0000000000000000 in ?? ()
#1 0x00007f4a18c8f88f in ?? () from /lib/x86_64-linux-gnu/liblzma.so.5
#2 0x000055b46c7867c0 in ?? ()
#3 0x000055b46c73f9d7 in ?? ()
#4 0x000055b46c73ff80 in ?? ()
#5 0x000055b46c71376b in ?? ()
#6 0x000055b46c715f36 in ?? ()
#7 0x000055b46c7199e0 in ?? ()
#8 0x000055b46c6ec10c in ?? ()
#9 0x00007f4a18e5824a in __libc_start_call_main (main=main@entry=0x55b46c6e7d50, argc=argc@entry=4,
argv=argv@entry=0x7ffcc6602eb8) at ../sysdeps/nptl/libc_start_call_main.h:58
#10 0x00007f4a18e58305 in __libc_start_main_impl (main=0x55b46c6e7d50, argc=4, argv=0x7ffcc6602eb8,
init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7ffcc6602ea8)
at ../csu/libc-start.c:360
#11 0x000055b46c6ec621 in ?? ()
It seems the sshd process crashed due to a function at 0x00007f4a18c8f88f
inside liblzma.so.5
file. This file is symlinked to liblzma.so.5.4.1
Let’s check where it crashed
(gdb) info sharedlibrary
0x00007f4a18c8ad40 0x00007f4a18ca8d26 Yes (*) /lib/x86_64-linux-gnu/liblzma.so.5
The above address is somewhere inside the text segment. In order to find out the base address of this we can check the mappings
(gdb) info proc mappings
0x7f4a18c86000 0x7f4a18c8a000 0x4000 0x0 / (deleted)
0x7f4a18c8a000 0x7f4a18ca9000 0x1f000 0x4000 / (deleted)
0x7f4a18ca9000 0x7f4a18cb7000 0xe000 0x23000 / (deleted)
We can’t see the module name but since the 0x00007f4a18c8ad40
address is inside the 0x7f4a18c86000 0x7f4a18c8a000
range we can conclude that our base address is 0x7f4a18c86000
We can open the file in Ghidra and set the base address to 0x7f4a18c86000
via Window -> Memory map -> Set Image Base.
After we load our file to disassembler rebase the binary and jump to our crash address 0x00007f4a18c8f88f
we see that it is crashing due to the dsylm function can’t find a function named RSA_public_decrypt
with extra space. If we check the crossreference, we can see that this function is used like below
void _INIT_1(void)
{
int iVar1;
long in_FS_OFFSET;
void *local_18;
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
local_18 = (void *)0x0;
iVar1 = FUN_7f4a18c8eb10(&local_18,_strlen,_strlen + 0x10);
if (iVar1 == 0) {
FUN_7f4a18c8f1b0(local_18,"RSA_public_decrypt",FUN_7f4a18c8f820,0);
if (local_18 != (void *)0x0) {
free(local_18);
}
}
if (local_10 == *(long *)(in_FS_OFFSET + 0x28)) {
return;
}
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
If we check the strings referenced by FUN_7f4a18c8eb10
such as failed to find DT_STRTAB
, we can find out that this function is used by plthook_elf.c
to hook functions. So our function at the 0x7f4a18c8f820
address is a hooked function of RSA_public_decrypt
. If we apply the function signature we get the following disassembly.
int RSA_public_decrypt_hook(int flen,void *from,uchar *to,RSA *rsa,int padding)
{
__uid_t _Var1;
int iVar2;
code *pcVar3;
void *__dest;
char *pcVar4;
long in_FS_OFFSET;
undefined local_108 [200];
long local_40;
local_40 = *(long *)(in_FS_OFFSET + 0x28);
_Var1 = getuid();
pcVar4 = "RSA_public_decrypt";
if (_Var1 == 0) {
/* WARNING: Load size is inaccurate */
if (*from == -0x3abf85b8) {
FUN_7f4a18c8f3f0(local_108,(long)from + 4,(long)from + 0x24,0);
__dest = mmap((void *)0x0,(long)DAT_7f4a18cb8360,7,0x22,-1,0);
pcVar3 = (code *)memcpy(__dest,&DAT_7f4a18ca9960,(long)DAT_7f4a18cb8360);
FUN_7f4a18c8f520(local_108,pcVar3,(long)DAT_7f4a18cb8360);
(*pcVar3)();
FUN_7f4a18c8f3f0(local_108,(long)from + 4,(long)from + 0x24,0);
FUN_7f4a18c8f520(local_108,pcVar3,(long)DAT_7f4a18cb8360);
}
pcVar4 = "RSA_public_decrypt ";
}
pcVar3 = (code *)dlsym(0,pcVar4);
iVar2 = (*pcVar3)(flen,from,to,rsa,padding);
if (local_40 == *(long *)(in_FS_OFFSET + 0x28)) {
return iVar2;
}
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
This hook is only applied if the from parameter is equal to 0xc5407a48
I don’t know why ghidra is showing as -0x3abf85b8
We must search these magic bytes 0x48, 0x7A, 0x40, 0xC5 inside the coredump to find the parameters of this function. This magic byte is shown in 4 places and like below
487a40c5943df638a81813e2d.....
FUN_7f4a18c8f3f0(local_108,(long)from + 4,(long)from + 0x24,0);
From the above disassembly, we can see that the first 4 bytes are skipped and the next 0x20(32) bytes are used as key for ChaCha and after the key, at 0x24 we have the nonce.
key = “943df638a81813e2de6318a507f9a0ba2dbb8a7ba63666d08d11a65ec914d66f” nonce = “f236839f4dcd711a52862955”
I used Unicorn to decrypt the buffer. After decryption, I created new binary and changed the bytes at 0x7f4a18ca9960
with decrypted ones. When we disassemble it, we see that this connects to 10.0.2.15
at port 13337
and receives 4 things from the server. Which are;
- Key 0x20 bytes
- Nonce 0xC bytes
- File name size
- Filename
Then it reads these 0x80 bytes from this file, encrypts it, and sends it to this server. We need to go back to our coredump file and find a suspicious file that could be used in this operation. We find the following data in the memory
00007FFCC6600BE8 db 8Dh, 0ECh, 91h, 12h, 0EBh, 76h, 0Eh, 0DAh, 7Ch, 7Dh
00007FFCC6600BF2 db 87h, 0A4h, 43h, 27h, 1Ch, 35h, 0D9h, 0E0h, 0CBh, 87h
00007FFCC6600BFC db 89h, 93h, 0B4h, 0D9h, 4, 0AEh, 0F9h, 34h, 0FAh, 21h
00007FFCC6600C06 db 66h, 0D7h
00007FFCC6600C08 db 11h, 11h, 11h, 11h, 11h, 11h, 11h, 11h, 11h, 11h, 11h
00007FFCC6600C13 db 11h
00007FFCC6600C14 dd 20h
00007FFCC6600C18 db '/root/certificate_authority_signing_key.txt',0
According to this, our key is 8DEC9112EB760EDA7C7D87A443271C35D9E0CB878993B4D904AEF934FA2166D7
and our nonce is 111111111111111111111111
However, encryption is not the same as before. Therefore, we need to again emulate this function. At this point, I lifted the functions from the binary and ran it manually which gave me the following flag.
supp1y_cha1n_sund4y@flare-on.com
6 bloke2
This challenge was easier than I thought. When we check the files we see a following line
localparam TEST_VAL = 512'h3c9cf0addf2e45ef548b011f736cc99144bdfee0d69df4090c8a39c520e18ec3bdc1277aad1706f756affca41178dac066e4beb8ab7dd2d1402c4d624aaabe40;
This looks like part of the flag. It is being used like below.
h <= h_in ^ (TEST_VAL & {(W*16){tst}});
The result h depends on the value of tst. We see that it is wired as tst <= finish;
If we change tst
as 1 tst <= 1'b1;
and run make test
we get our flag
please_send_help_i_am_trapped_in_a_ctf_flag_factory@flare-on.com
7 fullspeed
This file is .NET file compiled as AOT. It is hard to disassemble this file without the signatures. I saw some references to Bouncy Castle and ECC(Elliptic Curve Cryptography) in the binary. However, it was hard to understand what was going on.
I used this blog post to create a signature and applied to binary.
This program uses ECC to share information. Parameters of ECC are being xored with 133713371337133713371337133713371337133713371337133713371337133713371337133713371337133713371337
during key exchange. We have the following parameters for the ECC.
p = 0xc90102faa48f18b5eac1f76bb40a1b9fb0d841712bbe3e5576a7a56976c2baeca47809765283aa078583e1e65172a3fd
a = 0xa079db08ea2470350c182487b50f7707dd46a58a1d160ff79297dcc9bfad6cfc96a81c4a97564118a40331fe0fc1327f
b = 0x9f939c02a7bd7fc263a4cce416f4c575f28d0c1315c4f0c282fca6709a5f9f7f9c251c9eede9eb1baa31602167fa5380
gx = 0x087b5fe3ae6dcfb0e074b40f6208c8f6de4f4f0679d6933796d3b9bd659704fb85452f041fff14cf0e9aa7e45544f9d8
gy = 0x127425c1d330ed537663e87459eaa1b1b53edfe305f6a79b184b3180033aab190eb9aa003e02e9dbf6d593c5e3b08182
px = 0x195b46a760ed5a425dadcab37945867056d3e1a50124fffab78651193cea7758d4d590bed4f5f62d4a291270f1dcf499
py = 0x357731edebf0745d081033a668b58aaa51fa0b4fc02cd64c7e8668a016f0ec1317fcac24d8ec9f3e75167077561e2a15
The order is almost smooth.
35809 * 46027 * 56369 * 57301 * 65063 * 111659 * 113111 *
7072010737074051173701300310820071551428959987622994965153676442076542799542912293
We can leave out the last big prime factor and apply Pohlig-Hellman algorithm to recover the private key. I used the sage script to recover the private key.
The script calculates the shared secret as
3C54F90F4D2CC9C0B62DF2866C2B4F0C5AFAE8136D2A1E76D2694999624325F5609C50B4677EFA21A37664B50CEC92C0
The server and client then calculate the SHA256 of this secret.
b48f8fa4c856d496acdecd16d9c94cc6b01aa1c0065b023be97afdd12156f3dc3fd480978485d8183c090203b6d384c20e853e1f20f88d1c5e0f86f16e6ca5b2
The server and client use part of this hash for key and nonce.
key = bytes.fromhex("B48F8FA4C856D496ACDECD16D9C94CC6B01AA1C0065B023BE97AFDD12156F3DC")
nonce = bytes.fromhex("3FD480978485D818")
If we decrypt the traffic as a single stream we get our flag as base64 encoded string. Decodindg this string gives us the flag.
D0nt_U5e_y0ur_Own_CuRv3s@flare-on.com
8 clearlyfake
This challenge was mostly a guesswork. After you decrypt the javascript, we get the address of the smart contract 0x9223f0630c598a200f99c5d4746531d10319a569
We need to analyze the bytecode. I used the decompiler at dedaub.com
to further analyze it. If the parameter of the function is giV3_M3_p4yL04d!
we get the next address which is 0x5324EAB94b236D4d1456Edc574363B113CEbf09d
By using the block number 43152014
and smart contract address, we can further analyze the state of the contract at address https://testnet.bscscan.com/tx/0x05660d13d9d92bc1fc54fb44c738b7c9892841efc9df4b295e2b7fda79756c47#statechange
Base64 decoding this string gives us another PowerShell script. I decoded this script with PowerDecode Decoded script looks like below
$testnet_endpoint = "ENDPOINT"
$_body = '{"method":"eth_call","params":[{"to":"$address","data":"0x5c880fcb"},
BLOCK],"id":1,"jsonrpc":"2.0"}'
$resp = (Invoke-RestMethod -Uri $testnet_endpoint -Method 'Post' -Body $_body -ContentType
"application/json").result
# Remove the '0x' prefix from the response
Set-Variable -Name hexNumber -Value ($resp -replace '0x', '')
# Convert from hex to bytes
Set-Variable -Name bytes0 -Value (0..($hexNumber.Length / 2 - 1) | ForEach-Object {
Set-Variable -Name startIndex -Value ($_ * 2)
[Convert]::ToByte($hexNumber.Substring($startIndex, 2), 16)
})
# Convert bytes to string and extract part of it
Set-Variable -Name bytes1 -Value ([System.Text.Encoding]::UTF8.GetString($bytes0))
Set-Variable -Name bytes2 -Value ($bytes1.Substring(64, 188))
# Convert extracted string from base64 to bytes
Set-Variable -Name bytesFromBase64 -Value ([Convert]::FromBase64String($bytes2))
Set-Variable -Name resultAscii -Value ([System.Text.Encoding]::UTF8.GetString($bytesFromBase64))
# Convert ASCII result to hex bytes
Set-Variable -Name hexBytes -Value ($resultAscii | ForEach-Object {
'{0:X2}' -f $_ # Format each byte as two-digit hex
})
# Join hex bytes into a single string
Set-Variable -Name hexString -Value ($hexBytes -join ' ')
# Remove spaces from hex string
Set-Variable -Name hexBytes -Value ($hexBytes -replace " ", "")
# Convert hex back to bytes
Set-Variable -Name bytes3 -Value (0..($hexBytes.Length / 2 - 1) | ForEach-Object {
Set-Variable -Name startIndex -Value ($_ * 2)
[Convert]::ToByte($hexBytes.Substring($startIndex, 2), 16)
})
# Convert the resulting bytes back to a string
Set-Variable -Name bytes5 -Value ([System.Text.Encoding]::UTF8.GetString($bytes3))
# Convert the key "FLAREON24" to bytes
Set-Variable -Name keyBytes -Value ([System.Text.Encoding]::ASCII.GetBytes("FLAREON24"))
# Perform the XOR operation
Set-Variable -Name resultBytes -Value (@())
for (Set-Variable -Name i -Value (0); $i -lt $bytes5.Length; $i++) {
Set-Variable -Name resultBytes -Value ($resultBytes + ($bytes5[$i] -bxor $keyBytes[$i % $keyBytes.Length]))
}
# Convert the result back to an ASCII string
Set-Variable -Name resultString -Value ([System.Text.Encoding]::ASCII.GetString($resultBytes))
# Prepare the command for extracting a tar file
Set-Variable -Name command -Value ("tar -x --use-compress-program 'cmd /c echo $resultString > C:\\flag' -f C:\\flag")
# Execute the command
Invoke-Expression $command
This is basically base64 decodes a string and then converts the result from hex to string and finally xor the result with FLAREON24
key. By combining the state values at https://testnet.bscscan.com/tx/0xae4711c6e9d6d8f5d00a88e1adb35595bc7d7a73130e87356e3e71e65e17f337#statechange
we get the following string NDEgM2EgN2EgN2IgM2MgN2MgM2QgNGEgNTAgNGUgNWUgNzYgNDQgNTUgNjcgMTEgNTAgNWUgNjYgMTUgM2EgNTUgM2YgMTcgM2MgM2QgNTEgMTUgNjEgNTUgNTkgNDEgNmQgMzkgNGUgNDIgNjMgNmIgN2MgNDEgMjIgNjUgNjAgMGEgNmMgNjUgNjM=
Base64 decoding and xoring with FLAREON24
gives us the flag as
N0t_3v3n_DPRK_i5_Th15_1337_1n_Web3@flare-on.com
9 serpentine
This is it. This was the hardest challenge of Flareon-2024. I spent almost a month and only partially solved it and brute-forced a couple of bytes. I even bought a mini PC since I couldn’t trace the binary on my Macbook M3 pro machine via VM. This challenge cost me both time and money lol. Since I didn’t solve this one fully, I wouldn’t write my partial solution here. However, you can find the unicorn scripts I used to emulate famous UNWIND_INFO and other stuff.
10 CatbertRansomware
After spending so much time on serpentine, I had only 2 days left to solve this challenge. This challenge is a UEFI challenge. You can run the challenge via the below command:
qemu-system-x86_64 -drive format=raw,file=disk.img -biosbios.bin
We see a cute cat logo which says that our files are encrypted. To analyze this firmware, we need to find the module responsible for decryption. You can use UEFITool to extract the module. Make sure you click Show all assets
link and download UEFITool. After you download the tool open the bios.bin
and click Action/Search/Text
and type decrypt
Click one of the search results and right click PE32 Image and select extract body. The address of the decrypt function is 31BC4
. If checks whether the file header has C4TB
and tries to decrypt the files with the given password. The length of the password is 16 bytes(32 in Unicode). Before using the password, it checks the validity of a password by using the file as input and custom VM handler at address 31274
. The structure of the file is something like below
magic db "C4TB"
len dd 0
start dd 0
decrypt_len dd 0
If the key passes the checks of this VM, qword_168ED0
is set to 1. At this point, I didn’t have much time to write a decompiler for this custom VM. Instead, I lifted the VM logic to custom binary and wrote a script to solve it via angr. The first two files were decrypted very quickly but the last one took 1.5 hours. Running my script gives us the cute result
🐾🔍 Investigating meowstery catmeme1.jpg.c4tb
🐈 Found a paw-sible solution: DaCubicleLife101
🐾🔍 Investigating meowstery catmeme2.jpg.c4tb
🐈 Found a paw-sible solution: G3tDaJ0bD0neM4te
🐾🔍 Investigating meowstery catmeme3.jpg.c4tb
🐈 Found a paw-sible solution: VerYDumBpassword
Decrypting all those files one by one will give us the following partial flag.
th3_ro4d_t0_succ3ss_1s_alw4ys_
You need to decrypt files one by one in a single session. I made a mistake not once but twice to close the VM lol. After decrypting the files, it says:
0x10 bytes successfully written to your_mind.jpg.c4tb.
You thought you were clever, huh?
Thought you'd find your precious answer here? Well, tough luck.
You were almost right, but not quite.
I've left another little surprise for you on disk.
Your reward is the password: 'BrainNumbFromVm!'. Enjoy your headache, human.
And remember, I'm always watching.
When we decrypt the file via
decrupt_file your_mind. jpg.c4tb BrainNumbFromVm!
We get an ASCII string _und3r_c0nstructi0n
as the remaining part of the flag. Combining them gives us the final flag.
th3_ro4d_t0_succ3ss_1s_alw4ys_und3r_c0nstructi0n@flare-on.com