初探so层~更适合新手宝宝的教程

[复制链接]
查看783 | 回复0 | 2025-5-22 08:57:43 | 显示全部楼层 |阅读模式

立即注册,突破游客限制,与技术大牛交流切磋,畅享海量资源,让你的技术之路不再孤单!

您需要 登录 才可以下载或查看,没有账号?立即注册

×
本帖最后由 洋咩咩 于 2025-5-22 09:00 编辑



适用人群:适用于想要入门so层逆向的人


本次学习知识点:
1.什么是so文件?它的格式是什么
2.了解导入表和导出表
3.学会基础NDK开发,为ida逆向打下基础

温馨提示:观看本贴时开电脑模式会有更好的观感哦

话不多说,开始教程吧
什么是so文件?它的结构是什么?我们该怎么解析


so其实就是ELF可执行文件,所以,我们需要了解ELF文件是什么


截图202505220844275308.png

ELF 文件结构
我们可以使用010editor来解析so文件结构并观察(如何使用呢?)
在菜单栏中点击 Templates -> Template Repository
搜索ELF并选择ELF.bt。下载它,随后Templates -> Run Template,选择 ELF.bt
我们就可以看到一个ELF的文件结构啦

截图202505220845152850.png
可以看到,下框有刚刚我们说的五个部分,我们先看ELF Header部分

截图202505220846344258.png
截图202505220847005308.png
之后我们可以回到开头 可以看到,一个so文件是以ELF文件开头,而这就是ELF魔数了 什么是魔数?
ELF魔数
每种可执行文件的格式的开头几个字节都是很特殊的,特别是开头4个字节,通常被称为魔数(Magic Number)。通过对魔数的判断可以确定文件的格式和类型。如:ELF的可执行文件格式的头4个字节为0x7F、e、l、f;Java的可执行文件格式的头4个字节为c、a、f、e;如果被执行的是Shell脚本或perl、python等解释型语言的脚本,那么它的第一行往往是#!/bin/sh或#!/usr/bin/perl或#!/usr/bin/python,此时前两个字节#和!就构成了魔数,系统一旦判断到这两个字节,就对后面的字符串进行解析,以确定具体的解释程序路径。


ELF文件类型
ELF文件主要有三种类型,可以通过ELF Header中的e_type成员进行区分。
可重定位文件(Relocatable File):ETL_REL。一般为.o文件。可以被链接成可执行文件或共享目标文件。静态链接库属于可重定位文件。
可执行文件(Executable File):ET_EXEC。可以直接执行的程序。
共享目标文件(Shared Object File):ET_DYN。一般为.so文件。有两种情况可以使用。链接器将其与其他可重定位文件、共享目标文件链接成新的目标文件;
动态链接器将其与其他共享目标文件、结合一个可执行文件,创建进程映像。 (感兴趣的可以自己查看一下加深印象)


ELF Section Header Table
ELF 文件中的 Section Header Table(节头表) 是描述每个 Section(节)信息的一个结构数组,它存在于所有的 ELF 文件中(包括 .so、.o、可执行文件)。
每个节(Section)包含一类特定的数据,例如代码、符号表、字符串表、调试信息等,Section Header Table 就是描述这些节的元信息的集合。
32 位 ELF 文件中,Elf32_Shdr 的定义如下(64 位结构类似):
  1. typedef struct {
  2.     uint32_t sh_name;      // 节名称的字符串表索引
  3.     uint32_t sh_type;      // 节的类型(SHT_PROGBITS 等)
  4.     uint32_t sh_flags;     // 属性标志(可读、可写、可执行等)
  5.     uint32_t sh_addr;      // 在内存中的虚拟地址
  6.     uint32_t sh_offset;    // 文件中的偏移地址
  7.     uint32_t sh_size;      // 大小(字节)
  8.     uint32_t sh_link;      // 与其他节的链接(如关联的符号表)
  9.     uint32_t sh_info;      // 补充信息
  10.     uint32_t sh_addralign; // 对齐要求
  11.     uint32_t sh_entsize;   // 如果节中包含固定大小条目,此为每条目大小
  12. } Elf32_Shdr;
复制代码
节的类型相关常量以SHT_开头
截图202505220848432143.png
接下来说几个比较重要的段
截图202505220849058050.png
什么是导入表(Import Table)
导入表记录了一个 .so 文件依赖的外部符号(函数或变量),这些符号来自其他共享库,运行时由动态链接器进行解析。
导入表体现在哪些节中?
截图202505220849413881.png
什么是导出表(Export Table)
导出表列出了一个 .so 文件对外暴露的符号,即其他程序或库可以调用的函数和变量。导出符号同样保存在 .dynsym 中。


基础NDK开发(确保已配备开发环境)

截图202505220850331521.png
新建一个NDK项目,之后我们看到native-lib.cpp,native层的代码就在此处编写了。
初步了解静态注册和动态注册

静态注册(Static Registration)

优点:使用固定命名格式,系统自动匹配 Java 方法和 native 实现。      简单直接,不需要手动注册。

缺点:每个函数名非常长,容易出错,不灵活。且逆向成本较低

动态注册(Dynamic Registration)

优点:使用任意函数名,通过 JNI_OnLoad 中显式注册       结构清晰,支持混淆类名/方法名

缺点: 如果注册失败不会编译报错,只会在运行时崩溃或方法调用失败


现在,让我们来注册两个native函数(一个实现静态注册,另一个实现动态注册)

新建一个类math,接下来在这个类里这么写
  1. package com.lingxing.myapplication;
  2. public class math {
  3.     public static native int add(int a , int b);
  4.     public static native int sub(int a , int b);
  5. }
复制代码
之后,我们开始在native注册。
  1. #include <jni.h>           // JNI 头文件
  2. #include <string>
  3. extern "C" JNIEXPORT jstring JNICALL
  4. Java_com_lingxing_myapplication_MainActivity_stringFromJNI(
  5.         JNIEnv* env,           // JNI 接口指针,用于调用 JNI 提供的各种函数
  6.         jobject /* this */) {  // Java 中调用该方法的对象引用,这里未使用,可省略参数名
  7.     std::string hello = "Hello from C++";
  8.     return env->NewStringUTF(hello.c_str());
  9. }
  10. extern "C" JNIEXPORT jint JNICALL
  11. Java_com_lingxing_myapplication_math_add(
  12.         JNIEnv *env,         // JNI 接口指针
  13.         jclass clazz,        // 调用该静态 native 方法的 Java 类引用
  14.         jint a,              // 从 Java 传入的第一个 int 参数
  15.         jint b) {            // 从 Java 传入的第二个 int 参数
  16.     return a + b;
  17. }
  18. jint sub(JNIEnv* env, jclass clazz, jint a, jint b) {
  19.     return a - b;      
  20. }
  21. static JNINativeMethod methods[] = {
  22.     // {Java 层方法名, 方法签名, 对应的 C 函数指针}
  23.     // 方法签名 (II)I 表示:接收两个 int (I, I),返回一个 int (I)
  24.     {"sub", "(II)I", (void*)sub}
  25. };
  26. // JNI 默认加载函数,当 Java 层使用 System.loadLibrary() 加载本地库时会调用此函数
  27. JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
  28.     JNIEnv* env = nullptr;
  29.     // 获取 JNI 环境,指定最低支持的 JNI 版本为 1.6
  30.     if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
  31.         return JNI_ERR;     // 获取失败则返回错误,库加载失败
  32.     }
  33.     // 查找要注册 native 方法的 Java 类,使用全路径,包名与类名之间用 '/' 分隔
  34.     jclass clazz = env->FindClass("com/lingxing/myapplication/math");
  35.     if (clazz == nullptr) {
  36.         return JNI_ERR;     // 找不到类,返回错误
  37.     }
  38.     // 动态注册 methods 数组中定义的 native 方法
  39.     // 第三个参数是 methods 数组长度
  40.     if (env->RegisterNatives(
  41.             clazz,
  42.             methods,
  43.             sizeof(methods) / sizeof(methods[0])
  44.         ) < 0) {
  45.         return JNI_ERR;     // 注册失败,返回错误
  46.     }
  47.     // 返回支持的 JNI 版本号
  48.     return JNI_VERSION_1_6;
  49. }
复制代码
接下来我们就完成了静态注册和动态注册,就可以用IDA看导出表了
截图202505220851398282.png
直接搜java就能看到静态注册的方法了(动态注册下次再讲,三种对抗方法),点进去f5之后按y把一个参数改为
  1. JNIEnv *env
复制代码
截图202505220852112279.png

这样来说对于jni函数伪c效果更好,但是这里逻辑太简单,所以无所谓改不改。


截图202505220853581879.png
导出表搜索JNI_OnLoad
  1. if ( (*(int (__fastcall **)(_QWORD, __int64, _UNKNOWN **, __int64))(*(_QWORD *)v5[0] + 1720LL))(
  2.          v5[0],
  3.          v4,
  4.          &off_40108,
  5.          1) < 0 )
复制代码
那么这其中的off_40108就是它的方法了,我们点进去看可以看到
截图202505220855099203.png

可以看到
  1. Z3subP7_JNIEnvP7_jclassii ; sub(_JNIEnv *,_jclass *,int,int)
复制代码
截图202505220856257112.png
双击点进去就是减法的逻辑了

这个人还没有签名
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

图文推荐
大神求
大神求
6 天前
热门排行