PwNo0b
đây là bài viết nói về 1 số kiến thức liên quan đến pwnable mà tôi đạt được trong quá trình chơi ctf. CTF là gì? well, nếu các bạn đang đọc bài viết này chắc cũng không cần tôi phải giải thích nữa nhỉ. Sau khi team được thành lập thì do biết rằng các thành viên trong team tôi không có ai rõ ràng về mảng này (ngay cả tôi) thế là tôi quyết định dời concentration của mình 1 chút sang mảng này(dù sao nó cũng là kiến thức).
pwnable là gì? theo blogger Trái Ổi thì pwn là kỹ thuật tập trung vào việc attack vào nền tảng hệ thống OS. VD 1 vài lỗi kĩ thuật như bufferoverflow, format string, …
###Yêu cầu cần thiết
- Có nền tảng cơ bản về ngôn ngữ Assembly, C (python càng tốt)
- Biết dùng các loại tool như: IDA, OllyDBG, GDB, radare2,…
- Kiên trì trước mỗi chal mà ta gặp, nếu đã làm đủ mọi cách mà không solve được thì xem writeup, xem xong phải làm lại sau đó thì có 2 cách
- Tìm kiếm những chal khác có tương tự lỗi như vậy và khai thác nó
- tự mình viết code C rồi dịch sang assembly để solve
- Solved rồi thì các bạn hãy tự nghiệm lại những gì mình học được thông qua chal rồi hãy viết writeup về chal đó cho dù chal đó đã có hàng tá writeup tương tự trên mạng, nhưng vì cái này là bạn viết dành cho bạn nên đâu cần ngại ngùng gì?
- Nên lập mục tiêu cho bản thân mình VD: 1 ngày ít nhất phải solved 1 chal
- Quan trọng nhất: Biết tra google
Ở đây tôi không có ý chỉ riêng một cá nhân nào cả, nhưng tôi cũng đã từng trải qua giai đoạn đó giống như các bạn, không có cái cóc gì trong đầu liên quan về ctf cả. Đâu phải ai cũng là thánh thần cái gì cũng biết nên Google xuất hiện. Vậy sao không tận dụng nó như một nơi lưu trữ kiến thức cho mình?
###Knowledge
Vì mới dấn thân vào pwn chưa được lâu nên tôi chỉ có thể tổng kết sơ qua những gì tôi đã tích góp được qua những lần googling bục mặt cho 1 chal. Sau đây là 1 số kiến thức mà tôi sẽ nói,
nếu các bạn có thấy gì sai thì liện hệ mình để mình có thể sửa một cách sớm nhất!!!
$ASSEMBLY
Đầu tiên, hãy nói về assembly(hay còn được gọi là hợp ngữ). ASM là ngôn ngữ bậc thấp dùng các mnemonics để viết chỉ thị (instruction) từ mã máy của máy tính (mã nhị phân) //theo techtalk
VD 1 số dòng lệnh
PUSH rbp
lệnh push dùng để đẩy giá trị vào trong stack, như VD trên thì đẩy value của thanh ghi (register) vào trong stack
POP eax
khác hoàn toàn với lệnh push, pop sẽ lấy giá trị từ register rồi gán vào thanh ghi eax
$STACK
stack hoạt động giống như 1 chồng đĩa vậy, thằng nào vào cuối cùng thì thằng đó được bốc ra đầu tiên hay còn được gọi là phương pháp LIFO (last in, first out)
địa chỉ của buffer sẽ trải dài từ giá trị 0x00000000–0xFFFFFFFF
trong đó:
- kernel là nơi ta thực hiện command-line param , là cái mà được pass vào chương trình (program) và biến môi trường (environment variable)
- text là nơi chứa mnemonics, instruction
- data là nơi chứa biến chưa khởi tạo giá trị (uninitialized variables) và biến đã khởi tạo giá trị (initialized variables)
- heap là nơi chứa các loại data lớn như images, files,…
- stack là nơi chứa biến local cho các hàm (function). Khi 1 function được gọi, những biến này sẽ đẩy vào cuối stack
source:
https://www.coengoedegebure.com/buffer-overflow-attacks-explained/
$giải thích quá trình Buffer Overflow
ta có đoạn code sau:
void main(){
char buf[32];
gets(buf);
printf("you typed: %s",bùf);}
bây giờ ta biên dịch chương trình ( tắt chức năng stack protector để làm việc dễ dàng hơn
gcc test.c -o test -fno-stack-protector
như ta thấy, terminal của chương trình warning chúng ta về việc dùng lệnh gets
có thể gây nguyên hiểm như thế nào.
giờ thì ta hãy disassemble hàm main xem trong đó nó có gì
disas main
à trước tiên thì ta hãy set lại cách nhìn của chương trình theo kiểu intel với lệnh
set disassembly-flavor intel
lệnh trên đơn giản chỉ là thay đổi cách nhìn các instruction của chương trình, như sau là cách nhìn của hàm main đối với kiểu AT&T
đối với người không quen assembly thì nhìn cái này khá là intimidating, đau mắt nữa (LOL!!!)
còn đây là theo kiểu intel
Phew, đỡ hơn rồi một chút rồi. để tôi giải thích sơ về cái này
hàng bên trái là địa chỉ của các instruction, để tiện việc disassemble thì kế bên nó có hiện <+n>, tạo việc thuận tiện cho ta tạo breakpoint. Còn hàng bên phải là các instruction của dòng lệnh.
Do chúng ta mới bước chân vào pwnable nên ta chỉnh cần quan tâm nhất đó chính là cái mà ta thấy rõ, dễ hiểu nhất. Hãy để ý hàm <main +20>, tại đây khi ta step qua lệnh này thì chương trình sẽ thực hiện hàm gets
, và đó chính là nơi nó sẽ xảy ra buffer overflow. nhìn lại trước đó ta nhìn lại các dòng instruction
0x0000000000001145 <+0>: push rbp
0x0000000000001146 <+1>: mov rbp,rsp
0x0000000000001149 <+4>: sub rsp,0x20
0x000000000000114d <+8>: lea rax,[rbp-0x20]
sub
, lệnh này có nghĩa là lấy address value của thanh ghi rbp trừ cho 0x20(32d), lea
, gán vào thanh ghi rax và đó chính là độ dài của biến buf. ta có độ dài của biến buf là 32 bytes kí tự nhưng ta nhập tới 36 ký tự lận, tại sao chương trình lại không báo lỗi???
vì 4 bytes kí tự đó chính là giá trị rbp mà tôi đã đề cập trên, vì hàm gets đã làm tràn qua địa chỉ bộ nhớ chứa giá trị của rbp
không dông dài nữa, bây giờ ta sẽ bắt đầu run chương trình.(lưu ý tại đây địa chỉ mỗi máy có thể sẽ khác nhau). Dùng lệnh ni (next instruction) để step qua các dòng code.
lúc này chương trình đang dừng ngay tại hàm gets
, bây giờ ta sẽ kiểm tra xem stack frame của chương trình nó như thế nào(các bạn hãy tìm hiểu thêm cách hoạt động của stack frame)
lệnh x dùng để hiện thị contents của memory(ví dụ như trên là các thanh ghi $rsp, $rbp), x/50xg có nghĩa là sẽ hiện thị 50 blocks giá trị của stack frame dưới dạng hex, và size của giá trị là g — giant(64bits)
như trên thì ta đã xác định được địa chỉ của rbp, bây giờ hãy step qua lệnh call và nhập input
aaaabbbccccddddeeeeffffgggghhhhiiiijjjjkkkkllll
giờ kiểm tra stack frame
Wow, bây giờ địa chỉ của rbp đã bị đè rồi !!!
bây giờ ta tiếp tục step nào.
lúc này thanh rip đang trỏ vào địa chỉ của ret. Bạn thử suy nghĩ xem lúc này chương trình sẽ như thế nào?
Báo lỗi vì chúng ta đã overflow sang return address của chương trình rồi!!!
vì return address của chương trình được push vào trước tiên so với giá trị của thanh rbp (hãy tìm hiểu về stack frame)
vậy giả sử, nếu trong chương trình có 1 hàm như sau:
void win(){
printf("you did it!");
}
vì giờ ta đã có thể overflow được ret addr của file nên ta có thể điểu khiển flow của chương trình đi theo hướng mà mình muốn!!!
giả sử địa chỉ của hàm win là 0x0000000012345678
, thay vào địa chỉ ret trên ta sẽ control được flow của chương trình để nó có thể printf ra đoạn
you did it!
Hay kĩ thuật này còn được gọi là RET2RET
$format string
để có thể hiểu được lỗi format string này, trước tiên ta phải hiểu được mục đích của nó
#chức năng
- nó sẽ convert datatypes đơn giản trong ngôn ngữ C thành 1 chuỗi đại diện cho datatypes đó
- nó cho phép xác định định dạng của chuỗi được đại diện
- process the resulting string (output to stderr, stdout, syslog, …) (dòng này khó dịch quá LOL)
#cách thức hoạt động
- format string dùng để control các động thái mà 1 hàm có thể xảy ra
- nó sẽ xác định loại input mà ta nhập vào như thế nào, sau đó mới in nó ra
- các input đó (hay còn gọi là parameters) được push vào stack của chương trình
- được lưu 1 cách trực tiếp (giá trị), hay gián tiếp (địa chỉ)\
note: hàm call của format string
ta phải biết được bao nhiêu params đã được push vào trong stack, biết khi nào hàm format được trả về.
Nói huyên thuyên như thế, vậy format string nghĩa là gì?
format string là 1 chuỗi ASCII bao gồm text và format params
VD: printf("The magic number is %d\n",911)
đoạn text chính là the magic number is
còn format params chính là %d
,được thay thế bởi số 911.
1 số dạng format params
#sự liên quan của stack đối với format string (FS)
VD: ta có đoạn code sau
printf (“Number %d has no address, number %d has: %08x\n”, i, a, &a)
thì lúc này stack frame của chương trình sẽ là
bây giờ hàm format sẽ “đóng gói” FS “A”, bằng cách đọc từng ký tự mỗi lần. Nếu ký tự đó không phải là %
thì nó sẽ được in ra output. Còn nếu phải, ký tự đằng sau của %
sẽ được xác định giá trị trả về của param.
%%
được dùng để in%
#format string vulnerability
để có thể rõ ràng hơn ta sẽ viết thử 1 đoạn code đơn giản để làm rõ các ký tự trên
vậy là các bạn đã rõ được cách fs hoạt động như thế nào rồi. Bây giờ tôi sẽ dùng đoạn code khác trong đó có fmt vuln
bây giờ nhiệm vụ của chúng ta đó chính là dùng fmt-vuln để sửa giá trị của biến a. Theo thông thường thì với đoạn code như trên chúng ta không có cách nào mà có thể để làm được điều đó
test thử program
hm, địa chỉ của biến a luôn thay đổi sau mỗi lần exec, chính là do ASLR — address space layout randomization, nghĩa là mỗi lần chương trình exec, thì memory layout sẽ luôn random thành 1 kết quả khác, vậy để tiện làm việc thì tôi sẽ tắt chế độ này đi
OK, sounds good enough
vậy bây giờ việc của ta là vị trí của fs trong stack
vậy địa chỉ của hàm fs nằm ở vị trí thứ 7 vậy ta đã có sơ bộ được payload của chương trình
nhưng nếu lỡ vị trí của fs nó nằm ở 1000 thì sao? không lẽ ta lại input %x
1000 lần?
cũng may sao chúng ta có được cách viết khác
%7$x
okay, test thử
hmm, weird, tại sao lại không có gì nhỉ? đó là vì $
là 1 kí tự đặc biệt nên chúng ta cần thêm \
để escape nó
Noice!
updating…
Related:
https://bitvijays.github.io/LFC-BinaryExploitation.html
những page có thể luyện pwnable:
updating…