主頁 > 後端開發 > 緩沖區溢位與注入分析

緩沖區溢位與注入分析

2021-02-23 06:11:22 後端開發

前言

雖然程式加載以及動態符號鏈接都已經很理解了,但是這伙卻被行程的記憶體映像給”糾纏"住,看著看著就一發不可收拾——很有趣,

下面一起來探究“緩沖區溢位和注入”問題(主要是關心程式的記憶體映像),

行程的記憶體映像

永遠的 Hello World,太熟悉了吧,

#include <stdio.h>
int main(void)
{
    printf("Hello World\n");
    return 0;
}

如果要用行內匯編(inline assembly)來寫呢?

 /* shellcode.c */
 void main()
 3  {
 4      __asm__ __volatile__("jmp forward;"
 5                   "backward:"
 6                           "popl   %esi;"
 7                           "movl   $4, %eax;"
 8                           "movl   $2, %ebx;"
 9                           "movl   %esi, %ecx;"
10                           "movl   $12, %edx;"
11                           "int    $0x80;"    /* system call 1 */
12                           "movl   $1, %eax;"
13                           "movl   $0, %ebx;"
14                           "int    $0x80;"    /* system call 2 */
15                   "forward:"
16                           "call   backward;"
17                           ".string \"Hello World\\n\";");
18  }

看起來很復雜,實際上就做了一個事情,往終端上寫了個 Hello World ,不過這個非常有意思,先簡單分析一下流程:

  • 第 4 行指令的作用是跳轉到第 15 行(即 forward 標記處),接著執行第 16 行,
  • 第 16 行呼叫 backward,跳轉到第 5 行,接著執行 6 到 14 行,
  • 第 6 行到第 11 行負責在終端列印出 Hello World 字串(等一下詳細介紹),
  • 第 12 行到第 14 行退出程式(等一下詳細介紹),

為了更好的理解上面的代碼和后續的分析,先來介紹幾個比較重要的內容,

常用暫存器初識

X86 處理器平臺有三個常用暫存器:程式指令指標、程式堆疊指標與程式基指標:

暫存器名稱注釋
EIP 程式指令指標 通常指向下一條指令的位置
ESP 程式堆疊指標 通常指向當前堆疊的當前位置
EBP 程式基指標 通常指向函式使用的堆疊頂端

當然,上面都是擴展的暫存器,用于 32 位系統,對應的 16 系統為 ipspbp ,

call,ret 指令的作用分析

  • call 指令

    跳轉到某個位置,并在之前把下一條指令的地址(EIP)入堆疊(為了方便”程式“回傳以后能夠接著執行),這樣的話就有:

      call backward   ==>   push eip
                            jmp backward
  • ret 指令

    通常 call 指令和 ret 是配合使用的,前者壓入跳轉前的下一條指令地址,后者彈出 call 指令壓入的那條指令,從而可以在函式呼叫結束以后接著執行后面的指令,

      ret                    ==>   pop eip

通常在函式呼叫后,還需要恢復 esp 和 ebp,恢復 esp 即恢復當前堆疊指標,以便釋放呼叫函式時為存盤函式的區域變數而自動分配的空間;恢復 ebp 是從堆疊中彈出一個資料項(通常函式呼叫過后的第一條陳述句就是 push ebp),從而恢復當前的函式指標為函式呼叫者本身,這兩個動作可以通過一條 leave 指令完成,

這三個指令對我們后續的解釋會很有幫助,更多關于 Intel 的指令集,請參考:Intel 386 Manual, x86 Assembly Language FAQ:part1, part2, part3.

什么是系統呼叫(以 Linux 2.6.21 版本和 x86 平臺為例)

系統呼叫是用戶和內核之間的介面,用戶如果想寫程式,很多時候直接呼叫了 C 庫,并沒有關心系統呼叫,而實際上 C 庫也是基于系統呼叫的,這樣應用程式和內核之間就可以通過系統呼叫聯系起來,它們分別處于作業系統的用戶空間和內核空間(主要是記憶體地址空間的隔離),

用戶空間         應用程式(Applications)
                        |      |
                        |     C庫(如glibc)
                        |      |
                       系統呼叫(System Calls,如sys_read, sys_write, sys_exit)
                            |
內核空間              內核(Kernel)

系統呼叫實際上也是一些函式,它們被定義在 arch/i386/kernel/sys_i386.c (老的在 arch/i386/kernel/sys.c)檔案中,并且通過一張系統呼叫表組織,該表在內核啟動時就已經加載了,這個表的入口在內核源代碼的 arch/i386/kernel/syscall_table.S 里頭(老的在 arch/i386/kernel/entry.S),這樣,如果想添加一個新的系統呼叫,修改上面兩個內核中的檔案,并重新編譯內核就可以,當然,如果要在應用程式中使用它們,還得把它寫到 include/asm/unistd.h 中,

如果要在 C 語言中使用某個系統呼叫,需要包含頭檔案 /usr/include/asm/unistd.h,里頭有各個系統呼叫的宣告以及系統呼叫號(對應于呼叫表的入口,即在呼叫表中的索引,為方便查找呼叫表而設立的),如果是自己定義的新系統呼叫,可能還要在開頭用宏 _syscall(type, name, type1, name1...)來宣告好引數,

如果要在匯編語言中使用,需要用到 int 0x80 呼叫,這個是系統呼叫的中斷入口,涉及到傳送引數的暫存器有這么幾個,eax 是系統呼叫號(可以到 /usr/include/asm-i386/unistd.h 或者直接到 arch/i386/kernel/syscall_table.S 查到),其他暫存器如 ebxecxedxesiedi 一次存放系統呼叫的引數,而系統呼叫的回傳值存放在 eax 暫存器中,

下面我們就很容易解釋前面的 Shellcode.c 程式流程的 2,3 兩部分了,因為都用了 int 0x80 中斷,所以都用到了系統呼叫,

第 3 部分很簡單,用到的系統呼叫號是 1,通過查表(查 /usr/include/asm-i386/unistd.h 或 arch/i386/kernel/syscall_table.S)可以發現這里是 sys_exit 呼叫,再從 /usr/include/unistd.h 檔案看這個系統呼叫的宣告,發現引數 ebx 是程式退出狀態,

第 2 部分比較有趣,而且復雜一點,我們依次來看各個暫存器,首先根據 eax 為 4 確定(同樣查表)系統呼叫為 sys_write,而查看它的宣告(從 /usr/include/unistd.h),我們找到了引數依次為檔案描述符、字串指標和字串長度,

  • 第一個引數是 ebx,正好是 2,即標準錯誤輸出,默認為終端,
  • 第二個引數是 ecx,而 ecx 的內容來自 esiesi 來自剛彈出堆疊的值(見第 6 行 popl %esi;),而之前剛好有 call 指令引起了最近一次壓堆疊操作,入堆疊的內容剛好是 call 指令的下一條指令的地址,即 .string 所在行的地址,這樣 ecx 剛好參考了 Hello World\\n 字串的地址,
  • 第三個引數是 edx,剛好是 12,即 Hello World\\n 字串的長度(包括一個空字符),這樣,Shellcode.c 的執行流程就很清楚了,第 4,5,15,16 行指令的巧妙之處也就容易理解了(把 .string 存放在 call 指令之后,并用 popl 指令把 eip 彈出當作字串的入口),

什么是 ELF 檔案

這里的 ELF 不是“精靈”,而是 Executable and Linking Format 檔案,是 Linux 下用來做目標檔案、可執行檔案和共享庫的一種檔案格式,它有專門的標準,例如:X86 ELF format and ABI,中文版,

下面簡單描述 ELF 的格式,

ELF 檔案主要有三種,分別是:

  • 可重定位的目標檔案,在編譯時用 gcc 的 -c 引數時產生,
  • 可執行檔案,這類檔案就是我們后面要討論的可以執行的檔案,
  • 共享庫,這里主要是動態共享庫,而靜態共享庫則是可重定位的目標檔案通過 ar 命令組織的,

ELF 檔案的大體結構:

ELF Header               #程式頭,有該檔案的Magic number(參考man magic),型別等
Program Header Table     #對可執行檔案和共享庫有效,它描述下面各個節(section)組成的段
Section1
Section2
Section3
.....
Program Section Table   #僅對可重定位目標檔案和靜態庫有效,用于描述各個Section的重定位資訊等,

對于可執行檔案,檔案最后的 Program Section Table (節區表)和一些非重定位的 Section,比如 .comment.note.XXX.debug 等資訊都可以洗掉掉,不過如果用 stripobjcopy 等工具洗掉掉以后,就不可恢復了,因為這些資訊對程式的運行一般沒有任何用處,

ELF 檔案的主要節區(section)有 .data.text.bss.interp 等,而主要段(segment)有 LOADINTERP 等,它們之間(節區和段)的主要對應關系如下:

Section解釋實體
.data 初始化的資料 比如 int a=10
.bss 未初始化的資料 比如 char sum[100]; 這個在程式執行之前,內核將初始化為 0
.text 程式代碼正文 即可執行指令集
.interp 描述程式需要的解釋器(動態連接和裝載程式) 存有解釋器的全路徑,如 /lib/ld-linux.so

而程式在執行以后,.data.bss.text 等一些節區會被 Program header table 映射到 LOAD 段,.interp 則被映射到了 INTERP 段,

對于 ELF 檔案的分析,建議使用 filesizereadelfobjdumpstripobjcopygdbnm 等工具,

這里簡單地演示這幾個工具:

$ gcc -g -o shellcode shellcode.c  #如果要用gdb除錯,編譯時加上-g是必須的
shellcode.c: In function ‘main’:
shellcode.c:3: warning: return type of ‘main’ is not ‘int’
f$ file shellcode  #file命令查看檔案型別,想了解作業原理,可man magic,man file
shellcode: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV),
dynamically linked (uses shared libs), not stripped
$ readelf -l shellcode  #列出ELF檔案前面的program head table,后面是它描
                       #述了各個段(segment)和節區(section)的關系,即各個段包含哪些節區,
Elf file type is EXEC (Executable file)
Entry point 0x8048280
There are 7 program headers, starting at offset 52

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  PHDR           0x000034 0x08048034 0x08048034 0x000e0 0x000e0 R E 0x4
  INTERP         0x000114 0x08048114 0x08048114 0x00013 0x00013 R   0x1
      [Requesting program interpreter: /lib/ld-linux.so.2]
  LOAD           0x000000 0x08048000 0x08048000 0x0044c 0x0044c R E 0x1000
  LOAD           0x00044c 0x0804944c 0x0804944c 0x00100 0x00104 RW  0x1000
  DYNAMIC        0x000460 0x08049460 0x08049460 0x000c8 0x000c8 RW  0x4
  NOTE           0x000128 0x08048128 0x08048128 0x00020 0x00020 R   0x4
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x4

 Section to Segment mapping:
  Segment Sections...
   00
   01     .interp
   02     .interp .note.ABI-tag .hash .dynsym .dynstr .gnu.version .gnu.version_r
          .rel.dyn .rel.plt .init .plt .text .fini .rodata .eh_frame
   03     .ctors .dtors .jcr .dynamic .got .got.plt .data .bss
   04     .dynamic
   05     .note.ABI-tag
   06
$ size shellcode   #可用size命令查看各個段(對應后面將分析的行程記憶體映像)的大小
   text    data     bss     dec     hex filename
    815     256       4    1075     433 shellcode
$ strip -R .note.ABI-tag shellcode #可用strip來給可執行檔案“減肥”,洗掉無用資訊
$ size shellcode               #“減肥”后效果“明顯”,對于嵌入式系統應該有很大的作用
   text    data     bss     dec     hex filename
    783     256       4    1043     413 shellcode
$ objdump -s -j .interp shellcode #這個主要作業是反編譯,不過用來查看各個節區也很厲害

shellcode:     file format elf32-i386

Contents of section .interp:
 8048114 2f6c6962 2f6c642d 6c696e75 782e736f  /lib/ld-linux.so
 8048124 2e3200                               .2.

補充:如果要洗掉可執行檔案的 Program Section Table,可以用 A Whirlwind Tutorial on Creating Really Teensy ELF Executables for Linux 一文的作者寫的 elf kicker 工具鏈中的 sstrip 工具,

程式執行基本程序

在命令列下,敲入程式的名字或者是全路徑,然后按下回車就可以啟動程式,這個具體是怎么作業的呢?

首先要再認識一下我們的命令列,命令列是內核和用戶之間的介面,它本身也是一個程式,在 Linux 系統啟動以后會為每個終端用戶建立一個行程執行一個 Shell 解釋程式,這個程式解釋并執行用戶輸入的命令,以實作用戶和內核之間的介面,這類解釋程式有哪些呢?目前 Linux 下比較常用的有 /bin/bash ,那么該程式接收并執行命令的程序是怎么樣的呢?

先簡單描述一下這個程序:

  • 讀取用戶由鍵盤輸入的命令列,
  • 分析命令,以命令名作為檔案名,并將其它引數改為系統呼叫 execve 內部處理所要求的形式,
  • 終端行程呼叫 fork 建立一個子行程,
  • 終端行程本身用系統呼叫 wait4 來等待子行程完成(如果是后臺命令,則不等待),當子行程運行時呼叫 execve,子行程根據檔案名(即命令名)到目錄中查找有關檔案(這是命令解釋程式構成的檔案),將它調入記憶體,執行這個程式(解釋這條命令),
  • 如果命令末尾有 & 號(后臺命令符號),則終端行程不用系統呼叫 wait4 等待,立即發提示符,讓用戶輸入下一個命令,轉 1),如果命令末尾沒有 & 號,則終端行程要一直等待,當子行程(即運行命令的行程)完成處理后終止,向父行程(終端行程)報告,此時終端行程醒來,在做必要的判別等作業后,終端行程發提示符,讓用戶輸入新的命令,重復上述處理程序,

現在用 strace 來跟蹤一下程式執行程序中用到的系統呼叫,

$ strace -f -o strace.out test
$ cat strace.out | grep \(.*\) | sed -e "s#[0-9]* \([a-zA-Z0-9_]*\)(.*).*#\1#g"
execve
brk
access
open
fstat64
mmap2
close
open
read
fstat64
mmap2
mmap2
mmap2
mmap2
close
mmap2
set_thread_area
mprotect
munmap
brk
brk
open
fstat64
mmap2
close
close
close
exit_group

相關的系統呼叫基本體現了上面的執行程序,需要注意的是,里頭還涉及到記憶體映射(mmap2)等,

下面再羅嗦一些比較有意思的內容,參考《深入理解 Linux 內核》的程式的執行(P681),

Linux 支持很多不同的可執行檔案格式,這些不同的格式是如何解釋的呢?平時我們在命令列下敲入一個命令就完了,也沒有去管這些細節,實際上 Linux 下有一個 struct linux_binfmt 結構來管理不同的可執行檔案型別,這個結構中有對應的可執行檔案的處理函式,大概的程序如下:

  • 在用戶態執行了 execve 后,引發 int 0x80 中斷,進入內核態,執行內核態的相應函式 do_sys_execve,該函式又呼叫 do_execve 函式, do_execve 函式讀入可執行檔案,檢查權限,如果沒問題,繼續讀入可執行檔案需要的相關資訊(struct linux_binprm 描述的),

  • 接著執行 search_binary_handler,根據可執行檔案的型別(由上一步的最后確定),在 linux_binfmt 結構鏈表(formats,這個鏈表可以通過 register_binfmt 和 unregister_binfmt 注冊和洗掉某些可執行檔案的資訊,因此注冊新的可執行檔案成為可能,后面再介紹)上查找,找到相應的結構,然后執行相應的 load_binary 函式開始加載可執行檔案,在該鏈表的最后一個元素總是對解釋腳本(interpreted script)的可執行檔案格式進行描述的一個物件,這種格式只定義了 load_binary 方法,其相應的 load_script 函式檢查這種可執行檔案是否以兩個 #! 字符開始,如果是,這個函式就以另一個可執行檔案的路徑名作為引數解釋第一行的其余部分,并把腳本檔案名作為引數傳遞以執行這個腳本(實際上腳本程式把自身的內容當作一個引數傳遞給了解釋程式(如 /bin/bash),而這個解釋程式通常在腳本檔案的開頭用 #! 標記,如果沒有標記,那么默認解釋程式為當前 SHELL),

  • 對于 ELF 型別檔案,其處理函式是 load_elf_binary,它先讀入 ELF 檔案的頭部,根據頭部資訊讀入各種資料,再次掃描程式段描述表(Program Header Table),找到型別為 PT_LOAD 的段(即 .text.data.bss 等節區),將其映射(elf_map)到記憶體的固定地址上,如果沒有動態連接器的描述段,把回傳的入口地址設定成應用程式入口,完成這個功能的是 start_thread,它不啟動一個執行緒,而只是用來修改了 pt_regs 中保存的 PC 等暫存器的值,使其指向加載的應用程式的入口,當內核操作結束,回傳用戶態時接著就執行應用程式本身了,

  • 如果應用程式使用了動態連接庫,內核除了加載指定的可執行檔案外,還要把控制權交給動態連接器(ld-linux.so)以便處理動態連接的程式,內核搜尋段表(Program Header Table),找到標記為 PT_INTERP 段中所對應的動態連接器的名稱,并使用 load_elf_interp 加載其映像,并把回傳的入口地址設定成 load_elf_interp 的回傳值,即動態聯結器的入口,當 execve 系統呼叫退出時,動態連接器接著運行,它檢查應用程式對共享鏈接庫的依賴性,并在需要時對其加載,對程式的外部參考進行重定位(具體程序見《行程和行程的基本操作》),然后把控制權交給應用程式,從 ELF 檔案頭部中定義的程式進入點(用 readelf -h 可以出看到,Entry point address 即是)開始執行,(不過對于非 LIB_BIND_NOW 的共享庫裝載是在有外部參考請求時才執行的),

對于內核態的函式呼叫程序,沒有辦法通過 strace(它只能跟蹤到系統呼叫層)來做的,因此要想跟蹤內核中各個系統呼叫的執行細節,需要用其他工具,比如可以通過 Ftrace 來跟蹤內核具體呼叫了哪些函式,當然,也可以通過 ctags/cscope/LXR 等工具分析內核的源代碼,

Linux 允許自己注冊我們自己定義的可執行格式,主要介面是 /procy/sys/fs/binfmt_misc/register,可以往里頭寫入特定格式的字串來實作,該字串格式如下: :name:type:offset:string:mask:interpreter:

  • name 新格式的標示符
  • type 識別型別(M 表示魔數,E 表示擴展)
  • offset 魔數(magic number,請參考 man magic 和 man file)在檔案中的啟始偏移量
  • string 以魔數或者以擴展名匹配的位元組序列
  • mask 用來屏蔽掉 string 的一些位
  • interpreter 程式解釋器的完整路徑名

Linux 下程式的記憶體映像

Linux 下是如何給行程分配記憶體(這里僅討論虛擬記憶體的分配)的呢?可以從 /proc/<pid>/maps 檔案中看到個大概,這里的 pid 是行程號,

/proc 下有一個檔案比較特殊,是 self,它鏈接到當前行程的行程號,例如:

$ ls /proc/self -l
lrwxrwxrwx 1 root root 64 2000-01-10 18:26 /proc/self -> 11291/
$ ls /proc/self -l
lrwxrwxrwx 1 root root 64 2000-01-10 18:26 /proc/self -> 11292/

看到沒?每次都不一樣,這樣我們通過 cat /proc/self/maps 就可以看到 cat 程式執行時的記憶體映像了,

$ cat -n /proc/self/maps
     1  08048000-0804c000 r-xp 00000000 03:01 273716     /bin/cat
     2  0804c000-0804d000 rw-p 00003000 03:01 273716     /bin/cat
     3  0804d000-0806e000 rw-p 0804d000 00:00 0          [heap]
     4  b7b90000-b7d90000 r--p 00000000 03:01 87528      /usr/lib/locale/locale-archive
     5  b7d90000-b7d91000 rw-p b7d90000 00:00 0
     6  b7d91000-b7ecd000 r-xp 00000000 03:01 466875     /lib/libc-2.5.so
     7  b7ecd000-b7ece000 r--p 0013c000 03:01 466875     /lib/libc-2.5.so
     8  b7ece000-b7ed0000 rw-p 0013d000 03:01 466875     /lib/libc-2.5.so
     9  b7ed0000-b7ed4000 rw-p b7ed0000 00:00 0
    10  b7eeb000-b7f06000 r-xp 00000000 03:01 402817     /lib/ld-2.5.so
    11  b7f06000-b7f08000 rw-p 0001b000 03:01 402817     /lib/ld-2.5.so
    12  bfbe3000-bfbf8000 rw-p bfbe3000 00:00 0          [stack]
    13  ffffe000-fffff000 r-xp 00000000 00:00 0          [vdso]

編號是原檔案里頭沒有的,為了說明方便,用 -n 引數加上去的,我們從中可以得到如下資訊:

  • 第 1,2 行對應的記憶體區是我們的程式(包括指令,資料等)
  • 第 3 到 12 行對應的記憶體區是堆疊段,里頭也映像了程式參考的動態連接庫
  • 第 13 行是內核空間

總結一下:

  • 前兩部分是用戶空間,可以從 0x00000000 到 0xbfffffff (在測驗的 2.6.21.5-smp 上只到 bfbf8000),而內核空間從 0xC0000000 到 0xffffffff,分別是 3G 和 1G,所以對于每一個行程來說,共占用 4G 的虛擬記憶體空間
  • 從程式本身占用的記憶體,到堆疊段(動態獲取記憶體或者是函式運行程序中用來存盤區域變數、引數的空間,前者是 heap,后者是 stack),再到內核空間,地址是從低到高的
  • 堆疊頂并非 0xC0000000 下的一個固定數值

結合相關資料,可以得到這么一個比較詳細的行程記憶體映像表(以 Linux 2.6.21.5-smp 為例):

地址內核空間描述
0xC0000000    
  (program flie) 程式名 execve 的第一個引數
  (environment) 環境變數 execve 的第三個引數,main 的第三個引數
  (arguments) 引數 execve 的第二個引數,main 的形參
  (stack) 堆疊 自動變數以及每次函式呼叫時所需保存的資訊都
    存放在此,包括函式回傳地址、呼叫者的
    環境資訊等,函式的引數,區域變數都存放在此
  (shared memory) 共享記憶體 共享記憶體的大概位置
  ...  
  ...  
  (heap) 堆 主要在這里進行動態存盤分配,比如 malloc,new 等,
  ...  
  .bss (uninitilized data) 沒有初始化的資料(全域變數哦)
  .data (initilized global data) 已經初始化的全域資料(全域變數)
  .text (Executable Instructions) 通常是可執行指令
0x08048000    
0x00000000   ...

光看沒有任何概念,我們用 gdb 來看看剛才那個簡單的程式,

$ gcc -g -o shellcode shellcode.c   #要用gdb除錯,在編譯時需要加-g引數
$ gdb -q ./shellcode
(gdb) set args arg1 arg2 arg3 arg4  #為了測驗,設定幾個引數
(gdb) l                             #瀏覽代碼
1 /* shellcode.c */
2 void main()
3 {
4     __asm__ __volatile__("jmp forward;"
5     "backward:"
6        "popl   %esi;"
7        "movl   $4, %eax;"
8        "movl   $2, %ebx;"
9        "movl   %esi, %ecx;"
10               "movl   $12, %edx;"
(gdb) break 4               #在匯編入口設定一個斷點,讓程式運行后停到這里
Breakpoint 1 at 0x8048332: file shellcode.c, line 4.
(gdb) r                     #運行程式
Starting program: /mnt/hda8/Temp/c/program/shellcode arg1 arg2 arg3 arg4

Breakpoint 1, main () at shellcode.c:4
4     __asm__ __volatile__("jmp forward;"
(gdb) print $esp            #列印當前堆疊指標值,用于查找整個堆疊的堆疊頂
$1 = (void *) 0xbffe1584
(gdb) x/100s $esp+4000      #改變后面的4000,不斷往更大的空間找
(gdb) x/1s 0xbffe1fd9       #在 0xbffe1fd9 找到了程式名,這里是該次運行時的堆疊頂
0xbffe1fd9:      "/mnt/hda8/Temp/c/program/shellcode"
(gdb) x/10s 0xbffe17b7      #其他環境變數資訊
0xbffe17b7:      "CPLUS_INCLUDE_PATH=/usr/lib/qt/include"
0xbffe17de:      "MANPATH=/usr/local/man:/usr/man:/usr/X11R6/man:/usr/lib/java/man:/usr/share/texmf/man"
0xbffe1834:      "HOSTNAME=falcon.lzu.edu.cn"
0xbffe184f:      "TERM=xterm"
0xbffe185a:      "SSH_CLIENT=219.246.50.235 3099 22"
0xbffe187c:      "QTDIR=/usr/lib/qt"
0xbffe188e:      "SSH_TTY=/dev/pts/0"
0xbffe18a1:      "USER=falcon"
...
(gdb) x/5s 0xbffe1780    #一些傳遞給main函式的引數,包括檔案名和其他引數
0xbffe1780:      "/mnt/hda8/Temp/c/program/shellcode"
0xbffe17a3:      "arg1"
0xbffe17a8:      "arg2"
0xbffe17ad:      "arg3"
0xbffe17b2:      "arg4"
(gdb) print init  #列印init函式的地址,這個是/usr/lib/crti.o里頭的函式,做一些初始化操作
$2 = {<text variable, no debug info>} 0xb7e73d00 <init>
(gdb) print fini   #也在/usr/lib/crti.o中定義,在程式結束時做一些處理作業
$3 = {<text variable, no debug info>} 0xb7f4a380 <fini>
(gdb) print _start #在/usr/lib/crt1.o,這個才是程式的入口,必須的,ld會檢查這個
$4 = {<text variable, no debug info>} 0x8048280 <__libc_start_main@plt+20>
(gdb) print main   #這里是我們的main函式
$5 = {void ()} 0x8048324 <main>

補充:在行程的記憶體映像中可能看到諸如 initfini_start 等函式(或者是入口),這些東西并不是我們自己寫的啊?為什么會跑到我們的代碼里頭呢?實際上這些東西是鏈接的時候 gcc 默認給連接進去的,主要用來做一些行程的初始化和終止的動作,更多相關的細節可以參考資料如何獲取當前行程之靜態影像檔案和"The Linux Kernel Primer", P234, Figure 4.11,如果想了解鏈接(ld)的具體程序,可以看看本節參考《Unix環境高級編程編程》第7章 "UnIx行程的環境", P127和P13,ELF: From The Programmer's Perspective,GNU-ld 連接腳本 Linker Scripts,

上面的操作對堆疊的操作比較少,下面我們用一個例子來演示堆疊在記憶體中的情況,

堆疊在記憶體中的組織

這一節主要介紹一個函式被呼叫時,引數是如何傳遞的,區域變數是如何存盤的,它們對應的堆疊的位置和變化情況,從而加深對堆疊的理解,在操作時發現和參考資料的結果不太一樣(參考資料中沒有 edi 和 esi 相關資訊,再第二部分的一個小程式里頭也沒有),可能是 gcc 版本的問題或者是它對不同源代碼的處理不同,我的版本是 4.1.2 (可以通過 gcc --version 查看),

先來一段簡單的程式,這個程式除了做一個加法操作外,還復制了一些字串,

/* testshellcode.c */
#include <stdio.h>      /* printf */
#include <string.h>     /* memset, memcpy */

#define BUF_SIZE 8

#ifndef STR_SRC
# define STR_SRC "AAAAAAA"
#endif

int func(int a, int b, int c)
{
    int sum = 0;
    char buffer[BUF_SIZE];

    sum = a + b + c;

    memset(buffer, '\0', BUF_SIZE);
    memcpy(buffer, STR_SRC, sizeof(STR_SRC)-1);

    return sum;
}

int main()
{
    int sum;

    sum = func(1, 2, 3);

    printf("sum = %d\n", sum);

    return 0;
}

上面這個代碼沒有什么問題,編譯執行一下:

$ make testshellcode
cc     testshellcode.c   -o testshellcode
$ ./testshellcode
sum = 6

下面除錯一下,看看在呼叫 func 后的堆疊的內容,

$ gcc -g -o testshellcode testshellcode.c  #為了除錯,需要在編譯時加-g選項
$ gdb -q ./testshellcode   #啟動gdb除錯
...
(gdb) set logging on    #如果要記錄除錯程序中的資訊,可以把日志記錄功能打開
Copying output to gdb.txt.
(gdb) l main            #列出源代碼
20
21              return sum;
22      }
23
24      int main()
25      {
26              int sum;
27
28              sum = func(1, 2, 3);
29
(gdb) break 28   #在呼叫func函式之前讓程式停一下,以便記錄當時的ebp(基指標)
Breakpoint 1 at 0x80483ac: file testshellcode.c, line 28.
(gdb) break func #設定斷點在函式入口,以便逐步記錄堆疊資訊
Breakpoint 2 at 0x804835c: file testshellcode.c, line 13.
(gdb) disassemble main   #反編譯main函式,以便記錄呼叫func后的下一條指令地址
Dump of assembler code for function main:
0x0804839b <main+0>:    lea    0x4(%esp),%ecx
0x0804839f <main+4>:    and    $0xfffffff0,%esp
0x080483a2 <main+7>:    pushl  0xfffffffc(%ecx)
0x080483a5 <main+10>:   push   %ebp
0x080483a6 <main+11>:   mov    %esp,%ebp
0x080483a8 <main+13>:   push   %ecx
0x080483a9 <main+14>:   sub    $0x14,%esp
0x080483ac <main+17>:   push   $0x3
0x080483ae <main+19>:   push   $0x2
0x080483b0 <main+21>:   push   $0x1
0x080483b2 <main+23>:   call   0x8048354 <func>
0x080483b7 <main+28>:   add    $0xc,%esp
0x080483ba <main+31>:   mov    %eax,0xfffffff8(%ebp)
0x080483bd <main+34>:   sub    $0x8,%esp
0x080483c0 <main+37>:   pushl  0xfffffff8(%ebp)
0x080483c3 <main+40>:   push   $0x80484c0
0x080483c8 <main+45>:   call   0x80482a0 <printf@plt>
0x080483cd <main+50>:   add    $0x10,%esp
0x080483d0 <main+53>:   mov    $0x0,%eax
0x080483d5 <main+58>:   mov    0xfffffffc(%ebp),%ecx
0x080483d8 <main+61>:   leave
0x080483d9 <main+62>:   lea    0xfffffffc(%ecx),%esp
0x080483dc <main+65>:   ret
End of assembler dump.
(gdb) r        #運行程式
Starting program: /mnt/hda8/Temp/c/program/testshellcode

Breakpoint 1, main () at testshellcode.c:28
28              sum = func(1, 2, 3);
(gdb) print $ebp  #列印呼叫func函式之前的基地址,即Previous frame pointer,
$1 = (void *) 0xbf84fdd8
(gdb) n           #執行call指令并跳轉到func函式的入口

Breakpoint 2, func (a=1, b=2, c=3) at testshellcode.c:13
13              int sum = 0;
(gdb) n
16              sum = a + b + c;
(gdb) x/11x $esp  #列印當前堆疊的內容,可以看出,地址從低到高,注意標記有藍色和紅色的值
                 #它們分別是前一個堆疊基地址(ebp)和call呼叫之后的下一條指令的指標(eip)
0xbf84fd94:     0x00000000      0x00000000      0x080482e0      0x00000000
0xbf84fda4:     0xb7f2bce0      0x00000000      0xbf84fdd8      0x080483b7
0xbf84fdb4:     0x00000001      0x00000002      0x00000003
(gdb) n       #執行sum = a + b + c,后,比較堆疊內容第一行,第4列,由0變為6
18              memset(buffer, '\0', BUF_SIZE);
(gdb) x/11x $esp
0xbf84fd94:     0x00000000      0x00000000      0x080482e0      0x00000006
0xbf84fda4:     0xb7f2bce0      0x00000000      0xbf84fdd8      0x080483b7
0xbf84fdb4:     0x00000001      0x00000002      0x00000003
(gdb) n
19              memcpy(buffer, STR_SRC, sizeof(STR_SRC)-1);
(gdb) x/11x $esp #緩沖區初始化以后變成了0
0xbf84fd94:     0x00000000      0x00000000      0x00000000      0x00000006
0xbf84fda4:     0xb7f2bce0      0x00000000      0xbf84fdd8      0x080483b7
0xbf84fdb4:     0x00000001      0x00000002      0x00000003
(gdb) n
21              return sum;
(gdb) x/11x $esp #進行copy以后,這兩列的值變了,大小剛好是7個位元組,最后一個位元組為'\0'
0xbf84fd94:     0x00000000      0x41414141      0x00414141      0x00000006
0xbf84fda4:     0xb7f2bce0      0x00000000      0xbf84fdd8      0x080483b7
0xbf84fdb4:     0x00000001      0x00000002      0x00000003
(gdb) c
Continuing.
sum = 6

Program exited normally.
(gdb) quit

從上面的操作程序,我們可以得出大概的堆疊分布(func 函式結束之前)如下:

地址值(hex)符號或者暫存器注釋
低地址     堆疊頂方向
0xbf84fd98 0x41414141 buf[0] 可以看出little endian(小端,重要的資料在前面)
0xbf84fd9c 0x00414141 buf[1]  
0xbf84fda0 0x00000006 sum 可見這上面都是func函式里頭的區域變數
0xbf84fda4 0xb7f2bce0 esi 源索引指標,可以通過產生中間代碼查看,貌似沒什么作用
0xbf84fda8 0x00000000 edi 目的索引指標
0xbf84fdac 0xbf84fdd8 ebp 呼叫func之前的堆疊的基地址,以便呼叫函式結束之后恢復
0xbf84fdb0 0x080483b7 eip 呼叫func之前的指令指標,以便呼叫函式結束之后繼續執行
0xbf84fdb4 0x00000001 a 第一個引數
0xbf84fdb8 0x00000002 b 第二個引數
0xbf84fdbc 0x00000003 c 第三個引數,可見引數是從最后一個開始壓堆疊的
高地址     堆疊底方向

先說明一下 edi 和 esi 的由來(在上面的除錯程序中我們并沒有看到),是通過產生中間匯編代碼分析得出的,

$ gcc -S testshellcode.c

在產生的 testShellcode.s 代碼里頭的 func 部分看到 push ebp 之后就 push 了 edi 和 esi ,但是搜索了一下代碼,發現就這個函式里頭參考了這兩個暫存器,所以保存它們沒什么用,洗掉以后編譯產生目標代碼后證明是沒用的,

$ cat testshellcode.s
...
func:
        pushl   %ebp
        movl    %esp, %ebp
        pushl   %edi
        pushl   %esi
...
        popl    %esi
        popl    %edi
        popl    %ebp
...

下面就不管這兩部分(edi 和 esi)了,主要來分析和函式相關的這幾部分在堆疊內的分布:

  • 函式區域變數,在靠近堆疊頂一端
  • 呼叫函式之前的堆疊的基地址(ebpPrevious Frame Pointer),在中間靠近堆疊頂方向
  • 呼叫函式指令的下一條指令地址 ` (eip`),在中間靠近堆疊底的方向
  • 函式引數,在靠近堆疊底的一端,最后一個引數最先入堆疊

到這里,函式呼叫時的相關內容在堆疊內的分布就比較清楚了,在具體分析緩沖區溢位問題之前,我們再來看一個和函式關系很大的問題,即函式回傳值的存盤問題:函式的回傳值存放在暫存器 eax 中,

先來看這段代碼:

/**
 * test_return.c -- the return of a function is stored in register eax
 */

#include <stdio.h>

int func()
{
        __asm__ ("movl $1, %eax");
}

int main()
{
        printf("the return of func: %d\n", func());

        return 0;
}

編譯運行后,可以看到回傳值為 1,剛好是我們在 func 函式中 mov 到 eax 中的“立即數” 1,因此很容易理解回傳值存盤在 eax 中的事實,如果還有疑慮,可以再看看匯編代碼,在函式回傳之后,eax 中的值當作了 printf 的引數壓入了堆疊中,而在源代碼中我們正是把 func 的結果作為 printf 的第二個引數的,

$ make test_return
cc     test_return.c   -o test_return
$ ./test_return
the return of func: 1
$ gcc -S test_return.c
$ cat test_return.s
...
        call    func
        subl    $8, %esp
        pushl   %eax      #printf的第二個引數,把func的回傳值壓入了堆疊底
        pushl   $.LC0     #printf的第一個引數the return of func: %d\n
        call    printf
...

對于系統呼叫,回傳值也存盤在 eax 暫存器中,

緩沖區溢位

實體分析:字串復制

先來看一段簡短的代碼,

/* testshellcode.c */
#include <stdio.h>      /* printf */
#include <string.h>     /* memset, memcpy */

#define BUF_SIZE 8

#ifdef STR1
# define STR_SRC "AAAAAAA\0\1\0\0\0"
#endif

#ifndef STR_SRC
# define STR_SRC "AAAAAAA"
#endif

int func(int a, int b, int c)
{
        int sum = 0;
        char buffer[BUF_SIZE];

        sum = a + b + c;

        memset(buffer, '\0', BUF_SIZE);
        memcpy(buffer, STR_SRC, sizeof(STR_SRC)-1);

        return sum;
}

int main()
{
        int sum;

        sum = func(1, 2, 3);

        printf("sum = %d\n", sum);

        return 0;
}

編譯一下看看結果:

$ gcc -DSTR1 -o testshellcode testshellcode.c  #通過-D定義宏STR1,從而采用第一個STR_SRC的值
$ ./testshellcode
sum = 1

不知道你有沒有發現例外呢?上面用紅色標記的地方,本來 sum 為 1+2+3 即 6,但是實際回傳的竟然是 1 ,到底是什么原因呢?大家應該有所了解了,因為我們在復制字串 AAAAAAA\\0\\1\\0\\0\\0 到 buf 的時候超出 buf 本來的大小, buf 本來的大小是 BUF_SIZE,8 個位元組,而我們要復制的內容是 12 個位元組,所以超出了四個位元組,根據第一小節的分析,我們用堆疊的變化情況來表示一下這個復制程序(即執行 memcpy 的程序),

memcpy(buffer, STR_SRC, sizeof(STR_SRC)-1);

(低地址)
復制之前     ====> 復制之后
0x00000000       0x41414141      #char buf[8]
0x00000000       0x00414141
0x00000006       0x00000001      #int sum
(高地址)

下面通過 gdb 除錯來確認一下(只摘錄了一些片斷),

$ gcc -DSTR1 -g -o testshellcode testshellcode.c
$ gdb -q ./testshellcode
...
(gdb) l
21
22              memset(buffer, '\0', BUF_SIZE);
23              memcpy(buffer, STR_SRC, sizeof(STR_SRC)-1);
24
25              return sum;
...
(gdb) break 23
Breakpoint 1 at 0x804837f: file testshellcode.c, line 23.
(gdb) break 25
Breakpoint 2 at 0x8048393: file testshellcode.c, line 25.
(gdb) r
Starting program: /mnt/hda8/Temp/c/program/testshellcode

Breakpoint 1, func (a=1, b=2, c=3) at testshellcode.c:23
23              memcpy(buffer, STR_SRC, sizeof(STR_SRC)-1);
(gdb) x/3x $esp+4
0xbfec6bd8:     0x00000000      0x00000000      0x00000006
(gdb) n

Breakpoint 2, func (a=1, b=2, c=3) at testshellcode.c:25
25              return sum;
(gdb) x/3x $esp+4
0xbfec6bd8:     0x41414141      0x00414141      0x00000001

可以看出,因為 C 語言沒有對陣列的邊界進行限制,我們可以往陣列中存入預定義長度的字串,從而導致緩沖區溢位,

緩沖區溢位后果

溢位之后的問題是導致覆寫堆疊的其他內容,從而可能改變程式原來的行為,

如果這類問題被“黑客”利用那將產生非常可怕的后果,小則讓非法用戶獲取了系統權限,把你的服務器當成“僵尸”,用來對其他機器進行攻擊,嚴重的則可能被人洗掉資料(所以備份很重要),即使不被黑客利用,這類問題如果放在醫療領域,那將非常危險,可能那個被覆寫的數字剛好是用來控制治療癌癥的輻射量的,一旦出錯,那可能導致置人死地,當然,如果在航天領域,那可能就是好多個 0 的 money 甚至航天員的損失,呵呵,“緩沖區溢位,后果很嚴重!”

緩沖區溢位應對策略

那這個怎么辦呢?貌似Linux下緩沖區溢位攻擊的原理及對策提到有一個 libsafe 庫,可以至少用來檢測程式中出現的類似超出陣列邊界的問題,對于上面那個具體問題,為了保護 sum 不被修改,有一個小技巧,可以讓求和操作在字串復制操作之后來做,以便求和操作把溢位的部分給重寫,這個呆伙在下面一塊看效果吧,繼續看看緩沖區的溢位吧,

先來看看這個代碼,還是 testShellcode.c 的改進,

/* testshellcode.c */
#include <stdio.h>      /* printf */
#include <string.h> /* memset, memcpy */
#define BUF_SIZE 8

#ifdef STR1
# define STR_SRC "AAAAAAAa\1\0\0\0"
#endif
#ifdef STR2
# define STR_SRC "AAAAAAAa\1\0\0\0BBBBBBBB"
#endif
#ifdef STR3
# define STR_SRC "AAAAAAAa\1\0\0\0BBBBBBBBCCCC"
#endif
#ifdef STR4
# define STR_SRC "AAAAAAAa\1\0\0\0BBBBBBBBCCCCDDDD"
#endif

#ifndef STR_SRC
# define STR_SRC "AAAAAAA"
#endif

int func(int a, int b, int c)
{
        int sum = 0;
        char buffer[BUF_SIZE] = "";

        memset(buffer, '\0', BUF_SIZE);
        memcpy(buffer, STR_SRC, sizeof(STR_SRC)-1);

        sum = a + b + c;     //把求和操作放在復制操作之后可以在一定情況下“保護”求和結果

        return sum;
}

int main()
{
        int sum;

        sum = func(1, 2, 3);

        printf("sum = %d\n", sum);

        return 0;
}

看看運行情況:

$ gcc -D STR2 -o testshellcode testshellcode.c   #再多復制8個位元組,結果和STR1時一樣
                        #原因是edi,esi這兩個沒什么用的,覆寫了也沒關系
$ ./testshellcode       #看到沒?這種情況下,讓整數操作在字串復制之后做可以“保護‘整數結果
sum = 6
$ gcc -D STR3 -o testshellcode testshellcode.c  #再多復制4個位元組,現在就會把ebp給覆寫
                                               #了,這樣當main函式再要用ebp訪問資料
                                              #時就會出現訪問非法記憶體而導致段錯誤,
$ ./testshellcode
Segmentation fault

如果感興趣,自己還可以用gdb類似之前一樣來查看復制字串以后堆疊的變化情況,

如何保護 ebp 不被修改

下面來做一個比較有趣的事情:如何設法保護我們的 ebp 不被修改,

首先要明確 ebp 這個暫存器的作用和“行為”,它是堆疊基地址,并且發現在呼叫任何一個函式時,這個 ebp 總是在第一條指令被壓入堆疊中,并在最后一條指令(ret)之前被彈出,類似這樣:

func:                        #函式
       pushl %ebp            #第一條指令
       ...
       popl %ebp             #倒數第二條指令
       ret

還記得之前(第一部分)提到的函式的回傳值是存盤在 eax 暫存器中的么?如果我們在一個函式中僅僅做放這兩條指令:

popl %eax
pushl %eax

那不就剛好有:

func:                        #函式
       pushl %ebp            #第一條指令
       popl %eax             #把剛壓入堆疊中的ebp彈出存放到eax中
       pushl %eax            #又把ebp壓入堆疊
       popl %ebp             #倒數第二條指令
       ret

這樣我們沒有改變堆疊的狀態,卻獲得了 ebp 的值,如果在呼叫任何一個函式之前,獲取這個 ebp,并且在任何一條字串復制陳述句(可能導致緩沖區溢位的陳述句)之后重新設定一下 ebp 的值,那么就可以保護 ebp 啦,具體怎么實作呢?看這個代碼,

/* testshellcode.c */
#include <stdio.h>      /* printf */
#include <string.h> /* memset, memcpy */
#define BUF_SIZE 8

#ifdef STR1
# define STR_SRC "AAAAAAAa\1\0\0\0"
#endif
#ifdef STR2
# define STR_SRC "AAAAAAAa\1\0\0\0BBBBBBBB"
#endif
#ifdef STR3
# define STR_SRC "AAAAAAAa\1\0\0\0BBBBBBBBCCCC"
#endif
#ifdef STR4
# define STR_SRC "AAAAAAAa\1\0\0\0BBBBBBBBCCCCDDDD"
#endif

#ifndef STR_SRC
# define STR_SRC "AAAAAAA"
#endif

unsigned long get_ebp()
{
        __asm__ ("popl %eax;"
                                "pushl %eax;");
}

int func(int a, int b, int c, unsigned long ebp)
{
        int sum = 0;
        char buffer[BUF_SIZE] = "";

        sum = a + b + c;
        memset(buffer, '\0', BUF_SIZE);
        memcpy(buffer, STR_SRC, sizeof(STR_SRC)-1);
        *(unsigned long *)(buffer+20) = ebp;
        return sum;
}

int main()
{
        int sum, ebp;

        ebp = get_ebp();
        sum = func(1, 2, 3, ebp);

        printf("sum = %d\n", sum);

        return 0;
}

這段代碼和之前的代碼的不同有:

  • 給 func 函式增加了一個引數 ebp,(其實可以用全域變數替代的)
  • 利用了剛介紹的原理定義了一個函式 get_ebp 以便獲取老的 ebp
  • 在 main 函式中呼叫 func 之前呼叫了 get_ebp,并把它作為 func 的最后一個引數
  • 在 func 函式中呼叫 memcpy 函式(可能發生緩沖區溢位的地方)之后添加了一潭訓復設定 ebp 的陳述句,這條陳述句先把 buffer+20 這個地址(存放 ebp 的地址,你可以類似第一部分提到的用 gdb 來查看)強制轉換為指向一個 unsigned long 型的整數(4 個位元組),然后把它指向的內容修改為老的 ebp ,

看看效果:

$ gcc -D STR3 -o testshellcode testshellcode.c
$ ./testshellcode         #現在沒有段錯誤了吧,因為ebp得到了“保護”
sum = 6

如何保護 eip 不被修改?

如果我們復制更多的位元組過去了,比如再多復制四個位元組進去,那么 eip 就被覆寫了,

$ gcc -D STR4 -o testshellcode testshellcode.c
$ ./testshellcode
Segmentation fault

同樣會出現段錯誤,因為下一條指令的位置都被改寫了,func 回傳后都不知道要訪問哪個”非法“地址啦,呵呵,如果是一個合法地址呢?

如果在緩沖區溢位時,eip 被覆寫了,并且被修改為了一條合法地址,那么問題就非常”有趣“了,如果這個地址剛好是呼叫func的那個地址,那么整個程式就成了死回圈,如果這個地址指向的位置剛好有一段關機代碼,那么系統正在運行的所有服務都將被關掉,如果那個地方是一段更惡意的代碼,那就?你可以盡情想像哦,如果是黑客故意利用這個,那么那些代碼貌似就叫做shellcode了,

有沒有保護 eip 的辦法呢?呵呵,應該是有的吧,不知道 gas 有沒有類似 masm 匯編器中 offset 的偽操作指令(查找了一下,貌似沒有),如果有的話在函式呼叫之前設定一個標號,在后面某個位置獲取,再加上一個可能的偏移(包括 call 指令的長度和一些 push 指令等),應該可以算出來,不過貌似比較麻煩(或許你靈感大作,找到好辦法了!),這里直接通過 gdb 反匯編求得它相對 main 的偏移算出來得了,求出來以后用它來”保護“堆疊中的值,

看看這個代碼:

/* testshellcode.c */
#include <stdio.h>      /* printf */
#include <string.h> /* memset, memcpy */
#define BUF_SIZE 8

#ifdef STR1
# define STR_SRC "AAAAAAAa\1\0\0\0"
#endif
#ifdef STR2
# define STR_SRC "AAAAAAAa\1\0\0\0BBBBBBBB"
#endif
#ifdef STR3
# define STR_SRC "AAAAAAAa\1\0\0\0BBBBBBBBCCCC"
#endif
#ifdef STR4
# define STR_SRC "AAAAAAAa\1\0\0\0BBBBBBBBCCCCDDDD"
#endif

#ifndef STR_SRC
# define STR_SRC "AAAAAAA"
#endif

int main();
#define OFFSET  40

unsigned long get_ebp()
{
        __asm__ ("popl %eax;"
                                "pushl %eax;");
}

int func(int a, int b, int c, unsigned long ebp)
{
        int sum = 0;
        char buffer[BUF_SIZE] = "";

        memset(buffer, '\0', BUF_SIZE);
        memcpy(buffer, STR_SRC, sizeof(STR_SRC)-1);

        sum = a + b + c;

        *(unsigned long *)(buffer+20) = ebp;
        *(unsigned long *)(buffer+24) = (unsigned long)main+OFFSET;
        return sum;
}

int main()
{
        int sum, ebp;

        ebp = get_ebp();
        sum = func(1, 2, 3, ebp);

        printf("sum = %d\n", sum);

        return 0;
}

看看效果:

$ gcc -D STR4 -o testshellcode testshellcode.c
$ ./testshellcode
sum = 6

這樣,EIP 也得到了“保護”(這個方法很糟糕的,呵呵),

類似地,如果再多復制一些內容呢?那么堆疊后面的內容都將被覆寫,即傳遞給 func 函式的引數都將被覆寫,因此上面的方法,包括所謂的對 sum 和 ebp 等值的保護都沒有任何意義了(如果再對后面的引數進行進一步的保護呢?或許有點意義,呵呵),在這里,之所以提出類似這樣的保護方法,實際上只是為了討論一些有趣的細節并加深對緩沖區溢位這一問題的理解(或許有一些實際的價值哦,算是拋磚引玉吧),

緩沖區溢位檢測

要確實解決這類問題,從主觀上講,還得程式員來做相關的作業,比如限制將要復制的字串的長度,保證它不超過當初申請的緩沖區的大小,

例如,在上面的代碼中,我們在 memcpy 之前,可以加入一個判斷,并且可以對緩沖區溢位進行很好的檢查,如果能夠設計一些比較好的測驗實體把這些判斷覆寫到,那么相關的問題就可以得到比較不錯的檢查了,

/* testshellcode.c */
#include <stdio.h>      /* printf */
#include <string.h> /* memset, memcpy */
#include <stdlib.h>     /* exit */
#define BUF_SIZE 8

#ifdef STR4
# define STR_SRC "AAAAAAAa\1\0\0\0BBBBBBBBCCCCDDDD"
#endif

#ifndef STR_SRC
# define STR_SRC "AAAAAAA"
#endif

int func(int a, int b, int c)
{
        int sum = 0;
        char buffer[BUF_SIZE] = "";

        memset(buffer, '\0', BUF_SIZE);
        if ( sizeof(STR_SRC)-1 > BUF_SIZE ) {
                printf("buffer overflow!\n");
                exit(-1);
        }
        memcpy(buffer, STR_SRC, sizeof(STR_SRC)-1);

        sum = a + b + c;

        return sum;
}

int main()
{
        int sum;

        sum = func(1, 2, 3);

        printf("sum = %d\n", sum);

        return 0;
}

現在的效果如下:

$ gcc -DSTR4 -g -o testshellcode testshellcode.c
$ ./testshellcode      #如果存在溢位,那么就會得到阻止并退出,從而阻止可能的破壞
buffer overflow!
$ gcc -g -o testshellcode testshellcode.c
$ ./testshellcode
sum = 6

當然,如果能夠在 C 標準里頭加入對陣列操作的限制可能會更好,或者在編譯器中擴展對可能引起緩沖區溢位的語法檢查,

緩沖區注入實體

最后給出一個利用上述緩沖區溢位來進行緩沖區注入的例子,也就是通過往某個緩沖區注入一些代碼,并把eip修改為這些代碼的入口從而達到破壞目標程式行為的目的,

這個例子來自Linux 下緩沖區溢位攻擊的原理及對策,這里主要利用上面介紹的知識對它進行了比較詳細的分析,

準備:把 C 語言函式轉換為字串序列

首先回到第一部分,看看那個 Shellcode.c 程式,我們想獲取它的匯編代碼,并以十六進制位元組的形式輸出,以便把這些指令當字串存放起來,從而作為緩沖區注入時的輸入字串,下面通過 gdb 獲取這些內容,

$ gcc -g -o shellcode shellcode.c
$ gdb -q ./shellcode
(gdb) disassemble main
Dump of assembler code for function main:
...
0x08048331 <main+13>:   push   %ecx
0x08048332 <main+14>:   jmp    0x8048354 <forward>
0x08048334 <main+16>:   pop    %esi
0x08048335 <main+17>:   mov    $0x4,%eax
0x0804833a <main+22>:   mov    $0x2,%ebx
0x0804833f <main+27>:   mov    %esi,%ecx
0x08048341 <main+29>:   mov    $0xc,%edx
0x08048346 <main+34>:   int    $0x80
0x08048348 <main+36>:   mov    $0x1,%eax
0x0804834d <main+41>:   mov    $0x0,%ebx
0x08048352 <main+46>:   int    $0x80
0x08048354 <forward+0>: call   0x8048334 <main+16>
0x08048359 <forward+5>: dec    %eax
0x0804835a <forward+6>: gs
0x0804835b <forward+7>: insb   (%dx),%es:(%edi)
0x0804835c <forward+8>: insb   (%dx),%es:(%edi)
0x0804835d <forward+9>: outsl  %ds:(%esi),(%dx)
0x0804835e <forward+10>:        and    %dl,0x6f(%edi)
0x08048361 <forward+13>:        jb     0x80483cf <__libc_csu_init+79>
0x08048363 <forward+15>:        or     %fs:(%eax),%al
...
End of assembler dump.
(gdb) set logging on   #開啟日志功能,記錄操作結果
Copying output to gdb.txt.
(gdb) x/52bx main+14  #以十六進制單位元組(字符)方式列印出shellcode的核心代碼
0x8048332 <main+14>:    0xeb    0x20    0x5e    0xb8    0x04    0x00    0x00   0x00
0x804833a <main+22>:    0xbb    0x02    0x00    0x00    0x00    0x89    0xf1   0xba
0x8048342 <main+30>:    0x0c    0x00    0x00    0x00    0xcd    0x80    0xb8   0x01
0x804834a <main+38>:    0x00    0x00    0x00    0xbb    0x00    0x00    0x00   0x00
0x8048352 <main+46>:    0xcd    0x80    0xe8    0xdb    0xff    0xff    0xff   0x48
0x804835a <forward+6>:  0x65    0x6c    0x6c    0x6f    0x20    0x57    0x6f   0x72
0x8048362 <forward+14>: 0x6c    0x64    0x0a    0x00
(gdb) quit
$ cat gdb.txt | sed -e "s/^.*://g;s/\t/\\\/g;s/^/\"/g;s/\$/\"/g"  #把日志里頭的內容處理一下,得到這樣一個字串
"\0xeb\0x20\0x5e\0xb8\0x04\0x00\0x00\0x00"
"\0xbb\0x02\0x00\0x00\0x00\0x89\0xf1\0xba"
"\0x0c\0x00\0x00\0x00\0xcd\0x80\0xb8\0x01"
"\0x00\0x00\0x00\0xbb\0x00\0x00\0x00\0x00"
"\0xcd\0x80\0xe8\0xdb\0xff\0xff\0xff\0x48"
"\0x65\0x6c\0x6c\0x6f\0x20\0x57\0x6f\0x72"
"\0x6c\0x64\0x0a\0x00"

注入:在 C 語言中執行字串化的代碼

得到上面的字串以后我們就可以設計一段下面的代碼啦,

/* testshellcode.c */
char shellcode[]="\xeb\x20\x5e\xb8\x04\x00\x00\x00"
"\xbb\x02\x00\x00\x00\x89\xf1\xba"
"\x0c\x00\x00\x00\xcd\x80\xb8\x01"
"\x00\x00\x00\xbb\x00\x00\x00\x00"
"\xcd\x80\xe8\xdb\xff\xff\xff\x48"
"\x65\x6c\x6c\x6f\x20\x57\x6f\x72"
"\x6c\x64\x0a\x00";

void callshellcode(void)
{
   int *ret;
   ret = (int *)&ret + 2;
   (*ret) = (int)shellcode;
}

int main()
{
        callshellcode();

        return 0;
}

運行看看,

$ gcc -o testshellcode testshellcode.c
$ ./testshellcode
Hello World

竟然列印出了 Hello World,實際上,如果只是為了讓 Shellcode 執行,有更簡單的辦法,直接把 Shellcode 這個字串入口強制轉換為一個函式入口,并呼叫就可以,具體見這段代碼,

char shellcode[]="\xeb\x20\x5e\xb8\x04\x00\x00\x00"
"\xbb\x02\x00\x00\x00\x89\xf1\xba"
"\x0c\x00\x00\x00\xcd\x80\xb8\x01"
"\x00\x00\x00\xbb\x00\x00\x00\x00"
"\xcd\x80\xe8\xdb\xff\xff\xff\x48"
"\x65\x6c\x6c\x6f\x20\x57\x6f\x72"
"\x6c\x64\x0a\x00";

typedef void (* func)();            //定義一個指向函式的指標func,而函式的回傳值和引數均為void

int main()
{
        (* (func)shellcode)();

        return 0;
}

注入原理分析

這里不那樣做,為什么也能夠執行到 Shellcode 呢?仔細分析一下 callShellcode 里頭的代碼就可以得到原因了,

int *ret;

這里定義了一個指向整數的指標,ret 占用 4 個位元組(可以用 sizeof(int *) 算出),

ret = (int *)&ret + 2;

這里把 ret 修改為它本身所在的地址再加上兩個單位, 首先需要求出 ret 本身所在的位置,因為 ret 是函式的一個區域變數,它在堆疊中偏堆疊頂的地方, 然后呢?再增加兩個單位,這個單位是 sizeof(int),即 4 個位元組,這樣,新的 ret 就是 ret 所在的位置加上 8 個位元組,即往堆疊底方向偏移 8 個位元組的位置,對于我們之前分析的 Shellcode,那里應該是 edi,但實際上這里并不是 edi,可能是 gcc 在編譯程式時有不同的處理,這里實際上剛好是 eip,即執行這條陳述句之后 ret 的值變成了 eip 所在的位置,

(*ret) = (int)shellcode;

由于之前 ret 已經被修改為了 eip 所在的位置,這樣對 (*ret) 賦值就會修改 eip 的值,即下一條指令的地址,這里把 eip 修改為了 Shellcode 的入口,因此,當函式回傳時直接去執行 Shellcode 里頭的代碼,并列印了 Hello World ,

用 gdb 除錯一下看看相關變數的值的情況,這里主要關心 ret 本身, ret 本身是一個地址,首先它所在的位置變成了 EIP 所在的位置(把它自己所在的位置加上 2*4 以后賦于自己),然后,EIP 又指向了 Shellcode 處的代碼,

$ gcc -g -o testshellcode testshellcode.c
$ gdb -q ./testshellcode
(gdb) l
8       void callshellcode(void)
9       {
10         int *ret;
11         ret = (int *)&ret + 2;
12         (*ret) = (int)shellcode;
13      }
14
15      int main()
16      {
17              callshellcode();
(gdb) break 17
Breakpoint 1 at 0x804834d: file testshell.c, line 17.
(gdb) break 11
Breakpoint 2 at 0x804832a: file testshell.c, line 11.
(gdb) break 12
Breakpoint 3 at 0x8048333: file testshell.c, line 12.
(gdb) break 13
Breakpoint 4 at 0x804833d: file testshell.c, line 13.
(gdb) r
Starting program: /mnt/hda8/Temp/c/program/testshell

Breakpoint 1, main () at testshell.c:17
17              callshellcode();
(gdb) print $ebp       #列印ebp暫存器里的值
$1 = (void *) 0xbfcfd2c8
(gdb) disassemble main
...
0x0804834d <main+14>:   call   0x8048324 <callshellcode>
0x08048352 <main+19>:   mov    $0x0,%eax
...
(gdb) n

Breakpoint 2, callshellcode () at testshell.c:11
11         ret = (int *)&ret + 2;
(gdb) x/6x $esp
0xbfcfd2ac:     0x08048389      0xb7f4eff4      0xbfcfd36c      0xbfcfd2d8
0xbfcfd2bc:     0xbfcfd2c8      0x08048352
(gdb) print &ret #分別列印出ret所在的地址和ret的值,剛好在ebp之上,我們發現這里并沒有
       #之前的testshellcode代碼中的edi和esi,可能是gcc在匯編的時候有不同處理,
$2 = (int **) 0xbfcfd2b8
(gdb) print ret
$3 = (int *) 0xbfcfd2d8 #這里的ret是個隨機值
(gdb) n

Breakpoint 3, callshellcode () at testshell.c:12
12         (*ret) = (int)shellcode;
(gdb) print ret   #執行完ret = (int *)&ret + 2;后,ret變成了自己地址加上2*4,
                  #剛好是eip所在的位置,
$5 = (int *) 0xbfcfd2c0
(gdb) x/6x $esp
0xbfcfd2ac:     0x08048389      0xb7f4eff4      0xbfcfd36c      0xbfcfd2c0
0xbfcfd2bc:     0xbfcfd2c8      0x08048352
(gdb) x/4x *ret  #此時*ret剛好為eip,0x8048352
0x8048352 <main+19>:    0x000000b8      0x8d5d5900      0x90c3fc61      0x89559090
(gdb) n

Breakpoint 4, callshellcode () at testshell.c:13
13      }
(gdb) x/6x $esp #現在eip被修改為了shellcode的入口
0xbfcfd2ac:     0x08048389      0xb7f4eff4      0xbfcfd36c      0xbfcfd2c0
0xbfcfd2bc:     0xbfcfd2c8      0x8049560
(gdb) x/4x *ret  #現在修改了(*ret)的值,即修改了eip的值,使eip指向了shellcode
0x8049560 <shellcode>:  0xb85e20eb      0x00000004      0x000002bb      0xbaf18900

上面的程序很難弄,呵呵,主要是指標不大好理解,如果直接把它當地址繪出下面的圖可能會容易理解一些,

callshellcode堆疊的初始分布:

ret=(int *)&ret+2=0xbfcfd2bc+2*4=0xbfcfd2c0
0xbfcfd2b8      ret(隨機值)                     0xbfcfd2c0
0xbfcfd2bc      ebp(這里不關心)
0xbfcfd2c0      eip(0x08048352)         eip(0x8049560 )

(*ret) = (int)shellcode;即eip=0x8049560

總之,最后體現為函式呼叫的下一條指令指標(eip)被修改為一段注入代碼的入口,從而使得函式回傳時執行了注入代碼,

緩沖區注入與防范

這個程式里頭的注入代碼和被注入程式竟然是一個程式,傻瓜才自己攻擊自己(不過有些黑客有可能利用程式中一些空閑空間注入代碼哦),真正的緩沖區注入程式是分開的,比如作為被注入程式的一個字串引數,而在被注入程式中剛好沒有做字串長度的限制,從而讓這段字串中的一部分修改了 eip,另外一部分作為注入代碼運行了,從而實作了注入的目的,不過這會涉及到一些技巧,即如何剛好用注入代碼的入口地址來修改 eip (即新的 eip 能夠指向注入代碼)?如果 eip 的位置和緩沖區的位置之間的距離是確定,那么就比較好處理了,但從上面的兩個例子中我們發現,有一個編譯后有 edi 和 esi,而另外一個則沒有,另外,緩沖區的位置,以及被注入程式有多少個引數我們都無法預知,因此,如何計算 eip 所在的位置呢?這也會很難確定,還有,為了防止緩沖區溢位帶來的注入問題,現在的作業系統采取了一些辦法,比如讓 esp 隨機變化(比如和系統時鐘關聯起來),所以這些措施將導致注入更加困難,如果有興趣,你可以接著看看最后的幾篇參考資料并進行更深入的研究,

需要提到的是,因為很多程式可能使用 strcpy 來進行字串的復制,在實際撰寫緩沖區注入代碼時,會采取一定的辦法(指令替換),把代碼中可能包含的 \0 位元組去掉,從而防止 strcpy 中斷對注入代碼的復制,進而可以復制完整的注入代碼,具體的技巧可以參考 Linux下緩沖區溢位攻擊的原理及對策,Shellcode技術雜談,virus-writing-HOWTO,

后記

實際上緩沖區溢位應該是語法和邏輯方面的雙重問題,由于語法上的不嚴格(對陣列邊界沒有檢查)導致邏輯上可能出現嚴重缺陷(程式執行行為被改變),另外,這類問題是對程式運行程序中的程式映像的堆疊區進行注入,實際上除此之外,程式在安全方面還有很多類似的問題,比如,雖然程式映像的正文區受到系統保護(只讀),但是如果記憶體(硬體本身,記憶體條)出現故障,在程式運行的程序中,程式映像的正文區的某些位元組就可能被修改了,也可能發生非常嚴重的后果,因此程式運行程序的正文區檢查等可能的手段需要被引入,

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/262344.html

標籤:C

上一篇:C/C++編程筆記:C++中的isspace()及其在計算空格字符中的應用

下一篇:C/C++編程學習:百行代碼實作小游戲(剪刀石頭布)

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more