Previously in this series of articles, we modified our case study through the process of binary patching after analyzing it with the toolchain. In this article we will do the same but with Ghidra, a dedicated reverse-engineering framework for software.

Preparing our artifact

We will work with a version of our case study stripped of all of its debugging symbols, in order to use Ghidra while still having to do some of the work ourselves. Therefore, let’s prepare a suitable version of the executable:

$ mips-linux-gnu-strip --strip-all ascii-table.elf -o ascii-table.stripped.elf

Using the toolchain, we can introspect the resulting file:

$ mips-linux-gnu-objdump --wide --file-headers ascii-table.stripped.elf

ascii-table.stripped.elf:     file format elf32-tradlittlemips
architecture: mips:isa32r2, flags 0x00000102:
EXEC_P, D_PAGED
start address 0x00400568


$ mips-linux-gnu-objdump --wide --section-headers ascii-table.stripped.elf 

ascii-table.stripped.elf:     file format elf32-tradlittlemips

Sections:
Idx Name               Size      VMA       LMA       File off  Algn  Flags
  0 .MIPS.abiflags     00000018  004000d8  004000d8  000000d8  2**3  CONTENTS, ALLOC, LOAD, READONLY, DATA, LINK_ONCE_SAME_SIZE
  1 .reginfo           00000018  004000f0  004000f0  000000f0  2**2  CONTENTS, ALLOC, LOAD, READONLY, DATA, LINK_ONCE_SAME_SIZE
  2 .note.gnu.build-id 00000024  00400108  00400108  00000108  2**2  CONTENTS, ALLOC, LOAD, READONLY, DATA
  3 .text              00000450  00400130  00400130  00000130  2**4  CONTENTS, ALLOC, LOAD, READONLY, CODE
  4 .rodata            00000170  00400580  00400580  00000580  2**4  CONTENTS, ALLOC, LOAD, READONLY, DATA
  5 .comment           00000027  00000000  00000000  000006f0  2**0  CONTENTS, READONLY
  6 .pdr               00000220  00000000  00000000  00000718  2**2  CONTENTS, READONLY
  7 .gnu.attributes    00000010  00000000  00000000  00000938  2**0  CONTENTS, READONLY
  8 .mdebug.abi32      00000000  00000000  00000000  00000948  2**0  CONTENTS, READONLY

$ mips-linux-gnu-nm --format sysv --line-numbers --no-sort ascii-table.stripped.elf


Symbols from ascii-table.stripped.elf:

Name                  Value   Class        Type         Size     Line  Section

mips-linux-gnu-nm: ascii-table.stripped.elf: no symbols

$ qemu-mipsel ascii-table.stripped.elf
0000     c              0020    p s             0040 @ gp  !            0060 ` gp  !     
0001     c              0021 ! gp  !            0041 A gp   Aa U        0061 a gp   Aa  l
0002     c              0022 " gp  !            0042 B gp   Aa U        0062 b gp   Aa  l
0003     c              0023 # gp  !            0043 C gp   Aa U        0063 c gp   Aa  l
0004     c              0024 $ gp  !            0044 D gp   Aa U        0064 d gp   Aa  l
0005     c              0025 % gp  !            0045 E gp   Aa U        0065 e gp   Aa  l
0006     c              0026 & gp  !            0046 F gp   Aa U        0066 f gp   Aa  l
0007     c              0027 ' gp  !            0047 G gp   Aa U        0067 g gp   Aa  l
0008     c              0028 ( gp  !            0048 H gp   Aa U        0068 h gp   Aa  l
0009     cs             0029 ) gp  !            0049 I gp   Aa U        0069 i gp   Aa  l
000a     cs             002a * gp  !            004a J gp   Aa U        006a j gp   Aa  l
000b     cs             002b + gp  !            004b K gp   Aa U        006b k gp   Aa  l
000c     cs             002c , gp  !            004c L gp   Aa U        006c l gp   Aa  l
000d     cs             002d - gp  !            004d M gp   Aa U        006d m gp   Aa  l
000e     c              002e . gp  !            004e N gp   Aa U        006e n gp   Aa  l
000f     c              002f / gp  !            004f O gp   Aa U        006f o gp   Aa  l
0010     c              0030 0 gp   A d         0050 P gp   Aa U        0070 p gp   Aa  l
0011     c              0031 1 gp   A d         0051 Q gp   Aa U        0071 q gp   Aa  l
0012     c              0032 2 gp   A d         0052 R gp   Aa U        0072 r gp   Aa  l
0013     c              0033 3 gp   A d         0053 S gp   Aa U        0073 s gp   Aa  l
0014     c              0034 4 gp   A d         0054 T gp   Aa U        0074 t gp   Aa  l
0015     c              0035 5 gp   A d         0055 U gp   Aa U        0075 u gp   Aa  l
0016     c              0036 6 gp   A d         0056 V gp   Aa U        0076 v gp   Aa  l
0017     c              0037 7 gp   A d         0057 W gp   Aa U        0077 w gp   Aa  l
0018     c              0038 8 gp   A d         0058 X gp   Aa U        0078 x gp   Aa  l
0019     c              0039 9 gp   A d         0059 Y gp   Aa U        0079 y gp   Aa  l
001a     c              003a : gp  !            005a Z gp   Aa U        007a z gp   Aa  l
001b     c              003b ; gp  !            005b [ gp  !            007b { gp  !     
001c     c              003c < gp  !            005c \ gp  !            007c | gp  !     
001d     c              003d = gp  !            005d ] gp  !            007d } gp  !     
001e     c              003e > gp  !            005e ^ gp  !            007e ~ gp  !     
001f     c              003f ? gp  !            005f _ gp  !            007f     c       

Comparing with the results seen in part 3, all the debugging sections are now gone and no symbols are found. Nevertheless, the executable can still be run with no observable change in behavior.

Getting Ghidra

We can download a pre-built Ghidra package from the releases page located at GitHub. We will be using release 10.2.3.

First, we need to install Ghidra’s dependencies in our reverse-engineering environment:

$ sudo apt-get install \
    openjdk-17-jdk

Then, let’s download and run Ghidra:

$ wget --quiet https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_10.2.3_build/ghidra_10.2.3_PUBLIC_20230208.zip
$ unzip -q ghidra_10.2.3_PUBLIC_20230208.zip
$ cd ghidra_10.2.3_PUBLIC/
$ ./ghidraRun

We are first being presented with an EULA dialog:

After accepting the license, we can start using Ghidra.

Setting up a new project

When starting Ghidra, the first window shown is the projects window:

We’ll create a new project with File > New Project (Ctrl-N). We are then presented with a multi-step wizard that we’ll fill out as follows:

  • Select Non-shared project ;
  • Set project name to Case study.

Ghidra will automatically open the newly-created project:

We have a project, but it is currently empty. To study an artifact, we need to import it with File > Import File (I). Find and select the ascii-table.stripped.elf file in the dialog box, which will then prompt an import dialog box:

Ghidra auto-detected the file format (ELF) and language (MIPS 32-bit little-endian) of our executable. Since the default parameters are suitable, press OK to continue.

Ghidra will show a report once the importation is complete:

Press OK to dismiss it.

We now have a file in our project:

Double-click it to open it with the CodeBrowser tool.

First steps with Ghidra

Ghidra will automatically prompt to run its analyzers on unanalyzed programs. Since we’re going to run the analyzers manually later on, click No.

We have access to the main reverse-engineering tool of Ghidra for a program. The default view is split into three columns:

  • The first column has various tree widgets to navigate around memory blocks, symbols and data types ;
  • The second column is the listing, which is a low-level view of the program’s address space ;
  • The third column shows a decompilation of the current function.

We have loaded our program so the memory blocks on the top left are populated, but we have not run any analyzers so the database is uninitialized, save for the entry point FUN_00400568 directly taken from the ELF header. Before running the analyzers, we’ll do some operations by hand to show off some basics commands of Ghidra.

_start

Right now the only symbol defined in the database is the entrypoint FUN_00400568. Let’s analyze it by hand:

  • Go to the function, either by clicking on it in the symbol tree, hitting the G key (or selecting Navigation > Go To...) and entering its name or address. Select the function in the listing view, hit the L key to invoke the symbol rename dialog and set the function’s name to _start.
  • Then, hit the D key on the function in the listing view (or right-click it and select Disassemble) to disassemble it.
  • Finally, since our function does not have an epilogue (it never returns) the automatic disassembly goes beyond the end of the function. Select the instruction at 00400580 and hit the C key (or right-click it and select Clear Code Bytes) to clear the extraneous disassembly.

The disassembly automatically follows direct functions calls, so now we have six additional functions inside the symbol tree. Now that we saw how to do some basic operations by hand, we’ll let Ghidra auto-analyze the rest of the executable for us by hitting the A key (or clicking on Analysis > Auto Analyze ascii-table.stripped.elf...).

The analyzers in Ghidra takes care of most of the basic grunt work of reverse-engineering, which we help out by annotating the executable correctly as we figure things out. We’ll let it run with the default settings by clicking on the Analyze button.

Entrypoint functions generally have some initialization code, then call into main, then have some finalization code. Here, _start is extremely simple: it first calls FUN_00400130, then call FUN_00400560 with its return value and there is no function epilogue at the end.

From this, we can infer several things:

  • The entry point itself is a function that never returns ;
  • FUN_00400130 is probably the main function ;
  • FUN_00400560 does not return and is probably the _exit function.

To confirm what FUN_00400560 is, we can take a peek at its disassembly in the listing view:

                             **************************************************************
                             *                          FUNCTION                          *
                             **************************************************************
                             undefined FUN_00400560()
             undefined         v0:1           <RETURN>
                             FUN_00400560                                    XREF[1]:     _start:00400578(c)  
        00400560 a1 0f 02 24     li         v0,0xfa1
        00400564 0c 00 00 00     syscall

This function invokes the syscall 0xfa1 (4001 in decimal) and does not have an epilogue either. Looking at the Linux syscall numbers for the MIPS architecture and the syscalls(2) manpage, we observe that this corresponds to the _exit(int status) function. We can therefore annotate the function signature accordingly by hitting the F key (or right-clicking it and selecting Edit Function...):

We’ll assume FUN_00400130 is the main function, therefore we’ll rename it accordingly and edit its function signature to return an int. The decompile view of _start should then display the following:


void _start(void)

{
  int status;
  
  status = main();
                    /* WARNING: Subroutine does not return */
  _exit(status);
}

We are done with the entry point and _exit. Let’s continue with the remaining lead we have, main.

main()

The decompile view of the main function currently shows the following:


int main(void)

{
  uint uVar1;
  uint uVar2;
  undefined local_20 [8];
  
  uVar1 = 0;
  uVar2 = 0;
  do {
    FUN_0040025c((int)(char)((char)uVar2 * ' ' + (char)((int)uVar1 >> 2)),&PTR_FUN_00400584,10);
    local_20[0] = 10;
    if (uVar2 != 3) {
      local_20[0] = 9;
    }
    uVar1 = uVar1 + 1;
    FUN_00400550(1,local_20,1);
    uVar2 = uVar1 & 3;
  } while (uVar1 != 0x80);
  return 0;
}

The output looks a bit gibberish, this is because we are lacking proper typing and variable names. Studying FUN_00400550 reveals it’s another syscall wrapper function like we saw with _exit, but for write instead. Let’s edit its signature accordingly:

Since the first argument to write is the file descriptor, the second argument is a pointer to a buffer and the third argument is the buffer’s size, we can infer the following from this call:

  • The file descriptor is the standard output ;
  • The buffer is the variable local_20 from the stack ;
  • It is one byte long.

Since the program outputs an ASCII table (and therefore text instead of binary data), we can assume that the byte is in fact a character. Coincidentally, the buffer’s single byte is either 9 or 10, which corresponds to the horizontal tab and newline respectively. Therefore, let’s do the following operations:

  • We’ll rename the variable local_20 to buf by clicking on it and hitting the L key (or right-clicking it and selecting Rename Variable) ;
  • We’ll retype it from undefined[8] to charby clicking on it and hitting Ctrl-L (or right-clicking it and selecting Retype Variable).

The decompile view should now display the following:


int main(void)

{
  uint uVar1;
  uint uVar2;
  char buffer;
  
  uVar1 = 0;
  uVar2 = 0;
  do {
    FUN_0040025c((int)(char)((char)uVar2 * ' ' + (char)((int)uVar1 >> 2)),&PTR_FUN_00400584,10);
    buffer = '\n';
    if (uVar2 != 3) {
      buffer = '\t';
    }
    uVar1 = uVar1 + 1;
    write(1,&buffer,1);
    uVar2 = uVar1 & 3;
  } while (uVar1 != 0x80);
  return 0;
}

Now, we’ll take a look at uVar1 and uVar2:

  • uVar1 is a counter that gets incremented from 0 to 128 (0x80 in hexadecimal) ;
  • uVar2 has the value of uVar1 modulo 4 and controls whether the buffer has a tabulation or a newline ;
  • FUN_0040025c’s first parameter is effectively (uVar2 * 32) + (uVar1 % 4).

Since the ASCII table generated by this program has 128 entries formatted into four columns, we can infer that:

  • uVar1 is the ASCII character counter whose type is int ;
  • uVar2 is a column counter whose type is int ;
  • FUN_0040025c’s first parameter is probably an ASCII character number ;
  • FUN_0040025c is probably a function that outputs an ASCII table entry.

Annotating this yields the following decompile view:


int main(void)

{
  int counter;
  int column;
  char buffer;
  
  counter = 0;
  column = 0;
  do {
    print_ascii_entry((char)column * 32 + (char)(counter >> 2),(undefined *)&PTR_FUN_00400584,10);
    buffer = '\n';
    if (column != 3) {
      buffer = '\t';
    }
    counter = counter + 1;
    write(1,&buffer,1);
    column = counter & 3;
  } while (counter != 0x80);
  return 0;
}

The second and third parameters of print_ascii_entry are still a mystery. We have inferred everything we can for now in this function, let’s continue by studying print_ascii_entry.

print_ascii_entry()

The decompile view of the print_ascii_entry function currently shows the following:


void print_ascii_entry(char character,undefined *param_2,int param_3)

{
  bool bVar1;
  int iVar2;
  int iVar3;
  undefined3 in_register_00000011;
  int num;
  char local_20 [8];
  
  num = CONCAT31(in_register_00000011,character);
  FUN_004001d0(num);
  local_20[0] = ' ';
  write(1,local_20,1);
  iVar2 = FUN_00400410(num);
  local_20[0] = character;
  if (iVar2 == 0) {
    local_20[0] = ' ';
  }
  iVar2 = 0;
  write(1,local_20,1);
  local_20[0] = ' ';
  write(1,local_20,1);
  bVar1 = 0 < param_3;
  while (bVar1) {
    iVar3 = (**(code **)param_2)(num);
    if (iVar3 == 0) {
      local_20[0] = ' ';
    }
    else {
      local_20[0] = *(char *)((int)param_2 + 4);
    }
    iVar2 = iVar2 + 1;
    param_2 = (undefined *)((int)param_2 + 8);
    write(1,local_20,1);
    bVar1 = iVar2 < param_3;
  }
  return;
}

Here, we will focus on the while loop. Studying it yields the following observations:

  • The loop counts from 0 to the value of param_3 ;
  • param_2 = (undefined *)((int)param_2 + 8); shows that param_2 is incremented by 8 bytes per iteration ;
  • iVar3 = (**(code **)param_2)(num); shows that *(param_1) is a pointer to a function of type int (*)(int) ;
  • local_20[0] = *(char *)((int)param_2 + 4); shows that *(param_1) is a character.

Therefore, param_2 is a pointer to an array of a structure made of two elements ; this array also has a length of param_3. Click on param_2 and hit Shift+[ (or right-click it and select Auto Create Structure). Ghidra will automatically create a new structure data type named astruct and retype the variable accordingly.

Double-click on it in the Data Type panel (or right-click on param_2 and select Edit Data Type) to bring up the structure editor:

Here, Ghidra automatically created fields based on its best guess. We know that the first member of the structure is a pointer to a function of the form int (*)(int), but Ghidra guessed undefined *, which is just a pointer to some unknown type. Right-click on the ascii-table.stripped.elf data library in the Data Type panel and select New > Function Definition.... Here, fill in the dialog box as follows:

The new data type is named func and has been placed inside the ascii-table.stripped.elf data library. We can now fill in the missing information from the astruct data type (don’t forget to click on the floppy disk icon to commit the changes):

Recall how print_ascii_entry gets called by the main function:

print_ascii_entry((char)column * 32 + (char)(counter >> 2),(undefined *)&PTR_FUN_00400584,10);

We can therefore conclude that PTR_FUN_00400584 is the location of an array of ten ascii_property elements. Currently, Ghidra doesn’t know about this:

Click on PTR_FUN_00400584 and hit T (or right-click it and select Data > Choose Data Type...). In the dialog box, enter ascii_property[10] and validate it. Then, hit L (or right-click it and select Edit Label...) and name the variable s_ascii_properties. The result should look like this:

With this array property typed, the decompilation for main now looks like this:

int main(void)

{
  int counter;
  int column;
  char buffer;
  
  counter = 0;
  column = 0;
  do {
    print_ascii_entry((char)column * 32 + (char)(counter >> 2),s_ascii_properties,10);
    buffer = '\n';
    if (column != 3) {
      buffer = '\t';
    }
    counter = counter + 1;
    write(1,&buffer,1);
    column = counter & 3;
  } while (counter != 0x80);
  return 0;
}

We have covered the basics of reverse-engineering a program, without any debugging symbols, from scratch with Ghidra. Rather than continue the analysis of this program, we’ll skip ahead and get straight to binary patching.

Binary patching redux

We will recreate the binary patch done previously in part 5, but inside Ghidra this time. Currently, FUN_004001d0 has the following decompilation:


void FUN_004001d0(int param_1)

{
  int iVar1;
  uint uVar2;
  char local_20 [12];
  
  uVar2 = 0xc;
  iVar1 = param_1 >> 0xc;
  do {
    local_20[0] = (char)(iVar1 % 0x10);
    if (iVar1 % 0x10 < 10) {
      local_20[0] = local_20[0] + '0';
    }
    else {
      local_20[0] = local_20[0] + 'W';
    }
    uVar2 = uVar2 - 4;
    write(1,local_20,1);
    iVar1 = param_1 >> (uVar2 & 0x1f);
  } while (uVar2 != 0xfffffffc);
  return;
}

After making some annotations, we have this decompilation of the target of our binary patching, the print_number function:


void print_number(int num)

{
  int nextDigit;
  int n;
  char digit [12];
  
  n = 12;
  nextDigit = num >> 12;
  do {
    digit[0] = (char)(nextDigit % 16);
    if (nextDigit % 16 < 10) {
      digit[0] = digit[0] + '0';
    }
    else {
      digit[0] = digit[0] + 'W';
    }
    n = n - 4;
    write(1,digit,1);
    nextDigit = num >> (n & 0x1fU);
  } while (n != 0xfffffffc);
  return;
}

We can observe which instructions corresponds to which part of the decompilation by clicking parts of the decompiled code and cross-referencing what gets highlighted inside the listing view.

As a reminder, these were the patches done in part 5:

Address Original bytes Original instruction Patched bytes Patched instruction
4001d8 2413000c li s3,12 24130009 li s3,9
4001e0 2412fffc li s2,-4 2412fffd li s2,-3
4001e8 24110010 li s1,16 24110008 li s1,8
40022c 2673fffc addiu s3,s3,-4 2673fffd addiu s3,s3,-3

To patch an instruction, click on it and hit Ctrl-Shift-G (or right-click it and select Patch Instruction):

Performing all patches should yield the following decompilation:


void print_number(int num)

{
  int nextDigit;
  int n;
  char digit [12];
  
  n = 9;
  nextDigit = num >> 9;
  do {
    digit[0] = (char)(nextDigit % 8);
    if (nextDigit % 8 < 10) {
      digit[0] = digit[0] + '0';
    }
    else {
      digit[0] = digit[0] + 'W';
    }
    n = n - 3;
    write(1,digit,1);
    nextDigit = num >> (n & 0x1fU);
  } while (n != 0xfffffffd);
  return;
}

We can then export the program by hitting the O key (or clicking File > Export Program...):

Select the ELF format in the dialog box, choose a file name and click OK:

The executable has been exported with our patches. We can verify this by running it:

$ qemu-mipsel ascii-table.stripped.elf
0000     c              0020    p s             0040 @ gp  !            0060 ` gp  !     
0001     c              0021 ! gp  !            0041 A gp   Aa U        0061 a gp   Aa  l
0002     c              0022 " gp  !            0042 B gp   Aa U        0062 b gp   Aa  l
0003     c              0023 # gp  !            0043 C gp   Aa U        0063 c gp   Aa  l
0004     c              0024 $ gp  !            0044 D gp   Aa U        0064 d gp   Aa  l
0005     c              0025 % gp  !            0045 E gp   Aa U        0065 e gp   Aa  l
0006     c              0026 & gp  !            0046 F gp   Aa U        0066 f gp   Aa  l
0007     c              0027 ' gp  !            0047 G gp   Aa U        0067 g gp   Aa  l
0008     c              0028 ( gp  !            0048 H gp   Aa U        0068 h gp   Aa  l
0009     cs             0029 ) gp  !            0049 I gp   Aa U        0069 i gp   Aa  l
000a     cs             002a * gp  !            004a J gp   Aa U        006a j gp   Aa  l
000b     cs             002b + gp  !            004b K gp   Aa U        006b k gp   Aa  l
000c     cs             002c , gp  !            004c L gp   Aa U        006c l gp   Aa  l
000d     cs             002d - gp  !            004d M gp   Aa U        006d m gp   Aa  l
000e     c              002e . gp  !            004e N gp   Aa U        006e n gp   Aa  l
000f     c              002f / gp  !            004f O gp   Aa U        006f o gp   Aa  l
0010     c              0030 0 gp   A d         0050 P gp   Aa U        0070 p gp   Aa  l
0011     c              0031 1 gp   A d         0051 Q gp   Aa U        0071 q gp   Aa  l
0012     c              0032 2 gp   A d         0052 R gp   Aa U        0072 r gp   Aa  l
0013     c              0033 3 gp   A d         0053 S gp   Aa U        0073 s gp   Aa  l
0014     c              0034 4 gp   A d         0054 T gp   Aa U        0074 t gp   Aa  l
0015     c              0035 5 gp   A d         0055 U gp   Aa U        0075 u gp   Aa  l
0016     c              0036 6 gp   A d         0056 V gp   Aa U        0076 v gp   Aa  l
0017     c              0037 7 gp   A d         0057 W gp   Aa U        0077 w gp   Aa  l
0018     c              0038 8 gp   A d         0058 X gp   Aa U        0078 x gp   Aa  l
0019     c              0039 9 gp   A d         0059 Y gp   Aa U        0079 y gp   Aa  l
001a     c              003a : gp  !            005a Z gp   Aa U        007a z gp   Aa  l
001b     c              003b ; gp  !            005b [ gp  !            007b { gp  !     
001c     c              003c < gp  !            005c \ gp  !            007c | gp  !     
001d     c              003d = gp  !            005d ] gp  !            007d } gp  !     
001e     c              003e > gp  !            005e ^ gp  !            007e ~ gp  !     
001f     c              003f ? gp  !            005f _ gp  !            007f     c       

$ chmod +x ascii-table.stripped.patched.elf 
$ qemu-mipsel ascii-table.stripped.patched.elf 
0000     c              0040    p s             0100 @ gp  !            0140 ` gp  !     
0001     c              0041 ! gp  !            0101 A gp   Aa U        0141 a gp   Aa  l
0002     c              0042 " gp  !            0102 B gp   Aa U        0142 b gp   Aa  l
0003     c              0043 # gp  !            0103 C gp   Aa U        0143 c gp   Aa  l
0004     c              0044 $ gp  !            0104 D gp   Aa U        0144 d gp   Aa  l
0005     c              0045 % gp  !            0105 E gp   Aa U        0145 e gp   Aa  l
0006     c              0046 & gp  !            0106 F gp   Aa U        0146 f gp   Aa  l
0007     c              0047 ' gp  !            0107 G gp   Aa U        0147 g gp   Aa  l
0010     c              0050 ( gp  !            0110 H gp   Aa U        0150 h gp   Aa  l
0011     cs             0051 ) gp  !            0111 I gp   Aa U        0151 i gp   Aa  l
0012     cs             0052 * gp  !            0112 J gp   Aa U        0152 j gp   Aa  l
0013     cs             0053 + gp  !            0113 K gp   Aa U        0153 k gp   Aa  l
0014     cs             0054 , gp  !            0114 L gp   Aa U        0154 l gp   Aa  l
0015     cs             0055 - gp  !            0115 M gp   Aa U        0155 m gp   Aa  l
0016     c              0056 . gp  !            0116 N gp   Aa U        0156 n gp   Aa  l
0017     c              0057 / gp  !            0117 O gp   Aa U        0157 o gp   Aa  l
0020     c              0060 0 gp   A d         0120 P gp   Aa U        0160 p gp   Aa  l
0021     c              0061 1 gp   A d         0121 Q gp   Aa U        0161 q gp   Aa  l
0022     c              0062 2 gp   A d         0122 R gp   Aa U        0162 r gp   Aa  l
0023     c              0063 3 gp   A d         0123 S gp   Aa U        0163 s gp   Aa  l
0024     c              0064 4 gp   A d         0124 T gp   Aa U        0164 t gp   Aa  l
0025     c              0065 5 gp   A d         0125 U gp   Aa U        0165 u gp   Aa  l
0026     c              0066 6 gp   A d         0126 V gp   Aa U        0166 v gp   Aa  l
0027     c              0067 7 gp   A d         0127 W gp   Aa U        0167 w gp   Aa  l
0030     c              0070 8 gp   A d         0130 X gp   Aa U        0170 x gp   Aa  l
0031     c              0071 9 gp   A d         0131 Y gp   Aa U        0171 y gp   Aa  l
0032     c              0072 : gp  !            0132 Z gp   Aa U        0172 z gp   Aa  l
0033     c              0073 ; gp  !            0133 [ gp  !            0173 { gp  !     
0034     c              0074 < gp  !            0134 \ gp  !            0174 | gp  !     
0035     c              0075 = gp  !            0135 ] gp  !            0175 } gp  !     
0036     c              0076 > gp  !            0136 ^ gp  !            0176 ~ gp  !     
0037     c              0077 ? gp  !            0137 _ gp  !            0177     c       

Just like when we patched the executable with GDB, we can observe that the hexadecimal column is now in octal.

The files for this case study can be found here: case-study.tar.gz

Conclusion

We have learned how to use Ghidra to reverse-engineer a program and recreate the binary patch from part 5 for our case study. Next time, we will study an esoteric but extremely powerful reverse-engineering technique where we will slice an executable file back into relocatable object files.