Reverse-engineering part 5: old-school binary patching
Previously in this series of articles, we ran and stepped through the number printing function in our case study. In this article, we will modify our case study program so that it outputs the ASCII table with octal numbers instead of hexadecimal.
What we’ve learned so far
As a reminder, we have written and compiled the source code of the print_number()
in part 2 into an executable, then disassembled this function from the executable in part 3:
void print_number(int num) {
for (int n = 3; n >= 0; n--) {
int digit = (num >> (4 * n)) % 16;
if (digit < 10)
putchar('0' + digit);
else
putchar('a' + digit - 10);
}
}
004001d0 <print_number>:
4001d0: 27bdffd0 addiu sp,sp,-48
4001d4: afb30028 sw s3,40(sp)
4001d8: 2413000c li s3,12
4001dc: afb20024 sw s2,36(sp)
4001e0: 2412fffc li s2,-4
4001e4: afb10020 sw s1,32(sp)
4001e8: 24110010 li s1,16
4001ec: afb0001c sw s0,28(sp)
4001f0: 00808025 move s0,a0
4001f4: afbf002c sw ra,44(sp)
4001f8: 02701007 srav v0,s0,s3
4001fc: 0051001a div zero,v0,s1
400200: 00001010 mfhi v0
400204: 304300ff andi v1,v0,0xff
400208: 2842000a slti v0,v0,10
40020c: 10400011 beqz v0,400254 <print_number+0x84>
400210: 00000000 nop
400214: 24630030 addiu v1,v1,48
400218: 24060001 li a2,1
40021c: a3a30010 sb v1,16(sp)
400220: 27a50010 addiu a1,sp,16
400224: 24040001 li a0,1
400228: 0c100154 jal 400550 <write>
40022c: 2673fffc addiu s3,s3,-4
400230: 1672fff2 bne s3,s2,4001fc <print_number+0x2c>
400234: 02701007 srav v0,s0,s3
400238: 8fbf002c lw ra,44(sp)
40023c: 8fb30028 lw s3,40(sp)
400240: 8fb20024 lw s2,36(sp)
400244: 8fb10020 lw s1,32(sp)
400248: 8fb0001c lw s0,28(sp)
40024c: 03e00008 jr ra
400250: 27bd0030 addiu sp,sp,48
400254: 1000fff0 b 400218 <print_number+0x48>
400258: 24630057 addiu v1,v1,87
Finally, we’ve determined in part 4 through run-time examination the purposes of the following CPU registers used in that function:
Register | Purpose | Constant? |
---|---|---|
v0 |
Set to one if the current digit is less than 10 | No |
v1 |
ASCII character of the current digit | No |
s0 |
Number to print (num ) |
Yes |
s1 |
Value 16 (radix or base of hexadecimal) |
Yes |
s2 |
Value -4 (added to counter after an iteration) |
Yes |
s3 |
Iteration counter | No |
Modifying the executable
As stated in the introduction, we want to modify the print_number()
function so that it prints numbers in octal instead of hexadecimal.
We can achieve this by modifying the following four instructions in this function:
4001d8: 2413000c li s3,12
4001e0: 2412fffc li s2,-4
4001e8: 24110010 li s1,16
40022c: 2673fffc addiu s3,s3,-4
The encoding of these MIPS I-format instructions have their constants in the bottom two bytes, as a signed (two’s complement) 16-bit integer. Adjusting the constants to output octal numbers results in these four alternative instructions:
4001d8: 24130009 li s3,9
4001e0: 2412fffd li s2,-3
4001e8: 24110008 li s1,8
40022c: 2673fffd addiu s3,s3,-3
Note that this is a small modification of the function that merely changes some constants inside the existing program flow. More invasive modifications are possible by changing the actual instructions within the function, or even by redirecting the execution somewhere else if the patch doesn’t fit within the original space.
Patching the executable
We will be using GDB to patch our executable. First, let’s make a copy of it and invoke GDB in a manner that will let us patch the file:
$ cp ascii-table.elf ascii-table.patched.elf
$ gdb-multiarch
GNU gdb (Debian 10.1-1.7) 10.1.90.20210103-git
Copyright (C) 2021 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word".
(gdb) set architecture mips:isa32r2
The target architecture is set to "mips:isa32r2".
(gdb) set write on
(gdb) exec-file ascii-table.patched.elf
(gdb)
We will patch the first instruction, checking its disassembly before and after the modification:
(gdb) x/i 0x4001d8
0x4001d8: li s3,12
(gdb) set {unsigned int}0x4001d8 = 0x24130009
(gdb) x/i 0x4001d8
0x4001d8: li s3,9
(gdb)
Let’s fast forward the rest of the patch:
(gdb) set {unsigned int}0x4001e0 = 0x2412fffd
(gdb) set {unsigned int}0x4001e8 = 0x24110008
(gdb) set {unsigned int}0x40022c = 0x2673fffd
(gdb) quit
The executable file has been binary patched. We could’ve achieved the same result with a hex editor, if we computed by hand the file offset within the ELF executable to modify based on the patch’s virtual address like we did in part 3. GDB performed this operation automatically when we instructed it to modify memory within the executable’s virtual address space.
Results
After modifying the executable artifact, executing it yields the following output:
$ qemu-mipsel ascii-table.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.patched.elf
$ qemu-mipsel ascii-table.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
We can observe that the number columns are now in octal, instead of hexadecimal as previously.
The files for this case study can be found here: case-study.tar.gz
Conclusion
We have successfully modified the executable file from our case study to alter its behavior through the process of binary patching, without recompiling or relinking it, by leveraging the toolchain to achieve our objective. Next time, we will use a dedicated reverse-engineering program named Ghidra on the executable file, stripped of its debugging symbols, to perform the same operation.