Reverse-engineering part 6: crash course on binary patching with Ghidra
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 selectingNavigation > Go To...
) and entering its name or address. Select the function in the listing view, hit theL
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 selectDisassemble
) 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 theC
key (or right-click it and selectClear 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 themain
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
tobuf
by clicking on it and hitting theL
key (or right-clicking it and selectingRename Variable
) ; - We’ll retype it from
undefined[8]
tochar
by clicking on it and hittingCtrl-L
(or right-clicking it and selectingRetype 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 ofuVar1
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 isint
;uVar2
is a column counter whose type isint
;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 thatparam_2
is incremented by 8 bytes per iteration ;iVar3 = (**(code **)param_2)(num);
shows that*(param_1)
is a pointer to a function of typeint (*)(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.