小木曾雪菜 发表于 2021-8-11 11:35

Galgame汉化中的逆向(特别篇一):ArmArm64_ELF中汉化字符串超长修改方法

本帖最后由 小木曾雪菜 于 2022-2-3 23:15 编辑

# Galgame汉化中的逆向(特别篇一):ArmArm64_ELF中汉化字符串超长修改方法

by (https://github.com/YuriSizuku/GalgameReverse), 本贴论坛和[我的博客](https://blog.schnee.moe/posts/GalgameReverse_special1)同时发布

上篇链接:(https://www.52pojie.cn/thread-1478048-1-1.html)
为了方便,本篇的测试用例和代码我同时上传到附件上了。

### 0x0 前言

之前发的Galgame汉化中的逆向系列,大多是结合具体游戏来谈谈分析思路与汉化方法。大多数主机游戏可执行文件都是魔改的`ELF`文件(比如说psv,ps4系统内核就是由FreeBSD修改而来的),不同于以`x86`架构为主的pc平台,主机平台架构可谓是百花齐放:**psp,ps2是`mips`架构,ps3是`powerpc`架构,ps4是`x86`架构,psv是`arm`架构,switch是`arm64`架构**。ps4的`x86`架构修改方法和pc版类似,`powerpc`架构的汇编暂时还没研究,`mips`架构汇编相关的分析,今后有时间可能会更新教程。由于`arm`指令一般情况下都是4字节定长(还有2字节的`thumb`指令),`mov`,`ldw`等指令的立即数有范围限制,寻址方式多为相对于`PC`的偏移寻址,这与`x86`里立即数可以直接写VA地址(如`FF 15`系列长跳转指令、`b8`系列的`mov`)有很大区别。因此这篇作为《galgame汉化中的逆向》系列教程的补充,来聊聊**ELF结构、如何对ELF修改、Arm汇编寻址方法以及如何修改Arm汇编中字符串的指针**。

一般来说主机游戏剧本、字库等都在资源文件(封包文件)中,可执行文件中只有少量的系统文本。这部分通常也不需要汉化比原字符长(因为系统文本一般没有太多含义,且汉语本身就比较简洁),因此直接替换二进制字符就行了。但是对于一些特列,即字库或剧本整体封在了可执行文件中,就要增加区段和用汇编修改指针把字符串重定向到新增区段中了。如psv、switch版的Gnosia,和psv、ps3版的WhiteAlbum2,修改eboot方法详见(https://github.com/wetor/PSV_WHITE_ALBUM2/blob/master/psv_wa2_eboot_tool.py)。

为了更直观地说明原理和看到运行结果,本篇教程将不以具体游戏为例,而是用`arm`版helloword程序,讲解一下修改`arm`、`arm64` `ELF`的方法。理论上适用于psv、swich、android平台的可执行文件或动态库,根据不同平台可能需要再转换为其对应的魔改`ELF`格式。

## 0x1 工具准备

本篇教程需要的工具:

* Debain系Linux(WSL、虚拟机或实机)
* lief(elf解析与编辑),readelf(binutil里的elf查看器),hexdump(查看二进制)
* keystone(arm assembler),capstone(arm disassembler)
* qemu-arm-static,qemu-aarch64-static(qemu-user模式,用于运行arm elf)
* arm-linux-gnueabihf-g++,aarch64-linux-gnu-g++ (arm, arm64交叉编译)
* ida,ida64,arm-linux-gnueabihf-objdump,aarch64-linux-gnu-objdump (查看反汇编)

参考网站:

https://www.mztn.org/dragon/arm6405str.html

http://armconverter.com/

安装脚本如下:

```shell
sudo apt-get update
sudo apt-get install gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf # arm32
sudo apt-get install gcc-aarch64-linux-gnu g++-aarch64-linux-gnu # arm64
sudo apt-get install qemu-user-static binfmt-support
sudo apt-get install gdb-multiarch
pip3 install lief keystone-engine capstone
```

最简单的hello world程序代码:

```cpp
# include <iostream>
using namespace std;
int main()
{
    cout<<"hello world"<<endl;
}
```

交叉编译命令行:

```shell
cd build
arm-linux-gnueabihf-g++ ./../hello.cpp -o hello_arm
aarch64-linux-gnu-g++ ./../hello.cpp -o hello_arm64
```

运行命令行:

```shell
qemu-arm-static -L /usr/arm-linux-gnueabihf/ ./hello_arm
qemu-aarch64-static -L /usr/aarch64-linux-gnu/ ./hello_arm64
```

## 0x2 ELF介绍与增加区段方法

Linux系统的可执行文件和动态库为`ELF`格式,与windows的`pe`类似。里面的几个重要结构为:`ELF header`、`Section Header Table`‘、`Program Header Table`。关于`section`和`segment`关系如下

* segments与sections是包含的关系,一个segment包含若干个section。
* segments是从运行的角度来描述elf文件,通常在elf的头部
* sections是从链接的角度来描述elf文件,通常在elf的尾部
在链接阶段,可以忽略program header table来处理此文件,在运行阶段同理可忽略section
(所以很多加固手段删除了section header table)。

### (1) ELF header

```c
#define EI_NIDENT 16

struct Elf32_Ehdr {
   unsigned char e_ident; // ELF Identification bytes
   Elf32_Half e_type;                // Type of file (see ET_* below)
   Elf32_Half e_machine;   // Required architecture for this file (see EM_*)
   Elf32_Word e_version;   // Must be equal to 1
   Elf32_Addr e_entry;   // Address to jump to in order to start program
   Elf32_Off e_phoff;      // Program header table's file offset, in bytes
   Elf32_Off e_shoff;      // Section header table's file offset, in bytes
   Elf32_Word e_flags;   // Processor-specific flags
   Elf32_Half e_ehsize;    // Size of ELF header, in bytes
   Elf32_Half e_phentsize; // Size of an entry in the program header table
   Elf32_Half e_phnum;   // Number of entries in the program header table
   Elf32_Half e_shentsize; // Size of an entry in the section header table
   Elf32_Half e_shnum;   // Number of entries in the section header table
   Elf32_Half e_shstrndx;// Sect hdr table index of sect name string table
};
typedef struct {
         Elf64
      ...
} Elf64_Ehdr;
```

* e_entry:程序入口地址
* e_ehsize:ELF Header结构大小
* e_phoff、e_phentsize、e_phnum:描述Program Header Table的偏移、一项的大小、数量。
* e_shoff、e_shentsize、e_shnum:描述Section Header Table的偏移、一项的大小、数量。
* e_shstrndx:这一项描述的是section_name字符串表在Section Header Table中的索引,

### (2) Section Header

```C
typedef struct
{
Elf32_Word    sh_name;      /* Section name (string tbl index) */
Elf32_Word    sh_type;      /* Section type */
Elf32_Word    sh_flags;       /* Section flags */
Elf32_Addr    sh_addr;      /* Section virtual addr at execution */
Elf32_Off   sh_offset;      /* Section file offset */
Elf32_Word    sh_size;      /* Section size in bytes */
Elf32_Word    sh_link;      /* Link to another section */
Elf32_Word    sh_info;      /* Additional section information */
Elf32_Word    sh_addralign;       /* Section alignment */
Elf32_Word    sh_entsize;   /* Entry size if section holds table */
} Elf32_Shdr;

typedef struct{
   Elf64...
}Elf64_Shdr;
```

ELF文件的段结构就是由段表决定的。
编译器、链接器和装载器都是依靠段表来定位和访问各个段的属性的。
段表在ELF文件中的位置由ELF文件头的“e_shoff”成员决定的。

### (3) Program Header (segments)

```C
typedef struct {
   Elf32_Word p_type;   // Type of segment
   Elf32_Off p_offset;// File offset where segment is located, in bytes
   Elf32_Addr p_vaddr;// Virtual address of beginning of segment
   Elf32_Addr p_paddr;// Physical address of beginning of segment (OS-specific)
   Elf32_Word p_filesz; // Num. of bytes in file image of segment (may be zero)
   Elf32_Word p_memsz;// Num. of bytes in mem image of segment (may be zero)
   Elf32_Word p_flags;// Segment flags
   Elf32_Word p_align;// Segment alignment constraint
} Elf32_Phdr;

typedef struct {
      Elf64_Word
      ...   
} Elf64_Phdr;

```

### (4) readelf, objdump, hexdump

常用`readelf`来查看elf结构,`objdump`来查看反汇编,命令行如下:

```shell
readelf -h|--file-header elffile //查看elf头,可以与下面多个选项组合
readelf -S|--sections elffile//查看elf section头
readelf -l|-–segments elffile //查看elf segment头
readelf -x|--hexdump .sect_name|number elffile// 通过数字或name hexdump section
readelf -r elffile //查看elf 重定位
readelf -d elffile //查看elf .dynamic段, needed

objdump -f|--file-headers elffile //显示obj类型
objdump -x|--all-headers libxxxxx.so | grep NEEDED //-x 显示所可用的头信息
objdump -s [-j name] elf //-s 对section反汇编, -j 显示section的信息, 如.data
objdump -d [--start-address=address] [--stop-address=address] elffile //反编译

hexdump [-s|--skip skip_offset] [-n|--length size] <file>
-C 显示asci和hex
-d|x 两字节10进制,16进制显示
```

### (5) lief

关于添加新区段,可以自己手动修改`ELF`文件头对应的索引增加区段,可以参见(https://blog.csdn.net/qq_35559358/article/details/79064985)。我以前也写过windows`PE`增加区段的轮子(https://github.com/YuriSizuku/SimpleDpack),这次就不再自己造轮子了,直接用`lief`解析`ELF`了。这个库逻辑挺清晰的:

* `lief.ELF.parse(path)-> LIEF::ELF::Binary`,解析elf,

* `lief.ELF.Section(name, type)`,创建新`section`
* `binary.add(Section)-> LIEF::ELF::Section `,添加`section`,返回添加后的`section`
* `binary.patch_address(address, patch_value: List, va_type)`,patch`ELF`

> a. 添加区段并patch例子:

```python
libsdl_main_jni = lief.parse("hello")
sec_hookstr = lief.ELF.Section(".hookstr", lief.ELF.SECTION_TYPES.PROGBITS)
sec_hookstr += lief.ELF.SECTION_FLAGS.EXECINSTR
sec_hookstr.alignment = 4
sec_hookstr.content = list(bytes('hooked str hello world!\n', encoding='ansi'))
# add section will automaticlly add to segment, return section to see addr and size
sec_hookstr = libsdl_main_jni.add(sec_hookstr)

# para1, stored str address(this can be seen from ldr-4K~4K is the base addr)
# para2, edit stored str address content, hookstr rva 7000 -base 15CA
libsdl_main_jni.patch_address(0x15D0, )
libsdl_main_jni.write("hello_hook")
```

>b. 扩容区段例子:

```python
for section in libsdl_main_jni.sections:
    if section.name == '.rodata':
      print(hex(section.offset), section.size)
      byte_arr = list(bytes('/data/data/cn.natdon.onscripterv2yuri/lib/libapp_%s.so\0', encoding='ansi'))
      arr_offset = section.offset+section.size
      section.size += len(byte_arr)
      libsdl_main_jni.segments.virtual_size += len(byte_arr)
      libsdl_main_jni.segments.physical_size += len(byte_arr)
      libsdl_main_jni.patch_address(arr_offset, byte_arr)
      libsdl_main_jni.patch_address(0x21C8, list((arr_offset-0x2104).to_bytes(2, 'little')))
```

>c. sym替换(类似于IAThook)

```python
fopen_sym = next(filter(lambda e : e.name == "fopen", libxxx.imported_symbols))
fopen_sym.name = "fopen_saf"
```

## 0x3 Arm寻址分析与修改方法

32位的`arm`通常是通过当前`PC`寄存器找到`内存偏移表`地址,再通过表中偏移计算出最终地址,相当于两次对内存寻址找到最终地址。通常用`add pc, Rd`或`adr Rd, offset`,之后`ldr Rd`来获得最终地址。`内存偏移表`通常在一个函数的结尾处(`BLX LR `函数返回后),每项存储4字节偏移。

### (1) thumb ldr

`thumb`指令为2字节定长,在psv的eboot和android的so里面会经常出现这种指令集。

这种情况下`内存偏移表`存储的是相对于`add Rd, pc`的偏移,即`value_addr-(addpc_addr+4)`**(因为PC指向当前指令的两条之后,thumb指令集长度为2字节,所以此时的PC指向`cur_addr+4`)**,下面通过实例来分析:

```asm
// ida libapp_onscripter-32bpp.so
.text:00013F78 25 48LDRR0, =(aUsageOnscripte - 0x13F80) ; "Usage: onscripter ...
;aUsageOnscripte 00048B70, 0X13F7C+4=0x13F80, 0X48B70-0x13F80=34BF0, 相对于add pc偏移
.text:00013F7A 08 B5      PUSH       {R3,LR}
.text:00013F7C 78 44      ADD         R0, PC; "Usage: onscripter \n"
.text:00013F7E FF F7 E5 FFBL          sub_13F4C
....
.text:00014010 F0 4B 03 00 off_14010       DCD aUsageOnscripte - 0x13F80

// arm-linux-gnueabihf-objdump -d --start-address 0x13f78 --stop-address 0x14014 libapp_onscripter-32bpp.so
13f78:       4825            ldr   r0, ; 94h,(14010 <_Z10optionHelpv+0x98>)
13f7a:       b508            push    {r3, lr}
13f7c:       4478            add   r0, pc
13f7e:       f7ff ffe5       bl      13f4c <__gnu_Unwind_Find_exidx@plt+0x34>
```

可见ida里面机器码为文件中的字节顺序,指令转义为了`ldr`伪指令;而`objdump`中机器码为小端整数,指令转换成直接对应汇编的样子,因此学习arm汇编看`objdump`比较直观。

`13f78:4825 ldr r0, `为访问`内存偏移表`内容指令,偏移表地址`13f78h+4+#148(94h)=14010h`,即把偏移表存储的偏移`34BF0h`载入`r0`;偏移表里的内容为`.text:00014010 F0 4B 03 00`,是相对于`13f7c: 4478 add r0, pc`的偏移;`13f7c:4478 add r0, pc`,最终地址为`34BF0h+13F7Ch+4=48B70h`。

### (2) ldr, adr

`arm` 4字节`ldr`,`adr`伪指令转换为`add register, pc [#offset]`。机器码的存储格式如下:

```asm
      31      272625 24                     0500
LDR32        0 0        0 1 1        0        0 0        imm19 (4倍されて, ±1MB)        Rt(5bit)
```

同样有`内存偏移表`,但此偏移表存储的偏移值为相对于偏移表地址的偏移。下面为编译的`hello_arm`中`_start`函数的反汇编内容:

```asm
.text:00000648             _start
.text:00000648 4F F0 00 0B   MOV.W         R11, #0
.text:0000064C 4F F0 00 0E   MOV.W         LR, #0
.text:00000650 02 BC         POP             {R1}    ; argc
.text:00000652 6A 46         MOV             R2, SP; ubp_av
.text:00000654 04 B4         PUSH            {R2}    ; stack_end
.text:00000656 01 B4         PUSH            {R0}    ; rtld_fini
.text:00000658 DF F8 24 A0   LDR.W         R10, =($_GLOBAL_OFFSET_TABLE_ - 0x680)
; $_GLOBAL_OFFSET_TABLE_ 10FAC = 680 + 1092c 相对于偏移表的偏移。
.text:0000065C 08 A3         ADR             R3, off_680
.text:0000065E 9A 44         ADD             R10, R3 ; $_GLOBAL_OFFSET_TABLE_
.text:00000660 DF F8 20 C0   LDR.W         R12, =(__libc_csu_fini_ptr - 0x10FAC)
.text:00000664 5A F8 0C C0   LDR.W         R12, ; __libc_csu_fini
.text:00000668 4D F8 04 CD   PUSH.W          {R12}   ; fini
.text:0000066C 06 4B         LDR             R3, =(__libc_csu_init_ptr - 0x10FAC)
.text:0000066E 5A F8 03 30   LDR.W         R3, ; __libc_csu_init ; init
.text:00000672 06 48         LDR             R0, =(main_ptr - 0x10FAC)
.text:00000674 5A F8 00 00   LDR.W         R0, ; main ; main
.text:00000678 FF F7 D4 EF   BLX             __libc_start_main
.text:0000067C FF F7 BA EF   BLX             abort
.text:00000680 2C 09 01 00 off_680         DCD $_GLOBAL_OFFSET_TABLE_ - 0x680
.text:00000684 40 00 00 00 off_684         DCD __libc_csu_fini_ptr - 0x10FAC
.text:00000688 38 00 00 00 off_688         DCD __libc_csu_init_ptr - 0x10FAC
.text:0000068C 2C 00 00 00 off_68C         DCD main_ptr - 0x10FAC

// objdump -d hello_arm
00000648 <_start>:
648:   f04f 0b00       mov.w   fp, #0
64c:   f04f 0e00       mov.w   lr, #0
650:   bc02            pop   {r1}
652:   466a            mov   r2, sp
654:   b404            push    {r2}
656:   b401            push    {r0}
658:   f8df a024       ldr.w   sl,    ; 65ch+24h=680 <_start+0x38>
65c:   a308            add   r3, pc, #32   ; (adr r3, 680 <_start+0x38>)
65e:   449a            add   sl, r3
660:   f8df c020       ldr.w   ip,    ; 684 <_start+0x3c>
664:   f85a c00c       ldr.w   ip,
668:   f84d cd04       str.w   ip, !
66c:   4b06            ldr   r3,    ; (688 <_start+0x40>)
66e:   f85a 3003       ldr.w   r3,
672:   4806            ldr   r0,    ; (68c <_start+0x44>)
674:   f85a 0000       ldr.w   r0,
678:   f7ff efd4       blx   624 <__libc_start_main@plt>
67c:   f7ff efba       blx   5f4 <abort@plt>
680:   0001092c      .word   0x0001092c
684:   00000040      .word   0x00000040
688:   00000038      .word   0x00000038
68c:   0000002c      .word   0x0000002c
```

` 658:f8df a024 ldr.w sl, `为加载`内存偏移表`内容指令,`658h+#36(24h)+4=680h`为`内存偏移表`地址,加载`内存偏移表`存储的偏移值`1092ch`到`s1`寄存器;`65c:a308addr3, pc, #32`,紧接着将`r3`载入`内存偏移表`地址,即`65ch+#32(20h)+4=680h`。` 65e:449a add sl, r3`得到最终地址`1092ch+680h=10fach`,因此这里`内存偏移表`内存储的偏移是最终地址相对于`内存偏移表`地址的偏移。

### (3) 编程修改ARM汇编字符串地址

由于`arm`汇编是用`PC`和`内存偏移表`进行寻址,所以修改`ldr`的最终读取的地址仅需要修改`内存偏移表`中的偏移。

修改`arm`汇编字符串地址的测试用例如下:用`lief`增加区段,内容为"hooked str hello world!\n",`capstone`反汇编解析`ldr`的`内存偏移表`地址,修改`内存偏移表`内的值改为新增加区段的字符串相对偏移(注意偏移值是相对于`add pc`处的,脚本里统一用`VA`相减表示)

```python
import lief
from capstone.arm_const import ARM_OP_MEM, ARM_OP_REG
from keystone import Ks, KS_ARCH_ARM, KS_MODE_LITTLE_ENDIAN
from capstone import Cs, CS_ARCH_ARM, CS_MODE_ARM, CS_MODE_THUMB

# add section (as well as segment)
armelf = lief.parse("./build/hello_arm")
new_sect = lief.ELF.Section(".new", lief.ELF.SECTION_TYPES.PROGBITS)
new_sect += lief.ELF.SECTION_FLAGS.WRITE # .EXECINSTR
new_sect.alignment = 4
new_sect.content = list(bytes('hooked str hello world!\n', encoding='ansi'))
new_sect = armelf.add(new_sect)

#760 09 4BLDR R3, =(aHelloWorld - 0x766) ; "hello world"
#762 7B 44ADD R3, PC; "hello world"
#764 19 46MOV R1, R3
code = b"\x09\x4B\x7B\x44\x19\x46"
offset = 0x760
text_sect = armelf.get_section(".text")
addr = text_sect.search(code) + text_sect.virtual_address
print("text_sect base", hex(text_sect.virtual_address))

# disassembly loading in memory offset table
cs = Cs(CS_ARCH_ARM, CS_MODE_THUMB)
cs.detail = True
hello_offtable_addr = 0 # offset table for "hello world"
for i in cs.disasm(code, addr):
    print(hex(i.address), i.mnemonic, i.op_str)
    if i.mnemonic=='ldr' and \
       i.operands.type==ARM_OP_REG and \
       i.operands.type == ARM_OP_MEM:
      print("#ldr detail:", i.reg_name(i.operands.value.reg), i.operands.value.mem.disp)
      hello_offtable_addr = i.address + i.operands.value.mem.disp + 4

# modify offset table
# 788 EE 00 00 00 DCD aHelloWorld - 0x766 # 0x854
text_sect_content = bytes(text_sect.content)
start = hello_offtable_addr - text_sect.virtual_address
hello_offtable = int.from_bytes(text_sect_content, 'little')
print("hello_offtable at 0x%x, value 0x%x" %(hello_offtable_addr, hello_offtable))
target_offset = (new_sect.virtual_address) - (addr+2+4); # related to add pc

# patch offset table
armelf.patch_address(hello_offtable_addr, target_offset, 4)
armelf.write(r"./build/hello_arm_rebuild")
```

运行patch脚本后,qemu测试hook成功!

!(https://p.sda1.dev/4/c1156db8468bda130736bfa436140503)

## 0x4 Arm64寻址分析与修改方法

`arm64`和`arm`同样是定长指令和相对于`PC`的寻址,只不过`arm64`没有`内存偏移表`了,用`adrp @page`和`add|ldr @pageoff`来寻址。

### (1) adrp 指令分析

```asm
ADRP Xd, val_page_addr ;@pageXd = addr + (PC - (PC & 0xfff))
addXd, Xd, :lo12:val ;@pageoff
```

`ADRP`指令是以页为单位的大范围的地址读取指令,P为`page`, `pageoff`为`page offset`,这里填写的地址是最终地址的页基址,**汇编会自动把opcode转换成相对于pc页基址的地址。**

符号扩展一个21位的offset(`immhi+immlo`)。向左移动12位,将`PC`的值的低12位清零,然后把这两者相加,结果写入到`Xd`寄存器,用来得到一块含有label的4KB对齐内存区域的base地址(也就是说label所在的地址,一定落在这个4KB的内存区域里,指令助记符里Page也就是这个意思), 可用来寻址 +/- 4GB的范围(2^33次幂)。

下面是几种指令的机器码比较:

```ASM
      31      272625 24                     0500
LDR32        0 0        0 1 1        0        0 0        imm19 (4倍されて, ±1MB)        Rt(5bit)
LDR64        0 1        0 1 1        0        0 0        imm19 (4倍されて, ±1MB)        Rt(5bit)
LDRSW        1 0        0 1 1        0        0 0        imm19 (4倍されて, ±1MB)        Rt(5bit)

   31         29          24             05         00
ADR       0        immL(2bit)        1 0 0 0 0        immHi (19bit)        Rd(5bit)
ADRP 1        immL(2bit)        1 0 0 0 0        immHi (19bit)        Rd(5bit)
```

下面代码说明了,`adrp`的机器码`immhi+immlo`存储的是相对于当前页面的页偏移0x22000,但是反汇编后显示的则是目标的`VA`,即`adrp x0, #0x25000`(当此指令在0x3000处时候,`cs.disasm(code, 0x3000)`)。

```python
from capstone import Cs, CS_ARCH_ARM64, CS_MODE_ARM
code = b'\x00\x01\x00\xd0'
cs =Cs(CS_ARCH_ARM64, CS_MODE_ARM)
i = next(cs.disasm(code, 0x3000))
print(i.mnemonic, i.op_str) # adrp x0, #0x25000

code_int = int.from_bytes(code, 'little')
immHi = (code_int&0x00ffffff)>>5
immL = code_int>>29 & 0b11
imm =(immHi<<2|immL)<<12
print(f"code_int=0b{code_int:b}") # code_int=0b11010000000000000000000100000000
print(f"immHi=0b{immHi:b}, immL=0b{immL:b}, imm=0x{imm:x}") # immHi=0b1000, immL=0b10, imm=0x22000
```

### (2) @page,@pageoff寻址

`adrp @page` 找到目标页地址,`add @pageoff`加上页内偏移。如果用ida的keypatch修改,需要去掉`@page`,`@pageoff`后缀,手动来计算目标页地址和页内偏移才行。ida和`objdump`的反汇编代码如下:

```asm
// ida hello_arm64
.text:00000000000009DC             main ; DATA XREF: .got:main_ptr↓o
.text:00000000000009DC FD 7B BF A9   STPX29, X30, !
.text:00000000000009E0 FD 03 00 91   MOVX29, SP
.text:00000000000009E4 00 00 00 90   ADRP X0, #aHelloWorld@PAGE ; "hello world",
.text:00000000000009E8 01 00 2D 91   ADDX1, X0, #aHelloWorld@PAGEOFF;0xb40
.text:00000000000009EC 80 00 00 90   ADRP X0, #_ZSt4cout_ptr@PAGE
.text:00000000000009F0 00 E4 47 F9   LDRX0,
.text:00000000000009F4 9F FF FF 97   BL            ._ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc ; std::operator<<<std::char_traits<char>>(std::ostream &,char const*)
0000000009F8 E2 03 00 AA             MOVX2, X0
.text:00000000000009FC 80 00 00 90   ADRP X0, #_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6__ptr@PAGE
.text:0000000000000A00 01 DC 47 F9   LDRX1,
.text:0000000000000A04 E0 03 02 AA   MOVX0, X2
.text:0000000000000A08 9E FF FF 97   BL ._ZNSolsEPFRSoS_E ; std::ostream::operator<<(std::ostream & (*)(std::ostream &))
.text:0000000000000A0C 00 00 80 52   MOVW0, #0
.text:0000000000000A10 FD 7B C1 A8   LDPX29, X30, ,#0x10
.text:0000000000000A14 C0 03 5F D6   RET

// aarch64-linux-gnu-objdump -d hello_arm64
00000000000009dc <main>:
9dc:   a9bf7bfd      stp   x29, x30, !
9e0:   910003fd      mov   x29, sp
9e4:   90000000      adrp    x0, 0 <_init-0x810> ; aHelloWorld pageaddr
9e8:   912d0001      add   x1, x0, #0xb40 ; #aHelloWorld@PAGEOFF
9ec:   90000080      adrp    x0, 10000 <__FRAME_END__+0xf340>
9f0:   f947e400      ldr   x0,
9f4:   97ffff9f      bl      870 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@plt>
9f8:   aa0003e2      mov   x2, x0
9fc:   90000080      adrp    x0, 10000 <__FRAME_END__+0xf340>
a00:   f947dc01      ldr   x1,
a04:   aa0203e0      mov   x0, x2
a08:   97ffff9e      bl      880 <_ZNSolsEPFRSoS_E@plt>
a0c:   52800000      mov   w0, #0x0                        // #0
a10:   a8c17bfd      ldp   x29, x30, , #16
a14:   d65f03c0      ret
```

### (3) 编程修改ARM汇编字符串地址

下面的python脚本说明了如何用`capstone`进行反汇编,如何手动求`目标页地址`和`页内偏移`,以及之后用`keystone`进行汇编更新`adrp @page`和`add @pageoff`指令。

```python
import lief
from keystone import Ks, KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN
from capstone import Cs, CS_ARCH_ARM64, CS_MODE_ARM

# add section (as well as segment)
arm64elf = lief.parse("./build/hello_arm64")
new_sect = lief.ELF.Section(".new", lief.ELF.SECTION_TYPES.PROGBITS)
new_sect += lief.ELF.SECTION_FLAGS.WRITE # .EXECINSTR
new_sect.alignment = 4
new_sect.content = list(bytes('xxhooked str hello world!\n', encoding='ansi'))
new_sect = arm64elf.add(new_sect)

#9E4 00 00 00 90 ADRPX0, #aHelloWorld@PAGE ; "hello world"
#9E8 01 00 2D 91 ADD   X1, X0, #aHelloWorld@PAGEOFF ; "hello world"
code = b"\x00\x00\x00\x90\x01\x00\x2d\x91"
offset = 0x9e4
text_sect = arm64elf.get_section(".text")
addr = text_sect.search(code) + text_sect.virtual_address

# disassembly adrp "hello sdr"
cs = Cs(CS_ARCH_ARM64, CS_MODE_ARM)
cs.detail = True
for i in cs.disasm(code, addr):
    print(i.mnemonic, i.op_str)

# change addrss to new sect
ks = Ks(KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN)
page = new_sect.virtual_address - (new_sect.virtual_address & 0xfff) # address page
pageoff =(new_sect.virtual_address+2) & 0xfff
asm_str = "adrp x0, #0x{:x};add x1, x0, #0x{:x}".format(page, pageoff)
print("rebuild_asm:", asm_str)
code_rebuild, _ = ks.asm(asm_str, addr)
print("rebuild_code:", "".join())
for i in cs.disasm(bytes(code_rebuild), addr):
    print(i.mnemonic, i.op_str)

# patch file
arm64elf.patch_address(addr, code_rebuild)
arm64elf.write(r"./build/hello_arm64_rebuild")
```

运行patch脚本后,qemu测试成功!

!(https://p.sda1.dev/4/22132f608f1484bff270043c90a3fc58)

## 0x5 后记

这篇教程虽然难度不高,本来以为很快就能写完了,但是写了好久。很多东西叙述起来很麻烦,我尽量结合实例清晰和有条理性地讲述,不知不觉中写了好多东西。目前关于主机系列的汉化教程非常少,以往很多的教程也都随着论坛的关闭等无法访问了。同样,比起`x86`的汇编的修改,`arm64 elf`之类的分析也不多。我系统地做《galgame汉化中的逆向》系列是想总结一下汉化方法,结合我自身对一些问题的思考和理解,以使汉化逆向这门技术今后能更好地传承下去。

小木曾雪菜 发表于 2021-8-12 12:22

Days0708 发表于 2021-8-11 17:54
原来游戏汉化也要会逆向吗,这也太厉害了吧

游戏汉化肯定是需要逆向的,尤其是遇到那种自定义格式的字库,加密压缩封包等。

Days0708 发表于 2021-8-12 13:22

小木曾雪菜 发表于 2021-8-12 12:22
游戏汉化肯定是需要逆向的,尤其是遇到那种自定义格式的字库,加密压缩封包等。

原来如此,我一直以为是游戏里会有个专门的存放文本的库,只要改里面的内容就行了。原来汉化这么麻烦,更敬佩汉化组了!{:301_993:}

心里乐开花 发表于 2021-8-11 11:53

太强了膜拜

super7cheng 发表于 2021-8-11 12:19

这么强大,膜拜,nice!!!

ycy1406305 发表于 2021-8-11 12:43

牛牛牛!

玟篠 发表于 2021-8-11 12:54

想试试看。

从0开始的小小怪 发表于 2021-8-11 13:08

好文, 另外hello world程序代码这边```c++不支持, 需要编辑一下

Mr.Mi 发表于 2021-8-11 13:41

厉害了,学习了。

Days0708 发表于 2021-8-11 17:54

原来游戏汉化也要会逆向吗,这也太厉害了吧

咔c君 发表于 2021-8-11 21:23

学习了不错
页: [1] 2 3
查看完整版本: Galgame汉化中的逆向(特别篇一):ArmArm64_ELF中汉化字符串超长修改方法