One way to do this is to listen for the com.apple.system.config.network_change event from the Core Foundation Darwin notification center.
Register for the event:
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), //center
                                NULL, // observer
                                onNotifyCallback, // callback
                                CFSTR("com.apple.system.config.network_change"), // event name
                                NULL, // object
                                CFNotificationSuspensionBehaviorDeliverImmediately);
Here's a sample callback:
static void onNotifyCallback(CFNotificationCenterRef center, void *observer, CFStringRef name, const void *object, CFDictionaryRef userInfo)
{
    NSString* notifyName = (NSString*)name;
    // this check should really only be necessary if you reuse this one callback method
    //  for multiple Darwin notification events
    if ([notifyName isEqualToString:@"com.apple.system.config.network_change"]) {
        // use the Captive Network API to get more information at this point
        //  https://stackoverflow.com/a/4714842/119114
    } else {
        NSLog(@"intercepted %@", notifyName);
    }
}
See my link to another answer on how to use the Captive Network API to get the current SSID, for example.
Note that although the phone I tested this on is jailbroken (iOS 6.1), I don't think this requires jailbreaking to work correctly.  It certainly doesn't require the app being installed outside the normal sandbox area (/var/mobile/Applications/*).
P.S. I haven't tested this exhaustively enough to know whether this event gives any false positives (based on your definition of a network change).  However, it's simple enough just to store some state variable, equal to the last network's SSID, and compare that to the current one, whenever this event comes in.