一
程序内存布局
obf
和obf
。(其中一个用于存储数据,一个用于执行代码)二
混淆
三
反混淆
线性扫描算法
,也可以是递归行进算法
。出于方便,这里使用线性扫描算法。花指令
对抗,混淆通过添加基于固定模式生成的花指令达到抵御静态分析的效果,而反混淆(机器码层的特征码匹配)则将混淆中的固定模式转化为字节形式的模板,通过特征匹配,将对应的代码删除,达到反混淆的目的。; pushf
; jb loc_3
;loc_1: jmp loc_2
; _ONE_BYTE_JUNKCODE_
;loc_2: call loc_4
; _TWO_BYTE_JUNKCODE_
;loc_3: jb loc_1
; _ONE_BYTE_JUNKCODE_
;loc_4: add esp,4
; popf
; jmp loc_5
; _ONE_BYTE_JUNKCODE_
;loc_5: ....S = 9C720AEB01??E805000000????72F4??83C4049DEB01??
R = 9090909090909090909090909090909090909090909090
// asm
pushfd
push dword ptr ds:[433098]
pop dword ptr ds:[433038]
popfd
// machine
9C FF 35 98 30 43 00 8F 05 38 30 43 00 9D
// sig
9C FF 35 ?? ?? ?? ?? 8F 05 ?? ?? ?? ?? 9D
// patch
90 90 90 90 90 90 90 90 90 90 90 90 90 90
// script MAX_REF_COUNT 为 5000 所以需要多次运行脚本
findall 00A41000, "9C FF 35 ?? ?? ?? ?? 8F 05 ?? ?? ?? ?? 9D"
i = 0
loop:
memset ref.addr(i),90,E
i++
cmp i, ref.count()
jne loop
memset
命令来去除花指令,而该命令作用后,并不会将修改结果保存到补丁
当中。所以,这里使用scylla
(x64dbg 配套插件)来保存修改后的程序。EP
(EntryPoint)的位置,scylla 依次点击Dump
->PE Rebuild
。tacesrever
就是通过这一方式来反混淆的,但是效果不佳。mov rax, [rbp+0]
add [rbp+8], rax
pushfq
pop qword ptr [rbp+0]可以发现,这段程序做了个 add 指令,然后保存了 rflags。
总的来说,它是一个 add 类型的 handle。
CF
(控制流:control flow)。IDA 打开经过第一次修改的程序kctf_crackme_sbls_dump.exe
。sub_A41F0D
,IDA 转过去发现,正是下一个基本块。而dword_4AA610
与0Ah
进行异或,值是0042A9E0
,IDA 转过去发现,是_alloca_probe
函数。Call Imm
的指令做了变形,导致 IDA 的控制流分析失败了。利用python
编写个简单的脚本还原即可。还记得上文提到的线性扫描算法
吗?就是从一个base
开始死循环反汇编,脚本自己会异常打印日志的。from capstone import * CODE = open(r"f:\新建文件夹\样本\dmp\kctf_crackme_sbls_00A41000.bin", mode='rb').read()
md = Cs(CS_ARCH_X86, CS_MODE_32)
md.detail = Truedef MdDisasmLiteOne(md:Cs,eipCode,obfBase):
for v in md.disasm(eipCode, obfBase,1):
return vdef PrintIns(ins:CsInsn):
print("0x%x:\t%s\t%s" %(ins.address, ins.mnemonic, ins.op_str))
returnbase = 0x00400000
obfBase = 0x000A41000eipBlockAddress = obfBase-base
eipCode = CODE[eipBlockAddress:eipBlockAddress+16]currentBlockList = []
ins = MdDisasmLiteOne(md=md,eipCode=eipCode,obfBase=obfBase)scriptInsList = []
def ReadDword(addr):
fov = addr - base
v = CODE[fov:fov +4]
v = int.from_bytes(v,"little")
return vdef PrintScriptInsList(scriptInsList:list):
for i in scriptInsList:
print(i + ";")def PrintScriptInsListOne(scriptInsList:list):
v = ""
for i in scriptInsList:
v += i + ";"
print(v)def WriteScriptInsList(scriptInsList:list):
fo = open("reverseCFG.txt", "w")
fo.write("")
fo = open("reverseCFG.txt", "a")for i in scriptInsList:
fo.write(i + "\r\n")def WriteScriptInsListOne(scriptInsList:list):
fo = open("reverseCFG.txt", "w")
v = ""
for i in scriptInsList:
v += i + ";"
fo.write(v)try:
while 1:
# PrintIns(ins=ins)
currentBlockList.append(ins)
if ins.mnemonic == "ret":
nextBlockIpIns:CsInsn = currentBlockList[len(currentBlockList) - 4]
if nextBlockIpIns.mnemonic == "push":
nextBlockIp = nextBlockIpIns.operands[0].immcallImmIns:CsInsn = currentBlockList[len(currentBlockList) - 3]
callImmEnPoint = 0
callImmEn = 0
callImmKey = 0
callImm = 0
if callImmIns.mnemonic == "push":
callImmEnPoint = callImmIns.operands[0].mem.disp
callImmEn = ReadDword(callImmEnPoint)
callImmKeyIns:CsInsn = currentBlockList[len(currentBlockList) - 2]
if callImmKeyIns.mnemonic == "xor":
callImmKey = callImmKeyIns.operands[1].imm
callImm = callImmEn ^ callImmKey
# print(hex(callImm))
# print(hex(nextBlockIp))# scriptInsList.append("asm {},nop,1".format(hex(callImmIns.address)))
scriptInsList.append("asm {},nop,1".format(hex(callImmKeyIns.address)))
scriptInsList.append("asm {},nop,1".format(hex(ins.address)))
scriptInsList.append("asm {},\"call {}\",1".format(hex(nextBlockIpIns.address),hex(callImm)))
scriptInsList.append("asm {},\"jmp {}\",1".format(hex(callImmIns.address),hex(nextBlockIp)))nextfov = nextBlockIp-base
eipCode = CODE[nextfov:nextfov+16]
ins = MdDisasmLiteOne(md=md,eipCode=eipCode,obfBase=nextBlockIp)# PrintIns(ins=ins)
continuenextip = ins.address+ins.size
nextfov = nextip-base
eipCode = CODE[nextfov:nextfov+16]
ins = MdDisasmLiteOne(md=md,eipCode=eipCode,obfBase=nextip)
except:
WriteScriptInsList(scriptInsList)print("over")
CF
,因为Call
指令分为四种类型。(这里并未使用 intel 指令语法)tacesrever
通过汇编匹配来还原原始的汇编指令,我也写个脚本来做个简单的化简好了。IDA 将程序main
函数反编译后,可以发现这样形式的全局变量。这个全局变量有俩次读,一次写的操作。r sub_xxx+offset push ds:dword_43CC68
r sub_xxx+offset push ds:dword_43CC68
w sub_xxx+offset mov ds:dword_43CC68,4BFD4h
push eax
。看样子。这样的全局变量可以直接删除了。push ds:dword_4AA100
mov [esp], eax
from capstone import * CODE = open(r"f:\新建文件夹\样本\dmp\kctf_crackme_sbls_00A41000.bin", mode='rb').read()
md = Cs(CS_ARCH_X86, CS_MODE_32)
md.detail = Truedef MdDisasmLiteOne(md:Cs,eipCode,obfBase):
for v in md.disasm(eipCode, obfBase,1):
return vdef PrintIns(ins:CsInsn):
print("0x%x:\t%s\t%s" %(ins.address, ins.mnemonic, ins.op_str))
returndef GetInsStr(ins:CsInsn):
return "%s %s" %(ins.mnemonic, ins.op_str)base = 0x00400000
obfBase = 0x000A41000eipBlockAddress = obfBase-base
eipCode = CODE[eipBlockAddress:eipBlockAddress+16]currentBlockList = []
ins = MdDisasmLiteOne(md=md,eipCode=eipCode,obfBase=obfBase)scriptInsList = []
def ReadDword(addr):
fov = addr - base
v = CODE[fov:fov +4]
v = int.from_bytes(v,"little")
return vdef PrintScriptInsList(scriptInsList:list):
for i in scriptInsList:
print(i + ";")def PrintScriptInsListOne(scriptInsList:list):
v = ""
for i in scriptInsList:
v += i + ";"
print(v)def WriteScriptInsList(scriptInsList:list):
fo = open("reverseCFG_6.txt", "w")
fo.write("")
fo = open("reverseCFG_6.txt", "a")for i in scriptInsList:
fo.write(i + "\r\n")def WriteScriptInsListOne(scriptInsList:list):
fo = open("reverseCFG_6.txt", "w")
v = ""
for i in scriptInsList:
v += i + ";"
fo.write(v)GlobalVariantTable = {}
def GetInsFromInsOffset(ins:CsInsn,offset):
if offset < 0:
return None
else:
for i in range(0,offset):
nextip = ins.address + ins.size
nextfov = nextip-base
eipCode = CODE[nextfov:nextfov+16]
ins = MdDisasmLiteOne(md=md,eipCode=eipCode,obfBase=nextip)
return ins# push dword ptr ds:[0x00433088]
def FindGlobalVariantIsInvail(baseIns:CsInsn):
globalVariantAddress = baseIns.operands[0].mem.disp
globalVariantIsInvail = True
ins = baseIns
point = 0
itba = []sil = []
while point < 200:
basePoint = point
# str = GetInsStr(ins=ins)
if ins.mnemonic == "push":
if ins.operands[0].mem.disp == globalVariantAddress:
# PrintIns(ins=ins)
itba.append(ins)
point +=1if ins.mnemonic == "mov":
if ins.operands[0].mem.disp == globalVariantAddress:
# PrintIns(ins=ins)
itba.append(ins)if ins.mnemonic == "pop":
if ins.operands[0].mem.disp == globalVariantAddress:
# PrintIns(ins=ins)
itba.append(ins)ins = GetInsFromInsOffset(ins,point - basePoint + 1)
opsize = len(ins.operands)
if opsize > 1:
if ins.operands[1].mem.disp == disp:
PrintIns(ins=ins)
globalVariantIsInvail = False
point += 1
for ins in itba:
ins:CsInsn = ins
if ins.mnemonic == "push":
dins = GetInsFromInsOffset(ins,1)
if GetInsStr(dins) == "mov dword ptr [esp], eax":
sil.append("asm {},nop,1".format(hex(ins.address)))
sil.append("asm {},nop,1".format(hex(ins.address+6)))
sil.append("asm {},\"push eax\",1".format(hex(ins.address)))if globalVariantIsInvail == True:
if ins.mnemonic == "mov":
sil.append("asm {},nop,1".format(hex(ins.address)))
if ins.mnemonic == "pop":
sil.append("asm {},nop,1".format(hex(ins.address)))return sil
try:
while 1:
# PrintIns(ins=ins)
currentBlockList.append(ins)
if ins.mnemonic == "push":
disp = ins.operands[0].mem.disp
if disp>=0x0433000 and disp<=obfBase:
PrintIns(ins=ins)
if (disp in GlobalVariantTable) == False:
GlobalVariantTable[disp] = FindGlobalVariantIsInvail(ins)
if GlobalVariantTable[disp] != []:
for i in GlobalVariantTable[disp]:
scriptInsList.append(i)
ins = GetInsFromInsOffset(ins,1)continue
nextip = ins.address+ins.size
nextfov = nextip-base
eipCode = CODE[nextfov:nextfov+16]
ins = MdDisasmLiteOne(md=md,eipCode=eipCode,obfBase=nextip)
except:
WriteScriptInsList(scriptInsList)print("over 6")
scylla
这一插件进行保存修改。IDAPython
提供了操作AST
的接口,这里的AST
对应结构为ctree
,可以在idapython_docs
当中查看相关信息。ctree
中的节点称为citem_t
,而citem_t
这一抽象结构可以具体分化为cinsn_t
与cexpr_t
。(cinsn_t 的结构中包含cexpr
结构)ctree_visitor_t
类,通过继承该类,重写特定函数(visit_insn
与visit_expr
)达到对ctree
的curd
操作。eflags
呢?v8 = __readeflags(); // pushfd
__writeeflags(v8); // popfd
HRDevHelper
提取对应表达式的结构,这个结构用于判断是否是垃圾代码。// __readeflags
(i.op is idaapi.cit_expr and
i.cexpr.op is idaapi.cot_asg and
i.cexpr.x.op is idaapi.cot_var and
i.cexpr.y.op is idaapi.cot_call and
i.cexpr.y.x.op is idaapi.cot_helper)(i.op is idaapi.cit_expr and
i.cexpr.op is idaapi.cot_asg and
i.cexpr.x.op is idaapi.cot_var and
i.cexpr.y.op is idaapi.cot_cast and
i.cexpr.y.x.op is idaapi.cot_call and
i.cexpr.y.x.x.op is idaapi.cot_helper)// __writeeflags
(i.op is idaapi.cit_expr and
i.cexpr.op is idaapi.cot_call and
i.cexpr.x.op is idaapi.cot_helper and
i.cexpr.a[0].op is idaapi.cot_var)// __writeeflags
(i.op is idaapi.cit_expr and
i.cexpr.op is idaapi.cot_call and
i.cexpr.x.op is idaapi.cot_helper and
i.cexpr.a[0].op is idaapi.cot_var)
ctree
中特定类型的指令。import idaapi
import idc
import ida_bytes
import ida_hexraysclass JunkCodeVister(idaapi.ctree_visitor_t):
def __init__(self, cfunc):
global itlist
idaapi.ctree_visitor_t.__init__(self, idaapi.CV_PARENTS)
itlist = []
s = cfunc.body
s.swap(cfunc.body)
self.cfunc = cfunc
self.del_point = 0
def isjunk(self,ins) -> "int":
i = ins
if (i.op is idaapi.cit_expr and
i.cexpr.op is idaapi.cot_asg and
i.cexpr.x.op is idaapi.cot_var and
i.cexpr.y.op is idaapi.cot_call and
i.cexpr.y.x.op is idaapi.cot_helper):
return True
if (i.op is idaapi.cit_expr and
i.cexpr.op is idaapi.cot_call and
i.cexpr.x.op is idaapi.cot_helper and
i.cexpr.a[0].op is idaapi.cot_var):
return True
if (i.op is idaapi.cit_expr and
i.cexpr.op is idaapi.cot_call and
i.cexpr.x.op is idaapi.cot_helper and
i.cexpr.a[0].op is idaapi.cot_cast and
i.cexpr.a[0].x.op is idaapi.cot_var):
return True
return False
def visit_insn(self, ins: idaapi.cinsn_t) -> "int":
self.del_point +=1
if ins.op == idaapi.cit_if and self.del_point !=1:
my_visitor = JunkCodeVister(self.cfunc)
my_visitor.apply_to(ins, ins)
self.prune_now()
return 0
if not ins.cexpr:
return 0
f = self.isjunk(ins)
if f == True:
pd = self.parent_insn().details
dit = self.parent_insn().details.find(ins)
pd.erase(dit)
pass
return 0
class DelJunkCodeVister(idaapi.ctree_visitor_t):
def __init__(self, cfunc):
idaapi.ctree_visitor_t.__init__(self, idaapi.CV_FAST)
self.cfunc = cfunc
returndef main():
func = idaapi.get_func(idc.here())
cfunc = idaapi.decompile(func.start_ea)
my_visitor = JunkCodeVister(cfunc)
my_visitor.apply_to(cfunc.body, None)
print("deover")
if __name__ == '__main__':
main()
表达式中第一个操作数为全局变量
的指令全部删掉。它们的值无需关心,因为在这样情况下的静态分析,你已无法清晰地知道程序的动态结果。(若要用,跑个 Trace 补上就好)isjunk
函数中添加如下逻辑即可:if (i.op is idaapi.cit_expr andi.cexpr.op is idaapi.cot_asg andi.cexpr.x.op is idaapi.cot_obj):
return True
四
做题
请问大侠尊姓大名?: 梁山.zZhouQing
请问大侠是否有令牌入阁内?: 他时若遂凌云志,敢笑黄巢不丈夫。
内存执行
断点。00A6261C | jmp kctf_crackme_sbls_reverse_cfg_5_dump.A6262 |
sub_A6322D
函数,这个函数内容有点大,如出现function is too big
的问题,请查阅附录中的IDA问题解决->function is too big
。std::cout
,扫下它的引用看看。&unk_42C4DE
是什么。*(_DWORD *)(a1 + 116)
与*(_DWORD *)(*(_DWORD *)(a1 + 68) + 8)
识别为变量,故采取复制内容到VsCode
进行分析的方案。Ctrl+F
搜索*(_DWORD *)(a1 + 116)
发现其有俩个引用,好消息,这意味着*(_DWORD *)(*(_DWORD *)(a1 + 68) + 8)
很有可能为const_flag
。(经过搜索,发现其的确只引用了一次)*(_DWORD *)(a1 + 116) = *(_DWORD *)(a1 + 592376);
citem_t
即可。if ( timeGetTime() - *(_DWORD *)(a1 + 592380) > 0x1D4C0 ){
v10485 = v9397;
v9399 = dword_49E0D0;
v10425 = v9399;
*(_DWORD *)(a1 + 731596) = v10425;
v10485 = v9402;
v9404 = dword_49E1B8;
....
}
特殊性
,这是我们思考问题的方法。这PAIR64函数未免太特殊了,得多注意。if ( __PAIR64__((unsigned int)v9746, v9742) >= (unsigned int)v10506 )
{
*(_BYTE *)(a1 + 649976) = 1;
if ( (*(_BYTE *)(a1 + 649976) & 1) != 0 )
{
*(_BYTE *)(a1 + 649235) = 1;
if ( (*(_BYTE *)(a1 + 649235) & 1) == 0 )
{
if ( (*(_BYTE *)(a1 + 650096) & 1) != 0 )
{
if ( *(_DWORD *)(a1 + 726908) < *(_DWORD *)(a1 + 726920) )
{
....
*(_DWORD *)(a1 + 116) = *(_DWORD *)(a1 + 592376);
....
}
}
}
}
}
for ( ; *(int *)(a1 + 649860) < 256; ++*(_DWORD *)(a1 + 649860) )
{
*(_DWORD *)(a1 + 649856) = *(_DWORD *)(a1 + 649860);
v10506 = v8724;
v8726 = (void *)dword_496F28;
v10505 = v8726;
v10504 = (void *)v8728;
v10501 = (int *)dword_496F88;
v10502 = (int *)dword_496F48;
v10503 = dword_496F48;
*(_DWORD *)(a1 + 649852) = v10505;
for ( ; *(int *)(a1 + 649852) > 0; --*(_DWORD *)(a1 + 649852) )
{
if ( (*(_DWORD *)(a1 + 649856) & 1) != 0 )
*(_DWORD *)(a1 + 649856) = (*(int *)(a1 + 649856) >> 1) ^ 0x520;
else
*(int *)(a1 + 649856) >>= 1;
}
dword_42F0C4[*(_DWORD *)(a1 + 649860)] = *(_DWORD *)(a1 + 649856);
}
else
块里,通过另一个基本块内容,猜测其可能是程序的暗装。if(timeGetTime() - *(_DWORD *)(a1 + 370880) <= *(_DWORD *)(a1 + 370884) ){
...
fakecode
...
}else{
...
flagcode
...
}
*(_DWORD *)(a1 + 701100) -= 4;
*(_DWORD *)(a1 + 701112) = *(_DWORD *)(a1 + 701116);
*(_DWORD *)(a1 + 701116) = *(_DWORD *)(a1 + 701120);
*(_DWORD *)(a1 + 701120) = *(_DWORD *)(a1 + 701124);
*(_DWORD *)(a1 + 701124) = *(_DWORD *)(a1 + 701128);
*(_DWORD *)(a1 + 701128) = *(_DWORD *)(a1 + 701112);
++*(_DWORD *)(a1 + 701104);
}
while ( *(_DWORD *)(a1 + 701104) < 5u );LABEL_837:
*(_DWORD *)(a1 + 701116) -= **(_DWORD **)(a1 + 701100);
*(_DWORD *)(a1 + 701100) -= 4;
*(_DWORD *)(a1 + 701124) -= **(_DWORD **)(a1 + 701100);
*(_DWORD *)(a1 + 701100) -= 4;
for ( ; *(_DWORD *)(a1 + 420940) != 24; ++*(_DWORD *)(a1 + 420940) ){
...
rc6
flagcode
...
}
五
总结
六
附录
pip install qiling
命令即可安装,一应俱全。ctree
图形,并在其底层反编译代码和图形的各个项目之间创建可视化链接来发挥其作用。Local variables area
的值,我喜欢填0x1000
。(因为是 4096,hiahia,正好一页大小)如果反编译结果仍然出现 STACK[count],那就写个脚本改个别名。IDA\cfg\hexrays.cfg
MAX_FUNCSIZE = 64 // Functions over 64K are not decompiled
MAX_FUNCSIZE = 4096 // Functions over 64K are not decompiled
push eax
mov eax,dword ptr ds:[433038]
push eax
mov eax,dword ptr ss:[esp+4]
pop dword ptr ss:[esp]
pop eax
push
和pop
指令的存在,所以数据流分析需要考虑到栈。定义stack_point_offset
为 0。每条指令都记录修改前与修改后的stack_point_offset
,记得当时写的脚本所分析的数据流是这样子的。stack_point_offset = 0
[esp-4] = eax; //stack_point_offset = -4
eax = [433038]; //stack_point_offset = -4
[esp-4] = eax; //stack_point_offset = -8
eax = [esp + 4]; //stack_point_offset = -8
[esp+4] = [esp]; //stack_point_offset = -4
eax = [esp-4]; //stack_point_offset = 0
xchg [esp],[433038]
binary ninja
这一反编译器给用户提供了操作IL
的接口,但是我在测试时发现关于pop dword ptr ss:[esp]
这一指令的解析是错误的。(数据的传递出现错误,虽然不影响基于SSA
理论的编译优化,但看着碍眼)不过我的电脑太老旧了,在此已经无法继续细说。汇编代码匹配
方便许多,这是基于汇编代码匹配
的一种平替方向。看雪ID:zZhouQing
https://bbs.kanxue.com/user-home-960900.htm
# 往期推荐
2、恶意木马历险记
球分享
球点赞
球在看
点击阅读原文查看更多