├── README.md ├── fixresources └── manifest.xml /README.md: -------------------------------------------------------------------------------- 1 | fixresources 2 | ============ 3 | 4 | Introduction 5 | ------------ 6 | `fixresources` was created to help resolve resource identifiers in disassembled Android application files (smali files). For more information on this topic, check out my [blog](http://blog.thecobraden.com/2013/04/fixing-resource-identifiers-in.html). 7 | 8 | Usage 9 | ----- 10 | `fixresources` is meant to be used as a `dtf` module. After disassembling a application, you can use it as follows to enrich your smali code: 11 | 12 | ```bash 13 | dtf fixresources path/to/app_root 14 | ``` 15 | -------------------------------------------------------------------------------- /fixresources: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # DTF Core Content 4 | # Copyright 2013-2016 Jake Valletta (@jake_valletta) 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | """Resolve resource values in Smali files""" 19 | from lxml import etree 20 | import os 21 | import os.path 22 | import re 23 | 24 | import dtf.logging as log 25 | from dtf.module import Module 26 | 27 | DISPLAY_LEN = 25 28 | PUBLIC_FILE_PATH = "/res/values/public.xml" 29 | STRING_FILE_PATH = "/res/values/strings.xml" 30 | SMALI_FILES_PATH = "/smali" 31 | 32 | CONST_REGEX = re.compile("const[ \/\-].*0x[a-fA-F0-9]{4,8}$") 33 | PACKED_SWITCH_REGEX = re.compile("\.packed-switch 0x[a-fA-F0-9]{4,8}$") 34 | 35 | TAG = 'fixresources' 36 | 37 | class fixresources(Module): 38 | 39 | """Module class for resolving resources""" 40 | 41 | about = 'Resolve resource values in Smali files.' 42 | author = 'Jake Valletta (jakev)' 43 | health = 'beta' 44 | name = 'fixresources' 45 | version = '1.0.0' 46 | 47 | public_dict = {} 48 | has_strings = False 49 | 50 | def usage(self): 51 | 52 | """Module usage""" 53 | 54 | print "fixresources v%s" % self.version 55 | print "" 56 | print "Usage: fixresources [decoded_app_dir]" 57 | print "" 58 | 59 | return 0 60 | 61 | def do_parse_public(self, project_dir): 62 | 63 | """Parse public.xml file""" 64 | 65 | public_file_path = "%s/%s" % (project_dir, PUBLIC_FILE_PATH) 66 | 67 | if not os.path.isfile(public_file_path): 68 | log.e(TAG, "'%s' public resource file not found!" 69 | % public_file_path) 70 | return -1 71 | 72 | log.i(TAG, "Parsing public.xml...") 73 | 74 | for _, element in etree.iterparse(public_file_path): 75 | if element.tag == "public": 76 | 77 | try: 78 | res_id = element.attrib['id'] 79 | 80 | log.d(TAG, "Adding new public resource value %s" % (res_id)) 81 | self.public_dict[int(res_id, 16)] = [element.attrib['name'], 82 | element.attrib['type']] 83 | 84 | if element.attrib['type'] == "string": 85 | self.has_strings = True 86 | 87 | except KeyError: 88 | log.w(TAG, "KeyError iterating public.xml, skipping!") 89 | 90 | # Clear the element from memory 91 | element.clear() 92 | 93 | return 0 94 | 95 | def do_parse_strings(self, project_dir): 96 | 97 | """Parse strings.xml file""" 98 | 99 | string_file_path = "%s/%s" % (project_dir, STRING_FILE_PATH) 100 | 101 | if not os.path.isfile(string_file_path): 102 | log.e(TAG, "'%s' public resource file not found!") 103 | return -1 104 | 105 | log.i(TAG, "Parsing strings.xml...") 106 | 107 | for _, element in etree.iterparse(string_file_path): 108 | 109 | if element.tag == "string": 110 | 111 | try: 112 | string_name = element.attrib['name'] 113 | for pub in self.public_dict.keys(): 114 | if (self.public_dict[pub][0] == string_name and 115 | self.public_dict[pub][1] == "string"): 116 | log.d(TAG, "Adding string details to %s (0x%08x)" 117 | % (string_name, pub)) 118 | self.public_dict[pub].append(element.text) 119 | 120 | except KeyError: 121 | log.w(TAG, "KeyError iterating strings.xml, skipping!") 122 | 123 | # Clear the element from memory 124 | element.clear() 125 | 126 | return 0 127 | 128 | def do_changes(self, project_dir): 129 | 130 | """Do smali changes""" 131 | 132 | smali_files_dir = "%s/%s" % (project_dir, SMALI_FILES_PATH) 133 | 134 | if not os.path.isdir(smali_files_dir): 135 | log.e(TAG, "Smali files directory does not exist!") 136 | return -2 137 | 138 | log.i(TAG, "Making modifications to files in smali/*...") 139 | 140 | for root, dirs, files in os.walk(smali_files_dir): 141 | 142 | for filename in files: 143 | file_path = os.path.join(root, filename) 144 | 145 | # Smali only files and no R.smali junk 146 | if (re.search(".*\.smali$", file_path) 147 | and file_path != 'R.smali'): 148 | 149 | self.change_file(file_path) 150 | 151 | def change_file(self, file_path): 152 | 153 | """Perform change to a smali file""" 154 | 155 | data = '' 156 | file_modded = False 157 | 158 | for line in re.split("\n", open(file_path).read()): 159 | 160 | # First do "const-string" instances 161 | if re.search(CONST_REGEX, line): 162 | 163 | # Get the actual value 164 | res_value = line[line.find(",") + 2:len(line)] 165 | 166 | # To save space, some are const/high16 167 | if line.find("high16") != -1: 168 | res_value_int = int(res_value + "0000", 16) 169 | else: 170 | res_value_int = int(res_value, 16) 171 | 172 | # Determine if this is a value in our list 173 | if res_value_int in self.public_dict.keys(): 174 | log.d(TAG, "We found a resource identifier: %s [%s]" 175 | % (res_value, file_path)) 176 | 177 | line_len = len(line) 178 | line += ("\t#Public value '%s' (type=%s)" 179 | % (self.public_dict[res_value_int][0], 180 | self.public_dict[res_value_int][1])) 181 | 182 | if len(self.public_dict[res_value_int]) == 3: 183 | string_value = self.public_dict[res_value_int][2] 184 | 185 | if string_value is None: 186 | log.w(TAG, "String value for value %s not found!" 187 | % res_value) 188 | continue 189 | 190 | formatted_string_value = (string_value[0:DISPLAY_LEN] 191 | + ("..." if len(string_value) > DISPLAY_LEN 192 | else "")) 193 | 194 | 195 | line += ("\n%s\t#%s = '%s'" 196 | % (" " * line_len, 197 | self.public_dict[res_value_int][0], 198 | formatted_string_value)) 199 | 200 | file_modded = True 201 | 202 | # Now check for "packed-switch" instances 203 | elif re.search(PACKED_SWITCH_REGEX, line): 204 | 205 | res_value = line[line.find("0x"): len(line)] 206 | res_value_int = int(res_value, 16) 207 | 208 | # Determine if this is a value in our list 209 | if res_value_int in self.public_dict.keys(): 210 | log.d(TAG, "Found packed-switch resource identifier: %s" 211 | % res_value) 212 | 213 | line += ("\t#Public value '%s' (type=%s)" 214 | % (self.public_dict[res_value_int][0], 215 | self.public_dict[res_value_int][1])) 216 | 217 | file_modded = True 218 | 219 | # Add the new data to a buffer 220 | data += line + "\n" 221 | 222 | # Write the changes out. 223 | if file_modded == True: 224 | output = open(file_path, 'w') 225 | output.write(data.encode('utf-8')) 226 | log.d(TAG, "Changes applied to file '%s'" % (file_path)) 227 | output.close() 228 | else: 229 | log.d(TAG, "No changes to file '%s'" % (file_path)) 230 | 231 | return 0 232 | 233 | def do_fix(self, args): 234 | 235 | """Do fix resources""" 236 | 237 | arg_app_dir = args.pop() 238 | 239 | if not os.path.isdir(arg_app_dir): 240 | log.e(TAG, "Application directory '%s' doesnt exist!" 241 | % arg_app_dir) 242 | return -1 243 | 244 | self.has_strings = False 245 | self.public_dict = {} 246 | 247 | # First do the public resources 248 | if self.do_parse_public(arg_app_dir) != 0: 249 | return -2 250 | 251 | # Next do strings 252 | if self.has_strings: 253 | if self.do_parse_strings(arg_app_dir) != 0: 254 | return -3 255 | 256 | # Do actual changes 257 | if self.do_changes(arg_app_dir) != 0: 258 | return -4 259 | 260 | log.i(TAG, "Process complete!") 261 | 262 | 263 | def execute(self, args): 264 | 265 | """Main class execution""" 266 | 267 | if len(args) != 1: 268 | self.usage() 269 | return -1 270 | 271 | return self.do_fix(args) 272 | -------------------------------------------------------------------------------- /manifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | --------------------------------------------------------------------------------