「リンカに基づくSOパッキング技術の基本実装」のパート2
リンカによって維持されているこの soinfo を取得します
しかし、現在の so の soinfo ポインタのベース アドレスを取得するにはどうすればよいかという疑問が再び生じます。ネットの情報を見ると、dlopenでselfが開けると書いてありましたが、Android 7以前の方法とAndroid 8.1では対応していないので(555は詐欺ではありません)、Androidのソースコードを読んでみると、 soinfo を取得する方法です。パンチを組み合わせたものです。最初に自分自身を dlopen してから、soinfo_from_handle 関数を使用してハンドルを soinfo に変換できます。シンボルを確認するために ida を開いたところ、そのような関数がないことがわかりました。これはエクスポート関数 (sblinker 5555) ではありません。これは詐欺です。そのため、ida に従ってコードを少しずつ翻訳し、それを呼び出すための少し短い関数を見つけることしかできません。do_dlclose 関数と、真ん中はsoinfo_from_handleです。戻り値がsoinfo_unloadのパラメータであることがわかり、私は唖然としました、f5の後、これにはパラメータがありません(f5に対して)、アセンブリを観察することしかできません、幸いなことに、それは長くありません、それはx12+0x18 のアドレス値を切り取る 一見 v7[3] っぽいので、そうだ、soinfo に独自のハンドルを書くことができます
void* dlopen(const char* filename, int flag);
static soinfo* soinfo_from_handle(void* handle)
これは次の関数です。いくつかの処理が困難です。たとえば、多数のグローバル変数があるため、マップからリンカーのベース アドレスをスキャンし、残りをコピーする必要があります。
_QWORD * getsoinfo(unsigned __int64 a1,void* base){
unsigned int v2; // w19
unsigned __int64 v3; // x11
__int64 v4; // x9
__int64 v5; // x10
_QWORD *v6; // x12
uint64 *bas1e= reinterpret_cast<uint64 *>((char *) base + 0xFD468);
uint64 *bas2= reinterpret_cast<uint64 *>((char *) base + 0xFD460);
_QWORD qword_FD468=*bas1e;
_QWORD _dl_g_soinfo_handles_map=*bas2;
unsigned __int64 v7; // x13
__int64 v8; // x20
__int64 v9; // x0
__int64 v11; // [xsp+0h] [xbp-20h] BYREF
char v12[8]; // [xsp+8h] [xbp-18h] BYREF
if ( (a1 & 1) != 0 )
{
if ( qword_FD468 )
{
v3 = a1 - a1 / qword_FD468 * qword_FD468;
v4 = qword_FD468 - 1;
v5 = (qword_FD468 - 1) & qword_FD468;
if ( qword_FD468 > a1 )
v3 = a1;
if ( !v5 )
v3 = v4 & a1;
v6 = *(_QWORD **)(_dl_g_soinfo_handles_map + 8 * v3);
if ( v6 )
{
while ( 1 )
{
v6 = (_QWORD *)*v6;
if ( !v6 )
break;
v7 = v6[1];
if ( v7 == a1 )
{
if ( v6[2] == a1 )
{
if ( v6[3] )
break;
}
}
else
{
if ( v5 )
{
if ( v7 >= qword_FD468 )
v7 -= v7 / qword_FD468 * qword_FD468;
}
else
{
v7 &= v4;
}
if ( v7 != v3 )
break;
}
}
}
}
}
_QWORD * st= reinterpret_cast<uint64 *>((char *) (v6[3]) );
return st;
}
void* ax=dlopen("libnative-lib.so",RTLD_NOW);
__android_log_print(6,"r0ysue","%s",strerror(errno));
char line[1024];
int *startr;
int *end;
int n=1;
FILE *fp=fopen("/proc/self/maps","r");
while (fgets(line, sizeof(line), fp)) {
if (strstr(line, "linker64") ) {
__android_log_print(6,"r0ysue","%s", line);
if(n==1){
startr = reinterpret_cast<int *>(strtoul(strtok(line, "-"), NULL, 16));
end = reinterpret_cast<int *>(strtoul(strtok(NULL, " "), NULL, 16));
}
else{
strtok(line, "-");
end = reinterpret_cast<int *>(strtoul(strtok(NULL, " "), NULL, 16));
}
n++;
}
}
void** old_soinfo= reinterpret_cast<void **>(getsoinfo((unsigned __int64) ax, startr));
リンクと soinfo の修正
ここでは、soinfo を構造体の -> を直接使用するように修正しますが、soinfo クラスはまだ実装していないので、この記事はここで終わります。。。。。。。それは不可能です。ルーシ先生は私たちに決して諦めないことを教えてくれました。条件がない場合は、条件を作成してこの問題を解決する必要があります。soinfo が実現できないので、愚かな方法で実現します。のオフセットです。 c で、soinfo 内の変数を 1 つずつ数えます。変数が 555 個ありすぎてサイズが面倒なので、ida を使用してそのオフセットを表示できるようにしたいと考えました。まず、Load を直接確認します。 LoadTask オブジェクトの関数の場合、これは実際にはここにあり、1 対 1 の
対応が必要なだけです。つまり、
si_->base = *(si+16)
si_->size = *(si+24)
si_->load_bias =* (si+256)
si_->phnum = *(si+8)
si_->phdr = *(si)
次に、修正されたコードは次のとおりです
memcpy(&secstr,(char*)(start)+bb.sh_offset,bb.sh_size);
mprotect((void*)PAGE_START((ElfW(Addr))((char *)start)),a.load_size_,PROT_WRITE|PROT_READ|PROT_EXEC);//申请读写执行权限因为我们要执行插件so的代码所以要执行权限
__android_log_print(6,"r0ysue","size %s",strerror(errno));
*reinterpret_cast<uint64 *>((char *) old_soinfo + 16) = reinterpret_cast<uint64>(a.load_start_);
*(int*)((char*)(old_soinfo)+24)= a.load_size_;
*reinterpret_cast<uint64 *>((char *) old_soinfo + 256) = reinterpret_cast<uint64>(start);
*(int*)((char*)(old_soinfo)+8) = a.phdr_num_;
*reinterpret_cast<uint64 *>((char *) old_soinfo )= (uint64) a.loaded_phdr_;
次のステップはリンクプロセスです。関数の絶対アドレスを入力し、参照されている他の関数のアドレスも入力する必要があります。ここで Android ソースコードによって実装されている関数は prelink_image です。これは非常に長いので、注意深く読むことができ、実際にコピーすることができます。ここでは主にインポート テーブル、エクスポート テーブル、リダイレクト テーブル、シンボル テーブル、文字列テーブル、再配置テーブル、例外処理を変更しますが、実際には、Android に従ってすべてコピーできます。ソース コードと ida、ここでは、elf ヘッダーからプログラム ヘッダーを取得し、プログラム ヘッダーで動的セグメントを探します。これらのテーブルはすべて動的セグメント内にあるためです。開始アドレスについては、mmap を使用して、load_bias_ をマップするだけです。上記の負荷から得られます。
Elf64_Ehdr aa;
void* start= mmap(reinterpret_cast<void *>(a.load_bias_), sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
memcpy(&aa,start,sizeof(Elf64_Ehdr));//elf头解析,其实直接用a里面的也行我这里忘了
int secoff= aa.e_shoff;
int secsnum=aa.e_shnum;
Elf64_Shdr bb;
Elf64_Phdr cc;
memcpy (&cc,((char*)(start)+aa.e_phoff),sizeof(Elf64_Phdr));//将程序头表存入cc里面
for(int y=0;y<aa.e_phnum;y++){
//做遍历
memcpy(&cc, (char *) (start) +aa.e_phoff+sizeof(Elf64_Phdr) * y, sizeof(Elf64_Phdr));
if(cc.p_type==2){
//当p_type为0x2是就代表是Dynamic段
}
次に、長い修正プロセスが開始され、ソース コードを ida にコピーして、上記の段落が正常に修復される必要があります。主なことは、相対アドレスを絶対アドレスに変換することです。コンテンツ部分は Elf64_Dyn 構造体を使用して解析します。つまり、d_tag が 0x6fffff5 に等しい場合のエクスポート テーブル (したがってアートにエクスポートする必要があります)、 5 に等しい場合の文字列 テーブル、6 に等しい場合のシンボル テーブルなどを修正する必要がありますが、最終的には、いくつかのセグメント タイプだけを修正に使用しました。
if(dd.d_tag==0x6ffffef5 ){
//对导出表进行修正这个很重要导出失败则无法运行
size_t gnu_nbucket_ = reinterpret_cast<uint32_t*>((char*)start + dd.d_un.d_ptr)[0];
// skip symndx
uint32_t gnu_maskwords_ = reinterpret_cast<uint32_t*>((char*)start + dd.d_un.d_ptr)[2];
uint32_t gnu_shift2_ = reinterpret_cast<uint32_t*>((char*)start + dd.d_un.d_ptr)[3];
ElfW(Addr)* gnu_bloom_filter_ = reinterpret_cast<ElfW(Addr)*>((char*)start + dd.d_un.d_ptr + 16);
uint32_t* gnu_bucket_ = reinterpret_cast<uint32_t*>(gnu_bloom_filter_ + gnu_maskwords_);
// amend chain for symndx = header[1]
uint32_t* gnu_chain_ = reinterpret_cast<uint32_t *>( gnu_bucket_ +
gnu_nbucket_-reinterpret_cast<uint32_t *>(
(char *) start +
dd.d_un.d_ptr)[1]);
--gnu_maskwords_;
uint32_t flags_ = FLAG_GNU_HASH|flags_;
*reinterpret_cast<size_t *>((char *) old_soinfo + 344) = gnu_nbucket_;
*reinterpret_cast<uint32_t *>((char *) old_soinfo + 368) = gnu_maskwords_;
*reinterpret_cast<uint32_t *>((char *) old_soinfo + 372) = gnu_shift2_;
*reinterpret_cast< ElfW(Addr)* *>((char *) old_soinfo + 376) = gnu_bloom_filter_;
*reinterpret_cast<uint32_t **>((char *) old_soinfo + 352) = gnu_bucket_;
*reinterpret_cast<uint32_t **>((char *) old_soinfo + 360) = gnu_chain_;
*reinterpret_cast<uint32_t *>((char *) old_soinfo + 48) = *reinterpret_cast<uint32_t *>((char *) old_soinfo + 48) |FLAG_GNU_HASH;
}
if(dd.d_tag==2 ){
*reinterpret_cast<uint64 *>((char *) old_soinfo + 48)=dd.d_un.d_val / sizeof(ElfW(Rela));
}
if(dd.d_tag==0x17 ){
//导入表修正
*reinterpret_cast<uint64 *>((char *) old_soinfo + 104)= reinterpret_cast<uint64>(
(char *) start + dd.d_un.d_ptr);
}
if(dd.d_tag==7){
//重定位修正
*reinterpret_cast<uint64 *>((char *) old_soinfo + 120)= reinterpret_cast<uint64>(
(char *) start + dd.d_un.d_ptr);
}
if(dd.d_tag==5){
//对字符串表进行修正
*reinterpret_cast<char **>((char *) old_soinfo + 56) = reinterpret_cast< char*>((char *) start+dd.d_un.d_ptr);
}
if(dd.d_tag==6){
//对符号表进行修正
*reinterpret_cast<uint64 *>((char *) old_soinfo + 64) = reinterpret_cast<uint64>(
(char *) start + dd.d_un.d_ptr);
}
if(dd.d_tag==10){
*reinterpret_cast<uint64 *>((char *) old_soinfo + 336) = reinterpret_cast<uint64>(
(char *) start + dd.d_un.d_ptr);
}
if(dd.d_tag==8){
*reinterpret_cast<uint64 *>((char *) old_soinfo + 336) = dd.d_un.d_val / sizeof(ElfW(Rela));
}
if(dd.d_tag==0x6ffffff0){
*reinterpret_cast<uint64 *>((char *) old_soinfo + 440) = reinterpret_cast<uint64 >((char*)start + dd.d_un.d_ptr);
}
if(dd.d_tag==0x6fffffff){
*reinterpret_cast<uint64 *>((char *) old_soinfo + 472) = dd.d_un.d_val;
}
if(dd.d_tag==0x6ffffffe){
*reinterpret_cast<uint64 *>((char *) old_soinfo + 464) = reinterpret_cast<uint64>(
(char *) start + dd.d_un.d_ptr);
}
if(dd.d_tag==1){
mynedd[needed]=dd.d_un.d_val;
needed++;
}
このように、強化された so が外部関数を参照していない場合は、エクスポート テーブルを修復したため、正常に使用できます (so は外部関数を持たない可能性があります)。ただし、完全性を追求するには、補う必要があります。たとえば、パック内で printf または __android_log_print が引用されている場合、エラーが報告されます。
依存関数のアドレスを修正
上記では便宜上、neededso のロードとリンクを実装しなかったので、以下では依存する so のロードに dlopen と dlsym を使用します。ここでは、Android ソース コードの link_image 関数を確認できます。この関数は、relocate を呼び出して JMPREL 再配置テーブルを修復します。したがって、フォローアップして見てみましょう。実際、ここでは非常に明確です。反復メソッドを使用して、参照されるアドレスを取得します。に記入し、種類に応じて返送してください。
bool soinfo::relocate(const VersionTracker& version_tracker, ElfRelIteratorT&& rel_iterator,
const soinfo_list_t& global_group, const soinfo_list_t& local_group) {
....
ElfW(Word) type = ELFW(R_TYPE)(rel->r_info);
ElfW(Word) sym = ELFW(R_SYM)(rel->r_info);
....
if (!soinfo_do_lookup(this, sym_name, vi, &lsi, global_group, local_group, &s)) {
return false;
}
....
switch (type) {
...
}
}
soinfo を実装していないので、別の方法を見つけて、dlopen と dlsym を使用して原理に基づいた別のソリューションを記述することしかできません。まず上記のシンボルテーブルと文字列テーブルを使用し、次にソースコードに従ってトラバーサルクラスを実装します(実装しない場合はループを使用できますが、ctrl+cvだけで大丈夫なので、使用せずに実行してみてはいかがでしょうか)そして上記のインポートライブラリテーブルを使うには、Androidのソースコードがどのように混乱しているかはもちろん分かりませんが、R_SYMとR_TYPEの2種類の定義は存在せず、自分でインポートするしかありません。実際、これら 2 つは情報を解析するのが非常に簡単です。
class plain_reloc_iterator {
public:
plain_reloc_iterator(rel_t* rel_array, size_t count)
: begin_(rel_array), end_(begin_ + count), current_(begin_) {
}
bool has_next() {
return current_ < end_;
}
rel_t* next() {
return current_++;
}
public:
rel_t* const begin_;
rel_t* const end_;
rel_t* current_;
};
#define ELFW(what) ELF64_ ## what
#define R_TYPE(sym) ((((Elf64_Xword)sym) << 32)
#define R_SYM(type) ((type) & 0xffffffff))
char* strtab_= *reinterpret_cast<char **>((char *) old_soinfo + 56) ;//字符串表基址
Elf64_Sym* symtab_= *reinterpret_cast<Elf64_Sym **>((char *) old_soinfo + 64);//符号表基址
plain_reloc_iterator myit(
reinterpret_cast<rel_t *>(*reinterpret_cast<uint64 *>(
(char *) old_soinfo + 104)), *reinterpret_cast<size_t *>((char *) old_soinfo + 48));
__android_log_print(6,"r0ysue","finish xxx%x",*reinterpret_cast<size_t *>((char *) old_soinfo + 48));
最後にループバックフィルを書くだけです
for (size_t idx = 0; myit.has_next(); ++idx) {
const auto rel = myit.next();
ElfW(Word) type = ELFW(R_TYPE)(rel->r_info);
ElfW(Word) sym = ELFW(R_SYM)(rel->r_info);
ElfW(Addr) sym_addr = 0;
const char *sym_name = nullptr;
const Elf64_Sym *s = nullptr;
if (type == 0) {
//不处理类型为0的部分
continue;
}
sym_name = reinterpret_cast<const char *>(strtab_+symtab_[sym].st_name);//根据get_string函数改编
for(int s=0;s<needed;s++) {
//遍历所有的导入库表用dlopen和dlsym查找是否有我们需要的符号
void* handle=dlopen(strtab_ + mynedd[s],RTLD_NOW);
sym_addr= reinterpret_cast<Elf64_Addr>(dlsym(handle, sym_name));
if(sym_addr==0)
continue;
else
// __android_log_print(6, "r0ysue", "finish xxwwwwwwwwwwwwwwwx%p %s", sym_addr,sym_name);
break;
}
switch (type) {
case 1026://我只有0x402类型的部分所以就简化处理了
*reinterpret_cast<uint64 *>((char *) start+ rel->r_offset) = (sym_addr );
break;
}
}
ここまでが実際に完成したので結果を見てみましょう
//插件so当中的代码
extern "C"
JNIEXPORT jint JNICALL
Java_com_roysue_elfso_MainActivity_add(JNIEnv *env, jobject thiz, jint a, jint b) {
printf("cxzcxzcxz");
__android_log_print(6,"r0ysue","i am from 1.so %p",a);
return a+b;
}
最後のログでは、アートとの対話が完了し、init_arry 関数と Jni_Onload の実行も非常に簡単なので実装しません。
要約する
この記事は、初心者が so シェルを始めるための基礎的なもので、かなり落とし穴を踏んだと思われる簡単な so シェルをざっくり実装しましたが、エクスポート テーブルの修復に長い時間がかかり、ようやく成功しました。見てくれてありがとう
添付ファイルのパッキングのデモ
リンク: https://pan.baidu.com/s/1MZSjotH8cs7wrOIAiZM5NQ
抽出コード: kjvm
g_print(6,“r0ysue”,“私は 1 出身です。つまり %p”,a);
a+b を返します。
}
最后日志,这样就完成和art的交互,后面还有执行init_arry函数和Jni_Onload也是十分的简单我就不实现了
[外链图片转存中...(img-b6Y4RNqO-1632479487156)]
### 总结
本篇文章只是一个基础用于对新手的so加壳入门,我粗略的实现了一个简单的so壳,算是我踩到的许多坑,其中导出表的修复就花费了好久的时间最终才成功,感谢大家观看
附件加壳demo
链接:https://pan.baidu.com/s/1MZSjotH8cs7wrOIAiZM5NQ
提取码:kjvm
![](https://img-blog.csdnimg.cn/img_convert/566fd786064cfa723e4f2488dbfea74b.png)