反调试技术
NLFlagGlobal PEB的偏移 当被调试的时候会有标志位:
FLG_HEAP_ENABLE_TAIL-CHECK()
FLG_HEAP_ENABLE_FREE_CHECK()
FLG_HEAP_VALIDATE_PARAMETERS()
一般为:
mov eax,fs:[30h]
mov al,[eax+68h]
mov al,70h
cmp al,70h
其实是因为 isDebugger被检测到了 进而影响到了 NTFlagGlobal 从而进行了调试堆的创建
因此三种方法:
手动改注册表的里的flag的值
od中插件
windbg中禁用调试堆
当IsDebugged标志被为TRUE时,NtGlobalFLag也会得到一个0x70的值,消除即可 dump
Heap flags
Heap flags 含有两个标志跟ntflagglobal一起初始化
flags和forceflags
flags的正常值默认为HEAP_GROWABLE(2)
forceflags默认为0
32位程序如果对flags设为2 且对forceflag设为0 但没对subsystem进行检测 就是为了隐藏调试
获取堆的位置:GetProgressHeap()->ntdll.dll:RtlGetprocessHeaps()中的第一个数组返回
或
x86:
mov eax,fs:[30h]
mov eax,[eax+18h]
x64:
push 60h
pop rsi
gs:lodsq
mov eax,[rax+30]
The Heap
堆在初始化的时候会检测heap flags
分堆指针已知和堆指针未知:
已知就直接检查堆的内容 分32位和64位
未知就利用kernal32中的Heapwalk() 或 ntdll的Rtlwalkheap
isDebuggerPresent
存在调试器的时候 isDebuggerPresent 会返回非零值
检测代码:
call IsDebuggerPresent
test al,al
jne being_debugged
绕过:把bing_debugged的值设为0
CheckRemoteDebuggerPresent
CheckRemotDebuggerPresent()用于检测进程是否处于被调试的状态
是:0xfffffff
绕过:修改isDebuggerPresent为false 然后不触发being_debugged:
NtQueryInformationProcess
CheckRemoteDebuggerPresent()内部是调用的ntdll中的NtQueryInformationProcess进行的
因此对CheckRemoteDebuggerPresent绕过可以通过修改NtQueryInformationProcess
原理
CheckRemoteDebuggerPresent->NtQueryInformationProcess->EPROCESS->Debugport字段 返回0xffffffff
ZwSetInformationThread
ZwSetInformationThread 等同于 NtSetInformationThread,通过为线程设置 ThreadHideFromDebugger,可以禁止线程产生调试事件注意到该处
绕过ZwSetInformationThread 函数的第 2 个参数为 ThreadHideFromDebugger,其值为 0x11。调试执行到该函数时,若发现第 2 个参数值为 0x11,跳过或者将 0x11 修改为其他值即可
脱壳技术 压缩壳: esp定律 一步到位 内存镜像
pwn部分
函数调用
先来了解寄存器
共32位 分低16位和高16位
低16位可以分为独立的两个八位的寄存器 AL AH
EIP寄存器是存放下一条执行的指令
ESP始终指向栈顶 随着出栈或入栈 esp会变化
当esp和ebp相等是不是就意味着栈空了呢?
EBP是栈底基质 ebp+0xh。。
然后是如何调用的呢?
先说caller函数
caller函数的栈从上到下是:
callee的返回地址
callee的参数
callee 的ebp
caller的返回地址 要放到eip里保存
实参
在进入被调函数的时候 先把caller的ebp入栈作为基地址
也就是push ebp
随后将esp和ebp归一
也就是
push ebp
mov esp ebp
随后利用esp来开辟空间
ebp - 是向下 因为是高地址向低地址减小
高地址
|
|
|
v
低地址
对应的
参数
返回地址
向上是主调函数的部分
ebp 往下是被调用的函数的部分 :参数 返回地址
|
|
|
V
esp
注意 一句话:每个函数都有自己对应的栈
在调用结束后
mov ebp esp
将ebp和esp指向同一个地方 随后释放局部变量
总结:
首先看主调函数:
主调函数在初始化的时候 先按照参数调用约定依次入栈:从右到左 随后 将EIP入栈保存主调函数的返回地址:
随后在主调函数进入被调函数的时候
先把主调函数的ebp压入栈中(注:这里的ebp里的内容是指向主调函数的栈底)
push $ebp
mov $esp $ebp
随后 将被调函数的参数按照调用约定入栈
后改变esp的值来开辟空间
ebp+ 向上 是主调函数的内容
ebp- 向下 是被调函数的内容
当结束调用的时候
会mov esp ebp
让esp指向ebp 开始释放局部变量
随后 ebp里的内容弹出到eip:ebp里存放的是主调函数的栈底的地址!)所以eip会直接执行到主调函数的栈底 然后让esp保持不变 这里的esp指向的是主调函数压栈后的ebp的位置 也就是 主调函数的栈顶!
首先主调函数要进行函数调用 这个时候把即将执行的下一条指令压入到栈中 之后把自己的ebp里的值 也就是saved ebp压入栈中
push eip
push ebp
随后
mov ebp esp
然后
sub esp xx进行扩展
这是被调函数进行创建的过程
在被调用函数完成之后
将esp指向现在的ebp
mov esp ebp
随后开始释放被调函数的空间
直到esp和ebp相等 这个时候把ebp里的saved ebp 传递给eip
让eip回到主调函数的基地址
然后再进行
mov esp ebp
push eip
push ebp//saved ebp
mov ebp esp
sub esp
mov esp ebp
pop ebp
pop eip
mov esp ebp
注意事项
栈溢出 ret2text
常用命令:
checksec 没开ASLR
然后
gdb
dissamble 查看汇编
b 设置断点
X 打印值
x/20
strat 开始
ni 步入
si 步进
finish 跳出步进
i info 信息
比如有这样一个例子
int vu()
{
printf("1231");
get(s)//s14位
return 0;
}
然后又system函数的地址
假设为 system_addr
然后栈的结构应该是
eip
saved ebp
s[13]
s[12]
s[11]
...
s[0]
因此只要覆盖掉 这些内容
让弹出的eip的值给后门函数就好:
payload=a*14+bbbb(覆盖掉saved ebp)+backdoor()
ROP
rop是个很搞的东西哦~
最基础的rop是:
ret2text:这个不用多说 就是返回到一个system调用函数的地址就好
一般是通过确定可以溢出的元素的地址来判断大小
ret2shellcode:
这个就不太一样了
需要在没开启NX保护的情况下进行自己写shellcode
利用python的库 asm(shellcraft.sh())
ret2syscall
这个才是真正意义上的动用了gadget来完成getshell
在系统调用中以int 0x80为例子
首先要把调用号放到eax里
也就是 放到栈顶
pop eax ;ret
然后传入/bin/sh到ebx里
然后把参数 0和0传入到ecx和edx里 最后跳转到系统调用int0x80的地址上
当执行int0x80系统调用的时候 会触发接下来的寄存器的东西
首先 int0x80去eax找了0xb 随后读取 /bin/sh 0 0
构造出execve("/bin/sh",0,0)
最后getshell
也算是比较经典的gadget了
ret2libc
这是控制程序执行中的libc的函数了 一般是执行binsh
注意在调用函数的时候要给函数一个虚假的返回地址哦
deadbeef
canrry 用格式化字符串泄露
NX 组件ROP
Relro aslr 泄露got表 挟持got表
PIE stack pivoting
heap
brk会重新生成一个堆不与内存合并
而mmpa会生成一个与内存合并的堆
在申请堆的时候
操作系统嫌麻烦就索性给你一个比较大的堆空间
叫arena
后续申请的时候从arena里扣
当arena不足的时候
会brk方式来增加空间 也可以通过brk的方式减少空间
堆的结构:
malloc_chunk
在程序的执行过程中用malloc申请的内存叫chunk
chunk的结构体是:
struct malloc_chunk{
INTERNAL_SIZE_T prev_size;前一个堆的大小
INTERNAL_SIZE_T size;这一个堆的大小
struct malloc_chunk* fd;下一个堆向高地址
struct malloc_chunk* bk;上一个堆
如果空闲↑
如果空闲且大↓
struct malloc_chunk* fd_nextsize;
struct malloc_chunk* bk_nextsize;
}
实际上:
prev_size只有在空闲的时候会被使用
不空闲为前一个chunk的user区域
---prev_size------
--------size-N-M-P
p标志位表示前一个chunk是否被使用 是为1
64位的块大小是0x10的倍数
然后size为是3为单位的所以最后空出来三个标志位
bin chunk被释放后放入bin中
采取LIFO的策略 类似栈
根据chunk的大小和空闲情况分为:
fast bins
small bins
large bins
unsorted bin
TOP chunk
程序在第一次malloc的时候 heap被分为两半
一半给user
另一半叫top chunk 就是处于最高位的chunk
当所有的 bin 都无法满足用户请求的大小时,如果其大小不小于指定的大小,就进行分配,并将剩下的部分作为新的 top chunk
否则,就对 heap 进行扩展后再进行分配。在 main arena 中通过 sbrk 扩展 heap,而在 thread arena 中通过 mmap 分配新的 heap。
特点topchunk的p位始终为1
否则会被前面的chunk合并
last remainder
当arena被分配但是大小不合适的时候
会裁剪掉
剩下到叫last remainder (可以叫碎片吗?
堆溢出是利用释放的时候写回内存的操作来进行的
UAF
use after free
释放后没有被设置为 NULL 的内存指针为 dangling pointer
步骤
申请一个chunk
删除这个chunk
在申请这个chunk
在删除这个chunk
随后申请一个新的chunk
这个时候
第一个chunk未被删除的指针会被利用
off_BYONE
严格来说 off-by-one 漏洞是一种特殊的溢出漏洞,off-by-one 指程序向缓冲区中写入时,写入的字节数超过了这个缓冲区本身所申请的字节数并且只越界了一个字节。off-by-one的产生往往和字符串操作有关。如strlen计算长度时,不会计算最后的"0字节"(\x00)。循环读入时,<写成了<=。
利用思路
溢出字节为可控制任意字节:
通过修改下一个堆块的size造成块结构之间出现重叠,从而泄露其他块数据,或是覆盖其他块数据。也可使用 NULL 字节溢出的方法
溢出字节为 NULL 字节:
在 size 为 0x100 的时候,溢出 NULL 字节可以使得 prev_in_use 位被清,这样前块会被认为是 free 块。
IO_FILE
在标准的I/O库中,在一个程序开始时一般有三个流被自动创建,分别是:stderr,stdin,stdout(分别对应异常检测,标准输入和标准输出)它们都会有一个对应的FILE结构体,而这些FILE结构体通过结构体中的:*struct _IO_FILE _chain;进行连接,其表头是_IO_list_all
struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;
};
可以看见这个结构体里面有两个成员,一个_IO_FILE的结构体,一个_IO_jump_类型的指针,而这个指针vtable非常重要,它就是所谓的虚表。虚函数其实就是为后面对于类的继承提供一个函数的接口,,所以在每一个有虚函数参与的类中都会生成对应的一个虚表,而这个虚表中存放的就是相关操作函数的指针。
IO_puts的主要功能是调用一个IO_sputn 函数,而这个IO_sputn 函数是一个宏定义,它指向 IO_new_file_xsputn(在前面的_IO_jump_t结构体中有提到)的函数调用,所以IO_puts函数最终是要调用 IO_new_file_xsputn这个函数。
IO_new_file_xsputn:
首先计算缓冲区还有多少空间可以进行写入(通过一个: f->IO_write_end - f->_IO_write_ptr 的指针减法来计算),然后将需要写入的数据写入到缓冲区中,之后在检查一下需要写入的数据是不是已经完全写入,如果有剩余就说明缓冲区空间不足或者缓冲区还没有建立,这时通过if判断就需要调用 _IO_OVERFLOW 来进行缓冲区的建立和刷新,而这个 _IO_OVERFLOW 跟前面的 IO_sputn一样是个宏定义,它指向了虚表中的 _IO_overflow_t函数