Skip to content

Commit 8ea5418

Browse files
lukacanPavel Siska
authored andcommitted
QUIC: refactor and checked decryption
1 parent ccae582 commit 8ea5418

File tree

2 files changed

+96
-52
lines changed

2 files changed

+96
-52
lines changed

process/quic.cpp

Lines changed: 92 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ __attribute__((constructor)) static void register_this_plugin()
7272

7373
// Print debug message if debugging is allowed.
7474

75+
7576
#ifdef DEBUG_QUIC
7677
# define DEBUG_MSG(format, ...) fprintf(stderr, format, ## __VA_ARGS__)
7778
#else
@@ -105,11 +106,7 @@ QUICPlugin::QUICPlugin()
105106

106107
memset(decrypted_payload,0,1500);
107108
memset(assembled_payload,0,1500);
108-
//decrypted_payload = nullptr;
109-
decrypt_buffer_len = 0;
110109

111-
//assembled_payload = nullptr;
112-
assemble_buffer_len = 0;
113110

114111
final_payload = nullptr;
115112

@@ -393,7 +390,7 @@ bool QUICPlugin::parse_tls(RecordExtQUIC *rec)
393390
} // QUICPlugin::parse_tls
394391

395392
// --------------------------------------------------------------------------------------------------------------------------------
396-
// DECRYTP HEADER AND PAYLOAD
393+
// DECRYPT HEADER AND PAYLOAD
397394
// --------------------------------------------------------------------------------------------------------------------------------
398395

399396
bool QUICPlugin::expand_label(const char *label_prefix, const char *label, const uint8_t *context_hash,
@@ -749,38 +746,50 @@ bool QUICPlugin::quic_decrypt_header()
749746
}
750747

751748
EVP_CIPHER_CTX_free(ctx);
749+
// basically we create mask, as shown in code belove
752750
memcpy(mask, plaintext, sizeof(mask));
753751

754-
// https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-22#section-5.4.1
752+
// https://www.rfc-editor.org/rfc/rfc9001.html#name-header-protection-applicati
753+
754+
/*
755+
code belove shows a sample algorithm for applying header protection.
756+
757+
mask = header_protection(hp_key, sample)
755758
756-
// if (packet[0] & 0x80) == 0x80:
757-
// # Long header: 4 bits masked
758-
// packet[0] ^= mask[0] & 0x0f
759-
// else:
760-
// # Short header: 5 bits masked
761-
// packet[0] ^= mask[0] & 0x1f
759+
pn_length = (packet[0] & 0x03) + 1
760+
761+
if (packet[0] & 0x80) == 0x80:
762+
# Long header: 4 bits masked
763+
packet[0] ^= mask[0] & 0x0f
764+
else:
765+
# Short header: 5 bits masked
766+
packet[0] ^= mask[0] & 0x1f
767+
768+
*/
762769

770+
771+
763772
// we do not have to handle short header, Initial packets have only long header
764773

765774
first_byte = quic_h1->first_byte;
766775
first_byte ^= mask[0] & 0x0f;
767776
uint8_t pkn_len = (first_byte & 0x03) + 1;
768777

769-
// set decrypted first byte
778+
// set deobfuscated first byte
770779
header[0] = first_byte;
771780

772781

773-
// copy encrypted pkn into buffer
782+
// now we know pkn length, so copy pkn into buffer
774783
memcpy(&full_pkn, pkn, pkn_len);
775784

776785

777-
// decrypt pkn
786+
// we now de-obsfuscate pkn
778787
for (unsigned int i = 0; i < pkn_len; i++) {
779788
packet_number |= (full_pkn[i] ^ mask[1 + i]) << (8 * (pkn_len - 1 - i));
780789
}
781790

782791

783-
// after decrypting first byte, we know packet number length, so we can adjust payload start and lengths
792+
// after de-obfuscating pkn, we know exactly pkn length so we can correctly adjust start of payload
784793
payload = payload + pkn_len;
785794
payload_len = payload_len - pkn_len;
786795

@@ -794,6 +803,8 @@ bool QUICPlugin::quic_decrypt_header()
794803

795804

796805
// adjust nonce for payload decryption
806+
// https://www.rfc-editor.org/rfc/rfc9001.html#name-aead-usage
807+
// The exclusive OR of the padded packet number and the IV forms the AEAD nonce
797808
phton64(nonce + sizeof(nonce) - 8, pntoh64(nonce + sizeof(nonce) - 8) ^ packet_number);
798809

799810
return true;
@@ -841,11 +852,15 @@ bool QUICPlugin::quic_decrypt_payload()
841852
EVP_CIPHER_CTX_free(ctx);
842853
return false;
843854
}
855+
856+
// SET NONCE and KEY
844857
if (!EVP_DecryptInit_ex(ctx, NULL, NULL, initial_secrets.key, nonce)) {
845858
DEBUG_MSG("Payload decryption error, setting KEY and NONCE failed\n");
846859
EVP_CIPHER_CTX_free(ctx);
847860
return false;
848861
}
862+
863+
// SET ASSOCIATED DATA (HEADER with unprotected PKN)
849864
if (!EVP_DecryptUpdate(ctx, NULL, &len, header, header_len)) {
850865
DEBUG_MSG("Payload decryption error, initializing authenticated data failed\n");
851866
EVP_CIPHER_CTX_free(ctx);
@@ -886,12 +901,12 @@ bool QUICPlugin::quic_assemble()
886901
assembled_payload[0] = 0x06;
887902

888903

889-
// compute end of payload
904+
// set end of payload
890905
uint8_t * payload_end = decrypted_payload + payload_len;
891906

892907
uint64_t offset = 0;
893-
uint64_t offset_frame = 0;
894-
uint64_t length = 0;
908+
uint64_t frame_offset = 0;
909+
uint64_t frame_length = 0;
895910

896911

897912
// loop through whole padding, the logic is check first fragment (check type and length), if it`s of type crypto
@@ -904,22 +919,24 @@ bool QUICPlugin::quic_assemble()
904919
// process of computing offset length and length field length, is same as above in extracting user agent
905920
if (*(decrypted_payload + offset) == CRYPTO) {
906921
offset += 1;
907-
offset_frame = quic_get_variable_length(decrypted_payload,offset);
908-
909-
length = quic_get_variable_length(decrypted_payload,offset);
910-
911-
// copy crypto fragment into the buffer based on offset
912-
// + 4 bytes is because of final crypto header (this header technically contains no important information, but we
913-
// need the 4 bytes at the start because of compatibility with function which parse tls)
914-
if (assembled_payload + offset_frame + 4 * sizeof(uint8_t) < assembled_payload + 1500
922+
923+
frame_offset = quic_get_variable_length(decrypted_payload,offset);
924+
frame_length = quic_get_variable_length(decrypted_payload,offset);
925+
926+
// beware this part is tricky, tls parse data expects 4 bytes at the start of crypto frame (type(1), offset(1), length(2))
927+
// BUT google quic uses variable length of some fields (more precisely length can have variable length), we consider that
928+
// length field have 2 bytes, this is not wrong because length is not used in tls parse date.
929+
if (assembled_payload + frame_offset + 4 * sizeof(uint8_t) < assembled_payload + 1500
915930
&& decrypted_payload + offset < payload_end
916-
&& assembled_payload + offset_frame + 4 * sizeof(uint8_t) + length < assembled_payload + 1500)
931+
&& assembled_payload + frame_offset + 4 * sizeof(uint8_t) + frame_length < assembled_payload + 1500)
917932
{
918-
memcpy(assembled_payload + offset_frame + 4 * sizeof(uint8_t), decrypted_payload + offset, length);
919-
offset += length;
933+
memcpy(assembled_payload + frame_offset + 4 * sizeof(uint8_t), decrypted_payload + offset, frame_length);
934+
offset += frame_length;
920935
} else {
921936
return false;
922937
}
938+
// https://www.rfc-editor.org/rfc/rfc9000.html#name-frames-and-frame-types
939+
// only those frames can occure in initial packets
923940
} else if (*(decrypted_payload + offset) == PADDING
924941
|| *(decrypted_payload + offset) == PING
925942
|| *(decrypted_payload + offset) == ACK1
@@ -938,13 +955,22 @@ bool QUICPlugin::quic_assemble()
938955

939956
bool QUICPlugin::quic_parse_data(const Packet &pkt)
940957
{
958+
959+
941960
uint8_t *tmp_pointer = (uint8_t *) pkt.payload;
942961
uint64_t offset = 0;
943962
const uint8_t *payload_end = (uint8_t *) pkt.payload + pkt.payload_len;
944963

964+
965+
966+
// set header pointer to the start of header
945967
header = (uint8_t *) (tmp_pointer + offset); // set header pointer
946968

947-
quic_h1 = (quic_header1 *) (tmp_pointer + offset); // read first byte, version and dcid length
969+
970+
971+
972+
// pointer to the first byte, version and dcid length
973+
quic_h1 = (quic_header1 *) (tmp_pointer + offset);
948974

949975

950976
if (quic_h1->version == 0x0) {
@@ -953,75 +979,91 @@ bool QUICPlugin::quic_parse_data(const Packet &pkt)
953979

954980
offset += sizeof(quic_header1);
955981

982+
983+
956984
if ((tmp_pointer + offset) > payload_end) {
957985
return false;
958986
}
959987

988+
989+
990+
// if dcid length is not zero , read dcid
960991
if (quic_h1->dcid_len != 0) {
961-
dcid = (tmp_pointer + offset); // set dcid if dcid length is not 0
992+
dcid = (tmp_pointer + offset);
993+
offset += quic_h1->dcid_len;
962994
}
963-
offset += quic_h1->dcid_len;
964995

965-
quic_h2 = (quic_header2 *) (tmp_pointer + offset); // read scid length
996+
if ((tmp_pointer + offset) > payload_end) {
997+
return false;
998+
}
999+
1000+
1001+
// after dcid is scid length, so read scid length
1002+
quic_h2 = (quic_header2 *) (tmp_pointer + offset);
9661003

9671004
offset += sizeof(quic_header2);
9681005

9691006
if ((tmp_pointer + offset) > payload_end) {
9701007
return false;
9711008
}
9721009

973-
if (quic_h2->scid_len != 0) { // set scid if scid length is not 0
1010+
1011+
// if scid length is not zero, read scid
1012+
if (quic_h2->scid_len != 0) {
9741013
scid = (tmp_pointer + offset);
1014+
offset += quic_h2->scid_len;
9751015
}
9761016

977-
offset += quic_h2->scid_len;
978-
9791017
if ((tmp_pointer + offset) > payload_end) {
9801018
return false;
9811019
}
9821020

9831021

984-
// token length has variable length based on first two bits, so we cant use structure
1022+
// token length has variable length based on first two bits, after this offset should point to the token
9851023
uint64_t token_length = quic_get_variable_length(tmp_pointer,offset);
9861024

9871025
if ((tmp_pointer + offset) > payload_end) {
9881026
return false;
9891027
}
9901028

1029+
// after this offset should point after the token
9911030
offset += token_length;
9921031

9931032
if ((tmp_pointer + offset) > payload_end) {
9941033
return false;
9951034
}
9961035

1036+
1037+
// same as token length, payload length has variable length, after this offset should point to the packet number
9971038
payload_len = quic_get_variable_length(tmp_pointer,offset);
9981039

9991040
if ((tmp_pointer + offset) > payload_end) {
10001041
return false;
10011042
}
10021043

1003-
pkn = (tmp_pointer + offset); // set packet number
10041044

1005-
payload = (tmp_pointer + offset); // set payload start too, this pointer is adjusted later, because we do not know exact packet number length atm
1045+
// read packet number
1046+
pkn = (tmp_pointer + offset);
10061047

1007-
offset += sizeof(uint8_t) * 4; // skip packet number and go to sample start which is always after packet number(always assuming length of packet number == 4).
10081048

1009-
sample = (tmp_pointer + offset); // set sample pointer
1049+
// read payload, we do not know packet number length, so payload will be adjusted later (after de-obfuscating header)
1050+
payload = (tmp_pointer + offset);
1051+
1052+
1053+
// read sample, sample is always assuming that packet number has length 4 bytes, so we do not need to know exact pkn length for reading sample.
1054+
offset += sizeof(uint8_t) * 4;
1055+
sample = (tmp_pointer + offset);
10101056

10111057
if ((tmp_pointer + offset) > payload_end) {
10121058
return false;
10131059
}
10141060

10151061

10161062
/* DO NOT SET header length this way , if packet contains more frames , pkt.payload_len is length of whole quic packet (so it contains length of all frames inside packet)
1017-
* so then header length is not computed correctly. Instead of this approach calculate header length after decrypting packet number , this will ensure header length is computed correctly
1063+
* so then header length is not computed correctly. Instead of this approach calculate header length after de-obfuscating packet number , this will ensure header length is computed correctly
10181064
*/
10191065
// header_len = pkt.payload_len - payload_len;
10201066

1021-
1022-
if (payload_len > pkt.payload_len) {
1023-
return false;
1024-
}
10251067
return true;
10261068
} // QUICPlugin::quic_parse_data
10271069

@@ -1074,7 +1116,7 @@ bool QUICPlugin::process_quic(RecordExtQUIC *quic_data, const Packet &pkt)
10741116
return true;
10751117
}
10761118
} else if (pkt.src_port == 443) {
1077-
if (!quic_create_initial_secrets(CommSide::SERVER_IN,quic_data)) {
1119+
/*if (!quic_create_initial_secrets(CommSide::SERVER_IN,quic_data)) {
10781120
DEBUG_MSG("Error, creation of initial secrets failed (server side)\n");
10791121
return false;
10801122
}
@@ -1098,7 +1140,8 @@ bool QUICPlugin::process_quic(RecordExtQUIC *quic_data, const Packet &pkt)
10981140
}
10991141
else {
11001142
return true;
1101-
}
1143+
}*/
1144+
return true;
11021145
}
11031146
return false;
11041147
} // QUICPlugin::process_quic

process/quic.hpp

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,8 @@ typedef struct __attribute__((packed)) quic_header2 {
135135
// contains scid_len (which is 0 in context of Client Hello packet) but from server side, header contains SCID so SCID length is not 0
136136
} quic_header2;
137137

138+
139+
138140
struct __attribute__((packed)) tls_rec_lay {
139141
uint8_t type;
140142
uint8_t offset;
@@ -272,6 +274,7 @@ class QUICPlugin : public ProcessPlugin
272274
uint8_t quic_hp[quic_hp_hkdf];
273275
uint8_t client_In_Buffer[quic_clientIn_hkdf];
274276
uint8_t server_In_Buffer[quic_serverIn_hkdf];
277+
uint8_t nonce[TLS13_AEAD_NONCE_LENGTH] = { 0 };
275278

276279

277280
// important pointers into QUIC packet, used in decryption process
@@ -293,10 +296,8 @@ class QUICPlugin : public ProcessPlugin
293296
uint8_t assembled_payload[1500];
294297
uint8_t *final_payload;
295298

296-
uint64_t decrypt_buffer_len;
297-
uint64_t assemble_buffer_len;
298299

299-
uint8_t nonce[TLS13_AEAD_NONCE_LENGTH] = { 0 };
300+
300301

301302
// counter
302303
int parsed_initial;

0 commit comments

Comments
 (0)