首页
论坛
专栏
课程

[原创]看雪 CTF2018.12 第六题 无Win7的Win10静态分析

2018-12-11 22:28 1045

[原创]看雪 CTF2018.12 第六题 无Win7的Win10静态分析

HHHso
15
2018-12-11 22:28
1045

题目示意:“ 在win7运行 win10可能报错”
实际上由于其采用模块全径哈希来定位,而不仅是模块名,或全大或小写路径哈希,
其兼容性不是一般的差,路径稍有出入,win7都不见得一定能跑得起来,需要做一定修正。

这里在无法确定劫持模块时,从函数名的哈希值着手结合pefile搜索目标模块及函数。
这里给出了一种python代码如何无缝调用IDA函数shellcode的简单方式。
实际上IDA出现的任何函数都可以转为python调用,包括那些使用导入函数和多个子函数调用的,这里只做简单应用。


Just Do I.T.
上IDA静态分析,Exports中发现了线程回调函数,它会优先于其它程序代码先执行,
常见SMC手段之一,至于一些版本的VMP是见面必TlsCallback,这里与VMP无关。

回调函数逻辑相对简单,如图,
(1)调用 Hi_smc_mainW_jmpto_main_401D50 函数将封装函数mainW修正转调用到真正的主函数main。
(2)调用 Hi_hook_GetWindowTextA_401C10 完成 user32.GetWindowTextA 函数的劫持,以插入校验代码函数。
(3)启动线程函数 Hi_loop_checkset_hook_401CF0 对劫持状态检测,若劫持被还原,则进行劫持。
         (注:由于其循环没有任何如sleep(0.1)之类的延迟,循环CPU开销不是一般大)



(1) Hi_smc_mainW_jmpto_main_401D50 函数伪码如图

  (1.1)  Hi_VirtualProtect_P1addr_64h_RWE_4019B0  将特定内存属性修改为可读、可写、可执行,

     而  Hi_VirtualProtect_P1addr_64h_backProtect_4019E0 执行相反操作,将内存属性还原为原来的属性。

  (1.2)  unsigned char loc_long_jmp[5]存放用于放置在 Hi_fp_mainW_414018指向的封装函数Hi_mainW_401280偏移+4处的长跳转指令,

     跳转到Hi_fp_mainPtr_414014指向的主函数Hi_main_401220。

长跳指令偏移基本算法:long_jmp_E9_offset = tgt_addr - (long_jmp_E9_addr+5) //length of long_jmp_E9 = 5


 





Hi_mainW_401280 修改前后



(2)  user32.GetDlgItemTextA  劫持函数  Hi_hook_GetWindowTextA_401C10
(2.1)  Hi_VirtualProtect_P1addr_64h_RWE_4019B0 、 Hi_VirtualProtect_P1addr_64h_backProtect_4019E0 和长跳指令原理参考(1)
(2.2)   Hi_getaddr_of_user32_GetDlgItemTextA_4018D0主要是获取目标函数地址函数,以搜索模块路径哈希值和函数名哈希值的方式;
(2.3)  劫持点位置是 user32.GetWindowTextA+0x20
(2.4)  劫持嵌入的用户函数为Hi_GetWindowTextA_post_401A10
(2.5)  全局变量
全局变量  Hi_fp_user32_GetDlgItemTextA_4147E8  存放的是返回的GetDlgItemTextA地址,
全局变量  Hi_back_short_jmp_with3_raw_bytes_4147DC  存放被劫持点的5字节代码(刚好一条长跳指令)
全局变量  Hi_long_jmp_414028  存放我们根据前述长跳偏移算法计算得到的长跳指令。
我们主要关注(2.2)(2.3)(2.4)


(2.2) Hi_getaddr_of_user32_GetDlgItemTextA_4018D0如下
其先通过  Hi_getModule_byHash_4018F5  获取  user32.dll  模块句柄,以edx寄存器返回,
然后  Hi_getFunction_byHash_401928  获取函数  GetDlgItemTextA 的函数地址。
在这里,如果样例能正常运作,在0x4018ee处断下,观察eax的值,一般调试器都会指明是
GetDlgItemTextA 函数地址。但在无法执行的情况下该如何是好?且继续往下。



(2.2.1)Hi_getModule_byHash_4018F5原理
fs:[30]为进程快信息地址,
通过windbg的 dt _PEB dwo(fs:[0x30])我们可以知道, 
其中0x0C位置为进程先后加载的模块信息Ldr,样例选用
+1C位置的初始化顺序排列的模块链,其中
.+00.Next
.+08 ModuleBase
.+18 ModuleNamePtr



1:001> dt _PEB dwo(fs:[0x30])
ntdll!_PEB
   +0x000 InheritedAddressSpace : 0 ''
   +0x001 ReadImageFileExecOptions : 0 ''
   +0x002 BeingDebugged    : 0x1 ''
   +0x003 BitField         : 0x4 ''
   +0x003 ImageUsesLargePages : 0y0
   +0x003 IsProtectedProcess : 0y0
   +0x003 IsImageDynamicallyRelocated : 0y1
   +0x003 SkipPatchingUser32Forwarders : 0y0
   +0x003 IsPackagedProcess : 0y0
   +0x003 IsAppContainer   : 0y0
   +0x003 IsProtectedProcessLight : 0y0
   +0x003 IsLongPathAwareProcess : 0y0
   +0x004 Mutant           : 0xffffffff Void
   +0x008 ImageBaseAddress : 0x010c0000 Void
   +0x00c Ldr              : 0x77630c40 _PEB_LDR_DATA  <-----------------------------------先后加载的模块链信息
   +0x010 ProcessParameters : 0x001629d0 _RTL_USER_PROCESS_PARAMETERS
   (略)

1:001> dt _PEB_LDR_DATA dwo(dwo(fs:[0x30])+0x0C)
ntdll!_PEB_LDR_DATA
   +0x000 Length           : 0x30
   +0x004 Initialized      : 0x1 ''
   +0x008 SsHandle         : (null) 
   +0x00c InLoadOrderModuleList : _LIST_ENTRY [ 0x164fd0 - 0x166f50 ]             按加载先后排序的模块链
   +0x014 InMemoryOrderModuleList : _LIST_ENTRY [ 0x164fd8 - 0x166f58 ]        按内存地址排列的模块链
   +0x01c InInitializationOrderModuleList : _LIST_ENTRY [ 0x164ed8 - 0x165470 ] 按初始化顺序排列的模块链(样例所选)
   +0x024 EntryInProgress  : (null) 
   +0x028 ShutdownInProgress : 0 ''
   +0x02c ShutdownThreadId : (null) 

1:001> dt _LDR_DATA_TABLE_ENTRY dwo(dwo(dwo(fs:[0x30])+0x0C)+0x1C)-0x10
ntdll!_LDR_DATA_TABLE_ENTRY
   +0x000 InLoadOrderLinks : _LIST_ENTRY [ 0x165460 - 0x164fd0 ]
   +0x008 InMemoryOrderLinks : _LIST_ENTRY [ 0x165468 - 0x164fd8 ]
   +0x010 InInitializationOrderLinks : _LIST_ENTRY [ 0x165830 - 0x77630c5c ]  <---------------遍历位置
   +0x018 DllBase          : 0x77510000 Void                                         //若匹配则返回偏移+8位置的值,为模块基址
   +0x01c EntryPoint       : (null) 
   +0x020 SizeOfImage      : 0x19c000
   +0x024 FullDllName      : _UNICODE_STRING "C:\WINDOWS\SYSTEM32\ntdll.dll"  // 偏移+14,+16,+18位置 {len,max,strptr}
   +0x02c BaseDllName      : _UNICODE_STRING "ntdll.dll"
   +0x034 FlagGroup        : [4]  "???"
   +0x034 Flags            : 0xa2c4
   +0x034 PackagedBinary   : 0y0
   +0x034 MarkedForRemoval : 0y0
   +0x034 ImageDll         : 0y1
   +0x034 LoadNotificationsSent : 0y0

1:001> dt _UNICODE_STRING
ntdll!_UNICODE_STRING
   +0x000 Length           : Uint2B
   +0x002 MaximumLength    : Uint2B
   +0x004 Buffer           : Ptr32 Wchar

(2.2.2) Hi_getFunction_byHash_401928 原理
其主要是遍历其模块的导出函数表的导出函数

 
(2.2.3)虽然上面已经标注处模块路径是C:\Windows\sysWOW64\user32.dll,
但实际上win10会将Ldr中的模块链的模块路径修改为C:\windows\system32\user32.dll。
不会有sysWOW64的出现,即使用的是该目录模块,所以永远是搜不到,永远在循环中。
这时可将 004018D9 push    3BD696F4h处的常量修改为0x6B87C9A9  = hashw("C:\\WINDOWS\\System32\\USER32.dll"),即可在Win10中跑起来。
如图,hashw是我们python封装的IDA看到的样例hash函数,任何版本系统执行,修改为响应user32.dll全路径的hashw值即可。


(2.2.4)必须承认,开始是无法确定是 C:\Windows\sysWOW64\user32.dll的,所以只能从函数名着手。
我们遍历导入模块所有导出函数的hash值,找到目标函数。
DumpBytesFromIDAtoFile 用于从IDA中dump处  Hi_hash_bufPtr_bufSize_4017B0  函数的shellcode
getFuncPtrFromShellCodeFileWithModified  负责将shellcode生成python调用约定
我们对原hash的代码另外封装hashw和hashc版,hashw主要针对模块路径(UNICODE),而hashc针对函数名(ASCII)。



def DumpBytesFromIDAtoFile(ea_start = 0,ea_end = 0,full_file_path_name=''):
  cbsize = ea_end - ea_start
  bs = array.array('B','\0'*cbsize)
  for i in xrange(0,cbsize):
    bs[i] = Byte(ea_start+i)
  with open(full_file_path_name,'wb') as fout:
    bs.tofile(fout)

def LoadShellCodeFile(shellcodefile):
  sc = None
  with open(shellcodefile,'rb') as fin:
    fin.seek(0,2)
    scsize = fin.tell()
    fin.seek(0,0)
    sc = array.array('B')
    sc.fromfile(fin,scsize)
  return sc

def InitShellCode(sc):
  MEM_COMMIT = 0x00001000
  PAGE_EXECUTE_READWRITE = 0x40
  shellcodesize = 0x1000
  shellcodeaddr = windll.kernel32.VirtualAlloc(0,shellcodesize,MEM_COMMIT,PAGE_EXECUTE_READWRITE)
  MEM_RELEASE = 0x8000
  if sc.__len__() > shellcodesize: 
    raise Exception("ShellCodeSize > 0x{:X}".format(shellcodesize))
  memmove(shellcodeaddr,sc.buffer_info()[0],sc.__len__())
  return shellcodeaddr

def getFuncPtrFromShellCodeFileWithModified(shellcodefile,rModifyFunc,ProtoType):
  sc = LoadShellCodeFile(shellcodefile)
  shellcodeaddr = InitShellCode(sc)
  rModifyFunc(shellcodeaddr)
  return ProtoType(shellcodeaddr)

def getProtoType(ProtoTypeName = ""):
  from ctypes.wintypes import HWND, LPCSTR, UINT
  ProtoTypeName_ProtoType["windll_UintUintUnit"] = WINFUNCTYPE(UINT,UINT,UINT)
  return ProtoTypeName_ProtoType[ProtoTypeName]

def ModifyFunc(shellcodeaddr):
  pass

DumpBytesFromIDAtoFile(0x4017B0,0x40180B,r'.\CTF06_hash.b')
shellcodefile =r'.\CTF06_hash.b'
rModifyFunc = ModifyFunc
ProtoType = getProtoType("windll_UintUintUnit")
pysc_decrypteStrP1toP2 = getFuncPtrFromShellCodeFileWithModified(shellcodefile,rModifyFunc,ProtoType)

def hashw( s = r'C:\WINDOWS\SYSTEM32\ntdll.dll'):
  a = create_unicode_buffer(s)
  print "{:0X}".format(pysc_decrypteStrP1toP2(addressof(a),a.value.__len__()*2))

def hashc( s = r'C:\WINDOWS\SYSTEM32\ntdll.dll'):
  a = create_string_buffer(s)
  return pysc_decrypteStrP1toP2(addressof(a),a.value.__len__())
  #print "{:0X}".format(pysc_decrypteStrP1toP2(addressof(a),a.value.__len__()))

rp = r'c:\windows\system32'
ms = ['ntdll.dll','kernel32.dll','kernelbase.dll','user32.dll','gdi32.dll','msvcrt.dll']
mns = {}
for m in ms:
  mp = os.path.join(rp,m)
  pe = pefile.PE(mp)
  print mp
  for exp in pe.DIRECTORY_ENTRY_EXPORT.symbols:
    if exp.name not in [None,'']:
      h = hashc(exp.name)
      if h not in mns:
        mns[h] = [m+'|'+exp.name]
      else:
        mns[h].append(m+'|'+exp.name)

mns[0x925DF53F]
['user32.dll|GetDlgItemTextA']
于是我们得到要劫持的是user32.dll中的GetDlgItemTextA函数


(2.3) 劫持点 GetDlgItemTextA+0x20如图中jmp指令位置
即用户插入的函数  Hi_GetWindowTextA_post_401A10  会在  GetWindowTextA  获取输入key后调用。
这时已经取得用户输入key为图示lpString,长度为cchMax局部变量,(记下它们对应的位置ebp+10h,ebp+14h,圈重点,这个会考




(2.4) 由上述jmp劫持调用到 Hi_GetWindowTextA_post_401A10 中,我们的的考点来了,ebp不变,
即ebp+10h,ebp+14h对应中key和keylen。



Hi_GetWindowTextA_post_401A10 伪码如下,
(2.4.1)通过 Hi_initmap_run_401290(key,keylen) 对内置九宫图变换校验,应返回1。
(2.4.2)然后进一步通过 Hi_hash_bufPtr_bufSize_4017B0 检测器哈希值,防止多解。
(2.4.3)通过上面两步后,就会解密出 Hi_sucess_asc_41401C 和 Hi_OK_asc_414024 字符串用于显示成功。
(2.4.4)函数最后会还原劫持,以调用GetWindowTextA后续代码。(并进而被前述的循环检测线程再度劫持)


(2.4.1) Hi_initmap_run_401290九宫图校验
其定义了九宫图,unsigned char tblmap[3][3];
初始化为:
413
725
860

 
通过Hi_director_step_401380函数用输入key变换,最终要求九宫图变成
123
456
780

从图中我们知道key长都为偶数,每两个作出一个移动操作,每两个字符的第一个取值为wdsa之一,分别对应
w > 0  up
d > 1  right
s > 2  down
a > 3  left
而在 Hi_director_step_401380中,要求每对的第二各字符取值不能为0,取值只能在是九宫中出现的12345678之一。
如d3表示将九宫图中的3右移动一位,原位置置零。于是可得到最短移动方案。
即为
d 6
d 8
s 7
s 4
a 1
w 2
a 5
w 6
最终得到 d6d8s7s4a1w2a5w6,其也满足哈希要求5634D252

>>> hex(hashc('d6d8s7s4a1w2a5w6'))
'0x5634d252L'

再回头看看别的有啥发现?
主函数 Hi_main_401220 进来就是对话框显示了,
对话框消息处理函数 Hi_DialogFunc_4011A0 中 Hi_onClink_401040 为确认响应函数。
由于 GetDlgItemTextA 已经被劫持,调用GetDlgItemTextA时,若校验通过,
则 Hi_OK_fail_asc_41400C已被解密填充为OK!
Hi_tip_msg_sptr_414000 解密填充为success。
否则还是原来的”Try again!"和“fail"

>>> e = 'STA@AVU\x00'
>>> ''.join([chr((0x27-i)^ord(e[7-i])) for i in range(0,8)])[::-1]
"success'"
>>> e1 = 'oj\x03'
>>> ''.join([chr((0x22-i)^ord(e1[2-i])) for i in range(0,3)])[::-1]
'OK!'
>>>





cl = sark.Line(ea = 0x40D189)
cstr = ''
while cl.ea != 0x40D2A9:
  cstr+=chr(cl.insn.operands[1].imm)
  cl = cl.next

['Kernel32.dll', 'GetProcAddress', 'LoadLibraryA', 'User32.dll', 'MessageBoxA', 'success', '']
 Hi_nop_401020 只是作者源码没修改彻底的无用函数,作者另一个方案是通过Hi_load_call_MessageBoxA_success_40D180函数
动态加载调用显示成功信息,这里没用到。



[推荐]看雪企服平台,提供安全分析、定制项目开发、APP等级保护、渗透测试等安全服务!

最后于 2018-12-12 20:56 被HHHso编辑 ,原因: 审校
上一主题 下一主题
最新回复 (1)
cattrace 2019-3-4 10:10
2
0
厉害,学习了
游客
登录 | 注册 方可回帖
返回