코드 생성에는 GPT를 활용하였다.
문제 상황
Virtualize 난독화가 적용된 asm 코드를 입력으로 하여 코드를 분할하고 라벨링을 자동화 할 수 있는 코드 생성
가정
- 입력 : Virtualize 난독화가 적용된 asm 코드
- 출력 : 분할된 asm 코드 집합 + labeling된 데이터
- 요구사항 : "call"을 기준으로 코드 분할
위 상황에 대해 구체적인 문제에 대입 가능하도록 하는 코드 초안을 생성
사용된 원본 .c 코드는 약 4KB 코드를 사용하였으며, Virtualize 난독화를 적용한 후에는 약 695KB의 코드가 되었다.
Python Code 생성
직접 pass를 사용하고 생성하는 것을 바로 하기에는 힘들 것으로 보여 python code를 1차적으로 생성하고,
요구사항에 맞춰서 수정하였다.
python code 에 사용된 입력은 clang -S 으로 컴파일된 .s 파일을 사용하였다.
정규식을 이용하여 단순 "call" 찾아 코드를 분할 한 뒤, .jsonl에 id와 labeling 등의 정보를 저장할 수 있게 했다.
import re
import json
import os
def split_asm_file(input_file, output_file):
# 폴더 & 기본 이름 준비
base_name = os.path.splitext(os.path.basename(input_file))[0]
folder = os.path.join(os.path.dirname(output_file) or ".", base_name)
os.makedirs(folder, exist_ok=True)
with open(input_file, "r", encoding="utf-8") as f:
lines = f.readlines()
chunks_meta = []
current_chunk = []
chunk_id = 0
for line in lines:
line_strip = line.strip()
if not line_strip:
continue # 빈 줄 무시
current_chunk.append(line_strip)
# 함수 시작 라벨 추출(.globl, <func>: 같은 부분)
if re.match(r"^[a-zA-Z0-9_]+:$", line_strip):
labels = ["FUNC_ENTRY"]
else:
labels = []
# call 명령어 감지
if re.search(r"\bcall\b", line_strip):
labels.append("CALL_SITE")
# 청크 id 및 파일명 생성
chunk_name = f"{base_name}_chunk_{chunk_id}"
chunk_filename = chunk_name + ".s"
chunk_path = os.path.join(folder, chunk_filename)
# 청크 내용 파일로 저장 (각 라인 뒤에 newline 추가)
with open(chunk_path, "w", encoding="utf-8") as cf:
cf.write("\n".join(current_chunk) + "\n")
# 메타 정보만 JSONL에 쓸 리스트에 추가 (asm 필드 없음)
chunks_meta.append({
"id": chunk_name,
"labels": labels,
"file": os.path.relpath(chunk_path) # 상대경로로 저장
})
chunk_id += 1
current_chunk = [] # 새 청크 시작
# 마지막 청크가 남아있다면 파일로 저장하고 메타에 추가
if current_chunk:
chunk_name = f"{base_name}_chunk_{chunk_id}"
chunk_filename = chunk_name + ".s"
chunk_path = os.path.join(folder, chunk_filename)
with open(chunk_path, "w", encoding="utf-8") as cf:
cf.write("\n".join(current_chunk) + "\n")
chunks_meta.append({
"id": chunk_name,
"labels": [],
"file": os.path.relpath(chunk_path)
})
# JSONL 저장 (asm 필드 없음)
with open(output_file, "w", encoding="utf-8") as f:
for meta in chunks_meta:
f.write(json.dumps(meta, ensure_ascii=False) + "\n")
print(f"[+] Saved {len(chunks_meta)} chunks to {output_file}")
print(f"[+] Chunk files are in folder: {folder}")
if __name__ == "__main__":
input_file = "test_out.s" # 분석할 asm 파일
output_file = "test_out.jsonl" # 결과 저장할 JSONL 파일
split_asm_file(input_file, output_file)
결과 예시
생성된 파일은 약 1KB ~ 300KB 의 파일들이 41개 생성되었다.





C++ Code 생성
위에서 생성한 python 코드를 바탕으로 LLVM Pass 를 활용한 C++ 코드를 생성하도록 했다.
python에서 불필요한 정보들을 제거하고, 하나의 코드에서 분할된 코드 집합과 jsonl 파일을 한 파일에 생성되도록 수정하였다.
// CallSplitterPass.cpp
#include "llvm/ADT/StringRef.h"
#include "llvm/IR/Function.h"
#include "llvm/IR/InstrTypes.h"
#include "llvm/IR/Instructions.h"
#include "llvm/IR/Module.h"
#include "llvm/IR/PassManager.h"
#include "llvm/Passes/PassPlugin.h"
#include "llvm/Passes/PassBuilder.h"
#include "llvm/Support/CommandLine.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/Path.h"
#include "llvm/Support/raw_ostream.h"
#include <filesystem>
#include <vector>
#include <string>
#include <fstream>
using namespace llvm;
namespace fs = std::filesystem;
static cl::opt<std::string> OutputDir("split-asm-dir",
cl::desc("Directory to place chunk files"),
cl::init("split_asm_out"));
static cl::opt<std::string> JsonOut("split-asm-json",
cl::desc("JSONL output file"),
cl::init("chunks.jsonl"));
namespace {
struct CallSplitterPass : public PassInfoMixin<CallSplitterPass> {
PreservedAnalyses run(Module &M, ModuleAnalysisManager &MAM) {
// 1) 모듈 이름 가져오기
std::string moduleName = M.getSourceFileName();
if (moduleName.empty()) {
StringRef mid = M.getModuleIdentifier();
moduleName = sys::path::stem(mid).str(); // stem(StringRef) -> StringRef, .str()로 std::string으로 변환
}
if (moduleName.empty())
moduleName = "module";
// 2) 출력 디렉토리
fs::path baseDir = OutputDir.getValue();
std::error_code ec_fs;
fs::create_directories(baseDir, ec_fs);
if (ec_fs) {
errs() << "Warning: cannot create dir '" << baseDir.string() << "': " << ec_fs.message() << "\n";
}
fs::path moduleDir = baseDir / moduleName;
fs::create_directories(moduleDir, ec_fs);
if (ec_fs) {
errs() << "Warning: cannot create module dir '" << moduleDir.string() << "': " << ec_fs.message() << "\n";
}
fs::path jsonPath = moduleDir / JsonOut.getValue();
// 3) JSONL 파일 (덮어쓰기)
std::ofstream jsonl(jsonPath, std::ios::out | std::ios::trunc);
if (!jsonl.is_open()) {
errs() << "Error: cannot open JSONL file: " << JsonOut << "\n";
return PreservedAnalyses::all();
}
unsigned chunk_id = 0;
for (Function &F : M) {
if (F.isDeclaration()) continue;
std::vector<std::string> currentChunkLines;
std::vector<std::string> currentLabels;
bool seenAnyInstInFunction = false;
for (BasicBlock &BB : F) {
for (Instruction &I : BB) {
// 라벨 추가
if (!seenAnyInstInFunction) {
currentLabels.push_back("FUNC_ENTRY");
seenAnyInstInFunction = true;
}
// instruction -> 문자열
std::string instStr;
raw_string_ostream rso(instStr);
I.print(rso);
rso.flush();
currentChunkLines.push_back(instStr);
// Call 계열이면 청크 저장
if (isa<CallBase>(&I)) {
currentLabels.push_back("CALL_SITE");
std::string chunkName = "chunk_" + std::to_string(chunk_id);
std::string chunkFilename = chunkName + ".s";
fs::path chunkPath = moduleDir / chunkFilename;
// 파일 쓰기 (llvm raw_fd_ostream 사용)
std::error_code ec;
raw_fd_ostream ofs(chunkPath.string(), ec, sys::fs::OF_Text);
if (ec) {
errs() << "Warning: cannot write chunk file " << chunkPath.string() << ": " << ec.message() << "\n";
} else {
for (auto &L : currentChunkLines) ofs << L << "\n";
}
// JSONL 기록
jsonl << "{\"id\":\"" << chunkName << "\",";
jsonl << "\"labels\":[";
for (size_t li = 0; li < currentLabels.size(); ++li) {
jsonl << "\"" << currentLabels[li] << "\"";
if (li + 1 < currentLabels.size()) jsonl << ",";
}
jsonl << "]";
jsonl << "}\n";
++chunk_id;
currentChunkLines.clear();
currentLabels.clear();
seenAnyInstInFunction = false; // 다음 청크는 새로운 시작
}
}
}
// 함수 끝에 남은 청크 저장
if (!currentChunkLines.empty()) {
std::string chunkName = "chunk_" + std::to_string(chunk_id);
std::string chunkFilename = chunkName + ".s";
fs::path chunkPath = moduleDir / chunkFilename;
std::error_code ec;
raw_fd_ostream ofs(chunkPath.string(), ec, sys::fs::OF_Text);
if (ec) {
errs() << "Warning: cannot write chunk file " << chunkPath.string() << ": " << ec.message() << "\n";
} else {
for (auto &L : currentChunkLines) ofs << L << "\n";
}
jsonl << "{\"id\":\"" << chunkName << "\",";
jsonl << "\"labels\":[";
for (size_t li = 0; li < currentLabels.size(); ++li) {
jsonl << "\"" << currentLabels[li] << "\"";
if (li + 1 < currentLabels.size()) jsonl << ",";
}
jsonl << "]";
jsonl << "}\n";
++chunk_id;
}
}
jsonl.close();
errs() << "[CallSplitterPass] wrote " << chunk_id << " chunks to directory '" << moduleDir.string()
<< "' and JSONL '" << JsonOut << "'\n";
return PreservedAnalyses::all();
}
};
} // namespace
// plugin registration
extern "C" LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo llvmGetPassPluginInfo() {
return {
LLVM_PLUGIN_API_VERSION, "CallSplitterPass", LLVM_VERSION_STRING,
[](PassBuilder &PB) {
PB.registerPipelineParsingCallback(
[](StringRef Name, ModulePassManager &MPM,
ArrayRef<PassBuilder::PipelineElement>) -> bool {
if (Name == "call-splitter") {
MPM.addPass(CallSplitterPass());
return true;
}
return false;
});
}
};
}
사용한 컴파일 코드
clang++ -std=c++17 -fPIC -shared CallSplitterPass.cpp -o CallSplitterPass.so \
`llvm-config --cxxflags`
clang -O1 -emit-llvm -c -Xclang -disable-O0-optnone -o test_out.bc test_out.c
opt -load-pass-plugin=./CallSplitterPass.so -passes="call-splitter" \
-split-asm-dir=split_asm_out -split-asm-json=chunks.jsonl test_out.bc -o /dev/null
결과 예시
생성된 파일은 약 1KB ~ 40KB의 asm 파일들이 177개 생성되었다.





정리하며
정규화 식을 사용하여 단순 call을 기준으로 분할한 python 코드에 비해 llvm pass를 활용하여 분할한 코드가 훨씬 균일하게 분할되는 것을 확인할 수 있었다.
또한, llvm pass 를 활용하여 분할한 코드에서 tail call이 연속적으로 호출되어 1줄짜리 코드로 분할되어 다수 생성되는 것이 확인됐다.
전체적으로 단순한 동작을 구현하고 있으나, 후일 추가적인 요구사항에 따라 수정할 예정이다.