小弟最近整理之前的資料,偶然發現半年前的混淆對抗研究以及一道CTF練習題目,故分享以作記錄。限于水平,難免會有疏漏或者錯誤之處,望各位讀者批評指正。
jeb打開文件,找到方法校驗方法。邏輯很簡單,校驗函數既是Native函數check.
#!vb
public native boolean check(String arg1) {
}
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setContentView(2130903040);
this.inputCode = this.findViewById(2131099648);
this.btn_submit = this.findViewById(2131099649);
this.btn_submit.setOnClickListener(new View$OnClickListener() {
public void onClick(View v) {
if(MainActivity.this.check(MainActivity.this.inputCode.getText().toString())) {
MainActivity.this.startActivity(new Intent(MainActivity.this, ResultActivity.class));
}
else {
Toast.makeText(MainActivity.this.getApplicationContext(), "Incorrect Password!",
0).show();
}
}
});
}
直接使用IDA默認Loader打開直接崩潰,存在畸形ELF文件對抗,使用自定義LOADER加載,也是然并卵的節奏。
使用Tracer動態打印check函數地址,掛起進程,dump出對應的代碼段加載到IDA,找到check函數。
#!bash
seg000:4561E4E8 check
seg000:4561E4E8 LDR PC, =sub_4561E4EC
seg000:4561E4E8 ; End of function check
seg000:4561E4E8
seg000:4561E4EC ; =============== S U B R O U T I N E seg000:4561E4EC sub_4561E4EC ; CODE XREF: checkj
seg000:4561E4EC ; DATA XREF: seg000:4561E4EC STMFD SP!, {R0-R12,LR}
seg000:4561E4F0 LDR R0, =6
seg000:4561E4F4 B loc_4561E444
通過分析發現,其實為一個匯編stub,通過此stub跳到真正的check函數。
#!bash
seg013:80A0135C sub_80A0135C ; DATA XREF: seg013:80A13F98o
seg013:80A0135C B sub_80A065B8
seg013:80A0135C ; End of function sub_80A0135C
seg013:80A01360
seg013:80A01360 ; =============== S U B R O U T I N E seg013:80A01360 ; Attributes: thunk
seg013:80A01360
seg013:80A01360 sub_80A01360 ; DATA XREF: sub_80A065C4+Co
seg013:80A01360 B sub_80A065F8
seg013:80A01360 ; End of function sub_80A01360
seg013:80A01364 ; =============== S U B R O U T I N E seg013:80A01364 ; Attributes: thunk
seg013:80A01364
seg013:80A01364 sub_80A01364 ; CODE XREF: sub_80A06620j
seg013:80A01364 B sub_80A0663C
seg013:80A01364 ; End of function sub_80A01364
以80A0135C(B sub_80A065B8)為例子,跟進sub_80A065B8,可以看到如下指令:
#!bash
// 0x80A0135C
seg013:80A065B8 BEQ loc_80A0658C
seg013:80A065BC BNE loc_80A0658C
seg013:80A0658C STMFD SP!, {R3-R8,R10,LR} 真實指令
seg013:80A06590 STMFD SP!, {R8,LR}
seg013:80A06594 LDR R8, loc_80A065A4
seg013:80A06598 LDR R8, loc_80A065A8
seg013:80A0659C LDR R8, loc_80A065AC
seg013:80A065A0 LDR R8, locret_80A065B0
seg013:80A065A4 LDR R8, =(sub_80A065C4 - 0x80A065B0)
seg013:80A065A8 ADD R8, PC, R8 ; sub_80A065C4
seg013:80A065AC STR R8, [SP,#4]
seg013:80A065B0 LDMFD SP!, {R8,PC}
seg013:80A065C4 STMFD SP!, {R8,LR}
seg013:80A065C8 LDR R8, =0xFFFFAD25
seg013:80A065CC EOR R8, R8, #0xAD
seg013:80A065D0 ADD R8, PC, R8 ; loc_80A01360 //返回到80A01360
seg013:80A065D4 STR R8, [SP,#8+var_4]
seg013:80A065D8 LDMFD SP!, {R8,PC}
通過分析可以得到真實指令(STMFD SP!, {R3-R8,R10,LR}),其余指令為混淆指令,最終返回到下一條B即80A01360(B sub_80A065F8)指令。通過分析其他B指令,可以得到類似的混淆指令中夾在一條真實指令,只是存在多種混淆的方式。 至此,我們可以得到此混淆的思路:執行"一個B指令"即一條真實的指令,混淆抽象為:
不難發現,如果僅僅靠一條一條的尋找真實指令,是非常費時費力的。由于執行前后都存在多種模式的混淆,但總的模式是有限的,那么通過提取指令特征匹配即可以自動化實現去混淆,找出真實指令。
通過分析找到所有的混淆模式,最后大概幾種。限于篇幅,列舉一些做說明
#!bash
// 0x80A0135C
seg013:80A065B8 BEQ loc_80A0658C
seg013:80A065BC BNE loc_80A0658C
seg013:80A0658C STMFD SP!, {R3-R8,R10,LR} 真實指令
seg013:80A06590 STMFD SP!, {R8,LR}
seg013:80A06594 LDR R8, loc_80A065A4
seg013:80A06598 LDR R8, loc_80A065A8
seg013:80A0659C LDR R8, loc_80A065AC
seg013:80A065A0 LDR R8, locret_80A065B0
seg013:80A065A4 LDR R8, =(sub_80A065C4 - 0x80A065B0)
seg013:80A065A8 ADD R8, PC, R8 ; sub_80A065C4
seg013:80A065AC STR R8, [SP,#4]
seg013:80A065B0 LDMFD SP!, {R8,PC}
seg013:80A065C4 STMFD SP!, {R8,LR}
seg013:80A065C8 LDR R8, =0xFFFFAD25
seg013:80A065CC EOR R8, R8, #0xAD
seg013:80A065D0 ADD R8, PC, R8 ; loc_80A01360
seg013:80A065D4 STR R8, [SP,#8+var_4]
seg013:80A065D8 LDMFD SP!, {R8,PC}
執行前混淆:B(連續兩條條件完全相反的指令) next_jmp
執行后混淆:這里有兩組STMFD--LDMFD構成的跳轉stub,但其是為一種模式。那如何計算next_jmp
呢?這里我采用取巧的方式,通過從LDMFD所在地址反向找到ADD指令,得到";loc_80a01360"
,再解析出地址80a01360。當然,存在多種prefix,需要作簡單處理獲取地址。
#!python
def prefix_match(str):
pattern = ['sub_', 'loc_', 'unk_', 'locret_']
for prefix in pattern:
if str.find(prefix) > -1:
substr = str[str.find(prefix) + len(prefix):]
return string.atoi(substr, 16)
return 0xffffffff;
真實指令:通過解析跳轉遍歷完整個混淆后,通過堆棧平衡原理,提取出真實指令。以上述分析為例,遍歷回到下一條指令80a01360后,對指令進行分組即(B)(STMFD SP!, {R3-R8,R10,LR})(STMFD-LDMFD)(STMFD-LDMFD),非常容易獲取真實指令。實現時,可將分組過程可融入到指令的遍歷即可。
再接著看另一組混淆,以80A01360(B sub_80A065F8)為例。
#!bash
// 0x80A01360
seg013:80A065F8 STMFD SP!, {R0,LR}
seg013:80A065FC LDR R0, loc_80A0660C
seg013:80A06600 LDR R0, loc_80A06610
seg013:80A06604 LDR R0, loc_80A06614
seg013:80A06608 LDR R0, locret_80A06618
seg013:80A0660C LDR R0, =(loc_80A065E0 - 0x80A06618)
seg013:80A06610 ADD R0, PC, R0 ; loc_80A065E0
seg013:80A06614 STR R0, [SP,#4]
seg013:80A06618
seg013:80A06618 LDMFD SP!, {R0,PC}
seg013:80A065E0 LDR R3, [R0] //真實指令
seg013:80A065E4 STMFD SP!, {R0,LR}
seg013:80A065E8 MOV LR, PC
seg013:80A065EC BL loc_80A065F0
seg013:80A065F0
seg013:80A065F0 loc_80A065F0 ; CODE XREF: seg013:80A065ECj
seg013:80A065F0 LDMFD SP!, {R0,LR}
seg013:80A065F4 B sub_80A06620
seg013:80A06620 B sub_80A01364
執行前混淆:STMFD-LDMFD跳轉到loc_80A065E0
。獲取next_jmp
和上述一致。
執行后混淆:通過STMFD-LDMFD和兩次B直接跳轉返回到下一條B指令地址sub_80A01364。
真實指令:和上述類似,遍歷混淆指令時,對指令進行分組(STMFD-LDMFD)、(LDR R3, [R0])、(B)、(B)。易獲取真實指令(LDR R3, [R0])。
通過上述方法,大概分析20個多有的B指令即可找到所有的混淆模式,總的來說混淆的模式是有限的。
通過編寫IDAPython腳本,即可實現自動打印真實指令。
#!bash
0x80a0135c PUSH {r3, r4, r5, r6, r7, r8, sl, lr}
0x80a01360 LDR r3, [r0]
0x80a01364 MOV r1, r2
0x80a01368 MOV r6, r2
0x80a0136c LDR r3, [r3, #0x2a4]
0x80a01370 MOV r2, #0
0x80a01374 MOV r4, r0
0x80a01378 BLX r3
0x80a0137c MOV r7, r0
但存在問題,當IDA并沒有識別出指令時,無法通過GetMnem等API獲取信息。
由于混淆對IDA指令識別的影響,致使IDA無法自動將指令反匯編出來。可能已經有讀者意識到,遇到這種情況直接調用MakeCode將數據轉化為指令即可。然而,實際使用MakeCode自動處理時,并不能完成手動按'C'識別指令的功能。那么,是否遇到這種情況后,就手動去完成指令反匯編呢?答案是否定的。由于存在很多這種情況,手動轉化也很費時(測試環境IDA6.8)。
到這里可以看到,單純依靠簡單的字符串匹配比較的方法,并不能完全滿足自動化提取指令對抗混淆的需求。
通過上述分析,由于IDA存在無法自動反匯編一部分opcode數據,故單純依靠IDAPython是無法滿足指令解析指令的需求的。為了實現對指令的解析,可采用兩種途徑:
Capstone是一款支持多種架構的反匯編引擎,支持對匯編指令粗略和詳細的分析,支持多種語言。當然,Capstone還有很多其他優點,這里就不贅述了。
3.1 ARM處理器模擬
可能有讀者馬上會問,模擬arm處理器執行不又是一大工程呢。的確,完全模擬確實包含許多工作量。但結合此混淆的一些特性,整個模擬執行可簡化許多。
首先,此混淆并不存在流程分支扁平化(與OLLVM相對比)。結合上述分析也可以看到,所有的混淆執行并不會影響條件標志即CPSR寄存器。
再者,結合堆棧平衡原理,SP寄存器僅僅只需要保存堆棧的變化,比如stmfd僅僅對SP寄存器進行減法操作。
最后,根據上述找到的混淆模式,可以發現使用的指令其實很少,實際編寫模擬函數工作量也比較小。
#!python
def do_emulate(code, base, Rx):
ret_addr = 0xffffffff
emu = ARM_emu()
md = Cs(CS_ARCH_ARM, CS_MODE_ARM)
md.detail = True
for i in md.disasm(code, base):
emu.regs[PC] = i.address + 2 * inst_size
dst = i.operands[0]
src = i.operands[1]
if (i.mnemonic).upper() == 'LDR':
if dst.type == ARM_OP_REG and src.type == ARM_OP_MEM:
Rd = conv_reg(dst.value.reg)
Rs = conv_reg(src.value.mem.base)
addr = emu.regs[Rs] + src.value.mem.disp
emu.regs[Rd] = Dword(addr & 0xffffffff)
if Debug:
print ('\t LDR %s :\t0x%x' %(i.op_str, emu.regs[Rd]))
elif (i.mnemonic).upper() == 'ADD':
if i.operands[0].type == ARM_OP_REG and i.operands[1].type == ARM_OP_REG and i.operands[2].type == ARM_OP_REG:
Rd = conv_reg(i.operands[0].value.reg)
R1 = conv_reg(i.operands[1].value.reg)
R2 = conv_reg(i.operands[2].value.reg)
emu.regs[Rd] = (emu.regs[R1] + emu.regs[R2]) & 0xffffffff
if Debug:
print ('\t ADD %s :\t0x%x' %(i.op_str, emu.regs[Rd]))
...
在模擬執行一條真實指令時,首先將所有寄存器的初始值設置為0,通過主流程中的B指令進入到混淆指令。
3.2 真實指令提取
模擬執行時,將混淆中的每條指令都存儲到一個指令堆棧中。結合之前直接字符串模式的思路,來實現對真實指令的提取。
以80A01364為例子來說明真實指令的提取方法。
#!bash
seg013:80A01364 sub_80A01364 ; CODE XREF: sub_80A06620j
seg013:80A01364 B sub_80A0663C
seg013:80A0663C BMI loc_80A06648
seg013:80A06640 BPL loc_80A06644
seg013:80A06644
seg013:80A06644 loc_80A06644 ; CODE XREF: sub_80A0663C+4j
seg013:80A06644 ; sub_80A0663C:loc_80A06648j
seg013:80A06644 B loc_80A06624
seg013:80A06648 ;
seg013:80A06648
seg013:80A06648 loc_80A06648 ; CODE XREF: sub_80A0663Cj
seg013:80A06648 B loc_80A06644
seg013:80A06624 loc_80A06624 ; CODE XREF:
seg013:80A06624 MOV R1, R2
seg013:80A06628 STMFD SP!, {R0,LR}
seg013:80A0662C MOV LR, PC
seg013:80A06630 BL loc_80A06634
seg013:80A06634 ;
seg013:80A06634
seg013:80A06634 loc_80A06634 ; CODE XREF: sub_80A0663C-Cj
seg013:80A06634 LDMFD SP!, {R0,LR}
seg013:80A06638 B sub_80A0664C
seg013:80A0664C sub_80A0664C ; CODE XREF: sub_80A0663C-4p
seg013:80A0664C B sub_80A01368
若不在模擬執行中對指令堆棧修正,那么執行完后存儲指令如下所示:
#!bash
80A0663C BMI loc_80A06648
80A06648 B loc_80A06644
80A06644 B loc_80A06624
80A06624 MOV R1, R2
80A06628 STMFD SP!, {R0,LR}
80A06630 BL loc_80A06634
80A06634 LDMFD SP!, {R0,LR}
80A06638 B sub_80A0664C
80A0664C B sub_80A01368
對于BMI,雖然形式上和之前分析的(BEQ loc_80A0658C
,BNE loc_80A0658C
)直接跳到next_jmp
,但檢測下一條指令即可根據條件相反去處。
對于STM-LDM,當遇到LDM指令時,將STM-LDM及其之間的指令出棧移除。
剩余(B B MOV B)這些指令,根據上述人工分析的結果可知,因為只存在一條真實指令,那么MOV必定是真實指令。另外,存在這種情況(B B BNE B),產生這種情況的根本原因是混淆前這條指令是if或者循環語句的判定點,直接取出BNE指令即可。
3.3 函數識別
不管是基于指令名稱匹配還是解析執行,都需要對函數進行識別。先來看一個函數混淆片段:
#!bash
seg013:80A067F0 loc_80A067F0 ; CODE XREF: seg013:loc_80A06838j
seg013:80A067F0 ADR LR, sub_80A06814
seg013:80A067F4 STMFD SP!, {R8,R9,LR}
seg013:80A067F8 LDR R8, loc_80A067FC
seg013:80A067FC
seg013:80A067FC loc_80A067FC ; DATA XREF: seg013:80A067F8r
seg013:80A067FC LDR R9, =0x1A6016A4
seg013:80A06800 ADD R8, R9, R8
seg013:80A06804 ADD R8, PC, R8 ; j_strlen
seg013:80A06808 STR R8, [SP,#8]
seg013:80A0680C LDMFD SP!, {R8,R9,PC}
對于未混淆的指令,函數通常被編譯為BL或者BLX(指令模式切換)。由于B指令本身的跳轉地址范圍很有限,那么混淆后代碼膨脹必定需要對其指令修正,有點類似InlineHook指令修正。另外,函數的返回地址需要顯式存放到LR寄存器。
這樣,上述代碼在模擬執行時,當LR寄存器值不為0時,將后續的函數調用轉化為'Call sub_xxx'指令,將PC置為next_jmp
(sub_80A06814
)接著模擬。
另外,便于更加清晰的分析,將libc.so加載到和進程一致的基地址,通過IDAPython GetFunctionName獲取函數名稱。
至此,即可提取出真實指令,check函數流程:
#!bash
0x80a0135c PUSH {r3, r4, r5, r6, r7, r8, sl, lr}
0x80a01360 LDR r3, [r0]
0x80a01364 MOV r1, r2
0x80a01368 MOV r6, r2
0x80a0136c LDR r3, [r3, #0x2a4]
0x80a01370 MOV r2, #0
0x80a01374 MOV r4, r0
0x80a01378 BLX r3
0x80a0137c MOV r7, r0
0x80a01380 call j_strlen
0x80a01384 ADD sl, r0, #1
0x80a01388 MOV r8, r0
0x80a0138c MOV r0, sl
0x80a01390 call j_malloc_0
0x80a01394 MOV r1, r7
0x80a01398 MOV r2, sl
0x80a0139c MOV r5, r0
0x80a013a0 call j_memcpy
0x80a013a4 LDR r3, [r4]
0x80a013a8 MOV r2, #0
0x80a013ac STRB r2, [r5, r8]
0x80a013b0 LDR r3, [r3, #0x2a8]
0x80a013b4 MOV r2, r7
0x80a013b8 MOV r0, r4
0x80a013bc MOV r1, r6
0x80a013c0 BLX r3
0x80a013c4 LDR R0, =0x12BC4
0x80a013c8 MOV r1, #0x80
0x80a013cc LDR R0, [PC,R0]
0x80a013d0 call 0x80a01048
0x80a013d4 ADD r5, r5, r0
0x80a013d8 MOV r0, r5
0x80a013dc call 0x80a010c0
0x80a013e0 MOV r4, r0
0x80a013e4 MOV r0, r5
0x80a013e8 call j_free_1
0x80a013ec MOV r0, r4
0x80a013f0 POP {r3, r4, r5, r6, r7, r8, sl, pc}
通過簡單分析check即可看到算法的核心流程在0x80a010c0這個函數,而0x80a01048函數的功能是對指令路徑上的斷點進行檢測,和其他平臺的反調試思路類似,這里把重點放在0x80a010c0的逆向上。自動化分析得到0x80a010c0函數:
#!bash
0x80a010c0 PUSH {r4, r5, r6, r7, r8, sb, sl, lr}
0x80a010c4 LDR R7, =0x12EB4
0x80a010c8 SUB sp, sp, #0x308
0x80a010cc ADD r6, sp, #4
0x80a010d0 LDR R7, [PC,R7]
0x80a010d4 LDR r3, [r7]
0x80a010d8 MOV r4, r0
0x80a010dc MOV r1, #0
0x80a010e0 MOV r2, #0x100
0x80a010e4 MOV r0, r6
0x80a010e8 ADD r5, sp, #0x104
0x80a010ec STR r3, [sp, #0x304]
0x80a010f0 call j_memset
0x80a010f4 MOV r1, #0
0x80a010f8 MOV r2, #0x100
0x80a010fc MOV r0, r5
0x80a01100 call j_memset
0x80a01104 MOV r0, r4
0x80a01108 call j_strlen
0x80a0110c SUBS sb, r0, #0
0x80a01110 MOVEQ r0, sb
0x80a01114 BNE #0x80a01130
0x80a01118 LDR r2, [sp, #0x304]
0x80a0111c LDR r3, [r7]
0x80a01120 CMP r2, r3
0x80a01124 BNE #0x80a01334
0x80a01128 ADD sp, sp, #0x308
0x80a0112c POP {r4, r5, r6, r7, r8, sb, sl, pc}
//獲取代碼段起始256字節作為key
0x80a011bc LDR R0, =0x12DC0 //讀取代碼段起始地址
0x80a011c0 LDR LR, =0x66666667
0x80a011c4 MOV r4, #0
0x80a011c8 LDR R0, [PC,R0]
0x80a011cc MOV r3, r0
0x80a011d0 SMULL r2, ip, lr, r4
0x80a011d4 ASR r2, r4, #0x1f
0x80a011d8 LDRB r1, [r3]
0x80a011dc RSB r2, r2, ip, asr #1
0x80a011e0 ADD r2, r2, r2, lsl #2
0x80a011e4 RSB r2, r2, r4
0x80a011e8 STRB r1, [r6, r4]
0x80a011ec ADD r4, r4, #1
0x80a011f0 CMP r4, #0x100
0x80a011f4 ADD r3, r3, r2
0x80a011f8 BNE #0x80a011d0
//key變換流程
0x80a01218 MOV r3, #0
0x80a0121c MOV r0, r3
0x80a01220 ADD r4, r4, #1
0x80a01224 ADD r6, sp, #0x308
0x80a01228 AND r4, r4, #0xff
0x80a0122c ADD r1, r6, r4
0x80a01230 LDRB r2, [r1, #-0x304]
0x80a01234 LDRB r8, [r5, r3]
0x80a01238 AND ip, r3, #7
0x80a0123c ADD r0, r2, r0
0x80a01240 AND r0, r0, #0xff
0x80a01244 ADD r6, r6, r0
0x80a01248 LDRB sl, [r6, #-0x304]
0x80a0124c ASR sb, r8, #5
0x80a01250 ORR r8, sb, r8, lsl #3
0x80a01254 STRB sl, [r1, #-0x304]
0x80a01258 STRB r2, [r6, #-0x304]
0x80a0125c LDRB r6, [r1, #-0x304]
0x80a01260 ADD sl, sp, #0x308
0x80a01264 RSB r1, ip, #8 // 8 - [0, 7]
0x80a01268 ADD r2, r2, r6
0x80a0126c AND r2, r2, #0xff
0x80a01270 ADD r2, sl, r2
0x80a01274 LDRB r2, [r2, #-0x304]
0x80a01278 EOR r2, r2, r8
0x80a0127c AND r2, r2, #0xff
0x80a01280 LSL r1, r2, r1
0x80a01284 ORR ip, r1, r2, asr ip // 循環左移(8 - i)位
0x80a01288 STRB ip, [r5, r3]
0x80a0128c ADD r3, r3, #1
0x80a01290 CMP r3, #0x100
0x80a01294 BNE #0x80a01220
for(i = 0; i < 0x100; i++){
left_rotate(right_rotate(mid_code[i], 5) ^ key_stream[i], 8 - (i % 8));
...
限于篇幅,就不在一一分析。其中包括RC4算法。最后得到算法編碼主流程:
#!cpp
char gen_mid_code[N];
char key_stream[N];
for(i = 0; i < N; i++){
gen_mid_code[i] = left_rotate(str[gen_index(i, strlen(str))], 8 - (i % 8));
}
gen_key_stream(ori_key, key_stream);
RC4_encrypt(gen_mid_code, key_stream, final_code);
for(i = 0; i < N; i++){
if(final_code[i] != check_code[i]){
...
}
}
最后,得到flag:Hello Tomorrow!
至此,此ctf題目大致分析完畢。
題目下載地址:http://pan.baidu.com/s/1hrqZH9E