这篇文章上次修改于 575 天前,可能其部分内容已经发生变化,如有疑问可询问作者。
前言
无论是还原so的某个算法还是要分析so的逻辑,相信大家都会遇到这些场景:
- 处处都在取偏移,大概率来说,这些都是结构体
- 再跳转几个函数可能会出现取偏移处的指针,再取偏移,这很可能是结构体嵌套
- 这里变量取值偏移是8,其他地方有个函数中间变量偏移取值也是8,它们是同一个类型的吗
这就是so中无处不在的结构体,为了能更好地理解反汇编出来的函数,将结构体还原为可读性更高的形式是非常有必要的
可读性提高之后,就可以明确知道:「原来这里是在向xx的yy属性赋值」,而不是:「这里向xx偏移yy处赋值」
更重要的是,引入结构体之后,有了类型可以更容易理解其中的转换逻辑
针对Android平台,本文将以某so为例,结合该so使用到的某个开源三方库源码,使用一种「通用的方案」对结构体进行还原
既然是通用的方案自然也能套用在无源码的情况下,这一点将在文末说明
环境
- IDA 7.5 Pro
- 64位的sample.so
libevent源码
后文提到的源码均指so所使用的第三方库libevent的源码
你可以选择其他so参考本文实操练习
教程
Q: 有源码的情况下直接导入源码头文件不就行了?
A: 源码简单的情况下没问题,但对于源码复杂的情况,通常存在这些问题:
- 结构体往往多层嵌套
- 结构体定义不一定在头文件
- 跨平台库支持存在诸多宏定义,IDA并不能处理好
- 不能确定so使用的版本
- 不能确定是否进行了**自研优化**改动,如果有那么直接导入结构体可能理解成相反的含义
套路小结
首先要知道的是:IDA中,32位的指针类型是int*
,64位的指针类型是__int64*
个人感觉__int64
看着不太顺眼,后面将使用int64_t
来代替,IDA中没法直接找到这个类型,但是如果使用了这个写法IDA会自动关联
- 对于常见的数据类型,源码中是什么类型,IDA导入结构体定义时就是什么类型
对于结构体中的
struct xxx yyy;
如果yyy是指针(只要带有*,无论数量),比如是
*one_struct
,对于64位IDA导入结构体定义时写成int64_t *one_struct
- 关于这一点,其实写成
struct ABC *one_struct
也可以,IDA可以正常导入,这一点在后续说明,前面还请按照文章来
- 关于这一点,其实写成
- 否则一律写成
_BYTE one_struct[200]
,200
这个数请根据实际情况写一个比较大的数即可
还原步骤:
- 结合so前后信息和源码确定某个参数是源码中的某个类型的结构体
在知道
偏移i
处是结构体Y
的MM属性
,MM属性
的类型是*AAA
,那么在IDA中定义一个这样的结构体struct y { _BYTE padding1[i]; int64_t *MM; _BYTE padding[200] }
假定在后续分析过程中,发现
偏移j
处(偏移j
在偏移i
之后)是结构体y
的NN属性
,NN属性
的类型是*BBB
,那么将上面的结构体修改如下struct Y { _BYTE padding1[i]; int64_t *MM; _BYTE padding2[j - i]; int64_t *NN; _BYTE padding[200]; }
假定
NN属性
的前面是一个int类型
的QQ属性
,那么我们可以将结构体继续修正如下struct Y { _BYTE padding1[i]; int64_t *MM; _BYTE padding2[j - i - sizeof(int)]; int QQ; int64_t *NN; _BYTE padding[200]; }
- 不断重复上面的过程,直到得到全部或者部分的结构体定义
对于
结构体AAA
和结构体BBB
也采用相同的方法去定义,在自己认为还原得合适的时候再将结构体Y
中对应偏移位置的类型替换即可- 即将
结构体Y
中的int64_t *MM;
修改为struct AAA *MM;
- 即将
上面的说法看起来可能有些不好理解,请看下面实战演示,可以帮助你理解这个过程
实战演示
先用IDA反汇编sample.so
定位到sub_121E14
这个函数,并且打开libevent
源码
通过图中这两个字符串可以定位出来是evdns_nameserver_add_impl_
这个函数
Addrlen %d too long.
Couldn't bind to outgoing address
结合evdns_nameserver_add_impl_
源码,先对参数和部分函数进行重命名
结合源码可以知道这个函数主要涉及两个结构体,evdns_base
和nameserver
evdns_base
结构体定义
nameserver
结构体定义
相比之下nameserver
更简单一点,先尝试对nameserver
结构体进行还原
通过对比源码,可以找到一部分nameserver
属性所对应的偏移
nameserver
结构体定义如下:
struct nameserver {
evutil_socket_t socket;
struct tcp_connection *connection;
struct sockaddr_storage address;
ev_socklen_t addrlen;
int failed_times;
int timedout;
struct event event;
struct nameserver *next, *prev;
struct event timeout_event;
struct evdns_request *probe_request;
char state;
char choked;
char write_waiting;
struct evdns_base *base;
int requests_inflight;
};
检索源码,可以知道evutil_socket_t
类型其实是int
#ifdef _WIN32
#define evutil_socket_t intptr_t
#else
#define evutil_socket_t int
#endif
ev_socklen_t
的类型是socklen_t
#ifdef _WIN32
#define ev_socklen_t int
#elif defined(EVENT__socklen_t)
#define ev_socklen_t EVENT__socklen_t
#else
#define ev_socklen_t socklen_t
#endif
而根据AOSP的源码,可以知道__socklen_t
在64位
上实际类型是uint32_t
但是IDA的类型是int32_t
,我们最好修改为uint32_t
,正确的类型是非常有必要的
因为对于复杂的结构体有时候会影响到偏移计算
#if !defined(__LP64__)
/* This historical accident means that we had a signed socklen_t on 32-bit architectures. */
typedef int32_t __socklen_t;
#else
/* LP64 still has a 32-bit socklen_t. */
typedef uint32_t __socklen_t;
#endif
typedef __socklen_t socklen_t;
按照前面的套路,先写出下面的结构体,和原结构体相比变化如下:
evutil_socket_t
替换为int
- 因为我们确定其类型就是
int
- 因为我们确定其类型就是
struct tcp_connection *connection
替换为int64_t *connection
- 因为我们还不知道
tcp_connection
结构体啥样,暂时指明这是一个指针即可
- 因为我们还不知道
struct sockaddr_storage address
替换为_BYTE address[200]
- 因为这是一个嵌套结构体,我们还不知道在so中实际上多大,所以先定一个200字节大小
ev_socklen_t
替换为socklen_t
- 因为前面在IDA中修改了
__socklen_t
类型为uint32_t
,这里使用更符合场景的socklen_t
进行替换
- 因为前面在IDA中修改了
struct nameserver {
int socket;
int64_t *connection;
_BYTE address[200];
socklen_t addrlen;
};
打开Local Types
IDA菜单
->View
->Open subviews
->Local Types
右键Insert
,然后粘贴上面的结构体,选择OK
保存
这个时候选中nameserver
右键点击Edit
,可以看到现在编辑框里面IDA给你计算好了结构体各个属性的偏移
现在我们回到反汇编窗口,选择之前改好的伪代码中的_ns
,右键选择Convert to struct *...
然后在弹出的窗口中选择刚才定义的nameserver
结构体,选中后点击OK或者双击即可
前面我们已经对比过源码了,现在找到ns->addrlen
这个地方,查看真实的偏移情况
可以看到在当前设置的结构体下,addrlen
应该在address
偏移128
处,而address
在结构体中的偏移是0x10
,那么addrlen
在结构体中实际偏移是0x10 + 128 => 0x90
查看汇编窗口的代码,可以确定addrlen
在原始结构体中偏移就是0x90
Q: 这里为什么要这样操作一番呢,最开始的反汇编代码不就有这些值吗?
A: 因为要动态修改结构体定义,总不能把最开始的反汇编结果记下来或者截图吧,修改数量太多的时候就必须要有一种可靠的方案去确定相对结构体的正确偏移是多少。所以这里提供了两种方法来做:
- 一种是查看信息计算偏移
- 一种是根据汇编中的内容来确定,不过这个时候你需要确定哪个寄存器是结构体,这样才能找对偏移
不过还有一种更简单的方案,就是把结构体的类型改回去,即直接右键选择Reset pointer type
。方法多多益善,前面就当可选项了~~
这个时候我们去修改nameserver
结构体定义,调整address
的大小,让addrlen
可以刚好处于偏移0x90
的位置
在修改的时候,IDA会实时计算每个属性的起始偏移,结果显示在左侧,同时也会有每个属性的大小
所以我们不需要去手动计算0x90 - 0x10(addrlen的正确偏移 - address的正确偏移)
应该等于多少,只需要稍微做加减,像调整游标卡尺那样慢慢调整就行(不用费脑子)
然后再去反汇编窗口F5
一下就好了
简单小结一下就是:
- 只要知道了某个偏移处是什么属性,且它前面还有不确定的属性,先用最靠前的一个确定属性,类型设置为
_BYTE
,然后通过设置长度占坑,让这个属性处于正确偏移即可 - 如果它前面就是一个已知的属性,那就直接接着写即可,比如对于
failed_times
来说,它前面是addrlen
,现在这个值的偏移确定下来了,那么我们直接接着写定义就行了
evutil_socket_t socket;
struct tcp_connection *connection;
struct sockaddr_storage address;
ev_socklen_t addrlen;
int failed_times;
int timedout;
struct event event;
那么现在再次修改结构体如下,然后刷新反汇编窗口
struct nameserver {
int socket;
int64_t *connection;
_BYTE address[128];
socklen_t addrlen;
int failed_times;
int timedout;
_BYTE event[200];
};
我们继续对比源码和反汇编代码,可以发现432
是ns->base
,424
是ns->state
先看看nameserver
结构体的部分定义,按照最开始的套路做出修改
struct event event;
struct nameserver *next, *prev;
struct event timeout_event;
struct evdns_request *probe_request;
char state;
char choked;
char write_waiting;
struct evdns_base *base;
int requests_inflight;
对于常见类型,比如int
,char
就不用修改了,这里改了这几个点:
struct evdns_request *probe_request;
=>int64_t *probe_request;
struct evdns_base *base;
=>int64_t *base;
其实没什么新鲜的,就是套路中的第二条规则,把还不知道具体结构体定义的改成int64_t *
而event
到*probe_request
之间到底间隔多少先不管,填个数占坑即可
struct nameserver {
int socket;
int64_t *connection;
_BYTE address[128];
socklen_t addrlen;
int failed_times;
int timedout;
_BYTE event[200];
int64_t *probe_request;
char state;
char choked;
char write_waiting;
int64_t *base;
int requests_inflight;
};
已经知道state
的偏移是424 => 0x1A8
,简单调整event
占坑长度后得到下面的结构体定义
struct nameserver {
int socket;
int64_t *connection;
_BYTE address[128];
socklen_t addrlen;
int failed_times;
int timedout;
_BYTE event[254];
int64_t *probe_request;
char state;
char choked;
char write_waiting;
int64_t *base;
int requests_inflight;
};
现在得到的反汇编代码可读性更好了
已知event
的偏移是160 => 0xA0
,不过这个时候我们发现实际上在IDA中定义的结构体的event
偏移是0x9C
那么是不是什么地方错误了呢,我们看向event
前一个属性timedout
,IDA中给出的偏移是0x98 => 152
简单检索源码,可以发现在nameserver_read
中有对timedout
进行赋值,通过字符串特征可以定位到是sub_1273C4
将对应的参数转换为nameserver
结构体
可以看到直接出现了timedout
,说明timedout
的偏移肯定没问题,偏移是152 => 0x98
libevent
源码中nameserver
结构体定义,timedout
和event
之间并没有其他的属性了
这里只能暂时猜测是可能存在一个0xA0 - 0x9C = 4
字节大小的属性
添加_BYTE unknow_field_padding[4];
以及补充剩下的属性,然后调整event
占坑长度后得到下面的结构体定义
确定event
长度的时候,还是和之前一样,找个已知偏移的属性做比较,比如state
的偏移是0x1A8
struct nameserver {
int socket;
int64_t *connection;
_BYTE address[128];
socklen_t addrlen;
int failed_times;
int timedout;
_BYTE unknow_field_padding[4];
_BYTE event[120];
struct nameserver *next;
struct nameserver *prev;
_BYTE timeout_event[120];
int64_t *probe_request;
char state;
char choked;
char write_waiting;
int64_t *base;
int requests_inflight;
};
另外补充一点,在evdns_nameserver_add_impl_
函数中,ns
是会经历初始化的,所以可以看到这个结构体的实际大小,这里是448 => 0x1C0
根据这个还可以验证我们的结构体大小是否正确,如果发现不匹配那也能帮助我们检查出其中的错误
不过对于一些巨复杂的结构体可能没那么容易一下就找到,这只是辅助我们检查的手段之一
现在反汇编代码阅读起来是不是更加清晰了呢
简单小结一下就是:
- 不知道的地方先占坑,根据对比分析逐步确定一些属性的偏移,一步步完善
- 如果有偏移对不上的地方,另外设置一个属性占坑即可
在这个不断修正的过程也能发现一些改动,比如这里这个_BYTE unknow_field_padding[4];
,就是源码中不存在的属性
看到这里,想必你已经知道怎么应对相对复杂的结构体了
不过这里我还想延伸一点,在源码中*base
完整定义是struct evdns_base *base;
,以evdns_base
再做一点分析
这个结构体中有下面这种定义,一是TAILQ_HEAD
这种像是函数运算的,一是根据宏定义决定有没有lock
这个属性
前者可以找到TAILQ_HEAD
的定义搞清楚是怎么回事,后者则按前面的思路进行分析便可确定有没有这个属性了
TAILQ_HEAD(hosts_list, hosts_entry) hostsdb;
#ifndef EVENT__DISABLE_THREAD_SUPPORT
void *lock;
#endif
其中TAILQ_HEAD
定义如下
#ifndef TAILQ_HEAD
#define EVENT_DEFINED_TQHEAD_
#define TAILQ_HEAD(name, type) \
struct name { \
struct type *tqh_first; \
struct type **tqh_last; \
}
#endif
我简化为下面这样,在IDA中添加定义,但是添加之后就找不到了QAQ
#define TAILQ_HEAD(name, type) struct name {struct type *tqh_first;struct type **tqh_last;}
不过结合evdns_base
中的TAILQ_HEAD(hosts_list, hosts_entry) hostsdb;
这个定义,可以知道其实是等效于下面这个定义
struct hosts_list {
struct hosts_entry *tqh_first;
struct hosts_entry **tqh_last;
}
也就是说上面的TAILQ_HEAD
相当于是一个模板,另外这个hosts_list
结构体IDA可以直接添加,因为两个属性都是指针
如果你尝试设置一个非指针类型的,那么就无法添加,因为现在并没有真的存在hosts_entry
需要我们先手动添加定义才能添加这种非指针类型的struct xxx
定义
下面是evdns_base
结构体的完整定义
struct evdns_base {
struct request **req_heads;
struct request *req_waiting_head;
struct nameserver *server_head;
int n_req_heads;
struct event_base *event_base;
int global_good_nameservers;
int global_requests_inflight;
int global_requests_waiting;
int global_max_requests_inflight;
struct timeval global_timeout;
int global_max_reissues;
int global_max_retransmits;
int global_max_nameserver_timeout;
int global_randomize_case;
u16 global_max_udp_size;
struct timeval global_nameserver_probe_initial_timeout;
u16 global_tcp_flags;
struct timeval global_tcp_idle_timeout;
struct sockaddr_storage global_outgoing_address;
ev_socklen_t global_outgoing_addrlen;
struct timeval global_getaddrinfo_allow_skew;
int so_rcvbuf;
int so_sndbuf;
int getaddrinfo_ipv4_timeouts;
int getaddrinfo_ipv6_timeouts;
int getaddrinfo_ipv4_answered;
int getaddrinfo_ipv6_answered;
struct search_state *global_search_state;
TAILQ_HEAD(hosts_list, hosts_entry) hostsdb;
#ifndef EVENT__DISABLE_THREAD_SUPPORT
void *lock;
#endif
int disable_when_inactive;
int ns_max_probe_timeout;
int ns_timeout_backoff_factor;
};
我们做出修改,结果如下:
struct evdns_base {
struct request **req_heads;
struct request *req_waiting_head;
struct nameserver *server_head;
int n_req_heads;
struct event_base *event_base;
int global_good_nameservers;
int global_requests_inflight;
int global_requests_waiting;
int global_max_requests_inflight;
struct timeval global_timeout;
int global_max_reissues;
int global_max_retransmits;
int global_max_nameserver_timeout;
int global_randomize_case;
uint16_t global_max_udp_size;
struct timeval global_nameserver_probe_initial_timeout;
uint16_t global_tcp_flags;
struct timeval global_tcp_idle_timeout;
_BYTE global_outgoing_address[128];
socklen_t global_outgoing_addrlen;
struct timeval global_getaddrinfo_allow_skew;
int so_rcvbuf;
int so_sndbuf;
int getaddrinfo_ipv4_timeouts;
int getaddrinfo_ipv6_timeouts;
int getaddrinfo_ipv4_answered;
int getaddrinfo_ipv6_answered;
struct search_state *global_search_state;
struct hosts_list hostsdb;
void *lock;
int disable_when_inactive;
int ns_max_probe_timeout;
int ns_timeout_backoff_factor;
};
既然我们知道了,对于定义的是指针类型的struct xxx
,没有实际定义xxx
也是可以的,那么这一类的定义就不需要使用改为int64_t *
的方式替换
上面的struct xxx
中:
nameserver
已经定义了event_base
注意这不是结构体本身,结构体是evdns_base
不要看走眼了timeval
IDA已经定义了sockaddr_storage
在nameserver
它的定义是_BYTE address[128];
,沿用即可hosts_list
刚才也已经定义了
运气真不错,没有出现未知的嵌套struct xxx
,其他修改:
u16
替换为uint16_t
ev_socklen_t
替换为socklen_t
lock
先假定存在
于是这次看起来可以直接完整导入IDA,那么会不会这么顺利呢?
回到evdns_nameserver_add_impl_
,也就是sub_121E14
这个函数
根据源码我们知道参数一就是evdns_base
,将参数一转为evdns_base
类型结构体
看起来是不是很顺眼,好像非常完美,让我们往下翻一点,可以看到其中引用的一些属性和字符串的信息不匹配呀!
再看一眼刚才的结构体定义,似乎挑不出什么毛病,那么问题出在哪儿呢?
难道是不应该存在lock
?
不,显然不是!
因为图中指出的这些属性不管是getaddrinfo_ipv6_answered
还是hostsdb
它们都在lock
之前定义
也就是说就算没有lock,也不会对当前反汇编这部分有什么影响
让我们的目光向lock之前的属性定义看,发现好几处都是timeval
结构体
难道IDA还能把timeval
定义搞错?
把IDA的结构体定义和关联的定义提取出来看一下,显然按IDA中的定义,timeval
大小应该是8
typedef int __kernel_long_t;
typedef __kernel_long_t __kernel_time_t;
typedef __kernel_time_t __time_t;
typedef __kernel_long_t __kernel_suseconds_t;
typedef __kernel_suseconds_t __suseconds_t;
struct timeval {
__time_t tv_sec;
__suseconds_t tv_usec;
};
把AOSP中的定义也提取出来看一下,显然timeval
大小应该是0x10
- http://aospxref.com/android-11.0.0_r21/xref/bionic/libc/kernel/uapi/asm-generic/posix_types.h#23
- http://aospxref.com/android-11.0.0_r21/xref/bionic/libc/kernel/uapi/asm-generic/posix_types.h#77
- http://aospxref.com/android-11.0.0_r21/xref/bionic/libc/kernel/uapi/asm-generic/posix_types.h#43
- http://aospxref.com/android-11.0.0_r21/xref/bionic/libc/kernel/uapi/linux/time.h#30
#ifndef __kernel_long_t
typedef long __kernel_long_t;
#endif
typedef __kernel_long_t __kernel_old_time_t;
#ifndef __kernel_suseconds_t
typedef __kernel_long_t __kernel_suseconds_t;
#endif
struct timeval {
__kernel_old_time_t tv_sec;
__kernel_suseconds_t tv_usec;
};
很明显两者的差异在于__kernel_long_t
,那么我们应该手动修改__kernel_long_t
的定义为long
来尝试修正目前遇到的问题
修改后刷新下反汇编代码,这次的结果显然说明确实是应该这样修正
现在还有两个问题,一是hostsdb
那样设置的类型是正确的吗,一是lock
存在还是不存在
对于第二个问题,我们对evdns_nameserver_add_impl_
交叉引用,点第一个引用的位置,反汇编结果如下
图中已经是转换过类型的了,可以看到出现了lock
根据图中的字符串结合源码可以定位到是evdns_base_nameserver_ip_add
这个函数
可以看到源码中evdns_nameserver_add_impl_
附近的代码是这样的
EVDNS_LOCK(base);
res = evdns_nameserver_add_impl_(base, sa, len);
EVDNS_UNLOCK(base);
EVDNS_LOCK
和EVDNS_UNLOCK
的定义如下,和反汇编得到的结果一致,那么说明lock
这个属性确实存在
#ifdef EVENT__DISABLE_THREAD_SUPPORT
#define EVDNS_LOCK(base) EVUTIL_NIL_STMT_
#define EVDNS_UNLOCK(base) EVUTIL_NIL_STMT_
#define ASSERT_LOCKED(base) EVUTIL_NIL_STMT_
#else
#define EVDNS_LOCK(base) \
EVLOCK_LOCK((base)->lock, 0)
#define EVDNS_UNLOCK(base) \
EVLOCK_UNLOCK((base)->lock, 0)
#define ASSERT_LOCKED(base) \
EVLOCK_ASSERT_LOCKED((base)->lock)
#endif
在源码中检索hostsdb
,发现在evdns_base_new
中有使用到,根据字符串定位到是sub_122124
修改对应的变量类型,可以看到基本和源码的相差无几
源码中可以找到TAILQ_INIT
定义如下,IDA的结果确实一致,说明hostsdb
部分也是没问题的
#define TAILQ_INIT(head) do { \
(head)->tqh_first = NULL; \
(head)->tqh_last = &(head)->tqh_first; \
} while (0)
另外可以发现这里在初始化evdns_base
结构体,大小是360 => 0x168
再看一眼我们在IDA中定义的结构体大小,也是0x168,至此可以确定evdns_base
结构体定义没有问题
还记得前面提到的nameserver_read
吗,在这个函数里面还存在一些看起来没完成的偏移确定
现在我们将原先nameserver
结构体定义设置的int64_t *base;
修改为struct evdns_base *base;
再刷新下nameserver_read
的反汇编代码,可以看到和源码中也是能对应正确的
简单小结一下就是:
- 结构体定义有宏的,找到宏定义代码,带入替换
- 对于不确定是否存在的属性,可以先假定再根据已有信息验证
- 定义完善但是偏移对不上,要检查IDA内置定义是否正确
- 如果结构体内
struct xxx
定义的是指针,那么IDA也能导入识别,不需要改为int64_t *
进行替换
总结
总之呢,要还原结构体无非是做以下几个工作:
- 如果结构体嵌套,改成
_BYTE
类型数组占坑,然后根据其他确定的属性进行修正 - 如果
struct xxx
定义的是指针,不需要改为int64_t *
进行替换,即无需修改 - 如果定义完善但是偏移对不上,差异较大,要检查IDA有关内置定义是否正确
- 综合分析,确定偏移处的属性,不断调整那些占坑的结构体,即那些
_BYTE
数组
Q: 对于那些开发者自己编译的,无源码的部分,怎么分析呢?
A: 如果so有用到开源库,我们首先在开源库基础上进行做一定程度的还原,再查看交叉引用,对部分结构体的部分属性进行还原;而对于完全没有源码的部分,套路还是类似的,只不过从原来和源码对比确定某处偏移是某某属性,变成了结合so前后信息、hook结果等操作推断某处偏移应当是某某属性。
我们在实际分析中往往不需要特别完整的结构体信息,只需要知道是谁在嵌套谁,哪个地方的哪个偏移应当是什么属性就行了
对于so中信息很少的情况,即使不能知道某处偏移的属性应该叫什么,但只要取个名字,在其他函数中能够分辨出来即可
能分辨出来了,也就不会出现「这里变量取值偏移是8,其他地方有个函数中间变量偏移取值也是8,它们是同一个类型的吗」这样的疑惑了
已有 2 条评论
学到了,感谢分享!
解决了我对于复杂结构还原的问题,感谢无私分享。