There're two broad ways in which you can declare JNI
functions.
The first is the more obvious way in which the JNI
function has to follow a specific naming convention like JNIEXPORT void JNICALL Java_com_app_foo_bar
. You can easily identify such functions using readelf
.
The other not so obvious way is to use RegisterNatives
. Here your functions can have any signature and further they not need be exported from the shared library. Typically, you would call RegisterNatives
from JNI_OnLoad
to register the native functions to the Java Run-time.
For your binary libcms.so
, it uses the second method.
RegisterNatives
has the following prototype
jint RegisterNatives(JNIEnv *env, jclass clazz, const JNINativeMethod *methods, jint Methods);
If you analyze the code of JNI_OnLoad
you will come across RegisterNatives
call like below.

The third argument points to an array of JNINativeMethod
structures which is declared as
typedef struct {
char *name;
char *signature;
void *fnPtr;
} JNINativeMethod;
The first member is a pointer to a null terminated string denoting the function name. However, in your case all the names and signatures are encrypted.

These are decrypted at run-time by the .datadiv_decodeXXXXXXXX
family of functions. The .init_array
section contain pointers to these decryptor functions which implies they will be called at startup.

However, that is not all. The binary also employs Control Flow Flattening Obfuscation throughout, hence the execution path may not be easily discernible as shown below.

To analyze the binary you're better off resorting to dynamic analysis techniques using a tool such as Frida.
Further Read: