ACTF2023
# PREFACE:强度巨高在打,但是赛中算没出题 hhhh5555555,复盘以及复现一下
# tree
# 赛中最接近出的题
直接打开看行为可以发现它不解析 #include
头文件,会给你编译一个 cpp 代码
初步观察可以看到三个 check,全过可以从 server 返回 flag
查找字符串,一开始以为是模仿的 gcc 编译器,但是问学长说不像(这里就缺乏经验了已经,问的时候学长就说先看是啥的编译器,自己调了很久也没有去想)字符串发现是 clang
源码非常复杂,而且有很多 handler 的结构,导致函数调用往往是跳转表,这里一定得恢复符号(后面 check3 不恢复符号几乎没办法分析,相反能恢复符号就好做)
check1 有一个 == 25
,这里其实很容易测试,这里会计算符号的优先级,直接从最高等级的 * /
视到低等级的比较条件运算符,等级越低数字越大,然后就测到 += 1
可以让 check1 + 1,其它会减一,所以最后的结构里面 += 1
比其它的多一即可
其实是这个
然后有这个函数(这里已经恢复了符号表):
这里会捕捉一个名为 AAA
的 class
我们要让我们的结构通过这里的 check:
(这部分都是当时猜的,然后也没有去恢复符号表了)
然后多虚函数继承就过了这里的 check2
下午四点就过了 check2,本以为形势一片大好,结果游戏才刚刚开始,check3 做到早上四点,没救
小折腾了一下,直接搞了个 clang15.0.4 给 bindiff 进去,但这里当时是看不到的,而且直观上会往这里面看
总之这里一直边调边猜(现在看看这个凌晨两点的记录有些消愁了,但是确实调了几个小时没进展,各种玩意也在反复试)
晚上的时候放出 hint:需要看一下 ast_matcher
相关,然后我们一直在找已有的符号以及字符串
不料其实不是这样,应该自己搞一个 ast_matcher
的 api 调用,再 bindiff 进去,就好看了
这部分已经是赛后做的了(小问了一下出题人)
自己把 clang 安了,然后编译一个这个(这个会匹配 for(int i=1;i<2;i++){}
)
#include "clang/ASTMatchers/ASTMatchers.h" | |
#include "clang/ASTMatchers/ASTMatchFinder.h" | |
#include "clang/Tooling/Tooling.h" | |
#include "clang/Tooling/CommonOptionsParser.h" | |
using namespace clang; | |
using namespace clang::ast_matchers; | |
using namespace clang::tooling; | |
static llvm::cl::OptionCategory MyToolCategory("my-tool options"); | |
class LoopPrinter : public MatchFinder::MatchCallback { | |
public: | |
virtual void run(const MatchFinder::MatchResult &Result) { | |
if (const ForStmt *FS = Result.Nodes.getNodeAs<clang::ForStmt>("forLoop")) { | |
FS->dump(); | |
} | |
} | |
}; | |
int main(int argc, const char **argv) { | |
auto ExpectedParser = CommonOptionsParser::create(argc, argv, MyToolCategory); | |
if (!ExpectedParser) { | |
llvm::errs() << ExpectedParser.takeError(); // Print any errors. | |
return 1; | |
} | |
CommonOptionsParser& OptionsParser = ExpectedParser.get(); | |
if (argc < 2) { | |
llvm::errs() << "Usage: " << argv[0] << " <C++ source file>\n"; | |
return 1; | |
} | |
auto Matcher = | |
forStmt( | |
hasLoopInit(declStmt( | |
hasSingleDecl(varDecl( | |
hasInitializer(integerLiteral(equals(1))) | |
)) | |
)), | |
hasCondition(binaryOperator( | |
hasOperatorName("<"), | |
hasLHS(ignoringParenImpCasts(declRefExpr( | |
to(varDecl(hasType(isInteger()))) | |
))), | |
hasRHS(integerLiteral(equals(2))) | |
)), | |
hasIncrement(unaryOperator( | |
hasOperatorName("++"), | |
hasUnaryOperand(ignoringParenImpCasts(declRefExpr( | |
to(varDecl(hasType(isInteger()))) | |
))) | |
)) | |
).bind("forloop"); | |
LoopPrinter Printer; | |
MatchFinder Finder; | |
Finder.addMatcher(Matcher, &Printer); | |
ClangTool Tool(OptionsParser.getCompilations(), OptionsParser.getSourcePathList()); | |
return Tool.run(newFrontendActionFactory(&Finder).get()); | |
} | |
// clang++ -g test_tree.cpp -o loop_printer -I/usr/lib/llvm-14/include -L/usr/lib/llvm-14/lib -lclangTooling -lclangASTMatchers -lclangFrontend -lclangSerialization -lclangDriver -lclangParse -lclangSema -lclangAnalysis -lclangEdit -lclangAST -lclangLex -lclangBasic -lLLVM-14 -std=c++14 -pthread -ldl |
然后 bindiff 进去,结果:
对照自己编译出来的 loop_printer,就好搞多了
<
和 ++
的匹配点比较明显
这里一开始一直找不到比较的数字,然后发现会写在栈上:
前面的 ++
和 <
比较容易识别,就是这里的数字有点难找,然后注意这里的 "1" 和 "2" 和 "f" 是一样的,只是一个符号,相当于
没有实际意义
匹配上 check3 以后,把 check3=5 构造一下,把 check1 抵消一下,得到最后的 payload:
class d{ | |
public: | |
int AAA(){ | |
} | |
}; | |
class c : virtual public d{ | |
}; | |
class b : virtual public d{ | |
}; | |
namespace AAA{ | |
namespace AAA{ | |
struct AAA: virtual public b, virtual public c{ | |
public: | |
AAA() { | |
for (int a = 1 ;a < 10; a++){ | |
a += 1; | |
a += 1; | |
} | |
for (int a = 2 ;a < 10; a++){ | |
a += 1; | |
} | |
for (int a = 0 ;a < 10; a++){ | |
a += 1; | |
} | |
for (int a = 0 ;a < 10; a++){ | |
a += 1; | |
} | |
for (int a = 0 ;a < 10; a++){ | |
a += 1; | |
} | |
} | |
}; | |
} | |
} |
发给服务器远程也是可以通的(没截图),没问题了
这题有一个很麻烦的点,第三个 check 必须恢复符号表,否则根本看不出来它是匹配的符号还是自定义的符号名,前两个 check 中间过程比较清晰,好猜些,第三个的中间比对过程相当难看与抽象,甚至最后一个数字我还找了好一会