Add Asm2Vec method for comparing traces
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
import importlib
|
||||||
|
|
||||||
|
__all__ = ["model", "datatype", "utils"]
|
||||||
|
|
||||||
|
for module in __all__:
|
||||||
|
importlib.import_module(f".{module}", "asm2vec")
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
import torch
|
||||||
|
import random
|
||||||
|
import warnings
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
class Token:
|
||||||
|
def __init__(self, name, index):
|
||||||
|
self.name = name
|
||||||
|
self.index = index
|
||||||
|
self.count = 1
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class Tokens:
|
||||||
|
def __init__(self, name_to_index=None, tokens=None):
|
||||||
|
self.name_to_index = name_to_index or {}
|
||||||
|
self.tokens = tokens or []
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
if type(key) is str:
|
||||||
|
if self.name_to_index.get(key) is None:
|
||||||
|
warnings.warn("Unknown token in training dataset")
|
||||||
|
return self.tokens[self.name_to_index[""]]
|
||||||
|
return self.tokens[self.name_to_index[key]]
|
||||||
|
elif type(key) is int:
|
||||||
|
return self.tokens[key]
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
return [self[k] for k in key]
|
||||||
|
except:
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
|
def load_state_dict(self, sd):
|
||||||
|
self.name_to_index = sd["name_to_index"]
|
||||||
|
self.tokens = sd["tokens"]
|
||||||
|
|
||||||
|
def state_dict(self):
|
||||||
|
return {"name_to_index": self.name_to_index, "tokens": self.tokens}
|
||||||
|
|
||||||
|
def size(self):
|
||||||
|
return len(self.tokens)
|
||||||
|
|
||||||
|
def add(self, names):
|
||||||
|
if type(names) is not list:
|
||||||
|
names = [names]
|
||||||
|
for name in names:
|
||||||
|
if name not in self.name_to_index:
|
||||||
|
token = Token(name, len(self.tokens))
|
||||||
|
self.name_to_index[name] = token.index
|
||||||
|
self.tokens.append(token)
|
||||||
|
else:
|
||||||
|
self.tokens[self.name_to_index[name]].count += 1
|
||||||
|
|
||||||
|
def update(self, tokens_new):
|
||||||
|
for token in tokens_new:
|
||||||
|
if token.name not in self.name_to_index:
|
||||||
|
token.index = len(self.tokens)
|
||||||
|
self.name_to_index[token.name] = token.index
|
||||||
|
self.tokens.append(token)
|
||||||
|
else:
|
||||||
|
self.tokens[self.name_to_index[token.name]].count += token.count
|
||||||
|
|
||||||
|
def precompute_weights(self, pos):
|
||||||
|
"""
|
||||||
|
This process actually takes a long time due to the size of the weights,
|
||||||
|
so precompute them according to the shape of the dataset.
|
||||||
|
"""
|
||||||
|
from tqdm import tqdm
|
||||||
|
|
||||||
|
total = sum([token.count for token in self.tokens])
|
||||||
|
token_weights = torch.zeros(len(self.tokens))
|
||||||
|
for token in self.tokens:
|
||||||
|
token_weights[token.index] = (token.count / total) ** 0.75
|
||||||
|
weights = token_weights.repeat(pos.shape[0], 1)
|
||||||
|
for i in tqdm(range(pos.shape[0])):
|
||||||
|
for taken in pos[i]:
|
||||||
|
weights[i][taken] = 0
|
||||||
|
return weights
|
||||||
|
|
||||||
|
def sample(self, all_weights, batch_indices, num=5):
|
||||||
|
weights = all_weights[batch_indices]
|
||||||
|
return torch.multinomial(weights, num, replacement=False)
|
||||||
|
|
||||||
|
|
||||||
|
class Function:
|
||||||
|
def __init__(self, insts, blocks, meta):
|
||||||
|
self.insts = insts
|
||||||
|
self.blocks = blocks
|
||||||
|
self.meta = meta
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load(cls, text):
|
||||||
|
"""
|
||||||
|
gcc -S format compatiable
|
||||||
|
"""
|
||||||
|
label, labels, insts, blocks, meta = None, {}, [], [], {}
|
||||||
|
for line in text.strip("\n").split("\n"):
|
||||||
|
if line[0] in [" ", "\t"]:
|
||||||
|
line = line.strip()
|
||||||
|
# meta data
|
||||||
|
if line[0] == ".":
|
||||||
|
key, _, value = line[1:].strip().partition(" ")
|
||||||
|
meta[key] = value
|
||||||
|
# instruction
|
||||||
|
else:
|
||||||
|
inst = Instruction.load(line)
|
||||||
|
insts.append(inst)
|
||||||
|
if len(blocks) == 0 or blocks[-1].end():
|
||||||
|
blocks.append(BasicBlock())
|
||||||
|
# link prev and next block
|
||||||
|
if len(blocks) > 1:
|
||||||
|
blocks[-2].successors.add(blocks[-1])
|
||||||
|
if label:
|
||||||
|
labels[label], label = blocks[-1], None
|
||||||
|
blocks[-1].add(inst)
|
||||||
|
# label
|
||||||
|
else:
|
||||||
|
label = line.partition(":")[0]
|
||||||
|
# link label
|
||||||
|
for block in blocks:
|
||||||
|
inst = block.insts[-1]
|
||||||
|
if inst.is_jmp() and labels.get(inst.args[0]):
|
||||||
|
block.successors.add(labels[inst.args[0]])
|
||||||
|
# replace label with CONST
|
||||||
|
for inst in insts:
|
||||||
|
for i, arg in enumerate(inst.args):
|
||||||
|
if labels.get(arg):
|
||||||
|
inst.args[i] = "CONST"
|
||||||
|
return cls(insts, blocks, meta)
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash("\n".join((str(inst) for inst in self.insts)))
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if isinstance(other, Function):
|
||||||
|
a = "\n".join((str(inst) for inst in self.insts))
|
||||||
|
b = "\n".join((str(inst) for inst in other.insts))
|
||||||
|
return a == b
|
||||||
|
return False
|
||||||
|
|
||||||
|
def tokens(self):
|
||||||
|
return [token for inst in self.insts for token in inst.tokens()]
|
||||||
|
|
||||||
|
def random_walk(self, num=3):
|
||||||
|
return [self._random_walk() for _ in range(num)]
|
||||||
|
|
||||||
|
def _random_walk(self):
|
||||||
|
current, visited, seq = self.blocks[0], [], []
|
||||||
|
while current not in visited:
|
||||||
|
visited.append(current)
|
||||||
|
seq += current.insts
|
||||||
|
# no following block / hit return
|
||||||
|
if len(current.successors) == 0 or current.insts[-1].op == "ret":
|
||||||
|
break
|
||||||
|
current = random.choice(list(current.successors))
|
||||||
|
return seq
|
||||||
|
|
||||||
|
|
||||||
|
class BasicBlock:
|
||||||
|
def __init__(self):
|
||||||
|
self.insts = []
|
||||||
|
self.successors = set()
|
||||||
|
|
||||||
|
def add(self, inst):
|
||||||
|
self.insts.append(inst)
|
||||||
|
|
||||||
|
def end(self):
|
||||||
|
inst = self.insts[-1]
|
||||||
|
return inst.is_jmp() or inst.op == "ret"
|
||||||
|
|
||||||
|
|
||||||
|
class Instruction:
|
||||||
|
def __init__(self, op, args):
|
||||||
|
self.op = op
|
||||||
|
self.args = args
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'{self.op} {", ".join([str(arg) for arg in self.args if str(arg)])}'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load(cls, text):
|
||||||
|
text = text.strip().strip("bnd").strip() # get rid of BND prefix
|
||||||
|
text = text.replace(" - ", " + ")
|
||||||
|
text = re.sub(r"0x[0-9a-f]+", "CONST", text)
|
||||||
|
text = re.sub(r"\*[0-9]", "*CONST", text)
|
||||||
|
text = re.sub(r" [0-9]", " CONST", text)
|
||||||
|
op, _, args = text.strip().partition(" ")
|
||||||
|
if args:
|
||||||
|
args = [arg.strip() for arg in args.split(",")]
|
||||||
|
else:
|
||||||
|
args = []
|
||||||
|
args = (args + ["", ""])[:2]
|
||||||
|
return cls(op, args)
|
||||||
|
|
||||||
|
def tokens(self):
|
||||||
|
return [self.op] + self.args
|
||||||
|
|
||||||
|
def is_jmp(self):
|
||||||
|
return "jmp" in self.op or self.op[0] == "j"
|
||||||
|
|
||||||
|
def is_call(self):
|
||||||
|
return self.op == "call"
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import torch
|
||||||
|
import torch.nn as nn
|
||||||
|
|
||||||
|
bce, sigmoid, softmax = nn.BCELoss(), nn.Sigmoid(), nn.Softmax(dim=1)
|
||||||
|
|
||||||
|
# TODO: Document how this shit works because wtf
|
||||||
|
class ASM2VEC(nn.Module):
|
||||||
|
def __init__(self, vocab_size, function_size, embedding_size):
|
||||||
|
super(ASM2VEC, self).__init__()
|
||||||
|
# Dictionary of the token embeddings: v_t
|
||||||
|
self.embeddings = nn.Embedding(
|
||||||
|
vocab_size,
|
||||||
|
embedding_size,
|
||||||
|
_weight=(torch.rand(vocab_size, embedding_size) - 0.5) / embedding_size / 2,
|
||||||
|
)
|
||||||
|
# Dictionary of the function embeddings, \theta_f_s
|
||||||
|
self.embeddings_f = nn.Embedding(
|
||||||
|
function_size,
|
||||||
|
2 * embedding_size,
|
||||||
|
_weight=(torch.rand(function_size, 2 * embedding_size) - 0.5)
|
||||||
|
/ embedding_size
|
||||||
|
/ 2,
|
||||||
|
)
|
||||||
|
# Dictionary of outputs: Transposed v'_t
|
||||||
|
self.embeddings_r = nn.Embedding(
|
||||||
|
vocab_size,
|
||||||
|
2 * embedding_size,
|
||||||
|
_weight=torch.zeros(vocab_size, 2 * embedding_size),
|
||||||
|
)
|
||||||
|
# Where the old embeddings are stored once the training step is done
|
||||||
|
self.old_embeddings_f = None
|
||||||
|
|
||||||
|
def init_estimation_mode(self, function_size_new):
|
||||||
|
device = self.embeddings.weight.device
|
||||||
|
embedding_size = self.embeddings.embedding_dim
|
||||||
|
|
||||||
|
embedding_weights = self.embeddings.weight
|
||||||
|
self.embeddings = nn.Embedding.from_pretrained(embedding_weights)
|
||||||
|
|
||||||
|
output_weights = self.embeddings_r.weight
|
||||||
|
self.embeddings_r = nn.Embedding.from_pretrained(output_weights)
|
||||||
|
|
||||||
|
self.old_embeddings_f = self.embeddings_f
|
||||||
|
self.embeddings_f = nn.Embedding(
|
||||||
|
function_size_new,
|
||||||
|
2 * embedding_size,
|
||||||
|
_weight=(
|
||||||
|
(torch.rand(function_size_new, 2 * embedding_size) - 0.5)
|
||||||
|
/ embedding_size
|
||||||
|
/ 2
|
||||||
|
).to(device),
|
||||||
|
)
|
||||||
|
|
||||||
|
def v(self, inp):
|
||||||
|
# Retrieve the embeddings for all the context tokens
|
||||||
|
e = self.embeddings(inp[:, 1:])
|
||||||
|
# Retrieve the embedding for the function, \theta_f_s
|
||||||
|
v_f = self.embeddings_f(inp[:, 0])
|
||||||
|
# Calculate CT(in_(j-1))
|
||||||
|
v_prev = torch.cat([e[:, 0], (e[:, 1] + e[:, 2]) / 2], dim=1)
|
||||||
|
# Calculate CT(in_(j+1))
|
||||||
|
v_next = torch.cat([e[:, 3], (e[:, 4] + e[:, 5]) / 2], dim=1)
|
||||||
|
# delta(in_j, f_s) = 1/3 * (\theta_f_s + CT(in_(j-1)) + CT(in_(j+1)))
|
||||||
|
v = ((v_f + v_prev + v_next) / 3).unsqueeze(2)
|
||||||
|
return v
|
||||||
|
|
||||||
|
def forward(self, inp, pos, neg):
|
||||||
|
device, batch_size = inp.device, inp.shape[0]
|
||||||
|
v = self.v(inp)
|
||||||
|
# negative sampling loss
|
||||||
|
pred = torch.bmm(self.embeddings_r(torch.cat([pos, neg], dim=1)), v).squeeze()
|
||||||
|
label = torch.cat(
|
||||||
|
[torch.ones(batch_size, 1), torch.zeros(batch_size, neg.shape[1])], dim=1
|
||||||
|
).to(device)
|
||||||
|
return bce(sigmoid(pred), label)
|
||||||
|
|
||||||
|
def predict(self, inp, pos):
|
||||||
|
device, batch_size = inp.device, inp.shape[0]
|
||||||
|
v = self.v(inp)
|
||||||
|
probs = torch.bmm(
|
||||||
|
self.embeddings_r(
|
||||||
|
torch.arange(self.embeddings_r.num_embeddings)
|
||||||
|
.repeat(batch_size, 1)
|
||||||
|
.to(device)
|
||||||
|
),
|
||||||
|
v,
|
||||||
|
).squeeze(dim=2)
|
||||||
|
return softmax(probs)
|
||||||
@@ -0,0 +1,314 @@
|
|||||||
|
import os
|
||||||
|
import time
|
||||||
|
import torch
|
||||||
|
from torch.utils.data import DataLoader, Dataset
|
||||||
|
from pathlib import Path
|
||||||
|
from .datatype import Tokens, Function, Instruction
|
||||||
|
from .model import ASM2VEC
|
||||||
|
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
|
||||||
|
CONSTANTS_RE = re.compile(r"(-? 0x[0-9a-f]+)|\*([0-9])| ([0-9])")
|
||||||
|
|
||||||
|
|
||||||
|
class TraceData:
|
||||||
|
"""
|
||||||
|
A class that stores information about traces read from file.
|
||||||
|
|
||||||
|
Terminology:
|
||||||
|
Trace/Function:
|
||||||
|
The text produced by the generate_deep_traces.py script for each
|
||||||
|
opcode switch case.
|
||||||
|
|
||||||
|
Token:
|
||||||
|
A numerical representation of the each symbol that appears in the
|
||||||
|
trace. The tokens dictionary should be shared across all traces to
|
||||||
|
ensure the same representation matches.
|
||||||
|
|
||||||
|
ID:
|
||||||
|
A representative identifier for the trace of a switch case. An ID
|
||||||
|
may point to more than one switch case, if the traces of those
|
||||||
|
switch cases are textually identical (ignoring constants).
|
||||||
|
|
||||||
|
idx:
|
||||||
|
Index of the ID/Function. This is necessary to index into the
|
||||||
|
training model embeddings.
|
||||||
|
|
||||||
|
Opcode set:
|
||||||
|
A set of opcodes that a single switch case covers.
|
||||||
|
|
||||||
|
Pointer Opcode/ptr_opcode:
|
||||||
|
A single opcode that represents the opcode set.
|
||||||
|
|
||||||
|
Constants vector:
|
||||||
|
Since the training process removes constants from the input text,
|
||||||
|
the constants vector is a "signature" generated by looking at
|
||||||
|
constants used in the trace. These are correlated with ptr_opcodes
|
||||||
|
and not IDs since constants can differ among cases represented by
|
||||||
|
the same ID.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, tokens):
|
||||||
|
self.tokens = tokens
|
||||||
|
|
||||||
|
self.traces = dict()
|
||||||
|
"""
|
||||||
|
maps trace => ID.
|
||||||
|
See class docstring for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.ids = dict()
|
||||||
|
"""
|
||||||
|
maps ID => { idx, [ptr_opcodes...] }.
|
||||||
|
See class docstring for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.constants_vectors = dict()
|
||||||
|
"""
|
||||||
|
maps ptr_opcode => constants_vector.
|
||||||
|
See class docstriing for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.opcode_sets = dict()
|
||||||
|
"""
|
||||||
|
maps ptr_opcode => [opcodes...]
|
||||||
|
See class docstriing for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __process_trace(self, ptr_opcode, text):
|
||||||
|
fn = Function.load(text)
|
||||||
|
if fn in self.traces:
|
||||||
|
id = self.traces[fn]
|
||||||
|
self.ids[id]["ptr_opcodes"].append(ptr_opcode)
|
||||||
|
else:
|
||||||
|
# Use ptr_opcode as an ID
|
||||||
|
id = ptr_opcode
|
||||||
|
|
||||||
|
self.traces[fn] = id
|
||||||
|
self.tokens.add(fn.tokens())
|
||||||
|
self.ids[id] = {
|
||||||
|
"idx": len(self.ids),
|
||||||
|
"ptr_opcodes": [ptr_opcode],
|
||||||
|
}
|
||||||
|
|
||||||
|
self.constants_vectors[ptr_opcode] = self.__get_constants_vector(text)
|
||||||
|
|
||||||
|
def __process_opcode_sets(self, opcode_sets_file):
|
||||||
|
with open(opcode_sets_file) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
self.opcode_sets = {int(op): ops for op, ops in data.items()}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __read_trace_from_file(f):
|
||||||
|
lines = f.readlines()
|
||||||
|
normalized_lines = []
|
||||||
|
for line in lines:
|
||||||
|
normalized_lines.append(" " + line)
|
||||||
|
# cap lines at 200 since Asm2Vec performance goes way down if
|
||||||
|
# the text is too long
|
||||||
|
return "".join(normalized_lines[:200])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __get_constants_vector(trace):
|
||||||
|
constants = []
|
||||||
|
for line in trace.strip("\n").split("\n"):
|
||||||
|
match = CONSTANTS_RE.search(line)
|
||||||
|
if not match or not match.lastindex:
|
||||||
|
continue
|
||||||
|
|
||||||
|
const_str = match.group(match.lastindex)
|
||||||
|
const_str = "".join(const_str.split())
|
||||||
|
const = int(const_str, 16)
|
||||||
|
|
||||||
|
# We only really care about constants less than 10000
|
||||||
|
if abs(const) < 10000:
|
||||||
|
constants.append(const)
|
||||||
|
return constants
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def load_data(paths, tokens):
|
||||||
|
"""Reads traces from paths and returns TraceData for that path."""
|
||||||
|
if type(paths) is not list:
|
||||||
|
paths = [paths]
|
||||||
|
|
||||||
|
filenames = []
|
||||||
|
for path in paths:
|
||||||
|
if os.path.isdir(path):
|
||||||
|
filenames += [
|
||||||
|
Path(path) / filename
|
||||||
|
for filename in sorted(os.listdir(path))
|
||||||
|
if os.path.isfile(Path(path) / filename)
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
filenames += [Path(path)]
|
||||||
|
|
||||||
|
trace_data = TraceData(tokens)
|
||||||
|
for filepath in filenames:
|
||||||
|
filename = os.path.basename(filepath)
|
||||||
|
file_split = os.path.splitext(filename)
|
||||||
|
file_ext = file_split[-1]
|
||||||
|
if file_ext == ".json" and filename == "opcode_sets.json":
|
||||||
|
trace_data.__process_opcode_sets(filepath)
|
||||||
|
elif file_ext == ".asm":
|
||||||
|
ptr_opcode = int(file_split[0], base=16)
|
||||||
|
with open(filepath) as f:
|
||||||
|
text = trace_data.__read_trace_from_file(f)
|
||||||
|
trace_data.__process_trace(ptr_opcode, text)
|
||||||
|
|
||||||
|
return trace_data
|
||||||
|
|
||||||
|
|
||||||
|
class AsmDataset(Dataset):
|
||||||
|
def __init__(self, x, y):
|
||||||
|
self.x = x
|
||||||
|
self.y = y
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self.x)
|
||||||
|
|
||||||
|
def __getitem__(self, index):
|
||||||
|
return index, self.x[index], self.y[index]
|
||||||
|
|
||||||
|
|
||||||
|
def preprocess(functions, tokens):
|
||||||
|
x, y = [], []
|
||||||
|
for i, fn in enumerate(functions):
|
||||||
|
for j in range(1, len(fn.insts) - 1):
|
||||||
|
x.append(
|
||||||
|
[i]
|
||||||
|
+ [
|
||||||
|
tokens[token].index
|
||||||
|
for token in fn.insts[j - 1].tokens() + fn.insts[j + 1].tokens()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
y.append([tokens[token].index for token in fn.insts[j].tokens()])
|
||||||
|
return torch.tensor(x), torch.tensor(y)
|
||||||
|
|
||||||
|
|
||||||
|
def train(
|
||||||
|
trace_data: TraceData,
|
||||||
|
model=None,
|
||||||
|
embedding_size=100,
|
||||||
|
batch_size=1024,
|
||||||
|
epochs=10,
|
||||||
|
neg_sample_num=25,
|
||||||
|
calc_acc=False,
|
||||||
|
device="cpu",
|
||||||
|
mode="train",
|
||||||
|
callback=None,
|
||||||
|
learning_rate=0.02,
|
||||||
|
):
|
||||||
|
"""Trains the model on the provided trace data."""
|
||||||
|
functions = trace_data.traces.keys()
|
||||||
|
tokens = trace_data.tokens
|
||||||
|
|
||||||
|
if mode == "train":
|
||||||
|
if model is None:
|
||||||
|
model = ASM2VEC(
|
||||||
|
tokens.size(),
|
||||||
|
function_size=len(functions),
|
||||||
|
embedding_size=embedding_size,
|
||||||
|
).to(device)
|
||||||
|
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
|
||||||
|
elif mode == "test":
|
||||||
|
if model is None:
|
||||||
|
raise ValueError("test mode requires a pretrained model")
|
||||||
|
optimizer = torch.optim.Adam(model.embeddings_f.parameters(), lr=learning_rate)
|
||||||
|
else:
|
||||||
|
raise ValueError("Unknown mode")
|
||||||
|
|
||||||
|
# Precompute the token weights so that they are cached for later use
|
||||||
|
inp, pos = preprocess(functions, tokens)
|
||||||
|
token_weights = tokens.precompute_weights(pos)
|
||||||
|
loader = DataLoader(AsmDataset(inp, pos), batch_size=batch_size, shuffle=True)
|
||||||
|
for epoch in range(epochs):
|
||||||
|
start = time.time()
|
||||||
|
loss_sum, loss_count, accs = 0.0, 0, []
|
||||||
|
|
||||||
|
model.train()
|
||||||
|
"""
|
||||||
|
Recall that the model consumes context tokens as input and uses
|
||||||
|
embeddings for each output token as an output layer of the network. The
|
||||||
|
model should output a high score for the token that correctly matches
|
||||||
|
with the given context, and a low score for tokens that don't match the
|
||||||
|
given context.
|
||||||
|
|
||||||
|
Our dataloader outputs samples in the form of
|
||||||
|
batch_size x (batch_index, input_context, correct_token).
|
||||||
|
"""
|
||||||
|
for i, (batch_indices, inp, pos) in enumerate(loader):
|
||||||
|
for j in range(pos.shape[1]):
|
||||||
|
# Sample tokens that are not the positive token.
|
||||||
|
neg = tokens.sample(token_weights, batch_indices, neg_sample_num)
|
||||||
|
pos_token = torch.unsqueeze(pos[:, j], 1)
|
||||||
|
loss = model(inp.to(device), pos_token.to(device), neg.to(device))
|
||||||
|
loss_sum, loss_count = loss_sum + loss, loss_count + 1
|
||||||
|
|
||||||
|
optimizer.zero_grad()
|
||||||
|
loss.backward()
|
||||||
|
optimizer.step()
|
||||||
|
|
||||||
|
if i == 0 and calc_acc:
|
||||||
|
probs = model.predict(inp.to(device), pos.to(device))
|
||||||
|
accs.append(accuracy(pos, probs))
|
||||||
|
|
||||||
|
if callback:
|
||||||
|
callback(
|
||||||
|
{
|
||||||
|
"model": model,
|
||||||
|
"tokens": tokens,
|
||||||
|
"epoch": epoch,
|
||||||
|
"time": time.time() - start,
|
||||||
|
"loss": loss_sum / loss_count,
|
||||||
|
"accuracy": torch.tensor(accs).mean() if calc_acc else None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return model
|
||||||
|
|
||||||
|
|
||||||
|
def save_model(path, model, tokens):
|
||||||
|
torch.save(
|
||||||
|
{
|
||||||
|
"model_params": (
|
||||||
|
model.embeddings.num_embeddings,
|
||||||
|
model.embeddings_f.num_embeddings,
|
||||||
|
model.embeddings.embedding_dim,
|
||||||
|
),
|
||||||
|
"model": model.state_dict(),
|
||||||
|
"tokens": tokens.state_dict(),
|
||||||
|
},
|
||||||
|
path,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def load_model(path, device="cpu"):
|
||||||
|
checkpoint = torch.load(path, map_location=device)
|
||||||
|
tokens = Tokens()
|
||||||
|
tokens.load_state_dict(checkpoint["tokens"])
|
||||||
|
model = ASM2VEC(*checkpoint["model_params"])
|
||||||
|
model.load_state_dict(checkpoint["model"])
|
||||||
|
model = model.to(device)
|
||||||
|
return model, tokens
|
||||||
|
|
||||||
|
|
||||||
|
def accuracy(y, probs):
|
||||||
|
return torch.mean(torch.tensor([torch.sum(probs[i][yi]) for i, yi in enumerate(y)]))
|
||||||
|
|
||||||
|
|
||||||
|
def cosine_similarities(model):
|
||||||
|
"""
|
||||||
|
Reads the old and new embeddings from the model and returns the pairwise
|
||||||
|
cosine similarities between these embeddings.
|
||||||
|
"""
|
||||||
|
old_f = model.to("cpu").old_embeddings_f
|
||||||
|
new_f = model.to("cpu").embeddings_f
|
||||||
|
v_old = old_f(torch.tensor([i for i in range(old_f.num_embeddings)]))
|
||||||
|
v_new = new_f(torch.tensor([i for i in range(new_f.num_embeddings)]))
|
||||||
|
|
||||||
|
cs_matrix = torch.nn.functional.cosine_similarity(
|
||||||
|
v_old[:, :, None], v_new.t()[None, :, :]
|
||||||
|
)
|
||||||
|
|
||||||
|
return cs_matrix.detach().numpy()
|
||||||
+324
@@ -0,0 +1,324 @@
|
|||||||
|
import click
|
||||||
|
import torch
|
||||||
|
from asm2vec.utils import (
|
||||||
|
TraceData,
|
||||||
|
AsmDataset,
|
||||||
|
preprocess,
|
||||||
|
train,
|
||||||
|
save_model,
|
||||||
|
cosine_similarities,
|
||||||
|
)
|
||||||
|
from asm2vec.datatype import Tokens
|
||||||
|
import json
|
||||||
|
|
||||||
|
import Levenshtein as lev
|
||||||
|
|
||||||
|
|
||||||
|
class OpcodeMatcher:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
cosine_similarity_matrix,
|
||||||
|
old_trace_data: TraceData,
|
||||||
|
new_trace_data: TraceData,
|
||||||
|
):
|
||||||
|
self.csm = cosine_similarity_matrix
|
||||||
|
self.old_trace_data = old_trace_data
|
||||||
|
self.new_trace_data = new_trace_data
|
||||||
|
|
||||||
|
self.old_functions = list(old_trace_data.traces.keys())
|
||||||
|
self.new_functions = list(new_trace_data.traces.keys())
|
||||||
|
|
||||||
|
self.old_ptr_opcodes = self.__enumerate_ptr_opcodes(old_trace_data)
|
||||||
|
self.new_ptr_opcodes = self.__enumerate_ptr_opcodes(new_trace_data)
|
||||||
|
|
||||||
|
self.cand_dict = None
|
||||||
|
|
||||||
|
def length_heuristic(self, old_idx, new_idx, debug=False):
|
||||||
|
"""
|
||||||
|
Function length heuristic (since asm2vec is terrible at handling mismatched lengths)
|
||||||
|
|
||||||
|
Returns a similarity metric in the range [0, 1]
|
||||||
|
"""
|
||||||
|
l0 = len(self.old_functions[old_idx].insts)
|
||||||
|
l1 = len(self.new_functions[new_idx].insts)
|
||||||
|
length_diff = abs(l0 - l1)
|
||||||
|
# Weight mismatched lengths considerably lower, but clip factor to 0
|
||||||
|
length_factor = max(1 - 4 * (length_diff / (l0 + l1)), 0)
|
||||||
|
if debug:
|
||||||
|
print("Length factor", l0, l1, length_factor)
|
||||||
|
return length_factor
|
||||||
|
|
||||||
|
def constants_heuristic(self, old_opcode, new_opcode, debug=False):
|
||||||
|
"""
|
||||||
|
Constants vector heuristic.
|
||||||
|
Runs a bit slow because it uses Levenshtein distance.
|
||||||
|
|
||||||
|
Returns a similarity metric in the range [0, 1]
|
||||||
|
"""
|
||||||
|
v0 = self.old_trace_data.constants_vectors[old_opcode]
|
||||||
|
v1 = self.new_trace_data.constants_vectors[new_opcode]
|
||||||
|
constants_diff = lev.distance(v0[:50], v1[:50])
|
||||||
|
# Weight any constants differences harshly, but clip factor to 0
|
||||||
|
constants_factor = max(1 - (constants_diff * 0.1), 0)
|
||||||
|
if debug:
|
||||||
|
print("Constants factor", constants_diff)
|
||||||
|
print("v0", v0)
|
||||||
|
print("v1", v1)
|
||||||
|
return constants_factor
|
||||||
|
|
||||||
|
def case_length_heuristic(self, old_opcode, new_opcode, debug=False):
|
||||||
|
"""
|
||||||
|
Number of opcodes in case heuristic.
|
||||||
|
|
||||||
|
This one has questionable value because cases with different number of
|
||||||
|
opcodes are already different enough from other cases.
|
||||||
|
|
||||||
|
Returns a similarity metric in the range [0, 1]
|
||||||
|
"""
|
||||||
|
n0 = len(self.old_trace_data.opcode_sets[old_opcode])
|
||||||
|
n1 = len(self.new_trace_data.opcode_sets[new_opcode])
|
||||||
|
case_length_diff = abs(n0 - n1)
|
||||||
|
case_length_factor = max(1 - case_length_diff * 0.1, 0)
|
||||||
|
if debug:
|
||||||
|
print("Case length factor", n0, n1, case_length_diff)
|
||||||
|
|
||||||
|
return case_length_factor
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __enumerate_ptr_opcodes(trace_data: TraceData):
|
||||||
|
ptr_opcodes = []
|
||||||
|
for id, data in trace_data.ids.items():
|
||||||
|
idx = data["idx"]
|
||||||
|
for opcode in data["ptr_opcodes"]:
|
||||||
|
ptr_opcodes.append((idx, opcode))
|
||||||
|
return ptr_opcodes
|
||||||
|
|
||||||
|
def initialize_candidates(self):
|
||||||
|
"""
|
||||||
|
Generates a table of candidate matches between old pointer opcodes and
|
||||||
|
new pointer opcodes.
|
||||||
|
"""
|
||||||
|
self.cand_dict = dict()
|
||||||
|
|
||||||
|
for (old_idx, old_opcode) in self.old_ptr_opcodes:
|
||||||
|
candidates = []
|
||||||
|
for (new_idx, new_opcode) in self.new_ptr_opcodes:
|
||||||
|
length_factor = self.length_heuristic(old_idx, new_idx)
|
||||||
|
constants_factor = self.constants_heuristic(old_opcode, new_opcode)
|
||||||
|
case_length_factor = self.case_length_heuristic(old_opcode, new_opcode)
|
||||||
|
# Since cosine similarity is in the range (-1, 1), add 1 to push it
|
||||||
|
# into the range (0, 2).
|
||||||
|
cs = self.csm[old_idx, new_idx] + 1
|
||||||
|
|
||||||
|
# Multiply all these factors together to yield some value in the range
|
||||||
|
# (0, 2), then subtract 1 to get a score from range (-1, 1)
|
||||||
|
score = length_factor * constants_factor * case_length_factor * cs - 1
|
||||||
|
candidates.append((new_opcode, score))
|
||||||
|
candidates.sort(key=lambda x: x[1], reverse=True)
|
||||||
|
|
||||||
|
if candidates[0][1] < -0.99:
|
||||||
|
# If the top match is this low, then the heuristics screwed up the
|
||||||
|
# candidates, so we'll have to go with just the cosine similarity metric
|
||||||
|
candidates = [
|
||||||
|
(new_opcode, self.csm[old_idx, new_idx])
|
||||||
|
for (new_idx, new_opcode) in self.new_ptr_opcodes
|
||||||
|
]
|
||||||
|
candidates.sort(key=lambda x: x[1], reverse=True)
|
||||||
|
candidates = candidates[:5]
|
||||||
|
|
||||||
|
self.cand_dict[old_opcode] = candidates
|
||||||
|
|
||||||
|
def accept_confident_matches(self, matches, threshold=0.1):
|
||||||
|
"""
|
||||||
|
Accepts matches for candidates where the score difference between the
|
||||||
|
first and second best match is wider than the given threshold.
|
||||||
|
"""
|
||||||
|
num_new_matches = 0
|
||||||
|
accepted_match_targets = set()
|
||||||
|
unmatched = []
|
||||||
|
|
||||||
|
for opcode, candidates in self.cand_dict.items():
|
||||||
|
if len(candidates) == 1 or (
|
||||||
|
len(candidates) > 1
|
||||||
|
and (candidates[0][1] - candidates[1][1] > threshold)
|
||||||
|
):
|
||||||
|
matches[opcode] = {
|
||||||
|
"match": candidates[0][0],
|
||||||
|
"score_lead": candidates[0][1] - candidates[1][1]
|
||||||
|
if len(candidates) > 1
|
||||||
|
else 0,
|
||||||
|
}
|
||||||
|
accepted_match_targets.add(candidates[0][0])
|
||||||
|
num_new_matches += 1
|
||||||
|
else:
|
||||||
|
unmatched.append((opcode, candidates))
|
||||||
|
|
||||||
|
# Filter out match candidates that have already been matched
|
||||||
|
new_candidates = dict()
|
||||||
|
for opcode, candidates in unmatched:
|
||||||
|
new_candidates[opcode] = [
|
||||||
|
(cand_opcode, score)
|
||||||
|
for (cand_opcode, score) in candidates
|
||||||
|
if cand_opcode not in accepted_match_targets
|
||||||
|
]
|
||||||
|
|
||||||
|
self.cand_dict = new_candidates
|
||||||
|
return num_new_matches
|
||||||
|
|
||||||
|
def find_opcode_matches(self, threshold=0.1):
|
||||||
|
"""
|
||||||
|
Returns the best matches between old pointer opcodes and new pointer
|
||||||
|
opcodes where the confidence is greater than the given threshold.
|
||||||
|
"""
|
||||||
|
matches = dict()
|
||||||
|
self.initialize_candidates()
|
||||||
|
num_new_matches = self.accept_confident_matches(matches, threshold)
|
||||||
|
print("First pass added", num_new_matches, "matches")
|
||||||
|
while num_new_matches > 0:
|
||||||
|
num_new_matches = self.accept_confident_matches(matches, threshold)
|
||||||
|
print("Added", num_new_matches, "additional matches")
|
||||||
|
return matches
|
||||||
|
|
||||||
|
def find_matches_and_nonmatches(self):
|
||||||
|
"""
|
||||||
|
Returns the following information:
|
||||||
|
|
||||||
|
1. The best matches between old pointer opcodes and new pointer
|
||||||
|
opcodes where the confidence is greater than the given threshold.
|
||||||
|
2. Old opcodes for which a match could not be confidently found.
|
||||||
|
3. New opcodes for which a match could not be confidently found.
|
||||||
|
|
||||||
|
The format of the output is a list (all fields are optional):
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"old": (list of opcodes in the switch case),
|
||||||
|
"new": (list of opcodes in the switch case),
|
||||||
|
"score_lead": (confidence above 2nd best match),
|
||||||
|
"unknown": (true if a match was not made in this case),
|
||||||
|
"candidates: [
|
||||||
|
{
|
||||||
|
"set": (list of opcodes in switch case),
|
||||||
|
"score": (candidate score),
|
||||||
|
}
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
"""
|
||||||
|
output = []
|
||||||
|
|
||||||
|
matches = self.find_opcode_matches()
|
||||||
|
old_opcode_sets = self.old_trace_data.opcode_sets
|
||||||
|
new_opcode_sets = self.new_trace_data.opcode_sets
|
||||||
|
|
||||||
|
for opcode, data in matches.items():
|
||||||
|
output.append(
|
||||||
|
{
|
||||||
|
"old": [hex(old_opcode) for old_opcode in old_opcode_sets[opcode]],
|
||||||
|
"new": [
|
||||||
|
hex(new_opcode) for new_opcode in new_opcode_sets[data["match"]]
|
||||||
|
],
|
||||||
|
"score_lead": str(data["score_lead"]),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
for opcode, candidates in self.cand_dict.items():
|
||||||
|
output.append(
|
||||||
|
{
|
||||||
|
"old": [hex(old_opcode) for old_opcode in old_opcode_sets[opcode]],
|
||||||
|
"candidates": [
|
||||||
|
{
|
||||||
|
"set": [
|
||||||
|
hex(new_opcode)
|
||||||
|
for new_opcode in new_opcode_sets[candidate]
|
||||||
|
],
|
||||||
|
"score": str(score),
|
||||||
|
}
|
||||||
|
for (candidate, score) in candidates
|
||||||
|
if score > -1.0
|
||||||
|
],
|
||||||
|
"unknown": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
unmatched_new_opcodes = set([opcode for (idx, opcode) in self.new_ptr_opcodes])
|
||||||
|
for data in matches.values():
|
||||||
|
if data["match"] in unmatched_new_opcodes:
|
||||||
|
unmatched_new_opcodes.discard(data["match"])
|
||||||
|
|
||||||
|
for unmatched_opcode in unmatched_new_opcodes:
|
||||||
|
output.append(
|
||||||
|
{
|
||||||
|
"new": [
|
||||||
|
hex(new_opcode)
|
||||||
|
for new_opcode in new_opcode_sets[unmatched_opcode]
|
||||||
|
],
|
||||||
|
"unknown": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
def print_banner(text):
|
||||||
|
print("")
|
||||||
|
print(f"======= {text} =======")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
@click.argument(
|
||||||
|
"old_traces", type=click.Path(exists=True, file_okay=False, resolve_path=True)
|
||||||
|
)
|
||||||
|
@click.argument(
|
||||||
|
"new_traces", type=click.Path(exists=True, file_okay=False, resolve_path=True)
|
||||||
|
)
|
||||||
|
@click.argument("output_file", type=click.Path(dir_okay=False, resolve_path=True))
|
||||||
|
def traces_diff(old_traces, new_traces, output_file):
|
||||||
|
tokens = Tokens()
|
||||||
|
old_trace_data = TraceData.load_data(old_traces, tokens)
|
||||||
|
new_trace_data = TraceData.load_data(new_traces, tokens)
|
||||||
|
|
||||||
|
opath = "model.pt"
|
||||||
|
|
||||||
|
def training_callback(context):
|
||||||
|
progress = f'{context["epoch"]} | time = {context["time"]:.2f}, loss = {context["loss"]:.4f}'
|
||||||
|
if context["accuracy"]:
|
||||||
|
progress += f', accuracy = {context["accuracy"]:.4f}'
|
||||||
|
print(progress)
|
||||||
|
save_model(opath, context["model"], context["tokens"])
|
||||||
|
|
||||||
|
training_params = {
|
||||||
|
"embedding_size": 100,
|
||||||
|
"batch_size": 1024,
|
||||||
|
"epochs": 20,
|
||||||
|
"neg_sample_num": 25,
|
||||||
|
"calc_acc": True,
|
||||||
|
"device": "cuda" if torch.cuda.is_available() else "cpu",
|
||||||
|
"callback": training_callback,
|
||||||
|
"learning_rate": 0.02,
|
||||||
|
}
|
||||||
|
|
||||||
|
print_banner("Training embeddings from scratch on old trace data")
|
||||||
|
model = train(old_trace_data, **training_params)
|
||||||
|
|
||||||
|
# Prepare the model for new trace data and freeze all training from old trace data
|
||||||
|
model.init_estimation_mode(len(new_trace_data.traces))
|
||||||
|
|
||||||
|
print_banner("Calculating embeddings for new trace data")
|
||||||
|
model = train(new_trace_data, model=model, mode="test", **training_params)
|
||||||
|
|
||||||
|
print_banner("Calculating cosine similarities")
|
||||||
|
csm = cosine_similarities(model)
|
||||||
|
|
||||||
|
print_banner("Calculating matches between opcodes")
|
||||||
|
matcher = OpcodeMatcher(csm, old_trace_data, new_trace_data)
|
||||||
|
compiled_data = matcher.find_matches_and_nonmatches()
|
||||||
|
|
||||||
|
with open(output_file, "w+") as f:
|
||||||
|
json.dump(compiled_data, f, indent=2)
|
||||||
|
print_banner(f"Output written to {output_file}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
traces_diff()
|
||||||
Reference in New Issue
Block a user