HGAME 2023 Week1
文章目录
前言
Week1的大多数题对我这样的萌新还是很友好的,虽然但是还是很多没解出来,不过还是学到了蛮多东西,浅浅记录一下
Web
Classic Childhood Game
题目描述:兔兔最近迷上了一个纯前端实现的网页小游戏,但是好像有点难玩,快帮兔兔通关游戏!
-
打开发现是一个 JavaScript 前端小游戏,然后在源代码中找一下可能是通关的 js 文件
-
也可以几个都打开看看里面写的啥,最终可以发现 Events.js 里包含各种事件,那么就能找到关于通关事件的那段代码,也就知道flag怎么藏的了
-
在这个 js 文件中第607行开始,我们通过文字提示可以判断出,这后面是多种通关结局,也就是都能拿到flag,而且他们都调用了一个前面没有看到的函数 mota() ,在文件末我们可以看到 mota() 写的什么。
function mota() { var a = ['\x59\x55\x64\x6b\x61\x47\x4a\x58\x56\x6a\x64\x61\x62\x46\x5a\x31\x59\x6d\x35\x73\x53\x31\x6c\x59\x57\x6d\x68\x6a\x4d\x6b\x35\x35\x59\x56\x68\x43\x4d\x45\x70\x72\x57\x6a\x46\x69\x62\x54\x55\x31\x56\x46\x52\x43\x4d\x46\x6c\x56\x59\x7a\x42\x69\x56\x31\x59\x35']; (function (b, e) { var f = function (g) { while (--g) { b['push'](b['shift']()); } }; f(++e); }(a, 0x198)); var b = function (c, d) { c = c - 0x0; var e = a[c]; if (b['CFrzVf'] === undefined) { (function () { var g; try { var i = Function('return\x20(function()\x20' + '{}.constructor(\x22return\x20this\x22)(\x20)' + ');'); g = i(); } catch (j) { g = window; } var h = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; g['atob'] || (g['atob'] = function (k) { var l = String(k)['replace'](/=+$/, ''); var m = ''; for (var n = 0x0, o, p, q = 0x0; p = l['charAt'](q++); ~p && (o = n % 0x4 ? o * 0x40 + p : p, n++ % 0x4) ? m += String['fromCharCode'](0xff & o >> (-0x2 * n & 0x6)) : 0x0) { p = h['indexOf'](p); } return m; }); }()); b['fqlkGn'] = function (g) { var h = atob(g); var j = []; for (var k = 0x0, l = h['length']; k < l; k++) { j += '%' + ('00' + h['charCodeAt'](k)['toString'](0x10))['slice'](-0x2); } return decodeURIComponent(j); }; b['iBPtNo'] = {}; b['CFrzVf'] = !![]; } var f = b['iBPtNo'][c]; if (f === undefined) { e = b['fqlkGn'](e); b['iBPtNo'][c] = e; } else { e = f; } return e; }; alert(atob(b('\x30\x78\x30'))); }
-
不难发现这是一个解码的函数,直接在控制台调用 mota() 函数即可拿下flag,也可以根据他的代码逻辑解码出 flag ,unicode+base64解码
Become A Member
题目描述:学校通知放寒假啦,兔兔兴高采烈的打算购买回家的车票,这时兔兔发现成为购票网站的会员账户可以省下一笔money…… 想成为会员也很简单,只需要一点点HTTP的知识……等下,HTTP是什么,可以吃吗
-
根据题目描述是考察的 HTTP 知识,身份证明很容易联想到 User-Agent 消息头或者 Cookie ,但是只给出了一个名字,修改UA头为
User-Agent: Cute-Bunny
进入下一个页面 -
这里给出了两个单词,可以凑成键值对,可以联想到 Cookie,根据句意修改为
Cookie: code=Vidar
进入下一个界面
-
这个就很明确了,添加 Referer 头为
Referer: bunnybunnybunny.com
进入下一页面
-
考察本地请求,这个消息头很多比如 x-remote-IP、X-Real-IP、Client-Ip等,这里添加一个经典的
X-Forwarded-For: 127.0.0.1
即可
-
最后改完长个样子
-
拿到会员账号的键值对,要求我们用 json请求方式,我们用Burp Suite拦截后添加 json 数据,因为大多数时候使用POST方法提交json数据,我也是这么做的,但没有得到想要的结果,询问了出题人才知道请求方法不对,想了好久才发现改成GET即可,又拓宽了眼界
Guess Who I Am
题目描述:刚加入Vidar的兔兔还认不清协会成员诶,学长要求的答对100次问题可太难了,你能帮兔兔写个脚本答题吗?
-
很容易看出这是需要我们根据这些标签对应 Vidar 的成员,然后正确提交上去100次即可(其实100次用手工还是蛮快的,还不会下面内容的时候本人就是用手工做的,也就十多二十分钟)
-
查看源码可以发现这些标签没有在源码中,这是为啥捏?其实这个网站的内容由前端的 JS 动态生成,由于呈现在网页上的内容是由 JS 生成而来,我们能够在浏览器上看得到,但是在HTML源码中却发现不了。
-
在源码中有一条Hint,进入网址后可以发现是很多的 json 数据,这就是我们找到对应成员名字的”钥匙“
-
那么思路就清晰了,爬取标签内容 -> 找到 json 数据中对应成员 -> 提交给网页,那么逐一突破即可,具体可以参考这篇文章:
网络爬虫(1)----爬取JS动态数据(上) -
下面给出 exp:
import requests import json host = "http://week-1.hgame.lwsec.cn:32374/" getQ = host + "api/getQuestion" getS = host + "api/getScore" postA = host + "api/verifyAnswer" # 打开文本,这里是从HINT给的那个网站上复制下来的,然后做了手工处理方便后续代码执行, #一是把最外层的方括号和那串字符去掉了,二是把最后一个花括号去掉了,三是把avatar的值有require的把他处理成双引号包含的字符,因为字典没有长这样的 #经处理就长下面举例的这样,总之,不管怎样,我们的目的就是把这些转换成易于处理的字典,只是我想到的处理手段不同 ''' { "id": "ba1van4", "intro": "21级 / 不会Re / 不会美工 / 活在梦里 / 喜欢做不会的事情 / ◼◻粉", "avatar": "https://thirdqq.qlogo.cn/g?b=sdk&k=kSt5er0OQMXROy28nzTia0A&s=640", "url": "https://ba1van4.icu" }, { "id": "yolande", "intro": "21级 / 非常菜的密码手 / 很懒的摸鱼爱好者,有点呆,想学点别的但是一直开摆", "avatar": "https://thirdqq.qlogo.cn/g?b=sdk&k=rY328VIqDc7lNtujYic8JxA&s=640", "url": "https://y01and3.github.io/" ''' with open(r"C:\Users\admin\Desktop\1.txt", "r", encoding='utf-8') as f: data = f.read() # 读取文本 members = list(map(str, data.split('},'))) session = requests.session() # 保持会话,避免每次请求Cookie不同 for i in range(101): Q = json.loads(session.get(getQ).text)['message'] # 将获取到的json数据转换成字典 S = json.loads(session.get(getS).text)['message'] print('您要回答的问题 '+Q) print('您现在的分数为 '+str(S)) for member in members: member += '}' dic = json.loads(member) if dic['intro'] == Q: print('恭喜你找到了 '+dic['id']) session.post(postA, data={'id': dic['id']}) print('-----------------------------------------------------------')
Show Me Your Beauty
题目描述:登陆了之前获取的会员账号之后,兔兔想找一张自己的可爱照片,上传到个人信息的头像中 😄 不过好像可以上传些奇怪后缀名的文件诶 XD
-
根据题目描述,很明显是文件上传漏洞,而且应该是后缀名过滤
-
随便上传一个图片文件(主要是懒得和前端验证打交道),并使用Burp Suite拦截,发送到Repeater,修改文件后缀名、MIME类型,消息body,可以用 GIF89a 文件头防止有检测,然后写上一句话木马即可
-
这里后缀名尝试用等价的 .php2, .php3, .phtml 均上传失败,尝试用大小写绕过 .PHP,上传成功并返回了保存的路径,用蚁剑连接即可,flag 在根目录下
Misc
Sign In
题目描述:欢迎参加HGAME2023,Base64解码这段Flag,然后和兔兔一起开始你的HGAME之旅吧,祝你玩的愉快! aGdhbWV7V2VsY29tZV9Ub19IR0FNRTIwMjMhfQ==
- 价值1pt的签到题,base64解码就得到flag了,可惜师傅们太热情了把平台挤爆了,没能和师傅们拼搏手速了
*Where am I
题目描述:兔兔回家之前去了一个神秘的地方,并拍了张照上传到网盘,你知道他去了哪里吗? flag格式为: hgame{经度时_经度分_经度秒_东经(E)/西经(W)_纬度时_纬度分_纬度秒_南纬(S)/北纬(N)},秒精确到小数点后两位 例如: 11°22’33.99’‘E, 44°55’11.00’'S 表示为 hgame{11_22_3399_E_44_55_1100_S}
- 看题目描述预测是OSINT,附件是一个 .pcapng 流量包,用 binwalk 分离或者手工用 Wireshark 查看导出HTTP对象,是一个RAR文件
- 直接解压提示里面的图片文件头损坏,并且弹出需要解压密码,猜想是修复损坏的压缩包然后伪加密或者爆破密码得到图片文件,但我暂时没做出来。(最后本周比赛结束,了解到只是rar伪加密,解出来之后,图片右键属性,里面有GPS信息,就能做出来了)
神秘的海报
题目描述:坐车回到家的兔兔听说ek1ng在HGAME的海报中隐藏了一个秘密…(还记得我们的Misc培训吗?
-
附件是一个海报图片,图片中没有看到什么特别的信息,右键查看属性也没有有效信息,那用 binwalk 分离看看有没有包含其他文件,答案是没有的,既然是png文件那再用zsteg试试,看看有没有隐写数据
-
可以看到有一段英文文本,虽然不完整,但可以判断出是 LSB 隐写,使用强大的 stegsolve 提取 LSB 隐写数据即可
-
这里给出了前一部分flag,另一部分需要前往指定网址下载音频,题目中明确指出其使用steghide隐写工具加密,密钥是一个6位数,搜索引擎搜索其解密工具也很容易就不再赘述了
-
这里我做的时候小猜了一手123456,没想到真是这个密钥,一次猜对,非常快乐!但是如果不是这个密钥,其实也可以写脚本爆破,下面给出exp(需要下载好 steghide )
# -*- coding: utf-8 -*- # @Author : ph0ebus import os def foo(): File = "Bossanova.wav" # steghide加密的文件 errors = ['could not extract', 'steghide --help', 'Syntax error'] # 密钥错误的情况 cmdFormat = r'steghide extract -sf "%s" -p "%d" 2>&1' # steghide解密,2>&1是让标准错误输出重定向到标准输出,否则后续读取不到错误信息 #Windows环境下:cmdFormat = r'F:\CTFtools\steghide-0.5.1-win32\steghide\steghide.exe extract -sf "%s" -p "%d" 2>&1',或者添加到系统变量 for passwd in range(100000, 999999): # 六位数密钥逐一尝试 cmd = cmdFormat % (File, passwd) text = os.popen(cmd).read() # 执行系统命令 print('进度:%d / 999999' % passwd) # 输出当前爆破进度 for err in errors: if err in text: break else: print(text) content.close() print('the passphrase is %s' % passwd) return if __name__ == '__main__': foo() pass
-
不得不说真的慢,即使在Linux环境下我用这个脚本还是跑了差不多三四个小时才跑到123456,目前考虑用多线程爆破加快进度
e99p1ant_want_girlfriend
题目描述:兔兔在抢票网站上看到了一则相亲广告,人还有点小帅,但这个图片似乎有点问题,好像是 CRC 校验不太正确?
-
附件是某帅哥图片,根据题目描述是 CRC 校验)出现问题,这样的图片在 Windows 下可以打开,但是在 Linux 下会报错或显示空白,这是因为图片宽高被修改,和 CRC 校验码(循环冗余校验码)不对应造成的
-
所以我们可以通过遍历宽高看是否对应 CRC 码的方式来爆破宽高,下面给出来自大佬的exp:
import zlib import struct filename = r"C:\Users\admin\Downloads\e99p1ant_want_girlfriend\e99p1ant_want_girlfriend.png" with open(filename, 'rb') as f: all_b = f.read() crc32key = int(all_b[29:33].hex(),16) data = bytearray(all_b[12:29]) n = 4095 #理论上0xffffffff,但考虑到屏幕实际/cpu,0x0fff就差不多了 for w in range(n): #高和宽一起爆破 width = bytearray(struct.pack('>i', w)) #q为8字节,i为4字节,h为2字节 for h in range(n): height = bytearray(struct.pack('>i', h)) for x in range(4): data[x+4] = width[x] data[x+8] = height[x] crc32result = zlib.crc32(data) if crc32result == crc32key: print("宽为:",end="") print(width) print("高为:",end="") print(height) exit(0)
-
更多关于png图片文件结构分析可以看这篇博客:https://www.cnblogs.com/mengfanrong/p/3801583.html
更多关于图片CRC校验码爆破宽高原理可以看这篇:https://www.cnblogs.com/yunqian2017/p/14449346.html
Pwn
test_nc
- 获取题目环境后,在Linux环境使用命令行
nc week-1.hgame.lwsec.cn 30098
,然后ls
查看当前目录文件,cat flag
读取当前目录下名为flag的文件内容
overflow
-
看题目就能猜到这是一道简单的溢出题,用Die查看发现是64位文件,用ida64打开,F5查看伪代码如下
-
非常简短,
close(1)
意味着stdout(标准输出)关闭。程序能够拿到shell,如果程序关闭了stdout,则会无法正常得到回显。这时可以通过执行exec 1>&0
或exec 1>&2
,将标准输出重定向到标准输入或标准输出错误从而得到回显。 -
后面还有一个
read
操作,大小为0x100uLL。双击进入变量查看
-
从s到buf是
0000000000000000 - 0000000000000010
个地址(注意是16进制),也就是0x10个地址,这是s到buf的。从read到s0000000000000008 - 0000000000000000
个地址,也就是0x08个地址,这是从s到r的。两者相加,一共0x18个地址,想到栈溢出,把这些个地址用字符堵死,后续就能伪造执行的read。 -
问题来到read,我们肯定需要读取flag,但并没有直接
system("cat flag");
的函数,但是很容易发现有一个 b4ckd0or 的后门函数,双击进去后发现函数能拿到/bin/sh
的权限,正合我意,那么就去 Exports 窗口中找到 b4ckd0or 函数的地址,完整地址是:00000000000401176
,这里其实只取后八位就可以:00401176
,也就是0x00401176
。用脚本把前面0x88个空间打死然后把这个函数地址用p64(0x00401176)
拼接上,就可以让read拿到我们的/bin/sh权限了。最终,确定了通过栈溢出执行 b4ckd0or 从而得到shell
-
编写exp:
from pwn import * r = remote("week-1.hgame.lwsec.cn", 31246) payload = 'A' * 0x18 + p64(0x00401176).decode('unicode_escape') # 堵死0x18个空间然后拼接上b4ckd0or函数地址, r.sendline(payload) r.interactive()
我这里使用p64()报错
TypeError: can only concatenate str (not “bytes“) to str
,参考了该解决方法,所以后面有.decode('unicode_escape')
解决办法:https://blog.csdn.net/qq_39772215/article/details/110294527 -
最后根据上面执行脚本和命令,这里python用的Windows环境
Re
test your IDA
题目描述:签到
-
这道题可以用IDA打开即可看到明文的flag
-
甚至不用打开IDA,用Die查看文件的字符串也可以
-
除此之外,还可以使用010Editor、winHEX等强大的编辑器搜索到
easyasm
题目描述:非常简单的汇编
; void __cdecl enc(char *p)
.text:00401160 _enc proc near ; CODE XREF: _main+1B↑p
.text:00401160
.text:00401160 i = dword ptr -4
.text:00401160 Str = dword ptr 8
.text:00401160
.text:00401160 push ebp
.text:00401161 mov ebp, esp
.text:00401163 push ecx
.text:00401164 mov [ebp+i], 0
.text:0040116B jmp short loc_401176
.text:0040116D ; ---------------------------------------------------------------------------
.text:0040116D
.text:0040116D loc_40116D: ; CODE XREF: _enc+3B↓j
.text:0040116D mov eax, [ebp+i]
.text:00401170 add eax, 1
.text:00401173 mov [ebp+i], eax
.text:00401176
.text:00401176 loc_401176: ; CODE XREF: _enc+B↑j
.text:00401176 mov ecx, [ebp+Str]
.text:00401179 push ecx ; Str
.text:0040117A call _strlen
.text:0040117F add esp, 4
.text:00401182 cmp [ebp+i], eax
.text:00401185 jge short loc_40119D
.text:00401187 mov edx, [ebp+Str]
.text:0040118A add edx, [ebp+i]
.text:0040118D movsx eax, byte ptr [edx]
.text:00401190 xor eax, 33h
.text:00401193 mov ecx, [ebp+Str]
.text:00401196 add ecx, [ebp+i]
.text:00401199 mov [ecx], al
.text:0040119B jmp short loc_40116D
.text:0040119D ; ---------------------------------------------------------------------------
.text:0040119D
.text:0040119D loc_40119D: ; CODE XREF: _enc+25↑j
.text:0040119D mov esp, ebp
.text:0040119F pop ebp
.text:004011A0 retn
.text:004011A0 _enc endp
Input: your flag
Encrypted result: 0x5b,0x54,0x52,0x5e,0x56,0x48,0x44,0x56,0x5f,0x50,0x3,0x5e,0x56,0x6c,0x47,0x3,0x6c,0x41,0x56,0x6c,0x44,0x5c,0x41,0x2,0x57,0x12,0x4e
-
预备知识:
eip:运行程序下一个指令的地址
ebp:栈的基地址指针,指向栈底
esp:栈的栈顶指针,指向栈顶
push:入栈,esp 指向地址减 4
move:数据传送指令,完成赋值
jmp:无条件跳转指令,转移到指令指定的地址执行相应的指令
jge:大于或等于转移指令,用于对比内存中两个对象的大小关系
cmp:比较指令,执行从目的操作数中减去源操作数的隐含减法操作,并且不修改任何操作数
EAX:“累加器”(accumulator), 它是很多加法乘法指令的缺省寄存器。
EBX:“基地址”(base)寄存器, 在内存寻址时存放基地址。
ECX:计数器(counter), 是重复(REP)前缀指令和LOOP指令的内定计数器。
EDX:用来放整数除法产生的余数
-
可以看出这应该是一个从 ida 上扒下来的汇编代码,通过阅读代码可以发现,这个程序是将明文逐位和 0x33 异或得到的密文,那就直接利用异或的性质即可解密明文,下面给出 exp:
c = [0x5b, 0x54, 0x52, 0x5e, 0x56, 0x48, 0x44, 0x56, 0x5f, 0x50, 0x3, 0x5e, 0x56, 0x6c, 0x47, 0x3, 0x6c, 0x41, 0x56, 0x6c, 0x44, 0x5c, 0x41, 0x2, 0x57, 0x12, 0x4e] for i in range(len(c)): c[i] ^= 0x33 print(bytes(c))
easyenc
-
放入 ida 中,F5查看伪代码如下
int __cdecl main(int argc, const char **argv, const char **envp) { __int64 v3; // rbx __int64 v4; // rax char v5; // al char *v6; // rcx int v8[10]; // [rsp+20h] [rbp-19h] char v9; // [rsp+48h] [rbp+Fh] __int128 v10[3]; // [rsp+50h] [rbp+17h] BYREF __int16 v11; // [rsp+80h] [rbp+47h] v8[0] = 167640836; v8[1] = 11596545; v11 = 0; v8[2] = -1376779008; memset(v10, 0, sizeof(v10)); v3 = 0i64; v8[3] = 85394951; v8[4] = 402462699; v8[5] = 32375274; v8[6] = -100290070; v8[7] = -1407778552; v8[8] = -34995732; v8[9] = 101123568; v9 = -7; sub_140001064("%50s"); v4 = -1i64; do ++v4; while ( *((_BYTE *)v10 + v4) ); if ( v4 == 41 ) { while ( 1 ) { v5 = (*((_BYTE *)v10 + v3) ^ 0x32) - 86; *((_BYTE *)v10 + v3) = v5; if ( *((_BYTE *)v8 + v3) != v5 ) break; if ( ++v3 >= 41 ) { v6 = "you are right!"; goto LABEL_8; } } v6 = "wrong!"; LABEL_8: sub_140001010(v6); }c return 0; }
-
逐行分析伪代码的逻辑,写上通俗易懂的注释
int __cdecl main(int argc, const char **argv, const char **envp) { //定义数据 __int64 v3; // rbx __int64 v4; // rax char v5; // al char *v6; // rcx int v8[10]; // [rsp+20h] [rbp-19h] char v9; // [rsp+48h] [rbp+Fh] __int128 v10[3]; // [rsp+50h] [rbp+17h] BYREF __int16 v11; // [rsp+80h] [rbp+47h] //初始化值 v8[0] = 167640836; v8[1] = 11596545; v11 = 0; v8[2] = -1376779008; memset(v10, 0, sizeof(v10)); // void *memset(void *str, int c, size_t n) 复制字符 c(一个无符号字符)到参数 str 所指向的字符串的前 n 个字符 v3 = 0i64; v8[3] = 85394951; v8[4] = 402462699; v8[5] = 32375274; v8[6] = -100290070; v8[7] = -1407778552; v8[8] = -34995732; v8[9] = 101123568; v9 = -7; sub_140001064("%50s"); // scanf() 输入字符串 v4 = -1i64; // 计算输入的字符串长度 do ++v4;(_BYTE *) while ( *((_BYTE *)v10 + v4) ); //(_BYTE *)v10就是用于操作v10每一个byte的指针(int类型有4个byte,32位bit) // 输入字符串每一byte数据处理后逐一和v8每一byte数据对比 if ( v4 == 41 ) { while ( 1 ) { v5 = (*((_BYTE *)v10 + v3) ^ 0x32) - 86; *((_BYTE *)v10 + v3) = v5; if ( *((_BYTE *)v8 + v3) != v5 ) break; if ( ++v3 >= 41 ) // 两者每一位byte值都相同则flag正确 { v6 = "you are right!"; goto LABEL_8; } } v6 = "wrong!"; LABEL_8: sub_140001010(v6); } return 0; }
-
所以就很清晰了,只需要把 v8 的每一 byte 提取出来,然后利用异或的性质解出明文即可,那么问题来到了怎么提取出 int 类型每一位 byte 值。比如 0x12345678 它的每一位 byte 值就是 0x78、0x56、0x34、0x12 ,可能看到这里会有人发出疑问:为什么你这个不是 0x12 在前面,而要反着写呢?(没有疑问可以在此地休息一手)
-
要解决这个问题,先要引入“字节序”的概念,参考链接
字节序,又称端序或尾序,指的是多字节数据在计算机内存中的存放顺序。例如一个 int 型变量 x 占用4个字节,假设它的起始地址 &x 为 0x10,那么x将会被存储在 0x10、0x11、0x12 和 0x13 位置上。在用 C++ 写的客户端和 Java 写的服务端的通信时,发现数据通过 TCP 连接传输后收到的与发送的不一致,所以要引入大端和小端的概念。以一个两字节 short 型变量 0x0102 的存储举例:
- 大端字节序:高位字节在前,低位字节在后,01|02,符合人们的读写习惯。
- 小端字节序:低位字节在前,高位字节在后,02|01。
那为什么不统一使用符合人们读写习惯的大端字节序呢,这是因为计算机电路先处理低位字节,效率比较高,因为计算都是从低位开始的,所以计算机的内部处理都是小端字节序。但是人类还是习惯读写大端字节序,所以,除了计算机的内部处理,其他的场合几乎都是大端字节序,比如网络传输和文件储存。
-
好了,回到刚刚讨论的如何提取每一位 byte,可以借助计算器观察观察
-
不难发现可以通过移位操作取出每一 byte 的值
#include <stdio.h> #define GET_LOW_BYTE0(x) ((x >> 0) & 0x000000ff) /* 获取第0个字节 */ #define GET_LOW_BYTE1(x) ((x >> 8) & 0x000000ff) /* 获取第1个字节 */ #define GET_LOW_BYTE2(x) ((x >> 16) & 0x000000ff) /* 获取第2个字节 */ #define GET_LOW_BYTE3(x) ((x >> 24) & 0x000000ff) /* 获取第3个字节 */ int main(void) { unsigned int a = 0x12345678; printf("byte0 = 0x%x\n", GET_LOW_BYTE0(a)); printf("byte1 = 0x%x\n", GET_LOW_BYTE1(a)); printf("byte2 = 0x%x\n", GET_LOW_BYTE2(a)); printf("byte3 = 0x%x\n", GET_LOW_BYTE3(a)); return 0; } /* 运行结果: byte0 = 0x78 byte1 = 0x56 byte2 = 0x34 byte3 = 0x12 */
这也是获取数据各个字节的最常用也最有效的方法。
-
所以可以这样写代码(这里做的时候用C语言写的,这里就不翻译成python了)
#include<stdio.h> #include<string.h> #include<stdlib.h> #define GET_LOW_BYTE0(x) ((x >> 0) & 0x000000ff) /* 获取第0个字节 */ #define GET_LOW_BYTE1(x) ((x >> 8) & 0x000000ff) /* 获取第1个字节 */ #define GET_LOW_BYTE2(x) ((x >> 16) & 0x000000ff) /* 获取第2个字节 */ #define GET_LOW_BYTE3(x) ((x >> 24) & 0x000000ff) /* 获取第3个字节 */ int main() { int v8[10]; v8[0] = 167640836; v8[1] = 11596545; v8[2] = -1376779008; v8[3] = 85394951; v8[4] = 402462699; v8[5] = 32375274; v8[6] = -100290070; v8[7] = -1407778552; v8[8] = -34995732; v8[9] = 101123568; char v5=0; char v6[10]; int v3=0; while ( v3<10) { printf("%c",(GET_LOW_BYTE0(v8[v3])+86)^0x32); printf("%c",(GET_LOW_BYTE1(v8[v3])+86)^0x32); printf("%c",(GET_LOW_BYTE2(v8[v3])+86)^0x32); printf("%c",(GET_LOW_BYTE3(v8[v3++])+86)^0x32); } return 0; }
encode
题目描述:兔兔把自己行李箱的密码用一种编码写在了纸条上,但他忘了怎么解密,你能帮帮他吗?
-
IDA打开,F5查看伪代码
int __cdecl main(int argc, const char **argv, const char **envp) { int v4[100]; // [esp+0h] [ebp-1CCh] BYREF char v5[52]; // [esp+190h] [ebp-3Ch] BYREF int j; // [esp+1C4h] [ebp-8h] int i; // [esp+1C8h] [ebp-4h] memset(v5, 0, 0x32u); memset(v4, 0, sizeof(v4)); sub_4011A0(a50s, (char)v5); for ( i = 0; i < 50; ++i ) { v4[2 * i] = v5[i] & 0xF; v4[2 * i + 1] = (v5[i] >> 4) & 0xF; } for ( j = 0; j < 100; ++j ) { if ( v4[j] != dword_403000[j] ) { sub_401160(Format, v4[0]); return 0; } } sub_401160(aYesYouAreRight, v4[0]); return 0; }
-
开始分析代码逻辑,写上一些注释
int __cdecl main(int argc, const char **argv, const char **envp) { int v4[100]; // [esp+0h] [ebp-1CCh] BYREF char v5[52]; // [esp+190h] [ebp-3Ch] BYREF int j; // [esp+1C4h] [ebp-8h] int i; // [esp+1C8h] [ebp-4h] memset(v5, 0, 0x32u); memset(v4, 0, sizeof(v4)); sub_4011A0(a50s, (char)v5); // scanf() 输入字符串 // 对输入的字符串进行处理,将结果给v4 for ( i = 0; i < 50; ++i ) { v4[2 * i] = v5[i] & 0xF; // 这里和easyenc那道题有点类似,不过char只有一个字节,它将一个字节切成了两部分,每部分四个bit,这是后面那部分,比如0b10010010,这里是0010 v4[2 * i + 1] = (v5[i] >> 4) & 0xF; // 这是前面那部分 } // 将处理后的数据和程序内的数据逐一对比,若完全相同则是正确的flag for ( j = 0; j < 100; ++j ) { if ( v4[j] != dword_403000[j] ) // 因此我们跟进dword_403000,导出它的数据进行逆运算得到flag { sub_401160(Format, v4[0]); return 0; } }c sub_401160(aYesYouAreRight, v4[0]); return 0; }
-
双击打开如图所示,如果直接进行下拉复制,会发现有很多没用的数据,处理起来十分繁琐,当然你也可以正则提取,但是也可以采用其他简单的办法
-
鼠标移到
int dword_40300[100]
那一行,右键选择Array,单击进入
-
然后按下图进行设置就可以愉快的进行复制啦!(其实也可以通过python脚本来复制,Shift+F2 调出 Script 窗口,然后输入代码即可,更多详情参考:安卓逆向|菜鸟的IDA学习笔记:如何简单复制DCB数据)
-
最后得到数据,进行逆运算即可,下面给出 exp:
nums = [8, 6, 7, 6, 1, 6, 0x0D, 6, 5, 6, 0x0B, 7, 5, 6, 0x0E, 6, 3, 6, 0x0F, 6, 4, 6, 5, 6, 0x0F, 5, 9, 6, 3, 7, 0x0F, 5, 5, 6, 1, 6, 3, 7, 9, 7, 0x0F, 5, 6, 6, 0x0F, 6, 2, 7, 0x0F, 5, 1, 6, 0x0F, 5, 2, 7, 5, 6, 6, 7, 5, 6, 2, 7, 3, 7, 5, 6, 0x0F, 5, 5, 6, 0x0E, 6, 7, 6, 9, 6, 0x0E, 6, 5, 6, 5, 6, 2, 7, 0x0D, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] flag = "" for i in range(50): s = (nums[i * 2 + 1] << 4) + nums[i * 2] # 这里要注意运算符的优先级问题! flag += chr(s) print(flag)
Crypto
RSA
题目描述:众所周知,RSA的安全性基于整数分解难题。
from Crypto.Util.number import *
flag = open('flag.txt', 'rb').read()
p = getPrime(512)
q = getPrime(512)
n=p*q
e = 65537
m = bytes_to_long(flag)
c = pow(m, e, n)
print(f"c={c}")
print(f"n={n}")
"""
c=110674792674017748243232351185896019660434718342001686906527789876264976328686134101972125493938434992787002915562500475480693297360867681000092725583284616353543422388489208114545007138606543678040798651836027433383282177081034151589935024292017207209056829250152219183518400364871109559825679273502274955582
n=135127138348299757374196447062640858416920350098320099993115949719051354213545596643216739555453946196078110834726375475981791223069451364024181952818056802089567064926510294124594174478123216516600368334763849206942942824711531334239106807454086389211139153023662266125937481669520771879355089997671125020789
"""
-
这道题非常简单,只要明白RSA加密算法原理和安全性基于“两个大素数相乘很容易,而对得到的积求因子则很难”的数学事实就能做
-
我们可以yafu分解,但这道题给的数字比较大,yafu分解需要大量时间,所以我们可以使用factordb大数分解
-
然后就可以编写RSA解密脚本
# -*- coding:utf-8 -*- import binascii import gmpy2 n=135127138348299757374196447062640858416920350098320099993115949719051354213545596643216739555453946196078110834726375475981791223069451364024181952818056802089567064926510294124594174478123216516600368334763849206942942824711531334239106807454086389211139153023662266125937481669520771879355089997671125020789 p = 12022912661420941592569751731802639375088427463430162252113082619617837010913002515450223656942836378041122163833359097910935638423464006252814266959128953 q = 11239134987804993586763559028187245057652550219515201768644770733869088185320740938450178816138394844329723311433549899499795775655921261664087997097294813 e=0x10001 # 65537 c=110674792674017748243232351185896019660434718342001686906527789876264976328686134101972125493938434992787002915562500475480693297360867681000092725583284616353543422388489208114545007138606543678040798651836027433383282177081034151589935024292017207209056829250152219183518400364871109559825679273502274955582 phi=(p-1)*(q-1) d=gmpy2.invert(e,phi) m=pow(c,d,n) #print(hex(m)) print(binascii.unhexlify(hex(m)[2:].strip("L"))) # b'hgame{factordb.com_is_strong!}'
Be Stream
题目描述:很喜欢李小龙先生的一句话"Be water my friend",但是这条小溪的水好像太多了。
from flag import flag
assert type(flag) == bytes
key = [int.from_bytes(b"Be water", 'big'), int.from_bytes(b"my friend", 'big')]
def stream(i):
if i==0:
return key[0]
elif i==1:
return key[1]
else:
return (stream(i-2)*7 + stream(i-1)*4)
enc = b""
for i in range(len(flag)):
water = stream((i//2)**6) % 256
enc += bytes([water ^ flag[i]])
print(enc)
# b'\x1a\x15\x05\t\x17\t\xf5\xa2-\x06\xec\xed\x01-\xc7\xcc2\x1eXA\x1c\x157[\x06\x13/!-\x0b\xd4\x91-\x06\x8b\xd4-\x1e+*\x15-pm\x1f\x17\x1bY'
-
结合题目描述和附件,不难发现这是一个流密码,该
stream
函数以递归方式定义,并返回一个整数流。该enc
变量被定义为一个空字节对象,for
循环遍历该flag
对象(也是一个字节对象)。在循环的每次迭代中,water
为stream
返回的整数流的指定元素并以 256 取模的值,再和flag
的元素逐一异或运算,并将此结果赋给enc
。因此,enc
是将 的元素flag
与流的元素逐一异或的结果。这意味着flag
可以通过enc
与同一流进行异或来恢复,因为对相同的值进行两次异或可以抵消它。 -
要恢复
flag
,可以直接使用给出的stream
函数和key
生成密钥流,然后使用enc
对流的每个元素进行异或。然而,如果这么做会发现能跑出 hgam 然后就风扇嘎嘎转都跑不出下一个字母。这是因为给出的函数的实现stream
是递归的,这意味着它反复调用自身来计算流中每个元素的值。这可能效率低下,尤其是对于较大的 值i
,因为该函数多次调用自身来计算流的每个元素。 -
递归函数内部嵌套了对自身的调用,除非等到最内层的函数调用结束,否则外层的所有函数都不会调用结束。通俗地讲,外层函数被卡住了,它要等待所有的内层函数调用完成后,它自己才能调用完成。每一层的递归调用都会在栈上分配一块内存,有多少层递归调用就分配多少块相似的内存,所有内存加起来的总和是相当恐怖的,很容易超过栈内存的大小限制,这个时候就会导致程序崩溃。既然递归函数的解决方案存在巨大的内存开销和时间开销,那么我们如何进行优化呢?优化个毛,这是函数实现原理层面的缺陷,无法优化。其实,大部分能用递归解决的问题也能用迭代来解决。所谓迭代,就是循环。
-
那么重写
stream
函数如下def stream(i): if i==0: return key[0] elif i==1: return key[1] else: a, b = key[0], key[1] for j in range(2, i+1): a, b = b, (a*7 + b*4) % 256 return b
这个实现比递归的更快,因为它使用循环计算流的每个元素,而不是重复调用自己。
-
那么就可以得到 exp
enc = b'\x1a\x15\x05\t\x17\t\xf5\xa2-\x06\xec\xed\x01-\xc7\xcc2\x1eXA\x1c\x157[\x06\x13/!-\x0b\xd4\x91-\x06\x8b\xd4-\x1e+*\x15-pm\x1f\x17\x1bY' assert type(enc) == bytes key = [int.from_bytes(b"Be water", 'big'), int.from_bytes(b"my friend", 'big')] def stream(i): if i == 0: return key[0] elif i == 1: return key[1] else: a, b = key[0], key[1] for j in range(2, i + 1): a, b = b, (a * 7 + b * 4) % 256 return b dec = b"" for i in range(len(enc)): water = stream((i // 2) ** 6) % 256 dec += bytes([water ^ enc[i]]) print(dec)