Skip to content

Commit 1e44868

Browse files
committed
feat: add elf loader
1 parent 6d437f3 commit 1e44868

31 files changed

Lines changed: 1873 additions & 2110 deletions

README.md

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1-
# Libcore-Syscall
1+
# Libcore-Syscall / Libcore-ElfLoader
22

3-
Libcore-Syscall is a Java library for Android that allows you to make any Linux system calls directly from Java code.
3+
Libcore-Syscall is a Java library for Android that allows you to make any Linux system calls or load any ELF shared objects directly from Java code.
44

55
## Features
66

77
- Support Android 5.0 - 15
88
- Support any system calls (as long as they are permitted by the seccomp filter)
9+
- Support loading any ELF shared objects (lib*.so) directly from memory
910
- Implemented in 100% pure Java 1.8
1011
- No shared libraries (lib*.so) are shipped with the library
1112
- No `System.loadLibrary` or `System.load` is used
13+
- No temporary files are created on the disk (does not require a writable path/mount point)
1214
- Small, no dependencies
1315

1416
## Usage
@@ -18,10 +20,63 @@ The library provides the following classes:
1820
- MemoryAccess/MemoryAllocator: Allocate and read/write native memory.
1921
- NativeAccess: Register JNI methods, or call native functions (such as `dlopen`, `dlsym`, etc.) directly.
2022
- Syscall: Make any Linux system calls.
23+
- DlExtLibraryLoader: Load any ELF shared objects (lib*.so) directly from memory.
2124

22-
## Example
25+
## Examples
2326

24-
Here is an example of how to use the library. It calls the `uname` system call to get the system information.
27+
Here are some examples of possible use cases.
28+
29+
### Load ELF Shared Object from Memory
30+
31+
Here is an example of how to load an ELF shared object directly from memory.
32+
It loads the `libmmkv.so` shared object and calls the `MMKV.initialize` method.
33+
34+
See [TestNativeLoader.java](demo-app/src/main/java/com/example/test/app/TestNativeLoader.java) for the complete example.
35+
36+
<details>
37+
38+
```java
39+
import com.tencent.mmkv.MMKV;
40+
41+
import dev.tmpfs.libcoresyscall.core.NativeAccess;
42+
import dev.tmpfs.libcoresyscall.elfloader.DlExtLibraryLoader;
43+
44+
public static long initializeMMKV(@NonNull Context ctx) {
45+
String soname = "libmmkv.so";
46+
// get the ELF data from somewhere
47+
byte[] elfData = getElfData(soname);
48+
49+
// load the ELF shared object from byte array
50+
// if it fails, it throws an UnsatisfiedLinkError
51+
long sHandle = DlExtLibraryLoader.dlopenExtFromMemory(elfData, soname, DlExtLibraryLoader.RTLD_NOW, 0, 0);
52+
53+
// since dlopen from memory is not a standard function, ART does not know it
54+
// we need to call JNI_OnLoad manually, as if the shared object is loaded by System.loadLibrary
55+
long jniOnLoad = DlExtLibraryLoader.dlsym(sHandle, "JNI_OnLoad");
56+
if (jniOnLoad != 0) {
57+
long javaVm = NativeAccess.getJavaVM();
58+
long ret = NativeAccess.callPointerFunction(jniOnLoad, javaVm, 0);
59+
if (ret < 0) {
60+
throw new RuntimeException("JNI_OnLoad failed: " + ret);
61+
}
62+
} else {
63+
// should not happen, MMKV uses JNI_OnLoad to register native methods
64+
throw new IllegalStateException("JNI_OnLoad not found");
65+
}
66+
// initialize MMKV, since we have already loaded the libmmkv.so from memory
67+
// MMKV does not need to load the libmmkv.so shared object again
68+
MMKV.initialize(ctx, libName -> {
69+
// no-op
70+
});
71+
return sHandle;
72+
}
73+
```
74+
75+
</details>
76+
77+
### Make System Calls
78+
79+
Here is an example of how to make syscalls with the library. It calls the `uname` system call to get the system information.
2580

2681
See [TestMainActivity.java](demo-app/src/main/java/com/example/test/app/TestMainActivity.java) for the complete example.
2782

@@ -94,11 +149,6 @@ To build the demo app:
94149
./gradlew :demo-app:assembleDebug
95150
```
96151

97-
## The Future
98-
99-
- A symbol resolver that can resolve symbols in loaded native libraries.
100-
- Loading arbitrary shared libraries (lib*.so) with 100% pure Java code in memory without writing it to the disk.
101-
102152
## Credits
103153

104154
- [pine](https://github.com/canyie/pine)

core-syscall/src/main/java/dev/tmpfs/libcoresyscall/core/MemoryAccess.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ private MemoryAccess() {
1212
throw new AssertionError("no instances");
1313
}
1414

15+
private static final boolean IS_64_BIT = NativeHelper.isCurrentRuntime64Bit();
16+
1517
public static long getPageSize() {
1618
return NativeBridge.getPageSize();
1719
}
@@ -58,10 +60,26 @@ public static void pokeInt(long address, int value) {
5860
Memory.pokeInt(address, value, false);
5961
}
6062

61-
public static short peekByte(long address) {
63+
public static byte peekByte(long address) {
6264
return Memory.peekByte(address);
6365
}
6466

67+
public static void pokeByte(long address, byte value) {
68+
Memory.pokeByte(address, value);
69+
}
70+
71+
public static long peekPointer(long address) {
72+
return IS_64_BIT ? peekLong(address) : (peekInt(address) & 0xffffffffL);
73+
}
74+
75+
public static void pokePointer(long address, long value) {
76+
if (IS_64_BIT) {
77+
pokeLong(address, value);
78+
} else {
79+
pokeInt(address, (int) value);
80+
}
81+
}
82+
6583
/**
6684
* Peek a null-terminated string from the specified address.
6785
*

core-syscall/src/main/java/dev/tmpfs/libcoresyscall/core/NativeHelper.java

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import androidx.annotation.NonNull;
44

5+
import java.io.File;
56
import java.io.FileInputStream;
67
import java.io.IOException;
78
import java.io.InputStream;
@@ -54,7 +55,14 @@ public static int getCurrentRuntimeIsa() {
5455
return sCurrentRuntimeIsa;
5556
}
5657

57-
public static int getIsaFromElfHeader(byte[] header) {
58+
/**
59+
* Get the ISA value from an ELF header.
60+
*
61+
* @param header ELF header, must be at least 32 bytes
62+
* @return ISA value
63+
* @throws IllegalArgumentException if the header is not a valid ELF header
64+
*/
65+
public static int getIsaFromElfHeader(byte[] header) throws IllegalArgumentException {
5866
if (header.length < 32) {
5967
throw new IllegalArgumentException("Invalid ELF header: length < 32");
6068
}
@@ -112,6 +120,39 @@ public static String getIsaName(int isa) {
112120
}
113121
}
114122

123+
/**
124+
* Create an ISA value from ELF class and machine.
125+
*
126+
* @param elfClass ELF class, either {@link #ELF_CLASS_32} or {@link #ELF_CLASS_64}
127+
* @param machine machine value
128+
* @return ISA value
129+
*/
130+
public static int forElfClassAndMachine(int elfClass, int machine) {
131+
if (elfClass != ELF_CLASS_32 && elfClass != ELF_CLASS_64) {
132+
throw new IllegalArgumentException("Invalid ELF class: " + elfClass);
133+
}
134+
if (machine <= 0 || machine > 0xffff) {
135+
throw new IllegalArgumentException("Invalid machine: " + machine);
136+
}
137+
return (elfClass << 16) | machine;
138+
}
139+
140+
/**
141+
* Get the ISA value of an ELF file.
142+
*
143+
* @param file the ELF file
144+
* @return the ISA value
145+
* @throws IOException if file not found or read error
146+
* @throws IllegalArgumentException if the file is not an ELF file
147+
*/
148+
public static int getElfIsaFromFile(@NonNull File file) throws IOException, IllegalArgumentException {
149+
try (FileInputStream fis = new FileInputStream(file)) {
150+
byte[] header = new byte[32];
151+
readExactly(fis, header, 0, header.length);
152+
return getIsaFromElfHeader(header);
153+
}
154+
}
155+
115156
public static boolean is64Bit(int isa) {
116157
return (isa >> 16) == ELF_CLASS_64;
117158
}

core-syscall/src/main/java/dev/tmpfs/libcoresyscall/core/Syscall.java

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,18 +101,29 @@ public static long syscall(long number, long... args) throws ErrnoException {
101101
*
102102
* @param number the syscall number
103103
* @param args the arguments of the syscall, support up to 6 arguments
104-
* @return the result of the syscall
105-
* @throws ErrnoException if the result is an error
104+
* @return the result of the syscall, or -errno if the syscall fails
106105
*/
107-
public static long syscallTempFailureRetry(long number, long... args) throws ErrnoException {
106+
public static long syscallTempFailureRetryNoCheck(long number, long... args) {
108107
if (args.length > 6) {
109108
throw new IllegalArgumentException("Too many arguments: " + args.length);
110109
}
111110
long result;
112111
do {
113112
result = syscallNoCheck(number, args);
114113
} while (result == -OsConstants.EINTR);
115-
return getResultOrThrow(result, "syscall-" + number);
114+
return result;
115+
}
116+
117+
/**
118+
* Call the specified syscall with the specified arguments and retry if the result is EINTR.
119+
*
120+
* @param number the syscall number
121+
* @param args the arguments of the syscall, support up to 6 arguments
122+
* @return the result of the syscall
123+
* @throws ErrnoException if the result is an error
124+
*/
125+
public static long syscallTempFailureRetry(long number, long... args) throws ErrnoException {
126+
return getResultOrThrow(syscallTempFailureRetryNoCheck(number, args), "syscall-" + number);
116127
}
117128

118129
/**
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package dev.tmpfs.libcoresyscall.core.impl;
2+
3+
import androidx.annotation.NonNull;
4+
5+
public class ByteArrayUtils {
6+
7+
private ByteArrayUtils() {
8+
throw new AssertionError("no instances");
9+
}
10+
11+
public static void writeInt32(@NonNull byte[] buffer, int offset, int value) {
12+
buffer[offset] = (byte) (value & 0xff);
13+
buffer[offset + 1] = (byte) ((value >> 8) & 0xff);
14+
buffer[offset + 2] = (byte) ((value >> 16) & 0xff);
15+
buffer[offset + 3] = (byte) ((value >> 24) & 0xff);
16+
}
17+
18+
public static void writeInt64(@NonNull byte[] buffer, int offset, long value) {
19+
buffer[offset] = (byte) (value & 0xff);
20+
buffer[offset + 1] = (byte) ((value >> 8) & 0xff);
21+
buffer[offset + 2] = (byte) ((value >> 16) & 0xff);
22+
buffer[offset + 3] = (byte) ((value >> 24) & 0xff);
23+
buffer[offset + 4] = (byte) ((value >> 32) & 0xff);
24+
buffer[offset + 5] = (byte) ((value >> 40) & 0xff);
25+
buffer[offset + 6] = (byte) ((value >> 48) & 0xff);
26+
buffer[offset + 7] = (byte) ((value >> 56) & 0xff);
27+
}
28+
29+
public static long alignDown(long value, long alignment) {
30+
return value & -alignment;
31+
}
32+
33+
public static long alignUp(long value, long alignment) {
34+
return (value + alignment - 1) & -alignment;
35+
}
36+
37+
public static long alignDownToPage(long value) {
38+
return alignDown(value, NativeBridge.getPageSize());
39+
}
40+
41+
public static long alignUpToPage(long value) {
42+
return alignUp(value, NativeBridge.getPageSize());
43+
}
44+
45+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package dev.tmpfs.libcoresyscall.core.impl;
2+
3+
import androidx.annotation.NonNull;
4+
5+
import java.io.FileDescriptor;
6+
import java.lang.reflect.Method;
7+
8+
public class FileDescriptorHelper {
9+
10+
private FileDescriptorHelper() {
11+
throw new AssertionError("no instance for you!");
12+
}
13+
14+
private static Method sGetIntMethod;
15+
private static Method sSetIntMethod;
16+
17+
public static int getInt(@NonNull FileDescriptor fd) {
18+
if (sGetIntMethod == null) {
19+
try {
20+
//noinspection JavaReflectionMemberAccess
21+
sGetIntMethod = FileDescriptor.class.getDeclaredMethod("getInt$");
22+
sGetIntMethod.setAccessible(true);
23+
} catch (NoSuchMethodException e) {
24+
throw ReflectHelper.unsafeThrow(e);
25+
}
26+
}
27+
try {
28+
//noinspection DataFlowIssue
29+
return (int) sGetIntMethod.invoke(fd);
30+
} catch (ReflectiveOperationException e) {
31+
throw ReflectHelper.unsafeThrowForIteCause(e);
32+
}
33+
}
34+
35+
public static void setInt(@NonNull FileDescriptor fdObj, int fdInt) {
36+
if (sSetIntMethod == null) {
37+
try {
38+
//noinspection JavaReflectionMemberAccess
39+
sSetIntMethod = FileDescriptor.class.getDeclaredMethod("setInt$", int.class);
40+
sSetIntMethod.setAccessible(true);
41+
} catch (NoSuchMethodException e) {
42+
throw ReflectHelper.unsafeThrow(e);
43+
}
44+
}
45+
if (fdInt < 0) {
46+
throw new IllegalArgumentException("invalid raw fd given: " + fdInt);
47+
}
48+
if (fdObj.valid()) {
49+
throw new IllegalArgumentException("the file descriptor object is already has a valid fd set: " + fdObj);
50+
}
51+
try {
52+
sSetIntMethod.invoke(fdObj, fdInt);
53+
} catch (ReflectiveOperationException e) {
54+
throw ReflectHelper.unsafeThrowForIteCause(e);
55+
}
56+
}
57+
58+
public static FileDescriptor wrap(int fd) {
59+
FileDescriptor fileDescriptor = new FileDescriptor();
60+
setInt(fileDescriptor, fd);
61+
return fileDescriptor;
62+
}
63+
64+
}

core-syscall/src/main/java/dev/tmpfs/libcoresyscall/core/impl/NativeBridge.java

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44
import android.system.Os;
55
import android.system.OsConstants;
66

7+
import androidx.annotation.NonNull;
8+
79
import java.lang.reflect.Method;
810
import java.util.Map;
911

1012
import dev.tmpfs.libcoresyscall.core.Syscall;
13+
import dev.tmpfs.libcoresyscall.core.impl.trampoline.BaseShellcode;
1114
import dev.tmpfs.libcoresyscall.core.impl.trampoline.CommonSyscallNumberTables;
1215
import dev.tmpfs.libcoresyscall.core.impl.trampoline.ISyscallNumberTable;
13-
import dev.tmpfs.libcoresyscall.core.impl.trampoline.ITrampolineCreator;
1416
import dev.tmpfs.libcoresyscall.core.impl.trampoline.TrampolineCreatorFactory;
1517
import dev.tmpfs.libcoresyscall.core.impl.trampoline.TrampolineInfo;
1618
import libcore.io.Memory;
@@ -25,6 +27,7 @@ private NativeBridge() {
2527
private static long sTrampolineBase = 0;
2628
private static boolean sNativeMethodRegistered = false;
2729
private static boolean sTrampolineSetReadOnly = false;
30+
private static BaseShellcode sShellcode = null;
2831

2932
public static native long nativeSyscall(int number, long arg1, long arg2, long arg3, long arg4, long arg5, long arg6);
3033

@@ -54,13 +57,29 @@ public static long getPageSize() {
5457
return ps;
5558
}
5659

60+
/**
61+
* Get the base address of the trampoline, this address is typically NOT useful.
62+
* If the trampoline is not initialized, this method will return 0.
63+
*/
64+
public static long getTrampolineBase() {
65+
return sTrampolineBase;
66+
}
67+
68+
@NonNull
69+
public static BaseShellcode getShellcode() {
70+
if (sShellcode == null) {
71+
sShellcode = TrampolineCreatorFactory.create();
72+
}
73+
return sShellcode;
74+
}
75+
5776
public static synchronized void initializeOnce() {
5877
if (!sNativeMethodRegistered) {
5978
// 1. Get the page size.
6079
long pageSize = getPageSize();
6180
// 2. Prepare the trampoline.
62-
ITrampolineCreator creator = TrampolineCreatorFactory.create();
63-
TrampolineInfo trampoline = creator.generateTrampoline((int) pageSize);
81+
BaseShellcode shellcode = getShellcode();
82+
TrampolineInfo trampoline = shellcode.generateTrampoline();
6483
// 3. Allocate a memory region for the trampoline.
6584
final int MAP_ANONYMOUS = 0x20;
6685
long address;

0 commit comments

Comments
 (0)