Deep Dive into C/C++ Compilation and Linking (Bonus): Can Dynamic Libraries Be Executed Like Executables?
I know some friends might subconsciously laugh at this topic and think I am talking nonsense. Actually, in the very beginning, I also laughed it off, thinking it was too absurd. However, in reality, dynamic libraries can indeed be executed like executable files.
Some people might immediately throw a Segment Fault at me, telling me I am spouting nonsense. You can switch to the /lib directory yourself, find a library you like, for example, I have my eye on libcurl and libcrypt, and we can try to execute it directly.
$ /lib/x86_64-linux-gnu/libcurl.so.4.8.0
Segmentation fault (core dumped)Our first thought is—why? Why did things turn out this way? The answer is simple. In subsequent blog posts, I will emphasize that generally speaking, files ending in .so are usually dynamic libraries (or shared libraries; I have already explained that in today's operating systems, we no longer need to strictly distinguish between shared libraries and dynamic libraries).
Deep Dive into C/C++ Compilation and Linking 2: Intro to Dynamic and Static Libraries - CSDN Blog
Obviously, when we directly input the absolute path of the file, the operating system's shell attempts to treat it as an independently runnable program. However, this is inconsistent with our definition of a dynamic library: a dynamic shared component containing a set of functions and data. Since shared libraries are not designed with a standard main entry point (the main function) like ordinary programs, when run directly, the execution flow is likely to jump to an invalid memory address. When the operating system detects this illegal memory access (attempting to access a memory area that the program has no right to access), it triggers a segmentation fault. I think many people, upon seeing this, are convinced that the point I made in this blog post—that dynamic libraries can be executed like executable files—is wrong.
However, that is not the case. We can try executing the C library again:
$ /lib/x86_64-linux-gnu/libc.so.6
GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3.4) stable release version 2.35.
Copyright (C) 2022 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC 11.3.0.
libc ABIs: UNIQUE IFUNC ABSOLUTE
Default branch protection: none
...Hmm? This is very different from what we thought. This time, the C library not only didn't Segfault, but even printed a very distinctive string and exited gracefully! Very mysterious, right? Don't worry, I will take you step by step to explore exactly what happened.
So, What Actually Happened?
It's simple. Let's start like this—since this involves the start of program execution, obviously friends familiar with the ELF file format will point out—perhaps our trick lies in the address pointed to by the ELF Header. It's almost easy to guess—it must be that the entry point of libc's ELF Header is inconsistent with general component-purpose libraries like libcurl. So, the tool for viewing ELF header information is the famous readelf tool.
We need to emphasize a basic piece of knowledge about the ELF format—all ELF files (executables and shared libraries) have an "entry point," which is where the CPU starts executing instructions. In other words, it tells the CPU's execution flow (the value of EIP or RIP on x86-64) a definite initial value.
$ readelf -h /lib/x86_64-linux-gnu/libcurl.so.4.8.0
...
Entry point address: 0x12710
...$ readelf -h /lib/x86_64-linux-gnu/libc.so.6
...
Entry point address: 0x27834
...Oh ho, now isn't the truth revealed? If we try to treat libcurl as an executable file, the operating system's loader reads the ELF Header and passes general checks, then sets the jump address to 0x12710. Ah ha, isn't that accessing a null pointer?
This is exactly the same nature as doing this:
int main() {
return ((void (*)())0)();
}Compile and execute it, and you get exactly:
Segmentation fault (core dumped)So what about our libc library?
$ readelf -h /lib/x86_64-linux-gnu/libc.so.6
...
Entry point address: 0x27834
...Hmm? It's really different. Don't worry, with just an address, we know nothing. The next step is to bring out our objdump magic to see the details:
Friends might ask me, why not
nm? Well, for dynamic libraries,nmexposes the addresses of externally exported symbols. Generally speaking, you can't find what exactly corresponds to the EntryPoint. But don't worry, we still have a trick, which is usingobjdumpto look at the disassembly.
$ objdump -d /lib/x86_64-linux-gnu/libc.so.6 | grep -A 20 "27834"
...
0000000000027834 <__libc_start@@GLIBC_2.34>:
27834: 48 8d 3d a5 d8 18 00 lea 0x18d8a5(%rip),%rdi # 1b50e0 <_dl_discover_osversion+0x2b0>
2783b: 48 8d 35 57 d8 18 00 lea 0x18d857(%rip),%rsi # 1b5099 <_rtld_global+0x2d9>
27842: 31 c0 xor %eax,%eax
27844: e9 07 00 00 00 jmp 27850 <__libc_start@@GLIBC_2.34+0x1c>
27849: 0f 1f 84 00 00 00 00 nop %eax,0x0(%rax)
27850: bf 01 00 00 00 mov $0x1,%edi
27855: ba e3 01 00 00 mov $0x1e3,%edx
2785a: be 5a d8 18 00 mov $0x18d85a,%esi
2785f: b8 01 00 00 00 mov $0x1,%eax
27864: 0f 05 syscall
...Don't rush. Now, let's use our memory recall technique. Starting from 0x27834, the code attempts to do these things:
x64.syscall.sh, I've put the Syscall table here.
Put
0x01intoedi. Here, the first parameter required by the system call is placed.Then put the third parameter into
edx. Come on, isn't that just the length of the string? Decimal 483.Don't worry, we also need to place the string address in
rsilater, which is the second parameter. Notice that—the instruction islea(Load Effective Address), which adds the offset to the address after the current instruction. So we can't directly look for0x18d85a, but we must add the offset of the current instruction.Reviewing this, how did
objdumpcalculate0x1b50a0? First, the base address of the current instruction is at:0x27834. The length of the instruction itself isbe 5a d8 18 00, totaling 7 bytes. So the next instruction is at0x27834 + 0x7 = 0x2783b. Adding the given offset address, that is—0x2783b + 0x18d85a = 0x1b5095. Wait, let's recheck theobjdumpoutput. The comment says# 1b5099. Let's re-calculate.0x2783b + 0x18d85a=0x1b5095. Themovis at2785a.2785a + 0x5(length of mov) =2785f.2785f + 0x18d85a=0x1b5099. OK, we are confidentobjdumpdidn't lie to us (mostly, of course it wouldn't!).
Want to see if it's really put there?
$ xxd -s 0x1b5099 -l 64 /lib/x86_64-linux-gnu/libc.so.6
...
0001b5099: 474e 5520 4320 4c69 6272 6172 7920 2855 GNU C Library (U
0001b50a9: 6275 6e74 7520 474c 4942 4320 322e 3335 buntu GLIBC 2.35
...Enough! The subsequent analysis is obviously putting 0 as the argument for exit into edi and exiting gracefully.
Can We Do This Sort of Thing?
Come on! Of course we can! Now, I will accompany you to do this job! But it will be a bit difficult because we can't rely on the libc library now. The initialization of dynamic libraries is inconsistent with our executable programs. For example, it won't actively initialize the C Runtime, it can't actively link the C library (of course, I previously specified a dynamic linker and found it useless, and the code crashed on the stack function jump; I was a bit helpless and couldn't figure it out after a long time), etc.
So, now we can make one:
// mylib.c
int add(int a, int b) {
return a + b;
}
void _start() {
add(1, 2);
// exit gracefully
__asm__ __volatile__(
"movq $60, %%rax;" // syscall number for exit is 60
"xorq %%rdi, %%rdi;" // status 0
"syscall;"
: // no output
: // no input
: "%rax", "%rdi"
);
}Compile this code:
gcc -shared -fPIC -o libmylib.so mylib.cExecute it, and you get the result!
$ ./libmylib.so
$ echo $?
0Interested readers can follow my previous analysis to walk through the process again.
So the question is, can our other executable programs use this code like using a library? Yes, they can. We just need to move the visible add symbol into a header file: cclib.h
// cclib.h
#ifndef CCLIB_H
#define CCLIB_H
int add(int a, int b);
#endifAnd in main.c, do this just like our general library programming:
// main.c
#include <stdio.h>
#include "cclib.h"
int main() {
printf("1 + 2 = %d\n", add(1, 2));
return 0;
}gcc main.c -L. -lmylib -o test_appNo pressure at all!
$ ./test_app
1 + 2 = 3