├── README.md └── genshinjumpfixer2.py /README.md: -------------------------------------------------------------------------------- 1 | # genshinjumpfixer2 2 | 3 | **Moved to [another repo](https://github.com/khang06/misc/tree/master/reversing/genshin)!!!** 4 | 5 | This is an IDAPython script for cleaning up obfuscated branches seen in Genshin Impact's UnityPlayer.dll. It's only been tested on the 1.5 dev build leak, but I doubt much has changed since then. 6 | 7 | To use this, simply execute the script while selecting a specific block of code. The script will automatically calculate the jump targets and simplify the code. 8 | 9 | Examples of what to select can be seen below. If you're able to reverse enough code to get to this point, you can probably figure out the rest. 10 | 11 | 12 | ![](https://files.catbox.moe/xeitlv.png) 13 | ![](https://files.catbox.moe/nojqhd.png) 14 | ![](https://files.catbox.moe/dylvp9.png) 15 | -------------------------------------------------------------------------------- /genshinjumpfixer2.py: -------------------------------------------------------------------------------- 1 | from unicorn import * 2 | from unicorn.x86_const import * 3 | 4 | from keystone import Ks, KS_ARCH_X86, KS_MODE_64 5 | 6 | import idautils 7 | import idc 8 | import idaapi 9 | import ida_bytes 10 | import ida_allins 11 | 12 | from time import time 13 | 14 | class Emulator: 15 | def __init__(self): 16 | emu = Uc(UC_ARCH_X86, UC_MODE_64) 17 | 18 | # map in the required sections 19 | # .text and .data should be the only required ones 20 | # TODO: map even less data 21 | now = time() 22 | for s in idautils.Segments(): 23 | start = idc.get_segm_start(s) 24 | end = idc.get_segm_end(s) 25 | name = idc.get_segm_name(s) 26 | # end must be rounded up to the nearest page 27 | if end % 0x1000 != 0: 28 | end += 0x1000 - end % 0x1000 29 | if name == ".text" or name == ".data": 30 | print(f"adding segment {name} from {hex(start)} to {hex(end)}") 31 | # these should not be written to so i can ensure that i can reuse the same emulator instance 32 | emu.mem_map(start, end - start, UC_PROT_EXEC | UC_PROT_READ) 33 | seg_bytes = ida_bytes.get_bytes(start, end - start) 34 | #print(type(seg_bytes)) 35 | emu.mem_write(start, seg_bytes) 36 | #print("added") 37 | # for some reason, there are two .data segments 38 | # only need the first one, so i'll force it to stop after it 39 | if name == ".data": 40 | break 41 | # done for now 42 | self.emu = emu 43 | print(f"added segments in {time() - now}") 44 | 45 | def emu_start(self, start, end, max_insns): 46 | self.emu.emu_start(start, end, count=max_insns) 47 | 48 | def run(self, start, end): 49 | pc = start 50 | reached_jmp = False 51 | for _ in range(100): 52 | self.emu_start(pc, -1, 1) 53 | pc = self.reg_read(UC_X86_REG_RIP) 54 | if pc == end: 55 | self.emu_start(pc, -1, 1) 56 | pc = self.reg_read(UC_X86_REG_RIP) 57 | reached_jmp = True 58 | break 59 | if not reached_jmp: 60 | print("didn't reach the end of the block in 100 instructions. this should not happen at all") 61 | return 62 | return pc 63 | 64 | def reset(self): 65 | # i don't want to unmap everything, since it's possible to reuse the current mappings 66 | # so clearing every register it is 67 | # some registers can't be run in this loop since they're supposed to take multiple arguments 68 | # it should still be fine though 69 | EXCLUDED_REGISTERS = { 70 | UC_X86_REG_IDTR, 71 | UC_X86_REG_GDTR, 72 | UC_X86_REG_LDTR, 73 | UC_X86_REG_TR, 74 | UC_X86_REG_MSR 75 | } 76 | for x in range(x86_const.UC_X86_REG_FP0, x86_const.UC_X86_REG_FP0 + 8): 77 | EXCLUDED_REGISTERS.add(x) 78 | for x in range(x86_const.UC_X86_REG_XMM0, x86_const.UC_X86_REG_XMM0 + 8): 79 | EXCLUDED_REGISTERS.add(x) 80 | for x in range(x86_const.UC_X86_REG_YMM0, x86_const.UC_X86_REG_YMM0 + 16): 81 | EXCLUDED_REGISTERS.add(x) 82 | 83 | for x in range(UC_X86_REG_INVALID + 1, UC_X86_REG_ENDING): 84 | if x in EXCLUDED_REGISTERS: 85 | continue 86 | self.reg_write(x, 0) 87 | 88 | def reg_read(self, reg): 89 | return self.emu.reg_read(reg) 90 | def reg_write(self, reg, val): 91 | self.emu.reg_write(reg, val) 92 | 93 | def mem_write(self, addr, data): 94 | self.emu.mem_write(addr, data) 95 | 96 | def get_ida_insns(start, end): 97 | insns = [] 98 | for ea in idautils.Heads(start, end): 99 | insn = idaapi.insn_t() 100 | idaapi.decode_insn(insn, ea) 101 | #print(insn.itype) 102 | insns.append(insn) 103 | return insns 104 | 105 | def main(): 106 | emu = Emulator() 107 | #emu.reg_write(UC_X86_REG_RAX, 0x11223344) 108 | #print(hex(emu.reg_read(UC_X86_REG_RAX))) 109 | if idc.read_selection_start() == idc.BADADDR: 110 | print("nothing is selected...") 111 | return 112 | block_start = idc.read_selection_start() 113 | block_end = idc.read_selection_end() 114 | print(f"analyzing from {hex(block_start)} to {hex(block_end)}") 115 | insns = get_ida_insns(block_start, block_end) 116 | 117 | # get and save any special instructions between the add and the jmp 118 | # an example of this happening is at 0x1800F9F6B in 1.5-dev's UnityPlayer.dll 119 | # these instructions should be able to be safely moved 120 | # also, they are useless in the context of figuring out the branch targets 121 | add_idx = -1 122 | for i in range(len(insns) - 1, -1, -1): 123 | if i == len(insns) - 1: 124 | if insns[i].itype != ida_allins.NN_jmpni: 125 | print(f"wtf, last instruction is not a register jmp") 126 | return 127 | else: 128 | if insns[i].itype == ida_allins.NN_add: 129 | add_idx = i 130 | break 131 | if add_idx == -1: 132 | print("couldn't find the add...") 133 | return 134 | extra_insns = insns[add_idx + 1:-1] 135 | extra_insn_bytes = bytes() 136 | for x in extra_insns: 137 | extra_insn_bytes += ida_bytes.get_bytes(x.ea, x.size) 138 | print(f"got {len(extra_insns)} extra instructions ({len(extra_insn_bytes)} bytes)") 139 | if len(extra_insns) > 0: 140 | extra_insn_start = extra_insns[0].ea 141 | extra_insn_end = extra_insn_start + len(extra_insn_bytes) 142 | print(f"nopping emulator memory from {hex(extra_insn_start)} to {hex(extra_insn_end)}") 143 | patch = b'\x90' * len(extra_insn_bytes) 144 | emu.mem_write(extra_insn_start, patch) 145 | 146 | # emulate! 147 | CASES_TO_TEST = [ 148 | ("none", "none", 0x0000), 149 | ("eflags", "carry", 0x0001), 150 | ("eflags", "zero", 0x0040), 151 | ("eflags", "overflow", 0x0800), 152 | 153 | ("register", "dl", UC_X86_REG_DL), 154 | ("register", "r8b", UC_X86_REG_R8B), 155 | ] 156 | results = [None] * (len(CASES_TO_TEST) - 1) 157 | no_flag_pc = 0 158 | branch_pc = 0 159 | try: 160 | start = time() 161 | for i in range(len(CASES_TO_TEST)): 162 | x = CASES_TO_TEST[i] 163 | emu.reset() 164 | if x[0] == "eflags": 165 | emu.reg_write(UC_X86_REG_EFLAGS, x[2]) 166 | elif x[0] == "register": 167 | emu.reg_write(x[2], 1) 168 | pc = emu.run(block_start, insns[-1].ea) 169 | if x[0] == "eflags": 170 | print(f"jumped to {hex(pc)} with flag {x[1]} set") 171 | elif x[0] == "register": 172 | print(f"jumped to {hex(pc)} with register {x[1]} set") 173 | elif x[0] == "none": 174 | print(f"jumped to {hex(pc)} with nothing set") 175 | else: 176 | print("what") 177 | return 178 | if x[0] == "none": 179 | no_flag_pc = pc 180 | else: 181 | results[i - 1] = pc != no_flag_pc 182 | if pc != no_flag_pc: 183 | branch_pc = pc 184 | print(f"emulated stuff in {time() - start}") 185 | print(results) 186 | except UcError as e: 187 | print(f"got error {e} at {hex(emu.reg_read(UC_X86_REG_RIP))}") 188 | return 189 | 190 | print(f"targets are {hex(no_flag_pc)} and {hex(branch_pc)}") 191 | 192 | # find an appropriate branch instruction to replace the flags being checked 193 | # if all registers being 0 already satisfies the constraints, the target addresses will be swapped 194 | jump_insn = "" 195 | reg_to_test = "" 196 | swap_targets = None 197 | if results == [None, None, None]: 198 | print("something broke") 199 | return 200 | elif results == [False, True, False, False, False]: 201 | # branch if ZF=1 202 | jump_insn = "jz" 203 | swap_targets = False 204 | elif results == [True, False, False, False, False]: 205 | # branch if CF=1 206 | jump_insn = "jc" 207 | swap_targets = False 208 | elif results == [True, True, False, False, False]: 209 | # branch if CF=0 and ZF=0 210 | jump_insn = "jnbe" 211 | swap_targets = True 212 | #swap_targets = False 213 | elif results == [False, False, False, True, False]: 214 | # branch if DL=1 215 | reg_to_test = "dl" 216 | swap_targets = False 217 | elif results == [False, False, False, False, True]: 218 | # branch if R8B=1 219 | reg_to_test = "r8b" 220 | swap_targets = False 221 | else: 222 | print("unhandled results table") 223 | return 224 | if swap_targets == None: 225 | print("i am retarded") 226 | return 227 | 228 | # start assembling 229 | # probably not the best way to do this 230 | jump_targets = [None, None] 231 | if swap_targets: 232 | jump_targets = [ 233 | branch_pc, 234 | no_flag_pc 235 | ] 236 | else: 237 | jump_targets = [ 238 | no_flag_pc, 239 | branch_pc 240 | ] 241 | 242 | ks = Ks(KS_ARCH_X86, KS_MODE_64) 243 | patch = extra_insn_bytes 244 | 245 | if reg_to_test == "": 246 | # branch based on flag 247 | to_assemble = f"{jump_insn} {hex(jump_targets[1] - (block_start + len(patch)))}" 248 | patch += bytes(ks.asm(to_assemble)[0]) 249 | to_assemble = f"jmp {hex(jump_targets[0] - (block_start + len(patch)))}" 250 | patch += bytes(ks.asm(to_assemble)[0]) 251 | else: 252 | # branch based on register being non-zero 253 | to_assemble = f"test {reg_to_test},{reg_to_test}" 254 | patch += bytes(ks.asm(to_assemble)[0]) 255 | to_assemble = f"jnz {hex(jump_targets[1] - (block_start + len(patch)))}" 256 | patch += bytes(ks.asm(to_assemble)[0]) 257 | to_assemble = f"jmp {hex(jump_targets[0] - (block_start + len(patch)))}" 258 | patch += bytes(ks.asm(to_assemble)[0]) 259 | 260 | if len(patch) > block_end - block_start: 261 | print("generated patch is too big") 262 | return 263 | 264 | # pad the patch with int3s 265 | for _ in range(block_end - block_start - len(patch)): 266 | patch += b'\xCC' 267 | print(f"patch: {patch}") 268 | 269 | # go! 270 | ida_bytes.patch_bytes(block_start, patch) 271 | 272 | # clean stuff up a little 273 | for x in range(block_start, block_end): 274 | idc.create_insn(x) 275 | 276 | main() --------------------------------------------------------------------------------