Базы данныхИнтернетКомпьютерыОперационные системыПрограммированиеСетиСвязьРазное
Поиск по сайту:
Подпишись на рассылку:

Назад в раздел

Исследование ReGet 1.3.2.

Исследование ReGet 1.3.2.
  На безрыбье и рак - щука
Народная мудрость.

К чему такой эпиграф ? А к тому, что на развелось слишком много народу, считающих себя более умными, чем другие; оно, конечно, что-то в этом может быть и есть. Однако, что-то мало кто из них может написать описание препарирования программы в понятной форме1). В общем, это их проблемы, а свою проблему в виде этой статьи я выношу на всеобщее обозрение
Отчего бы не сломать более новую версию ? - спросит меня пытливый читатель. И будет совершенно неправ. Самая главная причина в том, что я живу в той же поганой стране 2), что и авторы сей не самой плохой программы на свете, и испытытываю на собственной заднице те же прелести проживания здесь, что и они. Короче, сокамерники мы, как бы. Так что зачем же так гадить людям - им ведь тоже нужно бабки делать как-то, вот когда следующая версия ReGet появится, тогда и 1.40, может быть, посмотрю. А пока, если шило в одном месте не даёт покоя, можете сами попробовать.
Значится, программа зовётся ReGet, v 1.3.2, взять можно много где, можно попробовать Две Коровы или KGB.ru. Умеет она докачивать порванные соединения по HTTP (если сервер на том конце провода двумя руками поддерживает протокол HTTP 1.1).
Всем хороша данная программа, но есть у неё маленький недостаток (как любят говорить ребята от Билли "feature") - начинает надоедать через 30 дней диалоговым окошком - дескать, зарегистрируй меня etc. А чтобы не было такого, просит зарегистрироваться. O`k.
Запускаем программу, выбираем в главном меню "?", "Регистрация ReGet"
Появляется диалоговое окно "Регистрационная информация". Заносим туда имя, свой e-mail - о-па, а что это за третье поле такое? "Регистрационный код" !
А сиё чудо где взять ? Нету ? Непорядок. Надо свою программу написать, чтоб такой генерила.
Однако, скоро сказка сказывается... Короче, как советуют на Fravia, налейте себе, если со вчерашнего осталось, и приступим.
Для начала, жизненно необходимо выяснить, с какого же места собственно и начинается обработка введённого пароля. Запускаем струмент - SI. Ставим стандартный набор 3)
bpx GetWindowTextA bpx GetDlgItemTexA bpx MessageBoxA bpx MessageBoxIndirectA Жмём педаль - и вот оно, всплываем по вызову GetWindowTextA; жмём F12 - и оказываемся по неважно какому адресу, зато важно, в каком модуле - mfc42. Значит, программа написана с применением MFC и это есть некая функция CWnd::DoDataExChange, в которой посредством DDX_ функций происходит извлечение введённых пользователем значенийRTFM).
Чтобы не всплывать зря ещё несколько раз на тех же граблях, отключим точку прерывания: bd 0. Жмём F12, пока не окажемся снова в модуле ReGet - адрес 0x4143B9. Самое время нажать F5 и запустить IDA Pro. Я не буду приводить листинг, который она сгенерировала для данного адреса, по следующей причине - это действительно переопределённая функция CWnd::DoDataExChange для данного конкретного диалога, в ней нет ничего интересного (несколько DDX_TextRTFM))
Ну что же, не много полезного мы выяснили на первый раз. Придётся повторить, но на этот раз жмём F12 дальше, пока снова не окажемся в модуле ReGet - на это раз по адресу 0x414492. Вот это уже интересно. Ну-ка, что нам IDA показывает (полный листинг я опять-таки не привожу4), только вызовы функций (комментарии, естественно, мои):
... 41448D CWnd::UpdateData ... 41449C CString::CString(CString const &) ... 4144AF CString::CString(CString const &) ... 4144C2 CString::CString(CString const &) ... 4144CB call 41410F 4144D0 add esp, 0Ch ; стандартная очистка стека от локальных переменных, ; используемых в пользовательских функциях 4144D3 test eax, eax ; смотрим, в eax 0 ? 4144D5 jz short loc_4144E2 ; а вот это что-то до боли знакомое... ... 4144DB CDialog::EndDialog ... 4144E2 push 0ffffffffh 4144E4 push 0 4144E6 push 41Dh 4144EB call AfxMessageBox(uint, uint, uint) ... Уж сколько раз говорено было... Короче, старо как Zip, если по возврате из call 41410F регистр eax не есть 0 - мы хорошие парни, и неплохо было бы закрыть диалоговое окно вызовом CDialog::EndDialog, иначе - мы плохие парни, на нас обиделись и нужно непременно высказаться вызовом AfxMessageBox (MFC-вариант обычного MessageBox). Всё ясно, нужно препарировать процедуру 41410F
Для начала, давайте дадим ей имя позвучнее, скажем, VerifyUs. Для этого подведём курсор на строку, содержащую фразу "proc near" на 4144E2 после адреса и нажмём N и в появившемся окне дадим волю фантазии5)
Процедура, опять же, слишком длинная и мне было утомительно приводить её здесь полностью, так что приведены только необходимые для понимания моменты:
41410F push eax, offset loc_42003C 414114 call __EH_prolog Стандартная для C++ инициализация exception frame handlerа, в стек помещается адрес функции очистки стека, которая вызовется при возникновении неперехваченного исключения и вызывается функция инициализации нового фрейма обработки исключений
Дальше какая-то не относящаяся к делу лажа, ага, вот интересное местечко:
414136 lea ecx, [ebp+10h] 414139 push offset a_contik_ 41413E call CString::Find(char const *) 414143 cmp eax, 0ffffffffh 414146 jz short loc_41415C По адресу a_contik_ находится строка "_contik_". Если эта строка не найдена в некоей строковой переменной класса CString (то есть, вызов CString::FindRTFM) вернул -1), выполнение переходит на 41415C. Ну-ка проверим под SI, чего это тут делается... Ага, прикол от программистов (сами посмотрите, если не лениво - считайте это домашним заданием :-). Ну ладно, поstepали дальше 41415C mov eax, [ebp+10h] ; загрузим строку пароля из класса CString 41415F xor esi, esi 414161 cmp [eax-8], edi ; не нулевая ли длина (выше по течению ; была инструкция xor edi, edi по адресу 414121) 414164 jle short loc_414188 ; если длина < 0 - переход на следующую проверку 414166 cmp edi, 14h ; длина > 14 ? 414169 jge short loc_414188 ; тогда на следующую проверку 41416B mov cl, [esi+eax] ; в cl - очередной байт нашего пароля ; помните, что в строках индексация начинается с 0 41416E cmp cl, 20h ; сравним с кодом пробела 414171 jz short loc_414182 ; если оно - перход на следующую итерацию 414173 push ecx ; наш байт положим на стек 414174 call sub_4140ED ; вызов некоей любопытной функции 414179 mov [ebp+edi-20h], al ; и результат (который всегда возвращается ; в регистре eax - ещё куда-то (один байт) 41417D mov eax, [ebp+10h] ; и снова загрузим адрес строки пароля 414180 pop ecx ; очистка стека 414181 inc edi ; накинем счётчики 414182 inc esi 414183 cmp esi, [eax-8] ; счетчик меньше длины строки ? 414186 jl short loc_414166 ; если нет - следующая итерация 414188 cmp edi, 10h ; сравним количество обработанных байт 41418B jnz loc_41423B ; если не равно 0x16 - переход довольно далеко ... 41423B ... ... 414242 call CString::~Cstring(void) ... 41424E call CString::~Cstring(void) ... 41425A call CString::~Cstring(void) 41425F xor eax, eax ; это плохой парень ... retn Не разобраться без (нет, не без пол-литра) SoftIce. Путём нескольких прогонов под отладчиком выяснилось кое-что, что я описал в комментариях (уж придётся вам поверить моему внутреннему голосу). Просмотрев этот фрагмент даже бегло, уже можно выяснить формат "Регистрационного кода" (в дальнейшем РН) - он не должен содержать пробелов и быть в длину ровно 10h = 16 символов. Посмотрим на процедуру sub_4140ED - ей передаётся в качестве параметра байт из РН
4140ED movsx eax, [esp+arg_0] ; в eax - параметр 4140F2 push eax ; его же - в стек 4140F3 call ds:toupper ; преобразуем сивол в верхний регистр 4140F9 pop ecx ; очистка стека 4140FA mov cl, al ; преобразованный символ - в cl 4140FC xor eax, eax ; обнулим eax 4140FE cmp cl, byte ptr a92bc4hjkldfw5z[eax] ; сравним байт по ; адресу адрес a92bc4hjkldfw5z + eax 414104 jz short locret_41410E ; если равны - возврат из функции 414106 inc eax ; накинем счётчик 414107 cmp eax, 20h ; сравним счётчик с 0x20 41410A jl short loc_4140FE ; если меньше - следующая итерация 41410C mov al, 20h ; ничего не нашли - вернём 0x20 41410E retn В строке a92bc4hjkldfw5z (как IDA иногда развозит) "9#2BC4HJKLDFW5Z$6G8MNXPQR7S3ETAU". Значит, по возможности РН должен состоять из таких символов в верхнем или нижнем регистре (не забывайте про вызов toupperRTFM)).
Замечание: инструкция movsx делает следующее: в самую младшую часть регистра загружается байт, а остальная часть регистра заполняется знаковым битом этого байта, то есть в данном случае, при использовании символов, меньших 127 (латинских букв и/или цифр) это будет всегда 0.
Сечас преобразованный символы РН лежат в буфере по адресу [ebp-20h]. Далее
414191 movsx eax, byte ptr [ebp-16h] ; берём 20h-16h=10ый сивол 414195 movsx ecx, byte ptr [ebp-13h] ; 20h-13h=13ый 414199 imul eax, ecx ; умножаем со знаком, результат в eax 41419C mov bl, [ebp-19h] ; 20h-19h=7ой символ 41419F push eax ; помещаем произведение в стек 4141A0 lea eax, [ebp-20h] ; в eax - адрес буфера с преобразованным РН 4141A3 push edi ; в edi - длина РН - в стек 4141A4 push eax ; помещаем в стек адрес буфера с РН 4141A5 mov byte ptr [ebp-19h], 42h ; 7ой символ замещаем кодом 0x42 4141A9 call sub_416691 ; вызываем некоторую процедуру 4141AE and al, 1Fh ; оставляем в результате младшие 5 бит 4141B0 add esp, 0Ch ; очистка стека 4141B3 cmp bl, al ; как интересно 4141B5 jnz loc_41423B Какую любопытную вещь мы сдесь наблюдаем ! Сильно это похоже на проверку правильности введённого РН. Не иначе как функция sub_416691 считает hash-код по нашему преобразованному РН, затем результат обрезается до 5 младших бит и сравнивается с 7мым преобразованным символом. Если они не равны - переход на уже знакомый нам адрес, возвращающий 0 (признак наличия плохих парней). Переходим на функцию 416691 и называем её, скажем, get_hash (я также обозвал и все её аргументы для большей наглядности:
416691 key = dword ptr 4 416691 len = dword ptr 8 416691 initial_seed = dword ptr 0Ch 416691 416691 mov eax, [esp+0Ch] ; поместим в eax initial_seed 416695 xor ecx, ecx ; обнулим ecx 416697 cmp [esp+len], ecx ; сравним длину с 0 41669B not eax ; инвертируем initial_seed 41669D jbe short get_hash_end ; переход, если длина меньше или равна 0 41669F push esi 4166A0 get_hash_loop: 4166A0 mov edx, [esp+4+key] ; в edx - адрес переданного ключа 4166A4 movzx esi, al ; в esi - младший байт initial_seed 4166A7 movzx edx, byte ptr [ecx+edx] ; в edx - очередной байт ключа 4166AB xor edx, esi 4166AD shr eax, 8 ; сдвигаем initial_seed вправо на 8 бит 4166B0 mov edx, array[edx*4] ; в edx - dword по адресу array + edx*4 4166B7 xor eax, edx ; формируем новый initial_seed 4166B9 inc ecx ; накинем счётчик 4166BA cmp ecx, [esp+4+len] ; счётчик равен длине ? 4166BE jb short get_hash_loop ; если меньше - следующая итерация 4166C0 pop esi 4166C1 get_hash_end: 4166C1 not eax ; перед возвратом ещё раз инвертируем initial_seed ; он и есть hash-code - результат функции 4166C3 retn Происходит здесь следующее: цикл по байтам ключа, число итераций цикла - длина ключа, передаваемая как второй параметр, при этом происходит изменение некоторого initial_seed. После цикла его значение инвертируется, и возвращается как результат. Попутно можно заметить, что для подсчёта hash-code используется массив dword, максимальная длина которого равна 256 элементам (или 4 * 256 = 1024 байт)
Замечание 1: инструкция movzx загружает байт в самую младшую часть регистра, а остальные биты регистра заполняет нулём.
Замечание 2: команда not, в отличие от neg, не влияет на значения флагов, поэтому можно сначала сравнить, потом инвертировать, а затем воспользоваться результатами сравнения.
Хорошо, смотрим далее:
4141BB lea ecx, [ebp+8] ; как можно проверить в SI, грузится адрес ; CString-класса с именем пользователя 4141BE call CString::MakeUpper(void) ; преобразуется в верхний регистр 4141C3 lea ecx, [ebp+0Ch] ; адрес CString с E-Mail 4141C6 call CString::MakeUpper(void) ; преобразуется в верхний регистр 4141CB mov eax, [ebp+8] ; адрес CString с именем пользователя 4141CE push 6347A267h ; помещается в стек initial_seed 4141D3 push dword ptr [eax-8] ; помещается в стек длина имени пользователя 4141D6 push eax ; помещается в стек адрес имени пользователя 4141D7 call get_hash ; вызов всё той же get_hash 4141DC add esp, 0Ch ; очистка стека 4141DF push eax ; хм, вычисленный hash снова помещается в стек ; как initial_seed 4141E0 mov eax, [ebp+0Ch] ; длина E-Mail 4141E3 push dword ptr [eax-8] ; адрес E-Mail 4141E6 push eax ; помещается в стек 4141E7 call get_hash ; снова call get_hash 4141EC add esp, 0Ch ; очистка стека 4141EF xor ecx, ecx ; обнуление счётчика 4141F1 mov dl, al ; в dl - младший байт вычисленного ; двойного hash-code по имени и E-Mail 4141F3 and dl, 1Fh ; оставляем младшие 5 бит 4141F6 cmp [ebp+ecx-20h], dl ; сравниваем с очередным преобразованным ; символом из буфера РН 4141FA jnz short loc_41423B ; если не равны - переход на приснопамятный адрес 4141FC shr eax, 5 ; сдвигаем has_code на 5 бит вправо 4141FF inc ecx ; накидываем счетчик 414200 cmp ecx, 7 ; счётчик < 7 ? 414203 jl short loc_4141F1 ; если да - следующий цикл Я надеюсь, что после всех моих комментариев уже должно быть понятно, как написать Key Generator. Он должен преобразовывать имя пользователя и E-Mail в верхний регистр, затем с помощью get_hash вычисляется по ним hash, далее пятёрки бит складываются в некоторый буфер, он дополняется до 16 байт каким-нибудь значением из допустимых6), в седьмой сивол буфера помещается код 0x42, затем считаем hash по нашему заполненному таким хитрым образом буферу (опять с помощью get_hash), оставляем от него младшие 5 бит, помещаем их в 7 символ, и, наконец, делаем обратное преобразование из кодов в удобочитаемые символы.
Однако, для полноценного crackа нам необходимо иметь массив Array в нашем KeyGenerator. Размер этого массива составляет около 1 Kb. Можно ,конечно, набить его вручную, и, когда я окончательно впаду в старческий маразм, я именно так и буду поступать. А пока я написал небольшую процедуру, которая сильно облегчит мне жизнь. int make_array(void) { /* эти два адреса известны из IDA */ const int a1 = 0x42cc98, // адрес первого байта массива Array a2 = 0x42d098, // offset = 0x2b898; // смещение на первый байт массива Array в .exe файле - выясняется в hex-editorе const char *progName = "reget.exe", // жертва *outFile = "out.c"; // куда выход складывать будем FILE *out; int inHandle; long f_pos; unsigned char *buffer = NULL; inHandle = _open(progName, _O_BINARY | _O_RDONLY); if (-1 == inHandle) { fprintf(stderr, "Cannot open %sn", progName); return -1; } f_pos = lseek(inHandle, offset, SEEK_SET); if (f_pos != offset) { fprintf(stderr, "Cannot seek, error=%dn", errno); close(inHandle); return -2; } buffer = (unsigned char *)malloc(1 + (a2 - a1)); if (a2 - a1 != read(inHandle, buffer, a2 - a1)) { fprintf(stderr, "Cannot read %d bytes from %sn", a2 - a1, progName); free(buffer); close(inHandle); return -3; } close(inHandle); if (NULL == (out = fopen(outFile, "w"))) { fprintf(stderr, "Cannot create out file %sn", outFile); free(buffer); return -4; } fprintf(out, "const unsigned char array[] = {n"); for (f_pos = 0; f_pos < a2 - a1; f_pos++) { fprintf(out, "0x%x%c%c", buffer[f_pos], f_pos != a2 - a1 - 1 ? ',' : ' ', (f_pos & 7) == 7 ? 'n': ' '); } fprintf(out, "n};n"); fclose(out); free(buffer); return 0; // success }
Note Если вы не понимаете, что делает данная (не самая сложная из писанных мною) процедура, лучше Вам бросить занятия Reverse Engeneeringом и сначала попробовать научиться простому Engeneeringу, потому что довольно часто приходится писать подобные болванки (например, в настоящий момент я занят ломкой программки XLNT (высокоуровневый сетевой коммандный язык сценариев для NT), и мне уже пришлось написать таких процедур штук примерно 6, а конца сему действу ещё и не видно...), и как Вы будете дальше жить - мне неизвестно 7)
Note 2 Функция, конечно, не Бог весть какая, тем не менее имеет смысл использовать её для выгрузки массивов данных в других проектах. Для этого нужно заменить нужными значениями a1, a2, offset, progName, и, возможно, outFile, а также заменить название и/или тип массива в первом fprintf
Ну а дальше всё настолько просто, что засим и разрешите откланяться...
Исходники и сам KeyGenerator можно выгрузить здесь
Кстати, чтобы сделать crack и написать этот несчастный KeyGenerator, у меня ушло около 4х с половиной часов, а на написание данного опуса - два дня...
  Ломать - не строить
Народная мудрость

1). Я лично с удовольствием прочитал бы несколько essays HiJackа о взломе программ, защищённых dongles; но ведь их нету в природе :-(
Back
2). Не думаю, что ещё остались люди, живущие здесь в счастливом неведении, что местное правительство на протяжении уже 80 (восьмидесяти) лет проводит геноцид против местного населения (как они его в последнее время стали называть презрительно-брезливо - "электорат". Если же такие люди ещё остались - мне не хочется общаться с ними дабы объяснять совершенно очевидные вещи...
Back
3). На самом деле, это чрезвычайно простой случай. Я просто решил, что, поскольку программа запускается и под Windows 95, и под NT (а у меня стоит Window NT 4.0 Workstation Rus), то в ней используются функции, использующие обычный однобайтовые строки - поэтому я ловлю функции API с суффиксом A. Если бы был .exe special for NT, имело бы смысл ловить функции с суффиксом W (Wide Char).
В тяжёлых случаях можно порекомендовать следующие варианты:
ShowWindow - вызывается, когда нужно показать какое-либо окно. Так как все controls, присутсвующие в, скажем диалоговом окне, в свою очередь являются опять таки окнами, которые также должны быть show при вызове диалогого окна, то вызовов ShowWindow может быть чересчур много, например у меня множество раз отлавливался этот вызов из функции ImageList_Add в COMCTL32.DLL, так что я не стал использовать этот метод. Запустить примочку трассирования оконных сообщений от Visual C++ - Spy++, найти в ней диалоговое окно, посмотреть в ней HWND (внутренней идентификаторы) окон и далее перехватывать либо wm_gettext, либо, если программа переопределяет оконную процедуру controlов и сама обрабатывает нажатия кнопок, wm_keydown. Того же можно достичь не выходя из SI:
task - перечисляет имена загруженных процессов (к сожалению у меня на NT эта команда выдаёт "No LDT" - я так и не смог победить, да и не смертельно это при наличии других инструментов)
hwnd имя_процесса
в появившемся списке отыскиваем что-нть типа Edit etc, затем
bmsg идентификатор_окна wm_сообщение_для_отлова Также в особо тяжёлых случаях может помочь следующее:
Находим одним из вышеперечисленных методом идентификатор самого диалогого окна,
ставим bmsg идентификатор_окна wm_command. Всплываем по нажатию какой-либо кнопки. Если нужно отловить не банальный MessageBox, а что-нть похлёще, могут помочь:
DialogBoxIndirectParam, причём с этой функцией вообще очень интересно. Она вызывается для создания модального диалога. Но ! Дело в том, что по крайней мере на NT есть аж три такие функции (95 под рукой нет, проверить не могу). Как вы могли ожидать, есть DialogBoxIndirectParamA для ASCII-строк, DialogBoxIndirectParamW для Wide-Chars, и ещё есть недокументированная (возможно, лишь в моей документации - я использую для помощи по Win32 API Visual C++ 5.0) - DialogBoxIndirectParamAorW - и она очень часто срабатывает в программах, написанных с применением MFC.
Для создания немодального диалога используется CreateDialogIndirectParam с аналогичным набором суффиксов SetWindowPos - используется для изменения размера, позиции и Z-порядка окон. К ней применимы все замечания, относящиеся к ShowWindow Можно также найти строку, которая выдаётся в диалоговом окне. Если она лежит в сегменте данных, можно просто поставить bpm адрес_строки. Но очень часто такие строки расположены в сегменте ресурсов. Малоизвестный факт, но на строки ресурса также можно ставить bpm. Краткое пояснение: все функции API для манипуляции ресурсами (LoadString, LoadResource etc) принимают в качестве первого аргумента HINSTANCE, который есть ни что иное, как указатель на начало отображенного в память .exe файла. Далее такие функции обрабатывают PE (или NE) заголовок загруженного модуля, находят сегмент ресурсов, и по переданному идентификатору находят нужный ресурс, который затем просто копируется во вновь выделенную память. Есть и свои грабли: ресурсы обычно (точнее, почти всегда) хранятся в Unicode. Я поступаю так - в IDA всегда указываю загружать секцию ресурсов, а затем помогает binary search с нужными значениями.
Если лениво выискивать адрес ресурса - можно поставить bpx на LoadString и смотреть её параметры и возвращаемые значения (и ещё неизвестно, какой способ более трудозатратный) Если программа никак не реагирует на неправильный Serial Number, можно попробовать найти строку, которая могла бы выдаваться на правильный. Я понимаю, что звучит чересчур расплывчато и никто, кроме авторов программы ,точно не знает, как она выглядит (да и авторы, наверняка, забыли уже), но всё же это лучше, чем совсем ничего. Здесь может помочь только обострённая голодом и хронической нищетой интуиция...
Ну и если ничего из вышеперечисленного не помогло - в конце концов, меня же зовут не Иисус Христос...RTFM)
Back
4) Ну что поделать - ленив я. Вам никогда не говорили, что лень - двигатель прогресса ?
Back
5) есть замечательное essay на fravia о том, как можно настроить IDA. Если Вы умеете настраивать IDA, вполне вероятно, что некоторых элементов у Вас не будет, или будут какие-нть другие. Однако в последнем случае Вы, видимо, не нуждаетесь в моих подсказках о именовании имён функций, их аргументов и проч.
Back
6) Я выбрал индекс сивола "$" - интересно, что бы сказал на это доктор Фрейд ?
Back
7). Это только в западных высокохудожественных полуфантастических фильмах всё выглядит красиво и эстетично - сидит этакий хукер и ломает программу, которая представлена на гигантском дисплее в трёхмерном виде (видимо, для наглядности). В жизни, как всегда, всё значительно прозаичнее - нужно много работать и иметь неитощимое терпение - короче, чтобы достичь вершины хотя бы невысокой кочки в Reverse Engeneering, Вы должны быть совсем ненормальным человеком, например, как я...
Back
RTFM) Если вторую неделю ничего не получается, прочти ,наконец, документацию. Я не верю в то, что человека ,позавчера впервые увидевшего контупер, можно научить ломать программы "за 21 день". Для этого необходимо как минимум (кроме подразумевающегося обязательного знания ассемблера) ещё и знание Win32 API хотя бы на уровне прикладного программиста, а также знание того framework, на котором написана предполагаемая жертва взлома (Delphi VCL + M$ MFC - минимум), и соответствующих языков программирования.

  • Главная
  • Новости
  • Новинки
  • Скрипты
  • Форум
  • Ссылки
  • О сайте




  • Emanual.ru – это сайт, посвящённый всем значимым событиям в IT-индустрии: новейшие разработки, уникальные методы и горячие новости! Тонны информации, полезной как для обычных пользователей, так и для самых продвинутых программистов! Интересные обсуждения на актуальные темы и огромная аудитория, которая может быть интересна широкому кругу рекламодателей. У нас вы узнаете всё о компьютерах, базах данных, операционных системах, сетях, инфраструктурах, связях и программированию на популярных языках!
     Copyright © 2001-2024
    Реклама на сайте