일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- tracerpid
- custom packer
- LLVM
- TLS
- tracing
- v8 tracing
- android inject
- apm
- so inject
- Android
- pinpoint
- v8 optimizing
- 안티디버깅
- Injection
- on stack replacement
- anti debugging
- linux debugging
- uftrace
- Obfuscator
- Linux packer
- LLVM Obfuscator
- on-stack replacement
- LLVM 난독화
- thread local storage
- 난독화
- initial-exec
- pthread
- OSR
- Linux custom packer
- linux thread
- Today
- Total
Why should I know this?
LLVM Obfuscator] Control Flow Graph Flattening 본문
본문 작성에 앞서 해당 난독화 기법의 모티브를 소개합니다.
engineering.linecorp.com/ko/blog/code-obfuscation-compiler-tool-ork-2/
Line에서 자체 개발 중인 난독화 기법 중에 Control Flow Graph Flattening 이라고 소개된 기법이 있습니다.
말하자면, 뭐 이것은 Line에서도 쓰는 기법이다! 라고 호가호위 해보려고 합니다.
난독화 기법을 조금이라도 더 쉽게 개발하는 방법은 다음과 같다.
- C로 짠 로직을 통해 IR을 뽑아내기
- IR을 기반으로 LLVM OPT 패스 만들기
먼저 순수 C로 예제코드를 만들고, C에서 목적한 난독화 기법을 구현해본다.
이번에 구현하고자 하는 난독화는 CALL 을 난독화 하는 것이며, C 로 구현된 코드는 아래와 같다.
<예제 코드 if-state 분기문>
#include <stdio.h>
int main()
{
char *test = "test";
int i = 0, j = 0;
if (i == 0)
j = 1;
if (i == 0)
printf("%s %d \n", test, j);
else if (i == 1)
printf("%s %d \n", test, j);
else if (i == 2)
printf("%s %d \n", test, j);
else if (i == 3)
printf("%s %d \n", test, j);
else if (i == 4)
printf("%s %d \n", test, j);
printf("%s %d \n", test, i);
return i;
}
위의 if-state 분기문을 switch 문으로 바꾸면 CFG-Flattening 완성입니다.
표본이 될 switch 문의 뼈대를 확보하기 위해서 마찬가지로 예제 코드를 만듭니다.
<예제 코드 switch 분기문>
int main()
{
int i = 0;
while(1) {
switch(i)
{
case 0:
break;
case 1:
break;
case 2:
break;
default:
break;
}
}
return 0;
}
<예제 코드 switch 분기문으로 확보한 IR>
; Function Attrs: noinline nounwind optnone uwtable
define dso_local i32 @main() #0 {
%1 = alloca i32, align 4
%2 = alloca i32, align 4
store i32 0, i32* %1, align 4
store i32 0, i32* %2, align 4
br label %3
3: ; preds = %0, %9
%4 = load i32, i32* %2, align 4
switch i32 %4, label %8 [
i32 0, label %5
i32 1, label %6
i32 2, label %7
]
5: ; preds = %3
br label %9
6: ; preds = %3
br label %9
7: ; preds = %3
br label %9
8: ; preds = %3
br label %9
9: ; preds = %8, %7, %6, %5
br label %3
}
위의 예제를 통해 switch-case 문의 IR 뼈대를 확보할 수 있다.
확인할 수 있는 사항은 다음과 같다.
1. switch-case 문은 label 로 구분되며
2. switch의 조건에 따라 case 별로 붙여진 label을 pair로 구성하며
3. switch 문의 outer에도 label이 붙으며 break는 outer label로 branch 하게끔 번역된다는 점.
기타 preds 같은게 붙는데 이게 매우 성가시다.
label이 붙은 구문은 IR상에서 눈으로는 확인할 수 없지만 서로 간에 연결이 되어 있다.
AST와 유사한 개념인 것 같으며, 이에 따라 Basic Block을 이동/생성/삽입할 경우,
LLVM opt path에서 검증하는 논리에 위배되지 않도록 조심해야 한다.
위의 사항을 유의하며 만든 CFG-Flattening 코드는 다음과 같다.
void Flatter::flatting(Function *Func)
{
BasicBlock* entry = &Func->getEntryBlock();
Instruction* term = entry->getTerminator();
if (nullptr == term || !isa<BranchInst>(term))
return;
SymbolTableList<Instruction> *entrylist = &entry->getInstList();
IntegerType *I32 = Type::getInt32Ty(Func->getContext());
BasicBlock *switchBB = BasicBlock::Create(Func->getContext(), "", Func);
switchBB->moveAfter(entry);
BasicBlock *zeroBB = BasicBlock::Create(Func->getContext(), "", Func);
zeroBB->moveAfter(switchBB);
SymbolTableList<Instruction> *zerolist = &zeroBB->getInstList();
zerolist->push_front(entrylist->remove(term));
BranchInst *br = cast<BranchInst>(term);
CmpInst* cond = cast<CmpInst>(br->getCondition());
zerolist->push_front(entrylist->remove(cond));
Instruction *Case = new AllocaInst(I32, 0, "CASE", &*entry->getFirstInsertionPt());
IRBuilder<> builder_sw(switchBB);
Value *load = builder_sw.CreateLoad(Case);
SwitchInst *swInst = SwitchInst::Create(load, nullptr, 0, switchBB);
for (Function::iterator BB = Func->begin(), E = Func->end(); BB != E; ++BB)
{
// add case
BasicBlock* bb = &*BB;
BasicBlock* end = &*E;
if (bb == switchBB) continue; // skip make case for itself
// if (bb == defaultBB) continue;
if (bb == entry) continue;
if (bb == end) continue;
int label = getLabel(bb);
ConstantInt *case_number = ConstantInt::get(I32, label);
swInst->addCase(case_number, bb);
// handle branch , do not need handle conditional branch
term = bb->getTerminator();
if (nullptr == term || !isa<BranchInst>(term) || cast<BranchInst>(term)->isConditional())
continue;
// handle br only
BasicBlock* el = term->getSuccessor(0);
label = getLabel(el);
case_number = ConstantInt::get(I32, label);
new StoreInst(case_number, Case, term);
BranchInst::Create(switchBB, term);
el->removePredecessor(term->getParent());
term->eraseFromParent();
}
IRBuilder<> builder_ent(entry);
auto val = ConstantInt::get(I32, getLabel(zeroBB));
Value *store = builder_ent.CreateStore(val, Case);
builder_ent.CreateBr(switchBB);
BasicBlock *whileBB = BasicBlock::Create(Func->getContext(), "", Func);
BranchInst::Create(switchBB, whileBB);
swInst->setDefaultDest(whileBB);
}
위의 LLVM opt path를 적용해보면 다음과 같은 비교 결과를 얻을 수 있다.
<if-state CFG>
<switch-case로 flattening된 CFG>
LLVM Call 난독화 기법에서 다뤘듯, 해당 코드는 github에 공개되어 있다.
- 끗 -
'Knowledge > Compiler' 카테고리의 다른 글
ART-Compiler] LoopOptimizing #1 (0) | 2022.12.05 |
---|---|
LLVM Obfuscator] Call (0) | 2021.02.13 |
LLVM - Why should we know this? (0) | 2020.07.19 |
GCC compile (0) | 2018.04.09 |
gcc 사용 중에 버그를 발견했을때 report 하는 방법. (0) | 2018.04.09 |