Segurança de Dados em IIoT usando o dataset WUSTL-IIoT-2021

Introdução

A Internet das Coisas (IoT) transformou profundamente a forma como sistemas conectados operam em ambientes domésticos, comerciais e especialmente industriais. Quando esses dispositivos são aplicados ao contexto fabril, nasce o conceito de Industrial Internet of Things (IIoT) — uma rede de sensores, atuadores, controladores lógicos, máquinas inteligentes e servidores conectados com o objetivo de automatizar processos, aumentar produtividade e reduzir custos operacionais.

Embora essa integração digital tenha potencializado a eficiência industrial, ela também ampliou a superfície de ataque dessas redes. Estimativas apontam que mais de 75% dos dispositivos industriais conectados possuem vulnerabilidades conhecidas e que o número de ataques contra sistemas de controle industrial (ICS) aumentou mais de 300% nos últimos anos (ENISA, 2020 ) e (IBM X-Force, 2022 ). Incidentes como o ataque ao oleoduto Colonial Pipeline (2021), o caso do ransomware LockerGoga (2019) e invasões a usinas de água e energia mostram como falhas de segurança em sistemas industriais podem gerar prejuízos financeiros, ambientais e até riscos à vida humana.

Diferentemente de redes convencionais, ambientes IIoT possuem características muito específicas: baixa tolerância a interrupções, necessidade de comunicação em tempo real, uso de protocolos proprietários e dispositivos com capacidade limitada de processamento. Além disso, ataques em redes industriais tendem a ser raros, difíceis de observar e com comportamento muito distinto do tráfego normal. Isso torna essencial o desenvolvimento de métodos robustos de detecção de anomalias baseados em dados reais que representem adequadamente a dinâmica de um ambiente industrial.

Nesse contexto, este trabalho utiliza o dataset público WUSTL-IIoT-2021, desenvolvido pela Washington University in St. Louis, que contém mais de 1,1 milhão de registros de tráfego realista de um testbed industrial, incluindo comunicações normais e diversos ataques como DoS, Reconnaissance, Command Injection e Backdoor. O dataset está disponível em: 🔗 http://www.cse.wustl.edu/~jain/iiot2/index.html

O conjunto de dados foi construído deliberadamente de forma desbalanceada, refletindo o cenário real das indústrias, onde ataques representam menos de 8% do tráfego total (Zolanvari et al., 2021 ).

O objetivo deste estudo é realizar uma análise exploratória detalhada desse dataset a fim de compreender padrões presentes no tráfego industrial, identificar diferenças entre fluxos benignos e maliciosos e discutir como essas informações podem apoiar modelos futuros de detecção de intrusão. Ao investigar métricas como volume de pacotes, taxas de transmissão, jitter, perda de pacotes e características de tempo de conexão, buscamos revelar comportamentos anômalos que podem sinalizar tentativas de ataque.

Assim, esta análise contribui para:

compreensão profunda da estrutura e padrões do tráfego industrial;

identificação de sinais característicos de ataques raros, essenciais para sistemas de detecção de intrusões (IDS);

reforço da importância da segurança em ambientes industriais conectados, tema cada vez mais relevante para infraestrutura crítica;

oferta de insights práticos para pesquisadores e profissionais, que podem aplicar as descobertas em soluções reais de proteção cibernética.

Dessa forma, o estudo da segurança de dados em IIoT não é apenas uma demanda técnica: é uma necessidade urgente para proteger sistemas que sustentam energia, água, transporte, saúde e produção industrial moderna.

Pacotes Requeridos

# Importar pacotes
library(readr)
library(tidyverse)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr     1.1.4     ✔ purrr     1.1.0
## ✔ forcats   1.0.1     ✔ stringr   1.5.2
## ✔ ggplot2   4.0.0     ✔ tibble    3.3.0
## ✔ lubridate 1.9.4     ✔ tidyr     1.3.1
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag()    masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(lubridate)

Preparação dos dados

Sobre o dataset

O dataset WUSTL-IIoT-2021 contém registros de tráfego de rede coletados em um testbed industrial realista, com comunicações normais e ataques cibernéticos reais (DoS, Reconnaissance, Command Injection e Backdoor). Ele representa o comportamento típico de um ambiente IIoT moderno, onde a maioria do tráfego é normal, mas ataques esporádicos representam grande risco.

Nosso objetivo nesta etapa é compreender a estrutura dos dados, observando:

  • Quantas linhas e colunas existem
  • Quais são os tipos das variáveis
  • Se há dados faltantes
  • Quais colunas representam informações importantes
  • Que comportamento geral os dados apresentam

Isso é fundamental antes de qualquer análise mais profunda.

# Carregar dataset
data <- read_csv("wustl_iiot_2021.csv")
## Rows: 1194464 Columns: 49
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr   (3): SrcAddr, DstAddr, Traffic
## dbl  (44): Mean, Sport, Dport, SrcPkts, DstPkts, TotPkts, DstBytes, SrcBytes...
## dttm  (2): StartTime, LastTime
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
View(data)

# Número de linhas e colunas
print(paste("O número de linhas é:", nrow(data)))
## [1] "O número de linhas é: 1194464"
print(paste("O número de colunas é:", ncol(data)))
## [1] "O número de colunas é: 49"

O dataset tem quase quase 1,2 milhão de registros e 49 colunas, então é um dataset consideravelmente grande de onde podemos tirar varias informações. Nos temos as colunas :

str(data)
## spc_tbl_ [1,194,464 × 49] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
##  $ StartTime : POSIXct[1:1194464], format: "2019-08-19 12:23:28" "2019-08-19 15:13:24" ...
##  $ LastTime  : POSIXct[1:1194464], format: "2019-08-19 12:23:28" "2019-08-19 15:13:24" ...
##  $ SrcAddr   : chr [1:1194464] "192.168.0.20" "192.168.0.20" "192.168.0.20" "209.240.235.92" ...
##  $ DstAddr   : chr [1:1194464] "192.168.0.2" "192.168.0.2" "192.168.0.2" "192.168.0.2" ...
##  $ Mean      : num [1:1194464] 0 0 0 0 3 0 0 0 0 0 ...
##  $ Sport     : num [1:1194464] 59034 55841 63774 61771 0 ...
##  $ Dport     : num [1:1194464] 502 502 502 80 0 80 502 80 502 502 ...
##  $ SrcPkts   : num [1:1194464] 10 10 10 4 14 2 6 2 10 10 ...
##  $ DstPkts   : num [1:1194464] 8 8 8 0 0 0 6 0 8 8 ...
##  $ TotPkts   : num [1:1194464] 18 18 18 4 14 2 12 2 18 18 ...
##  $ DstBytes  : num [1:1194464] 508 508 508 0 0 0 384 0 508 508 ...
##  $ SrcBytes  : num [1:1194464] 644 644 644 248 868 124 396 124 644 644 ...
##  $ TotBytes  : num [1:1194464] 1152 1152 1152 248 868 ...
##  $ SrcLoad   : num [1:1194464] 87486 88077 89587 1673 1842 ...
##  $ DstLoad   : num [1:1194464] 67123 67577 68735 0 0 ...
##  $ Load      : num [1:1194464] 154609 155654 158323 1673 1842 ...
##  $ SrcRate   : num [1:1194464] 169.69 170.84 173.77 3.37 3.71 ...
##  $ DstRate   : num [1:1194464] 132 133 135 0 0 ...
##  $ Rate      : num [1:1194464] 320.53 322.7 328.23 3.37 3.71 ...
##  $ SrcLoss   : num [1:1194464] 2 2 2 3 0 2 2 2 2 2 ...
##  $ DstLoss   : num [1:1194464] 2 2 2 0 0 0 2 0 2 2 ...
##  $ Loss      : num [1:1194464] 4 4 4 3 0 2 4 2 4 4 ...
##  $ pLoss     : num [1:1194464] 18.2 18.2 18.2 42.9 0 ...
##  $ SrcJitter : num [1:1194464] 527.4 17.2 523 419.3 525.1 ...
##  $ DstJitter : num [1:1194464] 11.5 13.2 12.3 0 0 ...
##  $ SIntPkt   : num [1:1194464] 5.89 7.53 5.75 296.52 321.43 ...
##  $ DIntPkt   : num [1:1194464] 7.41 7.34 7.3 0 0 ...
##  $ Proto     : num [1:1194464] 6 6 6 6 2054 ...
##  $ Dur       : num [1:1194464] 0.053 0.0527 0.0518 0.8896 3.5001 ...
##  $ TcpRtt    : num [1:1194464] 0.001266 0.00131 0.000766 0 0 ...
##  $ IdleTime  : num [1:1194464] 1.55e+09 1.55e+09 1.55e+09 1.55e+09 1.55e+09 ...
##  $ Sum       : num [1:1194464] 0.053 0.0527 0.0518 0.8896 3.5001 ...
##  $ Min       : num [1:1194464] 0.053 0.0527 0.0518 0.8896 3.5001 ...
##  $ Max       : num [1:1194464] 0.053 0.0527 0.0518 0.8896 3.5001 ...
##  $ sDSb      : num [1:1194464] 0 0 0 0 0 0 0 0 0 0 ...
##  $ sTtl      : num [1:1194464] 128 128 128 140 0 142 128 243 128 128 ...
##  $ dTtl      : num [1:1194464] 64 64 64 0 0 0 64 0 64 64 ...
##  $ sIpId     : num [1:1194464] 53331 37167 58712 21629 0 ...
##  $ dIpId     : num [1:1194464] 64402 31590 22717 0 0 ...
##  $ SAppBytes : num [1:1194464] 24 24 24 0 476 0 24 0 24 24 ...
##  $ DAppBytes : num [1:1194464] 20 20 20 0 0 0 20 0 20 20 ...
##  $ TotAppByte: num [1:1194464] 44 44 44 0 476 0 44 0 44 44 ...
##  $ SynAck    : num [1:1194464] 0.00118 0.00131 0.00069 0 0 ...
##  $ RunTime   : num [1:1194464] 0.053 0.0527 0.0518 0.8896 3.5001 ...
##  $ sTos      : num [1:1194464] 0 0 0 0 0 0 0 0 0 0 ...
##  $ SrcJitAct : num [1:1194464] 0 0 0 419 525 ...
##  $ DstJitAct : num [1:1194464] 0 0 0 0 0 0 0 0 0 0 ...
##  $ Traffic   : chr [1:1194464] "normal" "normal" "normal" "DoS" ...
##  $ Target    : num [1:1194464] 0 0 0 1 0 1 0 1 0 0 ...
##  - attr(*, "spec")=
##   .. cols(
##   ..   StartTime = col_datetime(format = ""),
##   ..   LastTime = col_datetime(format = ""),
##   ..   SrcAddr = col_character(),
##   ..   DstAddr = col_character(),
##   ..   Mean = col_double(),
##   ..   Sport = col_double(),
##   ..   Dport = col_double(),
##   ..   SrcPkts = col_double(),
##   ..   DstPkts = col_double(),
##   ..   TotPkts = col_double(),
##   ..   DstBytes = col_double(),
##   ..   SrcBytes = col_double(),
##   ..   TotBytes = col_double(),
##   ..   SrcLoad = col_double(),
##   ..   DstLoad = col_double(),
##   ..   Load = col_double(),
##   ..   SrcRate = col_double(),
##   ..   DstRate = col_double(),
##   ..   Rate = col_double(),
##   ..   SrcLoss = col_double(),
##   ..   DstLoss = col_double(),
##   ..   Loss = col_double(),
##   ..   pLoss = col_double(),
##   ..   SrcJitter = col_double(),
##   ..   DstJitter = col_double(),
##   ..   SIntPkt = col_double(),
##   ..   DIntPkt = col_double(),
##   ..   Proto = col_double(),
##   ..   Dur = col_double(),
##   ..   TcpRtt = col_double(),
##   ..   IdleTime = col_double(),
##   ..   Sum = col_double(),
##   ..   Min = col_double(),
##   ..   Max = col_double(),
##   ..   sDSb = col_double(),
##   ..   sTtl = col_double(),
##   ..   dTtl = col_double(),
##   ..   sIpId = col_double(),
##   ..   dIpId = col_double(),
##   ..   SAppBytes = col_double(),
##   ..   DAppBytes = col_double(),
##   ..   TotAppByte = col_double(),
##   ..   SynAck = col_double(),
##   ..   RunTime = col_double(),
##   ..   sTos = col_double(),
##   ..   SrcJitAct = col_double(),
##   ..   DstJitAct = col_double(),
##   ..   Traffic = col_character(),
##   ..   Target = col_double()
##   .. )
##  - attr(*, "problems")=<externalptr>
  • StartTime (POSIXct) — horário em que o fluxo começou
  • LastTime (POSIXct) — horário em que o fluxo terminou
  • SrcAddr (chr) — endereço IP de origem
  • DstAddr (chr) — endereço IP de destino
  • Mean (num) — tempo médio entre pacotes no fluxo
  • Sport (num) — porta de origem
  • Dport (num) — porta de destino
  • SrcPkts (num) — pacotes enviados pela origem
  • DstPkts (num) — pacotes enviados pelo destino
  • TotPkts (num) — total de pacotes no fluxo
  • DstBytes (num) — bytes recebidos pelo destino
  • SrcBytes (num) — bytes enviados pela origem
  • TotBytes (num) — total de bytes transmitidos no fluxo
  • SrcLoad (num) — carga da origem
  • DstLoad (num) — carga do destino
  • Load (num) — carga total do fluxo
  • SrcRate (num) — taxa de pacotes por segundo da origem
  • DstRate (num) — taxa de pacotes por segundo do destino
  • Rate (num) — taxa total de pacotes por segundo
  • SrcLoss (num) — pacotes perdidos ou retransmitidos pela origem
  • DstLoss (num) — pacotes perdidos ou retransmitidos pelo destino
  • Loss (num) — perdas totais no fluxo
  • pLoss (num) — porcentagem de perda de pacotes
  • SrcJitter (num) — jitter da origem
  • DstJitter (num) — jitter do destino
  • SIntPkt (num) — tempo entre pacotes enviados pela origem
  • DIntPkt (num) — tempo entre pacotes enviados pelo destino
  • Proto (num) — código do protocolo
  • Dur (num) — duração total do fluxo
  • TcpRtt (num) — tempo de RTT no handshake TCP
  • IdleTime (num) — tempo ocioso desde o último pacote
  • Sum (num) — soma das durações agregadas
  • Min (num) — menor duração agregada
  • Max (num) — maior duração agregada
  • sDSb (num) — Differentiated Services Byte
  • sTtl (num) — TTL da origem
  • dTtl (num) — TTL do destino
  • sIpId (num) — identificador IP da origem
  • dIpId (num) — identificador IP do destino
  • SAppBytes (num) — bytes da aplicação enviados pela origem
  • DAppBytes (num) — bytes da aplicação enviados pelo destino
  • TotAppByte (num) — total de bytes da camada de aplicação
  • SynAck (num) — tempo entre SYN e SYN-ACK
  • RunTime (num) — tempo total de execução do fluxo
  • sTos (num) — Type of Service
  • SrcJitAct (num) — jitter ativo da origem
  • DstJitAct (num) — jitter ativo do destino
  • Traffic (chr) — rótulo do tráfego
  • Target (num) — classe binária (0 normal, 1 ataque)

Pré-processamento dos dados

Antes de iniciar a análise exploratória, foram realizadas verificações básicas de integridade do dataset, incluindo a presença de valores ausentes e a existência de registros duplicados. O código abaixo foi utilizado para essa inspeção inicial:

sum(is.na(data))
## [1] 0
key_cols <- c("SrcAddr", "DstAddr", "Sport", "Dport", "StartTime")

sum(duplicated(data[key_cols]))
## [1] 93
data <- data[!duplicated(data[key_cols]), ]

Os resultados mostraram que não há valores ausentes no dataset, confirmando que os dados já vêm limpos de origem. Quanto à duplicação, ao realizar a checagem utilizando todas as colunas, o processamento se tornou extremamente lento devido ao tamanho do dataset (mais de 1,19 milhão de registros). Por isso, optou-se por verificar duplicatas apenas nas colunas-chave do fluxo, como IPs, portas e timestamp inicial. Essa abordagem é adequada para análise exploratória e reduz significativamente o custo computacional.

A partir dessa verificação, foram encontrados 93 registros duplicados entre as colunas-chave, o que representa uma proporção mínima diante do volume total do dataset. Assim, conclui-se que não há duplicações relevantes que comprometam a análise exploratória dos dados, porém mesmo assim preferi remove-los.

data <- data %>%
  mutate(
    Traffic = as.factor(Traffic),
    Target  = as.factor(Target)
  )

Traffic (normal, DoS, Recon, etc.) e Target (0/1) são categorias, não número, então transformá-las em fatores permite análises mais corretas, como gráficos comparativos e estatísticas por grupo

data <- data %>%
  mutate(
    hour      = hour(StartTime),
    duration  = as.numeric(LastTime - StartTime),
    byte_ratio = SrcBytes / (DstBytes + 1),
    pkt_ratio  = SrcPkts / (DstPkts + 1)
  )
data <- data %>%
  mutate(
    hour      = hour(StartTime),
    duration  = as.numeric(LastTime - StartTime),
    byte_ratio = SrcBytes / (DstBytes + 1),
    pkt_ratio  = SrcPkts / (DstPkts + 1)
  )

Explicação: Criamos métricas que ajudam na identificação de padrões maliciosos: hour : ataques podem ocorrer em horários específicos duration : ataques DoS tendem a aumentar a duração byte_ratio : scanners enviam muito mais do que recebem pkt_ratio : tráfego anormalmente assimétrico sugere reconhecimento ou varredura

Essas features adicionais tornam a análise mais rica.

numeric_cols <- sapply(data, is.numeric)

data <- data
data[, numeric_cols] <- scale(data[, numeric_cols])

A normalização coloca todas as variáveis na mesma escala. É útil em análises que comparam magnitudes, como gráficos de densidade ou PCA. Isso NÃO altera valores brutos, apenas cria uma versão normalizada opcional.

cols_to_remove <- c(
  "SrcAddr", "DstAddr", "sIpId", "dIpId", 
  "StartTime", "LastTime"
)

data <- data %>% select(-all_of(cols_to_remove))

Retiramos colunas que não ajudam a identificar ataques, pois são metadados e não características comportamentais. Isso deixa o dataset mais limpo e focado nas variáveis que descrevem o comportamento do tráfego.

Verificar estrutura final

str(data)
## tibble [1,194,371 × 47] (S3: tbl_df/tbl/data.frame)
##  $ Mean      : num [1:1194371] -0.188 -0.188 -0.188 -0.188 4.186 ...
##  $ Sport     : num [1:1194371] 0.381 0.115 0.776 0.61 -4.538 ...
##  $ Dport     : num [1:1194371] -0.0875 -0.0875 -0.0875 -0.2154 -0.2397 ...
##  $ SrcPkts   : num [1:1194371] -0.00297 -0.00297 -0.00297 -0.00309 -0.0029 ...
##  $ DstPkts   : num [1:1194371] -0.00781 -0.00781 -0.00781 -0.01484 -0.01484 ...
##  $ TotPkts   : num [1:1194371] -0.00299 -0.00299 -0.00299 -0.00326 -0.00307 ...
##  $ DstBytes  : num [1:1194371] -0.00945 -0.00945 -0.00945 -0.01012 -0.01012 ...
##  $ SrcBytes  : num [1:1194371] -0.00396 -0.00396 -0.00396 -0.00404 -0.00391 ...
##  $ TotBytes  : num [1:1194371] -0.0144 -0.0144 -0.0144 -0.0145 -0.0144 ...
##  $ SrcLoad   : num [1:1194371] -0.187 -0.187 -0.187 -0.188 -0.188 ...
##  $ DstLoad   : num [1:1194371] -0.0195 -0.0195 -0.0193 -0.028 -0.028 ...
##  $ Load      : num [1:1194371] -0.188 -0.188 -0.188 -0.19 -0.19 ...
##  $ SrcRate   : num [1:1194371] -0.186 -0.186 -0.186 -0.187 -0.187 ...
##  $ DstRate   : num [1:1194371] -0.0184 -0.0183 -0.0182 -0.027 -0.027 ...
##  $ Rate      : num [1:1194371] -0.187 -0.187 -0.187 -0.189 -0.189 ...
##  $ SrcLoss   : num [1:1194371] -0.0108 -0.0108 -0.0108 0.0271 -0.0867 ...
##  $ DstLoss   : num [1:1194371] -0.0108 -0.0108 -0.0108 -0.0488 -0.0488 ...
##  $ Loss      : num [1:1194371] -0.00125 -0.00125 -0.00125 -0.00165 -0.00283 ...
##  $ pLoss     : num [1:1194371] -0.242 -0.242 -0.242 2.813 -2.494 ...
##  $ SrcJitter : num [1:1194371] 0.123 -0.965 0.114 -0.107 0.118 ...
##  $ DstJitter : num [1:1194371] -0.01606 -0.00453 -0.01082 -0.09314 -0.09314 ...
##  $ SIntPkt   : num [1:1194371] -0.137 -0.134 -0.137 0.383 0.428 ...
##  $ DIntPkt   : num [1:1194371] -0.0126 -0.0136 -0.0141 -0.1172 -0.1172 ...
##  $ Proto     : num [1:1194371] -0.0549 -0.0549 -0.0549 -0.0549 1.2109 ...
##  $ Dur       : num [1:1194371] -1.62e-03 -1.62e-03 -1.62e-03 6.83e-05 5.34e-03 ...
##  $ TcpRtt    : num [1:1194371] -0.0103 -0.0095 -0.0194 -0.0334 -0.0334 ...
##  $ IdleTime  : num [1:1194371] 0.0173 0.0206 0.0204 0.0173 0.0205 ...
##  $ Sum       : num [1:1194371] -0.184 -0.184 -0.185 0.866 4.143 ...
##  $ Min       : num [1:1194371] -0.184 -0.184 -0.185 0.866 4.143 ...
##  $ Max       : num [1:1194371] -0.184 -0.184 -0.185 0.866 4.143 ...
##  $ sDSb      : num [1:1194371] -0.0102 -0.0102 -0.0102 -0.0102 -0.0102 ...
##  $ sTtl      : num [1:1194371] -0.0392 -0.0392 -0.0392 0.4485 -5.2412 ...
##  $ dTtl      : num [1:1194371] 0.306 0.306 0.306 -3.129 -3.129 ...
##  $ SAppBytes : num [1:1194371] -0.0685 -0.0685 -0.0685 -0.0769 0.09 ...
##  $ DAppBytes : num [1:1194371] -0.00944 -0.00944 -0.00944 -0.00947 -0.00947 ...
##  $ TotAppByte: num [1:1194371] -0.0158 -0.0158 -0.0158 -0.0158 -0.0158 ...
##  $ SynAck    : num [1:1194371] -0.01141 -0.00901 -0.02027 -0.03285 -0.03285 ...
##  $ RunTime   : num [1:1194371] -0.184 -0.184 -0.185 0.866 4.143 ...
##  $ sTos      : num [1:1194371] -0.0101 -0.0101 -0.0101 -0.0101 -0.0101 ...
##  $ SrcJitAct : num [1:1194371] -0.149 -0.149 -0.149 0.863 1.118 ...
##  $ DstJitAct : num [1:1194371] -0.0531 -0.0531 -0.0531 -0.0531 -0.0531 ...
##  $ Traffic   : Factor w/ 5 levels "Backdoor","CommInj",..: 4 4 4 3 4 3 4 3 4 4 ...
##  $ Target    : Factor w/ 2 levels "0","1": 1 1 1 2 1 2 1 2 1 1 ...
##  $ hour      : num [1:1194371] -0.376 1.076 0.108 -0.376 0.592 ...
##  $ duration  : num [1:1194371] -0.00994 -0.00994 -0.00994 0.05656 0.25606 ...
##  $ byte_ratio: num [1:1194371] -0.00383 -0.00383 -0.00383 -0.00378 -0.00364 ...
##  $ pkt_ratio : num [1:1194371] -0.133 -0.133 -0.133 0.148 1.118 ...

Analise exploratoria dos Dados

Conclusão