Tại sao mã này dễ bị tấn công tràn bộ đệm?


148
int func(char* str)
{
   char buffer[100];
   unsigned short len = strlen(str);

   if(len >= 100)
   {
        return (-1);
   }

   strncpy(buffer,str,strlen(str));
   return 0;
}

Mã này dễ bị tấn công tràn bộ đệm và tôi đang cố gắng tìm hiểu tại sao. Tôi nghĩ rằng nó có liên quan đến việc lenđược tuyên bố shortthay vì một int, nhưng tôi không thực sự chắc chắn.

Có ý kiến ​​gì không?


3
Có nhiều vấn đề với mã này. Hãy nhớ lại rằng các chuỗi C được kết thúc bằng null.
Dmitri Chubarov

4
@DmitriChubarov, không null kết thúc chuỗi sẽ chỉ là vấn đề nếu chuỗi được sử dụng sau khi gọi đến strncpy. Trong trường hợp này, nó không phải là.
R Sahu

43
Các vấn đề trong mã này chảy trực tiếp từ thực tế strlenđược tính toán, được sử dụng để kiểm tra tính hợp lệ và sau đó nó được tính lại một cách vô lý - đó là một lỗi DRY. Nếu cái thứ hai strlen(str)được thay thế bằng len, sẽ không có khả năng tràn bộ đệm, bất kể loại nào len. Các câu trả lời không giải quyết được điểm này, họ chỉ xoay sở để tránh nó.
Jim Balter

3
@CiaPan: Wenn chuyển một chuỗi không kết thúc null cho chuỗi đó, strlen sẽ hiển thị hành vi không xác định.
Kaiserludi

3
@JimBalter Nah, tôi nghĩ tôi sẽ để chúng ở đó. Có thể người khác sẽ có cùng quan niệm sai lầm ngu ngốc và học hỏi từ nó. Vui lòng gắn cờ họ nếu họ làm phiền bạn, ai đó có thể đến và xóa họ.
Asad Saeeduddin

Câu trả lời:


192

Trên hầu hết các trình biên dịch, giá trị tối đa của an unsigned shortlà 65535.

Bất kỳ giá trị nào ở trên được bao bọc xung quanh, vì vậy 65536 trở thành 0 và 65600 trở thành 65.

Điều này có nghĩa là các chuỗi dài có độ dài phù hợp (ví dụ 65600) sẽ vượt qua kiểm tra và tràn bộ đệm.


Sử dụng size_tđể lưu trữ kết quả của strlen(), không unsigned shortvà so sánh lenvới biểu thức mã hóa trực tiếp kích thước của buffer. Ví dụ:

char buffer[100];
size_t len = strlen(str);
if (len >= sizeof(buffer) / sizeof(buffer[0]))  return -1;
memcpy(buffer, str, len + 1);

2
@PatrickRoberts Về mặt lý thuyết, vâng. Nhưng bạn phải nhớ rằng 10% mã chịu trách nhiệm cho 90% thời gian chạy, vì vậy bạn không nên để hiệu suất đi trước bảo mật. Và hãy nhớ rằng theo thời gian mã thay đổi, điều này có thể đột ngột có nghĩa là kiểm tra trước đó đã biến mất.
orlp

3
Để ngăn chặn lỗi tràn bộ đệm, chỉ cần sử dụng lenlàm đối số thứ ba của strncpy. Sử dụng strlen một lần nữa là ngu ngốc trong mọi trường hợp.
Jim Balter

15
/ sizeof(buffer[0])- lưu ý rằng sizeof(char)trong C luôn là 1 (ngay cả khi một char chứa một bit gazillion) vì vậy điều đó là thừa khi không có khả năng sử dụng một loại dữ liệu khác. Vẫn ... lời khen cho một câu trả lời hoàn chỉnh (và cảm ơn vì đã phản hồi ý kiến).
Jim Balter

3
@ rr-: char[]char*không giống nhau. Có nhiều tình huống trong đó a char[]sẽ được chuyển đổi hoàn toàn thành a char*. Ví dụ, char[]hoàn toàn giống như char*khi được sử dụng làm kiểu cho các đối số hàm. Tuy nhiên, việc chuyển đổi không xảy ra sizeof().
Dietrich Epp

4
@Controll Bởi vì nếu bạn thay đổi kích thước buffertại một số điểm, biểu thức sẽ tự động cập nhật. Điều này rất quan trọng đối với bảo mật, bởi vì việc khai báo buffercó thể cách dòng kiểm tra trong mã thực tế khá nhiều. Vì vậy, thật dễ dàng để thay đổi kích thước của bộ đệm, nhưng quên cập nhật ở mọi vị trí sử dụng kích thước.
orlp

28

Vấn đề là ở đây:

strncpy(buffer,str,strlen(str));
                   ^^^^^^^^^^^

Nếu chuỗi lớn hơn độ dài của bộ đệm đích, strncpy vẫn sẽ sao chép nó qua. Bạn đang căn cứ số lượng ký tự của chuỗi là số cần sao chép thay vì kích thước của bộ đệm. Cách chính xác để làm điều này là như sau:

strncpy(buffer,str, sizeof(buff) - 1);
buffer[sizeof(buff) - 1] = '\0';

Điều này làm là giới hạn số lượng dữ liệu được sao chép vào kích thước thực của bộ đệm trừ đi một ký tự cho ký tự kết thúc null. Sau đó, chúng tôi đặt byte cuối cùng trong bộ đệm thành ký tự null dưới dạng bảo vệ được thêm vào. Lý do cho điều này là do strncpy sẽ sao chép tối đa n byte, bao gồm cả null kết thúc, nếu strlen (str) <len - 1. Nếu không, thì null không được sao chép và bạn có một kịch bản sự cố vì bây giờ bộ đệm của bạn đã bị lỗi chuỗi.

Hi vọng điêu nay co ich.

EDIT: Sau khi kiểm tra thêm và nhập từ người khác, mã hóa có thể cho chức năng sau:

int func (char *str)
  {
    char buffer[100];
    unsigned short size = sizeof(buffer);
    unsigned short len = strlen(str);

    if (len > size - 1) return(-1);
    memcpy(buffer, str, len + 1);
    buffer[size - 1] = '\0';
    return(0);
  }

Vì chúng ta đã biết độ dài của chuỗi, chúng ta có thể sử dụng memcpy để sao chép chuỗi từ vị trí được tham chiếu bởi str vào bộ đệm. Lưu ý rằng trên mỗi trang hướng dẫn cho strlen (3) (trên hệ thống FreeBSD 9.3), nội dung sau được nêu:

 The strlen() function returns the number of characters that precede the
 terminating NUL character.  The strnlen() function returns either the
 same result as strlen() or maxlen, whichever is smaller.

Điều tôi giải thích là độ dài của chuỗi không bao gồm null. Đó là lý do tại sao tôi sao chép len + 1 byte để bao gồm null và kiểm tra kiểm tra để đảm bảo rằng độ dài <kích thước của bộ đệm - 2. Trừ đi một vì bộ đệm bắt đầu ở vị trí 0 và trừ đi một bộ đệm khác để đảm bảo có chỗ trống cho null

EDIT: Hóa ra, kích thước của một cái gì đó bắt đầu bằng 1 trong khi truy cập bắt đầu bằng 0, vì vậy -2 trước đó không chính xác vì nó sẽ trả về lỗi cho bất cứ điều gì> 98 byte nhưng nó phải> 99 byte.

EDIT: Mặc dù câu trả lời về một ký hiệu không dấu thường nói chung là chính xác vì độ dài tối đa có thể được biểu thị là 65.535 ký tự, nhưng điều đó không thực sự quan trọng bởi vì nếu chuỗi dài hơn đó, giá trị sẽ bao quanh. Giống như lấy 75.231 (là 0x000125DF) và che giấu 16 bit hàng đầu mang lại cho bạn 9695 (0x000025DF). Vấn đề duy nhất mà tôi thấy với điều này là 100 ký tự đầu tiên vượt qua 65.535 vì kiểm tra độ dài sẽ cho phép sao chép, nhưng nó sẽ chỉ sao chép tối đa 100 ký tự đầu tiên của chuỗi trong mọi trường hợp và vô hiệu hóa chuỗi . Vì vậy, ngay cả với vấn đề xoay vòng, bộ đệm vẫn sẽ không bị tràn.

Điều này có thể hoặc không tự nó gây ra rủi ro bảo mật tùy thuộc vào nội dung của chuỗi và bạn đang sử dụng nó để làm gì. Nếu đó chỉ là văn bản thẳng mà con người có thể đọc được thì nhìn chung không có vấn đề gì. Bạn chỉ cần có được một chuỗi cắt ngắn. Tuy nhiên, nếu đó là một cái gì đó như một URL hoặc thậm chí là một chuỗi lệnh SQL, bạn có thể gặp vấn đề.


2
Đúng, nhưng đó là vượt quá phạm vi của câu hỏi. Mã cho thấy rõ chức năng được thông qua một con trỏ char. Ngoài phạm vi của chức năng, chúng tôi không quan tâm.
Daniel Rudy

"Bộ đệm trong đó str được lưu trữ" - đó không phải là lỗi tràn bộ đệm , đây là vấn đề ở đây. Và mọi câu trả lời đều có "vấn đề" đó, điều không thể tránh khỏi được đưa ra chữ ký của func... và mọi hàm C khác từng được viết có các chuỗi kết thúc NUL làm đối số. Đưa ra khả năng đầu vào không bị chấm dứt NUL là hoàn toàn không biết gì.
Jim Balter

"Điều đó nằm ngoài phạm vi của câu hỏi" - điều đáng buồn là vượt quá khả năng hiểu biết của một số người.
Jim Balter

"Vấn đề là ở đây" - bạn nói đúng, nhưng bạn vẫn thiếu vấn đề chính, đó là thử nghiệm ( len >= 100) đã được thực hiện đối với một giá trị nhưng độ dài của bản sao được đưa ra một giá trị khác ... điều này là vi phạm nguyên tắc DRY. Gọi đơn giản là strncpy(buffer, str, len)tránh khả năng tràn bộ đệm và hoạt động ít hơn strncpy(buffer,str,sizeof(buffer) - 1)... mặc dù ở đây nó chỉ tương đương chậm hơn memcpy(buffer, str, len).
Jim Balter

@JimBalter Nó nằm ngoài phạm vi của câu hỏi, nhưng tôi lạc đề. Tôi hiểu rằng các giá trị được sử dụng bởi thử nghiệm và những gì được sử dụng trong strncpy là hai giá trị khác nhau. Tuy nhiên, thực tiễn mã hóa nói chung nói rằng giới hạn sao chép phải là sizeof (bộ đệm) - 1 vì vậy không có vấn đề gì về độ dài của str trên bản sao. strncpy sẽ dừng sao chép byte khi nó đạt null hoặc sao chép n byte. Dòng tiếp theo đảm bảo rằng byte cuối cùng trong bộ đệm là null char. Mã này là an toàn, tôi đứng trước tuyên bố trước đây của tôi.
Daniel Rudy

11

Mặc dù bạn đang sử dụng strncpy, độ dài của điểm cắt vẫn phụ thuộc vào con trỏ chuỗi đã qua. Bạn không biết chuỗi đó dài bao nhiêu (vị trí của bộ kết thúc null so với con trỏ, nghĩa là). Vì vậy, gọi strlenmột mình mở ra cho bạn dễ bị tổn thương. Nếu bạn muốn an toàn hơn, hãy sử dụng strnlen(str, 100).

Mã đầy đủ được sửa sẽ là:

int func(char *str) {
   char buffer[100];
   unsigned short len = strnlen(str, 100); // sizeof buffer

   if (len >= 100) {
     return -1;
   }

   strcpy(buffer, str); // this is safe since null terminator is less than 100th index
   return 0;
}

@ user3386109 Bạn strlencũng sẽ không truy cập vào cuối bộ đệm chứ?
Patrick Roberts

2
@ user3386109 những gì bạn đang chỉ ra làm cho câu trả lời của orlp không hợp lệ như của tôi. Tôi không biết tại sao strnlenkhông giải quyết được vấn đề nếu những gì orlp đang gợi ý được cho là đúng.
Patrick Roberts

1
"Tôi không nghĩ strnlen giải quyết bất cứ điều gì ở đây" - tất nhiên là có; nó ngăn chặn tràn buffer. "vì str có thể trỏ đến bộ đệm gồm 2 byte, cả hai đều không phải là NUL." - điều đó không liên quan, vì nó đúng với bất kỳ việc thực hiện nàofunc . Câu hỏi ở đây là về lỗi tràn bộ đệm, không phải UB vì đầu vào không bị chấm dứt NUL.
Jim Balter

1
"Tham số thứ hai được truyền cho strnlen phải là kích thước của đối tượng mà tham số đầu tiên trỏ tới, hoặc strnlen là vô giá trị" - điều này là hoàn toàn và vô nghĩa. Nếu đối số thứ hai cho strnlen là độ dài của chuỗi đầu vào, thì strnlen tương đương với strlen. Làm thế nào bạn thậm chí có được số đó, và nếu bạn có nó, tại sao bạn cần gọi str [n] len? Đó không phải là những gì strnlen dành cho tất cả.
Jim Balter

1
1 Mặc dù câu trả lời này là không hoàn hảo bởi vì nó không tương đương với mã của OP - strncpy NUL-miếng đệm và không NUL chấm dứt, trong khi strcpy NUL-chấm dứt và không NUL-pad, nó không giải quyết được vấn đề, trái với nực cười, bình luận ngu dốt ở trên.
Jim Balter

4

Câu trả lời với gói là đúng. Nhưng có một vấn đề tôi nghĩ đã không được đề cập nếu (len> = 100)

Chà, nếu Len sẽ là 100, chúng tôi sẽ sao chép 100 phần tử mà chúng tôi không có dấu \ 0. Điều đó rõ ràng có nghĩa là bất kỳ chức năng nào khác tùy thuộc vào chuỗi kết thúc thích hợp sẽ đi xa hơn mảng ban đầu.

Chuỗi có vấn đề từ C là IMHO không thể giải quyết được. Dù sao bạn cũng có một số giới hạn trước cuộc gọi, nhưng thậm chí điều đó sẽ không giúp ích gì. Không có giới hạn kiểm tra và vì vậy tràn bộ đệm luôn có thể và không may sẽ xảy ra ....


Chuỗi có vấn đề có thể giải được: chỉ cần sử dụng các hàm thích hợp. I E. không phải strncpy() và bạn bè, nhưng các chức năng phân bổ bộ nhớ như strdup()và bạn bè. Chúng nằm trong tiêu chuẩn POSIX-2008, vì vậy chúng khá di động, mặc dù không có sẵn trên một số hệ thống độc quyền.
cmaster - phục hồi monica

"Bất kỳ chức năng nào khác tùy thuộc vào chuỗi kết thúc phù hợp" - bufferlà cục bộ của chức năng này và không được sử dụng ở nơi khác. Trong một chương trình thực tế, chúng ta sẽ phải kiểm tra xem nó được sử dụng như thế nào ... đôi khi không kết thúc NUL là chính xác (việc sử dụng strncpy ban đầu là tạo các mục nhập thư mục 14 byte của UNIX - được đệm NUL và không kết thúc NUL). "Chuỗi có vấn đề từ C là IMHO không thể giải quyết được" - trong khi C là một ngôn ngữ tuyệt vời đã bị vượt qua bởi công nghệ tốt hơn nhiều, mã an toàn có thể được viết trong đó nếu sử dụng đủ kỷ luật.
Jim Balter

Quan sát của bạn dường như sai lầm. if (len >= 100)là điều kiện khi kiểm tra thất bại , không phải khi nó vượt qua, điều đó có nghĩa là không có trường hợp nào chính xác 100 byte không có bộ kết thúc NUL được sao chép, vì độ dài đó được bao gồm trong điều kiện lỗi.
Patrick Roberts

@ cmaster. Trong trường hợp này bạn đã sai. Nó không thể giải quyết được, bởi vì người ta luôn có thể viết được giới hạn. Có, đó là hành vi không được thừa nhận nhưng không có cách nào để ngăn chặn hoàn toàn.
Friedrich

@Jim Balter. Không quan trọng. Tôi có khả năng có thể viết trên giới hạn của bộ đệm cục bộ này và do đó luôn có thể làm hỏng một số cơ sở dữ liệu khác.
Friedrich

3

Ngoài các vấn đề bảo mật liên quan đến việc gọi strlennhiều lần, người ta thường không nên sử dụng các phương thức chuỗi trên các chuỗi có độ dài được biết chính xác [đối với hầu hết các hàm chuỗi, chỉ có một trường hợp thực sự hẹp mà chúng nên được sử dụng - trên các chuỗi có tối đa chiều dài có thể được đảm bảo, nhưng độ dài chính xác không được biết]. Khi đã biết độ dài của chuỗi đầu vào và độ dài của bộ đệm đầu ra, người ta sẽ tìm ra mức độ lớn của một vùng sẽ được sao chép và sau đó sử dụng memcpy()để thực sự sao chép trong câu hỏi. Mặc dù có thể có khả năng strcpyvượt trội hơn memcpy()khi sao chép một chuỗi chỉ 1-3 byte hoặc hơn, nhưng trên nhiều nền tảng memcpy()có thể sẽ nhanh hơn gấp đôi khi xử lý các chuỗi lớn hơn.

Mặc dù có một số tình huống mà bảo mật sẽ phải trả giá bằng hiệu năng, nhưng đây là tình huống mà cách tiếp cận an toàn cũng nhanh hơn. Trong một số trường hợp, có thể hợp lý khi viết mã không an toàn trước các đầu vào có hành vi kỳ quặc, nếu mã cung cấp đầu vào có thể đảm bảo chúng sẽ hoạt động tốt và nếu bảo vệ chống lại các đầu vào có hành vi xấu sẽ cản trở hiệu suất. Đảm bảo rằng độ dài chuỗi chỉ được kiểm tra một lần giúp cải thiện cả hiệu suất và bảo mật, mặc dù có thể thực hiện thêm một điều nữa để giúp bảo vệ an ninh ngay cả khi theo dõi độ dài chuỗi theo cách thủ công: đối với mọi chuỗi được dự kiến ​​là null, hãy viết rõ ràng theo dõi null hơn mong đợi chuỗi nguồn có nó. Vì vậy, nếu một người đang viết strduptương đương:

char *strdupe(char const *src)
{
  size_t len = strlen(src);
  char *dest = malloc(len+1);
  // Calculation can't wrap if string is in valid-size memory block
  if (!dest) return (OUT_OF_MEMORY(),(char*)0); 
  // OUT_OF_MEMORY is expected to halt; the return guards if it doesn't
  memcpy(dest, src, len);      
  dest[len]=0;
  return dest;
}

Lưu ý rằng câu lệnh cuối cùng thường có thể được bỏ qua nếu memcpy đã xử lý len+1byte, nhưng một luồng khác là sửa đổi chuỗi nguồn, kết quả có thể là chuỗi đích không kết thúc NUL.


3
Bạn có thể vui lòng giải thích các vấn đề bảo mật liên quan đến việc gọi strlennhiều lần không?
Bogdan Alexandru

1
@BogdanAlexandru: Một khi người ta đã gọi strlenvà thực hiện một số hành động dựa trên giá trị được trả về (đó có lẽ là lý do để gọi nó ở vị trí đầu tiên), thì một cuộc gọi lặp lại (1) sẽ luôn mang lại câu trả lời giống như câu đầu tiên, trong trường hợp đó đơn giản là lãng phí công việc, hoặc (2) đôi khi có thể (vì một thứ khác - có thể là một luồng khác - đã sửa đổi chuỗi trong khi đó) mang lại một câu trả lời khác, trong trường hợp mã đó thực hiện một số điều có độ dài (ví dụ: cấp phát bộ đệm) có thể giả sử kích thước khác với mã thực hiện các thao tác khác (sao chép vào bộ đệm).
supercat
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.