【wp】2020HSCTF_PC逆向

是HSCTF2020新生赛的PC逆向部分的wp,尽量往详细写啦,希望能给师傅们提供良好的复现体验。

本次Reverse基本都为送分题,旨在科普和引导逆向入门=v=

已经疯狂砍了很多难度啦,H4ve fuN w1th C7F_r3vEr5e!!

0x01 Funny_game

多维度送分题(x)

显而易见的俄罗斯方块,打块拿flag。

Ⅰ. 暴力打穿

打到2020分自行结束游戏后在屏幕右侧出flag。花个十几分钟大概就能出(

p.s. 一定要在刚好2020分的时候结束游戏才能出flag哦,不然也没有flag出来的(所谓打得好不如打得巧

自己打出来的反面教材↓(。)

Ⅱ. 修改程序逻辑

在ida里按F5出伪代码,看到程序逻辑,

patch一下把if改成if not / 2020改成0 / …

(如果ida有装keypatch这个插件的话就能直接通过更改汇编语句从而patch机器码;方法见->IDA7.0安装keypatch和findcrypt-yara插件

↑ ① 相当于把if改成if not

↑ ② 把2020改成0

然后狂敲enter,1s拿flag。

Ⅲ. 定位到getflag()逆向求解

查看到程序逻辑以后双击getflag(),或者直接在函数栏搜索flag定位到关键函数。

getflag()程序逻辑:

逆向写出exp:

1
2
3
4
5
6
7
8
arr=[0x0000006E, 0x00000094, 0x00000061, 0x0000006F, 0x0000009B, 0x00000071, 0x0000009D, 0x00000051, 0x0000009C, 0x0000006D, 0x00000067, 0x00000061, 0x00000067, 0x0000006E, 0x0000009D, 0x00000096, 0x00000096, 0x00000099, 0x00000067, 0x0000006F, 0x0000005C, 0x00000095, 0x0000006D, 0x00000067, 0x00000069, 0x00000093, 0x00000096, 0x0000007C, 0x00000067, 0x00000069, 0x0000009C, 0x00000085]
flag=""
for i in range(32):
tmp=(arr[i]^20)-20
flag+=chr(tmp)
print(flag)

#flag{Qu1te_a_funny_g4me_isnT_it}

p.s. v1-v9在空间上是连续的,所以实际上这是一个长度为33(末尾有’\0’)的char型数组。


0x02 Magic_switch

还是送分题(linux下可直接打开

Ⅰ. 随手乱糊

其实根据xor的特性,只要1-8每个数字都敲一次就能出flag(比如12345678)。当时出题的时候就在想会不会有队伍在wp里一脸懵逼地写“随便打就出来”的,没想到还真有哈哈哈。

Ⅱ. 修改程序逻辑

可以看到程序逻辑是只要所有灯的状态都是1的时候,才能跳出while(1)死循环。

同funny_game,可以通过修改程序逻辑,把if(v6)改成if(!v6),然后程序运行时在while执行的第一次就能跳出循环获得flag,不过完全没必要(。
有patch的功夫不如直接乱糊2333。

Ⅲ. 定位到关键函数逆向求flag

按惯例按快捷键shift+F12,查看strings窗口,可以看到一个fake_flag,顾名思义是假flag(这一段没什么用,目的只是提一下IDA的常规操作罢辽)。

点进flag以后发现flag不是直接存储的,而是在某个函数里生成(?)的。
进到反汇编窗口,右键flag数组,查询引用情况。

then可以看到一个神秘的secret函数。

secret()的函数逻辑如下。

在题目里,可以看到flag[]为全局变量(函数内没有声明/存储在bss段/在ida里显示变量名字),全局变量默认初始化为0。而任何数异或0都为它本身,故flag[i]^=v2[i]实际上等同于flag[i]=v2[i]
一个简单xor,解密思路跟funny_game相同,写出exp:

1
2
3
4
5
6
7
8
9
v2=[14,16,35,28,15,42,14,16,29,60,53,12,35,46,116,15,92,56,42,19,3,20,28,37,6,19,13,20,56,6,20,27,20]
fake_flag="Hahahahahahah_Th1s_is_a_f4ke_f1ag"
flag=""
for i in range(33):
tmp=v2[i]^ord(fake_flag[i])^20
flag+=chr(tmp)
print(flag)

#Re_is_reaIIy_e4sy_and_int3rest1ng

这也说明了xor其实只是基操并非考点,考点还是ida的操作和阅读C代码的能力= =


0x03 Not_A_Sudoku

Not A Sudoku,不是一个数独,不要看到81和sudo的数组名就想到数独啦(

但跟数独也不是毫无关系的x。

这是一个神秘古老而冷门(误)的逻辑游戏,名为数织Nonograms 【数织 - 在线解谜游戏】(hint也给了这个网站的数独网址,往下翻一下更多解谜游戏就有),应用数织原理的手游也有Two Eyes、PicrossLUNA系列等。

看到伪代码,知道程序逻辑是输入一个长度为81的二进制数字串,且需要通过check(v4,0)check(v4,1)

这里不能通过动态调试的方法获取flag,因为这里的flag其实是用户输入进去的,它只负责测试对不对。

也不能用angr,因为分支太多会爆内存。

所以我们只能通过静态分析check()函数来判断怎样的flag才能通过检查。

双击C()可以看到其功能是return x==0? 1:9;,本来还想科普一波三目运算符的没想到ida给简化了。

关于xor,有0^1=1, 1^0=0,即,将参数取反。(逆向科普有认真看的想必对这一条不会陌生!xor还有更多有趣的特性等待你们去挖掘~)

回到check(),a2=0时的函数可以简化为:

1
2
3
4
5
6
7
8
9
10
11
for(int i=0;i<9*9;i+=9){ //i为行数
for(int j=0;j<9;j++){ //j为列数
if(s[i+j]=='0') continue;
int tmp=j;
while(s[i+j]=='1'&&j<9) j++;
if(sudo[x][pos]!=(j-tmp)) return 0;
else pos++;
}
if(sudo[x][pos]!=-1) return 0;
else pos++;
}

于是就很容易可以看出,这是在检验一个9 * 9的图里每一行中有多少个连续的1块,每个1块里有多少个1。同理可得,check(v4,1)的作用是检验一个9 * 9的图里每一列中有多少个连续的1块,每个1块里有多少个1。这些数据都存放在sudo[]里,并以-1作为每一行/列的界限。

这就是数织游戏的原理。

所以,我们可以画出这样的数织空图:

按照数织的玩法,我们填出下面的图(是一个很简单的9*9非常规数织,熟练的人很快就能填出来了)。

和尚玩家直呼眼熟(x)

填完以后,按照题目要求,把每一个格子的状态打下来就好了(有色为1,无色为0),然后能够得到flag:

flag{001111000011111100110001110111111110111111111011111111000011110000001110000111100}


0x04 Maze

其实这道题是出题的时候所有题里面最先有思路的一道,并且也花了点心思往里塞了很多科普(可惜写的时候忽略了防输入其他字符的default,不过还是做到就是赚到hhh。

如果认真看过CTF Wiki的人(或者攻防世界里reverse新手区刷完的人),看到这个题目应该有条件反射吧0v0

那就直接maze套路走起吧

1)在内存中布置一张“地图”

不同于常规的maze题目,本题的地图没有直接储存在全局变量中,而是由CreateMap()生成。

而这一串关键代码

1
2
3
4
5
do{
map[16 * i + 16 - v1] = v2 & 1;
v2 >>= 1;
result = (unsigned int)v1++;
}while ( (_DWORD)result && v1 <= 16 );

其实跟十进制转二进制的代码特别像(论熟悉进制转换的必要性

1
2
3
4
5
6
int num,pos=1,binary[16]={0};
cin>>num;
do{
binary[16-pos]=num&1;
pos++;
}while(num>>=1);

所以其实就是把num数组里的数(先转成无符号数再)转成二进制按行存到地图里。

然后可以得到16*16的地图:

2)将用户输入限制在少数几个字符范围内

可以看到,除了格式flag{}以外,其余的字符都由hjkl组成,表示在迷宫内的上下左右移动。

那么,为什么选这四个字母来作为上下左右而不是常规的↑↓←→或者wasd呢?

因为在文本编辑器vim里,让光标上下左右移动的快捷键分别就是h(左)、j(下)、k(上)、l(右)。(掌握vim快捷键是linux端快速敲码的第一步噢0v0

3)一般只有一个迷宫入口和一个迷宫出口

在迷宫画出来后,可以轻松看到唯一的出入口(入口在map[13][0]、出口在map[13][15])。因为有限制长度为59,所以很大可能是迷宫的最短路径。

在地图中用黄色标记表示(红色为混淆点):

也可以很明显地看到,全部为0的格子连起来其实是一个Sloth的文字押 (cy2谱面传统艺能,假装是音游元素)

跟着黄色路线一直走记下flag即可。

flag{llllkkkhhhkkkkkkkkklllljjjjllljjljjjjjjjlllkkkklljjjl}


0x05 Base64

这个名字,显然就是跟base64编码有密切联系呀= =

不知道的赶紧去看base64的简介和源码(pia飞。

某种程度来说也是一道送分题。

函数名也这么明显了hhh,超级友好(当然如果函数名不明显的话也可以通过查看加密函数,发现跟base64源码差不多)。

点进base64_char数组,能看到编码表,发现跟原版base64的表不同。

  • 本题:abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/
  • 原版编码:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/

所以是一个base64换表题,随便找一个在线网站利用已知密文src直接base64换表解密即可。

flag{dedf73ea-7252-4892-b882-9992708f0f62}


0x06 Bytecode

python字节码人工反编译,在网上能搜到很多死磕字节码的教程(比如hint给的 死磕python字节码 或者 python 字节码死磕),最需要的是耐心hhhh。

然后可以逆出源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def main():
flag=input("No Flag No Entry <(_^_)>:\n")
if len(flag)==34 and foo1(foo2(foo3(flag))):
print("\nAcc3pTed: You g0t iT! :D")
else:
print("\nErr0r: Unm4tched str1ng. :/")

def foo1(s):
arr=[51, 42, 67, 2, 100, 48, 94, 29, 25, 26, 9, 43, 25, 21, 53, 11, 11, 91, 0, 12, 14, 19, 122, 0, 44, 26, 58, 26, 28, 24, 50, 3, 93, 21]
if s==arr:
return True
else:
return False

def foo2(s):
arr1=list(map(ord,s))[::-1]
arr2=[74, 117, 115, 116, 84, 111, 111, 108, 109, 97, 110] #"JustToolman"
for i in range(len(arr1)):
arr1[i]^=arr2[i%11]
return arr1

def foo3(s):
a=1
b=-6
c=len(s)-33
a=(a+2*c)*3+4*c
b=b+2*a+4*c+c
return s[b:a:-1]+s[:b:-1]+s[:a+1]

简单逆向,主要的点在字节码反编译部分,只要把源码逆出来就能写出脚本啦,刚好能帮助入门python。

就是切片那一块有一点点坑,注意是前到后不到的就好。

本来还有一层加密的,为了降低难度删掉了。

1
2
3
4
5
6
7
8
9
10
11
12
arr1=[51, 42, 67, 2, 100, 48, 94, 29, 25, 26, 9, 43, 25, 21, 53, 11, 11, 91, 0, 12, 14, 19, 122, 0, 44, 26, 58, 26, 28, 24, 50, 3, 93, 21]
arr2=[74, 117, 115, 116, 84, 111, 111, 108, 109, 97, 110]
for i in range(len(arr1)):
arr1[i]^=arr2[i%11]
arr1=list(map(chr,arr1))[::-1]
s=''.join(arr1)
a=13
b=25
flag=s[-a-1:]+s[b-a-1::-1]+s[-a-2:b-a-1:-1]
print(flag)

#flag{tq1_0v0_y0u_nnust_b3_A_d4lao}

0x07 Deliver_TEA_to_dalao

其实名字有一点点暗示 给大佬递茶.jpg ,毕竟TEA这么明显的大写,在搜索引擎里能查到TEA加密;并且很明显的可以看到,在代码保护上运用了smc技术(热身赛有好好看wp的队肯定能发现)。

SMC(self-Modifying Code 自修改代码),就是在真正执行某一段代码时,程序会对自身的该段代码进行自修改,只有在修改后的代码才是可汇编、可执行的。在程序未对该段代码进行修改之前,在静态分析状态下,均是不可读的字节码,IDA之类的反汇编器无法识别程序的正常逻辑。

大概逻辑如图:

Ⅰ. 通过patch伪修正

代码逻辑看起来比较简单,就是把输入的字符串v21经过一个加密函数以后跟已知字符串做对比而已。

然而看到加密函数的伪代码&&反汇编却发现乱七八糟的。

这时候我们可以猜测它被smc技术保护,实锤就是在上面一个不起眼(?)的ignoreMe函数里把加密函数所在的地址进行了xor。

也就是说,程序目前的encrypt_0v0部分的机器码是有问题的,需要通过这个xor函数以后程序才能正常进行。

那我们可以通过IDA自带的脚本功能进行patch,将那一部分的函数“修正”,让改函数能正常反编译,给我们呈现出这个部分的伪代码。

注意,这里只是伪修正,只改这里然后将patch的部分应用到输入文件的话程序是无法运行的,需要把xor函数nop(全部patch成90)才能消除影响(不过在这里正常运行似乎没什么必要x)。

我们找到encrypt_0v0部分,看到函数起始地址是0x401448,加密长度是189。

然后写出patch的exp:

1
2
3
4
5
6
7
8
9
10
11
12
def patch(start,end,key):
n=0
while(start+n!=end+1):
addr=start+n
PatchByte(addr,Byte(addr)^key)
n+=1
print("%d Byte has been changed"%n)

codeStart=0x401448
codeLen=189
xorData=0x56
patch(codeStart,codeStart+codeLen-1,xorData)

点击File->Script file,导入脚本运行。

可以看到encrypt_0v0函数发生了变化,同时消息窗口有“189 Byte has been changed”字样。

点击Edit->Patch program->Apply patches to input file,将patch的部分应用到输入文件中(便于让ida重新分析)。

关掉,重新用ida打开,按F5进行反编译可得:

是显而易见的TEA加密形式(比如特殊常数0x9E3779B9或者0x61C88647)。

这里key是[v4,v5,v6,v7]=[0x33221100,0x77665544,0xBBAA9988,0xFFEEDDCC]

明文是flag,密文是v8~v12转成的char数组:{0xa8,0xa,0xe4,0xe3,0x13,0x5c,0xfa,0x8,0xd,0x5c,0xe1,0x90,0x25,0x12,0x76,0x36,0x51,0x10,0xc4,0x11,0xf6,0xd8,0xf8,0x82}

为什么不是{0xe3,0xe4,0x0a,0xa8,...}呢,这就要看它们在内存中的存储方式(小端序)了,具体自行搜索。

于是exp就可以写了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>
char flag[]={0xa8,0xa,0xe4,0xe3,0x13,0x5c,0xfa,0x8,0xd,0x5c,0xe1,0x90,0x25,0x12,0x76,0x36,0x51,0x10,0xc4,0x11,0xf6,0xd8,0xf8,0x82};
int key[]={0x33221100,0x77665544,0xBBAA9988,0xFFEEDDCC};
void decrypt(unsigned int * v, unsigned int * k){
int i;
unsigned int v0=v[0],v1=v[1],sum=0,delta=0x9e3779b9;
sum=delta*32;
for(i=0;i<32;i++){
v1-=((v0<<4)+k[2])^(v0+sum)^((v0>>5)+k[3]);
v0-=((v1<<4)+k[0])^(v1+sum)^((v1>>5)+k[1]);
sum-=delta;
}
v[0]=v0;
v[1]=v1;
return;
}
int main(){
int i;
unsigned int * p_int=(unsigned int*)flag;
for(i=0;i<6;i+=2) decrypt(&p_int[i],(unsigned int*)key);
printf("%s",flag);
return 0;
}

//flag{H4vE_gr3aT_7eA_Hah}

Ⅱ. 动态调试直接看函数

对付smc,还可以考虑动态调试。

环境是IDA pro v7.0,高版本可以直接调试32位程序。

因为是32位程序,所以在ida目录下找到.\dbgsrv\win32_remote.exe,并打开:

然后用ida打开文件,点击Debugger->Select debugger选择:

然后点击Debugger->Process options调整设置(将Hostname设置成my ip就好):

断点设在call ignoreMe后面,此时encrypt_0v0已被处理,是正常函数。

然后按F9,开始调试,调试开始后程序暂停在断点处。

点开encrypt_0v0,(按空格从图形界面转到文字)。

选中这一部分的汇编代码,按c强转成代码(->Force->Yes)。将还包括在函数体内的语句按d再按c

然后选中刚才的地址区段,按p生成函数。

最后按F5反编译得到:

就可以看到加密函数了,后续解密步骤同Ⅰ。

其实这里从强转成代码开始的步骤同样适用于Ⅰ,只是相比之下直接重新打开ida让其重新分析更方便而已hhh。

0xff EOF…?

一点乱七八糟的碎碎念,看完wp的可以关掉啦~

这次新生赛是我CTF生涯中第一次出题(很开心能混迹在大佬云集的筹备委员会中0v0),在pc逆向这边除了Base64以外背了其他六道题的锅,感觉是一次很宝贵的经历。

所谓“出题才能学到更多”是真的。以前做题的时候,在ida里总是会点进一些看起来操作很多好像很有用的函数,但实际上有很多都是没有用的初始函数(编译源码的时候自动生成、ida反编译的时候也算上了的那种),都是在自己出完题目然后试图从选手的角度做的时候发现的。

以及比如像smc这种有点进阶考点的,本来做题也不是很懂原理(吧),就是单纯地走一个找函数patch回去的流程,然后出题的时候就单纯地按自己的想法写了(.data段没有执行的权限hhhh 一开始还把函数当成数组写到.data段里了xswl),然后就失败了x,幸好有大佬可以求助=v=。

还有就是写wp的时候每道题会去想一些别的路,争取把各个思路&工具都科普出去(于是迫使自己接触一些平时不常用的工具)。比较遗憾的是没有合适的题目可以用来写ida利用wsl2远程调试linux程序angr/pintool的使用吧。前者是没有出到用动态调试性价比更高的linux题目,后者是那些可以走爆破的题目分支多容易爆内存(像这个东西也是从去年vm的wp里学到的,就是想把这些宝藏工具传承下去)。

出题的时候说不要变成萌新劝退赛+要放点有意思的题目,于是就有了Funny_gameMagic_switch这两道以游戏为载体的题目。Funny_game本来是打算出成贪吃蛇的,不过考虑到去年的杂项就有贪吃蛇(偶然看到我们队去年的wp发现的),就想着不要出重复的了。刚好那段时间在等俄罗斯方块手游的N测(卑微打块人已经在小程序上打了很久的马拉松了),于是就拿别人的俄罗斯方块程序改了一下,加了个出flag的函数。说起来那么简单,实际上还是得把别人的程序看懂了才知道能在哪里加啊qvq 给写这个源程序的大佬磕头。一定要等于2020那里是我故意的哈哈哈,因为读程序的时候发现2020其实很容易达到,又为了契合年份(这六个题的所有常数都是有意义的嘿嘿,甚至连smc那个异或的0x56也是有意义的 cy2的某首歌 ),于是就搞了个等于而不是大于等于。这个题出完以后我还闲着无聊打了几盘,没有保留功能不然就能凑tetris了。至于Magic_switch,参考了一下攻防世界逆向新手区里面game的玩法,但源码是自己撸的,感觉提升了一波写游戏代码的能力。

Not_A_Sudoku的原意其实是真的想出一道数独的,源于某次比赛逆出来的py源码排版超级乱(一堆lambda看到眼花),然后被某个大佬发现是数独检验,直接当数独题做了。出题的时候就想搞一个类似的,不过后来想了想觉得数织的检验代码比数独优雅好多啊,写源码的话还不如去写数织的呢(所以这道题就叫Not_A_Sudoku了,顺便还能科普一下冷门的数织游戏。至于谜面,一目了然就是本命禅雅塔的“乱”了hhh。

Maze真的是我最早有思路的题目,源于数据结构课本里用深搜/栈走迷宫的图(上课没怎么听讲实锤了,以及不用队列走迷宫这个点真的很想让我吐槽),then一整节数据结构课上都在想构造这个maze的谜面hhh,以及一个埋成sloth彩蛋的文字押,下课就迅速掏出电脑把生成地图的常数数组写进去。像是把数字转成十六位二进制当成数组和hjkl控制光标上下左右这些点也是想科普的点吧hhh。

Bytecode的出题本意是想让萌新们趁机入门python和一些反编译的操作,感觉比起汇编语法来说python字节码这个更容易理解一点,毕竟没涉及到寄存器操作之类的,甚至还保留了变量名。这个题真不难,就是考一点耐心和细心而已,欣慰的是还挺多队伍做出来了(自我感觉这个题比数织的要复杂,不知道为什么数织的解这么少XD)。原本还有一个foo4是维吉尼亚密码的加密,后来被嫌太难就删了。

预期中Deliver_TEA_to_dalao应该是最难的题,因为考虑到大部分人没接触过smc这种保护,没想到热身赛直接就放了个smc的题,那其实难度就大大降低(在热身赛的wp那里我连patch的脚本都给了),主要难点变成了TEA这个加密算法的识别了(对差点就做出来的TEA一直念念不忘),当然手写逆算法也不难hhh。

出完题以后觉得pc逆向这里防ak有点悬,没想到真就没防住被ak了(向大佬低头)。出题其实也是在另一个角度上的对基础知识的巩固吧,wp也写得很爽没有交作业的感觉,出题人表示体验极佳,下次一定再来 /Doge。

文章作者: c10udlnk
文章链接: https://c10udlnk.top/2020/11/27/wpFor-2020HSCTF/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 c10udlnk_Log