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 this seemed like a good candidate.

Intro

I’ve decided to take another crack at the microcorruptionCTF and document my progress as some form of tutorial type thing. I also wanted to have a play around with ghidra and this seemed like a good candidate.

Microcorruption is a 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-map

MSP430 is a pretty easy going flavour of assembly language, the lock manual contains a nice introduction in chater 5 so I won’t repeat those efforts here.

Ok, time to connect the first lock to our debugger and fire up it!

1. Tutorial

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:

  1. the first byte from the memory address pointed to by r15 (@r15) is copied into r14
  2. the value of r15 is incremented (by 1)
  3. the value of r12 is incremented (by 1)
  4. r14 is tested against zero (null byte, \0, indicating the end of a string)
  5. if r14 is non-zero, loop back to 1
  6. compare the value in r12 to the literal 9
  7. if r12 does not contain 9, jump to 10
  8. set r15 to 0
  9. return to main
  10. set r15 to 1
  11. 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, although ultimately useless for this level: entering any 8 character password confirms our static analysis and the lock is open!

2. New Orleans

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

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

So 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 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. Lets 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

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.

Lets 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. Lets 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. Lets 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 the manual, interrupt 0x7f is used to open the deadbolt confirming our suspicions.

5. Hanoi

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

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 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

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. I don’t have a comments section but feel free to shoot me a tweet if you have any comments, questions, or errata!