LLVM IR 深入研究分析

蚁景网安实验室發表於2024-08-20

前置知識

LLVM是C++編寫的構架編譯器的框架系統,可用於最佳化以任意程式語言編寫的程式。

LLVM IR可以理解為LLVM平臺的組合語言,所以官方也是以語言參考手冊(Language Reference Manual)的形式給出LLVM IR的文件說明。既然是組合語言,那麼就和傳統的CUP類似,有特定的彙編指令集。但是它又與傳統的特定平臺相關的指令集(x86,ARM,RISC-V等)不一樣,它定位為平臺無關的組合語言。也就是說,LLVM IR是一種相對於CUP指令集高階,但是又是一種低階的程式碼中間表示(比抽象語法樹等高階表示更加低階)。

LLVM IR即程式碼的中間表示,有三種形式:

  • .ll 格式:人類可以閱讀的文字(彙編碼) -->這個就是我們要學習的IR

  • .bc 格式:適合機器儲存的二進位制檔案

  • 記憶體表示

下面給出.ll格式和.bc格式生成及相互轉換的常用指令清單:

.c -> .ll:clang -emit-llvm -S a.c -o a.ll
.c -> .bc: clang -emit-llvm -c a.c -o a.bc
.ll -> .bc: llvm-as a.ll -o a.bc
.bc -> .ll: llvm-dis a.bc -o a.ll
.bc -> .s: llc a.bc -o a.s

那麼我們以一道CTF賽題來分析實驗,學習LLVM IR

實驗解析

題目附件直接給出了中間表示.II檔案

image.png

開啟檢視一下彙編碼,畢竟.II檔案是人類可以閱讀的文字,這邊筆者使用的是Sublime Text(使用VScode檢視即可)程式碼量不多,大概600行

image.png

題目初步分析

我們直接尋找一下main函式

image.png

image.png

我們可以看出題目經歷了兩次RC4,然後Base64,我們從上面可以看到密文,RC4_key,我們直接一把鎖,cyberchef啟動,會發現解不出來,那麼程式應該做了其他的操作,最樸素的,我們可以想到把RC4魔改了,base64魔改等等。

image.png

So!繼續學習研究ing

【----幫助網安學習,以下所有學習資料免費領!加vx:dctintin,備註 “部落格園” 獲取!】

 ① 網安學習成長路徑思維導圖
 ② 60+網安經典常用工具包
 ③ 100+SRC漏洞分析報告
 ④ 150+網安攻防實戰技術電子書
 ⑤ 最權威CISSP 認證考試指南+題庫
 ⑥ 超1800頁CTF實戰技巧手冊
 ⑦ 最新網安大廠面試題合集(含答案)
 ⑧ APP客戶端安全檢測指南(安卓+IOS)

.II詳細分析

所以本著學習的態度,我們這時候應該掏出LLVM Language Reference Manual(官方文件)來簡單瞭解學習一些常見指令、符號標識以及特性。這邊給出一些分析 .ll 中間檔案的演算法流程

@ - 全域性變數
% - 區域性變數
alloca - 在當前執行的函式的堆疊幀中分配記憶體,當該函式返回到其呼叫者時,將自動釋放記憶體
i32 - 32位4位元組的整數
align - 對齊
load - 讀出,store寫入
icmp - 兩個整數值比較,返回布林值
br - 選擇分支,根據條件來轉向label,不根據條件跳轉的話型別goto
label - 程式碼標籤
call - 呼叫函式
​

首先看到一些全域性變數,知道了RC4_key = llvmbitccipher = "TSzkWKgbMHszXaj@kLBmRrnTxsNtZsSOtZzqYikCw="

image.png

我們繼續分析,重點分析各個function

b64encode

b64encode 魔改

  1. 每三個字元,24位,切分成4斷,每段6位。

  2. 將6位對應的值 (value+ 59)&0xff 則是編碼後的值。

image.png

  %22 = getelementptr inbounds i8, i8* %19, i64 %21        // 取出當前處理字元
  %23 = load i8, i8* %22, align 1
  %24 = zext i8 %23 to i32                                 // 型別強制轉化
  %25 = ashr i32 %24, 2                                   // 算數右移兩位   input[i]>>2
  %26 = add nsw i32 %25, 59                                 //    input[i]+59
  %27 = trunc i32 %26 to i8                                //    強制轉化  相當於 &0xff
  %28 = load i8*, i8** %6, align 8
  %29 = load i32, i32* %9, align 4
  %30 = sext i32 %29 to i64
  %31 = getelementptr inbounds i8, i8* %28, i64 %30        // 儲存base64 編碼串
  store i8 %27, i8* %31, align 1
  %32 = load i8*, i8** %4, align 8
  %33 = load i32, i32* %7, align 4
  %34 = sext i32 %33 to i64
  %35 = getelementptr inbounds i8, i8* %32, i64 %34
  %36 = load i8, i8* %35, align 1
  %37 = zext i8 %36 to i32
  %38 = and i32 %37, 3                              // 獲取第一個字元 低兩位
  %39 = shl i32 %38, 4                                // 左移四位

RC4_init

RC4_init 正常,無魔改

image.png

image.png

define dso_local void @Rc4_Init(i8*, i32) #0 {                           //RC4_init function
  %3 = alloca i8*, align 8
  %4 = alloca i32, align 4
  %5 = alloca i32, align 4
  %6 = alloca i32, align 4
  store i8* %0, i8** %3, align 8
  store i32 %1, i32* %4, align 4                                         //初始化S,T盒
  call void @llvm.memset.p0i8.i64(i8* align 16 getelementptr inbounds ([256 x i8], [256 x i8]* @s, i64 0, i64 0), i8 0, i64 256, i1 false)
  call void @llvm.memset.p0i8.i64(i8* align 16 getelementptr inbounds ([256 x i8], [256 x i8]* @t, i64 0, i64 0), i8 0, i64 256, i1 false)
  store i32 0, i32* %5, align 4
  br label %7
​
7:                                                ; preds = %26, %2
  %8 = load i32, i32* %5, align 4
  %9 = icmp slt i32 %8, 256
  br i1 %9, label %10, label %29                          //如果 %9 為真(即 %8 小於 256),跳轉到標籤 %10;否則跳轉到標籤 %29,根據t打亂s盒
​
10:                                               ; preds = %7
  %11 = load i32, i32* %5, align 4
  %12 = trunc i32 %11 to i8
  %13 = load i32, i32* %5, align 4
  %14 = sext i32 %13 to i64
  %15 = getelementptr inbounds [256 x i8], [256 x i8]* @s, i64 0, i64 %14
  store i8 %12, i8* %15, align 1
  %16 = load i8*, i8** %3, align 8
  %17 = load i32, i32* %5, align 4
  %18 = load i32, i32* %4, align 4
  %19 = urem i32 %17, %18
  %20 = zext i32 %19 to i64
  %21 = getelementptr inbounds i8, i8* %16, i64 %20
  %22 = load i8, i8* %21, align 1
  %23 = load i32, i32* %5, align 4
  %24 = sext i32 %23 to i64
  %25 = getelementptr inbounds [256 x i8], [256 x i8]* @t, i64 0, i64 %24
  store i8 %22, i8* %25, align 1
  br label %26
​
26:                                               ; preds = %10
  %27 = load i32, i32* %5, align 4
  %28 = add nsw i32 %27, 1
  store i32 %28, i32* %5, align 4
  br label %7
​
29:                                               ; preds = %7
  store i32 0, i32* %6, align 4
  store i32 0, i32* %5, align 4
  br label %30
​
30:                                               ; preds = %54, %29
  %31 = load i32, i32* %5, align 4
  %32 = icmp slt i32 %31, 256
  br i1 %32, label %33, label %57
​
33:                                               ; preds = %30
  %34 = load i32, i32* %6, align 4
  %35 = load i32, i32* %5, align 4
  %36 = sext i32 %35 to i64
  %37 = getelementptr inbounds [256 x i8], [256 x i8]* @s, i64 0, i64 %36
  %38 = load i8, i8* %37, align 1
  %39 = zext i8 %38 to i32
  %40 = add nsw i32 %34, %39
  %41 = load i32, i32* %5, align 4
  %42 = sext i32 %41 to i64
  %43 = getelementptr inbounds [256 x i8], [256 x i8]* @t, i64 0, i64 %42
  %44 = load i8, i8* %43, align 1
  %45 = zext i8 %44 to i32
  %46 = add nsw i32 %40, %45
  %47 = srem i32 %46, 256
  store i32 %47, i32* %6, align 4
  %48 = load i32, i32* %5, align 4
  %49 = sext i32 %48 to i64
  %50 = getelementptr inbounds [256 x i8], [256 x i8]* @s, i64 0, i64 %49
  %51 = load i32, i32* %6, align 4
  %52 = sext i32 %51 to i64
  %53 = getelementptr inbounds [256 x i8], [256 x i8]* @s, i64 0, i64 %52
  call void @swap(i8* %50, i8* %53)                                                //call swap function
  br label %54

RC4_enc

RC4_enc 魔改 多了一層xor 89

image.png

image.png

define dso_local void @Rc4_Encrypt(i8*, i32) #0 {                                //RC4_enc function
  %3 = alloca i8*, align 8
  %4 = alloca i32, align 4
  %5 = alloca i8, align 1
  %6 = alloca i8, align 1
  %7 = alloca i8, align 1
  %8 = alloca i8, align 1
  store i8* %0, i8** %3, align 8
  store i32 %1, i32* %4, align 4
  store i8 0, i8* %6, align 1
  store i8 0, i8* %7, align 1
  store i8 0, i8* %8, align 1
  br label %9
​
9:                                                ; preds = %14, %2
  %10 = load i8, i8* %8, align 1
  %11 = zext i8 %10 to i32
  %12 = load i32, i32* %4, align 4
  %13 = icmp ult i32 %11, %12
  br i1 %13, label %14, label %64
​
14:                                               ; preds = %9
  %15 = load i8, i8* %6, align 1
  %16 = zext i8 %15 to i32
  %17 = add nsw i32 %16, 1
  %18 = srem i32 %17, 256
  %19 = trunc i32 %18 to i8
  store i8 %19, i8* %6, align 1
  %20 = load i8, i8* %7, align 1
  %21 = zext i8 %20 to i32
  %22 = load i8, i8* %6, align 1
  %23 = zext i8 %22 to i64
  %24 = getelementptr inbounds [256 x i8], [256 x i8]* @s, i64 0, i64 %23               //生成金鑰流
  %25 = load i8, i8* %24, align 1
  %26 = zext i8 %25 to i32
  %27 = add nsw i32 %21, %26
  %28 = srem i32 %27, 256
  %29 = trunc i32 %28 to i8
  store i8 %29, i8* %7, align 1
  %30 = load i8, i8* %6, align 1
  %31 = zext i8 %30 to i64
  %32 = getelementptr inbounds [256 x i8], [256 x i8]* @s, i64 0, i64 %31
  %33 = load i8, i8* %7, align 1
  %34 = zext i8 %33 to i64
  %35 = getelementptr inbounds [256 x i8], [256 x i8]* @s, i64 0, i64 %34              //經典Swap了再加
  call void @swap(i8* %32, i8* %35)
  %36 = load i8, i8* %6, align 1
  %37 = zext i8 %36 to i64
  %38 = getelementptr inbounds [256 x i8], [256 x i8]* @s, i64 0, i64 %37
  %39 = load i8, i8* %38, align 1
  %40 = zext i8 %39 to i32
  %41 = load i8, i8* %7, align 1
  %42 = zext i8 %41 to i64
  %43 = getelementptr inbounds [256 x i8], [256 x i8]* @s, i64 0, i64 %42
  %44 = load i8, i8* %43, align 1
  %45 = zext i8 %44 to i32
  %46 = add nsw i32 %40, %45
  %47 = srem i32 %46, 256
  %48 = sext i32 %47 to i64
  %49 = getelementptr inbounds [256 x i8], [256 x i8]* @s, i64 0, i64 %48
  %50 = load i8, i8* %49, align 1
  store i8 %50, i8* %5, align 1
  %51 = load i8, i8* %5, align 1
  %52 = zext i8 %51 to i32
  %53 = xor i32 %52, 89                                                         //xor 89
  %54 = load i8*, i8** %3, align 8
  %55 = load i8, i8* %8, align 1
  %56 = zext i8 %55 to i64
  %57 = getelementptr inbounds i8, i8* %54, i64 %56
  %58 = load i8, i8* %57, align 1
  %59 = zext i8 %58 to i32 
  %60 = xor i32 %59, %53                                                        //xor k
  %61 = trunc i32 %60 to i8
  store i8 %61, i8* %57, align 1
  %62 = load i8, i8* %8, align 1
  %63 = add i8 %62, 1
  store i8 %63, i8* %8, align 1
  br label %9
​
64:                                               ; preds = %9
  ret void
}

main

main函式邏輯cipher -->RC4_init-->RC4_enc-->RC4_enc-->b64encode需要注意一下在RC4_enc的引數中,傳入的資料塊長度是固定的16,所以說程式進行兩次RC4_enc的原因也就確定了,是為了分兩次對程式進行加密,也算是一點點小手段,總之,即使讓你好好分析.II程式碼,考察對軟體分析的細節,耐心,嘻嘻。

image.png

image.png

OK,理清楚邏輯,就可以試著敲程式碼解密啦。

解密

逆向分析過程明瞭之後,那麼寫程式碼就簡單多了

#include<stdio.h>
unsigned char s[300],t[300];
void b64decode(unsigned char * enc,unsigned char* dec);
void Rc4_dec1(int len, unsigned char *enc);
void Rc4_Init(char *key,int len);
void Rc4_dec2(int len, unsigned char *enc);
int main() {
    unsigned char enc[50]="TSz`kWKgbMHszXaj`@kLBmRrnTxsNtZsSOtZzqYikCw=";
    unsigned char dec1[50]={0x00};
    char key[10] ="llvmbitc";
    unsigned char a[50];
    int i=0;      
 
    b64decode(enc,dec1);
    Rc4_Init(key,8);
    Rc4_dec1(16,&dec1[16]);
    for(i=0;i<16;i++) {
        dec1[i+16]^=dec1[i];
    }
    Rc4_Init(key,8);
    Rc4_dec2(16,dec1);
    printf("%s",dec1);
 
    return 0;
}
void b64decode(unsigned char * enc,unsigned char* dec) {
    int i=0,j=0;
    for(i=0;i<40;i+=4) {
        dec[j] = ((enc[i]-59)<<2)&0xfc | (((enc[i+2]-59)>>4))&3;
        dec[j+1] = (((enc[i+2]-59)&0xf)<<4) | (((enc[i+1]-59)>>2)&0xf);
        dec[j+2] = (((enc[i+1]-59)&3)<<6) | ((enc[i+3]-59)&0x3f);
        j+=3;
    }
    dec[j] = ((enc[i]-59)<<2)&0xfc | (((enc[i+1]-59)>>4))&3;
    dec[j+1] = (((enc[i+2]-59)>>2)&0xf) | (((enc[i+1]-59)<<4)&0xf0);
    dec[j+2]=0;
}
 
void Rc4_Init(char *key,int len) {
    int i=0,v5=0;
    unsigned char temp;
    for(i=0;i<256;i++) {
        s[i] =i;
        t[i] = key[i%len];
    }
    for(i=0;i<256;i++) {
        v5=(s[i]+t[i]+v5)%256;
        temp = s[i];
        s[i]= s[v5];
        s[v5]=temp;
 
    }
}
void Rc4_dec1(int len, unsigned char *enc) {
    int v3=0,v5=0,i,j;
    unsigned char temp;
    for(i=0;i<len;i++) {
        v3=(v3+1)%256;
        v5=(s[v3]+v5)%256;
        temp=s[v3];
        s[v3]=s[v5];
        s[v5]=temp;
    }
    v5=v3=0;
    for(i=0;i<len;i++) {
        v3=(v3+1)%256;
        v5 = (s[v3]+v5)%256;
        temp = s[v3];
        s[v3]=s[v5];
        s[v5]=temp;
        enc[i]^=s[(s[v5]+s[v3])%256]^0x59;
    }
}
void Rc4_dec2(int len, unsigned char *enc) {
    int v3=0,v5=0,i,j;
    unsigned char temp;
    v5=v3=0;
    for(i=0;i<len;i++) {
        v3=(v3+1)%256;
        v5 = (s[v3]+v5)%256;
        temp = s[v3];
        s[v3]=s[v5];
        s[v5]=temp;
        
        enc[i]^=s[(s[v5]+s[v3])%256]^0x59;
    }
}

image.png

flag{Hacking_for_fun@reverser$!}

總結

透過這麼一道CTF題目,深入學習LLVM IR的冰山一角,認真實驗,細細分析,相信會對你有極大幫助。當然,如果單從解題來說,對於解決這道題有很多的辦法,比如說將.II轉化為可執行檔案,然後IDA分析,但我們旨在學習LLVM IR,這裡不再過多贅述。

更多網安技能的線上實操練習,請點選這裡>>

相關文章