ARM(手机、嵌入式)架构上ShellCode编写入门教程

原文地址:https://azeria-labs.com/writing-arm-shellcode/

编写ARM架构shellcode的基本介绍

如果你正在阅读这部分教程,请确保对ARM汇编有一个基础的了解(关于这点,你可以查看“ARM汇编基础”系列教程)。在这个部分,你将学习到怎么根据已有的ARM汇编基础知识,从而在ARM上创建你的第一个简单的shellcode。如果你手头没有ARM设备(ARM架构的手机、嵌入式设备等),那么你可以根据这篇教程:如何通过QEMU模拟树莓派,在VM虚拟机上模拟出你自己的树莓派,这样你就有了自己的ARM设备了。

这篇教程是为那些不满足于仅仅使用shellcode自动生成器,而想要自己使用ARM汇编来写出shellcode的人而写的。毕竟,知道在表象之下,这一切是怎么工作的,并且能够完全控制最终的shellcode结果,比单纯用工具可有趣多了,你说呢?而且用汇编写出自己的shellcoe,还是一项非常有用的技巧!因为有的时候你需要绕过shellcode检测算法,或者是在某些情况下绕过一些限制,此时shellcode自动生成器生成的代码则可能会失效,这些情况下,手写shellcode就能大放异彩。让我告诉你一个好消息吧,一旦你熟悉了编写shellcode的整个过程后,这将是一项非常容易学习的技术。

在这篇教程中,我们将使用到以下工具(如果你是Linux系统,那么其中大部分工具都已经默认安装好了):

  • GDB-我们选择的调试器
  • GEF-GDB的增强版,强烈推荐(由@hugsy开发)
  • GCC-Gnu编译集
  • as-汇编器(assembler)
  • ld-链接器(linker)
  • strace-追踪系统调用(system call)的工具‘
  • objdump-检查反汇编代码中是否有空字节(null-bytes)的工具
  • objcopy -从ELF二进制文件导出shellcode的工具

请保证你是在ARM架构下编译、运行这篇教程中的所有示例。

在开始写自己的shellcode之前,请保证你知道这些基本的原则,比如:

  1. 你需要让shellcode尽可能的简洁紧凑,并且没有空字节(null-bytes)
  • 原因:我们所写的shellcode,利用的是内存污染漏洞比如缓冲区溢出。一些缓冲区溢出发生的条件是,编程工作人员错误的使用了‘strcpy’函数。这个函数的作用就是拷贝数据,直到遇到了一个空字节(null-byte)。正是利用这种溢出,我们才能够获得程序运行流程的控制权。但如果‘strcpy’函数遇到了空字节,那么我们的shellcode将无法被完整的拷贝,我们的漏洞利用也很可能会失败。
  1. 你应该尽量避免对特殊库函数的调用,并且shellcode中不能出现绝对内存地址
  • 原因:为了让shellcode能在更多的设备上运行而不出错,我们就不能使用那些需要特殊依赖的库函数,也不能使用那些特定ARM设备才有的绝对不变的内存地址。

编写shellcode的过程,可分为以下几步:

  1. 知道你要使用哪种系统调用(system call)
  2. 弄清楚系统调用号(syscall number),以及这个系统调用函数所需要的系列参数
  3. 把你shellcode中的空字节去除掉
  4. 将你的shellcode转换成16进制的字符串

理解系统函数

在开发我们第一个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

通过观察这个函数原型,我们可以知道这个系统调用函数需要如下参数:

  • fd-1表示STDOUT
  • buf-指向字符串string的指针
  • count-要写入的字节数->13
  • write函数的系统调用号->0x4
    对于前3个参数,我们可以使用R0,R1和R2寄存器。对于系统调用号,我们可以使用R7寄存器,即把0x4传给R7。
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吧!

1. 追踪系统调用

第一个简单shellcode的例子,我们将会把下面的简单函数转变为ARM汇编:

#include <stdio.h>

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()这个系统调用。

2.系统调用号及其参数

弄清楚使用了哪些系统调用后,下一步就是找execve()对应的系统调用号,并且弄清楚需要哪些参数

Well, this essay is still on construction…
If you find it helpful, maybe you can buy me a cup of Cola.
文章将会继续更新,如果本文对你有帮助,不如请我一杯可乐 :jack_lantern:
在这里插入图片描述

发布了46 篇原创文章 · 获赞 28 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/LQMIKU/article/details/103533515