
집 Linux 박스에서 AFL++ 캠페인을 며칠씩 돌리고, 맥북을 덮었다가 다음 날 결과를 확인하고 싶었다. 그런데 SSH로 띄운 퍼저가 연결을 끊은 뒤 자꾸 사라졌다. 처음에는 코어를 많이 써서 OOM이 난 줄 알았고, 실제 원인은 프로세스가 SSH 세션에서 충분히 분리되지 않은 실행 방식이었다.
journalctl -k --since today | grep -Ei 'oom|killed process'
dmesg -T | grep -Ei 'oom|out of memory'
ps -o pid,ppid,sid,tty,stat,cmd -p <PID>
OOM killer 기록이 없고 SSH 종료 시점과 프로세스 종료가 맞아떨어지면 세션·터미널 신호를 본다. 단순히 “프로세스가 없다”만 보면 자원 부족과 세션 종료를 구분하기 어렵다.
nohup은 SIGHUP을 무시하게 하고 표준 출력·에러를 파일로 돌린다. 하지만 프로세스를 완전한 서비스로 만들거나 실패 시 재시작해주지는 않는다. 셸 스크립트 안에서 여러 워커를 띄우고 일부 스트림이 터미널에 남아 있으면 기대와 다른 수명 문제가 생길 수 있다.
nohup ./run-fuzzers.sh >campaign.log 2>&1 </dev/null &
disown
이 조합은 간단한 배치에는 충분하지만, 재부팅 후 자동 복구와 자원 제한까지 필요하면 systemd 쪽이 명확하다.
setsid -f sh -c '
exec afl-fuzz -i seeds -o out -M main -- ./target @@
' >main.log 2>&1 </dev/null
setsid -f는 새 세션을 만들기 위해 fork하고 호출 셸의 제어 터미널에서 분리한다. 예전에는 PPID가 1인지 하나만 봤지만, 시스템에 subreaper가 있으면 다른 부모로 재귀속될 수 있다. SID와 TTY=?, 실제 프로세스가 SSH 종료 뒤에도 남는지를 같이 확인하는 편이 정확하다.
afl-fuzz -i seeds -o sync -M main -- ./target @@
afl-fuzz -i seeds -o sync -S s01 -p fast -- ./target @@
afl-fuzz -i seeds -o sync -S s02 -p explore -- ./target @@
마스터와 세컨더리는 같은 -o 동기화 디렉터리를 사용하고 서로 다른 ID를 가져야 한다. 코어 수와 워커 수를 기계적으로 같게 두지는 않았다. 대상 프로그램의 메모리 사용량과 시스템 load, exec/s가 떨어지는 지점을 보고 여유 코어를 남겼다.
각 워커의 fuzzer_stats에서 last_update, execs_done, execs_per_sec, saved_crashes를 모으고, 전체 작업이 끝났을 때는 완료 마커를 남겼다.
printf 'ALL_DONE %s
' "$(date -Is)" > status.txt
로컬 SSH 명령이 타임아웃이나 aborted로 끝나도 서버 프로세스는 이미 실행 중일 수 있다. 같은 캠페인을 다시 띄우기 전에 서버의 PID, 로그 증가, 출력 디렉터리를 확인해야 중복 실행을 피할 수 있다.
며칠 이상 반복 실행하거나 재부팅 복구가 필요하면 unit 파일로 옮겼다. Restart=on-failure, WorkingDirectory, 로그 정책, CPU·메모리 제한을 명시할 수 있다. 일회성 실험은 setsid, 운영에 가까운 캠페인은 systemd라는 선을 잡으니 관리가 단순해졌다.
#!/usr/bin/env bash
set -euo pipefail
cd /srv/fuzz/campaign
if [[ -f campaign.pid ]] && kill -0 "$(cat campaign.pid)" 2>/dev/null; then
echo "already running: $(cat campaign.pid)"
exit 1
fi
setsid -f sh -c '
echo $$ > campaign.pid
exec ./run-workers.sh > logs/campaign.log 2>&1
'
sleep 1
cat campaign.pid
ps -o pid,ppid,sid,tty,stat,cmd -p "$(cat campaign.pid)"
PID 파일만 믿으면 이전 비정상 종료의 낡은 파일 때문에 막힐 수 있어 kill -0로 실제 프로세스 존재를 같이 확인했다. 캠페인 디렉터리와 출력 경로도 절대 경로로 고정해 SSH를 어디서 실행했는지에 따라 결과가 흩어지지 않게 했다.
last_update가 일정 시간 이상 갱신되지 않음watcher는 퍼저를 무조건 다시 띄우지 않았다. 디스크 부족이나 잘못된 대상 바이너리 때문에 죽었다면 자동 재시작이 같은 실패를 반복하기 때문이다. 상태를 기록하고 알림을 남긴 뒤 사람이 원인을 확인하도록 했다.
이 인프라에서 얻은 크래시를 중복 제거하고 공개 제보까지 연결한 과정은 AFL++ 네트워크 파서 퍼징과 GHSA 제보 기록에 정리했다.
| 얼굴인증 시스템에 5가지 적대적 공격을 걸고 방어를 우회해본 기록 (0) | 2026.07.29 |
|---|---|
| 취약점 심각도를 정직하게 매기기 — RCE라고 부풀리지 않으려고 한 일들 (0) | 2026.07.28 |
| EDR 탐지 아이디어를 검증 가능한 결과로 만든 팀 프로젝트 회고 (0) | 2026.07.28 |
| 오픈소스 취약점을 GitHub에 책임 있게 제보하기 — PVR이 없을 때 겪은 일들 (0) | 2026.07.27 |
| Boolean Blind SQLi로 한 글자씩 데이터 뽑아내기 — WHS 워게임 5단계 (0) | 2026.07.26 |