
FreeRDP - адкрытая рэалізацыя Remote Desktop Protocol (RDP), пратаколу, які рэалізуе выдаленае кіраванне кампутарам, распрацаванага кампаніяй Microsoft. Праект падтрымлівае мноства платформ, сярод якіх Windows, Linux, macOS і нават iOS з Android. Гэты праект абраны першым у рамках цыклу артыкулаў, прысвечаных праверцы RDP-кліентаў з дапамогай статычнага аналізатара PVS-Studio.
Трохі гісторыі
Праект з'явіўся пасля таго, як Microsoft адкрыла спецыфікацыі свайго прапрыетарнага пратакола RDP. На той момант існаваў кліент rdesktop, рэалізацыя якога грунтуецца на выніках Reverse Engineering.
У працэсе рэалізацыі пратакола станавілася больш складана дадаваць новы функцыянал з-за існаваўшай тады архітэктуры праекта. Змены ў ёй спарадзілі канфлікт паміж распрацоўшчыкамі, што прывяло да стварэння форка rdesktop – FreeRDP. Далейшае распаўсюджванне прадукта было абмежавана ліцэнзіяй GPLv2, у выніку чаго было прынята рашэнне аб рэліцэнзаванні на Apache License v2. Аднак не ўсе былі згодны мяняць ліцэнзію свайго кода, таму распрацоўшчыкі вырашылі перапісаць праект, у выніку чаго мы маем сучасны від кодавай базы.
Больш падрабязна пра гісторыю праекта можна прачытаць у нататцы афіцыйнага блога: "The history of the FreeRDP project".
У якасці прылады для выяўлення памылак і патэнцыйных уразлівасцяў у кодзе выкарыстоўваўся . Гэта статычны аналізатар кода для моваў C, C++, C# і Java, даступны на платформах Windows, Linux і macOS.
У артыкуле прадстаўлены толькі тыя памылкі, якія падаліся мне найболей цікавымі.
Уцечка памяці
Функцыя была завершана без выканання 'cwd' pointer. A memory leak is possible. 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;
....
}Дадзены фрагмент быў узяты з падсістэмы winpr, якая рэалізуе абгортку WINAPI для не-Windows сістэм, г.зн. гэта лёгкі аналаг Wine. Тут можна заўважыць уцечку: памяць, выдзеленая функцыяй getcwd, вызваляецца толькі пры апрацоўцы спецыяльных выпадкаў. Для ўхілення памылкі трэба дадаць выклік бясплатна пасля memcpy.
Выхад за межы масіва
Array overrun is possible. Вага 'event->EventHandlerCount' index could reach 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++;
}
....
}У гэтым прыкладзе новы элемент дадаецца ў спіс, нават калі колькасць элементаў дасягнула максімальнага. Тут дастаткова замяніць аператар <= на <, Каб не выходзіць за межы масіва.
Была знойдзена і іншая памылка такога тыпу:
- V557 Array overrun is possible. Вага 'iBitmapFormat' index could reach 8. orders.c 2623
памылкі друку
Фрагмент 1
Expression '!pipe->In' is always 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;
....
}Тут мы бачым звычайную памылку друку: у другой умове выконваецца праверка той жа зменнай, што і ў першай. Хутчэй за ўсё, памылка з'явілася ў выніку няўдалага капіявання кода.
Фрагмент 2
Two identical blocks of text were found. The second block begins from line 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);
....
}Яшчэ адна памылка друку: каментар да кода мае на ўвазе, што з струменя павінна прыйсці minorVersion, аднак счытванне адбываецца ў зменную з імем majorVersion. Тым не менш, я не знаёмы з пратаколам, так што гэта толькі здагадка.
Фрагмент 3
Гэта адпачынак, які функцыі функцыі 'trio_index_last' з'яўляецца цалкам equivalible функцыяй '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);
}Мяркуючы па каментары, функцыя trio_index знаходзіць першае супадзенне сімвала ў радку, калі trio_index_last - апошняе. Але целы гэтых функцый ідэнтычныя! Хутчэй за ўсё, гэта памылка друку, і ў функцыі trio_index_last трэба выкарыстоўваць strrchr замест strchr. Тады паводзіны будуць чаканымі.
Фрагмент 4
The 'data' pointer in expression equals nullptr. Паказвае значэнне арытметычных дзеянняў на гэтым плане не мае патрэбы і не павінна быць выкарыстана. 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;
....
}Падобна, тут выпадкова прапусцілі аператар адмаўлення ! побач з gegevens. Дзіўна, што гэта засталося незаўважаным.
Фрагмент 5
use of 'if (A) {…} else if (A) {…}' pattern was detected. Там ёсць верагоднасць лагічнай рашучасці. Check lines: 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 <= 0x3FFFFF)
{
byte = (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 <= 0x3FFFFF)
{
byte = (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);
}
....
}Апошнія дзве ўмовы аднолькавыя: відаць, нехта забыўся праверыць іх пасля капіравання. Па кодзе прыкметна, што апошняя частка працуе з чатырохбайтнымі значэннямі, таму можна меркаваць, што апошняя ўмова павінна быць value <= 0x3FFFFFFF.
Была знойдзена і іншая памылка такога тыпу:
- V517 З дапамогай 'if (A) {…} else if (A) {…}' pattern was detected. Там ёсць верагоднасць лагічнай рашучасці. Check lines: 169, 173. file.c 169
Праверка ўваходных дадзеных
Фрагмент 1
Expression 'strcat(target, source) != NULL' is always true. 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);
}Праверка выніку выканання функцыі ў гэтым прыкладзе некарэктная. Функцыя strcat вяртае паказальнік на канчатковы варыянт радка, г.зн. першы перададзены параметр. У дадзеным выпадку гэта мішэнь. Аднак калі ён роўны NULL, то правяраць яго позна, бо ў функцыі strcat адбудзецца яго разнайменаванне.
Фрагмент 2
Expression 'cache' is always true. 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)
{
....
}
....
}У гэтым выпадку зменнай кэш прысвойваецца адрас статычнага масіва glyphCache->glyphCache. Такім чынам, праверку if (cache) можна апусціць.
Памылка кіравання рэсурсамі
The resource была запатрабаваная шляхам 'CreateFileA' функцыі, але была выканана шляхам запуску 'fclose' function. 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;
}
....
}Дэскрыптар файла fp, створаны выклікам функцыі CreateFile, па памылцы зачынены функцыяй fclose са стандартнай бібліятэкі, а не CloseHandle.
Аднолькавыя ўмовы
Дадатковыя expressions of 'if' statements размешчаны вакол сябе ўсе іншыя identical. Check lines: 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);
}
....
}Магчыма, гэты прыклад не памылка. Аднак абедзве ўмовы ўтрымоўваюць аднолькавыя паведамленні, адно з якіх, хутчэй за ўсё, можна прыбраць.
Ачыстка нулявых паказальнікаў
null pointer is passed in 'free' function. Inspect the first argument. 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);
....
}У функцыю бясплатна можна перадаваць нулявы паказальнік і аналізатар пра гэта ведае. Але калі выяўляецца сітуацыя, пры якой паказальнік заўсёды перадаецца нулявым, як у гэтым фрагменце, будзе выдадзена папярэджанне.
паказальнік mszGroupsA першапачаткова роўны NULL і больш нідзе не ініцыялізуецца. Адзіная галіна кода, дзе паказальнік мог ініцыялізавацца, з'яўляецца недасяжнай.
Былі і іншыя паведамленні гэта тыпу:
- V575 null pointer з'яўляецца простым 'function'. Inspect the first argument. license.c 790
- V575 null pointer з'яўляецца 'праца' функцый. Inspect the first argument. rdpsnd_alsa.c 575
Хутчэй за ўсё, падобныя забытыя зменныя ўзнікаюць падчас рэфактарынгу і іх можна проста выдаліць.
Магчымае перапаўненне
Possible overflow. Consider casting operands, не лічыцца. 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));
....
}Прывядзенне выніку да доўга не з'яўляецца абаронай ад перапаўнення, бо само вылічэнне адбываецца з выкарыстаннем тыпу INT.
Разнайменне паказальніка ў ініцыялізацыі
The 'context' pointer быў выкарыстаны да таго, што быў зняволены да nullptr. Check lines: 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;
....
}Тут паказальнік кантэкст разназываецца ў ініцыялізацыі - раней, чым адбываецца яго праверка.
Былі знойдзены і іншыя памылкі такога тыпу:
- V595 The 'ntlm' pointer быў выкарыстаны перад тым, што ён быў зняволены да nullptr. Check lines: 236, 255. ntlm.c 236
- V595 The 'context' pointer быў выкарыстаны перад тым, што ён быў зняволены да nullptr. Check lines: 1003, 1007. rfx.c 1003
- V595 The 'rdpei' pointer быў выкарыстаны перад тым, што ён быў зняволены да nullptr. Check lines: 176, 180. rdpei_main.c 176
- V595 The 'gdi' pointer быў выкарыстаны перад тым, што ён быў зняволены да nullptr. Check lines: 121, 123. xf_gfx.c 121
Бессэнсоўная ўмова
Expression 'rdp->state >= CONNECTION_STATE_ACTIVE' is always true. connection.c 1489
int rdp_server_transition_to_state(rdpRdp* rdp, int state)
{
....
switch (state)
{
....
case CONNECTION_STATE_ACTIVE:
rdp->state = CONNECTION_STATE_ACTIVE; // <=
....
if (rdp->state >= CONNECTION_STATE_ACTIVE) // <=
{
IFCALLRET(client->Activate, client->activated, client);
if (!client->activated)
return -1;
}
....
}
....
}Лёгка заўважыць, што першая ўмова не мае сэнсу з-за прысвойвання адпаведнага значэння раней.
Некарэктны разбор радка
Incorrect format. Паняцце, якое выконвае 220-дзённы argument функцыі 'sscanf'. Пытанне да ненадпісанага тыпу тыпу з'яўляецца выяўленым. proxy.c XNUMX
Apart of conditional expression is always true: (rc >= 0). proxy.c 222
static BOOL check_no_proxy(....)
{
....
int sub;
int rc = sscanf(range, "%u", &sub);
if ((rc == 1) && (rc >= 0))
{
....
}
....
}Аналізатар для гэтага фрагмента выдае адразу 2 папярэджанні. Спецыфікатар %u чакае зменную тыпу unsigned int, Але зменная да поўдня мае тып INT. Далей мы бачым падазроную праверку: умова справа не мае сэнсу, бо напачатку ідзе параўнанне з адзінкай. Не ведаю, што меў на ўвазе аўтар гэтага кода, але тут відавочна нешта не так.
Неўпарадкаваныя праверкі
Expression 'status == 0x00090314' is always false. 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;
....
}Адзначаныя ўмовы будуць заўсёды ілжывыя, бо выкананне дойдзе да другой умовы толькі ў тым выпадку, калі status == SEC_E_OK. Правільны код можа выглядаць так:
if (status == SEC_I_COMPLETE_NEEDED)
status = SEC_E_OK;
else if (status == SEC_I_COMPLETE_AND_CONTINUE)
status = SEC_I_CONTINUE_NEEDED;
else if (status != SEC_E_OK)
{
....
return FALSE;
}Заключэнне
Такім чынам, праверка праекту выявіла мноства праблем, але толькі найболей цікавая іх частка была апісаная ў артыкуле. Распрацоўнікі праекту могуць самі праверыць праект, запытаўшы часовы ключ ліцэнзіі на сайце. . Былі таксама і ілжывыя спрацоўванні, праца над якімі дапаможа палепшыць аналізатар. Тым не менш, статычны аналіз важны, калі вы хочаце не толькі павысіць якасць кода, але і скараціць час на пошук памылак, і PVS-Studio можа ў гэтым дапамагчы.
Калі хочаце падзяліцца гэтым артыкулам з англамоўнай аўдыторыяй, то прашу выкарыстаць спасылку на пераклад: Sergey Larin.
Крыніца: habr.com
