agent.js

  1. 'use strict'
  2. const WebSocket = require('ws')
  3. const stream = require('stream')
  4. const { sleep } = require('./util/sleep')
  5. /**
  6. * @typedef {object} CommandResult
  7. * @property {integer} id - ID
  8. * @property {boolean} success - command result
  9. */
  10. /**
  11. * @typedef {object} ShellExecResult
  12. * @property {integer} id - ID
  13. * @property {integer} exit-status
  14. * @property {string} output - command output
  15. * @property {boolean} success - command result
  16. */
  17. /**
  18. * @typedef {object} FridaPsResult
  19. * @property {integer} id - ID
  20. * @property {integer} exit-status -
  21. * @property {string} output - frida-ps output
  22. * @property {boolean} success - command result
  23. */
  24. /**
  25. * @typedef {object} AppListEntry
  26. * @property {string} applicationType
  27. * @property {string} bundleID
  28. * @property {integer} date
  29. * @property {integer} diskUsage
  30. * @property {boolean} isLaunchable
  31. * @property {string} name
  32. * @property {boolean} running
  33. */
  34. /**
  35. * @typedef {object} StatEntry
  36. * @property {integer} atime
  37. * @property {integer} ctime
  38. * @property {object[]} entries
  39. * @property {integer} entries[].atime
  40. * @property {integer} entries[].stime
  41. * @property {integer} entries[].gid
  42. * @property {integer} entries[].mode
  43. * @property {integer} entries[].mtime
  44. * @property {string} entries[].name
  45. * @property {integer} entries[].size
  46. * @property {integer} entries[].uid
  47. * @property {integer} gid
  48. * @property {integer} mode
  49. * @property {integer} mtime
  50. * @property {string} name
  51. * @property {integer} size
  52. * @property {integer} uid
  53. */
  54. /**
  55. * @typedef {object} ProvisioningProfileInfo
  56. * @property {string} name
  57. * @property {string} uuid
  58. * @property {string} teamId
  59. * @property {string[]} certs
  60. */
  61. /**
  62. * A connection to the agent running on an instance.
  63. *
  64. * Instances of this class
  65. * are returned from {@link Instance#agent} and {@link Instance#newAgent}. They
  66. * should not be created using the constructor.
  67. * @hideconstructor
  68. */
  69. class Agent {
  70. constructor (instance) {
  71. this.instance = instance
  72. this.connected = false
  73. this.uploading = false
  74. this.connectPromise = null
  75. this.id = 0
  76. this._keepAliveTimeout = null
  77. this._startKeepAliveTimeout = null
  78. this._lastPong = null
  79. this._lastPing = null
  80. }
  81. /**
  82. * Ensure the agent is connected.
  83. * @private
  84. */
  85. async connect () {
  86. this.pendingConnect = true
  87. if (!this.connected) {
  88. return await this.reconnect()
  89. }
  90. }
  91. /**
  92. * Ensure the agent is disconnected, then connect the agent.
  93. * @private
  94. */
  95. async reconnect () {
  96. if (this.connected) this.disconnect()
  97. if (this.connectPromise) return this.connectPromise
  98. this.connectPromise = await (async () => {
  99. while (this.pendingConnect) {
  100. try {
  101. await this._connect()
  102. break
  103. } catch (err) {
  104. if (err.stack.includes('Instance likely does not exist')) {
  105. throw err
  106. }
  107. if (err.stack.includes('self-signed certificate')) {
  108. throw err
  109. }
  110. if (err.stack.includes('unexpected server response (502)')) {
  111. // 'Error: unexpected server response (502)' means the device is not likely up yet
  112. await sleep(10 * 1000)
  113. }
  114. if (err.stack.includes('closed before the connection')) {
  115. // Do nothing this is normal when trying to settle a connection for a vm coming up
  116. } else {
  117. await sleep(7.5 * 1000)
  118. }
  119. }
  120. }
  121. this.connectPromise = null
  122. })()
  123. return this.connectPromise
  124. }
  125. async _connect () {
  126. this.pending = new Map()
  127. const endpoint = await this.instance.agentEndpoint()
  128. if (!endpoint) {
  129. this.pendingConnect = false
  130. throw new Error('Instance likely does not exist')
  131. }
  132. // Detect if a disconnection happened before we were able to get the agent endpoint.
  133. if (!this.pendingConnect) throw new Error('connection cancelled')
  134. const ws = new WebSocket(
  135. /^https/.test(endpoint)
  136. ? endpoint.replace(/^https/, 'wss')
  137. : /^http/.test(endpoint)
  138. ? endpoint.replace(/^http/, 'ws')
  139. : endpoint
  140. )
  141. this.ws = ws
  142. ws.on('message', data => {
  143. try {
  144. let message
  145. let id = -1
  146. if (typeof data === 'string') {
  147. message = JSON.parse(data)
  148. id = message.id
  149. } else if (Buffer.isBuffer(data)) {
  150. try {
  151. message = JSON.parse(data.toString('utf8'))
  152. id = message.id
  153. } catch (e) {
  154. if (data.length >= 8) {
  155. id = data.readUInt32LE(0)
  156. message = data.slice(8)
  157. }
  158. }
  159. }
  160. if (id === -1) {
  161. throw new Error(`handler not found for id: ${id}`)
  162. }
  163. const handler = this.pending.get(id)
  164. if (handler) {
  165. // will work regardless of whether handler returns a promise
  166. Promise.resolve(handler(null, message)).then(shouldDelete => {
  167. if (shouldDelete) this.pending.delete(id)
  168. })
  169. }
  170. } catch (err) {
  171. console.error('error in agent message handler', err)
  172. }
  173. })
  174. ws.on('close', (code, _reason) => {
  175. this.pending.forEach(handler => {
  176. handler(new Error(`disconnected with code ${code}`))
  177. })
  178. this.pending = new Map()
  179. this._disconnect()
  180. })
  181. return await new Promise((resolve, reject) => {
  182. ws.once('open', () => {
  183. if (this.ws !== ws) {
  184. try {
  185. ws.close()
  186. } catch (e) {
  187. // Swallow ws.close() errors.
  188. }
  189. reject(new Error('connection cancelled'))
  190. return
  191. }
  192. ws.on('error', err => {
  193. this.pending.forEach(handler => {
  194. handler(err)
  195. })
  196. this.pending = new Map()
  197. if (this.ws === ws) {
  198. this._disconnect()
  199. } else {
  200. try {
  201. ws.close()
  202. } catch (e) {
  203. // Swallow ws.close() errors.
  204. }
  205. }
  206. console.error('error in agent socket', err)
  207. })
  208. resolve()
  209. })
  210. ws.once('error', err => {
  211. if (this.ws === ws) {
  212. this._disconnect()
  213. } else {
  214. try {
  215. ws.close()
  216. } catch (e) {
  217. // Swallow ws.close() errors.
  218. }
  219. }
  220. reject(err)
  221. })
  222. })
  223. .then(() => {
  224. this.pendingConnect = false
  225. this.connected = true
  226. clearTimeout(this._startKeepAliveTimeout)
  227. this._startKeepAlive()
  228. })
  229. .catch(async err => {
  230. await this.instance.update()
  231. throw err
  232. })
  233. }
  234. _startKeepAlive () {
  235. if (!this.connected) return
  236. const ws = this.ws
  237. // clean up any existing keepalive timers before registering new ones. This prevents a bug that occurs if _startKeepAlive() is invoked multiple times.
  238. // _startKeepAlive() invoked once -> this._keepAliveTimeout is registered in state with the pong listener relying on that state to clear it.
  239. // _startKeepAlive() invoked second -> overwrites this._keepAliveTimeout.
  240. // Now, when original pong listener goes to clear timer based on this._keepAliveTimeout state, it has lost the reference to the first timer which results in it blowing up at 10 seconds
  241. this._stopKeepAlive()
  242. ws.ping()
  243. this._keepAliveTimeout = setTimeout(() => {
  244. if (this.ws !== ws) {
  245. try {
  246. ws.close()
  247. } catch (e) {
  248. // Swallow ws.close() errors.
  249. }
  250. return
  251. }
  252. const err = new Error('Agent did not get a response to ping in 10 seconds, disconnecting.')
  253. console.error('Agent did not get a response to ping in 10 seconds, disconnecting.')
  254. this.pending.forEach(handler => {
  255. handler(err)
  256. })
  257. this.pending = new Map()
  258. this._disconnect()
  259. }, 10 * 1000)
  260. ws.once('pong', async () => {
  261. if (ws !== this.ws) {
  262. return
  263. }
  264. clearTimeout(this._keepAliveTimeout)
  265. this._keepAliveTimeout = null
  266. if (!this.uploading) {
  267. // use arrow function to ensure the "this" binding references the Agent context, NOT a Timer.
  268. this._startKeepAliveTimeout = setTimeout(() => this._startKeepAlive(), 10 * 1000)
  269. }
  270. })
  271. }
  272. _stopKeepAlive () {
  273. if (this._startKeepAliveTimeout) {
  274. clearTimeout(this._startKeepAliveTimeout)
  275. this._startKeepAliveTimeout = null
  276. }
  277. if (this._keepAliveTimeout) {
  278. clearTimeout(this._keepAliveTimeout)
  279. this._keepAliveTimeout = null
  280. }
  281. }
  282. /**
  283. * Disconnect an agent connection. This is usually only required if a new
  284. * agent connection has been created and is no longer needed, for example
  285. * if the `crashListener` in the example at {@link Agent#crashes} is not
  286. * needed anymore.
  287. * @example
  288. * agent.disconnect();
  289. */
  290. disconnect () {
  291. this.pendingConnect = false
  292. this._disconnect()
  293. }
  294. _disconnect () {
  295. this.connected = false
  296. this._stopKeepAlive()
  297. if (this.ws) {
  298. try {
  299. this.ws.close()
  300. } catch (e) {
  301. // Swallow ws.close() errors.
  302. }
  303. this.ws = null
  304. }
  305. }
  306. /**
  307. * Send a command to the agent.
  308. *
  309. * When the command is responded to with an error, the error is thrown.
  310. * When the command is responded to with success, the handler callback is
  311. * called with the response as an argument.
  312. *
  313. * If the callback returns a value, that value will be returned from
  314. * `command`; otherwise nothing will happen until the next response to the
  315. * command. If the callback throws an exception, that exception will be
  316. * thrown from `command`.
  317. *
  318. * If no callback is specified, it is equivalent to specifying the callback
  319. * `(response) => response`.
  320. *
  321. * @param {string} type - passed in the `type` field of the agent command
  322. * @param {string} op - passed in the `op` field of the agent command
  323. * @param {Object} params - any other parameters to include in the command
  324. * @param {function} [handler=(response) => response] - the handler callback
  325. * @param {function} [uploadHandler] - a kludge for file uploads to work
  326. * @private
  327. */
  328. async command (type, op, params, handler, uploadHandler) {
  329. if (handler === undefined) handler = response => response
  330. const id = this.id
  331. this.id++
  332. const message = Object.assign({ type, op, id }, params)
  333. while (!this.ws) {
  334. await this.connect()
  335. }
  336. this.ws.send(JSON.stringify(message))
  337. if (uploadHandler) uploadHandler(id)
  338. return await new Promise((resolve, reject) => {
  339. this.pending.set(id, async (err, response) => {
  340. if (err) {
  341. reject(err)
  342. return
  343. }
  344. if (response.error) {
  345. reject(Object.assign(new Error(), response.error))
  346. return
  347. }
  348. try {
  349. const result = await handler(response)
  350. if (result !== undefined) {
  351. resolve(result)
  352. return true // stop calling us
  353. }
  354. return false
  355. } catch (e) {
  356. reject(e)
  357. return true
  358. }
  359. })
  360. })
  361. }
  362. sendBinaryData (id, data) {
  363. const idBuffer = Buffer.alloc(8, 0)
  364. idBuffer.writeUInt32LE(id, 0)
  365. if (data) this.ws.send(Buffer.concat([idBuffer, data]))
  366. else this.ws.send(idBuffer)
  367. }
  368. /**
  369. * Wait for the instance to be ready to use. On iOS, this will wait until Springboard has launched.
  370. * @example
  371. * let agent = await instance.agent();
  372. * await agent.ready();
  373. */
  374. async ready () {
  375. await this.command('app', 'ready')
  376. }
  377. /**
  378. * Uninstalls the app with the given bundle ID.
  379. * @param {string} bundleID - The bundle ID of the app to uninstall.
  380. * @param {Agent~progressCallback} progress - The progress callback.
  381. * @example
  382. * await agent.uninstall('com.corellium.demoapp', (progress, status) => {
  383. * console.log(progress, status);
  384. * });
  385. */
  386. async uninstall (bundleID, progress) {
  387. await this.command('app', 'uninstall', { bundleID }, message => {
  388. if (message.success) return message
  389. if (progress && message.progress) progress(message.progress, message.status)
  390. })
  391. }
  392. /**
  393. * Launches the app with the given bundle ID.
  394. * @param {string} bundleID - The bundle ID of the app to launch.
  395. * @example
  396. * await agent.run("com.corellium.demoapp");
  397. */
  398. async run (bundleID) {
  399. await this.command('app', 'run', { bundleID })
  400. }
  401. /**
  402. * Executes a given command
  403. * @param {string} cmd - The cmd to execute
  404. * @return {Promise<ShellExecResult>}
  405. * @example
  406. * await agent.shellExec("uname");
  407. */
  408. async shellExec (cmd) {
  409. return await this.command('app', 'shellExec', { cmd })
  410. }
  411. /**
  412. * Launches the app with the given bundle ID.
  413. * @param {string} bundleID - The bundle ID of the app to launch, for android this is the package name.
  414. * @param {string} activity fully qualified activity to launch from bundleID
  415. * @example
  416. * await agent.runActivity('com.corellium.test.app', 'com.corellium.test.app/com.corellium.test.app.CrashActivity');
  417. */
  418. async runActivity (bundleID, activity) {
  419. await this.command('app', 'run', { bundleID, activity })
  420. }
  421. /**
  422. * Kill the app with the given bundle ID, if it is running.
  423. * @param {string} bundleID - The bundle ID of the app to kill.
  424. * @example
  425. * await agent.kill("com.corellium.demoapp");
  426. */
  427. async kill (bundleID) {
  428. await this.command('app', 'kill', { bundleID })
  429. }
  430. /**
  431. * Returns an array of installed apps.
  432. * @return {Promise<AppListEntry[]>}
  433. * @example
  434. * let appList = await agent.appList();
  435. * for (app of appList) {
  436. * console.log('Found installed app ' + app['bundleID']);
  437. * }
  438. */
  439. async appList () {
  440. const { apps } = await this.command('app', 'list')
  441. return apps
  442. }
  443. /**
  444. * Gets information about the file at the specified path. Fields are atime, mtime, ctime (in seconds after the epoch), size, mode (see mode_t in man 2 stat), uid, gid. If the path specified is a directory, an entries field will be present with
  445. * the same structure (and an additional name field) for each immediate child of the directory.
  446. * @return {Promise<StatEntry>}
  447. * @example
  448. * let scripts = await agent.stat('/data/corellium/frida/scripts/');
  449. */
  450. async stat (path) {
  451. const response = await this.command('file', 'stat', { path })
  452. return response.stat
  453. }
  454. /**
  455. * A callback for file upload progress messages. Can be passed to {@link Agent#upload} and {@link Agent#installFile}
  456. * @callback Agent~uploadProgressCallback
  457. * @param {number} bytes - The number of bytes that has been uploaded.
  458. */
  459. /**
  460. * A callback for progress messages. Can be passed to {@link Agent#install}, {@link Agent#installFile}, {@link Agent#uninstall}.
  461. * @callback Agent~progressCallback
  462. * @param {number} progress - The progress, as a number between 0 and 1.
  463. * @param {string} status - The current status.
  464. */
  465. /**
  466. * Installs an app. The app's IPA must be available on the VM's filesystem. A progress callback may be provided.
  467. *
  468. * @see {@link Agent#upload} to upload a file to the VM's filesystem
  469. * @see {@link Agent#installFile} to handle both the upload and install
  470. *
  471. * @param {string} path - The path of the IPA on the VM's filesystem.
  472. * @param {Agent~progressCallback} [progress] - An optional callback that
  473. * will be called with information on the progress of the installation.
  474. * @async
  475. *
  476. * @example
  477. * await agent.install('/var/tmp/temp.ipa', (progress, status) => {
  478. * console.log(progress, status);
  479. * });
  480. */
  481. async install (path, progress) {
  482. await this.command('app', 'install', { path }, message => {
  483. if (message.success) return message
  484. if (progress && message.progress) progress(message.progress, message.status)
  485. })
  486. }
  487. /**
  488. * Returns an array of Mobile Configuration profile IDs
  489. * @return {Promise<string[]>}
  490. * @example
  491. * let profiles = await agent.profileList();
  492. * for (p of profiles) {
  493. * console.log('Found configuration profile: ' + p);
  494. * }
  495. */
  496. async profileList () {
  497. const { profiles } = await this.command('profile', 'list')
  498. return profiles
  499. }
  500. /**
  501. * Installs Mobile Configuration profile
  502. * @param {Buffer} profile - profile binary
  503. * @example
  504. * var profile = fs.readFileSync(path.join(__dirname, "myprofile.mobileconfig"));
  505. * await agent.installProfile(profile);
  506. */
  507. async installProfile (profile) {
  508. await this.command('profile', 'install', {
  509. profile: Buffer.from(profile).toString('base64')
  510. })
  511. }
  512. /**
  513. * Deletes Mobile Configuration profile
  514. * @param {string} profileID - profile ID
  515. * @example
  516. * await agent.removeProfile('com.test.myprofile');
  517. */
  518. async removeProfile (profileID) {
  519. await this.command('profile', 'remove', { profileID })
  520. }
  521. /**
  522. * Gets Mobile Configuration profile binary
  523. * @param {string} profileID - profile ID
  524. * @return {Promise<Buffer>}
  525. * @example
  526. * var profile = await agent.getProfile('com.test.myprofile');
  527. */
  528. async getProfile (profileID) {
  529. const { profile } = await this.command('profile', 'get', { profileID })
  530. if (!profile) return null
  531. // eslint-disable-next-line new-cap
  532. return new Buffer.from(profile, 'base64')
  533. }
  534. /**
  535. * Returns an array of Provisioning profile descriptions
  536. * @return {Promise<ProvisioningProfileInfo[]>}
  537. * @example
  538. * let profiles = await agent.listProvisioningProfiles();
  539. * for (p of profiles) {
  540. * console.log(p['uuid']);
  541. * }
  542. */
  543. async listProvisioningProfiles () {
  544. const { profiles } = await this.command('provisioning', 'list')
  545. return profiles
  546. }
  547. /**
  548. * Installs Provisioning profile
  549. * @param {Buffer} profile - profile binary
  550. * @param {Boolean} trust - immediately trust installed profile
  551. * @example
  552. * var profile = fs.readFileSync(path.join(__dirname, "embedded.mobileprovision"));
  553. * await agent.installProvisioningProfile(profile, true);
  554. */
  555. async installProvisioningProfile (profile, trust = false) {
  556. await this.command('provisioning', 'install', {
  557. profile: Buffer.from(profile).toString('base64'),
  558. trust: trust
  559. })
  560. }
  561. /**
  562. * Deletes Provisioning profile
  563. * @param {string} profileID - profile ID
  564. * @example
  565. * await agent.removeProvisioningProfile('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa');
  566. */
  567. async removeProvisioningProfile (profileID) {
  568. await this.command('provisioning', 'remove', {
  569. uuid: profileID
  570. })
  571. }
  572. /**
  573. * Approves (makes trusted) profile which will be installed later in a future for example during app installation via Xcode.
  574. * @param {string} certID - profile ID
  575. * @param {string} profileID - profile ID
  576. * @example
  577. * await agent.preApproveProvisioningProfile('Apple Development: my@email.com (NKJDZ3DZJB)', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa');
  578. */
  579. async preApproveProvisioningProfile (certID, profileID) {
  580. await this.command('provisioning', 'preapprove', {
  581. cert: certID,
  582. uuid: profileID
  583. })
  584. }
  585. /**
  586. * Returns a temporary random filename on the VMs filesystem that by the
  587. * time of invocation of this method is guaranteed to be unique.
  588. * @return {Promise<string>}
  589. * @see example at {@link Agent#upload}
  590. */
  591. async tempFile () {
  592. const { path } = await this.command('file', 'temp')
  593. return path
  594. }
  595. /**
  596. * Reads from the specified stream and uploads the data to a file on the VM.
  597. * @param {string} path - The file path to upload the data to.
  598. * @param {ReadableStream} stream - The stream to read the file data from.
  599. * @param {Agent~uploadProgressCallback} progress - The callback for install progress information.
  600. * @example
  601. * const tmpName = await agent.tempFile();
  602. * await agent.upload(tmpName, fs.createReadStream('test.ipa'));
  603. */
  604. async upload (path, stream, progress) {
  605. // Temporarily stop the keepalive as the upload appears to backlog
  606. // the control packets (ping/pong) at the proxy which can cause issues
  607. // and a disconnect
  608. this._stopKeepAlive()
  609. this.uploading = true
  610. await this.command(
  611. 'file',
  612. 'upload',
  613. { path },
  614. message => {
  615. // This is hit after the upload is completed and the agent
  616. // on the other end sends the reply packet of success/fail
  617. // Restart the keepalive as the upload buffer should be cleared
  618. clearTimeout(this._startKeepAliveTimeout)
  619. this._startKeepAlive()
  620. this.uploading = false
  621. // Pass back the message to the command() function to prevent
  622. // blocking or returning an invalid value
  623. return message
  624. },
  625. id => {
  626. let total = 0
  627. stream.on('data', data => {
  628. this.sendBinaryData(id, data)
  629. total += data.length
  630. if (progress) progress(total)
  631. })
  632. stream.on('end', () => {
  633. this.sendBinaryData(id)
  634. })
  635. }
  636. )
  637. }
  638. /**
  639. * Downloads the file at the given path from the VM's filesystem. Returns a node ReadableStream.
  640. * @param {string} path - The path of the file to download.
  641. * @return {Readable}
  642. * @example
  643. * const dl = agent.download('/var/tmp/test.log');
  644. * dl.pipe(fs.createWriteStream('test.log'));
  645. */
  646. download (path) {
  647. let command
  648. const agent = this
  649. return new stream.Readable({
  650. read () {
  651. if (command) return
  652. command = agent.command('file', 'download', { path }, message => {
  653. if (!Buffer.isBuffer(message)) return
  654. if (message.length === 0) return true
  655. this.push(message)
  656. })
  657. command.then(() => this.push(null)).catch(err => this.emit('error', err))
  658. }
  659. })
  660. }
  661. /**
  662. * Reads a packaged app from the provided stream, uploads the app to the VM
  663. * using {@link Agent#upload}, and installs it using {@link Agent#install}.
  664. * @param {ReadableStream} stream - The app to install, the stream will be closed after it is uploaded.
  665. * @param {Agent~progressCallback} installProgress - The callback for install progress information.
  666. * @param {Agent~uploadProgressCallback} uploadProgress - The callback for file upload progress information.
  667. * @example
  668. * await agent.installFile(fs.createReadStream('test.ipa'), (installProgress, installStatus) => {
  669. * console.log(installProgress, installStatus);
  670. * });
  671. */
  672. async installFile (stream, installProgress, uploadProgress) {
  673. const path = await this.tempFile()
  674. await this.upload(path, stream, uploadProgress)
  675. stream.on('close', () => {
  676. stream.destroy()
  677. })
  678. await this.install(path, installProgress)
  679. try {
  680. await this.stat(path)
  681. await this.deleteFile(path)
  682. } catch (err) {
  683. if (!err.message.includes('Stat of file')) {
  684. throw err
  685. }
  686. }
  687. }
  688. /**
  689. * Delete the file at the specified path on the VM's filesystem.
  690. * @param {string} path - The path of the file on the VM's filesystem to delete.
  691. * @example
  692. * await agent.deleteFile('/var/tmp/test.log');
  693. */
  694. async deleteFile (path) {
  695. const response = await this.command('file', 'delete', { path })
  696. return response.path
  697. }
  698. /**
  699. * Change file attributes of the file at the specified path on the VM's filesystem.
  700. * @param {string} path - The path of the file on the VM's filesystem to delete.
  701. * @param {Object} attributes - An object whose members and values are the file attributes to change and what to change them to respectively. File attributes path, mode, uid and gid are supported.
  702. * @return {Promise<CommandResult>}
  703. * @example
  704. * await agent.changeFileAttributes(filePath, {mode: 511});
  705. */
  706. async changeFileAttributes (path, attributes) {
  707. const response = await this.command('file', 'modify', { path, attributes })
  708. return response
  709. }
  710. /**
  711. * Subscribe to crash events for the app with the given bundle ID. The callback will be called as soon as the agent finds a new crash log.
  712. *
  713. * The callback takes two parameters:
  714. * - `err`, which is undefined unless an error occurred setting up or waiting for crash logs
  715. * - `crash`, which contains the full crash report data
  716. *
  717. * **Note:** Since this method blocks the communication channel of the
  718. * agent to wait for crash reports, a new {@link Agent} connection should
  719. * be created with {@link Instance#newAgent}.
  720. *
  721. * @see Agent#disconnect
  722. *
  723. * @example
  724. * const crashListener = await instance.newAgent();
  725. * crashListener.crashes("com.corellium.demoapp", (err, crashReport) => {
  726. * if (err) {
  727. * console.error(err);
  728. * return;
  729. * }
  730. * console.log(crashReport);
  731. * });
  732. */
  733. async crashes (bundleID, callback) {
  734. await this.command('crash', 'subscribe', { bundleID }, async message => {
  735. const path = message.file
  736. const crashReport = await new Promise(resolve => {
  737. const stream = this.download(path)
  738. const buffers = []
  739. stream.on('data', data => {
  740. buffers.push(data)
  741. })
  742. stream.on('end', () => {
  743. resolve(Buffer.concat(buffers))
  744. })
  745. })
  746. await this.deleteFile(path)
  747. callback(null, crashReport.toString('utf8'))
  748. })
  749. }
  750. /** Locks the device software-wise.
  751. * @example
  752. * await agent.lockDevice();
  753. */
  754. async lockDevice () {
  755. await this.command('system', 'lock')
  756. }
  757. /** Unlocks the device software-wise.
  758. * @example
  759. * awaitagent.unlockDevice();
  760. */
  761. async unlockDevice () {
  762. await this.command('system', 'unlock')
  763. }
  764. /** Enables UI Automation.
  765. * @example
  766. * await agent.enableUIAutomation();
  767. */
  768. async enableUIAutomation () {
  769. await this.command('system', 'enableUIAutomation')
  770. }
  771. /** Disables UI Automation.
  772. * @example
  773. * await agent.disableUIAutomation();
  774. */
  775. async disableUIAutomation () {
  776. await this.command('system', 'disableUIAutomation')
  777. }
  778. /** Checks if SSL pinning is enabled. By default SSL pinning is disabled.
  779. * @returns {boolean}
  780. * @example
  781. * let enabled = await agent.isSSLPinningEnabled();
  782. * if (enabled) {
  783. * console.log("enabled");
  784. * } else {
  785. * console.log("disabled");
  786. * }
  787. */
  788. async isSSLPinningEnabled () {
  789. return (await this.command('system', 'isSSLPinningEnabled')).enabled
  790. }
  791. /** Enables SSL pinning.
  792. * @example
  793. * await agent.enableSSLPinning();
  794. */
  795. async enableSSLPinning () {
  796. await this.command('system', 'enableSSLPinning')
  797. }
  798. /** Disables SSL pinning.
  799. * @example
  800. * await agent.disableSSLPinning();
  801. */
  802. async disableSSLPinning () {
  803. await this.command('system', 'disableSSLPinning')
  804. }
  805. /** Shuts down the device.
  806. * @example
  807. * await agent.shutdown();
  808. */
  809. async shutdown () {
  810. await this.command('system', 'shutdown')
  811. }
  812. async acquireDisableAutolockAssertion () {
  813. await this.command('system', 'acquireDisableAutolockAssertion')
  814. }
  815. async releaseDisableAutolockAssertion () {
  816. await this.command('system', 'releaseDisableAutolockAssertion')
  817. }
  818. /** Connect device to WiFi.
  819. * @example
  820. * await agent.connectToWifi();
  821. */
  822. async connectToWifi () {
  823. await this.command('wifi', 'connect')
  824. }
  825. /** Disconnect device from WiFi.
  826. * @example
  827. * await agent.disconnectFromWifi();
  828. */
  829. async disconnectFromWifi () {
  830. await this.command('wifi', 'disconnect')
  831. }
  832. /** Get device property. */
  833. async getProp (property) {
  834. return await this.command('system', 'getprop', { property })
  835. }
  836. /**
  837. * Run frida on the device.
  838. * Please note that both arguments (pid and name) need to be provided as they are required by the Web UI.
  839. * @param {integer} pid
  840. * @param {string} name
  841. * @return {Promise<CommandResult>}
  842. * @example
  843. * await agent.runFrida(449, 'keystore');
  844. */
  845. async runFrida (pid, name) {
  846. return await this.command('frida', 'run-frida', {
  847. target_pid: pid.toString(),
  848. target_name: name.toString()
  849. })
  850. }
  851. /**
  852. * Run frida-ps on the device and return the command's output.
  853. * @return {Promise<FridaPsResult>}
  854. * @example
  855. * let procList = await agent.runFridaPs();
  856. * let lines = procList.output.trim().split('\n');
  857. * lines.shift();
  858. * lines.shift();
  859. * for (const line of lines) {
  860. * const [pid, name] = line.trim().split(/\s+/);
  861. * console.log(pid, name);
  862. * }
  863. */
  864. async runFridaPs () {
  865. return await this.command('frida', 'run-frida-ps')
  866. }
  867. /**
  868. * Run frida-kill on the device.
  869. * @return {Promise<CommandResult>}
  870. * @example
  871. * await agent.runFridaKill();
  872. */
  873. async runFridaKill () {
  874. return await this.command('frida', 'run-frida-kill')
  875. }
  876. }
  877. module.exports = Agent