meltdown分析

參考文章 寫的很不錯 http://open.appscan.io/article-348.html

meldown是2018年爆出的硬件漏洞,涉及Intel CPU和ARM Cortex A75。但影響沒有spectre來的深遠。
1. 背景知識

在深入分析Meltdown之前,我們需要了解一些背景知識。它包括CPU Cache,CPU指令執行,操作系統地址空間隔離的設計。接下來我們依次來看這些知識點。

1.1 CPU Cache

現代處理器執行指令的瓶頸已經不在CPU端,而是在內存訪問端。因爲CPU的處理速度要遠遠大於物理內存的訪問速度,所以爲了減輕CPU等待數據的時間,在現代處理器設計中都設置了多級的cache單元。

 圖1 經典處理器的存儲結構

如圖1所示,一個擁有2個CPU的系統, 每個CPU有兩個Core, 每個Core有兩個線程的Cache架構。每一個Core有單獨的L1 cache, 它由其中的線程所共享, 每一個CPU中的所有Core共享同一個L2 Cache和L3 Cache。
L1 cache最靠近處理器核心,因此它的訪問速度也是最快的,當然它的容量也是最小的。CPU訪問各級的Cache速度和延遲是不一樣的,L1 Cache的延遲最小,L2 Cache其次,L3 Cache最慢。
下面是Xeon 5500 Series的各級cache的訪問延遲:(根據CPU主頻的不同,1個時鐘週期代表的時間也不一樣,在1GHz主頻的CPU下,一個時鐘週期大概是1納秒,在2.1GHz主頻的CPU下,訪問L1 Cache也就2個納秒)。

表1:各級存儲結構的訪問延遲
這裏寫圖片描述
如表1所示,我們可以看到各級內存訪問的延遲有很大的差異。CPU訪問一塊新的內存時,它會首先把包含這塊內存的Cache Line大小的內容獲取到L3 Cache,然後是載入到L2 Cache,最後載入到了L1 Cache。這個過程需要訪問主存儲器,因此延遲會很大,大約需要幾十納秒。當下次再讀取相同一塊數據的時候直接從L1 Cache裏取數據的話,這個延遲大約只有4個時鐘週期。當L1 Cache滿了並且有新的數據要進來,那麼根據Cache的置換算法會選擇一個Cache line置換到L2 Cache裏,L3 Cache也是同樣的道理。
1.2 cache攻擊

我們已經知道同一個CPU上的Core共享L2 cache和L3 Cache, 如果內存已經被緩存到CPU cache裏, 那麼同一個CPU的Core就會用較短的時間取到內存裏的內容, 否則取內存的時間就會較長。兩者的時間差異非常明顯(大約有300個CPU時鐘週期), 因此攻擊者可以利用這個時間差異來進行攻擊。
來看下面的示例代碼:

1: clflush for user_probe[]; // 把user_probe_addr對應的cache全部都flush掉
2: u8 index = (u8 ) attacked _mem_addr; // attacked_mem_addr存放被攻擊的地址
3: data = user_probe_addr[index 4096]; // user_probe_addr
存放攻擊者可以放訪問的基地址

user_probe_addr[]是一個攻擊者可以訪問的, 255 4096 大小的數組。
第1行, 把user_probe_addr數組對應的cache全部清除掉。
第2行, 我們設法訪問到attacked_mem_addr中的內容. 由於CPU權限的保護, 我們不能直接取得裏面的內容, 但是可以利用它來造成我們可以觀察到影響。
第3行, 我們用訪問到的值做偏移, 以4096爲單位, 去訪問攻擊者有權限訪問的數組,這樣對應偏移處的內存就可以緩存到CPU cache裏。
這樣, 雖然我們在第2行處拿到的數據不能直接看到, 但是它的值對CPU cache已經造成了影響。
接下來可以利用CPU cache來間接拿到這個值. 我們以4096爲單位依次訪問user_probe_addr對應內存單位的前幾個字節, 並且測量這該次內存訪問的時間, 就可以觀察到時間差異, 如果訪問時間短, 那麼可以推測訪內存已經被cache, 可以反推出示例代碼中的index的值。
在這個例子裏, 之所以用4096字節做爲訪問單位是爲了避免內存預讀帶來的影響, 因爲CPU在每次從主存訪問內存的時候, 根據局部性原理, 有可能將鄰將的內存也讀進來。Intel的開發手冊上指明 CPU的內存預取不會跨頁面, 而每個頁面的大小是4096。
Meltdown[3]論文中給出了他們所做實驗的結果, 引用如下:
圖2 cache攻擊數據對比

這裏寫圖片描述

據此, 他們反推出index的值爲84。
1.3 指令執行

經典處理器架構使用五級流水線:取指(IF)、譯碼(ID)、執行(EX)、數據內存訪問(MEM)和寫回(WB)。
現代處理器在設計上都採用了超標量體系結構(Superscalar Architecture)和亂序執行(Out-of-Order)技術,極大地提高了處理器計算能力。超標量技術能夠在一個時鐘週期內執行多個指令,實現指令級的並行,有效提高了ILP(InstructionLevel Parallelism)指令級的並行效率,同時也增加了整個cache和memory層次結構的實現難度。
在一個支持超標量和亂序執行技術的處理器中,一條指令的執行過程被分解爲若干步驟。指令首先進入流水線(pipeline)的前端(Front-End),包括預取(fetch)和譯碼(decode),經過分發(dispatch)和調度(scheduler)後進入執行單元,最後提交執行結果。所有的指令採用順序方式(In-Order)通過前端,並採用亂序的方式進行發射,然後亂序執行,最後用順序方式提交結果。若是一條存儲讀寫指令最終結果更新到LSQ(Load-StoreQueue)部件。LSQ部件是指令流水線的一個執行部件,可以理解爲存儲子系統的最高層,其上接收來自CPU的存儲器指令,其下連接着存儲器子系統。其主要功能是將來自CPU的存儲器請求發送到存儲器子系統,並處理其下存儲器子系統的應答數據和消息。
這裏寫圖片描述
圖3 經典x86處理器架構
如圖3所示,在x86微處理器經典架構中,指令從L1指令cache中讀取指令,L1指令cache會做指令加載、指令預取、指令預解碼,以及分支預測。然後進入Fetch& Decode單元,會把指令解碼成macro-ops微操作指令,然後由Dispatch部件分發到Integer Unit或者Float Point Unit。Integer Unit由Integer Scheduler和Execution Unit組成,Execution Unit包含算術邏輯單元(arithmetic-logic unit,ALU)和地址生成單元(address generation unit,AGU),在ALU計算完成之後進入AGU,計算有效地址完畢後,將結果發送到LSQ部件。LSQ部件首先根據處理器系統要求的內存一致性(memory consistency)模型確定訪問時序,另外LSQ還需要處理存儲器指令間的依賴關係,最後LSQ需要準備L1 cache使用的地址,包括有效地址的計算和虛實地址轉換,將地址發送到L1 DataCache中。

1.4 亂序執行(out-of-order execution)

剛纔提到了現代的處理器爲了提高性能,實現了亂序執行(Out-of-Order,OOO)技術。在古老的處理器設計中,指令在處理器內部執行是嚴格按照指令編程順序的,這種處理器叫做順序執行的處理器。在順序執行的處理器中,當一條指令需要訪問內存的時候,如果所需要的內存數據不在Cache中,那麼需要去訪問主存儲器,訪問主存儲器的速度是很慢的,那麼這時候順序執行的處理器會停止流水線執行,在數據被讀取進來之後,然後流水線才繼續工作。這種工作方式大家都知道一定會很慢,因爲後面的指令可能不需要等這個內存數據,也不依賴當前指令的結果,在等待的過程中可以先把它們放到流水線上去執行。所以這個有點像在火車站排隊買票,正在買票的人發現錢包不見了,正在着急找錢,可是後面的人也必須停下來等,因爲不能插隊。
1967年Tomasulo提出了一系列的算法來實現指令的動態調整從而實現亂序執行,這個就是著名的Tomasulo算法。這個算法的核心是實現一個叫做寄存器重命名(Register Rename)來消除寄存器數據流之間依賴關係,從而實現指令的並行執行。它在亂序執行的流水線中有兩個作用,一是消除指令之間的寄存器讀後寫相關(Write-after-Read,WAR)和寫後寫相關(Write-after-Write,WAW);二是當指令執行發生例外或者轉移指令猜測錯誤而取消後面的指令時,可用來保證現場的精確。其思路爲當一條指令寫一個結果寄存器時不直接寫到這個結果寄存器,而是先寫到一箇中間寄存器過渡,當這條指令提交時再寫到結果寄存器中。
通常處理器實現了一個統一的保留站(reservationstation),它允許處理器把已經執行的指令的結果保存到這裏,然後在最後指令提交的時候會去做寄存器重命名來保證指令順序的正確性。
如圖3所示,經典的X86處理器中的“整數重命名”和“浮點重命名”部件(英文叫做reorder buffer,簡稱ROB),它會負責寄存器的分配、寄存器重命名以及指令丟棄(retiring)等作用。
x86的指令從L1 指令cache中預取之後,進入前端處理部分(Front-end),這裏會做指令的分支預測和指令編碼等工作,這裏是順序執行的(in-order)。指令譯碼的時候會把x86指令變成衆多的微指令(uOPs),這些微指令會按照順序發送到執行引擎(Execution Engine)。執行引擎這邊開始亂序執行了。這些指令首先會進入到重命名緩存(ROB)裏,然後ROB部件會把這些指令經由調度器單元發生到各個執行單元(Execution Unit,簡稱EU)裏。假設有一條指令需要訪問內存,這個EU單元就停止等待了,但是後面的指令不需要停頓下來等這條指令,因爲ROB會把後面的指令發送給空閒的EU單元,這樣就實現了亂序執行。
如果用高速公路要做比喻的話,多發射的處理器就像多車道一樣,汽車不需要按照發車的順序在高速公路上按順序執行,它們可以隨意超車。一個形象的比喻是,如果一個汽車拋錨了,後面的汽車不需要排隊等候這輛汽車,可以超車。
在高速公里的終點設置了一個很大的停車場,所有的指令都必須在停車場裏等候,然後停車場裏有設置了一個出口,所有指令從這個出口出去的時候必須按照指令原本的順序,並且指令在出口的時候必須進行寫寄存器操作。這樣從出口的角度看,指令就是按照原來的邏輯順序一條一條出去並且寫寄存器。
這樣從處理器角度看,指令是順序發車,亂序超車,順序歸隊。那麼這個停車場就是ROB,這個緩存機制可以稱爲保留站(reservation station),這種亂序執行的機制就是人們常說的亂序執行。

1.5 地址空間

現代的處理器爲了實現CPU的進程虛擬化,都採用了分頁機制,分頁機制保證了每個進程的地址空間的隔離性。分頁機制也實現了虛擬地址到物理地址的轉換,這個過程需要查詢頁表,頁表可以是多級頁表。那麼這個頁表除了實現虛擬地址到物理地址的轉換之外還定義了訪問屬性,比如這個虛擬頁面是隻讀的還是可寫的還是可執行的還是隻有特權用戶才能訪問等等權限。

每個進程的虛擬地址空間都是一樣的,但是它映射的物理地址是不一樣的,所以每一個進程都有自己的頁表,在操作系統做進程切換的時候,會把下一個進程的頁表的基地址填入到寄存器,從而實現進程地址空間的切換。以外,因爲TLB裏還緩存着上一個進程的地址映射關係,所以在切換進程的時候需要把TLB對應的部份也清除掉。
當進程在運行的時候不可避免地需要和內核交互,例如系統調用,硬件中斷。當陷入到內核後,就需要去訪問內核空間,爲了避免這種切換帶來的性能損失以及TLB刷新,現代OS的設計都把用戶空間和內核空間的映射放到了同一張頁表裏。這兩個空間有一個明顯的分界線,在Linux Kernel的源碼中對應PAGE_OFFSET。
這裏寫圖片描述
圖4 進程地址空間
如圖4所示,雖然兩者是在同一張頁表裏,但是他們對應的權限不一樣,內核空間部份標記爲僅在特權層可以訪問,而用戶空間部份在特權層與非特權層都可以訪問。這樣就完美地把用戶空間和內核空間隔離開來:當進程跑在用戶空間時只能訪問用戶空間的地址映射,而陷入到內核後就即可以訪問內核空間也可以訪問用戶空間。
對應地,頁表中的用戶空間映射部份只包含本機程可以訪問的物理內存映射,而任意的物理內存都有可能會被映射到內核空間部分。

1.5 異常處理

CPU指令在執行的過程過有可能會產生異常,但是我們的處理器是支持亂序執行的,那麼有可能異常指令後面的指令都已經執行了,那怎麼辦?
我們從處理器內部來考察這個異常的發生。操作系統爲了處理異常,有一個要求就是,當異常發生的時候,異常發生之前的指令都已經執行完成,異常指令後面的所有指令都沒有執行。但是我們的處理器是支持亂序執行的,那麼有可能異常指令後面的指令都已經執行了,那怎麼辦?
那麼這時候ROB就要起到清道夫的作用了。從之前的介紹我們知道亂序執行的時候,要修改什麼東西都通過中間的寄存器暫時記錄着,等到在ROB排隊出去的時候才真正提交修改,從而維護指令之間的順序關係。那麼當一條指令發生異常的時候,它就會帶着異常“寶劍”來到ROB中排隊。ROB按順序把之前的正常的指令都提交發送出去,當看到這個帶着異常“寶劍”的指令的時候,那麼就啓動應急預案,把出口封鎖了,也就是異常指令和其後面的指令會被丟棄掉,不提交。
但是,爲了保證程序執行的正確性,雖然異常指令後面的指令不會提交,可是由於亂序執行機制,後面的一些訪存指令已經把物理內存數據預取到cache中了,這就給Meltdown漏洞留下來後面,雖然這些數據會最終被丟棄掉。

2.Meltdown分析

下面我們對Meltdown漏洞做一些原理性的分析和後續修補的方案。

2.1 漏洞分析

理解了上述的背景知識以後就可以來看Meltdown是怎麼回事了. 我們再回過頭看看上面的例子:

1: clflush for user_probe[]; // 把user_probe_addr對應的cache全部都flush掉
2: u8 index = (u8 ) attacked _mem_addr; // attacked_mem_addr存放被攻擊的地址
3: data = user_probe_addr[index * 4096]; // user_probe_addr存放攻擊者可以放訪問的基地址

如果attached_mem_addr位於內核, 我們就可以利用它來讀取內核空間的內容。
如果CPU順序執行, 在第2行就會發現它訪問了一個沒有權限地址, 產生page fault (缺頁異常), 進而被內核捕獲, 第3行就沒有機會運行。不幸的是, CPU會亂序執行, 在某些條件滿足的情況下, 它取得了attacked _mem_addr裏的值, 並在CPU將該指令標記爲異常之前將它傳遞給了下一條指令, 並且隨後的指令利用它來觸發了內存訪問。在指令提交的階段CPU發現了異常,再將已經亂序執行指令的結果丟棄掉。這樣雖然沒有對指令的正確性造成影響, 但是亂序執行產生的CPU cache影響依然還是在那裏, 並能被利用。
該場景有個前置條件,該條件在Meltdown[3]的論文裏沒有被提到,但在cyber[1] 的文章指出,attached_mem_addr必須要已經被緩存到了 CPU L1,因爲這樣纔會有可能在CPU將指令標記爲異常之前指數據傳給後續的指令。 並且cyber 指出,只有attacked_mem_addr已經被緩存到CPU L1 纔有可能成功,在L2,L3均不行,其理由是:
“The L1 Cache is a so called VIPT or Virtually Indexed, PhysicallyTagged cache. This means the data can be looked up by directly using thevirtual address of the load request”
“If the requested data was not found in the L1 cache the load must bepassed down the cache hierarchy. This is the point where the page tables comeinto play. The page tables are used to translate the virtual address into aphysical address. This is essentially how paging is enabled on x64. It isduring this translation that privileges are checked”
這幾點理由很值得商榷:
1) AMD的開發手冊[10]沒有找到L1 cache是VIPT的證據,Intel的開發手冊[9]上只能從” L1 Data Cache Context Mode”的描述上推測NetBurst架構的L1 cache是VIPT的。
2) 就算L1 cache 是 VIPT,那也需要獲得physicaladdress,必然會用到TLB裏的內容或者進行頁表的遍歷。

那麼如何來將要被攻擊的內存緩存到L1裏呢? 有兩種方法

1) 利用系統調用進入內核。如果該系統調用的路徑訪問到了該內存,那麼很有可能會將該內存緩存到L1 (在footprint不大於L1大小的情況下)。
2) 其次是利用prefetch指令。 有研究[8]顯示,Intel的prefetch指令會完全忽略權限檢查,將數據讀到cache。
我們知道,如果進程觸發了不可修復的page fault,內核會向其發送SIGSEGV信號,而不能繼續往下執行。所以這裏有兩種操作方法,其一是創建一個子進程,在子進程中觸發上述的代碼訪問,然後在父進程中去測算user_probe_addr[]的訪問時間。 所以每一次探測都需要另起一個新進程,這樣會影響效率。
另一種方法是利用Intel的事務內存處理(Intel®Transactional Synchronization Extensions),該機制以事務爲單元來對一系列內存操作做原子操作,如果一個事務內的內存操作全部成功完成且沒有其它CPU造成內存的競爭,那麼就會將該事務對應的結果進行提交,否則將中斷該事務。如果我們將上述代碼包含到一個內存事務中,對被攻擊地址的訪問並不會造成pagefault,只會被打斷事務。 這樣我們可以在不需要生成子進程的條件下持續進行攻擊。
這裏有一個很有意思的現象,上述代碼在第2行處讀到的index有時會全爲0,不同的資料有不同的解釋:
1) cyber[1]給出的解釋是: “Fortunately Idid not get a slow read suggesting that Intel null’s the result when the access is notallowed”。
2) google zero project[2]給出的解釋是: “That (read from kernel address returns all-zeroes) seems to happen for memory that is not sufficiently cached but for which pagetable entries are present, at least after repeated read attempts. For unmapped memory, the kernel address read does not return a result at all.”
3) Meltdown paper[3]給出的解釋是: “If the exception is triggered whiletrying to read from an inaccessible kernel address,the register where the data shouldbe stored,appears to bezeroed out. This is reasonable because if the exception is unhandled,the user space application isterminated,and the valuefrom the inaccessible kernel address could be observed in the register contentsstored in the core dump of the crashed process. The direct solution to fix thisproblem is to zero out the corresponding registers. If the zeroing out of theregister is faster than the execution of the sub- sequent instruction (line 5in Listing 2),the attackermay read a false value in the third step”。
這個解釋比較有意思,他們首先認爲,將讀到的內容清零是有必要的,否則讀到的內容會保存到這個程序的core dump裏。 果真會如此麼? Terminate 進程和生產core dump都需要OS去做,軟件不可能直接訪問到亂序執行所訪問的寄存器。
其次他們認爲,該問到0是因爲值傳遞給下一條指令的速度要慢於將值清0的操作,所以他們的解決方法是:
“prevent the tran- sientinstruction sequence from continuing with a wrong value,i.e.,‘0’,Meltdown retries reading the address until itencounters a value different from ‘0’”
所以他們的示例代碼是長這樣的:
這裏寫圖片描述
他們在讀到0時現再重試. 然而, 如果真的讀到清0的數據, retry並不會有機會再被執行到, 因爲此時很有可能異常已經被捕獲。

2.2 漏洞修復

在漏洞被報告給相關廠商後,各OS和開源社區開始了修復工作,LinuxKernel採用的是Kernelpage-table isolation (KPTI)[6][7],據說Windows和Mac OS的修復也是類似的思路。
在前面的背景知識中看到, 當前的OS採用用戶空間和內核空間分段的設計, 這樣使得Kernel和Usersapce使用同一張頁表, 位於同一個TLB context中, 所以CPU在做預取和亂序的時候可以使用TLB中的cache做地址轉換, 進而獲得CPU Cache中的數據, 如果我們能夠讓用戶空間不能使用TLB中關於內核地址映射的信息, 這樣就可以斷掉用戶空間對Cache中kernel數據的訪問,這也是KPTI的思路。
KPTI將之前OS設計中, 每個進程使用一張頁表分隔成了兩張, kernelspace和userspace使用各自分離的頁表。我們暫且稱進程在kernel模式使用的頁表稱爲Kernel頁表, 相應地進程在用戶空間使用的頁表被稱爲用戶頁表。
具體地來說, 當進程運行在用戶空間時, 使用的是用戶頁表, 當發生中斷或者是異常時, 需要陷入到內核, 進入內核空間後, 有一小段內核跳板將頁表切換到內核頁表, 當進程從kernel空間跳回到用戶空間時, 頁表再次被切換回用戶頁表。
Kernel頁表包含了進程用戶空間地址的映射和Kernel使用的內存映射, 所以Kernel依然可以使用當前進程的內存映射。用戶頁表僅僅包含了用戶空間的內存映射以及內核跳板的內存映射。
2.3 性能影響
從這裏可以看到, 每一次用戶空間到內核空間的切換都需要切換頁表, 在沒有PCID支持的CPU上, 切換頁表 (reload CR3) 會flush除了global page以外的所有TLB。在支持PCID的情況下, 大部分應用場景的性能損失微不足道。

(下面這段是編輯添加的)
引用幾大科技公司的原話可以看出性能影響微不足道:
Apple: “Our testing with public benchmarks has shown that the changes in the December 2017 updates resulted in no measurable reduction in the performance of macOS and iOS as measured by the GeekBench 4 benchmark, or in common Web browsing benchmarks such as Speedometer, JetStream, and ARES-6.” (蘋果表示在macOS和iOS上沒發現有性能損失)
Microsoft: “The majority of Azure customers should not see a noticeable performance impact with this update. We’ve worked to optimize the CPU and disk I/O path and are not seeing noticeable performance impact after the fix has been applied.”(微軟表示在Azure中沒看到性能影響)
Amazon: “We have not observed meaningful performance impact for the overwhelming majority of EC2 workloads.”(亞馬遜說在EC2應用場景中沒觀察到性能損失)
Google: “On most of our workloads, including our cloud infrastructure, we see negligible impact on performance.”(谷歌表示在它們絕大部分的應用場景中包括雲設施,都只是微不足道的性能影響)

  1. 總結

在這裏需要指出的是, 也是所有paper沒有提到的, 雖然在當前的OS的設計中, 進程在內核空間和用戶空間使用的是同一張頁表, 但是該頁表的內核部份映射的生成是on-demand的, 即在訪問的時候纔會被逐漸映射,因此只有在系統調用上被touch到的內存纔有可能被映射到頁表裏。所以被嚴格限制系統調用的用戶攻擊難度會更大一些, 例如使用seccomp,然後儘管如此, 所有的代碼路徑不可能完全被審計到。
Meltdown漏洞並不需要利用已有的軟件缺陷, 僅僅只需攻擊者和受害者在只有一個地址空間中就會有影響, 比如基於container的Docker、 Xen的PV guest等等。基於硬件虛擬化的VM並不會受其影響,然而情況不容樂觀, 接下來要分析的Spectre具有更大範圍的破壞力。
雖然這篇文章是以cache爲例來描述攻擊, 但是對體系體構可觀察到的影響都可以拿來作爲攻擊的手段, 比如誘發CPU算術單元的繁忙運算後再來觀突某條算術指令執行的時間, 再如觀察不同情況下的電力消耗等等。
這一次漏洞的影響之大足以被寫進教科書, 甚至會影響接下來所有硬件和OS的設計, 2018年或許是OS, Hardware, Security的新起點。

參考資料
[1] Negative Result:Reading Kernel Memory From User Modehttps://cyber.wtf/2017/07/28/negative-result-reading-kernel-memory-from-user-mode/
[2] Google Zero Projecthttps://googleprojectzero.blogspot.com/
[3] Meltdownhttps://meltdownattack.com/meltdown.pdf
[4] Spectre Attacks:Exploiting Speculative Execution https://spectreattack.com/spectre.pdf
[5] KAISER: hiding thekernel from user space https://lwn.net/Articles/738975/
[6] The current state ofkernel page-table isolation https://lwn.net/Articles/741878/
[7] Kernel page-tableisolation https://en.wikipedia.org/wiki/Kernel_page-table_isolation
[8] Prefetch Side-ChannelAttacks: Bypassing SMAP and Kernel ASLR https://gruss.cc/files/prefetch.pdf
[9] Intel® 64 and IA-32architectures software developer’s manualhttps://software.intel.com/sites/default/files/managed/39/c5/325462-sdm-vol-1-2abcd-3abcd.pdf
[10] AMD64 ArchitectureProgrammer’s Manual https://developer.amd.com/resources/developer-guides-manuals/

原文 / From mp.weixin.qq.com/s/zlspXeDGlAEzVsq2h6gg8w

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章