(Reverse - FCSC 2024) PTSD (both parts)
TL;DR: Reverse of a custom network protocol based on a Elliptic Curve Diffie-Hellman (ECDH) key exchange with AES-GCM encryption + exploitation of an integer overflow in the implementation of the protocol to impersonate a client with a replay attack.
First part: PTSD - Init
Description (in French): Vous vous êtes porté volontaire pour mener une évaluation de PTSD : Protocole Très Sécurisé de transmission de Données. Ce protocole est utilisé par un serveur pour vérifier l’état des clients associés. Dans un premier temps, vous devez établir un canal de communication sécurisé avec le serveur en tant que client n°6. Le flag de l’épreuve sera envoyé lorsque le canal de communication sera établi. Note : les librairies libcrypto.so.3 et libssl.so.3 sont fournies mais ne sont pas à analyser dans le cadre de cette épreuve.
Approximate translation: You volunteered to evaluate PTSD: Protocole Très Sécurisé de transmission de Données (Very Secure Protocol for Data Transmission). This protocol is used by a server to check the status of the associated clients. In the first place, you have to establish a secure communication channel with the server as the client number 6. The flag of the challenge will be sent when the communication channel will be established. Note: the libraries libcrypto.so.3 and libssl.so.3 are provided but they are out-of-scope of this challenge.
Introduction
For this challenge, we are given:
- a file
keys.db
containing the following:05:0040:12121212121212121212121212121212 06:FFFF:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
- two files containing fake flags:
lv1.flag
andlv2.flag
(the second one will be for the second part of the challenge), - the libraries
libcrypto.so.3
andlibssl.so.3
, - a file
records.txt
for the second part of the challenge containing a capture of some exchanges between client 5 and the server:SEND: 0500010441046A3D5BB1422D914E7F9EADEDAD0F2AFBE0F8CCCD41D97AE89DE40E081C7CF623A528F770985E319B63805C59B267DC67F9712CF7284A6E496F2BBD185F58ED26 RECV: 050002020101 RECV: 050003044104CE16547B40B5CDBE7B4211C371B09F3B09C5BB4811CDFB3A787941B5B1E1B487A461928EB2167B7233EA4E61E0F51F580ADF080F7B709F7C0BB857A5B8801654 SEND: 050004020101 SEND: 05000509BF451252B5BC306E4E7B7BD192D7C261BC0C9C73A8F25C87DF50B0DE08F564A77A57BADA34 RECV: 050006020101 RECV: 05000709780E623D3DFA61E9565F58A991D331CC8024D1BB213B34E5BA99AA8D08FAF6AFA9E18935C4 SEND: 050008020101 SEND: 050009118226534CF1B81165981130FDBD44CF60ED046B7116D6836B65DF6CE246E0C63E6A4B748224F4958838A8852480864E4E12D5307A18BD1BDAD20800127E927C5B63FE579B47B399A5EFBA09B97ABD060AC330ADC18CE826388A2FD03087621C7B370948 RECV: 05000A033A16B0CF2CA0DFA9C3F856365BC41DE3DFAB62E8FCA6D51A69226A2901FA SEND: 05000B2153F7BC6C0614B8EF4732B055D833658E08CC29B89602BC9E9589498008F73F71CD5A4173AC RECV: 05000C036C4D63FC7F5FE25094D84F86157D7F1783EEAD297538B97C9F1C31BC0194 SEND: 05000D4168EA4C0EFBC5B6CB5113BF2BDE37386615AD65D77AD90992DDA8881803F621A4 RECV: 05000E03D5263BBB06616AF317B444049C2EAFA93DF4B11358A9D9E9A8612BBB01E4 RECV: 05000F81EA7140C5616837D0698F733A801FC4CB08180703E17AE332FB4997F4105FEA343BCB979948100C387DF2A1A070 SEND: 05001003A0BD2E66B65B053DC12AE07667FC6BF930A54279F487FF29438ADE2F01F7
- the remote binary
server
.
When we run the binary, the server performs some internal operations, sends us a packet, and waits for an answer.
INFO -- release -- --- PTSD Server v1.54 ---
INFO -- release -- Loading clients...
INFO -- release -- Initializing secure channels...
INFO -- release -- Secure channel already done for 5
SEND: 06000104410498125B036A337AB3EE6DAC6FDE487C00546281FE65605298CCF41F1873E690C30B8A79C63C0F5EE3D359AA306D6DFCBB7D461C4584B3A3A4ADD0BAC9E4522B61
RECV:
Let’s dive in the code of the binary to understand what happens.
After decompilation and variable renaming, we can identify the main parts of the program.
The main
function performs in an endless loop various operations depending on the status of the server.
__int64 __fastcall main(int a1, char **a2, char **a3, __int64 a4, __int64 a5, __int64 a6)
{
__int64 p_num_keys; // rsi
void **v7; // rdi
__int64 v8; // rdx
__int64 v9; // rcx
__int64 v10; // r8
__int64 v11; // r9
unsigned int num_keys; // [rsp+8h] [rbp-18h] BYREF
unsigned int status; // [rsp+Ch] [rbp-14h]
void *keys[2]; // [rsp+10h] [rbp-10h] BYREF
keys[1] = (void *)__readfsqword(0x28u);
keys[0] = 0LL;
num_keys = 0;
status = 2;
p_num_keys = (__int64)"--- PTSD Server v1.54 ---";
v7 = (void **)"release";
log("release", "--- PTSD Server v1.54 ---", (__int64)a3, a4, a5, a6);
while ( status > 1 )
{
switch ( status )
{
case 2u:
log("release", "Loading clients...", v8, v9, v10, v11);
p_num_keys = (__int64)&num_keys;
v7 = keys;
status = load_clients((__int64)keys, (__int64)&num_keys);
break;
case 3u:
log("release", "Initializing secure channels...", v8, v9, v10, v11);
p_num_keys = num_keys;
v7 = (void **)keys[0];
status = init_channels((__int64)keys[0], num_keys);
break;
case 4u:
log("release", "Checking clients health...", v8, v9, v10, v11);
p_num_keys = num_keys;
v7 = (void **)keys[0];
status = get_health((__int64)keys[0], num_keys);
break;
case 5u:
log("release", "Getting clients info...", v8, v9, v10, v11);
p_num_keys = num_keys;
v7 = (void **)keys[0];
status = get_info((__int64)keys[0], num_keys);
break;
case 6u:
status = attack_detected((__int64)v7, p_num_keys, v8, v9, v10, v11);
break;
default:
status = 0;
break;
}
}
if ( keys[0] )
{
free(keys[0]);
keys[0] = 0LL;
}
return 0LL;
}
We can identify four steps: loading clients (status 2), establishing secure channels (status 3), checking clients health (status 4) and getting clients info (status 5). Status 6 should not be reached. Other statuses stop the server.
Status 2 - Loading clients
When the server “loads the clients”, it basically reads the file keys.db
and parses it. This file contains, for each client, a line
NN:XXXX:YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
where NN is the client number. We will see later what correspond to XXXX and YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY.
__int64 __fastcall load_clients(void **keys, int *num_keys)
{
if ( !keys || (unsigned __int8)load_keys(keys, num_keys) != 1 )
return 1LL;
else
return 3LL;
}
Upon success, this function returns 3 so the server will go forward to the next step.
Status 3 - Establishing secure channels
Interesting things start as the server establish communication channels with the clients.
__int64 __fastcall init_channels(__int64 keys, int num_keys)
{
[...]
if ( gen_keypair((__int64)&id_server) && retrieve_pub_server(id_server, &pub_server, &len_pub_server) == 1 )
{
for ( i = 0; i < num_keys && *(_BYTE *)(41LL * i + keys) != 0xFF; ++i )
{
if ( !memcmp((const void *)(41LL * i + keys + 8), &str_0xfffffff, 0x10uLL) )
{
*(_WORD *)(41LL * i + keys + 3) = 0;
if ( (unsigned __int8)send_pub(41LL * i + keys, pub_server, len_pub_server) != 1
|| recv_ack(keys + 41LL * i, &packet) != 1
|| *((_BYTE *)packet + 6) != 1 )
{
goto LABEL_42;
}
if ( packet )
{
free(packet);
packet = 0LL;
}
if ( recv_pub(keys + 41LL * i, &packet) != 1
|| send_ack(41LL * i + keys, 1) != 1
|| !retrieve_pub_client((__int64)packet + 6, *((_BYTE *)packet + 5), (__int64)&pub_client)
|| derive_skey(id_server, pub_client, &skey) != 1 )
{
goto LABEL_42;
}
v5 = (_QWORD *)(41LL * i + keys + 8);
v6 = *(_QWORD *)((char *)skey + 9);
*v5 = *(_QWORD *)((char *)skey + 1);
v5[1] = v6;
if ( packet )
{
free(packet);
packet = 0LL;
}
if ( send_heloehlo(41LL * i + keys) != 1
|| recv_ack(keys + 41LL * i, &packet) != 1
|| *((_BYTE *)packet + 6) != 1 )
{
goto LABEL_42;
}
if ( packet )
{
free(packet);
packet = 0LL;
}
if ( recv_msg(keys + 41LL * i, &packet) != 1 || send_ack(41LL * i + keys, 1) != 1 )
goto LABEL_42;
if ( packet )
{
free(packet);
packet = 0LL;
}
if ( read_flag1((void **)a2) != 1
|| send_flag(41LL * i + keys, a2[0]) != 1
|| recv_encrypted_ack(keys + 41LL * i, &packet) != 1
|| *((_BYTE *)packet + 6) != 1 )
{
goto LABEL_42;
}
if ( packet )
{
free(packet);
packet = 0LL;
}
log(
"release",
"Succeeded in mounting the secure channel with %d",
*(unsigned __int8 *)(41LL * i + keys),
v7,
v8,
v9);
if ( skey )
{
free(skey);
skey = 0LL;
}
if ( a2[0] )
{
free(a2[0]);
a2[0] = 0LL;
}
if ( pub_client )
{
EVP_PKEY_free(pub_client);
pub_client = 0LL;
}
}
else
{
log("release", "Secure channel already done for %d", *(unsigned __int8 *)(41LL * i + keys), v2, v3, v4);
}
}
new_status = 4;
}
LABEL_42:
[some cleaning...]
return new_status;
}
First of all, for each key k
, the server checks if memory at k + 8
is equal to 16 bytes 0xFF. Actually, k + 8
corresponds to the field
“YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY” from the file keys.db
. If this
field is equal to FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, it means that a
channel must be established between the server and the client. The
client 5 have already been initialized (that is why we have a message
“Secure channel already done for 5” when running the server) but the
channel has to be established for client 6.
Format of the packets
But how are the exchanged packets formatted?
If we take a look at the file records.txt
, we can guess for the
first packets that:
- The packets all have a header containing:
- the client number N,
- a kind of “packet counter” C,
- a “packet type” T,
- the length of the transmitted data L.
For instance, the first packet is
05 0001 04 41 046A32D[...]ED26 [N][ C ][T][L][ data ]
- Some packets just carry the byte “01”, they are probably acknowledgement (ACK) packets.
This can be verified looking at the code of the functions recv_*
and
send_*
which we will dissect later. Moreover, for each client, the
“packet counter” is stored at key + 3
, which is loaded from the
“XXXX” parts of the file keys.db
.
Elliptic Curve Diffie-Hellman (ECDH) key agreement
The first exchanged messages consist in a basic Elliptic Curve Diffie-Hellman (ECDH) key agreement. To sum up:
- The server generates a private/public key pair (functions
gen_keypair
andretrieve_pub_server
) - The server sends its public key and waits for an ACK from the client
(functions
send_pub
andrecv_ack
) - The client sends back its public key to the server, the server
replies with an ACK (functions
recv_pub
andsend_ack
) - Both derive the same shared secret: the server derives the secret
from its private key and the public key of the client, and the
client derives it from its private key and the public key of the
server (server-side functions
retrieve_pub_client
andderive_skey
). - They both derive the same AES key to exchange encrypted messages
(function
derive_skey
).
If we look at the code of the function gen_keypair
called at the
beginning, we can know that the elliptic curve used is SECP256R1
,
also known as prime256v1
, which corresponds to nid = 415
in
EVP_PKEY_CTX_set_ec_paramgen_curve_nid
according to the openssl source code.
v4 = EVP_PKEY_CTX_new_id(408LL, 0LL);
if ( v4 )
{
if ( (unsigned int)EVP_PKEY_paramgen_init(v4) == 1
&& (unsigned int)EVP_PKEY_CTX_set_ec_paramgen_curve_nid(v4, 415LL) == 1
&& (unsigned int)EVP_PKEY_paramgen(v4, &v3) == 1 )
{
v5 = EVP_PKEY_CTX_new(v3, 0LL);
if ( v5 )
{
if ( (unsigned int)EVP_PKEY_keygen_init(v5) == 1 )
v2 = (unsigned int)EVP_PKEY_keygen() == 1;
}
}
}
The shared secret is basically a point on the elliptic curve, represented as bytes. The AES key is generated from the 16 first bytes of the SHA-1 of this secret: see the function derive_skey
:
__int8 __fastcall derive_skey(__int64 id_server, __int64 pub_client, void **a3)
{
[...]
ctx = EVP_PKEY_CTX_new(id_server, 0LL);
if ( ctx )
{
if ( (unsigned int)EVP_PKEY_derive_init(ctx) == 1
&& (unsigned int)EVP_PKEY_derive_set_peer(ctx, pub_client) == 1
&& (unsigned int)EVP_PKEY_derive(ctx, 0LL, &v6) == 1 )
{
*(_BYTE *)ptr = v6;
ptr = realloc(ptr, *(unsigned __int8 *)ptr + 1LL);
if ( ptr )
{
if ( (unsigned int)EVP_PKEY_derive(ctx, (char *)ptr + 1, &v6) == 1 )
{
*(_BYTE *)ptr = v6;
if ( SHA1((char *)ptr + 1, *(unsigned __int8 *)ptr, src) )
{
*(_BYTE *)ptr = 16;
ptr = realloc(ptr, *(unsigned __int8 *)ptr + 1LL);
if ( ptr )
{
memcpy((char *)ptr + 1, src, *(unsigned __int8 *)ptr);
*a3 = ptr;
v5 = 1;
}
}
}
}
}
}
[...]
return v5
}
Encrypted messages
After the key agreement phase, both parts send messages encrypted with AES-GCM using the shared key. AES-GCM (Galois/Counter mode) is an operation mode with authentication: the encryption takes as input a random nonce, the plain message and public authentication data (AD). It returns the ciphertext along with a tag which depends on the ciphertext, the AD and the key. On decryption, the tag is checked with respect to ciphertext and AD and an error is triggered if it isn’t the case: it means that AD or ciphertext has been tampered with. It theoretically guarantees the integrity of authentication data, since a man-in-the-middle attacker who does not know the key cannot forge a valid tag for corrupted AD or ciphertext.
The encrypted packets are formatted a bit differently: instead of the length and the data, we have:
- the nonce used for encryption
- the authentication tag
- the length of the ciphertext
- the ciphertext.
For example,
05000509BF451252B5BC306E4E7B7BD192D7C261BC0C9C73A8F25C87DF50B0DE 08 F564A77A57BADA34
[header][ nonce ][ tag ][LL][ data ]
But where is the authentication data here?
If we take a look at the function that sends encrypted messages, we can figure out that it is actually the packet counter:
if ( packet
&& (unsigned __int8)alloc_copy(packet + 2, 2, &ptr) == 1
&& (unsigned __int8)aes_encrypt(key + 8, packet + 5, (unsigned __int8 *)ptr, aes) == 1 )
The counter, stored at &packet + 2
, is copied into a new location
ptr
, and used as authentication data in aes_encrypt
(first
EVP_EncryptUpdate
):
__int64 __fastcall aes_encrypt(__int64 skey, unsigned __int8 *a2, unsigned __int8 *a3, void **a4)
{
[...]
nonce = malloc(*a2 + 29LL);
if ( nonce )
{
ctx = EVP_CIPHER_CTX_new();
if ( ctx )
{
v4 = EVP_aes_128_gcm();
if ( (unsigned int)EVP_EncryptInit_ex(ctx, v4, 0LL, 0LL, 0LL) == 1
&& (unsigned int)RAND_bytes(nonce, 12LL) == 1
&& (unsigned int)EVP_EncryptInit_ex(ctx, 0LL, 0LL, skey, nonce) == 1
&& (unsigned int)EVP_EncryptUpdate(ctx, 0LL, &v9, a3 + 1, *a3) == 1
&& (unsigned int)EVP_EncryptUpdate(ctx, (char *)nonce + 29, &v9, a2 + 1, *a2) == 1
&& *a2 <= v9 )
{
*((_BYTE *)nonce + 28) = v9;
if ( (unsigned int)EVP_EncryptFinal_ex(ctx, (char *)nonce + v9 + 29, &v9) == 1
&& *a2 <= *((unsigned __int8 *)nonce + 28) + v9 )
{
*((_BYTE *)nonce + 28) += v9;
if ( (unsigned int)EVP_CIPHER_CTX_ctrl(ctx, 16LL, 16LL, (char *)nonce + 12) == 1 )
{
*a4 = nonce;
v8 = 1;
}
}
}
}
}
[...]
return v8;
}
Establishment of the communication channel
After sharing the AES key, the communication channel is established as follows:
- The server sends the message “HELOEHLO” (sic), the client replies
with an ACK (functions
send_heloehlo
andrecv_ack
) - The client sends back any encrypted message with a valid tag
(function
recv_msg
). - If server-side decryption succeeds, the server replies with an ACK and the first flag is sent to the client (encrypted).
From now it is quite straightforward to get the flag of the first part: the goal is to implement a PTSD client.
The flag!
I implemented my client in Python + pwntools, with the libraries ec
and AESGCM
from cryptography.hazmat.primitives
to work with
elliptic curves and AES-GCM encryption.
It just consists in following the protocol: here is the main part of
my script. The full script is available
here. The functions send_msg
and recv_msg
respectively send and receive encrypted messages with the provided AES
key.
io = start()
# Get raw public key from server
io.recvuntil(b'SEND: ')
io.recv(10)
raw_pub_server = bytes.fromhex(io.recvuntil(b'\n')[:-1].decode('utf-8'))
pwn.debug("Server public key: %s" % raw_pub_server.hex())
# Convert it to an ec public key object
x_server = int.from_bytes(raw_pub_server[1:33], 'big')
y_server = int.from_bytes(raw_pub_server[33:65], 'big')
vals = ec.EllipticCurvePublicNumbers(x_server, y_server, ec.SECP256R1())
pub_server = vals.public_key()
# Generate a client private/public key pair
id_client = ec.generate_private_key(ec.SECP256R1())
x = id_client.public_key().public_numbers().x
y = id_client.public_key().public_numbers().y
pub_client = '04' + hex(x)[2:].rjust(64, '0') + hex(y)[2:].rjust(64, '0')
# Send it back to the server and derive locally the AES key
send_ack(io, 6, 2)
io.sendlineafter(b'RECV:\n', b'0600030441' + pub_client.upper().encode('utf-8')
shared_key = id_client.exchange(ec.ECDH(), pub_server)
aes_key = hashlib.sha1(shared_key).digest()[:16]
pwn.success("Derived AES key: %s" % aes_key.hex())
# Receive 'HELOHELO' and send back a dummy encrypted message
recv_ack(io)
recv_msg(io, aes_key, 5) # HELOEHLO
send_ack(io, 6, 6)
send_msg(io, 6, 9, aes_key, b'spikeroot', 7)
# Receive the flag!
recv_ack(io)
recv_msg(io, aes_key, 9) # flag 1
io.close()
FLAG:
FCSC{13c68c00895af039603fdaeefe36e3c5c4d6a40e76a256193c24391fa76f92d7}
Second part: PTSD - Encore une fois (PTSD - One more time)
Description (in French): Pendant que vous étiez en train de
confectionner votre client, le commanditaire vous envoie les traces de
l’initialisation et au-delà du client n°5 (records.txt
). Votre chef
de projet vous propose de rechercher une vulnérabilité dans le
protocole. Vous mettez en place une attaque de l’homme du milieu pour
vous faire passer pour le client n°5. Votre objectif est donc de vous
faire passer pour ce client en envoyant ses informations au
serveur. Attention, le temps de réponse du client n°5 est de 10
secondes.
Approximate translation: While you were developing your client,
the sponsor sends you a log of the initialization of the client 5, as
well as a few following packets (records.txt
). Your boss suggests
you to find a vulnerability in the protocol. You set up a
man-in-the-middle attack to impersonate the client number 5. Your
goal is to impersonate this client by sending their information to the
server. Warning, the response delay of client 5 is 10 seconds.
Status 4 - Checking clients health
After establishing successfully the communication channels, the
function init_channels
(status 3) returns 4. Thus, the server gets
forward to status 4: checking clients health.
__int64 __fastcall get_health(__int64 a1, int a2)
{
unsigned int v3; // [rsp+18h] [rbp-18h]
int i; // [rsp+1Ch] [rbp-14h]
void *ptr[2]; // [rsp+20h] [rbp-10h] BYREF
ptr[1] = (void *)__readfsqword(0x28u);
v3 = 4;
ptr[0] = 0LL;
if ( a1 )
{
for ( i = 0; i < a2 && *(_BYTE *)(41LL * i + a1) != 0xFF; ++i )
{
if ( (unsigned __int8)send_pullpull(41LL * i + a1) != 1 )
goto LABEL_12;
alarm(0xAu);
if ( (unsigned __int8)recv_encrypted_ack(a1 + 41LL * i, ptr) != 1 || *((_BYTE *)ptr[0] + 6) != 1 )
goto LABEL_12;
if ( ptr[0] )
{
free(ptr[0]);
ptr[0] = 0LL;
}
alarm(0);
}
v3 = 5;
}
LABEL_12:
alarm(0);
if ( ptr[0] )
{
free(ptr[0]);
ptr[0] = 0LL;
}
return v3;
}
During this phase, for each registered client, the server sends the
encrypted message “PULLPULL” (function send_pullpull
) and waits up
to 10 seconds for the client to send back an encrypted ACK (namely, an
encrypted message containing the byte “01”). After checking each
client’s health, the server resumes to status 5.
What prevents us from answering to client 5 health check is that we do not know the AES key for client 5. Indeed, we cannot respond to the server with valid encrypted messages. We can check for classical crypto vulnerabilities, such as a nonce reuse, but this direction do not leads anywhere here. However, the description states “Your goal is to impersonate this client by sending their information to the server.” This clearly suggests us to set up a replay attack with the intercepted packets.
When the server receives a packet, it checks that the value of the
packet counter in the packet is greater than the current server-side
counter value. Since the packet counter of the client 5 is equal to
0x40 according to keys.db
, we cannot simply send back the logged
packets from records.txt
, because their packet counter is
lower. This should in theory prevent replay attacks.
if ( *(_WORD *)(key + 3) < *((_WORD *)packet + 1) )
{
*(_WORD *)(key + 3) = *((_WORD *)packet + 1);
[...]
}
However, there is no upper bound on the counter received from the client, and the counter is updated to the value sent by the client. For example, if the current server-side counter is 42,
- If the client sends a packet with counter value 41, the reception fails
- If the client sends a packet with counter value 43, the reception succeeds and the new counter value is 43
- If the client sends a packet with counter value 666, the reception succeeds and the new counter value is 666
Moreover, when the server sends a message, it simply increments the current counter value (which is stored on 2 bytes). The server never performs any checks for integer overflow. Thus, if we send a packet with counter value FFFF, the next counter value from the server will be 0000! As a result, we will be able to replay intercepted packets since their counter value are obviously greater than 0. Thus, we can bypass authentication from AES-GCM and impersonate client 5.
Last but not least, if the decryption of the received ACK fails
(recv_encrypted_ack
returns a value not equal to 1), the server do
not stop, but instead the status remains equal to 4 and the phase
“Getting health clients” is executed again. As a result, we can reply
with invalid messages, the server will keep sending us “PULLPULL”
messages.
The attack scenario will then be the following:
- We receive a PULLPULL message from the server, we ignore it
- We send an ACK with a dummy nonce/tag and with counter value FFFF. The decryption will fail, but the server will continue sending packets…
- The server sends a PULLPULL message with a counter equal to 0000
- We send back the first intercepted packet:
05000C036C4D63FC7F5FE25094D84F86157D7F1783EEAD297538B97C9F1C31BC0194
. We managed to impersonate client 5.
Finally, we must not forget to answer the health check as client 6 too, since the server iterates on all the clients. This is not a problem since we know the AES key for client 6.
Status 5 - Getting clients info
__int64 __fastcall get_info(__int64 a1, int a2)
{
unsigned int v3; // [rsp+18h] [rbp-28h]
int i; // [rsp+1Ch] [rbp-24h]
void *ptr; // [rsp+20h] [rbp-20h] BYREF
void *v6; // [rsp+28h] [rbp-18h] BYREF
__int64 v7; // [rsp+30h] [rbp-10h]
unsigned __int64 v8; // [rsp+38h] [rbp-8h]
v8 = __readfsqword(0x28u);
v3 = 4;
ptr = 0LL;
v6 = 0LL;
v7 = 0LL;
for ( i = 0; i < a2 && *(_BYTE *)(41LL * i + a1) != 0xFF; ++i )
{
if ( *(_BYTE *)(41LL * i + a1) == 6 )
v7 = 41LL * i + a1;
if ( (unsigned __int8)send_0xface(41LL * i + a1, 0xFACE, 16) != 1
|| (unsigned __int8)recv_encrypted_ack(a1 + 41LL * i, &ptr) != 1
|| *((_BYTE *)ptr + 6) != 1 )
{
goto LABEL_27;
}
if ( ptr )
{
free(ptr);
ptr = 0LL;
}
if ( (unsigned __int8)recv_even(a1 + 41LL * i, &ptr) != 1
|| (unsigned __int8)send_encrypted_ack(41LL * i + a1, 1LL) != 1 )
{
goto LABEL_27;
}
if ( (unsigned __int8)check_even((unsigned __int8 *)ptr + 5) != 1 )
{
v3 = 6;
goto LABEL_27;
}
if ( ptr )
{
free(ptr);
ptr = 0LL;
}
if ( v6 )
{
free(v6);
v6 = 0LL;
}
}
if ( (unsigned __int8)read_flag2(&v6) == 1
&& (unsigned __int8)send_flag(v7, (unsigned __int8 *)v6) == 1
&& (unsigned __int8)recv_encrypted_ack(v7, &ptr) == 1
&& *((_BYTE *)ptr + 6) == 1 )
{
if ( ptr )
{
free(ptr);
ptr = 0LL;
}
v3 = 0;
}
LABEL_27:
[...]
return v3;
}
In this last phase, for each registered client:
- The server sends the (encrypted) bytes
\xce\xfa\x10
, the client answers with an encrypted ACK (functionssend_0xface
andrecv_encrypted_ack
) - The client sends back an encrypted message such that the plaintext
only contains even bytes (why not?). If decryption succeeds and the
client message indeed contains only even bytes (functions
recv_even
andsend_encrypted_ack
), the server replies with an encrypted ACK. Otherwise, it gets to status 6, which just prints the messages “Event raised: system under attacks” and “Action: active protection measures activated ‘pew pew pew’” :-) - Finally, the server sends the second flag to the client
The attack can be continued straightforwardly since we have
successfully tampered with the packet counter: in records.txt
, we
have all the packets exchanged with client 5 up to the moment the
second flag was sent.
But… the flag will be sent encrypted with AES key for client 5… how do I do…
It is not a problem: after successfully getting info for client 5, the server will get info for client 6. We can send back encrypted ACKs and even bytes without any difficulty since we know the AES key for client 6. And finally, the flag will be sent encrypted with the key for client 6. It’s a win!
The second part of the script (full script available here):
# Send dummy packet to trigger integer overflow on the packet counter
recv_encrypted_msg(io) # PULLPULL (encrypted)
io.sendlineafter(b'RECV:\n', b'05FFFF03' + b'00'*28 + b'0100')
# Replay response from records.txt
recv_encrypted_msg(io) # PULLPULL (encrypted)
io.sendlineafter(b'RECV:\n', b'05000C036C4D63FC7F5FE25094D84F86157D7F1783EEAD297538B97C9F1C31BC0194')
# Answer the health check for client 6
recv_msg(io, aes_key, 11) # PULLPULL
send_encrypted_ack(io, 6, aes_key, 12)
# Replay responses from records.txt
io.sendlineafter(b'RECV:\n', b'05000E03D5263BBB06616AF317B444049C2EAFA93DF4B11358A9D9E9A8612BBB01E4')
io.sendlineafter(b'RECV:\n', b'05000F81EA7140C5616837D0698F733A801FC4CB08180703E17AE332FB4997F4105FEA343BCB979948100C387DF2A1A070')
io.recvuntil(b'\n') # Encrypted flag for client 5
# Send back correct information for client 6
recv_msg(io, aes_key, 13)
send_encrypted_ack(io, 6, aes_key, 14)
send_msg(io, 6, 0x81, aes_key, b'\x20', 15)
# Receive the flag! (again)
recv_ack(io)
recv_msg(io, aes_key, 17) # flag 2
FLAG:
FCSC{e22bf8e131d553b7c80a2358f2d52b424d6b349581c893f6904806fe5b379ed6}