关于“我的 Pass”,我们在谈论什么?
关于“我的 Pass”,我们在谈论什么?之前的一段时间里,这里曾经出现过不少“我的 PASS”系列的帖子。据我的观察,直到目前尚未公开宣告解决的剩余 tommyyu 大佬的,另一个是我的。当然,我自然比不上大佬,当初就已经预料到必然是一个门可罗雀的未来,如今一语成谶,也未免能落个预言者的虚名,也不算坏事。然而回到初次见到有论坛的大佬发布这一类型的帖子时,我首先思考的问题却是直到我看到和分析了 tommyyu 大佬的实现后才勉强形成了一个答案:当我们在发布这样的帖子,提出这样的题目的时候,我们是在做些什么,我们希望带来什么?
似乎每一个作者都在“鼓励”使用者去进行暴力破解,换言之枚举每一个可能的 PASS 来尝试通过验证,但暴力枚举的方法真的就是我们想带来的了吗?它拥有这样的价值吗?我高度怀疑。数行代码的 Python 程序就可以轻松的完成任何类似问题的暴力枚举,而我甚至认为使用 Python 颇有些小题大做的嫌疑:一个几乎同样简短的 shell 脚本同样足够完成这一功能。
在这个可能主要以初学者为目标用户的论坛上,一个在我心目中非常简单、寥寥数行的代码,对很多朋友而言也许则是需要深思熟虑一段时间,精心完成编写方才能够落成的,使用其进行所谓的密码破解则对建立兴趣和信息大有裨益。
纵览过去的帖子,第一个直接将密码写在了代码中,因此被通过反编译轻松拿下;第二个将密码进行“加密”,或者说做了一些简单变换后存储,同样被反编译找出,使用调试器也可以毫不费力的将其导出,而所谓验证码更是直接以字符串形式存储,完全能够直接从文件中提取;第三个是我的实现,至今尚没有人说明其结果,我并无期望,但会继续等待;第四个和第一个较为接近,这一次则是通过直接修改可执行文件令其打印密码的方式获取到了密码的值;第五个同样类似第一个,解决方法亦与第五个类似;第六个通过 Python 打包而成,可执行文件非常臃肿,方法上使用了 sha256 和 sha512 ,程序比对散列之后的结果来判断原值是否相同,从而无法从反编译结果直接得到密码的值。
浏览了近期的全部内容,如果足够关注文流,一定会发现仍然没有回答先前的问题:我们究竟是在做些什么,又希望带来什么?我的回答是,在为练习分析可执行文件的能力提供一些简单的标靶,尽管这些标靶大多脆弱的不堪一击而不堪重用,更重要的,我将其视为一种自发的、原始的,对如何保存密码的探讨和尝试,在其中朋友们会意识到何种形式和流程是难以被攻克的,而何种是不可靠的。我们会发现,根本上这些程序实现的是一个保存密码,使用密码进行认证的过程,这在大量的实际应用中是广泛存在的。然而,如何有效、安全的保存密码实际上并不像直观想象中的一样简单。
这正是我如今写下这篇帖子的理由:我不会认为有人只从这里学习一些基本的知识就能够去开发一个完整的应用,但也见到过很多系统的设计和开发者毫无网络安全知识到令人惊诧的程度。因此,在这里我希望将简单的探讨较为公认的保存密码的最佳实践,作为应用网络安全的基础知识,同时也是我认为每一个编写代码的人应该了解的。我本人也不是任何相关方面的研究者或专家,只是兴趣使然的爱好者,但我会尽可能的查阅详实的资料。如果有任何疏漏和错误,欢迎指出。
从我们的 Pass 中的教训
为什么要讨论密码保存,我们在防范什么?
其实在论坛的例子中,我们就能看到其中的一部分理由:尽管这是应该避免的,但我们试图确保即使我们保存密码的数据库被攻破,其中的全部内容都被获取,也要能够阻止获取到这些内容的人使用密码进入系统。这可能听起来非常奇怪:当密码数据库被拿到了,整个系统也就沦陷了,再防止攻击者进入还有什么意义?实际上,安全性严格的应用中,不同部分之间的隔离是更加严格的:不仅密码数据库可能和其它数据库、主要服务器程序之间是分开存放的,密码数据库的权限也会是严格的。这意味着攻击者通过特定的方式能够读取全部的内容时,也可能无法对其进行修改,更无法访问其他组件。除非他继续找到了进入其他部分的途径,唯一获取访问权限的方式就是(利用已经获取的密码数据库信息)按照正常流程登录进系统。
退后一步,即使整个系统已经沦陷,也有责任继续保护用户的信息——这是一个糟糕的习惯,但不容否定有些用户会在不同的应用中使用完全一致的密码,这使得一处泄露的密码可能被用来在其他大量的应用上冒充他们的身份登入。用户使用你的应用,用户将他们重要的信息提供给你,他们信任你,你必须回应这种信任。
基本原则:如何保存密码?
从上面的逻辑我们不难看出,直接原样(相关领域中一般称为“明文”)保存密码是非常糟糕的:任何拿到数据库内容的人直接就获得了用户的密码,他们可以任意的使用之,无论是登录当前的应用还是在其他应用上用相同的用户标识和密码碰运气。如此,之前的我的 Pass 系列中大部分的实现都是糟糕的。
除此之外,像是第二个实现中使用的加密保存,即使在除去了使用自己“发明”的加密算法进行处理这一应该坚决反对的方式之后,也是糟糕的实践。理由很简单:加密是一个可逆函数,任何称为加密的操作都有对应的解密过程,这意味着从加密的结果恢复其明文是可能的,即使攻击者可能需要额外的找到一把正确的密钥。除非非常边缘的情况,验证用户的密码并不需要我们将存储的信息恢复成密码的明文,也就不应该使用可逆的加密方式进行保存留下一个潜在的隐患。
相对的,第六个实现使用了双重 SHA 将密码进行 hash,保留 hash 的结果。这种算法从设计上就是不可逆的:部分信息在进行正向运算时已经被丢弃,从结果恢复原始的明文是不可能的。因此,即使 hash 的结果被获取了,攻击者也不可能直接使用其登录到系统中,因为在验证时再次 hash 得到的结果几乎不可能和先前一致;同时,攻击者也不可能从 hash 值中通过某种算法求解出原本的明文。
一言以蔽之,除非极特殊的情况,密码应该被 hash 后存储。
问题已经解决了——一个 hash 的结果是不可能被还原出明文的,我们的防御牢不可破
遗憾的是,并非如此。尽管攻击者不可能从 hash 结果求解出明文,他们仍然有方法从 hash 结果“获取”明文:尝试。尝试所有可能的密码,计算每一个密码的 hash 结果,直到找到某一个密码 hash 后恰好和获得到的 hash 值一致。没错,暴力枚举。彩虹表、散列值字典等预先计算的信息可以极大的加速这一过程,一些常见的密码对应的散列值可能已经登记在字典中,一查即知。而当数据规模庞大时,一次散列值计算的结果可以用来和数据库中的全部数据比对,换言之任何一个密码被恢复都会导致所有使用这一密码的用户的账户可以被攻击者轻松登入。攻击者更可能积累这些宝贵的数据,不断扩充他们的字典,来更快的处理之后遇到的数据库。
给散列函数加点料: Salting
很明显,我们不能容忍这种事情发生。我们希望让攻击者精心准备和长久积累的字典失效,我们希望即使攻击者恢复出了某一个用户的密码,和他使用相同密码的用户仍然是安全的,即使是暂时的——让即使是同一个猜测,攻击者也必须为每一个用户计算一次散列值才能确定是否猜对了这个用户的密码。这将拖慢恢复过程,也就是让其变得昂贵。
这正是整个密码存储防护的核心思想:无效化或最大限度的压制攻击者的加速手段,迫使攻击者为恢复每一个密码都付出庞大的开销,花费大量的时间。假如攻击者恢复一个用户的密码花费的时间导致的成本,来自于如计算硬件的寿命损耗、能源的费用等等,超过了取得这个用户密码带来的价值,这次恢复对攻击者而言就是亏本的,这会自然的阻止攻击者进行尝试。同时,足够长的时间开销能够为应用和用户争取潜在的宝贵反应时间,来临时封锁信息泄露的账号或修改它们的密码。
当攻击者只能通过应用正规的途径,如尝试登录的方式枚举时,有各种控制手段来予以限制:限制尝试次数,限制间隔时间,使用验证码阻止自动尝试,自动锁定账号等等,但当密码数据库被他人获得时,只有在存储方式上做些文章才可能拖慢攻击者的脚步。
这时,我们有这样的一个方法:加盐。简单来说,加盐是在将密码进行散列函数计算之前首先将其进行一定变换的过程。这种变换可能是直接在密码前前缀一段字节:显然,这将使散列得到的结果变得不同。如果我们为每一个用户的密码加上不同的盐,那么即使这些用户使用的密码是完全相同的,加上的盐也会是散列得到的结果变得不同;更妙的是,当每一个用户使用不同的盐值,预先计算的数据将会失去价值——不同的盐值下即使是相同的密码也会得到不同的结果,最常见的密码 123456 也会因为加了盐而变得对字典非常陌生,除非攻击者能为每一个不同的盐值构建一个字典,然而,想象一下这背后的开销吧。如果要覆盖所有可能,盐值的长度每增加一个二进制位期望上都会使数据库的大小翻倍。这一增长速度很快就会使所需字典的大小压倒性的不可接受。
因此,密码应该被加盐后进行 hash ,且所加的盐必须具有足够的长度,为每一个用户独立的随机生成。
记住了: md5 加盐……
尽管已经在论坛混了半年有余,我个人却未曾看过一个小甲鱼的教程,无论是文章还是视频。然而从一些朋友的发帖中,推断小甲鱼曾经介绍过使用 md5 进行散列存储密码的方式。尽管没有检查实现的细节,论坛的登录过程中密码也首先在本地使用 md5 进行散列后再向服务器发送。这实际上是我的另一个备选题目:不止 md5 。
md5 在超过十年前就已经被公认为不安全的:它在对碰撞,即多个不同的明文值被散列到同一个散列值的情况,的抵御能力不足。通过恰当的设计,攻击者可能能够构造一个可附载任意内容的信息,使其和另一个信息具有相同的散列值,从而骗过基于 md5 的比对检测,如果使用得当这可以对特定的场景构成严重的威胁。
SHA-1 是另一个不安全的散列函数的例子,同样来自对碰撞的不健壮性。这种不健壮性的具体表现和含义是复杂的,在这里不会深入讨论,但公认的观点皆认为以上的两种散列函数是不安全的,应该被避免使用,尤其是安全相关的用途。
如果拥有足够活跃的思维,可能会发现这种碰撞实际上对于密码而言似乎并不致命。可能有多组字节映射到同一个散列值上,但为了恢复其中的任何一个,攻击者仍然需要枚举大量可能的字节串来碰运气。然而实质上,即使是第六个,相对出色的 SHA-256+SHA-512 双重散列的实现,在排除未进行加盐操作后仍然不是足够好的实践。这些算法对密码存储而言不够安全的根本原因在于这些算法是信息摘要算法,它们在设计时的目标之一是要能够以非常高的速度完成计算,同时其中的部分还考虑了算法要能够以非常简单的电路实现。这使得使用这些函数进行一次散列计算所开销的时间非常少,而对于一般的攻击者而言容易获得的高性能 GPU,乃至专业攻击者可能使用的专用高性能定制电路,能够通过强大的并行能力、专为计算散列值设计优化的电路在短时间内完成海量的计算。即使是常见的家用级 GPU 也可以在每秒内轻松的完成数十亿次 SHA-512 的计算,一个实际的例子是先前的第五个实现中压缩包密码的破解就是通过强大的 hashcat 工具利用 GPU 在数十秒内完成了密码恢复。
和这些算法相反,适用于密码存储的散列函数以计算速度慢且无法从算法层面上优化加速为设计目标,旨在让每一次计算的过程不可避免的显著变慢。同时,这些函数还通常包含一些参数可供控制其工作的时间开销,使其可以“想有多慢就有多慢”。
这类函数,包括 Argon2, yescrypt, scrypt, bcrypt 以及较老的 PBKDF2 等,应该是进行密码保存时首选的散列函数。其中先进的算法如 Argon2 出于考虑到了 PBKDF2 等算法未纳入考量的定制硬件并行计算加速等威胁的原因应该被更加优先的考虑。这些函数通常会带有一组推荐的参数配置来让其慢到能达成适当的安全性,然而必须认识到这种参数总体上是需要随着时间而增长的:硬件的性能在不断得到强化,面积和功耗在减小,需要使算法变得更慢来填补这种“降维打击”。了解各个参数的含义,随后在推荐参数的基础上尝试一些组合,挑选一组使实际计算时间在可接受范围内尽可能慢的参数通常可以取得合理的参数组合。
选择公认可靠的,专为保存密码而设计的散列函数,并为其设定适当的参数值。
一个通用考量
有这样一个常常被忽视然而同样致命的攻击方式,每当引入了某个会显著降低运行速率、可能导致某些用户无法使用服务的机制,像是被设计为要计算的足够慢的散列函数,像是数次失败的登陆尝试后临时禁用账户,时,都应该考虑到可能会导致受到这一攻击的风险增大,并分析其影响严重程度和解决方案。这种攻击就是拒绝服务攻击。
当使用恰当参数的合适散列函数时,每次验证用户输入的密码是否正确都需要进行大量的计算、内存访问等操作。如果恶意的攻击者大量的尝试进行认证,短时间内大量的验证操作就有可能压垮应用的服务器端。
一种可能合理的解决方案是将这些操作交给客户端完成:客户端进行这些相对昂贵的计算,将计算结果发送给服务端验证。这将主要的计算开销交给了客户端从而减小服务端被压垮的可能,而其中隐藏的对安全性至关重要的细节留待读者根据前文的分析自行领悟(笑)。
我还要做的更好!先用 SHA-224 散列输入再用 bcrypt……
作为一个经典案例,还是有拿来分析的价值的。很多开发者可能会想到组合一些公认较好的方法来达到更好的安全,殊不知这可能反而会引入漏洞。
在例子的情况中, bcrypt 的实现会在遇到输入中的 0x00 字节时回到输入头部重新开始读入,直到填满内部的一个缓冲区。这意味着当输入中存在 0x00 时,之后的全部内容都会被忽略。对于直接输入的密码,这不构成任何问题,因为人们不会使用空字节作为他们密码的一部分,这种尝试也应该被正确的过滤掉。但当对密码首先进行一次 SHA-2 散列操作时,空字节就有可能出现在 bcrypt 的输入中,导致意料之外的大量碰撞,极大的减少寻找到一组产生相同最终散列输出的输入期望的尝试次数。换言之,两者结合后的安全性反而大打折扣。
密码学算法,无论是原理还是实现,往往有着深刻而复杂的背景。除非深入的了解整个机制和过程,否则,也就是对于绝大多数人来说,使用专家设计并经过审计验证的算法和实现,不要自作聪明的使用任何自己设计的算法或实现。
将目光放长远
算法可能在将来被证实存在安全性漏洞,新的攻击方式可能会使其变得不安全,硬件的发展可能要求增大参数的取值来应对,新的、更好的算法可能被提出:事物总是在不断的发展和变化过程中,密码的存储也需要具有与时俱进的能力。对于一个预期长久运行的应用,从设计时就应该考虑到密码存储方式的更新。
考虑到更新会是一个持续的过程,应该使用某种固定格式的表示方式来存储散列值相关的元数据,如散列函数、散列函数的参数、使用的盐值等等,从而可以在同一个数据库中同时存放更新前后的散列值而不会对验证过程带来障碍。
由于无法从散列值恢复出密码明文,需要在用户下次登陆时方可能用新的算法重新散列密码明文,导致过时、脆弱的散列值可能要被存储很长时间,带来潜在的隐患。对安全性要求非常严苛的应用可以直接删除需要更新且在一定时间内仍没有登陆活动散列值,要求受影响的用户在下次登陆时使用其他方式完成认证并重新设置新的密码;不太严苛的应用可以选择将过期的散列值直接使用新的方法再次散列,直到用户下次登陆时将其更新从而略微加强其强度,然而需要注意到组合不同算法时潜在的风险,且需要扩展元数据的表示方式来记录这种迭代散列的详细信息,确保能够正确验证之。
这是一个较为复杂、应用相关的工程学问题,其核心是在设计时考虑散列值更新的方式,减小过时的散列值存储在数据库中带来的潜在风险
教训,另一方面
大多数人是应用的使用者而不是设计者。大多数应用的设计者使用的应用多于设计的应用。对于使用者这一身份,这些 Pass 又带来了什么样的教训?
确保高强度的密码
即使是最先进最安全的保存方式也无法保护一个使用 123456 作为密码的人,攻击者仍然几乎只需要一瞬间就能够将这种极弱的密码恢复。为了让恢复有合理程度的希望,之前提到的这些实现普遍使用了不超过 8 位纯数字的密码,这种设定是相对较弱,应该在实际中避免的。
一个高强度的密码应该具有足够的熵:足够的长度,以达到应用允许的最长长度为目标;足够宽广的字符范围,选择尽可能多应用允许的字符类型,英文大小写,数字,特殊符号,空白符号等等,如果应用允许汉字、 emoji 等也是很好的选择;足够的不可猜测性,避免包含任何可预测的信息,如完整的单词、个人信息等等。这将最小化其出现在某个字典中的概率,同时最大化搜索到其所需要的时间。
使用不同密码
一个应用应该负起责任保护用户的信息,而在不同应用使用不同的密码能够给你一层额外的保证,来应对它可能没能做到的情况。即使只是做出简单的修改,在不同应用使用不同密码也可以让攻击者恢复出某个应用上你的密码后也无法使用其以你的身份认证到任何其他应用中,至少可以阻挠自动化的进行测试的尝试。
一点总结
网络安全的范围宽广而细节繁多,这里讨论的仅仅是关于密码保存这一非常具体主题的内容。对于应用设计而言,密码保存只是实现安全中的一环,还有大量的其他方面:访问控制,认证授权机制,输入过滤,抵御劫持,阻止信息泄露等等;而对于个人,好的密码习惯更是网络安全中的冰山一角。但愿这些探讨能够成为抛出的砖,引来思考。 支持{:10_275:} 支持{:10_275:} 本帖最后由 hrpzcf 于 2022-12-25 16:07 编辑
个人认为:对于纯本地 app 而言,PassMe 学习意义不大,因为获取它的 password 难度本质上是对抗各种加密算法的难度,然而纯本地 app 有个很大的缺点:对于 Cracker 而言很多时候 Crack 它并不需要还原密码,只需要跳过相关判断逻辑即可!!所以纯本地 app 应该把重点放在 CrackMe 身上而不是 PassMe。 膜拜大佬,看完大佬的帖子后,我想到了一个问题,鱼C论坛的密码是在前端使用md5加密后再发送给后端进行判断,假如后端中储存的也是密码md5加密后内容,那如果有人从数据库拿到了md5加密后的密码,那是不是就可以直接向后端发送md5加密后的密码,从而实现登录操作,这种做法与明文储存密码是不是只是不能撞库 hrpzcf 发表于 2022-12-25 15:59
个人认为:对于纯本地 app 而言,PassMe 学习意义不大,因为获取它的 password 难度本质上是对抗各种加密算 ...
是这样的。论坛上的这些“例子”是相对空洞的,它们没有基本的功能,很难映射到实际中的应用上。唯一一个比较接近的是第二个,通过密码认证来获取访问隐藏内容的权限,然而就像您说的,绕过判断,甚至直接提取没有妥善保存的隐藏内容可能比恢复密码更简单直接。这种在纯本地应用中保护应用完整性等通常是尤为困难的,很多方面也已经远超出了密码保存的讨论范围。 临时号 发表于 2022-12-25 16:07
膜拜大佬,看完大佬的帖子后,我想到了一个问题,鱼C论坛的密码是在前端使用md5加密后再发送给后端进行判断,假 ...
我们无从得知论坛的登录过程以及保存密码相关信息的方式,但如果数据库中直接保存的即是经过 md5 散列的密码,考虑到没有观察到从后端提供的盐值的过程因此无法在前端加盐进行散列(没有仔细研究前端代码,也许盐值是用户名的函数因此可以无需额外信息完成计算,这种设计可以认为是安全的),这种实现在多种意义上确实是较为糟糕的。就像您说的,尽管这种方式减少了撞库攻击的可行性,但对于论坛本身而言和直接存储明文密码无异。
在客户端进行散列计算是可行的,并且能带来一定的好处:减少链路上被攻击带来的风险(尽管不应该依赖这一方式)、让服务端对密码明文一无所知(因此减少出现差错时后果的严重性)等,但一个好的实践应该在服务端将客户端散列后的结果仍视为明文密码,一个经过散列得到的,理想情况下高熵的优质密码,处理:加唯一的盐,使用合适的算法散列,小心验证输入,避免算法组合时可能出现的问题…… 不支持{:10_321:} 那么厉害的大佬等级才中级鱼油,太不值了
页:
[1]