Silver

黑夜无论怎样悠长,
白昼总会到来。
——莎士比亚《麦克白》, from here

Flare-on Challenge 2017 部分题解

Last updated:Oct.22, 2017 CST 18:38:42

//已经在内部论坛发过一次了,再发一次,凑凑博客更新量。

Flareon是主要题目为逆向的一场CTF比赛,已经是第四届了,比赛时间一个月左右。知道今年的比赛开始的时候就是九月底了,真正做题基本就是在10月1日开始,然而比赛结束是在北京时间10月13日中午,而且我还要上班……一共十二道题,绝大多数题是在十一假期完成的,最后差一块题拿牌子,实在是时间不够。

虽然Flareon官方也发了题解,但好歹也是做了很久的题……总之还是发上来吧。

看了看他们的WP,最后一题的页数竟有四十多页,就算一天半天不吃不喝也没法做完,何况还要上班……所以还是明年再战吧。

日志和压缩包还在整理,有时间传。

使用的软件版本:

感想:

另外近日听说x部下了指示,漏洞细节不能公开。希望大家不要受影响,不要被一些无谓的、傻逼的、操蛋的行政命令束缚住自己进步的脚步。


ROT13

JS里将字符串ROT13之后做的比较。直接用bash就可以了:

alias rot13="tr '[A-Za-z]' '[N-ZA-Mn-za-m]'"
echo [email protected] | rot13
# [email protected]

Igniteme

程序启动后输入字符串XOR然后做对比。那么就用python解开:

vp = [0x0D, 0x26, 0x49, 0x45, 0x2A, 0x17, 0x78, 0x44, 0x2B, 0x6C, 0x5D, 0x5E, 0x45, 0x12, 0x2F, 0x17, 0x2B, 0x44, 0x6F, 0x6E, 0x56, 0x09, 0x5F, 0x45, 0x47, 0x73, 0x26, 0x0A, 0x0D, 0x13, 0x17, 0x48, 0x42, 0x01, 0x40, 0x4D, 0x0C, 0x02, 0x69, 0x04]
for i in range(len(vp)-2, -1, -1):
  vp[i] = vp[i] ^ vp[i+1]
vp = ''.join([ chr(i) for i in vp[:-1]])
# [email protected]

Greektome

输入被分成多组,每组20个字符,密钥验证算法如下:

# # Key judgement
# for &bytes in 0x40107c -> 0x40107c+0x79:
#     bytes ^= key
#     bytes += 0x22
# assert( 0xFB5E == sub_4011E6(0x40107C, 0x79) )

# # sub_4011E6(x,y)
# assert( y != 0)
# ebx = x
# eax = 0x14
# {
#     di = (int16)var_4
#     esi = min(eax, edx)
#     edx -= esi
#     {
#         eax = 
#     }
# }
# // CMOVx: Conditional move according to X
# v2: 0x79 -> 0x65 -> 0x51 -> 0x3D -> 0x29 -> 0x15 -> 0x01 -> 0

最简单的方法还是用python爆破:

def generator(a1,a2):
    v2 = a2
    v3 = 0xff
    v8 = 0xff
    if not a2:
        raise ValueError
    v4 = a1
    scanned = 0
    while v2:
        v5 = v8
        v6 = 0x14 if v2>0x14 else v2
        v2 = v2 - v6
        for i in range(v6):
            v5 = v5 + a1[scanned]
            v5 = v5 & 0xFFFF
            v3 = v3 + v5
            v3 = v3 & 0xFFFF
            scanned = scanned + 1
            #print("% 3d - % 3d"%(scanned, v6))
        v8 = (v5 >> 8) + (0xff&v5)
        v8 = v8 & 0xFFFF
        v3 = (v3 >> 8) + (0xff&v3)
        v3 = v3 & 0xFFFF
        print("=== % 4x = % 4x ==="%(v8,v3))
    return ( (v8 >> 8) + ((0xFF) & v8) ) | ( 0xFFFF & ( (v3 << 8) + ((0xFF00) & v3) ) )

def check(xk):
    borg = """
    33 E1 C4 99 
    11 06 81 16 F0 32 9F C4  91 17 06 81 14 F0 06 81 
    15 F1 C4 91 1A 06 81 1B  E2 06 81 18 F2 06 81 19 
    F1 06 81 1E F0 C4 99 1F  C4 91 1C 06 81 1D E6 06 
    81 62 EF 06 81 63 F2 06  81 60 E3 C4 99 61 06 81 
    66 BC 06 81 67 E6 06 81  64 E8 06 81 65 9D 06 81 
    6A F2 C4 99 6B 06 81 68  A9 06 81 69 EF 06 81 6E 
    EE 06 81 6F AE 06 81 6C  E3 06 81 6D EF 06 81 72 
    E9 06 81 73 7C """
    bbs = [(0xff&((i^xk)+0x22)) for i in list(bytes.fromhex(borg.replace("\n", "")))]
    return generator(bbs, 0x79)
j = 0
for i in range(0xff):
    if ( check(i) == 0xFB5E ):
        j=i
        break

# j = 0xa2
# Now it is time to check bytes
for i in range(0x79):
    ida_bytes.patch_byte(0x40107c+i, ida_bytes.get_byte(0x40107c+i)^j)
# [email protected]

看来和出题人是一个想法。提交于October 2nd, 8:33:33 PM CST

Notepad

一道侧重于样本分析的程序。源程序是Windows的记事本,被在另一个段注入了新代码,并修改了OEP。新代码会使用ROR13获取所有加载模块的哈希,并按照如下算法比较并存储之:

for module in PEB->LDRDATA->InMemoryOrderModuleList
    if HashROR13(module.name)==HashROR13(TargetModuleName):
        return module # for futher usage ;)

注意的是,在我的Windows 10中,这段程序会出问题,因为由于一些未知的原因,这段代码在我机器上获取的模块名称为大写,而ROR13的实现是区分大小写的。我们在0x10153F6下断点,断点日志为Parsing {s:edx},查看日志:

Parsing L"4_notepad.exe"
Parsing L"ntdll.DLL"
Parsing L"KERNEL32.DLL"
Parsing L"KERNELBASE.dll"
Parsing L"comdlg32.dll"
...

可以发现KERNEL32被跳过了。patch掉此处,便于动态调试。

对于被调用的函数,我们可以用Flare-IDA中的Shellcode-hashes脚本来辅助分析。静态分析后发现,程序在遍历%USERPROFILE%/flareon2016challenge目录下的文件,检查其时间是否符合特征值。如果符合,就提取出其中的八个字节到key.bin中,以此类推。Swings师傅提醒后,发现正确的文件是从去年的题目包中下载的(P.S 我觉得这个题设计的一般)。释放文件后再次执行程序,会弹出对话框:

---------------------------

---------------------------
[email protected]
---------------------------
OK   
---------------------------

提交于October 3rd, 3:05:00 PM CST

Pewpewboat

这次是个小游戏,有100关,每一关的初始化结构体都是用同一个函数解密不同内存地址得到的,因此flag应该在这里。简单分析了这个结构体,如下:

typedef struct {
    QWORD GoodStatus;   // being fliped at y-axis. just the correct answer
    QWORD DiskStatus;   // contains current status
    char  Checksum[8];  // 恶魔妹妹摸咪咪……
    DWORD ammo_amount;
    char LastInputUpperCase[2];
    char LevelName[15];
    something[17];
    char HelloMessage[69];
}

这里本来是想通过patch的方法,跳过验证过程,直接打出提示消息,以为这样就会在最后得到flag。等全部patch完才发现,还是要上调试器脚本的。考虑到你可能也想试一下,我附上了自己patch后的文件。

回到游戏流程。每轮游戏中,你的输入值除了会影响DiskStatus外,还会被原样放到上面结构体的LastInputUpperCase中。如果对应的点在GoodStatus中为True,那么就算击中。此外,每轮游戏中,checksum值都会变,用来解密下一轮游戏。

总之,你有两个绕过验证的方案:

  1. 手动写程序计算所有正确的输入向量
  2. 像我一样打patch然后从内存里提出来

我在patch后这样设了下断点:

$ gef pewpewboat.patched
gef > b *0x403ec6
gef > commands
> append binary memory /tmp/5_final_full.bin `$rax+0x00 $`rax+0x240
> c
> end
gef > r

然后在dump文件中用strings简单看了看,发现了这么一行:

PEWYouPEWfoundPEWsomePEWlettersPEWdidPEWya?PEWToPEWfindPEWwhatPEWyou'rePEWlookingPEWfor,PEWyou'llPEWwantPEWtoPEWre-orderPEWthem:PEW9,PEW1,PEW2,PEW7,PEW3,PEW5,PEW6,PEW5,PEW8,PEW0,PEW2,PEW3,PEW5,PEW6,PEW1,PEW4.PEWNextPEWyouPEWletPEW13PEWROTPEWinPEWthePEWsea!PEWTHEPEWFINALPEWSECRETPEWCANPEWBEPEWFOUNDPEWWITHPEWONLYPEWTHEPEWUPPERPEWCASE.

把所有的PEW换成空格,会发现他想让你按照他给的数字,将对应局游戏答案对应的字符做ROT13。做完之后我得到了:

butwhereistherum

还是一头雾水。后来在师傅启发下,又看了看源码,发现输入的时候有一段代码忘了看。索性直接输进去:

[email protected]

提交于October 4th, 1:40:36 AM CST

Payload

因为很久没碰过Windows了,所以在读MSDN和逆向rundll32以及ntdll上花的时间比解题时间还长。

从几个字符串中可以看出,这个DLL正确的调用姿势是%WINDOWS%/system32/rundll32.exe payload.dll #1。DLL在rundll32.exe!InitCommandInfo+78处被加载,然后在rundll32.exe!wWinMain+2A2处查找并调用了导出函数。此外,DLL也可以有一些初始化函数,这些函数由 payload.dll!_initterm调用,地址存放在payload.dll!0x10250,在读取导出表前执行。显然是MSVC的作风。

总之,我目前知道第一个函数是sub_180004400sub_180005A50。他们会在DLL加载后即刻执行,并用这样的一组key来初始化导出表:

(ctime.year + ctime.monty) % 26

解密后,你可以看到这个DLL原始文件名为lustrated.dll,导出函数是orphanedirreproducibleconfidences。分析这个导出函数,发现他会检查输入参数是否符合导出函数名,如果符合就弹出一个这样的对话框:

---------------------------
key part
---------------------------
key[25] = 0x6d;
---------------------------
OK   
---------------------------

emmmmmm……于是我写了个编译器脚本,key从0到25循环,然后得到了这么一批日志:

ENT >>> "filingmeteorsgeminately" <<< ENT
ENT >>> "leggykickedflutters" <<< ENT
ENT >>> "incalculabilitycombustionsolvency" <<< ENT
ENT >>> "crappingrewardsanctity" <<< ENT
ENT >>> "evolvablepollutantgavial" <<< ENT
ENT >>> "ammoniatesignifiesshampoo" <<< ENT
ENT >>> "majesticallyunmarredcoagulate" <<< ENT
ENT >>> "roommatedecapitateavoider" <<< ENT
ENT >>> "fiendishlylicentiouslycolouristic" <<< ENT
ENT >>> "sororityfoxyboatbill" <<< ENT
ENT >>> "dissimilitudeaggregativewracks" <<< ENT
ENT >>> "allophoneobservesbashfulness" <<< ENT
ENT >>> "incuriousfatherlinessmisanthropically" <<< ENT
ENT >>> "screensassonantprofessionalisms" <<< ENT
ENT >>> "religionistmightplaythings" <<< ENT
ENT >>> "airglowexactlyviscount" <<< ENT
ENT >>> "thonggeotropicermines" <<< ENT
ENT >>> "gladdingcocottekilotons" <<< ENT
ENT >>> "diagrammaticallyhotfootsid" <<< ENT
ENT >>> "corkerlettermenheraldically" <<< ENT
ENT >>> "ulnacontemptuouscaps" <<< ENT
ENT >>> "impureinternationalisedlaureates" <<< ENT
ENT >>> "anarchisticbuttonedexhibitionistic" <<< ENT
ENT >>> "tantalitemimicryslatted" <<< ENT
ENT >>> "basophileslapsscrapping" <<< ENT
ENT >>> "orphanedirreproducibleconfidences" <<< ENT

再用另一个脚本读取这些参数并执行,最后拼成一组flag:[email protected]

提交于October 5th, 10:49:44 PM CST

zsud

这个程序很麻烦,花了差不多两天。

挂上调试器后,就注意到了一个操蛋的特征:

.text:004036A0 sub_4036A0      proc near               ; CODE XREF: [email protected]_0+2↓p
.text:004036A0                 jmp     sub_403630
.text:004036A0 sub_4036A0      endp
...
.text:00403630                 jmp     sub_4035C0
...
.text:004035C0                 jmp     sub_403550
...

每个函数调用前部都是连续的128个jmp。调试器还比较好办,用trace功能,追踪到第一个不是jmp的指令即可:

Break Condition:   byte(eip) != e9
Log Text:          Jumping over 0x{p:eip}
Log Condition:     byte(eip) == e9

IDA也还好。因为提供了很完整的函数信息,我们用下面的脚本可以做更精确的判断。如果这串脚本你看不懂,可以先去了解下IDA的ctree结构,这个在二次开发和复杂程序处理中比较重要。

def log(msg):
    print("[LOG-FixChain]%s"%msg)

def get_call_chain(ea=None, chainmax=100000):
    if not ea:  # use current cursor address
        ea = ida_kernwin.get_screen_ea()
    #ea = ida_func.get_func(ea)
    jmp_chain = [ea]
    while chainmax: # Search until found end or meet chianmax
        current = jmp_chain[-1]
        log("Processing %08x"%current)
        # Get ctree
        cfunc = ida_hexrays.decompile(current)
        # Will not work if we do not output a string
        s = str(cfunc)
        if not cfunc:  # We have encountered something unusual. Get back and jump to normal end
            jmp_chain.pop()
            log("Unusual call at %08x. Quit for safe."%(jmp_chain[-1]))
            jmp_chain.append(0)
            break
        if cfunc.entry_ea != current: 
            #reprocess immidately if this is not an entry
            log("Redirected to %08x"%(cfunc.entry_ea))
            jmp_chain[-1] = cfunc.entry_ea
            continue
        targets = 0
        target_ea = 0
        # Check all items
        # Is a jump slot if only one call found
        # Or we should stop here
        for citem in cfunc.treeitems:
            citem = citem.to_specific_type
            if citem.opname == "call":
                # Found a call. 
                targets += 1
                target_ea = citem.x.obj_ea
            if targets > 1:
                break
        if targets != 1:
            jmp_chain.append(0)
            break
        else:
            # Is a slot function. Add target to our list
            jmp_chain.append(target_ea)
        chainmax -= 1        
    return jmp_chain

def fix_call_chain(ea=None,chainmax=100000):
    chain = get_call_chain(ea,chainmax)
    if not chain[-1]==0:
        raise ValueError("Unexcepted chain end!")
    chain_target = chain[-2]
    log("Targeting to %08x"%(chain_target))
    # Get last function's def
    for i in range(len(chain)-2):
        ida_name.set_name(chain[i],"j_%s_id_%d"%(ida_name.get_name(chain_target), i), SN_CHECK)
        # Also try to modify its arguments
        p = ida_typeinf.tinfo_t()
        if ida_nalt.get_tinfo(p, chain_target) and ida_nalt.set_tinfo(chain[i], p):
            pass

工具准备好后,分析也简单了一些。程序启动了两个线程,一个是HTTP服务器,另一个是主线程。

主线程会解密402490的一长串字符串,你可以在解密后到eax找到这串大小为[esp+44h]的解密结果。这个结果是一些base64字符串,解码之后的东西同样是一团糟。

因此就看看接下来对这串字符串做了什么。这个程序用的一个一个技术叫.Net CLR Hosting,也就是说你可以调用一个垫片库mscoree.dll,在你自己的程序中启动一个.Net CLR环境,并执行CSharp代码。跟进去发现,启动这个环境时使用的DLL是一个不存在于磁盘上的文件。有经验的应该了解了,这里首先用MapViewOfFileSub, CreateFileMappingA/W之类的东西,把文件映射到内存中,然后去访问他。然而看到这块的时候并不知道怎么将一个Map好的文件Unmap回来,所以就很惨。

最后在漫长的逆向后,发现了402780的一个函数,这个函数应该是实现了一个PE Loader,会把一块内存当作一个DLL文件,映射到内存中。找到这里就好办了,在第一次映射之前下断点,直接从Memory Dump中拿到文件,也就是压缩包中的zsud_3c000_striped.dll

这个文件就是刚才提到的在CLR Hosting中跑的.Net程序。用dnSpy可以看到,程序用soooooo_sorry_zis_is_not_ze_flag作为密钥,用全0的IV,使用AES256解密了一组字符串,并将其加载到PowerShell中执行。因为不知道MS在加密这里有没有用什么骚操作,我就用了最原始的办法:直接把dnSpy的输出修改后重新编译一下。解密结果保存为一个ps1文件。

这个powershell文件就是整个MUD的核心。源码细节就不说了,总之你要拿到key的话,涉及到这么几个隐藏环节:

  1. 抽屉里有把钥匙,可以拿
  2. 拿钥匙后的所有步骤会被发给服务器,钥匙的内容也会发过去
  3. 钥匙内容满足一定条件,就会被传送到前门
  4. 在游戏(而不是Twitter)中找Kevin Mandia,带上他的头盔,扔下key,他会吐一串神器的Hex String

估计按这几步走,就能拿到flag。拿钥匙后步骤被发送到服务器的条件是directions_enum[step] == msvcrt.rand(),但是这个rand不知道是被重定义了还是怎样,我自己写程序得到的结果对不上。因此索性在CLR虚拟机中直接下断点,取rand的返回值并打印,然后一顿骚操作,得到了这么一组日志:

EAX ======== w
EAX ======== n
EAX ======== n
EAX ======== e
EAX ======== e
EAX ======== s
EAX ======== s
...

太长,就不都列出来了。然后重启程序,按照这些步走一遍,被发配到大门口,然后看看key:

> look key
You can start to make out some words but you need to follow the [email protected]

这个就符合第3步的要求:钥匙文本中有个at符号。看看at后面是什么:

In [4]: b=bytes.fromhex("66696e646b6576696e6d616e6469610d0a")

In [6]: b
Out[6]: b'findkevinmandia\r\n'

去右上角的房间找他,并按照第四步操作:

> say Kevin Mandia

Kevin says, with a nod and a wink: '6D 75 64 64 31 6E 67 5F 62 79 5F 79 30 75 72 35 33 6C 70 68 40 66 6C 61 72 65 2D 6F 6E 2E 63 6F 6D'.

Bet you didn't know he could speak hexadecimal! :-)

emmmm,我不知道他会说十六进制,但我知道python会:

In [7]: c=bytes.fromhex("6D 75 64 64 31 6E 67 5F 62 79 5F 79 30 75 72 35 33 6C 70 68 40 66 6C 61 72 65 2D 6F 6E 2E 63 6F 6D")

In [8]: c
Out[8]: b'[email protected]'

提交于October 7th, 2:53:36 PM CST

Flair

我一点Android也不懂,Java也不是太懂,但现代魔法的好处就在于,可以快速用JEB掌握 ;)

程序有四步,先来看第一步Michael。没有任何混淆,直接按照要求解出字符串MYPRSHE__FTW。注意,Java中hashCode算法描述里的^符号是数学中的意义,而不是C中的意义。

第二步Brain会读一些资源。用apktool解包后按照要求查找并填入格式化字符串,就得到了hashtag_covfefe_Fajitas!。注意中间的fefe是小写。

第三步Milton用了Stapler类中的一些方法,我写了组Python脚本,可以方便工作:

import base64
# from https://code.activestate.com/recipes/576736-rc4-arc4-arcfour-algorithm/
def rc4crypt(data, key):
    x = 0
    box = list(range(256))
    for i in range(256):
        x = (x + box[i] + (key[i % len(key)])) % 256
        box[i], box[x] = box[x], box[i]
    x = 0
    y = 0
    out = []
    for char in data:
        x = (x + 1) % 256
        y = (y + box[x]) % 256
        box[x], box[y] = box[y], box[x]
        out.append(((char) ^ box[(box[x] + box[y]) % 256]))    
    return bytes(out)

# neapucx is just hexstring to bytes, we will not use it yet
# poserw  is SHA1 digest. Just use python lib 
drdfg = lambda x,y:bytes(i^y for i in base64.b64decode(x))
vutfs = lambda x,y,z:rc4crypt(drdfg(x,y),z)


v0 = " !\"#$%&\'()*+,-./0123456789:;<=>[email protected][\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
v3 = vutfs("Zcj30d9jroqAN2mtzayVK0awaVTLnoXVcsjl9ujUAd22JiI9xfhqEW1BbkG3LsgQRoStjh+Eb6wTD4BwD9Kypa5ggXfHLWmFjFgERViV+5IRU4RbUPDUwYTivhsdZnA=", 17, b"for").decode()

iemm = lambda x:''.join([v0[v3.index(x[i])] for i in range(len(x))])

现在来看这一步。首先要给APP打四星,然后才能点按钮。你输入的值应该是下面这个字符串的SHA-1:

j = [
    ("JP+98sTB4Zt6q8g=", 56, "State"),
    ("rh6HkuflHmw5Rw==", 96, "Chile"),
    ("+BNtTP/6", 118, "eagle"),
    ("oLLoI7X/jIp2+w==", 33, "wind"),
    ("w/MCnPD68xfjSCE=", 148, "river")]
print(''.join([vutfs(e[0],e[1],bytes(e[2], "ascii")).decode() for e in j]))

字符串是 A rich man is nothing but a poor man with money.,SHA1是10aea594831e0b42b956c578ef9a6d44ee39938d

第四步就麻烦一点,这个程序会从资源文件tvps中按照这个格式读数据:

struct {
    short Index;
    byte  Value;
} tvps[112];

数据会被加载到Hashmap中并按照Index排序,然后拼接Value。时间仓促,写了段脚本:

fpr = open("tspe", "rb").read()
q = int(len(fpr)/3)
a = [0 for i in range(0xFFFF)]

for i in range(q):
    try:
        key = (fpr[ i*3 + 0 ]<<8) + (fpr[ i*3+1 ] << 0)
        a[key]=fpr[i*3+2]
    except IndexError:
        print("Overflow with key = %d and i = %d"%(key,i))
        break
print(''.join([chr(i) for i in a[:112]]))

字符串是Give a man a fire and he'll be warm for a day. Set a man on fire and he'll be warm for the rest of his life.,SHA1是5f1be3c9b081c40ddfc4a0238156008ee71e24a4

四步结束后,得到flag[email protected]

提交于October 7th, 10:14:55 PM, CST

remorse

文件名后缀是ino.hex,还有字符串Arduino UNO,应该要在板子上跑的。可惜我没有钱,所以就手动分析吧。

对于新的CPU,首先要掌握的有:

写了一些代码用于辅助IDA和GDB调试(是的,有模拟器,不过我不会操控IO),此外IDA自带的Auto Commends功能也帮了不少忙。之后的事情绝大多数时间就是逆向,逆向和逆向,还会通过avr-gdb和avr模拟器认识一些常见的avr执行特征。最后在0536发现了一组函数,好像在加载什么东西,并用一个密钥异或,而这个密钥的来源和某个计时器的输出结果有关。索性爆破:

p=[0xb5, 0xb5, 0x86, 0xb4, 0xf4, 0xb3, 0xf1, 0xb0, 0xb0, 0xf1, 0xed, 0x80, 0xbb, 0x8f, 0xbf, 0x8d, 0xc6, 0x85, 0x87, 0xc0, 0x94, 0x81, 0x8c]
q=0

for q in range(0, 0x100):
  print('%x: %s'%(q, ''.join([chr((p[i]^q)+i) for i in range(0x17)])))

然后搜索flare-on,发现:

...
da: op^q2n1qr4Aembsf,po-bpl
db: [email protected]
dc: ij\k,t3st6;gs`q`*jm/\rf
...

提交于October 10th, 3:15:50 AM CST

shell

第一组:设密文c是大小为c[count][keylen]的数组,密钥为w,明文为p[count][keylen]。那么:

p[0] = c[0] ^ w
p[1] = c[1] ^ p[0] = c[1] ^ c[0] ^ w
p[2] = c[2] ^ p[1] = c[2] ^ c[1] ^ c[0] ^ w
...

不妨令k[i] = c[0] ^ c[1] ^ ... ^ c[n-1] ^ c[n],则对任意i<count,有:

p[i] = k[i] ^ w
且
p[i]全部为可显示字符

写脚本求解,可得到后面的,思路基本一致,都是爆破。

然而自己太胖,不能穿女装,码力不够,因此从Swings师傅那里偷了个flag,[email protected]

covfefe

这个程序是用虚拟机保护的。虚拟机设计思路和subfuscator有相似之初,不了解这个的可以去看看movfuscator,就是用减法替代一切操作。

首先分析一下401070的主函数,我们可以知道这块vCPU可以这样描述:

typedef enum {
    SYSCALL_GET_CHAR,
    SYSCALL_PUT_CHAR
} SYSCALL_ID;

typedef struct {
    uint32 r0;                       //gpr r0
    uint32 syscall_args[SYSCALL_ID];
    uint32 syscall_flag[SYSCALL_ID]; // 1 means being called, 0 means call finished
    char ROM_PART_A[0x134];
    char RAM_PART_A[0x150];
    uint32 sp;                       // point at first stack item and grow from lower to higher
    char ROM_PART_B[0xF04];          // an index number at 00403448 will be changed upon start
    char RAM_PART_B[OTHER_RAM_SIZE]; // all other contents
};

void sub_401070(
    VM*    vm_addr,
    uint32 vm_mem_size,
    uint32 vm_start_point
);

vCPU的指令长度是3字节(24bit,不是21bit,真不是clemency架构)。VMM也很好描述:

while True:
    mem[inst[1]] -= mem[inst[0]]
    if inst[2]: # conditional jump
        if mem[inst[1]] <= 0:
            jmp(inst[2])
            continue
    process_sys_call()
    cursor+=3

注意,这里我们还是在用32-bit的视角去看。然而vCPU是在24bit的世界的。所以下面我会用1420代表32bit内存中的偏移量,基地址是0x403008,而同样的地址在24bit下表示为@508。换算方法是:

0x508 * 4 + 0x403008 == 0x403008+0x1420 == 0x404428

vCPU只提供了sub和jmp,这样的话追踪和逆向麻烦很多。函数调用约定类似于__cdecl,对指针的解引用用的方法差不多是SMC。

现在我们知道了参数传递、指令集、寄存器、SFR、内存布局和取地址/数据方式,那么剩下的事情多多少少好办一点。

为了更详细的了解这个程序的运行过程,做几个断点:

# Code break points to monitor jumps
bp 0x4010af
bpcnd 0x4010af, "dword(esp+c) && (dword(dword(esp+8)+dword(esp+0)) - dword(dword(esp+4)+dword(esp+0))) <= 0 "
bplog 0x4010af, "jmp from {p:dword(ebp-8)} => {p:dword(ebp-8)*4+dword(esp+0)} to {p:dword(esp+c)} => {p:dword(esp+C)*4+dword(esp+0)}"
SetBreakpointCommand 0x4010af, "g;"

# Memory access br. Reading hint offsets and hits. This helps to find where is `puts`
Address=00403458
Summary=access(dword), log("READ HINT OFFSET"), cmd("g")
Address=00403710
Summary=access(byte), log("READ HINT"), cmd("g")

trace记录在压缩包中可以看到。分析后发现,这个程序涉及flag验证的虚指令长度大概为1k条左右。考虑到这种虚拟方式对编码的影响,实际映射到x86上顶多300条汇编(取地址膨胀率5:1,数值操作膨胀率3:1,跳转膨胀率2:1),折算到C语言没几行,因此加密算法应该极其简单。

多次尝试后(看我的trace记录就能看到这个多次尝试的过程),可以知道密钥算法是:

for a,b in input: # Two bytes per time
    assert( (a*0xF)<<7+b^0x7F == arr[cnt++] )  # cnt from 0, and arr is at @e9c with 16 long ints

直接写脚本:

In [104]: '%[email protected]'%(''.join(["%c%c"%(((i>>7)//0xF),(i&0x7F)^0x7F) for i in [220810, 188179, 193934, 182430, 211227, 182413, 193947, 224668, 222742, 213152, 186267, 182430, 188172, 224653, 192010, 209407]])[:-1])
Out[104]: '[email protected]'

提交于October 11th, 5:34:07 PM CST

Contact webmaster at:
[email protected]