1 字符串
1.1 字符串基礎
字符串提供命令行參數、環境變量、控制台輸入、文本文件及網絡連
接,提供外部輸入方法來影響程序的行為和輸出,這也是程序容易出錯的地方。字符串是一個概念,並不是C/C++內置類型,標准C語言庫支持類型為char的字符串和類型為wchar_t的寬字符串。
字符串由一個以第一個空(null)字符作為結束的連續字符序列組成,並
包含此空字符(所以sizeof和strlen會差1)。一個指向字符串的指針實際指向該字符串的起始字符。目標大小,指sizeof(array)大小,注意與元素個數區分。
數組大小。數組帶來的問題之一是確定其元素數量,例如下面的例子:
void clear(int array[])
{
for (size_t i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
{
array[i] = 0;
}
}
void dowork()
{
int dis[12];
clear(dis);
/* ... */
}
array是一個參數,所以它的類型是指針。因此,sizeof(array)等於sizeof(int*),在x86 32機中,sizeof(array) / sizeof(array[0])計算結果都是1。
字符串字面值:簡而言之就是在雙引號中的值,在C中,字符串字面值的類型是一個char數組,但在C++中,它是一個const char數組。所以在C中可以修改字面值,但是程序如果試圖去修改,該行為是未定義的。不要試圖修改字符串字面值,編譯器有時會把多個相同的字符串字面值存儲在相同位置,例如只讀存儲器(ROM)中,看下面例子:
const char *s1 = "abc";
const char *s2 = "abc";
char *s3 = "abc";
char *s4 = "abc";
char s5[] = "abc";
char s6[] = "abc";
比較地址會發現s1,s2,s3,s4相同,用這4個指針去改變字符串字面值是會出問題的。s5,s6值不同
字符數組初始化:不要指定一個用字符串字面值初始化的字符數組的界限
const char s[3] = "abc"; //不安全寫法,少一個'\0'
const char s[] = "abc"; //推薦初始化方式
1.2 C++中的字符串
C++標准類模板std::basic_string。簡單來說就是string(basic_string<char>)
和wstring(basic_string<wchar_t>),basic_string的類的模版特化更不容易出現錯誤和安全漏洞,需要強調的是大多數C++字符串對象被視為不可分割的整體(通常按值傳遞和引用傳遞),內部字符串不一定是以空字符結束(大多數實現是以空字符結尾),C的庫函數都接受以空字符結尾的字符序列指針。
1.3 字符類型
char 是 signed char 還是 unsigned char 可由編譯器的配置項設定
當char有符號時,由unsigned char[]轉換為const char *
當char無符號時,由singned char[] 轉換為const char *
如果不強制轉換會有警告,建議使用普通的char
1.4 字符串的長度
混淆概念容易在C和C++中導致嚴重的錯誤,
wchar_t wide_str1[] = L"0123456789";
wchar_t *wide_str2 = (wchar_t*)malloc(strlen(wide_str1) + 1);
if(wide_str2 == NULL)
{
/*處理錯誤*/
}
free(wide_str2);
wide_str2 = NULL;
對一個以空字符結尾的字節字符串,strlen()統計終止空字節前面的字符數量。然而,寬字符可以包含空字節,所以計算結果會出問題。
使用wcslen可以計算寬字符串的大小
wchar_t wide_str1[] = L"0123456789";
wchar_t *wide_str2 = (wchar_t*)malloc(wcslen(wide_str1) + 1);
if(wide_str2 == NULL)
{
/*處理錯誤*/
}
free(wide_str2);
wide_str2 = NULL;
注意此長度沒有乘sizeof(wchar_t),所以還是不對,下面值最終正確寫法:
wchar_t wide_str1[] = L"0123456789";
wchar_t *wide_str2 = (wchar_t*)malloc((wcslen(wide_str1)+1)*sizeof(wchar_t));
if(wide_str2 == NULL)
{
/*處理錯誤*/
}
free(wide_str2);
wide_str2 = NULL;
2 常見的字符串操作錯誤
2.1 無界字符串復制
void get_y_or_n()
{
char response[8];
puts("Continue? [y] n:");
gets(response);
if(response[0] == 'n')
exit(0);
return;
}
其實gets()函數在C99中以廢棄並在C11中淘汰。它沒有提供方法指定讀入的字符數的限制。這種限制在此函數的如下一致實現中是顯而易見的:
char *gets(char *dest)
{
int c = getchar();
char *p = dest;
while(c != EOF && c != '\n')
{
*p++ = c;
c = getchar();
}
*p = '\0';
return dest;
}
如果輸入超出8個字符,那麼會導致未定義的行為。不要從一個無界源復制數據到定長數組中,禁止這種方法。
2.1.1 復制和連接字符串
例如strcpy(), strcat(), sprintf(), 容易執行無界操作。例如:
int main(int argc, char *argv[])
{
/*argc參數個數,argv參數數組*/
}
當argc大於0,按照慣例,argv[0]指向的字符串是程序名。若argc > 1,則argv[0]~argv[argc-1]引用的就是實際程序參數。
當分配的空間不足以復制一個程序的輸入,就會產生漏洞。攻擊者可以控制argv[0]的內容
int main(int argc, char *argv[])
{
/*argc參數個數,argv參數數組*/
char prog_name[128];
strcpy(prog_name, argv[0]);
/* ... */
}
輸入一個大於128個字節的字符,棧溢出,即緩沖區溢出漏洞。
標准的寫法應該是:
int main(int argc, char *argv[])
{
/* 不要假設argv[0]不許為空 */
const char *const name = argv[0]? argv[0] : "";
char *prog_name = (char*)malloc(strlen(name)+1);
if(prog_name != NULL)
{
strcpy(prog_name, name);
}
else
{
/* 復原 */
}
}
其實還有一種方法可以避免溢出,通過設置域寬可以消除gets()的缺陷
char buf[12];
std::cin::width(12);
std::cin >> buf;
std::cout << buf << std::endl;
2.2 差一錯誤
簡而言之就是從源字符串拷貝內容到目的字符串,剛好最後的'\0'沒有
拷貝到目的字符串中,在這之後對目的串調用C語言庫的函數可能會出問題,即空字符結尾錯誤,其余的還有字符串階截斷誤差,越界操作等。
2.3 字符串漏洞及其利用
大體上就是緩沖區溢出(詳細的可以自己網上查,有很多資料詳細介
紹),棧溢出的話,可以把目標代碼或者數據覆蓋到棧裡面,關於棧為什麼會溢出,其實是因為在編譯後,棧的大小就固定了。這種攻擊方式也稱注入,這裡涉及到匯編以及底層的結構,不做詳細解釋,不過解決方法也有很多,要麼做邊界檢查,要麼動態的分配內存,還有更簡單的那就是直接使用std::basic_string。當然使用string也會出問題,例如迭代器失效。
char input[];
string email;
string::iterator loc = email.begin();
//復制到string對象,同時把";" 轉換成" "
for (size_t i = 0; i < strlen(input); ++i)
{
if(input[i] != ";")
email.insert(loc++, input[i]);
else
email.insert(loc++, ' ');
}
第一次insert之後,loc就已經失效,後面的insert都將產生未定義行為。正確的寫法應該是
char input[];
string email;
string::iterator loc = email.begin();
//復制到string對象,同時把";" 轉換成" "
for (size_t i = 0; i < strlen(input); ++i)
{
if(input[i] != ";")
loc = email.insert(loc, input[i]);
else
loc = email.insert(loc, ' ');
++loc;
}
當然在編程的時候引用邊界之外的元素會拋出一個異常std::out_of _range。另外std::string.c_str()函數可以返回一個以空字符結尾的字符,const值,所以調用free()或者delete()會出錯,需要修改則只能修改副本。
#include<stdio.h>
#include<string.h>
char pass[101];
int main()
{
int i;
char s[101],key[101];
puts("設定密碼:");
scanf("%s",pass);
puts("輸入源字符串:");
scanf("%s",s);
puts("輸入密碼:");
scanf("%s",key);
while(strcmp(pass,key))
{
/*for(i=0;i<strlen(s);i++)
printf("%d ",s[i]+4);
puts("");*/
puts("密碼錯誤!重新輸入:");
scanf("%s",key);
}
puts(s);
for(i=0;i<strlen(s);i++)
printf("%d ",s[i]);
puts("");
}
其實 linux 和 windows 的系統函數都是C函數,並且提供了GB2312toUTF-8的函數,所以C語言是可以實現轉碼的。以下是windows的例子:int num = ::MultiByteToWideChar(CP_ACP, 0, "你好", -1, NULL, 0);wchar_t* m_arrayShort = new wchar_t[num];::MultiByteToWideChar(CP_ACP, 0, "你好", -1, m_arrayShort, num); int len = ::WideCharToMultiByte (CP_UTF8, 0, (LPCWSTR)m_arrayShort, num, 0, 0, NULL, NULL);char *tmpPT = new char[len+1];::WideCharToMultiByte(CP_UTF8, 0, (LPCWSTR)m_arrayShort, num, tmpPT, len, NULL, NULL);tmpPT[len] = 0;