Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add ObjC.ProxyBlock, #45

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open

add ObjC.ProxyBlock, #45

wants to merge 1 commit into from

Conversation

WeissRu
Copy link

@WeissRu WeissRu commented Dec 16, 2021

Change

Add ObjC.ProxyBlock.
Use stackblock instead of globalblock to solve the strange crash encountered in the process of replacing block implementation.

Adjust blockDescriptorAllocSize and blockDescriptorOffsets to fit ProxyBlock's 3part-Descriptor, and keep offset.rest for Original ObjC.block.

Change descriptor->size from blockDescriptorDeclaredSize to blockSize.

Problems

Haven't written a test case yet.

If the script is disposed, the application may crash.
(ObjC.block have the same problem?)

Reason

blockDescriptorDeclaredSize and blockSize

When Block_copy is used on stackblcok, descriptor->size is used to request memory to store "another part".So size should be block's size.(But have no effect on GlobalBlock)

//Code from libclosure-74/runtime.cpp _Block_copy

// Its a stack block.  Make a copy.
struct Block_layout *result =
    (struct Block_layout *)malloc(aBlock->descriptor->size);
if (!result) return NULL;
memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first

ProxyBlock

The globleblock has some difficulties in solving life cycle problems. For example:
-[NSURLSession dataTaskWithURL:completionHandler:], send a network request and process the response in a callback. Now you want to know which request created the response.

Solution 1 Try the example provided in the documentation:

const pendingBlocks = new Set();

Interceptor.attach(..., {
  onEnter(args) {
    const block = new ObjC.Block(args[4]);
    pendingBlocks.add(block); // Keep it alive
    const appCallback = block.implementation;
    block.implementation = (error, value) => {
      // Do your logging here
      const result = appCallback(error, value);
      pendingBlocks.delete(block);
      return result;
    };
  }
});

It looks good, but what happens if each incoming block is the same? For example, a developer is only concerned about whether a network request is in error and receives a response with a globalblock.

Solution 2
Create a new block to replace the old one. And call the old one in the new block. Work like ObjC.Proxy.

handler = _Block_copy(handler);
const ori_block = new ObjC.Block(handler);
const new_block = new ObjC.Block({
    implementation: function (data, resp, error) {
        // Do your logging here
        const result = ori_block.implementation(data, resp, error);
        _Block_release(ori_block.handle);
        setTimeout(function () { pendingBlocks.delete(K); }, 400);//When?
        return result;
    },
    types: ori_block.types,
});
pendingBlocks.add(new_block);
return new_block;

It looks good, doesn't it? Each request received a different callback. Just release the callback at the right time. No, not that simple.

What is the difference between pendingBlocks.delete in the first two examples? Why need 'setTimeout'?

descriptor(system) <-- block(program) --> imp(fridaFREED)

descriptor(fridaFREED) <-- block(fridaFREED) --> imp(fridaFREED) --> realblock(program)

The problem is that the callback "holder" does not necessarily release the callback immediately after the callback has been executed, and we don't know when the release will occur by current implementation.(Unless we monitor the call of _Block_relase and judge whether this parameter is a callback generated by frida, ah,and we have to maintain retain count ourselves)

For most cases, setTimeout400 is enough, but some applications will crash unless setTimeout is set to more than ten seconds. One possibility I can think of is that the callback will be released when an autorelease pool is released, so there are differences between different applications.

In summary, it might be a good idea to use a stackblock and wait for the lifecycle call(copy/dispose helper) of the OC.

use NSStackBlock instead of NSGlobalBlock.
Can auto delete js_object when oc_object being released.
@oleavr
Copy link
Member

oleavr commented Jul 20, 2022

Thanks! (And so sorry for the delay here, I somehow missed this PR.) This sounds great to me. This needs test coverage before we can land it though. Let me know if you need any guidance there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants