다른 앱에서 본 기능을 내 프로젝트에도 넣고 싶을 때가 있다. 소스가 없으면 디스어셈블러를 열어 함수를 따라가야 하는데, 이 작업은 도구도 많고 시간도 오래 걸린다. REA(Reverse Engineer Anything)는 이 작업을 코딩 에이전트에게 맡길 수 있게 해 주는 MIT 라이선스 도구다. MCP 서버 하나로 네이티브 바이너리, JavaScript/Electron 앱, .NET 어셈블리, 웹사이트를 분석하는 도구를 에이전트에 붙이고, 결론마다 근거(Evidence)와 한계를 함께 돌려준다. 이 글에서는 Ubuntu 24.04에 REA 6.7.0을 설치하고, 직접 작성한 C 바이너리와 Electron 구조 앱을 CLI와 Claude Code로 분석해 본 결과를 정리한다.
REA 구성
| 구성 요소 | 역할 |
|---|---|
MCP 서버 (rea --mcp) | 에이전트가 호출하는 분석 도구 묶음. 로컬에서 실행된다 |
CLI (rea <command>) | MCP와 같은 워크플로를 터미널에서 실행 |
| 워크플로 지침(skill) | setup이 에이전트에 설치하는 조사 절차 |
| 분석 엔진(provider) | 깊은 네이티브 분석은 Hopper·Ghidra·IDA, 오프라인 디스어셈블은 GNU objdump·Rizin. 정적 JS·.NET 분석은 엔진 불필요 |
| Evidence | 모든 결과에 대상 SHA-256, 사용한 엔진, 신뢰도, 한계(limitations)를 붙인 봉투 형식 |
지원 대상은 README의 표에 30종이 넘게 정리돼 있다. 주요 대상만 추리면 다음과 같다.
| 대상 | 돌려주는 것 | 필요한 것 |
|---|---|---|
| 네이티브 바이너리 | 의사코드, 어셈블리, 문자열, 심볼, 호출·참조 | Hopper, Ghidra 또는 IDA |
| JavaScript / Electron | 모듈, import, 소스맵, IPC, 네이티브 애드온 관계 | Node.js, 리눅스에서는 procps ps |
| .NET 어셈블리 | 메타데이터, CIL 명령, 네이티브 의존성 | 없음(정적) |
| Go / Rust 바이너리 | 컴파일러 버전·모듈, cargo-auditable 의존성 | 없음(정적) |
| 웹사이트 | 페이지 구조, 스크립트, 네트워크 관찰, 스크린샷 | Chrome 계열 브라우저 |
| Android APK | 매니페스트, 클래스, 디컴파일된 메서드 | headless JADX, JDK |
설치
Node.js 22.x(22.23.4 이상), 24.x(24.22.1 이상) 또는 26.12.1 이상이 필요하다. Ubuntu 24.04 기본 패키지(v18)로는 부족해서, 공식 바이너리 tarball을 사용자 디렉터리에 풀어 썼다.
mkdir -p ~/rea-test && cd ~/rea-test
curl -sSLO https://nodejs.org/dist/v24.22.1/node-v24.22.1-linux-x64.tar.xz
tar xf node-v24.22.1-linux-x64.tar.xz
export PATH=~/rea-test/npm-global/bin:~/rea-test/node-v24.22.1-linux-x64/bin:$PATH
npm config set prefix ~/rea-test/npm-global
npm install --global rea-agents@6.7.0
rea --version6.7.0
에이전트에 붙일 때는 npx rea-agents@latest setup을 실행한다. Claude Code, Codex, Cursor, Gemini CLI 등을 골라 변경 계획을 승인하면, 기존 설정을 백업한 뒤 MCP 서버와 지침을 등록한다. 설치 후 rea doctor로 준비 상태를 본다.
rea doctor | head -30healthy: false
environment_healthy: false
...
scope_checks[8]:
- name: node
ok: true
classification: healthy
detail: 24.22.1
- name: host
ok: true
classification: healthy
detail: ubuntu 24.04
- name: hopper
ok: false
classification: missing_analysis_engine
remediation: "Run rea setup to install Hopper, or set HOPPER_LAUNCHER_PATH."
- name: ghidra
ok: false
classification: missing_analysis_engine
detail: GHIDRA_INSTALL_DIR is not set
remediation: Set GHIDRA_INSTALL_DIR to an extracted Ghidra 12.1.x release directory.
...
깊은 분석 엔진이 하나도 없어서 healthy: false다. 아래 실습은 엔진 없이 쓸 수 있는 objdump 기반 검사와 정적 JS 분석만으로 진행한다.
직접 만든 바이너리 분석
REA 사이트의 첫 예제는 “Windows 계산기에서 왜 200 + 10%가 220인가”다. 같은 동작을 하는 C 프로그램을 직접 작성해 strip한 뒤, 소스 없이 무엇을 알아낼 수 있는지 확인했다. 정답을 알고 있으니 결과를 검증할 수 있다.
/* percent.c — 계산기의 % 키 동작을 흉내 낸 예제 */
#include <stdio.h>
#include <stdlib.h>
/* a op b% : 덧셈·뺄셈이면 a의 b%를, 곱셈·나눗셈이면 b/100을 피연산자로 쓴다 */
double apply_percent(double a, char op, double b)
{
switch (op) {
case '+': return a + a * b / 100.0;
case '-': return a - a * b / 100.0;
case '*': return a * (b / 100.0);
case '/': return a / (b / 100.0);
}
return a;
}
int main(int argc, char **argv)
{
if (argc != 4) {
fprintf(stderr, "usage: %s <a> <+|-|*|/> <b>\n", argv[0]);
return 1;
}
printf("%g\n", apply_percent(atof(argv[1]), argv[2][0], atof(argv[3])));
return 0;
}gcc -O2 -o percent percent.c && strip percent
./percent 200 + 10
./percent 200 "*" 10
file percent220
20
percent: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=ce6a6be7068a4acc278872779b459e70fb2302d4, for GNU/Linux 3.2.0, stripped
CLI: inspect-with-objdump
REA는 objdump 출력을 그대로 감싸 Evidence로 만든다. 출력이 길어서 --filter-output으로 메타데이터만 골랐다.
rea inspect-with-objdump ./percent disassemble \
--filter-output "provider.id,parameters.command_arguments,raw_result.exit_code,raw_result.artifact_changed,confidence,authority,limitations" \
--format yamlprovider:
id: gnu.objdump
parameters:
command_arguments:
- -d
- /home/noble/rea-test/percent
raw_result:
exit_code: 0
artifact_changed: false
confidence: observed
authority: shipped-artifact
limitations:
- The executable version and supported formats are reported as unknown because
no reliable version probe was performed.
- A non-zero process exit is preserved as an observed command result.
- When both are available, pre-run and post-run digests detect artifact
changes across the provider call but cannot prove the exact bytes consumed
during the call.
실행 전후 SHA-256을 비교해 대상이 바뀌지 않았음(artifact_changed: false)을 기록하고, 그 비교가 증명하지 못하는 것까지 한계로 적는다. raw_result.stdout에 담긴 디스어셈블 중 계산 함수 부분은 다음과 같다.
1220: f3 0f 1e fa endbr64
1224: 40 80 ff 2d cmp $0x2d,%dil
1228: 74 3e je 1268 <__fprintf_chk@plt+0x1d8>
122a: 7f 24 jg 1250 <__fprintf_chk@plt+0x1c0>
122c: 40 80 ff 2a cmp $0x2a,%dil
1230: 74 4e je 1280 <__fprintf_chk@plt+0x1f0>
1232: 40 80 ff 2b cmp $0x2b,%dil
1236: 75 60 jne 1298 <__fprintf_chk@plt+0x208>
1238: f2 0f 59 c8 mulsd %xmm0,%xmm1
123c: f2 0f 5e 0d e4 0d 00 divsd 0xde4(%rip),%xmm1 # 2028 <__fprintf_chk@plt+0xf98>
1243: 00
1244: f2 0f 58 c1 addsd %xmm1,%xmm0
1248: c3 ret
...
1280: f2 0f 5e 0d a0 0d 00 divsd 0xda0(%rip),%xmm1 # 2028 <__fprintf_chk@plt+0xf98>
1287: 00
1288: f2 0f 59 c1 mulsd %xmm1,%xmm0
128c: c3 ret
심볼이 없어도 연산자 문자(0x2b=’+’, 0x2a=’*’)로 분기하고 0x2028의 상수로 나누는 구조가 보인다. 그 상수는 objdump로 직접 확인했다.
objdump -s --start-address=0x2028 --stop-address=0x2030 percent | tail -1
python3 -c "import struct;d=open('percent','rb').read();print(struct.unpack('<d',d[0x2028:0x2030]))" 2028 00000000 00005940 ......Y@
(100.0,)
에이전트: Claude Code + REA MCP
REA의 본래 용도는 위 과정을 에이전트가 하게 하는 것이다. 로컬 에이전트 설정을 바꾸지 않으려고 setup 대신 일회성 MCP 설정 파일을 만들었다. REA는 SSH 너머의 테스트 호스트에서 실행된다.
{
"mcpServers": {
"rea": {
"command": "ssh",
"args": [
"-o", "BatchMode=yes", "noble@test-host",
"PATH=$HOME/rea-test/npm-global/bin:$HOME/rea-test/node-v24.22.1-linux-x64/bin:$PATH exec rea --mcp"
]
}
}
}claude -p "REA MCP 도구만 사용해서 원격 호스트의 /home/noble/rea-test/percent 바이너리(strip됨, 소스 없음)를 분석해줘. \
이 프로그램이 인자 3개(<a> <연산자> <b>)를 받을 때 연산자별로 어떤 계산을 하는지, \
특히 '200 + 10'과 '200 * 10'의 결과가 각각 무엇이 될지 근거(주소, 명령어, 상수)와 함께 한국어로 짧게 설명해줘. \
파일을 수정하거나 실행하지는 마." \
--mcp-config mcp.json --strict-mcp-config --allowedTools "mcp__rea__*"이 프로그램은 일반 계산기가 아닙니다. 두 번째 피연산자 `b`를 **퍼센트**로 해석합니다. 그래서 **`200 + 10` → 220**, **`200 * 10` → 20**이 나옵니다.
REA의 디컴파일 엔진(Ghidra, Hopper, IDA, Kuna)과 rizin은 모두 설정이 안 돼 있어서 쓸 수 없었습니다. 대신 REA의 `inspect_with_objdump`로 디스어셈블한 결과를 근거로 삼았습니다. 바이너리는 실행하지 않았고, 분석 전후 SHA-256이 같아 변경도 없었습니다.
**main (0x10a0)**
- `0x10ad`: `cmp $0x4,%edi`에서 argc가 4가 아니면 0x2004의 `"usage: %s <a> <+|-|*|/> <b>\n"`을 stderr에 출력하고 1을 반환합니다.
...
**상수 (0x2028)**: 바이트가 `00 00 00 00 00 00 59 40`입니다. 이는 double 0x4059000000000000, 즉 **100.0**입니다.
**계산 함수 (0x1220), 연산자별 분기**
| 연산자 | 주소 | 명령어 | 계산식 |
|---|---|---|---|
| `+` (0x2b) | 0x1238 | `mulsd`, `divsd [100.0]`, `addsd` | a + a·b/100 |
| `-` (0x2d) | 0x1268 | `mulsd`, `divsd [100.0]`, `subsd` | a − a·b/100 |
| `*` (0x2a) | 0x1280 | `divsd [100.0]`, `mulsd` | a × (b/100) |
| `/` (0x2f) | 0x1256 | `divsd [100.0]`, `divsd` | a ÷ (b/100) |
| 그 외 | 0x1290 / 0x1298 | 바로 `ret` | a를 그대로 출력 |
**질문하신 두 경우**
- **`200 + 10`**: 200 + 200×10/100 = 200 + 20 = **220**. 210이 아니라 "200에 10%를 더한 값"입니다.
- **`200 * 10`**: 200 × (10/100) = **20**. 2000이 아니라 "200의 10%"입니다.
...
쓸 수 있는 엔진이 objdump뿐이라는 사실을 스스로 밝히고, 그 안에서 연산자별 계산식 4개를 모두 소스와 일치하게 복원했다. 근거로 주소와 명령어, 상수 바이트를 함께 제시했다.
Electron 앱의 IPC 경계 추적
REA 쇼케이스 중 하나가 Notion의 클립보드 기능을 렌더러에서 preload, IPC를 거쳐 메인 프로세스까지 추적한 사례다. 같은 구조의 작은 앱을 직접 만들어 정적 분석에 넣었다.
// main.js — 메인 프로세스: 클립보드 읽기 요청을 IPC로 처리
const { app, BrowserWindow, ipcMain, clipboard } = require('electron');
const path = require('path');
ipcMain.handle('clipboard:read', () => clipboard.readText());
app.whenReady().then(() => {
const win = new BrowserWindow({
webPreferences: { preload: path.join(__dirname, 'preload.js') },
});
win.loadFile('index.html');
});// preload.js — 렌더러에 최소한의 API만 노출
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('api', {
readClipboard: () => ipcRenderer.invoke('clipboard:read'),
});여기에 package.json("main": "main.js"), window.api.readClipboard()를 부르는 renderer.js, index.html을 더해 디렉터리 하나로 만들었다. Electron은 설치하지 않았다. 정적 분석은 코드를 실행하지 않는다.
rea analyze-javascript-application ~/rea-test/clipapp --format json > js.json 2>&1
grep -c rea_progress js.json
python3 -c "
import json
d=[json.loads(l) for l in open('js.json') if l.strip() and not l.startswith('{\"rea_progress\"')][-1]
print(json.dumps(d['normalized_result']['summary'], indent=2))" | head -1618
{
"browser_windows": 1,
"explicit_web_preferences": 1,
"preload_entrypoints": 1,
"context_bridge_apis": 1,
"exposed_api_members": 1,
"ipc": {
"operations": 2,
"literal_channels": 1,
"dynamic_channel_operations": 0,
"renderer_transmissions": 1,
"renderer_listeners": 0,
"main_handlers": 1,
"paired_renderer_transmissions": 1,
"ambiguous_renderer_transmissions": 0,
"unpaired_literal_renderer_transmissions": 0
stderr의 진행 상황 줄 18개가 결과 앞에 섞이므로 마지막 JSON만 골라야 한다. 요약 숫자는 직접 만든 구조와 정확히 맞고, 송신 1건이 메인 핸들러와 짝지어졌다(paired_renderer_transmissions: 1). 관계 그래프에서 Electron 경계만 뽑는 스크립트를 따로 썼다.
#!/usr/bin/env python3
"""REA analyze-javascript-application 결과(JSON Lines)에서 Electron 경계 관계만 뽑는다."""
import json, sys
objs = [json.loads(l) for l in open(sys.argv[1]) if l.strip() and not l.startswith('{"rea_progress"')]
g = objs[-1]["normalized_result"]["graph"]
byid = {n["node_id"]: n for n in g["nodes"]}
def name(i):
n = byid[i]
o = (n.get("observations") or [{}])[0]
return f'{n["kind"]}({o.get("label")})'
focus = {"ipc-channel", "ipc-handler", "context-bridge-api", "electron-preload", "browser-window", "electron-main"}
for e in g["edges"]:
if {byid[e["source_node_id"]]["kind"], byid[e["target_node_id"]]["kind"]} & focus:
print(f'{name(e["source_node_id"])} --{e["relation"]}--> {name(e["target_node_id"])}')$ python3 ipc_chain.py js.json
javascript-asset(preload.js) --invokes--> ipc-handler(clipboard:read)
electron-preload(preload.js) --maps_to--> javascript-asset(preload.js)
javascript-module(main.js) --loads--> electron-preload(preload.js)
electron-main(main.js) --maps_to--> javascript-asset(main.js)
ipc-handler(clipboard:read) --handles--> ipc-channel(clipboard:read)
javascript-asset(main.js) --contains--> ipc-handler(clipboard:read)
package(clipapp) --loads--> electron-main(main.js)
javascript-asset(preload.js) --invokes--> ipc-channel(clipboard:read)
electron-preload(preload.js) --maps_to--> javascript-asset(preload.js)
javascript-asset(main.js) --contains--> browser-window(BrowserWindow@8:14-10:4)
browser-window(BrowserWindow@8:14-10:4) --loads--> electron-preload(preload.js)
javascript-asset(preload.js) --exposes--> context-bridge-api(api)
package → main.js → BrowserWindow → preload.js → api 노출 / clipboard:read 호출 → main 핸들러로 이어지는 경로가 위치 정보(8:14-10:4)와 함께 복원됐다.
주의사항
- 합법적인 대상에만: README의 면책 조항은 REA를 합법적인 연구, 교육, 상호운용성, 소프트웨어 보존, 그리고 본인 소유이거나 분석 권한이 있는 소프트웨어에만 쓰라고 명시한다. Hopper·IDA·JEB 같은 상용 엔진도 각자의 라이선스 조건을 따라야 한다. 이 글의 실습은 모두 직접 작성한 코드로 했다.
- 문서가 릴리스보다 앞서 있다: GitHub 문서는 main 브랜치 기준이다. 예를 들어
docs/kernel-modules.md의inspect-kernel-module은 npm 6.7.0에 없어서'inspect-kernel-module' is not a command for 'rea'로 실패했다. 문서의 기능을 쓰기 전에rea --help로 확인한다. - 엔진마다 별도 준비물이 있다: 의사코드 수준 분석은 Hopper·Ghidra·IDA 중 하나가 있어야 한다.
inspect-binary-layout은 pwntools 4.15.0이 깔린 파이썬을REA_PWNTOOLS_PYTHON으로 지정해야 한다. REA는 이것들을 대신 설치하지 않는다(Hopper만 setup에서 승인 후 설치 가능). inspect-artifact는 시작점일 뿐이다:.ko나 ELF에 돌리면 파일 목록과 해시만 나오고, 다음 단계(open_binary)로 엔진을 붙이라고 안내한다.- 출력이 크다: 파일 5개짜리 JS 앱 분석 결과가 약 280KB였다. CLI에서는
--filter-output,--token-limit,--token-count로 줄이고, MCP는 기본적으로 축약 입력 스키마를 써서 컨텍스트를 아낀다(REA_MCP_INPUT_SCHEMA_PROFILE=full로 전체 스키마). - 분석은 로컬, 결과는 모델로 간다: REA 자체는 대상을 업로드하지 않지만, 에이전트가 받은 결과는 모델 제공자의 데이터 정책을 따른다.
- setup은 에이전트 설정을 고친다: 백업을 남기긴 하지만 전역 MCP 등록이 바뀐다. 잠깐 써 보는 용도라면 위처럼
--mcp-config와--strict-mcp-config로 일회성 등록을 하는 편이 깔끔하다. 릴리스가 잦으니(10월에만 6.4 → 6.7) 계속 쓴다면rea update로 갱신한다.
마무리
REA의 핵심은 새로운 분석 엔진이 아니라, 기존 도구(objdump, Ghidra, JADX 등)의 결과를 근거와 한계가 붙은 일관된 형식으로 에이전트에게 넘기는 계층이다. 엔진 없이 objdump만 있어도 에이전트가 strip된 바이너리의 분기 구조를 정확히 복원했고, Electron 앱은 실행하지 않고도 IPC 경계를 따라갔다. 깊은 분석이 필요하다면 Ghidra부터 붙여 보는 것이 다음 단계다.