这篇文章发表于 694 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

这部分主要是关于针对Linux平台的shellcode亲手编写的基础方法。选自国外某位大哥的视频,在此进行整合了一下。

准备就是在Linux环境下安装好nasm即可。以下的环境是64位的,因此一些命令是适合于64位的。不过我生成的程序都是32位的。

一定要积极去调试生成的程序!我基本上不会在文中提及这部分,但是这是最重要的部分。

编写exit

以下就是exit程序的源代码demo1.nasm

1
2
3
4
5
6
7
8
9
10
11
;This is demo1.nasm
global _start

section .text
_start:

xor eax, eax
mov al, 0x1 ;mov eax, 1 ;;;后面被注释的部分会在之后的code中产生\x00字符,这个在shellcode中是致命错误,会引发Segmentation fault (core dumped)。于是使用这两条命令来实现
xor ebx, ebx
mov bl, 0xa ;mov ebx, 10 ;;;同上
int 0x80 ;退出

对其进行编译:

1
2
3
nasm -f elf32 demo1.nasm -o demo1.o
ld -m elf_i386 demo1.o -o demo1
#或者 gcc demo1.o -o demo1 -m32

链接后得到demo1,这时可以直接运行一下该程序,嗯,它退出了。

然后使用以下命令完成对该demo1程序的shellcode提取:

1
2
objdump -d ./demo1 |grep '[0-9a-f]:'|grep -v 'file'|cut -f2 -d:|cut -f1-6 -d' '|tr -s ' '|tr '\t' ' '|sed 's/ $//g'|sed 's/ /\\x/g'|paste -d '' -s |sed 's/^/"/'|sed 's/$/"/g'
#提取得到了 "\x31\xc0\xb0\x01\x31\xdb\xb3\x0a\xcd\x80"

然后将它写入下面模板exit.c的code数组中:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#include <string.h>
unsigned char code[] = \
"\x31\xc0\xb0\x01\x31\xdb\xb3\x0a\xcd\x80";
int main(){
printf("Shellcode Length: %d \n", strlen(code));
int (*ret)() = (int(*)())code;
ret();
}
// gcc -fno-stack-protector -z execstack exit.c -o exit -m32

运行之后也是直接退出。以上大概就是亲手编写shellcode的一个基本操作框架。

编写Hello world!

接下来就直接给出程序demo2.nasm和大致说明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
global _start

section .text

_start:

jmp short call_shellcode

shellcode:

xor eax, eax
mov al, 0x4

xor ebx, ebx
mov bl, 0x1

pop ecx

xor edx, edx
mov dl, 0xd

int 0x80

; exit the function
xor eax, eax
mov al, 0x1
xor ebx, ebx
int 0x80

call_shellcode:

call shellcode
message: db "Hello world!", 0xa

这里这样组织函数结构是为了让通过栈来使得字符串能够被轻松找到。如果将字符串写死在section .data里面的话,提取出来的shellcode并不会被最终的C语言程序所接受,理由很简单——地址都变了还怎么找……因此这里非常聪明地利用了栈的特性。

至于eax,ebx,ecx,edx的赋值——对于eax,参看Linux自带的unistd_32.h里面的内容,很容易能找到API函数write的代号是4。后面的几个寄存器其实是相当于把参数传入到一个函数的感觉,可以自行查阅资料来调查这个函数的参数位置该怎么安排,也可以尝试使用如下的办法(因为C程序最终调用的都是Linux提供的API):

1、先编写一个C程序why

1
2
3
4
5
#include <stdio.h>
int main(){
printf("%s", "UCASZ");
}
// gcc why.c -o why

2、使用strace命令:

1
2
3
4
5
6
7
strace -f ./why

------------------------output------------------------
...
write(1, "UCASZ", 5UCASZ) = 5
...
------------------------output------------------------

这样输出中体现的格式就应该能够和之前的处理挂上钩了。

编写execve

以下的nasm文件产生ELF文件能够执行但是提取出来的shellcode并不能运行,理由自己请调试理解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
        section .data
; strings
arg0 db "/bin/sh",0
arg1 db "sh",0
arg2 db "-c",0
arg3 db "/bin/sh",0

; arrays
align 4
argv dd arg1, arg2, arg3, 0
envp dd 0

section .text
global _start
_start:

xor eax, eax
mov al, 0xb ; SYS_execve
xor ebx, ebx
mov ebx, arg3 ; filename
mov ecx, argv ; argv
mov edx, envp ; envp
int 0x80 ; syscall

xor eax, eax
mov al, 0x1
int 0x80

需要一些小技巧来完成编写。在此之前,需要man execve来研究一下各个寄存器该如何配置。

1
2
int execve(const char *pathname, char *const argv[],
char *const envp[]);

因此,以/bin/sh为例,这里有一种简单易行的填入方案:eax=0xbebx="/bin/sh"ecx=ptr($ebx)edx=0x0。然而在具体的处理方面还是需要一些技巧(比如利用栈),从这篇文章中摘的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
global _start

section .text

_start:

xor eax, eax
push eax

;拼合//bin/bash(第一位的斜杠是为了凑字节数)
push 0x68732f6e ; hs/n
push 0x69622f2f ; ib//

mov ebx, esp

push eax
mov edx, esp

push ebx
mov ecx, esp

mov al, 0xb
int 0x80

之后的工作便是对shellcode进行加密解密等操作了,这里既能够按照。

最后推荐一个别人的自动化程序:https://github.com/NytroRST/ShellcodeCompiler