强网拟态2022初赛 - 复现
纯纯摸鱼,然后靠队友带飞,都太强了😭😭😭一定好好当端茶倒水小弟
S3qUenCEs
1 | You are given an array a consisting n(1=<n<=10^6) integers a1,a2,...,an(-10^9<=ai<=10^9 for each 1<=i<=n) and an integer k(1<=k<=n). |
算法思路:
1 | import pwn |
comeongo
本来想着这题能不能抢上血的,结果做到下午四点,急死了
golang逆向,需要获得name和pass
动调到此处可以看出name和pass的长度都是16,check有两个,都要满足。
check1:
找到一串密文,再动调进入encoding函数发现
是base58的表,于是解密密文,解出来是GoM0bi13G3tItEzF
,经过测试输入
发现check1检查的是name和pass的前八位,拆开即可。
check2:
在第一个memcmp处发现一串密文
动调进encoding函数发现是base64,解密得到_NubcfFq
再测试输入发现是对中间的9-12位进行操作的,且有9,12,-12的偏移,解出来是_Bin
和orRe
,那么还剩最后四位
动调到byte_compare,前面有个地方有个rax,和name和pass的最后四位有关系,密文就是那个unk,然后再点进去runtime_other发现最后两位的差分别是63和31,于是很容易得到,name的倒数34位是一个简单偏移,解出来是gG
,name和pass的最后四位可以设方程
2x + 63 + 2 = 161
2x + 31 + 3 = 100
求出name和pass的最后两位就是o@
和0!
最后找到pass的倒数34位vG
于是最终的flag就是MD5(flag{GoM0bi13_BingGo@G3tItEzForRevG0!})
这个题的附件pass的倒数3和4位你无论输入什么都能通过题目的flag检测,但是真flag需要md5校验以后才是对的…做到还剩最后两位的时候真就根据自然语言跟出题人对脑电波当misc做了
unlmbda
先放三篇博客:
python中的lambda函数用法 lambda演算 Y分钟入门lambda演算 SKI组合子演算
其中重点关注一下 lambda演算 => SKI演算 => SK演算 => Iota演算的过程,本题需要做的就是从 Iota 算子开始的这样一个逆过程。
前面的代码逻辑:
1 | arr = [] |
先看对 flag 的处理,发现是每 8 位做一次 check,输进去以后还要把字符转化为 8 位二进制数然后放进 arr 数组里。
观察到有两个 python 的 lambda 函数,同时 checker 里面也可以发现有 I
存在,猜测可以通过上面的那个 lambda 函数把 Iota 算子给转化成常数,也就是 arr 的下标。
1 | l = lambda _ : (_ (lambda _ : lambda __: lambda ___ : (_ (___)) (__ (___))))(lambda _ : lambda __: _) |
同时根据 SKI 和 Iota 的对应规则
我们可以对Iota组合子进行正则匹配,转化为 SKI 组合子。
就拿第一个 checker_0
来举例子吧(谔谔,有点大,大概长这样)
1 | l = lambda _ : (_ (lambda _ : lambda __: lambda ___ : (_ (___)) (__ (___))))(lambda _ : lambda __: _) |
转化完之后瞬间少了很多
1 |
|
通过观察发现有很多重复的运算,比如K(I),S(S(K(S(K(S))(K)))(S))
通过查询可知
原来 K
就是 True
, K(I)
就是 False
,S(S(K(S(K(S))(K)))(S))(K(K))
则对应一种 flip 操作,即# FLIP b e t = if b then t else e
。再写正则匹配替换
1 | checker = checker.replace('K(I)', 'F') |
1 | IF(arr[4])(IF(arr[0])(IF(arr[42])(IF(arr[28])(IF(arr[20])(IF(arr[39])(IF(arr[56])(IF(arr[30])(F)(IF(arr[27])(IF(arr[2])(F)(IF(arr[52])(IF(arr[62])(F)(IF(arr[48])(F)(IF(arr[58])(F)(IF(arr[60])(IF(arr[6])(F)(IF(arr[23])(IF(arr[19])(IF(arr[12])(IF(arr[43])(F)(IF(arr[57])(IF(arr[44])(IF(arr[53])(F)(IF(arr[55])(IF(arr[51])(IF(arr[25])(F)(IF(arr[26])(F)(IF(arr[29])(F)(IF(arr[17])(IF(arr[38])(F)(IF(arr[40])(IF(arr[32])(F)(IF(arr[37])(F)(IF(arr[18])(IF(arr[11])(F)(IF(arr[3])(IF(arr[50])(F)(IF(arr[21])(F)(IF(arr[8])(IF(arr[33])(F)(IF(arr[16])(F)(IF(arr[47])(IF(arr[22])(F)(IF(arr[9])(IF(arr[59])(F)(IF(arr[41])(F)(IF(arr[5])(F)(IF(arr[34])(IF(arr[10])(F)(IF(arr[61])(F)(IF(arr[45])(IF(arr[15])(IF(arr[1])(F)(IF(arr[7])(IF(arr[35])(F)(IF(arr[54])(F)(IF(arr[24])(F)(IF(arr[46])(F)(IF(arr[36])(F)(IF(arr[14])(F)(IF(arr[31])(IF(arr[13])(F)(IF(arr[49])(IF(arr[63])(T)(F))(F)))(F))))))))(F)))(F))(F))))(F)))))(F)))(F))))(F))))(F)))(F))))(F)))(F)))))(F))(F)))(F))(F)))(F))(F))(F)))(F)))))(F)))(F)))(F))(F))(F))(F))(F))(F))(F) |
由于是 if b then t else e
,我们发现化简完以后只有一个 T: IF(arr[63])(T)(F)
也就是说只有 arr[63]
为 F
时返回 T
,然后就是纯纯体力活,把整个表达式从里到外,把每一位都按照arr[id](F/T)(T/F)
的形式,找出每一位是 0 还是 1。
处理完以后得到flag{JelL0_wy_De@r_l@mBdA_ExprESzl0n_wITh_iOt@!}
windows_call
动调到输入的地方,前面的一大堆是md5,可不用管
flag长度为46,格式为flag{hex},hex长度为40且全部为数字和大写字母。
再往下看
还是一些对md5的操作,这里放一下 md5的源码 ,发现能找到很明显的常量特征
那么这一段代码也不用看了,接着往下看
发现flag中包裹的hex原来有40位,我们知道一般AES的题明文一般都是16的整数倍,那么多出来的这8位干啥去了呢?
我们发现前8位被转成了4位16进制数被赋值给了key1和key2,且 iv 和 key 都是和这前八位有关系的,同时在check函数中找到一长串条件
显然v42
就是加密后的密文,长度为32位,后面的条件是对前8位的限制,我们可以用z3脚本解出
1 | from z3 import * |
注意一下大小端序,前八位就是E8C9A0CC
然后重写一下 py 脚本获得 key 和 iv
1 | iv = [0 for i in range(16)] |
直接 cyberchef 就可以了
最后注意全部转成大写flag{E8C9A0CC8B9854CDD0AC321B790FC74EFA520FBC}
babyre
TLScallback
回调函数用来反调试+修改S_box,比赛的时候确实没想到,当然估计就算想到了也做不出来这题。😥
先插一个小技巧:ida pro 的 debugger option 中,把这个选项勾上,能够断在进程开始处,就是从 ntdll 开始调试。
TLS回调函数里面有个反调试,但是注意不要把异常 9/0
给绕过了,通过这个异常触发 VEH 进入handler 函数修改S_box
VEH相关可以看 这篇博客
但是注意触发条件的时候ida可能会出现警告,一定要选Yes,pass to app,不要像我一样傻逼
得到新的S_box
1 |
|
我们用这个S_box生成一个invS_box用于解密,生成过程可以参考 如何由AES中S盒推导出逆S盒
代码如下:
1 | S_box = [0x77, 0x68, 0x63, 0x6f, 0xe6, 0x7f, 0x7b, 0xd1, 0x24, 0x15, 0x73, 0x3f, 0xea, 0xc3, 0xbf, 0x62, 0xde, 0x96, 0xdd, 0x69, 0xee, 0x4d, 0x53, 0xe4, 0xb9, 0xc0, 0xb6, 0xbb, 0x88, 0xb0, 0x66, 0xd4, 0xa3, 0xe9, 0x87, 0x32, 0x22, 0x2b, 0xe3, 0xd8, 0x20, 0xb1, 0xf1, 0xe5, 0x65, 0xcc, 0x25, 0x1, 0x10, 0xd3, 0x37, 0xd7, 0xc, 0x82, 0x11, 0x8e, 0x13, 0x6, 0x94, 0xf6, 0xff, 0x33, 0xa6, 0x61, 0x1d, 0x97, 0x38, 0xe, 0xf, 0x7a, 0x4e, 0xb4, 0x46, 0x2f, 0xc2, 0xa7, 0x3d, 0xf7, 0x3b, 0x90, 0x47, 0xc5, 0x14, 0xf9, 0x34, 0xe8, 0xa5, 0x4f, 0x7e, 0xdf, 0xaa, 0x2d, 0x5e, 0x58, 0x4c, 0xdb, 0xc4, 0xfb, 0xbe, 0xef, 0x57, 0x59, 0x27, 0x91, 0x51, 0xed, 0x16, 0x6b, 0x44, 0x28, 0x8b, 0xbc, 0x45, 0xb7, 0x54, 0x9b, 0x86, 0x89, 0x2c, 0xe1, 0xa8, 0xa2, 0xce, 0x35, 0x4, 0xeb, 0xe7, 0xc6, 0xd9, 0x18, 0x7, 0xf8, 0x4b, 0x83, 0x50, 0x3, 0xd0, 0xb3, 0x6a, 0x29, 0x70, 0x49, 0xd, 0x67, 0x74, 0x95, 0x5b, 0xc8, 0x36, 0x3e, 0x84, 0x9c, 0x52, 0xfa, 0xac, 0x0, 0xca, 0x4a, 0x1f, 0xcf, 0xf4, 0x26, 0x2e, 0x1e, 0x5d, 0x12, 0x30, 0x48, 0xd6, 0xc7, 0xb8, 0x76, 0x85, 0x81, 0xf0, 0x6d, 0xf3, 0xdc, 0x23, 0x79, 0x99, 0xc1, 0x5a, 0xbd, 0x78, 0x42, 0xe0, 0xfe, 0x71, 0x6e, 0xba, 0x1c, 0xae, 0x6c, 0x31, 0x3a, 0x8, 0xb2, 0xa0, 0xd2, 0xfc, 0xc9, 0x60, 0xb, 0x5f, 0xa9, 0x9f, 0x9e, 0x64, 0x2a, 0xa1, 0x72, 0x5c, 0x17, 0xe2, 0x1a, 0x75, 0x21, 0x43, 0xad, 0x92, 0xd5, 0x9, 0x8a, 0xf5, 0xec, 0x8c, 0x5, 0x7d, 0xcd, 0x9a, 0x80, 0x8f, 0xa, 0x93, 0xfd, 0xda, 0x41, 0x3c, 0xcb, 0x98, 0xb5, 0x9d, 0x19, 0xab, 0xf2, 0x56, 0x7c, 0x55, 0x8d, 0x39, 0x1b, 0xa4, 0x40, 0xaf, 0x2] |
生成的逆S_box如下
1 |
|
下面开始分析主函数加密部分 ,发现是个 AES ,密钥已经知道了,输入32位字符串转成 16 个 2 位 hex,但是有魔改:
- S_box改变
- 把加密过程中的行列操作给调换了,这点非常坑,得面向黑盒动调很长时间才能搞明白,由于他是把行列操作对象给调换了,所以无论是加密还是解密之前,我们都需要把明文/密文给置换一下,(或者就都尝试一次看看哪个和动调结果能对的上)
- 第二次加密对key进行了改变
通过动态调试,发现在第一次addRoundKey
之前,函数对密文矩阵进行了一次转置,之后便是一模一样的AES加密算法。
可以发现第一次加密只是加密了前16位,对后16位没有进行操作。
我们可以通过本地的加密算法获得一样的结果,得到这个输出结果我应该用了一下午吧大概。
然后就是第二次AES加密,在第二次开始之前,先把密钥与上一轮加密得到的密文进行异或。
然后就是对第二段hex数据进行加密,和第一轮一样,把行列转置了一下
最后dump出密文解密即可
1 | unsigned char enc[] = |
完整exp
1 |
|
得到flag{LKsyByI7oHX0S9PiqY0G2qvpte4f30mC}
mcmc
使用了 控制流平坦化和虚假控制流混淆,但是一般的 deflat 脚本没有办法完全去除混淆,但是使用 D - 810
插件可以去除大部分混淆(但也就是让代码勉强能看)。
可以看到,在输入以后,先判断 flag 是否为 32 位,然后进行了一些加密操作,通过 findcrypt
插件可以发现
加密,通过交叉引用发现是在 init
函数中,但是整个流程还是很难看,而且通过对照 salsa20
加密的源码,也无法确定是否是 salsa20
加密,通过调试,发现只跑了LABEL_6
这里,说明上面的代码应该是复制了一份作为虚假控制流,不用管,在如图的地方下断起调试,这里的测试输入为 11112222333344445555666677778888
。
在经过第一步异或加密以后 flag 变成了这样,可以直接看出是对第 8, 16, 24, 32
位进行了异或。
然后是 sub_405480
函数,通过一些调整,可以看出是把 flag 分成 8 段,每段 4 bit,然后再分成两部分进行加密,我们步入该函数,发现也被混淆了,用 D - 810
修一下
发现即使是修过了以后,代码还是非常难看,选择嗯看汇编,调试到加密开始的地方
跟踪调试一下,得出加密逻辑如下:
1 | def Trans(s): |
接着看主函数,后面接着的是之前 findcrypt
出来的 salsa20
加密函数 sub_401820
调进去看一眼,用 D - 810
修复一下
发现逻辑是先生成了一个 a1
表,a1[8]
类似于一个计数器,从0开始递增,为奇数时执行 v4 = plain[(i + 1) % a3];
,为偶数时执行 v4 = plain[(i - 1) % a3];
,接着调试,发现又进行了 plain[i] ^= (v4 + (int)v7) % 256;
的异或加密,而 v7 是之前获得的表 a1
的 a1[a1[8]]
这一项。这里有两种方法获得 v7
- 我们考虑在之前赋值的那一步对表
a1
进行 dump,可以直接获得整个table
- 考虑在执行异或操作的那一步对存储
v7
这个变量的寄存器进行 dump,也可以获得table
获得的 table
如下
1 | table = [0xD3, 0x1B, 0xCC, 0x7D, 0xEF, 0xB9, 0x0D, 0xC6, 0xA0, 0xDB, 0xE2, 0x07, 0xFB, 0x0F, 0xB7, 0x0E, 0xCB, 0x73, 0x3A, 0x8A, 0xC5, 0x4E, 0x3E, 0xEC, 0x0C, 0xF0, 0xA2, 0x48, 0x94, 0x70, 0x27, 0x1B] |
到此整个加密流程就分析完了(好像跟Chacha20, Salsa20
加密算法没啥关系,白学了属于是),然而回到 main
一看发现少了啥东西,好像找不到 check
了,原来是 D - 810
的锅,不去混淆直接把密文先弄下来
1 | cipher = [0x06, 0x08, 0x65, 0x04, 0x60, 0x03, 0x08, 0x01, 0x4A, 0x10, 0x32, 0x58, 0xEE, 0x97, 0x65, 0x84, 0x44, 0xF2, 0x10, 0x6B, 0xE8, 0x50, 0x24, 0x99, 0xF6, 0xE3, 0x21, 0x51, 0xC2, 0x5D, 0xBF, 0x32] |
然后一步步写出解密脚本,先逆 sub_401820
1 | cipher = [0x06, 0x08, 0x65, 0x04, 0x60, 0x03, 0x08, 0x01, 0x4A, 0x10, 0x32, 0x58, 0xEE, 0x97, 0x65, 0x84, 0x44, 0xF2, 0x10, 0x6B, 0xE8, 0x50, 0x24, 0x99, 0xF6, 0xE3, 0x21, 0x51, 0xC2, 0x5D, 0xBF, 0x32] |
然后再用 z3 - solver
把 sub_405480
函数解了。
1 | from z3 import * |
最后再异或一下得到 flag{3ummer1s0ver_C0dingcn0tsT0p0hhhh}