Contents

Egalito札記

開發日常

想要開發自己的pass,可以加入main file和對應的header file在/src/pass裡面,在把選項放在ethardenetshell裡面。Compile就只要在/app資料夾裡面下make就好,他就會進行部分編譯和連結。

這裡記錄一些常見到的function:
recurse()可以在dump.h裡面找到:

1
2
3
4
5
void recurse(Type *root) {
    for(auto child : root->getChildren()->genericIterable()) {
        child->accept(this);
    }
}

可以知道這個函式會用來往下訪問所有的children…
accept()可以提供各種類型chunk的遍歷方式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
void Chunk::accept(ChunkVisitor *visitor) {
    visitor->visit(this);
}
...then traverse...
    virtual void visit(Program *program) {}
    virtual void visit(Module *module);
    virtual void visit(FunctionList *functionList);
    ...
    virtual void visit(Function *function);
    virtual void visit(Block *block);
    virtual void visit(Instruction *instruction);
    ...

Chunk的屬性和功能 /src/chunk/

我這裡就介紹一些我常用的功能,剩下更多可以到/src/chunk/裡的header file一探究竟…

不得不說,Egalito對binary representation架構的設計非常有趣,我們可以把整個架構視為一種樹狀結構。這裡不得不提到,為何Egalito是選擇lift to IR的這種recompilation方式呢?回歸到paper的title,他講求一個layout-agnostic,他需要設計一個對多種架構都相容的表達方式:也就是他的EIR!作者在documentation也很貼心得放上一張介紹圖:
/10-19-21/chunk.jpeg
首先,chunk是egalito設計階層式資料結構的基本單位(可以搭配上面這張圖)。Module代表載入記憶體的ELF; library代表還沒被解析的ELF。有些chunk也有position的屬性(圖中深藍色的那幾位),像是functions,instructions; Global variables就被放在Data Variable那塊裡面。還沒被解析的symbol就放在ExternalSymbol那塊裡面。大部分都有繼承chunk.h的功能,剩下chunk特有的功能可以看各個chunk的header file,e.g. 要看block特有的功能,就要去翻block.h

上面的圖上還有Non-chunk的類型,當然也不能落下。其中我覺得Link尤其重要,他在很多chunk之間扮演連結的角色。參考link.h,有很多種link包含NormalLink, PLTLink, UnresolvedLink。參考semantic.h,每個指令都會有其對應的作用,大部分都是對各種平台相容的(當然嘛,一定有共同作用的指令,只是format不一樣),當然也有只限於某個平台的semantic。最後Assembly則是每個指令disassembly的結果。

如果我們想要一次造訪多種chunk,最常見的方法就是用visitor。讓pass繼承ChunkVisitor,把不同單位的visit()改成自己想要的樣子。大體上會長這樣,

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Pass : public ChunkVisitor {
public:
    virtual void visit(Function *function) {
        recurse(function);
    }

    // Not needed -- this is provided by default
    //virtual void visit(Block *block) { recurse(block); }

    virtual void visit(Instruction *instruction) {
        ChunkDumper().visit(instruction);
    }
}

這個recurse()的功用就是讓他繼續往children chunk遍歷,也因此我們instruction就沒有繼續往下了,因為他幾乎算是最小單位了。

有一個archive功能可以把整個program的chunk序列化表示,可是好像不太能用…

基於指令的修改 [2]

指令插入

可以參考ChunkMutator這個class.裡面有提供像是insertBefore的函式,我們可以指定某個chunk,然後將指令插入在他前面.Egalito還有提供是否選擇insertBefore還是insertBeforeJumpTo,差別在於如果後面有往前跳的語句,那到底要跳到舊的地方還是新的插入點呢?Egalito透過修改InstructionSemantics來實現這一點.

插入新的code當然也可能是獨立的basic block,但是這部分不會在插入後就自動處理好,需要透過SplitBasicBlocks這個pass來做處理.

關於呼叫函式的修改

先談論一下caller-saved register和callee-saved register的區別.兩者的區別在於誰負責保存?Callee-saved在function call的整個過程中不能不修改到,但是caller-saved可以,因此只要caller-saved沒有被用作參數,就可以用來儲存我們的變數.在Egalito中,r10r11就常常在x86_64的平台上被使用來儲存變數.

接著介紹一下x86_64中的red zone[3].leaf function是指不會再呼叫其他function,呼叫鏈最底層的函式,編譯器會把它優化為不需要prologue和epilogue來創造stack,而是直接使用rsp0x80字節的區間作為他的stack.

除此之外,XMM register在函式呼叫的過程中也不會被修改到,但要注意的一點是如果要放上stack,他必須以16字節對齊,不然會segfault.

關於jmp的修改

以下幾個場景都會用到jmp:

  • 跳到指定的basic block
  • 尾遞歸<呼叫別的函式,並且讓他繼承目前的stack>
  • jmp table,也就是indirect jmp
  • 間接型尾遞歸<不常見>

Egalito將jmp區分為內部(函式內),外部(尾遞歸),以及jmp table幾種種類.

間接jmp或call

在x86_64的平台上,jmp的目標地址常常會用很複雜的方式計算出來,e.g.

1
2
jmpq *(%rax, %rbx, 8)   ; PIC的jmp table
callq *0x40(%rax)       ; C++的虛擬表

Egalito提供pass將目標地址存入%r11,好讓我們檢視目標地址的值,然後呼叫%r11

其實egalito在保留data避免覆蓋的部分做了不少努力,像是InstrumentInstructionPass讓我們可以在任何地方插入call指令,並且不用擔心register的細節!更多細節可以參考像是endbrenforce, syscallsandbox, retpoline, 或instrumentinstr等等pass.

手把手插入各種chunk

以block為單位操作

這裡演示一下怎麼用ChunkMutator刪除basic block,

1
2
ChunkMutator mutator(function);
mutator.remove(block);

沒錯!就是這麼簡單!

插入/修改指令

Egalito提供幾種插入指令語句的方法,其中一種是使用Disassemble::instruction({opcode}),這種方法可以讓使用者提供machine code,然後反編成assembly language插入要修改的地方。這裡順便提醒一下我實現的時候掉進去的坑 😅 注意!這個方法每次只能插入一行指令,不然會報類似terminate called after thowing an instance of char const的錯,推測是因為把指令全部混在一起了!

呼叫ChunkMutatorinsertBefore()的方法,一次只能插入一個語句:

1
mutator.insertBefore(insertpoint, Disassemble::instruction({opcode}));

那時候其實還擔心如果想這樣插入多語句在同一個地方,insertpoint不就要一直更新(畢竟index會往後加)?其實不用擔心這個問題,index會在mutator結束的時候才一起更新,包括address還有指向鄰居的pointer一堆東西都在mutator解構的時候才會一起更新,所以不用擔心這個問題!

btw, insertpoint要怎麼抓呢?如果insert point有一定的位置,那我們可以透過block->getChildren()->getIterable()->get(idx)的方法就好。但這裡再介紹一個很酷的方法,假設我想要抓function prologue為我的insert point,我也可以透過下面的方法:

1
2
3
auto parent = dynamic_cast<Function *>(block->getParent());
FrameType frameType(parent);
auto prologueEnd = frameType.getSetSPInstr();

這邊我是在找到想要的block之後,去把它cast成Function(因為這個FrameType是針對function的)。這個class下面有提供幾個直接抓指令的method!

當然插入的指令之間有可能存在著控制流的跳轉,我們知道哪個指令會jmp到哪個指令,但是不可能直接copy/paste指令過去,因為無法預測compiler怎麼處理這個工作。幸運的是,Egalito也提供我們這個功能,還記得前面提到的link能夠連結兩個chunk嘛?現在我們就來用用這個功能,假設我們要製作一個jmp指令讓他跳到instr:

1
2
3
4
5
auto jmp = new Instruction();
// the last parameter means the number of padding bytes
auto jmpSem = new ControlFlowInstruction(X86_INS_JMP, jmp, "\xeb\x0e", "jmp", 0);
jmpSem->setLink(new NormalLink(instr, Link::SCOPE_EXTERNAL_JUMP));
jmp->setSemantic(jmpSem);

記住!這裡還要記得#include "instr/linked-x86_64.h",不然沒辦法使用ControlFlowInstruction。更多可以參考@dwk大大丟給我的例子,自然就可以懂了:

1
2
3
4
5
6
7
8
src/analysis/frametype.cpp:249:            
cfi->setLink(new NormalLink(newInstr, Link::SCOPE_INTERNAL_JUMP));
src/pass/usegstable.cpp:734:    
semantic2->setLink(new DistanceLink(block->getParent(), push1)); // instr!
src/pass/shadowstack.cpp:26:        
callSem->setLink(new NormalLink(allocateFunc, Link::SCOPE_EXTERNAL_JUMP));
src/pass/ldsorefs.cpp:59:                
v->setLink(new NormalLink(emptyTarget, Link::SCOPE_WITHIN_MODULE));

Reference

  1. Egalito connected papers
  2. Instruction-Level Instrumentation
  3. red zone