How to capture print statements from iOS app installed on iOS device? - ios

I'm reading about some good practices for developing iOS apps and looking at the possibility of monitoring logs of an iOS app installed from App Store using Console.app. So, I was testing here, but I noticed that print statements didn't show up in Console.app, only NSLog does. My question is: is there any way that is possible to see logs that are made with print commands within iOS apps installed on a device? With Frida, Console.app or any other means?
If there is no other method, does it mean that print commands are more secure than NSLog? This seems very counterintuitive to me 🤔

print statement in iOS apps are not logged to one the [persistent] logging systems on iOS, therefore you can not access the output of an app via print statements if they had occur in the past.
By default you can only seem the output of print commands in XCode output panel. However the print commands themselves are always included in the debug and release builds and are therefore executed. Just the output of the print statements is discarded if no XCode is connected to retrieve it.
I tested this by building the following SwiftUI test app (see the end of this answer), made sure the Archive profile is set to RELEASE and the archived the project, to build an IPA file.
The IPA file was then analyzed in IdaPro to see the actual ARM assembler code.
And in all tests using different options (e.g. "Rebuild from Bitcode" (de)activated) the code was always there.
Therefore if you attach Frida to an app you can e.g. hook the print method print(_:separator:terminator:) to retrieve all output that would otherwise be discarded.
struct ContentView: View {
#State var number : Int = 1
var body: some View {
VStack {
Button(" Print ") {
print("print test abcdefgh number %d", number)
}.padding()
Button(" os_log ") {
os_log("os_log test abcdefgh number %d", number)
}.padding()
Button("randomize") {
self.number = Int.random(in: 1...11111)
}.padding()
}
}
}

If, and only if, you want to use print and printf in your app to go to a file or whatever file descriptor:
import SwiftUI
import Darwin
import os.log
extension OSLog {
private static var subsystem = Bundle.main.bundleIdentifier!
static let `default` = OSLog(subsystem: subsystem, category: "default")
}
extension TestApp {
func subscribeFileToStderrAndStdoutIfNotAttachedToDebugger() {
if isatty(STDERR_FILENO) != 1 {
let documentsUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let logfileUrl = documentsUrl.appendingPathComponent("out.log")
logfileUrl.withUnsafeFileSystemRepresentation { path in
guard let path = path else {
return
}
print("redirect stdout and stderr to: \(String(cString: path))")
let file = fopen(path, "a")
assert(file != nil, String(cString: strerror(errno)))
let fd = fileno(file)
assert(fd >= 0, String(cString: strerror(errno)))
let result1 = dup2(fd, STDERR_FILENO)
assert(result1 >= 0, String(cString: strerror(errno)))
let result2 = dup2(fd, STDOUT_FILENO)
assert(result2 >= 0, String(cString: strerror(errno)))
}
}
}
}
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
subscribeFileToStderrAndStdoutIfNotAttachedToDebugger()
return true
}
}

Related

How do I view logs from a REMOTE iOS device using the unified logging system?

First, it's important to know that, OSLogStore didn't work in iOS as recently as 4 months ago. Since that's so recent and documentation is so sparse, what may have been true a year ago may not be true today.
Here's some context to my question (pretty much stolen from this reddit post):
I have about a 1000 users for an open source app I developed and every now and then users will report some odd behavior.
The app uses [Logger] to log important events ... Is there a remote logging service that can be used so that I can get this info and resolve issues for users?
I only care if the unified logging system has a solution to this problem. Let's assume I'm able to converse with the user with the "odd behavior" and they are non-technical.
I've been given some hints that OSLogStore may be a way to get remote logs, but the official documentation is so sparse, I can't tell. Specifically, that init(url:) seems interesting, but maybe it only accepts file:// protocols or something.
The logging documentation says it can be used "When you are unable to attach a debugger to the app, such as when you’re diagnosing problems on a user’s machine," but nowhere does it say how to do this.
After reading the discussion in this post, I wanted to make a simple prototype to see whether it is possible to get the logs from the phone remotely. To accomplish this, I modified Steipete's code a little: I removed some code I didn't need and added a button to trigger the sending of the logs, named "Send logs to the developers".
Then, I created a codable struct called SendableLog that converted the OSLogEntryLog, making it possible to convert it to JSON. After getting the logs using getEntries() and mapping them to this new type, I converted the logs to JSON and sent an HTTP POST request to an endpoint (as suggested by #DanielKaplan) on a simple Python server I was running on my MacBook.
The Swift code (iOS 15 application):
//
// ContentView.swift
// OSLogStoreTesting
//
// Created by bbruns on 23/12/2021.
// Based on Peter Steinberger (23.08.20): https://github.com/steipete/OSLogTest/blob/master/LoggingTest/ContentView.swift.
//
import SwiftUI
import OSLog
import Combine
let subsystem = "com.bbruns.OSLogStoreTesting"
func getLogEntries() throws -> [OSLogEntryLog] {
let logStore = try OSLogStore(scope: .currentProcessIdentifier)
let oneHourAgo = logStore.position(date: Date().addingTimeInterval(-3600))
let allEntries = try logStore.getEntries(at: oneHourAgo)
return allEntries
.compactMap { $0 as? OSLogEntryLog }
.filter { $0.subsystem == subsystem }
}
struct SendableLog: Codable {
let level: Int
let date, subsystem, category, composedMessage: String
}
func sendLogs() {
let logs = try! getLogEntries()
let sendLogs: [SendableLog] = logs.map({ SendableLog(level: $0.level.rawValue,
date: "\($0.date)",
subsystem: $0.subsystem,
category: $0.category,
composedMessage: $0.composedMessage) })
// Convert object to JSON
let jsonData = try? JSONEncoder().encode(sendLogs)
// Send to my API
let url = URL(string: "http://x.x.x.x:8000")! // IP address and port of Python server
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = jsonData
let session = URLSession.shared
let task = session.dataTask(with: request) { (data, response, error) in
if let httpResponse = response as? HTTPURLResponse {
print(httpResponse.statusCode)
}
}
task.resume()
}
struct ContentView: View {
let logger = Logger(subsystem: subsystem, category: "main")
var logLevels = ["Default", "Info", "Debug", "Error", "Fault"]
#State private var selectedLogLevel = 0
init() {
logger.log("SwiftUI is initializing the main ContentView")
}
var body: some View {
return VStack {
Text("This is a sample project to test the new logging features of iOS 15.")
.padding()
Picker(selection: $selectedLogLevel, label: Text("Choose Log Level")) {
ForEach(0 ..< logLevels.count) {
Text(self.logLevels[$0])
}
}.frame(width: 400, height: 150, alignment: .center)
Button(action: {
switch(selectedLogLevel) {
case 0:
logger.log("Default log message")
case 1:
logger.info("Info log message")
case 2:
logger.debug("Debug log message")
case 3:
logger.error("Error log message")
default: // 4
logger.fault("Fault log message")
}
}) {
Text("Log with Log Level \(logLevels[selectedLogLevel])")
}.padding()
Button(action: sendLogs) {
Text("Send logs to developers")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I have this simple Python HTTP server listening to incoming POST requests, the IP address was set to the local IP address of my MacBook. This matches the IP address in the Swift code above.
from http.server import BaseHTTPRequestHandler, HTTPServer
import json
hostName = "x.x.x.x" # IP address of server
serverPort = 8000
class MyServer(BaseHTTPRequestHandler):
def _set_headers(self):
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
def do_HEAD(self):
self._set_headers()
def do_POST(self):
self._set_headers()
print("Received POST")
self.data_string = self.rfile.read(int(self.headers['Content-Length']))
self.send_response(200)
self.end_headers()
data = json.loads(self.data_string)
print(f"JSON received: \n\n {data}")
if __name__ == "__main__":
webServer = HTTPServer((hostName, serverPort), MyServer)
print("Server started http://%s:%s" % (hostName, serverPort))
try:
webServer.serve_forever()
except KeyboardInterrupt:
pass
webServer.server_close()
print("Server stopped.")
When I run the app and tap the Send logs to developers button, I see the following message in my terminal:
x.x.x.x - - [23/Dec/2021 13:56:47] "POST / HTTP/1.1" 200 -
JSON received:
[{'subsystem': 'com.bbruns.OSLogStoreTesting', 'level': 3, 'composedMessage': 'SwiftUI is initializing the main ContentView', 'category': 'main', 'date': '2021-12-23 12:56:43 +0000'}]
The logs are successfully retrieved from the phone and then sent to the server.
Caveat
When I (fully) close the app and reopen it, the previous logs are gone!
When creating the log store (let logStore = try OSLogStore(scope: .currentProcessIdentifier)) the scope is set to .currentProcessIdentifier, which is the only available scope on iOS. This thread makes me believe The .system scope would include previous logs as well, but the system scope is not available on iOS.
re: #RobNapier's comment on the original post that says, “The only question is whether you can get logs off of remote devices ... and even that is pretty tricky.” I'm starting to think OSLogStore only gets local logs, but this enables you to send them anywhere, or do anything you want with them, really.
Now that OSLogStore works on iOS, you can put a button in your app labeled "Send logs to dev," where clicking it sends the logs to a custom endpoint on your server. That requires two steps:
Get the local logs.
Another part of the article you linked says:
With OSLogStore, Apple added an API to access the log archive programmatically. It allows accessing OSLogEntryLog, which contains all the log information you’ll possibly ever need. ... Let’s look at how this works:
func getLogEntries() throws -> [OSLogEntryLog] {
let subsystem = Bundle.main.bundleIdentifier!
// Open the log store.
let logStore = try OSLogStore(scope: .currentProcessIdentifier)
// Get all the logs from the last hour.
let oneHourAgo = logStore.position(date: Date().addingTimeInterval(-3600))
// Fetch log objects.
let allEntries = try logStore.getEntries(at: oneHourAgo)
// Filter the log to be relevant for our specific subsystem
// and remove other elements (signposts, etc).
return allEntries
.compactMap { $0 as? OSLogEntryLog }
.filter { $0.subsystem == subsystem }
}
Send them to a custom endpoint on your server. With that function in your code base, I think you can use it like this:
let logs = getLogEntries();
sendLogsToServer(deviceId, appVersion, ..., logs); // this is your implementation
The one part that gives me pause is #RobNapier said getting "logs off of remote devices ... is pretty tricky." That makes me think there is something I'm missing. Hopefully #RobNapier will point out the flaws in my thinking.

SWIFTUI - File Not Found error when trying to import a file from a cloud file provider like OneDrive and GoogleDrive

I have the following SwiftUI code where a simple button brings up the iOS file manager and allows the user to select a CSV file to be imported. I've found that it works well for files that are stored locally on my device but if I try to select a file from Google Drive or OneDrive it gets a URL but when I then try to retrieve the data from it, it returns an error saying that the file was not found.
After a lot of head scratching, I've found that when using the file browser if I long press to bring up the context menu and then view the info for the file (which I'm guessing may be pulling it down to the phones local cache), it will then work as expected. This is shown in the following animated gif:
I've found that once I've done that caching trick, I can access the file without issue in other apps using the same code and I've also found that I can uninstall my app and reinstall it and it continues to work.
Can anyone advise on an approach using SwiftUI where I can avoid this File Not Found error when trying to import the file from Google Drive or OneDrive?
The entire code that I've been using for testing is as follows:
import SwiftUI
struct ContentView: View {
#State private var isImporting: Bool = false
#State private var fileContentString = ""
#State var alertMsg = ""
#State var showAlert = false
func reportError(error: String) {
alertMsg = error
showAlert.toggle()
}
var body: some View {
VStack {
Button(action: { isImporting = true}, label: {
Text("Select CSV File")
})
.padding()
Text(fileContentString) //This will display the imported CSV as text in the view.
}
.padding()
.fileImporter(
isPresented: $isImporting,
allowedContentTypes: [.commaSeparatedText],
allowsMultipleSelection: false
) { result in
do {
guard let selectedFileURL: URL = try result.get().first else {
alertMsg = "ERROR: Result.get() failed"
self.reportError(error: alertMsg)
return
}
print("selectedFileURL is \(selectedFileURL)")
if selectedFileURL.startAccessingSecurityScopedResource() {
//print("startAccessingSecurityScopedResource passed")
do {
print("Getting Data from URL...")
let inputData = try Data(contentsOf: selectedFileURL)
print("Converting data to string...")
let inputString = String(decoding: inputData, as: UTF8.self)
print(inputString)
fileContentString = inputString
}
catch {
alertMsg = "ERROR: \(error.localizedDescription)"
self.reportError(error: alertMsg)
print(alertMsg)
}
//defer { selectedFileURL.stopAccessingSecurityScopedResource() }
} else {
// Handle denied access
alertMsg = "ERROR: Unable to read file contents - Access Denied"
self.reportError(error: alertMsg)
print(alertMsg)
}
} catch {
// Handle failure.
alertMsg = "ERROR: Unable to read file contents - \(error.localizedDescription)"
self.reportError(error: alertMsg)
print(alertMsg)
}
}
.alert(isPresented: $showAlert, content: {
Alert(title: Text("Message"), message: Text(alertMsg), dismissButton: .destructive(Text("OK"), action: {
}))
})
}
}
The console log output is as follows:
selectedFileURL is file:///private/var/mobile/Containers/Shared/AppGroup/8F147702-8630-423B-9DA0-AE49667748EB/File%20Provider%20Storage/84645546/1aTSCPGxY3HzILlCIFlMRtx4eEWDZ2JAq/example4.csv
Getting Data from URL...
ERROR: The file “example4.csv” couldn’t be opened because there is no such file.
selectedFileURL is file:///private/var/mobile/Containers/Shared/AppGroup/8F147702-8630-423B-9DA0-AE49667748EB/File%20Provider%20Storage/84645546/1aTSCPGxY3HzILlCIFlMRtx4eEWDZ2JAq/example4.csv
Getting Data from URL...
Converting data to string...
First Name,Last Name
Luke,Skywalker
Darth,Vader
My testing has been done on a physical iPhone 12 Pro Max running iOS 14.2 and a physical iPad Air 2 running iPadOS 14.4.
I found an answer to my issue. The solution was to use a NSFileCoordinator() to force the file to be downloaded.
With the code below, if I access a file in cloud storage that hasn't been previously downloaded to the local device it will print "FILE NOT AVAILABLE" but it will now just download the file rather than throwing a file not found error.
Ideally I would like to be able to download just the file property metadata first to check how big the file is and then decide if I want to download the full file. The NSFileCoordinator has a metadata only option but I haven't worked out how to retrieve and interpret the results from that. This will do for now...
if selectedFileURL.startAccessingSecurityScopedResource() {
let fileManager = FileManager.default
if fileManager.fileExists(atPath: selectedFileURL.path) {
print("FILE AVAILABLE")
} else {
print("FILE NOT AVAILABLE")
}
var error: NSError?
NSFileCoordinator().coordinate(
readingItemAt: selectedFileURL, options: .forUploading, error: &error) { url in
print("coordinated URL", url)
let coordinatedURL = url
isShowingFileDetails = false
importedFileURL = selectedFileURL
do {
let resources = try selectedFileURL.resourceValues(forKeys:[.fileSizeKey])
let fileSize = resources.fileSize!
print ("File Size is \(fileSize)")
} catch {
print("Error: \(error)")
}
}
do {
print("Getting Data from URL...")
let inputData = try Data(contentsOf: selectedFileURL)
print("Do stuff with file....")
}
}

Flutter iOS File Sharing - Cannot Open Shared File

I am building a flutter app for iOS. I have created a file type that my app and another app can share back and forth. I am using the flutter receive_sharing_intent library to achieve this. To test it, I made all the necessary changes to my info.plist file to handle my custom file type, I placed an example file of that file type in the downloads folder of my device (testing on ipad), and I click on it from there to open it up in my app. The OS knows that my app can handle it. So, my app opens, it is receives the path of the shared file, but my app can't open the file. Here is the code in my main.dart that is handling reception of the file:
StreamSubscription _intentDataStreamSubscription =
ReceiveSharingIntent.getTextStream().listen((String value) {
setState(() {
try{
//This was added to test and make sure the directory actually exists.
//The value for the directory path was added after I ran it once to capture where the shared file was coming from.
Directory dir = Directory("/private/var/mobile/Containers/Shared/AppGroup/7DD4B316-3D73-4339-9B11-7516DE52F6FC/File Provider Storage/Downloads/");
bool dirExists = dir.existsSync();
var files = dir.listSync();
File drawingFile = File.fromUri(Uri.parse(value));
bool fileExists = drawingFile.existsSync();
var contents = drawingFile.readAsStringSync();
DrawingCanvas canvas = DrawingCanvas.fromFileContents(contents);
DrawingCanvasBloc canvasBloc = DrawingCanvasBloc(canvas);
Navigator.of(context).push(MaterialPageRoute(builder: (context) => CanvasScreen(canvasBloc)));
}
catch(e){
log(e.toString());
}
});
}, onError: (err) {
print("getLinkStream error: $err");
});
Scenario: I run the application. I go into my downloads folder in files on the ipad. I select my example.fbg file located there (my custom file type). My app opens up.
In the above code it blows up when I try to list the contents of the directory. I put this in here (after previously catching the directory path and hard coding it) to test to make sure I was even getting the right location. dirExists is true but I can't list the files in it. The error I get is:
FileSystemException: Directory listing failed, path = '/private/var/mobile/Containers/Shared/AppGroup/7DD4B316-3D73-4339-9B11-7516DE52F6FC/File Provider Storage/Downloads/' (OS Error: Operation not permitted, errno = 1)
If I take that line out and continue down to the opening of the file (readAsStringSync) I get:
FileSystemException: Cannot open file, path = '/private/var/mobile/Containers/Shared/AppGroup/7DD4B316-3D73-4339-9B11-7516DE52F6FC/File Provider Storage/Downloads/example.fbg' (OS Error: Operation not permitted, errno = 1)
I'm not sure why it won't let me access this file. Is there a permissions thing I'm missing? Let me know if I need to include more information in the question and I will update.
Finally found a work around and got it working using this answer. What I ended up doing was opening the file in the app delegate, saving the file to the apps documents directory, then passing that url to the Flutter application. I opened the iOS module in xcode and changed the AppDelegate.swift to the following:
import UIKit
import Flutter
#UIApplicationMain
#objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
override func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
var drawingFilename = ""
do {
let isAcccessing = url.startAccessingSecurityScopedResource()
var error: Error? = nil
let path = url.path
let string = try String(contentsOf: url)
drawingFilename = (url.path as NSString).lastPathComponent
print(drawingFilename)
let filename = getDocumentsDirectory().appendingPathComponent(drawingFilename)
do {
try string.write(to: filename, atomically: true, encoding: String.Encoding.utf8)
} catch {
// failed to write file – bad permissions, bad filename, missing permissions, or more likely it can't be converted to the encoding
}
if isAcccessing {
url.stopAccessingSecurityScopedResource()
}
if #available(iOS 9.0, *) {
return super.application(app, open: filename, options: options)
} else {
return false
}
} catch {
print("Unable to load data: \(error)")
return false
}
}
func getDocumentsDirectory() -> URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
return paths[0]
}
}
Hope this helps someone in the future.

Use AWSMobileClient without `awsconfiguration.json`in iOS

I'd like to authenticate an iOS device to use AppSync/S3 services via Cognito user pools. The AWSMobileClient provides some nice conveniences but the initialization requires that you're bundle have an awsconfiguration.json file -- which our application will define dynamically. Is there a way to configure that manually?
The current solution is to use the multi-environment workflow from the CLI.
https://aws-amplify.github.io/docs/cli/multienv?sdk=ios
Edit
If the multi-environment workflow from the Amplify team doesn't work for you, what you can do is create debug and prod versions of your config, and then create a build phase that copies the correct one based on your build settings (debug vs release, etc). This is working extremely well for one of my projects.
#export; #Prints list of all xcode variables with values
printf "$CONFIGURATION\n";
if [ "$CONFIGURATION" = "Debug" ]; then
printf "creating debug configuration";
cp -r "$PROJECT_DIR/awsconfiguration-debug.json" "$BUILT_PRODUCTS_DIR/$PRODUCT_NAME.app/awsconfiguration.json"
else
printf "creating production configuration";
cp -r "$PROJECT_DIR/awsconfiguration-prod.json" "$BUILT_PRODUCTS_DIR/$PRODUCT_NAME.app/awsconfiguration.json"
fi
As of AWS iOS SDK 2.11.0 (9th September 2019) it is now possible to configure without an awsconfiguration.json file.
It's even documented in the amplify documentation here
See also my answer to a related question
Here's a specific solution:
extension AWSMobileClient {
convenience init?(configuration url: URL) {
guard let data = try? Data(contentsOf: url) else { return nil }
guard let dict = try? JSONSerialization.jsonObject(with: data, options: []) as? [String : Any] else { return nil }
self.init(configuration: dict)
}
convenience init?(configuration name: String) {
guard let url = Bundle.main.url(forResource: name, withExtension: "json") else {
return nil
}
print("INITIALIZING AWSMobileClient [\(name)]")
self.init(configuration: url)
}
}
To use it, you can have as many different awsconfiguration-XXX.json files as you need, and at runtime you initialize with the one you want by:
let mobileClient = AWSMobileClient(configuration: "awsconfiguration-XXX")
mobileClient.initialize { (userState, error) in ... }

iOS HttpUrlResponse Header Field Case Sensitivity

When attempting to get the headers from an HttpUrlResponse, I am finding the iOS simulator is case-insensitive and a real device is case-sensitive.
The web service returns an HTTP header "Grandmas-Cookies: XXXX"
When the header key has uppercase letters:
urlResponse.response.allHeaderFields["Grandmas-Cookies"] as? String
The simulator does NOT find the key.
A real device sees the key.
When the header key has all lowercase letters:
urlResponse.response.allHeaderFields["grandmas-cookies"] as? String
The simulator does find the key.
A real device does NOT see the key.
Is there a setting I can make to the simulator to behave similarly to the real device? Changing the HTTP headers in the web service to lowercase is not desirable at this point but it is strange this started occurring only recently (yeah it's one of those fun times).
Edit:
#Adam I found a better way to ensure that this isn't an issue.
I created this function that makes the check case insensitive.
func find(header: String) -> String? {
let keyValues = allHeaderFields.map { (String(describing: $0.key).lowercased(), String(describing: $0.value)) }
if let headerValue = keyValues.filter({ $0.0 == header.lowercased() }).first {
return headerValue.1
}
return nil
}
The below may still be useful for some people.
To solve this issue I created a struct. Inside the struct I created a static variable grandmasCookies that can now be referenced from anywhere within your app. This returns the upper case
Grandmas-Cookies
when you are running on a phone device.
This returns lowercase
grandmas-cookies
when you are running in a simulator on a device such as a MacBook Pro.
struct Platform {
static let grandmasCookies: String = {
var xtoken = "Grandmas-Cookies"
#if arch(i386) || arch(x86_64)
xtoken = "grandmas-cookies"
#endif
return xtoken
}()
static let isSimulator: Bool = {
var isSim = false
#if arch(i386) || arch(x86_64)
isSim = true
#endif
return isSim
}()
}
I created a second convenience variable isSimulator which returns true when running from a simulator and false when running on a phone device.
I adapted code from this StackOverflow post to make a solution that works for your scenario and one that I faced as well.

Resources