whklhh 发表于 2017-12-11 01:02

【JarvisOJ】软件密码破解(3)

本帖最后由 whklhh 于 2017-12-11 23:52 编辑

这题就比较难了…
一样从JarvisOJ上看到的
对压缩包中的程序进行分析并获取flag。flag形式为16位大写md5。



题目来源:CFF2016
分享给星斗师傅结果他还抢先发了WP→ →
https://www.52pojie.cn/thread-673952-1-1.html

交流下不同的角度和思路吧-0-
我比较喜欢静态分析,IDA大法好~

查壳发现没有,直接打开,发现“确定”按钮是不可用状态
以为跟上次的一题一样是故意改的,于是使用SpyLite将按钮改为可用,结果马上就又变回去了,说明有代码在操作
那么就不是这种小花招了,去OD中断GetWindowText,发现“取消”按钮一直在不停地调用这个API来干扰
于是使用条件断点,设定句柄非“取消”按钮
成功断到sub_402060函数的调用

IDA中进行分析,可以看到是对长度进行了判断
http://img.blog.csdn.net/20171202230521070?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvd2hrbGhoaGg=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast
于是输入长度为16的校验码,果然“确定”按钮变得可用了

查看消息列表可以发现两个自定义的函数
http://img.blog.csdn.net/20171202230643237?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvd2hrbGhoaGg=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast
消息值Message一样,区别是消息特征code
前者就是刚才的402060,后者则是响应函数

反汇编可以发现对字符串571458进行了校验
http://img.blog.csdn.net/20171202230830094?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvd2hrbGhoaGg=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast
OD中查看内存可以发现它就是把输入直接转hex的结果,但是由于最后多了一个值而无法满足差值为8的条件

通过查找交叉引用发现了这个地方:
http://img.blog.csdn.net/20171203232348592?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvd2hrbGhoaGg=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast
OD跟踪也可以发现确实是这里在负责写入
里面有一些机制来判断数字范围,例如必须为十六进制大写数字,否则将直接return结束
该函数利用这个机制倒是可以写出长度为8的字符串,例如123456780xxxxxxx
但是此时确定按钮又不可用了,原因出在最后
return strlen(byte_571458) != 8
而这个函数的返回值被用在有效性的设置上
v5 = sub_4017C0(MultiByteStr) != 0;
    v6 = CWnd::GetDlgItem(v1, 1);
    CWnd::EnableWindow(v6, v5);
因此这就陷入了一个矛盾的境地–“确定”按钮可用需要长度非8,成功提示则需要长度为8
len和test cl, cl的原理是相同的,因此没有漏洞可以利用

从另一方面来考虑,由于FLAG的唯一性,所以自由组合的flag肯定是不可能的,那这里必然不是真正的解题点

找了一圈也没发现,于是去找WriteUp
http://www.mottoin.com/90073.html
这里给出了一些提示,但是太过简略所以还是挺困难的

首先存在很多反调,这个我发现了,但是由于没什么影响所以没太在意
另一方面关键函数是sub_401970,而我虽然在交叉引用中看到这个函数对关键区域有读写,但是却根本没有注意它

学着对其下断,整个运行过程中也没有调用
正当我自暴自弃地继续运行的时候,发现当程序走向错误路径时
http://img.blog.csdn.net/20171203233050274?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvd2hrbGhoaGg=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast

这里的内容是输入的第2、3个字符,所以jmp必然会引起错误而之前运行的时候从来没有异常结束的情况于是运行,发现程序在sub_401970的地方断下了!
说明这是个SEH(异常处理结构)于是在OD中查看SEH链
http://img.blog.csdn.net/20171203233356027?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvd2hrbGhoaGg=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast
果然其中有一些函数插入的程序,这点是我大意没有注意到

但是sub_401970的调用堆栈中也没有看到与SEH链中相重合的内容……

后来查了一波资料发现除了SEH结构化异常处理以外还有VEH,向量化异常处理
VEH有两个新增异常处理结构的API,分别是AddVectoredExceptionHandler和AddVectoredContinueHandler
前者的检查在SEH之前,后者则在SEH之后

而本题中使用的正好就是AddVectoredExceptionHandler

http://img.blog.csdn.net/20171204121138105?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvd2hrbGhoaGg=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast
函数过程位于sub_4024F0
并且作者还非常鸡贼的使用了动态获取API,动态解密字符串的方法,从而使得很难静态分析到这个函数调用……
另外使用的是ntdll中的核心API,而不是Kernel中的封装API-AddVectoredExceptionHandler
http://img.blog.csdn.net/20171204121233528?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvd2hrbGhoaGg=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast
太坑了……除了运行中检查VEH的内存结构以外,就只能对ntdll的API进行下断了
(搜了一圈没发现合适的插件来获取VEH链,以后有机会自己写个)

查找两个异常处理函数,前者直接退出
后者则进行了一个处理和校验
http://img.blog.csdn.net/20171204121902302?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvd2hrbGhoaGg=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast
流程应该是先进行sub_401B80,如果跳转到0x401947(成功分支)则执行对应代码,如果校验未通过则Return 0,系统将执行流转交给下一个异常处理函数sub_401CA0直接退出程序

于是先看校验结果,是个方程组,直接Z3求解就行
而401970中的内容是一个多次查表变换的函数
http://img.blog.csdn.net/20171204200335840?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvd2hrbGhoaGg=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast
仔细看看就可以发现一共循环了64次,每次循环中进行了4次循环查表,所以实际上就可以直接当做是循环了64*4次查表233
这个小坑意义不大

至于表的来源,通过查看表来搜索也行,密码学分析工具也行,可以发现它就是AES算法的S盒
那么求解只需要通过逆S盒(wiki上有)来查表就行了

求解算法过程:
首先通过方程求出解,然后将解查表64*4次即可,输出HEX

Python3:
from z3 import *

re_s_table = [0x52, 0x09, 0x6A, 0xD5, 0x30, 0x36, 0xA5, 0x38, 0xBF, 0x40, 0xA3, 0x9E, 0x81, 0xF3, 0xD7, 0xFB,
    0x7C, 0xE3, 0x39, 0x82, 0x9B, 0x2F, 0xFF, 0x87, 0x34, 0x8E, 0x43, 0x44, 0xC4, 0xDE, 0xE9, 0xCB,
    0x54, 0x7B, 0x94, 0x32, 0xA6, 0xC2, 0x23, 0x3D, 0xEE, 0x4C, 0x95, 0x0B, 0x42, 0xFA, 0xC3, 0x4E,
    0x08, 0x2E, 0xA1, 0x66, 0x28, 0xD9, 0x24, 0xB2, 0x76, 0x5B, 0xA2, 0x49, 0x6D, 0x8B, 0xD1, 0x25,
    0x72, 0xF8, 0xF6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xD4, 0xA4, 0x5C, 0xCC, 0x5D, 0x65, 0xB6, 0x92,
    0x6C, 0x70, 0x48, 0x50, 0xFD, 0xED, 0xB9, 0xDA, 0x5E, 0x15, 0x46, 0x57, 0xA7, 0x8D, 0x9D, 0x84,
    0x90, 0xD8, 0xAB, 0x00, 0x8C, 0xBC, 0xD3, 0x0A, 0xF7, 0xE4, 0x58, 0x05, 0xB8, 0xB3, 0x45, 0x06,
    0xD0, 0x2C, 0x1E, 0x8F, 0xCA, 0x3F, 0x0F, 0x02, 0xC1, 0xAF, 0xBD, 0x03, 0x01, 0x13, 0x8A, 0x6B,
    0x3A, 0x91, 0x11, 0x41, 0x4F, 0x67, 0xDC, 0xEA, 0x97, 0xF2, 0xCF, 0xCE, 0xF0, 0xB4, 0xE6, 0x73,
    0x96, 0xAC, 0x74, 0x22, 0xE7, 0xAD, 0x35, 0x85, 0xE2, 0xF9, 0x37, 0xE8, 0x1C, 0x75, 0xDF, 0x6E,
    0x47, 0xF1, 0x1A, 0x71, 0x1D, 0x29, 0xC5, 0x89, 0x6F, 0xB7, 0x62, 0x0E, 0xAA, 0x18, 0xBE, 0x1B,
    0xFC, 0x56, 0x3E, 0x4B, 0xC6, 0xD2, 0x79, 0x20, 0x9A, 0xDB, 0xC0, 0xFE, 0x78, 0xCD, 0x5A, 0xF4,
    0x1F, 0xDD, 0xA8, 0x33, 0x88, 0x07, 0xC7, 0x31, 0xB1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xEC, 0x5F,
    0x60, 0x51, 0x7F, 0xA9, 0x19, 0xB5, 0x4A, 0x0D, 0x2D, 0xE5, 0x7A, 0x9F, 0x93, 0xC9, 0x9C, 0xEF,
    0xA0, 0xE0, 0x3B, 0x4D, 0xAE, 0x2A, 0xF5, 0xB0, 0xC8, 0xEB, 0xBB, 0x3C, 0x83, 0x53, 0x99, 0x61,
    0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D]


def decrypt(c):
    result = b''
    for i in c:
      a = i // 0x10
      b = i % 0x10
      result += re_s_table.to_bytes(1, "little")
    return result

r =
s = Solver()
s.add(((r + r + r + r )%256 == 71, (r + r + r )%256 == 3, (r )%256 == r + 68, (r )%256 == r + 2, (r )%256 == r - 59, (r )%256 == r + 10, (r )%256 == r + 9, (r )%256 == r + 52))

for i in r:
    s.add(i>=0)
    s.add(i<256)

while(s.check()==sat):
    c = b''
    m = s.model()

    for i in range(8):
      c +=(((m].as_long()).to_bytes(1, 'little')))

    # 输出方程的解
    for i in c:
      print(hex(i).zfill(2).upper(), end='')
    print('\t', end='')

    # 查表
    for i in range(64*4):
      p = decrypt(c)
      c = p

    # 输出flag
    for i in c:
      print(hex(i).zfill(2).upper(), end='')

    #排除该解后继续求解
    s.add(r != m].as_long())

    print()
PS:WriteUp上都只提到了一个解,方程解和flag分别为
7733316C64306E65 C7C536FC625CEFCD
实际上可通过的flag也确实是这个
但是通过Z3一共可以算出3个方程的解,对应的flag也分别有3个,测试发现它们也都能通过程序的校验,弹出正确的提示B77371AC64306E65 0F73974F625CEFCD
7733316C64306E65 C7C536FC625CEFCD
F7B3B1EC64306E65 2DA0F111625CEFCD
其他地方也没有对flag的限定,仅说是16位MD5,但是MD5解不出也不一定代表就不是MD5值对吧……毕竟彩虹表并不能解出所有的MD5╮(╯_╰)╭


whyida 发表于 2017-12-11 11:59

本帖最后由 whyida 于 2017-12-11 12:00 编辑

请教一下这个帖子中用python 怎样写。
https://www.52pojie.cn/thread-673404-1-1.html
d='52pojie'

s=0
k=0
t=[]

for i in range(7):
    s+=ord(d)
    k=ord(d)^(0x41+i)
    t+=chr(k)
   
   
f=str(hex(s^0x12345678))

print(f)

fo = open("test.KEY", "w")
fo.writelines(f)
fo.close()

print(t)

十六进制值 f ,怎样才能以c 语言char 方式写入keyfile.

whklhh 发表于 2017-12-11 12:23

whyida 发表于 2017-12-11 11:59
请教一下这个帖子中用python 怎样写。
https://www.52pojie.cn/thread-673404-1-1.html


open("xxx", "wb")
就是写入二进制流了

h132011424 发表于 2017-12-11 01:27

厉害厉害,膜拜大神

七月十九 发表于 2017-12-11 02:01

{:17_1061:}MD5解不出去找啊

chaizy1 发表于 2017-12-11 02:40

破解个加速器吧

lshygw 发表于 2017-12-11 06:45

到底能用吗

gongyong728125 发表于 2017-12-11 07:59

学习了楼主!谢谢分享

whyida 发表于 2017-12-11 13:39

whklhh 发表于 2017-12-11 12:23
open("xxx", "wb")
就是写入二进制流了

试了没有用。

liuchunlin 发表于 2017-12-11 14:43

谢谢楼主,学习一下
页: [1] 2 3
查看完整版本: 【JarvisOJ】软件密码破解(3)