RCTF2017

吐槽一下自己这么久都没有写文章了怕是变懒了。。。


RCTF2017

这个RCTF2017其实蛮有意思的,题目种类非常多,但是我不知怎么的陷入了Reverse的坑?!说好的pwn手呢。。。。而且比赛最后10分钟结束前极限做出最后一题也没能挽救30开外的结局啊。。。

baby flash

flahs的题目嘛。。没怎么见过,花了不少时间熟悉工具:
运行swf后,发现是让我们输入flag。那么应该是在后台有一个比对的过程。
找了个半天发现也就一个叫做JPEXS的好用。
反编译后大致检查一下,找打一个奇怪的数据包叫MyCPP,检查内部函数,可以发现一个叫做check的函数,里面对字符串的处理逻辑为:

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
import C_Run_D_3A__2F_software_2F_Tools_2F_CrossBridge_2F_cygwin_2F_tmp_2F_cc9QAEpL_2E_o_3A_d1fd6bd0_2D_036b_2D_49da_2D_bcfb_2D_b5dce5104397.L__2E_str4;
import C_Run_D_3A__2F_software_2F_Tools_2F_CrossBridge_2F_cygwin_2F_tmp_2F_cc9QAEpL_2E_o_3A_d1fd6bd0_2D_036b_2D_49da_2D_bcfb_2D_b5dce5104397.L__2E_str5;
import C_Run_D_3A__2F_software_2F_Tools_2F_CrossBridge_2F_cygwin_2F_tmp_2F_cc9QAEpL_2E_o_3A_d1fd6bd0_2D_036b_2D_49da_2D_bcfb_2D_b5dce5104397.L__2E_str6;

ublic function check(src:String) : void
{
...
i0 = CModule.mallocString(src); // creaet string to i0
si32(i0,ebp - 4); // mov string [ebp - 4]
i0 = li32(ebp - 4); // mov i0 [ebp - 4]
esp = int(esp - 16); // sub esp -16
si32(i0,esp); // mov i
ESP = esp;
F_puts(); // print i0
esp = int(esp + 16); // add esp 16
int(eax); // eax to int
i0 = li32(ebp - 4); // mov i0 [ebp - 4]
esp = int(esp - 16); // move esp [esp - 16]
si32(i0,esp + 4);
si32(L__2E_str4,esp);
// 将L__2E_str4 与i0中字符串比较
ESP = esp;
F_strcmp();
esp = int(esp + 16);
i0 = eax;
// 如果此时返回值不为0(不相等)那么输出right
...


包的名字很长,可能是加密处理过(?),从中我们可以猜测,这个函数就是对输入的字符串与变量L__2E_str6处理进行比较。这里由于JPEXS的数据处理不太好。不能直接找到字符串所在的位置。不过顺着类的名字找到对应的数据包,里面有相关字符串的声明:

1
2
3
4
5
6
7
8
9
10
11
12
L__2E_str4:
[Csym(".rodata")]
public const L__2E_str4:int = S__2E_rodata + 0;

L__2E_str5:
[Csym(".rodata.str1.1")]
public const L__2E_str5:int = S__2E_rodata_2E_str1_2E_1 + 0;"Right"

L__2E_str6:
[Csym(".rodata.str1.1")]
public const L__2E_str6:int = S__2E_rodata_2E_str1_2E_1 + 7;"Try again!"

从这个地方还不能找到对应的数据段,他这个JPEXS真难用。。巧合的是,我们能够找到一个位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package C_Run_D_3A__2F_software_2F_Tools_2F_CrossBridge_2F_cygwin_2F_tmp_2F_cc9QAEpL_2E_o_3A_d1fd6bd0_2D_036b_2D_49da_2D_bcfb_2D_b5dce5104397
{
import com.adobe.flascc.CModule;

[ExecPolicy(OSR="2")]
function modAllocSects() : Object
{
return {
".data":[CModule.allocDataSect(modPkgName,".data",0,0),0],
".debug_abbrev":[CModule.allocDataSect(modPkgName,".debug_abbrev",175,0),175], DS2
".debug_frame":[CModule.allocDataSect(modPkgName,".debug_frame",80,4),80], DS5
".debug_info":[CModule.allocDataSect(modPkgName,".debug_info",426,0),426], DS3
".debug_line":[CModule.allocDataSect(modPkgName,".debug_line",92,0),92], DS4
".debug_loc":[CModule.allocDataSect(modPkgName,".debug_loc",0,0),0],
".debug_pubnames":[CModule.allocDataSect(modPkgName,".debug_pubnames",48,0),48], DS6
".debug_pubtypes":[CModule.allocDataSect(modPkgName,".debug_pubtypes",18,0),18], DS7/DS12
".debug_ranges":[CModule.allocDataSect(modPkgName,".debug_ranges",0,0),0],
".debug_str":[CModule.allocDataSect(modPkgName,".debug_str",0,0),0],
".rodata":[CModule.allocDataSect(modPkgName,".rodata",34,16),34], DS1
".rodata.str1.1":[CModule.allocDataSect(modPkgName,".rodata.str1.1",18,0),18], DS12/DS7
".rodata.str1.16":[CModule.allocDataSect(modPkgName,".rodata.str1.16",110,16),110], DS11
".text":[CModule.allocTextSect(modPkgName,".text",664),664]
};
}


由于工具翻译不全,在二进制数据选项中,通过比较数据的大小,能够确定DS1就是[.rodata]段数据
这个地方可以看到,这个L__2E_str4对应的字符串的内容为:
RCTF{_Dyiin9__F1ash__1ike5_CPP}
最初以为这就是答案,提交后发现不对。。。
于是重新怀疑函数strcmp可能是重载的。于是再次找到对应函数的位置:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public function F_strcmp() : void
{
var ebp:* = 0;
var i2:int = 0;
var esp:* = int(ESP);
ebp = esp;
esp = int(esp - 44);
si32(int(li32(ebp)),ebp - 4); // mov rctf, ebp - 4
si32(int(li32(ebp + 4)),ebp - 8); // mov input, ebp - 8
si32(int(li32(ebp - 4)),ebp - 24); // mov ebp - 4, ebp - 24
si32(int(li32(ebp - 8)),ebp - 28); // mov ebp - 8, ebp - 28
si32(2,ebp - 32); // mov 2 ebp - 32;var0 = 2
si32(3,ebp - 36); // mov 3 ebp - 36;var1 = 3
si32(0,ebp - 40); // mov 0 ebp - 40;index = 0
si32(0,ebp - 44); // mov 0 ebp - 44;var3 = 0
while(int(li8(int(li32(ebp - 24)))) != 0) // while(rctf[i]!='\x00')
{
if(int(li8(int(li32(ebp - 28)))) == 0) // if(input[i] == '\x00')
{
break; // break
}
var i1:int = li8(int(li32(ebp - 28))); // i1 = input[i]
if(int(li8(int(li32(ebp - 24)))) == i1)// if(i1 == rcft[i])
{
var i0:int = li32(ebp - 40); // i0 = index;
i2 = 1; // i2 = 1;
if(i0 != int(int(li32(ebp - 32)) << 1))// i0= !(var0<<2)
{
i2 = 0; // i2 = 0
}
i0 = i2 & 1; // i0 = i2 & 1
si8(i0,ebp - 17); // mov i0, ebp - 17
i0 = li32(ebp - 40); // -------
i0 = i0 + 1; // index ++
si32(i0,ebp - 40); // -------
i0 = li8(ebp - 17); // i0 = ebp - 17
if(i0 != 0)
{
si32(int(int(li32(ebp - 24)) + 1),ebp - 24); //*rctf++
si32(int(int(li32(ebp - 32)) + int(li32(ebp - 36))),ebp - 44); //var3 = var0 + var1
si32(int(li32(ebp - 36)),ebp - 32); //var0 = var1
si32(int(li32(ebp - 44)),ebp - 36); //var1 = var3
}
si32(int(int(li32(ebp - 24)) + 1),ebp - 24); // *rctf++
si32(int(int(li32(ebp - 28)) + 1),ebp - 28); // *input++
continue;
}
break;
}
i0 = si8(li8(int(li32(ebp - 28))));
si32(int(int(si8(li8(int(li32(ebp - 24))))) - i0),ebp - 16); // 求出最后字符串的差距
si32(int(li32(ebp - 16)),ebp - 12);
eax = int(li32(ebp - 12));
esp = ebp;
ESP = esp;
}

由备注可知,这个函数会将字符串的部分内容省略。于是此时我们编写脚本反向处理:

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
var0 = 2
var1 = 3
var3 = 0
index = 0
t_i = 0
index = 0
t = r"RCTF{_Dyiin9__F1ash__1ike5_CPP}"

while t_i < len(t):
i1 = t[t_i]
print(i1,end = '')
i0 = indexe
i2 = 1
if i0 != (var0<<1):
i2 = 0
i0 = (i2 & 1)
temp = i0
index+=1
temp = i0
if temp != 0:
t_i += 1
var3 = var0 + var1
var0 = var1
var1 = var3
t_i+=1

最终得到答案
RCTF{Dyin9_F1ash_1ike5_CPP}

这一题第一天花了我差不多6个小时,最后居然分数只剩230多。。。。我的天有没有那么简单。。。。不过我真是太naive了,因为马上就有更难的flash出现了。。。

actually cpp

发现和和上个flash逻辑几乎一样,也是有一个check函数。于是用JPEXS查看逻辑:

check

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
package MyCPP
{
...
public function check(src:String) : void
{

/******************
* ebp - 8: inputString
* ebp - 4: str7
******************/

F_puts();// 输出我们输入的字符串字符串
esp = int(esp + 16);
int(eax);
i2 = ebp - 24;
i3 = ebp - 40;
// ******* 以下重点 ***********
si32(-536244034,ebp - 12); e00990be
si32(2108577555,ebp - 16); 7dae5713
si32(-1182021347,ebp - 20);b98bc91d
si32(-1993905352,ebp - 24);89276b38
i2----->
si32(252579084,ebp - 28); f0e0d0c
si32(185207048,ebp - 32); b0a0908
si32(117835012,ebp - 36); 7060504
si32(50462976,ebp - 40); 3020100
i3----->

i0 = li32(ebp - 8);
i0 = inputStrnig
esp = int(esp - 16);
si32(i0,esp);
ESP = esp;
F_strlen(); // 读出字符串的长度
/*******************
* ebp - 44 : strlen(inputString)
*******************/
...

--------------------
brainfuck(str8, i2)
--------------------
F_malloc();
-----------------
i0 = malloc(64)
-----------------
...
/******************
* ebp - 48 : mallocString
******************/
...
------------------
memset(mallocString, 0, 64);
------------------
...
i0 = inputString
i5 = mallocString
...
--------------------
AES123_CBC_encrypt_buffer(mallocString, inputString, strlenOfinputString, i2, i3)
--------------------
esp = int(esp + 32);
i0 = li32(ebp - 4);
i0 = str7
esp = int(esp - 16);
si32(64,esp + 8);
si32(int(li32(ebp - 48)),esp + 4);
si32(i0,esp);
ESP = esp;
F_memcmp();
------------------------------
memcmp(str7, mallocString, 64);
------------------------------
...
F_free();
esp = int(esp + 16);
esp = ebp;
ESP = esp;
return _as3ReturnValue;
}
}

上述大致的逻辑如上,也就是

  • 【读取输入字符串】
  • 【将str8和i2传入brainfuck函数】
  • 【申请mallocString的字符串,将加密后的字符串放入str7】
  • 【将加密后的字符串与str7比较】

(我擦敢不敢再复杂一点!!!又是brainfuck又是加密的!!!)关键的字符串就是str7和str8,于是在JPEXS中找到对应数据段的数据:

str7:".rodata.str1.16"
str8:".rodata.str1.16"
public const str7:int = S__2E_rodata_2E_str1_2E_16 + 144;
public const str8:int = S__2E_rodata_2E_str1_2E_16 + 224;

这个str8是典型的brainfuck,但是整个执行过程有点奇怪:

1
str8 = >>>>>+++++<<<<----->>>++<<<<<<<<<-----

这段内容显然是不含有输入的,那么只能猜测这个brainfuck其实会作用在我们输入的另一个字符串上。于是继续分析下列代码:

brainfuck

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package C_Run
{
/************************
* str8: real brain fuck
* i2: magic number
***********************/
public function F__Z9brainfuckPKcPh(str8, i2) : void
{
var ebp:* = 0;
var i1:int = 0;
var esp:* = int(ESP);
ebp = esp;
/******************************
* ebp - 4 :str8
* ebp - 8 :i2
* ebp - 16:str8
* ebp - 20:i2
******************************/
while(int(li8(int(li32(ebp - 16)))) != 0) while(*str8!=0)
{
si32(int(si8(li8(int(li32(ebp - 16))))),ebp - 12); ebp - 12 = *str8
/*****************************
* ebp - 12: str8[i]
*****************************/

si32(int(int(li32(ebp - 16)) + 1),ebp - 16); str8++
i1 = li32(ebp - 12); i1 = ebp - 12(str8[i])
if(i1 > 45)//如果不为+或者-
{
if(i1 == 46)//如果为.(这段实际上没有用到,跳过)
{
...
}
else if(i1 == 60) // 如果为<
{
si32(int(int(li32(ebp - 20)) + -1),ebp - 20); i2 = i2 - 1

}
else if(i1 == 62) // 如果为>
{
si32(int(int(li32(ebp - 20)) + 1),ebp - 20);; i2 = i2 + 1
}
}
else if(i1 == 43)
{
var i0:int = li32(ebp - 20);
si8(int(int(li8(i0)) + 1),i0); *i2 = *i2 + 1
}
else if(i1 == 45)
{
i0 = li32(ebp - 20);
si8(int(int(li8(i0)) + -1),i0);
}
}
esp = ebp;
ESP = esp;
}
}

从上面的逻辑我们能够看出,这个代码的意思其实是使用类似brainfuck的函数,将brainfuck字符串作用在i2所指向的位置的数据。对于熟悉brainfuck的同学们来说应该会知道,符号<表示【当前指针向左移动】,而>表示【当前指针向/RCTF2017移动】。而这个传入的参数虽然是i2,然后brainfuck字符串为:

1
>>>>>+++++<<<<----->>>++<<<<<<<<<-----

最后一段[<]的数量非常多,超过了前面所有的>,说明指针**【会移动到i2所指位置之前的地方】**。知道这点很重要,因为我们重新会看i2赋值那段代码:

1
2
3
4
5
6
7
8
9
10
	  si32(-536244034,ebp - 12); e00990be
si32(2108577555,ebp - 16); 7dae5713
si32(-1182021347,ebp - 20);b98bc91d
si32(-1993905352,ebp - 24);89276b38
i2-----> 正常情况下的函数操作一般只会对i2一下的数据操作
si32(252579084,ebp - 28); f0e0d0c
si32(185207048,ebp - 32); b0a0908
si32(117835012,ebp - 36); 7060504
si32(50462976,ebp - 40); 3020100
i3-----> brainfuck函数操作了i3相关数据

这段可以看出,这个i2其实只是指向了某段数据中间的位置,也就是说,brainfuck函数不但会影响到i2以及其相关数据,还会影响到i3指针指向的数据上的数据
知道这一点很重要,在之后重现算法的时候不会踩坑。
最后我们粗略看一下AES128:

AES128_CBC_encrypt_buffer


之所以是粗略的看,因为一般题目中的AES都是真正实现的AES,一般不会出现重构这样的事情。所以这里我们大致浏览后发现的确是正常的AES的逻辑,我们就可以从github之类的地方找到一个AES128实现的算法直接使用。
大致流程知道了,关键就是这个AES128CBC,如果要解开AES的话,我们必须要知道密钥IV

根据分析,我们之前知道了函数原型为:

1
AES123_CBC_encrypt_buffer(mallocString, inputString, strlenOfinputString, i2, i3)

那么此时IV也就是此时的i3,Key就是此时的i2。
i3和i2都在调用此函数之前被处理过,自然不能用之前的数据。为了能够完全将上述加密反向运算解密,这里使用C语言重现上面代码的逻辑,同时使用AES128CBC解密函数进行解密:

最终得到flag:
RCTF{2b482cfdcc32e434a4527c3723bf89b8}
附上源代码:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include "aes.h"
char* str10 = "You are right!";
char* str9 = "Try again!";
char* str8 = ">>>>>+++++<<<<----->>>++<<<<<<<<<-----";
unsigned char* str7 =(unsigned char*) "\xd5\x18\x61\x03\x1e\x1c\x95\x3a\x62\xc2\x93\x8b\x39\x62\x35\xb1\xf3\x64\x94\x2f\x33\x95\x42\x23\xd3\x6c\x26\x88\xab\x2a\x3f\x47\x94\x28\xb4\x46\xa5\x09\x04\x21\xac\x1f\x82\xba\xb4\xb3\x28\x4e\xc0\xbc\xef\x53\xfc\x43\x31\x5c\xda\x7c\x83\xd0\xfa\x90\xb5\x9f";

void brainfuck(char*, int*);
void AES128_CBC_encrypt_buffer(char*, char*, int, int[], int[]);

void brainfuck(char* str8, int temp[]) {
uint8_t*i2 = (uint8_t*)temp;
while (*str8 != 0) {
char i1 = *str8;
str8++;
if (i1 > 45)//如果不为+或者-
{
if (i1 == 46)
{

}
else if (i1 == 60) { // 如果为<
i2 = i2 - 1;
// *i2 = *i2 - 1;
}
else if (i1 == 62) // 如果为>
{
i2 = i2 + 1;
// *i2 = *i2 + 1;
}
}
else if (i1 == 43) {
*i2 = (*i2) + 1;
}
else if (i1 == 45) {
*i2 = (*i2) - 1;
}
}
}
void check(char* inputString) {

puts(inputString);
int i3[8];
i3[4] = -1993905352;
i3[5] = -1182021347;
i3[6] = 0x7dae5713;
i3[7] = -536244034;
i3[0] = 50462976;
i3[1] = 117835012;
i3[2] = 185207048;
i3[3] = 252579084;
int *i2 = &i3[4];
int strLength = strlen(inputString);
brainfuck(str8, &i3[4]);
int i, j;
uint8_t* key = (uint8_t*)i2;
for (i = 0; i < 4; i++) {
for (j = i * 4; j<i * 4 + 4; j++)
printf("\\x%02x", key[j]);
}
printf("\n");
unsigned char* mallocString = (unsigned char*)malloc(65*sizeof(unsigned char));
memset(mallocString, '\x00', 65);

uint8_t* iv = (uint8_t*)i3;
AES128_CBC_decrypt_buffer((uint8_t *)mallocString, (uint8_t*) str7, 64, (uint8_t*) key, (uint8_t*)iv);
printf("%s", mallocString);
}
int main() {
check("test");
return 0;
}

这一题真的是极限做出啊好吧!!!我还以为有机会回到30名以内的QvQ。。。

easyre

观察程序逻辑,此程序中多次调用了系统操作,猜测是重构了loader部分的逻辑(?)。仔细观察,会发现尝试打开/proc/pid/exe文件,并且对文件本身进行一定的检测。

什么是/proc?

/proc目录是一种文件系统,即proc文件系统。与其它常见的文件系统不同的是,/proc是一种伪文件系统(也即虚拟文件系统),存储的是当前内核运行状态的一系列特殊文件,用户可以通过这些文件查看有关系统硬件及当前正在运行进程的信息,甚至可以通过更改其中某些文件来改变内核的运行状态。
/proc/pid/exe 包含了正在进程中运行的程序链接,也就是说这个exe实际上是当前pid对应的程序。


在检查文件没有问题后,该程序逻辑会强行执行一个unlink,由于本身并没有文件链接,于是会进入【创建一个可执行程序】的逻辑:

注意,在逻辑后方有一处对程序关键代码进行校验的地方(如下),在不进行任何操作下,这个校验一定是不能通过的。

但是这个步骤发生的时候,我们需要的文件已经生成了。于是此时我们可以直接拷贝一份文件出来(生成的文件因为忙碌不能使用),使用IDA分析:

(右边生成的文件,左边是拷贝的)

逻辑相比清晰多了:利用管道让两个进程间通信,子进程结束后,父进程就进入lol函数进行flag的处理。lol部分有一点麻烦,于是直接用gdb进行调试:

同样有一个坑点,显然是出题人故意不让程序流执行过去,如果我们绕过了这个逻辑后,会发现有一个输出:

这个就是处理后的flag:

1
rhelheg

话说这题很简单嘛???为什么我看最后就剩100多分。。。我其实并没有看懂逻辑来着。。。

更新

真tm的蠢啊我!!!!!!这个是一个UPX啊啊啊啊:

摔!!!!!!!!!!!!!!

最后吐槽一下

简直是pwn的转型啊!我pwn一题都没做啊摔!!!