Blog

bevkjbvkbdkdxoeoziejdoiehz fiugebfuyegwik

NOAuth

We all know those pesky UIWebViews for logging into services like Facebook, Twitter or Dropbox on our mobile devices. They look crappy and are annoying, but they protect our passwords from those evil app developers, right?

Wrong. They are security theater, nothing more. And one of the worst kinds, because they give users the impression that they do not have to trust the individual app developer, but that's not the case.

Those of you with a technical background will probably mumble something about same origin policy, but that doesn't help here at all. Being a mobile app means being the browser and when you are the browser, you can do whatever you want, no questions asked.

There are different approaches for getting to the user's credentials, I will present the easiest here and I will concentrate on iOS:

  1. We register a custom NSURLProtocol for 'keylogger://' URLs. It is a dummy implementation which just makes sure that those URLs aren't processed further by the framework.

  2. In the webView:didFinishLoad: method, inject some JavaScript into the loaded page. The JavaScript will attach a listener to every input element on the page and that listener will call a 'keylogger://' URL crafted by us which contains the character the user entered.

  3. In the shouldStartLoadWithRequest: method, we capture all of the 'keylogger://' requests and log the characters. Then we stop loading, because those URLs are just used to communicate between JS and Objective-C.

That's it. The process is so basic that any iOS developer can add it to his app and there is nothing the user can do the prevent it, apart from not entering their passwords into apps they do not trust.

In terms of prior coverage of this, I only discovered this article, but I assume the issue is well-known and documented for a long time, but as there seems to be no simple demonstration of the problem, I created one. It is a slight modification of the SCFacebook example app which will present you an UIAlertView with the credentials you just entered when you logged in.

The FBConnect login dialog

The login dialog's layout is slightly off, but that can easily be fixed by improving the keylogger's JavaScript. Don't get me started on how crappy this page looks and how easy it is to recreate it for phishing purposes. Remember, the user cannot see the URL in an UIWebView, either.

UIAlertView with 'foo@example.com12password#' in it

The logged keys are stored in one string, but it's not that hard to separate the address from the rest.

So that's it, OAuth with UIWebViews doesn't add any security. Facebook and friends should stop using them today - a native login communicates that you have to trust the app with the credentials you are giving it, fooling users with security theater is not good. After that, we need a proper and secure way for protecting passwords from apps.

WebInspector on iOS

A couple of weeks ago, after seeing SteveStreza's tweet on WebView+Debug, I started digging into class dumps of the iOS WebKit framework to find out what else is there for debugging. I stumbled upon Nathan de Vries' post on remote WebInspector, but trying it out revealed that it doesn't work on the device at all.

What does WebKit framework offer us to find out what's going on?

[NSClassFromString(@"WebView") performSelector:@selector(_enableRemoteInspector)];
id sharedServer = [NSClassFromString(@"WebView") performSelector:@selector(sharedWebInspectorServer)];

In the simulator, this gives us a WebInspectorServerHTTP, but on the device it is a WebInspectorServerXPC. We can only speculate here, but it seems reasonable to assume that Apple uses XPC so that no server socket is opened by the WebKit framework on the device, but rather wants to have a separate process to handle the HTTP connection. As this is all private API, we have no idea how to spawn that service or if it is even implemented yet, we need to find another way.

It turns out that the WebInspectorServer base class defines a start class method. Let's try this little snippet:

id newServer = [[NSClassFromString(@"WebInspectorServerHTTP") alloc] init];
[newServer performSelector:NSSelectorFromString(@"start")];

Obviously, we need to hold on to that instance for the server to stick around, so be careful with that in an ARC environment - that bit me in a UIWebView category during testing. With that, we have now started the HTTP WebInspector server, but there still isn't anything listening to port 9999 when we do a port scan of the iPhone. Unfortunately, it turns out that the port is only opened locally.

My next idea was setting up a proxy on the device which forwards the requests to the local WebInspector server. For that I used the GTMHTTPServer class from Google Toolbox for Mac. I won't bore you with the implementation here and unfortunately, this didn't work out, either. I could access the list of open pages just fine, but the details page didn't work at all through the proxy, but I couldn't figure out why. All the JavaScript and CSS files were loaded fine. I would appreciate any ideas on what went wrong with this approach.

Finally, I settled for writing an app with two web views, one for the actual page and one for WebInspector. Switching between them would be done using swipe gestures, so I had to dig into how to add gestures on top of a UIWebView, which isn't as difficult as one would think. For that, we are going to do what Apple does recommend and subclass UIWebView. In this case, we are probably OK, though - we are building a private app for testing websites, so in the worst case we have to test our websites the old way again if something breaks.

Just attaching a gesture recognizer on the web view itself doesn't work. From what I gathered, the issue seems to be that there's a custom hitTest:withEvent: implementation at play here. The actual subview which handles touches for the web view is UIWebDocumentView, so we first need to find that in the hierarchy:

-(UIView*)documentView {
    for (UIView* view in self.subviews) {
        if ([view isKindOfClass:[UIScrollView class]]) {
            for (UIView* subview in view.subviews) {
                if ([subview isKindOfClass:NSClassFromString(@"UIWebDocumentView")]) {
                    return subview;
                }
            }
        }
    }
    return nil;
}

Now we are building a custom initWithFrame: which attaches our gesture recognizers:

-(id)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        [self setMultipleTouchEnabled:YES];

        UISwipeGestureRecognizer* swipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipeLeft)];
        swipe.direction = UISwipeGestureRecognizerDirectionLeft;
        [self.documentView addGestureRecognizer:swipe];

        swipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipeRight)];
        swipe.direction = UISwipeGestureRecognizerDirectionRight;
        [self.documentView addGestureRecognizer:swipe];
    }
    return self;
}

With that, we can now do whatever we want in swipeLeft and swipeRight. This worked fine for me, but it will probably break if the webpage itself uses gesture recognition. You have to see for yourself. Another idea would be to utilize shake for switching or use a UIToolbar. I wanted to avoid the toolbar to not lose any screen estate.

To conclude, you can get the final app on Github. None of it is App Store safe of course, because of all the private API that is used. That doesn't keep you from utilizing the app to debug your mobile web sites, though. In a follow-up post, I will describe the techniques I used to discover private API, as it sometimes can turn out to be quite useful and the information needs to be scraped together in bits and pieces at the moment.