队伍成绩:学生组第一
这比赛确实没啥难度,毕竟只能省里的打,属于是圈地自萌了,和同期的祥云杯完全不是一个难度的。
lcs
HMGK-being_hacked[一血]
这题刚开始没啥头绪,想找tls流量结果全是正常的网站访问流量,然后按流量排序发现有一堆icmp流量,这在正常流量包中并不常见。
那就筛选一下icmp请求包,因为一般返回包没啥东西:icmp.type == 8
。
然后就发现每个包的长度都不太一样。
然后102
正好是f
的ascii码,但是后面几个就对应不上了。
这时候发现icmp请求包里面的seq
并不是顺序发送的,于是尝试按seq
排序。
结果发现不止一个seq==1
,比对了下id==1
的才是f
,那就筛选icmp.type == 8 && icmp.ident == 1
。
提取出来就是102 108 97 103 123 50 51 51 51 51 45 115 111 45 101 97 115 121 45 105 99 109 112 125
,转成文本就是flag{23333-so-easy-icmp}
。
flag{23333-so-easy-icmp}
HNGK-LAD Cal[三血]
做这题的时候其实对于plc是一无所知的,于是就下了个STEP 7 Micro/WIN SMART
,然后手动把梯形图给重新画了一遍。
然后发现这软件很贴心的有每个运算的中文说明。
于是就一个一个运算用js
模拟过去。
碰到未知的变量就开始倒着推。
然后V13=40
的BCD
编码是01000000
,所以flag就出来了。
flag{V8_80_V11_5_V1_01000000}
Reverse
HNGK-数独[二血]
这题主要就是for循环解析输入,然后4110e6
里面处理输入去填入数独,看看竖着、横着、3*3
的是不是都满足和为45==1+...+9
。
但是这题有一点需要注意,在for循环解析输入的时候,实际上并没有走v10[i] = Str[v11] - 48
这一句,因为上面有个try/except
操作,然后直接下一句就访问异常内存地址触发了,所以需要去看except
的逻辑,ida这里没反编译出来,得看汇编。
实际上也不复杂,就是在off_41A000[4*9]
里面循环查找输入字符的下标(起始值是1).
所以把41A028
的数独提取出来在线解一下。
所以输入的下标就是5619238183457621978469254539786692871328563671281793452
。
那么写个脚本去找每个下标对应的字符就行。
a = [i-ord('0') for i in b"5619238183457621978469254539786692871328563671281793452"]
mask = [
"{hXxAjYka",
"cX+8>h#<Z",
"9g<dqAQNx",
"Zp4Ccq3QL",
"jorvP+Ox3",
"oPOBiHD5r",
";gHuRa@yI",
"fmO2=K9br",
"ZMeO6Qz2K",
]
for i, x in enumerate(a):
print(mask[i%9][x-1], end="")
flag{Ah9LoOyf2X8q3+P;rzk8ALoiu=ea#Nq+rgbz{+gQPHHKz{XNZOrH26h}
HNGK-py字节码[一血]
这题其实没啥好说的,因为没有很冷门的字节码,都是挺常见的写法,所以边写正向脚本边python -m dis task.py
比对就行。
然后就写了个和题目给的字节码一模一样的正向脚本出来。
a = 17
b = 13
def rand():
global seed
seed = (a * seed + b) % 128
return seed
print('please input your flag:')
flag = str(input())
assert len(flag) >= 20
seed = ord(flag[19])
enc = [102, 3, 46, 0, 78, 102, 103, 57, 116, 63, 110, 127, 121, 59, 57, 33, 49, 11, 110, 18, 6]
data = [102, 50, 35, 35, 35, 17, 67, 35, 69, 35, 51, 34, 35, 69, 35, 69, 35, 51, 34, 35, 153]
for i in range(len(flag)):
tmp = data[i] ^ i ^ (rand() % 128)
data[i + 1] = ord(flag[i]) ^ tmp
if data[i + 1] != enc[i + 1]:
print('error!')
exit(0)
print('flag is %s' % flag)
然后这逻辑也不难,主要就是拿flag[19]
当随机数种子,然后每轮用data[i]
去异或下标和随机结果,再异或flag就得到了加密结果和新的data[i+1]
。
那就写个爆破种子的脚本。
a = 17
b = 13
def rand():
global seed
seed = (a * seed + b) % 128
return seed
for f0 in range(128):
seed = f0
enc = [102, 3, 46, 0, 78, 102, 103, 57, 116, 63, 110, 127, 121, 59, 57, 33, 49, 11, 110, 18, 6]
data = [102, 50, 35, 35, 35, 17, 67, 35, 69, 35, 51, 34, 35, 69, 35, 69, 35, 51, 34, 35, 153]
i = 0
tmp = data[i] ^ i ^ (rand() % 128)
if chr(enc[i + 1] ^ tmp) == 'f':
print('seed', f0)
可以得知种子是22(但是22好像不是可见字符...咋可能是flag[19]
的值,感觉是题目有问题)
然后用种子去反向计算每轮的加密前的值,即为flag。
a = 17
b = 13
def rand():
global seed
seed = (a * seed + b) % 128
return seed
for f0 in range(128):
seed = f0
enc = [102, 3, 46, 0, 78, 102, 103, 57, 116, 63, 110, 127, 121, 59, 57, 33, 49, 11, 110, 18, 6]
data = [102, 50, 35, 35, 35, 17, 67, 35, 69, 35, 51, 34, 35, 69, 35, 69, 35, 51, 34, 35, 153]
i = 0
tmp = data[i] ^ i ^ (rand() % 128)
if chr(enc[i + 1] ^ tmp) == 'f':
print('seed', f0)
break
seed = f0
enc = [102, 3, 46, 0, 78, 102, 103, 57, 116, 63, 110, 127, 121, 59, 57, 33, 49, 11, 110, 18, 6]
data = [102, 50, 35, 35, 35, 17, 67, 35, 69, 35, 51, 34, 35, 69, 35, 69, 35, 51, 34, 35, 153]
flag = bytearray(len(data)-1)
for i in range(len(flag)):
tmp = data[i] ^ i ^ (rand() % 128)
flag[i] = enc[i + 1] ^ tmp
data[i + 1] = enc[i + 1]
print(flag)
flag{Pyth0n_1s_yyds}
HNGK-xxtea[二血]
有轻微的混淆,不过没啥影响,140004038
的值就是switch
顺序。也就是acb08edf
。
a
只是在输出东西,不用管。
c
在接收输入然后计算长度并判断。
b
的话好像就是换了个地方存输入。
0
走默认,因为没定义,直接就开始xxtea
了。
然后8
这边就开始判断加密结果是否一致了。
这题既然是考xxtea
,那大概率又是改了魔数或者算法细节,所以就用偷懒办法。
先随便输入点符合长度的东西(4的倍数就行),跑到8
这边if
判断的时候下个断点看看预期结果v87
是啥。
rdx
指向的这块内存就是预期结果,只不过是倒着的,因为下标是--v87
递减的。
然后还要来个^0x56565656
的操作才是加密结果,所以写个脚本拿结果。
a = bytearray(bytes.fromhex("DFD2F732"+"817EC5C1"+"C11C8097"+"055BA086"+"07F5F7FA"+"9D36F9EA"))
for i in range(len(a)):
a[i] ^= 0x56
print(a.hex())
加密结果为8984a164d7289397974ad6c1530df6d051a3a1accb60afbc
。
然后再重新跑一下,去xxtea
函数开头下个断点,再随便输入点满足长度的东西(这回长度要确保是和预期一样,因为待会要替换)。
然后根据ida给的传参寄存器情况,数据指针在rcx
,加解密标识符在edx
,密钥在r8
。
咱们要把数据替换成上面的加密结果,然后标识改成负的。
然后运行到返回,数据就变成了加密之前的,也就是flag。
flag{W31C0M3_To_Rev3rs3}
HNGK-反调试[三血]
这题其实看着逻辑挺简单的,就是把输入给* $41A040 % 1031
,然后确保是等于1
就完事,但是写了脚本发现解不出啥东西。
b = [
0x0110, 0x03f8, 0x02f5, 0x039b,
0x017b, 0x00f7, 0x024f, 0x0036,
0x019f, 0x0117, 0x014d, 0x028a,
0x0366, 0x00eb, 0x039b, 0x0117,
0x00b7, 0x00eb, 0x00b0, 0x0036,
0x0117, 0x0162, 0x027f, 0x0047,
]
for x in b:
for i in range(10, 128):
if x * i % 1031 == 1:
print(chr(i), end="")
break
于是猜测$41A040
可能被修改过。
但是它的引用只有刚才那一处。
然后下面也没有其他变量命名了,所以就往上找找引用,可能会存在下标初始值大于0的情况。
8字节开外还真有一处。
跟过去一看,确实是走了不止8字节的步长,所以确实被修改了。
所以把^ 0x66
这个操作给不上,成功得到flag。
b = [
0x0110, 0x03f8, 0x02f5, 0x039b,
0x017b, 0x00f7, 0x024f, 0x0036,
0x019f, 0x0117, 0x014d, 0x028a,
0x0366, 0x00eb, 0x039b, 0x0117,
0x00b7, 0x00eb, 0x00b0, 0x0036,
0x0117, 0x0162, 0x027f, 0x0047,
]
for x in b:
for i in range(10, 128):
if (x ^ 0x66) * i % 1031 == 1:
print(chr(i), end="")
break
flag{@nt1_d3bug_Ju5t_s0}
HNGK-签到
这题upx特征明显,直接upx -d
脱。
打开后发现命名空间有混淆,不过问题不大,大致看出来逻辑。
dwfsxe::dwfsxe
和handvfiu::handvfiu
是初始化预期的运算数据,分别存到v7
和v8
里面,然后接收到输入存进v9
,再复制到v5
,然后用B::cewrwe23rf
去运算三个数据,然后里面顺带比较就完事。
所以直接在B::cewrwe23rf
函数开头下个断点,看看初始化的数据是啥。
先看输入的,在rdi
,但是过去看了下,实际上是往下走32字节才是,应该是命名空间也占了一些内存。
所以v7
的rsi
也要往下找。
然后v8
的rdx
也是同样的道理。
然后具体看比较的时候指针是啥。
所以v7
是0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()_+
,v8
是hP&p0!5L^#3NXLs@*QR%L&UN!L)0%Q^
。
按照函数的逻辑写个脚本成功得到flag。
v7 = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()_+"
v8 = b"hP&p0!5L^#3NXLs@*QR%L&UN!L)0%Q^"
for i in range(31):
print(chr(v7.index(v8[i])+ord('0')), end="")
flag{ActI0n5_sp3ak_Louder_than_w0rds}