首页
社区
课程
招聘
[推荐]看雪CTF.TSRC 2018 团队赛 第六题 『追凶者也』 解题思路
发表于: 2018-12-23 16:13 4345

[推荐]看雪CTF.TSRC 2018 团队赛 第六题 『追凶者也』 解题思路

2018-12-23 16:13
4345



第六题《追凶者也》在今天(12月13日)中午12:00 攻击结束。


pizzatql 以 2448s 的成绩首先攻破此题。


成功攻击第六题《追凶者也》的团队共计29支。


最新赛况战况一览


第六题之后的最新排名是什么样的呢?


前四名依然不变,值得一说得是第10名的A2 晋升到排行榜第5,祝贺~!



第六题 点评


crownless:

“追凶者也”此题的主要困难是设置了异常回调来进行反调试,并要求参赛者完成一个简单的数字拼图,主要考验了参赛者通过静态分析找到关键函数的能力和脑筋急转弯能力。



第六题 出题团队简介


出题团队: one team  




第六题 设计思路


由看雪论坛穿甲葡萄籽 原创



程序需要运行在win7 64系统下。



设计思路:


1. 使用tls将GetDlgItemTextA Hook。在GetDlgItemTextA 返回前先获取到输入文本,进行判断。如果成功则修改MessageBoxA的参数,使其弹出成功。


2. 设置异常回调来进行反调试。tls中会将回调函数的代码改为一个跳转到窗口创建函数。


3.设计简单的算法,数字拼图


413

725

860


输入规则为:wasd表示方向,例如:w1表示将1向上移动一下。


成功拼成


123

456

780


表示成功。


为防止出现多个解,密码长度限制在20之内,并且会进行哈希计算校验。


破解思路:


1.找到关键的Hook处理函数,可以根据Hook函数找到关键处理函数地址。


2. 其中可以找到一个初始化数字拼图的函数,初始化一个二维数组。


413

725

860


3. 很容易就能得到正确的结果。


原文链接:


https://bbs.pediy.com/thread-227070.htm(含附件)



第六题 追凶者也 解题思路


本题解析由看雪论坛HHHso 原创。



题目示意:“ 在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函数动态加载调用显示成功信息,这里没用到。




合作伙伴


腾讯安全应急响应中心 

TSRC,腾讯安全的先头兵,肩负腾讯公司安全漏洞、黑客入侵的发现和处理工作。这是个没有硝烟的战场,我们与两万多名安全专家并肩而行,捍卫全球亿万用户的信息、财产安全。一直以来,我们怀揣感恩之心,努力构建开放的TSRC交流平台,回馈安全社区。未来,我们将继续携手安全行业精英,探索互联网安全新方向,建设互联网生态安全,共铸“互联网+”新时代。



转载请注明:转自看雪学院



看雪CTF.TSRC 2018 团队赛 解题思路汇总:












[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

最后于 2018-12-23 17:00 被Editor编辑 ,原因:
收藏
免费 0
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回
//