Galgame汉化中的逆向:ArmArm64_ELF中汉化字符串超长修改方法
看雪论坛作者ID:devseed
之前发的Galgame汉化中的逆向系列,大多是结合具体游戏来谈谈分析思路与汉化方法。大多数主机游戏可执行文件都是魔改的ELF文件(比如说psv,ps4系统内核就是由FreeBSD修改而来的),不同于以x86架构为主的pc平台。
主机平台架构可谓是百花齐放:psp,ps2是mips架构,ps3是powerpc架构,ps4是x86架构,psv是arm架构,switch是arm64架构。ps4的x86架构修改方法和pc版类似,powerpc架构的汇编暂时还没研究,mips架构汇编相关的分析,今后有时间可能会更新教程。
但是对于一些特列,即字库或剧本整体封在了可执行文件中,就要增加区段和用汇编修改指针把字符串重定向到新增区段中了。如psv、switch版的Gnosia,和psv、ps3版的WhiteAlbum2,修改eboot方法详见PSV版WA2汉化移植。
1
工具准备
本篇教程需要的工具:
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 (查看反汇编)
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
# include <iostream>
using namespace std;
int main()
{
cout<<"hello world"<<endl;
}
cd build
arm-linux-gnueabihf-g++ ./../hello.cpp -o hello_arm
aarch64-linux-gnu-g++ ./../hello.cpp -o hello_arm64
qemu-arm-static -L /usr/arm-linux-gnueabihf/ ./hello_arm
qemu-aarch64-static -L /usr/aarch64-linux-gnu/ ./hello_arm64
2
ELF介绍与增加区段方法
关于section和segment关系如下:
segments与sections是包含的关系,一个segment包含若干个section segments是从运行的角度来描述elf文件,通常在elf的头部 sections是从链接的角度来描述elf文件,通常在elf的尾部
(1) ELF header
#define EI_NIDENT 16
struct Elf32_Ehdr {
unsigned char e_ident[EI_NIDENT]; // 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
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;
(3) Program Header (segments)
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 -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
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[int], va_type),patchELF
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, [0X36, 0X5A])
libsdl_main_jni.write("hello_hook")
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[1].virtual_size += len(byte_arr)
libsdl_main_jni.segments[1].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')))
fopen_sym = next(filter(lambda e : e.name == "fopen", libxxx.imported_symbols))
fopen_sym.name = "fopen_saf"
3
Arm寻址分析与修改方法
(1) thumb ldr
// ida libapp_onscripter-32bpp.so
.text:00013F78 25 48 LDR R0, =(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 [option ...]\n"
.text:00013F7E FF F7 E5 FF BL 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, [pc, #148]; 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>
(2) ldr, adr
31 27 26 25 24 05 00
LDR32 0 0 0 1 1 0 0 0 imm19 (4倍されて, ±1MB) Rt(5bit)
.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, [R10,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, [R10,R3] ; __libc_csu_init ; init
.text:00000672 06 48 LDR R0, =(main_ptr - 0x10FAC)
.text:00000674 5A F8 00 00 LDR.W R0, [R10,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, [pc, #36] ; 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, [pc, #32] ; 684 <_start+0x3c>
664: f85a c00c ldr.w ip, [sl, ip]
668: f84d cd04 str.w ip, [sp, #-4]!
66c: 4b06 ldr r3, [pc, #24] ; (688 <_start+0x40>)
66e: f85a 3003 ldr.w r3, [sl, r3]
672: 4806 ldr r0, [pc, #24] ; (68c <_start+0x44>)
674: f85a 0000 ldr.w r0, [sl, 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
(3) 编程修改ARM汇编字符串地址
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 4B LDR R3, =(aHelloWorld - 0x766) ; "hello world"
#762 7B 44 ADD R3, PC ; "hello world"
#764 19 46 MOV 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[0].type==ARM_OP_REG and \
i.operands[1].type == ARM_OP_MEM:
print("#ldr detail:", i.reg_name(i.operands[0].value.reg), i.operands[1].value.mem.disp)
hello_offtable_addr = i.address + i.operands[1].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[start:start+4], '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")
4
Arm64寻址分析与修改方法
(1) adrp 指令分析
ADRP Xd, val_page_addr ;@page Xd = addr + (PC - (PC & 0xfff))
add Xd, Xd, :lo12:val ;@pageoff
31 27 26 25 24 05 00
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)
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寻址
// ida hello_arm64
.text:00000000000009DC main ; DATA XREF: .got:main_ptr↓o
.text:00000000000009DC FD 7B BF A9 STP X29, X30, [SP,#-0x10+var_s0]!
.text:00000000000009E0 FD 03 00 91 MOV X29, SP
.text:00000000000009E4 00 00 00 90 ADRP X0, #aHelloWorld@PAGE ; "hello world",
.text:00000000000009E8 01 00 2D 91 ADD X1, X0, #aHelloWorld@PAGEOFF;0xb40
.text:00000000000009EC 80 00 00 90 ADRP X0, #_ZSt4cout_ptr@PAGE
.text:00000000000009F0 00 E4 47 F9 LDR X0, [X0,#_ZSt4cout_ptr@PAGEOFF]
.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 MOV X2, X0
.text:00000000000009FC 80 00 00 90 ADRP X0, #_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6__ptr@PAGE
.text:0000000000000A00 01 DC 47 F9 LDR X1, [X0,#_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6__ptr@PAGEOFF]
.text:0000000000000A04 E0 03 02 AA MOV X0, X2
.text:0000000000000A08 9E FF FF 97 BL ._ZNSolsEPFRSoS_E ; std::ostream::operator<<(std::ostream & (*)(std::ostream &))
.text:0000000000000A0C 00 00 80 52 MOV W0, #0
.text:0000000000000A10 FD 7B C1 A8 LDP X29, X30, [SP+var_s0],#0x10
.text:0000000000000A14 C0 03 5F D6 RET
// aarch64-linux-gnu-objdump -d hello_arm64
00000000000009dc <main>:
9dc: a9bf7bfd stp x29, x30, [sp, #-16]!
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, [x0, #4040]
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, [x0, #4024]
a04: aa0203e0 mov x0, x2
a08: 97ffff9e bl 880 <_ZNSolsEPFRSoS_E@plt>
a0c: 52800000 mov w0, #0x0 // #0
a10: a8c17bfd ldp x29, x30, [sp], #16
a14: d65f03c0 ret
(3) 编程修改ARM汇编字符串地址
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 ADRP X0, #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([f"{i:02x}" for i in code_rebuild]))
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")
看雪ID:devseed
https://bbs.pediy.com/user-home-617776.htm
# 往期推荐
1. V8利用初探 2019 StarCTF oob 复现分析
2. 新人PWN入坑总结
4. Cisco RV160W系列路由器漏洞:从1day分析到0day挖掘
5. 从SSL库的内存漫游开发dump自定义客户端证书的通杀脚本
球分享
球点赞
球在看
点击“阅读原文”,了解更多!