Skip to content

标题: 将PIE可执行程序转换成动态链接库

创建: 2020-12-24 15:48 更新: 2020-12-25 14:53 链接: https://scz.617.cn/unix/202012241548.txt

在《IDA flare-emu示例》中提过一句将普通可执行ELF转成.so文件,bluerust就此 向我推荐开源项目:

https://github.com/lief-project/LIEF

其原话是,"高级、好用"。bluerust本职工作是说单口相声的,平时嘴贫得不行,还 都是用英语砸我,难得用一次汉语,居然才四个字。这我一定得试试。

考虑这样一种场景,某ELF中有未导出函数对in实现某种数学运算产生out,暂时搞不 清细节,想快速利用之。简单情形可以直接读到内存里当成shellcode那样的可移动 代码用,复杂情形只好具体问题具体分析。

我最早干的是用dlopen()加载传统可执行程序,本文介绍的是将PIE转成动态链接库, 二者有所区别。不过来都来了,就过一遍吧。

本文是LIEF官方文档节选某一小段后的中译版,没有原创内容。

Transforming an ELF executable into a library https://lief.quarkslab.com/doc/latest/tutorials/08_elf_bin2lib.html

本文在x64/Ubuntu 16.04.6 LTS中测试。

安装LIEF:

python3 -m pip install --upgrade pip pip3 install setuptools --upgrade pip3 install lief

简单测试LIEF:


import lief

binary = lief.parse( "/bin/ls" ) print( binary )


一切正常的话,会看到很大一片输出,都是ELF相关信息。

$ readelf -h /bin/true | grep Type Type: EXEC (Executable file) // 传统可执行程序

$ readelf -h /usr/bin/ssh | grep Type Type: DYN (Shared object file) // PIE

$ readelf -h /lib/x86_64-linux-gnu/libc.so.6 | grep Type Type: DYN (Shared object file) // 动态链接库

true、ssh都是可执行ELF,但二者的Type不同;true是传统可执行程序,ssh是所谓 的位置无关可执行程序(PIE)。PIE与.so一样,其加载基址是浮动的。用gdb调试PIE 时,若想断在"Entry point",参看《GDB启动被调试进程时如何尽早断下》,需要一 些奇技淫巧。

$ vi lief_sample_0.py


-- encoding: cp936 --

python3 lief_sample_0.py /usr/bin/ssh

python3 lief_sample_0.py /lib/x86_64-linux-gnu/libc.so.6

import sys import lief

filename = sys.argv[1] binary = lief.parse( filename ) print( binary.header.file_type ) n = len( binary.exported_functions ) print( n ) if ( n > 0 ) : print( binary.exported_functions[0] )


$ python3 lief_sample_0.py /usr/bin/ssh E_TYPE.DYNAMIC 9 mkstemp - 0x67fe0

$ python3 lief_sample_0.py /lib/x86_64-linux-gnu/libc.so.6 E_TYPE.DYNAMIC 2062 putwchar - 0x710f0

PIE与.so的主要区别在于导出符号,ssh只有很少的导出符号,libc.so有很多导出符 号。

$ vi lief_sample_1.c


if 0

x64/Ubuntu 16.04.6 LTS + gcc 5.4.0

gcc -Wall -pipe -O3 -s -o lief_sample_1_a lief_sample_1.c gcc -Wall -pipe -O3 -fvisibility=hidden -fPIE -pie -Wl,-strip-all,--hash-style=both -o lief_sample_1_b lief_sample_1.c

endif

include

include

include

static attribute((noinline)) int check ( char *sth ) { if ( strcmp( sth, "magic" ) == 0 ) { return( 1 ); } return( 0 ); }

int main ( int argc, char * argv[] ) { if ( argc != 2 ) { printf( "Usage: %s \n", argv[0] ); return( -1 ); } if ( check( argv[1] ) ) { printf( "Ok\n" ); } else { printf( "Try again\n" ); } return( 0 ); }


本例刻意对比不同编译方式:

$ gcc -Wall -pipe -O3 -s -o lief_sample_1_a lief_sample_1.c $ gcc -Wall -pipe -O3 -fvisibility=hidden -fPIE -pie -Wl,-strip-all,--hash-style=both -o lief_sample_1_b lief_sample_1.c

$ file -b lief_sample_1_a ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=f17e46516ddb5d1767ba21fa767ac31f6a041421, stripped

$ file -b lief_sample_1_b ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=9d6acd73bd638251bb78e1e42647badbcd5e9b00, stripped

$ readelf -h lief_sample_1_a | grep Type Type: EXEC (Executable file) // 传统可执行程序

$ readelf -h lief_sample_1_b | grep Type Type: DYN (Shared object file) // PIE

$ python3 lief_sample_0.py lief_sample_1_a E_TYPE.EXECUTABLE 0

$ python3 lief_sample_0.py lief_sample_1_b E_TYPE.DYNAMIC 0

尽管lief_sample_1_b是PIE,但没有导出符号。

$ ./lief_sample_1_b magic Ok

$ ./lief_sample_1_b other Try again

用IDA反汇编lief_sample_1_b,check()符号被抹去,虚拟地址是0x860。

$ vi lief_sample_2.py


-- encoding: cp936 --

python3 lief_sample_2.py

python3 lief_sample_2.py lief_sample_1_b 0x860 check lief_sample_1_b.so

import sys import lief

piefile = sys.argv[1] funcoff = int( sys.argv[2], 0 ) funcname = sys.argv[3] sofile = sys.argv[4] binary = lief.parse( piefile ) binary.add_exported_function( funcoff, funcname ) try : # # glibc >= 2.29 deny calls to dlopen with PIE binaries. so we remove # DF_1_PIE flag. # binary[lief.ELF.DYNAMIC_TAGS.FLAGS_1].remove( lief.ELF.DYNAMIC_FLAGS_1.PIE ) except AttributeError : pass binary.write( sofile )


用LIEF将PIE转成.so:

$ python3 lief_sample_2.py lief_sample_1_b 0x860 check lief_sample_1_b.so

意思是,PIE加载后偏移0x860处的代码为之起名check(),按此要求生成.so。

$ python3 lief_sample_0.py lief_sample_1_b.so E_TYPE.DYNAMIC 1 check - 0x4860

对比lief_sample_1_b、lief_sample_1_b.so:

$ readelf --dyn-syms lief_sample_1_b

Symbol table '.dynsym' contains 13 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000668 0 SECTION LOCAL DEFAULT 12 ... 12: 0000000000201040 0 NOTYPE GLOBAL DEFAULT 27 __bss_start

$ readelf --dyn-syms lief_sample_1_b.so

Symbol table '.dynsym' contains 14 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000004668 0 SECTION LOCAL DEFAULT 12 ... 12: 0000000000205040 0 NOTYPE GLOBAL DEFAULT 27 __bss_start 13: 0000000000004860 0x7bb22900 FUNC GLOBAL DEFAULT 15 check

$ readelf -s lief_sample_1_b.so | grep "FUNC GLOBAL" 3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5 (2) 4: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2) 6: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __printf_chk@GLIBC_2.3.4 (3) 13: 0000000000004860 0x7bb22900 FUNC GLOBAL DEFAULT 15 check

$ nm -D lief_sample_1_b.so | grep " T " 0000000000004860 T check

$ objdump -T lief_sample_1_b.so | grep "g DF" 0000000000004860 g DF .text 000000007bb22900 check

常规ELF工具已经看到lief_sample_1_b.so中的导出符号check,IDA反汇编更显眼。

lief_sample_1_b.so现在可以当成可执行程序用,也可当成动态链接库用。

$ chmod +x lief_sample_1_b.so

$ ./lief_sample_1_b.so magic Ok

$ ./lief_sample_1_b.so other Try again

下面写个程序dlopen()打开lief_sample_1_b.so,调用其中的check()。

$ vi lief_sample_3.c


if 0

x64/Ubuntu 16.04.6 LTS + gcc 5.4.0

gcc -Wall -pipe -O3 -s -o lief_sample_3 lief_sample_3.c -ldl

endif

include

include

include

typedef int ( *some_t ) ( char * );

int main ( int argc, char * argv[] ) { char sofile, funcname, sth; void so; some_t some; int someret;

if ( argc != 4 )
{
    printf( "Usage: %s <sofile> <funcname> <sth>\n", argv[0] );
    return( -1 );
}
sofile      = argv[1];
funcname    = argv[2];
sth         = argv[3];
so          = dlopen( sofile, RTLD_LAZY );
if ( !so )
{
    fprintf( stderr, "dlopen error: %s\n", dlerror() );
    return( -1 );
}
some        = ( some_t )dlsym( so, funcname );
someret     = some( sth );
printf( "%s(\"%s\")=%d\n", funcname, sth, someret );
if ( someret )
{
    printf( "Ok\n" );
}
else
{
    printf( "Try again\n" );
}
return( 0 );

}

$ ./lief_sample_3 ./lief_sample_1_b.so check magic check("magic")=1 Ok

$ ./lief_sample_3 ./lief_sample_1_b.so check other check("other")=0 Try again

lief_sample_2.py负责PIE转.so。更多编程细节参看:


LIEF API https://lief.quarkslab.com/doc/latest/api/index.html

ELF API https://lief.quarkslab.com/doc/latest/api/python/elf.html


后面是些备忘,与标题无关。

用LIEF将ET_EXEC改成ET_DYN:


-- encoding: cp936 --

python3 lief_sample_4.py

python3 lief_sample_4.py lief_sample_1_a lief_sample_1_a.so

import sys import lief

exefile = sys.argv[1] sofile = sys.argv[2] binary = lief.parse( exefile ) print( binary.is_pie )

原来是lief.ELF.E_TYPE.EXECUTABLE。这个操作导致binary.is_pie返回True。

binary.header.file_type \ = lief.ELF.E_TYPE.DYNAMIC print( binary.is_pie ) binary.write( sofile )


binary.is_pie与lief.ELF.E_TYPE.DYNAMIC相关,与lief.ELF.DYNAMIC_FLAGS_1.PIE 无关。

修改第一个LOAD段的p_vaddr:


print( hex( binary.imagebase ) ) print( binary[lief.ELF.SEGMENT_TYPES.LOAD] ) print( hex( binary[lief.ELF.SEGMENT_TYPES.LOAD].virtual_address ) ) binary[lief.ELF.SEGMENT_TYPES.LOAD].virtual_address=

print( hex( binary.imagebase ) )


第一个LOAD段的p_vaddr就是ELF加载基址,但不让直接修改binary.imagebase。LIEF 能看到第一个LOAD段,若有多个LOAD段时,不知如何访问后续LOAD段?假设有多个 LOAD段,上述代码并不会同步修正后续LOAD段的p_vaddr,于是各LOAD段之间相对偏 移发生变化,这可能不符合预期,务必小心。

对ET_DYN,可以用prelink修改加载基址:

objdump -p test.so | grep -m 1 LOAD | awk -F' ' '{print $5;}' readelf -l test.so | grep -m 1 LOAD | awk -F' ' '{print $3;}'

readelf -Wl test.so | grep LOAD prelink -r 0xc00000 test.so readelf -Wl test.so | grep LOAD

若test.so有多个LOAD段,"prelink -r"会依次修正它们的p_vaddr,各LOAD段之间相 对偏移保持不变,挺智能的。"prelink -r"不能用于ET_EXEC。

关于PIE加载基址,参看:

How is the address of the text section of a PIE executable determined in Linux https://stackoverflow.com/questions/51343596/how-is-the-address-of-the-text-section-of-a-pie-executable-determined-in-linux

假设已经关闭ASLR,有啥办法加载ET_DYN到指定地址?

关于只在内存中创建、加载、执行ELF,放狗搜"memfd_create fexecve dlopen",下 面是其中几篇:


Super-Stealthy Droppers - [2017] https://0x00sec.org/t/super-stealthy-droppers/3715 (memfd_create and fexecve)

In-Memory-Only ELF Execution (Without tmpfs) https://magisterquis.github.io/2018/03/31/in-memory-only-elf-execution.html

Loading "fileless" Shared Objects (memfd_create + dlopen) - [2018-02-02] https://x-c3ll.github.io/posts/fileless-memfd_create/

Running ELF executables from memory - [2019-03-27] https://www.guitmz.com/running-elf-from-memory/

https://github.com/m1m1x/memdlopen

memdlopen is a proof of concept that demonstrate the possibility to fully load a dynamic library from memory on 64 bits linux systems.


这种不touch文件系统的方案多用于恶意软件。