二进制菜鸟的杂谈-调试与pwn

反调试技术

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函数

相关推荐
惜.己4 小时前
Jmeter中的断言(二)
测试工具·jmeter·1024程序员节
西电研梦14 小时前
考研倒计时30天丨和西电一起向前!再向前!
人工智能·考研·1024程序员节·西电·西安电子科技大学
惜.己14 小时前
Jmeter中的断言(四)
测试工具·jmeter·1024程序员节
·云扬·1 天前
Java IO 与 BIO、NIO、AIO 详解
java·开发语言·笔记·学习·nio·1024程序员节
网安_秋刀鱼1 天前
PHP代码审计 --MVC模型开发框架&rce示例
开发语言·web安全·网络安全·php·mvc·1024程序员节
HUODUNYUN1 天前
小程序免备案:快速部署与优化的全攻略
服务器·网络·web安全·小程序·1024程序员节
惜.己2 天前
Jmeter的后置处理器(二)
测试工具·github·1024程序员节
惜.己2 天前
Jmeter中的断言(一)
测试工具·jmeter·1024程序员节
cainiao0806052 天前
《物理学进展》
1024程序员节·核心期刊·知网期刊·职称评审
FFDUST2 天前
C++ —— string类(上)
c语言·开发语言·数据结构·c++·stl·1024程序员节