HGAME 2023 Week1

HGAME 2023 Week1

前言

Week1的大多数题对我这样的萌新还是很友好的,虽然但是还是很多没解出来,不过还是学到了蛮多东西,浅浅记录一下

Web

Classic Childhood Game

题目描述:兔兔最近迷上了一个纯前端实现的网页小游戏,但是好像有点难玩,快帮兔兔通关游戏!

  1. 打开发现是一个 JavaScript 前端小游戏,然后在源代码中找一下可能是通关的 js 文件
    在这里插入图片描述

  2. 也可以几个都打开看看里面写的啥,最终可以发现 Events.js 里包含各种事件,那么就能找到关于通关事件的那段代码,也就知道flag怎么藏的了

  3. 在这个 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')));
    }
    
  4. 不难发现这是一个解码的函数,直接在控制台调用 mota() 函数即可拿下flag,也可以根据他的代码逻辑解码出 flag ,unicode+base64解码
    在这里插入图片描述

Become A Member

题目描述:学校通知放寒假啦,兔兔兴高采烈的打算购买回家的车票,这时兔兔发现成为购票网站的会员账户可以省下一笔money…… 想成为会员也很简单,只需要一点点HTTP的知识……等下,HTTP是什么,可以吃吗

  1. 根据题目描述是考察的 HTTP 知识,身份证明很容易联想到 User-Agent 消息头或者 Cookie ,但是只给出了一个名字,修改UA头为User-Agent: Cute-Bunny进入下一个页面

    在这里插入图片描述

  2. 这里给出了两个单词,可以凑成键值对,可以联想到 Cookie,根据句意修改为Cookie: code=Vidar进入下一个界面
    在这里插入图片描述

  3. 这个就很明确了,添加 Referer 头为Referer: bunnybunnybunny.com 进入下一页面
    在这里插入图片描述

  4. 考察本地请求,这个消息头很多比如 x-remote-IP、X-Real-IP、Client-Ip等,这里添加一个经典的X-Forwarded-For: 127.0.0.1即可
    在这里插入图片描述

  5. 最后改完长个样子
    在这里插入图片描述

  6. 拿到会员账号的键值对,要求我们用 json请求方式,我们用Burp Suite拦截后添加 json 数据,因为大多数时候使用POST方法提交json数据,我也是这么做的,但没有得到想要的结果,询问了出题人才知道请求方法不对,想了好久才发现改成GET即可,又拓宽了眼界
    在这里插入图片描述
    在这里插入图片描述

Guess Who I Am

题目描述:刚加入Vidar的兔兔还认不清协会成员诶,学长要求的答对100次问题可太难了,你能帮兔兔写个脚本答题吗?

在这里插入图片描述

  1. 很容易看出这是需要我们根据这些标签对应 Vidar 的成员,然后正确提交上去100次即可(其实100次用手工还是蛮快的,还不会下面内容的时候本人就是用手工做的,也就十多二十分钟)

  2. 查看源码可以发现这些标签没有在源码中,这是为啥捏?其实这个网站的内容由前端的 JS 动态生成,由于呈现在网页上的内容是由 JS 生成而来,我们能够在浏览器上看得到,但是在HTML源码中却发现不了。
    在这里插入图片描述

  3. 在源码中有一条Hint,进入网址后可以发现是很多的 json 数据,这就是我们找到对应成员名字的”钥匙“
    在这里插入图片描述

  4. 那么思路就清晰了,爬取标签内容 -> 找到 json 数据中对应成员 -> 提交给网页,那么逐一突破即可,具体可以参考这篇文章:
    网络爬虫(1)----爬取JS动态数据(上)

  5. 下面给出 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

  1. 根据题目描述,很明显是文件上传漏洞,而且应该是后缀名过滤

  2. 随便上传一个图片文件(主要是懒得和前端验证打交道),并使用Burp Suite拦截,发送到Repeater,修改文件后缀名、MIME类型,消息body,可以用 GIF89a 文件头防止有检测,然后写上一句话木马即可

  3. 这里后缀名尝试用等价的 .php2, .php3, .phtml 均上传失败,尝试用大小写绕过 .PHP,上传成功并返回了保存的路径,用蚁剑连接即可,flag 在根目录下
    在这里插入图片描述
    在这里插入图片描述

Misc

Sign In

题目描述:欢迎参加HGAME2023,Base64解码这段Flag,然后和兔兔一起开始你的HGAME之旅吧,祝你玩的愉快! aGdhbWV7V2VsY29tZV9Ub19IR0FNRTIwMjMhfQ==

  1. 价值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}

  1. 看题目描述预测是OSINT,附件是一个 .pcapng 流量包,用 binwalk 分离或者手工用 Wireshark 查看导出HTTP对象,是一个RAR文件
  2. 直接解压提示里面的图片文件头损坏,并且弹出需要解压密码,猜想是修复损坏的压缩包然后伪加密或者爆破密码得到图片文件,但我暂时没做出来。(最后本周比赛结束,了解到只是rar伪加密,解出来之后,图片右键属性,里面有GPS信息,就能做出来了)
神秘的海报

题目描述:坐车回到家的兔兔听说ek1ng在HGAME的海报中隐藏了一个秘密…(还记得我们的Misc培训吗?

  1. 附件是一个海报图片,图片中没有看到什么特别的信息,右键查看属性也没有有效信息,那用 binwalk 分离看看有没有包含其他文件,答案是没有的,既然是png文件那再用zsteg试试,看看有没有隐写数据

  2. 可以看到有一段英文文本,虽然不完整,但可以判断出是 LSB 隐写,使用强大的 stegsolve 提取 LSB 隐写数据即可
    在这里插入图片描述

  3. 这里给出了前一部分flag,另一部分需要前往指定网址下载音频,题目中明确指出其使用steghide隐写工具加密,密钥是一个6位数,搜索引擎搜索其解密工具也很容易就不再赘述了
    在这里插入图片描述

  4. 这里我做的时候小猜了一手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
    
  5. 不得不说真的慢,即使在Linux环境下我用这个脚本还是跑了差不多三四个小时才跑到123456,目前考虑用多线程爆破加快进度
    在这里插入图片描述
    在这里插入图片描述

e99p1ant_want_girlfriend

题目描述:兔兔在抢票网站上看到了一则相亲广告,人还有点小帅,但这个图片似乎有点问题,好像是 CRC 校验不太正确?

  1. 附件是某帅哥图片,根据题目描述是 CRC 校验)出现问题,这样的图片在 Windows 下可以打开,但是在 Linux 下会报错或显示空白,这是因为图片宽高被修改,和 CRC 校验码(循环冗余校验码)不对应造成的

  2. 所以我们可以通过遍历宽高看是否对应 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)
    
  3. 更多关于png图片文件结构分析可以看这篇博客:https://www.cnblogs.com/mengfanrong/p/3801583.html
    更多关于图片CRC校验码爆破宽高原理可以看这篇:https://www.cnblogs.com/yunqian2017/p/14449346.html

Pwn

test_nc
  1. 获取题目环境后,在Linux环境使用命令行 nc week-1.hgame.lwsec.cn 30098,然后ls查看当前目录文件,cat flag读取当前目录下名为flag的文件内容
    在这里插入图片描述
overflow
  1. 看题目就能猜到这是一道简单的溢出题,用Die查看发现是64位文件,用ida64打开,F5查看伪代码如下
    在这里插入图片描述

  2. 非常简短,close(1)意味着stdout(标准输出)关闭。程序能够拿到shell,如果程序关闭了stdout,则会无法正常得到回显。这时可以通过执行exec 1>&0exec 1>&2,将标准输出重定向到标准输入或标准输出错误从而得到回显。

  3. 后面还有一个read操作,大小为0x100uLL。双击进入变量查看
    在这里插入图片描述

  4. 从s到buf是 0000000000000000 - 0000000000000010 个地址(注意是16进制),也就是0x10个地址,这是s到buf的。从read到s 0000000000000008 - 0000000000000000 个地址,也就是0x08个地址,这是从s到r的。两者相加,一共0x18个地址,想到栈溢出,把这些个地址用字符堵死,后续就能伪造执行的read。

  5. 问题来到read,我们肯定需要读取flag,但并没有直接system("cat flag");的函数,但是很容易发现有一个 b4ckd0or 的后门函数,双击进去后发现函数能拿到 /bin/sh 的权限,正合我意,那么就去 Exports 窗口中找到 b4ckd0or 函数的地址,完整地址是:00000000000401176 ,这里其实只取后八位就可以:00401176,也就是0x00401176。用脚本把前面0x88个空间打死然后把这个函数地址用p64(0x00401176)拼接上,就可以让read拿到我们的/bin/sh权限了。最终,确定了通过栈溢出执行 b4ckd0or 从而得到shell
    在这里插入图片描述

  6. 编写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

  7. 最后根据上面执行脚本和命令,这里python用的Windows环境

    在这里插入图片描述

Re

test your IDA

题目描述:签到

  1. 这道题可以用IDA打开即可看到明文的flag
    在这里插入图片描述

  2. 甚至不用打开IDA,用Die查看文件的字符串也可以
    在这里插入图片描述

  3. 除此之外,还可以使用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
  1. 预备知识:

    eip:运行程序下一个指令的地址

    ebp:栈的基地址指针,指向栈底

    esp:栈的栈顶指针,指向栈顶

    push:入栈,esp 指向地址减 4

    move:数据传送指令,完成赋值

    jmp:无条件跳转指令,转移到指令指定的地址执行相应的指令

    jge:大于或等于转移指令,用于对比内存中两个对象的大小关系

    cmp:比较指令,执行从目的操作数中减去源操作数的隐含减法操作,并且不修改任何操作数

    EAX:“累加器”(accumulator), 它是很多加法乘法指令的缺省寄存器。

    EBX:“基地址”(base)寄存器, 在内存寻址时存放基地址。

    ECX:计数器(counter), 是重复(REP)前缀指令和LOOP指令的内定计数器。

    EDX:用来放整数除法产生的余数

  2. 可以看出这应该是一个从 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
  1. 放入 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;
    }
    
  2. 逐行分析伪代码的逻辑,写上通俗易懂的注释

    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;
    }
    
  3. 所以就很清晰了,只需要把 v8 的每一 byte 提取出来,然后利用异或的性质解出明文即可,那么问题来到了怎么提取出 int 类型每一位 byte 值。比如 0x12345678 它的每一位 byte 值就是 0x78、0x56、0x34、0x12 ,可能看到这里会有人发出疑问:为什么你这个不是 0x12 在前面,而要反着写呢?(没有疑问可以在此地休息一手)

  4. 要解决这个问题,先要引入“字节序”的概念,参考链接

    字节序,又称端序或尾序,指的是多字节数据在计算机内存中的存放顺序。例如一个 int 型变量 x 占用4个字节,假设它的起始地址 &x 为 0x10,那么x将会被存储在 0x10、0x11、0x12 和 0x13 位置上。在用 C++ 写的客户端和 Java 写的服务端的通信时,发现数据通过 TCP 连接传输后收到的与发送的不一致,所以要引入大端和小端的概念。以一个两字节 short 型变量 0x0102 的存储举例:

    • 大端字节序:高位字节在前,低位字节在后,01|02,符合人们的读写习惯。
    • 小端字节序:低位字节在前,高位字节在后,02|01。

    那为什么不统一使用符合人们读写习惯的大端字节序呢,这是因为计算机电路先处理低位字节,效率比较高,因为计算都是从低位开始的,所以计算机的内部处理都是小端字节序。但是人类还是习惯读写大端字节序,所以,除了计算机的内部处理,其他的场合几乎都是大端字节序,比如网络传输和文件储存。

  5. 好了,回到刚刚讨论的如何提取每一位 byte,可以借助计算器观察观察
    在这里插入图片描述

  6. 不难发现可以通过移位操作取出每一 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
    */
    

    这也是获取数据各个字节的最常用也最有效的方法。

  7. 所以可以这样写代码(这里做的时候用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

题目描述:兔兔把自己行李箱的密码用一种编码写在了纸条上,但他忘了怎么解密,你能帮帮他吗?

  1. 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;
    }
    
  2. 开始分析代码逻辑,写上一些注释

    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;
    }
    
  3. 双击打开如图所示,如果直接进行下拉复制,会发现有很多没用的数据,处理起来十分繁琐,当然你也可以正则提取,但是也可以采用其他简单的办法
    在这里插入图片描述

  4. 鼠标移到int dword_40300[100]那一行,右键选择Array,单击进入
    在这里插入图片描述

  5. 然后按下图进行设置就可以愉快的进行复制啦!(其实也可以通过python脚本来复制,Shift+F2 调出 Script 窗口,然后输入代码即可,更多详情参考:安卓逆向|菜鸟的IDA学习笔记:如何简单复制DCB数据
    在这里插入图片描述

  6. 最后得到数据,进行逆运算即可,下面给出 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
"""
  1. 这道题非常简单,只要明白RSA加密算法原理和安全性基于“两个大素数相乘很容易,而对得到的积求因子则很难”的数学事实就能做

  2. 我们可以yafu分解,但这道题给的数字比较大,yafu分解需要大量时间,所以我们可以使用factordb大数分解

    在这里插入图片描述

  3. 然后就可以编写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'
  1. 结合题目描述和附件,不难发现这是一个流密码,该stream函数以递归方式定义,并返回一个整数流。该enc变量被定义为一个空字节对象,for循环遍历该flag对象(也是一个字节对象)。在循环的每次迭代中,waterstream返回的整数流的指定元素并以 256 取模的值,再和flag的元素逐一异或运算,并将此结果赋给enc。因此,enc是将 的元素flag与流的元素逐一异或的结果。这意味着flag可以通过enc与同一流进行异或来恢复,因为对相同的值进行两次异或可以抵消它。

  2. 要恢复flag,可以直接使用给出的stream函数和key生成密钥流,然后使用enc对流的每个元素进行异或。然而,如果这么做会发现能跑出 hgam 然后就风扇嘎嘎转都跑不出下一个字母。这是因为给出的函数的实现stream是递归的,这意味着它反复调用自身来计算流中每个元素的值。这可能效率低下,尤其是对于较大的 值i,因为该函数多次调用自身来计算流的每个元素。

  3. 递归函数内部嵌套了对自身的调用,除非等到最内层的函数调用结束,否则外层的所有函数都不会调用结束。通俗地讲,外层函数被卡住了,它要等待所有的内层函数调用完成后,它自己才能调用完成。每一层的递归调用都会在栈上分配一块内存,有多少层递归调用就分配多少块相似的内存,所有内存加起来的总和是相当恐怖的,很容易超过栈内存的大小限制,这个时候就会导致程序崩溃。既然递归函数的解决方案存在巨大的内存开销和时间开销,那么我们如何进行优化呢?优化个毛,这是函数实现原理层面的缺陷,无法优化。其实,大部分能用递归解决的问题也能用迭代来解决。所谓迭代,就是循环。

  4. 那么重写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
    

    这个实现比递归的更快,因为它使用循环计算流的每个元素,而不是重复调用自己。

  5. 那么就可以得到 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)
    
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值