일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 | 31 |
- TLS
- Linux custom packer
- LLVM Obfuscator
- Linux packer
- anti debugging
- LLVM 난독화
- uftrace
- initial-exec
- apm
- 안티디버깅
- on-stack replacement
- on stack replacement
- Android
- Obfuscator
- Injection
- pthread
- linux debugging
- linux thread
- android inject
- tracerpid
- pinpoint
- OSR
- tracing
- v8 optimizing
- so inject
- v8 tracing
- LLVM
- thread local storage
- 난독화
- custom packer
- Today
- Total
Why should I know this?
LLVM Obfuscator] Call 본문
난독화 기법을 조금이라도 더 쉽게 개발하는 방법은 다음과 같다.
- C로 짠 로직을 통해 IR을 뽑아내기
- IR을 기반으로 LLVM OPT 패스 만들기
먼저 순수 C로 예제코드를 만들고, C에서 목적한 난독화 기법을 구현해본다.
이번에 구현하고자 하는 난독화는 CALL 을 난독화 하는 것이며, C 로 구현된 코드는 아래와 같다.
#include <stdio.h>
void foo();
void (*t)() = foo;
void (*tt)() = (void (*)())((long)&foo + 1);
long test = ((long)&foo) + 65536;
long test2 = (long)&foo + 1;
void foo()
{
puts("TEST\n");
}
int main()
{
((int (*)()) (test - 65536))();
((int (*)()) (test2-1))();
foo();
return 0;
}
위의 코드에 대한 설명은 아래와 같다.
1. 선언된 함수 foo 의 주소를 정적 포인터 함수에 저장한다.
2. 1의 과정에서 임의의 값을 더해 난독화(?) 를 수행한다.
3. main() 함수 안에서 호출하는 모든 foo 함수를 정적 포인터 함수 호출로 바꾼다.
위의 코드에서 foo() 를 난독화 한 값을 보관하는 정적 포인터 함수 test, test2 를 clang으로 컴파일 해 IR로 확인해보면 다음과 같은 내용을 확인할 수 있다.
@t = dso_local global void (...)* bitcast (void ()* @foo to void (...)*), align 8
@tt = dso_local global void (...)* bitcast (i8* getelementptr (i8, i8* bitcast (void ()* @foo to i8*), i64 1) to void (...)*), align 8
@test = dso_local global i64 ptrtoint (i8* getelementptr (i8, i8* bitcast (void ()* @foo to i8*), i64 65536) to i64), align 8
@test2 = dso_local global i64 ptrtoint (i8* getelementptr (i8, i8* bitcast (void ()* @foo to i8*), i64 1) to i64), align 8
@.str = private unnamed_addr constant [6 x i8] c"TEST\0A\00", align 1
위의 IR에서 주목할 것은 foo()의 주소를 보관한 정적 함수 포인터 변수인 test, test2 의 형태이다. 이들은 i8* 의 형태로 선언되며, 이는 함수포인터의 기본 타입이니 유의해야 한다. 또한 정적 함수 포인터의 경우 보관하는 값이 상수 값이 아니기 때문에 +65536, +1 의 연산이 일반 연산이 아닌 주소 연산으로 들어가게 된다. 이 점 또한 유념해야 한다.
별 의미 없을 것 같지만 main() 내용도 확인하면 다음과 같다.
; Function Attrs: noinline nounwind optnone uwtable
define dso_local i32 @main() #0 {
%1 = alloca i32, align 4
store i32 0, i32* %1, align 4
%2 = load i64, i64* @test, align 8
%3 = sub nsw i64 %2, 65536
%4 = inttoptr i64 %3 to i32 (...)*
%5 = call i32 (...) %4()
%6 = load i64, i64* @test2, align 8
%7 = sub nsw i64 %6, 1
%8 = inttoptr i64 %7 to i32 (...)*
%9 = call i32 (...) %8()
call void @foo()
ret i32 0
}
약간 복잡해 보일 수 있으니 다음처럼 분리해서 보자.
((int (*)()) (test - 65536))();
%2 = load i64, i64* @test, align 8
%3 = sub nsw i64 %2, 65536
%4 = inttoptr i64 %3 to i32 (...)*
%5 = call i32 (...) %4()
((int (*)()) (test2-1))();
%6 = load i64, i64* @test2, align 8
%7 = sub nsw i64 %6, 1
%8 = inttoptr i64 %7 to i32 (...)*
%9 = call i32 (...) %8()
위처럼, call void @foo() 라는 한 줄의 코드가
A. 미리 연산된 주소 값을 로드하고
B. 주소 값에 더해줬던 값을 빼고난 뒤
C. 호출
하는 형식으로 다소 복잡하게 바뀐 것을 확인할 수 있을 것이다.
이를 디스어셈블 된 코드로 확인해보면 아래와 같다.
(gdb) disas main
Dump of assembler code for function main:
0x0000000000400520 <+0>: push %rbp
0x0000000000400521 <+1>: mov %rsp,%rbp
0x0000000000400524 <+4>: sub $0x10,%rsp
0x0000000000400528 <+8>: mov 0x601040,%rax
0x0000000000400530 <+16>: mov %rax,-0x8(%rbp)
0x0000000000400534 <+20>: mov $0x0,%al
0x0000000000400536 <+22>: mov -0x8(%rbp),%rcx
0x000000000040053a <+26>: callq *(%rcx)
0x000000000040053c <+28>: mov 0x601048,%rcx
0x0000000000400544 <+36>: sub $0x1,%rcx
0x000000000040054b <+43>: mov %eax,-0xc(%rbp)
0x000000000040054e <+46>: mov $0x0,%al
0x0000000000400550 <+48>: callq *%rcx
0x0000000000400552 <+50>: xor %edx,%edx
0x0000000000400554 <+52>: mov %eax,-0x10(%rbp)
0x0000000000400557 <+55>: mov %edx,%eax
0x0000000000400559 <+57>: add $0x10,%rsp
0x000000000040055d <+61>: pop %rbp
0x000000000040055e <+62>: retq
여기까지 내용을 정리하자면 함수 호출을 난독화 하기 위해서는,
1. 일반적인 함수 호출 구문 foo() 를 함수 주소를 보관한 함수 포인터에 난독화 용 값을 더해 보관하고
2. foo()를 호출하는 구문을 해당 함수 포인터에 더해줬던 값을 빼서 호출한다.
두 단계가 필요하다.
이를 LLVM 의 optmize path 를 통해 구현한 내용을 소개할 것이며, 소스코드는 다음 링크를 통해 확인할 수 있다.
https://github.com/ParkHanbum/mystudy.git
여기서는 주요 로직만 소개하도록 하겠다.
1. 일반적인 함수 호출 구문 foo() 를 함수 주소를 보관한 함수 포인터에 난독화 용 값을 더해 보관하고
for (auto &Func : M)
{
if (Func.isDeclaration())
continue;
Constant *var = CreateGlobalFunctionPtr(M, Func);
}
위의 코드는 module 의 모든 함수를 함수포인터로 보관하는 구문이다.
구현은 다음처럼 되어 있다.
Constant *CreateGlobalFunctionPtr(Module &M, Function &func)
{
int offset = getRandomV();
std::string name;
name.append("GlobalFunctionPtr_");
name.append(func.getName());
GlobalVariable *fnPtr = new GlobalVariable(/*Module=*/M,
/*Type=*/Type::getInt64Ty(func.getContext()),
/*isConstant=*/true,
/*Linkage=*/GlobalValue::ExternalLinkage,
/*Initializer=*/nullptr, // has initializer, specified below
/*Name=*/StringRef(name));
IntegerType *I64 = Type::getInt64Ty(func.getContext());
// i8* bitcast (void ()* @fez to i8*)
Constant *const_ptr_5 = ConstantExpr::getBitCast(&func, Type::getInt8PtrTy(func.getContext()));
// (i8* getelementptr (i8, i8* bitcast (i32 ()* @foo to i8*), i64 1)
Constant *One = ConstantInt::get(I64, offset);
Constant *const_ptr_6 = ConstantExpr::getGetElementPtr(Type::getInt8Ty(func.getContext()), const_ptr_5, One);
// constant i64 ptrtoint (void ()* @fez to i64)
Constant *const_ptr_7 = ConstantExpr::getPtrToInt(const_ptr_6, I64);
fnPtr->setInitializer(const_ptr_7);
FuncNameMap[func.getName()] = fnPtr;
FuncOffsetMap[func.getName()] = offset;
return fnPtr;
}
위에서 C로 작성했던 코드로 생성됐던 IR을 역으로 만드는 구문을 작성했다.
추가로 함수의 이름을 key로 함수포인터의 값과 +한 offset 값을 보관하는 map을 전역으로 선언해 보관한다.
2. foo()를 호출하는 구문을 해당 함수 포인터에 더해줬던 값을 빼서 호출한다.
for (auto &Func : M)
{
for (auto &BB : Func)
{
for (auto &Ins : BB)
{
if (isa<CallInst>(Ins) || isa<InvokeInst>(Ins))
{
IRBuilder<> builder(&Ins);
debugInst(&Ins);
CallInst *cinst = dyn_cast<CallInst>(&Ins);
Function *fn = cinst->getCalledFunction();
if (fn)
{
Constant *gFnAddr = FuncNameMap[fn->getName()];
LoadInst *load = builder.CreateLoad(gFnAddr);
int offset = FuncOffsetMap[fn->getName()];
Value* orig = builder.CreateSub(load, ConstantInt::get(I64, offset));
Value *tofn = new IntToPtrInst(orig, fn->getType(), "tofncast", &Ins);
builder.CreateCall(tofn);
}
}
}
}
}
Instruction이 Call 혹은 Invoke일 경우
Call instruction => Call 되는 함수 이름으로 저장된 fnPtr 값을 로드하여 Call 되는 함수 이름으로 저장된 offset 값을 뺀 뒤 호출하는 IR을 생성한다.
위의 과정으로 생성되는 코드는 다음과 같다.
대상 예제 코드)
#include <stdio.h>
int a() {
return 1+1;
}
int fez() {
return a()+1;
}
int main() {
fez();
return 0;
}
~
Call 난독화 IR 코드)
@GlobalFunctionPtr_a = constant i64 ptrtoint (i8* getelementptr (i8, i8* bitcast (i32 ()* @a to i8*), i64 1655182250) to i64)
@GlobalFunctionPtr_fez = constant i64 ptrtoint (i8* getelementptr (i8, i8* bitcast (i32 ()* @fez to i8*), i64 1462869916) to i64)
@GlobalFunctionPtr_main = constant i64 ptrtoint (i8* getelementptr (i8, i8* bitcast (i32 ()* @main to i8*), i64 1605830790) to i64)
; Function Attrs: noinline nounwind optnone uwtable
define dso_local i32 @a() #0 {
ret i32 2
}
; Function Attrs: noinline nounwind optnone uwtable
define dso_local i32 @fez() #0 {
%1 = load i64, i64* @GlobalFunctionPtr_a
%2 = sub i64 %1, 1655182250
%tofncast = inttoptr i64 %2 to i32 ()*
%3 = call i32 %tofncast()
%4 = call i32 @a()
%5 = add nsw i32 %4, 1
ret i32 %5
}
; Function Attrs: noinline nounwind optnone uwtable
define dso_local i32 @main() #0 {
%1 = alloca i32, align 4
store i32 0, i32* %1, align 4
%2 = load i64, i64* @GlobalFunctionPtr_fez
%3 = sub i64 %2, 1462869916
%tofncast = inttoptr i64 %3 to i32 ()*
%4 = call i32 %tofncast()
%5 = call i32 @fez()
ret i32 0
}
- 끗 -
'Knowledge > Compiler' 카테고리의 다른 글
ART-Compiler] LoopOptimizing #1 (0) | 2022.12.05 |
---|---|
LLVM Obfuscator] Control Flow Graph Flattening (0) | 2021.02.15 |
LLVM - Why should we know this? (0) | 2020.07.19 |
GCC compile (0) | 2018.04.09 |
gcc 사용 중에 버그를 발견했을때 report 하는 방법. (0) | 2018.04.09 |