diff --git a/agent/src/android/filesystem.ts b/agent/src/android/filesystem.ts index aaf0eaf..40c78e1 100644 --- a/agent/src/android/filesystem.ts +++ b/agent/src/android/filesystem.ts @@ -1,4 +1,5 @@ -import * as fs from "fs"; +import * as fs from "frida-fs"; +import { Buffer } from "buffer"; import { hexStringToBytes } from "../lib/helpers.js"; import { IAndroidFilesystem } from "./lib/interfaces.js"; import { @@ -78,7 +79,9 @@ export const pwd = (): Promise => { }; // heavy lifting is done in frida-fs here. -export const readFile = (path: string): Buffer => { +export const readFile = (path: string): string | Buffer => { + if (fs.statSync(path).size == 0) + return Buffer.alloc(0); return fs.readFileSync(path); }; diff --git a/agent/src/generic/http.ts b/agent/src/generic/http.ts index 8bc7ff4..9ffd595 100644 --- a/agent/src/generic/http.ts +++ b/agent/src/generic/http.ts @@ -1,4 +1,4 @@ -import * as fs from "fs"; +import * as fs from "frida-fs"; import * as httpLib from "http"; import * as url from "url"; import { colors as c } from "../lib/color.js"; @@ -11,7 +11,7 @@ const log = (m: string): void => { c.log(`[http server] ${m}`); }; -const dirListingHTML = (p: string): string => { +const dirListingHTML = (pwd: string, path: string): string => { let h = ` @@ -22,8 +22,15 @@ const dirListingHTML = (p: string): string => { `; h = h.replace(`{file_listing}`, () => { - return fs.readdirSync(p).map((f) => { - return `${f}`; + return fs.list(pwd + path).map((f) => { + // Add a slash at the end if it is a directory. + var fname = f.name + (f.type == 4 ? '/' : ''); + + if (path !== '/') { + return `${fname}`; + } else { + return `${fname}`; + } }).join("
"); }); @@ -49,16 +56,40 @@ export const start = (pwd: string, port: number = 9000): void => { log(`${c.redBright('Missing URL or request method.')}`); return; } - - const parsedUrl = new URL(req.url); - if (parsedUrl.pathname === "/") { - res.end(dirListingHTML(pwd)); - return; - } + try { + const parsedUrl = url.parse(req.url); + const fileLocation = pwd + decodeURIComponent(parsedUrl.path); + + if (fs.statSync(fileLocation).isDirectory()) { + res.end(dirListingHTML(pwd, decodeURIComponent(parsedUrl.path))); + return; + } - res.setHeader("Content-type", "application/octet-stream"); - res.end(fs.readFileSync(pwd + parsedUrl.pathname)); + res.setHeader("Content-type", "application/octet-stream"); + + // Check that we are not reading an empty file + if (fs.statSync(fileLocation).size !== 0) { + const file = fs.readFileSync(fileLocation); + res.write(file, 'utf-8') + } + res.end(); + + } catch (error) { + if (error instanceof Error && error.message == "No such file or directory") { + res.statusCode = 404; + res.end("File not found") + } else { + if (error instanceof Error) { + log(c.redBright(`${error.stack}`)); + } else { + log(c.redBright(`${error}`)); + } + + res.statusCode = 500; + res.end("Internal Server Error") + } + } }); httpServer.listen(port); @@ -75,12 +106,12 @@ export const stop = (): void => { httpServer.close() .once("close", () => { log(c.blackBright(`Server closed.`)); - // httpServer = undefined; + httpServer = undefined; }); }; export const status = (): void => { - if (httpServer.listening) { + if (httpServer && httpServer.listening) { log(`Server is running on port ` + `${c.greenBright(listenPort.toString())} serving ${c.greenBright(servePath)}`); return; diff --git a/agent/src/ios/binary.ts b/agent/src/ios/binary.ts index 4939754..7a0fd8f 100644 --- a/agent/src/ios/binary.ts +++ b/agent/src/ios/binary.ts @@ -31,6 +31,9 @@ export const info = (): IBinaryModuleDictionary => { const imports: Set = new Set(a.enumerateImports().map((i) => i.name)); const fb = iosfilesystem.readFile(a.path); + if (typeof(fb) == 'string') { + return; + } try { const exe = macho.parse(fb); diff --git a/agent/src/ios/filesystem.ts b/agent/src/ios/filesystem.ts index cf10b07..deaf562 100644 --- a/agent/src/ios/filesystem.ts +++ b/agent/src/ios/filesystem.ts @@ -1,4 +1,5 @@ -import * as fs from "fs"; +import * as fs from "frida-fs"; +import { Buffer } from "buffer"; import { hexStringToBytes } from "../lib/helpers.js"; import { getNSFileManager } from "./lib/helpers.js"; import { @@ -87,7 +88,9 @@ export const pwd = (): string => { }; // heavy lifting is done in frida-fs here. -export const readFile = (path: string): Buffer => { +export const readFile = (path: string): string | Buffer => { + if (fs.statSync(path).size == 0) + return Buffer.alloc(0); return fs.readFileSync(path); }; diff --git a/agent/src/ios/keychain.ts b/agent/src/ios/keychain.ts index d408696..e3d797f 100644 --- a/agent/src/ios/keychain.ts +++ b/agent/src/ios/keychain.ts @@ -111,9 +111,7 @@ const enumerateKeychain = (): IKeychainData[] => { }); const keyChainData: IKeychainData[] = []; - keyChainData.concat(...itemClassResults).filter((n) => n !== undefined); - - return keyChainData; + return keyChainData.concat(...itemClassResults).filter((n) => n !== undefined); }; // print raw entries using some Frida magic diff --git a/agent/src/rpc/ios.ts b/agent/src/rpc/ios.ts index 47a8eb8..9e7836e 100644 --- a/agent/src/rpc/ios.ts +++ b/agent/src/rpc/ios.ts @@ -40,7 +40,7 @@ export const ios = { // ios filesystem iosFileCwd: (): string => iosfilesystem.pwd(), iosFileDelete: (path: string): boolean => iosfilesystem.deleteFile(path), - iosFileDownload: (path: string): Buffer => iosfilesystem.readFile(path), + iosFileDownload: (path: string): string | Buffer => iosfilesystem.readFile(path), iosFileExists: (path: string): boolean => iosfilesystem.exists(path), iosFileLs: (path: string): IIosFileSystem => iosfilesystem.ls(path), iosFilePathIsFile: (path: string): boolean => iosfilesystem.pathIsFile(path), diff --git a/objection/commands/filemanager.py b/objection/commands/filemanager.py index 89f9956..ee0a2bf 100644 --- a/objection/commands/filemanager.py +++ b/objection/commands/filemanager.py @@ -16,6 +16,17 @@ _ls_cache = {} +def _should_download_folder(args: list) -> bool: + """ + Checks if --json is in the list of tokens received from the command line. + + :param args: + :return: + """ + + return len(args) > 0 and '--folder' in args + + def cd(args: list) -> None: """ Change the current working directory of the device. @@ -44,7 +55,9 @@ def cd(args: list) -> None: return # moving one directory back - if path == '..': + device_path_separator = device_state.platform.path_separator + + if path == '..' or path == '..'+device_path_separator: split_path = os.path.split(current_dir) @@ -66,6 +79,10 @@ def cd(args: list) -> None: # assume the path does not exist by default does_exist = False + # normalise path to remove '../' + if '..'+device_path_separator in path: + path = os.path.normpath(path).replace('\\', device_path_separator) + # check for existence based on the runtime if device_state.platform == Ios: does_exist = _path_exists_ios(path) @@ -89,7 +106,13 @@ def cd(args: list) -> None: # see if its legit. else: - proposed_path = device_state.platform.path_separator.join([current_dir, path]) + proposed_path = device_path_separator.join([current_dir, path]) + + # normalise path to remove '../' + if '..'+device_path_separator in proposed_path: + proposed_path = os.path.normpath(proposed_path).replace('\\', device_path_separator) + if proposed_path == '//': + return # assume the proposed_path does not exist by default does_exist = False @@ -393,14 +416,16 @@ def download(args: list) -> None: source = args[0] destination = args[1] if len(args) > 1 else os.path.basename(source) + should_download_folder = _should_download_folder(args) + if device_state.platform == Ios: - _download_ios(source, destination) + _download_ios(source, destination, should_download_folder) if device_state.platform == Android: - _download_android(source, destination) + _download_android(source, destination, should_download_folder) -def _download_ios(path: str, destination: str) -> None: +def _download_ios(path: str, destination: str, should_download_folder: bool, path_root: bool = True) -> None: """ Download a file from an iOS filesystem and store it locally. @@ -416,27 +441,55 @@ def _download_ios(path: str, destination: str) -> None: api = state_connection.get_api() - click.secho('Downloading {0} to {1}'.format(path, destination), fg='green', dim=True) + if path_root: + click.secho('Downloading {0} to {1}'.format(path, destination), fg='green', dim=True) if not api.ios_file_readable(path): click.secho('Unable to download file. File is not readable.', fg='red') return if not api.ios_file_path_is_file(path): - click.secho('Unable to download file. Target path is not a file.', fg='yellow') + if not should_download_folder: + click.secho('To download folders, specify --folder.', fg='yellow') + return + + if os.path.exists(destination): + click.secho('The target path already exists.', fg='yellow') + return + + os.makedirs(destination) + + if path_root: + if not click.confirm('Do you want to download the full directory?', default=True): + click.secho('Download aborted.', fg='yellow') + return + click.secho('Downloading directory recursively...', fg='green') + + data = api.ios_file_ls(path) + for name, _ in data['files'].items(): + sub_path = device_state.platform.path_separator.join([path, name]) + sub_destination = os.path.join(destination, name) + + _download_ios(sub_path, sub_destination, True, False) + if path_root: + click.secho('Recursive download finished.', fg='green') + return - click.secho('Streaming file from device...', dim=True) + if path_root: + click.secho('Streaming file from device...', dim=True) file_data = api.ios_file_download(path) - click.secho('Writing bytes to destination...', dim=True) + if path_root: + click.secho('Writing bytes to destination...', dim=True) + with open(destination, 'wb') as fh: fh.write(bytearray(file_data['data'])) click.secho('Successfully downloaded {0} to {1}'.format(path, destination), bold=True) -def _download_android(path: str, destination: str) -> None: +def _download_android(path: str, destination: str, should_download_folder: bool, path_root: bool = True) -> None: """ Download a file from the Android filesystem and store it locally. @@ -452,20 +505,47 @@ def _download_android(path: str, destination: str) -> None: api = state_connection.get_api() - click.secho('Downloading {0} to {1}'.format(path, destination), fg='green', dim=True) + if path_root: + click.secho('Downloading {0} to {1}'.format(path, destination), fg='green', dim=True) if not api.android_file_readable(path): click.secho('Unable to download file. Target path is not readable.', fg='red') return if not api.android_file_path_is_file(path): - click.secho('Unable to download file. Target path is not a file.', fg='yellow') + if not should_download_folder: + click.secho('To download folders, specify --folder.', fg='yellow') + return + + if os.path.exists(destination): + click.secho('The target path already exists.', fg='yellow') + return + + os.makedirs(destination) + + if path_root: + if not click.confirm('Do you want to download the full directory?', default=True): + click.secho('Download aborted.', fg='yellow') + return + click.secho('Downloading directory recursively...', fg='green') + + data = api.android_file_ls(path) + for name, _ in data['files'].items(): + sub_path = device_state.platform.path_separator.join([path, name]) + sub_destination = os.path.join(destination, name) + + _download_android(sub_path, sub_destination, True, False) + if path_root: + click.secho('Recursive download finished.', fg='green') return - click.secho('Streaming file from device...', dim=True) + if path_root: + click.secho('Streaming file from device...', dim=True) file_data = api.android_file_download(path) - click.secho('Writing bytes to destination...', dim=True) + if path_root: + click.secho('Writing bytes to destination...', dim=True) + with open(destination, 'wb') as fh: fh.write(bytearray(file_data['data'])) @@ -785,7 +865,10 @@ def list_folders_in_current_fm_directory() -> dict: file_name, file_type = entry if file_type == 'directory': - resp[file_name] = file_name + if ' ' in file_name: + resp[f"'{file_name}'"] = file_name + else: + resp[file_name] = file_name return resp @@ -816,6 +899,41 @@ def list_files_in_current_fm_directory() -> dict: file_name, file_type = entry if file_type == 'file': - resp[file_name] = file_name + if ' ' in file_name: + resp[f"'{file_name}'"] = file_name + else: + resp[file_name] = file_name + + return resp + + +def list_content_in_current_fm_directory() -> dict: + """ + Return folders and files in the current working directory of the + Frida attached device. + """ + + resp = {} + + # check for existence based on the runtime + if device_state.platform == Ios: + response = _get_short_ios_listing() + + elif device_state.platform == Android: + response = _get_short_android_listing() + + # looks like we landed in an unknown runtime. + # just return. + else: + return resp + + # loop the response to get entries. + for entry in response: + name, _ = entry + + if ' ' in name: + resp[f"'{name}'"] = name + else: + resp[name] = name return resp diff --git a/objection/console/commands.py b/objection/console/commands.py index 2702a1b..14ee58f 100644 --- a/objection/console/commands.py +++ b/objection/console/commands.py @@ -119,7 +119,7 @@ 'exec': filemanager.pwd_print, }, - 'file': { + 'filesystem': { 'meta': 'Work with files on the remote filesystem', 'commands': { 'cat': { @@ -132,8 +132,9 @@ 'exec': filemanager.upload }, 'download': { - 'meta': 'Download a file', - 'dynamic': filemanager.list_files_in_current_fm_directory, + 'meta': 'Download a file or folder', + 'flags': ['--folder'], + 'dynamic': filemanager.list_content_in_current_fm_directory, 'exec': filemanager.download }, diff --git a/objection/utils/agent.py b/objection/utils/agent.py index cf2a4ed..64ecc7b 100644 --- a/objection/utils/agent.py +++ b/objection/utils/agent.py @@ -216,6 +216,8 @@ def set_target_pid(self): elif self.config.spawn: if self.config.uid is not None: + if self.device.query_system_parameters()['os']['id'] != 'android': + raise Exception('--uid flag can only be used on Android.') self.pid = self.device.spawn(self.config.name, uid=int(self.config.uid)) else: self.pid = self.device.spawn(self.config.name) @@ -245,9 +247,10 @@ def attach(self): raise Exception('A PID needs to be set before attach()') if self.config.uid is None: + debug_print(f'Attaching to PID: {self.pid}') self.session = self.device.attach(self.pid) else: - self.session = self.device.attach(self.pid, uid=self.config.uid) + self.session = self.device.attach(self.pid) self.session.on('detached', self.handlers.session_on_detached) diff --git a/requirements.txt b/requirements.txt index 1f678cf..2b309d3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,3 +9,4 @@ requests Flask>=3.0.0 Pygments>=2.0.0 litecli>=1.3.0 +setuptools>=70.0.0