21 minutes
Obscurity is not Security: A Case Study of a Cross-Platform Mobile Application
This research presents a security analysis on a cross-platform mobile application. The My Vodafone (Ghana) application formed the basis for the analysis; detailing both static and dynamic analysis.
Technical Details
The platform for the analysis was iOS. Details are below:
Title: My Vodafone (Ghana)
Version: 4.3.0
Bundle URL: com.vodafone.gh.myvodafone.app
Directory Structure
A decrypted and decompressed ipa file shows the following contents:
.
├── AccessibilityResources.bundle
│ ├── Info.plist
│ └── en.lproj
│ └── Localizable.strings
├── AirshipAutomationResources.bundle
├── AirshipConfig.plist
├── AirshipCoreResources.bundle
├── AirshipExtendedActionsResources.bundle
├── AirshipMessageCenterResources.bundle
├── Foundation.ttf
├── Frameworks
│ ├── NetPerformSDK.framework
│ ├── SecLibRNFramework.framework
│ ├── TealiumIOS.framework
│ └── TealiumIOSLifecycle.framework
├── GoogleMaps.bundle
├── GoogleService-Info.plist
├── Info.plist
├── Ionicons.ttf
├── LaunchScreen.storyboardc
├── MaterialCommunityIcons.ttf
├── MaterialIcons.ttf
├── Octicons.ttf
├── PkgInfo
├── SC_Info
├── SimpleLineIcons.ttf
├── Vodafone Rg Bold.ttf
├── VodafoneLt.ttf
├── VodafoneRg.ttf
├── Zocial.ttf
├── _CodeSignature
│ └── CodeResources
├── assets
│ ├── App
│ │ ├── ExternalComponents
│ │ ├── Images
├── device-names.json
├── main.jsbundle
└── myvodafoneapp
The Info.plist file contents a summary of the information related to the application.
MinimumOSVersion: 13.0
NSAppTransportSecurity:
NSExceptionDomains:
localhost:
NSExceptionAllowsInsecureHTTPLoads: true
New Exception Domain:
NSExceptionAllowsInsecureHTTPLoads: true
NSIncludesSubdomains: true
NSAllowsArbitraryLoads: true
DTXcodeBuild: 13F17a
firebase_json_raw: eyJhbmRyb2lkX3Rhc2tfZXhlY3V0b3JfbWF4aW11bV9wb29sX3NpemUiOiAxMCwgImFuZHJvaWRfdGFza19leGVjdXRvcl9rZWVwX2FsaXZlX3NlY29uZHMiOiAzfQ==
UISupportedDevices[0]: iPhone10,1
UISupportedDevices[1]: iPhone10,4
UISupportedDevices[2]: iPhone12,8
UISupportedDevices[3]: iPhone9,1
UISupportedDevices[4]: iPhone9,3
DTAppStoreToolsBuild: 13F100
CFBundleName: myvodafoneapp
CFBundleSupportedPlatforms[0]: iPhoneOS
CFBundleDisplayName: My Vodafone
ITSDRMScheme: v2
DTPlatformBuild: 19F64
CFBundleSignature: ????
DTXcode: 1340
CFBundleVersion: 82
DTSDKName: iphoneos15.5
UIDeviceFamily[0]: 1
UIDeviceFamily[1]: 2
UIBackgroundModes[0]: remote-notification
UIFileSharingEnabled: true
CFBundleIcons:
CFBundlePrimaryIcon:
CFBundleIconName: AppIcon
CFBundleIconFiles[0]: AppIcon60x60
DTPlatformName: iphoneos
CFBundleDevelopmentRegion: en
NSLocationWhenInUseUsageDescription: To show you Vodafone retail shops and other important information based on your location
FirebaseDynamicLinksCustomDomains[0]: https://vodafone.com.gh/home
NSLocationAlwaysAndWhenInUseUsageDescription: My Vodafone would like to access your location
CFBundleURLTypes[0]:
CFBundleURLSchemes[0]: mva
CFBundleTypeRole: Editor
CFBundleURLName: myvodafoneapp
CFBundleURLTypes[1]: com.vodafone.gh.myvodafone.app
LSRequiresIPhoneOS: true
CFBundleURLTypes[2]: To set profile pictures for your accounts
CFBundleURLTypes[3]: myvodafoneapp
CFBundleURLTypes[4]: 21G72
CFBundleURLTypes:
CFBundlePackageType: APPL
LSApplicationQueriesSchemes[0]: whatsapp
LSApplicationQueriesSchemes[1]: vodafonemusic
LSApplicationQueriesSchemes[2]: fb
LSApplicationQueriesSchemes[3]: youtube
LSApplicationQueriesSchemes[4]: twitter
LSApplicationQueriesSchemes[5]: 2ctv
LSApplicationQueriesSchemes[6]: wi-flix
LSApplicationQueriesSchemes[7]: dreamlab
NSContactsUsageDescription: Get easy access to your contacts during transactions. Eg: VFCash and Top Up
UIUserInterfaceStyle: Light
DTCompiler: com.apple.compilers.llvm.clang.1_0
UIRequiredDeviceCapabilities[0]: arm64
NSLocationAlwaysUsageDescription: To show you Vodafone retail shops and other important information based on your location
UIViewControllerBasedStatusBarAppearance: false
NSCameraUsageDescription: To set profile pictures for your accounts
UISupportedInterfaceOrientations[0]: UIInterfaceOrientationPortrait
CFBundleInfoDictionaryVersion: 6.0
UIAppFonts[0]: Vodafone Rg Bold.ttf
UIAppFonts[1]: VodafoneLt.ttf
UIAppFonts[2]: VodafoneRg.ttf
UIAppFonts[3]: AntDesign.ttf
UIAppFonts[4]: Entypo.ttf
UIAppFonts[5]: EvilIcons.ttf
UIAppFonts[6]: Feather.ttf
UIAppFonts[7]: FontAwesome.ttf
UIAppFonts[8]: FontAwesome5_Brands.ttf
UIAppFonts[9]: FontAwesome5_Regular.ttf
UIAppFonts[10]: FontAwesome5_Solid.ttf
UIAppFonts[11]: Fontisto.ttf
UIAppFonts[12]: Foundation.ttf
UIAppFonts[13]: Ionicons.ttf
UIAppFonts[14]: MaterialCommunityIcons.ttf
UIAppFonts[15]: MaterialIcons.ttf
UIAppFonts[16]: Octicons.ttf
UIAppFonts[17]: SimpleLineIcons.ttf
UIAppFonts[18]: Zocial.ttf
NSAppleMusicUsageDescription: To allow access to Apple Music
FirebaseCrashlyticsCollectionEnabled: false
DTSDKBuild: 19F64
UILaunchStoryboardName: LaunchScreen
DTPlatformVersion: 15.5
CFBundleShortVersionString: 4.3.0
LSSupportsOpeningDocumentsInPlace: YES
UIRequiresFullScreen: true
In this file the supported devices are indicated; iPhone 10,4 etc. There is an exception to allow insecure HTTP loads.
There is a firebase_json_raw
field which is base64 encoded and its decode value is {"android_task_executor_maximum_pool_size": 10, "android_task_executor_keep_alive_seconds": 3}
; nothing really interesting here.
From the Info.plist
file, there is a permission request for location, camera and contacts; NSLocation
, NSCamera
and NSContacts
.
Interestingly, there is a permission to allow access to Apple Music
; 😅 indicated as NSAppleMusicUsageDescription: To allow access to Apple Music
.
The default application is indicated by CFBundleURLName
and is myvodafoneapp
. The bundle URL is defined by CFBundleURLTypes
and the value is com.vodafone.gh.myvodafone.app
.
There are eight(8) launch services registered in the application (for more details on launch services visit https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/LaunchServicesKeys.html); whatsapp
, vodafonemusic
, fb
, youtube
, twitter
, 2ctv
, wi-flix
and dreamlab
.
There are about 19 fonts that are reference in the Info.plist
file. The app version is indicated by the key CFBundleShortVersionString
and it’s value is 4.3.0
.
Inside the directory, there is a GoogleService-Info.plist
file; which contains google services API keys, firebase database URL etc.
ANDROID_CLIENT_ID: ***bc89vjr4p4.apps.googleusercontent.com
API_KEY: AIza***3kzY
BUNDLE_ID: com.vodafone.gh.myvodafone.app
CLIENT_ID: ***-3fb9i3mkeok8dd***2t.apps.googleusercontent.com
DATABASE_URL: https://vodaf***.firebaseio.com
GCM_SENDER_ID: 63***49
GOOGLE_APP_ID: 1:63*****749:ios:e91d****7a04
IS_ADS_ENABLED: false
IS_ANALYTICS_ENABLED: false
IS_APPINVITE_ENABLED: true
IS_GCM_ENABLED: true
IS_SIGNIN_ENABLED: true
PLIST_VERSION: 1
PROJECT_ID: vodafoneapp-****
REVERSED_CLIENT_ID: com.googleusercontent.apps.63***49-3fb9***g2t
STORAGE_BUCKET: vodafoneapp-****.ot.com
The application contains AirShip
(https://www.airship.com) configuration files and bundles; the default AirshipConfig.plist
file contains the following contents:
developmentAppKey: Fzs****RhBw
developmentAppSecret: Vbv***oMXMQ
inProduction: true
productionAppKey: bo****-ZQV1g
productionAppSecret: zTY****l8Q
The framework directory contains, NetPerformSDK.framework
, SecLibRNFramework.framework
, TealiumIOS.framework
and TealiumIOSLifecycle.framework
. The NetPerformSDK
is used for network speed testing, the SecLibRNFramework
implement security functions (such as encryption/decryption of bytes of data, etc.), the TealiumIOS
is a customer data hub framework.
The directory contains device-names.json
file which indicates the various iOS devices supported by the application.
Static Analysis of Application
The application was developed using React Native
. In the root directory of the application is a file named main.jsbundle
; a bundled javascript of the whole application.
The bundle javascript when opened in a TextEditor contains a bunch of minified javascript code; shown below.
var __BUNDLE_START_TIME__=this.nativePerformanceNow?nativePerformanceNow():Date.now(),__DEV__=false,process=this.process||{},__METRO_GLOBAL_PREFIX__='';process.env=process.env||{};process.env.NODE_ENV=process.env.NODE_ENV||"production";
!(function(r){"use strict";r.__r=o,r[__METRO_GLOBAL_PREFIX__+"__d"]=function(r,i,n){if(null!=e[i])return;var o={dependencyMap:n,factory:r,hasError:!1,importedAll:t,importedDefault:t,isInitialized:!1,publicModule:{exports:{}}};e[i]=o},r.__c=n,r.__registerSegment=function(r,t,i){s[r]=t,i&&i.forEach(function(t){e[t]||v.has(t)||v.set(t,r)})};var e=n(),t={},i={}.hasOwnProperty;function n(){return e=Object.create(null)}function o(r){var t=r,i=e[t];return i&&i.isInitialized?i.publicModule.exports:d(t,i)}function l(r){var i=r;if(e[i]&&e[i].importedDefault!==t)return e[i].importedDefault;var n=o(i),l=n&&n.__esModule?n.default:n;return e[i].importedDefault=l}function u(r){var n=r;if(e[n]&&e[n].importedAll!==t)return e[n].importedAll;var l,u=o(n);if(u&&u.__esModule)l=u;else{if(l={},u)for(var a in u)i.call(u,a)&&(l[a]=u[a]);l.default=u}return e[n].importedAll=l}o.importDefault=l,o.importAll=u;var a=!1;function d(e,t){if(!a&&r.ErrorUtils){var i;a=!0;try{i=h(e,t)}catch(e){r.ErrorUtils.reportFatalError(e)}return a=!1,i}return h(e,t)}var f=16,c=65535;function p(r){return{segmentId:r>>>f,localId:r&c}}o.unpackModuleId=p,o.packModuleId=function(r){return(r.segmentId<<f)+r.localId};var s=[],v=new Map;function h(t,i){if(!i&&s.length>0){var n,a=null!==(n=v.get(t))&&void 0!==n?n:0,d=s[a];null!=d&&(d(t),i=e[t],v.delete(t))}var f=r.nativeRequire;if(!i&&f){var c=p(t),h=c.segmentId;f(c.localId,h),i=e[t]}if(!i)throw Error('Requiring unknown module "'+t+'".');if(i.hasError)throw _(t,i.error);i.isInitialized=!0;var m=i,g=m.factory,I=m.dependencyMap;try{var M=i.publicModule;return M.id=t,g(r,o,l,u,M,M.exports,I),i.factory=void 0,i.dependencyMap=void 0,M.exports}catch(r){throw i.hasError=!0,i.error=r,i.isInitialized=!1,i.publicModule.exports=void 0,r}}function _(r,e){return Error('Requiring module "'+r+'", which threw an exception: '+e)}})('undefined'!=typeof globalThis?globalThis:'undefined'!=typeof global?global:'undefined'!=typeof window?window:this);
!(function(n){var e=(function(){function n(n,e){return n}function e(n){var e={};return n.forEach(function(n,r){e[n]=!0}),e}function r(n,r,u){if(n.formatValueCalls++,n.formatValueCalls>200)return"[TOO BIG formatValueCalls "+n.formatValueCalls+" exceeded limit of 200]";var f=t(n,r);if(f)return f;var c=Object.keys(r),s=e(c);if(d(r)&&(c.indexOf('message')>=0||c.indexOf('description')>=0))return o(r);if(0===c.length){if(v(r)){var g=r.name?': '+r.name:'';return n.stylize('[Function'+g+']','special')}if(p(r))return n.stylize(RegExp.prototype.toString.call(r),'regexp');if(y(r))return n.stylize(Date.prototype.toString.call(r),'date');if(d(r))return o(r)}var h,b,m='',j=!1,O=['{','}'];(h=r,Array.isArray(h)&&(j=!0,O=['[',']']),v(r))&&(m=' [Function'+(r.name?': '+r.name:'')+']');return p(r)&&(m=' '+RegExp.prototype.toString.call(r)),y(r)&&(m=' '+Date.prototype.toUTCString.call(r)),d(r)&&(m=' '+o(r)),0!==c.length||j&&0!=r.length?u<0?p(r)?n.stylize(RegExp.prototype.toString.call(r),'regexp'):n.stylize('[Object]','special'):(n.seen.push(r),b=j?i(n,r,u,s,c):c.map(function(e){return l(n,r,u,s,e,j)}),n.seen.pop(),a(b,m,O)):O[0]+m+O[1]}function t(n,e){if(s(e))return n.stylize('undefined','undefined');if('string'==typeof e){var r="'"+JSON.stringify(e).replace(/^"|"$/g,'').replace(/'/g,"\\'").replace(/\\"/g,'"')+"'";return n.stylize(r,'string')}return c(e)?n.stylize(''+e,'number'):u(e)?n.stylize(''+e,'boolean'):f(e)?n.stylize('null','null'):void 0}function o(n){return'['+Error.prototype.toString.call(n)+']'}function i(n,e,r,t,o){for(var i=[],a=0,u=e.length;a<u;++a)b(e,String(a))?i.push(l(n,e,r,t,String(a),!0)):i.push('');return o.forEach(function(o){o.match(/^\d+$/)||i.push(l(n,e,r,t,o,!0))}),i}function l(n,e,t,o,i,l){var a,u,c;if((c=Object.getOwnPropertyDescriptor(e,i)||{value:e[i]}).get?u=c.set?n.stylize('[Getter/Setter]','special'):n.stylize('[Getter]','special'):c.set&&(u=n.stylize('[Setter]','special')),b(o,i)||(a='['+i+']'),u||(n.seen.indexOf(c.value)<0?(u=f(t)?r(n,c.value,null):r(n,c.value,t-1)).indexOf('\n')>-1&&(u=l?u.split('\n').map(function(n){return' '+n}).join('\n').substr(2):'\n'+u.split('\n').map(function(n){return' '+n}).join('\n')):u=n.stylize('[Circular]','special')),s(a)){if(l&&i.match(/^\d+$/))return u;(a=JSON.stringify(''+i)).match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)?(a=a.substr(1,a.length-2),a=n.stylize(a,'name')):(a=a.replace(/'/g,"\\'").replace(/\\"/g,'"').replace(/(^"|"$)/g,"'"),a=n.stylize(a,'string'))}return a+': '+u}function a(n,e,r){return n.reduce(function(n,e){return 0,e.indexOf('\n')>=0&&0,n+e.replace(/\u001b\[\d\d?m/g,'').length+1},0)>60?r[0]+(''===e?'':e+'\n ')+' '+n.join(',\n ')+' '+r[1]:r[0]+e+' '+n.join(', ')+' '+r[1]}function u(n){return'boolean'==typeof n}function f(n){return null===n}function c(n){return'number'==typeof n}function s(n){return void 0===n}function p(n){return g(n)&&'[object RegExp]'===h(n)}function g(n){return'object'==typeof n&&null!==n}function y(n){return g(n)&&'[object Date]'===h(n)}function d(n){return g(n)&&('[object Error]'===h(n)||n instanceof Error)}function v(n){return'function'==typeof n}function h(n){return Object.prototype.toString.call(n)}function b(n,e){return Object.prototype.hasOwnProperty.call(n,e)}return function(e,t){return r({seen:[],formatValueCalls:0,stylize:n},e,t.depth)}})(),r='(index)',t={trace:0,info:1,warn:2,error:3},o=[];o[t.trace]='debug',o[t.info]='log',o[t.warn]='warning',o[t.error]='error';var i=1;function l(r){return function(){var l;l=1===arguments.length&&'string'==typeof arguments[0]?arguments[0]:Array.prototype.map.call(arguments,function(n){return e(n,{depth:10})}).join(', ');var a=arguments[0],u=r;'string'==typeof a&&'Warning: '===a.slice(0,9)&&u>=t.error&&(u=t.warn),n.__inspectorLog&&n.__inspectorLog(o[u],l,[].slice.call(arguments),i),s.length&&(l=p('',l)),n.nativeLoggingHook(l,u)}}function a(n,e){return Array.apply(null,Array(e)).map(function(){return n})}var u="\u2502",f="\u2510",c="\u2518",s=[];function p(n,e){return s.join('')+n+' '+(e||'')}if(n.nativeLoggingHook){n.console;n.console={error:l(t.error),info:l(t.info),log:l(t.info),warn:l(t.warn),trace:l(t.trace),debug:l(t.trace),table:function(e){if(!Array.isArray(e)){var o=e;for(var i in e=[],o)if(o.hasOwnProperty(i)){var l=o[i];l[r]=i,e.push(l)}}if(0!==e.length){var u=Object.keys(e[0]).sort(),f=[],c=[];u.forEach(function(n,r){c[r]=n.length;for(var t=0;t<e.length;t++){var o=(e[t][n]||'?').toString();f[t]=f[t]||[],f[t][r]=o,c[r]=Math.max(c[r],o.length)}});for(var s=y(c.map(function(n){return a('-',n).join('')}),'-'),p=[y(u),s],g=0;g<e.length;g++)p.push(y(f[g]));n.nativeLoggingHook('\n'+p.join('\n'),t.info)}else n.nativeLoggingHook('',t.info);function y(n,e){var r=n.map(function(n,e){return n+a(' ',c[e]-n.length).join('')});return e=e||' ',r.join(e+'|'+e)}},group:function(e){n.nativeLoggingHook(p(f,e),t.info),s.push(u)},groupEnd:function(){s.pop(),n.nativeLoggingHook(p(c),t.info)},groupCollapsed:function(e){n.nativeLoggingHook(p(c,e),t.info),s.push(u)},assert:function(e,r){e||n.nativeLoggingHook('Assertion failed: '+r,t.error)}},Object.defineProperty(console,'_isPolyfilled',{value:!0,enumerable:!1})}else if(!n.console){function g(){}var y=n.print||g;n.console={debug:y,error:y,info:y,log:y,trace:y,warn:y,assert:function(n,e){n||y('Assertion failed:
An unbundled code can be generated by dumping the entired bundled JS in an unminify (https://unminifyjs.sperixlabs.org). This results in a more refined JS; as shown below.
var __BUNDLE_START_TIME__ = this.nativePerformanceNow ? nativePerformanceNow() : Date.now(),
__DEV__ = false,
process = this.process || {},
__METRO_GLOBAL_PREFIX__ = '';
process.env = process.env || {};
process.env.NODE_ENV = process.env.NODE_ENV || "production";
!(function(r) {
"use strict";
r.__r = o, r[__METRO_GLOBAL_PREFIX__ + "__d"] = function(r, i, n) {
if (null != e[i]) return;
var o = {
dependencyMap: n,
factory: r,
hasError: !1,
importedAll: t,
importedDefault: t,
isInitialized: !1,
publicModule: {
exports: {}
}
};
e[i] = o
}, r.__c = n, r.__registerSegment = function(r, t, i) {
s[r] = t, i && i.forEach(function(t) {
e[t] || v.has(t) || v.set(t, r)
})
};
var e = n(),
t = {},
i = {}.hasOwnProperty;
function n() {
return e = Object.create(null)
}
function o(r) {
var t = r,
i = e[t];
return i && i.isInitialized ? i.publicModule.exports : d(t, i)
}
function l(r) {
var i = r;
if (e[i] && e[i].importedDefault !== t) return e[i].importedDefault;
var n = o(i),
l = n && n.__esModule ? n.default : n;
return e[i].importedDefault = l
}
function u(r) {
var n = r;
if (e[n] && e[n].importedAll !== t) return e[n].importedAll;
var l, u = o(n);
if (u && u.__esModule) l = u;
else {
if (l = {}, u)
for (var a in u) i.call(u, a) && (l[a] = u[a]);
l.default = u
}
return e[n].importedAll = l
}
o.importDefault = l, o.importAll = u;
var a = !1;
function d(e, t) {
if (!a && r.ErrorUtils) {
var i;
a = !0;
try {
i = h(e, t)
} catch (e) {
r.ErrorUtils.reportFatalError(e)
}
return a = !1, i
}
return h(e, t)
}
var f = 16,
c = 65535;
function p(r) {
return {
segmentId: r >>> f,
localId: r & c
}
}
o.unpackModuleId = p, o.packModuleId = function(r) {
return (r.segmentId << f) + r.localId
};
var s = [],
v = new Map;
function h(t, i) {
if (!i && s.length > 0) {
var n, a = null !== (n = v.get(t)) && void 0 !== n ? n : 0,
d = s[a];
null != d && (d(t), i = e[t], v.delete(t))
}
var f = r.nativeRequire;
if (!i && f) {
var c = p(t),
h = c.segmentId;
f(c.localId, h), i = e[t]
}
if (!i) throw Error('Requiring unknown module "' + t + '".');
if (i.hasError) throw _(t, i.error);
i.isInitialized = !0;
var m = i,
g = m.factory,
I = m.dependencyMap;
try {
var M = i.publicModule;
return M.id = t, g(r, o, l, u, M, M.exports, I), i.factory = void 0, i.dependencyMap = void 0, M.exports
} catch (r) {
throw i.hasError = !0, i.error = r, i.isInitialized = !1, i.publicModule.exports = void 0, r
}
}
A deep dive through the unminified JS file reveals some default endpoints and constants such as:
var o = {
vfCashQueryStatement: "vfCashQueryStatement",
smartSurfPurchase: "smartSurfPurchase",
smartSurfBalanceSummary: "smartSurfBalanceSummary",
vfCashGetTerminalDetails: "vfCashGetTerminalDetails",
payMerchantQR: "payMerchantQR",
vfCashQRCodeDetails: "getQRDetails",
vfCashNewEndpoint: "https://*****appmw.vodafone.com.gh/MVAppAPI/VF_Cash",
vfCashNewEndpointUAT: "https://*****appmw.vodafone.com.gh/MVAppAPIUAT/VF_Cash",
vfCashDeleteFreqContacts: "vfCashDeleteFreqContacts",
vfCashGetFreqContacts: "vfCashGetFreqContacts",
vfCashAddFreqContact: "vfCashAddFreqContacts",
TestUrl: "https://*****appmw.vodafone.com.gh/MVAppAPIUAT/User",
vfCashTestUrl: "https://*****appmw.vodafone.com.gh/MVAppAPI/User",
.....
};
The UserLoginAction
is shown below:
var u = n.default.loginUserAction,
S = {
username: t,
password: s,
action: u,
OS: o
};
return function(t, s) {
t(x(!0)), (0, r(d[16]).makeRequest)(n.default.userAuthenticationUrl, 'post', S, function(n) {
t(x(!1));
var o = n.RESPONSECODE,
u = s(),
S = JSON.parse(JSON.stringify(u.authenticate.userData)),
E = JSON.parse(JSON.stringify(u.authenticate.defaultService));
if (0 == o) {
var f = n.SESSION.key,
l = n.SESSION.secret,
p = n.SESSION.session,
A = (0, r(d[17]).handleSessionVals)(f, p, l),
R = p.replace(/-/g, '');
n.RESPONSEDATA.ServiceList;
S.formattedSessionId = R, S.hashedKey = A, S.sessionId = p, t(z(S, E, '')), t(w(!0)), c(!0, 'Successfully logged in')
} else if (1 == o) {
n.RESPONSEMESSAGE;
t(w(!1))
} else if (2 == o) {
n.RESPONSEMESSAGE;
t(w(!1))
}
}, null)
}
The parameters for the user login action are the username, password, action and os. From the code snippet, the response returns a session key, secret and a session value.
A hashedKey is computed from the expression A = (0, r(d[17]).handleSessionVals)(f, p, l)
. A formattedSesssionId is also derived from the expression R = p.replace(/-/g, '')
.
handleSessionVals function is shown below:
e.handleSessionVals = function(t, c, o) {
var u = t.concat(c).concat(o);
return n.default.hex_md5(u).substring(0, 16)
};
From the above function, the handleSessionVals takes the following parameters respectively: SESSION.key, SESSION.session, and SESSION.secret. To generate the handleSessionVals, the SESSION.key is concatenated with the SESSION.session, and further concatenated with the SESSION.secret. The result is passed to hex_md5 function and first 16 characters returned.
The hex_md5
function is shown below:
m.exports.hex_md5 = function(n) {
return _(o(l(n), n.length * t))
}
It also relies on other subroutines as shown below:
var n = "", t = 8;
function o(n, t) {
n[t >> 5] |= 128 << t % 32, n[14 + (t + 64 >>> 9 << 4)] = t;
for (var o = 1732584193, u = -271733879, i = -1732584194, l = 271733878, d = 0; d < n.length; d += 16) {
var _ = o,
s = u,
x = i,
A = l;
u = h(u = h(u = h(u = h(u = f(u = f(u = f(u = f(u = a(u = a(u = a(u = a(u = c(u = c(u = c(u = c(u, i = c(i, l = c(l, o = c(o, u, i, l, n[d + 0], 7, -680876936), u, i, n[d + 1], 12, -389564586), o, u, n[d + 2], 17, 606105819), l, o, n[d + 3], 22, -1044525330), i = c(i, l = c(l, o = c(o, u, i, l, n[d + 4], 7, -176418897), u, i, n[d + 5], 12, 1200080426), o, u, n[d + 6], 17, -1473231341), l, o, n[d + 7], 22, -45705983), i = c(i, l = c(l, o = c(o, u, i, l, n[d + 8], 7, 1770035416), u, i, n[d + 9], 12, -1958414417), o, u, n[d + 10], 17, -42063), l, o, n[d + 11], 22, -1990404162), i = c(i, l = c(l, o = c(o, u, i, l, n[d + 12], 7, 1804603682), u, i, n[d + 13], 12, -40341101), o, u, n[d + 14], 17, -1502002290), l, o, n[d + 15], 22, 1236535329), i = a(i, l = a(l, o = a(o, u, i, l, n[d + 1], 5, -165796510), u, i, n[d + 6], 9, -1069501632), o, u, n[d + 11], 14, 643717713), l, o, n[d + 0], 20, -373897302), i = a(i, l = a(l, o = a(o, u, i, l, n[d + 5], 5, -701558691), u, i, n[d + 10], 9, 38016083), o, u, n[d + 15], 14, -660478335), l, o, n[d + 4], 20, -405537848), i = a(i, l = a(l, o = a(o, u, i, l, n[d + 9], 5, 568446438), u, i, n[d + 14], 9, -1019803690), o, u, n[d + 3], 14, -187363961), l, o, n[d + 8], 20, 1163531501), i = a(i, l = a(l, o = a(o, u, i, l, n[d + 13], 5, -1444681467), u, i, n[d + 2], 9, -51403784), o, u, n[d + 7], 14, 1735328473), l, o, n[d + 12], 20, -1926607734), i = f(i, l = f(l, o = f(o, u, i, l, n[d + 5], 4, -378558), u, i, n[d + 8], 11, -2022574463), o, u, n[d + 11], 16, 1839030562), l, o, n[d + 14], 23, -35309556), i = f(i, l = f(l, o = f(o, u, i, l, n[d + 1], 4, -1530992060), u, i, n[d + 4], 11, 1272893353), o, u, n[d + 7], 16, -155497632), l, o, n[d + 10], 23, -1094730640), i = f(i, l = f(l, o = f(o, u, i, l, n[d + 13], 4, 681279174), u, i, n[d + 0], 11, -358537222), o, u, n[d + 3], 16, -722521979), l, o, n[d + 6], 23, 76029189), i = f(i, l = f(l, o = f(o, u, i, l, n[d + 9], 4, -640364487), u, i, n[d + 12], 11, -421815835), o, u, n[d + 15], 16, 530742520), l, o, n[d + 2], 23, -995338651), i = h(i, l = h(l, o = h(o, u, i, l, n[d + 0], 6, -198630844), u, i, n[d + 7], 10, 1126891415), o, u, n[d + 14], 15, -1416354905), l, o, n[d + 5], 21, -57434055), i = h(i, l = h(l, o = h(o, u, i, l, n[d + 12], 6, 1700485571), u, i, n[d + 3], 10, -1894986606), o, u, n[d + 10], 15, -1051523), l, o, n[d + 1], 21, -2054922799), i = h(i, l = h(l, o = h(o, u, i, l, n[d + 8], 6, 1873313359), u, i, n[d + 15], 10, -30611744), o, u, n[d + 6], 15, -1560198380), l, o, n[d + 13], 21, 1309151649), i = h(i, l = h(l, o = h(o, u, i, l, n[d + 4], 6, -145523070), u, i, n[d + 11], 10, -1120210379), o, u, n[d + 2], 15, 718787259), l, o, n[d + 9], 21, -343485551), o = v(o, _), u = v(u, s), i = v(i, x), l = v(l, A)
}
return Array(o, u, i, l)
}
function u(n, t, o, u, c, a) {
return v((f = v(v(t, n), v(u, a))) << (h = c) | f >>> 32 - h, o);
var f, h
}
function c(n, t, o, c, a, f, h) {
return u(t & o | ~t & c, n, t, a, f, h)
}
function a(n, t, o, c, a, f, h) {
return u(t & c | o & ~c, n, t, a, f, h)
}
function f(n, t, o, c, a, f, h) {
return u(t ^ o ^ c, n, t, a, f, h)
}
function h(n, t, o, c, a, f, h) {
return u(o ^ (t | ~c), n, t, a, f, h)
}
function i(n, u) {
var c = l(n);
c.length > 16 && (c = o(c, n.length * t));
for (var a = Array(16), f = Array(16), h = 0; h < 16; h++) a[h] = 909522486 ^ c[h], f[h] = 1549556828 ^ c[h];
var i = o(a.concat(l(u)), 512 + u.length * t);
return o(f.concat(i), 640)
}
function v(n, t) {
var o = (65535 & n) + (65535 & t);
return (n >> 16) + (t >> 16) + (o >> 16) << 16 | 65535 & o
}
function l(n) {
for (var o = Array(), u = 0; u < n.length * t; u += t) o[u >> 5] |= (255 & n.charCodeAt(u / t)) << u % 32;
return o
}
function d(n) {
for (var o = "", u = 0; u < 32 * n.length; u += t) o += String.fromCharCode(n[u >> 5] >>> u % 32 & 255);
return o
}
function _(n) {
for (var t = "", o = 0; o < 4 * n.length; o++) t += "0123456789abcdef".charAt(n[o >> 2] >> o % 4 * 8 + 4 & 15) + "0123456789abcdef".charAt(n[o >> 2] >> o % 4 * 8 & 15);
return t
}
function s(t) {
for (var o = "", u = 0; u < 4 * t.length; u += 3)
for (var c = (t[u >> 2] >> u % 4 * 8 & 255) << 16 | (t[u + 1 >> 2] >> (u + 1) % 4 * 8 & 255) << 8 | t[u + 2 >> 2] >> (u + 2) % 4 * 8 & 255, a = 0; a < 4; a++) 8 * u + 6 * a > 32 * t.length ? o += n : o += "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".charAt(c >> 6 * (3 - a) & 63);
return o
}
From the analysis of the bundle JS, all network requests aside the UserLoginAction
involve an encryption
of the request payload and a decryption
of the response payload. Example is shown below:
return function(t) {
(0, r(d[18]).encrypt2)(l, S, S).then(function(s) {
t(Y('deleting default service'));
var f = {
requestBody: s
};
(0, r(d[16]).makeRequest)(n.default.userAuthenticationUrl, 'post', f, function(n) {
(0, r(d[18]).decrypt2)(n.responseBody, S, S).then(function(n) {
t(Y('deleting account response'));
var s = JSON.parse(n),
f = s.RESPONSECODE;
if (0 === f) {
t(ie()), t(se(E, o, c, u, S)), t(Y('deleting account passed'));
var l = s.RESPONSEMESSAGE;
t(oe(l))
} else if (1 === f || 2 === f) {
t(Y('deleting account failed'));
var l = s.RESPONSEMESSAGE;
t(oe(l))
}
})
}, p)
})
}
The next action is to figure out what encrypt2
and decrypt2
do. These two functions are shown below:
e.encrypt2 = e.decrypt2 = void 0;
var n = r(d[0])(r(d[1])),
t = r(d[0])(r(d[2])),
u = r(d[3]),
c = u.NativeModules.AesCrypto;
e.encrypt2 = function(o, f, l) {
return n.default.async(function(n) {
for (;;) switch (n.prev = n.next) {
case 0:
if ('android' !== u.Platform.OS) {
n.next = 2;
break
}
return n.abrupt("return", c.encrypt(o, f, l).then(function(n) {
return n
}).catch(function(n) {}));
case 2:
return n.abrupt("return", t.default.encrypt(o, f, l).then(function(n) {
return n
}).catch(function(n) {}));
case 3:
case "end":
return n.stop()
}
}, null, null, null, Promise)
};
e.decrypt2 = function(o, f, l) {
return n.default.async(function(n) {
for (;;) switch (n.prev = n.next) {
case 0:
if ('android' !== u.Platform.OS) {
n.next = 2;
break
}
return n.abrupt("return", c.decrypt(o, f, l).then(function(n) {
return n
}).catch(function(n) {}));
case 2:
return n.abrupt("return", t.default.decrypt(o, f, l).then(function(n) {
return n
}).catch(function(n) {}));
case 3:
case "end":
return n.stop()
}
}, null, null, null, Promise)
}
The above encryption and decryption function relies on a native AES crypto library c = u.NativeModules.AesCrypto
; a React-Native bridge.
Decompiling the myvodafoneapp
binary in Hopper (https://www.hopperapp.com) shows the following RCTAesCrypto
encrypt/decrypt function. The parameters of both the encryption/decryption function takes two arguments; the key and the initialization vector.
Encryption Code (Assembly):
-[RCTAesCrypto encrypt:appkey:gIv:resolver:rejecter:]:
00000001006b4d34 stp x28, x27, [sp, #-0x60]! ; Objective C Implementation defined at 0x10172eb58 (instance method), DATA XREF=0x10172eb58
00000001006b4d38 stp x26, x25, [sp, #0x10]
00000001006b4d3c stp x24, x23, [sp, #0x20]
00000001006b4d40 stp x22, x21, [sp, #0x30]
00000001006b4d44 stp x20, x19, [sp, #0x40]
00000001006b4d48 stp fp, lr, [sp, #0x50]
00000001006b4d4c add fp, sp, #0x50
00000001006b4d50 mov x22, x6
00000001006b4d54 mov x23, x5
00000001006b4d58 mov x21, x4
00000001006b4d5c mov x20, x3
00000001006b4d60 mov x0, x2 ; argument "instance" for method imp___stubs__objc_retain
00000001006b4d64 bl imp___stubs__objc_retain ; objc_retain
00000001006b4d68 mov x19, x0
00000001006b4d6c mov x0, x20 ; argument "instance" for method imp___stubs__objc_retain
00000001006b4d70 bl imp___stubs__objc_retain ; objc_retain
00000001006b4d74 mov x20, x0
00000001006b4d78 mov x0, x21 ; argument "instance" for method imp___stubs__objc_retain
00000001006b4d7c bl imp___stubs__objc_retain ; objc_retain
00000001006b4d80 mov x21, x0
00000001006b4d84 mov x0, x22 ; argument "instance" for method imp___stubs__objc_retain
00000001006b4d88 bl imp___stubs__objc_retain ; objc_retain
00000001006b4d8c mov x22, x0
00000001006b4d90 adrp x27, #0x101880000
00000001006b4d94 ldr x25, [x27, #0x160] ; objc_cls_ref_SecurityUtil,__objc_class_SecurityUtil_class
00000001006b4d98 adrp x8, #0x10186e000 ; &@selector(sharedMenuController)
00000001006b4d9c ldr x24, [x8, #0x398] ; "encryptAESData:app_key:gIv:",@selector(encryptAESData:app_key:gIv:)
00000001006b4da0 mov x0, x23 ; argument "instance" for method imp___stubs__objc_retain
00000001006b4da4 bl imp___stubs__objc_retain ; objc_retain
00000001006b4da8 mov x23, x0
00000001006b4dac mov x0, x25 ; argument "instance" for method imp___stubs__objc_msgSend
00000001006b4db0 mov x1, x24 ; argument "selector" for method imp___stubs__objc_msgSend
00000001006b4db4 mov x2, x19
00000001006b4db8 mov x3, x20
00000001006b4dbc mov x4, x21
00000001006b4dc0 bl imp___stubs__objc_msgSend ; objc_msgSend
00000001006b4dc4 mov fp, fp
00000001006b4dc8 bl imp___stubs__objc_retainAutoreleasedReturnValue ; objc_retainAutoreleasedReturnValue
00000001006b4dcc mov x25, x0
00000001006b4dd0 adrp x8, #0x101861000 ; &@selector(dealloc)
00000001006b4dd4 ldr x1, [x8, #0x58] ; argument "selector" for method imp___stubs__objc_msgSend, "length",@selector(length)
00000001006b4dd8 bl imp___stubs__objc_msgSend ; objc_msgSend
00000001006b4ddc mov x26, x0
00000001006b4de0 mov x0, x25 ; argument "instance" for method imp___stubs__objc_release
00000001006b4de4 bl imp___stubs__objc_release ; objc_release
Encryption Code (Obj C):
-(void)encrypt:(void *)arg2 appkey:(void *)arg3 gIv:(void *)arg4 resolver:(void *)arg5 rejecter:(void *)arg6 {
var_50 = r28;
stack[-88] = r27;
r31 = r31 + 0xffffffffffffffa0;
var_40 = r26;
stack[-72] = r25;
var_30 = r24;
stack[-56] = r23;
var_20 = r22;
stack[-40] = r21;
var_10 = r20;
stack[-24] = r19;
saved_fp = r29;
stack[-8] = r30;
r19 = [arg2 retain];
r20 = [arg3 retain];
r21 = [arg4 retain];
r22 = [arg6 retain];
r23 = [arg5 retain];
r0 = [SecurityUtil encryptAESData:r19 app_key:r20 gIv:r21];
r29 = &saved_fp;
r0 = [r0 retain];
r26 = [r0 length];
[r0 release];
if (r26 == 0x0) {
(*(r22 + 0x10))(r22, @"ERROR", @"decrypt failed", 0x0);
}
(*(r23 + 0x10))(r23, [[SecurityUtil encryptAESData:r19 app_key:r20 gIv:r21] retain]);
[r23 release];
[r24 release];
[r22 release];
[r21 release];
[r20 release];
[r19 release];
return;
}
Decryption Code (Assembly):
-[RCTAesCrypto decrypt:appkey:gIv:resolver:rejecter:]:
00000001006b4e94 stp x28, x27, [sp, #-0x60]! ; Objective C Implementation defined at 0x10172eb70 (instance method), DATA XREF=0x10172eb70
00000001006b4e98 stp x26, x25, [sp, #0x10]
00000001006b4e9c stp x24, x23, [sp, #0x20]
00000001006b4ea0 stp x22, x21, [sp, #0x30]
00000001006b4ea4 stp x20, x19, [sp, #0x40]
00000001006b4ea8 stp fp, lr, [sp, #0x50]
00000001006b4eac add fp, sp, #0x50
00000001006b4eb0 mov x22, x6
00000001006b4eb4 mov x23, x5
00000001006b4eb8 mov x21, x4
00000001006b4ebc mov x20, x3
00000001006b4ec0 mov x0, x2 ; argument "instance" for method imp___stubs__objc_retain
00000001006b4ec4 bl imp___stubs__objc_retain ; objc_retain
00000001006b4ec8 mov x19, x0
00000001006b4ecc mov x0, x20 ; argument "instance" for method imp___stubs__objc_retain
00000001006b4ed0 bl imp___stubs__objc_retain ; objc_retain
00000001006b4ed4 mov x20, x0
00000001006b4ed8 mov x0, x21 ; argument "instance" for method imp___stubs__objc_retain
00000001006b4edc bl imp___stubs__objc_retain ; objc_retain
00000001006b4ee0 mov x21, x0
00000001006b4ee4 mov x0, x22 ; argument "instance" for method imp___stubs__objc_retain
00000001006b4ee8 bl imp___stubs__objc_retain ; objc_retain
00000001006b4eec mov x22, x0
00000001006b4ef0 adrp x27, #0x101880000
00000001006b4ef4 ldr x25, [x27, #0x160] ; objc_cls_ref_SecurityUtil,__objc_class_SecurityUtil_class
00000001006b4ef8 adrp x8, #0x10186e000 ; &@selector(sharedMenuController)
00000001006b4efc ldr x24, [x8, #0x3a0] ; "decryptAESNString:app_key:gIv:",@selector(decryptAESNString:app_key:gIv:)
00000001006b4f00 mov x0, x23 ; argument "instance" for method imp___stubs__objc_retain
00000001006b4f04 bl imp___stubs__objc_retain ; objc_retain
00000001006b4f08 mov x23, x0
00000001006b4f0c mov x0, x25 ; argument "instance" for method imp___stubs__objc_msgSend
00000001006b4f10 mov x1, x24 ; argument "selector" for method imp___stubs__objc_msgSend
00000001006b4f14 mov x2, x19
00000001006b4f18 mov x3, x20
00000001006b4f1c mov x4, x21
00000001006b4f20 bl imp___stubs__objc_msgSend ; objc_msgSend
00000001006b4f24 mov fp, fp
00000001006b4f28 bl imp___stubs__objc_retainAutoreleasedReturnValue ; objc_retainAutoreleasedReturnValue
00000001006b4f2c mov x25, x0
00000001006b4f30 adrp x8, #0x101861000 ; &@selector(dealloc)
00000001006b4f34 ldr x1, [x8, #0x58] ; argument "selector" for method imp___stubs__objc_msgSend, "length",@selector(length)
00000001006b4f38 bl imp___stubs__objc_msgSend ; objc_msgSend
00000001006b4f3c mov x26, x0
00000001006b4f40 mov x0, x25 ; argument "instance" for method imp___stubs__objc_release
00000001006b4f44 bl imp___stubs__objc_release ; objc_release
Decryption Code (Obj C):
-(void)decrypt:(void *)arg2 appkey:(void *)arg3 gIv:(void *)arg4 resolver:(void *)arg5 rejecter:(void *)arg6 {
var_50 = r28;
stack[-88] = r27;
r31 = r31 + 0xffffffffffffffa0;
var_40 = r26;
stack[-72] = r25;
var_30 = r24;
stack[-56] = r23;
var_20 = r22;
stack[-40] = r21;
var_10 = r20;
stack[-24] = r19;
saved_fp = r29;
stack[-8] = r30;
r19 = [arg2 retain];
r20 = [arg3 retain];
r21 = [arg4 retain];
r22 = [arg6 retain];
r23 = [arg5 retain];
r0 = [SecurityUtil decryptAESNString:r19 app_key:r20 gIv:r21];
r29 = &saved_fp;
r0 = [r0 retain];
r26 = [r0 length];
[r0 release];
if (r26 == 0x0) {
(*(r22 + 0x10))(r22, @"ERROR", @"decrypt failed", 0x0);
}
(*(r23 + 0x10))(r23, [[SecurityUtil decryptAESNString:r19 app_key:r20 gIv:r21] retain]);
[r23 release];
[r24 release];
[r22 release];
[r21 release];
[r20 release];
[r19 release];
return;
}
From the above analysis, it can be figured out that the handleSessionVals is used in the encryption and decryption of the payload; it is passed as both the key and iv.
Dynamic Analysis of the Application
Proxying the app through Burpsuite, a sample request payload for the UserLoginAction
is shown below:
POST /MVAppAPI/UserSvc HTTP/1.1
Host: *****.vodafone.com.gh
Cookie: visid_incap_2779200=fGaJ****TC8gi1Asypddu; visid_incap_2779192=yX5OR1yhT***J0jIO4sGROSVst
Accept: application/json, text/plain, */*
Content-Type: application/json
User-Agent: myvodafoneapp/85 CFNetwork/1237 Darwin/20.4.0
Accept-Language: en-gb
Content-Length: 170
Accept-Encoding: gzip, deflate
Connection: close
{"username":"****","password":"****","action":"loginToAccount","os":"iOS v4.3.2","udid":"***"}
The response payload for the UserLoginAction
contains the SESSION variables need to generate the AES Key.
"SESSION":{"session":"cf31ac47-**-46a6-bea3-**","secret":"*****-68ab-4303-b400-d0620751e51b","key":"cb4****f-a7af-4cff-****-03ab9c584563"},"RESPONSEMESSAGE":"Successfully processed"
Any subsequent request made after the UserLoginAction
uses an encrypted payload, which is decrypted using the AES Key.
POST /MVAppAPI/User HTTP/2
Host: ***.vodafone.com.gh
Accept: application/json, text/plain, */*
Content-Type: application/json
Accept-Encoding: gzip, deflate
User-Agent: myvodafoneapp/85 CFNetwork/1237 Darwin/20.4.0
Username: ****
Session: cf31ac474f7d46a6bea3ef1c2bbf5d4b
Accept-Language: en-gb
Content-Length: 150
{"requestBody":"e1jzR6XK+kEQHazEFOvFUOTqx/eDkFZ2MvbiUsXEWiejd/ENUdLt1G0aIMtnqEmP\r\n35QQIJ24dpBv4yIiEm2lyF+FLX6UAdtXmJA9GDBhVaL+rMTT1N7RLK9e3Ov9mes5"}
A session value is added as part of the headers; Session: cf31ac474f7d46a6bea3ef1c2bbf5d4b
. The value for the session key is the same as the SESSION.session variable with all the -
replaced with
.
A sample encrypted request payload is shown below:
POST /MVAppAPI/UserSvc HTTP/2
Host: ****.vodafone.com.gh
Cookie: incap_ses_1700_2779192=Tsu/V8sg8W0inf4mEZ2XF19YQWMAAAAARfvc5voyaCHQ85F5P4wG9A==; visid_incap_2779200=fGaJiBltQTWwMlBPhy/wUa5JL2MAAAAAQUIPAAAAAABOZ1nGbBQTC8gi1Asypddu; visid_incap_2779192=yX5OR1yhTBGPcy0+DGw0M3lJL2MAAAAAQUIPAAAAAACBiVyt59J0jIO4sGROSVst
Accept: application/json, text/plain, */*
Content-Type: application/json
Accept-Encoding: gzip, deflate
User-Agent: myvodafoneapp/85 CFNetwork/1237 Darwin/20.4.0
Username: ***
Session: cf31ac474f7d46a6bea3ef1c2bbf5d4b
Accept-Language: en-gb
Content-Length: 150
{"requestBody":"PY9DdDAVRfZat+uP/kVWa54ykhCmeS8N82elBHP3wiiLdQOnsbY+gb3DvQrTtdto\r\ngkoK/EirSeJ79ahHZ+p+A4lSCO7aOh697hn02G+PIP6Yf5jgLKGQL+DsW33NxQzC"}
For instance, the encrypted PY9DdDAVRfZat+uP/kVWa54ykhCmeS8N82elBHP3wiiLdQOnsbY+gb3DvQrTtdto\r\ngkoK/EirSeJ79ahHZ+p+A4lSCO7aOh697hn02G+PIP6Yf5jgLKGQL+DsW33NxQzC
decrypts to {"action":"getAccountServices","msisdn":"***","username":"****","os":"iOS v4.3.2"}
using the derived AES Key.