PATH
, compiler include path, linker path, dynamic library search path LD_LIBRARY_PATH
.
PATH
is an environment variable. It defines the executable search path. When you run executable file without path(absolute or relative path), it will search the executable file from directories that defined by PATH
.
At initial, the PATH
is defined in /etc/environment
.
1 | $ cat /etc/environment |
We can append the PATH
in the terminal:
1 | export $PATH=$PATH:/path/append |
We can append the PATH
in .bashrc
(if terminal is bash
) or .zshrc
(if terminal is zsh
), so the appened PATH
is avaiable whenever we open a new terminal.
For gcc, it will look in several different places for headers[1]. It will look for headers requested with #include gcc --verbose
1 | $ cpp --verbose |
We can specify the include path when we compile the source code by adding -I/path/to/be/added
.
In order to do the symbol resolution, linker will search for the unresolved symbols in linker search path.
We can use gcc -print-search-dirs | sed '/^lib/b 1;d;:1;s,/[^/.][^/]*/\.\./,/,;t 1;s,:[^=]*=,:;,;s,;,; ,g' | tr \; \\012
to print default linker search path:
1 | $ gcc -print-search-dirs | sed '/^lib/b 1;d;:1;s,/[^/.][^/]*/\.\./,/,;t 1;s,:[^=]*=,:;,;s,;,; ,g' | tr \; \\012 |
We can specify the linker search path to gcc by adding -L/path/to/be/added
.
We can use -B/path/to/directory
of gcc to specify the gcc search path. It will insert the path in the head of the searh path list.
For example, we add -B/usr/test/
path to gcc:
1 | $ gcc -B/usr/test/ -print-search-dirs | sed '/^lib/b 1;d;:1;s,/[^/.][^/]*/\.\./,/,;t 1;s,:[^=]*=,:;,;s,;,; ,g' | tr \; \\012 |
LD_LIBRARY_PATH is an environment variable you set to give the run-time shared library loader (ld.so) an extra set of directories to look for when searching for shared libraries[2].
/opt/ghidra/Ghidra/Features/Python/data/jython-2.7.1/Lib
. It only supports some uses of basic packages. And I want to use other packages(such as protobuf
) in Ghidra python, I did not find a proper way to install the protobuf
in Ghidra Jython’s site-packages location.
And I find another way to solve it. Firstly, I install protobuf
in my system’s default python2.7.
1 | sudo pip2 install protobuf |
The protobuf
is installed into python2.7 site-packages location. To find the site-packges location, you can open python2.7 and type below:
1 | In [1]: import site |
You can find that there are two folders. Add these folders into Ghidra python’s sys.path
.
In Ghidra python file, add belows at the start of the file:
1 | import sys |
Firstly, get the python default path:
1 | import site |
Then, create a new file binaryninja.pth
in /usr/local/lib/python3.7/dist-packages
and store the binaryninja python path.
Suppose the binaryninja is installed in /opt/binaryninja
, then
1 | $ echo "/opt/binaryninja/python" > /usr/local/lib/python3.7/dist-packages/binaryninja.pth |
push $rbx
in the binary program. And it will increment the stack by 8 bytes. It sounds good for the binary program.
And I debug the modified binary with gdb, and find that it crashes in movaps
instruction.
1 | movaps xmmword ptr [rsp+0x50], xmm0 |
So what happend?
And I found the answers from some blogs(see reference). movaps
is “move aligned packed single-precision floating-point values”. If the instruction’s operand is memory, the memory address must be aligned to 16 bytes. And I print the $rsp+0x50
, its not 16 bytes alignment. That’s because I pushed $rbx
into the stack and increment the rsp to 8 bytes, and that results in rsp is not aligned to 16 bytes.
The picture from Patrick Horgan describes what will the c program do before main function.
If you compile a c program, the linker will link crt0.o, ctri.o, ctrbegin.o, ctrn.o with the target object together.
crt0.o contains _start
function, it will initialize the process before call main
function, it is defined in libc’s crt0.s file.
According to osdev, ctri.o defines the header of _init
and _fini
function, and ctrn.o defines the footer of _init
and _fini
function. And linker will link ctrbegin.o’s section .init
and .fini
between ctri.o and ctrn.o
ctrbegin.o also defines some functions such as deregister_tm_clones, register_tm_clones, __do_global_dtors_aux, frame_dummy
1 | git remote add -f repo-name git@github.com:repo.git |
1 | git merge -s ours --no-commit --allow-unrelated-histories repo-name/master |
1 | git read-tree --prefix=pro-name/ -u spoon-knife/master |
1 | git commit -m "subtree merged" |
1 | git pull -s subtree repo-name branchname |
However, I met overlaps with
problem. And need add -Xsubtree
to specify the directory where the sub-project should pull.
1 | git pull -s subtree -Xsubtree=pro-name repo-name branchname |
1 | git clone https://github.com/ramosian-glider/clang-kernel-build.git |
1 | cd $WORLD |
(To update Clang later on, do (cd TMP_CLANG/clang ; git pull) and run update.py again.)
1 | cd $WORLD |
(Note that kernel version v4.16 or older is fine, otherwise the latest clang lacks asm-goto support(llvm already support))ref1 and ref2。
1 | cd $WORLD |
1 | cd $WORLD |
1 | cd $WORLD |
Compile with KASAN
Edit .config file and add
1 | CONFIG_KASAN=y |
Regenerage config file:
1 | make oldconfig |
bcmp
Solutions: reference
Add ‘-fno-builtin-bcmp’ to CLANG_FLAGS
While the LLVM has supported asm-goto already, it seems that clang doesn’t support asm-goto.
kernel v4.16及之前版本不会有asm-goto的问题
Solutions: ref
Add CONFIG_DEBUG_VM, the boot will hang at
1 | [ 0.000000] Booting paravirtualized kernel on KVM |
in drivers/gpu/drm/amd/display/dc/cals/Makefile and drivers/gpu/drm/amd/display/dc/dml/Makefile file, they specify CFLAGS with mpreferred-stack-boundary, which is not supported by clang. Clang has the flag -mstack-alignment=4 that equals. So replace them
with -mstack-alignment=4 in these two files.
comment out CONFIG_EXOFS_FS
最近和jaguo在给南京大学软件安全课程出Buffer Overflow实验的时候,发现了出现shellcode exit normally的情况,但是并没有”发现”启动了新的shell。
1 | // buf2.c |
我们一开始的设计是漏洞程序如上所示,由于需要注入shellcode,有些是不可见字符,所以我们一开始设计将shellcode写入到一个文件attack_input中,然后通过重定位将输入定位到attack_input文件中,可以将shellcode传给输入。
1 | ./buf2 < attack_input |
然而这样就导致虽然能够正确注入shellcode,但是只要执行shellcode中的execve(‘/bin//sh’, 0, 0),就立即退出/bin//sh程序,而不是给出bash的命令行窗口。
经过查阅资料发现,我们在使用输入重定位的时候,就相当于将进程的输入重定位到文件中,而当程序将文件中的内容读完之后,会关闭该文件,此时相当于将程序的输入(stdin)关闭了。
当该进程启动一个shell进程时,shell进程是该进程的子进程,继承了父进程的文件描述符(包括已经关闭了的标准输入),此时shell发现标准输入已经关闭了,就会退出。
下面我们通过下面的实验来验证一下:
1 |
|
编译运行该程序,会发现shell也会正常退出。
我们的解决方案就是使用文件读函数正常的从文件中读取shellcode。
1 |
|
linux kernel开启KASAN和debug信息
1 | CONFIG_KCOV=y |
在编译的时候出现‘undefined reference to `____ilog2_NaN’ ‘
解决方案:patch 将该patch保存为patch.diff,拷贝到linux内核根目录下。
运行命令:patch -i patch.diff,提示输入文件时,先后输入include/linux/log2.h和tools/include/linux/log2.h即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CONFIG_DEBUG_KERNEL=y
CONFIG_DEBUG_INFO=y
CONFIG_CONSOLE_POLL=y
CONFIG_KDB_CONTINUE_CATASTROPHIC=0
CONFIG_KDB_DEFAULT_ENABLE=0x1
CONFIG_KDB_KEYBOARD=y
CONFIG_KGDB=y
CONFIG_KGDB_KDB=y
CONFIG_KGDB_LOW_LEVEL_TRAP=y
CONFIG_KGDB_SERIAL_CONSOLE=y
CONFIG_KGDB_TESTS=y
CONFIG_KGDB_TESTS_ON_BOOT=n
CONFIG_MAGIC_SYSRQ=y
CONFIG_MAGIC_SYSRQ_DEFAULT_ENABLE=0x1
CONFIG_SERIAL_KGDB_NMI=n
1
2
append 'kgdbwait kgdboc=ttyS0,115200' \
serial tcp::1234,server,nowait
可以发现其访问了(rcx+rax)的地址区域, 而该地址是不可访问地址区域
1 | git clone https://github.com/torvalds/linux.git $KERNEL |
1 | cd $KERNEL |
1 | gdb config |
1 | make oldconfig |
1 | make -j$(nproc) |
参考qemu配置如下:
1 | qemu-system-x86_64 -S -smp 2 -m 4G -enable-kvm -cpu host \ |
gdb 命令:
1 | gdb vmlinux |
参考qemu配置:
1 | qemu-system-x86_64 -smp 2 -m 4G -enable-kvm -cpu host \ |
强制下断点:
开一个终端连接qemu里的系统,以root用户执行:
1 | echo g > /proc/sysrq-trigger |
git stash
git pull
此时本地的文件已被远程仓库的内容覆盖
git stash pop stash@{0}
此时会提示你有冲突,让你手动解决。
]]>该paper主要对AFL有两个改进:
我个人认为,该论文的主要贡献是提供了一个机制来解决路径的hash collision问题,使得coverage判断更加准确。
AFL使用bitmap(默认64KB)来跟踪edge coverage。没一个字节都对应特定edge的hit count。AFL通过对每个basic block进行插桩,为每个basic block都随机分配一个id,当执行每条路径时,对该路径上的每个basic block都进行如下操作:
1 | cur_location= <COMPILE_TIME_RANDOM>; |
其中上面的prev_location右移一位主要是为了区分路径A->B和B->A。由于每个basic block的id是随机分配的,所以这种hash方法很容易产生collision,特别当程序比较大的时候,collision rate也越大。
CollAFL通过三种方式来解决hash collision:
我们都知道,fuzzing对于一些比较宽松的限制(比如x>0)能够很容易的通过变异产生一些输入达到该条件;而symbolic execution非常擅长求解一下magic value(比如x == deadleaf)。这是一篇比较经典的将concolic execution和fuzzing结合在一起的文章,该文章的主要思想就是先用AFL等Fuzzer根据seed进行变异,来测试程序。当产生的输入一直走某些路径,并没有探测到新的路径时,此时就”stuck”了。这时,就是用concolic execution来产生输入,保证该输入能走到一些新的分支。从而利用concolic execution来辅助fuzz。
Vuzzer是公认的比较好的类AFL fuzzer。它主要利用Data-flow features和Control-flow features来辅助fuzzer变异和进行seed的选择。
利用dynamic taint analysis 来推断input的结构和类型,以及某段数据在input的偏移。比如,它通过对每个cmp指令进行插桩来判断input的哪些字节与输入有关,并且知道与它比较的另外一个值。同时,Vuzzer也可以对lea指令进行插桩,从而检测index操作是不是与input某些bytes有关。
Control-flow features可以让Vuzzer推断出执行路径的重要性。比如,某些执行路径最后到达了error-hanling blocks。Vuzzer就通过静态的方法识别出了一下error-handling code。同时,Vuzzer通过对每个basic block赋予特定的权重,来促使fuzzer走到更深的路径中去。
This paper’s contributations:
该paper是Usenix 18的Distinguished Paper,其主要针对了当前的concolic execution的三个方面进行了优化: Slow Symbolic Emulation, Ineffective Snapshot and Slow and Inflexible Sound Analysis. 从而使得concolic execution更好的适应fuzzing场景。
现在主流的conclic executors做符号执行的时候是针对IR中间语言做的(比如KLEE的LLVM IR和angr的VEX IR),对中间语言模拟执行。其采用IR的原因是实现起来比较简单。由于Intel 64位指令集包含1795条指令,所以针对每条指令总结出来符号的语义对于人工来说是一个非常大的工作量,而IR的指令较少(LLVM IR有62条指令),符号化这些指令相对比较简单。
然而使用IR则引发了额外的overhead。首先,从机器指令到IR的转换本身就有overhead。由于amd64是CISC(complex instruction set computer),而IR是RISC(reduced instruction set computer),一般一条amd64的指令需要转换成多条IR指令,拿angr为例,如果将amd64指令转为VEX IR,则平均增加的指令数是4.69倍。其次,采用IR导致basic block level taint。因为由于效率的原因,从native instructions到IR的转换一般是以basic block为单位的,这样就导致无法将单个的native instruction转换成IR,所以也就只能做到哪些basic block需要符号化,而不是具体的某条指令需要符号化。这样做导致的后果就是如果某个basic block中只有一条指令和输入有关需要符号化,则整个basic block都需要符号模拟,这样就会造成很高的overhead。如果没有IR的话就可以做到指令级别的taint,就能够清楚的判断哪些指令需要符号模拟,哪些指令只需native execution,减少了不必要的符号模拟。实验表明,在一个basic block中,只有30%的指令需要符号模拟。
snapshot是conclic execution常用的一个技术,它能够保存某条分支前的状态S,当该分支执行完或者”stuck”时,可以从该状态S直接执行另外一个分支,避免了重新执行的overhead。然而snapshot本身就有一些缺点:snapshot需要保存一些外部的状态(文件系统,内存管理系统),则此时需要对影响外部状态的系统调用进行处理,一般有两个方法: full system concolic execution and External environment modeling。这两个方法都有一些缺陷:第一个方法是由于外部环境比较复杂,实现起来比较难,overhead较高;第二个则是model的system call较少,并且有些system call建模的不够完全。另外由于fuzzing的输入一般不会共享同一个分支,所以snapshot可能对于fuzzing这个场景也不是很好,所以该paper就没有采用snapshot的机制,对于每个输入都会重新执行,对于系统调用,则具体执行。
现在的concolic execution是将某条路径上的所有contraints都满足,从而求解出具体的input。然而复杂的contraints可能会导致输入求解不出。所以该paper的一个解决方法就是只求解出部分contraints。
FairFuzz focus on branch coverage, it works in two main steps.
First, it identifies the program branches that are rarely hit by previously-generated inputs. It call such branches rare branches. These rare branches guard under-explored functionalities of the program. By generating more random inputs hitting these rare branches, FairFuzz greatly increases the coverage of the parts of the code guarded by them.
Second, FairFuzz uses a novel lightweight mutation technique to increase the probability of hitting these rare branches. The mutation stategy is based on the observation that certain parts of an input already hitting a rare branch are crucial to satify the conditions necessary to hit that branch. Therefore, to generate more inputs hitting the rare branch via mutation, the parts of the input that are crucial for hitting the branch should not be mutated.
1 | Input: Seed Input S |
类AFL的fuzzing一般步骤如上所示,该paper主要关注于ASSIGNENERGY(s)这一操作,他们通过对不同的seed s赋予不同的energy,即如果一个seed s’产生的trace距离目标基本块targetB较近,则其energy(p)就较大,基于种子s’进行的变异操作就会变多。所以该paper主要有两个contributation: 设计一套算法计算seed s’产生的trace与targetB的距离;通过模拟退火算法来为每个seed s分配energy。
During fuzzing, the fuzzer selects a seed from a priority seed queue. The fuzzer applies a power scheduling against the seed with the goal of giving those seeds that are considered to be “closer” to the target sites more mutation chances, i.e, energy. Specifically, this is achieved through a power function, which is a combination of the covered function similarity and the basic block trace distance. For each newly generated test seed during mutation, after capturing its execution trace, the fuzzer will calculate the covered function similarity and the basic block trace distance based on the utilities. For each input execution trace, its basic block trace distance is calculated as the accumulated basic block level distances divided by the total number of executed basic blocks; and its covered function similarity is calculated based on the overlapping of current executed functions and the target function trace closure, as well as the function level distance.
After the energy is determined, the fuzzer adaptively allocates mutation budgets on two different categories of mutations according to mutators’ granularities on the seed(coarse-grained mutations and fine-grained mutations). Afterwards, the fuzzer evaluates the newly generated seeds to prioritize those that have more energy or that have reached the target functions.
They found that:
1 | git clone --depth 1 git://sourceware.org/git/binutils-gdb.git binutils |
1 | mkdir build |
1 | https://github.com/llvm-mirror/llvm.git |
1 | mkdir build |
And the LLVMgold.so will appear in the lib folder.
1 | sudo cp lib/LLVMgold.so /usr/local/lib |
fuzz解析数据的库函数的方法一般是找一个简单的二进制来测试库函数的功能,通过生成不同的输入来不断地运行该二进制程序。一般是通过fork和execve来生成子进程运行目标二进制程序,fuzz程序通过waitpid()函数来等待子进程退出,如果子进程发出SIGSEGV或SIGABORT等信号,则证明子进程崩溃了,此时可能会发生了memory corruption bugs。然而没有一个输入,就调用ececve()函数来进行程序的链接,库函数的初始化等操作,大大地降低了fuzzing的效率[1]。AFL通过在目标程序中插入fork server的逻辑代码来保证在fuzzing的时候只进行一次程序的链接,库函数的初始化等操作,而通过fork()函数的copy-on-write机制,大大提高了fuzzing的效率。
通过在二进制程序中插入fork server代码,该fork server会在main函数之前执行,它会暂停,等待AFL fuzzing端的输入,当AFL fuzzing端”发号施令”给fork server之后,fork server此时就通过fork()函数来生成子进程,子进程继续main函数的逻辑,由于fork server已经将各种资源都加载好,所以每次子进程只需要执行main函数的代码即可。
上面的例子是在afl中的llvm_mode文件夹中的afl-llvm-rt.o.c文件中定义的,fork server的逻辑也是比较简单,一个while循环,从FORKSRV_FD文件中读取AFL端给传来的数据,其中FORKSRV_FD是一个管道的一端,负责从AFL端读取数据。如果AFL端传来数据,则证明此时AFL的输入已准备好,则可以通过fork()来生成一个子进程,来运行main函数,进行fuzzing。
由于AFL进程与要fuzzing的进程不是父子关系(AFL与fork server是父子关系,fork server与要fuzzing的进程是父子关系)。所以AFL通过管道与fork server进程进行通信,而fork server通过waitpid()函数等待要fuzzing的子进程完成,得到其退出是的状态status,并将status通过管道传给AFL进程。
其中在afl-fuzz.c中的init_forkserver函数中,是对管道进行的初始化,感兴趣的可以看一下。
AFL是使用比较广泛的fuzzing工具,ASAN(AddressSanitizer)是google的一个非常高效的内存错误检测工具,其能够检查出UAF,Heap/Stack buffer overflow, Use after return, Use after scope, Initialization order bugs and Memory leaks。这两者都有基于llvm的版本,所以将这两者相结合效果也是非常好的。
在用AFL和ASAN来fuzzing heartbleed(教程链接afl-training)的时候出现了一个问题:
1 | Since it seems to be built with ASAN and you have a |
这是因为ASAN工具是跟踪所有内存的,所以理论上可能需要的内存比较大,在32位系统中,最多占用800多MB内存。在64位系统中,ASAN的shadow memory的理论上占用的最大内存是17.5TB和20TB,而一般的电脑并没有这么大的内存,所以可能会使电脑死机。所以AFL会在64位机器运行64位程序的时候,报出这种错误。链接也提供了这种情况的解决方法。
实际上,以上最大内存只是理论上的,一般运行的程序shadow memory所占用的内存并没有这么多,所以第一种解决方法就是使用-m none选项,来忽略此错误:
1 | afl-fuzz -i in -o out -m none ./executable |
第二种方法就是使用cgroup来限定改程序使用的资源:
1 | sudo ~/afl/experimental/asan_cgroups/limit_memory.sh -u usename afl-fuzz -i in -o out -m none ./executable |
第二种方法是比较稳妥的方法,并不会对系统造成非常大的影响,因为其限定了程序所使用的内存资源。
这两天做了一个CTF的题目,该题目的二进制链接。该题目的逻辑非常简单,就是接受输入,并将其打印,在打印的时候利用了printf函数,很明显是个format string漏洞。但由于格式化的字符串并没有在栈中,所以利用起来有一点困难,在此记录一下自己利用的方法。
格式化字符串函数可以接受可变数量的参数,并将第一个参数作为格式化字符串,根据其来解析之后的参数 参考。
一般发生格式化字符串漏洞的原因是因为并没有指定第一个参数格式化字符串(或者格式化字符串可以更改),所以给了攻击者一个可以控制格式化字符串的机会,进而可以实现任意的内存读写能力。其中能触发格式化字符串漏洞的函数有如下几个: scanf, printf, vprintf, vfprintf, sprintf, vsprintf, vsnprintf, setproctitle, syslog等,如果想比较系统的了解格式化字符串漏洞,可以访问链接。
首先拿到程序,先分析一下该程序的保护措施:
发现其除了canary保护之外,其它防护都开了(主要是输入的buff并不在栈上,所以并没有canary保护,并不代表着可以通过buffer overflow来溢出返回地址-_-)。
然后扔给IDA pro分析其逻辑:
该程序的逻辑非常简单,首先是给你三次机会,让你进行格式化字符串攻击,COUNT是全局变量,COUNT=3。接下来是exploit_me函数,该函数的逻辑更加简单,现将BUFF变量清空,然后读入13个字节,再将输入的字符串输出,在输出的时候会发生格式化字符串的攻击。其中BUFF是一个全局变量,大小是16个字节。该程序攻击起来主要有如下几个难点:
接下来主要针对以上提出来的两个难点进行攻击。
由于只允许三次输入,并且输入的长度有限,很难进行有效的攻击,所以接下来思路就是首先利用这三次输入将控制输入的计数变量修改掉,使其能够进行多次输入。
有上面程序分析可以看到,计数变量有两个:MACRO_COUNT局部变量和COUNT全局变量,只要将其中一个值修改掉,就可以进行多次输入,方便进行接下来的攻击。所以现在思路主要如下:
以上的每一个目的都可以利用一次format string攻击实现。
上面该图是在printf调用前的栈的内容,可以看出第一个参数是格式化字符串的地址,而接下来的一个内存单元0xffffcf6c存储的也是格式化字符串的地址,所以可以通过泄露该内存单元的内容来泄露BUFF变量的地址,从而可以算出程序的基址。接下来,ebp的内存单元存储的是saved ebp,上一个函数的ebp值,该值是栈的地址,所以可以通过泄露该地址来泄露栈的地址。所以可以输入
1 | %p%6$p |
来泄露栈的地址和程序的地址。
由于格式化字符串不在栈上,所以想通过格式化字符串来修改某个内存单元的值,首先得先把该内存的地址写入栈中。通过上面分析我们知道了栈上的地址和程序的地址,通过偏移也能计算出MACRO_COUNT和COUNT的地址。接下来则需要将MACRO_COUNT或者COUNT的地址写入栈中。在此,我选择将MACRO_COUNT的地址写入到栈中,理由如下:
从上图可以看到0xffffcf84地址处存储的是内存单元0xffffd044的地址,而0xffffd044存储的值是0xffffd224,也是栈上的一个地址,而MACRO_COUNT也是栈上的变量,其地址与0xffffd224的高16位应该是相等的,所以此时只需要修改0xffffd044地址存储的低16位即可。这样能保证攻击顺利进行(如果修改整个32位的话,则输出的数太多,需要花费很长时间,还有一个原因是导致输入的字符串过长,没办法实现攻击)。
所以具体的攻击手段就是将0xffffd044内存单元存储的值的低16位改为MACRO_COUNT的高位byte地址即可。
假设MACRO_COUNT的地址为addr。
则可以输入
1 | "%" + str(addr & 0xffff) + "d" + "%9$hn" |
即可。
通过前面的步骤,实现了将0xffffd224的地址处存储了MACRO_COUNT的地址,而0xffffd044相对于0xffffcf60(printf的第一个参数)的offset为0xE4,则可以进行如下输入使的MACRO_COUNT的高位为0xFF。
1 | "%255d%57$hhn" |
其中57为0xE4/4,因为地址是4字节的。
通过以上的努力,我们可以进行多次的输入。由于输入的格式化字符串是全局变量,并不在栈上,我们就不能通过一次简单的输入就能读写任意内存,此时需要通过格式化字符串来间接的修改内存地址到栈上。具体思路如下:
如果我想要将地址addr写入到栈上的某个内存单元上去,设栈上的该内存单元地址为stack_addr。则我需要一次中介来完成此类攻击。
我们再来看一下调用printf时栈中的布局:
可以看到0xffffcf84和0xffffcf88两个内存单元存储的内容是栈上的地址,而其又指向了一个栈上的地址。所以可以通过格式化字符串将0xffffd044地址处的内容改为stack_addr+2,将0xffffd04c地址处的内容改为stack_addr,然后再通过$hn分别向stack_addr+2处写入addr的高16位((addr&0xffff0000)>>16),stack_addr处写入addr的低16位(addr&0xffff)。
具体的攻击过程如下:
1 | def modify(address, modifiedAddress): |
其中address就是此处的stack_addr,modifiedAddress就是此处的addr。
有了可以向栈中写入任意地址的能力,我们就可以进行libc地址的泄露和修改返回地址及其参数了。
通过以上的方法,我们可以将printf函数的got地址写入到栈上,然后通过%s读取got的内容,从而泄露libc的地址。
由于改题目并没有提供具体的libc版本,所以可以通过泄露的printf的地址,到libc database search网站进行查询。通过绣楼libc地址,我们可以得到system的地址和”/bin/sh”字符串的地址。
由于泄露了libc的地址,所以将main函数的返回地址修改为system的地址,并将其参数设为”/bin/sh”字符串的地址,输入EXIT,即可完成攻击。
整个的攻击脚本如下:
1 |
|
gdb-multiarch /path/of/arm-executable
target remote 1234
,调试即可Enjoy it!!!
]]>随着IoT(Internet of Things)设备快速增长,IoT设备的安全也逐渐引起大家的注意。如论文[1]所述,IoT的安全问题主要包括如下方面:
它们的关系如下图所示:
图1 IoT安全概览
由于IoT设备对于能耗和及时性的要求比较高,所以其具体实现(操作系统及软件的保护机制)都和PC端和手机端有很大的区别。由于能耗的要求,大部分IoT设备都采用低能耗的处理器(比如arm Contex-M系列),这些处理器大部分都没有MMU,所以没有虚拟地址到物理地址的转换,更无法提供ASLR等防护(arm Contex-M由于有MPU功能,能提供比较局限的内存防护机制);由于实时性的要求,大部分采用的系统是RTOS(Real Time Operating System)或者直接是bare mental system,其每个设备的内存布局可能都是固定的。所以IoT设备的应用层的安全也是非常严峻的。
在IoT设备中,其代码和数据一般存储在ROM中(大部分都是Flash,关于Flash的种类可以访问here来了解一下)。一般将这部分代码和数据称为Firmware(可能表述不准确,欢迎指正)。Firmware没有一个固定的格式,它更像是一个binary blob,具体的格式和解析根据设备的不同而有所不同。
一般获取firmware的方式主要有三种:
由于现在有很多IoT设备都是Over-The-Air Firmware Update,所以有很多厂商并不会在官网上提供firmware的下载,所以一般比较通用的获取firmware的方法都是通过硬件逆向方法。关于硬件逆向,推荐两篇文章物联网硬件安全分析基础-固件提取和物联网硬件安全分析基础-串口调试。
最近在查看关于firmware逆向有关的资料,发现有如下几个问题:
在浏览了很多教程之后,发现了关于Marvell IoT SDK的一些小经验,特总结下来,以备日后查阅。具体的教程可参阅Inside The Bulb: Adventures in Reverse Engineering Smart Bulb Firmware和dustcloud。dustcloud做了挺多关于小米iot逆向的工作的,其中小米的yeelight和智能网管设备的firmware都是采用的Marvell IoT SDK。由于dustcloud直接提供了yeelight的firmware,所以就省去了我硬件逆向提取firmware的步骤了,我直接从dustcloud下载firmware。
Inside The Bulb: Adventures in Reverse Engineering Smart Bulb Firmware介绍了如何将Marvell IoT SDK格式的firmware提取出代码,并将其合并成elf文件的,由于里面细节有限,我在此重复了里面的步骤,并总结出了一些方法。
首先在二进制编辑器中可以看到该firmware是MRVL(Marvell)的,而该文件含有一些entries, 表示了不同”段”的偏移,大小和地址信息:
1 | DWORD magic; // Always 0x2 |
具体的firmware二进制数据如下图所示:
图2 firmware二进制
可知其含有三个不同的entry,可使用dd工具将这三个不同的”段”提取出来:
1 | dd if=yeelink.light.strip1.bin bs=1 skip=200 count=12824 of=s1.bin |
此时,得到了三个二进制文件,使用arm-none-eabi-objcopy将其合并成ELF文件:
1 | arm-none-eabi-objcopy -I binary -O elf32-littlearm --adjust-vma 0x100000 --binary-architecture arm --rename-section .data=.text,contents,alloc,load,readonly,code --add-section .text2=s2.bin --set-section-flags .text2=contents,alloc,load,readonly,code --change-section-address .text2=0x1f0032e0 --add-section .text3=s3.bin --set-section-flags .text3=contents,alloc,load,readonly,code --change-section-address .text3=0x20000040 s1.bin firmware_yeelink.elf |
上面的命令就是将三个文件合并成一个ELF文件,并且分别将其置为不同的section,设置virtual address。
如果直接将生成的文件firmware_yeelink.elf扔到IDA pro中会出现一个问题:由于objcopy生成的elf文件是可重定位类型(relocatable file),扔到IDA中虚拟地址是从0开始的,并不是从0x100000开始的。
我最后终于找到一个方法:再使用ld链接器将可重定位类型的文件生成可执行类型(executable file),并给每一个section添加虚拟地址:
1 | arm-none-eabi-ld --section-start=.text=0x100000 --section-start=.text2=0x1f0032e0 --section-start=.text3=0x20000040 firmware_yeelink.elf -o firmware.elf |
此时扔给IDA,虚拟地址正确.
(–未完待更–)