本文分析了ida81对输入密码的验证流程,分别对输入密码到生成解密密钥、密码素材的生成过程以及文件数据的加密过程这三个流程进行分析,并尝试找一些可利用的破绽。很遗憾,由于水平有限,目前也只是有个思路未能完全实现,故分享出来抛砖引玉。
 
 
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | proc m4ZQu {password LZQjH} {
     set::maui::util::H6VeX ""
     set::maui::util::EL0BW [string repeat \\032]
     package require tcltwofish
     package require sha256
     if{[string length $LZQjH] ==0} {
         error "Unable to initialize encryption - missing essential data"
     }
     
     //1.展开payload 获取相关参数
     if{[binary scan $LZQjH Ia16a32a64a32a*times iv passwordKey encryptedKey payloadIVsHash encryptedPayloadIVs] < 6} {
         error "Unable to initialize encryption - header missing or corrupt"
     }
     //2.使用输入的密码计算sha256,然后根据迭代次数times加密passwordkey,最后计算结果的sha256
     setiHcWR [a64bL $password $passwordKey $iv $times]
     
     //3.取上一步中的后32字节
     setBJvoG [string range[tcltwofish::decrypt $iHcWR $encryptedKey] 3263]
     
     //4.使用上一步中的结果作为key 循环times/64次 解密encryptedPayloadIVs
     //这一步没有指定iv 实际过程为16个字节的0x00
     for{seti 0} {$i < $times} {incr i 64} {
         setencryptedPayloadIVs [tcltwofish::decrypt $BJvoG $encryptedPayloadIVs]
     }
     //5.获取32字节后的数据
     setZQpQw [string range$encryptedPayloadIVs 32end]
     //6.计算上一步结果的sha256,和预置的hash比对,一致则密码正确
     if{[sha2::sha256 -bin$ZQpQw] !=$payloadIVsHash} {
         maui::d_RUj "Invalid payload password"
     }
     
     //7.使用第五步中的结果作为后续选取iv的数据
     set::maui::util::H6VeX $ZQpQw
     //8.使用第三步中的结果作为密钥
     set::maui::util::EL0BW $BJvoG
 }
 | 
 
在这个过程中,第一步需要的payload为4020字节,这是已知的。意味着可以获取到第六步中正确的hash结果。
 然而,根据下面的帖子总结,ida的密码选取字符为58个,密码长度为14个字符,这个测试结果是非常大的。
破密行動: 以不尋常的角度破解 IDA Pro 偽隨機數 | DEVCORE 戴夫寇爾
[下载] ida 8.1-资源下载-看雪-安全社区|安全招聘|kanxue.com
 
而我在i9-13900K测试从随机生成输入密码到第六步验证hash整个过程,测试一个密码就需要0.045秒。所以手里有超算也不建议直接爆破。
 
 
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | //根据输入密码生成payload
 proc yOZ7q {password {times 16000}} {
         set::maui::util::H6VeX ""
         set::maui::util::EL0BW [string repeat \\032]
         package require tcltwofish
         package require sha256
         
         setBJvoG [qA4kM 32]
         setpasswordKey [qA4kM 32]
         setZQpQw [qA4kM [expr {16*256}]]
         setpayloadIVsHash [sha2::sha256 -bin$ZQpQw]
         setencryptedPayloadIVs "[qA4kM 32]$ZQpQw"
         for{seti 0} {$i < $times} {incr i 64} {
             setencryptedPayloadIVs [tcltwofish::encrypt $BJvoG $encryptedPayloadIVs]
         }
         setiv [qA4kM 16]
         setiHcWR [a64bL $password $passwordKey $iv $times]
         
         setencryptedKey [tcltwofish::encrypt $iHcWR "[qA4kM 32]$BJvoG"]
         setLZQjH [binary formatIa16a32a64a32a*$times $iv $passwordKey $encryptedKey $payloadIVsHash $encryptedPayloadIVs]
         set::maui::util::H6VeX $ZQpQw
         set::maui::util::EL0BW $BJvoG
         
         return$LZQjH
     }
 //生成随机数据
  proc qA4kM {length} {
         setYKL8w ""
         if{[RLPut unix]} {
             foreach xML4B {/dev/urandom /dev/random} {
                 if{[catch {
                     setWyLEv [open$xML4B r]
                     fconfigure $WyLEv -translation binary
                     setYKL8w [read $WyLEv $length]
                     close $WyLEv
                 }]} {
                     catch {close $WyLEv}
                 }  else{
                     break
                 }
             }
         }  else{
         
         }
         if{[string length $YKL8w] !=$length} {
             incr length -[string length $YKL8w]
             while{$length >=2} {
                 append YKL8w [binary formatS [expr {int([maui::util::rand] *0x10000)}]]
                 incr length -2
             }
             if{$length > 0} {
                 append YKL8w [binary formatc [expr {int([maui::util::rand] *0x100)}]]
             }
         }
         return$YKL8w
     }
     proc rand {} {
         if{[maui::util::RLPut "windows"] && [info commands ::bitrock::secure::rand_s] !=""} {
             if{[catch {
                 setr [::bitrock::secure::rand_s]
             } V77Ls]} {
                 maui::util::debug "failed to call rand_s: $V77Ls"
                 setr [expr {rand()}]
             }
         } else{
             setr [expr {rand()}]
         }
         return$r
     }
 | 
 
继续分析输入密码的时生成payload的算法,根据安装密码生成payload的过程中,很多数据包括最后解密数据用的密钥BJvoG也是随机生成的,如果这部分随机数生成流程能复现那也是可以破解的。仔细看随机数的生成,已经排除了以前的LCG随机数生成算法,换成了更安全的,前后状态无影响的方式,linux或unix下用/dev/random或者/dev/urandom,windows下调用rand_s。
rand_s | Microsoft Learn
 
 
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | proc MmyBM {compressionAlgorithm command data} {
         if{[catch {
             //1.压缩数据
             if{($compressionAlgorithm =="lzma") || ($compressionAlgorithm =="lzma-ultra") || ($compressionAlgorithm =="lzham") || ($compressionAlgorithm =="lzham-ultra")} {
                 setdata [eval[concat $command [list$data]]]
             }  else{
                 setdata [vfs::zip-mode compress -nowrap 1$data]
             }
             //2.1前面添加四个字节的长度,然后末尾补全32的倍数
             //2.2前面添加32字节的随机数据
             setdata [qA4kM 32][UOijU $data 32] 
             //3.在H6VeX中截取16字节的iv
             setPG2JA [expr {int([maui::util::rand] *0x100)}]
             setiv [string range$::maui::util::H6VeX [expr {$PG2JA *16}] [expr {$PG2JA *16+15}]]
             //4.计算crc32
             sethbA8n [format0x%08x[expr {[zlib crc32 $data] & 0xffffffff}]]
             
             //5.使用EL0BW中的数据作为key 加密
             setdata [tcltwofish::encrypt $::maui::util::EL0BW $data iv]
             
             //6.前面填充四个字节的crc32以及选取iv的位置,共加了五个字节
             setYKL8w [binary formatIca*$hbA8n $PG2JA $data]
         } V77Ls]} {
             setykaki $::errorCode
             setqXoNi $::errorInfo
             maui::util::debug "encryptPayload: error encrypting: $V77Ls"
             error $V77Ls $qXoNi $ykaki
         }
         return$YKL8w
     }
 | 
 
| 1 2 3 4 5 6 7 8 9 | proc UOijU {data DeSRF} {
     //1.将四字节的长度添加在数据前
     setdata [binary formatIa*[string length $data] $data]
     //2.计算末尾添加padding的长度
     setadd [expr {($DeSRF -([string length $data] %$DeSRF)) %$DeSRF}]
     //3.生成随机padding并附加在末尾
     append data [qA4kM $add]
     return$data
 }
 | 
 
再看看文件数据的加密流程。从上面的脚本中,可以看到加密前的数据结果为 32字节随机数据+4字节数据+$data+最后补全32字节的padding 然后使用twofish进行加密。而twofish是分组加密算法,所以加密过程就是下面这样的

 
 通过在前面添加32字节的随机数据,猜测是为了避免明文攻击。因为很多文件前面都有固定字节的数据
 
 比如sqlite3数据库的前16字节固定为SQLite format 3\x00,在ECB模式下,相同密钥加密任何sqlite3数据库的前16个字节都是一样的。所以衍生出了CBC模式,在每个分组加密之前用iv或者上一个密文分组异或明文以产生不一样的数据。
 
 
然而,这个例子中前面32个字节的随机数的使用却颇有点画蛇添足的味道。
 首先,它不能掩盖明文数据或者阻止明文攻击(稍后提到),其次因为这32字节的加密数据,我们在后面进行密码分析的时候可以忽略了原本的初始iv,初始iv来自哪里,来自输入密码后进行的16000次twofish加密和256次twofish解密后生成的0x1000字节的encryptedPayloadIVs,而现在却可以跳过了,从而暴露了它的阿克琉斯之踵。根据密码算法,现在解密每一段文件数据时只需要从第三个分组的数据开始解密即可,输入iv来源于上一个分组的的密文(即第二个分组),输入的key对所有文件都是一样的(也就是本次要分析出来的目标)。
 
再回头说说有哪些明文,从脚本可知加密的第三个分组前四个字节为加密数据的长度。在安装过程中会生成installbuilder_installer_{random}.log文件,统计生成的文件数。
 
在hook解密函数的过程中也发现了同样数量的加密数据,而这些数据中最小长度为0x000012a0,最大为0x00040060。所以这1212个文件在相同的密钥解密之后的前四个字节肯定是
00 0x xx xx。如果能将每段加密数据对应到具体的文件,可能还会有更多的明文信息。
 
综上所述,根据解密的tcl安装脚本共分析了三种破解思路:
 第一种,根据输入密码爆破,工作量太大,PC上几乎不可能成功。
 第二种,随机数攻击。新版本的安装程序摒弃了LCG算法的使用,使用了更加安全的随机数生成方式。
 第三种,根据部分明文信息和1212个样本数据爆破32字节密钥,爆破的过程中每次验证只需要一次twofish解密16个字节即可。再配合密码差分分析,可以减少一定的尝试次数。有熟悉angr的大佬也可以尝试一下形式化验证的方式去破解。
 
另外,ida安装脚本中的twofish算法来源于GitHub - rageworx/libtwofish: TwoFish encryption library for modern C++ (dev.in progress)
 ,但是作了一些修改。文末附带校正后的版本,可以直接用。另外还有调试时用的frida脚本。