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.