原文地址:https://azeria-labs.com/writing-arm-shellcode/
如果你正在阅读这部分教程,请确保对ARM汇编有一个基础的了解(关于这点,你可以查看“ARM汇编基础”系列教程)。在这个部分,你将学习到怎么根据已有的ARM汇编基础知识,从而在ARM上创建你的第一个简单的shellcode。如果你手头没有ARM设备(ARM架构的手机、嵌入式设备等),那么你可以根据这篇教程:如何通过QEMU模拟树莓派,在VM虚拟机上模拟出你自己的树莓派,这样你就有了自己的ARM设备了。
这篇教程是为那些不满足于仅仅使用shellcode自动生成器,而想要自己使用ARM汇编来写出shellcode的人而写的。毕竟,知道在表象之下,这一切是怎么工作的,并且能够完全控制最终的shellcode结果,比单纯用工具可有趣多了,你说呢?而且用汇编写出自己的shellcoe,还是一项非常有用的技巧!因为有的时候你需要绕过shellcode检测算法,或者是在某些情况下绕过一些限制,此时shellcode自动生成器生成的代码则可能会失效,这些情况下,手写shellcode就能大放异彩。让我告诉你一个好消息吧,一旦你熟悉了编写shellcode的整个过程后,这将是一项非常容易学习的技术。
在这篇教程中,我们将使用到以下工具(如果你是Linux系统,那么其中大部分工具都已经默认安装好了):
请保证你是在ARM架构下编译、运行这篇教程中的所有示例。
在开始写自己的shellcode之前,请保证你知道这些基本的原则,比如:
编写shellcode的过程,可分为以下几步:
在开发我们第一个shellcode之前,不如先来写一个简单的ARM汇编程序,其功能为输出字符串。第一步是找到我们所需要的系统调用,在本例中,就是“write”了。而这个系统调用的原型可以在Linux man pages中找到:
ssize_t write(int fd, const void *buf, size_t count);
从更高级编程语言的角度来看,比如C语言,这个系统调用的使用可能是如下形式的代码:
const char string[12] = "Azeria Labs\n";
write(1, string, sizeof(string)); //Here sizeof(string) is 13
通过观察这个函数原型,我们可以知道这个系统调用函数需要如下参数:
mov ro, #1 @fd 1 = STDOUT
ldr r1, string @loading the string from memory to R1
mov r2, #13 @write 13 bytes to STDOUT
mov r7, #4 @write()函数的系统调用号 = 0x4
svc #0
根据上面的这一小段代码,一个完整的ARM汇编程序看起来可能如下:
.data
string: .asciz "Azeria Labs\n" @.asciz adds a null-byte to the end of the string
after_string: .set size_of_string, after_string - string
.text
.global _start
_start:
mov r0, #1 @ STDOUT
ldr r1, addr_of_string @ memory address of string
mov r2, #size_of_string @ size of string
mov r7, $4 @ write syscall
swi #0 @ invoke syscall
_exit:
mov r7, #1 @ eixt syscall
swi #0 @ invoke syscall
addr_of_string: .word string
在上面程序的.data数据段中,我们通过string的结束地址(after_string)减去开始地址(string),得到了字符串的大小(size_of_string)。如果我们自己手动计算字符串的大小,并直接将计算结果传送至R2寄存器,那么.data中的计算过程就不是必需的了。另外,为了退出我们的程序,我们使用了系统调用号1号来调用exit()函数。
将上面这段程序保存为write.s文件,并编译和执行:
azeria@labs:~$ as write.s -o write.o && ld write.o -o wirte
azeria@labs:~$ ./write
Azeria Labs
很酷,从输出结果来看,我们的程序正确执行了。现在我们已经了解了编写ARM汇编程序的过程,接下来就更进一步关注其他细节,使用ARM汇编来开发出我们的第一个简单shellcode吧!
第一个简单shellcode的例子,我们将会把下面的简单函数转变为ARM汇编:
#include
void main()
{
system("/bin/sh");
}
第一步是找出这个简单函数使用了哪些系统调用,并且弄清楚这些系统调用函数需要哪些参数。通过’strace’这个工具,我们可以在系统内核中监测到程序的系统调用。
把上面的代码保存为system.c文件,并完成编译,之后再使用’strace’的命令。
azeria@labs:~$ gcc system.c -o system
azeria@labs:~$ strace -h
-f -- follow forks, -ff -- with output into separate files
-v -- verbose mode: print unabbreviated argv, stat, termio[s], etc. args
--- snip --
azeria@labs:~$ starce -f -v system
--- snip --
[pid 4575] execve("/bin/sh", ["/bin/sh"], ["MAIL=/var/mail/pi", "SSH_CLIENT=192.168.200.1 42616 2"..., "USER=pi", "SHLVL=1", "OLDPWD=/home/azeria", "HOME=/home/azeria", "XDG_SESSION_COOKIE=34069147acf8a"..., "SSH_TTY=/dev/pts/1", "LOGNAME=pi", "_=/usr/bin/strace", "TERM=xterm", "PATH=/usr/local/sbin:/usr/local/"..., "LANG=en_US.UTF-8", "LS_COLORS=rs=0:di=01;34:ln=01;36"..., "SHELL=/bin/bash", "EGG=AAAAAAAAAAAAAAAAAAAAAAAAAAAA"..., "LC_ALL=en_US.UTF-8", "PWD=/home/azeria/", "SSH_CONNECTION=192.168.200.1 426"...]) = 0
--- snip --
[pid 4575] write(2, "$ ", 2$) = 2
[pid 4575] read(0, exit
--- snip --
exit_group(0) = ?
+++ exited with 0 +++
上面的输出说明了,程序使用了execve()这个系统调用。
弄清楚使用了哪些系统调用后,下一步就是找execve()对应的系统调用号,并且弄清楚需要哪些参数。
Well, this essay is still on construction…
If you find it helpful, maybe you can buy me a cup of Cola.
文章将会继续更新,如果本文对你有帮助,不如请我一杯可乐 :jack_lantern: