├── Jodel-Keyhack-v3 ├── frontend-src │ ├── assets │ │ └── .gitkeep │ ├── styles.less │ ├── environments │ │ ├── environment.prod.ts │ │ ├── environment.ts │ │ └── environment.hmr.ts │ ├── favicon.ico │ ├── proxy.conf.json │ ├── app │ │ ├── app.component.less │ │ ├── components │ │ │ ├── copy-input.component.html │ │ │ ├── copy-input.component.ts │ │ │ └── copy-input.component.less │ │ ├── app.module.ts │ │ ├── app.component.ts │ │ ├── app.component.spec.ts │ │ └── app.component.html │ ├── tsconfig.app.json │ ├── .editorconfig │ ├── tsconfig.spec.json │ ├── index.html │ ├── browserslist │ ├── tslint.json │ ├── hmr.ts │ ├── test.ts │ ├── main.ts │ ├── karma.conf.js │ └── polyfills.ts ├── .vscode │ ├── settings.json │ └── tasks.json ├── tsconfig.json ├── backend │ ├── test │ │ ├── hmac_test.py │ │ └── api_test.py │ ├── decrypt.py │ ├── r2instance.py │ └── server.py ├── requirements.txt ├── README.md ├── package.json ├── .gitignore ├── tslint.json └── angular.json ├── Jodel-Keyhack-Frida ├── extract_hmac_ios.js ├── extract_hmac.js ├── Readme.md └── extract_hmac.py ├── README.md ├── Jodel-Keyhack-v2 ├── jodel_ios.py ├── decrypt.py ├── jodel.py └── README.md ├── 03_Reversing_SSL-Pinning.md ├── 01_Reversing_App.md ├── 02_Reversing_HMAC.md └── 04_Reversing_Android_Email_Verification.md /Jodel-Keyhack-v3/frontend-src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Jodel-Keyhack-v3/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.venvPath": "venv", 3 | "python.linting.enabled": false 4 | } -------------------------------------------------------------------------------- /Jodel-Keyhack-v3/frontend-src/styles.less: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /Jodel-Keyhack-v3/frontend-src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | hmr: false 4 | }; 5 | -------------------------------------------------------------------------------- /Jodel-Keyhack-v3/frontend-src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JodelRaccoons/JodelReversing/HEAD/Jodel-Keyhack-v3/frontend-src/favicon.ico -------------------------------------------------------------------------------- /Jodel-Keyhack-v3/frontend-src/proxy.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "/api": { 3 | "target": "http://127.0.0.1:5000/", 4 | "secure": false 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Jodel-Keyhack-v3/frontend-src/app/app.component.less: -------------------------------------------------------------------------------- 1 | .container { 2 | height: 100%; 3 | display: flex; 4 | align-items: center; 5 | width: 550px; 6 | margin: auto; 7 | justify-content: center; 8 | } 9 | -------------------------------------------------------------------------------- /Jodel-Keyhack-v3/frontend-src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "types": ["node"] 6 | }, 7 | "exclude": [ 8 | "test.ts", 9 | "**/*.spec.ts" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /Jodel-Keyhack-v3/frontend-src/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /Jodel-Keyhack-v3/frontend-src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "test.ts", 12 | "polyfills.ts" 13 | ], 14 | "include": [ 15 | "**/*.spec.ts", 16 | "**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /Jodel-Keyhack-Frida/extract_hmac_ios.js: -------------------------------------------------------------------------------- 1 | Interceptor.attach(ObjC.classes.JDLAPIRequestHMACHashBuilder['- secretKey'].implementation, { 2 | onLeave: function (retval) { 3 | console.log('[+] HMAC-Key: ', new ObjC.Object(ptr(retval)).toString()); 4 | console.log('[+] Version: ' + ObjC.classes.NSBundle.mainBundle().objectForInfoDictionaryKey_("CFBundleShortVersionString").UTF8String()) 5 | }, 6 | }); 7 | -------------------------------------------------------------------------------- /Jodel-Keyhack-v3/frontend-src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | R2 HMAC Extractor 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Jodel-Keyhack-v3/frontend-src/browserslist: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # 5 | # For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed 6 | 7 | last 2 Chrome versions 8 | not IE 9-11 9 | -------------------------------------------------------------------------------- /Jodel-Keyhack-v3/frontend-src/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Jodel-Keyhack-v3/frontend-src/app/components/copy-input.component.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 |
9 | 10 |
11 |
12 | -------------------------------------------------------------------------------- /Jodel-Keyhack-v3/frontend-src/app/components/copy-input.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, ViewEncapsulation } from "@angular/core"; 2 | 3 | @Component({ 4 | selector: "app-copy-input", 5 | templateUrl: "./copy-input.component.html", 6 | styleUrls: ["./copy-input.component.less"], 7 | encapsulation: ViewEncapsulation.None 8 | }) 9 | export class CopyInputComponent { 10 | @Input() value = ""; 11 | 12 | async copy(value) { 13 | await (navigator as any).clipboard.writeText(value); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Jodel-Keyhack-v3/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "module": "es2015", 9 | "moduleResolution": "node", 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "importHelpers": true, 13 | "target": "es5", 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ], 17 | "lib": [ 18 | "es2018", 19 | "dom" 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Jodel-Keyhack-v3/frontend-src/hmr.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationRef, NgModuleRef } from "@angular/core"; 2 | import { createNewHosts } from "@angularclass/hmr"; 3 | 4 | export const hmrBootstrap = ( 5 | module: any, 6 | bootstrap: () => Promise> 7 | ) => { 8 | let ngModule: NgModuleRef; 9 | module.hot.accept(); 10 | bootstrap().then(mod => (ngModule = mod)); 11 | module.hot.dispose(() => { 12 | const appRef: ApplicationRef = ngModule.injector.get(ApplicationRef); 13 | const elements = appRef.components.map(c => c.location.nativeElement); 14 | const makeVisible = createNewHosts(elements); 15 | ngModule.destroy(); 16 | makeVisible(); 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /Jodel-Keyhack-v3/backend/test/hmac_test.py: -------------------------------------------------------------------------------- 1 | import jodel_api 2 | 3 | if __name__ == '__main__': 4 | 5 | "a4a8d4d7b09736a0f65596a868cc6fd620920fb0" \ 6 | "PUT@/api/v2/posts/5d239cac23e3e1001bfbb1cf/upvote, PUT" \ 7 | "%" \ 8 | "api.go-tellm.com" \ 9 | "%" \ 10 | "443" \ 11 | "%" \ 12 | "/api/v2/posts/5d239cac23e3e1001bfbb1cf/upvote" \ 13 | "%" \ 14 | "12259522-a84741ac-a4e89ecc-acdc-4aba-aaf5-4e51572ea674" \ 15 | "%" \ 16 | "49.8831;8.6690" \ 17 | "%" \ 18 | "2019-07-08T19:44:29Z" \ 19 | "%" \ 20 | "home" \ 21 | "%" \ 22 | "false" \ 23 | "%" \ 24 | "{}" \ 25 | "Returned: 463582933e3b5a1e0a1a9cf89ea8842924f2701f" 26 | -------------------------------------------------------------------------------- /Jodel-Keyhack-v3/frontend-src/app/components/copy-input.component.less: -------------------------------------------------------------------------------- 1 | @import "~ng-zorro-antd/input/style/mixin.less"; 2 | .custom-copy-input { 3 | input { 4 | border-right: 0; 5 | &:hover, 6 | &:focus { 7 | border-color: @input-border-color; 8 | box-shadow: none; 9 | } 10 | } 11 | 12 | .ant-input-group-addon { 13 | transition: all 0.05s; 14 | padding: 0; 15 | line-height: 2; 16 | &:last-child { 17 | border-left: 1px solid @input-border-color; 18 | } 19 | &:hover { 20 | .hover; 21 | } 22 | &:active { 23 | .active; 24 | } 25 | 26 | .button-ify { 27 | padding: 0 11px; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Jodel-Keyhack-v3/requirements.txt: -------------------------------------------------------------------------------- 1 | -e git+https://github.com/krokofant/apk-signature-verify.git@215b9a86979eb8e2a9ff97b7a8758e70ee361345#egg=apkverify 2 | asn1crypto==0.24.0 3 | autopep8==1.4.3 4 | certifi==2018.11.29 5 | chardet==3.0.4 6 | Click==7.0 7 | Flask==1.0.2 8 | future==0.17.1 9 | idna==2.8 10 | itsdangerous==1.1.0 11 | Jinja2==2.11.3 12 | -e git+https://github.com/Unbrick/jodel_api.git@be12ddcabd59b9fcd9ad08c1ff703a05b1d08b00#egg=jodel_api 13 | lxml==4.9.1 14 | MarkupSafe==1.1.0 15 | mock==2.0.0 16 | pbr==5.1.1 17 | protobuf==3.15.0 18 | pyaxmlparser==0.3.13 19 | pycodestyle==2.4.0 20 | r2pipe==1.2.0 21 | requests==2.21.0 22 | six==1.12.0 23 | urllib3==1.26.5 24 | varint==1.0.2 25 | Werkzeug==0.15.3 26 | -------------------------------------------------------------------------------- /Jodel-Keyhack-v3/frontend-src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /Jodel-Keyhack-v3/frontend-src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | hmr: false 8 | }; 9 | 10 | /* 11 | * For easier debugging in development mode, you can import the following file 12 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 13 | * 14 | * This import should be commented out in production mode because it will have a negative impact 15 | * on performance if an error is thrown. 16 | */ 17 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 18 | -------------------------------------------------------------------------------- /Jodel-Keyhack-v3/frontend-src/environments/environment.hmr.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | hmr: true 8 | }; 9 | 10 | /* 11 | * For easier debugging in development mode, you can import the following file 12 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 13 | * 14 | * This import should be commented out in production mode because it will have a negative impact 15 | * on performance if an error is thrown. 16 | */ 17 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 18 | -------------------------------------------------------------------------------- /Jodel-Keyhack-v3/frontend-src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from "@angular/core"; 2 | import { platformBrowserDynamic } from "@angular/platform-browser-dynamic"; 3 | import { AppModule } from "./app/app.module"; 4 | import { environment } from "./environments/environment"; 5 | import { hmrBootstrap } from "./hmr"; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | const bootstrap = () => platformBrowserDynamic().bootstrapModule(AppModule); 12 | 13 | if (environment.hmr) { 14 | if (module["hot"]) { 15 | hmrBootstrap(module, bootstrap); 16 | } else { 17 | console.error("HMR is not enabled for webpack-dev-server!"); 18 | console.log("Are you using the --hmr flag for ng serve?"); 19 | } 20 | } else { 21 | bootstrap().catch(err => console.log(err)); 22 | } 23 | -------------------------------------------------------------------------------- /Jodel-Keyhack-v3/frontend-src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { registerLocaleData } from "@angular/common"; 2 | import { HttpClientModule } from "@angular/common/http"; 3 | import en from "@angular/common/locales/en"; 4 | import { NgModule } from "@angular/core"; 5 | import { FormsModule } from "@angular/forms"; 6 | import { BrowserModule } from "@angular/platform-browser"; 7 | import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; 8 | import { en_US, NgZorroAntdModule, NZ_I18N } from "ng-zorro-antd"; 9 | import { AppComponent } from "./app.component"; 10 | import { CopyInputComponent } from "./components/copy-input.component"; 11 | 12 | registerLocaleData(en); 13 | 14 | @NgModule({ 15 | declarations: [AppComponent, CopyInputComponent], 16 | imports: [ 17 | BrowserModule, 18 | NgZorroAntdModule, 19 | FormsModule, 20 | HttpClientModule, 21 | BrowserAnimationsModule 22 | ], 23 | providers: [{ provide: NZ_I18N, useValue: en_US }], 24 | bootstrap: [AppComponent] 25 | }) 26 | export class AppModule {} 27 | -------------------------------------------------------------------------------- /Jodel-Keyhack-v3/frontend-src/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../coverage'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false 30 | }); 31 | }; -------------------------------------------------------------------------------- /Jodel-Keyhack-v3/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "windows": { 6 | "options": { 7 | "shell": { 8 | "executable": "powershell.exe" 9 | } 10 | } 11 | }, 12 | "tasks": [ 13 | { 14 | "label": "Create virtualenv", 15 | "type": "shell", 16 | "windows": { 17 | "command": "py -3 -m venv venv" 18 | }, 19 | "osx": { 20 | "command": "python3 -m venv venv" 21 | }, 22 | "presentation": { 23 | "echo": false 24 | }, 25 | "problemMatcher": [] 26 | }, 27 | { 28 | "label": "Activate virtualenv", 29 | "type": "shell", 30 | "windows": { 31 | "command": "venv\\Scripts\\activate", 32 | "options": { "shell": { "args": ["-NoExit"] } } 33 | }, 34 | "osx": { 35 | "command": ". venv/bin/activate" 36 | }, 37 | "presentation": { 38 | "echo": true, 39 | "panel": "new" 40 | }, 41 | "problemMatcher": [] 42 | } 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /Jodel-Keyhack-v3/frontend-src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "@angular/core"; 2 | import { UploadChangeParam, UploadFile } from "ng-zorro-antd"; 3 | 4 | export interface UploadResponse { 5 | package: string; 6 | version_name: string; 7 | version_code: string; 8 | signature_verified: boolean; 9 | is_jodel_signature: boolean; 10 | certs: string; 11 | hmac_key: string; 12 | key_status: { working: boolean }; 13 | error: false; 14 | message: string; 15 | } 16 | 17 | @Component({ 18 | selector: "app-root", 19 | templateUrl: "./app.component.html", 20 | styleUrls: ["./app.component.less"] 21 | }) 22 | export class AppComponent { 23 | successfulExtraction = false; 24 | extractionData: UploadResponse; 25 | fileList: UploadFile[] = []; 26 | 27 | fileChange(uploadChangeParam: UploadChangeParam) { 28 | const { type, file, fileList } = uploadChangeParam; 29 | if (type === "success") { 30 | this.extractionData = file.response; 31 | this.successfulExtraction = true; 32 | } else { 33 | this.successfulExtraction = false; 34 | this.fileList = fileList.slice(-1); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Jodel-Keyhack-v3/frontend-src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | 4 | describe('AppComponent', () => { 5 | beforeEach(async(() => { 6 | TestBed.configureTestingModule({ 7 | declarations: [ 8 | AppComponent 9 | ], 10 | }).compileComponents(); 11 | })); 12 | 13 | it('should create the app', () => { 14 | const fixture = TestBed.createComponent(AppComponent); 15 | const app = fixture.debugElement.componentInstance; 16 | expect(app).toBeTruthy(); 17 | }); 18 | 19 | it(`should have as title 'my-project'`, () => { 20 | const fixture = TestBed.createComponent(AppComponent); 21 | const app = fixture.debugElement.componentInstance; 22 | expect(app.title).toEqual('my-project'); 23 | }); 24 | 25 | it('should render title in a h1 tag', () => { 26 | const fixture = TestBed.createComponent(AppComponent); 27 | fixture.detectChanges(); 28 | const compiled = fixture.debugElement.nativeElement; 29 | expect(compiled.querySelector('h1').textContent).toContain('Welcome to my-project!'); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /Jodel-Keyhack-v3/README.md: -------------------------------------------------------------------------------- 1 | # Jodel Keyhack v3 (now with fancy angular gui) 2 | 3 | # Install on Windows 4 | 5 | (for Windows) Install package manager [Chocolatey](https://chocolatey.org/) for programs and 6 | [scoop](https://scoop.sh/) for dev tools 7 | 8 | 1. Install requirements 9 | - `scoop install radare2` 10 | - `choco install nodejs` 11 | - `choco install python3` 12 | 2. Create virtual env 13 | - `py -3 -m venv venv` 14 | 3. Activate virtualenv 15 | - `venv\Scripts\activate` 16 | 4. Install python deps 17 | - `pip install -r requirements.txt` 18 | 5. Build frontend 19 | - `npm ci` 20 | - `npm run build:prod` 21 | 6. Start backend 22 | - `python3 backend/server.py` 23 | 24 | # Install on macOS 25 | 26 | 1. Install requirements 27 | - `brew install radare2` 28 | - `brew install nodejs` 29 | - `brew install python3` 30 | 2. Create virtual env 31 | - `python3 -m venv venv` 32 | 3. Activate virtualenv 33 | - `. venv/bin/activate` 34 | 4. Install python deps 35 | - `pip install -r requirements.txt` 36 | 5. Build frontend 37 | - `npm ci` 38 | - `npm run build:prod` 39 | 6. Start backend 40 | - `python3 backend/server.py` 41 | -------------------------------------------------------------------------------- /Jodel-Keyhack-v3/backend/test/api_test.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | 3 | import jodel_api 4 | import hmac, hashlib 5 | 6 | from jodel_api import gcmhack 7 | 8 | if __name__ == '__main__': 9 | lat, lng, city = 48.148434, 11.567867, "Munich" 10 | account = gcmhack.AndroidAccount() 11 | sleep(5) 12 | token = account.get_push_token() 13 | j = jodel_api.JodelAccount(lat=lat, lng=lng, city=city, pushtoken=token) 14 | print('Account created') 15 | print('Verification: {}'.format(j.verify(android_account=account))) 16 | print(j.get_posts_popular()) 17 | print('Karma is: {}'.format(j.get_karma())) 18 | print(j.upvote('5d238be544a6a0001a0360d6')) 19 | print('Karma is: {}'.format(j.get_karma())) 20 | 21 | #req = 'POST%api.go-tellm.com%443%/api/v2/users/%%%2019-01-10T21:11:59Z%%{"location":{"country":"DE","city":"Heilbronn","loc_coordinates":{"lng":9.2070918,"lat":49.1208046},"loc_accuracy":16.581},"registration_data":{"channel":"","provider":"branch.io","campaign":"","feature":"","referrer_branch_id":"","referrer_id":""},"client_id":"81e8a76e-1e02-4d17-9ba0-8a7020261b26","device_uid":"573b2e648c7b3849f1a533167354f4752f0466010c72bdde378a5d856421b122","language":"de-DE"}' 22 | 23 | #signature = hmac.new('TNHfHCaBjTvtrjEFsAFQyrHapTHdKbJVcraxnTzd'.encode("utf-8"), req.encode("utf-8"), hashlib.sha1).hexdigest().upper() 24 | #print(signature) 25 | -------------------------------------------------------------------------------- /Jodel-Keyhack-v3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-project", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "hmr": "ng serve --configuration hmr", 8 | "build": "ng build", 9 | "build:watch": "ng build --watch", 10 | "build:prod": "ng build --prod", 11 | "test": "ng test", 12 | "lint": "ng lint" 13 | }, 14 | "private": true, 15 | "dependencies": { 16 | "@angular/animations": "~8.1.1", 17 | "@angular/common": "~8.1.1", 18 | "@angular/compiler": "~8.1.1", 19 | "@angular/core": "~11.0.5", 20 | "@angular/forms": "~8.1.1", 21 | "@angular/platform-browser": "~8.1.1", 22 | "@angular/platform-browser-dynamic": "~8.1.1", 23 | "@angular/router": "~8.1.1", 24 | "core-js": "^3.1.4", 25 | "ng-zorro-antd": "8.0.2", 26 | "rxjs": "~6.5.2", 27 | "tslib": "^1.10.0", 28 | "zone.js": "~0.9.1" 29 | }, 30 | "devDependencies": { 31 | "@angular-devkit/build-angular": "~0.801.1", 32 | "@angular/cli": "~8.1.1", 33 | "@angular/compiler-cli": "~8.1.1", 34 | "@angular/language-service": "~8.1.1", 35 | "@angularclass/hmr": "^2.1.3", 36 | "@types/jasmine": "~3.3.13", 37 | "@types/jasminewd2": "~2.0.6", 38 | "@types/node": "~12.6.2", 39 | "codelyzer": "~5.1.0", 40 | "jasmine-core": "~3.4.0", 41 | "jasmine-spec-reporter": "~4.2.1", 42 | "karma": "~6.3.16", 43 | "karma-chrome-launcher": "~3.0.0", 44 | "karma-coverage-istanbul-reporter": "~2.0.5", 45 | "karma-jasmine": "~2.0.1", 46 | "karma-jasmine-html-reporter": "^1.4.2", 47 | "protractor": "~5.4.2", 48 | "ts-node": "~8.3.0", 49 | "tslint": "~5.18.0", 50 | "typescript": "3.4.5" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Jodel-Keyhack-Frida/extract_hmac.js: -------------------------------------------------------------------------------- 1 | const DEBUG = false; 2 | 3 | function stringFromByteArray(bArray) { 4 | const extraByteMap = [ 1, 1, 1, 1, 2, 2, 3, 0 ]; 5 | var count = bArray.length; 6 | var str = ""; 7 | 8 | for (var index = 0;index < count;) 9 | { 10 | var ch = bArray[index++]; 11 | if (ch & 0x80) 12 | { 13 | var extra = extraByteMap[(ch >> 3) & 0x07]; 14 | if (!(ch & 0x40) || !extra || ((index + extra) > count)) 15 | return null; 16 | 17 | ch = ch & (0x3F >> extra); 18 | for (;extra > 0;extra -= 1) 19 | { 20 | var chx = bArray[index++]; 21 | if ((chx & 0xC0) != 0x80) 22 | return null; 23 | 24 | ch = (ch << 6) | (chx & 0x3F); 25 | } 26 | } 27 | 28 | str += String.fromCharCode(ch); 29 | } 30 | return str; 31 | } 32 | 33 | Java.perform(function () { 34 | 35 | var alreadyPrinted = false; 36 | 37 | Java.use('com.jodelapp.jodelandroidv3.JodelApp').onCreate.overload().implementation = function() { 38 | this.onCreate(); 39 | console.log("\r\nVersion: " + this.getPackageManager().getPackageInfo(this.getPackageName(), 0).versionName.value); 40 | } 41 | 42 | Java.use('javax.crypto.Mac').init.overload('java.security.Key').implementation = function (v) { 43 | if (!alreadyPrinted) { 44 | console.log("HMAC-Key: " + stringFromByteArray(v.getEncoded())); 45 | alreadyPrinted = true; 46 | } 47 | 48 | return this.init(v); 49 | }; 50 | 51 | if (DEBUG) 52 | Java.use('javax.crypto.Mac').doFinal.overload('[B').implementation = function(toBeHmaced) { 53 | console.log("To be HMACed: " + stringFromByteArray(toBeHmaced)); 54 | return this.doFinal(toBeHmaced); 55 | } 56 | }); 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JodelReversing 2 | Some details about reversing the Jodel-App 3 | 4 | This repository consists of different parts. 5 | 6 | #### Jodel-Keyhack-Frida 7 | There are three different methods to extract the HMAC-Key from the Jodel APK. 8 | The most reliable method as of my tests is the [JodelKeyhack-Frida](https://github.com/JodelRaccoons/JodelReversing/tree/master/Jodel-Keyhack-Frida) but it requires a rooted Android Device with SELinux set to permissive and [frida-server](https://github.com/frida/frida/releases) installed (Android) or a Jailbroken iDevice. 9 | 10 | #### Jodel-Keyhack-v2 11 | If Jodel-Keyhack-Frida is not an option for you, you could try the Jodel-Keyhack-v2 which is based on IDA Pro and their Python scripting interface. 12 | From time to time it is a little bit unstable but generally it produces working and valid results. 13 | 14 | #### Jodel-Keyhack-v3 15 | The last option is to use the Jodel-Keyhack-v3 which is based on radare2 and includes a fancy webinterface for uploading the APK and displaying the signature. 16 | The v3 is currently not working as radare2 is not able to resolve the function names correctly. A quick and dirty hack would be searching for the correct pattern inside the functions. 17 | Althogh this is a possibility, it did not prove to be reliable. Therefore the v3 is as of now **BROKEN**. 18 | 19 | ### Information about reversing 20 | The markdown documents included in this repository sums up basics about the application and steps to reverse engenieer it. 21 | There might be some caveats and none of the things documented are guaranteed to work anymore. 22 | The algorithms used could change at any point in time. 23 | 24 | In case some things do not work anymore or parts of the documentation are not understandable, feel free to create an issue on this repository. 25 | -------------------------------------------------------------------------------- /Jodel-Keyhack-v2/jodel_ios.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | from idautils import * 3 | from idaapi import * 4 | from idc import * 5 | import sys, traceback 6 | import decrypt 7 | import re 8 | 9 | FUNCTION_PATTERN = 'convertNSStringToCString' 10 | 11 | REGEX_EXTRACT_LOCATION = r'byte_(\S*)' 12 | 13 | 14 | def locate_function(): 15 | for segea in Segments(): 16 | found = False 17 | for funcea in Functions(segea, idc.get_segm_end(segea)): 18 | functionName = idc.get_func_name(funcea) 19 | if FUNCTION_PATTERN in functionName: 20 | found = True 21 | print("Found function ", functionName, " at ", funcea) 22 | continue 23 | if found: 24 | return funcea 25 | return None 26 | 27 | 28 | def get_disassembly(funcea): 29 | disasm = [] 30 | for (startea, endea) in Chunks(funcea): 31 | for head in Heads(startea, endea): 32 | disasm.append(GetDisasm(head)) 33 | return disasm 34 | 35 | 36 | def extract_key(): 37 | xor_key = "ed25b40c912702e08c2b2a06eae635e03f475cc3" 38 | target_function = locate_function() 39 | print("Found function with pattern",FUNCTION_PATTERN, "at", target_function) 40 | 41 | disasm = get_disassembly(locate_function()) 42 | for asm in disasm: 43 | if 'MOV' in asm: 44 | # find offset of key 45 | raw_location = re.findall(REGEX_EXTRACT_LOCATION, asm) 46 | # get key bytes 47 | raw_bytes = idc.get_bytes(int(raw_location[0],16), 40) 48 | print("Got raw key:", raw_bytes.hex()) 49 | # perform XOR on key with xor_key 50 | print("Decrypted key:", ''.join([chr(_byte ^ ord(xor_key[count])) for count, _byte in enumerate(raw_bytes)])) 51 | return 52 | 53 | 54 | if __name__ == '__main__': 55 | try: 56 | key = extract_key() 57 | except Exception as e: 58 | print('Exception: {}'.format(e)) 59 | print(traceback.print_exc(file=sys.stdout)) 60 | -------------------------------------------------------------------------------- /Jodel-Keyhack-v2/decrypt.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | #tm 3 | import binascii 4 | 5 | CLIENT_SECRET_SIZE = 40 6 | CRYPTTABLE_SIZE = 256 7 | SIGNATURE = "a4a8d4d7b09736a0f65596a868cc6fd620920fb0" 8 | 9 | def GenerateEncryptionKey(): 10 | encryptionKey = [None] * CRYPTTABLE_SIZE 11 | sig = SIGNATURE 12 | signatureLength = len(sig) 13 | 14 | shuffleCounter = 0 15 | 16 | for i in range(CRYPTTABLE_SIZE): 17 | encryptionKey[i] = i & 0xff 18 | 19 | for shuffleIndex in range(CRYPTTABLE_SIZE): 20 | encryptionKeyByte = encryptionKey[shuffleIndex] & 0xff 21 | shuffleCounter += ord(sig[shuffleIndex % signatureLength]) 22 | shuffleCounter += encryptionKeyByte 23 | shuffleCounter &= 0xff 24 | 25 | encryptionKey[shuffleIndex] = encryptionKey[shuffleCounter] 26 | encryptionKey[shuffleCounter] = encryptionKeyByte 27 | 28 | return encryptionKey 29 | 30 | 31 | def decrypt(xorKey): 32 | #xorKey = b''.join(map(lambda x: int(x, 16).to_bytes(1, 'little'), xorKey)) 33 | 34 | clientSecret = [None] * (CLIENT_SECRET_SIZE+1) 35 | secretCounter = 0 36 | 37 | encryptionKey = GenerateEncryptionKey() 38 | 39 | for secretIndex in range(CLIENT_SECRET_SIZE): 40 | encryptionKeyByte = encryptionKey[secretIndex + 1] & 0xff 41 | secretCounter += encryptionKeyByte 42 | secretCounter &= 0xff 43 | 44 | encryptionKey[secretIndex + 1] = encryptionKey[secretCounter] 45 | encryptionKey[secretCounter] = encryptionKeyByte 46 | clientSecret[secretIndex] = (xorKey[secretIndex] ^ encryptionKey[(encryptionKey[secretIndex + 1] + encryptionKeyByte) & 0xff]) & 0xff 47 | 48 | s = '' 49 | for i in range(CLIENT_SECRET_SIZE): 50 | s += "%02x" % clientSecret[i] 51 | 52 | return binascii.unhexlify(s) -------------------------------------------------------------------------------- /Jodel-Keyhack-Frida/Readme.md: -------------------------------------------------------------------------------- 1 | # Jodel Keyhack using Frida 2 | 3 | #### Requirements: 4 | 5 | - python 6 | - pip 7 | - adb 8 | - root on Android device 9 | - SELinux permissive kernel! 10 | 11 | #### Download and references 12 | [Frida for Android reference](https://www.frida.re/docs/android/) 13 | [frida-server releases](https://github.com/frida/frida/releases) 14 | - Download frida-server-[latest]-android-[arch_of_your_phone].xz and unpack 15 | 16 | #### Bring it to work: 17 | ``` 18 | pip install frida frida-tools 19 | adb push frida-server-[latest]-android-[arch_of_your_phone] /data/local/tmp/ 20 | adb shell 21 | cd /data/local/tmp 22 | su 23 | chmod +x frida-server-[latest]-android-[arch_of_your_phone] 24 | ./frida-server-[latest]-android-[arch_of_your_phone] & 25 | 26 | ``` 27 | 28 | ### Run it 29 | To begin, start Jodel on your Android device. Afterwards start the python script: 30 | 31 | ```bash 32 | $ python extract_hmac.py 33 | Version: 7.25.8 34 | HMAC-Key: mNnKgrObHerZMdpPffBMbKvYZxNDpQYGDWzzsyjH 35 | ``` 36 | 37 | Or run the javascript file directly with frida: 38 | 39 | ```bash 40 | frida -U -l extract_hmac.js -f com.tellm.android.app --no-pause 41 | ____ 42 | / _ | Frida 15.0.8 - A world-class dynamic instrumentation toolkit 43 | | (_| | 44 | > _ | Commands: 45 | /_/ |_| help -> Displays the help system 46 | . . . . object? -> Display information about 'object' 47 | . . . . exit/quit -> Exit 48 | . . . . 49 | . . . . More info at https://frida.re/docs/home/ 50 | Spawned `com.tellm.android.app`. Resuming main thread! 51 | [Pixel::com.tellm.android.app]-> 52 | Version: 7.25.8 53 | HMAC-Key: mNnKgrObHerZMdpPffBMbKvYZxNDpQYGDWzzsyjH 54 | ``` 55 | 56 | ### iOS version 57 | To also support iOS versions, a new script has been added. Run it with: 58 | 59 | ```bash 60 | $ frida -U -l extract_hmac_ios.js -n Jodel 61 | ``` 62 | -------------------------------------------------------------------------------- /Jodel-Keyhack-v3/backend/decrypt.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | #tm 3 | import binascii 4 | 5 | #KEY_LOCATION_START = 0xFC00 6 | #KEY_LOCATION_HAYSTACK_SIZE = 0x1000 7 | CLIENT_SECRET_SIZE = 40 8 | CRYPTTABLE_SIZE = 256 9 | SIGNATURE = "a4a8d4d7b09736a0f65596a868cc6fd620920fb0" 10 | 11 | def GenerateEncryptionKey(): 12 | encryptionKey = [None] * CRYPTTABLE_SIZE 13 | sig = SIGNATURE 14 | signatureLength = len(sig) 15 | 16 | shuffleCounter = 0 17 | 18 | for i in range(CRYPTTABLE_SIZE): 19 | encryptionKey[i] = i & 0xff 20 | 21 | for shuffleIndex in range(CRYPTTABLE_SIZE): 22 | encryptionKeyByte = encryptionKey[shuffleIndex] & 0xff 23 | shuffleCounter += ord(sig[shuffleIndex % signatureLength]) 24 | shuffleCounter += encryptionKeyByte 25 | shuffleCounter &= 0xff 26 | 27 | encryptionKey[shuffleIndex] = encryptionKey[shuffleCounter] 28 | encryptionKey[shuffleCounter] = encryptionKeyByte 29 | 30 | return encryptionKey 31 | 32 | 33 | def decrypt(xorKey): 34 | #xorKey = b''.join(map(lambda x: int(x, 16).to_bytes(1, 'little'), xorKey)) 35 | 36 | clientSecret = [None] * (CLIENT_SECRET_SIZE+1) 37 | secretCounter = 0 38 | 39 | encryptionKey = GenerateEncryptionKey() 40 | 41 | for secretIndex in range(CLIENT_SECRET_SIZE): 42 | encryptionKeyByte = encryptionKey[secretIndex + 1] & 0xff 43 | secretCounter += encryptionKeyByte 44 | secretCounter &= 0xff 45 | 46 | encryptionKey[secretIndex + 1] = encryptionKey[secretCounter] 47 | encryptionKey[secretCounter] = encryptionKeyByte 48 | clientSecret[secretIndex] = (xorKey[secretIndex] ^ encryptionKey[(encryptionKey[secretIndex + 1] + encryptionKeyByte) & 0xff]) & 0xff 49 | 50 | s = '' 51 | for i in range(CLIENT_SECRET_SIZE): 52 | s += "%02x" % clientSecret[i] 53 | 54 | return binascii.unhexlify(s) -------------------------------------------------------------------------------- /Jodel-Keyhack-v3/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | src/ 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # IPython 79 | profile_default/ 80 | ipython_config.py 81 | 82 | # pyenv 83 | .python-version 84 | 85 | # celery beat schedule file 86 | celerybeat-schedule 87 | 88 | # SageMath parsed files 89 | *.sage.py 90 | 91 | # Environments 92 | .env 93 | .venv 94 | env/ 95 | venv/ 96 | ENV/ 97 | env.bak/ 98 | venv.bak/ 99 | 100 | # Spyder project settings 101 | .spyderproject 102 | .spyproject 103 | 104 | # Rope project settings 105 | .ropeproject 106 | 107 | # mkdocs documentation 108 | /site 109 | 110 | # mypy 111 | .mypy_cache/ 112 | .dmypy.json 113 | dmypy.json 114 | 115 | # Pyre type checker 116 | .pyre/ 117 | 118 | 119 | # angular 120 | node_modules/ 121 | frontend-dist/ 122 | 123 | # macos 124 | .DS_Store 125 | -------------------------------------------------------------------------------- /Jodel-Keyhack-v3/frontend-src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 11 |

12 |

Upload Jodel APK

13 |

Just drag and drop the APK file

14 |
15 |
16 |
17 | 18 | 19 | 20 |
21 |
Offical Jodel APK
22 |
{{ extractionData.is_jodel_signature ? "Yes" : "No" }} 23 |
24 |
25 |
26 |
Package name
27 |
{{ extractionData.package }}
28 |
29 |
30 |
Version name
31 |
{{ extractionData.version_name }}
32 |
33 |
34 |
Version code
35 |
{{ extractionData.version_code }}
36 |
37 |
38 |
HMAC key
39 |
40 | 41 |
42 |
43 |
44 |
Key status
45 |
46 | {{ extractionData.key_status.working ? "Working" : "Not working" }} 47 |
48 |
49 |
50 |
51 |
52 |
53 | -------------------------------------------------------------------------------- /Jodel-Keyhack-Frida/extract_hmac.py: -------------------------------------------------------------------------------- 1 | import frida, sys 2 | 3 | def on_message(message, data): 4 | if message['type'] == 'send': 5 | print("[*] {0}".format(message['payload'])) 6 | else: 7 | print(message) 8 | 9 | jscode = """ 10 | const DEBUG = false; 11 | 12 | function stringFromByteArray(bArray) { 13 | const extraByteMap = [ 1, 1, 1, 1, 2, 2, 3, 0 ]; 14 | var count = bArray.length; 15 | var str = ""; 16 | 17 | for (var index = 0;index < count;) 18 | { 19 | var ch = bArray[index++]; 20 | if (ch & 0x80) 21 | { 22 | var extra = extraByteMap[(ch >> 3) & 0x07]; 23 | if (!(ch & 0x40) || !extra || ((index + extra) > count)) 24 | return null; 25 | 26 | ch = ch & (0x3F >> extra); 27 | for (;extra > 0;extra -= 1) 28 | { 29 | var chx = bArray[index++]; 30 | if ((chx & 0xC0) != 0x80) 31 | return null; 32 | 33 | ch = (ch << 6) | (chx & 0x3F); 34 | } 35 | } 36 | 37 | str += String.fromCharCode(ch); 38 | } 39 | return str; 40 | } 41 | 42 | Java.perform(function () { 43 | 44 | var alreadyPrinted = false; 45 | 46 | Java.use('com.jodelapp.jodelandroidv3.JodelApp').onCreate.overload().implementation = function() { 47 | this.onCreate(); 48 | console.log("Version: " + this.getPackageManager().getPackageInfo(this.getPackageName(), 0).versionName.value); 49 | } 50 | 51 | Java.use('javax.crypto.Mac').init.overload('java.security.Key').implementation = function (v) { 52 | if (!alreadyPrinted) { 53 | console.log("HMAC-Key: " + stringFromByteArray(v.getEncoded())); 54 | alreadyPrinted = true; 55 | } 56 | 57 | return this.init(v); 58 | }; 59 | 60 | if (DEBUG) 61 | Java.use('javax.crypto.Mac').doFinal.overload('[B').implementation = function(toBeHmaced) { 62 | console.log("To be HMACed: " + stringFromByteArray(toBeHmaced)); 63 | return this.doFinal(toBeHmaced); 64 | } 65 | }); 66 | """ 67 | 68 | try: 69 | device = frida.get_usb_device() 70 | pid = device.spawn(['com.tellm.android.app']) 71 | session = device.attach(pid) 72 | script = session.create_script(jscode) 73 | script.on('message', on_message) 74 | device.resume(pid) 75 | script.load() 76 | sys.stdin.read() 77 | except Exception as e: 78 | print(e) 79 | -------------------------------------------------------------------------------- /03_Reversing_SSL-Pinning.md: -------------------------------------------------------------------------------- 1 | ## Bypass SSL-Pinning 2 | In the following, bypassing SSL/TLS-Pinning within the Jodel app is described. 3 | 4 | ### Android 5 | This script is for use with [frida](https://frida.re/). As Jodel is heavily obfuscated, bypassing the TLS pinning needs to be done a little different. Method names in the Jodel app are unicode characters which are not directly usable in frida. A possible workaround is shown below. Just copy & paste the unicode character method name of your current Jodel version in the script and run it. This also circumvents certificate validation. 6 | 7 | Due to the obfuscation, finding the correct class and method is not always easy. Therefore you might want to try to search for the strings contained in the method, e.g. `unsupported hashAlgorithm: ` (see [here](https://github.com/square/okhttp/blob/3ad1912f783e108b3d0ad2c4a5b1b89b827e4db9/okhttp/src/jvmMain/kotlin/okhttp3/CertificatePinner.kt#L177)). Just check the source file for more strings if something changes over time. 8 | 9 | The method can be easily identified as it is the only method accepting a `String` and `Function0` (Kotlin) as parameters. 10 | 11 | ``` 12 | import frida, sys, time 13 | 14 | def on_message(message, data): 15 | if message['type'] == 'send': 16 | print("[*] {0}".format(message['payload'])) 17 | else: 18 | print(message) 19 | 20 | jscode = """ 21 | Java.perform(function () { 22 | let b = Java.use("okhttp3.b"); 23 | b["b"].overload('java.lang.String', 'kotlin.jvm.functions.Function0').implementation = function (str, function0) { 24 | console.log('OkHTTP 3.x check() called. Not throwing an exception.'); 25 | }; 26 | }) 27 | """ 28 | 29 | pid = frida.get_usb_device().spawn('com.tellm.android.app') 30 | frida.get_usb_device().resume(pid) 31 | #time.sleep(1) #Without it Java.perform silently fails 32 | session = frida.get_usb_device().attach(pid) 33 | script = session.create_script(jscode) 34 | script.on('message', on_message) 35 | print('Running...') 36 | script.load() 37 | sys.stdin.read() 38 | ``` 39 | 40 | Keep in mind that Android version 7 and above only accepts certificates installed in the system CA store (and ignores the one installed by the user). To circumvent this restriction patching the NetworkSecurityConfig is one possibility, another is utilizing [this Magisk module](https://github.com/NVISO-BE/MagiskTrustUserCerts) to move user certificates to the system store. 41 | 42 | ### iOS 43 | For iOS, TLS pinning is done by using the respecitve system APIs. Multiple Cydia packages exist to circumvent such pinning mechanisms. The package [SSL Kill Switch 2](https://github.com/nabla-c0d3/ssl-kill-switch2) should do the trick here. 44 | 45 | Alternatively you can use the [objection](https://github.com/sensepost/objection) frameworks command `ios sslpinning disable` which should have the same effect. 46 | -------------------------------------------------------------------------------- /01_Reversing_App.md: -------------------------------------------------------------------------------- 1 | ## Analysis of the application 2 | 3 | ### The APK-File 4 | Only a few details here: 5 | - The app is most probably obfuscated using [DexGuard](https://www.guardsquare.com/en/products/dexguard) 6 | - Heavy control-flow obfuscation by stub math operations 7 | - Heavy method name obfuscation, method names are not human readable anymore, mostly special characters or weired white-spaces 8 | - Ressource names are scrambled and in raw apk (not packed in asrc file) 9 | - Filenames are in non-ASCII characters 10 | - Extracting the APK under windows os will result in errors as windows makes no difference between unicode 11 | spaces like `U+0020` (Unicode space) and `U+00A0` (Unicode non-breaking space) and so on. This results in filename collisions and owerwriting of files. 12 | - Resource ids were mostly stripped out of the xml file, no chance of recovering them 13 | 14 | ### Unpacking / Repacking 15 | - Unpacking / repacking should work using latest version of [apktool](https://ibotpeaches.github.io/Apktool/) under linux-based OS 16 | - Simply re-signing the APK will result in a [HMAC signature error](#HMAC-signature-error). This is caused by a invalid signature used by the libhmac.so to decrypt the HMAC-Key (see the HMAC-reversing). A possible workaroud is implementing the HMAC-routine on your own [like in JodelPatched](https://github.com/JodelRaccoons/JodelPatched/blob/master/patched/src/main/java/com/jodelapp/jodelandroidv3/api/HmacInterceptor.java). Process would be as follows: 17 | 1. Compile or grab 'latest' JodelPatched apk 18 | 2. Decompile the JodelPatched apk to smali code 19 | 3. Extract the classes*.dex from the original Jodel APK 20 | 4. Convert it to smali using latest [baksmali](https://bitbucket.org/JesusFreke/smali/downloads/) (Steps 3 and 4 are to circumvent problems with the scrambled ressources) 21 | 5. Edit HmacInterceptor.smali by copy&pasting the modified methods from the decompiled JodelPatched apk. Double check syntax and registers used to avoid complications with the smalivm. 22 | 6. Recompile dex with [smali](https://bitbucket.org/JesusFreke/smali/downloads/), pack into apk, sign with [dex2jar-apk-sign.sh](https://github.com/pxb1988/dex2jar) and install on device 23 | 7. If you did all correct: profit!!! 24 | 25 | ##### HMAC signature error 26 | ``` 27 | com.tellm.android.app A/art: art/runtime/java_vm_ext.cc:410] JNI DETECTED ERROR IN APPLICATION: input is not valid Modified UTF-8: illegal start byte 0x8e 28 | com.tellm.android.app A/art: art/runtime/java_vm_ext.cc:410] string: '��M�L�n�@�}[ 29 | com.tellm.android.app A/art: art/runtime/java_vm_ext.cc:410] )Ί;0=�m��H|�c`}��' 30 | com.tellm.android.app A/art: art/runtime/java_vm_ext.cc:410] in call to NewStringUTF 31 | com.tellm.android.app A/art: art/runtime/java_vm_ext.cc:410] from byte[] com.jodelapp.jodelandroidv3.api.HmacInterceptor.sign(java.lang.String, java.lang.String, byte[]) 32 | ``` 33 | 34 | -------------------------------------------------------------------------------- /Jodel-Keyhack-v2/jodel.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | from idautils import * 3 | from idaapi import * 4 | from idc import * 5 | import sys, traceback 6 | import decrypt 7 | import re 8 | import ida_hexrays 9 | 10 | FUNCTION_PATTERN = 'Java_com_jodelapp_jodelandroidv3_api_HmacInterceptor_init' 11 | 12 | #REGEX_EXTRACT_BYTES = r'(?<=[^ ] )\d\w*' 13 | REGEX_EXTRACT_BYTES = r'#0x\d\w*' 14 | REGEX_EXTRACT_LOCATION = r'(?<=_)\w*' 15 | 16 | 17 | def locate_function(): 18 | for segea in Segments(): 19 | for funcea in Functions(segea, idc.get_segm_end(segea)): 20 | functionName = idc.get_func_name(funcea) 21 | if FUNCTION_PATTERN in functionName: 22 | return funcea 23 | return None 24 | 25 | 26 | def get_disassembly(funcea): 27 | disasm = [] 28 | for (startea, endea) in Chunks(funcea): 29 | for head in Heads(startea, endea): 30 | disasm.append(GetDisasm(head)) 31 | return disasm 32 | 33 | 34 | def extract_bytes(): 35 | instr = {} 36 | target_function = locate_function() 37 | print("Found function with pattern ",FUNCTION_PATTERN, " at ", target_function) 38 | # get decompiled function 39 | decompiled_function = ida_hexrays.decompile(target_function) 40 | # get result as string and only match lines containing 0x 41 | decompiled_instructions = [instr for instr in str(decompiled_function).split("\n") if '0x' in instr] 42 | # remove prefixes in lines 43 | decompiled_instructions = [re.sub(r'(byte_|unk_|dword_)', '', instr) for instr in decompiled_instructions] 44 | # remove whitespaces 45 | decompiled_instructions = [re.sub(r'(\s)', '', instr) for instr in decompiled_instructions] 46 | # remove semicolons 47 | decompiled_instructions = [re.sub(r'(;)', '', instr) for instr in decompiled_instructions] 48 | # remove 0x prefix 49 | decompiled_instructions = [re.sub(r'(0x)', '', instr) for instr in decompiled_instructions] 50 | 51 | instructions_dict = dict((int(instr.split("=")[0], 16), rev(str(instr.split("=")[1]))) for instr in decompiled_instructions) 52 | 53 | sorted_keys = ''.join(OrderedDict(sorted(instructions_dict.items())).values()) 54 | 55 | print(sorted_keys) 56 | 57 | import decrypt 58 | if len(sorted_keys) != decrypt.CLIENT_SECRET_SIZE*2: 59 | print("Keysize is {}, should be {} exiting".format(len(sorted_keys), decrypt.CLIENT_SECRET_SIZE*2)) 60 | return None 61 | return [int(sorted_keys[x:x + 2], 16) for x in range(0, len(sorted_keys), 2)] 62 | 63 | 64 | def rev(a): 65 | new = "" 66 | for x in range(-1, -len(a), -2): 67 | new += a[x-1] + a[x] 68 | return new 69 | 70 | 71 | if __name__ == '__main__': 72 | try: 73 | key = extract_bytes() 74 | if key: 75 | print('Derived key of length {} from library, now decrypting it...'.format(len(key))) 76 | print('Got raw key array: {}'.format(key)) 77 | _result = decrypt.decrypt(key) 78 | print('Got decrypted key: {}'.format(_result)) 79 | except Exception as e: 80 | print('Exception: {}'.format(e)) 81 | print(traceback.print_exc(file=sys.stdout)) 82 | -------------------------------------------------------------------------------- /Jodel-Keyhack-v3/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "deprecation": { 15 | "severity": "warn" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-blacklist": [ 20 | true, 21 | "rxjs/Rx" 22 | ], 23 | "import-spacing": true, 24 | "indent": [ 25 | true, 26 | "spaces" 27 | ], 28 | "interface-over-type-literal": true, 29 | "label-position": true, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-access": false, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-arg": true, 47 | "no-bitwise": true, 48 | "no-console": [ 49 | true, 50 | "debug", 51 | "info", 52 | "time", 53 | "timeEnd", 54 | "trace" 55 | ], 56 | "no-construct": true, 57 | "no-debugger": true, 58 | "no-duplicate-super": true, 59 | "no-empty": false, 60 | "no-empty-interface": true, 61 | "no-eval": true, 62 | "no-inferrable-types": [ 63 | true, 64 | "ignore-params" 65 | ], 66 | "no-misused-new": true, 67 | "no-non-null-assertion": true, 68 | "no-redundant-jsdoc": true, 69 | "no-shadowed-variable": true, 70 | "no-string-literal": false, 71 | "no-string-throw": true, 72 | "no-switch-case-fall-through": true, 73 | "no-trailing-whitespace": true, 74 | "no-unnecessary-initializer": true, 75 | "no-unused-expression": true, 76 | "no-use-before-declare": true, 77 | "no-var-keyword": true, 78 | "object-literal-sort-keys": false, 79 | "one-line": [ 80 | true, 81 | "check-open-brace", 82 | "check-catch", 83 | "check-else", 84 | "check-whitespace" 85 | ], 86 | "prefer-const": true, 87 | "quotemark": [ 88 | true, 89 | "single" 90 | ], 91 | "radix": true, 92 | "semicolon": [ 93 | true, 94 | "always" 95 | ], 96 | "triple-equals": [ 97 | true, 98 | "allow-null-check" 99 | ], 100 | "typedef-whitespace": [ 101 | true, 102 | { 103 | "call-signature": "nospace", 104 | "index-signature": "nospace", 105 | "parameter": "nospace", 106 | "property-declaration": "nospace", 107 | "variable-declaration": "nospace" 108 | } 109 | ], 110 | "unified-signatures": true, 111 | "variable-name": false, 112 | "whitespace": [ 113 | true, 114 | "check-branch", 115 | "check-decl", 116 | "check-operator", 117 | "check-separator", 118 | "check-type" 119 | ], 120 | "no-output-on-prefix": true, 121 | "use-input-property-decorator": true, 122 | "use-output-property-decorator": true, 123 | "use-host-property-decorator": true, 124 | "no-input-rename": true, 125 | "no-output-rename": true, 126 | "use-life-cycle-interface": true, 127 | "use-pipe-transform-interface": true, 128 | "component-class-suffix": true, 129 | "directive-class-suffix": true 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /Jodel-Keyhack-v3/frontend-src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10, IE11, and Chrome <55 requires all of the following polyfills. 22 | * This also includes Android Emulators with older versions of Chrome and Google Search/Googlebot 23 | */ 24 | 25 | // import 'core-js/es6/symbol'; 26 | // import 'core-js/es6/object'; 27 | // import 'core-js/es6/function'; 28 | // import 'core-js/es6/parse-int'; 29 | // import 'core-js/es6/parse-float'; 30 | // import 'core-js/es6/number'; 31 | // import 'core-js/es6/math'; 32 | // import 'core-js/es6/string'; 33 | // import 'core-js/es6/date'; 34 | // import 'core-js/es6/array'; 35 | // import 'core-js/es6/regexp'; 36 | // import 'core-js/es6/map'; 37 | // import 'core-js/es6/weak-map'; 38 | // import 'core-js/es6/set'; 39 | 40 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 41 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 42 | 43 | /** IE10 and IE11 requires the following for the Reflect API. */ 44 | // import 'core-js/es6/reflect'; 45 | 46 | /** 47 | * Web Animations `@angular/platform-browser/animations` 48 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 49 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 50 | */ 51 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 52 | 53 | /** 54 | * By default, zone.js will patch all possible macroTask and DomEvents 55 | * user can disable parts of macroTask/DomEvents patch by setting following flags 56 | * because those flags need to be set before `zone.js` being loaded, and webpack 57 | * will put import in the top of bundle, so user need to create a separate file 58 | * in this directory (for example: zone-flags.ts), and put the following flags 59 | * into that file, and then add the following code before importing zone.js. 60 | * import './zone-flags.ts'; 61 | * 62 | * The flags allowed in zone-flags.ts are listed here. 63 | * 64 | * The following flags will work for all browsers. 65 | * 66 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 67 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 68 | * (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 69 | * 70 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 71 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 72 | * 73 | * (window as any).__Zone_enable_cross_context_check = true; 74 | * 75 | */ 76 | 77 | /*************************************************************************************************** 78 | * Zone JS is required by default for Angular itself. 79 | */ 80 | import 'zone.js/dist/zone'; // Included with Angular CLI. 81 | 82 | 83 | /*************************************************************************************************** 84 | * APPLICATION IMPORTS 85 | */ 86 | -------------------------------------------------------------------------------- /Jodel-Keyhack-v2/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Jodel Keyhack v2 3 | 4 | Tested with 5 | - IDA 7.7 6 | - Python3 support is required 7 | - The Android version requires a hexrays decompiler license 8 | - Jodel 8.0.1 arm64-v8a (Android) 9 | - Jodel version 4.139 and version 7.51 (iOS) 10 | 11 | ## iOS 12 | Just a small script to use with IDA to extract the HMAC key from a decrypted binary. The key extraction the script performs is pretty straightforward: 13 | - Locating the `+[JDLCrypto convertNSStringToCString:]` function 14 | - Decompile the function AFTER the `+[JDLCrypto convertNSStringToCString:]` function (usually `sub_10004F2E8` or simmilar), here a reference to the XORed key is present 15 | - Look for the first MOV instruction (which contains the reference to the XORed key) and extract the address of the XORed key 16 | - XOR the key with the static value `ed25b40c912702e08c2b2a06eae635e03f475cc3` (extracted from the app) 17 | 18 | #### How to use 19 | - Save the `jodel_ios.py` file somewhere to your local drive 20 | - Fire up IDA and load a **DECRYPTED** Jodel binary into it 21 | - Wait for the initial IDA analysis to finish 22 | - Hit ALT+F7 or choose `File -> Script File` 23 | - Choose the `jodel_ios.py` file 24 | - Profit 25 | 26 | Running the script should provide the following output: 27 | 28 | ``` 29 | [...] 30 | Found function +[JDLCrypto convertNSStringToCString:] at 4295128280 31 | Found function with pattern convertNSStringToCString at 4295128424 32 | Found function +[JDLCrypto convertNSStringToCString:] at 4295128280 33 | Got raw key: 3c21795415577f264e4b5b505f4413677d2559206436607f16062d5e5d7c235d552b40515f3a2f60 34 | Decrypted key: YEKawcOEwzigovvWEFkBVWPIsgHhnIFmfMtfjYLS 35 | ``` 36 | 37 | ## Android 38 | 39 | ### What this is 40 | This is just a small IDA script, extracting the HMAC key from the library and decoding it as [cfib90s](https://bitbucket.org/cfib90/) script was broken for me. 41 | 42 | The approach of this script is scraping the bare `mov` instructions, stripping the key from them and unscrambling it. 43 | As the key is somehow XORed with a scrambled version of the APKs signature the decrypt.py (ported by tm, based on the OJOC-Keyhack) unscrambles and XORs it. 44 | 45 | This script was developed and tested with IDA 7.7 but should also work on other versions. 46 | 47 | ### Requirements 48 | - IDA Pro version 7.0 or higher 49 | - Any version of the Jodel APK 50 | - Hexrays decompiler license 51 | 52 | ### How to use 53 | - Clone this repo or download the `jodel.py` and `decrypt.py` 54 | - Fire up IDA 7.0 or later 55 | - Feed it with the latest libhmac.so (arm64) 56 | - Open the APK with 7-Zip or simmilar 57 | - Extract the file /lib/x86/libX.so where X is a random lowercase character 58 | - The correct library file is around 200 kb 59 | - Wait for the initial IDA analysis to finish 60 | - Hit ALT+F7 or choose `File -> Script File` 61 | - A file explorer should open, choose the `jodel.py` file 62 | - The IDA console should display the extracted HMAC key 63 | 64 | ### Example output 65 | ``` 66 | --------------------------------------------------------------------------------------------- 67 | Python 3.9.7 (tags/v3.9.7:1016ef3, Aug 30 2021, 20:19:38) [MSC v.1929 64 bit (AMD64)] 68 | IDAPython 64-bit v7.4.0 final (serial 0) (c) The IDAPython Team 69 | --------------------------------------------------------------------------------------------- 70 | Found function with pattern Java_com_jodelapp_jodelandroidv3_api_HmacInterceptor_init at 54624 71 | C6DEAEB09FFCCE3AB116032DB95617381927B96672E02A271F5957B2AF7CD400C67A97378BCF34B4 72 | Derived key of length 40 from library, now decrypting it... 73 | Got raw key array: [198, 222, 174, 176, 159, 252, 206, 58, 177, 22, 3, 45, 185, 86, 23, 56, 25, 39, 185, 102, 114, 224, 42, 39, 31, 89, 87, 178, 175, 124, 212, 0, 198, 122, 151, 55, 139, 207, 52, 180] 74 | Got decrypted key: b'PohIBVvuWFhSLydTFZSjDMWmHrpRQuEGEBPfgIxB' 75 | ``` 76 | -------------------------------------------------------------------------------- /Jodel-Keyhack-v3/backend/r2instance.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | 3 | import r2pipe 4 | import re 5 | 6 | R2_LIST_FUNCTIONS = 'afl' 7 | R2_DISASSEMBLE_INSTRUCTIONS = 's {}; pi 25' 8 | 9 | REGEX_EXTRACT_BYTES = r'(?<=[^ ] )\d\w*' 10 | REGEX_FIND_FUNCTIONS = r'fcn.\w+' 11 | 12 | 13 | def rev(a): 14 | new = "" 15 | for x in range(-1, -len(a), -2): 16 | new += a[x - 1] + a[x] 17 | 18 | return new 19 | 20 | 21 | class R2Instance: 22 | def __init__(self, path): 23 | self.r2 = r2pipe.open(path) 24 | self.r2.cmd('aaa') 25 | self.is_correct_binary = False 26 | 27 | self.key = self.get_method_name() 28 | if self.key is not False: 29 | print("Correct binary is {}".format(path)) 30 | self.is_correct_binary = True 31 | 32 | def __enter__(self): 33 | return self 34 | 35 | def __exit__(self, type, value, traceback): 36 | pass 37 | 38 | def __del__(self): 39 | self.r2.quit() 40 | 41 | def get_method_name(self): 42 | func = self.r2.cmd(R2_LIST_FUNCTIONS).split('\r\n') 43 | regexp = re.compile(REGEX_FIND_FUNCTIONS) 44 | functions = [] 45 | for f in func: 46 | reg_res = regexp.search(f) 47 | if reg_res: 48 | functions.append(reg_res.group(0)) 49 | 50 | 51 | possibly_correct1 = [] 52 | for f in functions: 53 | len = int(self.r2.cmd('s {};pif~?'.format(f)).rstrip()) 54 | if len == 21: 55 | possibly_correct1.append(f) 56 | 57 | print('PossiblyCorrect1: {}'.format(possibly_correct1)) 58 | 59 | count_mov_byte = 8 #should be 8 60 | count_mov_dword = 8 # should be 8 61 | count_add_eax = 1 #should be 1 62 | count_pop_eax = 1 #should be 1 63 | 64 | possibly_correct2 = [] 65 | for f in possibly_correct1: 66 | instr = self.r2.cmd('s {};pi 18'.format(f)) 67 | _count_move_byte = sum(1 for _ in re.finditer(r'\b%s\b' % re.escape('mov byte'), instr)) 68 | _count_move_dword = sum(1 for _ in re.finditer(r'\b%s\b' % re.escape('mov dword'), instr)) 69 | _count_add_eax = sum(1 for _ in re.finditer(r'\b%s\b' % re.escape('add eax'), instr)) 70 | _count_pop_eax = sum(1 for _ in re.finditer(r'\b%s\b' % re.escape('pop eax'), instr)) 71 | 72 | if _count_move_byte == count_mov_byte and _count_move_dword == count_mov_dword and _count_add_eax == count_add_eax and _count_pop_eax == count_pop_eax: 73 | possibly_correct2.append(f) 74 | 75 | for f in possibly_correct2: 76 | try: 77 | return self.extract_bytes(f) 78 | except Exception as e: 79 | pass 80 | 81 | return False 82 | 83 | def extract_bytes(self, function_name): 84 | instr = {} 85 | # https://memegenerator.net/img/instances/75909642/how-does-this-even-work.jpg 86 | instructions = [d for d in self.r2.cmd(R2_DISASSEMBLE_INSTRUCTIONS.format( 87 | function_name)).split('\r') if 'mov' in d and 'eax' in d] 88 | for i in instructions: 89 | matches = re.findall(REGEX_EXTRACT_BYTES, i) 90 | value = matches[1].replace('0x', '').strip() 91 | if len(value) <= 1 or (8 > len(value) > 2): 92 | value = '0' + value 93 | if len(value) > 8 and value.startswith('0'): 94 | value = value[1:] 95 | instr[int(matches[0], 0)] = rev(value) 96 | 97 | sorted_keys = ''.join(OrderedDict(sorted(instr.items())).values()) 98 | 99 | import decrypt 100 | if len(sorted_keys) != decrypt.CLIENT_SECRET_SIZE*2: 101 | print("Keysize is {}, exiting".format(len(sorted_keys))) 102 | return [int(sorted_keys[x:x + 2], 16) for x in range(0, len(sorted_keys), 2)] 103 | -------------------------------------------------------------------------------- /Jodel-Keyhack-v3/backend/server.py: -------------------------------------------------------------------------------- 1 | import shutil, zipfile, jodel_api, tempfile, os, time 2 | 3 | from flask import Flask, request, redirect, url_for 4 | from werkzeug.utils import secure_filename 5 | import decrypt as decrypt 6 | from r2instance import R2Instance 7 | from pyaxmlparser import APK 8 | 9 | UPLOAD_FOLDER = tempfile.gettempdir() 10 | ALLOWED_EXTENSIONS = {'apk'} 11 | 12 | app = Flask(__name__, static_url_path="/static", static_folder="../frontend-dist") 13 | 14 | app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER 15 | 16 | 17 | @app.route('/', methods=['GET']) 18 | def index(): 19 | return redirect(url_for('static', filename='index.html')) 20 | 21 | 22 | @app.route('/api/upload', methods=['POST']) 23 | def upload(): 24 | if 'file' not in request.files: 25 | return redirect(request.url) 26 | file = request.files['file'] 27 | if file.filename == '': 28 | return redirect(request.url) 29 | if file and allowed_file(file.filename): 30 | filename = secure_filename(file.filename) 31 | filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename) 32 | file.save(filepath) 33 | return jodel_api.json.dumps(process_file(filepath)) 34 | else: 35 | return {'error':True, 'message': 'File type not allowed!'} 36 | 37 | 38 | def allowed_file(filename): 39 | return '.' in filename and \ 40 | filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS 41 | 42 | 43 | def gather_apk_information(apk_file_path): 44 | try: 45 | apk = APK(apk_file_path) 46 | return {'error':False, 'package': apk.package, 'version_name': apk.version_name, 47 | 'version_code': apk.version_code, 48 | 'is_jodel_signature': True if apk.package == 'com.tellm.android.app' else False} 49 | except: 50 | return {'error':True, 'message': 'Failed verifying APK file!'} 51 | 52 | 53 | def process_file(apk_file_path): 54 | apk_information = gather_apk_information(apk_file_path) 55 | if not apk_information['error']: 56 | r2instance, unzip_directory = extract_zip(apk_file_path) 57 | clean_up_mess(apk_file_path, unzip_directory) 58 | if r2instance is None: 59 | return {'error':True, 'message': 'Library file not found, exiting...'} 60 | apk_information['hmac_key'] = decrypt.decrypt(r2instance.key).decode("utf-8") 61 | apk_information['key_status'] = is_key_working(apk_information['hmac_key'], apk_information['version_name']) 62 | apk_information['error'] = False 63 | apk_information['message'] = 'Successfully extracted key!' 64 | 65 | return apk_information 66 | 67 | 68 | def clean_up_mess(apk_file_path, extracted_file_path): 69 | try: 70 | if apk_file_path and os.path.isfile(apk_file_path): 71 | os.remove(apk_file_path) 72 | print('Removed APK file') 73 | 74 | if extracted_file_path and os.path.isdir(extracted_file_path): 75 | shutil.rmtree(extracted_file_path) 76 | print('Removed extracted files') 77 | except: 78 | print('failed to remove files') 79 | 80 | 81 | def extract_zip(path): 82 | with zipfile.ZipFile(path) as archive: 83 | unzip_directory = os.path.join(UPLOAD_FOLDER, str(time.time())) 84 | for file in archive.namelist(): 85 | if file.startswith('lib/') and file.find('x86') != -1: 86 | extracted_file = os.path.join( 87 | unzip_directory, archive.extract(file, unzip_directory)) 88 | _r2instance = R2Instance(extracted_file) 89 | if _r2instance.is_correct_binary: 90 | return _r2instance, unzip_directory 91 | else: 92 | del _r2instance 93 | 94 | return None, unzip_directory 95 | 96 | 97 | def is_key_working(key, version): 98 | try: 99 | lat, lng, city = 48.148900, 11.567400, "Munich" 100 | j = jodel_api.JodelAccount(lat=lat, lng=lng, city=city) 101 | return {'working': True, 'account': j.get_account_data()} 102 | except Exception as e: 103 | print(e) 104 | return {'working': False} 105 | 106 | 107 | if __name__ == '__main__': 108 | Flask.run(app, debug=False) 109 | 110 | -------------------------------------------------------------------------------- /Jodel-Keyhack-v3/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "my-project": { 7 | "root": "", 8 | "sourceRoot": "frontend-src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": { 12 | "@schematics/angular:component": { 13 | "styleext": "less" 14 | } 15 | }, 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:browser", 19 | "options": { 20 | "outputPath": "frontend-dist", 21 | "index": "frontend-src/index.html", 22 | "main": "frontend-src/main.ts", 23 | "polyfills": "frontend-src/polyfills.ts", 24 | "tsConfig": "frontend-src/tsconfig.app.json", 25 | "assets": [ 26 | "frontend-src/favicon.ico", 27 | "frontend-src/assets", 28 | { 29 | "glob": "**/*", 30 | "input": "./node_modules/@ant-design/icons-angular/src/inline-svg/", 31 | "output": "/assets/" 32 | } 33 | ], 34 | "styles": [ 35 | "./node_modules/ng-zorro-antd/ng-zorro-antd.min.css", 36 | "frontend-src/styles.less" 37 | ], 38 | "scripts": [] 39 | }, 40 | "configurations": { 41 | "production": { 42 | "fileReplacements": [ 43 | { 44 | "replace": "frontend-src/environments/environment.ts", 45 | "with": "frontend-src/environments/environment.prod.ts" 46 | } 47 | ], 48 | "optimization": true, 49 | "outputHashing": "all", 50 | "sourceMap": false, 51 | "extractCss": true, 52 | "namedChunks": false, 53 | "aot": true, 54 | "extractLicenses": true, 55 | "vendorChunk": false, 56 | "buildOptimizer": true, 57 | "budgets": [ 58 | { 59 | "type": "initial", 60 | "maximumWarning": "2mb", 61 | "maximumError": "5mb" 62 | } 63 | ] 64 | }, 65 | "hmr": { 66 | "fileReplacements": [ 67 | { 68 | "replace": "frontend-src/environments/environment.ts", 69 | "with": "frontend-src/environments/environment.hmr.ts" 70 | } 71 | ] 72 | } 73 | } 74 | }, 75 | "serve": { 76 | "builder": "@angular-devkit/build-angular:dev-server", 77 | "options": { 78 | "browserTarget": "my-project:build", 79 | "proxyConfig": "frontend-src/proxy.conf.json" 80 | }, 81 | "configurations": { 82 | "production": { 83 | "browserTarget": "my-project:build:production" 84 | }, 85 | "hmr": { 86 | "hmr": true, 87 | "browserTarget": "my-project:build:hmr" 88 | } 89 | } 90 | }, 91 | "extract-i18n": { 92 | "builder": "@angular-devkit/build-angular:extract-i18n", 93 | "options": { 94 | "browserTarget": "my-project:build" 95 | } 96 | }, 97 | "test": { 98 | "builder": "@angular-devkit/build-angular:karma", 99 | "options": { 100 | "main": "frontend-src/test.ts", 101 | "polyfills": "frontend-src/polyfills.ts", 102 | "tsConfig": "frontend-src/tsconfig.spec.json", 103 | "karmaConfig": "frontend-src/karma.conf.js", 104 | "styles": [ 105 | "./node_modules/ng-zorro-antd/ng-zorro-antd.min.css", 106 | "frontend-src/styles.less" 107 | ], 108 | "scripts": [], 109 | "assets": [ 110 | "frontend-src/favicon.ico", 111 | "frontend-src/assets" 112 | ] 113 | } 114 | }, 115 | "lint": { 116 | "builder": "@angular-devkit/build-angular:tslint", 117 | "options": { 118 | "tsConfig": [ 119 | "frontend-src/tsconfig.app.json", 120 | "frontend-src/tsconfig.spec.json" 121 | ], 122 | "exclude": [ 123 | "**/node_modules/**" 124 | ] 125 | } 126 | } 127 | } 128 | } 129 | }, 130 | "defaultProject": "my-project" 131 | } -------------------------------------------------------------------------------- /02_Reversing_HMAC.md: -------------------------------------------------------------------------------- 1 | ## How does the HMAC-Signing in Jodel work? 2 | 3 | First of all read [this](https://en.wikipedia.org/wiki/HMAC)! It's important to understand what HMAC is used for in order to understand what Jodel is doing there. 4 | 5 | Each version of the Jodel app has a version-specific HMAC key. It is used to sign requests, the signature is checked by the Jodel API server. HMAC (Keyed-Hash Message Authentication Code) is a mechanism that generates a signature of given data (in our case HTTP request data) in combination with a key. Without knowledge of the HMAC key, it is impossible to sign requests and therefore it is impossible to use the Jodel API. 6 | 7 | ### Locating the HMAC signing library 8 | 9 | Since the HMAC key is an important measure to prevent attacks on the Jodel API, the HMAC key is well hidden in the application. Jodel follows best practices (see MASVS) and hides the key in a shared library bundled with the APK file. The name of the shared library responsible for generating the HMAC key is renamed in each build (/lib/\/libX.so), determining which is the correct one is only possible based on file sizes: 10 | 11 | | architecture | approx. size | 12 | |--- |--- | 13 | | x86 | 194 KB | 14 | | x86-64 | 219 KB | 15 | | armeabi-v7a | 102 KB | 16 | | arm64-v8a | 199 KB | 17 | 18 | (as of 09.04.2020, Jodel version 5.77.0, libb.so) 19 | 20 | ### What's inside 21 | The signing inside the Jodel application works as follows: 22 | 23 | The class `com.jodelapp.jodelandroidv3.api.HmacInterceptor` is used for signing the requests. It has three methods which calls the shared library using the Java native interface: 24 | ``` 25 | - private native void init(); 26 | - private native synchronized void register(String str); 27 | - private native synchronized byte[] sign(String str, String str2, byte[] bArr); 28 | ``` 29 | 30 | ### The shared library 31 | Method names in the shared library are (as of v. 5.77.0) generated during runtime. Therefore a static analysis using tools like IDA does not reveal the method names. Looking at older versions of the Jodel app shows the following methods: 32 | 33 | ##### private native void init(); 34 | This method calls the native method `sym.Java_com_jodelapp_jodelandroidv3_api_HmacInterceptor_init` in the corresponding shared object. The native method generates the HMAC-Key at runtime. 35 | 36 | Reading the assembler code (of the x86 binary) looks similar to this: 37 | ``` 38 | 39 | mov byte [eax + 0x198], 0x95 40 | mov dword [eax + 0x194], 0x9f8effc2 41 | mov byte [eax + 0x19d], 4 42 | mov dword [eax + 0x199], 0x8c0dd9e9 43 | 44 | ``` 45 | 46 | Thinking of `eax` as the address of a bytearray, the assembler code just initializes a byte array with given values. The values can be considered the _"encrypted"_ (XORed with the applications signature) key. 47 | 48 | ##### private native synchronized void register(String str); 49 | This method calls the native method `sym.Java_com_jodelapp_jodelandroidv3_api_HmacInterceptor_register`. It takes one string parameter which describes what kind of request is going to be signed in one of the next `sign()` calls. These strings look like the following example: 50 | 51 | ``` 52 | GET@/api/v3/user/config 53 | ``` 54 | 55 | This could be the implementation of a queue. A signing request is passed and queued in a std::map. The signing routine uses the values of the map to locate the signing request. 56 | 57 | ##### private native synchronized byte[] sign(String sig, String method, byte[] payload); 58 | The `sign()` method of the HmacInterceptor class performs the signing itself. It calls the native method `sym.Java_com_jodelapp_jodelandroidv3_api_HmacInterceptor_sign`. It takes three arguments: 59 | ``` 60 | sig: The APKs SHA1 signature: a4a8d4d7b09736a0f65596a868cc6fd620920fb0 (should be always this value!) 61 | method: Same string which gets called to register(String str): GET@/api/v3/user/recommendedChannels 62 | payload: GET%api.go-tellm.com%443%/api/v3/posts/location/combo%39422506-d25adbe9-4c85ef1a-1dca-4771-abd7-249e4eb16047%49.6679;9.9074%2019-01-12T12:03:32Z%channels%true%home%false%lat%49.667938232421875%lng%9.907393455505371%radius%true%skipHometown%false%stickies%true% 63 | ``` 64 | 65 | 66 | The pseudo code of the `sign()` function looks like this: 67 | 68 | ``` 69 | int Java_com_jodelapp_jodelandroidv3_api_HmacInterceptor_sign(JNIEnv *env, jobject jobj, char *sig_1, char *key_in_map, char *hmac_input) { 70 | secretKey = signature ^ xor_key; //a little more complicated, see the decryption routine 71 | 72 | // this Java calls are done from the shared library, this is only the Java part of it 73 | SecretKeySpec signingKey = new SecretKeySpec(key.getBytes(), HMAC_SHA1_ALGORITHM); 74 | Mac mac = Mac.getInstance(HMAC_SHA1_ALGORITHM); 75 | mac.init(signingKey); 76 | return toHexString(mac.doFinal(data.getBytes())); 77 | } 78 | ``` 79 | 80 | --- 81 | 82 | ### Extracting the HMAC-Key 83 | 84 | There are multiple methods extracting the HMAC key: 85 | 86 | Good old static way 87 | - Extracting the hmac-key the static way requires the hmac-signing library (as the base key is in there) and some decryption magic. There are several projects doing this, some of them work, some dont. The ones i'm aware of are: 88 | - [ojoc-keyhack by cfib90](https://bitbucket.org/cfib90/ojoc-keyhack) (the original one, utilizing objdump) 89 | - [Jodel-Keyhack-v2](https://github.com/JodelRaccoons/JodelReversing/blob/master/Jodel-Keyhack-v2) utilizing IDA Pro 7.x 90 | - [Jodel-Keyhack-v3](https://github.com/JodelRaccoons/JodelReversing/blob/master/Jodel-Keyhack-v3) this project, utilizing radare2 91 | 92 | #### Dynamic (runtime hooking ftw!) 93 | 94 | - As the native library is NOT implementing their own HMAC-Signing function, they are using the one javax.crypto classes. Hooking them using librarys like frida is [pretty easy](https://github.com/JodelRaccoons/JodelReversing/tree/master/Jodel-Keyhack-Frida) (See Readme for instructions) 95 | 96 | --- 97 | 98 | 99 | As of that, i wrote a python script which disassembles the shared object, collects the bytes and decrypts it (credits for the decryption logic to [cfib90](https://bitbucket.org/cfib90/ojoc-keyhack)), the python implementation of the decryption routine was coded by tm. To make it look better i developed this keyhack with fancy angular gui. 100 | 101 | --- 102 | 103 | 104 | ## How to get the key 105 | - lokalisieren der funktionen 106 | - Funktionsnamen sind obfuskiert 107 | - string suche nach (Ljava/lang/String;)Ljavax/crypto/Mac; 108 | - xrefs auf (Ljava/lang/String;)Ljavax/crypto/Mac; finden, funktion ist sign() 109 | - funktionslayout ist init, register, sign -> zwei funktionen weiter oben ist init() mit dem hmac key 110 | - Statische analyse und mir skript entschlüsseln 111 | - Link zu skript 112 | - Native library weist 113 | - Java funktion für HMAC wird aufgerufen 114 | 115 | -------------------------------------------------------------------------------- /04_Reversing_Android_Email_Verification.md: -------------------------------------------------------------------------------- 1 | # Android account validation (by mail) 2 | 3 | - Validation is performed using E-Mail address 4 | - Process used is [Google Firebase Authentication using E-Mail links](https://firebase.google.com/docs/auth/android/email-link-auth) 5 | 6 | ### Step 1: Requesting login 7 | 8 | - Request login using Firebase 9 | 10 | ```HTTP 11 | POST /identitytoolkit/v3/relyingparty/getOobConfirmationCode?key=AIzaSyDFUC30aJbUREs-vKefE6QmvoVL0qqOv60 HTTP/2 12 | Host: www.googleapis.com 13 | Content-Type: application/json 14 | X-Android-Package: com.tellm.android.app 15 | X-Android-Cert: A4A8D4D7B09736A0F65596A868CC6FD620920FB0 16 | Accept-Language: en-US 17 | X-Client-Version: Android/Fallback/X21000001/FirebaseCore-Android 18 | Content-Length: 236 19 | Connection: Keep-Alive 20 | Accept-Encoding: gzip, deflate 21 | 22 | { 23 | "requestType": 6, 24 | "email": "", 25 | "androidInstallApp": true, 26 | "canHandleCodeInApp": true, 27 | "continueUrl": "https:\/\/jodel.com\/app\/magic-link-fallback", 28 | "androidPackageName": "com.tellm.android.app", 29 | "androidMinimumVersion": "5.116.0" 30 | } 31 | ``` 32 | 33 | - Response simply contains confirmation of success 34 | 35 | ```HTTP 36 | HTTP/2 200 OK 37 | Cache-Control: no-cache, no-store, max-age=0, must-revalidate 38 | Pragma: no-cache 39 | Expires: Mon, 01 Jan 1990 00:00:00 GMT 40 | Content-Type: application/json; charset=UTF-8 41 | Vary: Origin 42 | Vary: X-Origin 43 | Vary: Referer 44 | Server: ESF 45 | Content-Length: 94 46 | X-Xss-Protection: 0 47 | X-Frame-Options: SAMEORIGIN 48 | X-Content-Type-Options: nosniff 49 | 50 | { 51 | "kind": "identitytoolkit#GetOobConfirmationCodeResponse", 52 | "email": "" 53 | } 54 | ``` 55 | 56 | ### Step 1½: Extracting oobCode from link 57 | 58 | - The mail sent to the target address contains the "magic" link 59 | - The link itself already contains the required oobCode (out-of-band code) 60 | 61 | ``` 62 | https://ae3ts.app.goo.gl/?link=https://tellm-android.firebaseapp.com/__/auth/action?apiKey%3DAIzaSyBC5AfciIsT15NSwrfhLhsLG5UtFisbeSA%26mode%3DsignIn%26oobCode%3DXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX%26continueUrl%3Dhttps://jodel.com/app/magic-link-fallback%26lang%3Den&apn=com.tellm.android.app&amv=5.116.0 63 | => URL Decode 64 | https://ae3ts.app.goo.gl/?link=https://tellm-android.firebaseapp.com/__/auth/action?apiKey=AIzaSyBC5AfciIsT15NSwrfhLhsLG5UtFisbeSA&mode=signIn&oobCode=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX&continueUrl=https://jodel.com/app/magic-link-fallback&lang=en&apn=com.tellm.android.app&amv=5.116.0 65 | => extract URL parameter 66 | oobCode=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 67 | ``` 68 | 69 | ### Step 2: Redeeming oobCode 70 | 71 | - The code needs to be redeemed with the corresponding mail address 72 | - Creates a new Jodel Firebase user 73 | 74 | ```HTTP 75 | POST /identitytoolkit/v3/relyingparty/emailLinkSignin?key=AIzaSyDFUC30aJbUREs-vKefE6QmvoVL0qqOv60 HTTP/2 76 | Host: www.googleapis.com 77 | Content-Type: application/json 78 | X-Android-Package: com.tellm.android.app 79 | X-Android-Cert: A4A8D4D7B09736A0F65596A868CC6FD620920FB0 80 | Accept-Language: en-US 81 | X-Client-Version: Android/Fallback/X21000001/FirebaseCore-Android 82 | Content-Length: 95 83 | Connection: Keep-Alive 84 | Accept-Encoding: gzip, deflate 85 | 86 | { 87 | "email": "", 88 | "oobCode": "" 89 | } 90 | ``` 91 | 92 | - Response contains `idToken` as well as `refreshToken` 93 | - Maybe the `idToken` can already be used to register a Jodel user and save us the token refresh? 94 | 95 | ```HTTP 96 | HTTP/2 200 OK 97 | Cache-Control: no-cache, no-store, max-age=0, must-revalidate 98 | Expires: Mon, 01 Jan 1990 00:00:00 GMT 99 | Pragma: no-cache 100 | Content-Type: application/json; charset=UTF-8 101 | Vary: Origin 102 | Vary: X-Origin 103 | Vary: Referer 104 | Server: ESF 105 | Content-Length: 1351 106 | X-Xss-Protection: 0 107 | X-Frame-Options: SAMEORIGIN 108 | X-Content-Type-Options: nosniff 109 | 110 | { 111 | "kind": "identitytoolkit#EmailLinkSigninResponse", 112 | "idToken": "", 113 | "email": "", 114 | "refreshToken": "", 115 | "expiresIn": "3600", 116 | "localId": "", 117 | "isNewUser": false 118 | } 119 | ``` 120 | 121 | ### Step 3: Refreshing tokens 122 | 123 | - Not sure whether this step is neccessary 124 | - Token refresh is performed using the `refreshToken` provided in the previous response 125 | 126 | ```HTTP 127 | POST /v1/token?key=AIzaSyDFUC30aJbUREs-vKefE6QmvoVL0qqOv60 HTTP/2 128 | Host: securetoken.googleapis.com 129 | Content-Type: application/json 130 | X-Android-Package: com.tellm.android.app 131 | X-Android-Cert: A4A8D4D7B09736A0F65596A868CC6FD620920FB0 132 | Accept-Language: en-US 133 | X-Client-Version: Android/Fallback/X21000001/FirebaseCore-Android 134 | Content-Length: 273 135 | Connection: Keep-Alive 136 | Accept-Encoding: gzip, deflate 137 | 138 | { 139 | "grantType": "refresh_token", 140 | "refreshToken": "" 141 | } 142 | ``` 143 | 144 | - Response contains `access_token` and `id_token` which happened to be identical 145 | - `refresh_token` can be used to get fresh access tokens for the Firebase user 146 | 147 | ```HTTP 148 | HTTP/2 200 OK 149 | Pragma: no-cache 150 | Cache-Control: no-cache, no-store, max-age=0, must-revalidate 151 | Expires: Mon, 01 Jan 1990 00:00:00 GMT 152 | Content-Type: application/json; charset=UTF-8 153 | Vary: Origin 154 | Vary: X-Origin 155 | Vary: Referer 156 | Server: ESF 157 | Content-Length: 2237 158 | X-Xss-Protection: 0 159 | X-Frame-Options: SAMEORIGIN 160 | X-Content-Type-Options: nosniff 161 | 162 | { 163 | "access_token": "", 164 | "expires_in": "3600", 165 | "token_type": "Bearer", 166 | "refresh_token": "", 167 | "id_token": "", 168 | "user_id": "", 169 | "project_id": "425112442765" 170 | } 171 | ``` 172 | 173 | ### Step 4: Creating account using firebaseJWT 174 | 175 | - Account is created using a `device_uid` as well as the `firebaseJWT` (`access_token` or `id_token` from previous response) 176 | - Request requires HMAC signing 177 | 178 | 179 | ```HTTP 180 | POST /api/v2/users/ HTTP/1.1 181 | Host: api.jodelapis.com 182 | X-Client-Type: android_x.x.x 183 | X-Api-Version: 0.2 184 | X-Timestamp: 2022-XX-XXTXX:XX:XXZ 185 | X-Authorization: HMAC 8DECAECD87546C899E6F8865AF3D61DA7F99D6C6 186 | Content-Type: application/json; charset=UTF-8 187 | Content-Length: 1711 188 | Accept-Encoding: gzip, deflate 189 | Connection: close 190 | 191 | { 192 | "firebase_uid": "jtNECbcwmfPGgQVuyKVPpsW8UIE3", // not sure whether neccesary 193 | "device_uid": "", // can probably be random 194 | "firebaseJWT": "", // from previous request 195 | "client_id": "81e8a76e-1e02-4d17-9ba0-8a7020261b26", 196 | "iid": "", // installation id, should be ok if omitted 197 | "iid_provider": "google", 198 | "location": { 199 | "city": "Town", 200 | "country": "DE", 201 | "loc_coordinates": { 202 | "lat": , 203 | "lng": 204 | }, 205 | "loc_accuracy": 206 | }, 207 | "language": "en-US", 208 | "registration_data": { 209 | "provider": "branch.io", 210 | "channel": "", 211 | "campaign": "", 212 | "feature": "", 213 | "referrer_id": "", 214 | "referrer_branch_id": "" 215 | }, 216 | "registration_type": "signup", 217 | "adId": "", 218 | "dId": "" 219 | } 220 | ``` 221 | 222 | - Response indicates successful user registration 223 | - User is **NOT** blocked 224 | - `access_` and `refresh_token` can be used for further API requests as usual 225 | 226 | ```HTTP 227 | HTTP/1.1 200 OK 228 | Server: nginx/1.13.12 (this version is from 2018 with HIGH CVEs (CVE-2021-23017)??? might consider patching...) 229 | Content-Type: application/json; charset=utf-8 230 | Connection: close 231 | Vary: Accept-Encoding 232 | Vary: X-HTTP-Method-Override 233 | Access-Control-Allow-Origin: * 234 | X-User-Blocked: false 235 | X-Feed-Internationalizable: false 236 | X-Feed-Internationalized: false 237 | Content-Length: 2080 238 | 239 | { 240 | "access_token": "...", 241 | "refresh_token": "...", 242 | "token_type": "bearer", 243 | "expires_in": 604800, 244 | [...] 245 | ``` 246 | --------------------------------------------------------------------------------