From 461be07d8c2f3a1d5d7817bf424c6e78141aa37f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20De=20Keersmaeker?= <francois.dekeersmaeker@uclouvain.be> Date: Wed, 31 May 2023 10:40:14 +0200 Subject: [PATCH] If packet not tweaked, loop on lower layers until edit or not supported --- src/packet/Packet.py | 32 +++++++++++++++++++++++--------- src/packet/Transport.py | 5 +++-- src/pcap_tweaker.py | 35 ++++++++++++++++++++--------------- traces/udp-stream.pcap | Bin 0 -> 6636 bytes 4 files changed, 46 insertions(+), 26 deletions(-) create mode 100644 traces/udp-stream.pcap diff --git a/src/packet/Packet.py b/src/packet/Packet.py index 9808fe0..85d3d20 100644 --- a/src/packet/Packet.py +++ b/src/packet/Packet.py @@ -97,17 +97,20 @@ class Packet: @classmethod - def init_packet(c, packet: scapy.Packet, id: int = 0) -> Packet: + def init_packet(c, packet: scapy.Packet, id: int = 0, last_layer_index: int = -1) -> Packet: """ Factory method to create a packet of a given protocol. :param packet: Scapy Packet to be edited. - :param id: Packet integer identifier. + :param id: [Optional] Packet integer identifier. Default is 0. + :param last_layer_index: [Optional] Index of the last layer of the packet. + If not specified, it will be calculated. :return: Packet of given protocol, or generic Packet if protocol is not supported. """ # Try creating specific packet if possible - last_layer_index = Packet.get_last_layer_index(packet) + if last_layer_index == -1: + last_layer_index = Packet.get_last_layer_index(packet) for i in range(last_layer_index, -1, -1): layer = packet.getlayer(i) try: @@ -120,7 +123,7 @@ class Packet: protocol = Packet.protocols.get(protocol, protocol) module = importlib.import_module(f"packet.{protocol}") cls = getattr(module, protocol) - return cls(packet, id) + return cls(packet, id, i) except ModuleNotFoundError: # Layer protocol not supported continue @@ -128,19 +131,21 @@ class Packet: raise ValueError(f"No supported protocol found for packet: {packet.summary()}") - def __init__(self, packet: scapy.Packet, id: int = 0) -> None: + def __init__(self, packet: scapy.Packet, id: int = 0, last_layer_index: int = -1) -> None: """ Generic packet constructor. :param packet: Scapy Packet to be edited. :param id: Packet integer identifier. + :param last_layer_index: [Optional] Index of the last layer of the packet. + If not specified, it will be calculated. """ self.id = id self.packet = packet - try: - self.layer = packet.getlayer(self.name) - except AttributeError: - self.layer = packet.lastlayer() + self.layer_index = last_layer_index if last_layer_index != -1 else Packet.get_last_layer_index(packet) + self.layer = packet.getlayer(self.name) + if self.layer is None: + self.layer = packet.getlayer(self.layer_index) def get_packet(self) -> scapy.Packet: @@ -171,6 +176,15 @@ class Packet: return len(self.packet.getlayer(layer)) + def get_layer_index(self) -> int: + """ + Get packet layer index. + + :return: Packet layer index. + """ + return self.layer_index + + def rebuild(self) -> None: """ Rebuild packet. diff --git a/src/packet/Transport.py b/src/packet/Transport.py index 444149d..9109bac 100644 --- a/src/packet/Transport.py +++ b/src/packet/Transport.py @@ -20,8 +20,9 @@ class Transport(Packet): def tweak(self) -> dict: """ - Randomly edit destination or source port, - in this order of priority. + If one of the ports is a well-known port, + randomly edit destination or source port, + in this respective order of priority. :return: Dictionary containing tweak information, or None if no tweak was performed. diff --git a/src/pcap_tweaker.py b/src/pcap_tweaker.py index 47a7df5..16dbd65 100644 --- a/src/pcap_tweaker.py +++ b/src/pcap_tweaker.py @@ -96,25 +96,30 @@ def tweak_pcaps(pcaps: list, output: str, random_range: int = 1, packet_numbers: if must_edit_packet(i, packet_numbers, random_range): # Edit packet, if possible - try: - my_packet = Packet.init_packet(packet, i) - except ValueError: - # No supported protocol found in packet, skip it - new_packets.append(rebuild_packet(packet)) - pass - else: - d = my_packet.tweak() - new_packets.append(my_packet.get_packet()) - if d is not None: - writer.writerow(d) - finally: - i += 1 + last_layer_index = Packet.get_last_layer_index(packet) + while True: + try: + my_packet = Packet.init_packet(packet, i, last_layer_index) + except ValueError: + # No supported protocol found in packet, skip it + new_packets.append(rebuild_packet(packet)) + break + else: + d = my_packet.tweak() + if d is None: + # Packet was not edited, try editing one layer lower + last_layer_index = my_packet.get_layer_index() - 1 + else: + # Packet was edited + new_packets.append(my_packet.get_packet()) + writer.writerow(d) + break else: # Packet won't be edited - # Go to next packet - i += 1 new_packets.append(rebuild_packet(packet)) + i += 1 + # Write output PCAP file output_pcap = "" if output is not None and len(pcaps) == 1: diff --git a/traces/udp-stream.pcap b/traces/udp-stream.pcap new file mode 100644 index 0000000000000000000000000000000000000000..c9a4ab0e54dce3380a0474f3a3db09733a2174a9 GIT binary patch literal 6636 zcmaKsWl&t(qHeo!cMa|m+}$B)@ZcIWSO=G&0fGk7xJ&TH-Q5~@mjD5R2Y0w(zq9L| zeb244s=lf<|BNwf{unbo)j2Q#IKcn^-~hls2eK>?a}q3cfb^gLmswcVT-j(;_Gw9L zeMSPv0sy=eo)Q2F?6c6$BA`o<%}?GO{00dC6^jt<GXNhzVue3e{W$Pbnm5CGD#zVE z@Z!q-1*!bskzRixL;ejZ`3EWZUy-cuFaSn>Zul~hQKQ`A`G32m>Qdkfa@qR@`Dbg1 zHgj<R;p7ooN&tI+sfQWZ^;KqY*H%xrjumc04Jn9iO3A?3CfzphZ4!|={i|O53?R+J zcr_XFoWslgmvt}z_J2e#`wyA%QqW(r5C4!U&M48+0K@_Y0SNHL@4+}&q;Xw9-1zK; z{Wlog<aP6`h%NJ5`P()1QoJ{@ww8LYzhqP34s`?o06+(Mf+8B8&Ay%QFK%oj41n?< z@5}zfEw=ReFL(Gq+{{ukwA29d0JBK_kmXLi7eAq4MN0A~1LZ>ZW)7_ac{6V67q*Zo z20-Bt_htXV7G4Veiyiq7HiPHkpGPG5^N8kPSA6!DN1U+bMP<|Jid(m6Z!&Q5egWrN zVgP2~0e|g(gW>y^LSDeL&;JgFuMYSFhA;R7e*ZT(ms5+O_xj#^*A_^xt%QInsLm!s zIBL5YgJVn0_$8Twx2t+bcr}+e(9qM+ya8+<amQk1y(nfl`&&qQ$bYRT1j`eQ2iKm0 zaz@)Rv!yp(3GBOC3_SCNWW=RL__lx|AQs%oGRNNo-CKgz^ASXwnum4o`$rh{=iw-} zkh!N~-=cod4YY+!ajsXRw(yr!{c3Ec9ph=V4bl^7e2iZ3TI`~efGvv9-554I5|NbW zGjOJ4+7$CsYu-X(;2yB}JT<_<cs6GDqivk72=_*Z!|+Ecd%{QojSft<W^7Nbe<GgP z8My;`X>|W&i;+>0LTHt0Z-IA|5oqk*``N7Y?t!N~;_m0WQ3t60BMK1319|Z$Ph#mV zAH@+q!k3=0_S}I~_b)5&lJZ7F^kp|k9p{82-shal+Jirjxd5P@&guqy59RL^&Ui5k zD}2pE(h-8I5s{?DR+u8!S2C82rP?%U0klL+MR)>Y67)wIf~!e!HJYY|xET`Y@No+( z1tkH$Fci2<U(cbs_d=~r>7R57ZcgdHFjR}e?$ha2$s!D8SICr&xdg^UwYUG6vsCm! zV>bPe+xUS}pZ@iwS^T+`Nh6aKr!^HWM2Iy^18!r}*9YCpt2!}yh02a$-w>XHeN1_P zDh#vsz^ILrkd>PB&1hT)M4*^K%by_&A>mxp{I&#K6^(kRUbx^1SNQlVv}R`7*wd3C zG$d%EE$2Q*j7-Kmcq4#@%?>9sR*(HmhmiBsAdl}{(ZnnPbB&1{{J`6MCqm1%dwqL% zStNe;OT{mO+ONqKXIJ?Zn;S@(hNOcfVOBMEN_MlhL2tJZ1x6zWZcfj>jQ@HkH?Ym3 z#gY$b9hjyF*C4kl*!@1=S-`m;nHZ#XKTTgy$t8v_7L!n3z)@2eies0(;wq`$Wo)5p zT-TDiX$rk9Xanci@cD_b_%R5w;Ipc7+Shgbc;x!zbv`kQsDY(8u}8nqWA1~|!jsIO zQBOL|8}%G0bF(;8iS5_n+0TCG5_&?0j6@}Z*60xdZX`{ebevShAw;f7uMh@71z6w8 zrvqRG+OKU{8co7DMq?2c!U!I8LtAQmrm|_L_ii3a4P~DA9Rgdps|!^UsK+ohf(Yvi zy~pfxe{17dW4rSd0VsalMVii?45P%+fy{yHS`=zi{xmyxPc{k!b5+jR7|wC7hNo|2 z(2#bTp})wQd?@D{R#V~K-UyE7)X&QJ{k*_=hZ#CNSd=Kr%)yYp=l8o8fle6jiO057 z%GN6SG>YrvqGn@bc2wQ*{6lrA4RHzB2GA<o1ACRF^u>!cRhO|o`zBKmEoA+JM^KM+ zSJVkgA-u8Y@cu_lk;MMfcHRavimi{`7r&kR13$uz4E7&XbRPEv7&->DPR(VBHL*UU z`!q6tKQEWIW)0md9bMH8c)z<BJ<`xusk*c0uWgkY0cAhm@KQ(I_i2wDx*hsy?g=3- ztb3AUzATGlX)Q!3;O+w^WqjC~cvP<yOlH<lPFv#f2`4DpuUXJAxRbtAlG;b+{X92T zL|6cQ-AboT+IK88n@YZ?W+B6dOgAwS*IhXp8@E6Bt-@L2F^QOGcvF_l$XM?&z5`-r z>>+MU@hbK(`^*+6?`IudZ1YFZZ>2ZAH%-D`G0zeZ%K_sHw!(F@ORv0|Vy?Q|QIA6h zL``2^$~>?=po0fCoL<}+zQ0u~aEA1W@P4ShRUd|nAGcL<Ou@4vMS^={;v3Qqvs_UK zlf{Yct9c@WfsyyyNNI{abzdu#K9QF8Nq(<1CmIgEHMgdA<Qoa=hJ0sMarLGKjvQu= z#5n2<-r%tXZ=!+7L%!d|fzHS9*=9b&?RlsjedLb--h>8#4bLVqVYAHovuMgV#M#(Q z&z>#~$X3kPIRzbx$^SH-&gcL2{iXB&N8>~PYW&kb8-FYGuf|*a-T33ahj~xEy1A?G ziDY(s4`<Exx;++g81lP1gji2p3t=T}nI!&>VMC>DqUrmpkMD###(^XaZPS#$xe~cJ zDq-%x0`2s{Ub}Vlxov8UXKdTbnst}q!U(a?O9hBeF|&Bq?K}HNgiGEXhQ-DjbI(%z zTFfUYQw0nt#A2E#nxVqs3#56gOd{)kD~KB5dptKjdn)B6Hp9;VMmvk&U6gwEHILL$ zWi4nv5>7HIW0A7TapiC&<WLx~V%U_l#z;9ouLg<ZY2p#89|EIs%P`0uECO$+ua~R) zBu=6q>4cL-4&a!SVh1s`m2j&T1@X1=ajqb3a=on_F7PS*r~61$IGCq<QX|3Asxdq6 ziwPo0y*9s5cF|m*gu!kjxVjthHei#QQIL=JML6{RI_`xEYuMg9=~%_;3N)eay2HWg znlqx0>R&QH*H@~iQ9WBC%idrITdJDI-XMNg^gE`BlNkB6Pzxk=_L+35oYU6%8La@K z1Vo>tZFUEK$ZKwTkV!a+LbsF(X3FdG`NXVcSTA0jw9dA>sJ}No!)Lu~D8#tjgxVPW zFaw_OQhm<xMaq`qAQmbgsx7&G?W#}E&JW4c4vRBym5(VG3SA?`-&I}L(*6coSzAlt ztR4;>MlYKDMV<9{c5|klwHRu!-BYz?EZz6y0;0C>X9-xNoueR$a;CeK37gb?M?*16 znOM-jlRbdNc9fNj|Mo-c(^#-b#>MW08PgjQjIE<CI$2wOCQwnT))vZo!<W3eXcP}c z-bmE}Ts1TAc{XNm2V%Kj%t#SVRpVe+Nijd$^9^{}!rfGYLH!5^uS*VX|4#A7&+RIt z*pP=wUnXa0Jbawr@}x9|Y7i1A$hOXKOwOqZ2bTf^mw4EzdgAr!hk)YtCsQ{iyLG$A zVe61{i_Gi@0?lSul@I{q+|m}mGb9crg8DgGzsJ-IY1ZEU5ipwZ*5gVwkPrFmGc1}& zdwiXm@w{LS_gKaXMdy7I4pZ9n!Kp-LXZBjk1aylqO|QX<nx=WU$^aHimK58yKXT0Y zmG{zXWK5=77Ll@<wBrSW(5x>I?Agh;XRwa}oV=2u6l5FffZe7z!iI}yt*@afaoh|; zJ6r{o=~W|l501SMbgdPtiT)PUhuAHFL^+yklK~TA;=)hE6gvJSKD*QU<kAKnk%%QQ zlwV!WlS&DM;jN5xLbi^Mldj#N{Yuz~$Hf<|vMBW>cQ-k6f&<kR(@bMRDGAO=9=_7! zNM?((&$jlcR-jIcFvIG;WI@d)0+<2QnI(*-DoMZMdWuBn0c>O6oUvk15tg#KJ<EQr zsTZ8+B^DN&5M<leEuwm&^Sl1ZIK0xz=etw*Dd%Y0IoNL(^W^kEr+QNH-x!qHGIk)r zPL@Yk29AP7JmA26AB`L|X9_I(EMQHz;@zA0y@_{!1>I;HUA~+qfKY@Fv=M=hWJ~eD zeCA#u`UXbmb!~$^*Kw4Y0%D6D)B}vf_A1!Kl_Pj{V33WwP(sI@ldCp9-#p>7&nAFP ziR!Y)B5;Ssz_kxD9Y{p$FCw!pjzghK<$2N?3^|g7oemLH3w5uU@10BcGmCusZJDeQ z$Bx54^r33=M=#xu?B>i(JoK1KMgG?e9CAwS_U?(iBS|<$j5GbETD?0yY3MBX0jUml z!QGV12Z9*j%~=Ykne>7<_M0n)IT>{e$bnm09sMB%sdNjM&1p+Y5$c4)t$17d>Bkc= z@T>&FLTf5AFh|^jUc`fOoyC0I&-x=2j|g9*rDEt}U7BN9`rEVMog|vJ`U=|*$i#lA z?_h#<{)lK-R!u+rcrb67|Ese*;^ZB+ZtUWi@B8+f;cE~>ROsd)h|BFr9~8_GtumR@ z`2&QxfR<ad?Sr!t=8Qqt%+^k>|I+w8-~Zb9FMl=u@t=(^>ibvYE&pzO$5A1Jj*G^< zwqI@j8O*m#M$wVV&iyzC&^9z&@Eum#`kfGE#4_Bx1-DCCnNe`!L~T~b8w%k_O)0O# z4mVnUnV`D<T}9Wimc6Gu<OF5od_F~;%8R2*&t@_N7l1U@wKS@e-*$|<A+Bnl+2M3A zcl&`t%N;Rec*}WsBaPVCRwChdYgA4rRJTV7dIH!V*Q_B)-1|;$k=Lr88BW>_fkSxm ze6u}#CnC<G!)8HZJ$FTG1CL=7UNU4T$BOYEW`qlt&oR(ANNM_`-pm*Os;~|(zFBf@ zva+oD6&Cq4iQ*<!Dx;uF2qp_c>kf^A`_3fWa@vWf8Z{PMwW6TAx(OiAkkc5iWK8h( z_H`erf8Vm#%D1t(E#|a)^jh4Q@!AFH_SnM@#1ef!G%`YxJ6Da+#i#R3X(Wyj4UhAH zMV>SS3Hvb8_DPf9_#tKU8~sr0t)J9eH;ix|?(;q8at^q)Zo1p*jUNPNW*z8cz>eyt z+Eu@bqaP_}rS7rGYMoF?mf&ZI2L7Gy0V?))Wn>T9#42xp$)~}3wyW=QavGyexe+XK zy;Qf8G{t7t1e4)O9nI#a%?CK`wQ#`Lvg)R_H<a6MQ82h}HDU^TCx`t<<3SS;JM9On zMQ7-lz_rYruEdC)lkijUl|iFfpMYG;Gi*M0$OZ-F6&u=UZgx~Fj*CpL>PjbV24($7 zLNLCgPJYn-rlk~Kyv1EPER~%+nSz0YIF^<KUWy~!cCs{V$dEhTH@)IHBJXOPCJDoy zOU2UAkjSNGTrNt<Kyt6HTJG{N`#VcTp{w1FnSl_=ZFN|k%KcjAtz_`m8vY!NQSE`W zVd}i`w9m)3xc5rr#DXYcaSva;;0{YP6(!!YPAR}*oH7LG56%$PS`Wxix<4gDeg<#d zY{ulSumpr#`d_;@;WA$|KJPC?Jk}7eGIz!Af}Z@>?PO&U85~<FvYko&yjIfN-Nosf zWI9Gs(?ieT)1{-1N$*mLui6c@{ZomGE1_$oYM=D(gPBf0pV&h=5w*Wd5`qEZfhm|J zbac@TG*L!Cz9eqdk66d&&y)7GNFhWT>KSyT7>jO}>6KNV(OxGN6*h8qVf7Qi#FlzE zymqY9rA`lSkc;@z5=7yvH#HozGQxUa)E?*&Rw{C)vl@uxYsBEuxO!TiT)3_(-pJRk z&NQ{Ktiw?^>Y)4X6Vbvbx46!5$B2-;l&yL00z<UBDhlyk=Af{{dbt|KuJu)di9>y3 zYT8+(Lc5)qU!5{^Fo&RucgOjVLU-ldB>P>+G>3O*mpMD4hc^S-JMJ>Gc(+8Inl|OE z?0jYpFE-VifQcHx4x<jrt)PZG{PML4+;VsZ@J9RTf<I{b!0=M~b!-u|HsM#KFSeI^ zx)x@FvXPwD3GgE5touF0L~lfZ3_HdkGd=_h;RH-IS?Wtd)PkruS9Ab_K<6c9-xVU% zXznDyd~p2x+JD*e3f5SVkW{oI0=nucSqHJciKiELw7_4F*fS{ZuW6EYFD}swfTfK} zl)K=-=`Ka8`7n-bCCggb6qMFpV|!V1V5WEfjd&y4kY*KQxkEG&ZmWr+v2rA{^E5Hp zZkE^wWnqIEy-Hx5mrSCUD1!G(f4%8vR6!esw86{Sof}}JQf`pYVOiVIV2Z|&ZW~Gh zQ*?QFEoj5B%jIC%dDo0xxad+baWh6y$J0u#DgivEjIhX1`&i+_=xe!abn6Tz+hUth ze73sNm0~5a_<@>Ved4i?!FEoIT&rL-9E&Aet>T_v_VxOJP|#)cAuW%<(9!-5#FTCk z5VNSJhl|%SHBxd&(*J2bK<|D__w$<oiN$=Alb=X+l%ATpd9{N0v=TD-;xh1F8~BKL z<_5Sj8u8R@UGq?kI{d87BR2Qo6Dcjlm&Qv4{MW{Z{nhw~e>NVq;9rfm`n&PuN+0W< z(;VL*tE0E4&DRcZRt&d<$LhGM^whxbBjK16CBAL_%}qk9s14AR|MD4WN%32;)K_mV zSnj;F3|^_ag7=D!9JgyxB$=DF@+~5m<A5jRQ*V`Th(P70hVsG~FwS<ea%U9woKVqM z`%ku~ILW`zTAnT2*k66Tb!6bkDU;uY^}sNytx**;bt?jrJF=K{iwy~#ks6c1TgBU8 zwaCt7LY8<(iF`7BBFtH%=epQsNY5<~c7C(D@D0$-cS#=)pdU&WNT4FN;rfhBSv0jC zrOl~d=2r30>|4k8?Qj!pWH5UUQV5vLA!q#DNk}uEpGrp0a~oN~k@XU^*J0rFgVa`z zsk9fKuWi!rnf6g3T`UnN4@{sngl1fpT3T=;I~|%|O={!p#IO+18aH+tkjkNbv(iVE zPd^KjOmqV)<|vMRdq<SJou{Y~?08itr$b3rgo#&;7<#7bJ}oK*e)3CeOkq5*{3KPe z&0~3%jrAcOH=g~RQ=V5kMIV<2)2$_-eM;TtcNM2wii*MkgU__tJD6lMNJ1PsgGh4+ z9p`rMX4OWk<xdF}0Z66l`&;3T9s@=lTf>cIkR$6PVPUnHC84GzIXa!+YTjmH&(W1L zC=%Bcr_#LpD^H~Hycv5D;{z1mR3*mf?I#}l6BH1YW!)m2nN@1C?%3Y@W?iy^PlP${ z-$VxJy^|}oaa^!kejpt(v~0)<lY&@GKs{grCwj0&)DiazpfDpT!-2nb<aJk!CJRIi zFrg#YMs%8W?vNp&aBf%!L;jm<#*k&L%)z12*V+Xr8V3{Wj6#f4R8InTp}8t|gzKvc zttYGTDs!r470t{`cYcyKSW(V;C6<Kn>sCfGQSTND=Y&U1l7`Rib+B5Vt^xWRC3{oN zbfH&`aB)H%_t?l^cyCrqXo6z~n0+D>T{`b_zL;_|+&#GxKVD1i(gj~|86c!qSF4-r zqu$kgl-LRjG?G%J6WX)WwM=+V8}^Dw)qKgsYAEy7;mCmZp$-8r@iv1~>Y6=R9=gd_ z01MSFog_277hhv>?lXO;*9J<eQR{3FvbwRz*Le~<yU>BKfE4o`>pJ-bHt~6Jd~LCG z1`fsc?A&t)pFY$e;5cb!tka<flS?ib&-=o#V%5?f)h0hX)$ppKrBQuT^3nFcHz|o2 z9G4)cf)n0aR>EPd)8NEQamO!Cgc|K6ksB9n4KnXmTs4ugP!GSelm8^Pd_R@RvBGzS zp!lmC{Srtm+Qb^-Hn5yCekKzM9`~ydbZ0V0_Y5rqwumgaUUR!E4EKT!Sd3(_f`0nX zy}dAvvEzb>0^_Md0#fQJ(fwQ(tI`uol@CZauC8L1#${*akv^)A$M_n>uY{18OMUz; zFi-z%Q!(_7jeoHHz70|!6hk9$fG7m-HSz04NRZb_PUk^#(!@4TF_NnAW}f)$9#(BB z$EbBS5Om2DEQ;xI3IcA+%8y`{4f2#?{xF`Pztuc#_h9<U()r-XP@654hFe@GX<|#N z=m4Rr;flLWxR`7T5zhcRZ*p9-2wD&L)yPC~2nZ|rh{%+A2Txg=#iW5i^<U}TN^e70 zNUZwANb#^f2}t)!me@cUqkTzbYHc>mm7Q4=cnjUyabZtZfc2CuyU4!#tq5K5eJIi> z6@dKcNoMwq_jS8lQ($+Nf$Av%WxQ!_YV>VH+HNOF+tGRl`~tGsV$+0WT1ZHDUFlxi z5{4V@yoiSiYmu4D(32_(7j+awSbFPK(iDz=3Uq|Y4oaboa;G*M-PD5jx(w(oO!;8y z^lFSYlt;(pnZW$L;H6n}`Ho1pbI{>R3G{@jfEO_=zn;X6x~j95gFxB%Cq<VC*3D19 glWLk6<fWwTj<v4?V+I=@N*jnzcm(I{C}c?g1HizC<NyEw literal 0 HcmV?d00001 -- GitLab