看雪秋季赛第2题出题思路
by Fpc
--------------------------------------
首先,第2题完赛了,近1万的关注度,99人过关,从防守方的角度看是很理想的,有参与,而且付出努力(通篇看代码)后可解,努力就可能有收获。
ctf防守方有多种选择,比如之前比赛有强度很大的壳,无人攻破,是一种思路;或者明码比较,这也是一种思路。年中赛时,有的题令人印象深刻,爱琴海vb pcode加了smc改关键运算代码,readyu数论或密码学应用,还有令人感叹的围绕brainfucker出题和解题技巧,等等,各有侧重,不管能否解出与否,相信参与者都会在相关方向上有所学习和收获。
防守方最难是题目难度与通过率的估计,出得太难,难以说这是防守方的成功;出得太易,攻防双方都觉得不爽。个人理解,出题解题就是制迷与猜迷的过程,要达到一个平衡很不容易,最终目标即是完成一个较好的心理攻防游戏。
这道题的思路也是很偶然。大家知道,任何一本C语言教材,开始介绍输入输出时就会涉及到printf, scanf,但一般到高级应用部分才会告诉你scanf有栈溢出漏洞,缓冲区要够用才行。《软件漏洞分析》一书中,failwest也是从strcpy(比scanf应用范围更广)这个函数入手,逐步介绍栈溢出知识的。于是就想,为何不用scanf溢出来控制程序流程,让它走到真正的验证处,实现明暗的转换,对验证处再加以适当的花指令保护。
题目的设计主体是VC6作个框架,以masm作辅助。有网友怀疑我改了系统函数,这个要求对我是有点高了
主程序是VC6,流程比较简单。设了两个flag,实际有用的一处,模拟注册成功过程
#include "stdafx.h"
#ifdef __cplusplus
extern "C" {
#endif
int __cdecl Add3(int, int); // proc in asm obj
char * __cdecl very2(); // right verify proc, in asm obj, @413131
#ifdef __cplusplus
} /* extern "C" */
#endif
inline void G(){ printf("You get it!\n");} // show good msg, will not have a chance to run
inline void B(){ printf("Bad register-code, keep trying.\n");} // show err msg
char * InputKey(); // get register code, in stack
void very1(); // fake verify proc 1
void very3(); // fake verify proc 3
int Flag=0x21; // register flag, 21 is a bad-guy
extern "C" int cFlag=0x11; // anti-wrong jump flag
int main(int argc, char* argv[])
{
printf("\n Crackme for CTF2017 @Pediy.\n"); // show welcome msg
char Key0[10]; // local var, never use
char *p1; // pointor to stack var, useless
cFlag=2;
p1=InputKey(); // get register code, it must over-flow to very2()
//printf("%s\n", p1);
very1(); // useless
very3(); // useless
if(cFlag==0)
G();
else
B(); // boom! bad cracker, just wonder where to verify it???
//getch();
return 0;
}
char * InputKey() // get register code, it must over-flow to very2(), which is coded in ASM, with junk-code
// dword1 dword2 dword3 11A (address of very2, @413131 )
{
char Key[10];
char *p2;
p2=(char *)&Key;
printf(" Coded by Fpc.\n\n");
printf(" Please input your code: ");
scanf("%s", Key);
return p2;
}
开赛前夕,稍有调整,主要是在挖坑部分,让主流程的两个验证函数(原来是不能正常工作的)更像一些。这里用到了一点心理诱导(ransomware就是这样做的):近在眼前的东西,但却不能得到时最痛苦。这题的坑、抓人的地方正在于此。原谅作者吧。有坑才好玩
。
这两个fake check function是用asm写的,原因是C的代码用到的寄存器会入栈,而栈里面一个字节的多余空间都没有了,不能再允许push任何数据了
void __declspec(naked) very1() // useless
{
long int x, y, z;
__asm
{
push ebp
mov ebp, esp
//mov eax, esp
//sub eax, 8
//mov edx, [eax]
//mov [ebp-4], edx
//mov edx, [eax+4]
//mov [ebp-8], edx
mov ecx, [ebp-4]
test ecx, ecx
jz end1
mov edx, [ebp-8]
test edx, edx
jz end1
mov eax, ecx
sub eax, edx
jz end1
mov [ebp-0x0c], eax
imul eax, 5
add ecx, eax
cmp ecx, 0x08f503a42
jnz end1
mov eax, [ebp-0x0c]
imul eax, 13
add edx, eax
cmp edx, 0x0ef503a42
jnz end1
mov edx, 0x543f30 // dec flag, anti ida x-ref trick
xor edx, 0x158f04
mov eax, [edx]
dec eax
mov [edx], eax
//mov cFlag, eax
//if (x==0)
// goto end1;
//if (y==0)
// goto end1;
//z=x-y;
//if (z==0)
// goto end1;
//if( ((x+z*5)==0x08f503a42) && ((y+z*13)==0x0ef503a42) )
// G();
end1:
mov esp, ebp
pop ebp
ret
}
//return;
}
void __declspec(naked) very3() // useless
{
long int x, y, z;
__asm
{
push ebp
mov ebp, esp
mov ecx, [ebp-4]
test ecx, ecx
jz end1
mov edx, [ebp-8]
test edx, edx
jz end1
mov eax, ecx
sub eax, edx
jz end1
mov [ebp-0x0c], eax
imul eax, 17
add ecx, eax
cmp ecx, 0x0f3a94883
jnz end1
mov eax, [ebp-0x0c]
imul eax, 7
add edx, eax
cmp edx, 0x033a94883
jnz end1
mov edx, 0x543f30
xor edx, 0x158f04
mov eax, [edx]
dec eax
mov [edx], eax
end1:
mov esp, ebp
pop ebp
ret
}
}
实话实说,这几个方程我是随便设的,没验证过,感觉即使有解,也就能解开一组,不会两组同时成立,辛苦暴力验证的同学了
验证部分是masm写的,加了花,编译成obj,加入VC工程,编译后IDA里面分析,再手动跟一遍,确定和修正里面与地址有关的数据
public c very2 ;声明引出的proc
extern cFlag:near
.code
db 10000 dup(0) ; pushing addr of very2 to 413131 in exe-file
db 2000h dup(0)
db 0c3 ; ret, give some sign for crackerz
;
very2 proc near c ; right verify proc here, at 413131
add esp, -10 ; restore esp, crack must input:0c bytes regcode and addr of very2
xor eax, eax
mov DWORD ptr cFlag, eax
;
; check register code
; correct register code at stack:
; Just_for_fun + 11A(addr of very2)
;
;解个三元一次方程
;5x-4y+z=A
;4x-3y+z=B
;4x-3y-z=C
;
pop eax
mov ecx, eax ;eax=x 7473754a
pop eax ;eax=y 726f6630
mov ebx, eax
pop eax ;z 6e756630
mov edx, eax
mov eax, ecx ;x
sub eax, ebx ;x-y 2040f1a
shl eax, 2 ;4(x-y) 8103c68
add eax, ecx ;x=x+4(x-y) 7c83b1b2
add eax, edx ;x=x+4(x-y)+z eaf917e2
sub eax, 0eaf917e2 ;A
jnz bad_boy
add eax, ecx
sub eax, ebx ;x-y
mov ebx, eax
shl eax, 1
add eax, ebx ;3(x-y) 60c2d4e
add eax, ecx ;x+3(x-y)
mov ecx, eax ;x+3(x-y) 7a7fa298
add eax, edx ;x+3(x-y)+z e8f508c8
sub eax, 0e8f508c8 ;B
jnz bad_boy
mov eax, ecx
sub eax, edx ;x+3(x-y)-z c0a3c68
sub eax, 0c0a3c68 ;C
jnz bad_boy
;
;eax=0
;ebx=02040f1a
;ecx=7a7fa298
;edx=6e756630
pop eax ; balance stack, eax=413131
;set flag to 0, show good cracker msg
xor eax, 8101 ;eax=41b030, addr of flag
mov edi, eax
xor eax, eax
stosd ;set flag
call @f
;buffer for good message: "You get it!", in 3 dwords
dd 0050600e8, 1702,1702
@@:
; recover good message in code segment
pop eax
push eax
mov edi, eax
;push 20756f59
push 4e000969 ;first dword
pop eax
;use ebx,edx value
xor eax, edx
stosd
;push 20746567
xor eax, 10a3e ;second dword
stosd
;push 217469
xor eax, ebx ;22706b8c, third dword
xor eax, 22511e14
stosd
;mov eax, 401044
xor eax, 61642d
jmp ret1
bad_boy:
;mov eax, 40103f
pop eax ;eax=413131, addr of very2
xor eax, 1210e
ret1:
xor eax, DWORD ptr cFlag
jmp eax
nop
;ret
very2 endp
实际上,这题是在合理利用规则,如果输入内容不限制为数字和字母,即使防守的代码上一点漏洞没有也一定是多解的,即输入 “任意数据”+“Addr(jmp esp)”+ shellcode(未限制长度,可手动构造注册成功信息,修改注册标志位等等),所以这题天然只能在有限定条件下求解。
其实这题最值得怀疑之处就是前面一段代码之后,放了大段的0,这可以说是为推进到41XXXX处的无奈之举,逆向者就是要具有合理怀疑的想法。如果作者空地里面添加垃圾数据,很有可能大家就找到不答案了,失去了游戏的意义,当然另一方面更多的数据也意味多解的可能性增加了(比如有rop可用)
最后感谢众位解题大神,有的分析已经超出作者的预想,我也学到很多;感谢林版、netwind等各位版主辛苦地维护论坛;感谢kanxue长期以来为大家提供这一方乐土。希望以后还有机会将游戏进行下去。
安卓应用层抓包通杀脚本发布!《高研班》2021年3月班开始招生!