Skip to content

Commit a700471

Browse files
committed
[fisim/crypto] Add instruction skip simulator
This only works for a debug enabled FPGA/chip and is only been tested on a CW340! Add a script to load the pentest framework, connect to OpenOCD, and connect to GDB. Generate a trace file of a called function. Use the trace file to insert instruction skips in order to simulate fault attacks and test countermeasures. Signed-off-by: Siemen Dhooghe <[email protected]>
1 parent fc2d73b commit a700471

File tree

8 files changed

+2064
-36
lines changed

8 files changed

+2064
-36
lines changed

my-changes.patch

Lines changed: 1290 additions & 0 deletions
Large diffs are not rendered by default.

sw/device/tests/penetrationtests/BUILD

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,22 @@ pentest_cryptolib_fi_asym(
295295
test_vectors = [],
296296
)
297297

298+
pentest_cryptolib_fi_asym(
299+
name = "fi_asym_cryptolib_python_gdb_test",
300+
tags = [],
301+
test_args = "",
302+
test_harness = "//sw/host/penetrationtests/python/fi:fi_asym_cryptolib_python_gdb_test",
303+
test_vectors = [],
304+
)
305+
306+
pentest_cryptolib_fi_asym(
307+
name = "fi_asym_cryptolib_python_gdb_test_manual",
308+
tags = [],
309+
test_args = "",
310+
test_harness = "//sw/host/penetrationtests/python/fi:fi_asym_cryptolib_python_gdb_test_manual",
311+
test_vectors = [],
312+
)
313+
298314
CRYPTOLIB_SCA_SYM_TESTVECTOR_TARGETS = [
299315
"//sw/host/penetrationtests/testvectors/data:sca_sym_cryptolib",
300316
]

sw/host/penetrationtests/python/fi/BUILD

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,50 @@ py_binary(
168168
],
169169
)
170170

171+
py_binary(
172+
name = "fi_asym_cryptolib_python_gdb_test",
173+
testonly = True,
174+
srcs = ["gdb_testing/fi_asym_cryptolib_python_gdb_test.py"],
175+
data = [
176+
"//sw/host/opentitantool",
177+
"//third_party/openocd:openocd_bin",
178+
"//util/openocd/board:cw340_ftdi.cfg",
179+
"//util/openocd/target:lowrisc-earlgrey.cfg",
180+
],
181+
deps = [
182+
"//sw/host/penetrationtests/python/fi:fi_asym_cryptolib_commands",
183+
"//sw/host/penetrationtests/python/fi:fi_asym_cryptolib_functions",
184+
"//sw/host/penetrationtests/python/util:common_library",
185+
"//sw/host/penetrationtests/python/util:gdb_controller",
186+
"//sw/host/penetrationtests/python/util:targets",
187+
"//sw/host/penetrationtests/python/util:utils",
188+
"@rules_python//python/runfiles",
189+
requirement("pycryptodome"),
190+
],
191+
)
192+
193+
py_binary(
194+
name = "fi_asym_cryptolib_python_gdb_test_manual",
195+
testonly = True,
196+
srcs = ["gdb_testing/fi_asym_cryptolib_python_gdb_test_manual.py"],
197+
data = [
198+
"//sw/host/opentitantool",
199+
"//third_party/openocd:openocd_bin",
200+
"//util/openocd/board:cw340_ftdi.cfg",
201+
"//util/openocd/target:lowrisc-earlgrey.cfg",
202+
],
203+
deps = [
204+
"//sw/host/penetrationtests/python/fi:fi_asym_cryptolib_commands",
205+
"//sw/host/penetrationtests/python/fi:fi_asym_cryptolib_functions",
206+
"//sw/host/penetrationtests/python/util:common_library",
207+
"//sw/host/penetrationtests/python/util:gdb_controller",
208+
"//sw/host/penetrationtests/python/util:targets",
209+
"//sw/host/penetrationtests/python/util:utils",
210+
"@rules_python//python/runfiles",
211+
requirement("pycryptodome"),
212+
],
213+
)
214+
171215
py_library(
172216
name = "fi_ibex_functions",
173217
srcs = ["host_scripts/fi_ibex_functions.py"],
Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
# Copyright lowRISC contributors (OpenTitan project).
2+
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
3+
# SPDX-License-Identifier: Apache-2.0
4+
5+
from sw.host.penetrationtests.python.fi.host_scripts import fi_asym_cryptolib_functions
6+
from sw.host.penetrationtests.python.fi.communication.fi_asym_cryptolib_commands import (
7+
OTFIAsymCrypto,
8+
)
9+
from python.runfiles import Runfiles
10+
from sw.host.penetrationtests.python.util import targets
11+
from sw.host.penetrationtests.python.util import utils
12+
from sw.host.penetrationtests.python.util import common_library
13+
from sw.host.penetrationtests.python.util.gdb_controller import GDBController
14+
import json
15+
import unittest
16+
import argparse
17+
import sys
18+
import random
19+
from Crypto.PublicKey import RSA, ECC
20+
from Crypto.Signature import pkcs1_15, DSS
21+
from Crypto.Hash import SHA256, SHA384
22+
23+
import subprocess
24+
import os
25+
import sys
26+
import time
27+
28+
ignored_keys_set = set(["status"])
29+
opentitantool_path = ""
30+
iterations = 1
31+
repetitions = 3
32+
33+
target = None
34+
asymfi = None
35+
36+
# Read in the extra arguments from the opentitan_test.
37+
parser = argparse.ArgumentParser()
38+
parser.add_argument("--bitstream", type=str)
39+
parser.add_argument("--bootstrap", type=str)
40+
41+
args, config_args = parser.parse_known_args()
42+
43+
BITSTREAM = args.bitstream
44+
BOOTSTRAP = args.bootstrap
45+
46+
47+
# Preparing the input for an invalid signature
48+
key = ECC.generate(curve="P-384")
49+
pubx = [x for x in key.pointQ.x.to_bytes(48, "little")]
50+
puby = [x for x in key.pointQ.y.to_bytes(48, "little")]
51+
message = [i for i in range(16)]
52+
h = SHA384.new(bytes(message))
53+
signer = DSS.new(key, "fips-186-3")
54+
signature = [x for x in signer.sign(h)]
55+
# Corrupted the signature for FiSim Testing
56+
signature[0] ^= 0x1
57+
r_bytes = signature[:48]
58+
s_bytes = signature[48:]
59+
r_bytes.reverse()
60+
s_bytes.reverse()
61+
cfg = 0
62+
trigger = 1
63+
h = SHA384.new(bytes(message))
64+
message_digest = [x for x in h.digest()]
65+
66+
67+
def trigger_testos_init(print_output=True):
68+
# Initializing the testOS (setting up the alerts and accelerators)
69+
device_id, sensors, alerts, owner_page, boot_log, boot_measurements, version = asymfi.init(
70+
alert_config=common_library.default_fpga_friendly_alert_config
71+
)
72+
if print_output:
73+
print("Output from init ", device_id)
74+
75+
76+
def trigger_p384_verify():
77+
asymfi.handle_p384_verify(pubx, puby, r_bytes, s_bytes, message_digest, cfg, trigger)
78+
79+
80+
def read_testos_output():
81+
# Read the output from the operation
82+
response = target.read_response(max_tries=500)
83+
return response
84+
85+
86+
if __name__ == "__main__":
87+
r = Runfiles.Create()
88+
# Get the openocd path.
89+
openocd_path = r.Rlocation("lowrisc_opentitan/third_party/openocd/build_openocd/bin/openocd")
90+
# Get the openocd config files.
91+
# The first file is on the cw340 (this is specific to the cw340)
92+
CONFIG_FILE_CHIP = r.Rlocation("lowrisc_opentitan/util/openocd/board/cw340_ftdi.cfg")
93+
# The config for the earlgrey design
94+
CONFIG_FILE_DESIGN = r.Rlocation("lowrisc_opentitan/util/openocd/target/lowrisc-earlgrey.cfg")
95+
# Get the opentitantool path.
96+
opentitantool_path = r.Rlocation("lowrisc_opentitan/sw/host/opentitantool/opentitantool")
97+
# The path for GDB and the default port (set up by OpenOCD)
98+
# TODO change to something reliable
99+
GDB_PATH = "/opt/riscv/bin//riscv32-unknown-elf-gdb"
100+
GDB_PORT = 3333
101+
# Directory for the trace log files
102+
log_dir = os.environ.get("TEST_UNDECLARED_OUTPUTS_DIR")
103+
pc_trace_file = os.path.join(log_dir, "pc_trace.log")
104+
# Directory for the output results
105+
test_results_file = os.path.join(log_dir, "test_results.log")
106+
# Program the bitstream for FPGAs.
107+
bitstream_path = None
108+
if BITSTREAM:
109+
bitstream_path = r.Rlocation("lowrisc_opentitan/" + BITSTREAM)
110+
# Get the firmware path.
111+
firmware_path = r.Rlocation("lowrisc_opentitan/" + BOOTSTRAP)
112+
# Get the disassembly path.
113+
dis_path = firmware_path.replace(".img", ".dis")
114+
# And the path for the elf.
115+
elf_path = firmware_path.replace(".img", ".elf")
116+
117+
if "fpga" in BOOTSTRAP:
118+
target_type = "fpga"
119+
else:
120+
target_type = "chip"
121+
122+
target_cfg = targets.TargetConfig(
123+
target_type=target_type,
124+
interface_type="hyperdebug",
125+
fw_bin=firmware_path,
126+
opentitantool=opentitantool_path,
127+
bitstream=bitstream_path,
128+
tool_args=config_args,
129+
openocd=openocd_path,
130+
openocd_chip_config=CONFIG_FILE_CHIP,
131+
openocd_design_config=CONFIG_FILE_DESIGN,
132+
)
133+
134+
target = targets.Target(target_cfg)
135+
asymfi = OTFIAsymCrypto(target)
136+
successful_faults = 0
137+
138+
# How to read outputs in this script:
139+
# To view the UART output from the testOS or the chip in general, use: target.print_all() or print(read_testos_output())
140+
# In order to print the OpenOCD output use print(target.read_openocd())
141+
# In order to print the output from GDB use print(gdb.read_output()) or when you want to know the output from a gdb.send_command() print it: print(gdb.send_command())
142+
143+
try:
144+
# Program the bitstream, flash the target, and set up OpenOCD
145+
target.initialize_target()
146+
147+
# Initialize the testOS
148+
trigger_testos_init()
149+
150+
# Connect to GDB
151+
gdb = GDBController(gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=elf_path)
152+
153+
# Provide the function name and extract the start and end address from the dis file
154+
function_name = "OUTLINED_FUNCTION_22"
155+
# Gives back an array of hits where the function is called
156+
addresses = gdb.get_function_addresses(dis_path, function_name)
157+
print("Start and stop addresses of ", function_name, ": ", addresses, flush=True)
158+
159+
# Start the tracing
160+
gdb.send_command("-exec-interrupt")
161+
# We pick the second address hit
162+
gdb.setup_pc_trace(pc_trace_file, addresses[1][0], addresses[1][1])
163+
print("setting trace done ... ", flush=True)
164+
# Remove the output so far
165+
gdb.dump_output()
166+
gdb.send_command("-exec-continue")
167+
168+
# Trigger the p384 verify from the testOS (we do not read its output)
169+
trigger_p384_verify()
170+
171+
# We set a minute as timeout
172+
timeout = 60
173+
start_time = time.time()
174+
stopped = False
175+
176+
# Run the tracing to get the trace log
177+
while time.time() - start_time < timeout:
178+
output = gdb.read_output(timeout=0.5)
179+
if "PC trace complete" in output:
180+
print("\nTrace complete", flush=True)
181+
stopped = True
182+
break
183+
if not stopped:
184+
print("No final break point found, stopping")
185+
sys.exit(1)
186+
187+
# Parse the trace log to get all PCs in a list
188+
pc_list = gdb.parse_pc_trace_file(pc_trace_file)
189+
print("Tracing has a total of", len(pc_list), "PCs", flush=True)
190+
191+
if len(pc_list) <= 0:
192+
print("Found no tracing, stopping")
193+
sys.exit(1)
194+
195+
# Reset the connection of GDB
196+
gdb.close_gdb()
197+
gdb = GDBController(gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=elf_path)
198+
199+
# Reset the target, flush the output, and init again
200+
gdb.reset_target()
201+
target.dump_all()
202+
trigger_testos_init(print_output=False)
203+
# Flush GDB and OpenOCD
204+
openocd_response = target.read_openocd()
205+
gdb.close_gdb()
206+
207+
# Open the results file
208+
test_results = open(test_results_file, "w")
209+
test_results.write(f"Listing successful skips:\n")
210+
211+
idx = 0
212+
213+
while idx < len(pc_list) - 1:
214+
print("-" * 80)
215+
print("Applying instruction skip in ", pc_list[idx], flush=True)
216+
print("-" * 80)
217+
218+
gdb = GDBController(gdb_path=GDB_PATH, gdb_port=GDB_PORT)
219+
gdb.dump_output()
220+
221+
gdb.send_command("-exec-interrupt")
222+
# TODO Instead of giving pc_list[idx + 1], parse the dis and go to the next instruction
223+
# TODO Provide observation points such that GDB can check the state instead of relying on UART
224+
gdb.apply_instruction_skip(pc_list[idx], pc_list[idx + 1])
225+
gdb.send_command("-exec-continue")
226+
227+
# The instruction skip loop
228+
trigger_p384_verify()
229+
# TODO Rewrite this to rely only on GDB instead of on UART
230+
time.sleep(0.02)
231+
testos_response = read_testos_output()
232+
try:
233+
gdb_response = gdb.read_output()
234+
if "instruction skip applied" in gdb_response:
235+
idx += 1
236+
print("Instruction skip applied", flush=True)
237+
238+
testos_response_json = json.loads(testos_response)
239+
if testos_response_json["result"] == "True":
240+
successful_faults += 1
241+
print("-" * 80, flush=True)
242+
print("Successful FI attack!", flush=True)
243+
print("Response:", testos_response_json)
244+
print("Location:", pc_list[idx])
245+
print("-" * 80, flush=True)
246+
test_results.write(f"{pc_list[idx]}: {testos_response_json}\n")
247+
else:
248+
print(
249+
"Unsuccessful attack, output was: ",
250+
testos_response_json,
251+
flush=True,
252+
)
253+
else:
254+
print("No break point found, something went wrong", flush=True)
255+
256+
gdb.close_gdb() # 0.5s
257+
gdb = GDBController(gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=elf_path) # 0.1s
258+
gdb.reset_target() # 0.01s
259+
target.dump_all() # 1.35s
260+
trigger_testos_init(print_output=False) # 0.4s
261+
gdb.close_gdb() # 0.5s
262+
except json.JSONDecodeError as e:
263+
print(f"Error: JSON decoding failed. Invalid response format.", flush=True)
264+
gdb.close_gdb()
265+
gdb = GDBController(gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=elf_path)
266+
gdb.reset_target()
267+
target.dump_all()
268+
trigger_testos_init(print_output=False)
269+
gdb.close_gdb()
270+
except KeyError as e:
271+
print(f"Error: Required key {e} is missing in the JSON response.", flush=True)
272+
gdb.close_gdb()
273+
gdb = GDBController(gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=elf_path)
274+
gdb.reset_target()
275+
target.dump_all()
276+
trigger_testos_init(print_output=False)
277+
gdb.close_gdb()
278+
279+
finally:
280+
print("-" * 80)
281+
print("Trace data is logged in ", pc_trace_file, flush=True)
282+
print("Instruction skip results are logged in ", test_results_file, flush=True)
283+
print(f"There were a total of {successful_faults} successful faults", flush=True)
284+
print("You can find the dissassembly in ", dis_path, flush=True)
285+
# Close the OpenOCD and GDB connection at the end
286+
target.close_openocd()

sw/host/penetrationtests/python/util/BUILD

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,8 @@ py_library(
3030
name = "hyperdebug",
3131
srcs = ["hyperdebug.py"],
3232
)
33+
34+
py_library(
35+
name = "gdb_controller",
36+
srcs = ["gdb_controller.py"],
37+
)

0 commit comments

Comments
 (0)