5

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

  • Xcode should support it out of the box, for apps you compiled yourself. And [this](https://stackoverflow.com/q/11607613/2302862) worked some 9 years ago. I'm sure there's some equivalent that can be hacked up today. printf is in no way "secure" against people trying to read its output. – Siguza Sep 18 '21 at 00:33
  • I tried to make a dumb app to test this and the logs were only available when using `NSLog`. No logs appeared when using `print` and I believe this is because of the way they both work: apparently `NSLog` stores information in some system-specific file while `print` just prints the text to the standart stdout. So, I think to get access to what's in the `print` you would need some sort of stdout redirect or something like that, but I haven't found anything about it. – Izabella Melo Sep 18 '21 at 00:52
  • print writes to "stdout". It's possible to add code in the app, that redirects [stdout and stderr](https://en.wikipedia.org/wiki/Standard_streams) into a file or into os_log. However, if you are the owner of the app, I would just follow matt's answer. – CouchDeveloper Sep 18 '21 at 11:18

2 Answers2

3

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()
        }
    }
}
Robert
  • 39,162
  • 17
  • 99
  • 152
1

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
    }
}
CouchDeveloper
  • 18,174
  • 3
  • 45
  • 67