
L3HCTF 2025 - Library

Are you a cat?? Writing everywhere?
文件属性
属性 | 值 |
---|---|
Arch | amd64 |
RELRO | Partial |
Canary | off |
NX | on |
PIE | off |
strip | yes |
libc | 2.39-0ubuntu8.4 |
解题思路
背景知识
刚拿到binary的时候就知道不对,有440K,显然已经超过了常规题目的大小。查看编译器信息,
有gcc 8.3.0、clang等,搜索kexe发现,这是一个 Kotlin/Native 生成的ELF。
但是剥了符号,要怎么逆呢?我们可以手动下载 kotlin-native 工具链,
然后写一个简单的kt,并生成一个ELF。通过对比题目与我们的binary,可以发现由于激进的优化策略,
EnterFrame
等函数都被内联了,导致题目的kt::main
函数特别特别大。找到真正的main函数以后,
我们就可以开始理解程序逻辑了。
有师傅可能想在程序里寻找字符串,结果发现一个都找不到,这是因为Kotlin是一个基于JVM的上层语言, 它的字符串和Java的字符串一样,是用 UTF-16 编码的,在输出到终端时,再转换回UTF-8, 因此为了搜索字符串,必须用UTF-16来寻找。
在kotlin中,处理一个对象是从Object Header开始的,然后对象的第一个QWORD会存放一个Object
Header的指针,然后再存放具体的数据。从我们之前生成的带调试符号的binary中找,
他们大多是0x90
字节大小。由于我没有找到相关的资料,下图是我对结构的解释,
要写exp也不需要了解更多,如果有人从源码里找到更多信息也可以发到评论区。
在运行时我们能发现在内存布局中有大小为0x800000
的rw段和一个0x100000
的rw段,
前者是两块由pthread开的线程栈—— GC Timer Thread 和 GC Timer Thread ;
后者是kotlin用来存放对象的段,我称其为 jvm heap。
给调试加点符号
手动调试这么一个大型的无符号ELF还是过于困难了,不过我恰好在翻LIEF的文档时, 找到了能添加符号的函数。在变调试边探索的过程中,我推测出了很多符号, 可以使用以下脚本,将符号加入到原来的binary中,这样我们在使用gdb调试时, 就可以直接使用这些符号了,而不是只能对着一系列抽象的地址做操作。
1 | #!/bin/python |
将函数符号的大小设置为0,gdb就会认为它是一个“标签”,它没有范围。除此之外,
你还可以看到LIEF可以用来设置解释器,添加RUNPATH,可以说,它完全可以替代patchelf
。
什么,你还不知道LIEF是什么?LIEF是一个解析二进制文件的库,由C++编写, 未来pwntools将使用它来解析二进制文件。我之后也会考虑单独出一期博客讲讲这个好用的库。
具体分析
找到kt_main
函数后,可以看到在菜单打印之后有一个switch case,不难发现在default的逻辑里,
藏了一个额外的case 114514: magic cat 戳啦,木猫嘛,在patch完elf后通过g
可以打印栈地址,
通过p
可以往那一块写16字节。在调试完所有的功能以后,可以发现,
有一个org.l3hsec.ctf2025.helloKotlin.Library
的类对象一直在被访问,里面有2个ArrayList
,
以及2个栈上的地址(一个是触发magic cat以后才会出现的)。
其中ArrayList的结构大概是这样的:
1 | struct String { |
我们可以总结一下功能: 1: borrow 可以将一个String
添加到ArrayList
当中;
2: return 输入String d
,foreach ArrayList
,其中的字符串包含d
子串则从ArrayList
中删除;
3: read 第一次输入没啥用,不会用到的,第二次输入一个偏移offset
,
然后触发read(0, &ArrayList.array + offset * 8, 16)
,主要的利用点也是在这里;
4: watch 基本上就是输入一个电影名,然后做了一下拼接,没啥用;5: play 6: exercise
7: listen 都没啥用,只是打印了内置的字符串; 8: study
会调用ArrayList.toString()
打印列表中的内容,如果我们能控制ArrayList.array
,
就可以用来泄露信息; 9: leave 返回。
由于对象都是分配在jvm heap中且相对距离一般是固定的,并且可以泄露一个栈地址,
因此我们可以在那一块伪造一个String
,然后将其放入ArrayList.array
中,
在使用 study 打印其中的内容。由于我们能控制栈上的16字节,因此可以把size
伪造得很大,
这样就可以在打印时泄露大量内容。
当打印字符串时,会把String.str
先从UTF-16 cast 到UTF-8,为此,我们从中获取输出时,
需要先用UTF-8解码,再用UTF-16编码才能获取原始内容。
到了这一步以后就很简单了,由于栈离jvm heap实在太远,索引超过了32位的限制,
输入偏移的时候会被kt_toint
返回NULL
而导致程序退出,因此选择打IO。
在jvm heap上反复利用 read 构造一个fake file,然后正常退出执行FSOP拿shell。
有师傅通过 watch 可以将输入的字符串放到栈上,但是我这里不行...
hkbin 师傅的思路是通过修改Library
中关于magic cat的使用计数和指针,
从而达到任意读写的目的,也挺巧妙的。
EXPLOIT
1 | from pwn import * |

参考
- 标题: L3HCTF 2025 - Library
- 作者: RocketDev
- 创建于 : 2025-07-18 23:19:00
- 更新于 : 2025-07-18 23:32:00
- 链接: https://rocketma.dev/2025/07/18/library/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。