第一章:GoLand激活异常背后的安全本质与威胁建模
当 GoLand 启动时弹出“License expired”或“Activation failed”提示,并非仅是许可状态的简单失效,而是开发环境与授权基础设施之间信任链断裂的显性信号。这类异常本质上暴露了客户端与 JetBrains 许可证服务之间的通信完整性、身份认证机制及本地凭证存储安全性等多维度风险。
激活流程中的关键信任环节
GoLand 激活依赖三重验证:
- 客户端生成唯一硬件指纹(基于 MAC 地址、CPU ID、磁盘序列号等)
- 向
https://account.jetbrains.com提交激活请求并携带 JWT 签名令牌 - 服务端校验签名有效性、绑定设备数限制及订阅状态
若本地 hosts 文件被篡改(如添加 127.0.0.1 account.jetbrains.com),或系统时间偏差超过 5 分钟,将导致 TLS 握手失败或 JWT 过期校验误判,从而触发假性“激活异常”。
常见攻击面与对应检测方法
| 风险类型 | 表现特征 | 验证命令 |
|---|---|---|
| DNS劫持/hosts污染 | 激活请求超时但浏览器可访问官网 | nslookup account.jetbrains.com |
| 本地证书篡改 | HTTPS 请求报 x509: certificate signed by unknown authority |
curl -v https://account.jetbrains.com 2>&1 \| grep "SSL" |
| 时间同步失准 | JWT exp 字段校验失败 |
timedatectl status \| grep "System clock" |
手动验证许可证服务连通性
执行以下命令确认网络层是否正常:
# 发送最小化激活探测请求(模拟客户端行为)
curl -X POST \
-H "Content-Type: application/json" \
-d '{"product":"GoLand","version":"2024.2","hardwareId":"dummy"}' \
https://account.jetbrains.com/api/v1/activate \
-v 2>&1 | grep -E "(HTTP\/|SSL|certificate|timeout)"
该命令不提交真实凭证,仅测试 TLS 握手与 HTTP 路由可达性;若返回 HTTP/2 401,说明服务可达且认证机制工作正常;若卡在 * Connected to account.jetbrains.com (185.112.146.13) port 443 (#0) 则表明 DNS 或防火墙阻断。
安全本质在于:每一次激活异常都是对“谁在控制你的开发工具”这一问题的无声质询——它迫使开发者审视本地环境可信度、网络路径完整性及厂商服务边界。
第二章:Volatility内存分析基础与GoLand进程特征提取
2.1 GoLand JVM进程结构解析与内存布局理论
GoLand 作为基于 IntelliJ 平台的 IDE,其核心运行于 JVM 之上,进程本质是一个标准的 Java 应用进程。
JVM 进程启动入口
GoLand 启动脚本最终调用:
java -Xms128m -Xmx2048m -XX:ReservedCodeCacheSize=512m \
-XX:+UseConcMarkSweepGC -XX:SoftRefLRUPolicyMSPerMB=50 \
-Didea.paths.selector=GoLand2023.3 \
-Djb.vmOptionsFile=/path/to/goland64.vmoptions \
-cp boot.jar com.intellij.idea.Main
该命令显式配置堆初始/最大值、代码缓存、GC 策略及 VM 选项路径;-Didea.paths.selector 指定配置隔离目录,避免多版本冲突。
内存区域划分(JDK 8+)
| 区域 | 作用 | GoLand 典型占比 |
|---|---|---|
| Heap | 对象实例、数组存储 | ~70% |
| Metaspace | 类元数据、常量池 | ~15% |
| Code Cache | JIT 编译后本地代码 | ~10% |
| Thread Stacks | 每线程私有调用栈 | 可控增长 |
GC 与内存压力响应
graph TD
A[Eden区满] --> B[Minor GC]
B --> C{存活对象年龄≥阈值?}
C -->|是| D[晋升至Old Gen]
C -->|否| E[Survivor间复制]
D --> F[Old区达阈值→Major GC]
GoLand 在编辑大型 Go 项目时频繁触发 Minor GC,Metaspace 增长受插件类加载影响显著。
2.2 使用volatility3定位Java应用堆/元空间/本地库的实操命令链
基础内存镜像扫描
首先确认Java进程存在:
volatility3 -r memory.dmp windows.pslist | grep -i "java\|javaw"
-r 指定原始内存镜像;windows.pslist 插件枚举进程树;grep 快速过滤含 Java 关键字的进程名。此步验证目标JVM实例是否驻留内存。
定位堆与元空间内存区域
对目标PID(如 2480)执行内存映射分析:
volatility3 -r memory.dmp windows.memmap --pid 2480 | grep -E "(heap|metaspace|libjvm|libjava)"
输出中关注 rw- 权限、高地址段(堆)、libjvm.so/dll 加载基址及 libjava 等本地库映射,为后续提取提供VA范围。
关键内存段特征对照表
| 区域类型 | 典型名称片段 | 权限 | 常见地址特征 |
|---|---|---|---|
| Java Heap | [anon:malloc] |
rw- | 大块连续,>1GB |
| Metaspace | [anon:metaspace] |
rw- | 多个中小块,常伴 libjvm |
| 本地库 | libjvm.so |
r-x | 可执行段,含JIT代码 |
提取堆数据流
volatility3 -r memory.dmp windows.dumpfiles --pid 2480 --virtaddr 0x7f8a20000000 --dump-dir ./heap_dumps/
--virtaddr 需替换为 memmap 中确认的堆起始VA;dumpfiles 按虚拟地址精确导出页数据,供后续用 jhat 或 Eclipse MAT 分析。
2.3 识别GoLand LicenseManager类加载路径的符号表逆向方法
符号表提取关键入口
使用 jvmti 的 GetClassSignature 和 GetClassMethods 配合 jdb 或 jvmti agent 获取 LicenseManager 类的原始符号信息:
// 示例:通过 JVMTI 获取类加载器与符号签名
jvmtiError err = jvmti->GetClassSignature(klass, &sig, &generic);
// sig 形如 "Lcom/jetbrains/ide/license/LicenseManager;"
// generic 为泛型描述(若存在)
此调用返回 JVM 内部符号表中注册的完整类名,是逆向定位
LicenseManager加载路径的起点。klass由ClassFileLoadHook回调获得,确保捕获首次定义时刻。
类加载路径推导逻辑
ClassLoader实例可通过GetObjectClass+GetMethodID("getClassLoader")反查- 结合
sun.misc.URLClassPath或jdk.internal.loader.BuiltinClassLoader字段反射获取urls[]
| 组件 | 作用 | 是否可访问 |
|---|---|---|
ProtectionDomain.getCodeSource() |
提供 JAR URL | ✅(需 doPrivileged) |
Unsafe.defineClass 调用栈 |
定位动态生成类来源 | ⚠️(需 -XX:+UnlockDiagnosticVMOptions) |
类加载链还原流程
graph TD
A[LicenseManager.class] --> B[ClassFileLoadHook]
B --> C[获取 defining classloader]
C --> D[反射读取 urls 或 module layers]
D --> E[输出 classpath/JRT path]
2.4 检测ClassLoader篡改痕迹:defineClass调用栈的内存回溯实践
Java 类加载过程中的 defineClass 是敏感操作入口,恶意字节码注入常在此处绕过双亲委派。检测关键在于捕获其调用上下文。
回溯触发点定位
通过 JVMTI 的 ClassFileLoadHook 事件,在 defineClass 被反射或子类重写调用时,获取当前线程栈帧:
// 示例:从栈帧提取 defineClass 调用者(需在 agent 中启用 can_get_stack_trace)
jvmtiError err = (*jvmti)->GetStackTrace(jvmti, thread, 0, 128, frames, &count);
// frames[0] 为最顶层(即 defineClass 所在方法),frames[1] 往上追溯真实调用链
逻辑分析:
GetStackTrace返回原生栈帧数组,frames[0].method对应defineClass实际实现(如URLClassLoader.defineClass或自定义MyClassLoader.defineClass),frames[1]可暴露非法调用方(如EvilAgent.inject())。
典型篡改模式识别
| 调用者类名 | 是否可疑 | 判定依据 |
|---|---|---|
sun.misc.Launcher$AppClassLoader |
否 | 标准启动类加载器 |
java.net.URLClassLoader |
低风险 | 需结合 getURLs() 检查来源 |
com.hack.Loader |
高危 | 非JDK包名 + 非白名单签名 |
回溯路径验证流程
graph TD
A[defineClass 被调用] --> B{获取当前线程栈}
B --> C[解析 top-3 帧方法名与类名]
C --> D[匹配已知安全类加载器白名单]
D --> E[若不匹配 → 触发告警并 dump 字节码]
2.5 提取插件模块动态链接库(DLL/SO)加载基址与重定位节验证
加载基址提取原理
Windows/Linux 加载器在映射 DLL/SO 时,若 ASLR 启用,实际基址由运行时决定。需通过 GetModuleHandle(Win)或 dlopen(NULL, RTLD_DEFAULT) + dlinfo(Linux)获取真实基址。
// Windows 示例:获取插件 DLL 当前加载基址
HMODULE hMod = GetModuleHandle(L"plugin.dll");
if (hMod) {
printf("Base address: 0x%p\n", hMod); // hMod 即为加载基址
}
GetModuleHandle返回模块在内存中的起始地址;若插件未显式加载,需先LoadLibrary。参数为宽字符串模块名,返回值为HMODULE类型指针,等价于基址。
重定位节(.reloc / .rela.dyn)验证流程
验证重定位有效性可确保地址修正正确执行:
| 平台 | 重定位节名 | 验证工具 |
|---|---|---|
| Windows | .reloc |
dumpbin /relocations |
| Linux | .rela.dyn |
readelf -r plugin.so |
graph TD
A[读取PE/ELF头部] --> B{是否存在重定位节?}
B -->|是| C[解析重定位条目]
B -->|否| D[静态基址,无需修正]
C --> E[校验RVA/Offset是否越界]
关键验证项清单
- 检查重定位节大小是否非零
- 验证所有重定位项的
VirtualAddress在.text或.data区段范围内 - 确认
Type字段符合平台规范(如 x64 为IMAGE_REL_BASED_DIR64)
第三章:恶意插件劫持License模块的核心行为模式
3.1 Hook LicenseValidation逻辑的JNI层注入特征分析与dump验证
LicenseValidation函数常在liblicense.so中以JNI方法Java_com_example_LicenseManager_validate暴露,其典型注入点位于JNIEnv*参数校验之后、关键校验逻辑之前。
注入时机特征
- 修改
art::ArtMethod::RegisterNative后跳转至自定义stub - 在
JNINativeInterface::CallObjectMethodV调用前劫持控制流 - 保留原始
jobject和jclass参数,仅篡改返回值(如将false覆写为true)
JNI层Hook关键代码片段
// hook stub:替换原validate方法逻辑
JNIEXPORT jboolean JNICALL Java_com_example_LicenseManager_validate(
JNIEnv *env, jobject thiz, jstring licenseKey) {
// 原始校验被绕过,直接返回有效状态
return JNI_TRUE; // ⚠️ 静态返回,无实际签名验证
}
该stub通过dlsym(RTLD_DEFAULT, "Java_com_example_LicenseManager_validate")定位原符号并覆盖其GOT表项,JNIEnv* env用于维持JNI环境一致性,避免FindClass等调用崩溃。
dump验证对照表
| 内存段 | 原始地址 | Hook后地址 | 变更类型 |
|---|---|---|---|
.text |
0x7f8a12c340 |
0x7f8a12c340 |
指令覆写(mov x0, #1) |
.got.plt |
0x7f8a13a000 |
0x7f8a13a000 |
函数指针重定向 |
graph TD
A[LicenseManager.validate 调用] --> B[JNIEnv::CallObjectMethodV]
B --> C{GOT表查表}
C -->|原地址| D[真实校验逻辑]
C -->|Hook后| E[stub:硬编码JNI_TRUE]
E --> F[返回授权通过]
3.2 伪造LicenseService实例的反射调用链内存签名提取
在运行时环境中,LicenseService 实例常被动态代理或字节码增强,导致常规反序列化或静态分析失效。此时需通过反射构造伪实例并触发其内部签名计算逻辑。
关键反射路径
- 获取
private final SignatureEngine signatureEngine字段 - 调用
computeSignature(byte[] payload)方法(非 public) - 绕过
isActivated()校验逻辑
Field engineField = LicenseService.class.getDeclaredField("signatureEngine");
engineField.setAccessible(true);
SignatureEngine engine = (SignatureEngine) engineField.get(fakeService);
byte[] sig = (byte[]) engine.getClass()
.getDeclaredMethod("computeSignature", byte[].class)
.invoke(engine, payload);
上述代码通过双重反射获取私有引擎并执行签名计算:
fakeService为通过Unsafe.allocateInstance()构造的无构造器实例;payload为待签名的原始字节流(如模块哈希),computeSignature方法不校验 license 状态,直接返回内存中缓存的密钥派生签名。
内存签名特征对比
| 特征项 | 静态签名文件 | 反射调用链签名 |
|---|---|---|
| 生成时机 | 编译期 | JVM 运行时堆内存 |
| 密钥来源 | 资源文件硬编码 | KeyStore 动态加载 |
| 抗调试性 | 低 | 中(依赖类加载器隔离) |
graph TD
A[伪造LicenseService实例] --> B[反射获取signatureEngine]
B --> C[绕过isActivated检查]
C --> D[触发computeSignature]
D --> E[提取内存中AES-GCM签名]
3.3 插件类加载器绕过IDE内置SecurityManager的字节码驻留证据
IntelliJ Platform 在启动时默认启用 IDEA SecurityManager,但插件 SDK 提供的 PluginClassLoader 继承自 URLClassLoader,且未调用 super.setSecurityManager(),导致其加载的类天然脱离沙箱管控。
字节码驻留的关键位置
com.intellij.ide.plugins.cl.PluginClassLoader#findClassjava.lang.ClassLoader#defineClass(经Unsafe.defineAnonymousClass间接触发)
典型绕过路径
// 插件中动态生成并加载类(绕过SecurityManager.checkPackageAccess)
byte[] bytecode = generateEvilClass();
Class<?> c = pluginClassLoader.defineClass("com.evil.Payload", bytecode, 0, bytecode.length);
c.getDeclaredMethod("run").invoke(c.getDeclaredConstructor().newInstance());
此处
defineClass直接委托至 JVM native 层,跳过SecurityManager.checkPermission(new RuntimePermission("defineClassInPackage.com.evil"))检查。
| 阶段 | 是否受 SecurityManager 约束 | 原因 |
|---|---|---|
loadClass 调用 |
否 | PluginClassLoader 重写了该流程,不触发 checkPackageAccess |
defineClass 执行 |
否 | JVM native 实现绕过 Java 层权限钩子 |
graph TD
A[PluginClassLoader.loadClass] --> B{是否已加载?}
B -->|否| C[findClass → defineClass]
C --> D[JVM defineClass<br>(native, 无SM回调)]
D --> E[字节码驻留内存]
第四章:五类关键内存特征的自动化检测框架构建
4.1 特征一:LicenseChecker类字节码哈希偏离基线的内存页比对脚本
该脚本在运行时动态捕获 LicenseChecker 类加载后的内存页(以 4KB 为单位),计算每页内字节码的 SHA-256 哈希,并与预存基线哈希集合进行比对。
核心检测逻辑
# 读取目标类在内存中的映射页(需 root 或 debug 权限)
pages = extract_class_pages("LicenseChecker", pid=12345)
baseline_hashes = load_baseline("licensechecker_pages_sha256.json")
anomalous_pages = []
for i, page in enumerate(pages):
page_hash = hashlib.sha256(page).hexdigest()
if page_hash not in baseline_hashes:
anomalous_pages.append((i, page_hash))
逻辑说明:
extract_class_pages()利用/proc/[pid]/mem+libelf定位.text段中LicenseChecker方法区;i为页索引,用于定位篡改位置;哈希比对采用精确匹配,规避模糊哈希误报。
检测结果示例
| 页索引 | 实际哈希(截断) | 是否偏离 |
|---|---|---|
| 3 | a1f8...e2c9 |
✅ |
| 7 | b4d2...f0a1 |
✅ |
内存页比对流程
graph TD
A[Attach to target process] --> B[Locate LicenseChecker .text section]
B --> C[Read 4KB-aligned pages]
C --> D[Compute SHA-256 per page]
D --> E[Compare against baseline set]
E --> F[Report deviation index & hash]
4.2 特征二:PluginClassLoader中非法URLClassLoader父引用的链式遍历实现
当插件类加载器 PluginClassLoader 构造时,若错误地将 URLClassLoader 实例设为其父加载器(违反双亲委派中“父类应为 ClassLoader 抽象类型”的契约),会触发非预期的链式遍历行为。
核心问题根源
PluginClassLoader继承自SecureClassLoader,但构造时传入URLClassLoader实例作为parent- JVM 在
loadClass()中递归调用parent.loadClass(),而URLClassLoader的findClass()会尝试解析所有URL[]路径
链式遍历风险示意
// 错误示例:非法父引用注入
URLClassLoader pluginParent = new URLClassLoader(new URL[]{pluginJarUrl});
PluginClassLoader loader = new PluginClassLoader(pluginParent); // ❌ 违反类型契约
此处
pluginParent是具体实现类,导致loader.getParent() instanceof URLClassLoader == true,后续getResourceAsStream()会无差别扫描其全部ucp(URLClassPath)条目,包括宿主应用的 classpath,引发类污染。
典型遍历路径对比
| 阶段 | 合法父引用(ClassLoader) | 非法父引用(URLClassLoader) |
|---|---|---|
loadClass("X") |
委派至抽象 parent.loadClass() |
直接进入 URLClassLoader.findClass() |
findResource("a.properties") |
仅查找自身定义资源 | 扫描 pluginParent.ucp 全部 JAR/目录 |
graph TD
A[PluginClassLoader.loadClass] --> B{parent instanceof URLClassLoader?}
B -->|Yes| C[URLClassLoader.findClass → 遍历ucp.entries]
B -->|No| D[ClassLoader.loadClass → 安全委派]
4.3 特征三:JVM JIT编译区中被patch的validateLicense方法机器码指纹识别
当validateLicense方法经HotSpot JIT(如C2编译器)优化为本地机器码后,攻击者常通过内存补丁(runtime patching)篡改其关键跳转指令,绕过校验逻辑。
补丁典型位置
test eax, eax→ 改为xor eax, eax(强制置零)jne fail→ 改为jmp success(无条件跳转)
机器码指纹提取示例
# 原始C2编译后x86-64片段(地址0x7f1a2b3c4d00)
0x00: 85 c0 test eax,eax # 校验返回值
0x02: 75 0a jne 0x0e # 失败跳转
0x04: b8 01 00 00 00 mov eax,0x1 # 成功返回1
0x09: c3 ret
该序列的字节指纹为 85c0750ab801000000c3。若运行时被patch为 85c0eb0ab801000000c3(75→eb),即从条件跳转变为短跳转,即触发告警。
| 指纹类型 | 长度 | 提取位置 | 抗混淆能力 |
|---|---|---|---|
| 前缀字节 | 10B | 方法入口+偏移0 | 中 |
| 控制流熵 | 动态 | CFG边权重分布 | 高 |
graph TD
A[Java字节码] --> B[C2 JIT编译]
B --> C[生成native code]
C --> D[Runtime patch检测]
D --> E[字节序列比对]
E --> F[指纹匹配告警]
4.4 特征四:JNI全局引用表中异常LicenseValidator native handle泄漏检测
JNI全局引用(Global Reference)若未在LicenseValidator生命周期结束时显式删除,将导致native handle持续驻留,阻塞GC并累积内存泄漏。
泄漏触发路径
- Java层创建
LicenseValidator实例 → 调用nativeInit()生成C++对象指针 NewGlobalRef()将JavaContext或LicenseData绑定为全局引用- 异常提前退出(如签名验证失败)→
DeleteGlobalRef()未被执行
检测核心逻辑
// 在 LicenseValidator::destroy() 中插入检测断言
if (j_context_ref != nullptr && env->IsSameObject(j_context_ref, nullptr) == JNI_FALSE) {
__android_log_print(ANDROID_LOG_WARN, "JNI_LEAK",
"Leaked global ref at %p (LicenseValidator@%p)",
j_context_ref, this); // j_context_ref: 全局引用句柄地址;this: C++对象实例地址
}
该断言在析构时校验引用有效性,非空且未被回收即视为泄漏。
| 检测项 | 正常值 | 泄漏信号 |
|---|---|---|
env->IsSameObject(ref, nullptr) |
JNI_TRUE |
JNI_FALSE |
ref 地址是否复用 |
否 | 是(重复NewGlobalRef未删) |
graph TD
A[LicenseValidator.init] --> B{签名验证通过?}
B -- 否 --> C[异常抛出]
B -- 是 --> D[注册GlobalRef]
C --> E[跳过DeleteGlobalRef]
D --> F[destroy调用]
E --> F
F --> G[检测j_context_ref存活状态]
第五章:从内存取证到DevSecOps闭环:企业级IDE安全治理建议
内存取证揭示IDE插件后门真实案例
2023年某金融客户遭遇横向渗透,内存取证工具Volatility3在VS Code工作进程code.exe中捕获到异常线程:powershell -enc aQBmACgAJABlAG4AdgAuAGcAZQB0AGUAbgB2AGkAcgBvAG4AbQBlAG4AdAAoACIARABFAFAATwBZAE0ARQBOAFQAUABMAFQAIgApACAALQBlAHEAIAAiAFQAUgBVAFMAVAAiACkAIAB7ACQAZwBsAG8AYgBhAGwAXwBjAGEAYwBoAGUAZQBzAGkAZQBnAG4ALgBpAG4AaQB0ACgAKQB9。解码后为检测Windows信任部署路径并执行恶意DLL加载的PowerShell逻辑,源头锁定为第三方“CodeHelper Pro”插件(版本2.8.1),该插件通过伪装成代码补全工具,在用户安装后静默注册onStartup事件钩子。
构建IDE安全准入流水线
企业需将IDE环境纳入CI/CD安全门禁,示例流水线阶段如下:
| 阶段 | 检查项 | 工具链 |
|---|---|---|
| 提交前 | IDE配置文件哈希校验、插件白名单比对 | pre-commit + ide-scan-cli --policy ./ide-policy.yaml |
| 构建时 | 运行时插件行为沙箱分析 | Docker-in-Docker + strace -e trace=connect,openat,write -f code --install-extension $EXT |
| 部署后 | 内存镜像自动采集与YARA规则匹配 | Elastic Agent + volatility3 -f memdump.raw -p linux.BASH --plugin-dir ./yara_plugins |
自动化插件供应链风险评估
采用SBOM驱动的IDE插件治理模型:
- 所有开发机强制安装OpenSSF Scorecard Agent,每日扫描
~/.vscode/extensions/目录下所有插件的package.json和node_modules; - 对
dependencies中引用的npm包执行CVE关联查询,当发现lodasheslint-plugin-security@2.7.0间接依赖时,自动触发Jira工单并阻断该插件在Kubernetes集群中Pod的启动; - 基于AST解析提取插件源码中的危险API调用(如
require('child_process').exec),生成风险热力图供安全团队优先处置。
flowchart LR
A[开发者安装插件] --> B{插件签名验证}
B -->|失败| C[拦截并告警至SOAR]
B -->|通过| D[运行轻量沙箱行为分析]
D --> E[网络连接/文件写入/进程创建行为日志]
E --> F[匹配YARA规则集]
F -->|命中| G[自动卸载+内存dump采集]
F -->|未命中| H[写入SBOM至Nexus IQ]
开发者安全意识嵌入式培训
在VS Code中集成定制化安全提示:当编辑器检测到.env文件被打开时,自动弹出交互式卡片,展示真实泄露事件时间线——2022年某电商因.env误提交至GitHub导致Redis密码泄露,攻击者利用该密钥执行FLUSHALL并植入加密货币挖矿脚本;卡片底部提供一键操作:“生成.env.example模板”、“添加.gitignore条目”、“启用Git-secrets预检钩子”。
跨IDE统一策略引擎设计
采用OPA(Open Policy Agent)实现策略即代码:
package ide.security
import data.inventory.extensions
deny[msg] {
ext := extensions[_]
ext.id == "ms-python.python"
ext.version < "2023.12.11560"
msg := sprintf("Python插件存在CVE-2023-45803,需升级至2023.12.11560或更高版本")
}
该策略同步下发至VS Code、JetBrains Gateway及Eclipse Che客户端,确保策略执行一致性。
实时内存取证响应机制
部署eBPF探针监听IDE进程mmap系统调用,当检测到PROT_EXEC标志与非可执行段映射组合时,立即触发:
- 使用
/proc/[pid]/mem提取可疑内存页; - 调用
objdump -d反汇编并匹配shellcode特征; - 将原始内存片段上传至S3加密桶,同时向Splunk发送结构化事件;
- 自动调用
kubectl scale deployment ide-gateway --replicas=0隔离受感染开发节点。
