恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

picoCTF rop chain 实战:x64栈溢出与ROP链构造详解

  • 首页
  • 资讯中心
  • /
  • picoCTF rop chain 实战:x64栈溢出与ROP链构造详解

相关资讯

分布式电源无功注入的配电网电压控制:IEEE 56节点Matlab仿真实践 2026/10/15 23:21:57
ViTPose 中基于 ResNeXt 骨干的 MPII 人体姿态估计:配置解析、源码实现与训练实战 2026/10/15 23:16:57
Harvester 项目中的 go-containerregistry remote 包:面向 OCI 镜像仓库的完整客户端指南 2026/10/15 23:16:57

最新资讯

RxJS响应式编程实战:用Observable统一处理前端异步流
OpenProse 示例库实战指南:从 48 个 .prose 示例掌握多智能体工作流语言
Android开发中高效查看SQLite数据库的三种实战方案
C#与SharpPcap实战:局域网ARP抓包监控与IP冲突检测
BP神经网络PID控制器原理与Simulink仿真:自适应调参实战
rea Android APK 静态分析实战:JDK 与 JADX 0.7.1 引擎集成、五操作全流程与验证复现

今日推荐

Java八种基本类型深度解析:从内存布局到实战选型
基础:常用快捷键——IDEA、VS Code、PyCharm 与 Linux 终端效率速查
解决ultraedit下verilog .v文件中文注释乱码:把编码配置改到TaoToken辅助排查

本周热门

UE动画修改实战:从资产编辑到重定向与蒙太奇驱动
统计随机数生成器攻击下的KLJN安全密钥交换协议Matlab仿真
政务API安全治理:资产测绘、低代码编排与行标对标实践

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

picoCTF rop chain 实战:x64栈溢出与ROP链构造详解

发布时间:2026/10/15 23:21:57
picoCTF rop chain 实战:x64栈溢出与ROP链构造详解 去年第一次下载 picoCTF 2018 的 rop chain 时我还停留在“栈溢出就是塞 shellcode”的阶段。程序里明晃晃一个 gets我以为又是一个五分钟就能搞定的送分题结果把自己写的一堆机器码打进栈里程序直接 Segfault连个水花都没有。后来才反应过来这道题真正的门槛不在于找漏洞而在于系统已经把栈设置成了不可执行你精心准备的 shellcode 永远只会被当成普通数据根本轮不到 CPU 去执行它。这道题的价值也正在这里它逼着你离开“复制一段 shellcode 进栈”的舒适区老老实实理解 ROP 链到底是怎么工作的。如果你手里已经有一份 Linux x64 的 pwn 基础知道缓冲区溢出但还没系统搞过 ROP或者刚刷完几道入门题想往深走一步这篇文章会非常适合你。我尽量不绕弯子从拿到文件的第一眼开始到最终用 pwntools 打出 flag 的完整链路重走一遍。中间会穿插我实际调试时踩过的坑那些才是写 writeup 的人经常懒得讲的东西。1. 拿到题目文件后的第一轮侦查1.1 文件类型与安全机制决定了整道题的走向不管题目叫什么拿到二进制后我的顺序永远是三件事file看类型checksec看保护然后立刻确认有没有符号表。先别急着逆向这几条信息能帮你省掉大量无用功。我本地把题目给的二进制命名为ropchainfile输出大概是这样的ropchain: ELF 64-bit LSB executable, x86-64, dynamically linked, not stripped一个 64 位动态链接的 ELF最重要的是最后的not stripped——符号表没有被剥掉。这意味着你不需要跟 IDA 较劲直接objdump -t就能把所有函数名、全局变量名看得清清楚楚。对于入门题来说这基本等于出题人把答案放在了桌面上。接着是checksec我用的是 pwntools 自带的那个输出信息整理成下面这张表项目状态对这个题目意味着什么Canary关闭栈上可以放心覆盖不需要先泄露金丝雀NX开启栈和堆数据不可执行shellcode 方案直接作废PIE关闭代码段地址固定gadget 地址可以直接写死在 payload 里Relro部分开启对这道题影响不大重点是上面三行先说 NX。它开启之后你往栈里写任何机器码都只是“数据”CPU 一旦打算以指令方式去执行它就会被硬件拦下来。所以传统思路彻底没戏这也是为什么这道题被称为入门 ROP 题而不是入门 shellcode 题。再配合 PIE 关闭意味着程序运行时 .text 段的加载地址是固定不变的你从反汇编里看到的地址是多少运行时就是多少。这两个条件凑在一起才是 ROP 能“可复现”的前提。Canary 关闭则解决了最后一个顾虑gets 读入时发生的缓冲区溢出不会被程序察觉你可以一口气把返回地址覆盖成任意值。如果这题开了 Canary你就得先想办法泄露它难度直接上一个台阶。这道题没开那么就往下走。1.2 反汇编打底三个函数和一个隐藏的全局变量因为有符号表直接在 gdb 里info functions或者用objdump -t ropchain | grep F能看到除了 main 之外的三个关键函数名字非常直白win、flag、val。初学者看到这种命名通常会犯一个错误以为调用 win 就能拿 flag。别急着下结论把反汇编逐条看一遍才是正经。我用objdump -d ropchain把.text段拉出来重点看了这几个函数。main 很短做的事情就是打印一行提示然后调用 vulnvuln 里就是一个典型的栈溢出点声明了 64 字节的局部缓冲区然后 gets 往里读没有任何长度限制。win 函数确实存在但它做的事情是在终端打印一句“You win!”并不包含读 flag 文件的逻辑。真正藏着 flag 的是 flag 函数而 flag 函数又不是无条件执行的它里面有一连串的比较指令条件全部满足后才会打开 flag 文件输出内容。再往下看val 函数同样有参数校验校验通过后会修改某个全局变量。把几个函数拼起来逻辑就清楚了这道题需要你先调用一次 val让它把全局标志位置为满足条件的状态然后再调用 flag让它读 flag 文件。而且两次调用都需要传入特定的参数参数不对函数内部依然不会走真正的分支。也就是说这道题表面上是“栈溢出控制返回地址”实际考的是两件事一是你能不能按正确的顺序调用多个函数二是你能不能给函数设置正确的参数。前者需要你对栈布局有感觉后者需要你对 x64 传参约定有肌肉记忆。这两个点恰恰就是 ROP 链的核心。2. 为什么一条 ret 指令就能“变”出任意执行流2.1 ret 的真正语义它是一台“弹栈跳转机”很多刚开始学 pwn 的同学对 ret 的理解停留在“函数结束就该 ret”。这个理解没有错但太粗了。实际上 ret 是一条非常机械的指令它做两件事把栈顶的 8 字节弹出来塞进指令指针寄存器 RIP然后跳过去执行。也就是说栈顶是什么地址CPU 就跳到什么地址。放到正常程序里函数返回地址是 call 指令自动压栈的所以 ret 能把控制权还给调用者。但一旦这块内存可以被你改写情况就完全变了。你往栈里写多少字节、写什么值ret 就会老老实实地把那个“伪造的返回地址”当作下一站目的地。你可以把它理解为一张被调包的机票登机口不看你是谁只看你手里的票上写着哪里。回到这道题vuln 函数的缓冲区溢出发生在栈上gets 读入的数据从低地址往高地址覆盖最终可以一路覆盖到函数返回地址。配合 Canary 关闭这一点我们可以精确计算偏移让 vuln 函数结束时的 ret 直接跳到我们指定的函数。2.2 从“跳一次”到“跳一串”gadget 的概念如果只是跳一次那栈溢出问题就简单了直接把返回地址改成 flag 函数地址就完事。但 flag 函数要求参数满足特定条件x64 架构下函数调用的第一个参数放在 RDI 寄存器第二个参数放在 RSI 寄存器。问题来了ROP 链只能控制栈上的数据你没有机会直接写 RDI 寄存器怎么办答案是从程序现有的代码里找“片段”。一个以 ret 结尾的指令序列被称为 gadget比如pop rdi; ret。pop rdi 会把栈顶的 8 字节弹进 RDI然后 ret 又把新栈顶弹进 RIP。这样一小段代码就把“往 RDI 里塞值”这个原本做不到的操作变成了“在栈上放一个值就行”。把多个 gadget 串起来就是 ROP 链。每一个 ret 都相当于接力棒交接执行完一个片段后自动跑到下一个片段。你在栈上布置的不是单一返回地址而是一整条“指令流水线”先是 pop rdi接着是它要弹出的参数再是 pop rsi跟着是第二个参数最后是目标函数地址。理解这一点后再看网上那些又长又怪的 payload就不会觉得它们神秘了本质上就是往栈上按顺序摆好“片段地址”和“参数预留位”。2.3 x64 传参约定绕不开的 System V ABIx64 下的函数调用约定和 x86 完全不同。x86 时代参数靠压栈所以栈溢出攻击里只要在栈上布置好参数就行x64 下前六个整型参数分别通过 RDI、RSI、RDX、RCX、R8、R9 传递更多参数才会用栈。这意味着攻击者不仅要控制跳转地址还要控制这些寄存器。这就是为什么 ROP 工具里最常见的 gadget 是pop rdi; ret和pop rsi; ret。你可以把 RDI 类比成快递单上的收件人RSI 是收件人手机号POP 指令则是一个临时中转站栈上的值先被卸到中转站再被 ret 带着去往下一个目的地。掌握了这个思维方式后面遇到传三个参数、四个参数的题目思路是一致的只是需要找对应更多寄存器的 gadget。2.4 偏移量 72 是怎么来的别背答案用 cyclic 实测构建 ROP 链之前最基础的一步是确定“覆盖到返回地址需要多少字节”。很多人喜欢背答案觉得这题缓冲区是 64 字节加上保存的 RBP 8 字节所以偏移是 72。这个说法对应这道题确实没错但它不是规律只是结果。编译器的栈帧布局、是否开启栈帧指针、优化级别都会改变偏移量真正靠谱的做法是用工具实测。我习惯用 pwntools 的 cyclic 生成一段有规律的长字符串把它喂给程序然后看程序崩溃时 RIP 寄存器里出现的“乱码”是哪四个字节再用cyclic -l反查偏移。比如生成 200 字节跑出崩溃地址的片段是0x6161616c那么cyclic -l 0x6161616c输出的数字就是偏移量。我在这道题上实测得到 72但这 72 只对当前这个二进制有效。换个编译选项、换个 GCC 版本结果就可能变成 80 甚至 88。所以我的建议永远是每个新题目都老老实实跑一遍 cyclic两分钟的事情不要省。3. 找 gadget工具推荐与手工核对的经验3.1 用 ROPgadget 快速筛选目标片段地址偏移确认之后接下来就是找能够控制参数的 gadget。这类工具很多我用得最多的是 ROPgadget其次是 ropper。针对这道题我只需要两类片段能把栈顶值弹进 RDI 的和能把栈顶值弹进 RSI 的。命令非常直观ROPgadget --binary ./ropchain --only pop|ret这条命令会列出所有由 pop 和 ret 组成的指令序列里面既有pop rdi; ret也可能有pop rsi; pop r15; ret这种带“多余操作”的片段。只看这个输出还不够还要注意pop rsi后面跟了什么有的二进制里找不到干净的pop rsi; ret只能找到pop rsi; pop r15; ret。后者同样能用但使用的时候有一个关键细节你需要为 R15 在栈上多留一个 8 字节的占位值否则 pop r15 会把你原本放好的“下一个函数地址”给吃掉整条链就会断掉。我在这道题里的实际选择是一个pop rdi; ret和一个带pop r15的pop rsi片段。第一次构建 payload 时我就吃过亏少塞了一个占位结果执行到一半程序跳到 0x4141414141414141 去了。这个教训后面专门讲。3.2 手工核对地址工具不是万能的工具输出的地址我强烈建议你用反汇编再核对一遍。别以为工具不会出错我自己遇过 ROPgadget 把一段数据误报成指令的情况。反汇编的姿势很简单拿到地址后用 objdump 直接看这段内存里的字节objdump -d --start-address0x400xxx --stop-address0x400xxx16 ./ropchain或者用 gdb 的x/2i 地址查看前两条指令。确认你看到的确实是pop rdi和ret而不是工具“脑补”出来的伪序列。这个习惯能帮你省下大量无效调试时间。其实从反汇编的角度你也可以完全不依赖工具直接用 objdump 搜索5f c3pop rdi; ret对应的机器码这样的字节序列。但现实是手工搜索在大文件里太费眼睛工具筛选后再人工验证效率和准确率都能兼顾。3.3 为什么地址可以直接写死PIE 关闭的红利找 gadget 的时候你会发现ROPgadget 输出的地址都是 0x400xxx 这种固定值。这正是 PIE 关闭带来的红利程序每次加载代码段都落在同一个虚拟地址上。你反汇编时看到是什么地址运行时就是什么地址payload 里直接p64(0x400xxx)写死即可。对比一下开了 PIE 的情况你就会珍惜这个条件。PIE 开启时每次运行代码段基址都会随机化你必须先通过泄露函数地址来算出基址再在远程脚本里动态计算 gadget 地址payload 不能一次写死。而这题连 PIE 都没有所以本地打通的脚本把交互对象从 process 换成 remote 基本就通吃这对新手来讲非常友好。4. 组装 ROP 链先 val 后 flag 的完整操作4.1 反汇编确认每个函数的参数需求很多人在这一步急着写 payload结果链子搭了一半发现参数不够用。我建议先把目标函数的反汇编看完搞清楚它到底要几个参数、比较的是什么常量。我在这道题反汇编里看到的逻辑可以概括成下面这张表不同镜像的具体校验值可能略有差异以你手里的文件为准函数需要的参数内部检查逻辑效果val第一个参数与 0xdeadbeef 比较相等则设置全局标志位flag第一、第二个参数先检查全局标志位再比较两个参数与 0xdeadbeef、0xc0debabe全部满足则读取并输出 flagwin无无条件执行打印提示文本无实际 flag看到这张表构建链条的思路就清晰了不能直接调 flag因为全局标志位一开始是默认值必须先调一次 val让它把标志位置成合法状态然后再调 flag并且两次调用都要给 RDI 传 0xdeadbeef给 RSI 传 0xc0debabe。这里就引出一个关键点ROP 链是严格按顺序执行的栈上的地址从低到高依次被消费。所以 payload 里必须先放 val 相关的片段再放 flag 相关的片段。顺序反了或者把 val 调用漏了flag 函数里的分支检查就会失败程序不会崩溃但它只会安静地走 else 分支你连报错都看不到。4.2 完整的 payload 构建逐段拆解假设我已经拿到了如下几个关键地址用占位符表示实际以你反汇编的结果为准pop rdi; ret 的地址POP_RDIpop rsi; pop r15; ret 的地址POP_RSIval 的地址VAL_ADDRflag 的地址FLAG_ADDR那么 payload 可以这样写from pwn import * context.binary elf ELF(./ropchain) # 本地调试 io process(./ropchain) # 远程连线时改成 # io remote(目标地址, 端口) # 占位地址务必替换成你的文件里实际 gadget 地址 POP_RDI 0x400000 POP_RSI 0x400000 VAL_ADDR elf.symbols[val] FLAG_ADDR elf.symbols[flag] # 1) 覆盖 64 字节缓冲区 8 字节 RBP payload bA * 72 # 2) 第一次调用 val设置全局标志位 payload p64(POP_RDI) payload p64(0xdeadbeef) # val 的第一个参数 payload p64(POP_RSI) payload p64(0xc0debabe) # val 的第二个参数如果 val 只需要一个参数此段可省略 payload p64(0) # 占位给 pop r15 用 payload p64(VAL_ADDR) # 3) 第二次调用 flag参数满足后输出 flag payload p64(POP_RDI) payload p64(0xdeadbeef) # flag 的第一个参数 payload p64(POP_RSI) payload p64(0xc0debabe) # flag 的第二个参数 payload p64(0) # 占位给 pop r15 用 payload p64(FLAG_ADDR) io.sendline(payload) print(io.recvall().decode())这里每一个 p64 都是 8 字节。建议你把 payload 打印出来对照栈布局数一数每个字段的位置这一步做熟了后面遇到 Ret2Libc、Ret2dl_resolve 之类的复杂链就不会发怵。需要额外强调占位符的作用。因为我用的 pop rsi 片段是pop rsi; pop r15; ret所以每用完这个 gadget后面必须紧跟一个 8 字节的“垃圾值”去填 R15。如果没有这个占位ret 会把原本属于 VAL_ADDR 或 FLAG_ADDR 的那个地址当成返回地址弹出去程序要么跑到参数值 0xdeadbeef 上去执行要么直接崩溃。这类问题排查起来特别隐蔽因为崩溃地址看起来毫无规律。4.3 栈布局速查每个 8 字节在干什么为了直观我把上面 payload 的内存布局按“栈地址从低到高、执行顺序从上到下”列出来顺序栈上的 8 字节内容作用1AAAA... (共72字节)填满缓冲区与保存的 RBP2POP_RDIvuln 的 ret 跳到此处30xdeadbeef被 pop rdi 弹入 RDI4POP_RSIret 跳转到此50xc0debabe被 pop rsi 弹入 RSI60x0000000000000000被 pop r15 弹入 R15占位7VAL_ADDR进入 val 函数8POP_RDIval 函数 ret 后进入90xdeadbeef第二次参数一10POP_RSI再次准备参数二110xc0debabe第二次参数二120x0000000000000000R15 占位13FLAG_ADDR进入 flag 函数这张表就是整个攻击链的地图。我调试时会在 gdb 里按这个顺序逐项核对栈上内容而不是一次性把整段 payload 跑完然后看结果。分段核对的方式能让你精确定位到是哪个环节出了问题比瞎猜效率高很多。4.4 跑通之后的验证与常见崩溃表现如果一切都顺利程序会先执行 val把全局标志位改掉再调 flag进入读 flag 文件的分支最终把 flag 字符串打印到终端。但我第一次写完脚本运行时程序直接卡在 printf 附近报段错误而不是正常输出。如果你也遇到这个现象先别怀疑 gadget 地址抄错了最大的嫌疑是栈对齐问题。这是 x64 下 ROP 最容易忽视的细节System V ABI 要求函数调用前栈指针按 16 字节对齐。glibc 从某个版本开始printf 这类函数内部会用到需要对齐的 SIMD 指令一旦栈没对齐程序会在库函数内部触发段错误。解决办法是在整条链的最前面额外插入一个裸retgadget。裸 ret 本身不做任何事但它会把栈指针推进 8 字节从而把对齐状态调整正确。所以我的最终 payload 里第一个跳转目标加了一个裸 ret然后才是 pop rdi。这个细节在很多入门 writeup 里不会专门讲但实际调试中几乎必踩。5. 我在实际调试中踩过的四个坑5.1 pop rsi 后面带着一个多余的 r15差点让我怀疑人生第一次用工具搜pop rsi时我把输出里的pop rsi; pop r15; ret直接当成简单版pop rsi; ret用了。结果很惨payload 执行完pop rsi后紧接着pop r15把栈顶的VAL_ADDR弹走了ret 于是跳到下一个值——也就是我精心准备的 0xdeadbeef。你能想象调试时看到 RIP 变成 0x00000000deadbeef 时的表情么。这个坑的本质是没有理解 gadget 是“按字节序列匹配”的它不一定符合你的理想形状。遇到pop rsi; pop r15; ret这种片段正确姿势是老老实实在后面补一个 8 字节占位。同样的道理适用于pop rdi; pop rbp; ret、pop rdx; pop rbx; ret这类组合 gadget。我给自己定的规矩是每用一个带多余 pop 的 gadget就在它后面数出相应数量的占位字段并在注释里标明每个字段对应的寄存器。5.2 栈对齐问题不加一个裸 ret 就是过不了 printf这个问题前面已经提到但值得单独拿出来再强调一遍因为它是 x64 ROP 里最隐蔽的一类问题。现象是payload 逻辑看着完全正确gadget 地址也没错但程序在运行到 flag 函数里调用 puts 或 printf 时突然 SIGSEGV。我用 gdb 回溯一看崩溃地址在一个 movaps 指令上而 movaps 要求内存地址按 16 字节对齐否则直接抛异常。这里附上对齐的快速判断方法在调用目标函数之前观察栈指针 RSP 的值如果它除以 16 余 8通常就需要在前面补一个裸 ret 来调整。不同工具链、不同版本的 glibc 对对齐的敏感度还不一样所以更稳妥的做法是先不加裸 ret 试跑一次如果崩溃在库函数内部且报错涉及 movaps再加。不要凭空猜直接用失败结果说话。5.3 小端序地址和参数不是你肉眼看到的样子写 ROP payload 时新手最容易犯的认知错误是用人脑的方式理解内存。0xdeadbeef 这个数在 x86 小端序下实际写入内存的字节顺序是ef be ad de 00 00 00 00。你用 gdb 看栈上的数据时看到的可能是0x00000000deadbeef也可能看到一堆反序的字节这两者其实是一回事。理解小端序不只是一个理论问题它直接影响你排查 payload 的速度。我在调试时会用x/20gx $rsp查看栈内容屏幕上按 8 字节一组显示的数值是用小端序还原后的结果看起来和源码里的 p64 参数一致但如果你用x/40bx逐字节看就会看到完全倒序的字节。如果你还在用十六进制编辑器手工构造 payload这一点尤其致命。最好的做法是从一开始就用 pwntools 的 p64 处理别自己拼字节。5.4 本机打通了远程为什么拿不到 flag本地用 process 跑通了 payload输出 flag 干净利落结果一换 remote 就各种诡异。这种情况我遇到过三次原因各不相同但基本都能归到几类远程程序的工作目录里没有 flag 文件或者 flag 文件名不一样远程系统用的是不同的 glibc 版本对栈对齐更敏感远程程序可能开启了不同的终端交互方式需要多读一次输出。最典型的坑是盲等。有些远程题目不会打印“输入 payload”这样的提示你 sendline 之后直接recvall就能拿到输出但如果你在交互时先 recv 等待提示字符串程序可能因为已经输出完毕而挂在那里你就再也等不到它的返回了。我的建议是先用recvall(timeout5)这种带超时的方式收数据收不到再分析是交互时序还是 payload 本身的问题。还有一类问题是本地不注意对齐、远程才崩溃。本地系统 glibc 版本旧printf 内部没用 movaps所以没触发远程是较新系统马上就崩。这提醒我最佳实践是无论本地跑不跑得通都保留那个裸 ret。多加 8 字节对栈布局没有负面影响却能避免一类很难排查的环境差异问题。5.5 用 gdb 逐段跟踪而不是依赖“跑一次看结果”最后算是一条方法论建议。ROP 链调试最忌讳的是一次性把整段 payload 发出去然后盯着终端看它到底是成功还是崩溃。我现在的做法是在关键跳转点打断点比如 val 函数入口、flag 函数入口用 gdb 的x/10gx $rsp看一眼当前栈顶即将被消费的地址确认和预期一致后再continue。这样做的最大好处是把“整条链黑盒”拆成了“每一跳白盒”。如果某一次跳转后的地址和预期不符立刻就能定位到是哪个 gadget 的偏移算错了还是哪个占位少写了。这个习惯一开始会显得慢但多调几条链之后你会发现它比蒙头试错要快得多。尤其当你从这种入门题转向更复杂的题目时ROP 链动辄几十个字段没有逐段跟踪的能力几乎没法干活。最后再分享一点个人体会这道题我前后写了三个版本才跑通第一版栽在忘记补 R15 占位第二版栽在栈对齐第三版才把所有细节理顺。回过头看最大的收获不是记住了几个 gadget 地址而是学会了把每 8 字节拆开理解哪个是弹参数用的哪个是占位哪个是真正要跳的函数。这个“逐字段拆解”的习惯后来看更复杂的 ROP 链时也一直在用。如果你现在正卡在类似的问题上我的建议是不要急着把网上 writeup 里的 payload 抄过来跑。打开 gdb对着栈一个字段一个字段地走等你能闭着眼说出“这个字段会被 pop 进哪个寄存器、下一个 ret 会跳到哪”你就真正掌握了这一题也能顺带掌握 x64 ROP 最核心的思维方法。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号