首页
论坛
专栏
课程

[原创][爱琴海]2017 最新 CTF Demo 试玩版题目 ------WriteUp

2017-7-18 19:43 2965

[原创][爱琴海]2017 最新 CTF Demo 试玩版题目 ------WriteUp

2017-7-18 19:43
2965

  • 下载题目,直接IDA打开查看字符串, 发现有很多py开头的函数,并且还有GetProcessAddress失败的提示信息,看来确实是Demo试玩版,提示信息很足,所以推测应该是内置了python脚本运行的。
    .rdata:0040CF21 00000015 C PyUnicode_FromFormat
    .rdata:0040CED0 0000001A C PyUnicode_DecodeFSDefault
    .rdata:0040CE90 00000011 C PyUnicode_Decode
    .rdata:0040CCCF 0000000E C PySys_SetPath
    .rdata:0040CC93 00000010 C PySys_SetObject
    .rdata:0040CC1B 00000010 C PySys_SetArgvEx
    .rdata:0040CC57 00000010 C PySys_GetObject
    .rdata:0040CBD7 00000014 C PySys_AddWarnOption
    .rdata:0040CB4E 00000014 C PyString_FromString
    .rdata:0040CB93 00000014 C PyString_FromFormat
    .rdata:0040CB0A 00000013 C PyRun_SimpleString
    .rdata:0040CAC1 00000017 C PyObject_SetAttrString
    .rdata:0040CA78 00000016 C PyObject_CallFunction
    .rdata:0040CA39 00000011 C PyModule_GetDict
    .rdata:0040CD48 0000001F C PyMarshal_ReadObjectFromString
    .rdata:0040CA02 0000000E C PyLong_AsLong
    .rdata:0040C9D1 0000000B C PyList_New
    .rdata:0040C999 0000000E C PyList_Append
    。。。。。。
    。。。。。。
    .rdata:0040CDF8 0000002B C Cannot GetProcAddress for Py_DecodeLocale\n
    .rdata:0040C660 00000025 C Cannot GetProcAddress for Py_DecRef\n
    .rdata:0040C62C 00000029 C Cannot GetProcAddress for Py_BuildValue\n
    .rdata:0040CDB8 00000030 C Cannot GetProcAddress for PyUnicode_FromString\n
    .rdata:0040CE60 00000030 C Cannot GetProcAddress for PyUnicode_FromFormat\n
    .rdata:0040CEA4 0000002C C Cannot GetProcAddress for PyUnicode_Decode\n
    .rdata:0040CEEC 00000035 C Cannot GetProcAddress for PyUnicode_DecodeFSDefault\n
    .rdata:0040CCE0 00000029 C Cannot GetProcAddress for PySys_SetPath\n
    .rdata:0040CCA4 0000002B C Cannot GetProcAddress for PySys_SetObject\n
    .rdata:0040CC2C 0000002B C Cannot GetProcAddress for PySys_SetArgvEx\n
    .rdata:0040CC68 0000002B C Cannot GetProcAddress for PySys_GetObject\n
    .rdata:0040CBEC 0000002F C Cannot GetProcAddress for PySys_AddWarnOption\n
    .rdata:0040CB64 0000002F C Cannot GetProcAddress for PyString_FromString\n
    .rdata:0040CBA8 0000002F C Cannot GetProcAddress for PyString_FromFormat\n
    .rdata:0040CB20 0000002E C Cannot GetProcAddress for PyRun_SimpleString\n
  • 由于之前没搞过python的,直接百度下相关内容了解到以下两个信息

    • 如果只是单独的python脚本行,可以使用PyRun_SimpleString
    • 如果是脚本源文件,需要先编译再调用,也可以直接加载编译后的pyc使用PyEval_EvalCode调用
  • 了解以上两个信息后,打开OD调试,在IDA中通过交叉引用找到GetProcessAddress后的函数地址的call调用处 


    对其下断点,运行发现断不下来!!!
    OD中ctrl+F2重新运行,发现了个奇怪的现象,之前的程序窗口没有被关闭,就又开了一个新进程。于是,关掉所有crackme, 打开任务管理器,重新运行cm,看到出现了两个进程

  • 在IDA导入表中查找CrateProcess, 可以看到程序使用的是CreateProcessW, OD中使用bp CreateProcessW对其下断运行

    0022AE1C 00404FB7 /CALL 到 CreateProcessW 来自 CrackMe_.00404FB1
    0022AE20 0022AEC0 |ModuleFileName = "C:\Documents and Settings\Administrator\桌面\CrackMe_CTF_Demo.exe"
    0022AE24 00020644 |CommandLine = ""C:\Documents and Settings\Administrator\桌面\CrackMe_CTF_Demo.exe""
    0022AE28 0022AE60 |pProcessSecurity = 0022AE60
    0022AE2C 00000000 |pThreadSecurity = NULL
    0022AE30 00000001 |InheritHandles = TRUE
    0022AE34 00000000 |CreationFlags = 0
    0022AE38 00000000 |pEnvironment = NULL
    0022AE3C 00000000 |CurrentDir = NULL
    0022AE40 0022AE7C |pStartupInfo = 0022AE7C
    0022AE44 0022AE6C \pProcessInfo = 0022AE6C

      这里发现没有额外的命令行参数,并且在IDA中可以看到,CreateProcess之后,进程1就在WaitForSingleObject了,所以python相关的代码应该全部在进程2中执行,那么怎么调试进程2呢?     

  由于进程1没有使用调试模式来创建进程2,所以在进程2被创建成功后,使用od附加是可以成功附加的,但是这个时候附加已经过了python脚本的加载时机了,需要更早一些才行,所以我这里采用修改CC来触发异常给调试器接管。 

  考虑到GetProcessAddress之用后这些函数才正式开始使用,所以我找的是执行了GetProcessAddress函数之后的sub_403E10函数,这个函数里可以看到有Py_SetProgramName,Py_Initialize等初始化设置的函数,所以我选择在sub_403E10处修改0x56为0xCC     

  修改完成后直接运行CM,触发CC直接断在进程2的0x403E10处要设置OD为默认的实时调试器

00403E10 $ CC              int3
00403E11 . 53              push ebx
00403E12 . 83EC 14         sub esp,0x14
00403E15 . A1 E4D54100     mov eax,dword ptr ds:[0x41D5E4]
00403E1A . 8B5C24 20       mov ebx,dword ptr ss:[esp+0x20] ; CrackMe_.00402973
00403E1E . C74424 08 001>  mov dword ptr ss:[esp+0x8],0x1000
00403E26 . 85C0            test eax,eax
00403E28 . 8D43 68         lea eax,dword ptr ds:[ebx+0x68]
00403E2B . 894424 04       mov dword ptr ss:[esp+0x4],eax
00403E2F . 74 22           je short CrackMe_.00403E53

       单步跟看到这样一个路径0022CE90 00416140 UNICODE "C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\_MEI33682\base_"找过去看了下,是一些python的运行环境,并没有关键的东西,继续跟代码。

00402430 . 8BB424 401000>  mov esi,dword ptr ss:[esp+0x1040]
00402437 . 8B5E 08         mov ebx,dword ptr ds:[esi+0x8]
0040243A . C70424 6FC240>  mov dword ptr ss:[esp],CrackMe_.0040C26F ; ASCII "__main__"
00402441 . FF15 80104100   call dword ptr ds:[0x411080] ; python34.PyImport_AddModule
00402447 . 85C0            test eax,eax
00402449 . 894424 14       mov dword ptr ss:[esp+0x14],eax

  这里添加了__main__,看来下面就快要到调用__main__函数的地方了, 继续跟代码, 就在下方不远处有一个小循环

00402480 > /895C24 04      mov dword ptr ss:[esp+0x4],ebx
00402484 . |893424         mov dword ptr ss:[esp],esi
00402487 . |E8 44F1FFFF    call CrackMe_.004015D0
0040248C . |3946 0C        cmp dword ptr ds:[esi+0xC],eax
0040248F . |89C3           mov ebx,eax
00402491 . |0F86 09010000  jbe CrackMe_.004025A0
00402497 > |807B 11 73     cmp byte ptr ds:[ebx+0x11],0x73
0040249B .^ 75 E3          jnz short CrackMe_.00402480

  这个一直在查找[ebx+0x11]处等于0x73的结果,查看EBX对应的地址发现有这样一个表

003E92B0 00 00 00 20 00 00 00 00 00 00 01 00 00 00 01 49 … ………I
003E92C0 01 6D 73 74 72 75 63 74 00 00 00 00 00 00 00 00 mstruct……..
003E92D0 00 00 00 30 00 00 01 00 00 00 04 79 00 00 07 BF …0..…y..
003E92E0 01 6D 70 79 69 6D 6F 64 30 31 5F 6F 73 5F 70 61 mpyimod01_os_pa
003E92F0 74 68 00 00 00 00 00 00 00 00 00 00 00 00 00 00 th…………..
003E9300 00 00 00 30 00 00 05 79 00 00 10 A7 00 00 25 27 …0..y..?.%'
003E9310 01 6D 70 79 69 6D 6F 64 30 32 5F 61 72 63 68 69 mpyimod02_archi
003E9320 76 65 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ve…………..
003E9330 00 00 00 30 00 00 16 20 00 00 17 C1 00 00 3D 27 …0.. ..?.='
003E9340 01 6D 70 79 69 6D 6F 64 30 33 5F 69 6D 70 6F 72 mpyimod03_impor
003E9350 74 65 72 73 00 00 00 00 00 00 00 00 00 00 00 00 ters…………
003E9360 00 00 00 30 00 00 2D E1 00 00 06 75 00 00 0E AE …0..-?.u..
003E9370 01 73 70 79 69 62 6F 6F 74 30 31 5F 62 6F 6F 74 spyiboot01_boot
003E9380 73 74 72 61 70 00 00 00 00 00 00 00 00 00 00 00 strap………..
003E9390 00 00 00 20 00 00 34 56 00 00 04 29 00 00 07 2C … ..4V..)..,
003E93A0 01 73 4B 45 52 4E 45 4C 33 32 2E 64 6C 6C 00 00 sKERNEL32.dll..
003E93B0 00 00 00 30 00 00 38 7F 00 00 01 83 00 00 02 C0 …0..8..?.
003E93C0 01 62 4B 45 52 4E 45 4C 33 32 2E 64 6C 6C 2E 65 bKERNEL32.dll.e
003E93D0 78 65 2E 6D 61 6E 69 66 65 73 74 00 00 00 00 00 xe.manifest…..

  简单推测可知道开头4个字节为每一项的长度,0x11偏移处为字符串起始,0x73是s, 以s开头的有spyiboot01_bootstrap sKERNEL32.dll 

  找到一个后就去打开自身,从自身0x1F000 + offset处读取数据,解压数据,执行。最终分析的这个表的结构是|长度|偏移|大小|解压后大小|名字| 解压后的数据就是pyc,只不过被抹去了前12字节,抹去的前12字节主要的是前4字节Magic Number     
   
  打开自身

00402FA6 |. 897424 04       mov dword ptr ss:[esp+0x4],esi ; |mode = "rb"
00402FAA |. 891C24          mov dword ptr ss:[esp],ebx ;     |path = "C:\Documents and Settings\Administrator\桌面\CrackMe_CTF_Demo.exe"
00402FAD |. FF15 20E34100   call dword ptr ds:[<&msvcrt._wfopen>] ; \_wfopen

  读取padding信息(数据为压缩过的)

00401645 . 897C24 04          mov dword ptr ss:[esp+0x4],edi ; |offset = 21DE1 (138721.)
00401649 . C74424 08 00000000 mov dword ptr ss:[esp+0x8],0x0 ; |whence = SEEK_SET
00401651 . 8B03               mov eax,dword ptr ds:[ebx] ;     |
00401653 . 890424             mov dword ptr ss:[esp],eax ;     |stream = msvcrt.77C2FCE0
00401656 . E8 2D950000        call <jmp.&msvcrt.fseek> ;       \fseek
……
……
0040168D . C74424 08 01000000 mov dword ptr ss:[esp+0x8],0x1 ; |n = 0x1
00401695 . 894424 04          mov dword ptr ss:[esp+0x4],eax ; |size = 675 (1653.)
00401699 . 893C24             mov dword ptr ss:[esp],edi ;     |ptr = 003E82A8
0040169C . 895424 0C          mov dword ptr ss:[esp+0xC],edx ; |stream = msvcrt.77C2FCE0
004016A0 . E8 F3940000        call <jmp.&msvcrt.fread> ;       \fread

  解压使用的是zlib库,版本1.2.8,可根据代码中的错误提示字符串搜索得到。 

  C代码的执行的逻辑到这就结束了,因为程序运行起来看到的界面都是在python中执行的,后面就说下怎么获取并反编译pyc     

  • 前面说到了一个表,这个表在原始的cm文件的末尾,可以直接拷贝出来单独保存为dataInfo,然后用下面的脚本解出所有的pyc   
    #-*- coding:utf-8 -*-
    import struct
    import zlib
    if __name__ == "__main__":
        fp = open('dataInfo', 'rb')
        while True:
            data = fp.read(16);
            if data == '':
                break;
            blockSize, dataOffset, dataLen, unknow = struct.unpack('>IIII', data)
            print 'blockSize:\t0x%08x\ndataOffset:\tx%08x\ndataLen:\t0x%08x\nunknow: 0x%08x' % (blockSize, dataOffset, dataLen, unknow)
            fp.read(1)
            dataName = fp.read(blockSize - 17)
            name = dataName.strip('\x00').replace('\\','_')
            print 'dataName: %s\n' % dataName
            if dataLen == 0:
                continue
            fout = open(name, 'wb')
            fexe = open('CrackMe_CTF_Demo.exe', 'rb')
            fexe.seek(0x1F000 + dataOffset)
            fout.write(fexe.read(dataLen))
            fout.close()
            fexe.close()
            if dataLen > 0x11f000:
                continue
            decompress(name, name + '.pyc')
      解压出来的pyc中主要的是sKERNEL32.dll.pyc 下载 Easy Python Decompiler v1.3.2 参考这篇http://o1o1o1o1o.blogspot.hk/2016/11/python-pyinstaller-reverse-engineer.html 中的说法

下面這包是用 python 3.4,一樣可以查表 
或是利用 archive_viewer.py 顯示前幾包的內容,開頭的 4 bytes 就是 magic number 
'EE 0C 0D 0A'    

  在sKERNEL32.dll.pyc最前面添加 EE 0C 0D 0A XX XX XX XX XX XX XX XX 十二个字节,注意是添加不是修改 我一开始是修改了开头的12字节,结果导致Easy Python Decompiler无法还原,后来在这个帖子里 
https://stackoverflow.com/questions/21067313/how-convert-and-save-python-module-from-pyobject-as-binary-data-to-use-it-lat 看到一段代码

codeobj = PyMarshal_ReadObjectFromString(python_code+8, size-8);

这里减8 是因为是python2.7 只有前8字节是magic value, 而3.4是前12字节 

补上12字节的magic value后直接拖到Easy Python Decompiler就能看到python代码了,如下

# File: s (Python 3.4)
import os
import hashlib
def z1(z2):
    for n in range(0, len(z2)):
        if ord(z2[n]) not in range(48, 58) and ord(z2[n]) not in range(97, 103):
            return 0
    return 1
def b(e2, c_2):
    g = ''
    for f2 in range(0, len(e2)):
        g += chr(e2[f2] ^ c_2 ^ f2 + 1)
    return g
m1 = [244,208,209,214,215,212,213,218,219,216,217,
        222,223,209,179,157,143,142,135,134,143,201,
        197,202,203,200,201,206,207,204,205,242,243,240]
        
m2 = [244,169,148,158,218,159,148,150,145,213,157,128,
        210,136,159,154,156,205,143,132,152,155,141,132,
        146,197,148,130,145,146,151,176,172,185,214]
        
m3 = [142,145,153,154,137,156,216,158,152,133,129,
        135,210,129,145,156,157,154,131,153,142,211,200]
        
m4 = [250,161,151,146,157,132,130,139,153,143,147,146,146,
        223,141,148,131,128,135,150,151,129,147,133,201,225]
        
m5 = [ 219,223,216,137,217,209,140,129,209,131,129,215,214,133,130,198,
        152,196,158,205,204,203,201,145,199,193,193,198,194,148,197,250]
print(b(m1, 255))
print(b(m2, 255))
while None:
    try:
        z4 = input(b(m3, 255))                        #获取输入
        if len(z4) == 10 and z1(z4) == 1:             #检查输入
            z3 = str(int(z4, 16))                     #16进制转10进制字符串
            if hashlib.md5(z3.encode('utf-8')).hexdigest() == b(m5, 239):    #求md5验证
                print(b(m4, int(z4[8:10], 16)))
                break
    continue     
    continue
    continue
    return None
    
#调整一下while使他能直接运行
#while True:
#    z4 = input(b(m3, 255))                        #获取输入
#    if len(z4) == 10 and z1(z4) == 1:             #检查输入
#        z3 = str(int(z4, 16))                     #16进制转10进制字符串
#        if hashlib.md5(z3.encode('utf-8')).hexdigest() == b(m5, 239):    #求md5验证
#            print(b(m4, int(z4[8:10], 16)))
#            break
#    continue

  代码看起来有些问题,先注释掉while后面的用python运行下看看,注意运行环境是python3.x

———— Crackme ————
The flag is your correct password
Press any key to continue …

  看到了这个界面,看来这就是验证的地方,仔细阅读下while循环中的代码,得知z4是输入且长度为10,z1用来验证输入的有效性, 

可知key的长度为10且由0-9,a-f组成,str(int(z4, 16))用来把输入的16进制串转为10进制串。   

  关键的判断点是if hashlib.md5(z3.encode('utf-8')).hexdigest() == b(m5, 239)     

  所以算法就是判断输入的值转10进制字符串后的md5是否与预设值相等,将b(m5, 239)放在前面直接print得到md5为524b38df7fe44db9f9b6621f14550e55

    但是这个md5值与实际执行时的并不一样。


      代码中其实还有smc, 最终运行的python脚本与文件里保存的有一点区别,修改代码的函数是sub_407CC0

00401773 . 892C24          mov dword ptr ss:[esp],ebp
00401776 . E8 45650000     call CrackMe_.00407CC0

   这里有个有趣的事情,下面这段代码如果单步运行,那么最后不会修改python代码,直接走free,如果在这段代码之后下断点直接run过去,就会多走修改python代码的处理,经过几次单步感觉是pop ss影响了push fd的执行,noppush sspop ss就能单步跟到smc处

0040AD45 > 60        pushad
0040AD46 . 16        push ss
0040AD47 . EB 01     jmp short CrackMe_.0040AD4A
0040AD49   18 db 18
0040AD4A > 17        pop ss
0040AD4B . 9C        pushfd
0040AD4C . EB 01     jmp short CrackMe_.0040AD4F

      smc代码

0040ADA2 . A1 F0BF4000   mov eax,dword ptr ds:[0x40BFF0]      ;读取解压后的代码长度
0040ADA7 . EB 01         jmp short CrackMe_.0040ADAA
0040ADA9 05 db 05
0040ADAA > 3D 2C070000   cmp eax,0x72C                        ;比较代码长度是否为0x72C
0040ADAF . EB 01         jmp short CrackMe_.0040ADB2
0040ADB1 0F db 0F
0040ADB2 >^ 75 D3        jnz short CrackMe_.0040AD87          ;不相等不修改,跳走
0040ADB4 . EB 01         jmp short CrackMe_.0040ADB7          ;相等准备修改
0040ADB6 1D db 1D
0040ADB7 > A1 F4BF4000   mov eax,dword ptr ds:[0x40BFF4]      ;获取解压后代码起始位置
0040ADBC . EB 01         jmp short CrackMe_.0040ADBF
0040ADBE C7 db C7
0040ADBF > 05 C5010000   add eax,0x1C5                        ;偏移0x1c5
0040ADC4 . EB 01         jmp short CrackMe_.0040ADC7
0040ADC6 C8 db C8
0040ADC7 > FE08          dec byte ptr ds:[eax]                ;减去1
0040ADC9 . EB 01         jmp short CrackMe_.0040ADCC

       所以最终修改后的代码应该在0x1c5处比原始的小 1 ,用比较软件打开内存dump的代码和文件中解压出来的代码可以看到确实只有此处是不同的 


       最终运行的脚本m5数组应该是

m5 = [218,223,216,137,217,209,140,129,209,131,129,215,214,133,130,198,
152,196,158,205,204,203,201,145,199,193,193,198,194,148,197,250]

#print b(m5, 239) 的结果是424b38df7fe44db9f9b6621f14550e5

       cmd5查到这个md5 424b38df7fe44db9f9b6621f14550e55 对应的数为597480335089 转为十六进制小写就是最终的key:8b1c9a66f1

       感谢 风间仁 大神指出错误!


      > 附件里的文档格式应该会好一些...
  



[招聘]欢迎市场人员加入看雪学院团队!

上传的附件:
打赏 + 5.00
打赏次数 1 金额 + 5.00
收起 
赞赏  CCkicker   +5.00 2017/07/20
最新回复 (8)
NearJMP 5 2017-7-18 19:45
2
0
代码好像没显示出来...
爱琴海 13 2017-7-18 19:54
3
0
NearJMP 5 2017-7-18 20:04
4
0
爱琴海 [em_63]
膜拜大神
风间仁 19 2017-7-18 21:26
5
0
应该是424b38df7fe44db9f9b6621f14550e55啊,  这个dump结果应该是修改之前的
kanxue 8 2017-7-18 22:17
6
0
爱琴海 13 2017-7-19 06:24
7
0
风间仁 应该是424b38df7fe44db9f9b6621f14550e55啊, 这个dump结果应该是修改之前的
NearJMP 5 2017-7-19 09:18
8
0
风间仁 应该是424b38df7fe44db9f9b6621f14550e55啊, 这个dump结果应该是修改之前的
风大神说的没错,  有smc...我是因为cmd5上两个md5给的结果是一样的,导致我没被smc坑到  ,找了下修改代码的位置,等下修改下文章
爱琴海 13 2017-7-19 23:46
9
0
游客
登录 | 注册 方可回帖
返回