Skip to content

Commit b9dc25c

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 b9dc25c

File tree

8 files changed

+2046
-36
lines changed

8 files changed

+2046
-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: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,14 @@ 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+
298306
CRYPTOLIB_SCA_SYM_TESTVECTOR_TARGETS = [
299307
"//sw/host/penetrationtests/testvectors/data:sca_sym_cryptolib",
300308
]

sw/host/penetrationtests/python/fi/BUILD

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