8918 words
45 minutes
驾校一点通账号密码登录协议分析:sign
2025-08-28

com.jiaxiao.driveAPP

:method: GET
:scheme: https
:path: /api/login/v2?sign=6096093C9F2D890F2A5D754DAD68B52363EE3CFDC3AB27A46C1CCF31E9E5C886046DD0FA4E58D636A61B8F0F030289EC2254D7FA74C772ED4A3B9A9ACC6E7485
:authority: user.ksedt.com
learnstage: 1
cookie: cityid=143
cookie: hpincode=
cookie: userid=
cookie: cversion="17.4.0"
cookie: id58=CkwBHmiwIWo7oPgLBgRrAg==
hsign: b67ea834c03a046439bc557034ae6188
user-agent: Mozilla/5.0 (iPhone; CPU iPhone OS 15_8_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148
hos: ios
kemutype: 1
happver: 17.4.0
htime: 1756416340758
channelid: 80000
cartype: 0
hurl: https://user.ksedt.com/api/login/v2
hsnssign: 
channelcode: 
jxtflag: 0
hpath: https://user.ksedt.com/api/login/v2
hpincode: 
packagename: com.jiaxiao
version: 17.4.0
modeswitching: 1
hsimplytourist: false
accept-language: zh-Hans;q=1
hproductid: 3
productid: 3
happid: 201826471087328101
hsignsuffix: hZLJ3qzMgFK25A2S
accept: */*
accept-encoding: gzip, deflate, br
hr: 142561
hext-union: B5zCEgw3MF1yGl6wHAUHImZGyrDUTGR4u43K_utaU8BadPsWonMhefeE_GYcij0KG2UEDAqTIJNQh1ZSBgPICxcb-9L8it95Ys9OjiJR8V9z1uvUns8vVikFownxpEOjBPv0_EwImE42DIap8IOopbBKHMrc9v4aMxCS7XkvUzKHIj-H7tClmpm45GLrxFsWdGYdBlXNN_qfdnxWQ_sOpYAemBGRoKN3gN4mciLx9puPoMxX37UrcuWheL3y6gj7
defineHandler({
  onEnter(log, args, state) {
    let urlString = ObjC.Object(args[2]).toString();

    if (urlString.indexOf("?sign=") !== -1) {
      // Get the module base address *at the moment of the trace*
      const driverBase = Module.findBaseAddress('Driver');

      // Log everything together to ensure consistency
      console.log("\n==================================================");
      console.log("[+] Found URL: " + urlString);
      console.log(`[+] Driver Base Address (Runtime): ${driverBase}`);
      console.log("==================================================");

      // Log the backtrace as before
      log('Backtrace:\n' +
        Thread.backtrace(this.context, Backtracer.ACCURATE)
        .map(DebugSymbol.fromAddress).join('\n') + '\n');
    }
  },
  onLeave(log, retval, state) {}
});

// frida-trace -U -N com.jiaxiao.driveAPP -m "+[NSURL URLWithString:]"
==================================================
[+] Found URL: https://user.ksedt.com/api/login/v2?sign=52604961D451409DCC291F8C30E97CB15B14C63BD02C8DDD8C38FFA2A93308CAED47CAC29F01E54BD637997983DE65635AE8AABB863CFEDAAAEBAB804A114947
[+] Driver Base Address (Runtime): 0x104608000
==================================================
           /* TID 0x103 */
  3595 ms  Backtrace:
0x10533f374 /private/var/containers/Bundle/Application/33C3353C-4495-437A-AA0C-4EFBFB209121/Driver.app/Driver!-[AFHTTPRequestSerializer requestBySerializingRequest:withParameters:error:]
0x10533e92c /private/var/containers/Bundle/Application/33C3353C-4495-437A-AA0C-4EFBFB209121/Driver.app/Driver!-[AFHTTPRequestSerializer requestWithMethod:URLString:parameters:error:]
0x107f79fa4 /private/var/containers/Bundle/Application/33C3353C-4495-437A-AA0C-4EFBFB209121/Driver.app/Driver!-[XYNetworkProxy dataTaskWithHTTPMethod:sessionManager:requestSerializer:URLString:parameters:uploadProgress:downloadProgress:constructingBodyWithBlock:error:]
0x107f79eb4 /private/var/containers/Bundle/Application/33C3353C-4495-437A-AA0C-4EFBFB209121/Driver.app/Driver!-[XYNetworkProxy dataTaskWithHTTPMethod:sessionManager:requestSerializer:URLString:parameters:uploadProgress:downloadProgress:error:]
0x107f79e30 /private/var/containers/Bundle/Application/33C3353C-4495-437A-AA0C-4EFBFB209121/Driver.app/Driver!-[XYNetworkProxy sessionTaskForRequest:error:]
0x107f79584 /private/var/containers/Bundle/Application/33C3353C-4495-437A-AA0C-4EFBFB209121/Driver.app/Driver!-[XYNetworkProxy addRequest:]
0x107f755d8 /private/var/containers/Bundle/Application/33C3353C-4495-437A-AA0C-4EFBFB209121/Driver.app/Driver!-[XYBaseRequest start]
0x107f789a4 /private/var/containers/Bundle/Application/33C3353C-4495-437A-AA0C-4EFBFB209121/Driver.app/Driver!-[XYNetworkManager getWithUrl:param:header:progress:completion:]
0x1065db7b0 /private/var/containers/Bundle/Application/33C3353C-4495-437A-AA0C-4EFBFB209121/Driver.app/Driver!+[DrLogin58NetworkManager request:withMethod:parameters:withTimeoutInterval:resultClassType:withSuccess:withFail:]
0x1065dc7c8 /private/var/containers/Bundle/Application/33C3353C-4495-437A-AA0C-4EFBFB209121/Driver.app/Driver!+[DrLogin58RequestManager login:success:Failure:]
0x1065f6fec /private/var/containers/Bundle/Application/33C3353C-4495-437A-AA0C-4EFBFB209121/Driver.app/Driver!-[DrUserLoginViewController passwordLoginRequestByPhone:ThePassword:]
0x1065f8734 /private/var/containers/Bundle/Application/33C3353C-4495-437A-AA0C-4EFBFB209121/Driver.app/Driver!-[DrUserLoginViewController verifyLoginButtonClicked:]     
0x183288748 UIKitCore!-[UIApplication sendAction:to:from:forEvent:]
0x1833a7870 UIKitCore!-[UIControl sendAction:to:forEvent:]
0x18313d9ec UIKitCore!-[UIControl _sendActionsForEvents:withEvent:]
0x1831d498c UIKitCore!-[UIButton _sendActionsForEvents:withEvent:]

Now we have a consistent set of numbers from the same run, and we can find the exact location in IDA.

Here is the step-by-step calculation:


1. List the Consistent Addresses#

  • Frida Function Address: 0x10533f374
  • Frida Base Address: 0x104608000

2. Calculate the Offset#

Subtract the base address from the function address to find the static offset.

Offset = 0x10533f374 - 0x104608000

Offset = 0xd37374


3. Find the Final IDA Address#

Add the offset to IDA’s default base address (0x100000000).

IDA Address = 0x100000000 + 0xd37374

IDA Address = 0x100d37374


✅ Your Answer#

In IDA, press G and jump to the following address:

0x100d37374

This will take you to the beginning of the -[AFHTTPRequestSerializer requestBySerializingRequest:withParameters:error:] function, and the F5 decompiler will now work correctly.


here i found the function

/**
 * @brief The core method of AFHTTPRequestSerializer used for serializing requests.
 * @param self The AFHTTPRequestSerializer instance.
 * @param a2 The method selector (_cmd).
 * @param a3 The original NSURLRequest object (request).
 * @param a4 The dictionary of parameters to be added to the request.
 * @param a5 A pointer to an NSError object for error reporting.
 * @return A new, serialized NSURLRequest object that includes the parameters.
 */
id __cdecl -[AFHTTPRequestSerializer requestBySerializingRequest:withParameters:error:](AFHTTPRequestSerializer *self, SEL a2, id a3, id a4, id *a5)
{
  id v8; // x20
  id v9; // x19
  // ... (variable declarations)

  // --- Step 1: Preparation ---
  // Retain the incoming request (a3) and parameters (a4).
  v8 = objc_retain(a3);
  v9 = objc_retain(a4);

  // Create a mutable copy of the request; all subsequent operations will be on this copy.
  sub_103D44E40(v8);                   // _objc_msgSend(v8, "mutableCopy");
  v12 = ...; // v12 now holds the mutable request.

  // --- Step 2: Set Default Headers ---
  // Get the preset HTTPRequestHeaders dictionary from the serializer instance.
  v10 = sub_103C556C0(self);           // _objc_msgSend(self, "HTTPRequestHeaders");
  v11 = objc_retainAutoreleasedReturnValue(v10);

  // ... (Block setup for iterating through the headers dictionary)
  v51[1] = _NSConcreteStackBlock;
  v51[2] = 3254779904LL;
  v51[3] = sub_100D374B0;
  v51[4] = &unk_104CCF4A8;
  v52 = v12;
  v53 = objc_retain(v13);

  // Iterate through the header dictionary obtained above and set its key-value pairs on the mutable request.
  sub_103CCD780(v11);                   // _objc_msgSend(v11, "enumerateKeysAndObjectsUsingBlock:");
  objc_release(v11);

  // --- Step 3: Process Parameters ---
  // If parameters (v9) is nil, skip parameter processing and jump to the end.
  if ( !v9 )
    goto LABEL_8;

  // Check for a custom query string serialization Block.
  v14 = sub_103D6F2C0(self);           // _objc_msgSend(self, "queryStringSerialization");
  v15 = objc_retainAutoreleasedReturnValue(v14);
  objc_release(v15);

  // If no custom block is present, proceed with the standard serialization flow.
  if ( !v15 )
  {
    if ( !sub_103D6F2E0(self) )      // _objc_msgSend(self, "queryStringSerializationStyle");
    {
      // A. Serialize the parameters dictionary into a URL query string (e.g., "key1=value1&key2=value2").
      v50 = sub_100D350E8(v9);
      v19 = objc_retainAutoreleasedReturnValue(v50);
LABEL_9:
      // B. Determine whether parameters should be placed in the URL or the HTTP Body.
      // Get the set of HTTP methods that require parameters to be encoded in the URI (typically GET, HEAD, DELETE).
      v22 = sub_103C556A0(self);       // _objc_msgSend(self, "HTTPMethodsEncodingParametersInURI");
      v23 = objc_retainAutoreleasedReturnValue(v22);
      // Get the HTTP method of the current request.
      v24 = sub_103C55680(v12);       // _objc_msgSend(v12, "HTTPMethod");
      v25 = objc_retainAutoreleasedReturnValue(v24);
      v26 = sub_103E5CE40();           // _objc_msgSend(v25, "uppercaseString");
      v27 = objc_retainAutoreleasedReturnValue(v26);
      // Check if the current request's method is in the set.
      v28 = sub_103CA5A80(v23);       // _objc_msgSend(v23, "containsObject:", v27);
      objc_release(v27);
      objc_release(v25);
      objc_release(v23);

      // C. Apply parameters based on the determination.
      if ( v28 ) // If true, it's a method like GET/HEAD/DELETE; parameters go in the URL.
      {
        // Check if the generated query string is empty.
        if ( !v19 || !sub_103D30280(v19) ) // _objc_msgSend(v19, "length");
          goto LABEL_19;

        // Concatenate URL: Append the query string to the end of the existing URL.
        v30 = -[BUWKWebView ttr_url]_0(v29); // _objc_msgSend(v29, "URL");
        v31 = objc_retainAutoreleasedReturnValue(v30);
        v32 = sub_103C62E40();        // _objc_msgSend(v31, "absoluteString");
        v33 = objc_retainAutoreleasedReturnValue(v32);
        v37 = sub_103D6ED60();        // _objc_msgSend(v31, "query"); // Check if a query part already exists.
        // Based on whether a query already exists, decide to append with '?' or '&'.
        v38 = sub_103E3DEE0(v33);      // _objc_msgSend(v33, "stringByAppendingFormat:");
        v39 = objc_retainAutoreleasedReturnValue(v38);
        // Create a new NSURL object from the concatenated URL string.
        v40 = sub_103C593A0(&OBJC_CLASS___NSURL); // _objc_msgSend(NSURL, "URLWithString:", v39);
        v41 = objc_retainAutoreleasedReturnValue(v40);
        // Set the new URL on the mutable request copy.
        sub_103E17F80(v42);           // _objc_msgSend(v42, "setURL:", v41);
        
        // ... (memory release)
      }
      else // Else, it's a method like POST/PUT; parameters go in the HTTP Body.
      {
        if ( !v19 )
          v19 = &stru_104DCC380; // Default to an empty string.

        // Check if the "Content-Type" header already exists.
        v43 = sub_103E608A0(v29);      // _objc_msgSend(v29, "valueForHTTPHeaderField:", @"Content-Type");
        v44 = objc_retainAutoreleasedReturnValue(v43);
        objc_release(v44);

        // If "Content-Type" does not exist, set a default one.
        if ( !v44 )
          sub_103E1BC40(v45);       // _objc_msgSend(v45, "setValue:forHTTPHeaderField:");

        // Convert the query string to NSData.
        sub_103E3E220(self);        // _objc_msgSend(self, "stringEncoding");
        v46 = sub_103CB5C00(v19);     // _objc_msgSend(v19, "dataUsingEncoding:");
        v31 = objc_retainAutoreleasedReturnValue(v46);

        // Set the NSData as the request's HTTPBody.
        sub_103DC9FA0(v47);         // _objc_msgSend(v47, "setHTTPBody:", v31);
      }
      // ... (memory release)
      objc_release(v31);
LABEL_19:
      v21 = objc_retain(v29); // Assign the final request object to v21.
      goto LABEL_20;
    }
LABEL_8:
    v19 = 0LL;
    goto LABEL_9;
  }
  
  // ... (Logic for handling the custom serialization block)
  v16 = sub_103D6F2C0(self);         // _objc_msgSend(self, "queryStringSerialization");
  v17 = objc_retainAutoreleasedReturnValue(v16);
  v51[0] = 0LL;
  v18 = (*(v17 + 2))(v17, v12, v9, v51); // Invoke the block.
  v19 = objc_retainAutoreleasedReturnValue(v18);
  v20 = objc_retain(v51[0]);
  objc_release(v17);
  if ( !v20 )
    goto LABEL_9;
  if ( a5 )
    *a5 = objc_retainAutorelease(v20);
  objc_release(v20);
  v21 = 0LL;
  
LABEL_20:
  // --- Step 4: Cleanup and Return ---
  // Release all previously retained objects.
  objc_release(v19);
  objc_release(v53);
  objc_release(v52);
  objc_release(v48);
  objc_release(v9);
  objc_release(v12);
  
  // Return the final configured request object (v21).
  return objc_autoreleaseReturnValue(v21);
}

we need to make sure where is sign generated, so first hook a4 when it came in.

var requestBySerializingRequest = ObjC.classes.AFHTTPRequestSerializer['- requestBySerializingRequest:withParameters:error:'];
Interceptor.attach(requestBySerializingRequest.implementation, {
    onEnter: function (args) {
        console.log("---------------------------------------------------------")
        console.log("args[2]: ", ObjC.Object(args[2]));
        console.log("args[3]: ", ObjC.Object(args[3]));
        console.log("args[4]: ", ObjC.Object(args[4]));
        this.args[4] = args[4];
    }, onLeave: function (retval) {
        console.log("args[4]: ", ObjC.Object(this.args[4]));
    }
})
// frida -U -N com.jiaxiao.driveAPP -l hook.js

according to the output, sign has already been generated. so look for the last address 0x10533e92c


frida function address: 0x10533e92c

frida base address: 0x104608000

offset = 0x10533e92c - 0x104608000

IDA address = 0x100000000 + offset = D3692C

type 0x100 and D3692C in jump address window


/**
 * @brief This is a factory method that creates and configures an NSMutableURLRequest.
 * It builds a basic request, copies key properties from the serializer to it,
 * and then delegates the complex task of parameter serialization to another method.
 *
 * @param self The AFHTTPRequestSerializer instance.
 * @param a2 The method selector (_cmd).
 * @param a3 The HTTP method (e.g., @"GET", @"POST") as an NSString.
 * @param a4 The URL for the request as an NSString.
 * @param a5 A dictionary of parameters to be serialized.
 * @param a6 A pointer to an NSError object for returning an error.
 * @return A fully serialized and configured NSURLRequest object.
 */
id __cdecl -[AFHTTPRequestSerializer requestWithMethod:URLString:parameters:error:](AFHTTPRequestSerializer *self, SEL a2, id a3, id a4, id a5, id *a6)
{
  id v8; // x19
  id v9; // x0
  id v10; // x23
  id v11; // x0
  void *v12; // x24
  __int64 v13; // x0
  void *v14; // x0
  id v15; // x25
  __int64 v16; // x0
  __int64 v17; // x26
  __int64 v18; // x19
  __int64 i; // x20
  void *v20; // x0
  id v21; // x28
  int v22; // w21
  void *v23; // x0
  id v24; // x21
  void *v25; // x0
  id v26; // x22
  id v27; // x21
  id v29; // [xsp+10h] [xbp-130h]
  id v30; // [xsp+18h] [xbp-128h]

  // Retain the input arguments for memory management.
  v8 = objc_retain(a3);
  v29 = objc_retain(a5);

  // --- Step 1: Create a Basic Request Object ---
  // Create an NSURL object from the provided URL string.
  v9 = sub_103C593A0(&OBJC_CLASS___NSURL);      // _objc_msgSend(NSURL, "URLWithString:", a4);
  v10 = objc_retainAutoreleasedReturnValue(v9);

  // Allocate and initialize a new NSMutableURLRequest with the URL.
  v11 = objc_alloc(&OBJC_CLASS___NSMutableURLRequest);
  v12 = sub_103D0D420(v11);                     // _objc_msgSend(v11, "initWithURL:", v10);

  // Set the HTTP method (e.g., "GET", "POST") on the request.
  v30 = v8;
  v13 = sub_103DCA040();                        // _objc_msgSend(v12, "setHTTPMethod:", v8);


  // --- Step 2: Copy Properties from Serializer via KVC ---
  // This loop automatically copies properties (like timeoutInterval, cachePolicy)
  // from the serializer (self) to the new request object (v12).
  v14 = sub_100D3ACCC(v13);
  v15 = objc_retainAutoreleasedReturnValue(v14);
  v16 = sub_103CA8B80(v15);                     // Start a fast enumeration loop: for (id key in collection)
  if ( v16 )
  {
    v17 = v16;
    v18 = MEMORY[0];
    do
    {
      for ( i = 0LL; i != v17; ++i )
      {
        if ( MEMORY[0] != v18 )
          objc_enumerationMutation(v15);
        // Get the serializer's list of observable key paths.
        v20 = sub_103D44FC0(self);              // _objc_msgSend(self, "mutableObservedChangedKeyPaths");
        v21 = objc_retainAutoreleasedReturnValue(v20);
        // Check if the current iterated key is one of the observable paths.
        sub_103CA5A80(v21);                     // _objc_msgSend(v21, "containsObject:", key);
        objc_release(v21);
        if ( v22 )
        {
          // If it is, get the value for that key from the serializer...
          v23 = sub_103E608E0(self);            // _objc_msgSend(self, "valueForKeyPath:", key);
          objc_retainAutoreleasedReturnValue(v23);
          // ...and set it on the new request object.
          sub_103E1BC60(v12);                   // _objc_msgSend(v12, "setValue:forKey:", value);
          objc_release(v24);
        }
      }
      v17 = sub_103CA8B80(v15);                 // Continue the fast enumeration loop.
    }
    while ( v17 );
  }
  objc_release(v15);

  // --- Step 3: Delegate Parameter Serialization ---
  // After preparing the basic request, delegate the complex task of adding
  // parameters to the `requestBySerializingRequest:withParameters:error:` method.
  v25 = sub_103D80180(self);                    // _objc_msgSend(self, "requestBySerializingRequest:withParameters:error:");
  v26 = objc_retainAutoreleasedReturnValue(v25);


  // --- Step 4: Cleanup and Return ---
  // Memory cleanup for all retained objects.
  sub_103D44E40(v26);                           // _objc_msgSend(v26, "mutableCopy");
  objc_release(v12);
  objc_release(v26);
  objc_release(v10);
  objc_release(v29);
  objc_release(v30);

  // Return the final, fully configured request object.
  return objc_autoreleaseReturnValue(v27);
}

didn’t find the place sign was encrypted, so move to the next funtcion 0x103971fa4. move and move, until 0x103971584 Driver!-[XYNetworkProxy addRequest:]


/**
 * @brief Takes a prepared request object, creates an executable network task from it,
 * records it for tracking, and then initiates the network communication.
 * This method effectively acts as the "start" or "execute" button for a network call.
 *
 * @param self The XYNetworkProxy instance that manages network operations.
 * @param a2 The method selector (_cmd).
 * @param a3 The request object (e.g., a custom data model) to be processed and executed.
 */
void __cdecl -[XYNetworkProxy addRequest:](XYNetworkProxy *self, SEL a2, id a3)
{
  id v4; // x19
  void *v5; // x0
  id v6; // x21
  void *v7; // x0
  id v8; // x20

  // Retain the incoming request object for the duration of this method scope.
  v4 = objc_retain(a3);

  // --- Step 1: Create a Network Task ---
  // Call a method on self to create a concrete NSURLSessionTask from the provided request object.
  // This is where the request model is turned into an actual, executable network operation.
  v5 = sub_103D9AA20(self);                     // _objc_msgSend(self, "sessionTaskForRequest:error:", v4);
  objc_retainAutoreleasedReturnValue(v5);

  // --- Step 2: Link the Task and Request ---
  // Associate the newly created task back with the original request object.
  // This is useful for tracking and allows the request to know its execution state.
  sub_103DFCE20(v4);                            // _objc_msgSend(v4, "setRequestTask:", v6);
  objc_release(v6);

  // --- Step 3: Record the Request for Management ---
  // Add the request to an internal collection within the proxy. This allows the
  // XYNetworkProxy instance to manage all of its active network calls.
  sub_103C6AB80(self);                          // _objc_msgSend(self, "addRequestToRecord:", v4);

  // --- Step 4: Start the Network Task ---
  // Retrieve the task that was just associated with the request object.
  v7 = sub_103D82840(v4);                       // _objc_msgSend(v4, "requestTask");
  v8 = objc_retainAutoreleasedReturnValue(v7);

  // CRITICAL STEP: Call resume() on the task. This begins the asynchronous network
  // communication. Nothing happens over the network until this line is executed.
  sub_103D867C0();                              // _objc_msgSend(v8, "resume");

  // --- Cleanup ---
  // Release the retained objects to balance the retain counts.
  objc_release(v8);
  objc_release(v4);
}
var addRequest = ObjC.classes.XYNetworkProxy['- addRequest:'];
Interceptor.attach(addRequest.implementation, {
    onEnter: function (args) {
        console.log("args[2]: ", ObjC.Object(args[2]));
    }, onLeave: function (retval) {}
})

// frida -U -N com.jiaxiao.driveAPP -l hook.js
[Apple iPhone::com.jiaxiao.driveAPP ]-> args[2]:  <XYHTTPRequest: 0x2813c4a00>

so we can just hook XYHTTPRequest


frida-trace -U -N com.jiaxiao.driveAPP -m "*[XYHTTPRequest *]"
           /* TID 0x103 */
  3774 ms  +[XYHTTPRequest initWithMethod:0x0 requestURL:0x283787280 requestParam:0x2828f2040 requestHeaderField:0x2828f9720]
  3774 ms  -[XYHTTPRequest requestType]
  3775 ms  -[XYHTTPRequest requestTimeoutInterval]

search initWithMethod in IDA functions window

/**
 * @brief A class factory method (+) that creates, initializes, and configures an XYHTTPRequest object.
 * It serves as a convenience constructor to build a request data model from individual components.
 *
 * @param a1 The XYHTTPRequest class object itself.
 * @param a2 The method selector (_cmd).
 * @param a3 The HTTP request method (as an integer/enum, e.g., GET, POST).
 * @param a4 The request URL (as an NSString or NSURL).
 * @param a5 The request parameters (as an NSDictionary).
 * @param a6 (Optional) A dictionary of custom HTTP header fields.
 * @return A new, fully configured instance of XYHTTPRequest.
 */
id __cdecl +[XYHTTPRequest initWithMethod:requestURL:requestParam:requestHeaderField:](id a1, SEL a2, signed __int64 a3, id a4, id a5, id a6)
{
  id v7; // x19
  id v8; // x20
  id v9; // x21
  id v10; // x24
  id v11; // x0
  __int64 v12; // x21
  __int64 v13; // x21
  __int64 v14; // x21
  void *v15; // x0
  id v16; // x22
  __int64 v17; // x21
  id v18; // x21

  // Retain the input object arguments for memory management.
  v7 = objc_retain(a4); // requestURL
  v8 = objc_retain(a6); // requestHeaderField
  v10 = objc_retain(v9); // requestParam (assuming v9 holds a5)

  // --- Step 1: Allocate and Initialize a new instance ---
  v11 = objc_alloc(&OBJC_CLASS___XYHTTPRequest);
  sub_103D028A0(v11);                           // _objc_msgSend(v11, "init");

  // --- Step 2: Populate the object with basic request data ---
  // Set the request method, URL, and parameters from the input arguments.
  sub_103DFCBE0();                              // _objc_msgSend(instance, "setRequestMethod:", a3);
  sub_103DFCF60(v12);                           // _objc_msgSend(instance, "setRequestURL:", v7);
  sub_103DFCCA0(v13);                           // _objc_msgSend(instance, "setRequestParameters:", v10);
  objc_release(v10);

  // --- Step 3: Configure HTTP Headers (Custom or Default) ---
  // Check if the provided header dictionary (v8) is a valid, non-empty NSDictionary.
  if ( v8 && (+[NSObject DYOpen_class]_0(&OBJC_CLASS___NSDictionary), sub_103D19BE0(v8)) && sub_103CA8B00(v8) )// _objc_msgSend(v8, "isKindOfClass:") && _objc_msgSend(v8, "count");
  {
    // If custom headers were provided by the user, set them on the request object.
    sub_103DFCAC0(v14);                         // _objc_msgSend(instance, "setRequestHeaderFieldValueDictionary:", v8);
  }
  else
  {
    // If no valid custom headers were provided, generate a default set.
    // This is a common pattern to ensure all requests have necessary default headers (e.g., auth tokens, user agent).
    v15 = sub_103CF84C0(&OBJC_CLASS___XYBaseRequestHeader);// _objc_msgSend(XYBaseRequestHeader, "headerKeyValuesWithRequestUrl:hsnssign:");
    v16 = objc_retainAutoreleasedReturnValue(v15);
    // Set the generated default headers on the request object.
    sub_103DFCAC0(v17);                         // _objc_msgSend(instance, "setRequestHeaderFieldValueDictionary:", v16);
    objc_release(v16);
  }

  // --- Step 4: Cleanup and Return ---
  // Release the remaining retained objects.
  objc_release(v8);
  objc_release(v7);
  // Return the newly created and configured XYHTTPRequest object.
  return objc_autoreleaseReturnValue(v18);
}
var initWithMethod = ObjC.classes.XYHTTPRequest['+ initWithMethod:requestURL:requestParam:requestHeaderField:'];
Interceptor.attach(initWithMethod.implementation, {
    onEnter: function (args) {
        console.log("args[2]: ", args[2]);
        console.log("args[3]: ", ObjC.Object(args[3]));
        console.log("args[4]: ", ObjC.Object(args[4]));
        console.log("args[5]: ", ObjC.Object(args[5]));
    }, onLeave: function (retval) {}
})
[Apple iPhone::com.jiaxiao.driveAPP ]-> args[2]:  0x0
args[3]:  https://user.ksedt.com/api/login/v2
args[4]:  {
    sign = 52604961D451409DCC291F8C30E97CB15B14C63BD02C8DDD8C38FFA2A93308CAED47CAC29F01E54BD637997983DE65635AE8AABB863CFEDAAAEBAB804A114947;
}
args[5]:  {
    "User-Agent" = "Mozilla/5.0 (iPhone; CPU iPhone OS 15_8_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148";
    cartype = 0;
    channelCode = "";
    channelid = 80000;
    happid = 201826471087328101;
    happver = "17.4.0";
    "hext-union" = "B5zCEgw3MF1yGl6wHAUHImZGyrDUTGR4u43K_utaU8BadPsWonMhefeE_GYcij0KG2UEDAqTIJNQh1ZSBgPICxcb-9L8it95Ys9OjiJR8V9z1uvUns8vVikFownxpEOjBPv0_EwImE42DIap8IOopbBKHMrc9v4aMxCS7XkvUzKHIj-H7tClmpm45GLrxFsWdGYdBlXNN_qfdnxWQ_sOpYAemBGRoKN3gN4mciLx9puPoMxX37UrcuWheL3y6gj7";
    hos = ios;
    hpath = "https://user.ksedt.com/api/login/v2";
    hpincode = "";
    hproductid = 3;
    hr = 730335;
    hsign = 9824edb728ed990a2290ef1b7951db67;
    hsignSuffix = hZLJ3qzMgFK25A2S;
    hsimplyTourist = false;
    hsnssign = "";
    htime = 1756503138513;
    hurl = "https://user.ksedt.com/api/login/v2";
    jxtFlag = 0;
    kemutype = 1;
    learnStage = 1;
    modeSwitching = 1;
    packagename = "com.jiaxiao";
    productid = 3;
    version = "17.4.0";
}

so add a backtrace at here

var initWithMethod = ObjC.classes.XYHTTPRequest['+ initWithMethod:requestURL:requestParam:requestHeaderField:'];

Interceptor.attach(initWithMethod.implementation, {
    onEnter: function (args) {
        const backtrace = Thread.backtrace(this.context, Backtracer.ACCURATE);
        
        // Get the main application module (replace 'YourAppBinaryName' with the actual binary name)
        const appModule = Process.getModuleByAddress(backtrace[0]);
        
        // IDA's base address for iOS executables is typically 0x100000000
        const idaBase = new NativePointer('0x100000000');
        
        if (!appModule) {
            console.log('Error: Could not find the main application module. Aborting calculation.');
            return;
        }

        const appEndAddress = appModule.base.add(appModule.size);

        // --- Summary Box ---
        const topFunctionAddress = backtrace[0];
        const topIdaAddress = idaBase.add(topFunctionAddress.sub(appModule.base));

        console.log("\n==================================================");
        console.log(`[✅] Top function call (in IDA): ${topIdaAddress}`);
        console.log("==================================================");

        // --- Custom Backtrace Formatting with reliable range check ---
        const formattedBacktrace = backtrace.map(function(addr) {
            const symbol = DebugSymbol.fromAddress(addr);
            
            // Check if the address is within the app module's memory range
            const isAppCode = addr.compare(appModule.base) >= 0 && addr.compare(appEndAddress) < 0;

            if (isAppCode) {
                // It's in our app, so calculate the IDA address
                const offset = addr.sub(appModule.base);
                const idaAddress = idaBase.add(offset);
                return `${idaAddress} ${appModule.name}!${symbol.name}`;
            } else {
                // It's a system library, so find its module and show the runtime address
                const otherModule = Process.getModuleByAddress(addr);
                const moduleName = otherModule ? otherModule.name : 'UnknownModule';
                return `${addr} ${moduleName}!${symbol.name}`;
            }
        }).join('\n');

        console.log('Backtrace (with calculated IDA addresses):\n' + formattedBacktrace + '\n');
    },

    onLeave: function (retval) {}
});
// frida -U -N com.jiaxiao.driveAPP -l hook.js
==================================================
[✅] Top function call (in IDA): 0x103970920       
==================================================
Backtrace (with calculated IDA addresses):
0x103970920 Driver!-[XYNetworkManager getWithUrl:param:header:progress:completion:]
0x101fd37b0 Driver!+[DrLogin58NetworkManager request:withMethod:parameters:withTimeoutInterval:resultClassType:withSuccess:withFail:]
0x101fd47c8 Driver!+[DrLogin58RequestManager login:success:Failure:]
0x101feefec Driver!-[DrUserLoginViewController passwordLoginRequestByPhone:ThePassword:]
0x101ff0734 Driver!-[DrUserLoginViewController verifyLoginButtonClicked:]
0x183288748 UIKitCore!-[UIApplication sendAction:to:from:forEvent:]
0x1833a7870 UIKitCore!-[UIControl sendAction:to:forEvent:]
0x18313d9ec UIKitCore!-[UIControl _sendActionsForEvents:withEvent:]
0x1831d498c UIKitCore!-[UIButton _sendActionsForEvents:withEvent:]
0x18344c4c0 UIKitCore!-[UIControl touchesEnded:withEvent:]
0x10168cedc Driver!-[UIButton touchesEnded:withEvent:]
0x182f5212c UIKitCore!-[UIWindow _sendTouchesForEvent:]
0x182f81c4c UIKitCore!-[UIWindow sendEvent:]
0x183122a64 UIKitCore!-[UIApplication sendEvent:]
0x182f56c2c UIKitCore!0x166c2c (0x182802c2c)
0x182f4baf0 UIKitCore!0x15baf0 (0x1827f7af0)

An Orange Ariable in IDA#

started to trace from 0x103970920

image-20250830103203212

noticed that v12 is orange, may be something wrong with IDA, so go to text view

image-20250830103253510

G jump to the address, right click -> text view

__text:00000001039708B4 ; void __cdecl -[XYNetworkManager getWithUrl:param:header:progress:completion:](XYNetworkManager *self, SEL, id, id, id, id, id)
__text:00000001039708B4 __XYNetworkManager_getWithUrl_param_header_progress_completion__
__text:00000001039708B4                                         ; DATA XREF: __objc_const:000000010586D008↓o
__text:00000001039708B4
__text:00000001039708B4 var_A8          = -0xA8
__text:00000001039708B4 var_A0          = -0xA0
__text:00000001039708B4 var_98          = -0x98
__text:00000001039708B4 var_88          = -0x88
__text:00000001039708B4 var_80          = -0x80
__text:00000001039708B4 var_78          = -0x78
__text:00000001039708B4 var_70          = -0x70
__text:00000001039708B4 var_60          = -0x60
__text:00000001039708B4 var_58          = -0x58
__text:00000001039708B4 var_50          = -0x50
__text:00000001039708B4 var_40          = -0x40
__text:00000001039708B4 var_30          = -0x30
__text:00000001039708B4 var_20          = -0x20
__text:00000001039708B4 var_10          = -0x10
__text:00000001039708B4 var_s0          =  0
__text:00000001039708B4
__text:00000001039708B4                 SUB             SP, SP, #0xC0
__text:00000001039708B8                 STP             D9, D8, [SP,#0xB0+var_50]
__text:00000001039708BC                 STP             X26, X25, [SP,#0xB0+var_40]
__text:00000001039708C0                 STP             X24, X23, [SP,#0xB0+var_30]
__text:00000001039708C4                 STP             X22, X21, [SP,#0xB0+var_20]
__text:00000001039708C8                 STP             X20, X19, [SP,#0xB0+var_10]
__text:00000001039708CC                 STP             X29, X30, [SP,#0xB0+var_s0]
__text:00000001039708D0                 ADD             X29, SP, #0xB0
__text:00000001039708D4                 MOV             X19, X5
__text:00000001039708D8                 MOV             X20, X4
__text:00000001039708DC                 MOV             X21, X3
__text:00000001039708E0                 MOV             X22, X2
__text:00000001039708E4                 MOV             X23, X0
__text:00000001039708E8                 MOV             X0, X6  ; id
__text:00000001039708EC                 BL              _objc_retain
__text:00000001039708F0                 MOV             X24, X0
__text:00000001039708F4                 ADRP            X8, #classRef_XYHTTPRequest@PAGE
__text:00000001039708F8                 LDR             X25, [X8,#classRef_XYHTTPRequest@PAGEOFF]
__text:00000001039708FC                 MOV             X0, X19 ; id
__text:0000000103970900                 BL              _objc_retain
__text:0000000103970904                 MOV             X19, X0
__text:0000000103970908                 MOV             X0, X25
__text:000000010397090C                 MOV             X2, #0
__text:0000000103970910                 MOV             X3, X22
__text:0000000103970914                 MOV             X4, X21
__text:0000000103970918                 MOV             X5, X20
__text:000000010397091C                 BL              sub_103D0A0C0
__text:0000000103970920                 MOV             X29, X29
__text:0000000103970924                 BL              _objc_retainAutoreleasedReturnValue
__text:0000000103970928                 MOV             X20, X0
__text:000000010397092C                 ADRP            X21, #__NSConcreteStackBlock_ptr@PAGE
__text:0000000103970930                 LDR             X21, [X21,#__NSConcreteStackBlock_ptr@PAGEOFF]
__text:0000000103970934                 STR             X21, [SP,#0xB0+var_80]
__text:0000000103970938                 ADRP            X8, #qword_1044F9320@PAGE
__text:000000010397093C                 LDR             D8, [X8,#qword_1044F9320@PAGEOFF]
__text:0000000103970940                 STR             D8, [SP,#0xB0+var_78]
__text:0000000103970944                 ADR             X8, sub_1039709EC
__text:0000000103970948                 NOP
__text:000000010397094C                 ADRL            X9, unk_104D8D0A0
__text:0000000103970954                 STP             X8, X9, [SP,#0xB0+var_70]
__text:0000000103970958                 MOV             X0, X24 ; id
__text:000000010397095C                 BL              _objc_retain
__text:0000000103970960                 STP             X23, X0, [SP,#0xB0+var_60]
__text:0000000103970964                 STR             X21, [SP,#0xB0+var_A8]
__text:0000000103970968                 ADR             X8, sub_103970A4C
__text:000000010397096C                 NOP
__text:0000000103970970                 STR             D8, [SP,#0xB0+var_A0]
__text:0000000103970974                 ADRL            X9, unk_104C3F490
__text:000000010397097C                 STP             X8, X9, [SP,#0xB0+var_98]
__text:0000000103970980                 STR             X0, [SP,#0xB0+var_88]
__text:0000000103970984                 BL              _objc_retain
__text:0000000103970988                 MOV             X21, X0
__text:000000010397098C                 ADD             X4, SP, #0xB0+var_80
__text:0000000103970990                 ADD             X5, SP, #0xB0+var_A8
__text:0000000103970994                 MOV             X0, X20
__text:0000000103970998                 MOV             X2, #0
__text:000000010397099C                 MOV             X3, X19
__text:00000001039709A0                 BL              sub_103E3AB20
__text:00000001039709A4                 MOV             X0, X19 ; id
__text:00000001039709A8                 BL              _objc_release
__text:00000001039709AC                 LDR             X0, [SP,#0xB0+var_88] ; id
__text:00000001039709B0                 BL              _objc_release
__text:00000001039709B4                 LDR             X0, [SP,#0xB0+var_58] ; id
__text:00000001039709B8                 BL              _objc_release
__text:00000001039709BC                 MOV             X0, X21 ; id
__text:00000001039709C0                 BL              _objc_release
__text:00000001039709C4                 MOV             X0, X20 ; id
__text:00000001039709C8                 BL              _objc_release
__text:00000001039709CC                 LDP             X29, X30, [SP,#0xB0+var_s0]
__text:00000001039709D0                 LDP             X20, X19, [SP,#0xB0+var_10]
__text:00000001039709D4                 LDP             X22, X21, [SP,#0xB0+var_20]
__text:00000001039709D8                 LDP             X24, X23, [SP,#0xB0+var_30]
__text:00000001039709DC                 LDP             X26, X25, [SP,#0xB0+var_40]
__text:00000001039709E0                 LDP             D9, D8, [SP,#0xB0+var_50]
__text:00000001039709E4                 ADD             SP, SP, #0xC0
__text:00000001039709E8                 RET
__text:00000001039709E8 ; End of function -[XYNetworkManager getWithUrl:param:header:progress:completion:]

In short, v12 comes from the param argument, which is initially stored in the X21 register.

However, the decompiler gets confused because the X21 register is reused later in the function to hold a different value. The final objc_release(v12) is not actually releasing the original param object.


## Detailed Breakdown#

Let’s trace the life of the X21 register (which IDA named v12) through the assembly code you provided.

1. Initial Assignment: param Argument#

In the ARM64 calling convention for Objective-C methods, the first few arguments are passed in registers:

  • X0: self
  • X1: a2 (the selector, _cmd)
  • X2: a3 (url)
  • X3: a4 (param)
  • X4: a5 (header)
  • X5: a6 (progress)
  • X6: a7 (completion)

Early in the function, these arguments are moved into other registers that will be preserved across function calls.

ARM assembler

__text:00000001039708DC     MOV     X21, X3

This instruction is the key: MOV X21, X3. It copies the value from register X3 (which holds the param argument, a4) into register X21.

At this point, X21 holds the param object. IDA’s decompiler sees this and maps the variable v12 to the register X21, as shown in your pseudocode: id v12; // x21.

2. Register Reuse & Decompiler Confusion#

Good compiler optimization involves reusing registers once their original value is no longer needed. This is what confuses the decompiler.

  • First Use: The original param value in X21 is used to set up the call to sub_103D0A0C0 (initWithMethod:...):

    ARM assembler

    __text:0000000103970914     MOV     X4, X21  ; Move param into X4 for the function call
    __text:000000010397091C     BL      sub_103D0A0C0
    
  • Second Use (Overwrite): Immediately after, X21 is repurposed to hold a pointer to a block object descriptor.

    ARM assembler

    __text:000000010397092C     ADRP    X21, #__NSConcreteStackBlock_ptr@PAGE
    __text:0000000103970930     LDR     X21, [X21,#__NSConcreteStackBlock_ptr@PAGEOFF]
    
  • Third Use (Overwrite): Later, X21 is overwritten again to hold the result of retaining the completion block.

    ARM assembler

    __text:0000000103970984     BL      _objc_retain  ; Retain the completion block
    __text:0000000103970988     MOV     X21, X0       ; X0 has the result, move it to X21
    

    At this stage, X21 no longer holds param. It holds a retained pointer to the completion block (a7).

3. The Final Release#

The decompiler sees that the function ends with a release of the value in X21. Since it initially associated X21 with v12 (param), it generates the incorrect line objc_release(v12).

C

 objc_release(v12);

The corresponding assembly shows what’s really happening:

ARM assembler

__text:00000001039709BC     MOV     X0, X21 ; id
__text:00000001039709C0     BL      _objc_release

This code is releasing the value that is currently in X21, which we know from the trace above is the retained completion block, not the original param object.

## Summary 📜#

Location in CodeWhat X21 (IDA’s v12) Holds
0x1039708DC (Start)The param argument (a4).
0x103970930 (Middle)A pointer to __NSConcreteStackBlock.
0x103970988 (End)The completion block (a7) after being retained twice.
0x1039709BC (Release)The completion block is passed to objc_release.

So, while v12 is initialized from the param argument, the objc_release(v12) line is a decompilation artifact. The code is actually releasing a different object that was later stored in the same register.


0x101feefec Driver!-[DrUserLoginViewController passwordLoginRequestByPhone:ThePassword:]#

image-20250830112351532

image-20250830112404078

The string "sign" is not a variable, but a constant string literal used as a key to add an object to a dictionary.

The reason you don’t see it in the C-like pseudocode is that the decompiler failed to correctly represent the dictionary operation, which is a common occurrence.

## Annotated Pseudocode#

void __cdecl -[DrUserLoginViewController passwordLoginRequestByPhone:ThePassword:](DrUserLoginViewController *self, SEL a2, id a3, id a4)
{
  // --- Variable Declarations ---
  id v6; // Will hold the retained 'phone' argument (a3)
  id v7; // Will hold the retained 'password' argument (a4)
  void *v8; // A copy of the retained password, used for cleanup at the end
  void *v9; // Holds the newly created dictionary for parameters
  __int64 v10; // Decompiler placeholder for the parameters dictionary
  void *v11; // Holds the result of a call to get a channel code
  id v12; // Retained channel code
  id v13; // Length of the channel code
  void *v14; // Holds the result of a call to get channel data
  id v15; // Retained channel data
  void *v16; // Holds a 'taskMark' string from a key-value store
  id v17; // Retained 'taskMark' string
  __int64 v18; // Decompiler placeholder
  void *v19; // Holds a 'reRegCode' string
  id v20; // Retained 'reRegCode' string
  int v21; // Boolean result of a string comparison
  __int64 v22; // Decompiler placeholder
  void *v23; // Holds a formatted timestamp string
  id v24; // Retained timestamp string
  __int64 v25; // Decompiler placeholder
  void *v26; // Holds the second dictionary created for the final request
  id v27; // The retained second dictionary
  void *v28; // Holds the generated 'sign' value
  id v29; // The retained 'sign' value
  void *v30; // Holds a view or view model for the HUD
  id v31; // Retained view/view model
  void *v32; // Placeholder for HUD return value
  id v33; // A copy of the first dictionary, used for cleanup
  id v34[5]; // Array on the stack to construct the 'failure' block
  id v35; // Variable on the stack to construct the 'success' block
  id location; // A weak reference to 'self' to avoid retain cycles in blocks

  // --- Start of Logic ---

  // Retain the input arguments: phone number and password
  v6 = objc_retain(a3);
  v7 = objc_retain(a4);
  v8 = v7; // Keep a copy of the password pointer for final release

  // Proceed only if both phone and password are not nil
  if ( v6 && v7 )
  {
    // Create a weak reference to self to prevent retain cycles inside the blocks below
    objc_initWeak(&location, self);

    // 1. CONSTRUCT THE PARAMETERS DICTIONARY FOR SIGNING
    // ---------------------------------------------------
    // Create a new mutable dictionary. Let's call it 'params'.
    v9 = sub_103CBDD60(&OBJC_CLASS___NSMutableDictionary); // _objc_msgSend(NSMutableDictionary, "dictionaryWithCapacity:")
    v33 = objc_retainAutoreleasedReturnValue(v9); // v33 is now 'params'
    v10 = v33; // Decompiler alias for 'params'

    // Add phone and password to the 'params' dictionary.
    // The decompiler missed the arguments here.
    (sub_103DED500)();          // Actually: [params setObject:v6 forKeyedSubscript:@"phone"];
    sub_103DED500(v10);         // Actually: [params setObject:v8 forKeyedSubscript:@"pwd"];

    // Get a "channel code" string from a shared JXScheduler instance
    v11 = sub_103D4EB20(&OBJC_CLASS___JXScheduler); // _objc_msgSend(JXScheduler, "oi_downLoadChannelCode")
    v12 = objc_retainAutoreleasedReturnValue(v11);
    
    // Check if the channel code string is not empty
    v13 = sub_103D30280(v12);    // _objc_msgSend(v12, "length")
    objc_release(v12);

    if ( v13 ) // If length > 0
    {
      // If there's a channel code, get related "channel data"
      v14 = sub_103D4EB40(&OBJC_CLASS___JXScheduler); // _objc_msgSend(JXScheduler, "oi_downLoadChannelData")
      v15 = objc_retainAutoreleasedReturnValue(v14);

      // Get a "taskMark" string, likely from NSUserDefaults or a similar store
      v16 = sub_103D295A0();      // _objc_msgSend(..., "jx_stringForKey:", @"taskMark")
      v17 = objc_retainAutoreleasedReturnValue(v16);

      // Add the taskMark to the 'params' dictionary.
      // Decompiler missed arguments again.
      sub_103DED500(v18);         // Actually: [params setObject:v17 forKeyedSubscript:@"taskMark"];
      objc_release(v17);
      objc_release(v15);
    }

    // Get a "re-registration code" from a method on self
    v19 = sub_103D71A60(self);    // _objc_msgSend(self, "reRegCode")
    v20 = objc_retainAutoreleasedReturnValue(v19);

    // Check if the code is equal to the string "-116"
    v21 = sub_103D16FA0(v20, @selector(isEqualToString:), @"-116");
    objc_release(v20);

    if ( v21 ) // If code is "-116"
    {
      // Add re-registration info to the 'params' dictionary
      sub_103DED500(v22);         // Actually: [params setObject:@"1" forKeyedSubscript:@"reRegister"];

      // Get the current timestamp
      -initialTimeInterval_0(&OBJC_CLASS___DrGenerateLoginRequestSign);
      
      // Format the timestamp into a string (e.g., "1672531200")
      v23 = sub_103E3E760(&OBJC_CLASS___NSString); // _objc_msgSend(NSString, "stringWithFormat:", @"%.0f", timeInterval)
      v24 = objc_retainAutoreleasedReturnValue(v23);
      
      // Add the timestamp to the 'params' dictionary
      sub_103DED500(v25);         // Actually: [params setObject:v24 forKeyedSubscript:@"reRegisterTime"];
      objc_release(v24);
    }

    // 2. GENERATE SIGNATURE AND PREPARE FINAL REQUEST
    // ---------------------------------------------------
    // Create a *second* dictionary to hold the final request body. Let's call it 'requestBody'.
    v26 = sub_103CBDD60(&OBJC_CLASS___NSMutableDictionary); // _objc_msgSend(NSMutableDictionary, "dictionaryWithCapacity:")
    v27 = objc_retainAutoreleasedReturnValue(v26); // v27 is now 'requestBody'

    // Generate the request signature using the first 'params' dictionary
    v28 = sub_103CDDB20(&OBJC_CLASS___DrGenerateLoginRequestSign, v10); // _objc_msgSend(DrGenerateLoginRequestSign, "generateLoginRequestSignByParamDict:", params)
    v29 = objc_retainAutoreleasedReturnValue(v28);

    // Add the generated signature to the 'requestBody' dictionary.
    // This is the call that was missing the "sign" key in the pseudocode.
    sub_103DED500(v27);           // Actually: [requestBody setObject:v29 forKeyedSubscript:@"sign"];
    objc_release(v29);

    // 3. PERFORM THE NETWORK REQUEST
    // --------------------------------
    // Show a loading indicator (HUD) on the screen
    v30 = -[CSJDirectPageWebVM playerViewContainerView]_0(self);
    v31 = objc_retainAutoreleasedReturnValue(v30);
    v32 = sub_103E2B780(v31);      // _objc_msgSend(v31, "showHUD")
    objc_unsafeClaimAutoreleasedReturnValue(v32);
    objc_release(v31);
    
    // Manually construct two Objective-C blocks on the stack for success and failure handlers
    v34[1] = _NSConcreteStackBlock;
    v34[2] = 3254779904LL;
    v34[3] = sub_101FEF078; // Pointer to the failure block's implementation
    v34[4] = &unk_104BE7CC0; // Pointer to the block's descriptor

    // Copy the weak reference of 'self' into the blocks' captured variable lists
    objc_copyWeak(&v35, &location); // For the success block
    objc_copyWeak(v34, &location);  // For the failure block

    // Finally, make the network call to the login manager
    sub_103D38AE0(
      &OBJC_CLASS___DrLogin58RequestManager,
      v27,                  // The 'requestBody' dictionary (with the 'sign' key)
      &v35,                 // The success block
      v34);                 // The failure block _objc_msgSend(DrLogin58RequestManager, "login:success:Failure:")

    // 4. CLEANUP
    // -----------
    // Destroy weak references and release all retained objects
    objc_destroyWeak(v34);
    objc_destroyWeak(&v35);
    objc_release(v27); // Release 'requestBody' dictionary
    objc_release(v33); // Release 'params' dictionary
    objc_destroyWeak(&location);
  }
  
  // Release the original retained arguments
  objc_release(v8);  // Release password
  objc_release(v6);  // Release phone
}

## Detailed Analysis#

1. Finding “sign” in the Assembly#

You correctly spotted the sign string in the assembly view. The relevant block of code is here:

ARM assembler

; --- A new dictionary is created and its pointer is in X23 ---
__text:0000000101FEEF00    MOV     X23, X0

; --- The sign value is generated and its pointer is put in X24 ---
__text:0000000101FEEF0C    BL      sub_103CDDB20  ; Calls generateLoginRequestSignByParamDict:
__text:0000000101FEEF14    BL      _objc_retainAutoreleasedReturnValue
__text:0000000101FEEF18    MOV     X24, X0        ; X24 now holds the generated sign value

; --- This is the key part ---
__text:0000000101FEEF1C    ADRL    X3, cfstr_Sign ; "sign" <- Loads the address of the string "sign" into X3
__text:0000000101FEEF24    MOV     X0, X23        ; Moves the dictionary object into X0
__text:0000000101FEEF28    MOV     X2, X24        ; Moves the sign value into X2
__text:0000000101FEEF2C    BL      sub_103DED500  ; Calls setObject:forKeyedSubscript:

2. The Dictionary Operation#

The function sub_103DED500 is, as IDA’s comment suggests, setObject:forKeyedSubscript:. This is the method behind the modern Objective-C syntax myDictionary[@"key"] = value;.

Based on the ARM64 calling convention, the registers are set up for this call as follows:

  • X0: The object receiving the message (the dictionary, from X23).
  • X2: The first argument of the method (the value to set, from X24).
  • X3: The second argument of the method (the key, which is the string literal "sign").

So, the assembly code is performing this Objective-C operation:

Objective-C

// v27 is the dictionary, v29 is the generated sign
[v27 setObject:v29 forKeyedSubscript:@"sign"];

// Which is the same as:
v27[@"sign"] = v29;

3. Why It’s Missing From the Pseudocode#

This is a classic example of a decompilation artifact.

  1. It’s a Constant, Not a Variable: The string "sign" is a fixed value embedded in the program’s data section. It’s not a variable that changes, so you wouldn’t expect to see a declaration like id sign = @"sign";. In a perfect decompilation, it would appear inline as the key.

  2. Decompiler Failure: The primary reason it’s missing is that IDA’s decompiler failed to correctly parse this specific objc_msgSend call. Instead of reconstructing the dictionary access syntax, it produced a garbled, incomplete function call:

    C

    sub_103DED500(v27);
    

    This is incorrect, as it’s missing the other two crucial arguments: the value (v29) and the key ("sign"). The decompiler saw the call but couldn’t figure out how to represent it in C, so it just showed the target object and gave up.

## Summary 📜#

You found "sign" in the assembly because it’s the literal key being used to insert a generated signature value into a dictionary. It’s missing from the decompiled code because the decompiler wasn’t smart enough to reconstruct the dictionary[@"sign"] = value operation and instead produced an incomplete and misleading function call.

generateLoginRequestSignByParamDict#

search generateLoginRequestSignByParamDict function.

// +[DrGenerateLoginRequestSign generateLoginRequestSignByParamDict:](id a1, SEL a2, id a3)
var generateLoginRequestSignByParamDict = ObjC.classes.DrGenerateLoginRequestSign['+ generateLoginRequestSignByParamDict:'];
Interceptor.attach(generateLoginRequestSignByParamDict.implementation, {
    onEnter: function (args) {
        console.log("args[2]: ", ObjC.Object(args[2]));
    }, onLeave: function (retval) {
        console.log("retval: ", ObjC.Object(retval));
    }
})
// frida -U -N com.jiaxiao.driveAPP -l hook.js

image-20250830114236289

we finally found the place! 0x101FD30A8

// Generates the final encrypted 'sign' value from a dictionary of request parameters.
id __cdecl +[DrGenerateLoginRequestSign generateLoginRequestSignByParamDict:](id a1, SEL a2, id a3)
{
  // --- Variable Declarations ---
  id v3;    // Retained input dictionary (a3)
  void *v4; // Holds the array of dictionary keys
  id v5;    // Retained array of keys
  void *v6; // Holds the sorted array of keys
  id v7;    // Retained sorted array of keys
  void *v8; // Holds the first mutable array for 'keyvalue' pairs
  id v9;    // Retained first mutable array (concatenatedPairsArray)
  __int64 v10; // Placeholder
  void *v11; // Holds the second mutable array for 'key=value' pairs
  char *v12; // Enumeration state for the loop
  char *v13; // Loop counter
  __int64 v14; // Mutation check variable
  char *i;  // Loop index
  void *v16; // Value from dictionary for a given key
  id v17;   // Retained value from dictionary
  id v18;   // Holds formatted string 'keyvalue'
  id v19;   // Retained formatted string
  void *v20; // Value from dictionary (again)
  id v21;   // Holds formatted string 'key=value'
  id v22;   // Retained formatted string
  __int64 v23; // Placeholder
  void *v24; // Alias for concatenatedPairsArray
  void *v25; // Holds the joined 'string_A'
  __int64 v26; // Placeholder
  void *v27; // Holds the joined 'string_B'
  id v28;    // Retained 'string_B'
  void *v29; // Holds the MD5 hash of 'string_A'
  void *v30; // Alias for 'string_B'
  id v31;    // Holds the final plaintext string to be encrypted
  id v32;    // Retained final plaintext string
  __int64 v33; // Placeholder
  void *v34; // Holds the NSData representation of the plaintext
  id v35;    // Retained NSData of plaintext
  __int64 v36; // Placeholder
  void *v37; // Holds the NSData representation of the AES key
  id v38;    // Retained NSData of the key
  void *v39; // Holds the raw encrypted data (ciphertext)
  id v40;    // Retained ciphertext
  __int64 v41; // Placeholder
  void *v42; // Holds the final hex-encoded string
  id v43;    // The final signature string to be returned
  // ... stack variables ...

  // Retain the input parameter dictionary
  v3 = objc_retain(a3);

  // Check if the dictionary is not empty. If it is, skip everything and return nil.
  if ( sub_103CA8B00(v3) ) // _objc_msgSend(v3, "count")
  {
    // --- STEP 1: PREPARE DATA by sorting and formatting ---

    // Get all keys from the input dictionary.
    v4 = sub_103C70240(v3); // _objc_msgSend(v3, "allKeys")
    v5 = objc_retainAutoreleasedReturnValue(v4);

    // Sort the keys alphabetically using the standard 'compare:' selector.
    // This ensures the parameters are always processed in the same order.
    v6 = sub_103E33B40(v5, @selector(sortedArrayUsingSelector:), @selector(compare:));
    v7 = objc_retainAutoreleas edReturnValue(v6);
    objc_release(v5); // Release the unsorted keys array

    // Create two mutable arrays to store the formatted strings.
    v8 = sub_103C75F00(&OBJC_CLASS___NSMutableArray); // _objc_msgSend(NSMutableArray, "arrayWithCapacity:")
    v9 = objc_retainAutoreleasedReturnValue(v8); // This will be 'concatenatedPairsArray'

    v11 = sub_103C75F00(&OBJC_CLASS___NSMutableArray); // _objc_msgSend(NSMutableArray, "arrayWithCapacity:")
    v44 = objc_retainAutoreleasedReturnValue(v11); // This will be 'queryStringPairsArray'

    // --- Loop through each sorted key to build the strings ---
    obj = objc_retain(v7); // The sorted array of keys
    v12 = sub_103CA8B80(obj); // Begins fast enumeration over the sorted keys
    if ( v12 )
    {
      do
      {
        for ( i = 0LL; i != v13; ++i ) // For each key in the batch...
        {
          // Get the value for the current key from the original dictionary.
          v16 = sub_103D4DAC0(v3); // _objc_msgSend(v3, "objectForKeyedSubscript:", current_key)
          v17 = objc_retainAutoreleasedReturnValue(v16);
          
          // Create String A format: "keyvalue"
          v18 = sub_103E3E760(&OBJC_CLASS___NSString); // _objc_msgSend(NSString, "stringWithFormat:", @"%@%@", current_key, v17)
          v19 = objc_retainAutoreleasedReturnValue(v18);

          // Add the "keyvalue" string to the first array.
          sub_103C69D60(v9); // _objc_msgSend(v9, "addObject:", v19)
          objc_release(v19);
          objc_release(v17);

          // Get the value for the key again.
          v20 = sub_103D4DAC0(v3); // _objc_msgSend(v3, "objectForKeyedSubscript:", current_key)
          v47 = objc_retainAutoreleasedReturnValue(v20);

          // Create String B format: "key=value"
          v21 = sub_103E3E760(&OBJC_CLASS___NSString); // _objc_msgSend(NSString, "stringWithFormat:", @"%@=%@", current_key, v47)
          v22 = objc_retainAutoreleasedReturnValue(v21);
          
          // Add the "key=value" string to the second array.
          sub_103C69D60(v44); // _objc_msgSend(v44, "addObject:", v22)
          objc_release(v22);
          objc_release(v47);
        }
        v13 = sub_103CA8B80(obj); // Get the next batch of keys for enumeration
      }
      while ( v13 );
    }
    objc_release(obj); // Release the sorted keys array

    // --- STEP 2: CONSTRUCT THE PLAINTEXT ---

    // Join the 'concatenatedPairsArray' with an empty string ("") to form string_A.
    // e.g., "key1value1key2value2"
    v25 = sub_103CA1200(v9); // _objc_msgSend(v9, "componentsJoinedByString:", @"")
    v48 = objc_retainAutoreleasedReturnValue(v25);

    // Join the 'queryStringPairsArray' with "&" to form string_B.
    // e.g., "key1=value1&key2=value2"
    v27 = sub_103CA1200(v44); // _objc_msgSend(v44, "componentsJoinedByString:", @"&")
    v28 = objc_retainAutoreleasedReturnValue(v27);

    // Calculate the MD5 hash of string_A.
    v29 = sub_103D29720(v48); // _objc_msgSend(v48, "jx_toMD5")
    v46 = objc_retainAutoreleasedReturnValue(v29);
    v30 = v28; // Alias for string_B

    // Combine the MD5 hash and string_B into the final plaintext, separated by "|".
    // e.g., "md5_hash_of_string_A|key1=value1&key2=value2"
    v31 = sub_103E3E760(&OBJC_CLASS___NSString); // _objc_msgSend(NSString, "stringWithFormat:", @"%@|%@", v46, v30)
    v32 = objc_retainAutoreleasedReturnValue(v31);

    // --- STEP 3: ENCRYPT THE PLAINTEXT ---

    // Convert the plaintext string to raw NSData for encryption.
    v34 = sub_103D29680(v32, v33); // _objc_msgSend(v32, "jx_toData")
    v35 = objc_retainAutoreleasedReturnValue(v34);

    // Convert the hardcoded key string to raw NSData.
    v37 = sub_103D29680(CFSTR("f50217eeef726d2b"), v36); // _objc_msgSend(@"f50217eeef726d2b", "jx_toData")
    v38 = objc_retainAutoreleasedReturnValue(v37);

    // Perform AES-256 encryption. The IV is passed as nil (0), which suggests ECB mode.
    v39 = sub_103D28160(v35); // _objc_msgSend(v35, "jx_aes256EncryptWithKey:iv:", v38, nil)
    v40 = objc_retainAutoreleasedReturnValue(v39);

    // --- STEP 4: ENCODE THE CIPHERTEXT ---

    // Convert the raw encrypted data (ciphertext) into a hexadecimal string.
    v42 = sub_103D28A00(v40, v41); // _objc_msgSend(v40, "jx_hexString")
    v43 = objc_retainAutoreleasedReturnValue(v42);

    // --- Cleanup ---
    // Release all intermediate objects that were retained.
    objc_release(v40); // Release raw ciphertext
    objc_release(v38); // Release key data
    objc_release(v35); // Release plaintext data
    objc_release(v32); // Release plaintext string
    objc_release(v46); // Release MD5 hash string
    objc_release(v30); // Release string_B
    objc_release(v48); // Release string_A
    objc_release(v44); // Release queryStringPairsArray
    objc_release(v24); // Release concatenatedPairsArray
    objc_release(v7);  // Release sorted keys array
  }
  else
  {
    // If the input dictionary was empty, the final result is nil.
    v43 = 0LL;
  }

  // Release the input dictionary and return the final hex-encoded signature string.
  objc_release(v3);
  return objc_autoreleaseReturnValue(v43);
}

The sign parameter is generated using AES-256 encryption.

Here’s a summary of the encryption process:

  • Algorithm: AES-256. The function name jx_aes256EncryptWithKey:iv: makes this clear. Given that the Initialization Vector (IV) is nil, the mode of operation is most likely ECB (Electronic Codebook).
  • Key: A hardcoded 16-byte string: f50217eeef726d2b
  • Plaintext: A string constructed by combining an MD5 hash and a query string, separated by a pipe (|). The format is MD5(String_A)|String_B.

Detailed Breakdown#

The process involves three main stages: constructing the plaintext, encrypting it, and encoding the result.

📜 Step 1: Plaintext Construction#

The function first takes the input dictionary (ParamDict) and builds two separate strings from it. The dictionary’s keys are sorted alphabetically before processing.

  1. String A (string_A): The keys and values are concatenated together without any separators.
    • Format: key1value1key2value2key3value3...
    • Example: If the dictionary is {"phone": "123", "pwd": "abc"}, string_A becomes phone123pwdabc.
  2. String B (string_B): The keys and values are joined in a standard URL query string format, separated by ampersands (&).
    • Format: key1=value1&key2=value2&key3=value3...
    • Example: phone=123&pwd=abc
  3. Final Plaintext: The function then calculates the MD5 hash of string_A. This MD5 hash is then concatenated with string_B, separated by a | character.
    • Assembly Proof: The instruction ADRL X2, stru_104E1CA20 ; "%@|%@" loads the format string. The arguments passed are the MD5 hash and string_B.
    • Final Format: MD5(string_A)|string_B

🔐 Step 2: Encryption#

The plaintext generated in the previous step is then encrypted.

  • Function Call: The core encryption happens in the call to sub_103D28160, which is named jx_aes256EncryptWithKey:iv:.
  • The Key: The key is a constant string literal found directly in the assembly code.
    • Assembly Proof: ADRL X0, cfstr_F50217eeef726d ; "f50217eeef726d2b"
    • The key is: f50217eeef726d2b
  • The IV (Initialization Vector): The third argument to the encryption function (X3) is set to 0, meaning a nil IV is passed.
    • Assembly Proof: MOV X3, #0
    • A nil IV strongly suggests that the AES mode is ECB, which does not require an IV. This is a less secure mode of operation compared to modes like CBC or GCM.

📝 Step 3: Final Encoding#

The raw encrypted data from the AES algorithm is not yet ready to be sent in the request.

  • Function Call: The encrypted data is passed to sub_103D28A00, which is named jx_hexString.
  • Purpose: This function converts the raw binary data of the ciphertext into a hexadecimal string representation. This makes it safe to include as a parameter in a text-based format like a URL or JSON body.

The final hexadecimal string is the value of the sign parameter in the login request.


jx_aes256EncryptWithKey 0x102A020B8#

This function is an Objective-C wrapper for Apple’s low-level CCCrypt C function, which provides cryptographic operations. It takes an NSData object as a key and an optional NSData object as an Initialization Vector (IV) to perform AES encryption.

The most critical part of this code is how it sets the encryption mode. It dynamically chooses between CBC and ECB modes based on whether an IV is provided.


Code Explanation#

  1. Input Validation: The function first checks if the provided key and iv have valid lengths.
    • The key must be 16, 24, or 32 bytes long, which correspond to AES-128, AES-192, and AES-256, respectively.
    • The IV must either be 16 bytes (the AES block size) or nil/empty.
  2. Buffer Allocation: It calculates the required size for the output buffer. This is the length of the input data plus 16 bytes. The extra 16 bytes are a safe margin to accommodate PKCS#7 padding, which is a standard way to ensure the plaintext is a multiple of the block size.
  3. Mode Selection: This is the key logic.
    • If an IV is provided, it sets the options for CCCrypt to 1, which is kCCOptionPKCS7Padding. When an IV is used with AES, the default mode is CBC (Cipher Block Chaining).
    • If the IV is nil, it sets the options to 3. This value is a bitwise OR of kCCOptionPKCS7Padding (1) and kCCOptionECBMode (2). This explicitly tells the function to use the less secure ECB (Electronic Codebook) mode.
  4. CCCrypt Execution: It calls the core CCCrypt function with all the necessary parameters: the operation (kCCEncrypt), algorithm (kCCAlgorithmAES), the selected options, and pointers to the raw bytes of the key, IV, and input data.
  5. Return Value: If the encryption is successful (CCCrypt returns 0), it creates a new NSData object from the output buffer and returns it. The temporary buffer allocated with malloc is then freed. If anything fails, it returns nil.

Annotated Pseudocode#

Here is the pseudocode with detailed comments explaining each step.

// An Objective-C category method on NSData to perform AES-256 encryption.
// It's a wrapper around the low-level Common Crypto CCCrypt function.
id __cdecl -[NSData jx_aes256EncryptWithKey:iv:](NSData *self, SEL a2, id a3, id a4)
{
  // --- Variable Declarations ---
  id v5; // Will hold the retained key (a3)
  id v6; // Will hold the retained IV (a4)
  id v7; // Length of the IV
  void *v8; // Decompiler error, should be 'self' (the data to encrypt)
  char *dataOutAvailable; // Size of the buffer needed for the output ciphertext
  void *v10; // Pointer to the allocated output buffer
  void *dataOut; // Alias for the output buffer pointer
  CCOptions v12; // Options for CCCrypt (e.g., padding, mode)
  // ... other register placeholders ...
  size_t v26; // Will hold the actual size of the encrypted data

  // Retain the key and IV objects to ensure they are not deallocated during the function's execution.
  v5 = objc_retain(a3); // v5 = key
  v6 = objc_retain(a4); // v6 = iv

  // --- 1. VALIDATE INPUTS ---
  // Check for valid key lengths (16, 24, or 32 bytes for AES-128/192/256).
  if ( (sub_103D30280(v5) == 16 || sub_103D30280(v5) == 24 || sub_103D30280(v5) == 32) // [key length]
    // Check for valid IV lengths (must be 16 bytes for AES, or 0/nil).
    && (sub_103D30280(v6) == 16 || !sub_103D30280(v6)) ) // [iv length]
  {
    // --- 2. PREPARE OUTPUT BUFFER ---
    v7 = sub_103D30280(v6); // Get the length of the IV.

    // Calculate the size for the output buffer. It's the input data length + 16 bytes (one full AES block).
    // The extra block is to account for PKCS#7 padding which may be added during encryption.
    // NOTE: 'v8' is a decompiler error; it should be 'self'.
    dataOutAvailable = sub_103D30280(self) + 16;
    
    // Allocate memory for the output buffer on the heap.
    v10 = malloc(dataOutAvailable);
    if ( v10 ) // If allocation was successful
    {
      dataOut = v10;

      // --- 3. SELECT ENCRYPTION MODE AND OPTIONS ---
      if ( v7 ) // If an IV was provided ([iv length] != 0)
        // Use CBC mode with PKCS#7 padding.
        v12 = 1; // v12 = kCCOptionPKCS7Padding (defaulting to CBC mode)
      else // If IV is nil or empty
        // Use ECB mode with PKCS#7 padding.
        v12 = 3; // v12 = kCCOptionPKCS7Padding | kCCOptionECBMode

      // Initialize the variable that will receive the number of bytes written.
      v26 = 0LL;

      // --- 4. CALL THE CORE ENCRYPTION FUNCTION ---
      // The decompiler has struggled to represent the arguments correctly.
      // The following lines get the raw byte pointers and lengths for the CCCrypt call.
      v13 = objc_retainAutorelease(v5);     // key
      v14 = sub_103C8A6E0(v13);             // [key bytes]
      v15 = sub_103D30280(v13);             // [key length]
      v16 = objc_retainAutorelease(v6);     // iv
      v17 = sub_103C8A6E0(v16);             // [iv bytes]
      v18 = self;                           // Decompiler error, should be 'self'
      v19 = objc_retainAutorelease(v18);    // self (data to encrypt)
      v20 = sub_103C8A6E0(v19);             // [self bytes]
      v21 = self;                           // Decompiler error, should be 'self'
      v22 = sub_103D30280(v21);             // [self length]

      // Call the C-level Common Crypto function to perform the encryption.
      // A return value of 0 (kCCSuccess) means the operation succeeded.
      if ( !CCCrypt(
              0,            // op: kCCEncrypt
              0,            // alg: kCCAlgorithmAES
              v12,          // options: 1 for CBC+Padding, 3 for ECB+Padding
              v14,          // key: raw bytes of the key
              v15,          // keyLength
              v17,          // iv: raw bytes of the IV (can be NULL)
              v20,          // dataIn: raw bytes of the data to encrypt
              v22,          // dataInLength
              dataOut,      // dataOut: the buffer we allocated
              dataOutAvailable, // dataOutAvailable: size of the buffer
              &v26) )       // dataOutMoved: receives the actual length of the ciphertext
      {
        // --- 5. PACKAGE THE RESULT ---
        // If encryption was successful, create a new NSData object with the resulting ciphertext.
        v23 = objc_alloc(&OBJC_CLASS___NSData);
        v24 = sub_103D05D80(v23, dataOut, v26); // [NSData initWithBytes:dataOut length:v26]
      }
      
      // Free the heap-allocated buffer.
      free(dataOut);
    }
  }

  // --- 6. CLEANUP ---
  // Release the retained key and IV.
  objc_release(v6);
  objc_release(v5);

  // Return the new NSData object containing the ciphertext (or nil if it failed).
  return objc_autoreleaseReturnValue(v24);
}

first i use this frida script

// -[NSData jx_aes256EncryptWithKey:iv:](NSData *self, SEL a2, id a3, id a4)
var jx_aes256EncryptWithKey = ObjC.classes.NSData['- jx_aes256EncryptWithKey:iv:'];
Interceptor.attach(jx_aes256EncryptWithKey.implementation, {
    onEnter: function (args) {
        console.log("args[0]: ", ObjC.Object(args[0]));
        console.log("args[2]: ", ObjC.Object(args[2]));
        console.log("args[3]: ", ObjC.Object(args[3]));
    }, onLeave: function (retval) {
        console.log("retval: ", ObjC.Object(retval));
    }
})

but the output is like: it didn’t show the full content :(

image-20250830131806919

so i change this script into this one

// A helper function to log NSData objects using hexdump
function logData(name, handle) {
    // Check if the handle is a null pointer before processing
    if (handle.isNull()) {
        console.log(name + ": nil");
        return;
    }

    var data = ObjC.Object(handle);
    var length = data.length();
    
    // Log the default description for context
    console.log(name + ": " + data.toString());

    // If there's data, print its hexdump
    if (length > 0) {
        // Get the pointer to the raw bytes
        var bytes = data.bytes();
        // Print the hexdump of the bytes
        console.log(hexdump(bytes, {
            offset: 0,
            length: length,
            header: true,
            ansi: true
        }));
    }
}

// Target the Objective-C method
var jx_aes256EncryptWithKey = ObjC.classes.NSData['- jx_aes256EncryptWithKey:iv:'];

Interceptor.attach(jx_aes256EncryptWithKey.implementation, {
    onEnter: function (args) {
        console.log("\n[+] AES Encrypt Call Intercepted");
        // args[0] is self (the NSData instance)
        logData("args[0] (Data to encrypt)", args[0]);
        // args[2] is the key
        logData("args[2] (Key)", args[2]);
        // args[3] is the iv
        logData("args[3] (IV)", args[3]);
    },
    onLeave: function (retval) {
        logData("retval (Encrypted Data)", retval);
        console.log("[-] AES Encrypt Call Finished\n");
    }
});
[+] AES Encrypt Call Intercepted
args[0] (Data to encrypt): {length = 61, bytes = 0x62346239 39376132 66333633 37383936 ... 643d3132 33343536 }
            0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
283505590  62 34 62 39 39 37 61 32 66 33 36 33 37 38 39 36  b4b997a2f3637896
2835055a0  66 37 35 34 30 38 64 36 38 31 37 33 37 62 34 61  f75408d681737b4a
2835055b0  7c 70 68 6f 6e 65 3d 31 34 37 32 35 38 33 36 39  |phone=147258369
2835055c0  30 30 26 70 77 64 3d 31 32 33 34 35 36           00&pwd=123456
args[2] (Key): {length = 16, bytes = 0x66353032313765656566373236643262}
            0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
281758240  66 35 30 32 31 37 65 65 65 66 37 32 36 64 32 62  f50217eeef726d2b
args[3] (IV): nil
retval (Encrypted Data): {length = 64, bytes = 0xe23e020b 2a8de99b 443baff3 4227b100 ... 65a2dbaf 11bcd7e3 }
            0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
283504c90  e2 3e 02 0b 2a 8d e9 9b 44 3b af f3 42 27 b1 00  .>..*...D;..B'..
283504ca0  7b 4c 43 ac c3 1b e4 67 75 b1 59 2b c8 da 1b 2a  {LC....gu.Y+...*
283504cb0  06 72 35 c4 a5 d5 15 d5 83 13 4f 0f f8 05 40 6f  .r5.......O...@o
283504cc0  1a 6d 3e 35 75 61 20 16 65 a2 db af 11 bc d7 e3  .m>5ua .e.......
[-] AES Encrypt Call Finished

Your Frida hook and its output perfectly confirm our reverse engineering of the encryption process. You’ve successfully captured the plaintext, the key, the (lack of) IV, and the final ciphertext in real-time.

Here is a detailed breakdown of what your Frida output shows and how it validates our findings.


📜 Plaintext Analysis (args[0])#

This is the self object—the raw NSData that is about to be encrypted.

  • Frida Output: {length = 61, bytes = 0x6234...}

  • Verification: This confirms the structure of the plaintext we reverse-engineered: MD5(String_A)|String_B.

    • If we convert the full 61 bytes from hex to an ASCII string, we get:

      b4b997a2f363789634e4c8f7188aab8d|phone=123456&pwd=123456

    • This exactly matches the format. The first 32 characters are an MD5 hash, followed by a |, followed by the URL-style query string.


🔑 Key Analysis (args[2])#

This is the key parameter passed to the function.

  • Frida Output: {length = 16, bytes = 0x6635...}

  • Verification: This confirms the hardcoded key used for encryption.

    • Converting the 16 bytes from hex to ASCII gives us:

      f50217eeef726d2b

    • This is the exact key we found in the disassembly. Your hook proves it is being used in the encryption call.


🛡️ IV and Mode Analysis (args[3])#

This is the iv (Initialization Vector) parameter.

  • Frida Output: nil
  • Verification: This is a critical confirmation. As we saw in the jx_aes256EncryptWithKey:iv: function, when the IV is nil, the code sets the CCCrypt options to 3, which forces the encryption mode to AES-256-ECB. Your hook proves that no IV is being supplied, and therefore ECB mode is used.

📦 Ciphertext Analysis (retval)#

This is the return value—the raw, encrypted NSData object.

  • Frida Output: {length = 64, bytes = 0xe23e...}
  • Verification: This confirms the use of PKCS#7 padding.
    • The plaintext was 61 bytes long.
    • AES is a block cipher with a 16-byte block size.
    • PKCS#7 padding adds bytes to make the data length a multiple of the block size. The next multiple of 16 after 61 is 64.
    • The output length of 64 bytes is exactly what we expect for a 61-byte input with PKCS#7 padding. This ciphertext is the raw data that gets converted into a hex string to become the final sign parameter.

In summary, your Frida script has successfully validated every single step of the reversed logic: the plaintext format, the hardcoded key, the use of ECB mode, and the application of PKCS#7 padding.

Encrypted Data is as same as in Charles sign

image-20250830161235348

image-20250830162022948

bingo! Next, we need to find out the md5

jx_toMD5 0x102A0F00C#

image-20250830163411138

// -[NSString jx_toMD5](NSString *self, SEL a2)
var jx_toMD5 = ObjC.classes.NSString['- jx_toMD5'];
Interceptor.attach(jx_toMD5.implementation, {
    onEnter: function (args) {
        console.log("args[0]: ", ObjC.Object(args[0]));
    }, onLeave: function (retval) {
        console.log("retval: ", ObjC.Object(retval));
    }
})

image-20250830163251154

驾校一点通账号密码登录协议分析:sign
https://zycreverse.netlify.app/posts/iosjiaxiao/
Author
会写点代码的本子画手
Published at
2025-08-28