题目质量蛮不错的,学到了很多东西,RE是2道VM的题,一道花指令+ollvm混淆,也有两道Android题在Misc和Crypto中,最刺激的是距比赛结束还有两分钟时解出了RE3。
一、Misc
badPDF
拿到文件拖入010editor中,发现是4c 00开头,是win下快捷方式的标志,并且本身后缀为.link文件。
通过搜索找到一篇文章https://www.sohu.com/a/387683719_476857
得知是2020年一个病毒的弱化样本,利用LNK快捷方式伪装nCov-19疫情的恶意攻击。
大致流程是运行时会在AppData/Local/Temp目录下加载js、exe文件等用于下一次攻击。该附件的执行流程与所述病毒样本类似,关键点在于拿到WsmPty.xsl文件,查看js可知他写在了\AppData\Local\Temp\cscript.exe目录下,运行fakepdf,在xsl加载到目录下时用notepad打开,内容如下。
1 2 3 4 5 6 7 8 9 10 11 12 <?xml version='1.0'?> <stylesheet xmlns ="http://www.w3.org/1999/XSL/Transform" xmlns:ms ="urn:schemas-microsoft-com:xslt" xmlns:user ="placeholder" version ="1.0" ><output method ="text" /> <ms:script implements-prefix ="user" language ="VBScript" > <![CDATA[ rBOH7OLTCVxzkH=HrtvBsRh3gNUbe("676d60667a64333665326564333665326564333665326536653265643336656564333665327c"):execute(rBOH7OLTCVxzkH):function HrtvBsRh3gNUbe(bhhz6HalbOkrki):for rBOH7OLTCVxzkH=1 to len(bhhz6HalbOkrki)step 2:HrtvBsRh3gNUbe=HrtvBsRh3gNUbe&chr(asc(chr("&h"&mid(bhhz6HalbOkrki,rBOH7OLTCVxzkH,2)))xor 1):next:end function: ]]> </ms:script > </stylesheet >
可见按照VBS脚本正常执行就能拿到flag串,加密很容易hex_to_str后与1异或即可。
1 2 3 4 a=bytes .fromhex('676d60667a64333665326564333665326564333665326536653265643336656564333665327c' ) for i in range (len (a)): print (chr (a[i]^1 ),end='' )
gogogo
考点是拼图、取证和Aztec
首先是puzzle,一共有256个分辨率为160x100,如果用gaps的话需要是正方形,所以先用convert强行转分辨率为160x160。
1 find ./ -name '*.png' -exec convert -resize 160x160! {} {} \;
之后montage指令将图片拼接
1 montage *png -tile 16x16 -geometry +0+0 out.png
之后直接gaps自动化拼即可 size参数选160
1 gaps --image=out.png --generations=20 --population=256 --size=160
拿到passwd: 3e8f092d4d7b80ce338d6e238efb01
之后还有2.raw文件,因为我们有个passwd所以猜测有zip文件,直接filescan找zip
1 2 3 volatility -f 2.raw imageinfo #WinXPSP2x86 volatility -f 2.raw --profile=WinXPSP2x86 filescan | grep "zip" volatility -f 2.raw --profile=WinXPSP2x86 dumpfiles -Q [csgo.zip偏移] -D .
能导出一个csgo.zip,并且能用puzzle得到的passwd解压,拿到二张图片(foremost分离csgo.png得到)
一个是阿兹特克的图片,一个是没有定位符的图片码,百度能确定是Aztec,那个枪名的谐音是提示,啊这。
网上生成一个标准的Aztec码,修复定位符即可,御用PDF修复,不会PS :)。
二、Crypto
babyrsa
签到题,n能在线分解,正常rsa解密即可。
1 2 3 4 5 6 7 8 9 10 from Crypto.Util.number import *import gmpy2n=13123058934861171416713230498081453101147538789122070079961388806126697916963123413431108069961369055630747412550900239402710827847917960870358653962948282381351741121884528399369764530446509936240262290248305226552117100584726616255292963971141510518678552679033220315246377746270515853987903184512948801397452104554589803725619076066339968999308910127885089547678968793196148780382182445270838659078189316664538631875879022325427220682805580410213245364855569367702919157881367085677283124732874621569379901272662162025780608669577546548333274766058755786449491277002349918598971841605936268030140638579388226573929 e=2199344405076718723439776106818391416986774637417452818162477025957976213477191723664184407417234793814926418366905751689789699138123658292718951547073938244835923378103264574262319868072792187129755570696127796856136279813658923777933069924139862221947627969330450735758091555899551587605175567882253565613163972396640663959048311077691045791516671857020379334217141651855658795614761069687029140601439597978203375244243343052687488606544856116827681065414187957956049947143017305483200122033343857370223678236469887421261592930549136708160041001438350227594265714800753072939126464647703962260358930477570798420877 c=1492164290534197296766878830710549288168716657792979479408332026408553210558539364503279432780006256047888761718878241924947937039103166564146378209168719163067531460700424309878383312837345239570897122826051628153030129647363574035072755426112229160684859510640271933580581310029921376842631120847546030843821787623965614564745724229763999106839802052036834811357341644073138100679508864747009014415530176077648226083725813290110828240582884113726976794751006967153951269748482024859714451264220728184903144004573228365893961477199925864862018084224563883101101842275596219857205470076943493098825250412323522013524 p=98197216341757567488149177586991336976901080454854408243068885480633972200382596026756300968618883148721598031574296054706280190113587145906781375704611841087782526897314537785060868780928063942914187241017272444601926795083433477673935377466676026146695321415853502288291409333200661670651818749836420808033 q=133639826298015917901017908376475546339925646165363264658181838203059432536492968144231040597990919971381628901127402671873954769629458944972912180415794436700950304720548263026421362847590283353425105178540468631051824814390421486132775876582962969734956410033443729557703719598998956317920674659744121941513 d=gmpy2.invert(e,(p-1 )*(q-1 )) print (long_to_bytes(pow (c,d,n)))
crypto_Elgamal
1 2 3 4 assert ( A * s0 + B ) % q == s1assert ( A * s1 + B ) % q == s2assert ( A * s2 + B ) % q == s3assert ( A * s3 + B ) % q == s4
第一段是LCG,未知参数是A、B、q
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 s0 = 543263588863771657634119 s1 = 628899245716105951093835 s2 = 78708024695487418261582 s3 = 598971435111109998816796 s4 = 789474285039501272453373 s=[s0,s1,s2,s3,s4] t=[] for i in range (len (s)-1 ): t.append(s[i+1 ]-s[i]) print (t)nn=[] for i in range (1 ,3 ): nn.append(abs (t[i+1 ]*t[i-1 ]-t[i]*t[i])) print (nn)q=gmpy2.gcd(nn[0 ],nn[1 ])
现在我们拿到(g, h, A, B, p, q),之后是ElGamal解密,可知他在生成r时也是线性同余,并且参数我们已知。
参考:2018 Code Blue lagalem
https://ctf-wiki.org/crypto/asymmetric/discrete-log/elgamal/#2018-code-blue-lagalem
即
1 2 3 4 5 6 tmp = gmpy2.powmod(c2, A, p) * gmpy2.powmod(h, B, p) * gmpy2.invert(c2_, p) tmp = tmp % p gg, x, y = gmpy2.gcdext(A - 1 , p - 1 ) print (gg)m = gmpy2.powmod(tmp, x, p) print (long_to_bytes(m))
如果是直接解出的q这里的gg则是7438,即我们求出的m 是 pow(m,7438,p)而7438是p-1的因数,所以rsa也不可解,可知在推导中如果我们取q`=k*q,那么A和B的值会变大,但是LCG的结果不会改变,而我们推导中用到了A和B的值来求t和gg,当我们改变q时,gg的值也在改变,故当gg足够小,为1时,则pow(tmp,x,p) = flag。
综上exp为:
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 import gmpy2from Crypto.Util.number import *s0 = 543263588863771657634119 s1 = 628899245716105951093835 s2 = 78708024695487418261582 s3 = 598971435111109998816796 s4 = 789474285039501272453373 s=[s0,s1,s2,s3,s4] t=[] for i in range (len (s)-1 ): t.append(s[i+1 ]-s[i]) print (t)nn=[] for i in range (1 ,3 ): nn.append(abs (t[i+1 ]*t[i-1 ]-t[i]*t[i])) print (nn)q=375487011827303788911792498742822292475328703777 *3 A=gmpy2.invert(s1-s0,q)*(s2-s1) %q B=(s1-A*s[0 ])%q p = 65211247300401312530078141569304950676358489059623557848188896752173856845051471066071652073612337629832155846984721797768267868868902023383604553319793550396610085424563231688918357710337401138108050205457200940158475922063279384491022916790549837379548978141370347556053597178221402425212594060342213485311 g = 27642593390439430783453736408814717946185190497201679721975757020767271070510268596627490205095779429964809833535285315202625851326460572368018875381603399143376574281200028337681552876140857556460885848491160812604549770668188783258592940823128376128198726254875984002214053523752696104568469730021811399216 h = 54585833166051670245656045196940486576634589000609010947618047461787934106392112227019662788387352615714332234871251868259282522817042504587428441746855906297390193418159792477477443129333707197013251839952389651332058368911829464978546505729530760951698134101053626585254469108630886768357270544236516534904 c1 = 60724920570148295800083597588524297283595971970237964464679084640302395172192639331196385150232229004030419122038089044697951208850497923486467859070476427472465291810423905736825272208842988090394035980454248119048131354993356125895595138979611664707727518852984351599604226889848831071126576874892808080133 c2 = 48616294792900599931167965577794374684760165574922600262773518630884983374432147726140430372696876107933565006549344582099592376234783044818320678499613925823621554608542446585829308488452057340023780821973913517239972817669309837103043456714481646128392677624092659929248296869048674230341175765084122344264 c1_ = 42875731538109170678735196002365281622531058597803022779529275736483962610547258618168523955709341579773947887175626960699426438456382655370090748369934296474999389316334717699127421889816721511602392591677377678759026657582648354688447456509292302633971842316239774410380221303269351351929586256938787054867 c2_ = 64829024929257668640929285124747107162970460545535885047576569803424908055130477684809317765011143527867645692710091307694839524199204611328374569742391489915929451079830143261799375621377093290249652912850024319433129432676683899459510155157108727860920017105870104383111111395351496171846620163716404148070 tmp = gmpy2.powmod(c2, A, p) * gmpy2.powmod(h, B, p) * gmpy2.invert(c2_, p) tmp = tmp % p gg, x, y = gmpy2.gcdext(A - 1 , p - 1 ) print (gg)m = gmpy2.powmod(tmp, x, p) print (long_to_bytes(m))
三、REVERSE
EasyVM
修复花指令,patch call的第一个字节,之后观察控制流,尝试后在except异常处理块发现关键函数sub_4012F0
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 int sub_4012F0 () { _DWORD *v0; int (__thiscall ***v1)(_DWORD, void *, void *, _DWORD, _BYTE *); _BYTE *v2; char flag[256 ]; v0 = (_DWORD *)sub_401889(0x24 u); if ( v0 ) { *v0 = &off_40A0D0; v0[1 ] = 0 ; v0[2 ] = 0 ; v0[3 ] = 0 ; v0[4 ] = 0 ; v0[5 ] = 0 ; v0[6 ] = 0 ; v0[7 ] = 0 ; v0[8 ] = 0 ; v1 = (int (__thiscall ***)(_DWORD, void *, void *, _DWORD, _BYTE *))v0; } else { v1 = 0 ; } scanf ("%s" , flag); if ( strlen (flag) == 42 ) { v2 = sub_4011E0(flag); if ( (**v1)(v1, &unk_40B030, &unk_40B050, 0 , v2) ) sub_4016F9((int )aCongratulation); else sub_4016F9((int )aUnfortunatelyI); } else { sub_4016F9((int )aVerificationFa); } system(Command); return 0 ; }
可知flag长度为42位,加密流程是sub_4011E0 和 v1指向的函数。
sub_4011E0是个魔改的base64,修改了表的值通过异或。
1 2 3 4 5 6 7 8 9 { v6 += 4 ; v8 = *((unsigned __int8 *)v7 - 1 ); v7 += 3 ; v4[v6 - 4 ] = aAbcdefghijklmn[v8 >> 2 ] ^ 0xA ; v4[v6 - 3 ] = aAbcdefghijklmn[(*((unsigned __int8 *)v7 - 3 ) >> 4 ) | (16 * (*(v7 - 4 ) & 3 ))] ^ 0xB ;/ v4[v6 - 2 ] = aAbcdefghijklmn[(*((unsigned __int8 *)v7 - 2 ) >> 6 ) | (4 * (*(v7 - 3 ) & 0xF ))] ^ 0xC ; v4[v6 - 1 ] = aAbcdefghijklmn[*(v7 - 2 ) & 0x3F ] ^ 0xD ; }
v1指向的函数是虚拟机处理函数,传入的参数依次为opcode,密文和经过base64处理后的密文。
结合动态调试可知是按字节处理第三个参数,当前直接与前一个处理后的字节异或后在异或0xee在和密文比较(第一个字节异或0异或0xee)
综上编写exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import base64enc=[0 ,0xBE , 0x36 , 0xAC , 0x27 , 0x99 , 0x4F , 0xDE , 0x44 , 0xEE , 0x5F , 0xDA , 0x0B , 0xB5 , 0x17 , 0xB8 , 0x68 , 0xC2 , 0x4E , 0x9C , 0x4A , 0xE1 , 0x43 , 0xF0 , 0x22 , 0x8A , 0x3B , 0x88 , 0x5B , 0xE5 , 0x54 , 0xFF , 0x68 , 0xD5 , 0x67 , 0xD4 , 0x06 , 0xAD , 0x0B , 0xD8 , 0x50 , 0xF9 , 0x58 , 0xE0 , 0x6F , 0xC5 , 0x4A , 0xFD , 0x2F , 0x84 , 0x36 , 0x85 , 0x52 , 0xFB , 0x73 , 0xD7 , 0x0D , 0xE3 ] m=[] for i in range (56 ,0 ,-1 ): m.append(enc[i]^enc[i-1 ]^0xee ) print (bytes (m)[::-1 ])a=bytes (m)[::-1 ] s='' key=[0xa ,0xb ,0xc ,0xd ] for i in range (len (a)): s+=chr (a[i]^key[i%4 ]) print (base64.b64decode(s.encode()))
babyre
开局多种花指令,简单的跳转直接nop即可,有两种会影响反编译并且比较有趣。
1 2 3 4 5 6 7 8 9 10 11 12 13 .text:00401838 push eax .text:00401839 mov eax, 7F h .text:0040183 E test eax, eax .text:00401840 jz short loc_401847 .text:00401842 call near ptr loc_401847+1 .text:00401847 .text:00401847 loc_401847: ; CODE XREF: .text:00401840 ↑j .text:00401847 ; .text:00401842 ↑p .text:00401847 call fword ptr [eax+58 h] .text:00401848 loc_401848: ; CODE XREF: .text:00401842 ↑p .text:00401848 pop eax .text:00401849 pop eax
第一种分析知eax被赋0x7f后jz跳转恒不成立,故通过call来跳转,会压入地址进栈,nop下面call的第一个字节后出现pop,其实是恢复eax值,所以从push eax 到pop eax全nop掉即可。
第二种更复杂一点,不过也有push和pop恢复现场,全nop掉即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 .text:00401 A28 push eax .text:00401 A29 push ecx .text:00401 A2A call sub_4010A0 .text:00401 A2F add eax, 15 h .text:00401 A32 push eax .text:00401 A33 pop eax .text:00401 A34 mov ecx, 0F FFFFFFFh .text:00401 A39 xor eax, ecx .text:00401 A3B push eax .text:00401 A3C pop ecx .text:00401 A3D not ecx .text:00401 A3F push ecx .text:00401 A40 retn .text:00401 A40 ; --------------------------------------------------------------------------- .text:00401 A41 db 7 Eh, 0F Fh, 15 h .text:00401 A44 ; --------------------------------------------------------------------------- .text:00401 A44 pop ecx .text:00401 A45 pop eax
简易版IPYTHON脚本手动去除,生成patch文件。
1 2 3 4 5 adr1=0x00401A28 adr2=0x00401A46 for ad in range (adr1,adr2): PatchByte(ad,0x90 ) print ('yes' )
之后查看控制流,发现ollvm混淆的特征,起初想找个自动化的脚本,可惜没找到!
https://www.52pojie.cn/thread-1488350-1-1.html
基本思路是,每个真实块的首地址下断点,运行并记录来还原,因为我们主要关注输入的变化所以,在所有输入的地方下断点,之后调试,多次尝试后执行如下。
第一块
1 2 3 v31 = ~*(_DWORD *)&input_str[4 * v30] & v31 | ~v31 & *(_DWORD *)&input_str[4 * v30]; v32 = ~*(_DWORD *)&input_str[4 * v30];
该块为与或非来实现异或,v31依次与所有输入异或(4byte一组)。
第二块
1 2 *(_DWORD *)&input_str[4 * v29] = ~*(_DWORD *)&input_str[4 * v29] & v31 | ~v31 & *(_DWORD *)&input_str[4 * v29]; v32 = v29;
该块为输入4byte一组依次与v31异或。
第三块
1 input_str[32] = (input_str[v28] & 0x98 | ~input_str[v28] & 0x67) ^ (input_str[32] & 0x98 | ~input_str[32] & 0x67);
把进过异或后的所有字节异或到一起填充为33位,用于接下来的base64运算。
第四块
1 2 3 4 5 6 v15 = v24 & 0xF4 | ~v24 & 0xF0C4020B ; input_str[v20] = v15 ^ (input_str[v20] & 0xF4 | ~input_str[v20] & 0xB ); input_str[v20 + 1 ] = ~v23 & input_str[v20 + 1 ] | ~input_str[v20 + 1 ] & v23; LOBYTE(v15) = ~v22 & *(_BYTE *)(v20 + 10825322 ) | ~*(_BYTE *)(v20 + 10825322 ) & v22; *(_BYTE *)(v20 + 10825322 ) = v15; v32 = v15;
这一块比较复杂,结合动态调试,观察输入的变化,发现是3个一组与3个数异或,而异或的三个数是前一个数据base64加密时转化为的前3个下标。
在执行时这个异或是从当前开始,之后所有三个一组与该组转化的key异或,第一组不做处理。
第五块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 v24 = (int )input_str[v25] >> 2 ; v10 = ~(16 * ~(~input_str[v25] | 0xFC )); v11 = ~((int )input_str[v25 + 1 ] >> 4 ); v23 = ~(v11 | v10) | (((int )input_str[v25 + 1 ] >> 4 ) & 0x24 | v11 & 0xDB ) ^ ((16 * ~(~input_str[v25] | 0xFC )) & 0x24 | v10 & 0xDB ); LOBYTE(v11) = ~(4 * (input_str[v25 + 1 ] & (input_str[v25 + 1 ] ^ 0xF0 ))); v12 = ~((int )input_str[v25 + 2 ] >> 6 ); v22 = ~(v12 | v11) | (((int )input_str[v25 + 2 ] >> 6 ) & 0x11 | v12 & 0xEE ) ^ ((4 * (input_str[v25 + 1 ] & (input_str[v25 + 1 ] ^ 0xF0 ))) & 0x11 | v11 & 0xEE ); v21 = input_str[v25 + 2 ] & (input_str[v25 + 2 ] ^ 0xC0 ); v32 = 0x7F ; Str1[v26] = *(_BYTE *)sub_A22210(&off_A52E38, v24); Str1[v26 + 1 ] = *(_BYTE *)sub_A22210(&off_A52E38, v23); Str1[v26 + 2 ] = *(_BYTE *)sub_A22210(&off_A52E38, v22); v13 = v26 + 3 ; v26 += 4 ; Str1[v13] = *(_BYTE *)sub_A22210(&off_A52E38, v21); v20 = v25 + 3 ; v19 = 0xC90DC21D ;
base64加密,动调测试得知3byte变为4byte的每组6bit的顺序没有变,只不过用与或非实现起来比较难读,这一步和第四块是交替进行的,每进行一次第五块再进行第四块,base64的表需要动调获得。
通过&off_A52E38间接寻址获得。
耐心调试,了解块的执行顺序后梳理解密逻辑,先是base64换表,之后逆第四块的异或,再之后需要拿到v31,假设我们的base解密后的为 c d 即 a^v31 b^v31 而 v31是由a^b得到所以 c^d == a^d 其中异或的v31抵消了,因此异或的4byte的key可由密文4个一组异或得到,之后再进行解密。
完整exp如下:
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 import base64s='Fi9X/fxX6Q6JBfUfBM1V/y6V6PcPjMaQLl9IuttFuH68' t1='QVEJAfHmUYjBac+u8Ph5n9Od16FrICL/X0GvtM4qk7T2z3wNSsyoebilxWKgZpRD' t2='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' c='' for i in range (len (s)): c+=t2[t1.index(s[i])] cc=list (base64.b64decode(c.encode())) t=[] for i in range (0 ,len (cc),3 ): t.append(cc[i:i+3 ]) print (t)new=[t[0 ]] key=[] for i in range (1 ,len (t)): tmp=[0 ,0 ,0 ] a=base64.b64encode(bytes (t[i-1 ])).decode() k=[] for j in range (3 ): k.append(t2.find(a[j])) key.append(k) for i in range (len (t)): for j in range (i): k=key[j] for p in range (3 ): t[i][p]^=k[p] print (t) m=[] for i in t: m.extend(i) print (bytes (m))mm=[0 ,0 ,0 ,0 ] def xor (a,b ): for i in range (len (a)): a[i]^=b[i] for i in range (0 ,len (m)-1 ,4 ): xor(mm,m[i:i+4 ]) for i in range (len (m)): print (chr (mm[i%4 ]^m[i]),end='' )
re1理解错了,之前也没有编写解释器的经验,后续会复现补上。
同时赛中花大量时间去搞Android了,可惜最终没能调动起so文件,可能是模拟器有点问题把,一直attach目标程序不上,但最终收获也是蛮大的。
这次写解密脚本失误有点大,差点re3就废了,看m师傅用z3解的,确实又快又准,自己还要好好加油哦