1. Objetivo
Roteiro de resposta a incidente para uma EC2 Linux comprometida, no cenário mais comum que aparece em conta AWS: minerador de criptomoeda (XMRig ou parecido) rodando na instância, muitas vezes com roubo das credenciais da instance role via metadata.
A ideia é conter rápido sem destruir evidência, entender como o atacante entrou e o que ele fez com as credenciais, e só depois reconstruir. A ordem dos passos importa. Desligar a instância no susto é o erro mais comum, e ele apaga a memória, que é onde está boa parte da história.
Resultado esperado ao final:
- Instância isolada na rede, fora do Auto Scaling e com proteção contra término.
- Credenciais da role invalidadas.
- Memória e artefatos voláteis coletados com hash.
- Snapshots dos volumes em conta de forense.
- Linha do tempo do ataque e lista de ações feitas com as credenciais roubadas.
- Ambiente reconstruído a partir de imagem limpa.
2. Sinais típicos
| Sinal | Onde aparece |
|---|---|
| CPU em 100% constante sem mudança de tráfego | CloudWatch CPUUtilization |
CryptoCurrency:EC2/BitcoinTool.B!DNS | GuardDuty |
UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration.OutsideAWS | GuardDuty (credencial da role usada fora da AWS) |
| Conexões de saída para portas 3333, 4444, 5555, 7777, 14444 | VPC Flow Logs |
Processos com nome genérico (kworker, systemd-xxx, .x) em /tmp ou /dev/shm | Dentro da instância |
| Instâncias novas em regiões que você não usa | Billing, CloudTrail |
| E-mail da AWS sobre uso abusivo | Caixa do root da conta |
Se apareceu o finding de exfiltração de credencial, o incidente não é só da instância. É da conta. Trate o passo 4 como prioridade.
3. Antes de começar
- [ ] Abra um registro do incidente (ticket, documento) e anote cada comando com horário UTC. Isso vira o relatório depois.
- [ ] Separe uma conta ou VPC de forense, sem rota para a internet e sem peering com produção.
- [ ] Bucket S3 de evidências com versionamento e Object Lock, se possível.
- [ ] Não use a instância comprometida para nada além da coleta.
export AWS_REGION="us-east-1" export IID="i-<INSTANCE_ID>" export VPC_ID="vpc-<ID>" export ROLE_NAME="<ROLE_DA_INSTANCIA>" export BUCKET_EVID="<BUCKET_EVIDENCIAS>" export CASO="IR-$(date -u +%Y%m%d)-01" 4. Passo 1: Travar a instância
# Ninguem termina por engano (inclusive voce) aws ec2 modify-instance-attribute --instance-id "$IID" --disable-api-termination aws ec2 modify-instance-attribute --instance-id "$IID" --instance-initiated-shutdown-behavior stop # Marca para todo mundo saber aws ec2 create-tags --resources "$IID" \ --tags Key=Incidente,Value="$CASO" Key=NaoMexer,Value=forense Se a instância está num Auto Scaling group, tire de lá antes de qualquer coisa. Senão, quando você isolar a rede, o health check falha e o ASG termina a instância sozinho:
ASG=$(aws autoscaling describe-auto-scaling-instances --instance-ids "$IID" \ --query 'AutoScalingInstances[0].AutoScalingGroupName' --output text) if [ "$ASG" != "None" ]; then aws autoscaling detach-instances --instance-ids "$IID" \ --auto-scaling-group-name "$ASG" --no-should-decrement-desired-capacity fi Se estiver atrás de load balancer, desregistre dos target groups. O ASG coloca uma instância nova no lugar e o serviço continua.
5. Passo 2: Isolar a rede (do jeito certo)
Aqui tem uma pegadinha que pouca gente conhece: trocar o security group não derruba conexões já rastreadas. Se o atacante tem uma sessão reversa aberta, ela continua viva depois que você coloca um SG sem regras.
O truque documentado no guia de resposta a incidentes da AWS é passar por um SG intermediário que deixa as conexões não rastreadas, e só então aplicar o SG de isolamento:
# SG 1: tudo liberado (0.0.0.0/0 entrada e saida em todos os protocolos = conexao nao rastreada) SG_UNTRACK=$(aws ec2 create-security-group --group-name "ir-untrack-${CASO}" \ --description "IR passo intermediario" --vpc-id "$VPC_ID" --query GroupId --output text) aws ec2 authorize-security-group-ingress --group-id "$SG_UNTRACK" \ --ip-permissions 'IpProtocol=-1,IpRanges=[{CidrIp=0.0.0.0/0}]' # SG 2: isolamento, sem nenhuma regra SG_ISOL=$(aws ec2 create-security-group --group-name "ir-isolamento-${CASO}" \ --description "IR isolamento" --vpc-id "$VPC_ID" --query GroupId --output text) aws ec2 revoke-security-group-egress --group-id "$SG_ISOL" \ --ip-permissions 'IpProtocol=-1,IpRanges=[{CidrIp=0.0.0.0/0}]' # Aplica o 1 e logo depois o 2 aws ec2 modify-instance-attribute --instance-id "$IID" --groups "$SG_UNTRACK" sleep 5 aws ec2 modify-instance-attribute --instance-id "$IID" --groups "$SG_ISOL" Se a instância tem mais de uma ENI, repita para cada uma (modify-network-interface-attribute).
Conexão parada não é convertida. O truque funciona para conexões com tráfego passando no momento da troca. Uma shell reversa ociosa pode continuar rastreada e voltar a funcionar depois. Se você já sabe o IP do atacante (Flow Logs,
ssda coleta), complemente com uma regra de deny na NACL da subnet para esse IP. A NACL é stateless e corta na hora, mas vale para a subnet inteira, então use regra específica.E a coleta, se a rede está fechada? O SSM Agent precisa sair para os endpoints do Systems Manager, e a coleta do passo 5 manda arquivos para o S3. No SG de isolamento, libere só a saída 443 para o SG dos endpoints de interface
ssm,ssmmessageseec2messages, e para o prefix list do S3 (com VPC endpoint gateway do S3 na tabela de rotas):PL_S3=$(aws ec2 describe-managed-prefix-lists \ --filters Name=prefix-list-name,Values=com.amazonaws.${AWS_REGION}.s3 \ --query 'PrefixLists[0].PrefixListId' --output text) aws ec2 authorize-security-group-egress --group-id "$SG_ISOL" \ --ip-permissions "IpProtocol=tcp,FromPort=443,ToPort=443,UserIdGroupPairs=[{GroupId=<SG_VPC_ENDPOINTS>}]" \ "IpProtocol=tcp,FromPort=443,ToPort=443,PrefixListIds=[{PrefixListId=${PL_S3}}]"Sem endpoint, a alternativa é coletar pelo snapshot e abrir mão da memória.
Desassocie também o Elastic IP, se houver, e anote qual era.
6. Passo 3: Invalidar as credenciais da role
As credenciais que o atacante pegou pelo IMDS continuam válidas até expirar (até 6 horas, dependendo do caso), e funcionam de qualquer lugar do mundo. Remover a role da instância não invalida o que já foi emitido.
Aplique uma deny para tudo emitido antes de agora. É o mesmo que o botão Revoke active sessions do console faz:
AGORA=$(date -u +%Y-%m-%dT%H:%M:%SZ) cat > deny-old-sessions.json <<EOF { "Version": "2012-10-17", "Statement": [ { "Effect": "Deny", "Action": "*", "Resource": "*", "Condition": { "DateLessThan": { "aws:TokenIssueTime": "${AGORA}" } } } ] } EOF aws iam put-role-policy --role-name "$ROLE_NAME" \ --policy-name "AWSRevokeOlderSessions" \ --policy-document file://deny-old-sessions.json Isso derruba também as sessões legítimas de outras instâncias que usam a mesma role. Elas pegam credencial nova no IMDS automaticamente, então o impacto costuma ser só alguns erros por alguns minutos.
Se a instância tinha access key em arquivo (~/.aws/credentials, .env, variável de app), desative todas agora:
aws iam list-access-keys --user-name <USUARIO> aws iam update-access-key --user-name <USUARIO> --access-key-id <AKIA...> --status Inactive 7. Passo 4: O que o atacante fez com as credenciais
Para credenciais de instance role, o nome da sessão é o ID da instância. Isso facilita muito a busca:
aws cloudtrail lookup-events \ --lookup-attributes AttributeKey=Username,AttributeValue="$IID" \ --start-time "$(date -u -d '-90 days' +%FT%TZ)" \ --max-items 5000 \ --query 'Events[].CloudTrailEvent' --output text \ | jq -r '[.eventTime, .awsRegion, .eventSource, .eventName, .sourceIPAddress, (.errorCode // "ok")] | @tsv' \ | sort > "cloudtrail-${IID}.tsv" O que procurar no resultado:
-
sourceIPAddressque não é IP da AWS nem da própria instância. Isso é a credencial sendo usada de fora. -
ConsoleLoginouGetFederationToken: o atacante transformou a credencial em sessão de console. -
CreateUser,CreateAccessKey,AttachUserPolicy,CreateLoginProfile: persistência. -
RunInstancesem outras regiões: mais mineradores. -
PutBucketPolicy,GetObjectem massa: exfiltração. - Muitos
AccessDeniedseguidos: reconhecimento, tentando descobrir o que a role pode fazer.
O lookup-events só cobre 90 dias e só eventos de gerenciamento, e roda por região. Se o trail da organização manda para S3, faça a busca no Athena ou no CloudTrail Lake, em todas as regiões.
Varredura rápida de recursos criados em todas as regiões:
for r in $(aws ec2 describe-regions --query 'Regions[].RegionName' --output text); do echo "== $r" aws ec2 describe-instances --region "$r" \ --query 'Reservations[].Instances[].[InstanceId,State.Name,LaunchTime,InstanceType]' --output text done Qualquer usuário IAM, access key ou instância que apareça e ninguém reconhece: desative, anote e não apague ainda. Apagar perde evidência.
8. Passo 5: Coletar dados voláteis
Com a rede isolada e o SSM funcionando, rode a coleta. Ela escreve no disco da própria instância, calcula hash e manda para o bucket de evidências. Isso altera um pouco o estado da máquina, é inevitável, e por isso fica registrado.
coleta.sh:
#!/bin/bash set -u CASO="$1"; BUCKET="$2" OUT="/root/ir-${CASO}"; mkdir -p "$OUT"; cd "$OUT" date -u +%FT%TZ > 00-inicio.txt uname -a > 01-uname.txt uptime > 02-uptime.txt ps auxwwf > 10-ps.txt ls -la /proc/*/exe 2>/dev/null > 11-proc-exe.txt for p in /proc/[0-9]*; do echo "$(basename "$p") $(tr '\0' ' ' < "$p/cmdline" 2>/dev/null)" done > 12-cmdlines.txt ss -tunap > 20-conexoes.txt ip a; ip r > 21-rede.txt 2>&1 cat /etc/ld.so.preload > 30-ld-preload.txt 2>&1 crontab -l -u root > 31-cron-root.txt 2>&1 ls -la /etc/cron* /var/spool/cron* > 32-cron-dirs.txt 2>&1 systemctl list-units --type=service --all > 33-services.txt find /etc/systemd /lib/systemd /usr/lib/systemd -newermt "-60 days" -type f > 34-systemd-recentes.txt 2>/dev/null find /tmp /var/tmp /dev/shm -xdev -type f -exec ls -la {} \; > 40-tmp.txt 2>/dev/null cat /root/.ssh/authorized_keys /home/*/.ssh/authorized_keys > 41-authorized-keys.txt 2>&1 last -Faiw > 50-last.txt 2>&1 lastb -Faiw > 51-lastb.txt 2>&1 cp -a /var/log ./var-log 2>/dev/null cp /root/.bash_history /home/*/.bash_history ./ 2>/dev/null date -u +%FT%TZ > 99-fim.txt cd /root && tar czf "ir-${CASO}.tgz" "ir-${CASO}" sha256sum "ir-${CASO}.tgz" | tee "ir-${CASO}.tgz.sha256" aws s3 cp "ir-${CASO}.tgz" "s3://${BUCKET}/${CASO}/" aws s3 cp "ir-${CASO}.tgz.sha256" "s3://${BUCKET}/${CASO}/" A instância precisa de permissão
s3:PutObjectno bucket de evidências. Como a role foi bloqueada no passo 3 (a deny vale só para tokens antigos, os novos funcionam), confirme que a role tem essa permissão ou anexe temporariamente uma policy só para o prefixo do caso.
Envio pelo Run Command:
aws s3 cp coleta.sh "s3://${BUCKET_EVID}/scripts/coleta.sh" CMD_ID=$(aws ssm send-command --instance-ids "$IID" \ --document-name AWS-RunShellScript \ --parameters "commands=[\"aws s3 cp s3://${BUCKET_EVID}/scripts/coleta.sh /root/coleta.sh\",\"bash /root/coleta.sh ${CASO} ${BUCKET_EVID}\"]" \ --output-s3-bucket-name "$BUCKET_EVID" --output-s3-key-prefix "${CASO}/ssm" \ --query 'Command.CommandId' --output text) aws ssm get-command-invocation --command-id "$CMD_ID" --instance-id "$IID" \ --query '{status:Status,saida:StandardOutputContent}' 8.1 Memória
Para captura de RAM, o caminho mais simples hoje é o AVML (binário estático, não precisa compilar módulo de kernel como o LiME):
Baixe o binário numa máquina limpa e suba para o bucket de evidências antes (https://github.com/microsoft/avml/releases). Na instância, que está isolada, copie de lá:
aws s3 cp s3://${BUCKET_EVID}/tools/avml /root/avml chmod +x /root/avml /root/avml --compress /root/mem-${CASO}.lime.compressed sha256sum /root/mem-${CASO}.lime.compressed aws s3 cp /root/mem-${CASO}.lime.compressed s3://${BUCKET_EVID}/${CASO}/ A imagem de memória tem o tamanho da RAM, então confirme espaço em disco antes.
Rootkit de usuário. Se o
/etc/ld.so.preloadnão está vazio, os comandosps,lsessda própria máquina podem estar mentindo. A coleta continua valendo (ela registra o próprio arquivo), mas a análise confiável é a do snapshot montado em outra máquina, e a da memória.
9. Passo 6: Snapshots e envio para a conta de forense
VOLS=$(aws ec2 describe-instances --instance-ids "$IID" \ --query 'Reservations[0].Instances[0].BlockDeviceMappings[].Ebs.VolumeId' --output text) for v in $VOLS; do aws ec2 create-snapshot --volume-id "$v" \ --description "${CASO} ${IID} ${v}" \ --tag-specifications "ResourceType=snapshot,Tags=[{Key=Incidente,Value=${CASO}}]" \ --query SnapshotId --output text done Compartilhar com a conta de forense:
- Volume sem criptografia:
modify-snapshot-attributecomcreateVolumePermissionpara a conta de forense. - Criptografado com a chave padrão
aws/ebs: não dá para compartilhar. Façacopy-snapshotrecriptografando com uma CMK sua, dêkms:Decrypt,kms:CreateGrantekms:DescribeKeydessa CMK para a conta de forense e compartilhe a cópia.
aws ec2 copy-snapshot --source-region "$AWS_REGION" --source-snapshot-id <SNAP_ID> \ --encrypted --kms-key-id <ARN_CMK_FORENSE> --description "${CASO} copia forense" aws ec2 modify-snapshot-attribute --snapshot-id <SNAP_COPIA> \ --attribute createVolumePermission --operation-type add \ --user-ids <ACCOUNT_FORENSE> 10. Passo 7: Análise do disco
Na conta de forense, suba uma EC2 de análise numa subnet sem internet, crie um volume a partir do snapshot na mesma AZ e anexe. Monte somente leitura e sem replay de journal. Sem noload, o ext4 aplica o journal ao montar e altera o disco que você quer analisar:
lsblk sudo mkdir -p /mnt/evid # ext4 sudo mount -o ro,noload,noexec,nosuid,nodev /dev/nvme1n1p1 /mnt/evid # xfs (Amazon Linux 2/2023 e RHEL usam xfs) sudo mount -o ro,norecovery,nouuid,noexec,nosuid,nodev /dev/nvme1n1p1 /mnt/evid O nouuid no xfs é porque o volume analisado costuma ter o mesmo UUID do disco raiz da máquina de análise, se as duas vieram da mesma AMI.
10.1 Linha do tempo
sudo find /mnt/evid -xdev -newermt "<DATA_INICIO_SUSPEITA>" \ -printf '%TY-%Tm-%Td %TH:%TM:%TS %u %m %p\n' 2>/dev/null \ | sort > timeline.txt grep -E "/tmp/|/dev/shm|/var/tmp|/\.[^/]+$|cron|systemd/system|authorized_keys|ld.so.preload" timeline.txt 10.2 O minerador
# Config do XMRig e derivados sudo grep -rIl --exclude-dir={proc,sys} -E '"(pool|url)"\s*:\s*".*:[0-9]{4,5}"|stratum\+tcp|donate-level' /mnt/evid 2>/dev/null # Binarios ELF em diretorios onde nao deveriam existir sudo find /mnt/evid/tmp /mnt/evid/var/tmp /mnt/evid/dev/shm /mnt/evid/root /mnt/evid/home \ -type f -exec file {} \; 2>/dev/null | grep ELF # Persistencia sudo cat /mnt/evid/etc/ld.so.preload 2>/dev/null sudo ls -la /mnt/evid/etc/cron.d /mnt/evid/var/spool/cron/ /mnt/evid/var/spool/cron/crontabs 2>/dev/null sudo grep -rl -E "curl|wget|base64 -d|/tmp/|/dev/shm" /mnt/evid/etc/systemd /mnt/evid/etc/cron* /mnt/evid/etc/rc.local 2>/dev/null Calcule o SHA-256 dos binários suspeitos e consulte em base pública de malware. Não execute nada.
10.3 Por onde entrou
Os vetores que mais aparecem nesse tipo de caso:
| Vetor | Onde olhar |
|---|---|
| SSH com senha fraca | /var/log/auth.log ou /var/log/secure: muitos Failed password e depois um Accepted password |
PostgreSQL exposto com COPY ... FROM PROGRAM | Log do Postgres procurando FROM PROGRAM, e pg_hba.conf aceitando 0.0.0.0/0 |
| Redis sem senha | CONFIG SET dir e escrita em authorized_keys ou cron |
| Aplicação web vulnerável | Access log do Nginx/Apache próximo ao horário do primeiro arquivo malicioso |
| Docker API exposta (2375) | Containers e imagens estranhas, /var/lib/docker |
sudo grep -h "Accepted" /mnt/evid/var/log/auth.log* /mnt/evid/var/log/secure* 2>/dev/null | tail -50 sudo grep -rh -i "from program" /mnt/evid/var/lib/pgsql /mnt/evid/var/log/postgresql 2>/dev/null | head Cruze o horário do primeiro acesso com o primeiro arquivo malicioso na timeline e com o primeiro evento de fora da AWS no CloudTrail do passo 4. Quando os três batem, você tem a história.
10.4 Roubo de credencial pelo IMDS
Se o GuardDuty acusou exfiltração, procure como o atacante chegou no metadata:
sudo grep -rh "169.254.169.254" /mnt/evid/root/.bash_history /mnt/evid/home/*/.bash_history /mnt/evid/tmp 2>/dev/null E confira a configuração da instância. IMDSv2 dificulta SSRF, mas não impede quem já tem shell na máquina. Quem tem shell faz o PUT do token e pega a credencial do mesmo jeito.
aws ec2 describe-instances --instance-ids "$IID" \ --query 'Reservations[0].Instances[0].MetadataOptions' 11. Passo 8: Erradicar e reconstruir
Não "limpe" a instância. Não dá para ter certeza de que tudo saiu.
- Suba uma instância nova a partir de AMI oficial ou da sua pipeline de imagem, nunca de AMI criada a partir da comprometida.
- Restaure dados de um backup anterior à data de entrada, validado.
- Troque todos os segredos que existiam na máquina: senhas de banco, tokens de API, chaves SSH, certificados, variáveis de ambiente da aplicação.
- Remova a política de deny do passo 3 só depois de confirmar que não existe credencial persistente criada pelo atacante.
- Apague usuários, chaves e instâncias criados pelo atacante, depois de documentar.
- Mantenha a instância isolada e os snapshots pelo prazo que o jurídico ou o contrato pedirem.
12. Passo 9: Para não acontecer de novo
# IMDSv2 obrigatorio e hop limit 1 (container nao alcanca o IMDS do host) aws ec2 modify-instance-metadata-options --instance-id <NOVA_INSTANCIA> \ --http-tokens required --http-put-response-hop-limit 1 # Padrao da conta para instancias novas aws ec2 modify-instance-metadata-defaults --http-tokens required --http-put-response-hop-limit 1 Restrinja o uso da credencial da role ao lugar de onde ela deveria vir. A condição abaixo, numa SCP ou na própria policy, nega uso fora da VPC da instância:
{ "Effect": "Deny", "Action": "*", "Resource": "*", "Condition": { "StringNotEquals": { "aws:EC2InstanceSourceVPC": "${aws:SourceVpc}" }, "Null": { "aws:ec2InstanceSourceVPC": "false" }, "BoolIfExists": { "aws:ViaAWSService": "false" } } } Essa regra depende do aws:SourceVpc, que só existe quando a chamada passa por VPC endpoint. Então ela funciona bem em VPC que acessa os serviços por endpoint (STS, S3, SSM etc.). Sem endpoint, use a variação com aws:EC2InstanceSourcePrivateIPv4 e aws:VpcSourceIp, que está na mesma documentação. Teste em uma conta de homologação antes: serviços que chamam outros serviços em seu nome podem cair nessa regra.
Checklist:
- [ ] Nenhum SG com 22, 3389, 5432, 3306, 6379, 2375 ou 9200 aberto para
0.0.0.0/0 - [ ] Acesso administrativo só por SSM Session Manager
- [ ] No PostgreSQL, ninguém além do DBA com
pg_execute_server_programou superuser - [ ] GuardDuty com Runtime Monitoring habilitado em todas as regiões
- [ ] Alarme de
CPUUtilizationalto e prolongado em instâncias que não deveriam ter carga constante - [ ] VPC Flow Logs habilitados e retidos
- [ ] CloudTrail da organização para S3 com retenção longa
- [ ] Alerta de billing por região
13. Referências
- AWS Security Incident Response Guide: https://docs.aws.amazon.com/whitepapers/latest/aws-security-incident-response-guide/welcome.html
- Connection tracking em security groups: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/security-group-connection-tracking.html
- Revogar sessões de role: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_revoke-sessions.html
- Findings do GuardDuty para EC2: https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_finding-types-ec2.html
- AVML: https://github.com/microsoft/avml
- Condition keys de credencial de instância: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-keys.html
