首页
社区
课程
招聘
[原创]看雪 2016 CTF 第九题 老鼠打洞
发表于: 2016-11-19 16:52 5918

[原创]看雪 2016 CTF 第九题 老鼠打洞

HHHso 活跃值
22
2016-11-19 16:52
5918

这里尝试分析老鼠洞的成因,以及尝试引出一条可能行的快速解码方式的破解思路雏形,未拓展至256-bit大整数。
作者本意应该是让输入产生的key完全匹配 "57AEA642D24E4080B23177BFCC40814EB73DBD01E92480C85C3C4046662C10000";
其设计逻辑概括为:
inputkey = 用户输入 //通过GetWindowTextA获取Edit用户输入内容到 thisPtr.5Ch 成员变量,类型为 basic_string<char>
.text:00402CF9 mov     ecx, [ebp+var_4_thisPtr]
.text:00402CFC add     ecx, 5Ch
.text:00402CFF push    ecx
.text:00402D00 push    3E8h
.text:00402D05 mov     edx, [ebp+arg_0]
.text:00402D08 push    edx
.text:00402D09 call    Hi_get_inputkey_sub_4A60BC

在 GetWindowTextA 处下断,确认后会断在上述的 Hi_get_inputkey_sub_4A60BC 中,
返回后可在 var_4_thisPtr 的偏移 0x5Ch 处下访问断点(如硬件read dword断点),
之后会在 主要校验逻辑函数 Hi_MainCheck_sub_403160 下处调用的 Hi_bastr_00hwwPtr_sub_4D353A内断下
.text:00403190 add     ecx, 5Ch
.text:00403193 call    Hi_bastr_00hwwPtr_sub_4D353A

主要校验函数 Hi_MainCheck_sub_403160 的基本流程如下
其主要是以"0"|"1"字符串的形式表达128bit和256bit整数及其计算
a.函数 Hi_64strHex_to_128strBin_sub_401046 将128bit整数的十六进制转为二进制
b.函数 Hi_BinStr_to_HexStr_sub_4010AA 跟上一函数执行相反的操作
c.函数 Hi_128strBin_dmovzx_to_256strBin_sub_401050 将128bit拓展为256bit
d.函数 P3 = Hi_iterDifMulXor_sub_401037(P1,P2) 是卷乘异或(名字我起的,哈哈),基本运算逻辑举例如下
(其中 Hi_iterDifMulXor_sub_401037 的一个非256bit的python实现版本参考后续代码)
设若 P1 = 0b10101 (二进制), P2 = 0b1011 = 0b1000 + 0b0010 + 0b0001(二进制),
则 P3 = (P1*0b1000) ^ (P1*0b0010) ^ (P1*0b0001),

10101    = (P1*0b1000)
  10101  = (P1*0b0010)
   10101 = (P1*0b0001)
----------------------xor
10010111 = P3
****特别地,P1,P2任意一个为零时,P3为零****----
这是导致tgtkey组成部分(tgtkey1,tgtkey2,tgtkey3,tgtkey4)称为空字符串的核爆点,
利用这个特点就可以充分构造出穿透空串漏洞或不完全匹配漏铜完成验证。

Hi_MainCheck_sub_403160{
  inputkey = 用户输入
  if strlen(inputkey) != 0x17:   //检测输入注册码 inputkey 的长度,必须为 23字节
    return //donothing, just return to window
  strHexNum1 = "0"*64 = itoastr(int(inputkey[0~4],0x1A)) //将五位二十六进制真值按十进制转换为字符串,而识别为十六进制字符数据, a~z 分别对应 0~25
  strHexNum2 = "0"*64 = itoastr(int(inputkey[7~12],0x1A))
  strHexNum3 = "0"*64 = itoastr(int(inputkey[16~22],0x1A))
  strHexNum4 = "0"*64 = itoastr(int(inputkey[21~22],0x10)) //直接当做十六进制字符处理
  strHexNum5 = "0"*64 = itoastr(int(inputkey[5~6],0x10))
  strHexNum6 = "0"*64 = itoastr(int(inputkey[12~13],0x10))
  strHexNum7 = "0"*64 = itoastr(int(inputkey[14~15],0x10))
  DnumVal = int(inputkey[14~15],0x0A)//当两位十进制处理
  256bitInt1 = new Hi_256bit_strINT_sub_40105F()
  256bitInt2 = new Hi_256bit_strINT_sub_40105F()
  256bitInt3 = new Hi_256bit_strINT_sub_40105F()
  #
  256bitInt1 = Hi_128strBin_dmovzx_to_256strBin_sub_401050(Hi_64strHex_to_128strBin_sub_401046(strHexNum1))
  256bitInt2 = Hi_128strBin_dmovzx_to_256strBin_sub_401050(Hi_64strHex_to_128strBin_sub_401046(strHexNum5))
  256bitInt3 = Hi_iterDifMulXor_sub_401037(256bitInt1,256bitInt2)
  tgtKey1 = Hi_BinStr_to_HexStr_sub_4010AA(256bitInt3)
  #
  256bitInt1 = Hi_128strBin_dmovzx_to_256strBin_sub_401050(Hi_64strHex_to_128strBin_sub_401046(strHexNum2))
  256bitInt2 = Hi_128strBin_dmovzx_to_256strBin_sub_401050(Hi_64strHex_to_128strBin_sub_401046(strHexNum6))
  256bitInt3 = Hi_iterDifMulXor_sub_401037(256bitInt1,256bitInt2)
  tgtKey2 = Hi_BinStr_to_HexStr_sub_4010AA(256bitInt3)
  #
  256bitInt1 = Hi_128strBin_dmovzx_to_256strBin_sub_401050(Hi_64strHex_to_128strBin_sub_401046(strHexNum3))
  256bitInt2 = Hi_128strBin_dmovzx_to_256strBin_sub_401050(Hi_64strHex_to_128strBin_sub_401046(strHexNum7))
  256bitInt3 = Hi_iterDifMulXor_sub_401037(256bitInt1,256bitInt2)
  tgtKey3 = Hi_BinStr_to_HexStr_sub_4010AA(256bitInt3)
  #
  strHexNum8 = "1"
  256bitInt1 = Hi_128strBin_dmovzx_to_256strBin_sub_401050(Hi_64strHex_to_128strBin_sub_401046(strHexNum4))
  256bitInt2 = Hi_128strBin_dmovzx_to_256strBin_sub_401050(Hi_64strHex_to_128strBin_sub_401046(strHexNum8))
  while (DnumVal--):
    256bitInt3 = Hi_iterDifMulXor_sub_401037(256bitInt1,256bitInt2)
    256bitInt2= 256bitInt3//Hi_256bitINT_P2_EQ_P1_sub_40100A
  tgtkey4 = Hi_BinStr_to_HexStr_sub_4010AA(256bitInt3)
  tgtkey = tgtkey1 + tgtkey2 + tgtkey3 + tgtkey4
  #
  refkey = '57AEA642D24E4080B23177BFCC40814EB73DBD01E92480C85C3C4046662C10000'
  bSuccess = True           //代码逻辑是先假设key是正确的,直接导致若tgtkey为空串时则默认成功, 空串老鼠洞
  length1 = strlen(refkey)
  length2 = strlen(refkey)  //此处作者程序源代码变量名称写错的原因,导致下面的比较没有意义
  if length1 != length2:
    bSuccess = False
  i = 0
  while tgtkey[i++] != '\0':  //由于其是以tgtkey是否结束为主,所以会出现不完全匹配的漏洞,即只需匹配任意前面字节即可
    if tgtkey[i] != refkey[i]:
      bSuccess = False //失败为先天的隐性基因
  if bSuccess:
    MessageBox(0,"Success!","Congratulations",0)
}

一种完美的空串构造为 "aaaaa00aaaaa0000aaaaa00"
tgtkey1 = Hi_iterDifMulXor_sub_401037(aaaaa(二十六进制的零),00)
tgtkey2 = Hi_iterDifMulXor_sub_401037(aaaaa(二十六进制的零),00)
tgtkey3 = Hi_iterDifMulXor_sub_401037(00,aaaa00(二十六进制溢出))
tgtkey4 = Hi_iterDifMulXor_sub_401037(1,00 of aaaa00)

有多少种组合?其中一种不管二十六进制是否溢出,"aaaaa00aaaaa0000aaaaa00"中a的位置可以换成任意可输入字符
另,在确保a不变的情况下,00位置也可以相应变化
这些都是空字符串漏洞集合

下面说说不完全匹配的情形,也即对refkey解码
这里列举两种方式对比效率
(1)第一种值分段穷举
    例如tgtkey1 = Hi_iterDifMulXor_sub_401037(key1_P1,key1_P2)
    其中在不溢出的情况下,取值范围 key1_P1为 (0,0x10000000),key1_P2为(0,0x100) //零的情形为空串,不需考虑
    由于 refkey = '57AEA642D24E4080B23177BFCC40814EB73DBD01E92480C85C3C4046662C10000'
    是以tgtkey1开始,所以可以通过下面的方式穷举key1_P1,key1_P2的可能组合Mm,
由 start_enum_thread(Tn)启动穷举,Tn为使用的线程数(也是分区间枚举的区间数),
结果Mm得到的是(key1_P1,key1_P2)组合,从而确定 tgtkey1 的组合
由于 tgtkey1 的不同,对tgtkey2的枚举也会不同,必需留意到,
这里虽然使用了多线程,但计算量还是太大,不科学。后面给出另一种直接快速收敛的解码方式。

#------------------------------------------------------------------------
import threading
import time  
import thread  
Mm = []
mutex_Mm = threading.Lock()
mutex_bprint = threading.Lock()
def enumse(s,e):
  global Mm
  tgt = "57AEA642D24E4080B23177BFCC40814EB73DBD01E92480C85C3C4046662C10000"
  for i in xrange(s,e):
    for j in xrange(1,0x100):
      hstr = "{:X}".format(iterDifMulxor(i,j))
      if tgt.startswith(hstr):
        if mutex_Mm.acquire(1):
          Mm.append([i,j])
          mutex_Mm.release()
  if mutex_bprint.acquire(1):
    print "{} {:08X} {:08X}".format(time.ctime(),s,e)
    mutex_bprint.release()

def todo(Tn):
  imax = 0x10000000
  stepwidth = imax / Tn
  for i in xrange(0,imax,stepwidth):
    thread.start_new_thread(enumse, (i,i+stepwidth,))
  if  stepwidth * Tn != imax:
    thread.start_new_thread(enumse, (stepwidth * Tn,imax,))

def start_enum_thread(Tn):
  print "start time %s"%time.ctime()  
  todo(Tn)
  print "end time %s"%time.ctime()

#Hi_iterDifMulXor_sub_401037 的非256bit实现版本
def iterDifMulxor(P1,P2):
  r = 0
  if P1 == 0:
    return r
  xors = []
  while True:
    if (P2&1) == 1:
      xors.append(P1)
    P2 = P2 >> 1
    P1 = P1 << 1
    if P2 == 0:
      break
  for x in xors:
    r = r ^ x
  return r
#------------------------------------------------------------------------

下面的解码方案是针对 Hi_iterDifMulXor_sub_401037 的计算设计的
10101    = (P1*0b1000)
  10101  = (P1*0b0010)
   10101 = (P1*0b0001)
目的还是没变,寻找 tgtkey1 = Hi_iterDifMulXor_sub_401037(key1_P1,key1_P2)中的 key1_P1和key1_P2,
先比上一种的第一个优势是主动引入tgtkey1进行解码
refkey = '57AEA642D24E4080B23177BFCC40814EB73DBD01E92480C85C3C4046662C10000'
基本原理是选定不同长度的 tgtkey1(长度为4bit*n,如0x5,0x57,0x57A,...)
然后尝试枚举不同的 key1_P1 与 key1_P2,看似跟上面差不多,
但由于我们主动引入了tgtkey1,并在 Hi_iterDifMulXor_sub_401037 的边计算边比对以实现快速比对收敛。
上面的穷举算法中,iterDifMulxor 需要完成计算才能确定是否是目标的tgtkey1,
而根据 iterDifMulxor 的运行特点,P3 = (P1*0b1000) ^ (P1*0b0010) ^ (P1*0b0001)
其每次异或一个乘积,实际就能确定 tgtkey1 的至少1位(最低位起,或最高位起),
即对于特别是比较大的数据(如256bit) key1_P1 和 key1_P2,我们就能根据有限的几次乘积命中不匹配key1_P1,key1_P2
P1:11111111111111111111111111111111111111111111111111111111111111111111111111111111
P2:10010100001100101000011001010000110010100001100101000011001010000110010100001010
完成iterDifMulxor(P1,P2)的运算是比较大的,但如果在初次两次计算P2_10的时候我们就发现01不匹配tgtkey1的10低两位时,
我们就可以不必后续计算了。比较低位是引入低位收敛的情形,实际还可以同时引入部分高位收敛以进一步提高拆解效率。

def check_unpack(seqbin = None):
  seqbin_bak = seqbin
  """
  seqbin_vbn = 0
  while seqbin != 0:
    seqbin_vbn = seqbin_vbn + 1
    seqbin = seqbin >> 1
  seqbin_max = (1 << (seqbin_vbn)) - 1
  """
  seqbin_vbl = bin(seqbin).__len__()-2
  seqbin_end = 1 << seqbin_vbl
  print bin(seqbin_bak),bin(seqbin_end-1)
  #
  ebm_itr = []
  for ebm in xrange(1,seqbin_end):
    ebm_l = bin(ebm).__len__()-2
    itx_l = seqbin_vbl - ebm_l
    itx_m = 1 << (itx_l + 1)
    for itb in xrange(1,itx_m):
      if ebm > itx_m: #由于是对称的,这句的的引入减少一半的计算量 key1_P1,key1_P2互换
        break
      itr = 0
      iebm = ebm
      gi = 0
      while (itb>>gi) :
        if ((itb>>gi) & 1) == 1:
          itr = itr ^ iebm
          gm = (1 << (gi+1)) - 1  #gm 为已经确定的tgtkey的低位Mask,与目标tgtkey比对
          if ((itr ^ seqbin) & gm) != 0: #若不等则结束计算
            break
        gi = gi + 1
        iebm = iebm << 1
      if itr == seqbin:
        ebm_itr.append([ebm,itb])
  return ebm_itr

tgtkey1 = check_unpack(0x5) >> [[1, 5], [3, 3]] >> aaaab05,aaaad03
tgtkey2 = check_unpack(0x7) >> [[1, 7]]         >> aaaab07
如上种种,我们就可以得到不同的不完成的匹配非空串比对key,
解密速度只是相对的快,而且上述还没有引入高位收敛,也没有实现256bit的大数拆解,也就只能是鸡肋
>>> check_unpack(5)
0b101 0b111
[[1, 5], [3, 3]]
>>> check_unpack(0x57)
0b1010111 0b1111111
[[1, 87]]
>>> check_unpack(0x57A)
0b10101111010 0b11111111111
[[1, 1402], [2, 701]]
>>> check_unpack(0x57AE)
0b101011110101110 0b111111111111111
[[1, 22446], [2, 11223], [3, 12954], [6, 6477],
[11, 2474], [13, 3462], [22, 1237], [23, 1154],
[26, 1731], [29, 1894], [46, 577], [58, 947],
[87, 258], [127, 498], [129, 174], [174, 129], [249, 254], [254, 249]]
>>> check_unpack(0x57AEA)
0b1010111101011101010 0b1111111111111111111
[[1, 359146], [2, 179573], [3, 207270], [6, 103635],
[285, 1298], [391, 1614], [570, 649], [649, 570], [782, 807], [807, 782]]
>>> check_unpack(0x57AEA6)
0b10101111010111010100110 0b11111111111111111111111
[[1, 5746342], [2, 2873171], [3, 3316322], [5, 1149406], [6, 1658161],
[10, 574703], [11, 633362], [22, 316681], [29, 484878], [39, 187898], [58, 242439], [78, 93949]]
>>> check_unpack(0x57AEA64)
0b101011110101110101001100100 0b111111111111111111111111111
[[1, 91941476], [2, 45970738], [4, 22985369], [7, 29948108], [14, 14974054],
[28, 7487027], [319, 369068], [638, 184534], [1276, 92267], [1981, 122964], [3962, 61482], [7924, 30741]]
>>> check_unpack(0x57AEA642)
0b1010111101011101010011001000010 0b1111111111111111111111111111111
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 16, in check_unpack
OverflowError: Python int too large to convert to C long
#######不好意思,python 的bit座位不够了,需要另一套256bit计计算体系。洞就打到这里!#######


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

收藏
免费 2
支持
分享
最新回复 (1)
雪    币: 112
活跃值: (27)
能力值: ( LV7,RANK:110 )
在线值:
发帖
回帖
粉丝
2
很佩服楼主追求技术的执着
2016-11-20 15:55
0
游客
登录 | 注册 方可回帖
返回
//