CopyCat: Controlled Instruction-Level Attacks on Enclaves • Daniel Moghimi • Jo Van Bulck • Nadia Heninger • Frank Piessens • Berk Sunar

Intel Labs – Sept. 10 2020 OS/Hypervisor Security Model

App App App OS

Hypervisor Trusted Hardware

Traditional Security Model

2 Trusted Execution Environment (TEE) – SGX

• Intel Software Guard eXtensions (SGX)

App App App App App App OS OS

Hypervisor Hypervisor Trusted Hardware Hardware

Traditional Security Model Traditional Security Model

3 Trusted Execution Environment (TEE) – Intel SGX

• Intel Software Guard eXtensions (SGX) • Enclave: Hardware protected user-level software module • Mapped by the Operating System • Loaded by the user program • Authenticated and Encrypted by CPU App App App OS Hypervisor Hardware

Traditional Security Model

4 Trusted Execution Environment (TEE) – Intel SGX

• Intel Software Guard eXtensions (SGX) • Enclave: Hardware protected user-level software module • Mapped by the Operating System • Loaded by the user program • Authenticated and Encrypted by CPU App App App • Protects against system level adversary OS blocked

blocked Hypervisor New Attacker Model: Hardware Attacker gets full control over OS Traditional Security Model

5 Intel SGX Attack Taxonomy

• Intel’s Responsibility SGX Attacks • Patches / Hardware mitigation • TCB Recovery • Old Keys are Revoked Intel’s • Remote attestation succeeds only with mitigation. Responsibility Hyperthreading is out • Foreshadow [1] • Remote Attestation Warning Plundervolt [2]

[1] Van Bulck et al. "Foreshadow: Extracting the keys to the intel SGX kingdom with transient out-of-order execution." USENIX Security 2018. [2] Murdock et al. "Plundervolt: Software-based fault injection attacks against Intel SGX." IEEE S&P 2020. 6 Intel SGX Attack Taxonomy

• Intel’s Responsibility SGX Attacks • Microcode Patches / Hardware mitigation • TCB Recovery • Old Keys are Revoked Intel’s Software Dev • Remote attestation succeeds only with mitigation. Responsibility Responsibility Hyperthreading is out • Foreshadow [1] • Remote Attestation Warning Plundervolt [2]

[1] Van Bulck et al. "Foreshadow: Extracting the keys to the intel SGX kingdom with transient out-of-order execution." USENIX Security 2018. [2] Murdock et al. "Plundervolt: Software-based fault injection attacks against Intel SGX." IEEE S&P 2020. 7 Intel SGX Attack Taxonomy

• Intel’s Responsibility SGX Attacks • Microcode Patches / Hardware mitigation • TCB Recovery • Old Keys are Revoked Intel’s Software Dev • Remote attestation succeeds only with mitigation. Responsibility Responsibility Hyperthreading is out • Foreshadow [1] • Remote Attestation Warning µarch Side Plundervolt [2] Channel • µarch Side Channel [3][4][5] • Constant-time Coding Branch Predictors • Flushing and Isolating buffers [6][7] • Probabilistic Interrupt Latency [8]

[1] Van Bulck et al. "Foreshadow: Extracting the keys to the intel SGX kingdom with transient out-of-order execution." USENIX Security 2018. [6] Evtyushkin, Dmitry, et al. "Branchscope: A new side-channel attack on directional branch predictor." ACM SIGPLAN 2018. [2] Murdock et al. "Plundervolt: Software-based fault injection attacks against Intel SGX." IEEE S&P 2020. [7] Lee, Sangho, et al. "Inferring fine-grained inside {SGX} enclaves with branch shadowing." USENIX Security 2017. [3] Moghimi et al. "Cachezoom: How SGX amplifies the power of cache attacks." CHES 2017. [8] Van Bulck et al. "Nemesis: Studying microarchitectural timing leaks in rudimentary CPU interrupt logic." ACM CCS 2018. [4] Brasser et al. "Software grand exposure:{SGX} cache attacks are practical." USENIX WOOT 2017. [5] Schwarz et al. "Malware guard extension: Using SGX to conceal cache attacks." DIMVA 2017. 8 Intel SGX Attack Taxonomy

• Intel’s Responsibility SGX Attacks • Microcode Patches / Hardware mitigation • TCB Recovery • Old Keys are Revoked Intel’s Software Dev • Remote attestation succeeds only with mitigation. Responsibility Responsibility Hyperthreading is out • Foreshadow [1] • Remote Attestation Warning µarch Side Deterministic Plundervolt [2] Channel – Ctrl Channel • µarch Side Channel Cache [3][4][5] Page Fault [9] • Constant-time Coding Branch Predictors A/D Bit [10] • Flushing and Isolating buffers [6][7] • Probabilistic Interrupt Latency [8] • Deterministic Attacks • Page Fault, A/D Bit, etc. (4kB Granularity)

[1] Van Bulck et al. "Foreshadow: Extracting the keys to the intel SGX kingdom with transient out-of-order execution." USENIX Security 2018. [6] Evtyushkin, Dmitry, et al. "Branchscope: A new side-channel attack on directional branch predictor." ACM SIGPLAN 2018. [2] Murdock et al. "Plundervolt: Software-based fault injection attacks against Intel SGX." IEEE S&P 2020. [7] Lee, Sangho, et al. "Inferring fine-grained control flow inside {SGX} enclaves with branch shadowing." USENIX Security 2017. [3] Moghimi et al. "Cachezoom: How SGX amplifies the power of cache attacks." CHES 2017. [8] Van Bulck et al. "Nemesis: Studying microarchitectural timing leaks in rudimentary CPU interrupt logic." ACM CCS 2018. [4] Brasser et al. "Software grand exposure:{SGX} cache attacks are practical." USENIX WOOT 2017. [9] Xu et al. "Controlled-channel attacks: Deterministic side channels for untrusted operating systems." IEEE S&P 2015. [5] Schwarz et al. "Malware guard extension: Using SGX to conceal cache attacks." DIMVA 2017. [10] Wang, Wenhao, et al. "Leaky cauldron on the dark land: Understanding memory side-channel hazards in SGX." ACM CCS 2017. 9 CopyCat Attack

10 CopyCat Attack

• Malicious OS controls the interrupt handler

NOP ADD XOR MUL DIV ADD MUL NOP NOP Enclave Execution Time Starts

11 CopyCat Attack

• Malicious OS controls the interrupt handler

IRQ Range

NOP ADD XOR MUL DIV ADD MUL NOP NOP

푡1 푡2 Time

12 CopyCat Attack

• Malicious OS controls the interrupt handler

IRQ Range

3

4

NOP ADD XOR MUL DIV ADD MUL NOP NOP

푡1 푡2 Time

13 CopyCat Attack

• Malicious OS controls the interrupt handler • A threshold to execute 1 or 0 instructions

IRQ Range

0

1

NOP ADD XOR MUL DIV ADD MUL NOP NOP

푡1 푡2 Time

14 CopyCat Attack

• Malicious OS controls the interrupt handler • A threshold to execute 1 or 0 instructions

IRQ Range

0

NOP ADD XOR MUL DIV ADD MUL NOP NOP

푡1 푡2 Time

15 CopyCat Attack

• Malicious OS controls the interrupt handler • A threshold to execute 1 or 0 instructions

IRQ Range

NOP ADD XOR MUL DIV ADD MUL NOP NOP

푡1 푡2 Time

16 CopyCat Attack

• Malicious OS controls the interrupt handler • A threshold to execute 1 or 0 instructions

IRQ Range

1

NOP ADD XOR MUL DIV ADD MUL NOP NOP

푡1 푡2 Time

17 CopyCat Attack

• Malicious OS controls the interrupt handler • A threshold to execute 1 or 0 instructions

IRQ Range

0

1

NOP ADD XOR MUL DIV ADD MUL NOP NOP

푡1 푡2 Time

18 CopyCat Attack

• Malicious OS controls the interrupt handler • A threshold to execute 1 or 0 instructions

IRQ Range

0

1

NOP ADD XOR MUL DIV ADD MUL NOP NOP

푡1 푡2 Time

19 CopyCat Attack

• Malicious OS controls the interrupt handler • A threshold to execute 1 or 0 instructions

IRQ Range

0

1

NOP ADD XOR MUL DIV ADD MUL NOP NOP

푡1 푡2 Time

20 CopyCat Attack

• Malicious OS controls the interrupt handler • A threshold to execute 1 or 0 instructions

I got 15 IRQs. How many zeros?

21 CopyCat Attack

• Malicious OS controls the interrupt handler • A threshold to execute 1 or 0 instructions • Filtering Zeros out: Clear the A bit before, Check the A bit after

I got 15 IRQs. How many Code Page Virtual Address zeros? 0x000401 Page PMH Walk

DTLB R U Physical Page P … A … … W S Number R U Physical Page P … A … … W S Number R U Physical Page P … A … … W S Number The A Bit is only set when an instruction is retired

22 CopyCat Attack

• Malicious OS controls the interrupt handler • A threshold to execute 1 or 0 instructions • Filtering Zeros out: Clear the A bit before, Check the A bit after • Deterministic Instruction Counting

23 CopyCat Attack

• Malicious OS controls the interrupt handler • A threshold to execute 1 or 0 instructions • Filtering Zeros out: Clear the A bit before, Check the A bit after • Deterministic Instruction Counting • Counting from start to end is not useful. • A Secondary oracle • Page table attack as a deterministic secondary oracle

Target Code Page

CALL ADD XOR MUL PUSH ADD MUL MOV NOP Time

24 CopyCat Attack

• Malicious OS controls the interrupt handler • A threshold to execute 1 or 0 instructions • Filtering Zeros out: Clear the A bit before, Check the A bit after • Deterministic Instruction Counting • Counting from start to end is not useful. • A Secondary oracle • Page table attack as a deterministic secondary oracle

Stack Target 4 Steps Code Page Page

CALL ADD XOR MUL PUSH ADD MUL MOV NOP Time

25 CopyCat Attack

• Malicious OS controls the interrupt handler • A threshold to execute 1 or 0 instructions • Filtering Zeros out: Clear the A bit before, Check the A bit after • Deterministic Instruction Counting • Counting from start to end is not useful. • A Secondary oracle • Page table attack as a deterministic secondary oracle

Stack Target 4 Steps 3 Steps Data Code Page Page Page

CALL ADD XOR MUL PUSH ADD MUL MOV NOP Time

26 CopyCat Attack

• Previous Controlled Channel attacks leak Page Access Patterns

Page A

Page D

Page B

Page C

Traditional Page-table Attacks 27 CopyCat Attack

• Previous Controlled Channel attacks leak Page Access Patterns • CopyCat additionally leaks number of instructions per page

Page A Page A

Page D 4 4 Page D

Page B 6 Page B Additional Data

Page C 8 Page C

Traditional CopyCat Page-table Attack Attacks 28 CopyCat – Leaking Branches

Stack S if(c == 0) { test %eax, %eax Code P1 r = add(r, d); je label Code P2 } mov %edx, %esi Compile else { label: Stack S r = add(r, s); call add Code P1 } mov %eax, -0xc(%rbp) Code P2 C Code

29 CopyCat – Leaking Branches

if(c == 0) { r = add(r, d); } else { r = add(r, s); } C Code

30 CopyCat – Leaking Branches

Stack S if(c == 0) { test %eax, %eax Code P1 r = add(r, d); je label Code P2 } mov %edx, %esi Compile else { label: Stack S r = add(r, s); call add Code P1 } mov %eax, -0xc(%rbp) Code P2 C Code

31 CopyCat – Leaking Branches

Stack S if(c == 0) { test %eax, %eax Code P1 r = add(r, d); je label Code P2 } mov %edx, %esi Compile else { label: Stack S r = add(r, s); call add Code P1 } mov %eax, -0xc(%rbp) Code P2 C Code

32 CopyCat – Leaking Branches

Stack S if(c == 0) { test %eax, %eax Code P1 r = add(r, d); je label Code P2 } mov %edx, %esi Compile else { label: Stack S r = add(r, s); call add Code P1 } mov %eax, -0xc(%rbp) Code P2 C Code

33 CopyCat – Leaking Branches

Stack S if(c == 0) { test %eax, %eax Code P1 r = add(r, d); je label Code P2 } mov %edx, %esi Compile else { label: Stack S r = add(r, s); call add Code P1 } mov %eax, -0xc(%rbp) Code P2 C Code

(c){ Data case 0:

r = 0xbeef; Code break; case 1: Data

r = 0xcafe; Code break; default: Data

r = 0; Code } C Code 34 35 Binary Extended Euclidean Algorithm (BEEA)

• Previous attacks only leak some of the branches w/ some noise

36 Binary Extended Euclidean Algorithm

• Previous attacks only leak some of the branches w/ some noise • CopyCat synchronously leaks all the branches wo/ any noise

37 CopyCat on WolfSSL

• Translate instruction Counts to Basic Block Transitions

38 CopyCat on WolfSSL

• Translate instruction Counts to Basic Block Transitions

39 CopyCat on WolfSSL

• Translate instruction Counts to Basic Block Transitions

40 CopyCat on WolfSSL - Cryptanalysis

−1 • Single-trace Attack during DSA signing: 푘푖푛푣 = 푘 푚표푑 푛 • Iterative over the entire recovered trace with 푛 as input → 푘푖푛푣 −1 • Plug 푘푖푛푣 in 푠1 = 푘1 ℎ − 푟1. 푥 푚표푑 푛 → get private key 푥

41 CopyCat on WolfSSL - Cryptanalysis

−1 • Single-trace Attack during DSA signing: 푘푖푛푣 = 푘 푚표푑 푛 • Iterative over the entire recovered trace with 푛 as input → 푘푖푛푣 −1 • Plug 푘푖푛푣 in 푠1 = 푘1 ℎ − 푟1. 푥 푚표푑 푛 → get private key 푥 −1 • Single-trace Attack during RSA Key Generation: 푞푖푛푣 = 푞 푚표푑 푝 • We know that p. q = N

42 CopyCat on WolfSSL - Cryptanalysis

−1 • Single-trace Attack during DSA signing: 푘푖푛푣 = 푘 푚표푑 푛 • Iterative over the entire recovered trace with 푛 as input → 푘푖푛푣 −1 • Plug 푘푖푛푣 in 푠1 = 푘1 ℎ − 푟1. 푥 푚표푑 푛 → get private key 푥 −1 • Single-trace Attack during RSA Key Generation: 푞푖푛푣 = 푞 푚표푑 푝 • We know that p. q = N • Branch and prune Algorithm with the help of the recovered trace p = . . . X q = . . . X

p = . . . 0 p = . . . 0 p = . . . 1 p = . . . 1 q = . . . 0 q = . . . 1 q = . . . 0 q = . . . 1

43 CopyCat on WolfSSL - Cryptanalysis

−1 • Single-trace Attack during DSA signing: 푘푖푛푣 = 푘 푚표푑 푛 • Iterative over the entire recovered trace with 푛 as input → 푘푖푛푣 −1 • Plug 푘푖푛푣 in 푠1 = 푘1 ℎ − 푟1. 푥 푚표푑 푛 → get private key 푥 −1 • Single-trace Attack during RSA Key Generation: 푞푖푛푣 = 푞 푚표푑 푝 • We know that p. q = N, and N is public • Branch and prune Algorithm with the help of the recovered trace p = . . . X q = . . . X

p = . . X 0 p = . . . 0 p = . . . 1 p = . . X 1 N = 1 1 1 0 q = . . X 0 q = . . . 1 q = . . . 0 q = . . X 1

44 CopyCat on WolfSSL - Cryptanalysis

−1 • Single-trace Attack during DSA signing: 푘푖푛푣 = 푘 푚표푑 푛 • Iterative over the entire recovered trace with 푛 as input → 푘푖푛푣 −1 • Plug 푘푖푛푣 in 푠1 = 푘1 ℎ − 푟1. 푥 푚표푑 푛 → get private key 푥 −1 • Single-trace Attack during RSA Key Generation: 푞푖푛푣 = 푞 푚표푑 푝 • We know that p. q = N, and N is public • Branch and prune Algorithm with the help of the recovered trace p = . . . X q = . . . X

p = . . X 0 p = . . . 0 p = . . . 1 p = . . X 1 N = 1 1 1 0 q = . . X 0 q = . . . 1 q = . . . 0 q = . . X 1

p = . . 0 0 p = . . 1 0 p = . . 0 0 p = . . 1 1 q = . . 1 0 q = . . 0 0 q = . . 1 0 q = . . 0 1

45 CopyCat on WolfSSL - Cryptanalysis

−1 • Single-trace Attack during DSA signing: 푘푖푛푣 = 푘 푚표푑 푛 • Iterative over the entire recovered trace with 푛 as input → 푘푖푛푣 −1 • Plug 푘푖푛푣 in 푠1 = 푘1 ℎ − 푟1. 푥 푚표푑 푛 → get private key 푥 −1 • Single-trace Attack during RSA Key Generation: 푞푖푛푣 = 푞 푚표푑 푝 • We know that p. q = N, and N is public • Branch and prune Algorithm with the help of the recovered trace

p = . . . X q = . . . X

p = . . X 0 p = . . X 1 N = 1 1 1 0 q = . . X 0 q = . . X 1

p = . X 0 0 p = . X 1 0 p = . X 0 0 p = . X 1 1 q = . X 1 0 q = . X 0 0 q = . X 1 0 q = . X 0 1

p = . 0 0 0 p = . 1 0 0 p = . 0 1 0 p = . 1 1 0 p = . 0 0 0 p = . 1 0 0 p = . 0 1 1 p = . 1 1 1 q = . 1 1 0 q = . 0 1 0 q = . 1 0 0 q = . 0 0 0 q = . 1 1 0 q = . 0 1 0 q = . 1 0 1 q = . 0 0 1 46 CopyCat on WolfSSL - Cryptanalysis

−1 • Single-trace Attack during DSA signing: 푘푖푛푣 = 푘 푚표푑 푛 • Iterative over the entire recovered trace with 푛 as input → 푘푖푛푣 −1 • Plug 푘푖푛푣 in 푠1 = 푘1 ℎ − 푟1. 푥 푚표푑 푛 → get private key 푥 −1 • Single-trace Attack during RSA Key Generation: 푞푖푛푣 = 푞 푚표푑 푝 • We know that p. q = N, and N is public • Branch and prune Algorithm with the help of the recovered trace

p = . . . X q = . . . X

p = . . X 0 p = . . X 1 N = 1 1 1 0 q = . . X 0 q = . . X 1

p = . X 0 0 p = . X 1 0 q = . X 1 0 q = . X 0 0

p = . 0 1 0 p = . 1 1 0 q = . 1 0 0 q = . 0 0 0 47 CopyCat on WolfSSL - Cryptanalysis

−1 • Single-trace Attack during DSA signing: 푘푖푛푣 = 푘 푚표푑 푛 • Iterative over the entire recovered trace with 푛 as input → 푘푖푛푣 −1 • Plug 푘푖푛푣 in 푠1 = 푘1 ℎ − 푟1. 푥 푚표푑 푛 → get private key 푥 −1 • Single-trace Attack during RSA Key Generation: 푞푖푛푣 = 푞 푚표푑 푝 • We know that p. q = N, and N is public • Branch and prune Algorithm with the help of the recovered trace • Single-trace Attack during RSA Key Generation: 푑 = 푒−1 푚표푑 휆 푁 푝−1 푞−1 • Similar attack but instead use 휆 푁 = 2푖 • Only 81% of the keys have the above property • It works even on a hardcoded and big value for 푒, i.e. 푒 ≠ 65537

48 CopyCat on WolfSSL – Cryptanalysis Results

• Executed each attack 100 times. • DSA 푘−1 푚표푑 푛 • Average 22,000 IRQs • 75 ms to iterate over an average of 6,320 steps • RSA 푞−1 푚표푑 푝 • Average 106490 IRQs • 365 ms to iterate over an average of 39,400 steps • RSA 푒−1 푚표푑 휆 푁 • 푒−1 푚표푑 휆 푁 • Average 230,050 IRQs • 800ms to iterate over an average of 81,090 steps • Experimental traces always match the leakage model in all experiments → Successful single-trace key recovery

49 CopyCat – Bypassing ECDSA Timing Countermeasure

50 How about other Crypto libraries?

• Libgcrypt uses a variant of BEEA • Single trace attack on DSA, Elgamal, ECDSA, RSA Key generation • OpenSSL uses BEEA for computing GCD • Single trace attack on RSA Key generation when computing gcd 푞 − 1, 푝 − 1 • There is still lots of other cases of micro leakages due to usage of branches, e.g. Intel IPP Crypto lehmer’s GCD with optimizations

51

• WolfSSL fixed the issues in 4.3.0 and 4.4.0 • Blinding for 푘−1 푚표푑 푛 and 푒−1 푚표푑 휆 푁 • Alternate formulation for 푞−1 푚표푑 푝: 푞푝−2 푚표푑 푝 • Using a constant-time (branchless) modular inverse [11] • Libgcrypt fixed the issues in 1.8.6 • Using a constant-time (branchless) modular inverse [11] • OpenSSL fixed the issue in 1.1.1e • Using a constant-time (branchless) GCD algorithm [11]

[11] Bernstein, Daniel J., and Bo-Yin Yang. "Fast constant-time gcd computation and modular inversion." CHES 2019.

52 Interrupt Driven Attacks and Single Stepping

• Amplifying Transient Execution Attacks • Foreshadow, ZombieLoad, LVI, CrossTalk • Amplifying Microarchitectural Side Channels • CacheZoom, BranchScope, Branch Shadowing, Bluethunder, etc. • Interrupt Latency as a Side Channel • Nemesis, Frontal Attack

53 Comparison to other Attacks

54 Comparison to other Attacks

• Some do not work when hyper-threadnig is disabled (Strong TCB of Intel SGX)

55 Comparison to other Attacks

• Some do not work when hyper-threadnig is disabled (Strong TCB of Intel SGX) • Some can be mitigated by flushing/isolating microarchitectural buffers.

56 Comparison to other Attacks

• Some do not work when hyper-threadnig is disabled (Strong TCB of Intel SGX) • Some can be mitigated by flushing/isolating microarchitectural buffers. • Some only apply to legacy enclave (32-bit)

57 Comparison to other Attacks

• Some do not work when hyper-threadnig is disabled (Strong TCB of Intel SGX) • Some can be mitigated by flushing/isolating microarchitectural buffers. • Some only apply to legacy enclave (32-bit) • Some are limited to be applied synchronously.

58 CopyCat and Macro-fusion

• Fused instructions are counted as one. • Confirm/RE of the behavior of macro-fusion on Intel CPUs • Macro-fusion is dependent on the program layout → deterministic • The offset of a cmp+branch within a cache line • True when hyperthreading is disabled (Intel SGX TCB)

https://en.wikichip.org/wiki/macro-operation_fusion 59 Conclusion

• Instruction Level Granularity SGX Attacks • Imbalance number of instructions

• Leak the outcome of branches Intel’s Software Dev Responsibility Responsibility

µarch Side Deterministic Channel – Ctrl Channel

This work

60 Conclusion

• Instruction Level Granularity SGX Attacks • Imbalance number of instructions

• Leak the outcome of branches Intel’s Software Dev Responsibility Responsibility • Fully Deterministic and reliable µarch Side Deterministic • Millions of instructions tested Channel – Ctrl Channel • Attacks match the exact leakage model

This work

61 Conclusion

• Instruction Level Granularity SGX Attacks • Imbalance number of instructions

• Leak the outcome of branches Intel’s Software Dev Responsibility Responsibility • Fully Deterministic and reliable µarch Side Deterministic • Millions of instructions tested Channel – Ctrl Channel • Attacks match the exact leakage model of branches • Easy to scale and replicate • No reverse engineering of branches and This work microarchitectural components • Tracking all the branches synchronously

62 Conclusion

• Instruction Level Granularity SGX Attacks • Imbalance number of instructions

• Leak the outcome of branches Intel’s Software Dev Responsibility Responsibility • Fully Deterministic and reliable µarch Side Deterministic • Millions of instructions tested Channel – Ctrl Channel • Attacks match the exact leakage model of branches • Easy to scale and replicate • No reverse engineering of branches and This work microarchitectural components • Tracking all the branches synchronously • Branchless programming is hard!

63 Future Directions – Other TEE Models

• Virtual Machine TEE • AMD SEV • Intel TDX

• What are other ways to interrupt a TEE in the above models?

• What is the impact? • Guest OSS • Cryptographic Services • Other Applications • …

64 Future Directions – Non-cryptographic Application of Enclaves

• Data-dependent secret-processing applications • Confidential Deep Learning ( • Trusted Database (EnclaveDB)

• Automated Leakage Analysis and Exploit Generation • Fuzzing and Taint Analysis • Dynamic Analysis

65 Future Directions – Mitigation

-based Solutions • Balancing secret-dependent branches with dummy instructions

• System-level Mitigation • Self-paging Enclave (Autarky)

66 Questions?!

https://github.com/j ovanbulck/sgx-step

67