From bb01dcbe791ffaaf49df299d486cee8d408bd1f8 Mon Sep 17 00:00:00 2001 From: Nardi Ivan Date: Mon, 29 Jul 2024 14:32:14 +0200 Subject: [PATCH] Add support for GTP de-tunneling Close #6 --- pl7m.c | 182 +++++++++++++++++++++++++++ tests/pcaps/gtpu-dns.pcapng | Bin 0 -> 476 bytes tests/pcaps/gtpu-tls.pcapng | Bin 0 -> 14420 bytes tests/results/gtpu-dns.pcapng.fuzzed | Bin 0 -> 19623 bytes tests/results/gtpu-tls.pcapng.fuzzed | Bin 0 -> 74101 bytes 5 files changed, 182 insertions(+) create mode 100644 tests/pcaps/gtpu-dns.pcapng create mode 100644 tests/pcaps/gtpu-tls.pcapng create mode 100644 tests/results/gtpu-dns.pcapng.fuzzed create mode 100644 tests/results/gtpu-tls.pcapng.fuzzed diff --git a/pl7m.c b/pl7m.c index dac5fc1..827e538 100644 --- a/pl7m.c +++ b/pl7m.c @@ -159,6 +159,39 @@ struct gre_header { __u16 protocol; }; +#define GTP_MSG_TPDU 0xFF + +struct gtp_header { +#if defined(__LITTLE_ENDIAN_BITFIELD) + u_int16_t n_pdu:1, + sequence:1, + extension:1, + reserved:1, + protocol:1, + version:3, + type:8; +#elif defined(__BIG_ENDIAN_BITFIELD) + u_int16_t version:3, + protocol:1, + reserved:1, + extension:1, + sequence:1, + n_pdu:1, + type:8; +#else +#error "Adjust your defines" +#endif + u_int16_t total_length; + u_int32_t teid; +}; + +struct gtp_header_optional { + u_int16_t sn; + u_int8_t n_pdu_nbr; + u_int8_t next_hdr; +}; + + struct m_pkt { unsigned char *raw_data; struct pcap_pkthdr header; @@ -166,6 +199,7 @@ struct m_pkt { int l2_offset; int prev_l3_offset; u_int16_t prev_l3_proto; + int gtp_offset; int l3_offset; u_int16_t l3_proto; int l4_offset; @@ -708,6 +742,136 @@ static int dissect_l4(struct m_pkt *p) } return 0; } +static int is_gtp_u(unsigned char *gtp_buffer, int gtp_buffer_len, + const struct m_pkt *p, u_int16_t *l3_proto) +{ + struct gtp_header *gtp_h; + struct udphdr *udp_h; + uint16_t new_layer_len = 0; + unsigned char sub_proto; + + if (p->l4_proto != IPPROTO_UDP || + gtp_buffer_len < (int)sizeof(struct gtp_header)) + return 0; + + /* Only default port */ + udp_h = (struct udphdr *)(p->raw_data + p->l4_offset); + if(udp_h->source != htons(2152) && + udp_h->dest != htons(2152)) + return 0; + + gtp_h = (struct gtp_header *)gtp_buffer; + + if (gtp_h->version != 1 || + gtp_h->type != GTP_MSG_TPDU || + gtp_h->reserved != 0 || + ntohs(gtp_h->total_length) > (gtp_buffer_len - sizeof(struct gtp_header))) { + ddbg("Invalid gtp header: %d, 0x%x, 0x%0x, %d vs %d\n", + gtp_h->version, gtp_h->type, gtp_h->reserved, + ntohs(gtp_h->total_length), gtp_buffer_len); + return 0; + } + + new_layer_len = sizeof(struct gtp_header); + + /* Optional header version 1 */ + if (gtp_h->extension || gtp_h->sequence || gtp_h->n_pdu) { + new_layer_len += sizeof(struct gtp_header_optional); + + if (gtp_buffer_len < new_layer_len) + return 0; + } + if (gtp_h->extension) { + unsigned int length = 0; + + while (new_layer_len < (gtp_buffer_len - 1)) { + length = gtp_buffer[new_layer_len] << 2; + new_layer_len += length; + if (new_layer_len > gtp_buffer_len || + gtp_buffer[new_layer_len - 1] == 0 || length == 0) + break; + } + if (new_layer_len > gtp_buffer_len || + gtp_buffer[new_layer_len - 1] != 0 || + length == 0) { + return 0; + } + } + + /* Trying to detect next proto. Code taken from wireshark */ + if (gtp_buffer_len < new_layer_len + 1) + return 0; + sub_proto = gtp_buffer[new_layer_len]; + if ((sub_proto >= 0x45) && (sub_proto <= 0x4e)) { + /* This is most likely an IPv4 packet + * we can exclude 0x40 - 0x44 because the minimum header size is 20 octets + * 0x4f is excluded because PPP protocol type "IPv6 header compression" + * with protocol field compression is more likely than a plain + * IPv4 packet with 60 octet header size */ + *l3_proto = ETH_P_IP; + } else if ((sub_proto & 0xf0) == 0x60) { + /* This is most likely an IPv6 packet */ + *l3_proto = ETH_P_IPV6; + } else { + /* This seems to be a PPP packet */ + /* TODO: code not back-ported from wireshark yet*/ + return 0; + } + + return new_layer_len; +} +static int dissect_l4_detunneling(struct m_pkt *p) +{ + unsigned char *data = p->raw_data + p->l5_offset; + int data_len = p->header.caplen - p->l5_offset; + u_int16_t next_l3_proto; + int gtp_header_len, rc; + + ddbg("L4(detunel): l4_proto %d data_len %d l5_length %d\n", + p->l4_proto, data_len, p->l5_length); + + if (data_len < 0 || p->l5_length > data_len) + return -1; + + /* TODO: try to handle tunnel over fragment */ + if (p->is_l3_fragment) { + ddbg("Skip L4(detunnel) dissection because it is a fragment\n"); + return 0; + } + /* No reasons to detunnel if we skipped L4 dissection */ + if (p->skip_l4_dissection) { + ddbg("Skip L4 dissection\n"); + return 0; + } + + /* GTP detunneling: looking only for MSG T-PDU that carries + encapsulated data */ + gtp_header_len = is_gtp_u(data, data_len, p, &next_l3_proto); + if (gtp_header_len > 0) { + ddbg("Found GTP-U\n"); + if (p->prev_l3_proto == 0) { + assert(p->prev_l3_offset == 0); + p->prev_l3_proto = p->l3_proto; + p->prev_l3_offset = p->l3_offset; + } else { + derr("Multiple tunnels. Unsupported\n"); + return -1; + } + assert(p->gtp_offset == 0); + p->gtp_offset = p->l5_offset; + p->l3_proto = next_l3_proto; + p->l3_offset = p->l5_offset + gtp_header_len; + rc = dissect_l3(p); + if (rc != 0) { + derr("Error dissect_l3 (after gtp)\n"); + return -1; + } + return dissect_l4(p); + } + + /* "Normal" L4 traffic */ + return 0; +} static int dissect_do(int datalink_type, struct m_pkt *p) { int rc; @@ -727,6 +891,12 @@ static int dissect_do(int datalink_type, struct m_pkt *p) derr("Error dissect_l4\n"); return -1; } + /* Some kind of detunneling over L4 (usually over UDP). Example: GTP */ + rc = dissect_l4_detunneling(p); + if (rc != 0) { + derr("Error dissect_l5\n"); + return -1; + } return 0; } @@ -792,6 +962,7 @@ static void update_do_l7(struct m_pkt *p) { struct udphdr *udp_h; struct tcphdr *tcp_h; + struct gtp_header *gtp_h; size_t new_l5_len; int l4_header_len = 0; int l5_len_diff; @@ -857,6 +1028,16 @@ static void update_do_l7(struct m_pkt *p) ip6->ip6_plen = htons(ntohs(ip6->ip6_plen) + l5_len_diff); } + /* Update GTP header */ + if (p->gtp_offset) { + gtp_h = (struct gtp_header *)(p->raw_data + p->gtp_offset); + gtp_h->total_length = htons(ntohs(gtp_h->total_length) + l5_len_diff); + /* Update previous UDP header */ + assert(p->gtp_offset > (int)sizeof(struct udphdr)); + udp_h = (struct udphdr *)(p->raw_data + p->gtp_offset - sizeof(struct udphdr)); + udp_h->len = htons(ntohs(udp_h->len) + l5_len_diff); + } + p->l5_length = new_l5_len; ddbg("cap_len %u->%zu\n", p->header.caplen, p->l5_offset + new_l5_len); p->header.caplen = p->l5_offset + new_l5_len; @@ -955,6 +1136,7 @@ static struct m_pkt *__dup_pkt(struct m_pkt *p) n->l2_offset = p->l2_offset; n->prev_l3_offset = p->prev_l3_offset; n->prev_l3_proto = p->prev_l3_proto; + n->gtp_offset = p->gtp_offset; n->l4_offset = p->l4_offset; n->l3_offset = p->l3_offset; n->l3_proto = p->l3_proto; diff --git a/tests/pcaps/gtpu-dns.pcapng b/tests/pcaps/gtpu-dns.pcapng new file mode 100644 index 0000000000000000000000000000000000000000..ec8e9146aaff9b405270a717927cad271f938934 GIT binary patch literal 476 zcmd<$<>d-sU|{gI(UxKa(*L1=g+ZGkI597?B(o|tMIotDA*3iVIW@c}F)uwQwMe1N zK+jCiLLsR%GbcsC(!>&|lYs$b4#*4*DEO71G(Cex;ScltE*Isa`42T_C3c0b@Ex zc}8MMabiIMb6#o*NE|2wawiNd05U-CI|3B|xi2xMWAjuXAB2Gb)qU+!a~K%x1ds0r zxeeq-u=|P`7!3Y1B%9dY^#Qsq+4C<0LyBPZ6Og+=?qe_oy3YUWF1^ym2A~U=K*nNo t+W{U1R*(yMOBuLXl7TK~PEN@?z;{3g;&29Q1{Qf0aAcRdL2rg_dVHaeT%@svl5VS)G5Jf-|1b09dMHB{5qy
6-5+VMMd1erCV(gL4(&tmKCW@_F1a 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 literal 0 HcmV?d00001 diff --git a/tests/results/gtpu-dns.pcapng.fuzzed b/tests/results/gtpu-dns.pcapng.fuzzed new file mode 100644 index 0000000000000000000000000000000000000000..47141bf3322a7a0d8ccad8deeb4051116a025573 GIT binary patch literal 19623 zcmeI)KQ9Ae9Ki8kwc--0Dm5VSXJBfYM%*NX;Uw}HYF46YrDxDIw5bt^L}D;VOd=5j z8?i_%J?UWRsEfhoE!c3LhZ@8?knfW`Pjb(5xhMB|0pCXL=B!0R@-uv2B%mMC{Z_he zMdV4z*VJ3?%vIOI#`3*wiL<5Zu{V+FXnl9&sI7PQaWrPRmaA?>#(Z@VNqr2R_J}y5 z&ev+@y%op199Zv;SM!ZMvDM+Voha*j%vE{>@>a#olr}TPVz97UGW*Sw?{iiJ5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R;Yuz>*Gq4IhXc>;ExRk6)oL z+3!0MnTkGCP2|hOzD(##bfYWjOKyDfdEODp&Asa-y7*!OUncY=w$6N|Uhfj+oo$`+ qYB8Vv8~Lh8!0V7!)A|aRr9G6@4X|J~SMWN$cryVuA)&#rN`3;1BvO0; literal 0 HcmV?d00001 diff --git a/tests/results/gtpu-tls.pcapng.fuzzed b/tests/results/gtpu-tls.pcapng.fuzzed new file mode 100644 index 0000000000000000000000000000000000000000..d748e649126a4e996ad2f9a4c59c05320ef714f6 GIT binary patch literal 74101 zcmeI&2~-o;{s-_onMnvCtU*?n0D>FB4Lc&Ti7Zv5vZx5!fNUB;Nf6wsK|zZPJaD5G zjRHjlt61w&MDZzF6*q8c)rze9Y)l-HafxI9AL$&~q=AW1Is3JVB;00@8p2!H?xfB*=900@8p2!H?x zfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=9 z00@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p z2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?x zfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=9 z00@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p z2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?xfB*=900@8p2!H?x zfB*=900@8p2!H?xfB*=900@8p2!H?x{D%k>ST2n?vsneD(7%^i-z_BlO3D#`s~jOV z7kS4a@jicqT(kpAc+IbEdCdtzZX7obO+W~DA=k5mEpogOaz)4;*>Y~K(Z9B~nrB=g zCCc;fKCIVO^ITk%q|lisj1(foqS&bvi_43te&kE2JDCzBse+ki?2A@Kg9)TudpAQu zH{%OsGYE~@bSt~GFJESwz%)hKjMvI$H0fmT?4X;`lpo@b+&R{nOcR)<IBKi%z5%CZganJzz5C)9dpaLGc#YW1NIVZh*a_QJrW#^bn=Xd7AOUEu_%-Gwz zFCBZkmbp8!BEl?Quc$PoYReu#^UL<3IYP^pVxRRzqqV zky7E6nFFnh_04Vd7p$q$t0G}d8tj_U9X-;CeY2||sH@N|(H zR^^Z(6i2`c2@>_g0_F`*z#khQ8}AjJBoq22M2QTsKJ&4fKr1CB#cC$KDT?02TpZB= zY|LyT1-d@biNQ(9Qkig4kcZIALui9VPI#ccy2u&Z+Sp^|5~iiLeEK;17`IB{-tl2{g>m>?DP$41>P?(1g5X@u#AMg$v?X@n-CKj*R12m(D{ z_jvuu&kqy?Z?7EnrD|*De$4NGag)o_(GK4}=frm0sP8xAz;(YV6#_VNT z>+Wu@9@zN(G#^EFg!s*^vBCF+TFdM0^9((=A4)M>{k&5`cn~c)z>G4$ddqaPr*!SxlDzZt;FPkbFABW$oVH4; z@Bn594N8yFecE(;*rhR~&e_ty6=%N0#{@;;##sJ>*{)5=o3Q+GT3;6=%^)sWoDv;x zql|@-ALK%sQDUTYq*Nx3jZcWRlFnX`AeJnU#!H#oGR>IY7M(P|`;vdWQEyJ7EHU-d z1L{e|bEL_Ad`_p*%;@bOA&Ec!#lmBl-3L*Q*dE&|L<;MSkxa;qvbK(zIoB#SI#I?vbj**o z?s)>~2hhuddHk%Rk|cOM^L-)ZjlHlVNA-&gUm~MZLJ)X39@=vj7NOUlIjEFA>c>yG zwO3#e=5jc(ay}g>IJxIL1VP2)SZvYr9upZx?;1v56IrsfSKF}bAN#Q~L_I%1%*06V z9sB$dGG#^$5Kt`4Jc(=$k9MMn>kC*Ei;}ld@)q=*?vKlVsXIC-bKVb3)GsRHp__DC zGV8nL)2KgHc3v6vr+Y>HK4sJ^-bKAa%R(9Ttlm*?nA#Kd;dI~hVbtfYH`}y3XOWN7 zvVbj!}(TxWjU1!4M(G-DOZX0?9f(0sHq z`S|0hi`&l5nG<^RJMCk`}qO;;?}>^||RTIGp-us2IM(`J52obOSq z_|IM~Mz=F3{gRveX6;;0)}xo%SDp+LbkOI`u-hhCuRn;y%u|CYwT@n zY|qL$*c!1us8TjUXczxvOUXdPf*seRE}9uV3zBX>`PkuY1KJ$wb_BQhjJoWyQF$u2MU~6qffiqOtF1ob(7St$TM?Co)2P*SGD_cdzwB{u4a|P8Uq7H=+eO_|V(CX} zSlnmt_fK@cLOhUuyy@;&|BnvaN0JyRmIx;$Cd%k;SlI(RibU8!WMgA%XV3J&AHMy| zD}X=iP>*L`iJDMv(6ln5L2q={(u2oV>N^(l+eL2Myo23MZsel2Pg2tfKNMTw!9DNkgg1DX+=)@rxxMN@?AhH*xnO7P zsIXVqz7P2%iEXc4t$Xg&?i-c4PrW>pJ(X*BPvwB^unmWzJ1z8Qom5ZKS5pWv>wlN@ z)WNc9Ch7faK1_OQ2b1(vPIuB%Ia))Qq_5KGo%91AD3jg*JsFePJL!9hU}YECyRcT~ zA%4Cx#B~L?!)p4h9~akLxMq3(80%)lS1C_lHilN_JR2S|();)R!`-x|wKUcrt8YZ+ zM}99jK^ouR8n7gN6~S^?6dem$5n_&8A1s<8#aM+}>Q$S$f{pz$g7@ z-7ypS>)jT$YVZ79@@D53cxU$Z)rnT#w;T`l|FS}Fs!5qm?4Gp+pZU4D=ongd=hr9mPyNljGCfG+p8Sf20X`dBSa+GHld9s*F4J5d#JPOiKe{YnkI*1lMr=gsrYu~vGv)eo&Hjx-lZ|g?8znfl zKJEKxpZ;0241ID2g?7(JHuN7WwwO6-AJz+GDTM2QeeU()(T`sMy|+bZsZv1{VZQ%`#r z@%;}c)$egX88nXXm0PsyGwTtSd)GIQ%w1u=N8lnGZ&Ryr3%96$#~3c1o|tJw~&|G+chI=;8XTZKaucdvq-y z>>FRy6qG|=bN4LV(YdlNk{KzWH0U5>vMqSI8oEbUu*}*!+ujfIu&ZlMGlM+O?sgCI zB=lnjdDw>$vg=~+e2p)e)8DKl$j!(bv7wrUY8I;5e|3YA?wYa2puwZpg{hTNxR?WXRs_J4Ig4yNOJ&j-SE98AY` zPsYP^98AY0{R`7^|Fgtr1~T4iKZk(~n3jWSxxe;5=D