首页
论坛
专栏
课程

[原创]看雪 CTF2018.12 第二题 回马枪&牛刀

HHHso
14
2018-12-5 11:37 397

提纲:
一、 回马枪校验机制,擒贼先擒王, 拼手速。
二、番外牛刀小试(符号计算应用)


一、 回马枪校验机制,擒贼先擒王, 拼手速。
(1)根据输入提示'Please Input:',在IDA中String窗口定位该字符串位置及其引用位置;
引用位置即为主函数位置。
我们可能会遇到主函数堆栈平衡分析错误,如下图的控制流实际是存在分析错误的情形,从而引起堆栈平衡冲突:


实际是因为 Hi_exit_or_terminate_process_54A7B0_w1 函数并不返回造成。而IDA无法识别而有不确定其不返回。
我们做一下修改,右键编辑函数"Edit Function",选择不返回。


这时的graph控制流才是正确的,堆栈不平衡问题也自然地解决。


主函数代码相对简单,伪码如图,
读入用户输入key到Hi_key_byte_5F3068
长度要求在[10,30)之间,然后拷贝到动态分配的内存Hi_innerkey_byte_5F3088中,并要求key[7]='A'
最后由主函数进入Hi_set_ch23h_and_xor_1F_49DBD0_w1,
而 Hi_set_ch23h_and_xor_1F的业务逻辑也很简单,就是将key[7]='A'强制更改为'#'字符后,与0x1F异或加密。
到此,主函数的完了,其中看不到任何校验逻辑,似乎有点费解。



快捷键X查阅Hi_innerkey_byte_5F3088的交叉引用,如图
第一条是动态内存分配,后面三条是主函数的操作,都无关紧要。
我们将注意力放在第二条。


Hi_IsKeyOK_49DC80 的算法,如下,是很明显的校验逻辑
其输入参数refKey=“invalid argument"
refKey 经过与0x1C异或加密后在与之前已经通过设置'#'和异或0x1F加密的 Hi_innerkey_byte_5F3088 比较。
匹配即输出‘ok’,以下是python表演时间
整个校验算法如下:
key[7]='A'
key[7]='#'
key = key^0x1F
refkey = 'invalid argument'
refkey = refkey ^0x1C
key==refkey

python逆运算可以得到key
a = "invalid argument"
b = ''.join(chr(ord(ch)^0x1C) for ch in a)
#b = 'urj}pux<}n{iqyrh'
c = ''.join(chr(ord(ch)^0x1F) for ch in b)
#c='jmubojg#bqdvnfmw'
key = jmubojgAbqdvnfmw

于是我们得到 key = jmubojgAbqdvnfmw





问题:为何主函数看不到对这个校验逻辑的引用?它是如何被调用的?
我们不妨继续追溯Hi_IsKeyOK_49DC80的调用层次
Hi_check_key_when_exit_5AFCB0
->
Hi_printOK_if_key_match_49CEB0_w1 -> Hi_printOK_if_key_match_49CEB0
 -> 
Hi_IsKeyOK_49DC80_w1 -> Hi_IsKeyOK_49DC80

至此即一目了然, Hi_check_key_when_exit_5AFCB0实际是在C\C++运行时初始化时,
由初始化函数组中的函数Hi_initarray_func_4957B0注册的退出时调用的函数。
类似我们通常说的析构函数。即其实际校验是下退出时杀个回马枪,
不过抓住 Hi_innerkey_byte_5F3088这个主要矛盾,并不一定需要了解其运作机制。




二、番外牛刀小试
这个样例是静态编译样例。许多函数都无法签名识别,且开启了调式特性,导致大量的封装函数。
如样例中虽然由一万多个函数名,实际由于调试封装,可以折半。
这里,我们编写自动标识封装函数的处理IDAPython脚本。
另外,对于其中有趣的内部动态加载的类似IAT的处理,我们也尝试处理一下(尝试符号计算的应用)。

(1)封装函数识别重命名
如图,这类是编译器开启调式选项形成的。

另外一类如只是简单地三部曲 enter,call,leave。
这里我们只识别这两类,实际,一个函数中,若只对一个函数调用,我们都可以认为是封装型函数。

def sark_name_func_wx():
  wd = {} #wrap:{func_w:func}
  for func in sark.functions():
    wl = sark_get_func_wrapline(func)
    if wl is not None:
      wd[func.name] = wl.insn.operands[0].text
  #
  nf = [] #name-func which are not wrapper for others
  for k,v in wd.viewitems():#
    if v not in wd.keys():
      nf.append(v)
  #mark func
  #return wd,nf
  for k,v in wd.viewitems():
    if k in nf:
      continue
    d,n = sark_get_call_deep(k,wd,nf)
    if n.startswith('Hi_'):
       new_name= "{}_w{}".format(n,d)
    else:
      new_name= "Hi_{}_w{}".format(n,d)
    print sark.Function(name=k).name,new_name
    sark.Function(name=k).name = new_name

调用sark_name_func_wx()后,就会自动重命名封装函数,在原函数基础上加上后缀”_wX“,其中X为封装深度,或说第X层封装。
一般我们完成了底层函数的意义名称逆向后,在执行 sark_name_func_wx(),封装函数就会被自动更新名称。
实际应用一般会重复调用多次,比较随着逆向解析的深入,更多的函数会被我们重新命名。
这函数可以帮我们省略再次对封装函数的命名操作。
(2)符号计算的引用,这里我们以Hi_inneriat_init_4A9D60为例。
Hi_inneriat_init_4A9D60函数主要完成了内置函数表Hi_inneriat_5F32C8的初始化
Hi_inneriat_5F32C8 用于实现涉及的函数的间接调用,也间接让IDA摸不着头脑。




红线标识是我们标定后识别命名的函数,重命名后再执行下(1)的函数是个不错的主意。

基本思路
(1)找到函数表中对应位置的指针代表的函数,即主要为了得到下面字典。
inneriat = {
  0x00:'FlsAlloc',
  0x04:'FlsFree',
  0x08:'FlsGetValue',
  0x0C:'FlsSetValue',
  0x10:'InitializeCriticalSectionEx',
  0x14:'InitOnceExecuteOnce',
  0x18:'CreateEventExW',
  0x1C:'CreateSemaphoreW',
  0x20:'CreateSemaphoreExW',
  0x24:'CreateThreadpoolTimer',
  0x28:'SetThreadpoolTimer',
  0x2C:'WaitForThreadpoolTimerCallbacks',
  0x30:'CloseThreadpoolTimer',
  0x34:'CreateThreadpoolWait',
  0x38:'SetThreadpoolWait',
  0x3C:'CloseThreadpoolWait',
  0x40:'FlushProcessWriteBuffers',
  0x44:'FreeLibraryWhenCallbackReturns',
  0x48:'GetCurrentProcessorNumber',
  0x4C:'CreateSymbolicLinkW',
  0x50:'GetCurrentPackageId',
  0x54:'GetTickCount64',
  0x58:'GetFileInformationByHandleEx',
  0x5C:'SetFileInformationByHandle',
  0x60:'GetSystemTimePreciseAsFileTime',
  0x64:'InitializeConditionVariable',
  0x68:'WakeConditionVariable',
  0x6C:'WakeAllConditionVariable',
  0x70:'SleepConditionVariableCS',
  0x74:'InitializeSRWLock',
  0x78:'AcquireSRWLockExclusive',
  0x7C:'TryAcquireSRWLockExclusive',
  0x80:'ReleaseSRWLockExclusive',
  0x84:'SleepConditionVariableSRW',
  0x88:'CreateThreadpoolWork',
  0x8C:'SubmitThreadpoolWork',
  0x90:'CloseThreadpoolWork',
  0x94:'CompareStringEx',
  0x98:'GetLocaleInfoEx',
  0x9C:'LCMapStringEx'}


我们用IDAPython脚本自动找出Hi_inneriat_init_4A9D60函数中每个GetProcAddress引用位置;
然后取该位置函数调用的参数2获取函数名;
而在函数调用的后面,其是对函数表偏移量指针赋值,我们通过符号计算得到偏移;
(当然,不借助符号运算,我们也可以针对每种指令集合情形,采用硬编码针对性获取偏移量,
但,这里不会只是mov ecx, imul edx,ecx.3的情形,如前面的mov ecx,4,shl ecx,0,
还有不是以edx为偏移的情形等等,我们初衷还是希望能找到一种灵活应对各种情况的解决方案,
而这里,符号计算是个不错选择,虽然有些杀鸡用牛刀,这里只是做符号计算的简单演示,
其在逆向中的威力取决于我们的想象力)
下图是我们的符号计算引擎,用于计算 mov     Hi_inneriat_5F32C8[ecx], eax引用处前两条指令时产生的IR表示,
并获取偏移寄存器的计算值,比如
mov ecx,4
imul edx,ecx,3
的IR表示如图



from miasm2.analysis.machine import Machine
from miasm2.core.bin_stream import bin_stream_str
from miasm2.ir.symbexec import SymbolicExecutionEngine
from miasm2.expression.expression import ExprId
machine = Machine('x86_32')
dis_engine,ira = machine.dis_engine,machine.ira
def miasm2_get_symbol_run_result(l,reg='edx'):
    global machine,dis_engine,ira
    shellcode=l.bytes+l.next.bytes
    bs = bin_stream_str(shellcode)
    bs_dis_engine = dis_engine(bs)
    bs_dis_engine.dont_dis = [shellcode.__len__()]
    block = bs_dis_engine.dis_block(0)
    ir_arch = ira(bs_dis_engine.loc_db)
    ircfg = ir_arch.new_ircfg()
    ir_block = ir_arch.add_asmblock_to_ircfg(block,ircfg)
    sb = SymbolicExecutionEngine(ir_arch)
    sb.run_block_at(ircfg,0)
    ret = sb.symbols[ExprId('{}'.format(reg.upper()),32)]
    return ret.arg.arg
以下用于获取函数内所有对GetProcAddress的引用位置,
然后向前定位参数2,获取函数名,向后定位Hi_inneriat_5F32C8引用,利用符号计算来获取偏移
hlafindpX,hla_find_next_instr_x在以前的文章中已经提供过,可以参考最后

GetProcAddress_ea = 0x005F5044
Hi_inneriat_init_4A9D60_ea = 0x4A9D60
def get_crefs_to_func1_wihtin_func2(func1,func2):
  within_fun_sea = sark.Function(ea = func2).startEA
  call_crefs_to = []
  for cr in sark.Line(ea = func1).crefs_to:
    if sark.Function(ea = cr).startEA == within_fun_sea:
      call_crefs_to.append(cr)
  return call_crefs_to

crs = get_crefs_to_func1_wihtin_func2(GetProcAddress_ea,Hi_inneriat_init_4A9D60_ea)

for cr in crs:
  push_func_name = hlafindpX(cr,1)
  func_name = sark.get_string(sark.Line(ea = push_func_name).insn.operands[0].imm)
  opl = sark.Line(ea = (hla_find_next_instr_x(cr,['Hi_inneriat_'],0,SEARCH_DOWN)))
  reg_name = list(opl.insn.operands[0].regs)[0]
  tbloff = miasm2_get_symbol_run_result(opl.prev.prev,reg_name)
  print "  0x{:02X}:'{}',".format(tbloff,func_name)

有了偏移和函数名,我们就可以进行标定命名。
get_proxy_crefs_to_tgt用于找到所有对函数表Hi_inneriat_5F32C8_ea的调用
如图,通过检测 Hi_inneriat_5F32C8_ea的引用后是否用security_cookie解密函数指针来判定
代码块是对函数表函数的封装调用。
也可以看到,其函数表的偏移由引用处的前两条指令计算出来,
这里我们还是用前面的符号计算引擎计算获取。


Hi_inneriat_5F32C8_ea = 0x005F32C8
def get_proxy_crefs_to_tgt(tgt):
  proxy_crefs_to = []
  for cr in sark.Line(ea = tgt).drefs_to:
    proxy_rl = sark.Line(ea = cr)
    if 'security_cookie' in proxy_rl.next.disasm:
      proxy_crefs_to.append(cr)
  return proxy_crefs_to

pcrs = get_proxy_crefs_to_tgt(Hi_inneriat_5F32C8_ea)
for pcr in pcrs:
  pcrl = sark.Line(ea = pcr)
  reg_name = list(pcrl.insn.operands[1].regs)[0]
  tbloff = miasm2_get_symbol_run_result(pcrl.prev.prev,reg_name)
  func_name = inneriat[tbloff]
  enter_el = sark.Line(ea = (hla_find_next_instr_x(pcr,['mov     ebp, esp'],0,SEARCH_UP)))
  if enter_el.prev.disasm == 'push    ebp':
    sark.function.add_func(enter_el.prev.ea)
    f = sark.Function(enter_el.prev.ea)
    new_name = "Hi_{}_{:X}".format(func_name,f.startEA)
    f.name = new_name
  else:
    print "{:X} maybe need your manual check.".format(enter_el.ea),'-'*80
  print "{:X} {}".format(enter_el.prev.ea,new_name)

最终,我们标定创建 以下函数,并给以函数命名
4AA460 Hi_AcquireSRWLockExclusive_4AA460
4AA4B0 Hi_CloseThreadpoolTimer_4AA4B0
4AA500 Hi_CloseThreadpoolWait_4AA500
4AA550 Hi_CloseThreadpoolWork_4AA550
4AA5A0 Hi_CreateEventExW_4AA5A0
4AA620 Hi_CreateSemaphoreExW_4AA620
4AA620 Hi_CreateSemaphoreW_4AA620
4AA6E0 Hi_CreateSymbolicLinkW_4AA6E0
4AA740 Hi_CreateThreadpoolTimer_4AA740
4AA7A0 Hi_CreateThreadpoolWait_4AA7A0
4AA800 Hi_CreateThreadpoolWork_4AA800
4AA850 Hi_FlsAlloc_4AA850
4AA8B0 Hi_FlsFree_4AA8B0
4AA910 Hi_FlsGetValue_4AA910
4AA970 Hi_FlsSetValue_4AA970
4AA9D0 Hi_FlushProcessWriteBuffers_4AA9D0
4AAA20 Hi_FreeLibraryWhenCallbackReturns_4AAA20
4AAA70 Hi_GetCurrentProcessorNumber_4AAA70
4AAAC0 Hi_GetFileInformationByHandleEx_4AAAC0
4AAB30 Hi_GetSystemTimePreciseAsFileTime_4AAB30
4AAB90 Hi_GetTickCount64_4AAB90
4AABE0 Hi_InitOnceExecuteOnce_4AABE0
4AAD40 Hi_InitializeConditionVariable_4AAD40
4AAD90 Hi_InitializeCriticalSectionEx_4AAD90
4AAE00 Hi_InitializeSRWLock_4AAE00
4AAEA0 Hi_ReleaseSRWLockExclusive_4AAEA0
4AAEF0 Hi_SetFileInformationByHandle_4AAEF0
4AAF60 Hi_SetThreadpoolTimer_4AAF60
4AAFC0 Hi_SetThreadpoolWait_4AAFC0
4AB020 Hi_SleepConditionVariableCS_4AB020
4AB070 Hi_SleepConditionVariableSRW_4AB070
4AB0D0 Hi_SubmitThreadpoolWork_4AB0D0
4AB120 Hi_TryAcquireSRWLockExclusive_4AB120
4AB170 Hi_WaitForThreadpoolTimerCallbacks_4AB170
4AB1C0 Hi_WakeAllConditionVariable_4AB1C0
4AB210 Hi_WakeConditionVariable_4AB210
4AB260 Hi_GetCurrentPackageId_4AB260
4B55F0 Hi_CompareStringEx_4B55F0
4B57B0 Hi_GetLocaleInfoEx_4B57B0
4B5830 Hi_LCMapStringEx_4B5830

hlafindpX,hla_find_next_instr_x代码
def hlafindpX(calladdr = 0x0, paramX = 0x0):
    searchflag = SEARCH_NEXT | SEARCH_CASE & ~SEARCH_DOWN
    #paramX is the (paramX+1)th parameter, so-called --index parameters
    pX = 0
    codeaddr = calladdr
    while pX <= paramX:
        codeaddr = FindCode(codeaddr,searchflag)
        if IshlaPush(codeaddr):
            pX = pX + 1
    return codeaddr

def IshlaPush(codeaddr=0x0):
    retbool = 0
    hlacodebyte = Byte(codeaddr)
    for pushflag in xrange(0x50,0x58):
        if hlacodebyte == pushflag:
            retbool = 2
            return retbool
    if hlacodebyte == 0x6A:
        retbool = 2
        return retbool
    if hlacodebyte == 0x68:
        retbool = 3
        return retbool
    if hlacodebyte == 0x0E or hlacodebyte == 0x16:
        retbool = 4
        return retbool
    if hlacodebyte == 0x1E or hlacodebyte == 0x06:
        retbool = 4
        return retbool
    if Word(codeaddr) == 0xA00F or Word(codeaddr) == 0xA80F:
        retbool = 4
        return retbool
    if Word(codeaddr) in [0x77FF,0x73FF,0x75FF,0x76FF,0x70FF,0x71FF,0x72FF,0x74FF,0x76FF]:
        #70 eax,ecx,edx,ebx,esp,ebp,esi,edi
        retbool = 5
        return retbool

def hla_find_next_instr_x(ea,instr = ['push'], index = 0,up_down = SEARCH_DOWN):
  # note: this is a weak func
  if (ea < 0) or (index < 0):
    Message('ea_cur can not set to zero.\n')
    return
  cnt = 0
  cur_code_addr = ea
  if up_down == SEARCH_DOWN:
      while 1:
         #Message('{:08X} {}\n'.format(cur_code_addr,GetDisasm(cur_code_addr)))
         hla_curasm = GetDisasm(cur_code_addr)
         #Message('{}\n'.format(hla_curasm))
         for instr_ in instr:
           if instr_ in hla_curasm:
             cnt = cnt + 1
             #Message('cnt-1:{} index{}\n'.format(cnt-1,index))
             if (cnt - 1) == index:
               return cur_code_addr
         cur_code_addr = FindCode(cur_code_addr,SEARCH_NEXT|SEARCH_DOWN)
  else:
      while 1:
         #Message('{:08X} {}\n'.format(cur_code_addr,GetDisasm(cur_code_addr)))
         hla_curasm = GetDisasm(cur_code_addr)
         for instr_ in instr:
           if instr_ in hla_curasm:
             cnt = cnt + 1
             if (cnt - 1) == index:
               return cur_code_addr
         cur_code_addr = FindCode(cur_code_addr,SEARCH_NEXT|SEARCH_UP)






[推荐]十年磨一剑!《加密与解密(第4版)》上市发行

最后于 2018-12-5 15:16 被HHHso编辑 ,原因: 修正
最新回复 (1)
月落之汀 1 2018-12-5 21:35
2

0

大佬,小声的说一下,ida7.2是可以正确识别大半的签名的,7.0不支持新VC支持库的签名的,不过还是跟您学到了ida歇菜时候的处理办法
返回