
FreeRDP â Ă«shtĂ« njĂ« implementim i hapur i Protokollit tĂ« Desktopit tĂ« LargĂ«t (RDP), njĂ« protokoll qĂ« mundĂ«son menaxhimin e largĂ«t tĂ« kompjuterĂ«ve, zhvilluar nga kompania Microsoft. Projekti mbĂ«shtet shumĂ« platforma, pĂ«rfshirĂ« Windows, Linux, macOS dhe madje edhe iOS me Android. Ky projekt Ă«shtĂ« zgjedhur si i pari nĂ« ciklin e artikujve qĂ« kushtohen kontrollit tĂ« klientĂ«ve RDP me ndihmĂ«n e analizatorit statik PVS-Studio.
Pak histori
Projekt u shfaq pasi Microsoft publikoi specifikimet e protokollit të saj pronësor RDP. Në atë kohë ekzistonte klienti rdesktop, implementimi i të cilit bazohej në rezultatet e inxhinierisë së kundërt.
GjatĂ« realizimit tĂ« protokollit, u bĂ« mĂ« e vĂ«shtirĂ« tĂ« shtohej funksionalitet i ri pĂ«r shkak tĂ« arkitekturĂ«s ekzistuese tĂ« projektit. Ndryshimet nĂ« tĂ« shkaktuan njĂ« konflikt midis zhvilluesve, çka çoi nĂ« krijimin e fork-ut rdesktop â FreeRDP. RrĂ«fimi mĂ« tej i produktit ishte i kufizuar nga licenca GPLv2, pĂ«r kĂ«tĂ« arsye u mor vendimi pĂ«r ri-licencimin nĂ«n Apache License v2. MegjithatĂ«, jo tĂ« gjithĂ« ishin dakord tĂ« ndĂ«rronin licencĂ«n e kodit tĂ« tyre, prandaj zhvilluesit vendosĂ«n ta ridizajnojnĂ« projektin, çka rezultoi nĂ« pamjen moderne tĂ« bazĂ«s sĂ« kodit.
Më shumë rreth historisë së projektit mund të lexoni në shënimin e blogut zyrtar: «Historia e projektit FreeRDP».
Si një mjet për identifikimin e gabimeve dhe potencialeve të cenueshmërisë në kod, është përdorur . Ky është një analizues statik i kodit për gjuhët C, C++, C# dhe Java, i disponueshëm në platformat Windows, Linux dhe macOS.
Në këtë artikull paraqiten vetëm ato gabime që më dukeshin më interesante.
Memorie leaking
Funksioni u mbyll pa liruar treguesin âcwdâ. NjĂ« shpĂ«rthim memorie Ă«shtĂ« i mundshĂ«m. environment.c 84
DWORD GetCurrentDirectoryA(DWORD nBufferLength, LPSTR lpBuffer)
{
char* cwd;
....
cwd = getcwd(NULL, 0);
....
if (lpBuffer == NULL)
{
free(cwd);
return 0;
}
if ((length + 1) > nBufferLength)
{
free(cwd);
return (DWORD) (length + 1);
}
memcpy(lpBuffer, cwd, length + 1);
return length;
....
}Kyç këtu është marrë nga sistemi winpr, që zbaton një paketë WINAPI për sistemet jo-Windows, domethënë, është një ekvivalent i lehtë i Wine. Këtu mund të vërehet një rrjedhje: memoria e alokuar nga funksioni getcwd, çlirohet vetëm kur trajtohen raste speciale. Për të eliminuar gabimin, duhet të shtoni thirrjen free pas memcpy.
Shkëputje jashtë kufijve të array
ĂshtĂ« e mundur njĂ« rrjedhje nga array. Vlera e indekse âevent->EventHandlerCountâ mund tĂ« arrijĂ« 32. PubSub.c 117
#define MAX_EVENT_HANDLERS 32
struct _wEventType
{
....
int EventHandlerCount;
pEventHandler EventHandlers[MAX_EVENT_HANDLERS];
};
int PubSub_Subscribe(wPubSub* pubSub, const char* EventName,
pEventHandler EventHandler)
{
....
if (event->EventHandlerCount <= MAX_EVENT_HANDLERS)
{
event->EventHandlers[event->EventHandlerCount] = EventHandler;
event->EventHandlerCount++;
}
....
}Në këtë shembull, një element i ri shtohet në listë, edhe nëse numri i elementeve ka arritur maksimumin. Mjafton të zëvendësoni operatorin <= në <, për të mos kaluar kufijtë e array.
Një gabim tjetër i këtij tipi u gjet:
- V557 ĂshtĂ« e mundur njĂ« rrjedhje nga array. Vlera e indekse âiBitmapFormatâ mund tĂ« arrijĂ« 8. orders.c 2623
Gabime shkrimi
Fragmenti 1
Shprehja â!pipe->Inâ Ă«shtĂ« gjithmonĂ« false. MessagePipe.c 63
wMessagePipe* MessagePipe_New()
{
....
pipe->In = MessageQueue_New(NULL);
if (!pipe->In)
goto error_in;
pipe->Out = MessageQueue_New(NULL);
if (!pipe->In) // <=
goto error_out;
....
}Këtu shohim një gabim të zakonshëm: në kushtin e dytë kontrollohet e njëjta variabël si në të parin. Probabiliteti është që gabimi ndodhi si rezultat i kopjimit të dështëpër të kodit.
Fragmenti 2
U gjetën dy blloqe identike teksti. Blloku i dytë fillon nga linja 771. tsg.c 770
typedef struct _TSG_PACKET_VERSIONCAPS
{
....
UINT16 majorVersion;
UINT16 minorVersion;
....
} TSG_PACKET_VERSIONCAPS, *PTSG_PACKET_VERSIONCAPS;
static BOOL TsProxyCreateTunnelReadResponse(....)
{
....
PTSG_PACKET_VERSIONCAPS versionCaps = NULL;
....
/* MajorVersion (2 bytes) */
Stream_Read_UINT16(pdu->s, versionCaps->majorVersion);
/* MinorVersion (2 bytes) */
Stream_Read_UINT16(pdu->s, versionCaps->majorVersion);
....
}Një tjetër gabim: komentari i kodit sugjeron se nga rrjedha duhet të vijë minorVersion, megjithatë leximi ndodh në një variabël me emrin majorVersion. Megjithatë, nuk jam i njohur me protokollin, kështu që kjo është thjesht një supozim.
Fragmenti 3
ĂshtĂ« çuditshĂ«m qĂ« trupi i funksionit âtrio_index_lastâ Ă«shtĂ« plotĂ«sisht ekuivalent me trupin e funksionit âtrio_indexâ. triostr.c 933
/**
Find first occurrence of a character in a string.
....
*/
TRIO_PUBLIC_STRING char *
trio_index
TRIO_ARGS2((string, character),
TRIO_CONST char *string,
int character)
{
assert(string);
return strchr(string, character);
}
/**
Find last occurrence of a character in a string.
....
*/
TRIO_PUBLIC_STRING char *
trio_index_last
TRIO_ARGS2((string, character),
TRIO_CONST char *string,
int character)
{
assert(string);
return strchr(string, character);
}Sipas komentit, funksioni trio_index gjen shkallĂ«n e parĂ« tĂ« pĂ«rputhjes sĂ« karakterit nĂ« varg, ndĂ«rsa trio_index_last â e fundit. Por trupat e kĂ«tyre funksioneve janĂ« identike! Probabiliteti Ă«shtĂ« se Ă«shtĂ« njĂ« gabim, dhe nĂ« funksionin trio_index_last duhet tĂ« pĂ«rdoret strrchr nĂ« vend tĂ« strchr. AtĂ«herĂ« sjellja do tĂ« jetĂ« siç pritet.
Fragmenti 4
Pika âdataâ nĂ« kĂ«tĂ« shprehje Ă«shtĂ« baraz me nullptr. Vlera e rezultateve nga operacionet aritmetike mbi kĂ«tĂ« tregues nuk ka kuptim dhe nuk duhet pĂ«rdorur. nsc_encode.c 124
static BOOL nsc_encode_argb_to_aycocg(NSC_CONTEXT* context,
const BYTE* data,
UINT32 scanline)
{
....
if (!context || data || (scanline == 0))
return FALSE;
....
src = data + (context->height - 1 - y) * scanline;
....
}Duket se Ă«shtĂ« harruar aksioni i mohimit kĂ«tu rastĂ«sisht ! pranĂ« data. ĂuditĂ«risht, qĂ« kjo ka mbetur e papĂ«rmendur.
Fragmenti 5
ĂshtĂ« zbuluar modeli âif (A) {âŠ} else if (A) {âŠ}â. Ka njĂ« probabilitet pĂ«r praninĂ« e gabimit logjik. Kontrolloni linjat: 213, 222. rdpei_common.c 213
BOOL rdpei_write_4byte_unsigned(wStream* s, UINT32 value)
{
BYTE byte;
if (value <= 0x3F)
{
....
}
else if (value <= 0x3FFF)
{
....
}
else if (value > 16) & 0x3F;
Stream_Write_UINT8(s, byte | 0x80);
byte = (value >> 8) & 0xFF;
Stream_Write_UINT8(s, byte);
byte = (value & 0xFF);
Stream_Write_UINT8(s, byte);
}
else if (value > 24) & 0x3F;
Stream_Write_UINT8(s, byte | 0xC0);
byte = (value >> 16) & 0xFF;
Stream_Write_UINT8(s, byte);
byte = (value >> 8) & 0xFF;
Stream_Write_UINT8(s, byte);
byte = (value & 0xFF);
Stream_Write_UINT8(s, byte);
}
....
}Këto kushte të fundit janë të njëjta: duket se dikush ka harruar t'i kontrollojë ato pas kopjimit. Nga kodi, është e dukshme se pjesa e fundit punon me vlera katërbajtëshe, prandaj mund të supozojmë se kushti i fundit duhet të jetë value <= 0x3FFFFFFF.
Një gabim tjetër i këtij tipi u gjet:
- V517 U zbulua modeli 'if (A) {âŠ} else if (A) {âŠ}'. Ka njĂ« probabilitet tĂ« pranishĂ«m tĂ« gabimeve logjike. Kontrolloni linjat: 169, 173. file.c 169
Kontrollimi i të dhënave hyrëse
Fragmenti 1
Shprehja âstrcat(target, source) != NULLâ Ă«shtĂ« gjithmonĂ« e vĂ«rtetĂ«. triostr.c 425
TRIO_PUBLIC_STRING int
trio_append
TRIO_ARGS2((target, source),
char *target,
TRIO_CONST char *source)
{
assert(target);
assert(source);
return (strcat(target, source) != NULL);
}Kontrollimi i rezultatit të ekzekutimit të funksionit në këtë shembull është i pasaktë. Funksioni strcat kthen një tregues në variantin përfundimtar të vargjes, domethënë parametrin e parë të kaluar. Në këtë rast, ky është target. Por nëse ai është i barabartë me NULL, atëherë kontrollimi i tij është tepër vonë, pasi në funksionin strcat do të ndodhë dereferencimi.
Fragmenti 2
Shprehja âcacheâ Ă«shtĂ« gjithmonĂ« e vĂ«rtetĂ«. glyph.c 730
typedef struct rdp_glyph_cache rdpGlyphCache;
struct rdp_glyph_cache
{
....
GLYPH_CACHE glyphCache[10];
....
};
void glyph_cache_free(rdpGlyphCache* glyphCache)
{
....
GLYPH_CACHE* cache = glyphCache->glyphCache;
if (cache)
{
....
}
....
}Në këtë rast, variabli cache i jepet adresa e një array statik glyphCache->glyphCache. Në këtë mënyrë, kontrolli if (cache) mund të lihet
Gabim në menaxhimin e burimeve
Burimi u marrĂ« duke pĂ«rdorur funksionin âCreateFileAâ por u lirua me funksionin e papĂ«rputhshĂ«m âfcloseâ. certificate.c 447
BOOL certificate_data_replace(rdpCertificateStore* certificate_store,
rdpCertificateData* certificate_data)
{
HANDLE fp;
....
fp = CreateFileA(certificate_store->file, GENERIC_READ | GENERIC_WRITE, 0,
NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
....
if (size < 1)
{
CloseHandle(fp);
return FALSE;
}
....
if (!data)
{
fclose(fp);
return FALSE;
}
....
}Përshkrimi i skedarit fp, i krijuar nga thirrja e funksionit CreateFile, ka mbyllur gabimisht me funksionin fclose nga biblioteka standarde, dhe jo CloseHandle.
Kushtet e njëjta
Shprehjet kushtore tĂ« deklaratave âifâ tĂ« vendosura pĂ«rkrah njĂ«ra-tjetrĂ«s janĂ« identike. Kontrolloni linjat: 269, 283. ndr_structure.c 283
void NdrComplexStructBufferSize(PMIDL_STUB_MESSAGE pStubMsg,
unsigned char* pMemory, PFORMAT_STRING pFormat)
{
....
if (conformant_array_description)
{
ULONG size;
unsigned char array_type;
array_type = conformant_array_description[0];
size = NdrComplexStructMemberSize(pStubMsg, pFormat);
WLog_ERR(TAG, "warning: NdrComplexStructBufferSize array_type: "
"0x%02X unimplemented", array_type);
NdrpComputeConformance(pStubMsg, pMemory + size,
conformant_array_description);
NdrpComputeVariance(pStubMsg, pMemory + size,
conformant_array_description);
MaxCount = pStubMsg->MaxCount;
ActualCount = pStubMsg->ActualCount;
Offset = pStubMsg->Offset;
}
if (conformant_array_description)
{
unsigned char array_type;
array_type = conformant_array_description[0];
pStubMsg->MaxCount = MaxCount;
pStubMsg->ActualCount = ActualCount;
pStubMsg->Offset = Offset;
WLog_ERR(TAG, "warning: NdrComplexStructBufferSize array_type: "
"0x%02X unimplemented", array_type);
}
....
}Ndoshta ky shembull nuk është një gabim. Sidoqoftë, të dy kushtet përmbajnë të njëjtat mesazhe, njëra prej të cilave ndoshta mund të hiqet.
Pastrimi i treguesve nul
Treguesi nul kalon nĂ« funksionin âfreeâ. Kontrolloni argumentin e parĂ«. smartcard_pcsc.c 875
WINSCARDAPI LONG WINAPI PCSC_SCardListReadersW(
SCARDCONTEXT hContext,
LPCWSTR mszGroups,
LPWSTR mszReaders,
LPDWORD pcchReaders)
{
LPSTR mszGroupsA = NULL;
....
mszGroups = NULL; /* mszGroups is not supported by pcsc-lite */
if (mszGroups)
ConvertFromUnicode(CP_UTF8,0, mszGroups, -1,
(char**) &mszGroupsA, 0,
NULL, NULL);
status = PCSC_SCardListReaders_Internal(hContext, mszGroupsA,
(LPSTR) &mszReadersA,
pcchReaders);
if (status == SCARD_S_SUCCESS)
{
....
}
free(mszGroupsA);
....
}Në funksionin free mund të kalojë një tregues të null dhe analizatori e di këtë. Por nëse ndodh një situatë ku treguesi gjithmonë kalon si null, siç është në këtë fragment, do të lëshohet një paralajmërim.
Tregues mszGroupsA fillimisht është e barabartë NULL dhe nuk inicializohet askund tjetër. Dega e vetme e kodit ku treguesi mund të inicializohet është e paarritshme.
Ishin edhe mesazhe të tjera të këtij lloji:
- V575 Treguesi null po kalon nĂ« funksionin âfreeâ. Kontrolloni argumentin e parĂ«. license.c 790
- V575 Treguesi null po kalon nĂ« funksionin âfreeâ. Kontrolloni argumentin e parĂ«. rdpsnd_alsa.c 575
Më së shpeshti, këto variabla të harruara ndodhin gjatë procesit të ristrukturimit dhe mund të fshihen lehtësisht.
Përgjithësia e mundshme
Përgjithësi e mundshme. Merrni parasysh hedhjen e operandëve, jo rezultatit. makecert.c 1087
// openssl/x509.h
ASN1_TIME *X509_gmtime_adj(ASN1_TIME *s, long adj);
struct _MAKECERT_CONTEXT
{
....
int duration_years;
int duration_months;
};
typedef struct _MAKECERT_CONTEXT MAKECERT_CONTEXT;
int makecert_context_process(MAKECERT_CONTEXT* context, ....)
{
....
if (context->duration_months)
X509_gmtime_adj(after, (long)(60 * 60 * 24 * 31 *
context->duration_months));
else if (context->duration_years)
X509_gmtime_adj(after, (long)(60 * 60 * 24 * 365 *
context->duration_years));
....
}Hedhja e rezultatit në long nuk është mbrojtje nga mbipërçimi, sepse vetë llogaritja ndodh duke përdorur tipin int.
Zhbllokimi i treguesit në inicializim
Treguesi âcontextâ u shfrytĂ«zua para se tĂ« verifikohej nĂ« lidhje me nullptr. Kontrolloni linjat: 746, 748. gfx.c 746
static UINT gdi_SurfaceCommand(RdpgfxClientContext* context,
const RDPGFX_SURFACE_COMMAND* cmd)
{
....
rdpGdi* gdi = (rdpGdi*) context->custom;
if (!context || !cmd)
return ERROR_INVALID_PARAMETER;
....
}KĂ«tu treguesi context shkĂ«putet nĂ« inicializim â pĂ«rpara se tĂ« kontrollohet.
U gjetën edhe gabime të tjera të këtij lloji:
- V595 Treguesi ântlmâ u shfrytĂ«zua para se tĂ« verifikohej nĂ« lidhje me nullptr. Kontrolloni linjat: 236, 255. ntlm.c 236
- V595 Treguesi âcontextâ u shfrytĂ«zua para se tĂ« verifikohej nĂ« lidhje me nullptr. Kontrolloni linjat: 1003, 1007. rfx.c 1003
- V595 Treguesi ârdpeiâ u shfrytĂ«zua para se tĂ« verifikohej nĂ« lidhje me nullptr. Kontrolloni linjat: 176, 180. rdpei_main.c 176
- V595 Treguesi âgdiâ u shfrytĂ«zua para se tĂ« verifikohej nĂ« lidhje me nullptr. Kontrolloni linjat: 121, 123. xf_gfx.c 121
Kusht i paarsyeshëm
Shprehja ârdp->state >= CONNECTION_STATE_ACTIVEâ Ă«shtĂ« gjithmonĂ« e vĂ«rtetĂ«. connection.c 1489
int rdp_server_transition_to_state(rdpRdp* rdp, int state)
{
....
switch (state)
{
....
case CONNECTION_STATE_ACTIVE:
rdp->state = CONNECTION_STATE_ACTIVE; // state >= CONNECTION_STATE_ACTIVE) // Activate, client->activated, client);
if (!client->activated)
return -1;
}
....
}
....
}ĂshtĂ« e lehtĂ« tĂ« vĂ«resh se kushti i parĂ« nuk ka kuptim pĂ«r shkak tĂ« caktimit tĂ« vlerĂ«s pĂ«rkatĂ«se mĂ« parĂ«.
Analizë e gabuar e vargut
Format i gabuar. Merrni parasysh kontrollimin e argumentit tĂ« tretĂ« aktual tĂ« funksionit âsscanfâ. Pritet njĂ« tregues pĂ«r tipin unsigned int. proxy.c 220
Një pjesë e shprehjes kushtore është gjithmonë e vërtetë: (rc >= 0). proxy.c 222
static BOOL check_no_proxy(....)
{
....
int sub;
int rc = sscanf(range, "%u", &sub);
if ((rc == 1) && (rc >= 0))
{
....
}
....
}Analizatori për këtë fragment jep menjëherë 2 paralajmërime. Specifikatori %u pret një variabël të tipit unsigned int, por variabli sub ka tipin int. Më pas shohim një kontroll të dyshimtë: kushti përtej nuk ka kuptim, pasi fillimisht bëhet krahasimi me një.
Kontrolle të paorganizuar
Shprehja âstatus == 0x00090314â Ă«shtĂ« gjithmonĂ« e gabuar. ntlm.c 299
BOOL ntlm_authenticate(rdpNtlm* ntlm, BOOL* pbContinueNeeded)
{
....
if (status != SEC_E_OK)
{
....
return FALSE;
}
if (status == SEC_I_COMPLETE_NEEDED) // <=
status = SEC_E_OK;
else if (status == SEC_I_COMPLETE_AND_CONTINUE) // <=
status = SEC_I_CONTINUE_NEEDED;
....
}Kushtet e shënuara do të jenë gjithmonë të falsifikuara, pasi ekzekutimi do të arrijë në kushtin e dytë vetëm nëse status == SEC_E_OK. Kodi i duhur mund të duket kështu:
nëse (statusi == SEC_I_COMPLETE_NEEDED)
statusi = SEC_E_OK;
ndryshe nëse (statusi == SEC_I_COMPLETE_AND_CONTINUE)
statusi = SEC_I_CONTINUE_NEEDED;
ndryshe nëse (statusi != SEC_E_OK)
{
....
kthe FALSE;
}Përfundimi
Kështu, kontrolli i projektit zbuloi shumë probleme, por vetëm pjesa më interesante e tyre u përshkrua në artikull. Zhvilluesit e projektit mund ta kontrollojnë vetë projektin, duke kërkuar një çelës përkohës në faqen e internetit. . Kishte edhe njoftime të rrema, puna mbi të cilat do të ndihmojë për të përmirësuar analizuesin. Megjithatë, analiza statike është e rëndësishme nëse dëshironi jo vetëm të përmirësoni cilësinë e kodit, por edhe të shkurtoni kohën për të gjetur gabime, dhe PVS-Studio mund të ndihmojë për këtë.
Nëse dëshironi ta ndani këtë artikull me audiencën anglishtfolëse, ju lutem përdorni lidhjen për përkthimin: Sergey Larin.
Burimi: habr.com
